<script lang="ts" context="module">
	import type { Writable, Readable } from "svelte/store";
	import type { Spring } from "svelte/motion";
	import { type PixiApp } from "./utils/pixi";
	import { type CommandManager } from "./utils/commands";

	export const EDITOR_KEY = Symbol("editor");
	export type context_type = "bg" | "layers" | "crop" | "draw" | "erase";
	type PartialRecord<K extends keyof any, T> = Partial<Record<K, T>>;
	import { type tool } from "./tools";

	export interface EditorContext {
		pixi: Writable<PixiApp | null>;
		current_layer: Writable<LayerScene | null>;
		dimensions: Writable<[number, number]>;
		editor_box: Writable<{
			parent_width: number;
			parent_height: number;
			parent_left: number;
			parent_top: number;
			parent_right: number;
			parent_bottom: number;
			child_width: number;
			child_height: number;
			child_left: number;
			child_top: number;
			child_right: number;
			child_bottom: number;
		}>;
		active_tool: Writable<tool>;
		toolbar_box: Writable<DOMRect>;
		crop: Writable<[number, number, number, number]>;
		position_spring: Spring<{
			x: number;
			y: number;
		}>;
		command_manager: CommandManager;
		current_history: CommandManager["current_history"];
		register_context: (
			type: context_type,
			{
				reset_fn,
				init_fn
			}: {
				reset_fn?: () => void;
				init_fn?: (dimensions?: [number, number]) => void;
			}
		) => void;
		reset: (clear_image: boolean, dimensions: [number, number]) => void;
	}
</script>

<script lang="ts">
	import { onMount, setContext, createEventDispatcher, tick } from "svelte";
	import { writable } from "svelte/store";
	import { spring } from "svelte/motion";
	import { Rectangle } from "pixi.js";

	import { command_manager } from "./utils/commands";

	import { type LayerScene } from "./layers/utils";
	import { create_pixi_app, type ImageBlobs } from "./utils/pixi";
	import Controls from "./Controls.svelte";
	export let antialias = true;
	export let crop_size: [number, number] | undefined;
	export let changeable = false;
	export let history: boolean;
	export let bg = false;
	export let sources: ("clipboard" | "webcam" | "upload")[];
	const dispatch = createEventDispatcher<{
		clear?: never;
		save: void;
		change: void;
	}>();
	export let crop_constraint = false;
	export let canvas_size: [number, number] | undefined;

	$: orig_canvas_size = canvas_size;

	const BASE_DIMENSIONS: [number, number] = canvas_size || [800, 600];

	let dimensions = writable(BASE_DIMENSIONS);
	export let height = 0;

	let editor_box: EditorContext["editor_box"] = writable({
		parent_width: 0,
		parent_height: 0,
		parent_top: 0,
		parent_left: 0,
		parent_right: 0,
		parent_bottom: 0,
		child_width: 0,
		child_height: 0,
		child_top: 0,
		child_left: 0,
		child_right: 0,
		child_bottom: 0
	});

	$: height = $editor_box.child_height;

	const crop = writable<[number, number, number, number]>([0, 0, 1, 1]);
	const position_spring = spring(
		{ x: 0, y: 0 },
		{
			stiffness: 0.1,
			damping: 0.5
		}
	);
	const pixi = writable<PixiApp | null>(null);

	const CommandManager = command_manager();

	const { can_redo, can_undo, current_history } = CommandManager;

	$: $current_history.previous, dispatch("change");

	$: {
		history = !!$current_history.previous || $active_tool !== "bg";
	}

	const active_tool: Writable<tool> = writable("bg");
	const reset_context: Writable<PartialRecord<context_type, () => void>> =
		writable({});
	const init_context: Writable<
		PartialRecord<context_type, (dimensions?: typeof $dimensions) => void>
	> = writable({});
	const contexts: Writable<context_type[]> = writable([]);
	const toolbar_box: Writable<DOMRect> = writable(new DOMRect());

	const sort_order = ["bg", "layers", "crop", "draw", "erase"] as const;
	const editor_context = setContext<EditorContext>(EDITOR_KEY, {
		pixi,
		current_layer: writable(null),
		dimensions,
		editor_box,
		toolbar_box,
		active_tool,
		crop,
		position_spring,
		command_manager: CommandManager,
		current_history,
		register_context: (
			type: context_type,
			{
				reset_fn,
				init_fn
			}: {
				reset_fn?: () => void;
				init_fn?: (dimensions?: [number, number]) => void;
			}
		) => {
			contexts.update((c) => [...c, type]);
			init_context.update((c) => ({ ...c, [type]: init_fn }));
			reset_context.update((c) => ({ ...c, [type]: reset_fn }));
		},
		reset: (clear_image: boolean, dimensions: [number, number]) => {
			bg = false;

			const _sorted_contexts = $contexts.sort((a, b) => {
				return sort_order.indexOf(a) - sort_order.indexOf(b);
			});
			for (const k of _sorted_contexts) {
				if (k in $reset_context && typeof $reset_context[k] === "function") {
					$reset_context[k]?.();
				}
			}

			for (const k of _sorted_contexts) {
				if (k in $init_context && typeof $init_context[k] === "function") {
					if (k === "bg" && !clear_image) {
						continue;
					} else {
						$init_context[k]?.(dimensions);
					}
				}
				CommandManager.reset();
				$pixi?.resize?.(...dimensions);
			}
		}
	});

	let pixi_target: HTMLDivElement;

	let canvas_wrap: HTMLDivElement;

	function get_dimensions(parent: HTMLDivElement, child: HTMLDivElement): void {
		if (!parent || !child) return;
		const {
			width: parent_width,
			height: parent_height,
			top: parent_top,
			left: parent_left,
			right: parent_right,
			bottom: parent_bottom
		} = canvas_wrap.getBoundingClientRect();
		const {
			width: child_width,
			height: child_height,
			top: child_top,
			left: child_left,
			right: child_right,
			bottom: child_bottom
		} = child.getBoundingClientRect();
		editor_box.set({
			child_width,
			child_height,
			child_left,
			child_right,
			child_top,
			child_bottom,

			parent_width,
			parent_height,
			parent_left,
			parent_right,
			parent_top,
			parent_bottom
		});
	}

	$: if (crop_constraint && bg && !history) {
		set_crop();
	}

	function set_crop(): void {
		requestAnimationFrame(() => {
			tick().then((v) => ($active_tool = "crop"));
		});
	}

	function reposition_canvas(): void {
		if (!$editor_box) return;
		const [l, t, w, h] = $crop;

		const cx = l * $editor_box.child_width;
		const cy = t * $editor_box.child_height;
		const cw = w * $editor_box.child_width;
		const ch = h * $editor_box.child_height;

		const x = 0.5 * $editor_box.child_width - cx - cw / 2;
		const y = 0.5 * $editor_box.child_height - cy - ch / 2;

		position_spring.set({ x, y });
	}

	export async function get_blobs(): Promise<ImageBlobs> {
		if (!$pixi || !$pixi.get_layers)
			return { background: null, layers: [], composite: null };
		const [l, t, w, h] = $crop;

		return $pixi?.get_blobs(
			$pixi.get_layers(),
			new Rectangle(
				Math.round(l * $dimensions[0]),
				Math.round(t * $dimensions[1]),
				Math.round(w * $dimensions[0]),
				Math.round(h * $dimensions[1])
			),
			$dimensions
		);
	}

	$: $crop && reposition_canvas();
	$: $position_spring && get_dimensions(canvas_wrap, pixi_target);

	export function handle_remove(): void {
		editor_context.reset(
			true,
			orig_canvas_size ? orig_canvas_size : $dimensions
		);
		if (!sources.length) {
			set_tool("draw");
		} else {
			set_tool("bg");
		}
		dispatch("clear");
	}

	onMount(() => {
		const _size = (canvas_size ? canvas_size : crop_size) || [800, 600];
		const app = create_pixi_app({
			target: pixi_target,
			dimensions: _size,
			antialias
		});

		function resize(width: number, height: number): void {
			app.resize(width, height);
			dimensions.set([width, height]);
		}

		pixi.set({ ...app, resize });

		const resizer = new ResizeObserver((entries) => {
			for (const entry of entries) {
				get_dimensions(canvas_wrap, pixi_target);
			}
		});

		resizer.observe(canvas_wrap);
		resizer.observe(pixi_target);

		for (const k of $contexts) {
			if (k in $init_context && typeof $init_context[k] === "function") {
				$init_context[k]?.($dimensions);
			}
		}

		resize(...$dimensions);

		return () => {
			$pixi?.destroy();
			resizer.disconnect();
			for (const k of $contexts) {
				if (k in $reset_context) {
					$reset_context[k]?.();
				}
			}
		};
	});

	let saved_history: null | typeof $current_history = $current_history;

	function handle_save(): void {
		saved_history = $current_history;
		dispatch("save");
	}

	export function set_tool(tool: tool): void {
		$active_tool = tool;
	}
</script>

<svelte:window on:scroll={() => get_dimensions(canvas_wrap, pixi_target)} />

<div data-testid="image" class="image-container">
	<Controls
		can_undo={$can_undo}
		can_redo={$can_redo}
		can_save={saved_history !== $current_history}
		{changeable}
		on:undo={CommandManager.undo}
		on:redo={CommandManager.redo}
		on:remove_image={handle_remove}
		on:save={handle_save}
	/>
	<div class="wrap" bind:this={canvas_wrap}>
		<div
			bind:this={pixi_target}
			class="stage-wrap"
			class:bg={!bg}
			style:transform="translate({$position_spring.x}px, {$position_spring.y}px)"
		></div>
	</div>
	<div class="tools-wrap">
		<slot />
	</div>
	<div
		class="border"
		style:width="{$crop[2] * $editor_box.child_width + 1}px"
		style:height="{$crop[3] * $editor_box.child_height + 1}px"
		style:top="{$crop[1] * $editor_box.child_height +
			($editor_box.child_top - $editor_box.parent_top) -
			0.5}px"
		style:left="{$crop[0] * $editor_box.child_width +
			($editor_box.child_left - $editor_box.parent_left) -
			0.5}px"
	></div>
</div>

<style>
	.wrap {
		display: flex;
		width: 100%;
		height: 100%;
		position: relative;
		justify-content: center;
		align-items: flex-start;
	}
	.border {
		position: absolute;
		border: var(--block-border-color) 1px solid;
		pointer-events: none;
		border-radius: var(--radius-md);
	}

	.stage-wrap {
		margin: var(--size-8);
		margin-bottom: var(--size-1);
		border-radius: var(--radius-md);
		overflow: hidden;
	}

	.tools-wrap {
		display: flex;
		justify-content: center;
		align-items: center;
		padding: 0 var(--spacing-xl) 0 0;
		border: 1px solid var(--block-border-color);
		border-radius: var(--radius-sm);
		margin: var(--spacing-xxl) 0 var(--spacing-xxl) 0;
	}

	.image-container {
		display: flex;
		height: 100%;
		flex-direction: column;
		justify-content: center;
		align-items: center;
		max-height: 100%;
	}
</style>
