'use strict';

var utils = require('../utils.js');
require('eslint-visitor-keys');
require('espree');
require('estraverse');

const messages = {
  bracketLocation: "The closing bracket must be {{location}}{{details}}"
};
var jsxClosingBracketLocation = utils.createRule({
  name: "jsx-closing-bracket-location",
  package: "jsx",
  meta: {
    type: "layout",
    docs: {
      description: "Enforce closing bracket location in JSX"
    },
    fixable: "code",
    messages,
    schema: [{
      anyOf: [
        {
          type: "string",
          enum: ["after-props", "props-aligned", "tag-aligned", "line-aligned"]
        },
        {
          type: "object",
          properties: {
            location: {
              type: "string",
              enum: ["after-props", "props-aligned", "tag-aligned", "line-aligned"]
            }
          },
          additionalProperties: false
        },
        {
          type: "object",
          properties: {
            nonEmpty: {
              oneOf: [
                {
                  type: "string",
                  enum: ["after-props", "props-aligned", "tag-aligned", "line-aligned"]
                },
                {
                  type: "boolean",
                  enum: [false]
                }
              ]
            },
            selfClosing: {
              oneOf: [
                {
                  type: "string",
                  enum: ["after-props", "props-aligned", "tag-aligned", "line-aligned"]
                },
                {
                  type: "boolean",
                  enum: [false]
                }
              ]
            }
          },
          additionalProperties: false
        }
      ]
    }]
  },
  create(context) {
    const MESSAGE_LOCATION = {
      "after-props": "placed after the last prop",
      "after-tag": "placed after the opening tag",
      "props-aligned": "aligned with the last prop",
      "tag-aligned": "aligned with the opening tag",
      "line-aligned": "aligned with the line containing the opening tag"
    };
    const DEFAULT_LOCATION = "tag-aligned";
    const config = context.options[0];
    const options = {
      nonEmpty: DEFAULT_LOCATION,
      selfClosing: DEFAULT_LOCATION
    };
    if (typeof config === "string") {
      options.nonEmpty = config;
      options.selfClosing = config;
    } else if (typeof config === "object") {
      if ("location" in config) {
        options.nonEmpty = config.location;
        options.selfClosing = config.location;
      }
      if ("nonEmpty" in config)
        options.nonEmpty = config.nonEmpty;
      if ("selfClosing" in config)
        options.selfClosing = config.selfClosing;
    }
    function getExpectedLocation(tokens) {
      let location;
      if (typeof tokens.lastProp === "undefined")
        location = "after-tag";
      else if (tokens.opening.line === tokens.lastProp.lastLine)
        location = "after-props";
      else
        location = tokens.selfClosing ? options.selfClosing : options.nonEmpty;
      return location;
    }
    function getCorrectColumn(tokens, expectedLocation) {
      switch (expectedLocation) {
        case "props-aligned":
          return tokens.lastProp.column;
        case "tag-aligned":
          return tokens.opening.column;
        case "line-aligned":
          return tokens.openingStartOfLine.column;
        default:
          return null;
      }
    }
    function hasCorrectLocation(tokens, expectedLocation) {
      switch (expectedLocation) {
        case "after-tag":
          return tokens.tag.line === tokens.closing.line;
        case "after-props":
          return tokens.lastProp.lastLine === tokens.closing.line;
        case "props-aligned":
        case "tag-aligned":
        case "line-aligned": {
          const correctColumn = getCorrectColumn(tokens, expectedLocation);
          return correctColumn === tokens.closing.column;
        }
        default:
          return true;
      }
    }
    function getIndentation(tokens, expectedLocation, correctColumn) {
      const newColumn = correctColumn || 0;
      let indentation;
      let spaces = [];
      switch (expectedLocation) {
        case "props-aligned":
          indentation = /^\s*/.exec(context.sourceCode.lines[tokens.lastProp.firstLine - 1])[0];
          break;
        case "tag-aligned":
        case "line-aligned":
          indentation = /^\s*/.exec(context.sourceCode.lines[tokens.opening.line - 1])[0];
          break;
        default:
          indentation = "";
      }
      if (indentation.length + 1 < newColumn) {
        spaces = Array.from({ length: +correctColumn + 1 - indentation.length });
      }
      return indentation + spaces.join(" ");
    }
    function getTokensLocations(node) {
      const sourceCode = context.sourceCode;
      const opening = sourceCode.getFirstToken(node).loc.start;
      const closing = sourceCode.getLastTokens(node, node.selfClosing ? 2 : 1)[0].loc.start;
      const tag = sourceCode.getFirstToken(node.name).loc.start;
      let lastProp;
      if (node.attributes.length) {
        lastProp = node.attributes[node.attributes.length - 1];
        lastProp = {
          column: sourceCode.getFirstToken(lastProp).loc.start.column,
          firstLine: sourceCode.getFirstToken(lastProp).loc.start.line,
          lastLine: sourceCode.getLastToken(lastProp).loc.end.line
        };
      }
      const openingLine = sourceCode.lines[opening.line - 1];
      const closingLine = sourceCode.lines[closing.line - 1];
      const isTab = {
        openTab: /^\t/.test(openingLine),
        closeTab: /^\t/.test(closingLine)
      };
      const openingStartOfLine = {
        column: /^\s*/.exec(openingLine)?.[0].length,
        line: opening.line
      };
      return {
        isTab,
        tag,
        opening,
        closing,
        lastProp,
        selfClosing: node.selfClosing,
        openingStartOfLine
      };
    }
    return {
      "JSXOpeningElement:exit": function(node) {
        const lastAttributeNode = node.attributes.at(-1);
        const cachedLastAttributeEndPos = lastAttributeNode ? lastAttributeNode.range[1] : null;
        let expectedNextLine;
        const tokens = getTokensLocations(node);
        let expectedLocation = getExpectedLocation(tokens);
        let usingSameIndentation = true;
        if (expectedLocation === "tag-aligned")
          usingSameIndentation = tokens.isTab.openTab === tokens.isTab.closeTab;
        const lastComment = context.sourceCode.getCommentsInside(node).at(-1);
        const hasTrailingComment = lastComment && lastComment.range[0] > (lastAttributeNode ?? node.name).range[1];
        if ((expectedLocation === "after-props" || expectedLocation === "after-tag") && !(hasCorrectLocation(tokens, expectedLocation) && usingSameIndentation) && hasTrailingComment) {
          expectedLocation = "line-aligned";
        }
        if (hasCorrectLocation(tokens, expectedLocation) && usingSameIndentation)
          return;
        const data = {
          location: MESSAGE_LOCATION[expectedLocation],
          details: ""
        };
        const correctColumn = getCorrectColumn(tokens, expectedLocation);
        if (correctColumn !== null) {
          expectedNextLine = tokens.lastProp && tokens.lastProp.lastLine === tokens.closing.line;
          data.details = ` (expected column ${correctColumn + 1}${expectedNextLine ? " on the next line)" : ")"}`;
        }
        context.report({
          node,
          messageId: "bracketLocation",
          loc: tokens.closing,
          data,
          fix(fixer) {
            const closingTag = tokens.selfClosing ? "/>" : ">";
            switch (expectedLocation) {
              case "after-tag":
                if (cachedLastAttributeEndPos)
                  return fixer.replaceTextRange([cachedLastAttributeEndPos, node.range[1]], (expectedNextLine ? "\n" : "") + closingTag);
                return fixer.replaceTextRange([node.name.range[1], node.range[1]], (expectedNextLine ? "\n" : " ") + closingTag);
              case "after-props":
                return fixer.replaceTextRange([cachedLastAttributeEndPos, node.range[1]], (expectedNextLine ? "\n" : "") + closingTag);
              case "props-aligned":
              case "tag-aligned":
              case "line-aligned": {
                const rangeStart = hasTrailingComment ? lastComment.range[1] : cachedLastAttributeEndPos;
                return fixer.replaceTextRange([rangeStart, node.range[1]], `
${getIndentation(tokens, expectedLocation, correctColumn)}${closingTag}`);
              }
            }
            return null;
          }
        });
      }
    };
  }
});

module.exports = jsxClosingBracketLocation;
