'use strict';

const _ = require('lodash');
const execall = require('execall');
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 ruleName = 'max-line-length';
const EXCLUDED_PATTERNS = [
	/url\(\s*([^\s].*[^\s])\s*\)/gi, // allow tab, whitespace in url content
	/@import\s+(['"].*['"])/gi,
];

const messages = ruleMessages(ruleName, {
	expected: (max) =>
		`Expected line length to be no more than ${max} ${max === 1 ? 'character' : 'characters'}`,
});

function rule(maxLength, options, context) {
	return (root, result) => {
		const validOptions = validateOptions(
			result,
			ruleName,
			{
				actual: maxLength,
				possible: _.isNumber,
			},
			{
				actual: options,
				possible: {
					ignore: ['non-comments', 'comments'],
					ignorePattern: [_.isString, _.isRegExp],
				},
				optional: true,
			},
		);

		if (!validOptions) {
			return;
		}

		const ignoreNonComments = optionsMatches(options, 'ignore', 'non-comments');
		const ignoreComments = optionsMatches(options, 'ignore', 'comments');
		const rootString = context.fix ? root.toString() : root.source.input.css;
		// Array of skipped sub strings, i.e `url(...)`, `@import "..."`
		let skippedSubStrings = [];
		let skippedSubStringsIndex = 0;

		EXCLUDED_PATTERNS.forEach((pattern) =>
			execall(pattern, rootString).forEach((match) => {
				const startOfSubString =
					match.index + match.match.indexOf(_.get(match, 'subMatches[0]', ''));

				return skippedSubStrings.push([
					startOfSubString,
					startOfSubString + _.get(match, 'subMatches[0].length', 0),
				]);
			}),
		);

		skippedSubStrings = skippedSubStrings.sort((a, b) => a[0] - b[0]);

		// Check first line
		checkNewline(rootString, { endIndex: 0 }, root);
		// Check subsequent lines
		styleSearch({ source: rootString, target: ['\n'], comments: 'check' }, (match) =>
			checkNewline(rootString, match, root),
		);

		function complain(index, root) {
			report({
				index,
				result,
				ruleName,
				message: messages.expected(maxLength),
				node: root,
			});
		}

		function tryToPopSubString(start, end) {
			const [startSubString, endSubString] = skippedSubStrings[skippedSubStringsIndex];

			// Excluded substring does not presented in current line
			if (end < startSubString) {
				return 0;
			}

			// Compute excluded substring size regarding to current line indexes
			const excluded = Math.min(end, endSubString) - Math.max(start, startSubString);

			// Current substring is out of range for next lines
			if (endSubString <= end) {
				skippedSubStringsIndex++;
			}

			return excluded;
		}

		function checkNewline(rootString, match, root) {
			let nextNewlineIndex = rootString.indexOf('\n', match.endIndex);

			if (rootString[nextNewlineIndex - 1] === '\r') {
				nextNewlineIndex -= 1;
			}

			// Accommodate last line
			if (nextNewlineIndex === -1) {
				nextNewlineIndex = rootString.length;
			}

			const rawLineLength = nextNewlineIndex - match.endIndex;
			const excludedLength = skippedSubStrings[skippedSubStringsIndex]
				? tryToPopSubString(match.endIndex, nextNewlineIndex)
				: 0;
			const lineText = rootString.slice(match.endIndex, nextNewlineIndex);

			// Case sensitive ignorePattern match
			if (optionsMatches(options, 'ignorePattern', lineText)) {
				return;
			}

			// If the line's length is less than or equal to the specified
			// max, ignore it ... So anything below is liable to be complained about.
			// **Note that the length of any url arguments or import urls
			// are excluded from the calculation.**
			if (rawLineLength - excludedLength <= maxLength) {
				return;
			}

			const complaintIndex = nextNewlineIndex - 1;

			if (ignoreComments) {
				if (match.insideComment) {
					return;
				}

				// This trimming business is to notice when the line starts a
				// comment but that comment is indented, e.g.
				//       /* something here */
				const nextTwoChars = rootString.slice(match.endIndex).trim().slice(0, 2);

				if (nextTwoChars === '/*' || nextTwoChars === '//') {
					return;
				}
			}

			if (ignoreNonComments) {
				if (match.insideComment) {
					return complain(complaintIndex, root);
				}

				// This trimming business is to notice when the line starts a
				// comment but that comment is indented, e.g.
				//       /* something here */
				const nextTwoChars = rootString.slice(match.endIndex).trim().slice(0, 2);

				if (nextTwoChars !== '/*' && nextTwoChars !== '//') {
					return;
				}

				return complain(complaintIndex, root);
			}

			// If there are no spaces besides initial (indent) spaces, ignore it
			const lineString = rootString.slice(match.endIndex, nextNewlineIndex);

			if (!lineString.replace(/^\s+/, '').includes(' ')) {
				return;
			}

			return complain(complaintIndex, root);
		}
	};
}

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