/**
 * @fileoverview disallow usage of `this` in template.
 * @author Armano
 */
'use strict'

// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------

const utils = require('../utils')
const RESERVED_NAMES = new Set(require('../utils/js-reserved.json'))

// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------

module.exports = {
  meta: {
    type: 'suggestion',
    docs: {
      description: 'disallow usage of `this` in template',
      categories: ['vue3-recommended', 'recommended'],
      url: 'https://eslint.vuejs.org/rules/this-in-template.html'
    },
    fixable: 'code',
    schema: [
      {
        enum: ['always', 'never']
      }
    ]
  },

  /**
   * Creates AST event handlers for this-in-template.
   *
   * @param {RuleContext} context - The rule context.
   * @returns {Object} AST event handlers.
   */
  create(context) {
    const options = context.options[0] !== 'always' ? 'never' : 'always'
    /**
     * @typedef {object} ScopeStack
     * @property {ScopeStack | null} parent
     * @property {Identifier[]} nodes
     */

    /** @type {ScopeStack | null} */
    let scopeStack = null

    return utils.defineTemplateBodyVisitor(context, {
      /** @param {VElement} node */
      VElement(node) {
        scopeStack = {
          parent: scopeStack,
          nodes: scopeStack
            ? scopeStack.nodes.slice() // make copy
            : []
        }
        if (node.variables) {
          for (const variable of node.variables) {
            const varNode = variable.id
            const name = varNode.name
            if (!scopeStack.nodes.some((node) => node.name === name)) {
              // Prevent adding duplicates
              scopeStack.nodes.push(varNode)
            }
          }
        }
      },
      'VElement:exit'() {
        scopeStack = scopeStack && scopeStack.parent
      },
      ...(options === 'never'
        ? {
            /** @param { ThisExpression & { parent: MemberExpression } } node */
            'VExpressionContainer MemberExpression > ThisExpression'(node) {
              if (!scopeStack) {
                return
              }
              const propertyName = utils.getStaticPropertyName(node.parent)
              if (
                !propertyName ||
                scopeStack.nodes.some((el) => el.name === propertyName) ||
                RESERVED_NAMES.has(propertyName) || // this.class | this['class']
                /^[0-9].*$|[^a-zA-Z0-9_$]/.test(propertyName) // this['0aaaa'] | this['foo-bar bas']
              ) {
                return
              }

              context.report({
                node,
                loc: node.loc,
                fix(fixer) {
                  // node.parent should be some code like `this.test`, `this?.['result']`
                  return fixer.replaceText(node.parent, propertyName)
                },
                message: "Unexpected usage of 'this'."
              })
            }
          }
        : {
            /** @param {VExpressionContainer} node */
            VExpressionContainer(node) {
              if (!scopeStack) {
                return
              }
              if (node.parent.type === 'VDirectiveKey') {
                // We cannot use `.` in dynamic arguments because the right of the `.` becomes a modifier.
                // For example, In `:[this.prop]` case, `:[this` is an argument and `prop]` is a modifier.
                return
              }
              if (node.references) {
                for (const reference of node.references) {
                  if (
                    !scopeStack.nodes.some(
                      (el) => el.name === reference.id.name
                    )
                  ) {
                    context.report({
                      node: reference.id,
                      loc: reference.id.loc,
                      message: "Expected 'this'.",
                      fix(fixer) {
                        return fixer.insertTextBefore(reference.id, 'this.')
                      }
                    })
                  }
                }
              }
            }
          })
    })
  }
}