import deindent from '../../utils/deindent';
import Node from './shared/Node';
import { DomGenerator } from '../dom/index';
import Block from '../dom/Block';
import PendingBlock from './PendingBlock';
import ThenBlock from './ThenBlock';
import CatchBlock from './CatchBlock';
import createDebuggingComment from '../../utils/createDebuggingComment';

export default class AwaitBlock extends Node {
	value: string;
	error: string;
	expression: Node;

	pending: PendingBlock;
	then: ThenBlock;
	catch: CatchBlock;

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

		this.var = block.getUniqueName('await_block');
		block.addDependencies(this.metadata.dependencies);

		let dynamic = false;

		[
			['pending', null],
			['then', this.value],
			['catch', this.error]
		].forEach(([status, arg]) => {
			const child = this[status];

			const context = block.getUniqueName(arg || '_'); // TODO can we remove the extra param from pending blocks?
			const contexts = new Map(block.contexts);
			contexts.set(arg, context);

			const contextTypes = new Map(block.contextTypes);
			contextTypes.set(arg, status);

			child.block = block.child({
				comment: createDebuggingComment(child, this.generator),
				name: this.generator.getUniqueName(`create_${status}_block`),
				params: block.params.concat(context),
				context,
				contexts,
				contextTypes
			});

			child.initChildren(child.block, stripWhitespace, nextSibling);
			this.generator.blocks.push(child.block);

			if (child.block.dependencies.size > 0) {
				dynamic = true;
				block.addDependencies(child.block.dependencies);
			}
		});

		this.pending.block.hasUpdateMethod = dynamic;
		this.then.block.hasUpdateMethod = dynamic;
		this.catch.block.hasUpdateMethod = dynamic;
	}

	build(
		block: Block,
		parentNode: string,
		parentNodes: string
	) {
		const name = this.var;

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

		const params = block.params.join(', ');

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

		const promise = block.getUniqueName(`promise`);
		const resolved = block.getUniqueName(`resolved`);
		const await_block = block.getUniqueName(`await_block`);
		const await_block_type = block.getUniqueName(`await_block_type`);
		const token = block.getUniqueName(`token`);
		const await_token = block.getUniqueName(`await_token`);
		const handle_promise = block.getUniqueName(`handle_promise`);
		const replace_await_block = block.getUniqueName(`replace_await_block`);
		const old_block = block.getUniqueName(`old_block`);
		const value = block.getUniqueName(`value`);
		const error = block.getUniqueName(`error`);
		const create_pending_block = this.pending.block.name;
		const create_then_block = this.then.block.name;
		const create_catch_block = this.catch.block.name;

		block.addVariable(await_block);
		block.addVariable(await_block_type);
		block.addVariable(await_token);
		block.addVariable(promise);
		block.addVariable(resolved);

		// the `#component.root.set({})` below is just a cheap way to flush
		// any oncreate handlers. We could have a dedicated `flush()` method
		// but it's probably not worth it

		block.builders.init.addBlock(deindent`
			function ${replace_await_block}(${token}, type, ${value}, ${params}) {
				if (${token} !== ${await_token}) return;

				var ${old_block} = ${await_block};
				${await_block} = (${await_block_type} = type)(${params}, ${resolved} = ${value}, #component);

				if (${old_block}) {
					${old_block}.u();
					${old_block}.d();
					${await_block}.c();
					${await_block}.m(${updateMountNode}, ${anchor});

					#component.root.set({});
				}
			}

			function ${handle_promise}(${promise}, ${params}) {
				var ${token} = ${await_token} = {};

				if (@isPromise(${promise})) {
					${promise}.then(function(${value}) {
						${replace_await_block}(${token}, ${create_then_block}, ${value}, ${params});
					}, function (${error}) {
						${replace_await_block}(${token}, ${create_catch_block}, ${error}, ${params});
					});

					// if we previously had a then/catch block, destroy it
					if (${await_block_type} !== ${create_pending_block}) {
						${replace_await_block}(${token}, ${create_pending_block}, null, ${params});
						return true;
					}
				} else {
					${resolved} = ${promise};
					if (${await_block_type} !== ${create_then_block}) {
						${replace_await_block}(${token}, ${create_then_block}, ${resolved}, ${params});
						return true;
					}
				}
			}

			${handle_promise}(${promise} = ${snippet}, ${params});
		`);

		block.builders.create.addBlock(deindent`
			${await_block}.c();
		`);

		if (parentNodes) {
			block.builders.claim.addBlock(deindent`
				${await_block}.l(${parentNodes});
			`);
		}

		const initialMountNode = parentNode || '#target';
		const anchorNode = parentNode ? 'null' : 'anchor';

		block.builders.mount.addBlock(deindent`
			${await_block}.m(${initialMountNode}, ${anchorNode});
		`);

		const conditions = [];
		if (this.metadata.dependencies) {
			conditions.push(
				`(${this.metadata.dependencies.map(dep => `'${dep}' in changed`).join(' || ')})`
			);
		}

		conditions.push(
			`${promise} !== (${promise} = ${snippet})`,
			`${handle_promise}(${promise}, ${params})`
		);

		if (this.pending.block.hasUpdateMethod) {
			block.builders.update.addBlock(deindent`
				if (${conditions.join(' && ')}) {
					// nothing
				} else {
					${await_block}.p(changed, ${params}, ${resolved});
				}
			`);
		} else {
			block.builders.update.addBlock(deindent`
				if (${conditions.join(' && ')}) {
					${await_block}.c();
					${await_block}.m(${anchor}.parentNode, ${anchor});
				}
			`);
		}

		block.builders.unmount.addBlock(deindent`
			${await_block}.u();
		`);

		block.builders.destroy.addBlock(deindent`
			${await_token} = null;
			${await_block}.d();
		`);

		[this.pending, this.then, this.catch].forEach(status => {
			status.children.forEach(child => {
				child.build(status.block, null,'nodes');
			});
		});
	}
}