import deindent from '../../utils/deindent';
import stringifyProps from '../../utils/stringifyProps';
import CodeBuilder from '../../utils/CodeBuilder';
import getTailSnippet from '../../utils/getTailSnippet';
import getObject from '../../utils/getObject';
import quoteIfNecessary from '../../utils/quoteIfNecessary';
import mungeAttribute from './shared/mungeAttribute';
import Node from './shared/Node';
import Block from '../dom/Block';
import Attribute from './Attribute';

export default class Component extends Node {
	type: 'Component';
	name: string;
	attributes: Attribute[];
	children: Node[];

	init(
		block: Block,
		stripWhitespace: boolean,
		nextSibling: Node
	) {
		this.cannotUseInnerHTML();

		this.attributes.forEach((attribute: Node) => {
			if (attribute.type === 'Attribute' && attribute.value !== true) {
				attribute.value.forEach((chunk: Node) => {
					if (chunk.type !== 'Text') {
						const dependencies = chunk.metadata.dependencies;
						block.addDependencies(dependencies);
					}
				});
			} else {
				if (attribute.type === 'EventHandler' && attribute.expression) {
					attribute.expression.arguments.forEach((arg: Node) => {
						block.addDependencies(arg.metadata.dependencies);
					});
				} else if (attribute.type === 'Binding') {
					block.addDependencies(attribute.metadata.dependencies);
				}
			}
		});

		this.var = block.getUniqueName(
			(
				this.name === ':Self' ? this.generator.name :
				this.name === ':Component' ? 'switch_instance' :
				this.name
			).toLowerCase()
		);

		if (this.children.length) {
			this._slots = new Set(['default']);

			this.children.forEach(child => {
				child.init(block, stripWhitespace, nextSibling);
			});
		}
	}

	build(
		block: Block,
		parentNode: string,
		parentNodes: string
	) {
		const { generator } = this;
		generator.hasComponents = true;

		const name = this.var;

		const componentInitProperties = [`root: #component.root`];

		if (this.children.length > 0) {
			const slots = Array.from(this._slots).map(name => `${quoteIfNecessary(name, generator.legacy)}: @createFragment()`);
			componentInitProperties.push(`slots: { ${slots.join(', ')} }`);

			this.children.forEach((child: Node) => {
				child.build(block, `${this.var}._slotted${generator.legacy ? `["default"]` : `.default`}`, 'nodes');
			});
		}

		const allContexts = new Set();
		const statements: string[] = [];

		const name_initial_data = block.getUniqueName(`${name}_initial_data`);
		const name_changes = block.getUniqueName(`${name}_changes`);
		let name_updating: string;
		let beforecreate: string = null;

		const attributes = this.attributes
			.filter(a => a.type === 'Attribute' || a.type === 'Spread')
			.map(a => mungeAttribute(a, block));

		const bindings = this.attributes
			.filter(a => a.type === 'Binding')
			.map(a => mungeBinding(a, block));

		const eventHandlers = this.attributes
			.filter((a: Node) => a.type === 'EventHandler')
			.map(a => mungeEventHandler(generator, this, a, block, allContexts));

		const ref = this.attributes.find((a: Node) => a.type === 'Ref');
		if (ref) generator.usesRefs = true;

		const updates: string[] = [];

		const usesSpread = !!attributes.find(a => a.spread);

		const attributeObject = usesSpread
			? '{}'
			: stringifyProps(
				attributes.map((attribute: Attribute) => `${attribute.name}: ${attribute.value}`)
			);

		if (attributes.length || bindings.length) {
			componentInitProperties.push(`data: ${name_initial_data}`);
		}

		if ((!usesSpread && attributes.filter(a => a.dynamic).length) || bindings.length) {
			updates.push(`var ${name_changes} = {};`);
		}

		if (attributes.length) {
			if (usesSpread) {
				const levels = block.getUniqueName(`${this.var}_spread_levels`);

				const initialProps = [];
				const changes = [];

				attributes
					.forEach(munged => {
						const { spread, name, dynamic, value, dependencies } = munged;

						if (spread) {
							initialProps.push(value);

							const condition = dependencies && dependencies.map(d => `changed.${d}`).join(' || ');
							changes.push(condition ? `${condition} && ${value}` : value);
						} else {
							const obj = `{ ${quoteIfNecessary(name, this.generator.legacy)}: ${value} }`;
							initialProps.push(obj);

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

				statements.push(deindent`
					var ${levels} = [
						${initialProps.join(',\n')}
					];

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

				updates.push(deindent`
					var ${name_changes} = @getSpreadUpdate(${levels}, [
						${changes.join(',\n')}
					]);
				`);
			} else {
				attributes
					.filter((attribute: Attribute) => attribute.dynamic)
					.forEach((attribute: Attribute) => {
						if (attribute.dependencies.length) {
							updates.push(deindent`
								if (${attribute.dependencies
									.map(dependency => `changed.${dependency}`)
									.join(' || ')}) ${name_changes}.${attribute.name} = ${attribute.value};
							`);
						}

						else {
							// TODO this is an odd situation to encounter – I *think* it should only happen with
							// each block indices, in which case it may be possible to optimise this
							updates.push(`${name_changes}.${attribute.name} = ${attribute.value};`);
						}
					});
				}
		}

		if (bindings.length) {
			generator.hasComplexBindings = true;

			name_updating = block.alias(`${name}_updating`);
			block.addVariable(name_updating, '{}');

			let hasLocalBindings = false;
			let hasStoreBindings = false;

			const builder = new CodeBuilder();

			bindings.forEach((binding: Binding) => {
				let { name: key } = getObject(binding.value);

				binding.contexts.forEach(context => {
					allContexts.add(context);
				});

				let setFromChild;

				if (block.contexts.has(key)) {
					const computed = isComputed(binding.value);
					const tail = binding.value.type === 'MemberExpression' ? getTailSnippet(binding.value) : '';

					const list = block.listNames.get(key);
					const index = block.indexNames.get(key);

					setFromChild = deindent`
						${list}[${index}]${tail} = childState.${binding.name};

						${binding.dependencies
							.map((name: string) => {
								const isStoreProp = generator.options.store && name[0] === '$';
								const prop = isStoreProp ? name.slice(1) : name;
								const newState = isStoreProp ? 'newStoreState' : 'newState';

								if (isStoreProp) hasStoreBindings = true;
								else hasLocalBindings = true;

								return `${newState}.${prop} = state.${name};`;
							})}
					`;
				}

				else {
					const isStoreProp = generator.options.store && key[0] === '$';
					const prop = isStoreProp ? key.slice(1) : key;
					const newState = isStoreProp ? 'newStoreState' : 'newState';

					if (isStoreProp) hasStoreBindings = true;
					else hasLocalBindings = true;

					if (binding.value.type === 'MemberExpression') {
						setFromChild = deindent`
							${binding.snippet} = childState.${binding.name};
							${newState}.${prop} = state.${key};
						`;
					}

					else {
						setFromChild = `${newState}.${prop} = childState.${binding.name};`;
					}
				}

				statements.push(deindent`
					if (${binding.prop} in ${binding.obj}) {
						${name_initial_data}.${binding.name} = ${binding.snippet};
						${name_updating}.${binding.name} = true;
					}`
				);

				builder.addConditional(
					`!${name_updating}.${binding.name} && changed.${binding.name}`,
					setFromChild
				);

				updates.push(deindent`
					if (!${name_updating}.${binding.name} && ${binding.dependencies.map((dependency: string) => `changed.${dependency}`).join(' || ')}) {
						${name_changes}.${binding.name} = ${binding.snippet};
						${name_updating}.${binding.name} = true;
					}
				`);
			});

			componentInitProperties.push(`data: ${name_initial_data}`);

			const initialisers = [
				'state = #component.get()',
				hasLocalBindings && 'newState = {}',
				hasStoreBindings && 'newStoreState = {}',
			].filter(Boolean).join(', ');

			componentInitProperties.push(deindent`
				_bind: function(changed, childState) {
					var ${initialisers};
					${builder}
					${hasStoreBindings && `#component.store.set(newStoreState);`}
					${hasLocalBindings && `#component._set(newState);`}
					${name_updating} = {};
				}
			`);

			beforecreate = deindent`
				#component.root._beforecreate.push(function() {
					${name}._bind({ ${bindings.map(b => `${b.name}: 1`).join(', ')} }, ${name}.get());
				});
			`;
		}

		if (this.name === ':Component') {
			const switch_value = block.getUniqueName('switch_value');
			const switch_props = block.getUniqueName('switch_props');

			block.contextualise(this.expression);
			const { dependencies, snippet } = this.metadata;

			const anchor = this.getOrCreateAnchor(block, parentNode, parentNodes);

			block.builders.init.addBlock(deindent`
				var ${switch_value} = ${snippet};

				function ${switch_props}(state) {
					${(attributes.length || bindings.length) && deindent`
					var ${name_initial_data} = ${attributeObject};`}
					${statements}
					return {
						${componentInitProperties.join(',\n')}
					};
				}

				if (${switch_value}) {
					var ${name} = new ${switch_value}(${switch_props}(state));

					${beforecreate}
				}

				${eventHandlers.map(handler => deindent`
					function ${handler.var}(event) {
						${handler.body}
					}

					if (${name}) ${name}.on("${handler.name}", ${handler.var});
				`)}
			`);

			block.builders.create.addLine(
				`if (${name}) ${name}._fragment.c();`
			);

			if (parentNodes) {
				block.builders.claim.addLine(
					`if (${name}) ${name}._fragment.l(${parentNodes});`
				);
			}

			block.builders.mount.addBlock(deindent`
				if (${name}) {
					${name}._mount(${parentNode || '#target'}, ${parentNode ? 'null' : 'anchor'});
					${ref && `#component.refs.${ref.name} = ${name};`}
				}
			`);

			const updateMountNode = this.getUpdateMountNode(anchor);

			block.builders.update.addBlock(deindent`
				if (${switch_value} !== (${switch_value} = ${snippet})) {
					if (${name}) ${name}.destroy();

					if (${switch_value}) {
						${name} = new ${switch_value}(${switch_props}(state));
						${name}._fragment.c();

						${this.children.map(child => child.remount(name))}
						${name}._mount(${updateMountNode}, ${anchor});

						${eventHandlers.map(handler => deindent`
							${name}.on("${handler.name}", ${handler.var});
						`)}

						${ref && `#component.refs.${ref.name} = ${name};`}
					}

					${ref && deindent`
						else if (#component.refs.${ref.name} === ${name}) {
							#component.refs.${ref.name} = null;
						}`}
				}
			`);

			if (updates.length) {
				block.builders.update.addBlock(deindent`
					else {
						${updates}
						${name}._set(${name_changes});
						${bindings.length && `${name_updating} = {};`}
					}
				`);
			}

			if (!parentNode) block.builders.unmount.addLine(`if (${name}) ${name}._unmount();`);

			block.builders.destroy.addLine(`if (${name}) ${name}.destroy(false);`);
		} else {
			const expression = this.name === ':Self'
				? generator.name
				: `%components-${this.name}`;

			block.builders.init.addBlock(deindent`
				${(attributes.length || bindings.length) && deindent`
				var ${name_initial_data} = ${attributeObject};`}
				${statements}
				var ${name} = new ${expression}({
					${componentInitProperties.join(',\n')}
				});

				${beforecreate}

				${eventHandlers.map(handler => deindent`
					${name}.on("${handler.name}", function(event) {
						${handler.body}
					});
				`)}

				${ref && `#component.refs.${ref.name} = ${name};`}
			`);

			block.builders.create.addLine(`${name}._fragment.c();`);

			if (parentNodes) {
				block.builders.claim.addLine(
					`${name}._fragment.l(${parentNodes});`
				);
			}

			block.builders.mount.addLine(
				`${name}._mount(${parentNode || '#target'}, ${parentNode ? 'null' : 'anchor'});`
			);

			if (updates.length) {
				block.builders.update.addBlock(deindent`
					var ${name_changes} = {};
					${updates}
					${name}._set(${name_changes});
					${bindings.length && `${name_updating} = {};`}
				`);
			}

			if (!parentNode) block.builders.unmount.addLine(`${name}._unmount();`);

			block.builders.destroy.addLine(deindent`
				${name}.destroy(false);
				${ref && `if (#component.refs.${ref.name} === ${name}) #component.refs.${ref.name} = null;`}
			`);
		}
	}

	remount(name: string) {
		return `${this.var}._mount(${name}._slotted${this.generator.legacy ? `["default"]` : `.default`}, null);`;
	}
}

function mungeBinding(binding: Node, block: Block): Binding {
	const { name } = getObject(binding.value);
	const { contexts } = block.contextualise(binding.value);
	const { dependencies, snippet } = binding.metadata;

	const contextual = block.contexts.has(name);

	let obj;
	let prop;

	if (contextual) {
		obj = `state.${block.listNames.get(name)}`;
		prop = `${block.indexNames.get(name)}`;
	} else if (binding.value.type === 'MemberExpression') {
		prop = `[✂${binding.value.property.start}-${binding.value.property.end}✂]`;
		if (!binding.value.computed) prop = `'${prop}'`;
		obj = `[✂${binding.value.object.start}-${binding.value.object.end}✂]`;
	} else {
		obj = 'state';
		prop = `'${name}'`;
	}

	return {
		name: binding.name,
		value: binding.value,
		contexts,
		snippet,
		obj,
		prop,
		dependencies
	};
}

function mungeEventHandler(generator: DomGenerator, node: Node, handler: Node, block: Block, allContexts: Set<string>) {
	let body;

	if (handler.expression) {
		generator.addSourcemapLocations(handler.expression);
		generator.code.prependRight(
			handler.expression.start,
			`${block.alias('component')}.`
		);

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

			contexts.forEach(context => {
				allContexts.add(context);
			});
		});

		body = deindent`
			[✂${handler.expression.start}-${handler.expression.end}✂];
		`;
	} else {
		body = deindent`
			${block.alias('component')}.fire('${handler.name}', event);
		`;
	}

	return {
		name: handler.name,
		var: block.getUniqueName(`${node.var}_${handler.name}`),
		body
	};
}

function isComputed(node: Node) {
	while (node.type === 'MemberExpression') {
		if (node.computed) return true;
		node = node.object;
	}

	return false;
}