import * as namespaces from '../../utils/namespaces';
import validateEventHandler from './validateEventHandler';
import validate, { Validator } from '../index';
import { Node } from '../../interfaces';
import { dimensions } from '../../utils/patterns';
import isVoidElementName from '../../utils/isVoidElementName';
import isValidIdentifier from '../../utils/isValidIdentifier';

const svg = /^(?:altGlyph|altGlyphDef|altGlyphItem|animate|animateColor|animateMotion|animateTransform|circle|clipPath|color-profile|cursor|defs|desc|discard|ellipse|feBlend|feColorMatrix|feComponentTransfer|feComposite|feConvolveMatrix|feDiffuseLighting|feDisplacementMap|feDistantLight|feDropShadow|feFlood|feFuncA|feFuncB|feFuncG|feFuncR|feGaussianBlur|feImage|feMerge|feMergeNode|feMorphology|feOffset|fePointLight|feSpecularLighting|feSpotLight|feTile|feTurbulence|filter|font|font-face|font-face-format|font-face-name|font-face-src|font-face-uri|foreignObject|g|glyph|glyphRef|hatch|hatchpath|hkern|image|line|linearGradient|marker|mask|mesh|meshgradient|meshpatch|meshrow|metadata|missing-glyph|mpath|path|pattern|polygon|polyline|radialGradient|rect|set|solidcolor|stop|switch|symbol|text|textPath|tref|tspan|unknown|use|view|vkern)$/;

export default function validateElement(
	validator: Validator,
	node: Node,
	refs: Map<string, Node[]>,
	refCallees: Node[],
	stack: Node[],
	elementStack: Node[]
) {
	if (elementStack.length === 0 && validator.namespace !== namespaces.svg && svg.test(node.name)) {
		validator.warn(node, {
			code: `missing-namespace`,
			message: `<${node.name}> is an SVG element – did you forget to add { namespace: 'svg' } ?`
		});
	}

	if (node.name === 'slot') {
		const nameAttribute = node.attributes.find((attribute: Node) => attribute.name === 'name');
		if (nameAttribute) {
			if (nameAttribute.value.length !== 1 || nameAttribute.value[0].type !== 'Text') {
				validator.error(nameAttribute, {
					code: `dynamic-slot-name`,
					message: `<slot> name cannot be dynamic`
				});
			}

			const slotName = nameAttribute.value[0].data;
			if (slotName === 'default') {
				validator.error(nameAttribute, {
					code: `invalid-slot-name`,
					message: `default is a reserved word — it cannot be used as a slot name`
				});
			}

			// TODO should duplicate slots be disallowed? Feels like it's more likely to be a
			// bug than anything. Perhaps it should be a warning

			// if (validator.slots.has(slotName)) {
			// 	validator.error(`duplicate '${slotName}' <slot> element`, nameAttribute.start);
			// }

			// validator.slots.add(slotName);
		} else {
			// if (validator.slots.has('default')) {
			// 	validator.error(`duplicate default <slot> element`, node.start);
			// }

			// validator.slots.add('default');
		}
	}

	if (node.name === 'title') {
		if (node.attributes.length > 0) {
			validator.error(node.attributes[0], {
				code: `illegal-attribute`,
				message: `<title> cannot have attributes`
			});
		}

		node.children.forEach(child => {
			if (child.type !== 'Text' && child.type !== 'MustacheTag') {
				validator.error(child, {
					code: 'illegal-structure',
					message: `<title> can only contain text and {tags}`
				});
			}
		});
	}

	let hasIntro: boolean;
	let hasOutro: boolean;
	let hasTransition: boolean;
	let hasAnimation: boolean;

	node.attributes.forEach((attribute: Node) => {
		if (attribute.type === 'Ref') {
			if (!isValidIdentifier(attribute.name)) {
				const suggestion = attribute.name.replace(/[^_$a-z0-9]/ig, '_').replace(/^\d/, '_$&');
				
				validator.error(attribute, {
					code: `invalid-reference-name`,
					message: `Reference name '${attribute.name}' is invalid — must be a valid identifier such as ${suggestion}`
				});	
			} else {
				if (!refs.has(attribute.name)) refs.set(attribute.name, []);
				refs.get(attribute.name).push(node);
			}
		}

		if (attribute.type === 'Binding') {
			const { name } = attribute;

			if (name === 'value') {
				if (
					node.name !== 'input' &&
					node.name !== 'textarea' &&
					node.name !== 'select'
				) {
					validator.error(attribute, {
						code: `invalid-binding`,
						message: `'value' is not a valid binding on <${node.name}> elements`
					});
				}

				if (node.name === 'select') {
					const attribute = node.attributes.find(
						(attribute: Node) => attribute.name === 'multiple'
					);

					if (attribute && isDynamic(attribute)) {
						validator.error(attribute, {
							code: `dynamic-multiple-attribute`,
							message: `'multiple' attribute cannot be dynamic if select uses two-way binding`
						});
					}
				} else {
					checkTypeAttribute(validator, node);
				}
			} else if (name === 'checked' || name === 'indeterminate') {
				if (node.name !== 'input') {
					validator.error(attribute, {
						code: `invalid-binding`,
						message: `'${name}' is not a valid binding on <${node.name}> elements`
					});
				}

				if (checkTypeAttribute(validator, node) !== 'checkbox') {
					validator.error(attribute, {
						code: `invalid-binding`,
						message: `'${name}' binding can only be used with <input type="checkbox">`
					});
				}
			} else if (name === 'group') {
				if (node.name !== 'input') {
					validator.error(attribute, {
						code: `invalid-binding`,
						message: `'group' is not a valid binding on <${node.name}> elements`
					});
				}

				const type = checkTypeAttribute(validator, node);

				if (type !== 'checkbox' && type !== 'radio') {
					validator.error(attribute, {
						code: `invalid-binding`,
						message: `'checked' binding can only be used with <input type="checkbox"> or <input type="radio">`
					});
				}
			} else if (name == 'files') {
				if (node.name !== 'input') {
					validator.error(attribute, {
						code: `invalid-binding`,
						message: `'files' binding acn only be used with <input type="file">`
					});
				}

				const type = checkTypeAttribute(validator, node);

				if (type !== 'file') {
					validator.error(attribute, {
						code: `invalid-binding`,
						message: `'files' binding can only be used with <input type="file">`
					});
				}
			} else if (
				name === 'currentTime' ||
				name === 'duration' ||
				name === 'paused' ||
				name === 'buffered' ||
				name === 'seekable' ||
				name === 'played' ||
				name === 'volume'
			) {
				if (node.name !== 'audio' && node.name !== 'video') {
					validator.error(attribute, {
						code: `invalid-binding`,
						message: `'${name}' binding can only be used with <audio> or <video>`
					});
				}
			} else if (dimensions.test(name)) {
				if (node.name === 'svg' && (name === 'offsetWidth' || name === 'offsetHeight')) {
					validator.error(attribute, {
						code: 'invalid-binding',
						message: `'${attribute.name}' is not a valid binding on <svg>. Use '${name.replace('offset', 'client')}' instead`
					});
				} else if (svg.test(node.name)) {
					validator.error(attribute, {
						code: 'invalid-binding',
						message: `'${attribute.name}' is not a valid binding on SVG elements`
					});
				} else if (isVoidElementName(node.name)) {
					validator.error(attribute, {
						code: 'invalid-binding',
						message: `'${attribute.name}' is not a valid binding on void elements like <${node.name}>. Use a wrapper element instead`
					});
				}
			} else {
				validator.error(attribute, {
					code: `invalid-binding`,
					message: `'${attribute.name}' is not a valid binding`
				});
			}
		} else if (attribute.type === 'EventHandler') {
			validator.used.events.add(attribute.name);
			validateEventHandler(validator, attribute, refCallees);
		} else if (attribute.type === 'Transition') {
			validator.used.transitions.add(attribute.name);

			const bidi = attribute.intro && attribute.outro;

			if (hasTransition) {
				if (bidi) {
					validator.error(attribute, {
						code: `duplicate-transition`,
						message: `An element can only have one 'transition' directive`
					});
				}

				validator.error(attribute, {
					code: `duplicate-transition`,
					message: `An element cannot have both a 'transition' directive and an '${attribute.intro ? 'in' : 'out'}' directive`
				});
			}

			if ((hasIntro && attribute.intro) || (hasOutro && attribute.outro)) {
				if (bidi) {
					validator.error(attribute, {
						code: `duplicate-transition`,
						message: `An element cannot have both an '${hasIntro ? 'in' : 'out'}' directive and a 'transition' directive`
					});
				}

				validator.error(attribute, {
					code: `duplicate-transition`,
					message: `An element can only have one '${hasIntro ? 'in' : 'out'}' directive`
				});
			}

			if (attribute.intro) hasIntro = true;
			if (attribute.outro) hasOutro = true;
			if (bidi) hasTransition = true;

			if (!validator.transitions.has(attribute.name)) {
				validator.error(attribute, {
					code: `missing-transition`,
					message: `Missing transition '${attribute.name}'`
				});
			}
		} else if (attribute.type === 'Animation') {
			validator.used.animations.add(attribute.name);

			if (hasAnimation) {
				validator.error(attribute, {
					code: `duplicate-animation`,
					message: `An element can only have one 'animate' directive`
				});
			}

			if (!validator.animations.has(attribute.name)) {
				validator.error(attribute, {
					code: `missing-animation`,
					message: `Missing animation '${attribute.name}'`
				});
			}

			const parent = stack[stack.length - 1];
			if (!parent || parent.type !== 'EachBlock' || !parent.key) {
				// TODO can we relax the 'immediate child' rule?
				validator.error(attribute, {
					code: `invalid-animation`,
					message: `An element that use the animate directive must be the immediate child of a keyed each block`
				});
			}

			if (parent.children.length > 1) {
				validator.error(attribute, {
					code: `invalid-animation`,
					message: `An element that use the animate directive must be the sole child of a keyed each block`
				});
			}

			hasAnimation = true;
		} else if (attribute.type === 'Attribute') {
			if (attribute.name === 'value' && node.name === 'textarea') {
				if (node.children.length) {
					validator.error(attribute, {
						code: `textarea-duplicate-value`,
						message: `A <textarea> can have either a value attribute or (equivalently) child content, but not both`
					});
				}
			}

			if (attribute.name === 'slot') {
				checkSlotAttribute(validator, node, attribute, stack);
			}
		} else if (attribute.type === 'Action') {
			validator.used.actions.add(attribute.name);

			if (!validator.actions.has(attribute.name)) {
				validator.error(attribute, {
					code: `missing-action`,
					message: `Missing action '${attribute.name}'`
				});
			}
		}
	});
}

function checkTypeAttribute(validator: Validator, node: Node) {
	const attribute = node.attributes.find(
		(attribute: Node) => attribute.name === 'type'
	);
	if (!attribute) return null;

	if (attribute.value === true) {
		validator.error(attribute, {
			code: `missing-type`,
			message: `'type' attribute must be specified`
		});
	}

	if (isDynamic(attribute)) {
		validator.error(attribute, {
			code: `invalid-type`,
			message: `'type' attribute cannot be dynamic if input uses two-way binding`
		});
	}

	return attribute.value[0].data;
}

function checkSlotAttribute(validator: Validator, node: Node, attribute: Node, stack: Node[]) {
	if (isDynamic(attribute)) {
		validator.error(attribute, {
			code: `invalid-slot-attribute`,
			message: `slot attribute cannot have a dynamic value`
		});
	}

	let i = stack.length;
	while (i--) {
		const parent = stack[i];

		if (parent.type === 'Component') {
			// if we're inside a component or a custom element, gravy
			if (parent.name === 'svelte:self' || parent.name === 'svelte:component' || validator.components.has(parent.name)) return;
		} else if (parent.type === 'Element') {
			if (/-/.test(parent.name)) return;
		}

		if (parent.type === 'IfBlock' || parent.type === 'EachBlock') {
			const message = `Cannot place slotted elements inside an ${parent.type === 'IfBlock' ? 'if' : 'each'}-block`;
			validator.error(attribute, {
				code: `invalid-slotted-content`,
				message
			});
		}
	}

	validator.error(attribute, {
		code: `invalid-slotted-content`,
		message: `Element with a slot='...' attribute must be a descendant of a component or custom element`
	});
}

function isDynamic(attribute: Node) {
	if (attribute.value === true) return false;
	return attribute.value.length > 1 || attribute.value[0].type !== 'Text';
}