/**
 * @fileoverview enforce usage of `exact` modifier on `v-on`.
 * @author Armano
 */
'use strict'

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

/**
 * @typedef { {name: string, node: VDirectiveKey, modifiers: string[] } } EventDirective
 */

const utils = require('../utils')

const SYSTEM_MODIFIERS = new Set(['ctrl', 'shift', 'alt', 'meta'])
const GLOBAL_MODIFIERS = new Set([
  'stop',
  'prevent',
  'capture',
  'self',
  'once',
  'passive',
  'native'
])

// ------------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------------

/**
 * Finds and returns all keys for event directives
 *
 * @param {VStartTag} startTag Element startTag
 * @param {SourceCode} sourceCode The source code object.
 * @returns {EventDirective[]} [{ name, node, modifiers }]
 */
function getEventDirectives(startTag, sourceCode) {
  return utils.getDirectives(startTag, 'on').map((attribute) => ({
    name: attribute.key.argument
      ? sourceCode.getText(attribute.key.argument)
      : '',
    node: attribute.key,
    modifiers: attribute.key.modifiers.map((modifier) => modifier.name)
  }))
}

/**
 * Checks whether given modifier is key modifier
 *
 * @param {string} modifier
 * @returns {boolean}
 */
function isKeyModifier(modifier) {
  return !GLOBAL_MODIFIERS.has(modifier) && !SYSTEM_MODIFIERS.has(modifier)
}

/**
 * Checks whether given modifier is system one
 *
 * @param {string} modifier
 * @returns {boolean}
 */
function isSystemModifier(modifier) {
  return SYSTEM_MODIFIERS.has(modifier)
}

/**
 * Checks whether given any of provided modifiers
 * has system modifier
 *
 * @param {string[]} modifiers
 * @returns {boolean}
 */
function hasSystemModifier(modifiers) {
  return modifiers.some(isSystemModifier)
}

/**
 * Groups all events in object,
 * with keys represinting each event name
 *
 * @param {EventDirective[]} events
 * @returns { { [key: string]: EventDirective[]  } } { click: [], keypress: [] }
 */
function groupEvents(events) {
  return events.reduce((acc, event) => {
    if (acc[event.name]) {
      acc[event.name].push(event)
    } else {
      acc[event.name] = [event]
    }
    return acc
  }, /** @type { { [key: string]: EventDirective[] } }*/ ({}))
}

/**
 * Creates alphabetically sorted string with system modifiers
 *
 * @param {string[]} modifiers
 * @returns {string} e.g. "alt,ctrl,del,shift"
 */
function getSystemModifiersString(modifiers) {
  return modifiers.filter(isSystemModifier).sort().join(',')
}

/**
 * Creates alphabetically sorted string with key modifiers
 *
 * @param {string[]} modifiers
 * @returns {string} e.g. "enter,tab"
 */
function getKeyModifiersString(modifiers) {
  return modifiers.filter(isKeyModifier).sort().join(',')
}

/**
 * Compares two events based on their modifiers
 * to detect possible event leakeage
 *
 * @param {EventDirective} baseEvent
 * @param {EventDirective} event
 * @returns {boolean}
 */
function hasConflictedModifiers(baseEvent, event) {
  if (event.node === baseEvent.node || event.modifiers.includes('exact'))
    return false

  const eventKeyModifiers = getKeyModifiersString(event.modifiers)
  const baseEventKeyModifiers = getKeyModifiersString(baseEvent.modifiers)

  if (
    eventKeyModifiers &&
    baseEventKeyModifiers &&
    eventKeyModifiers !== baseEventKeyModifiers
  )
    return false

  const eventSystemModifiers = getSystemModifiersString(event.modifiers)
  const baseEventSystemModifiers = getSystemModifiersString(baseEvent.modifiers)

  return (
    baseEvent.modifiers.length >= 1 &&
    baseEventSystemModifiers !== eventSystemModifiers &&
    baseEventSystemModifiers.indexOf(eventSystemModifiers) > -1
  )
}

/**
 * Searches for events that might conflict with each other
 *
 * @param {EventDirective[]} events
 * @returns {EventDirective[]} conflicted events, without duplicates
 */
function findConflictedEvents(events) {
  return events.reduce((acc, event) => {
    return [
      ...acc,
      ...events
        .filter((evt) => !acc.find((e) => evt === e)) // No duplicates
        .filter(hasConflictedModifiers.bind(null, event))
    ]
  }, /** @type {EventDirective[]} */ ([]))
}

// ------------------------------------------------------------------------------
// Rule details
// ------------------------------------------------------------------------------

module.exports = {
  meta: {
    type: 'suggestion',
    docs: {
      description: 'enforce usage of `exact` modifier on `v-on`',
      categories: ['vue3-essential', 'essential'],
      url: 'https://eslint.vuejs.org/rules/use-v-on-exact.html'
    },
    fixable: null,
    schema: []
  },

  /**
   * Creates AST event handlers for use-v-on-exact.
   *
   * @param {RuleContext} context - The rule context.
   * @returns {Object} AST event handlers.
   */
  create(context) {
    const sourceCode = context.getSourceCode()

    return utils.defineTemplateBodyVisitor(context, {
      /** @param {VStartTag} node */
      VStartTag(node) {
        if (node.attributes.length === 0) return

        const isCustomComponent = utils.isCustomComponent(node.parent)
        let events = getEventDirectives(node, sourceCode)

        if (isCustomComponent) {
          // For components consider only events with `native` modifier
          events = events.filter((event) => event.modifiers.includes('native'))
        }

        const grouppedEvents = groupEvents(events)

        Object.keys(grouppedEvents).forEach((eventName) => {
          const eventsInGroup = grouppedEvents[eventName]
          const hasEventWithKeyModifier = eventsInGroup.some((event) =>
            hasSystemModifier(event.modifiers)
          )

          if (!hasEventWithKeyModifier) return

          const conflictedEvents = findConflictedEvents(eventsInGroup)

          conflictedEvents.forEach((e) => {
            context.report({
              node: e.node,
              loc: e.node.loc,
              message: "Consider to use '.exact' modifier."
            })
          })
        })
      }
    })
  }
}
