/**
 * @author Toru Nagashima
 * @copyright 2017 Toru Nagashima. All rights reserved.
 * See LICENSE file in root directory for full license.
 */
'use strict'

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

/**
 * @typedef { VDirectiveKey & { name: VIdentifier & { name: 'bind' }, argument: VExpressionContainer | VIdentifier } } VBindDirectiveKey
 * @typedef { VDirective & { key: VBindDirectiveKey } } VBindDirective
 */

/**
 * @param {VBindDirective} node
 * @returns {boolean}
 */
function isSameName(node) {
  const attrName =
    node.key.argument.type === 'VIdentifier' ? node.key.argument.rawName : null
  const valueName =
    node.value?.expression?.type === 'Identifier'
      ? node.value.expression.name
      : null
  return Boolean(
    attrName &&
      valueName &&
      casing.camelCase(attrName) === casing.camelCase(valueName)
  )
}

/**
 * @param {VBindDirectiveKey} key
 * @returns {number}
 */
function getCutStart(key) {
  const modifiers = key.modifiers
  return modifiers.length > 0
    ? modifiers[modifiers.length - 1].range[1]
    : key.argument.range[1]
}

module.exports = {
  meta: {
    type: 'suggestion',
    docs: {
      description: 'enforce `v-bind` directive style',
      categories: ['vue3-strongly-recommended', 'strongly-recommended'],
      url: 'https://eslint.vuejs.org/rules/v-bind-style.html'
    },
    fixable: 'code',
    schema: [
      { enum: ['shorthand', 'longform'] },
      {
        type: 'object',
        properties: {
          sameNameShorthand: { enum: ['always', 'never', 'ignore'] }
        },
        additionalProperties: false
      }
    ],
    messages: {
      expectedLonghand: "Expected 'v-bind' before ':'.",
      unexpectedLonghand: "Unexpected 'v-bind' before ':'.",
      expectedLonghandForProp: "Expected 'v-bind:' instead of '.'.",
      expectedShorthand: 'Expected same-name shorthand.',
      unexpectedShorthand: 'Unexpected same-name shorthand.'
    }
  },
  /** @param {RuleContext} context */
  create(context) {
    const preferShorthand = context.options[0] !== 'longform'
    /** @type {"always" | "never" | "ignore"} */
    const sameNameShorthand = context.options[1]?.sameNameShorthand || 'ignore'

    /** @param {VBindDirective} node */
    function checkAttributeStyle(node) {
      const shorthandProp = node.key.name.rawName === '.'
      const shorthand = node.key.name.rawName === ':' || shorthandProp
      if (shorthand === preferShorthand) {
        return
      }

      let messageId = 'expectedLonghand'
      if (preferShorthand) {
        messageId = 'unexpectedLonghand'
      } else if (shorthandProp) {
        messageId = 'expectedLonghandForProp'
      }

      context.report({
        node,
        loc: node.loc,
        messageId,
        *fix(fixer) {
          if (preferShorthand) {
            yield fixer.remove(node.key.name)
          } else {
            yield fixer.insertTextBefore(node, 'v-bind')

            if (shorthandProp) {
              // Replace `.` by `:`.
              yield fixer.replaceText(node.key.name, ':')

              // Insert `.prop` modifier if it doesn't exist.
              const modifier = node.key.modifiers[0]
              const isAutoGeneratedPropModifier =
                modifier.name === 'prop' && modifier.rawName === ''
              if (isAutoGeneratedPropModifier) {
                yield fixer.insertTextBefore(modifier, '.prop')
              }
            }
          }
        }
      })
    }

    /** @param {VBindDirective} node */
    function checkAttributeSameName(node) {
      if (sameNameShorthand === 'ignore' || !isSameName(node)) return

      const preferShorthand = sameNameShorthand === 'always'
      const isShorthand = utils.isVBindSameNameShorthand(node)
      if (isShorthand === preferShorthand) {
        return
      }

      const messageId = preferShorthand
        ? 'expectedShorthand'
        : 'unexpectedShorthand'

      context.report({
        node,
        loc: node.loc,
        messageId,
        *fix(fixer) {
          if (preferShorthand) {
            /** @type {Range} */
            const valueRange = [getCutStart(node.key), node.range[1]]

            yield fixer.removeRange(valueRange)
          } else if (node.key.argument.type === 'VIdentifier') {
            yield fixer.insertTextAfter(
              node,
              `="${casing.camelCase(node.key.argument.rawName)}"`
            )
          }
        }
      })
    }

    return utils.defineTemplateBodyVisitor(context, {
      /** @param {VBindDirective} node */
      "VAttribute[directive=true][key.name.name='bind'][key.argument!=null]"(
        node
      ) {
        checkAttributeSameName(node)
        checkAttributeStyle(node)
      }
    })
  }
}
