'use strict';

var acorn = require('acorn');
var escodegen = require('escodegen');
var vm = require('vm');
var fs = require('fs');
var path = require('path');

var PDFJS_PREPROCESSOR_NAME = 'PDFJSDev';
var ROOT_PREFIX = '$ROOT/';

function isLiteral(obj, value) {
  return obj.type === 'Literal' && obj.value === value;
}

function isPDFJSPreprocessor(obj) {
  return obj.type === 'Identifier' &&
         obj.name === PDFJS_PREPROCESSOR_NAME;
}

function evalWithDefines(code, defines, loc) {
  if (!code || !code.trim()) {
    throw new Error('No JavaScript expression given');
  }
  return vm.runInNewContext(code, defines, {displayErrors: false});
}

function handlePreprocessorAction(ctx, actionName, args, loc) {
  try {
    var arg;
    switch (actionName) {
      case 'test':
        arg = args[0];
        if (!arg || arg.type !== 'Literal' ||
            typeof arg.value !== 'string') {
          throw new Error('No code for testing is given');
        }
        var isTrue = !!evalWithDefines(arg.value, ctx.defines);
        return {type: 'Literal', value: isTrue, loc: loc};
      case 'eval':
        arg = args[0];
        if (!arg || arg.type !== 'Literal' ||
            typeof arg.value !== 'string') {
          throw new Error('No code for eval is given');
        }
        var result = evalWithDefines(arg.value, ctx.defines);
        if (typeof result === 'boolean' || typeof result === 'string' ||
            typeof result === 'number') {
          return {type: 'Literal', value: result, loc: loc};
        }
        if (typeof result === 'object') {
          var parsedObj = acorn.parse('(' + JSON.stringify(result) + ')');
          parsedObj.body[0].expression.loc = loc;
          return parsedObj.body[0].expression;
        }
        break;
      case 'json':
        arg = args[0];
        if (!arg || arg.type !== 'Literal' ||
            typeof arg.value !== 'string') {
          throw new Error('Path to JSON is not provided');
        }
        var jsonPath = arg.value;
        if (jsonPath.indexOf(ROOT_PREFIX) === 0) {
          jsonPath = path.join(ctx.rootPath,
                               jsonPath.substring(ROOT_PREFIX.length));
        }
        var jsonContent = fs.readFileSync(jsonPath).toString();
        var parsedJSON = acorn.parse('(' + jsonContent + ')');
        parsedJSON.body[0].expression.loc = loc;
        return parsedJSON.body[0].expression;
    }
    throw new Error('Unsupported action');
  } catch (e) {
    throw new Error('Could not process ' + PDFJS_PREPROCESSOR_NAME + '.' +
                    actionName + ' at ' + JSON.stringify(loc) + '\n' +
                    e.name + ': ' + e.message);
  }
}

function postprocessNode(ctx, node) {
  switch (node.type) {
    case 'IfStatement':
      if (isLiteral(node.test, true)) {
        // if (true) stmt1; => stmt1
        return node.consequent;
      } else if (isLiteral(node.test, false)) {
        // if (false) stmt1; else stmt2; => stmt2
        return node.alternate || {type: 'EmptyStatement', loc: node.loc};
      }
      break;
    case 'ConditionalExpression':
      if (isLiteral(node.test, true)) {
        // true ? stmt1 : stmt2 => stmt1
        return node.consequent;
      } else if (isLiteral(node.test, false)) {
        // false ? stmt1 : stmt2 => stmt2
        return node.alternate;
      }
      break;
    case 'UnaryExpression':
      if (node.operator === 'typeof' &&
          isPDFJSPreprocessor(node.argument)) {
        // typeof PDFJSDev => 'object'
        return {type: 'Literal', value: 'object', loc: node.loc};
      }
      if (node.operator === '!' &&
          node.argument.type === 'Literal' &&
          typeof node.argument.value === 'boolean') {
        // !true => false,  !false => true
        return {type: 'Literal', value: !node.argument.value, loc: node.loc};
      }
      break;
    case 'LogicalExpression':
      switch (node.operator) {
        case '&&':
          if (isLiteral(node.left, true)) {
            return node.right;
          }
          if (isLiteral(node.left, false)) {
            return node.left;
          }
          break;
        case '||':
          if (isLiteral(node.left, true)) {
            return node.left;
          }
          if (isLiteral(node.left, false)) {
            return node.right;
          }
          break;
      }
      break;
    case 'BinaryExpression':
      switch (node.operator) {
        case '==':
        case '===':
        case '!=':
        case '!==':
          if (node.left.type === 'Literal' &&
              node.right.type === 'Literal' &&
              typeof node.left.value === typeof node.right.value) {
             // folding two literals == and != check
             switch (typeof node.left.value) {
               case 'string':
               case 'boolean':
               case 'number':
                 var equal = node.left.value === node.right.value;
                 return {
                   type: 'Literal',
                   value: (node.operator[0] === '=') === equal,
                   loc: node.loc
                 };
             }
          }
          break;
      }
      break;
    case 'CallExpression':
      if (node.callee.type === 'MemberExpression' &&
          isPDFJSPreprocessor(node.callee.object) &&
          node.callee.property.type === 'Identifier') {
        // PDFJSDev.xxxx(arg1, arg2, ...) => tranform
        var action = node.callee.property.name;
        return handlePreprocessorAction(ctx, action,
                                        node.arguments, node.loc);
      }
      break;
    case 'BlockStatement':
      var subExpressionIndex = 0;
      while (subExpressionIndex < node.body.length) {
        switch (node.body[subExpressionIndex].type) {
          case 'EmptyStatement':
            // Removing empty statements from the blocks.
            node.body.splice(subExpressionIndex, 1);
            continue;
          case 'BlockStatement':
            // Block statements inside a block are moved to the parent one.
            var subChildren = node.body[subExpressionIndex].body;
            Array.prototype.splice.apply(node.body,
              [subExpressionIndex, 1].concat(subChildren));
            subExpressionIndex += Math.max(subChildren.length - 1, 0);
            continue;
          case 'ReturnStatement':
          case 'ThrowStatement':
            // Removing dead code after return or throw.
            node.body.splice(subExpressionIndex + 1,
                             node.body.length - subExpressionIndex - 1);
            break;
        }
        subExpressionIndex++;
      }
      break;
    case 'FunctionDeclaration':
    case 'FunctionExpression':
      var block = node.body;
      if (block.body.length > 0 &&
          block.body[block.body.length - 1].type === 'ReturnStatement' &&
          !block.body[block.body.length - 1].argument) {
        // Function body ends with return without arg -- removing it.
        block.body.pop();
      }
      break;
    case 'Program':
      // Checking for a function closure that looks like UMD header.
      node.body.some(function (item, index) {
        // Is it `(function (root, factory) { ? }(this, function (?) {?}));` ?
        if (item.type !== 'ExpressionStatement' ||
            item.expression.type !== 'CallExpression' ||
            item.expression.callee.type !== 'FunctionExpression' ||
            item.expression.callee.params.length !== 2 ||
            item.expression.arguments.length !== 2 ||
            item.expression.arguments[0].type !== 'ThisExpression' ||
            item.expression.arguments[1].type !== 'FunctionExpression') {
          return false;
        }
        var init = item.expression.callee;
        // Is init body looks like
        // `if (?) { ? } else if (typeof exports !== 'undefined') { ? } ...`?
        if (init.body.type !== 'BlockStatement' ||
            init.body.body.length !== 1 ||
            init.body.body[0].type !== 'IfStatement') {
          return false;
        }
        var initIf = init.body.body[0];
        if (initIf.alternate.type !== 'IfStatement' ||
            initIf.alternate.test.type !== 'BinaryExpression' ||
            initIf.alternate.test.operator !== '!==' ||
            initIf.alternate.test.left.type !== 'UnaryExpression' ||
            initIf.alternate.test.left.operator !== 'typeof' ||
            initIf.alternate.test.left.argument.type !== 'Identifier' ||
            initIf.alternate.test.left.argument.name !== 'exports' ||
            initIf.alternate.test.right.type !== 'Literal' ||
            initIf.alternate.test.right.value !== 'undefined' ||
            initIf.alternate.consequent.type !== 'BlockStatement') {
          return false;
        }
        var commonJsInit = initIf.alternate.consequent;
        // Is commonJsInit `factory(exports, ...)` ?
        if (commonJsInit.body.length !== 1 ||
            commonJsInit.body[0].type !== 'ExpressionStatement' ||
            commonJsInit.body[0].expression.type !== 'CallExpression' ||
            commonJsInit.body[0].expression.callee.type !== 'Identifier') {
          return false;
        }
        var commonJsInitArgs = commonJsInit.body[0].expression.arguments;
        if (commonJsInitArgs.length === 0 ||
            commonJsInitArgs[0].type !== 'Identifier' ||
            commonJsInitArgs[0].name !== 'exports') {
          return false;
        }
        var factory = item.expression.arguments[1];
        // Is factory `function (exports, ....) { ? }` ?
        if (factory.params.length === 0 ||
            factory.params[0].type !== 'Identifier' ||
            factory.params[0].name !== 'exports' ||
            factory.body.type !== 'BlockStatement') {
          return true;
        }
        var factoryParams = factory.params;
        var factoryBody = factory.body;

        // Remove closure and function and replacing parameters with vars.
        node.body.splice(index, 1);
        for (var i = 1, ii = factoryParams.length; i < ii; i++) {
          var varNode = {
            type: 'VariableDeclaration',
            'declarations': [{
                type: 'VariableDeclarator',
                id: factoryParams[i],
                init: commonJsInitArgs[i] || null,
                loc: factoryParams[i].loc
            }],
            kind: 'var'
          };
          node.body.splice(index++, 0, varNode);
        }
        factoryBody.body.forEach(function (item) {
          node.body.splice(index++, 0, item);
        });
        return true;
      });
      break;
  }
  return node;
}

function fixComments(ctx, node) {
  if (!ctx.saveComments) {
    return;
  }
  // Fixes double comments in the escodegen output.
  delete node.trailingComments;
  // Removes ESLint and other service comments.
  if (node.leadingComments) {
    var CopyrightRegExp = /\bcopyright\b/i;
    var BlockCommentRegExp = /^\s*(globals|eslint|falls through)\b/;
    var LineCommentRegExp = /^\s*eslint\b/;

    var i = 0;
    while (i < node.leadingComments.length) {
      var type = node.leadingComments[i].type;
      var value = node.leadingComments[i].value;

      if (ctx.saveComments === 'copyright') {
        // Remove all comments, except Copyright notices and License headers.
        if (!(type === 'Block' && CopyrightRegExp.test(value))) {
          node.leadingComments.splice(i, 1);
          continue;
        }
      } else if ((type === 'Block' && BlockCommentRegExp.test(value)) ||
                 (type === 'Line' && LineCommentRegExp.test(value))) {
        node.leadingComments.splice(i, 1);
        continue;
      }
      i++;
    }
  }
}

function traverseTree(ctx, node) {
  // generic node processing
  for (var i in node) {
    var child = node[i];
    if (typeof child === 'object' && child !== null && child.type) {
      var result = traverseTree(ctx, child);
      if (result !== child) {
        node[i] = result;
      }
    } else if (Array.isArray(child)) {
      child.forEach(function (childItem, index) {
        if (typeof childItem === 'object' && childItem !== null &&
            childItem.type) {
          var result = traverseTree(ctx, childItem);
          if (result !== childItem) {
            child[index] = result;
          }
        }
      });
    }
  }

  node = postprocessNode(ctx, node) || node;

  fixComments(ctx, node);
  return node;
}

function preprocessPDFJSCode(ctx, code) {
  var format = ctx.format || {
    indent: {
      style: ' ',
    }
  };
  var parseOptions = {
    locations: true,
    sourceFile: ctx.sourceFile,
    sourceType: 'module',
  };
  var codegenOptions = {
    format: format,
    parse: acorn.parse,
    sourceMap: ctx.sourceMap,
    sourceMapWithCode: ctx.sourceMap,
  };
  var syntax = acorn.parse(code, parseOptions);
  traverseTree(ctx, syntax);
  return escodegen.generate(syntax, codegenOptions);
}

exports.preprocessPDFJSCode = preprocessPDFJSCode;