/**
 * @fileoverview Detect classnames which do not belong to Tailwind CSS
 * @author no-custom-classname
 */
'use strict';

const docsUrl = require('../util/docsUrl');
const defaultGroups = require('../config/groups').groups;
const customConfig = require('../util/customConfig');
const astUtil = require('../util/ast');
const groupUtil = require('../util/groupMethods');
const getOption = require('../util/settings');
const parserUtil = require('../util/parser');
const getClassnamesFromCSS = require('../util/cssFiles');
const createContextFallback = require('tailwindcss/lib/lib/setupContextUtils').createContext;
const generated = require('../util/generated');

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

// Predefine message for use in context.report conditional.
// messageId will still be usable in tests.
const CUSTOM_CLASSNAME_DETECTED_MSG = `Classname '{{classname}}' is not a Tailwind CSS class!`;

// Group/peer names can be arbitrarily named and are not
// generated by generateRules. Using a custom regexp to
// validate these avoids false reports.
const GROUP_NAME_REGEXP = /^(group|peer)\/[\w\$\#\@\%\^\&\*\_\-]+$/i;

const contextFallbackCache = new WeakMap();

module.exports = {
  meta: {
    docs: {
      description: 'Detect classnames which do not belong to Tailwind CSS',
      category: 'Best Practices',
      recommended: false,
      url: docsUrl('no-custom-classname'),
    },
    messages: {
      customClassnameDetected: CUSTOM_CLASSNAME_DETECTED_MSG,
    },
    fixable: null,
    schema: [
      {
        type: 'object',
        properties: {
          callees: {
            type: 'array',
            items: { type: 'string', minLength: 0 },
            uniqueItems: true,
          },
          ignoredKeys: {
            type: 'array',
            items: { type: 'string', minLength: 0 },
            uniqueItems: true,
          },
          config: {
            // default: 'tailwind.config.js',
            type: ['string', 'object'],
          },
          cssFiles: {
            type: 'array',
            items: { type: 'string', minLength: 0 },
            uniqueItems: true,
          },
          cssFilesRefreshRate: {
            type: 'number',
            // default: 5_000,
          },
          tags: {
            type: 'array',
            items: { type: 'string', minLength: 0 },
            uniqueItems: true,
          },
          whitelist: {
            type: 'array',
            items: { type: 'string', minLength: 0 },
            uniqueItems: true,
          },
        },
      },
    ],
  },

  create: function (context) {
    const callees = getOption(context, 'callees');
    const ignoredKeys = getOption(context, 'ignoredKeys');
    const skipClassAttribute = getOption(context, 'skipClassAttribute');
    const tags = getOption(context, 'tags');
    const twConfig = getOption(context, 'config');
    const cssFiles = getOption(context, 'cssFiles');
    const cssFilesRefreshRate = getOption(context, 'cssFilesRefreshRate');
    const whitelist = getOption(context, 'whitelist');
    const classRegex = getOption(context, 'classRegex');

    const mergedConfig = customConfig.resolve(twConfig);
    const contextFallback = // Set the created contextFallback in the cache if it does not exist yet.
      (
        contextFallbackCache.has(mergedConfig)
          ? contextFallbackCache
          : contextFallbackCache.set(mergedConfig, createContextFallback(mergedConfig))
      ).get(mergedConfig);

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

    // Init assets before sorting
    const groups = groupUtil.getGroups(defaultGroups, mergedConfig);
    const classnamesFromFiles = getClassnamesFromCSS(cssFiles, cssFilesRefreshRate);

    /**
     * Parse the classnames and report found conflicts
     * @param {Array} classNames
     * @param {ASTNode} node
     */
    const parseForCustomClassNames = (classNames, node) => {
      classNames.forEach((className) => {
        const gen = generated(className, contextFallback);
        if (gen.length) {
          return; // Lazier is faster... processing next className!
        }
        const idx = groupUtil.getGroupIndex(className, groups, mergedConfig.separator);
        if (idx >= 0) {
          return; // Lazier is faster... processing next className!
        }
        const whitelistIdx = groupUtil.getGroupIndex(className, whitelist, mergedConfig.separator);
        if (whitelistIdx >= 0) {
          return; // Lazier is faster... processing next className!
        }
        const fromFilesIdx = groupUtil.getGroupIndex(className, classnamesFromFiles, mergedConfig.separator);
        if (fromFilesIdx >= 0) {
          return; // Lazier is faster... processing next className!
        }
        if (GROUP_NAME_REGEXP.test(className)) {
          return; // Lazier is faster... processing next className!
        }

        // No match found
        context.report({
          node,
          messageId: 'customClassnameDetected',
          data: {
            classname: className,
          },
        });
      });
    };

    //----------------------------------------------------------------------
    // Public
    //----------------------------------------------------------------------

    const attributeVisitor = function (node) {
      if (!astUtil.isClassAttribute(node, classRegex) || skipClassAttribute) {
        return;
      }
      if (astUtil.isLiteralAttributeValue(node)) {
        astUtil.parseNodeRecursive(node, null, parseForCustomClassNames, false, false, ignoredKeys);
      } else if (node.value && node.value.type === 'JSXExpressionContainer') {
        astUtil.parseNodeRecursive(node, node.value.expression, parseForCustomClassNames, false, false, ignoredKeys);
      }
    };

    const callExpressionVisitor = function (node) {
      const calleeStr = astUtil.calleeToString(node.callee);
      if (callees.findIndex((name) => calleeStr === name) === -1) {
        return;
      }
      node.arguments.forEach((arg) => {
        astUtil.parseNodeRecursive(node, arg, parseForCustomClassNames, false, false, ignoredKeys);
      });
    };

    const scriptVisitor = {
      JSXAttribute: attributeVisitor,
      TextAttribute: attributeVisitor,
      CallExpression: callExpressionVisitor,
      TaggedTemplateExpression: function (node) {
        if (!tags.includes(node.tag.name)) {
          return;
        }
        astUtil.parseNodeRecursive(node, node.quasi, parseForCustomClassNames, false, false, ignoredKeys);
      },
    };

    const templateVisitor = {
      CallExpression: callExpressionVisitor,
      /*
      Tagged templates inside data bindings
      https://github.com/vuejs/vue/issues/9721
      */
      VAttribute: function (node) {
        switch (true) {
          case !astUtil.isValidVueAttribute(node, classRegex):
            return;
          case astUtil.isVLiteralValue(node):
            astUtil.parseNodeRecursive(node, null, parseForCustomClassNames, false, false, ignoredKeys);
            break;
          case astUtil.isArrayExpression(node):
            node.value.expression.elements.forEach((arg) => {
              astUtil.parseNodeRecursive(node, arg, parseForCustomClassNames, false, false, ignoredKeys);
            });
            break;
          case astUtil.isObjectExpression(node):
            node.value.expression.properties.forEach((prop) => {
              astUtil.parseNodeRecursive(node, prop, parseForCustomClassNames, false, false, ignoredKeys);
            });
            break;
        }
      },
    };

    return parserUtil.defineTemplateBodyVisitor(context, templateVisitor, scriptVisitor);
  },
};
