"use strict";

/* eslint-disable operator-linebreak */

/* eslint-disable prefer-const */
const common = require('../prelude/common.js');

const generate = require('escodegen').generate;

const babelParser = require('@babel/parser');

const parse = babelParser.parse;
const ALIAS_AS_RELATIVE = common.ALIAS_AS_RELATIVE;
const ALIAS_AS_RESOLVABLE = common.ALIAS_AS_RESOLVABLE;

function forge(pattern, was) {
  return pattern.replace('{c1}', ', ').replace('{v1}', '"' + was.v1 + '"').replace('{c2}', was.v2 ? ', ' : '').replace('{v2}', was.v2 ? '"' + was.v2 + '"' : '').replace('{c3}', was.v3 ? ' from ' : '').replace('{v3}', was.v3 ? was.v3 : '');
}

function valid2(v2) {
  return v2 === undefined || v2 === null || v2 === 'must-exclude' || v2 === 'may-exclude';
}

function visitor_REQUIRE_RESOLVE(n) {
  // eslint-disable-line camelcase
  const c = n.callee;
  if (!c) return null;
  const ci = c.object && c.object.type === 'Identifier' && c.object.name === 'require' && c.property && c.property.type === 'Identifier' && c.property.name === 'resolve';
  if (!ci) return null;
  const f = n.type === 'CallExpression' && n.arguments && n.arguments[0] && n.arguments[0].type === 'Literal';
  if (!f) return null;
  const m = n.arguments[1] && n.arguments[1].type === 'Literal';
  return {
    v1: n.arguments[0].value,
    v2: m ? n.arguments[1].value : null
  };
}

function visitor_REQUIRE(n) {
  // eslint-disable-line camelcase
  const c = n.callee;
  if (!c) return null;
  const ci = c.type === 'Identifier' && c.name === 'require';
  if (!ci) return null;
  const f = n.type === 'CallExpression' && n.arguments && n.arguments[0] && n.arguments[0].type === 'Literal';
  if (!f) return null;
  const m = n.arguments[1] && n.arguments[1].type === 'Literal';
  return {
    v1: n.arguments[0].value,
    v2: m ? n.arguments[1].value : null
  };
}

function visitor_IMPORT(n) {
  // eslint-disable-line camelcase
  const ni = n.type === 'ImportDeclaration';
  if (!ni) return null;
  const s = n.specifiers;
  return {
    v1: n.source.value,
    v3: reconstructSpecifiers(s)
  };
}

function visitor_PATH_JOIN(n) {
  // eslint-disable-line camelcase
  const c = n.callee;
  if (!c) return null;
  const ci = c.object && c.object.type === 'Identifier' && c.object.name === 'path' && c.property && c.property.type === 'Identifier' && c.property.name === 'join';
  if (!ci) return null;
  const dn = n.arguments[0] && n.arguments[0].type === 'Identifier' && n.arguments[0].name === '__dirname';
  if (!dn) return null;
  const f = n.type === 'CallExpression' && n.arguments && n.arguments[1] && n.arguments[1].type === 'Literal' && n.arguments.length === 2; // TODO concate them

  if (!f) return null;
  return {
    v1: n.arguments[1].value
  };
}

module.exports.visitor_SUCCESSFUL = function (node, test) {
  // eslint-disable-line camelcase
  let mustExclude, mayExclude, was;
  was = visitor_REQUIRE_RESOLVE(node);

  if (was) {
    if (test) return forge('require.resolve({v1}{c2}{v2})', was);
    if (!valid2(was.v2)) return null;
    mustExclude = was.v2 === 'must-exclude';
    mayExclude = was.v2 === 'may-exclude';
    return {
      alias: was.v1,
      aliasType: ALIAS_AS_RESOLVABLE,
      mustExclude: mustExclude,
      mayExclude: mayExclude
    };
  }

  was = visitor_REQUIRE(node);

  if (was) {
    if (test) return forge('require({v1}{c2}{v2})', was);
    if (!valid2(was.v2)) return null;
    mustExclude = was.v2 === 'must-exclude';
    mayExclude = was.v2 === 'may-exclude';
    return {
      alias: was.v1,
      aliasType: ALIAS_AS_RESOLVABLE,
      mustExclude: mustExclude,
      mayExclude: mayExclude
    };
  }

  was = visitor_IMPORT(node);

  if (was) {
    if (test) return forge('import {v3}{c3}{v1}', was);
    return {
      alias: was.v1,
      aliasType: ALIAS_AS_RESOLVABLE
    };
  }

  was = visitor_PATH_JOIN(node);

  if (was) {
    if (test) return forge('path.join(__dirname{c1}{v1})', was);
    return {
      alias: was.v1,
      aliasType: ALIAS_AS_RELATIVE,
      mayExclude: false
    };
  }

  return null;
};

function visitor_NONLITERAL(n) {
  // eslint-disable-line camelcase
  return function () {
    const c = n.callee;
    if (!c) return null;
    const ci = c.object && c.object.type === 'Identifier' && c.object.name === 'require' && c.property && c.property.type === 'Identifier' && c.property.name === 'resolve';
    if (!ci) return null;
    const f = n.type === 'CallExpression' && n.arguments && n.arguments[0] && n.arguments[0].type !== 'Literal';
    if (!f) return null;
    const m = n.arguments[1];
    if (!m) return {
      v1: reconstruct(n.arguments[0])
    };
    const q = n.arguments[1] && n.arguments[1].type === 'Literal';
    if (!q) return null;
    return {
      v1: reconstruct(n.arguments[0]),
      v2: n.arguments[1].value
    };
  }() || function () {
    const c = n.callee;
    if (!c) return null;
    const ci = c.type === 'Identifier' && c.name === 'require';
    if (!ci) return null;
    const f = n.type === 'CallExpression' && n.arguments && n.arguments[0] && n.arguments[0].type !== 'Literal';
    if (!f) return null;
    const m = n.arguments[1];
    if (!m) return {
      v1: reconstruct(n.arguments[0])
    };
    const q = n.arguments[1] && n.arguments[1].type === 'Literal';
    if (!q) return null;
    return {
      v1: reconstruct(n.arguments[0]),
      v2: n.arguments[1].value
    };
  }();
}

module.exports.visitor_NONLITERAL = function (node) {
  // eslint-disable-line camelcase
  let mustExclude, mayExclude, was;
  was = visitor_NONLITERAL(node);

  if (was) {
    if (!valid2(was.v2)) return null;
    mustExclude = was.v2 === 'must-exclude';
    mayExclude = was.v2 === 'may-exclude';
    return {
      alias: was.v1,
      mustExclude: mustExclude,
      mayExclude: mayExclude
    };
  }

  return null;
};

function visitor_MALFORMED(n) {
  // eslint-disable-line camelcase
  return function () {
    const c = n.callee;
    if (!c) return null;
    const ci = c.object && c.object.type === 'Identifier' && c.object.name === 'require' && c.property && c.property.type === 'Identifier' && c.property.name === 'resolve';
    if (!ci) return null;
    const f = n.type === 'CallExpression' && n.arguments && n.arguments[0];
    if (!f) return null;
    return {
      v1: reconstruct(n.arguments[0])
    };
  }() || function () {
    const c = n.callee;
    if (!c) return null;
    const ci = c.type === 'Identifier' && c.name === 'require';
    if (!ci) return null;
    const f = n.type === 'CallExpression' && n.arguments && n.arguments[0];
    if (!f) return null;
    return {
      v1: reconstruct(n.arguments[0])
    };
  }();
}

module.exports.visitor_MALFORMED = function (node) {
  // eslint-disable-line camelcase
  let was;
  was = visitor_MALFORMED(node);
  if (was) return {
    alias: was.v1
  };
  return null;
};

function visitor_USESCWD(n) {
  // eslint-disable-line camelcase
  const c = n.callee;
  if (!c) return null;
  const ci = c.object && c.object.type === 'Identifier' && c.object.name === 'path' && c.property && c.property.type === 'Identifier' && c.property.name === 'resolve';
  if (!ci) return null;
  return {
    v1: n.arguments.map(reconstruct).join(', ')
  };
}

module.exports.visitor_USESCWD = function (node) {
  // eslint-disable-line camelcase
  let was;
  was = visitor_USESCWD(node);
  if (was) return {
    alias: was.v1
  };
  return null;
};

function reconstructSpecifiers(specs) {
  if (!specs || !specs.length) return '';
  const defaults = [];

  for (const spec of specs) {
    if (spec.type === 'ImportDefaultSpecifier') {
      defaults.push(spec.local.name);
    }
  }

  const nonDefaults = [];

  for (const spec of specs) {
    if (spec.type === 'ImportSpecifier') {
      if (spec.local.name === spec.imported.name) {
        nonDefaults.push(spec.local.name);
      } else {
        nonDefaults.push(spec.imported.name + ' as ' + spec.local.name);
      }
    }
  }

  if (nonDefaults.length) {
    defaults.push('{ ' + nonDefaults.join(', ') + ' }');
  }

  return defaults.join(', ');
}

function reconstruct(node) {
  let v = generate(node).replace(/\n/g, '');
  let v2;

  while (true) {
    v2 = v.replace(/\[ /g, '[').replace(/ \]/g, ']').replace(/ {2}/g, ' ');
    if (v2 === v) break;
    v = v2;
  }

  return v2;
}

function traverse(ast, visitor) {
  // modified esprima-walk to support
  // visitor return value and "trying" flag
  let stack = [[ast, false]];
  let i, j, key;
  let len, item, node, trying, child;

  for (i = 0; i < stack.length; i += 1) {
    item = stack[i];
    node = item[0];

    if (node) {
      trying = item[1] || node.type === 'TryStatement';

      if (visitor(node, trying)) {
        for (key in node) {
          child = node[key];

          if (child instanceof Array) {
            len = child.length;

            for (j = 0; j < len; j += 1) {
              stack.push([child[j], trying]);
            }
          } else if (child && typeof child.type === 'string') {
            stack.push([child, trying]);
          }
        }
      }
    }
  }
}

module.exports.parse = function (body) {
  return parse(body, {
    allowImportExportEverywhere: true,
    allowReturnOutsideFunction: true,
    ecmaVersion: 8,
    plugins: ['estree', 'bigInt']
  });
};

module.exports.detect = function (body, visitor) {
  const json = module.exports.parse(body);
  if (!json) return;
  traverse(json, visitor);
};