/**
 * @file Ensures that definitions and uses of properties on the
 * ``lazy`` object are valid.
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

import helpers from "../helpers.mjs";

const items = [
  "loader",
  "XPCOMUtils",
  "Integration",
  "ChromeUtils",
  "DevToolsUtils",
  "Object",
  "Reflect",
];

const callExpressionDefinitions = [
  /^loader\.lazyGetter\(lazy, "(\w+)"/,
  /^loader\.lazyServiceGetter\(lazy, "(\w+)"/,
  /^loader\.lazyRequireGetter\(lazy, "(\w+)"/,
  /^Integration\.downloads\.defineESModuleGetter\(lazy, "(\w+)"/,
  /^ChromeUtils\.defineLazyGetter\(lazy, "(\w+)"/,
  /^XPCOMUtils\.defineLazyPreferenceGetter\(lazy, "(\w+)"/,
  /^XPCOMUtils\.defineLazyScriptGetter\(lazy, "(\w+)"/,
  /^XPCOMUtils\.defineLazyServiceGetter\(lazy, "(\w+)"/,
  /^XPCOMUtils\.defineConstant\(lazy, "(\w+)"/,
  /^DevToolsUtils\.defineLazyGetter\(lazy, "(\w+)"/,
  /^Object\.defineProperty\(lazy, "(\w+)"/,
  /^Reflect\.defineProperty\(lazy, "(\w+)"/,
];

const callExpressionMultiDefinitions = [
  "ChromeUtils.defineESModuleGetters(lazy,",
  "XPCOMUtils.defineLazyServiceGetters(lazy,",
  "Object.defineProperties(lazy,",
  "loader.lazyRequireGetter(lazy,",
];

export default {
  meta: {
    docs: {
      url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/valid-lazy.html",
    },
    messages: {
      duplicateSymbol: "Duplicate symbol {{name}} being added to lazy.",
      incorrectType: "Unexpected literal for property name {{name}}",
      unknownProperty: "Unknown lazy member property {{name}}",
      unusedProperty: "Unused lazy property {{name}}",
      topLevelAndUnconditional:
        "Lazy property {{name}} is used at top-level unconditionally. It should be non-lazy.",
    },
    schema: [],
    type: "problem",
  },

  create(context) {
    let lazyProperties = new Map();
    let unknownProperties = [];
    let isLazyExported = false;

    function getAncestorNodes(node) {
      const ancestors = [];
      node = node.parent;
      while (node) {
        ancestors.unshift(node);
        node = node.parent;
      }
      return ancestors;
    }

    // Returns true if lazy getter definitions in prevNode and currNode are
    // duplicate.
    // This returns false if prevNode and currNode have the same IfStatement as
    // ancestor and they're in different branches.
    function isDuplicate(prevNode, currNode) {
      const prevAncestors = getAncestorNodes(prevNode);
      const currAncestors = getAncestorNodes(currNode);

      for (
        let i = 0;
        i < prevAncestors.length && i < currAncestors.length;
        i++
      ) {
        const prev = prevAncestors[i];
        const curr = currAncestors[i];
        if (prev === curr && prev.type === "IfStatement") {
          if (prevAncestors[i + 1] !== currAncestors[i + 1]) {
            return false;
          }
        }
      }

      return true;
    }

    function addProp(callNode, propNode, name) {
      if (
        lazyProperties.has(name) &&
        isDuplicate(lazyProperties.get(name).callNode, callNode)
      ) {
        context.report({
          node: propNode,
          messageId: "duplicateSymbol",
          data: { name },
        });
        return;
      }
      lazyProperties.set(name, { used: false, callNode, propNode });
    }

    function setPropertiesFromArgument(callNode, arg) {
      if (arg.type === "ObjectExpression") {
        for (let propNode of arg.properties) {
          if (propNode.key.type == "Literal") {
            context.report({
              node: propNode,
              messageId: "incorrectType",
              data: { name: propNode.key.value },
            });
            continue;
          }
          addProp(callNode, propNode, propNode.key.name);
        }
      } else if (arg.type === "ArrayExpression") {
        for (let propNode of arg.elements) {
          if (propNode.type != "Literal") {
            continue;
          }
          addProp(callNode, propNode, propNode.value);
        }
      }
    }

    return {
      VariableDeclarator(node) {
        if (
          node.id.type === "Identifier" &&
          node.id.name == "lazy" &&
          node.init.type == "CallExpression" &&
          node.init.callee.name == "createLazyLoaders" &&
          node.init.arguments.length >= 1
        ) {
          setPropertiesFromArgument(node.init, node.init.arguments[0]);
        }
        if (
          node.id.type === "Identifier" &&
          node.id.name == "lazy" &&
          node.init.type == "CallExpression" &&
          node.init.callee.type === "MemberExpression" &&
          node.init.callee.object.type === "Identifier" &&
          node.init.callee.object.name === "XPCOMUtils" &&
          node.init.callee.property.name === "declareLazy" &&
          node.init.arguments.length >= 1
        ) {
          setPropertiesFromArgument(node.init, node.init.arguments[0]);
        }
      },

      CallExpression(node) {
        if (
          node.callee.type != "MemberExpression" ||
          (node.callee.object.type == "MemberExpression" &&
            !items.includes(node.callee.object.object.name)) ||
          (node.callee.object.type != "MemberExpression" &&
            !items.includes(node.callee.object.name))
        ) {
          return;
        }

        let source;
        try {
          source = helpers.getASTSource(node);
        } catch (e) {
          return;
        }

        for (let reg of callExpressionDefinitions) {
          let match = source.match(reg);
          if (match) {
            if (
              lazyProperties.has(match[1]) &&
              isDuplicate(lazyProperties.get(match[1]).callNode, node)
            ) {
              context.report({
                node,
                messageId: "duplicateSymbol",
                data: { name: match[1] },
              });
              return;
            }
            lazyProperties.set(match[1], {
              used: false,
              callNode: node,
              propNode: node,
            });
            break;
          }
        }

        if (
          callExpressionMultiDefinitions.some(expr =>
            source.startsWith(expr)
          ) &&
          node.arguments[1]
        ) {
          setPropertiesFromArgument(node, node.arguments[1]);
        }
      },

      MemberExpression(node) {
        if (node.computed || node.object.type !== "Identifier") {
          return;
        }

        let name;
        if (node.object.name == "lazy") {
          name = node.property.name;
        } else {
          return;
        }
        let property = lazyProperties.get(name);
        if (!property) {
          // These will be reported on Program:exit - some definitions may
          // be after first use, so we need to wait until we've processed
          // the whole file before reporting.
          unknownProperties.push({ name, node });
        } else {
          property.used = true;
        }
        if (
          helpers.getIsTopLevelAndUnconditionallyExecuted(
            context.sourceCode.getAncestors(node)
          )
        ) {
          context.report({
            node,
            messageId: "topLevelAndUnconditional",
            data: { name },
          });
        }
      },

      ExportNamedDeclaration(node) {
        for (const spec of node.specifiers) {
          if (spec.local.name === "lazy") {
            // If the lazy object is exported, do not check unused property.
            isLazyExported = true;
            break;
          }
        }
      },

      "Program:exit": function () {
        for (let { name, node } of unknownProperties) {
          let property = lazyProperties.get(name);
          if (!property) {
            context.report({
              node,
              messageId: "unknownProperty",
              data: { name },
            });
          } else {
            property.used = true;
          }
        }
        if (!isLazyExported) {
          for (let [name, property] of lazyProperties.entries()) {
            if (!property.used) {
              context.report({
                node: property.propNode,
                messageId: "unusedProperty",
                data: { name },
              });
            }
          }
        }
      },
    };
  },
};
