const symbolNames = {
  ' ': 'SPACE',
  '!': 'BANG',
  '.': 'DOT',
  '=': 'EQUALS',
  '~': 'TILDE',
  $: 'DOLLAR',
  '#': 'HASH',
  '%': 'PERCENT',
  '/': 'SLASH',
  '<': 'LESSTHAN',
  '>': 'GREATERTHAN',
  '&': 'AMPERSAND',
  '?': 'QUESTIONMARK',
  "'": 'SINGLEQUOTE',
  '"': 'DOUBLEQUOTE',
  '+': 'PLUS',
  ',': 'COMMA',
  ';': 'SEMICOLON',
  ':': 'COLON',
  '(': 'LPAREN',
  ')': 'RPAREN',
  '{': 'LBRACE',
  '}': 'RBRACE',
  '[': 'LSQBRACE',
  ']': 'RSQBRACE',
};
const escodegen = require('escodegen');
const replaceDescendantNode = require('../replaceDescendantNode');
const estraverse = require('estraverse-fb');

module.exports = (
  queryObj,
  { globalNames = [], wrapInFunction = false, stringLiterals = false } = {}
) => {
  const allGlobalNames = [
    ...globalNames,
    'eval',
    'clearInterval',
    'clearTimeout',
    'document',
    'event',
    'frames',
    'history',
    'Image',
    'localStorage',
    'location',
    'name',
    'navigator',
    'Option',
    'parent',
    'screen',
    'sessionStorage',
    'setInterval',
    'setTimeout',
    'Storage',
    'window',
    'XMLHttpRequest',
    'Math',
    'Math.min',
    'Math.max',
    'Math.round',
    'Function',
    'Date',
    'Date.prototype',
    'Math.E',
    'Math.LN2',
    'Math.LN10',
    'Math.LOG2E',
    'Math.LOG10E',
    'Math.PI',
    'Math.SQRT1_2',
    'Math.SQRT2',
    'Math.abs',
    'Math.acos',
    'Math.asin',
    'Math.atan',
    'Math.atan2',
    'Math.ceil',
    'Math.cos',
    'Math.exp',
    'Math.floor',
    'Math.log',
    'Math.max',
    'Math.min',
    'Math.pow',
    'Math.random',
    'Math.round',
    'Math.sin',
    'Math.sqrt',
    'Math.tan',
    'parseInt',
    'parseFloat',
    'isNaN',
    'NaN',
    'RegExp',
    'RegExp.prototype',
    'RegExp.prototype.compile',
    'RegExp.prototype.test',
    'RegExp.prototype.exec',
    'String',
    'String.fromCharCode',
    'String.prototype',
    'String.prototype.charAt',
    'String.prototype.charCodeAt',
    'String.prototype.indexOf',
    'String.prototype.match',
    'String.prototype.replace',
    'String.prototype.slice',
    'String.prototype.split',
    'String.prototype.substr',
    'String.prototype.substring',
    'String.prototype.toLowerCase',
    'String.prototype.toUpperCase',
    'Array',
    'Array.prototype',
    'Array.prototype.concat',
    'Array.prototype.indexOf',
    'Array.prototype.join',
    'Array.prototype.pop',
    'Array.prototype.push',
    'Array.prototype.reverse',
    'Array.prototype.shift',
    'Array.prototype.slice',
    'Array.prototype.sort',
    'Array.prototype.splice',
    'Array.prototype.unshift',
    'Number',
    'Number.prototype',
    'Number.prototype.toFixed',
    'Number.MAX_VALUE',
    'Number.MIN_VALUE',
    'Number.NEGATIVE_INFINITY',
    'Number.NaN',
    'Number.POSITIVE_INFINITY',
    'Number.prototype',
    'Boolean',
    'Boolean.prototype',
    'Error',
    'Error.prototype',
    'EvalError',
    'EvalError.prototype',
    'Infinity',
    'JSON',
    'JSON.stringify',
    'JSON.parse',
    'Object',
    'Object.prototype',
    'Object.prototype.toString',
    'RangeError',
    'RangeError.prototype',
    'ReferenceError',
    'ReferenceError.prototype',
    'SyntaxError',
    'SyntaxError.prototype',
    'TypeError',
    'TypeError.prototype',
    'URIError',
    'URIError.prototype',
    'decodeURI',
    'decodeURIComponent',
    'encodeURI',
    'encodeURIComponent',
    'isFinite',
    'undefined',
  ];

  const globalsObj = {};
  for (const globalName of allGlobalNames) {
    globalsObj[globalName] = true;
  }
  return function pullGlobalsIntoVariables(assetGraph) {
    for (const javaScriptAsset of assetGraph.findAssets({
      type: 'JavaScript',
      ...queryObj,
    })) {
      const occurrencesByGlobalName = {};
      const occurrencesByString = {};
      const ast = javaScriptAsset.parseTree;
      const seenNames = {}; // To avoid collisions when introducing new vars
      const seenLocals = {}; // To avoid aliasing globals that are shadowed by a local var somewhere

      estraverse.traverse(ast, {
        enter(node, parentNode) {
          if (node.type === 'MemberExpression' && !node.computed) {
            const name = escodegen.generate(node);
            if (typeof globalsObj[name] !== 'undefined') {
              if (
                !Object.prototype.hasOwnProperty.call(
                  occurrencesByGlobalName,
                  name
                )
              ) {
                occurrencesByGlobalName[name] = [];
              }
              occurrencesByGlobalName[name].push({ node, parentNode });
            } else if (node.property.name.length > 2) {
              // .foo() => [a]() won't save anything if the method name is 2 chars or less

              if (
                !Object.prototype.hasOwnProperty.call(
                  occurrencesByString,
                  node.property.name
                )
              ) {
                occurrencesByString[node.property.name] = [];
              }
              occurrencesByString[node.property.name].push({
                node,
                parentNode,
              });
            }
          } else if (node.type === 'FunctionDeclaration') {
            seenLocals[node.id.name] = true;
          } else if (node.type === 'FunctionExpression') {
            if (node.id) {
              seenLocals[node.id.name] = true;
            }
          } else if (node.type === 'VariableDeclaration') {
            for (const declaration of node.declarations) {
              seenNames[declaration.id.name] = true;
              seenLocals[declaration.id.name] = true;
            }
          } else if (
            node.type === 'Literal' &&
            typeof node.value === 'string'
          ) {
            if (
              !Object.prototype.hasOwnProperty.call(
                occurrencesByString,
                node.value
              )
            ) {
              occurrencesByString[node.value] = [];
            }
            occurrencesByString[node.value].push({ node, parentNode });
          } else if (node.type === 'Identifier') {
            seenNames[node.name] = true;
            if (Object.prototype.hasOwnProperty.call(globalsObj, node.name)) {
              if (
                !Object.prototype.hasOwnProperty.call(
                  occurrencesByGlobalName,
                  node.name
                )
              ) {
                occurrencesByGlobalName[node.name] = [];
              }
              occurrencesByGlobalName[node.name].push({ node, parentNode });
            }
          }
        },
      });

      // Order by number of dots ascending so e.g. Math is considered before Math.min:
      const globalNames = Object.keys(occurrencesByGlobalName).sort((a, b) => {
        return a.split('.').length - b.split('.').length;
      });
      const aliasDeclarations = [];
      const aliasByGlobalName = {};

      /* eslint-disable no-inner-declarations */
      function nameToAst(name) {
        name =
          (!wrapInFunction &&
            Object.prototype.hasOwnProperty.call(aliasByGlobalName, name) &&
            aliasByGlobalName[name]) ||
          name;
        const nameFragments = name.split('.');
        if (nameFragments.length > 1) {
          return {
            type: 'MemberExpression',
            computed: false,
            object:
              nameToAst(
                nameFragments.slice(0, nameFragments.length - 1).join('.')
              ) || 'wat1',
            property: {
              type: 'Identifier',
              name: nameFragments[nameFragments.length - 1],
            },
          };
        } else {
          return { type: 'Identifier', name: nameFragments[0] };
        }
      }

      for (const globalName of globalNames) {
        if (
          !Object.prototype.hasOwnProperty.call(seenLocals, globalName) &&
          occurrencesByGlobalName[globalName].length > 1
        ) {
          let alias = globalName.replace(/\./g, '').toUpperCase();
          while (Object.prototype.hasOwnProperty.call(seenNames, alias)) {
            alias += '_';
          }
          seenNames[alias] = true;
          aliasDeclarations.push({
            name: alias,
            valueAst: nameToAst(globalName),
          });
          for (const occurrence of occurrencesByGlobalName[globalName]) {
            replaceDescendantNode(occurrence.parentNode, occurrence.node, {
              type: 'Identifier',
              name: alias,
            });
          }
          aliasByGlobalName[globalName] = alias;
        } else {
          occurrencesByGlobalName[globalName] = undefined;
        }
      }

      if (stringLiterals) {
        for (const string of Object.keys(occurrencesByString)) {
          const occurrences = occurrencesByString[string];

          if (occurrences.length >= 2) {
            let alias = string || 'EMPTY';
            for (const symbolName of Object.keys(symbolNames)) {
              while (alias.includes(symbolName)) {
                alias = alias.replace(symbolName, symbolNames[symbolName]);
              }
            }

            if (/^[0-9]/.test(alias)) {
              alias = `_${alias}`;
            }
            alias = alias.replace(/[^a-z0-9_]/gi, '').toUpperCase();
            while (
              !alias ||
              Object.prototype.hasOwnProperty.call(seenNames, alias)
            ) {
              alias += '_';
            }
            seenNames[alias] = true;
            aliasDeclarations.push({
              name: alias,
              valueAst: { type: 'Literal', value: string },
            });
            for (const occurrence of occurrences) {
              if (
                occurrence.node.type === 'Literal' &&
                typeof occurrence.node.value === 'string'
              ) {
                replaceDescendantNode(occurrence.parentNode, occurrence.node, {
                  type: 'Identifier',
                  name: alias,
                });
              } else {
                // parent is AST_Dot
                replaceDescendantNode(occurrence.parentNode, occurrence.node, {
                  type: 'MemberExpression',
                  computed: true,
                  property: { type: 'Identifier', name: alias },
                  object: occurrence.node.object,
                });
              }
            }
          }
        }
      }

      if (aliasDeclarations.length) {
        if (wrapInFunction) {
          ast.body = [
            {
              type: 'ExpressionStatement',
              expression: {
                type: 'CallExpression',
                arguments: aliasDeclarations.map((d) => d.valueAst),
                callee: {
                  type: 'FunctionExpression',
                  params: aliasDeclarations.map((aliasDeclaration) => {
                    return { type: 'Identifier', name: aliasDeclaration.name };
                  }),
                  body: {
                    type: 'BlockStatement',
                    body: ast.body,
                  },
                },
              },
            },
          ];
        } else {
          ast.body.unshift({
            type: 'VariableDeclaration',
            kind: 'var',
            declarations: aliasDeclarations.map((aliasDeclaration) => {
              return {
                type: 'VariableDeclarator',
                id: { type: 'Identifier', name: aliasDeclaration.name },
                init: aliasDeclaration.valueAst,
              };
            }),
          });
        }
        javaScriptAsset.markDirty();
      }
    }
  };
};
