import Wrapper from './shared/Wrapper.js';
import create_debugging_comment from './shared/create_debugging_comment.js';
import FragmentWrapper from './Fragment.js';
import { b, x } from 'code-red';
import get_object from '../../utils/get_object.js';
import { add_const_tags, add_const_tags_context } from './shared/add_const_tags.js';
import Expression from '../../nodes/shared/Expression.js';

/** @extends Wrapper<import('../../nodes/ElseBlock.js').default> */
export class ElseBlockWrapper extends Wrapper {
	/** @type {import('../Block.js').default} */
	block;

	/** @type {import('./Fragment.js').default} */
	fragment;

	/** @type {boolean} */
	is_dynamic;

	var = null;

	/**
	 * @param {import('../Renderer.js').default} renderer
	 * @param {import('../Block.js').default} block
	 * @param {import('./shared/Wrapper.js').default} parent
	 * @param {import('../../nodes/ElseBlock.js').default} node
	 * @param {boolean} strip_whitespace
	 * @param {import('./shared/Wrapper.js').default} next_sibling
	 */
	constructor(renderer, block, parent, node, strip_whitespace, next_sibling) {
		super(renderer, block, parent, node);
		add_const_tags_context(renderer, this.node.const_tags);
		this.block = block.child({
			comment: create_debugging_comment(node, this.renderer.component),
			name: this.renderer.component.get_unique_name('create_else_block'),
			type: 'else'
		});
		this.fragment = new FragmentWrapper(
			renderer,
			this.block,
			this.node.children,
			parent,
			strip_whitespace,
			next_sibling
		);
		this.is_dynamic = this.block.dependencies.size > 0;
	}
}

/** @extends Wrapper<import('../../nodes/EachBlock.js').default> */
export default class EachBlockWrapper extends Wrapper {
	/** @type {import('../Block.js').default} */
	block;

	/** @type {import('./Fragment.js').default} */
	fragment;

	/** @type {ElseBlockWrapper} */
	else;
	/**
	 * @type {{
	 * 		create_each_block: import('estree').Identifier;
	 * 		each_block_value: import('estree').Identifier;
	 * 		get_each_context: import('estree').Identifier;
	 * 		iterations: import('estree').Identifier;
	 * 		fixed_length: number;
	 * 		data_length: import('estree').Node | number;
	 * 		view_length: import('estree').Node | number;
	 * 	}}
	 */
	vars;

	/** @type {Array<import('estree').Node | import('estree').Node[]>} */
	context_props;

	/** @type {import('estree').Identifier} */
	index_name;

	/** @type {Array<import('estree').Node | import('estree').Node[]>} */
	updates = [];

	/** @type {Set<string>} */
	dependencies;

	/** @type {import('estree').Identifier} */
	var = { type: 'Identifier', name: 'each' };

	/**
	 * @param {import('../Renderer.js').default} renderer
	 * @param {import('../Block.js').default} block
	 * @param {import('./shared/Wrapper.js').default} parent
	 * @param {import('../../nodes/EachBlock.js').default} node
	 * @param {boolean} strip_whitespace
	 * @param {import('./shared/Wrapper.js').default} next_sibling
	 */
	constructor(renderer, block, parent, node, strip_whitespace, next_sibling) {
		super(renderer, block, parent, node);
		const { dependencies } = node.expression;
		block.add_dependencies(dependencies);
		this.node.contexts.forEach((context) => {
			if (context.type !== 'DestructuredVariable') return;
			renderer.add_to_context(context.key.name, true);
		});
		add_const_tags_context(renderer, this.node.const_tags);
		this.block = block.child({
			comment: create_debugging_comment(this.node, this.renderer.component),
			name: renderer.component.get_unique_name('create_each_block'),
			type: 'each',
			// @ts-ignore todo: probably error
			key: /** @type {string} */ (node.key),
			bindings: new Map(block.bindings)
		});
		// TODO this seems messy
		this.block.has_animation = this.node.has_animation;
		this.index_name = this.node.index
			? { type: 'Identifier', name: this.node.index }
			: renderer.component.get_unique_name(`${this.node.context}_index`);
		const fixed_length =
			node.expression.node.type === 'ArrayExpression' &&
			node.expression.node.elements.every((element) => element.type !== 'SpreadElement')
				? node.expression.node.elements.length
				: null;
		// hack the sourcemap, so that if data is missing the bug
		// is easy to find
		let c = this.node.start + 2;
		while (renderer.component.source[c] !== 'e') c += 1;
		const start = renderer.component.locate(c);
		const end = { line: start.line, column: start.column + 4 };
		const length = {
			type: 'Identifier',
			name: 'length',
			loc: { start, end }
		};
		const each_block_value = renderer.component.get_unique_name(`${this.var.name}_value`);
		const iterations = block.get_unique_name(`${this.var.name}_blocks`);
		renderer.add_to_context(each_block_value.name, true);
		renderer.add_to_context(this.index_name.name, true);
		this.vars = {
			create_each_block: this.block.name,
			each_block_value,
			get_each_context: renderer.component.get_unique_name(`get_${this.var.name}_context`),
			iterations,
			// optimisation for array literal
			fixed_length,
			data_length: fixed_length === null ? x`${each_block_value}.${length}` : fixed_length,
			view_length: fixed_length === null ? x`${iterations}.length` : fixed_length
		};
		const object = get_object(node.expression.node);
		const store =
			object.type === 'Identifier' && object.name[0] === '$' ? object.name.slice(1) : null;
		node.contexts.forEach((prop) => {
			if (prop.type !== 'DestructuredVariable') return;
			this.block.bindings.set(prop.key.name, {
				object: this.vars.each_block_value,
				property: this.index_name,
				modifier: prop.modifier,
				snippet: prop.modifier(
					/** @type {import('estree').Node} */ (
						x`${this.vars.each_block_value}[${this.index_name}]`
					)
				),
				store
			});
		});
		if (this.node.index) {
			this.block.get_unique_name(this.node.index); // this prevents name collisions (#1254)
		}
		renderer.blocks.push(this.block);
		this.fragment = new FragmentWrapper(
			renderer,
			this.block,
			node.children,
			this,
			strip_whitespace,
			next_sibling
		);
		if (this.node.else) {
			this.else = new ElseBlockWrapper(
				renderer,
				block,
				this,
				this.node.else,
				strip_whitespace,
				next_sibling
			);
			renderer.blocks.push(this.else.block);
			if (this.else.is_dynamic) {
				this.block.add_dependencies(this.else.block.dependencies);
			}
		}
		block.add_dependencies(this.block.dependencies);
		if (this.block.has_outros || (this.else && this.else.block.has_outros)) {
			block.add_outro();
		}
	}

	/**
	 * @param {import('../Block.js').default} block
	 * @param {import('estree').Identifier} parent_node
	 * @param {import('estree').Identifier} parent_nodes
	 */
	render(block, parent_node, parent_nodes) {
		if (this.fragment.nodes.length === 0) return;
		const { renderer } = this;
		const { component } = renderer;
		const needs_anchor = this.next
			? !this.next.is_dom_node()
			: !parent_node || !this.parent.is_dom_node();
		const snippet = x`@ensure_array_like(${this.node.expression.manipulate(block)})`;
		block.chunks.init.push(b`let ${this.vars.each_block_value} = ${snippet};`);

		/** @type {import('estree').Identifier} */
		const initial_anchor_node = {
			type: 'Identifier',
			name: parent_node ? 'null' : '#anchor'
		};

		/** @type {import('estree').Identifier} */
		const initial_mount_node = parent_node || { type: 'Identifier', name: '#target' };
		const update_anchor_node = needs_anchor
			? block.get_unique_name(`${this.var.name}_anchor`)
			: (this.next && this.next.var) || { type: 'Identifier', name: 'null' };

		/** @type {import('estree').Identifier} */
		const update_mount_node = this.get_update_mount_node(
			/** @type {import('estree').Identifier} */ (update_anchor_node)
		);
		const args = {
			block,
			parent_node,
			parent_nodes,
			snippet,
			initial_anchor_node,
			initial_mount_node,
			update_anchor_node,
			update_mount_node
		};
		const all_dependencies = new Set(this.block.dependencies); // TODO should be dynamic deps only
		this.node.expression.dynamic_dependencies().forEach((dependency) => {
			all_dependencies.add(dependency);
		});
		if (this.node.key) {
			this.node.key.dynamic_dependencies().forEach((dependency) => {
				all_dependencies.add(dependency);
			});
		}
		this.dependencies = all_dependencies;
		if (this.node.key) {
			this.render_keyed(args);
		} else {
			this.render_unkeyed(args);
		}
		if (this.block.has_intro_method || this.block.has_outro_method) {
			block.chunks.intro.push(b`
				for (let #i = 0; #i < ${this.vars.data_length}; #i += 1) {
					@transition_in(${this.vars.iterations}[#i]);
				}
			`);
		}
		if (needs_anchor) {
			block.add_element(
				/** @type {import('estree').Identifier} */ (update_anchor_node),
				x`@empty()`,
				parent_nodes && x`@empty()`,
				parent_node
			);
		}
		if (this.else) {
			let else_ctx = x`#ctx`;
			if (this.else.node.const_tags.length > 0) {
				const get_ctx_name = this.renderer.component.get_unique_name('get_else_ctx');
				this.renderer.blocks.push(b`
					function ${get_ctx_name}(#ctx) {
						const child_ctx = #ctx.slice();
						${add_const_tags(block, this.else.node.const_tags, 'child_ctx')}
						return child_ctx;
					}
				`);
				else_ctx = x`${get_ctx_name}(#ctx)`;
			}
			const each_block_else = component.get_unique_name(`${this.var.name}_else`);
			block.chunks.init.push(b`let ${each_block_else} = null;`);
			// TODO neaten this up... will end up with an empty line in the block
			block.chunks.init.push(b`
				if (!${this.vars.data_length}) {
					${each_block_else} = ${this.else.block.name}(${else_ctx});
				}
			`);
			block.chunks.create.push(b`
				if (${each_block_else}) {
					${each_block_else}.c();
				}
			`);
			if (this.renderer.options.hydratable) {
				block.chunks.claim.push(b`
					if (${each_block_else}) {
						${each_block_else}.l(${parent_nodes});
					}
				`);
			}
			block.chunks.mount.push(b`
				if (${each_block_else}) {
					${each_block_else}.m(${initial_mount_node}, ${initial_anchor_node});
				}
			`);
			const has_transitions = !!(
				this.else.block.has_intro_method || this.else.block.has_outro_method
			);
			const destroy_block_else = this.else.block.has_outro_method
				? b`
					@group_outros();
					@transition_out(${each_block_else}, 1, 1, () => {
						${each_block_else} = null;
					});
					@check_outros();`
				: b`
					${each_block_else}.d(1);
					${each_block_else} = null;`;
			if (this.else.block.has_update_method) {
				this.updates.push(b`
					if (!${this.vars.data_length} && ${each_block_else}) {
						${each_block_else}.p(${else_ctx}, #dirty);
					} else if (!${this.vars.data_length}) {
						${each_block_else} = ${this.else.block.name}(${else_ctx});
						${each_block_else}.c();
						${has_transitions && b`@transition_in(${each_block_else}, 1);`}
						${each_block_else}.m(${update_mount_node}, ${update_anchor_node});
					} else if (${each_block_else}) {
						${destroy_block_else};
					}
				`);
			} else {
				this.updates.push(b`
					if (${this.vars.data_length}) {
						if (${each_block_else}) {
							${destroy_block_else};
						}
					} else if (!${each_block_else}) {
						${each_block_else} = ${this.else.block.name}(${else_ctx});
						${each_block_else}.c();
						${has_transitions && b`@transition_in(${each_block_else}, 1);`}
						${each_block_else}.m(${update_mount_node}, ${update_anchor_node});
					}
				`);
			}
			block.chunks.destroy.push(b`
				if (${each_block_else}) ${each_block_else}.d(${parent_node ? '' : 'detaching'});
			`);
		}
		if (this.updates.length) {
			block.chunks.update.push(b`
				if (${block.renderer.dirty(Array.from(all_dependencies))}) {
					${this.updates}
				}
			`);
		}
		this.fragment.render(this.block, null, /** @type {import('estree').Identifier} */ (x`#nodes`));
		if (this.else) {
			this.else.fragment.render(
				this.else.block,
				null,
				/** @type {import('estree').Identifier} */ (x`#nodes`)
			);
		}
		this.context_props = this.node.contexts.map((prop) => {
			if (prop.type === 'DestructuredVariable') {
				/** @param {string} name */
				const to_ctx = (name) =>
					renderer.context_lookup.has(name)
						? x`child_ctx[${renderer.context_lookup.get(name).index}]`
						: /** @type {import('estree').Node} */ ({ type: 'Identifier', name });
				return b`child_ctx[${
					renderer.context_lookup.get(prop.key.name).index
				}] = ${prop.default_modifier(prop.modifier(x`list[i]`), to_ctx)};`;
			} else {
				const expression = new Expression(
					this.renderer.component,
					this.node,
					this.node.scope,
					prop.key
				);
				return b`const ${prop.property_name} = ${expression.manipulate(block, 'child_ctx')};`;
			}
		});
		if (this.node.has_binding)
			this.context_props.push(
				b`child_ctx[${renderer.context_lookup.get(this.vars.each_block_value.name).index}] = list;`
			);
		if (this.node.has_binding || this.node.has_index_binding || this.node.index)
			this.context_props.push(
				b`child_ctx[${renderer.context_lookup.get(this.index_name.name).index}] = i;`
			);
		// TODO which is better — Object.create(array) or array.slice()?
		renderer.blocks.push(b`
			function ${this.vars.get_each_context}(#ctx, list, i) {
				const child_ctx = #ctx.slice();
				${this.context_props}
				${add_const_tags(this.block, this.node.const_tags, 'child_ctx')}
				return child_ctx;
			}
		`);
	}
	/**
	 * @param {{
	 * 		block: import('../Block.js').default;
	 * 		parent_node: import('estree').Identifier;
	 * 		parent_nodes: import('estree').Identifier;
	 * 		snippet: import('estree').Node;
	 * 		initial_anchor_node: import('estree').Identifier;
	 * 		initial_mount_node: import('estree').Identifier;
	 * 		update_anchor_node: import('estree').Identifier;
	 * 		update_mount_node: import('estree').Identifier;
	 * 	}} params
	 */
	render_keyed({
		block,
		parent_node,
		parent_nodes,
		snippet,
		initial_anchor_node,
		initial_mount_node,
		update_anchor_node,
		update_mount_node
	}) {
		const { create_each_block, iterations, data_length, view_length } = this.vars;
		const get_key = block.get_unique_name('get_key');
		const lookup = block.get_unique_name(`${this.var.name}_lookup`);
		block.add_variable(iterations, x`[]`);
		block.add_variable(lookup, x`new @_Map()`);
		if (this.fragment.nodes[0].is_dom_node()) {
			this.block.first = this.fragment.nodes[0].var;
		} else {
			this.block.first = this.block.get_unique_name('first');
			this.block.add_element(this.block.first, x`@empty()`, parent_nodes && x`@empty()`, null);
		}
		block.chunks.init.push(b`
			const ${get_key} = #ctx => ${this.node.key.manipulate(block)};

			${
				this.renderer.options.dev &&
				b`@validate_each_keys(#ctx, ${this.vars.each_block_value}, ${this.vars.get_each_context}, ${get_key});`
			}
			for (let #i = 0; #i < ${data_length}; #i += 1) {
				let child_ctx = ${this.vars.get_each_context}(#ctx, ${this.vars.each_block_value}, #i);
				let key = ${get_key}(child_ctx);
				${lookup}.set(key, ${iterations}[#i] = ${create_each_block}(key, child_ctx));
			}
		`);
		block.chunks.create.push(b`
			for (let #i = 0; #i < ${view_length}; #i += 1) {
				${iterations}[#i].c();
			}
		`);
		if (parent_nodes && this.renderer.options.hydratable) {
			block.chunks.claim.push(b`
				for (let #i = 0; #i < ${view_length}; #i += 1) {
					${iterations}[#i].l(${parent_nodes});
				}
			`);
		}
		block.chunks.mount.push(b`
			for (let #i = 0; #i < ${view_length}; #i += 1) {
				if (${iterations}[#i]) {
					${iterations}[#i].m(${initial_mount_node}, ${initial_anchor_node});
				}
			}
		`);
		const dynamic = this.block.has_update_method;
		const destroy = this.node.has_animation
			? this.block.has_outros
				? '@fix_and_outro_and_destroy_block'
				: '@fix_and_destroy_block'
			: this.block.has_outros
			? '@outro_and_destroy_block'
			: '@destroy_block';
		if (this.dependencies.size) {
			this.block.maintain_context = true;
			this.updates.push(b`
				${this.vars.each_block_value} = ${snippet};

				${this.block.has_outros && b`@group_outros();`}
				${
					this.node.has_animation &&
					b`for (let #i = 0; #i < ${view_length}; #i += 1) ${iterations}[#i].r();`
				}
				${
					this.renderer.options.dev &&
					b`@validate_each_keys(#ctx, ${this.vars.each_block_value}, ${this.vars.get_each_context}, ${get_key});`
				}
				${iterations} = @update_keyed_each(${iterations}, #dirty, ${get_key}, ${dynamic ? 1 : 0}, #ctx, ${
				this.vars.each_block_value
			}, ${lookup}, ${update_mount_node}, ${destroy}, ${create_each_block}, ${update_anchor_node}, ${
				this.vars.get_each_context
			});
				${
					this.node.has_animation &&
					b`for (let #i = 0; #i < ${view_length}; #i += 1) ${iterations}[#i].a();`
				}
				${this.block.has_outros && b`@check_outros();`}
			`);
		}
		if (this.block.has_outros) {
			block.chunks.outro.push(b`
				for (let #i = 0; #i < ${view_length}; #i += 1) {
					@transition_out(${iterations}[#i]);
				}
			`);
		}
		block.chunks.destroy.push(b`
			for (let #i = 0; #i < ${view_length}; #i += 1) {
				${iterations}[#i].d(${parent_node ? null : 'detaching'});
			}
		`);
	}
	/**
	 * @param {{
	 * 		block: import('../Block.js').default;
	 * 		parent_nodes: import('estree').Identifier;
	 * 		snippet: import('estree').Node;
	 * 		initial_anchor_node: import('estree').Identifier;
	 * 		initial_mount_node: import('estree').Identifier;
	 * 		update_anchor_node: import('estree').Identifier;
	 * 		update_mount_node: import('estree').Identifier;
	 * 	}} params
	 */
	render_unkeyed({
		block,
		parent_nodes,
		snippet,
		initial_anchor_node,
		initial_mount_node,
		update_anchor_node,
		update_mount_node
	}) {
		const { create_each_block, iterations, fixed_length, data_length, view_length } = this.vars;
		block.chunks.init.push(b`
			let ${iterations} = [];

			for (let #i = 0; #i < ${data_length}; #i += 1) {
				${iterations}[#i] = ${create_each_block}(${this.vars.get_each_context}(#ctx, ${this.vars.each_block_value}, #i));
			}
		`);
		block.chunks.create.push(b`
			for (let #i = 0; #i < ${view_length}; #i += 1) {
				${iterations}[#i].c();
			}
		`);
		if (parent_nodes && this.renderer.options.hydratable) {
			block.chunks.claim.push(b`
				for (let #i = 0; #i < ${view_length}; #i += 1) {
					${iterations}[#i].l(${parent_nodes});
				}
			`);
		}
		block.chunks.mount.push(b`
			for (let #i = 0; #i < ${view_length}; #i += 1) {
				if (${iterations}[#i]) {
					${iterations}[#i].m(${initial_mount_node}, ${initial_anchor_node});
				}
			}
		`);
		if (this.dependencies.size) {
			const has_transitions = !!(this.block.has_intro_method || this.block.has_outro_method);
			const for_loop_body = this.block.has_update_method
				? b`
					if (${iterations}[#i]) {
						${iterations}[#i].p(child_ctx, #dirty);
						${has_transitions && b`@transition_in(${this.vars.iterations}[#i], 1);`}
					} else {
						${iterations}[#i] = ${create_each_block}(child_ctx);
						${iterations}[#i].c();
						${has_transitions && b`@transition_in(${this.vars.iterations}[#i], 1);`}
						${iterations}[#i].m(${update_mount_node}, ${update_anchor_node});
					}
				`
				: has_transitions
				? b`
						if (${iterations}[#i]) {
							@transition_in(${this.vars.iterations}[#i], 1);
						} else {
							${iterations}[#i] = ${create_each_block}(child_ctx);
							${iterations}[#i].c();
							@transition_in(${this.vars.iterations}[#i], 1);
							${iterations}[#i].m(${update_mount_node}, ${update_anchor_node});
						}
					`
				: b`
						if (!${iterations}[#i]) {
							${iterations}[#i] = ${create_each_block}(child_ctx);
							${iterations}[#i].c();
							${iterations}[#i].m(${update_mount_node}, ${update_anchor_node});
						}
					`;
			const start = this.block.has_update_method ? 0 : '#old_length';

			/** @type {import('estree').Node[]} */
			let remove_old_blocks;
			if (this.block.has_outros) {
				const out = block.get_unique_name('out');
				block.chunks.init.push(b`
					const ${out} = i => @transition_out(${iterations}[i], 1, 1, () => {
						${iterations}[i] = null;
					});
				`);
				remove_old_blocks = b`
					@group_outros();
					for (#i = ${data_length}; #i < ${view_length}; #i += 1) {
						${out}(#i);
					}
					@check_outros();
				`;
			} else {
				remove_old_blocks = b`
					for (${this.block.has_update_method ? null : x`#i = ${data_length}`}; #i < ${
					this.block.has_update_method ? view_length : '#old_length'
				}; #i += 1) {
						${iterations}[#i].d(1);
					}
					${!fixed_length && b`${view_length} = ${data_length};`}
				`;
			}
			// We declare `i` as block scoped here, as the `remove_old_blocks` code
			// may rely on continuing where this iteration stopped.
			const update = b`
				${!this.block.has_update_method && b`const #old_length = ${this.vars.each_block_value}.length;`}
				${this.vars.each_block_value} = ${snippet};

				let #i;
				for (#i = ${start}; #i < ${data_length}; #i += 1) {
					const child_ctx = ${this.vars.get_each_context}(#ctx, ${this.vars.each_block_value}, #i);

					${for_loop_body}
				}

				${remove_old_blocks}
			`;
			this.updates.push(update);
		}
		if (this.block.has_outros) {
			block.chunks.outro.push(b`
				${iterations} = ${iterations}.filter(@_Boolean);
				for (let #i = 0; #i < ${view_length}; #i += 1) {
					@transition_out(${iterations}[#i]);
				}
			`);
		}
		block.chunks.destroy.push(b`@destroy_each(${iterations}, detaching);`);
	}
}
