'use strict';

const _ = require('lodash');
const beforeBlockString = require('../../utils/beforeBlockString');
const blurComments = require('../../utils/blurComments');
const hasBlock = require('../../utils/hasBlock');
const isCustomProperty = require('../../utils/isCustomProperty');
const isLessVariable = require('../../utils/isLessVariable');
const isMathFunction = require('../../utils/isMathFunction');
const keywordSets = require('../../reference/keywordSets');
const optionsMatches = require('../../utils/optionsMatches');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const styleSearch = require('style-search');
const validateOptions = require('../../utils/validateOptions');
const valueParser = require('postcss-value-parser');

const ruleName = 'length-zero-no-unit';

const messages = ruleMessages(ruleName, {
	rejected: 'Unexpected unit',
});

function rule(actual, secondary, context) {
	return (root, result) => {
		const validOptions = validateOptions(result, ruleName, { actual });

		if (!validOptions) {
			return;
		}

		root.walkDecls((decl) => {
			if (decl.prop.toLowerCase() === 'line-height') {
				return;
			}

			const stringValue = blurComments(decl.toString());
			const ignorableIndexes = new Array(stringValue.length).fill(false);
			const parsedValue = valueParser(stringValue);

			parsedValue.walk((node, nodeIndex) => {
				if (decl.prop.toLowerCase() === 'font' && node.type === 'div' && node.value === '/') {
					const lineHeightNode = parsedValue.nodes[nodeIndex + 1];
					const lineHeightNodeValue = valueParser.stringify(lineHeightNode);

					for (let i = 0; i < lineHeightNodeValue.length; i++) {
						ignorableIndexes[lineHeightNode.sourceIndex + i] = true;
					}

					return;
				}

				if (node.type !== 'function') {
					return;
				}

				const stringValue = valueParser.stringify(node);
				const ignoreFlag = isMathFunction(node);

				for (let i = 0; i < stringValue.length; i++) {
					ignorableIndexes[node.sourceIndex + i] = ignoreFlag;
				}
			});

			check(stringValue, decl, ignorableIndexes);
		});

		root.walkAtRules((atRule) => {
			// Ignore Less variables
			if (isLessVariable(atRule)) {
				return;
			}

			const source = hasBlock(atRule)
				? beforeBlockString(atRule, { noRawBefore: true })
				: atRule.toString();

			check(source, atRule);
		});

		function check(value, node, ignorableIndexes = []) {
			if (optionsMatches(secondary, 'ignore', 'custom-properties') && isCustomProperty(value)) {
				return;
			}

			const fixPositions = [];

			styleSearch({ source: value, target: '0' }, (match) => {
				const index = match.startIndex;

				// Given a 0 somewhere in the full property value (not in a string, thanks
				// to styleSearch) we need to isolate the value that contains the zero.
				// To do so, we'll find the last index before the 0 of a character that would
				// divide one value in a list from another, and the next index of such a
				// character; then we build a substring from those indexes, which we can
				// assess.

				// If a single value includes multiple 0's (e.g. 100.01px), we don't want
				// each 0 to be treated as a separate value, possibly resulting in multiple
				// warnings for the same value (e.g. 0.00px).
				//
				// This check prevents that from happening: we build and check against a
				// Set containing all the indexes that are part of a value already validated.
				if (ignorableIndexes[index]) {
					return;
				}

				const prevValueBreakIndex = _.findLastIndex(value.substr(0, index), (char) => {
					return [' ', ',', ')', '(', '#', ':', '\n', '\t'].includes(char);
				});

				// Ignore hex colors
				if (value[prevValueBreakIndex] === '#') {
					return;
				}

				// If no prev break was found, this value starts at 0
				const valueWithZeroStart = prevValueBreakIndex === -1 ? 0 : prevValueBreakIndex + 1;

				const nextValueBreakIndex = _.findIndex(value.substr(valueWithZeroStart), (char) => {
					return [' ', ',', ')', '/'].includes(char);
				});

				// If no next break was found, this value ends at the end of the string
				const valueWithZeroEnd =
					nextValueBreakIndex === -1 ? value.length : nextValueBreakIndex + valueWithZeroStart;

				const valueWithZero = value.slice(valueWithZeroStart, valueWithZeroEnd);
				const parsedValue = valueParser.unit(valueWithZero);

				if (!parsedValue || (parsedValue && !parsedValue.unit)) {
					return;
				}

				if (parsedValue.unit.toLowerCase() === 'fr') {
					return;
				}

				// Add the indexes to ignorableIndexes so the same value will not
				// be checked multiple times.
				_.range(valueWithZeroStart, valueWithZeroEnd).forEach((i) => (ignorableIndexes[i] = true));

				// Only pay attention if the value parses to 0
				// and units with lengths
				if (
					parseFloat(valueWithZero) !== 0 ||
					!keywordSets.lengthUnits.has(parsedValue.unit.toLowerCase())
				) {
					return;
				}

				if (context.fix) {
					fixPositions.unshift({
						startIndex: valueWithZeroStart,
						length: valueWithZeroEnd - valueWithZeroStart,
					});

					return;
				}

				report({
					message: messages.rejected,
					node,
					index: valueWithZeroEnd - parsedValue.unit.length,
					result,
					ruleName,
				});
			});

			if (fixPositions.length) {
				fixPositions.forEach((fixPosition) => {
					if (node.type === 'atrule') {
						// Use `-1` for `@` character before each at rule
						const realIndex =
							fixPosition.startIndex - node.name.length - node.raws.afterName.length - 1;

						node.params = replaceZero(node.params, realIndex, fixPosition.length);
					} else {
						const realIndex = fixPosition.startIndex - node.prop.length - node.raws.between.length;

						node.value = replaceZero(node.value, realIndex, fixPosition.length);
					}
				});
			}
		}
	};
}

function replaceZero(input, startIndex, length) {
	const stringStart = input.slice(0, startIndex);
	const stringEnd = input.slice(startIndex + length);

	return `${stringStart}0${stringEnd}`;
}

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