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 { quoteNameIfNecessary, quotePropIfNecessary } from '../../utils/quoteIfNecessary';
import { escape, escapeTemplate, stringify } from '../../utils/stringify';
import Node from './shared/Node';
import Block from '../dom/Block';
import Attribute from './Attribute';
import mapChildren from './shared/mapChildren';
import Binding from './Binding';
import EventHandler from './EventHandler';
import Expression from './shared/Expression';
import { AppendTarget } from '../../interfaces';
import addToSet from '../../utils/addToSet';
import Component from '../Component';
import isValidIdentifier from '../../utils/isValidIdentifier';
import Ref from './Ref';

export default class InlineComponent extends Node {
	type: 'InlineComponent';
	name: string;
	expression: Expression;
	attributes: Attribute[];
	bindings: Binding[];
	handlers: EventHandler[];
	children: Node[];
	ref: Ref;

	constructor(component: Component, parent, scope, info) {
		super(component, parent, scope, info);

		component.hasComponents = true;

		this.name = info.name;

		if (this.name !== 'svelte:self' && this.name !== 'svelte:component') {
			if (!component.components.has(this.name)) {
				component.error(this, {
					code: `missing-component`,
					message: `${this.name} component is not defined`
				});
			}

			component.used.components.add(this.name);
		}

		this.expression = this.name === 'svelte:component'
			? new Expression(component, this, scope, info.expression)
			: null;

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

		info.attributes.forEach(node => {
			switch (node.type) {
				case 'Action':
					component.error(node, {
						code: `invalid-action`,
						message: `Actions can only be applied to DOM elements, not components`
					});

				case 'Attribute':
				case 'Spread':
					this.attributes.push(new Attribute(component, this, scope, node));
					break;

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

				case 'Class':
					component.error(node, {
						code: `invalid-class`,
						message: `Classes can only be applied to DOM elements, not components`
					});

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

				case 'Ref':
					this.ref = new Ref(component, this, scope, node);
					break;

				case 'Transition':
					component.error(node, {
						code: `invalid-transition`,
						message: `Transitions can only be applied to DOM elements, not components`
					});

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

		this.children = mapChildren(component, this, scope, info.children);
	}

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

		if (this.expression) {
			block.addDependencies(this.expression.dependencies);
		}

		this.attributes.forEach(attr => {
			block.addDependencies(attr.dependencies);
		});

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

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

		this.var = block.getUniqueName(
			(
				this.name === 'svelte:self' ? this.component.name :
				this.name === 'svelte:component' ? 'switch_instance' :
				this.name
			).toLowerCase()
		);

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

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

		if (this.component.options.nestedTransitions) {
			block.addOutro();
		}
	}

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

		const name = this.var;

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

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

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

		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 updates: string[] = [];

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

		const attributeObject = usesSpread
			? '{}'
			: stringifyProps(
				this.attributes.map(attr => `${quoteNameIfNecessary(attr.name)}: ${attr.getValue()}`)
			);

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

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

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

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

				const allDependencies = new Set();

				this.attributes.forEach(attr => {
					addToSet(allDependencies, attr.dependencies);
				});

				this.attributes.forEach(attr => {
					const { name, dependencies } = attr;

					const condition = dependencies.size > 0 && (dependencies.size !== allDependencies.size)
						? `(${[...dependencies].map(d => `changed.${d}`).join(' || ')})`
						: null;

					if (attr.isSpread) {
						const value = attr.expression.snippet;
						initialProps.push(value);

						changes.push(condition ? `${condition} && ${value}` : value);
					} else {
						const obj = `{ ${quoteNameIfNecessary(name)}: ${attr.getValue()} }`;
						initialProps.push(obj);

						changes.push(condition ? `${condition} && ${obj}` : obj);
					}
				});

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

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

				const conditions = [...allDependencies].map(dep => `changed.${dep}`).join(' || ');

				updates.push(deindent`
					var ${name_changes} = ${allDependencies.size === 1 ? `${conditions}` : `(${conditions})`} ? @getSpreadUpdate(${levels}, [
						${changes.join(',\n')}
					]) : {};
				`);
			} else {
				this.attributes
					.filter((attribute: Attribute) => attribute.isDynamic)
					.forEach((attribute: Attribute) => {
						if (attribute.dependencies.size > 0) {
							updates.push(deindent`
								if (${[...attribute.dependencies]
									.map(dependency => `changed.${dependency}`)
									.join(' || ')}) ${name_changes}${quotePropIfNecessary(attribute.name)} = ${attribute.getValue()};
							`);
						}
					});
				}
		}

		if (this.bindings.length) {
			component.target.hasComplexBindings = true;

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

			let hasLocalBindings = false;
			let hasStoreBindings = false;

			const builder = new CodeBuilder();

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

				let setFromChild;

				if (binding.isContextual) {
					const computed = isComputed(binding.value.node);
					const tail = binding.value.node.type === 'MemberExpression' ? getTailSnippet(binding.value.node) : '';

					const head = block.bindings.get(key);

					const lhs = binding.value.node.type === 'MemberExpression'
						? binding.value.snippet
						: `${head}${tail} = childState${quotePropIfNecessary(binding.name)}`;

					setFromChild = deindent`
						${lhs} = childState${quotePropIfNecessary(binding.name)};

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

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

								return `${newState}${quotePropIfNecessary(prop)} = ctx${quotePropIfNecessary(name)};`;
							})}
					`;
				}

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

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

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

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

				statements.push(deindent`
					if (${binding.value.snippet} !== void 0) {
						${name_initial_data}${quotePropIfNecessary(binding.name)} = ${binding.value.snippet};
						${name_updating}${quotePropIfNecessary(binding.name)} = true;
					}`
				);

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

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

			block.maintainContext = true; // TODO put this somewhere more logical

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

			// TODO use component.on('state', ...) instead of _bind
			componentInitProperties.push(deindent`
				_bind(changed, childState) {
					var ${initialisers};
					${builder}
					${hasStoreBindings && `#component.store.set(newStoreState);`}
					${hasLocalBindings && `#component._set(newState);`}
					${name_updating} = {};
				}
			`);

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

		this.handlers.forEach(handler => {
			handler.var = block.getUniqueName(`${this.var}_${handler.name}`); // TODO this is hacky
			handler.render(component, block, false); // TODO hoist when possible
			if (handler.usesContext) block.maintainContext = true; // TODO is there a better place to put this?
		});

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

			const { snippet } = this.expression;

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

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

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

					${beforecreate}
				}

				${this.handlers.map(handler => deindent`
					function ${handler.var}(event) {
						${handler.snippet || `#component.fire("${handler.name}", event);`}
					}

					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'});
					${this.ref && `#component.refs.${this.ref.name} = ${name};`}
				}
			`);

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

			if (updates.length) {
				block.builders.update.addBlock(deindent`
					${updates}
				`);
			}

			block.builders.update.addBlock(deindent`
				if (${switch_value} !== (${switch_value} = ${snippet})) {
					if (${name}) {
						${this.component.options.nestedTransitions
						? deindent`
						@groupOutros();
						const old_component = ${name};
						old_component._fragment.o(() => {
							old_component.destroy();
						});`
						: `${name}.destroy();`}
					}

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

						${this.bindings.length > 0 && deindent`
						#component.root._beforecreate.push(() => {
							const changed = {};
							${this.bindings.map(binding => deindent`
							if (${binding.value.snippet} === void 0) changed.${binding.name} = 1;`)}
							${name}._bind(changed, ${name}.get());
						});`}
						${name}._fragment.c();

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

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

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

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

			block.builders.destroy.addLine(`if (${name}) ${name}.destroy(${parentNode ? '' : 'detach'});`);
		} else {
			const expression = this.name === 'svelte:self'
				? component.name
				: `%components-${this.name}`;

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

				${beforecreate}

				${this.handlers.map(handler => deindent`
					${name}.on("${handler.name}", function(event) {
						${handler.snippet || `#component.fire("${handler.name}", event);`}
					});
				`)}

				${this.ref && `#component.refs.${this.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`
					${updates}
					${name}._set(${name_changes});
					${this.bindings.length && `${name_updating} = {};`}
				`);
			}

			block.builders.destroy.addLine(deindent`
				${name}.destroy(${parentNode ? '' : 'detach'});
				${this.ref && `if (#component.refs.${this.ref.name} === ${name}) #component.refs.${this.ref.name} = null;`}
			`);
		}

		if (this.component.options.nestedTransitions) {
			block.builders.outro.addLine(
				`if (${name}) ${name}._fragment.o(#outrocallback);`
			);
		}
	}

	remount(name: string) {
		return `${this.var}._mount(${name}._slotted.default, null);`;
	}

	ssr() {
		function stringifyAttribute(chunk: Node) {
			if (chunk.type === 'Text') {
				return escapeTemplate(escape(chunk.data));
			}

			return '${@escape( ' + chunk.snippet + ')}';
		}

		const bindingProps = this.bindings.map(binding => {
			const { name } = getObject(binding.value.node);
			const tail = binding.value.node.type === 'MemberExpression'
				? getTailSnippet(binding.value.node)
				: '';

			return `${quoteNameIfNecessary(binding.name)}: ctx${quotePropIfNecessary(name)}${tail}`;
		});

		function getAttributeValue(attribute) {
			if (attribute.isTrue) return `true`;
			if (attribute.chunks.length === 0) return `''`;

			if (attribute.chunks.length === 1) {
				const chunk = attribute.chunks[0];
				if (chunk.type === 'Text') {
					return stringify(chunk.data);
				}

				return chunk.snippet;
			}

			return '`' + attribute.chunks.map(stringifyAttribute).join('') + '`';
		}

		const usesSpread = this.attributes.find(attr => attr.isSpread);

		const props = usesSpread
			? `Object.assign(${
				this.attributes
					.map(attribute => {
						if (attribute.isSpread) {
							return attribute.expression.snippet;
						} else {
							return `{ ${quoteNameIfNecessary(attribute.name)}: ${getAttributeValue(attribute)} }`;
						}
					})
					.concat(bindingProps.map(p => `{ ${p} }`))
					.join(', ')
			})`
			: `{ ${this.attributes
				.map(attribute => `${quoteNameIfNecessary(attribute.name)}: ${getAttributeValue(attribute)}`)
				.concat(bindingProps)
				.join(', ')} }`;

		const expression = (
			this.name === 'svelte:self'
				? this.component.name
				: this.name === 'svelte:component'
					? `((${this.expression.snippet}) || @missingComponent)`
					: `%components-${this.name}`
		);

		this.bindings.forEach(binding => {
			const conditions = [];

			let node = this;
			while (node = node.parent) {
				if (node.type === 'IfBlock') {
					// TODO handle contextual bindings...
					conditions.push(`(${node.expression.snippet})`);
				}
			}

			conditions.push(
				`!('${binding.name}' in ctx)`,
				`${expression}.data`
			);

			const { name } = getObject(binding.value.node);

			this.component.target.bindings.push(deindent`
				if (${conditions.reverse().join('&&')}) {
					tmp = ${expression}.data();
					if ('${name}' in tmp) {
						ctx${quotePropIfNecessary(binding.name)} = tmp.${name};
						settled = false;
					}
				}
			`);
		});

		let open = `\${@validateSsrComponent(${expression}, '${this.name}')._render(__result, ${props}`;

		const options = [];
		options.push(`store: options.store`);

		if (this.children.length) {
			const appendTarget: AppendTarget = {
				slots: { default: '' },
				slotStack: ['default']
			};

			this.component.target.appendTargets.push(appendTarget);

			this.children.forEach((child: Node) => {
				child.ssr();
			});

			const slotted = Object.keys(appendTarget.slots)
				.map(name => `${quoteNameIfNecessary(name)}: () => \`${appendTarget.slots[name]}\``)
				.join(', ');

			options.push(`slotted: { ${slotted} }`);

			this.component.target.appendTargets.pop();
		}

		if (options.length) {
			open += `, { ${options.join(', ')} }`;
		}

		this.component.target.append(open);
		this.component.target.append(')}');
	}
}

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

	return false;
}