import deindent from '../../utils/deindent';
import Node from './shared/Node';
import ElseBlock from './ElseBlock';
import Block from '../dom/Block';
import createDebuggingComment from '../../utils/createDebuggingComment';
import Expression from './shared/Expression';
import mapChildren from './shared/mapChildren';
import TemplateScope from '../dom/TemplateScope';

export default class EachBlock extends Node {
	type: 'EachBlock';

	block: Block;
	expression: Expression;

	iterations: string;
	index: string;
	context: string;
	key: string;
	scope: TemplateScope;
	destructuredContexts: string[];

	children: Node[];
	else?: ElseBlock;

	constructor(compiler, parent, scope, info) {
		super(compiler, parent, scope, info);

		this.expression = new Expression(compiler, this, scope, info.expression);
		this.context = info.context;
		this.index = info.index;
		this.key = info.key;

		this.scope = scope.child();

		this.scope.add(this.context, this.expression.dependencies);

		if (this.index) {
			// index can only change if this is a keyed each block
			const dependencies = this.key ? this.expression.dependencies : [];
			this.scope.add(this.index, dependencies);
		}

		// TODO more general approach to destructuring
		this.destructuredContexts = info.destructuredContexts || [];
		this.destructuredContexts.forEach(name => {
			this.scope.add(name, this.expression.dependencies);
		});

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

		this.else = info.else
			? new ElseBlock(compiler, this, this.scope, info.else)
			: null;
	}

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

		this.var = block.getUniqueName(`each`);
		this.iterations = block.getUniqueName(`${this.var}_blocks`);
		this.each_context = block.getUniqueName(`${this.var}_context`);

		const { dependencies } = this.expression;
		block.addDependencies(dependencies);

		this.block = block.child({
			comment: createDebuggingComment(this, this.compiler),
			name: this.compiler.getUniqueName('create_each_block'),
			key: this.key,

			indexNames: new Map(block.indexNames),
			listNames: new Map(block.listNames)
		});

		const listName = this.compiler.getUniqueName('each_value');
		const indexName = this.index || this.compiler.getUniqueName(`${this.context}_index`);

		this.block.indexNames.set(this.context, indexName);
		this.block.listNames.set(this.context, listName);

		if (this.index) {
			this.block.getUniqueName(this.index); // this prevents name collisions (#1254)
		}

		this.contextProps = [
			`${listName}: ${listName}`,
			`${this.context}: ${listName}[#i]`,
			`${indexName}: #i`
		];

		if (this.destructuredContexts) {
			for (let i = 0; i < this.destructuredContexts.length; i += 1) {
				this.contextProps.push(`${this.destructuredContexts[i]}: ${listName}[#i][${i}]`);
			}
		}

		this.compiler.blocks.push(this.block);
		this.initChildren(this.block, stripWhitespace, nextSibling);
		block.addDependencies(this.block.dependencies);
		this.block.hasUpdateMethod = this.block.dependencies.size > 0;

		if (this.else) {
			this.else.block = block.child({
				comment: createDebuggingComment(this.else, this.compiler),
				name: this.compiler.getUniqueName(`${this.block.name}_else`),
			});

			this.compiler.blocks.push(this.else.block);
			this.else.initChildren(
				this.else.block,
				stripWhitespace,
				nextSibling
			);
			this.else.block.hasUpdateMethod = this.else.block.dependencies.size > 0;
		}
	}

	build(
		block: Block,
		parentNode: string,
		parentNodes: string
	) {
		if (this.children.length === 0) return;

		const { compiler } = this;

		const each = this.var;

		const create_each_block = this.block.name;
		const each_block_value = this.block.listNames.get(this.context);
		const iterations = this.iterations;

		const needsAnchor = this.next ? !this.next.isDomNode() : !parentNode || !this.parent.isDomNode();
		const anchor = needsAnchor
			? block.getUniqueName(`${each}_anchor`)
			: (this.next && this.next.var) || 'null';

		// hack the sourcemap, so that if data is missing the bug
		// is easy to find
		let c = this.start + 2;
		while (compiler.source[c] !== 'e') c += 1;
		compiler.code.overwrite(c, c + 4, 'length');
		const length = `[✂${c}-${c+4}✂]`;

		const mountOrIntro = this.block.hasIntroMethod ? 'i' : 'm';
		const vars = {
			each,
			create_each_block,
			each_block_value,
			length,
			iterations,
			anchor,
			mountOrIntro,
		};

		const { snippet } = this.expression;

		block.builders.init.addLine(`var ${each_block_value} = ${snippet};`);

		if (this.key) {
			this.buildKeyed(block, parentNode, parentNodes, snippet, vars);
		} else {
			this.buildUnkeyed(block, parentNode, parentNodes, snippet, vars);
		}

		if (needsAnchor) {
			block.addElement(
				anchor,
				`@createComment()`,
				parentNodes && `@createComment()`,
				parentNode
			);
		}

		if (this.else) {
			const each_block_else = compiler.getUniqueName(`${each}_else`);

			block.builders.init.addLine(`var ${each_block_else} = null;`);

			// TODO neaten this up... will end up with an empty line in the block
			block.builders.init.addBlock(deindent`
				if (!${each_block_value}.${length}) {
					${each_block_else} = ${this.else.block.name}(#component, ctx);
					${each_block_else}.c();
				}
			`);

			block.builders.mount.addBlock(deindent`
				if (${each_block_else}) {
					${each_block_else}.${mountOrIntro}(${parentNode || '#target'}, null);
				}
			`);

			const initialMountNode = parentNode || `${anchor}.parentNode`;

			if (this.else.block.hasUpdateMethod) {
				block.builders.update.addBlock(deindent`
					if (!${each_block_value}.${length} && ${each_block_else}) {
						${each_block_else}.p(changed, ctx);
					} else if (!${each_block_value}.${length}) {
						${each_block_else} = ${this.else.block.name}(#component, ctx);
						${each_block_else}.c();
						${each_block_else}.${mountOrIntro}(${initialMountNode}, ${anchor});
					} else if (${each_block_else}) {
						${each_block_else}.u();
						${each_block_else}.d();
						${each_block_else} = null;
					}
				`);
			} else {
				block.builders.update.addBlock(deindent`
					if (${each_block_value}.${length}) {
						if (${each_block_else}) {
							${each_block_else}.u();
							${each_block_else}.d();
							${each_block_else} = null;
						}
					} else if (!${each_block_else}) {
						${each_block_else} = ${this.else.block.name}(#component, ctx);
						${each_block_else}.c();
						${each_block_else}.${mountOrIntro}(${initialMountNode}, ${anchor});
					}
				`);
			}

			block.builders.unmount.addLine(
				`if (${each_block_else}) ${each_block_else}.u()`
			);

			block.builders.destroy.addBlock(deindent`
				if (${each_block_else}) ${each_block_else}.d();
			`);
		}

		this.children.forEach((child: Node) => {
			child.build(this.block, null, 'nodes');
		});

		if (this.else) {
			this.else.children.forEach((child: Node) => {
				child.build(this.else.block, null, 'nodes');
			});
		}
	}

	buildKeyed(
		block: Block,
		parentNode: string,
		parentNodes: string,
		snippet: string,
		{
			each,
			create_each_block,
			each_block_value,
			length,
			anchor,
			mountOrIntro,
		}
	) {
		const key = block.getUniqueName('key');
		const blocks = block.getUniqueName(`${each}_blocks`);
		const lookup = block.getUniqueName(`${each}_lookup`);

		block.addVariable(blocks, '[]');
		block.addVariable(lookup, `@blankObject()`);

		if (this.children[0].isDomNode()) {
			this.block.first = this.children[0].var;
		} else {
			this.block.first = this.block.getUniqueName('first');
			this.block.addElement(
				this.block.first,
				`@createComment()`,
				parentNodes && `@createComment()`,
				null
			);
		}

		block.builders.init.addBlock(deindent`
			for (var #i = 0; #i < ${each_block_value}.${length}; #i += 1) {
				var ${key} = ${each_block_value}[#i].${this.key};
				${blocks}[#i] = ${lookup}[${key}] = ${create_each_block}(#component, ${key}, @assign(@assign({}, ctx), {
					${this.contextProps.join(',\n')}
				}));
			}
		`);

		const initialMountNode = parentNode || '#target';
		const updateMountNode = this.getUpdateMountNode(anchor);
		const anchorNode = parentNode ? 'null' : 'anchor';

		block.builders.create.addBlock(deindent`
			for (#i = 0; #i < ${blocks}.length; #i += 1) ${blocks}[#i].c();
		`);

		if (parentNodes) {
			block.builders.claim.addBlock(deindent`
				for (#i = 0; #i < ${blocks}.length; #i += 1) ${blocks}[#i].l(${parentNodes});
			`);
		}

		block.builders.mount.addBlock(deindent`
			for (#i = 0; #i < ${blocks}.length; #i += 1) ${blocks}[#i].${mountOrIntro}(${initialMountNode}, ${anchorNode});
		`);

		const dynamic = this.block.hasUpdateMethod;

		block.builders.update.addBlock(deindent`
			var ${each_block_value} = ${snippet};

			${blocks} = @updateKeyedEach(${blocks}, #component, changed, "${this.key}", ${dynamic ? '1' : '0'}, ${each_block_value}, ${lookup}, ${updateMountNode}, ${String(this.block.hasOutroMethod)}, ${create_each_block}, "${mountOrIntro}", ${anchor}, function(#i) {
				return @assign(@assign({}, ctx), {
					${this.contextProps.join(',\n')}
				});
			});
		`);

		if (!parentNode) {
			block.builders.unmount.addBlock(deindent`
				for (#i = 0; #i < ${blocks}.length; #i += 1) ${blocks}[#i].u();
			`);
		}

		block.builders.destroy.addBlock(deindent`
			for (#i = 0; #i < ${blocks}.length; #i += 1) ${blocks}[#i].d();
		`);
	}

	buildUnkeyed(
		block: Block,
		parentNode: string,
		parentNodes: string,
		snippet: string,
		{
			create_each_block,
			each_block_value,
			length,
			iterations,
			anchor,
			mountOrIntro,
		}
	) {
		block.builders.init.addBlock(deindent`
			var ${iterations} = [];

			for (var #i = 0; #i < ${each_block_value}.${length}; #i += 1) {
				${iterations}[#i] = ${create_each_block}(#component, @assign(@assign({}, ctx), {
					${this.contextProps.join(',\n')}
				}));
			}
		`);

		const initialMountNode = parentNode || '#target';
		const updateMountNode = this.getUpdateMountNode(anchor);
		const anchorNode = parentNode ? 'null' : 'anchor';

		block.builders.create.addBlock(deindent`
			for (var #i = 0; #i < ${iterations}.length; #i += 1) {
				${iterations}[#i].c();
			}
		`);

		if (parentNodes) {
			block.builders.claim.addBlock(deindent`
				for (var #i = 0; #i < ${iterations}.length; #i += 1) {
					${iterations}[#i].l(${parentNodes});
				}
			`);
		}

		block.builders.mount.addBlock(deindent`
			for (var #i = 0; #i < ${iterations}.length; #i += 1) {
				${iterations}[#i].${mountOrIntro}(${initialMountNode}, ${anchorNode});
			}
		`);

		const allDependencies = new Set(this.block.dependencies);
		const { dependencies } = this.expression;
		dependencies.forEach((dependency: string) => {
			allDependencies.add(dependency);
		});

		// TODO do this for keyed blocks as well
		const condition = Array.from(allDependencies)
			.map(dependency => `changed.${dependency}`)
			.join(' || ');

		if (condition !== '') {
			const forLoopBody = this.block.hasUpdateMethod
				? this.block.hasIntroMethod
					? deindent`
						if (${iterations}[#i]) {
							${iterations}[#i].p(changed, ${this.each_context});
						} else {
							${iterations}[#i] = ${create_each_block}(#component, ${this.each_context});
							${iterations}[#i].c();
						}
						${iterations}[#i].i(${updateMountNode}, ${anchor});
					`
					: deindent`
						if (${iterations}[#i]) {
							${iterations}[#i].p(changed, ${this.each_context});
						} else {
							${iterations}[#i] = ${create_each_block}(#component, ${this.each_context});
							${iterations}[#i].c();
							${iterations}[#i].m(${updateMountNode}, ${anchor});
						}
					`
				: deindent`
					${iterations}[#i] = ${create_each_block}(#component, ${this.each_context});
					${iterations}[#i].c();
					${iterations}[#i].${mountOrIntro}(${updateMountNode}, ${anchor});
				`;

			const start = this.block.hasUpdateMethod ? '0' : `${iterations}.length`;

			const outro = block.getUniqueName('outro');
			const destroy = this.block.hasOutroMethod
				? deindent`
					function ${outro}(i) {
						if (${iterations}[i]) {
							${iterations}[i].o(function() {
								${iterations}[i].u();
								${iterations}[i].d();
								${iterations}[i] = null;
							});
						}
					}

					for (; #i < ${iterations}.length; #i += 1) ${outro}(#i);
				`
				: deindent`
					for (; #i < ${iterations}.length; #i += 1) {
						${iterations}[#i].u();
						${iterations}[#i].d();
					}
					${iterations}.length = ${each_block_value}.${length};
				`;

			block.builders.update.addBlock(deindent`
				var ${each_block_value} = ${snippet};

				if (${condition}) {
					for (var #i = ${start}; #i < ${each_block_value}.${length}; #i += 1) {
						var ${this.each_context} = @assign(@assign({}, ctx), {
							${this.contextProps.join(',\n')}
						});

						${forLoopBody}
					}

					${destroy}
				}
			`);
		}

		block.builders.unmount.addBlock(deindent`
			for (var #i = 0; #i < ${iterations}.length; #i += 1) {
				${iterations}[#i].u();
			}
		`);

		block.builders.destroy.addBlock(`@destroyEach(${iterations});`);
	}

	remount(name: string) {
		// TODO consider keyed blocks
		return `for (var #i = 0; #i < ${this.iterations}.length; #i += 1) ${this.iterations}[#i].m(${name}._slotted.default, null);`;
	}
}