use :node;

var parser = module.require('./parser'),

escodegen = require('escodegen'),
ast = module.require('./ast'),
traceur = require("traceur"),
chalk = require("chalk"),
transfer = require("multi-stage-sourcemap").transfer;

/**

* Compiles Spider code to JavaScript.
*
* @param {string} options.text
* @param {string} options.fileName 
* @param {string} options.target
* @param {boolean} [options.verbose]
* @param {boolean} [options.generateSourceMap]
* @param {boolean} [options.iifi]
* @param {boolean} [options.useStrict]
*/

exports.compile = fn (options) {

var result = {
  errors: [],
  result: null,
  sourceMap: null
};

options.fileName = options.fileName ?? "tmp";

var outFileNameWithoutExtension = options.fileName.substring(0, 
  options.fileName.lastIndexOf('.'));    

var outFileName = outFileNameWithoutExtension + ".js";
var mapFileName = outFileNameWithoutExtension + ".map";

::resetVariableNames();  
ast.Node.setErrorManager(new ::ErrorManager(result.errors));

var parsed;

try {
  parsed = parser.parse(options.text);
} catch e {
  result.errors.push(::getParsingError(e));
}

if not parsed {
  return result;
}

var tree = parsed.codegen();
if options.target != "ES5" {
  tree = ::wrapCode(tree, 
    options.useStrict ?? true, 
    options.iifi ?? true);
}

var output = escodegen.generate(tree, {
  sourceMap: options.fileName if options.generateSourceMap else null,
  sourceMapWithCode: options.generateSourceMap,
  format: { 
    quotes: 'double'
  }
});

if options.generateSourceMap { 
  result.result = output.code;
  result.sourceMap = output.map.toString();
} else {
  result.result = output;
}

if options.target == "ES5" {
  var traceurCompiler = new traceur.NodeCompiler({
    sourceMaps: options.generateSourceMap,
    asyncFunctions: true
  });

  result.result = traceurCompiler.compile(result.result, options.fileName, outFileName);

  if options.generateSourceMap {
    result.sourceMap = transfer({
      toSourceMap: result.sourceMap,
      fromSourceMap: traceurCompiler.getSourceMap().toString()
    });
  }
} else if options.generateSourceMap {
  result.result = result.result + "\n\n//# sourceMappingURL=" + mapFileName;
}

if options.verbose {
  console.log(JSON.stringify(parsed, null, 4));
  console.log(result.result);
}

return result;

};

/**

* Format errors array with colors
*/

exports.formatErrors = fn (fileName, content, errors) {

var output = [];

var maxCol = 0;
var maxLine = 0;

output.push(chalk.white(fileName), "\n");

var lines = content.split("\n");
var tabCharacter = "__SPIDER_TAB";

for error, errorIndex in errors {
  var line = error.loc.start.line;
  var column = error.loc.start.column + 1;

  var lineCharCount = line.toString().length;
  var columnCharCount = column.toString().length;

  maxCol = Math.max(maxCol, columnCharCount);
  maxLine = Math.max(maxCol, lineCharCount);

  output.push(tabCharacter);
  output.push(chalk.gray("line", line));
  output.push(tabCharacter, lineCharCount);
  output.push(chalk.gray("col", column));
  output.push(tabCharacter, columnCharCount);

  output.push(chalk.red(error.message), "\n");

  if error.loc?.start {
    var start = error.loc.start;
    var end = error.loc.end;

    if 0 < start.line <= lines.length {
      output.push(tabCharacter, tabCharacter, tabCharacter);

      output.push(chalk.green(lines[start.line - 1].replace(/(\r\n|\n|\r)/gm, ""), 
        "\n", tabCharacter, tabCharacter));
      output.push(chalk.red(::generateErrorColumnString(start.column, end.column - 1 if end else 0)));
    }
  }

  output.push("\n");
}

var str = output.join("");
var tabLength = Math.max(maxLine, maxCol);

for var i = 1; i <= tabLength; i++ {
  var regex = new RegExp(tabCharacter + i, "g");
  str = str.replace(regex, ::generateSpace(Math.max(2 + tabLength - i, 2)));
}

return str.replace(new RegExp(tabCharacter, "g"), ::generateSpace(2));

};

fn ErrorManager(errors) {

this.errors = errors;

}

ErrorManager.prototype.error = (e) -> {

this.errors.push(e);

};

fn getParsingError(e) {

var message;

if e.expected {
  if e.found {
    message = "unexpected " + e.found;
  } else {
    message = "unexpected end of input";
  }
} else {
  message = e.message;
}

return {
  type: "SyntaxError",
  message: message,
  loc: {
    start: {
      line: e.line,
      column: e.column - 1
    }
  }
};

}

fn resetVariableNames() {

ast.NullPropagatingExpression.resetVariableNames();
ast.NullCoalescingExpression.resetVariableNames();
ast.NullCheckCallExpression.resetVariableNames();
ast.ExistentialExpression.resetVariableNames();  
ast.FunctionExpression.resetVariableNames();  
ast.ForOfStatement.resetVariableNames();  
ast.ForInExpression.resetVariableNames();  
ast.FallthroughStatement.resetVariableNames();  
ast.SwitchStatement.resetVariableNames();

}

fn wrapCode(tree, useStrict = false, iifi = false) {

if iifi or useStrict {
  var body = [];

  if useStrict {
    body.push({
      "type": "ExpressionStatement",
      "expression": {
        "type": "Literal",
        "value": "use strict"
      }
    });
  }

  // Add (function () { ... })()
  if iifi {
    body.push({
      "type": "ExpressionStatement",
      "expression": {
        "type": "CallExpression",
        "callee": {
          "type": "FunctionExpression",
          "id": null,
          "params": [],
          "defaults": [],
          "body": {
            "type": "BlockStatement",
            "body": tree.body
          },
          "rest": null,
          "generator": false,
          "expression": false
        },
        "arguments": []
      }
    });
  }

  tree = {
    "type": "Program",
    "body": body
  };
}

return tree;

}

fn generateSpace(len) {

var chars = [];
for var i = 0; i < len; i++ {
  chars.push(' ');
}

return chars.join('');

}

fn generateErrorColumnString(errorStartIndex, errorEndIndex) {

var chars = [];
var i = 0;

if (!errorEndIndex) {
  errorEndIndex = errorStartIndex;
}

for ; i < errorStartIndex; i++ {
  chars.push(' ');
}

for i = errorStartIndex; i <= errorEndIndex; i++ {
  chars.push('^');
}

return chars.join('');

}