// This file cannot be Coffeescript because it loads before the
// Coffeescript interpreter. Note that it runs in both browser and
// renderer processes.

var ErrorLogger, _, fs, path, app, os, remote;
os = require('os');
fs = require('fs-plus');
path = require('path');
let ipcRenderer = null;
if (process.type === 'renderer') {
  ipcRenderer = require('electron').ipcRenderer;
  remote = require('electron').remote;
  app = remote.app;
} else {
  app = require('electron').app;
}

var crashReporter = require('electron').crashReporter
var RavenErrorReporter = require('./error-logger-extensions/raven-error-reporter');

// A globally available ErrorLogger that can report errors to various
// sources and enhance error functionality.
//
// This runs in both the backend browser process and each and every
// renderer process.
//
// This is available as `global.errorLogger` in the backend browser
// process.
//
// It is available at `NylasEnv.errorLogger` in each renderer process.
// You should almost always use `NylasEnv.reportError` in the renderer
// processes instead of manually accessing the `errorLogger`
//
// The errorLogger will report errors to a log file as well as to 3rd
// party reporting services if enabled.
module.exports = ErrorLogger = (function() {

  function ErrorLogger(args) {
    this.reportError = this.reportError.bind(this)
    this.inSpecMode = args.inSpecMode
    this.inDevMode = args.inDevMode
    this.resourcePath = args.resourcePath

    this._startCrashReporter();

    this._extendErrorObject();

    this._extendNativeConsole();

    this.extensions = [
      new RavenErrorReporter({
        inSpecMode: args.inSpecMode,
        inDevMode: args.inDevMode,
        resourcePath: args.resourcePath,
      }),
    ]

    if (this.inSpecMode) { return }

    this._cleanOldLogFiles();
    this._setupNewLogFile();
    this._hookProcessOutputsToLogFile();
  }

  /////////////////////////////////////////////////////////////////////
  /////////////////////////// PUBLIC METHODS //////////////////////////
  /////////////////////////////////////////////////////////////////////

  ErrorLogger.prototype.reportError = function(error, extra = {}) {
    if (this.inSpecMode) { return }
    if (!error) { error = {stack: ""} }
    this._appendLog(error.stack)
    if (extra) { this._appendLog(extra) }
    if (process.type === "renderer") {
      var errorJSON = "{}";
      try {
        errorJSON = JSON.stringify(error);
      } catch (err) {
        var recoveredError = new Error();
        recoveredError.stack = error.stack;
        recoveredError.message = `Recovered Error: ${error.message}`;
        errorJSON = JSON.stringify(recoveredError)
      }

      var extraJSON;
      try {
        extraJSON = JSON.stringify(extra);
      } catch (err) {
        extraJSON = "{}";
      }

      /**
       * We synchronously send all errors to the backend main process.
       *
       * This is important because errors can frequently happen right
       * before a renderer window is closing. Since error reporting hits
       * APIs and is asynchronous it's possible for the window to be
       * destroyed before the report makes it.
       *
       * This is a rare use of `sendSync` to ensure the command has made
       * it before the window closes.
       */
      ipcRenderer.sendSync("report-error", {errorJSON: errorJSON, extra: extraJSON})

    } else {
      this._notifyExtensions("reportError", error, extra)
    }
    console.error(error, extra);
  }

  ErrorLogger.prototype.openLogs = function() {
    var shell = require('electron').shell;
    shell.openItem(this._logPath());
  };


  /////////////////////////////////////////////////////////////////////
  ////////////////////////// PRIVATE METHODS //////////////////////////
  /////////////////////////////////////////////////////////////////////

  ErrorLogger.prototype._startCrashReporter = function(args) {
    crashReporter.start({
      productName: 'Merani',
      companyName: 'Merani',
      submitURL: 'http://merani_prod.bugsplat.com/post/bp/crash/postBP.php',
      uploadToServer: true,
      autoSubmit: true,
    })
  }

  ErrorLogger.prototype._extendNativeConsole = function(args) {
    console.debug = this._consoleDebug.bind(this)

    if (process.type === 'browser' && process.platform === 'darwin') {
      var nslog = require('nslog');
      console.log = nslog;
      console.error = nslog;
    }
  }

  // globally define Error.toJSON. This allows us to pass errors via IPC
  // and through the Action Bridge. Note:they are not re-inflated into
  // Error objects automatically.
  ErrorLogger.prototype._extendErrorObject = function(args) {
    Object.defineProperty(Error.prototype, 'toJSON', {
      value: function () {
        var alt = {};

        Object.getOwnPropertyNames(this).forEach(function (key) {
          alt[key] = this[key];
        }, this);

        return alt;
      },
      configurable: true,
    });
  }

  ErrorLogger.prototype._logPath = function() {
    var tmpPath = app.getPath('temp');

    var logpid = process.pid;
    if (process.type === 'renderer') {
      logpid = remote.process.pid + "." +  process.pid;
    }
    return path.join(tmpPath, 'Nylas-Mail-' + logpid + '.log');
  }

  // If we're the browser process, remove log files that are more than
  // two days old. These log files get pretty big because we're logging
  // so verbosely.
  ErrorLogger.prototype._cleanOldLogFiles = function() {
    if (process.type === 'browser') {
      var tmpPath = app.getPath('temp');
      fs.readdir(tmpPath, function(err, files) {
        if (err) {
          console.error(err);
          return;
        }

        var logFilter = new RegExp("Nylas-Mail-[.0-9]*.log$");
        files.forEach(function(file) {
          if (logFilter.test(file) === true) {
            var filepath = path.join(tmpPath, file);
            fs.stat(filepath, function(err, stats) {
              if (!err && stats) {
                var lastModified = new Date(stats.mtime);
                var fileAge = Date.now() - lastModified.getTime();
                if (fileAge > (1000 * 60 * 60 * 24 * 2)) { // two days
                  fs.unlink(filepath, () => {});
                }
              }
            });
          }
        });
      });
    }
  }

  ErrorLogger.prototype._setupNewLogFile = function() {
    // Open a file write stream to log output from this process
    console.log("Streaming log data to " + this._logPath());

    this.loghost = os.hostname();
    this.logstream = fs.createWriteStream(this._logPath(), {
      flags: 'a',
      encoding: 'utf8',
      fd: null,
      mode: 666,
    });
  }

  ErrorLogger.prototype._hookProcessOutputsToLogFile = function() {
    var self = this;
    // Override stdout and stderr to pipe their output to the file
    // in addition to calling through to the existing implementation
    function hook_process_output(channel, callback) {
      var old_write = process[channel].write;
      process[channel].write = (function(write) {
          return function(string, encoding, fd) {
              write.apply(process[channel], arguments)
              callback(string, encoding, fd)
          }
      })(process[channel].write)

      // Return a function that can be used to undo this change
      return function() {
        process[channel].write = old_write
      };
    }

    hook_process_output('stdout', function(string, encoding, fd) {
      self._appendLog.apply(self, [string]);
    });
    hook_process_output('stderr', function(string, encoding, fd) {
      self._appendLog.apply(self, [string]);
    });
  }

  ErrorLogger.prototype._notifyExtensions = function() {
    var command, args;
    command = arguments[0]
    args = 2 <= arguments.length ? Array.prototype.slice.call(arguments, 1) : [];
    for (var i = 0; i < this.extensions.length; i++) {
      const extension = this.extensions[i]
      extension[command].apply(extension, args);
    }
  }

  // Create a new console.debug option, which takes `true` (print)
  // or `false`, don't print in console as the first parameter.
  // This makes it easy for developers to turn on and off
  // "verbose console" mode.
  ErrorLogger.prototype._consoleDebug = function() {
    var args = [];
    var showIt = arguments[0];
    for (var ii = 1; ii < arguments.length; ii++) {
      args.push(arguments[ii]);
    }
    if ((this.inDevMode === true) && (showIt === true)) {
      console.log.apply(console, args);
    }
    this._appendLog.apply(this, [args]);
  }

  ErrorLogger.prototype._appendLog = function(obj) {
    if (this.inSpecMode) { return; }

    try {
      var message = JSON.stringify({
        host: this.loghost,
        timestamp: (new Date()).toISOString(),
        payload: obj
      }) + "\n";

      this.logstream.write(message, 'utf8', function (err) {
        if (err) {
          console.error("ErrorLogger: Unable to write to the log stream!" + err.toString());
        }
      });
    } catch (err) {
      console.error("ErrorLogger: Unable to write to the log stream." + err.toString());
    }
  };

  return ErrorLogger;
})();
