'use strict';

const _ = require('lodash');
const atRuleParamIndex = require('../../utils/atRuleParamIndex');
const declarationValueIndex = require('../../utils/declarationValueIndex');
const isWhitespace = require('../../utils/isWhitespace');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const styleSearch = require('style-search');
const validateOptions = require('../../utils/validateOptions');

const ruleName = 'function-whitespace-after';

const messages = ruleMessages(ruleName, {
	expected: 'Expected whitespace after ")"',
	rejected: 'Unexpected whitespace after ")"',
});

const ACCEPTABLE_AFTER_CLOSING_PAREN = new Set([')', ',', '}', ':', '/', undefined]);

function rule(expectation, options, context) {
	return (root, result) => {
		const validOptions = validateOptions(result, ruleName, {
			actual: expectation,
			possible: ['always', 'never'],
		});

		if (!validOptions) {
			return;
		}

		function check(node, value, getIndex, fix) {
			styleSearch(
				{
					source: value,
					target: ')',
					functionArguments: 'only',
				},
				(match) => {
					checkClosingParen(value, match.startIndex + 1, node, getIndex, fix);
				},
			);
		}

		function checkClosingParen(source, index, node, getIndex, fix) {
			const nextChar = source[index];

			if (expectation === 'always') {
				// Allow for the next character to be a single empty space,
				// another closing parenthesis, a comma, or the end of the value
				if (nextChar === ' ') {
					return;
				}

				if (nextChar === '\n') {
					return;
				}

				if (source.substr(index, 2) === '\r\n') {
					return;
				}

				if (ACCEPTABLE_AFTER_CLOSING_PAREN.has(nextChar)) {
					return;
				}

				if (fix) {
					fix(index);

					return;
				}

				report({
					message: messages.expected,
					node,
					index: getIndex(node) + index,
					result,
					ruleName,
				});
			} else if (expectation === 'never') {
				if (isWhitespace(nextChar)) {
					if (fix) {
						fix(index);

						return;
					}

					report({
						message: messages.rejected,
						node,
						index: getIndex(node) + index,
						result,
						ruleName,
					});
				}
			}
		}

		function createFixer(value) {
			let fixed = '';
			let lastIndex = 0;
			let applyFix;

			if (expectation === 'always') {
				applyFix = (index) => {
					// eslint-disable-next-line prefer-template
					fixed += value.slice(lastIndex, index) + ' ';
					lastIndex = index;
				};
			} else if (expectation === 'never') {
				applyFix = (index) => {
					let whitespaceEndIndex = index + 1;

					while (whitespaceEndIndex < value.length && isWhitespace(value[whitespaceEndIndex])) {
						whitespaceEndIndex++;
					}
					fixed += value.slice(lastIndex, index);
					lastIndex = whitespaceEndIndex;
				};
			}

			return {
				applyFix,
				get hasFixed() {
					return Boolean(lastIndex);
				},
				get fixed() {
					return fixed + value.slice(lastIndex);
				},
			};
		}

		root.walkAtRules(/^import$/i, (atRule) => {
			const param = _.get(atRule, 'raws.params.raw', atRule.params);
			const fixer = context.fix && createFixer(param);

			check(atRule, param, atRuleParamIndex, fixer && fixer.applyFix);

			if (fixer && fixer.hasFixed) {
				if (atRule.raws.params) {
					atRule.raws.params.raw = fixer.fixed;
				} else {
					atRule.params = fixer.fixed;
				}
			}
		});
		root.walkDecls((decl) => {
			const value = _.get(decl, 'raws.value.raw', decl.value);
			const fixer = context.fix && createFixer(value);

			check(decl, value, declarationValueIndex, fixer && fixer.applyFix);

			if (fixer && fixer.hasFixed) {
				if (decl.raws.value) {
					decl.raws.value.raw = fixer.fixed;
				} else {
					decl.value = fixer.fixed;
				}
			}
		});
	};
}

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