import deindent from '../../utils/deindent';
import { stringify, escapeHTML } from '../../utils/stringify';
import flattenReference from '../../utils/flattenReference';
import isVoidElementName from '../../utils/isVoidElementName';
import validCalleeObjects from '../../utils/validCalleeObjects';
import reservedNames from '../../utils/reservedNames';
import fixAttributeCasing from '../../utils/fixAttributeCasing';
import quoteIfNecessary from '../../utils/quoteIfNecessary';
import mungeAttribute from './shared/mungeAttribute';
import Node from './shared/Node';
import Block from '../dom/Block';
import Attribute from './Attribute';
import Binding from './Binding';
import EventHandler from './EventHandler';
import Ref from './Ref';
import Transition from './Transition';
import Action from './Action';
import Text from './Text';
import * as namespaces from '../../utils/namespaces';
import mapChildren from './shared/mapChildren';

export default class Element extends Node {
	type: 'Element';
	name: string;
	attributes: Attribute[];
	bindings: Binding[];
	handlers: EventHandler[];
	intro: Transition;
	outro: Transition;
	children: Node[];

	ref: string;
	namespace: string;

	constructor(compiler, parent, info: any) {
		super(compiler, parent, info);
		this.name = info.name;

		const parentElement = parent.findNearest(/^Element/);
		this.namespace = this.name === 'svg' ?
			namespaces.svg :
			parentElement ? parentElement.namespace : this.compiler.namespace;

		this.attributes = [];
		this.bindings = [];
		this.handlers = [];

		this.intro = null;
		this.outro = null;

		info.attributes.forEach(node => {
			switch (node.type) {
				case 'Attribute':
					// special case
					if (node.name === 'xmlns') this.namespace = node.value[0].data;

					this.attributes.push(new Attribute(compiler, this, node));
					break;

				case 'Binding':
					this.bindings.push(new Binding(compiler, this, node));
					break;

				case 'EventHandler':
					this.handlers.push(new EventHandler(compiler, this, node));
					break;

				case 'Transition':
					const transition = new Transition(compiler, this, node);
					if (node.intro) this.intro = transition;
					if (node.outro) this.outro = transition;
					break;

				case 'Ref':
					// TODO catch this in validation
					if (this.ref) throw new Error(`Duplicate refs`);

					compiler.usesRefs = true
					this.ref = node.name;
					break;

				default:
					throw new Error(`Not implemented: ${node.type}`);
			}
		});

		// TODO break out attributes and directives here

		this.children = mapChildren(compiler, this, info.children);
	}

	init(
		block: Block,
		stripWhitespace: boolean,
		nextSibling: Node
	) {
		if (this.name === 'slot' || this.name === 'option') {
			this.cannotUseInnerHTML();
		}

		this.attributes.forEach(attr => {
			if (attr.dependencies.size) {
				this.parent.cannotUseInnerHTML();
				block.addDependencies(attr.dependencies);

				// special case — <option value={foo}> — see below
				if (this.name === 'option' && attr.name === 'value') {
					let select = this.parent;
					while (select && (select.type !== 'Element' || select.name !== 'select')) select = select.parent;

					if (select && select.selectBindingDependencies) {
						select.selectBindingDependencies.forEach(prop => {
							dependencies.forEach((dependency: string) => {
								this.compiler.indirectDependencies.get(prop).add(dependency);
							});
						});
					}
				}
			}
		});

		this.bindings.forEach(binding => {
			this.cannotUseInnerHTML();
			block.addDependencies(binding.value.dependencies);
		});

		this.handlers.forEach(handler => {
			this.cannotUseInnerHTML();
			block.addDependencies(handler.dependencies);
		});

		if (this.intro) {
			this.compiler.hasIntroTransitions = block.hasIntroMethod = true;
		}

		if (this.outro) {
			this.compiler.hasOutroTransitions = block.hasOutroMethod = true;
			block.outros += 1;
		}

		this.attributes.forEach(attribute => {
			if (attribute.type === 'Attribute' && attribute.value !== true) {
				// removed
			} else {
				if (this.parent) this.parent.cannotUseInnerHTML();

				if (attribute.type === 'Action' && attribute.expression) {
					block.addDependencies(attribute.metadata.dependencies);
				} else if (attribute.type === 'Spread') {
					block.addDependencies(attribute.metadata.dependencies);
				}
			}
		});

		const valueAttribute = this.attributes.find((attribute: Attribute) => attribute.name === 'value');

		if (this.name === 'textarea') {
			// this is an egregious hack, but it's the easiest way to get <textarea>
			// children treated the same way as a value attribute
			if (this.children.length > 0) {
				this.attributes.push(new Attribute(this.compiler, this, {
					name: 'value',
					value: this.children
				}));

				this.children = [];
			}
		}

		// special case — in a case like this...
		//
		//   <select bind:value='foo'>
		//     <option value='{{bar}}'>bar</option>
		//     <option value='{{baz}}'>baz</option>
		//   </option>
		//
		// ...we need to know that `foo` depends on `bar` and `baz`,
		// so that if `foo.qux` changes, we know that we need to
		// mark `bar` and `baz` as dirty too
		if (this.name === 'select') {
			const binding = this.attributes.find(node => node.type === 'Binding' && node.name === 'value');
			if (binding) {
				// TODO does this also apply to e.g. `<input type='checkbox' bind:group='foo'>`?
				const dependencies = binding.metadata.dependencies;
				this.selectBindingDependencies = dependencies;
				dependencies.forEach((prop: string) => {
					this.compiler.indirectDependencies.set(prop, new Set());
				});
			} else {
				this.selectBindingDependencies = null;
			}
		}

		const slot = this.getStaticAttributeValue('slot');
		if (slot && this.hasAncestor('Component')) {
			this.cannotUseInnerHTML();
			this.slotted = true;
			// TODO validate slots — no nesting, no dynamic names...
			const component = this.findNearest(/^Component/);
			component._slots.add(slot);
		}

		if (this.spread) {
			block.addDependencies(this.spread.metadata.dependencies);
		}

		this.var = block.getUniqueName(
			this.name.replace(/[^a-zA-Z0-9_$]/g, '_')
		);

		if (this.children.length) {
			if (this.name === 'pre' || this.name === 'textarea') stripWhitespace = false;
			this.initChildren(block, stripWhitespace, nextSibling);
		}
	}

	build(
		block: Block,
		parentNode: string,
		parentNodes: string
	) {
		const { compiler } = this;

		if (this.name === 'slot') {
			const slotName = this.getStaticAttributeValue('name') || 'default';
			this.compiler.slots.add(slotName);
		}

		if (this.name === 'noscript') return;

		const childState = {
			parentNode: this.var,
			parentNodes: parentNodes && block.getUniqueName(`${this.var}_nodes`) // if we're in unclaimable territory, i.e. <head>, parentNodes is null
		};

		const name = this.var;
		const allUsedContexts: Set<string> = new Set();

		const slot = this.attributes.find((attribute: Node) => attribute.name === 'slot');
		const initialMountNode = this.slotted ?
			`${this.findNearest(/^Component/).var}._slotted.${slot.value[0].data}` : // TODO this looks bonkers
			parentNode;

		block.addVariable(name);
		const renderStatement = getRenderStatement(this.compiler, this.namespace, this.name);
		block.builders.create.addLine(
			`${name} = ${renderStatement};`
		);

		if (this.compiler.hydratable) {
			if (parentNodes) {
				block.builders.claim.addBlock(deindent`
					${name} = ${getClaimStatement(compiler, this.namespace, parentNodes, this)};
					var ${childState.parentNodes} = @children(${name});
				`);
			} else {
				block.builders.claim.addLine(
					`${name} = ${renderStatement};`
				);
			}
		}

		if (initialMountNode) {
			block.builders.mount.addLine(
				`@appendNode(${name}, ${initialMountNode});`
			);

			if (initialMountNode === 'document.head') {
				block.builders.unmount.addLine(`@detachNode(${name});`);
			}
		} else {
			block.builders.mount.addLine(`@insertNode(${name}, #target, anchor);`);

			// TODO we eventually need to consider what happens to elements
			// that belong to the same outgroup as an outroing element...
			block.builders.unmount.addLine(`@detachNode(${name});`);
		}

		// TODO move this into a class as well?
		if (this._cssRefAttribute) {
			block.builders.hydrate.addLine(
				`@setAttribute(${name}, "svelte-ref-${this._cssRefAttribute}", "");`
			)
		}

		// insert static children with textContent or innerHTML
		if (!this.namespace && this.canUseInnerHTML && this.children.length > 0) {
			if (this.children.length === 1 && this.children[0].type === 'Text') {
				block.builders.create.addLine(
					`${name}.textContent = ${stringify(this.children[0].data)};`
				);
			} else {
				block.builders.create.addLine(
					`${name}.innerHTML = ${stringify(this.children.map(toHTML).join(''))};`
				);
			}
		} else {
			this.children.forEach((child: Node) => {
				child.build(block, childState.parentNode, childState.parentNodes);
			});
		}

		this.addBindings(block, allUsedContexts);
		const eventHandlerUsesComponent = this.addEventHandlers(block, allUsedContexts);
		if (this.ref) this.addRef(block);
		this.addAttributes(block);
		this.addTransitions(block);
		this.addActions(block);

		if (allUsedContexts.size || eventHandlerUsesComponent) {
			const initialProps: string[] = [];
			const updates: string[] = [];

			if (eventHandlerUsesComponent) {
				initialProps.push(`component: #component`);
			}

			allUsedContexts.forEach((contextName: string) => {
				if (contextName === 'state') return;
				if (block.contextTypes.get(contextName) !== 'each') return;

				const listName = block.listNames.get(contextName);
				const indexName = block.indexNames.get(contextName);

				initialProps.push(
					`${listName}: state.${listName},\n${indexName}: state.${indexName}`
				);
				updates.push(
					`${name}._svelte.${listName} = state.${listName};\n${name}._svelte.${indexName} = state.${indexName};`
				);
			});

			if (initialProps.length) {
				block.builders.hydrate.addBlock(deindent`
					${name}._svelte = {
						${initialProps.join(',\n')}
					};
				`);
			}

			if (updates.length) {
				block.builders.update.addBlock(updates.join('\n'));
			}
		}

		if (this.initialUpdate) {
			block.builders.mount.addBlock(this.initialUpdate);
		}

		if (childState.parentNodes) {
			block.builders.claim.addLine(
				`${childState.parentNodes}.forEach(@detachNode);`
			);
		}

		function toHTML(node: Element | Text) {
			if (node.type === 'Text') {
				return node.parent &&
					node.parent.type === 'Element' &&
					(node.parent.name === 'script' || node.parent.name === 'style')
					? node.data
					: escapeHTML(node.data);
			}

			if (node.name === 'noscript') return '';

			let open = `<${node.name}`;

			if (node._cssRefAttribute) {
				open += ` svelte-ref-${node._cssRefAttribute}`;
			}

			node.attributes.forEach((attr: Node) => {
				open += ` ${fixAttributeCasing(attr.name)}${stringifyAttributeValue(attr.value)}`
			});

			if (isVoidElementName(node.name)) return open + '>';

			return `${open}>${node.children.map(toHTML).join('')}</${node.name}>`;
		}
	}

	addBindings(
		block: Block,
		allUsedContexts: Set<string>
	) {
		if (this.bindings.length === 0) return;

		if (this.name === 'select' || this.isMediaNode()) this.compiler.hasComplexBindings = true;

		const needsLock = this.name !== 'input' || !/radio|checkbox|range|color/.test(this.getStaticAttributeValue('type'));

		// TODO munge in constructor
		const mungedBindings = this.bindings.map(binding => binding.munge(block, allUsedContexts));

		const lock = mungedBindings.some(binding => binding.needsLock) ?
			block.getUniqueName(`${this.var}_updating`) :
			null;

		if (lock) block.addVariable(lock, 'false');

		const groups = events
			.map(event => {
				return {
					events: event.eventNames,
					bindings: mungedBindings.filter(binding => event.filter(this, binding.name))
				};
			})
			.filter(group => group.bindings.length);

		groups.forEach(group => {
			const handler = block.getUniqueName(`${this.var}_${group.events.join('_')}_handler`);

			const needsLock = group.bindings.some(binding => binding.needsLock);

			group.bindings.forEach(binding => {
				if (!binding.updateDom) return;

				const updateConditions = needsLock ? [`!${lock}`] : [];
				if (binding.updateCondition) updateConditions.push(binding.updateCondition);

				block.builders.update.addLine(
					updateConditions.length ? `if (${updateConditions.join(' && ')}) ${binding.updateDom}` : binding.updateDom
				);
			});

			const usesContext = group.bindings.some(binding => binding.handler.usesContext);
			const usesState = group.bindings.some(binding => binding.handler.usesState);
			const usesStore = group.bindings.some(binding => binding.handler.usesStore);
			const mutations = group.bindings.map(binding => binding.handler.mutation).filter(Boolean).join('\n');

			const props = new Set();
			const storeProps = new Set();
			group.bindings.forEach(binding => {
				binding.handler.props.forEach(prop => {
					props.add(prop);
				});

				binding.handler.storeProps.forEach(prop => {
					storeProps.add(prop);
				});
			}); // TODO use stringifyProps here, once indenting is fixed

			// media bindings — awkward special case. The native timeupdate events
			// fire too infrequently, so we need to take matters into our
			// own hands
			let animation_frame;
			if (group.events[0] === 'timeupdate') {
				animation_frame = block.getUniqueName(`${this.var}_animationframe`);
				block.addVariable(animation_frame);
			}

			block.builders.init.addBlock(deindent`
				function ${handler}() {
					${
						animation_frame && deindent`
							cancelAnimationFrame(${animation_frame});
							if (!${this.var}.paused) ${animation_frame} = requestAnimationFrame(${handler});`
					}
					${usesContext && `var context = ${this.var}._svelte;`}
					${usesState && `var state = #component.get();`}
					${usesStore && `var $ = #component.store.get();`}
					${needsLock && `${lock} = true;`}
					${mutations.length > 0 && mutations}
					${props.size > 0 && `#component.set({ ${Array.from(props).join(', ')} });`}
					${storeProps.size > 0 && `#component.store.set({ ${Array.from(storeProps).join(', ')} });`}
					${needsLock && `${lock} = false;`}
				}
			`);

			group.events.forEach(name => {
				block.builders.hydrate.addLine(
					`@addListener(${this.var}, "${name}", ${handler});`
				);

				block.builders.destroy.addLine(
					`@removeListener(${this.var}, "${name}", ${handler});`
				);
			});

			const allInitialStateIsDefined = group.bindings
				.map(binding => `'${binding.object}' in state`)
				.join(' && ');

			if (this.name === 'select' || group.bindings.find(binding => binding.name === 'indeterminate' || binding.isReadOnlyMediaAttribute)) {
				this.compiler.hasComplexBindings = true;

				block.builders.hydrate.addLine(
					`if (!(${allInitialStateIsDefined})) #component.root._beforecreate.push(${handler});`
				);
			}
		});

		this.initialUpdate = mungedBindings.map(binding => binding.initialUpdate).filter(Boolean).join('\n');
	}

	addAttributes(block: Block) {
		if (this.attributes.find(attr => attr.type === 'Spread')) {
			this.addSpreadAttributes(block);
			return;
		}

		this.attributes.forEach((attribute: Attribute) => {
			attribute.render(block);
		});
	}

	addSpreadAttributes(block: Block) {
		const levels = block.getUniqueName(`${this.var}_levels`);
		const data = block.getUniqueName(`${this.var}_data`);

		const initialProps = [];
		const updates = [];

		this.attributes
			.filter(attr => attr.type === 'Attribute' || attr.type === 'Spread')
			.forEach(attr => {
				if (attr.type === 'Attribute') {
					const { dynamic, value, dependencies } = mungeAttribute(attr, block);

					const snippet = `{ ${quoteIfNecessary(attr.name)}: ${value} }`;
					initialProps.push(snippet);

					const condition = dependencies && dependencies.map(d => `changed.${d}`).join(' || ');
					updates.push(condition ? `${condition} && ${snippet}` : snippet);
				}

				else {
					block.contextualise(attr.expression); // TODO gah
					const { snippet, dependencies } = attr.metadata;

					initialProps.push(snippet);

					const condition = dependencies && dependencies.map(d => `changed.${d}`).join(' || ');
					updates.push(condition ? `${condition} && ${snippet}` : snippet);
				}
			});

		block.builders.init.addBlock(deindent`
			var ${levels} = [
				${initialProps.join(',\n')}
			];

			var ${data} = {};
			for (var #i = 0; #i < ${levels}.length; #i += 1) {
				${data} = @assign(${data}, ${levels}[#i]);
			}
		`);

		block.builders.hydrate.addLine(
			`@setAttributes(${this.var}, ${data});`
		);

		block.builders.update.addBlock(deindent`
			@setAttributes(${this.var}, @getSpreadUpdate(${levels}, [
				${updates.join(',\n')}
			]));
		`);
	}

	addEventHandlers(block: Block, allUsedContexts) {
		const { compiler } = this;
		let eventHandlerUsesComponent = false;

		this.handlers.forEach(handler => {
			const isCustomEvent = compiler.events.has(handler.name);
			const shouldHoist = !isCustomEvent && this.hasAncestor('EachBlock');

			const context = shouldHoist ? null : this.var;
			const usedContexts: string[] = [];

			if (handler.callee) {
				handler.render(this.compiler, block);

				if (!validCalleeObjects.has(handler.callee.name)) {
					if (shouldHoist) eventHandlerUsesComponent = true; // this feels a bit hacky but it works!
				}

				// handler.expression.arguments.forEach((arg: Node) => {
				// 	const { contexts } = block.contextualise(arg, context, true);

				// 	contexts.forEach(context => {
				// 		if (!~usedContexts.indexOf(context)) usedContexts.push(context);
				// 		allUsedContexts.add(context);
				// 	});
				// });
			}

			const ctx = context || 'this';

			// get a name for the event handler that is globally unique
			// if hoisted, locally unique otherwise
			const handlerName = (shouldHoist ? compiler : block).getUniqueName(
				`${handler.name.replace(/[^a-zA-Z0-9_$]/g, '_')}_handler`
			);

			const component = block.alias('component'); // can't use #component, might be hoisted

			// create the handler body
			const handlerBody = deindent`
				${eventHandlerUsesComponent &&
					`var #component = ${ctx}._svelte.component;`}
				${handler.dependencies.size > 0 && `const ctx = #component.get();`}
				${handler.snippet ?
					handler.snippet :
					`#component.fire("${handler.name}", event);`}
			`;

			if (isCustomEvent) {
				block.addVariable(handlerName);

				block.builders.hydrate.addBlock(deindent`
					${handlerName} = %events-${handler.name}.call(#component, ${this.var}, function(event) {
						${handlerBody}
					});
				`);

				block.builders.destroy.addLine(deindent`
					${handlerName}.destroy();
				`);
			} else {
				const handlerFunction = deindent`
					function ${handlerName}(event) {
						${handlerBody}
					}
				`;

				if (shouldHoist) {
					compiler.blocks.push(handlerFunction);
				} else {
					block.builders.init.addBlock(handlerFunction);
				}

				block.builders.hydrate.addLine(
					`@addListener(${this.var}, "${handler.name}", ${handlerName});`
				);

				block.builders.destroy.addLine(
					`@removeListener(${this.var}, "${handler.name}", ${handlerName});`
				);
			}
		});
		return eventHandlerUsesComponent;
	}

	addRef(block: Block) {
		const ref = `#component.refs.${this.ref}`;

		block.builders.mount.addLine(
			`${ref} = ${this.var};`
		);

		block.builders.destroy.addLine(
			`if (${ref} === ${this.var}) ${ref} = null;`
		);
	}

	addTransitions(
		block: Block
	) {
		const { intro, outro } = this;

		if (!intro && !outro) return;

		if (intro === outro) {
			const name = block.getUniqueName(`${this.var}_transition`);
			const snippet = intro.expression
				? intro.expression.snippet
				: '{}';

			block.addVariable(name);

			const fn = `%transitions-${intro.name}`;

			block.builders.intro.addBlock(deindent`
				#component.root._aftercreate.push(function() {
					if (!${name}) ${name} = @wrapTransition(#component, ${this.var}, ${fn}, ${snippet}, true, null);
					${name}.run(true, function() {
						#component.fire("intro.end", { node: ${this.var} });
					});
				});
			`);

			block.builders.outro.addBlock(deindent`
				${name}.run(false, function() {
					#component.fire("outro.end", { node: ${this.var} });
					if (--#outros === 0) #outrocallback();
					${name} = null;
				});
			`);
		} else {
			const introName = intro && block.getUniqueName(`${this.var}_intro`);
			const outroName = outro && block.getUniqueName(`${this.var}_outro`);

			if (intro) {
				block.addVariable(introName);
				const snippet = intro.expression
					? intro.expression.snippet
					: '{}';

				const fn = `%transitions-${intro.name}`; // TODO add built-in transitions?

				if (outro) {
					block.builders.intro.addBlock(deindent`
						if (${introName}) ${introName}.abort();
						if (${outroName}) ${outroName}.abort();
					`);
				}

				block.builders.intro.addBlock(deindent`
					#component.root._aftercreate.push(function() {
						${introName} = @wrapTransition(#component, ${this.var}, ${fn}, ${snippet}, true, null);
						${introName}.run(true, function() {
							#component.fire("intro.end", { node: ${this.var} });
						});
					});
				`);
			}

			if (outro) {
				block.addVariable(outroName);
				const snippet = outro.expression
					? outro.expression.snippet
					: '{}';

				const fn = `%transitions-${outro.name}`;

				// TODO hide elements that have outro'd (unless they belong to a still-outroing
				// group) prior to their removal from the DOM
				block.builders.outro.addBlock(deindent`
					${outroName} = @wrapTransition(#component, ${this.var}, ${fn}, ${snippet}, false, null);
					${outroName}.run(false, function() {
						#component.fire("outro.end", { node: ${this.var} });
						if (--#outros === 0) #outrocallback();
					});
				`);
			}
		}
	}

	addActions(block: Block) {
		this.attributes.filter((a: Action) => a.type === 'Action').forEach((attribute:Action) => {
			const { expression } = attribute;
			let snippet, dependencies;
			if (expression) {
				this.compiler.addSourcemapLocations(expression);
				block.contextualise(expression);
				snippet = attribute.metadata.snippet;
				dependencies = attribute.metadata.dependencies;
			}

			const name = block.getUniqueName(
				`${attribute.name.replace(/[^a-zA-Z0-9_$]/g, '_')}_action`
			);

			block.addVariable(name);
			const fn = `%actions-${attribute.name}`;

			block.builders.hydrate.addLine(
				`${name} = ${fn}.call(#component, ${this.var}${snippet ? `, ${snippet}` : ''}) || {};`
			);

			if (dependencies && dependencies.length) {
				let conditional = `typeof ${name}.update === 'function' && `;
				const deps = dependencies.map(dependency => `changed.${dependency}`).join(' || ');
				conditional += dependencies.length > 1 ? `(${deps})` : deps;

				block.builders.update.addConditional(
					conditional,
					`${name}.update.call(#component, ${snippet});`
				);
			}

			block.builders.destroy.addLine(
				`if (typeof ${name}.destroy === 'function') ${name}.destroy.call(#component);`
			);
		});
	}

	getStaticAttributeValue(name: string) {
		const attribute = this.attributes.find(
			(attr: Attribute) => attr.type === 'Attribute' && attr.name.toLowerCase() === name
		);

		if (!attribute) return null;

		if (attribute.value === true) return true;
		if (attribute.value.length === 0) return '';

		if (attribute.value.length === 1 && attribute.value[0].type === 'Text') {
			return attribute.value[0].data;
		}

		return null;
	}

	isMediaNode() {
		return this.name === 'audio' || this.name === 'video';
	}

	remount(name: string) {
		const slot = this.attributes.find(attribute => attribute.name === 'slot');
		if (slot) {
			return `@appendNode(${this.var}, ${name}._slotted.${this.getStaticAttributeValue('slot')});`;
		}

		return `@appendNode(${this.var}, ${name}._slotted.default);`;
	}

	addCssClass() {
		const classAttribute = this.attributes.find(a => a.name === 'class');
		if (classAttribute && classAttribute.value !== true) {
			if (classAttribute.value.length === 1 && classAttribute.value[0].type === 'Text') {
				classAttribute.value[0].data += ` ${this.compiler.stylesheet.id}`;
			} else {
				(<Node[]>classAttribute.value).push(
					new Node({ type: 'Text', data: ` ${this.compiler.stylesheet.id}` })
				);
			}
		} else {
			this.attributes.push(
				new Attribute({
					compiler: this.compiler,
					name: 'class',
					value: [new Node({ type: 'Text', data: `${this.compiler.stylesheet.id}` })],
					parent: this,
				})
			);
		}
	}
}

function getRenderStatement(
	compiler: DomGenerator,
	namespace: string,
	name: string
) {
	if (namespace === 'http://www.w3.org/2000/svg') {
		return `@createSvgElement("${name}")`;
	}

	if (namespace) {
		return `document.createElementNS("${namespace}", "${name}")`;
	}

	return `@createElement("${name}")`;
}

function getClaimStatement(
	compiler: DomGenerator,
	namespace: string,
	nodes: string,
	node: Node
) {
	const attributes = node.attributes
		.filter((attr: Node) => attr.type === 'Attribute')
		.map((attr: Node) => `${quoteIfNecessary(attr.name)}: true`)
		.join(', ');

	const name = namespace ? node.name : node.name.toUpperCase();

	return `@claimElement(${nodes}, "${name}", ${attributes
		? `{ ${attributes} }`
		: `{}`}, ${namespace === namespaces.svg ? true : false})`;
}

function stringifyAttributeValue(value: Node | true) {
	if (value === true) return '';
	if (value.length === 0) return `=""`;

	const data = value[0].data;
	return `=${JSON.stringify(data)}`;
}

const events = [
	{
		eventNames: ['input'],
		filter: (node: Element, name: string) =>
			node.name === 'textarea' ||
			node.name === 'input' && !/radio|checkbox/.test(node.getStaticAttributeValue('type'))
	},
	{
		eventNames: ['change'],
		filter: (node: Element, name: string) =>
			node.name === 'select' ||
			node.name === 'input' && /radio|checkbox|range/.test(node.getStaticAttributeValue('type'))
	},

	// media events
	{
		eventNames: ['timeupdate'],
		filter: (node: Element, name: string) =>
			node.isMediaNode() &&
			(name === 'currentTime' || name === 'played')
	},
	{
		eventNames: ['durationchange'],
		filter: (node: Element, name: string) =>
			node.isMediaNode() &&
			name === 'duration'
	},
	{
		eventNames: ['play', 'pause'],
		filter: (node: Element, name: string) =>
			node.isMediaNode() &&
			name === 'paused'
	},
	{
		eventNames: ['progress'],
		filter: (node: Element, name: string) =>
			node.isMediaNode() &&
			name === 'buffered'
	},
	{
		eventNames: ['loadedmetadata'],
		filter: (node: Element, name: string) =>
			node.isMediaNode() &&
			(name === 'buffered' || name === 'seekable')
	},
	{
		eventNames: ['volumechange'],
		filter: (node: Element, name: string) =>
			node.isMediaNode() &&
			name === 'volume'
	}
];