"use strict";

// This file is currently named parser-babylon.js to maintain backwards compatibility.
// However, it should be named parser-babel.js in the next major release.

const createError = require("../common/parser-create-error");
const hasPragma = require("./pragma").hasPragma;
const locFns = require("./loc");
const postprocess = require("./postprocess");

function babelOptions(extraOptions, extraPlugins) {
  return Object.assign(
    {
      sourceType: "module",
      allowAwaitOutsideFunction: true,
      allowImportExportEverywhere: true,
      allowReturnOutsideFunction: true,
      allowSuperOutsideMethod: true,
      allowUndeclaredExports: true,
      errorRecovery: true,
      plugins: [
        "jsx",
        "doExpressions",
        "objectRestSpread",
        "classProperties",
        "exportDefaultFrom",
        "exportNamespaceFrom",
        "asyncGenerators",
        "functionBind",
        "functionSent",
        "dynamicImport",
        "numericSeparator",
        "importMeta",
        "optionalCatchBinding",
        "optionalChaining",
        "classPrivateProperties",
        ["pipelineOperator", { proposal: "minimal" }],
        "nullishCoalescingOperator",
        "bigInt",
        "throwExpressions",
        "logicalAssignment",
        "classPrivateMethods",
        "v8intrinsic",
        "partialApplication"
      ].concat(extraPlugins)
    },
    extraOptions
  );
}

function createParse(parseMethod, extraPlugins) {
  return (text, parsers, opts) => {
    // Inline the require to avoid loading all the JS if we don't use it
    const babel = require("@babel/parser");

    const combinations = [
      babelOptions(
        { strictMode: true },
        ["decorators-legacy"].concat(extraPlugins)
      ),
      babelOptions(
        { strictMode: false },
        ["decorators-legacy"].concat(extraPlugins)
      ),
      babelOptions(
        { strictMode: true },
        [["decorators", { decoratorsBeforeExport: false }]].concat(extraPlugins)
      ),
      babelOptions(
        { strictMode: false },
        [["decorators", { decoratorsBeforeExport: false }]].concat(extraPlugins)
      )
    ];

    let ast;
    try {
      ast = tryCombinations(babel[parseMethod].bind(null, text), combinations);
    } catch (error) {
      throw createError(
        // babel error prints (l:c) with cols that are zero indexed
        // so we need our custom error
        error.message.replace(/ \(.*\)/, ""),
        {
          start: {
            line: error.loc.line,
            column: error.loc.column + 1
          }
        }
      );
    }
    delete ast.tokens;
    return postprocess(ast, Object.assign({}, opts, { originalText: text }));
  };
}

const parse = createParse("parse", ["flow"]);
const parseFlow = createParse("parse", [["flow", { all: true, enums: true }]]);
const parseExpression = createParse("parseExpression");

function tryCombinations(fn, combinations) {
  let error;
  for (let i = 0; i < combinations.length; i++) {
    try {
      return fn(combinations[i]);
    } catch (_error) {
      if (!error) {
        error = _error;
      }
    }
  }
  throw error;
}

function parseJson(text, parsers, opts) {
  const ast = parseExpression(text, parsers, opts);

  ast.comments.forEach(assertJsonNode);
  assertJsonNode(ast);

  return ast;
}

function assertJsonNode(node, parent) {
  switch (node.type) {
    case "ArrayExpression":
      return node.elements.forEach(assertJsonChildNode);
    case "ObjectExpression":
      return node.properties.forEach(assertJsonChildNode);
    case "ObjectProperty":
      // istanbul ignore if
      if (node.computed) {
        throw createJsonError("computed");
      }
      // istanbul ignore if
      if (node.shorthand) {
        throw createJsonError("shorthand");
      }
      return [node.key, node.value].forEach(assertJsonChildNode);
    case "UnaryExpression":
      switch (node.operator) {
        case "+":
        case "-":
          return assertJsonChildNode(node.argument);
        // istanbul ignore next
        default:
          throw createJsonError("operator");
      }
    case "Identifier":
      if (parent && parent.type === "ObjectProperty" && parent.key === node) {
        return;
      }
      throw createJsonError();
    case "NullLiteral":
    case "BooleanLiteral":
    case "NumericLiteral":
    case "StringLiteral":
      return;
    // istanbul ignore next
    default:
      throw createJsonError();
  }

  function assertJsonChildNode(child) {
    return assertJsonNode(child, node);
  }

  // istanbul ignore next
  function createJsonError(attribute) {
    const name = !attribute
      ? node.type
      : `${node.type} with ${attribute}=${JSON.stringify(node[attribute])}`;
    return createError(`${name} is not allowed in JSON.`, {
      start: {
        line: node.loc.start.line,
        column: node.loc.start.column + 1
      }
    });
  }
}

const babel = Object.assign({ parse, astFormat: "estree", hasPragma }, locFns);
const babelFlow = Object.assign({}, babel, { parse: parseFlow });
const babelExpression = Object.assign({}, babel, { parse: parseExpression });

// Export as a plugin so we can reuse the same bundle for UMD loading
module.exports = {
  parsers: {
    babel,
    "babel-flow": babelFlow,
    // aliased to keep backwards compatibility
    babylon: babel,
    json: Object.assign({}, babelExpression, {
      hasPragma() {
        return true;
      }
    }),
    json5: babelExpression,
    "json-stringify": Object.assign(
      {
        parse: parseJson,
        astFormat: "estree-json"
      },
      locFns
    ),
    /** @internal */
    __js_expression: babelExpression,
    /** for vue filter */
    __vue_expression: babelExpression,
    /** for vue event binding to handle semicolon */
    __vue_event_binding: babel
  }
};
