import Block from './Block';
import { CompileOptions, Var } from '../../interfaces';
import Component from '../Component';
import FragmentWrapper from './wrappers/Fragment';
import { x } from 'code-red';
import { Node, Identifier, MemberExpression, Literal, Expression, BinaryExpression } from 'estree';
import flatten_reference from '../utils/flatten_reference';
import { reserved_keywords } from '../utils/reserved_keywords';

interface ContextMember {
	name: string;
	index: Literal;
	is_contextual: boolean;
	is_non_contextual: boolean;
	variable: Var;
	priority: number;
}

type BitMasks = Array<{
	n: number;
	names: string[];
}>;

export default class Renderer {
	component: Component; // TODO Maybe Renderer shouldn't know about Component?
	options: CompileOptions;

	context: ContextMember[] = [];
	initial_context: ContextMember[] = [];
	context_lookup: Map<string, ContextMember> = new Map();
	context_overflow: boolean;
	blocks: Array<Block | Node | Node[]> = [];
	readonly: Set<string> = new Set();
	meta_bindings: Array<Node | Node[]> = []; // initial values for e.g. window.innerWidth, if there's a <svelte:window> meta tag
	binding_groups: Map<string, { binding_group: (to_reference?: boolean) => Node; is_context: boolean; contexts: string[]; index: number }> = new Map();

	block: Block;
	fragment: FragmentWrapper;

	file_var: Identifier;
	locate: (c: number) => { line: number; column: number };

	constructor(component: Component, options: CompileOptions) {
		this.component = component;
		this.options = options;
		this.locate = component.locate; // TODO messy

		this.file_var = options.dev && this.component.get_unique_name('file');

		component.vars.filter(v => !v.hoistable || (v.export_name && !v.module)).forEach(v => this.add_to_context(v.name));

		// ensure store values are included in context
		component.vars.filter(v => v.subscribable).forEach(v => this.add_to_context(`$${v.name}`));

		reserved_keywords.forEach(keyword => {
			if (component.var_lookup.has(keyword)) {
				this.add_to_context(keyword);
			}
		});

		if (component.slots.size > 0) {
			this.add_to_context('$$scope');
			this.add_to_context('$$slots');
		}

		if (this.binding_groups.size > 0) {
			this.add_to_context('$$binding_groups');
		}

		// main block
		this.block = new Block({
			renderer: this,
			name: null,
			type: 'component',
			key: null,

			bindings: new Map(),

			dependencies: new Set(),
		});

		this.block.has_update_method = true;

		this.fragment = new FragmentWrapper(
			this,
			this.block,
			component.fragment.children,
			null,
			true,
			null
		);

		// TODO messy
		this.blocks.forEach(block => {
			if (block instanceof Block) {
				block.assign_variable_names();
			}
		});

		this.block.assign_variable_names();

		this.fragment.render(this.block, null, x`#nodes` as Identifier);

		this.context_overflow = this.context.length > 31;

		this.context.forEach(member => {
			const { variable } = member;
			if (variable) {
				member.priority += 2;
				if (variable.mutated || variable.reassigned) member.priority += 4;

				// these determine whether variable is included in initial context
				// array, so must have the highest priority
				if (variable.export_name) member.priority += 16;
				if (variable.referenced) member.priority += 32;
			} else if (member.is_non_contextual) {
				// determine whether variable is included in initial context
				// array, so must have the highest priority
				member.priority += 8;
			}

			if (!member.is_contextual) {
				member.priority += 1;
			}
		});

		this.context.sort((a, b) => (b.priority - a.priority) || ((a.index.value as number) - (b.index.value as number)));
		this.context.forEach((member, i) => member.index.value = i);

		let i = this.context.length;
		while (i--) {
			const member = this.context[i];
			if (member.variable) {
				if (member.variable.referenced || member.variable.export_name) break;
			} else if (member.is_non_contextual) {
				break;
			}
		}
		this.initial_context = this.context.slice(0, i + 1);
	}

	add_to_context(name: string, contextual = false) {
		if (!this.context_lookup.has(name)) {
			const member: ContextMember = {
				name,
				index: { type: 'Literal', value: this.context.length }, // index is updated later, but set here to preserve order within groups
				is_contextual: false,
				is_non_contextual: false, // shadowed vars could be contextual and non-contextual
				variable: null,
				priority: 0
			};

			this.context_lookup.set(name, member);
			this.context.push(member);
		}

		const member = this.context_lookup.get(name);

		if (contextual) {
			member.is_contextual = true;
		} else {
			member.is_non_contextual = true;
			const variable = this.component.var_lookup.get(name);
			member.variable = variable;
		}

		return member;
	}

	invalidate(name: string, value?) {
		const variable = this.component.var_lookup.get(name);
		const member = this.context_lookup.get(name);

		if (variable && (variable.subscribable && (variable.reassigned || variable.export_name))) {
			return x`${`$$subscribe_${name}`}($$invalidate(${member.index}, ${value || name}))`;
		}

		if (name[0] === '$' && name[1] !== '$') {
			return x`${name.slice(1)}.set(${value || name})`;
		}

		if (
			variable && (
				variable.module || (
					!variable.referenced &&
					!variable.is_reactive_dependency &&
					!variable.export_name &&
					!name.startsWith('$$')
				)
			)
		) {
			return value || name;
		}

		if (value) {
			return x`$$invalidate(${member.index}, ${value})`;
		}

		// if this is a reactive declaration, invalidate dependencies recursively
		const deps = new Set([name]);

		deps.forEach(name => {
			const reactive_declarations = this.component.reactive_declarations.filter(x =>
				x.assignees.has(name)
			);
			reactive_declarations.forEach(declaration => {
				declaration.dependencies.forEach(name => {
					deps.add(name);
				});
			});
		});

		// TODO ideally globals etc wouldn't be here in the first place
		const filtered = Array.from(deps).filter(n => this.context_lookup.has(n));
		if (!filtered.length) return null;

		return filtered
			.map(n => x`$$invalidate(${this.context_lookup.get(n).index}, ${n})`)
			.reduce((lhs, rhs) => x`${lhs}, ${rhs}`);
	}

	dirty(names, is_reactive_declaration = false): Expression {
		const renderer = this;

		const dirty = (is_reactive_declaration
			? x`$$self.$$.dirty`
			: x`#dirty`) as Identifier | MemberExpression;

		const get_bitmask = () => {
			const bitmask: BitMasks = [];
			names.forEach((name) => {
				const member = renderer.context_lookup.get(name);

				if (!member) return;

				if (member.index.value === -1) {
					throw new Error(`unset index`);
				}

				const value = member.index.value as number;
				const i = (value / 31) | 0;
				const n = 1 << (value % 31);

				if (!bitmask[i]) bitmask[i] = { n: 0, names: [] };

				bitmask[i].n |= n;
				bitmask[i].names.push(name);
			});
			return bitmask;
		};

		// TODO: context-overflow make it less gross
		return {
			// Using a ParenthesizedExpression allows us to create
			// the expression lazily. TODO would be better if
			// context was determined before rendering, so that
			// this indirection was unnecessary
			type: 'ParenthesizedExpression',
			get expression() {
				const bitmask = get_bitmask();

				if (!bitmask.length) {
					return x`${dirty} & /*${names.join(', ')}*/ 0` as BinaryExpression;
				}

				if (renderer.context_overflow) {
					return bitmask
						.map((b, i) => ({ b, i }))
						.filter(({ b }) => b)
						.map(({ b, i }) => x`${dirty}[${i}] & /*${b.names.join(', ')}*/ ${b.n}`)
						.reduce((lhs, rhs) => x`${lhs} | ${rhs}`);
				}

				return x`${dirty} & /*${names.join(', ')}*/ ${bitmask[0].n}` as BinaryExpression;
			}
		} as any;
	}

	reference(node: string | Identifier | MemberExpression) {
		if (typeof node === 'string') {
			node = { type: 'Identifier', name: node };
		}

		const { name, nodes } = flatten_reference(node);
		const member = this.context_lookup.get(name);

		// TODO is this correct?
		if (this.component.var_lookup.get(name)) {
			this.component.add_reference(name);
		}

		if (member !== undefined) {
			const replacement = x`/*${member.name}*/ #ctx[${member.index}]` as MemberExpression;

			if (nodes[0].loc) replacement.object.loc = nodes[0].loc;
			nodes[0] = replacement;

			return nodes.reduce((lhs, rhs) => x`${lhs}.${rhs}`);
		}

		return node;
	}

	remove_block(block: Block | Node | Node[]) {
		this.blocks.splice(this.blocks.indexOf(block), 1);
	}
}