/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

const path = require('path');
const util = require('util');

const chalk = require('chalk');
const dbug = require('dbug');
const stack = require('stack-trace');
const utc = require('utcstring');

const intel = require('./');

const ALIASES = [
  'trace',
  'debug',
  'info',
  'warn',
  'error'
];

function copyProperties(source, target, props) {
  props.forEach(function(prop) {
    target[prop] = source[prop];
  });
}

var ORIGINAL_METHODS = {};
const METHOD_NAMES = [
  'trace',
  'debug',
  'dir',
  'error',
  'info',
  'log',
  'warn'
];

var ORIGINAL_STDERR;

function endsWith(str, suffix) {
  return str.indexOf(suffix, str.length - suffix.length) !== -1;
}

var root;
var ignore;
var parentLogger;
var basename;

function getLoggerName(debugName) {
  var trace = stack.get();
  // walk up the stack until we find a function that isn't from this
  // module. that's the calling module.
  // ALSO: 'console.js' could be on the stack if console.trace() or
  // something similar was called, cause we don't modify those, since
  // they internally call console.log(), which is us!
  var filename;
  var debug = [];
  if (debugName) {
    debug = [
      // debug@0.x
      path.join('node_modules', 'debug', 'lib', 'debug.js'),
      // debug@1.x
      path.join('node_modules', 'debug', 'node.js'),
      path.join('node_modules', 'debug', 'debug.js'),
      // dbug@0.x
      path.join('node_modules', 'dbug', 'lib', 'dbug.js'),
      path.join('node_modules', 'dbug', 'index.js')
    ];
  }

  function skip(name) {
    if (name === __filename || name === 'console.js') {
      return true;
    }
    return debug.some(function(d) {
      return endsWith(name, d);
    });
  }

  for (var i = 0, len = trace.length; i < len; i++) {
    filename = path.normalize(trace[i].getFileName());
    if (!skip(filename)) {
      break;
    }
  }
  var topName = basename || path.basename(root);
  topName = topName.replace(path.extname(topName), '');

  var moduleName = path.join(topName, path.relative(root, filename));
  moduleName = moduleName.replace(path.extname(moduleName), '');
  moduleName = moduleName.replace(/[\\\/]/g, '.');

  // lib is the defacto place to store js files, but removing lib looks
  // better: connect.lib.session -> connect.session
  moduleName = moduleName.replace(/\.lib\./g, '.');

  // index.js filename shouldn't be used, since it's really the folder
  // up. better: gryphon.index -> gryphon
  moduleName = moduleName.replace(/\.index/g, '');


  if (debugName) {
    // clean up duplicated parts of the name
    // ex: node_modules.intel.logger.intel.logger =>
    // node_modules.intel.logger
    if (!endsWith(moduleName, debugName)) {
      moduleName += '.' + debugName;
    }
  }

  if (parentLogger) {
    moduleName = parentLogger + '.' + moduleName;
  }

  return moduleName;
}

function setRoot(r) {
  root = r;
}

function setIgnore(i) {
  if (typeof i === 'string') {
    i = [i];
  }
  ignore = i;
}

const DEBUG_COLORED_RE = new RegExp([
  '^',
  '  ', // starts with 2 spaces. yea really
  '\\u001b\\[\\d{1,2};1m', // colored debug has colors
  '(.+)', // logger name
  '\\u001b\\[\\d?0m', // color end
  '(.+)', // message
  '\n?', // debug 2.0 addes a newline
  '$'
].join(''));

const DBUG_LEVELS = ['debug', 'info', 'warn', 'error'];


function getLoggerLevel(name) {
  var i = DBUG_LEVELS.length;
  while (i--) {
    var level = DBUG_LEVELS[i];
    if (endsWith(name, '.' + level)) {
      return level;
    }
  }
  return 'debug';
}

function dbugName(name) {
  if (!name) {
    return;
  }
  if (name.indexOf('.') === -1) {
    return name;
  }
  var level = getLoggerLevel(name.toLowerCase());
  level = new RegExp('\\.' + level + '$', 'i');
  return name.replace(level, '');
}


function parseDebug(args) {
  // O_O
  // Dear reader: I'm so sorry.
  var str = String(args[0]);

  // is it colored debug() ?
  var match = str.match(DEBUG_COLORED_RE);
  if (match) {
    var logger = chalk.stripColor(match[1]).trim();
    var msg = chalk.stripColor(match[2]).trim();
    args[0] = msg; // overwrite the message portion
    return logger.replace(/:/g, '.');
  } else if (utc.has(str)) {
    str = str.replace(utc.get(str), '').trim();
    var logger = str.split(' ').shift();
    var msg = str.replace(logger, '').trim();
    args[0] = msg;
    return logger.replace(/:/g, '.');
  }
}

// cached loggers that hook into dbug.__log
// this way, we don't format a level message, and then parse it with
// regex, just to format a new message in intel
// ALSO: we only get a stacktrace once, and then save the name, so
// PERFORMANCE WIN!
var dbugLoggers = {};
function dbugHook(name, level, args) {
  var logger = dbugLoggers[name];
  if (!logger) {
    logger = dbugLoggers[name] =
      intel.getLogger(getLoggerName(name.replace(/:/g, '.')));
  }
  logger[level].apply(logger, args);
}

var isDebugging = false;
var __log = dbug.__log;
function setDebug(debug) {
  if (debug === true) {
    process.env.DEBUG = dbug.env = '*';
  } else if (debug === false) {
    process.env.DEBUG = dbug.env = '';
  } else if (debug) {
    if (process.env.DEBUG) {
      process.env.DEBUG += ',' + debug;
      dbug.env = process.env.DEBUG;
    } else {
      process.env.DEBUG = dbug.env = '' + debug;
    }
  }
  isDebugging = !!debug;
  dbug.__log = dbugHook;
}

function setLoggerBaseName(bn){
  basename = bn;
}

function deliver(method, args) {
  var debugged = isDebugging && parseDebug(args);
  var name = getLoggerName(dbugName(debugged));
  var i = ignore.length;
  var logger = intel.getLogger(name);
  name = logger._name;
  while (i--) {
    if (name.indexOf(ignore[i]) === 0) {
      if (method === 'stderr') {
        ORIGINAL_STDERR.call(process.stderr, args[0]);
      } else {
        ORIGINAL_METHODS[method].apply(console, args);
      }
      return;
    }
  }
  if (debugged) {
    method = getLoggerLevel(debugged);
  }
  var level = ALIASES.indexOf(method) !== -1 ? method : 'debug';
  logger[level].apply(logger, args);
}


function overrideConsole(options) {
  options = options || {};
  setRoot(options.root || path.join(
    stack.get(options.__trace || overrideConsole)[0].getFileName(),
    '..'
  ));
  setIgnore(options.ignore || []);
  parentLogger = options.logger;

  setDebug(options.debug);

  setLoggerBaseName(options.basename);
  if (!ORIGINAL_METHODS.log) {
    copyProperties(console, ORIGINAL_METHODS, METHOD_NAMES);
  }

  if (!ORIGINAL_STDERR) {
    ORIGINAL_STDERR = process.stderr.write;
  }

  ALIASES.forEach(function(method) {
    console[method] = function alias(){
      deliver(method, arguments);
    };
  });

  console.log = function log() {
    deliver('log', arguments);
  };

  console.dir = function dir(obj) {
    deliver('dir', [util.inspect(obj)]);
  };

  process.stderr.write = function write(str) {
    deliver('stderr', [str]);
    return true;
  };
}

function restoreConsole() {
  for (var name in ORIGINAL_METHODS) {
    if (ORIGINAL_METHODS.hasOwnProperty(name) && ORIGINAL_METHODS[name]) {
      console[name] = ORIGINAL_METHODS[name];
    }
  }
  copyProperties({}, ORIGINAL_METHODS, METHOD_NAMES);
  dbug.__log = __log;
  if (ORIGINAL_STDERR) {
    process.stderr.write = ORIGINAL_STDERR;
    ORIGINAL_STDERR = undefined;
  }
}

module.exports = exports = overrideConsole;
exports.restore = restoreConsole;
