import '../AST/nodeTypes';

import { getNodeType, } from '../AST';

import CSSTransformerBase from './CSSTransformerBase';


/**
 * Compare values.
 * @memberof CSSTransformers
 * @param {any} a Value A.
 * @param {any} b Value B.
 * @returns {number}
 */
function compareValues(a, b) {
	return a < b
		? -1
		: a > b
			? 1
			: 0;
}

/**
 * Compare nodes.
 * Higher means closer to the start.
 * Sorting rule is simple: atRule > rule > anything else.
 * Same typed nodes are sorted in alphabetic order.
 * @memberof CSSTransformers
 * @param {ASTNode} a Node A.
 * @param {ASTNode} b Node B.
 * @returns {number}
 */
function nodesSorter(a, b) {
	if ('atRule' !== getNodeType(a) && 'atRule' === getNodeType(b))
		return 1;

	if ('atRule' === getNodeType(a)) {
		if ('atRule' === getNodeType(b))
			return compareValues(a.value, b.value);
		return -1;
	}

	if ('rule' !== getNodeType(a) && 'rule' === getNodeType(b))
		return 1;

	if ('rule' === getNodeType(a)) {
		if ('rule' === getNodeType(b))
			return compareValues(a.props.join(','), b.props.join(','));
		return -1;
	}

	return 0;
}

/**
 * Merge duplicate declarations. For duplicated identifies last one would be used.
 * To apply this transformer call `SortAndMerge.transform(el)`.
 * @memberof CSSTransformers
 * @class
 */
class SortAndMerge extends CSSTransformerBase {
	/**
	 * Internal state initializer.
	 * To apply transformer use `SortAndMerge.transform(el)` instead.
	 */
	constructor() {
		super();
		this.rulesMaps = new WeakMap();
		this.rootsMaps = new WeakMap();
	}

	/**
	 * Root node transformer.
	 * @param {Rule[]}     el             Root element.
	 * @param {?number}    i              Root element index if any.
	 * @param {?ASTNode[]} parentChildren Children of root element parent if there is any parent.
	 * @param {?Function}  cb             Callback.
	 */
	root(el, i, parentChildren, cb) { // eslint-disable-line no-unused-vars
		let rootAnchor = Object.create(null);

		el.forEach(child => {
			if ('object' !== typeof (child.parent ?? void 0))
				child.parent = rootAnchor;
			else
				rootAnchor = child.parent;
		});

		this.transformSubElements(el);

		const
			{ rootsMaps, } = this,
			root = rootsMaps.get(rootAnchor),
			rules = 'object' === typeof root
				? Object.keys(root)
				: [];

		let _i = 0;

		const droppedItems = [];
		el.forEach((child, childIndex, _el) => {
			if ([ 'rule', 'atRule', ].includes(getNodeType(child))) {
				if (rules[_i]) {
					_el[childIndex] = root[rules[_i]];
					_i++;
					return true;
				}
				droppedItems.push(child);
				return false;
			}
			return true;
		});
		droppedItems.forEach(
			item => el.splice(el.indexOf(item), 1)
		);

		el.sort(nodesSorter).map((item, i) => el[i] = item);
	}

	/**
	 * At rule transformer.
	 * @param {AtRule}    el             Element.
	 * @param {number}    i              Element index.
	 * @param {ASTNode[]} parentChildren Element's parent children list.
	 * @param {?Function} cb             Callback.
	 */
	atRule(el, i, parentChildren, cb) { // eslint-disable-line no-unused-vars
		this.rule(el, i, parentChildren, cb);
		this.root(el.children, i, parentChildren, cb);
	}

	/**
	 * Rule transformer.
	 * @param {Rule}      el             Element.
	 * @param {number}    i              Element index.
	 * @param {ASTNode[]} parentChildren Element's parent children list.
	 * @param {?Function} cb             Callback.
	 */
	rule(el, i, parentChildren, cb) { // eslint-disable-line no-unused-vars
		const
			{ rootsMaps, } = this,
			{ parent, } = el,
			id = 'atRule' !== getNodeType(el)
				? el.props.join(',')
				: el.value;

		if (!rootsMaps.has(parent))
			rootsMaps.set(parent, {});

		const root = rootsMaps.get(parent);

		if (root[id])
			root[id].children.forEach(child => {
				child.parent = el;
				el.children.unshift(child);
			});

		root[id] = el;

		if ('atRule' !== getNodeType(el))
			this.transformSubElements(el.children);
		else
			this.transformSubElements(el.children, [ 'declaration', ], true);

		const
			{ rulesMaps, } = this,
			rule = rulesMaps.get(el),
			declarations = 'object' === typeof rule
				? Object.keys(rule).sort()
				: [];

		let _i = 0;

		const droppedItems = [];
		el.children.forEach(child => {
			if ('declaration' === getNodeType(child)) {
				if (declarations[_i]) {
					child.props = declarations[_i];
					child.children = rule[declarations[_i]];
					child.value = `${child.props}:${child.children};`;
					_i++;
					return true;
				}
				droppedItems.push(child);
				return false;
			}
			return true;
		});
		droppedItems.forEach(
			item => el.children.splice(el.children.indexOf(item), 1)
		);

		return;
	}
	/**
	 * Declaration transformer.
	 * @param {Declaration} el             Element.
	 * @param {number}      i              Element index.
	 * @param {ASTNode[]}   parentChildren Element's parent children list.
	 * @param {?Function}   cb             Callback.
	 */
	declaration(el, i, parentChildren, cb) { // eslint-disable-line no-unused-vars
		const
			{ rulesMaps, } = this,
			{ parent, } = el;

		if (!rulesMaps.has(parent))
			rulesMaps.set(parent, {});

		const rule = rulesMaps.get(parent);

		rule[el.props] = el.children;
		return;
	}
}

export default SortAndMerge;