'use strict';

// ### Module dependencies
var colors = require('colors/safe');
var Utils = require('./utils');

var conflictChars = /[^\w\s\n\r\v\t\.,]/i;

exports.version = require('../package.json').version;

// Helper function to detect if an object should be printed or ignored
var isPrintable = function(input, options) {
  return input !== undefined || options.renderUndefined;
};

// Helper function to detect if an object can be directly serializable
var isSerializable = function(input, onlyPrimitives, options) {
  if (
    typeof input === 'boolean' ||
    typeof input === 'number' ||
    typeof input === 'function' ||
    input === null ||
    input === undefined ||
    input instanceof Date
  ) {
    return true;
  }
  if (typeof input === 'string' && input.indexOf('\n') === -1) {
    return true;
  }

  if (options.inlineArrays && !onlyPrimitives) {
    if (Array.isArray(input) && isSerializable(input[0], true, options)) {
      return true;
    }
  }

  return false;
};

var addColorToData = function(input, options) {
  if (options.noColor) {
    return input;
  }

  if (typeof input === 'string') {
    // Print strings in regular terminal color
    return options.stringColor ? colors[options.stringColor](input) : input;
  }

  var sInput = input + '';

  if (input === true) {
    return colors.green(sInput);
  }
  if (input === false) {
    return colors.red(sInput);
  }
  if (input === null || input === undefined) {
    return colors.grey(sInput);
  }
  if (typeof input === 'number') {
    if (input >= 0) {
      return colors[options.positiveNumberColor](sInput);
    } else {
      return colors[options.negativeNumberColor](sInput);
    }
  }
  if (typeof input === 'function') {
    return 'function() {}';
  }

  if (Array.isArray(input)) {
    return input.join(', ');
  }

  return sInput;
};

var colorMultilineString = function(options, line) {
    if (options.multilineStringColor === null || options.noColor) {
        return line;
    } else {
        return colors[options.multilineStringColor](line);
    }
};

var indentLines = function(string, spaces, options){
  var lines = string.split('\n');
  lines = lines.map(function(line){
    return Utils.indent(spaces) + colorMultilineString(options, line);
  });
  return lines.join('\n');
};

var renderToArray = function(data, options, indentation) {

  if (typeof data === 'string' && data.match(conflictChars) &&
      options.escape) {
    data = JSON.stringify(data);
  }

  if (!isPrintable(data, options)) {
    return [];
  }

  if (isSerializable(data, false, options)) {
    return [Utils.indent(indentation) + addColorToData(data, options)];
  }

  // Unserializable string means it's multiline
  if (typeof data === 'string') {
    return [
      Utils.indent(indentation) + colorMultilineString(options, '"""'),
      indentLines(data, indentation + options.defaultIndentation, options),
      Utils.indent(indentation) + colorMultilineString(options, '"""')
    ];
  }

  if (Array.isArray(data)) {
    // If the array is empty, render the `emptyArrayMsg`
    if (data.length === 0) {
      return [Utils.indent(indentation) + options.emptyArrayMsg];
    }

    var outputArray = [];

    data.forEach(function(element) {
      if (!isPrintable(element, options)) {
        return;
      }

      // Prepend the dash at the begining of each array's element line
      var line = '- ';
      if (!options.noColor) {
        line = colors[options.dashColor](line);
      }
      line = Utils.indent(indentation) + line;

      // If the element of the array is a string, bool, number, or null
      // render it in the same line
      if (isSerializable(element, false, options)) {
        line += renderToArray(element, options, 0)[0];
        outputArray.push(line);

      // If the element is an array or object, render it in next line
      } else {
        outputArray.push(line);
        outputArray.push.apply(
          outputArray,
          renderToArray(
            element, options, indentation + options.defaultIndentation
          )
        );
      }
    });

    return outputArray;
  }

  if (data instanceof Error) {
    return renderToArray(
      {
        message: data.message,
        stack: data.stack.split('\n')
      },
      options,
      indentation
    );
  }

  // If values alignment is enabled, get the size of the longest index
  // to align all the values
  var maxIndexLength = options.noAlign ? 0 : Utils.getMaxIndexLength(data);
  var key;
  var output = [];

  Object.getOwnPropertyNames(data).forEach(function(i) {
    if (!isPrintable(data[i], options)) {
      return;
    }

    // Prepend the index at the beginning of the line
    key = (i + ': ');
    if (!options.noColor) {
      key = colors[options.keysColor](key);
    }
    key = Utils.indent(indentation) + key;

    // If the value is serializable, render it in the same line
    if (isSerializable(data[i], false, options)) {
      var nextIndentation = options.noAlign ? 0 : maxIndexLength - i.length;
      key += renderToArray(data[i], options, nextIndentation)[0];
      output.push(key);

      // If the index is an array or object, render it in next line
    } else {
      output.push(key);
      output.push.apply(
        output,
        renderToArray(
          data[i],
          options,
          indentation + options.defaultIndentation
        )
      );
    }
  });
  return output;
};

// ### Render function
// *Parameters:*
//
// * **`data`**: Data to render
// * **`options`**: Hash with different options to configure the parser
// * **`indentation`**: Base indentation of the parsed output
//
// *Example of options hash:*
//
//     {
//       emptyArrayMsg: '(empty)',    // Rendered message on empty strings
//       keysColor: 'blue',           // Color for keys in hashes
//       dashColor: 'red',            // Color for the dashes in arrays
//       stringColor: 'grey',         // Color for strings
//       multilineStringColor: 'cyan' // Color for multiline strings
//       defaultIndentation: 2        // Indentation on nested objects
//     }
exports.render = function render(data, options, indentation) {
  // Default values
  indentation = indentation || 0;
  options = options || {};
  options.emptyArrayMsg = options.emptyArrayMsg || '(empty array)';
  options.keysColor = options.keysColor || 'green';
  options.dashColor = options.dashColor || 'green';
  options.numberColor = options.numberColor || 'blue';
  options.positiveNumberColor = options.positiveNumberColor
    || options.numberColor;
  options.negativeNumberColor = options.negativeNumberColor
    || options.numberColor;
  options.defaultIndentation = options.defaultIndentation || 2;
  options.noColor = !!options.noColor;
  options.noAlign = !!options.noAlign;
  options.escape = !!options.escape;
  options.renderUndefined = !!options.renderUndefined;

  options.stringColor = options.stringColor || null;
  options.multilineStringColor = options.multilineStringColor || null;

  return renderToArray(data, options, indentation).join('\n');
};

// ### Render from string function
// *Parameters:*
//
// * **`data`**: Data to render as a string
// * **`options`**: Hash with different options to configure the parser
// * **`indentation`**: Base indentation of the parsed output
//
// *Example of options hash:*
//
//     {
//       emptyArrayMsg: '(empty)', // Rendered message on empty strings
//       keysColor: 'blue',        // Color for keys in hashes
//       dashColor: 'red',         // Color for the dashes in arrays
//       defaultIndentation: 2     // Indentation on nested objects
//     }
exports.renderString = function renderString(data, options, indentation) {

  var output = '';
  var parsedData;
  // If the input is not a string or if it's empty, just return an empty string
  if (typeof data !== 'string' || data === '') {
    return '';
  }

  // Remove non-JSON characters from the beginning string
  if (data[0] !== '{' && data[0] !== '[') {
    var beginingOfJson;
    if (data.indexOf('{') === -1) {
      beginingOfJson = data.indexOf('[');
    } else if (data.indexOf('[') === -1) {
      beginingOfJson = data.indexOf('{');
    } else if (data.indexOf('{') < data.indexOf('[')) {
      beginingOfJson = data.indexOf('{');
    } else {
      beginingOfJson = data.indexOf('[');
    }
    output += data.substr(0, beginingOfJson) + '\n';
    data = data.substr(beginingOfJson);
  }

  try {
    parsedData = JSON.parse(data);
  } catch (e) {
    // Return an error in case of an invalid JSON
    return colors.red('Error:') + ' Not valid JSON!';
  }

  // Call the real render() method
  output += exports.render(parsedData, options, indentation);
  return output;
};
