'use strict';

const _ = require('lodash');
const declarationValueIndex = require('../../utils/declarationValueIndex');
const isStandardSyntaxFunction = require('../../utils/isStandardSyntaxFunction');
const isStandardSyntaxValue = require('../../utils/isStandardSyntaxValue');
const keywordSets = require('../../reference/keywordSets');
const namedColorDataHex = require('../../reference/namedColorData');
const optionsMatches = require('../../utils/optionsMatches');
const propertySets = require('../../reference/propertySets');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const valueParser = require('postcss-value-parser');

const generateColorFuncs = require('./generateColorFuncs');

const ruleName = 'color-named';

const messages = ruleMessages(ruleName, {
	expected: (named, original) => `Expected "${original}" to be "${named}"`,
	rejected: (named) => `Unexpected named color "${named}"`,
});

// Todo tested on case insensivity
const NODE_TYPES = ['word', 'function'];

function rule(expectation, options) {
	return (root, result) => {
		const validOptions = validateOptions(
			result,
			ruleName,
			{
				actual: expectation,
				possible: ['never', 'always-where-possible'],
			},
			{
				actual: options,
				possible: {
					ignoreProperties: [_.isString, _.isRegExp],
					ignore: ['inside-function'],
				},
				optional: true,
			},
		);

		if (!validOptions) {
			return;
		}

		const namedColors = Object.keys(namedColorDataHex);
		const namedColorData = {};

		namedColors.forEach((name) => {
			const hex = namedColorDataHex[name];

			namedColorData[name] = {
				hex,
				func: generateColorFuncs(hex[0]),
			};
		});

		root.walkDecls((decl) => {
			if (propertySets.acceptCustomIdents.has(decl.prop)) {
				return;
			}

			// Return early if the property is to be ignored
			if (optionsMatches(options, 'ignoreProperties', decl.prop)) {
				return;
			}

			valueParser(decl.value).walk((node) => {
				const value = node.value;
				const type = node.type;
				const sourceIndex = node.sourceIndex;

				if (optionsMatches(options, 'ignore', 'inside-function') && type === 'function') {
					return false;
				}

				if (!isStandardSyntaxFunction(node)) {
					return false;
				}

				if (!isStandardSyntaxValue(value)) {
					return;
				}

				// Return early if neither a word nor a function
				if (!NODE_TYPES.includes(type)) {
					return;
				}

				// Check for named colors for "never" option
				if (
					expectation === 'never' &&
					type === 'word' &&
					namedColors.includes(value.toLowerCase())
				) {
					complain(messages.rejected(value), decl, declarationValueIndex(decl) + sourceIndex);

					return;
				}

				// Check "always-where-possible" option ...
				if (expectation !== 'always-where-possible') {
					return;
				}

				// First by checking for alternative color function representations ...
				if (type === 'function' && keywordSets.colorFunctionNames.has(value.toLowerCase())) {
					// Remove all spaces to match what's in `representations`
					const normalizedFunctionString = valueParser.stringify(node).replace(/\s+/g, '');
					let namedColor;

					for (let i = 0, l = namedColors.length; i < l; i++) {
						namedColor = namedColors[i];

						if (namedColorData[namedColor].func.includes(normalizedFunctionString.toLowerCase())) {
							complain(
								messages.expected(namedColor, normalizedFunctionString),
								decl,
								declarationValueIndex(decl) + sourceIndex,
							);

							return; // Exit as soon as a problem is found
						}
					}

					return;
				}

				// Then by checking for alternative hex representations
				let namedColor;

				for (let i = 0, l = namedColors.length; i < l; i++) {
					namedColor = namedColors[i];

					if (namedColorData[namedColor].hex.includes(value.toLowerCase())) {
						complain(
							messages.expected(namedColor, value),
							decl,
							declarationValueIndex(decl) + sourceIndex,
						);

						return; // Exit as soon as a problem is found
					}
				}
			});
		});

		function complain(message, node, index) {
			report({
				result,
				ruleName,
				message,
				node,
				index,
			});
		}
	};
}

rule.ruleName = ruleName;
rule.messages = messages;
module.exports = rule;
