import {
	Container,
	type IRenderer,
	type DisplayObject,
	RenderTexture,
	Texture,
	Sprite,
	Filter
} from "pixi.js";

/**
 * GLSL Shader that takes two textures and erases the second texture from the first.
 */
export const erase_shader = `
precision highp float;

uniform sampler2D uDrawingTexture;
uniform sampler2D uEraserTexture;

varying vec2 vTextureCoord;

void main(void) {
	vec4 drawingColor = texture2D(uDrawingTexture,vTextureCoord);
	vec4 eraserColor = texture2D(uEraserTexture, vTextureCoord);

	// Use the alpha of the eraser to determine how much to "erase" from the drawing
	float alpha = 1.0 - eraserColor.a;
	gl_FragColor = vec4(drawingColor.rgb * alpha, drawingColor.a * alpha);
}`;

/**
 * Interface holding data for a layer
 */
export interface LayerScene {
	/**
	 * The texture used for tracking brush strokes.
	 */
	draw_texture: RenderTexture;
	/**
	 * The texture used for tracking eraser strokes.
	 */
	erase_texture: RenderTexture;
	/**
	 * The sprite used for displaying the composite of the draw and erase textures.
	 */
	composite: Sprite;
	/**
	 * The filter used for combining the draw and erase textures into a composite texture.
	 */
	filter?: Filter;
}

/**
 * Interface for managing layers.
 */
interface LayerManager {
	/**
	 * Adds a layer to the container.
	 * @param layer The container to add the layer to.
	 * @param renderer The renderer to use for the layer.
	 * @param width the width of the layer
	 * @param height the height of the layer
	 */
	add_layer(
		container: Container,
		renderer: IRenderer,
		width: number,
		height: number
	): [LayerScene, LayerScene[]];
	/**
	 * Swaps the layer with the layer above or below it.
	 * @param layer The index layer to swap.
	 * @param direction The direction to swap the layer.
	 */
	swap_layers(layer: number, direction: "up" | "down"): LayerScene;
	/**
	 * Changes the active layer.
	 * @param layer The index of the layer to make active.
	 */
	change_active_layer(layer: number): LayerScene;
	/**
	 * Resizes the layers.
	 * @param width The new width of the layers.
	 * @param height The new height of the layers.
	 */
	reset(): void;
	/**
	 * Gets the layers.
	 * @returns The layers.
	 */
	get_layers(): LayerScene[];

	add_layer_from_blob(
		container: Container,
		renderer: IRenderer,
		blob: Blob,
		view: HTMLCanvasElement
	): Promise<[LayerScene, LayerScene[]]>;
}

/**
 * Swaps two adjacent elements in an array.
 * @param array The array to swap elements in.
 * @param index The index of the first element to swap.
 */
function swap_adjacent(array: any[], index: number): void {
	if (index < 0 || index >= array.length - 1) {
		throw new Error("Index out of bounds");
	}

	[array[index], array[index + 1]] = [array[index + 1], array[index]];
}

/**
 * Creates a layer manager.
 * @param canvas_resize a function to resize the canvas
 * @returns a layer manager
 */
export function layer_manager(): LayerManager {
	let _layers: LayerScene[] = [];
	let current_layer = 0;
	let position = 0;

	return {
		add_layer: function (
			container: Container,
			renderer: IRenderer,
			width: number,
			height: number
		): [LayerScene, LayerScene[]] {
			const layer_container = new Container() as Container & DisplayObject;
			position++;
			layer_container.zIndex = position;

			const composite_texture = RenderTexture.create({
				width,
				height
			});

			const composite = new Sprite(composite_texture) as Sprite & DisplayObject;

			layer_container.addChild(composite);

			composite.zIndex = position;

			const layer_scene: LayerScene = {
				draw_texture: RenderTexture.create({
					width,
					height
				}),
				erase_texture: RenderTexture.create({
					width,
					height
				}),
				composite
			};

			const erase_filter = new Filter(undefined, erase_shader, {
				uEraserTexture: layer_scene.erase_texture,
				uDrawingTexture: layer_scene.draw_texture
			});

			composite.filters = [erase_filter];

			container.addChild(layer_container);

			_layers.push(layer_scene);

			return [layer_scene, _layers];
		},

		swap_layers: function (
			layer: number,
			direction: "up" | "down"
		): LayerScene {
			if (direction === "up") {
				swap_adjacent(_layers, layer);
			} else {
				swap_adjacent(_layers, layer - 1);
			}
			return _layers[layer];
		},

		change_active_layer: function (layer: number): LayerScene {
			current_layer = layer;
			return _layers[layer];
		},
		reset() {
			_layers.forEach((layer) => {
				layer.draw_texture.destroy(true);
				layer.erase_texture.destroy(true);
				layer.composite.destroy(true);
			});
			_layers = [];
			current_layer = 0;
			position = 0;
		},
		async add_layer_from_blob(
			container: Container,
			renderer: IRenderer,
			blob: Blob,
			view: HTMLCanvasElement
		) {
			const img = await createImageBitmap(blob);
			const bitmap_texture = Texture.from(img);

			const [w, h] = resize_to_fit(
				bitmap_texture.width,
				bitmap_texture.height,
				view.width,
				view.height
			);

			const sprite = new Sprite(bitmap_texture) as Sprite & DisplayObject;
			sprite.zIndex = 0;

			sprite.width = w;
			sprite.height = h;

			const [layer, layers] = this.add_layer(
				container,
				renderer,
				view.width,
				view.height
			);

			renderer.render(sprite, {
				renderTexture: layer.draw_texture
			});

			return [layer, layers];
		},
		get_layers() {
			return _layers;
		}
	};
}

function resize_to_fit(
	inner_width: number,
	inner_height: number,
	outer_width: number,
	outer_height: number
): [number, number] {
	if (inner_width <= outer_width && inner_height <= outer_height) {
		return [inner_width, inner_height];
	}

	const inner_aspect = inner_width / inner_height;
	const outer_aspect = outer_width / outer_height;

	let new_width, new_height;

	if (inner_aspect > outer_aspect) {
		new_width = outer_width;
		new_height = outer_width / inner_aspect;
	} else {
		new_height = outer_height;
		new_width = outer_height * inner_aspect;
	}

	return [new_width, new_height];
}
