/**
 * FIXME: remove this comments and import when below issue is fixed.
 * This import is necessary for type generation due to a bug in the TypeScript compiler.
 * See: https://github.com/microsoft/TypeScript/issues/42873
 */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import type { TSESLint } from "@typescript-eslint/utils";

import { TSESTree as T, ESLintUtils, ASTUtils } from "@typescript-eslint/utils";
import { isFunctionNode, isJSXElementOrFragment } from "../utils";

const createRule = ESLintUtils.RuleCreator.withoutDocs;
const { getPropertyName } = ASTUtils;

export default createRule({
  meta: {
    type: "problem",
    docs: {
      description:
        "Enforce using Solid's `<For />` component for mapping an array to JSX elements.",
      url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/prefer-for.md",
    },
    fixable: "code",
    schema: [],
    messages: {
      preferFor:
        "Use Solid's `<For />` component for efficiently rendering lists. Array#map causes DOM elements to be recreated.",
      preferForOrIndex:
        "Use Solid's `<For />` component or `<Index />` component for rendering lists. Array#map causes DOM elements to be recreated.",
    },
  },
  defaultOptions: [],
  create(context) {
    const reportPreferFor = (node: T.CallExpression) => {
      const jsxExpressionContainerNode = node.parent as T.JSXExpressionContainer;
      const arrayNode = (node.callee as T.MemberExpression).object;
      const mapFnNode = node.arguments[0];
      context.report({
        node,
        messageId: "preferFor",
        fix: (fixer) => {
          const beforeArray: [number, number] = [
            jsxExpressionContainerNode.range[0],
            arrayNode.range[0],
          ];
          const betweenArrayAndMapFn: [number, number] = [arrayNode.range[1], mapFnNode.range[0]];
          const afterMapFn: [number, number] = [
            mapFnNode.range[1],
            jsxExpressionContainerNode.range[1],
          ];
          // We can insert the <For /> component
          return [
            fixer.replaceTextRange(beforeArray, "<For each={"),
            fixer.replaceTextRange(betweenArrayAndMapFn, "}>{"),
            fixer.replaceTextRange(afterMapFn, "}</For>"),
          ];
        },
      });
    };

    return {
      CallExpression(node) {
        const callOrChain = node.parent?.type === "ChainExpression" ? node.parent : node;
        if (
          callOrChain.parent?.type === "JSXExpressionContainer" &&
          isJSXElementOrFragment(callOrChain.parent.parent)
        ) {
          // check for Array.prototype.map in JSX
          if (
            node.callee.type === "MemberExpression" &&
            getPropertyName(node.callee) === "map" &&
            node.arguments.length === 1 && // passing thisArg to Array.prototype.map is rare, deopt in that case
            isFunctionNode(node.arguments[0])
          ) {
            const mapFnNode = node.arguments[0];
            if (mapFnNode.params.length === 1 && mapFnNode.params[0].type !== "RestElement") {
              // The map fn doesn't take an index param, so it can't possibly be an index-keyed list. Use <For />.
              // The returned JSX, if it's coming from React, will have an unnecessary `key` prop to be removed in
              // the useless-keys rule.
              reportPreferFor(node);
            } else {
              // Too many possible solutions to make a suggestion or fix
              context.report({
                node,
                messageId: "preferForOrIndex",
              });
            }
          }
        }
      },
    };
  },
});
