import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";

import * as PIXI from "pixi.js";
import { Container, Stage } from "@pixi/react";

import { CanvasContext } from "@/context/Canvas";
import { SelectionContext } from "@/context/Selection";
import { SettingsContext } from "@/context/Settings";
import { TexturesContext } from "@/context/Textures";
import { ThemeContext } from "@/context/Theme";
import { ToolContext } from "@/context/Tool";

import { useTextures } from "@/hooks/useTextures";
import { useBlockData } from "@/hooks/useBlockData";

import * as selection from "@/utils/selection";
import * as clipboard from "@/utils/clipboard";

import Blocks from "./Blocks";
import Cursor from "./Cursor";
import Selection from "./Selection";
import Grid from "./Grid";
import CanvasBorder from "./CanvasBorder";

import CursorInformation from "./information/Cursor";
import CanvasInformation from "./information/Canvas";
import SelectionBar from "./SelectionBar";

// Set scale mode to NEAREST
PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST;

function Canvas() {
	const { stageSize, canvasSize, blocks, coords, scale, version, setStageSize, setBlocks, setCoords, setScale } = useContext(CanvasContext);
	const { selectionCoords, selectionLayerBlocks, setSelectionCoords, setSelectionLayerBlocks } = useContext(SelectionContext);
	const { settings } = useContext(SettingsContext);
	const { missingTexture } = useContext(TexturesContext);
	const { isDark } = useContext(ThemeContext);
	const { tool, radius, selectedBlock, setTool, setSelectedBlock } = useContext(ToolContext);

	const textures = useTextures(version);
	const blockData = useBlockData(version);
	const stageContainerRef = useRef<HTMLDivElement>(null);

	const [mousePosition, setMousePosition] = useState<Position>({ x: 0, y: 0 });
	const [mouseCoords, setMouseCoords] = useState<Position>({ x: 0, y: 0 });
	const mouseMovementRef = useRef<Position>();
	const [dragging, setDragging] = useState(false);
	const dragStartCoordsRef = useRef<Position>();

	const holdingShiftRef = useRef(false);
	const holdingAltRef = useRef(false);
	const oldToolRef = useRef<Tool>();
	const [cssCursor, setCssCursor] = useState("crosshair");

	const visibleArea = useMemo(() => {
		const blockSize = 16 * scale;

		const visibleWidthBlocks = Math.ceil(stageSize.width / blockSize);
		const visibleHeightBlocks = Math.ceil(stageSize.height / blockSize);

		const startX = Math.floor(-coords.x / blockSize);
		const startY = Math.floor(-coords.y / blockSize);

		return {
			startX,
			startY,
			endX: startX + visibleWidthBlocks + 1,
			endY: startY + visibleHeightBlocks + 1,
		};
	}, [coords, scale, stageSize]);

	const visibleBlocks = useMemo(() => {
		return blocks.filter(
			(block) => block.x >= visibleArea.startX && block.x < visibleArea.endX && block.y >= visibleArea.startY && block.y < visibleArea.endY
		);
	}, [blocks, visibleArea]);

	const zoom = useCallback(
		(newScale: number) => {
			setScale(newScale);
			setCoords({
				x: mousePosition.x - ((mousePosition.x - coords.x) / scale) * newScale,
				y: mousePosition.y - ((mousePosition.y - coords.y) / scale) * newScale,
			});
		},
		[coords, mousePosition, scale, setCoords, setScale]
	);

	const updateCssCursor = useCallback(() => {
		const cursorMapping: Partial<Record<Tool, string>> = {
			hand: dragging ? "grab" : "grabbing",
			move: "move",
			zoom: holdingAltRef.current ? "zoom-out" : "zoom-in",
		};

		setCssCursor(cursorMapping[tool] || "crosshair");
	}, [dragging, holdingAltRef, tool, setCssCursor]);

	const onToolUse = useCallback(() => {
		// If number is odd, cursor is in the center
		// if number is even, cursor is in the top-left corner
		const getRadiusPosition = (): Position => {
			const halfSize = Math.floor(radius / 2);
			const x = mouseCoords.x - (radius % 2 === 0 ? 0 : halfSize);
			const y = mouseCoords.y - (radius % 2 === 0 ? 0 : halfSize);
			return { x, y };
		};

		const eraseTool = () => {
			const radiusPosition = getRadiusPosition();
			const updated = blocks.filter((block) => {
				const withinRadius =
					block.x >= radiusPosition.x && block.x < radiusPosition.x + radius && block.y >= radiusPosition.y && block.y < radiusPosition.y + radius;
				return !withinRadius || !selection.isIn(selectionCoords, block.x, block.y);
			});

			setBlocks(updated);
		};

		switch (tool) {
			case "move": {
				const mouseMovement = mouseMovementRef.current;
				if (!mouseMovement) return;

				// If there is no selection currently being moved...
				if (selectionLayerBlocks.length == 0) {
					const result: Block[] = [];

					setBlocks((prev) =>
						prev.filter((b) => {
							const isSelected = selection.isIn(selectionCoords, b.x, b.y);

							// Add blocks in the selection coords to the selection layer
							if (isSelected) result.push(b);

							// Remove blocks originally there
							return !isSelected;
						})
					);
					setSelectionLayerBlocks(result);
				}

				// Increase each coordinate in the selection by the mouse movement
				setSelectionCoords((prev) => prev.map(([x, y]) => [x + mouseMovement.x, y + mouseMovement.y]));
				setSelectionLayerBlocks((prev) => prev.map((b) => ({ ...b, x: b.x + mouseMovement.x, y: b.y + mouseMovement.y })));
				break;
			}
			case "lasso": {
				setSelectionCoords((prev) => {
					const radiusPosition = getRadiusPosition();
					const radiusCoords: CoordinateArray = [];

					for (let x = 0; x < radius; x++) {
						for (let y = 0; y < radius; y++) {
							const tileX = radiusPosition.x + x;
							const tileY = radiusPosition.y + y;

							const exists = prev.some(([x2, y2]) => x2 === tileX && y2 === tileY);
							if ((holdingAltRef.current && exists) || !exists) radiusCoords.push([tileX, tileY]);
						}
					}

					if (holdingAltRef.current) {
						return prev.filter(([x, y]) => !radiusCoords.some(([x2, y2]) => x2 === x && y2 === y));
					} else {
						return [...prev, ...radiusCoords];
					}
				});
				break;
			}
			case "pencil": {
				if (selectedBlock == "air") {
					eraseTool();
					break;
				}

				const radiusPosition = getRadiusPosition();
				const radiusBlocks: Block[] = [];

				for (let x = 0; x < radius; x++) {
					for (let y = 0; y < radius; y++) {
						const tileX = radiusPosition.x + x;
						const tileY = radiusPosition.y + y;

						// Only add blocks within the selection
						if (selection.isIn(selectionCoords, tileX, tileY)) {
							radiusBlocks.push({
								name: selectedBlock,
								x: tileX,
								y: tileY,
							});
						}
					}
				}

				const mergedBlocks = blocks.filter((block) => {
					return !radiusBlocks.some((newBlock) => block.x === newBlock.x && block.y === newBlock.y);
				});

				setBlocks([...mergedBlocks, ...radiusBlocks]);
				break;
			}
			case "eraser": {
				eraseTool();
				break;
			}
		}
	}, [
		tool,
		mouseCoords,
		selectedBlock,
		blocks,
		radius,
		selectionCoords,
		selectionLayerBlocks,
		setSelectionCoords,
		setSelectionLayerBlocks,
		setBlocks,
	]);

	const onMouseMove = useCallback(
		(e: React.MouseEvent) => {
			if (!stageContainerRef.current) return;

			const oldMouseCoords = mouseCoords;

			const rect = stageContainerRef.current.getBoundingClientRect();
			const mouseX = e.clientX - rect.left;
			const mouseY = e.clientY - rect.top;

			const newMouseCoords = {
				x: Math.floor((mouseX - coords.x) / (16 * scale)),
				y: Math.floor((mouseY - coords.y) / (16 * scale)),
			};

			setMousePosition({
				x: mouseX,
				y: mouseY,
			});
			setMouseCoords(newMouseCoords);

			mouseMovementRef.current = {
				x: newMouseCoords.x - oldMouseCoords.x,
				y: newMouseCoords.y - oldMouseCoords.y,
			};

			if (dragging) {
				switch (tool) {
					case "hand":
						setCoords((prevCoords) => ({
							x: prevCoords.x + e.movementX,
							y: prevCoords.y + e.movementY,
						}));
						break;
					case "rectangle-select": {
						const dragStartCoords = dragStartCoordsRef.current;
						if (!dragStartCoords) return;

						setSelectionCoords(() => {
							const newSelection: CoordinateArray = [];

							const startX = Math.min(dragStartCoords.x, mouseCoords.x);
							let endX = Math.max(dragStartCoords.x, mouseCoords.x);
							const startY = Math.min(dragStartCoords.y, mouseCoords.y);
							let endY = Math.max(dragStartCoords.y, mouseCoords.y);

							const isRadiusEven = radius == 1 || radius % 2 == 0;
							const radiusOffset = isRadiusEven ? radius : radius - 1;

							// If holding shift, create a square selection
							if (holdingShiftRef.current) {
								const width = Math.abs(endX - startX);
								const height = Math.abs(endY - startY);
								const size = Math.max(width, height);

								endX = startX + (endX < startX ? -size : size);
								endY = startY + (endY < startY ? -size : size);
							}

							for (let x = startX; x < endX + radiusOffset; x++) {
								for (let y = startY; y < endY + radiusOffset; y++) {
									newSelection.push([x, y]);
								}
							}

							return newSelection;
						});
						break;
					}
				}

				onToolUse();
			}
		},
		[dragging, coords, scale, tool, mouseCoords, onToolUse, setCoords, setSelectionCoords, radius]
	);

	const onMouseDown = useCallback(() => {
		setDragging(true);
		onToolUse();
		updateCssCursor();

		dragStartCoordsRef.current = mouseCoords;

		// Clear selection on click
		if (tool === "rectangle-select") setSelectionCoords([]);
	}, [onToolUse, updateCssCursor, mouseCoords, tool, setSelectionCoords]);

	const onMouseUp = useCallback(() => {
		setDragging(false);
		updateCssCursor();
	}, [updateCssCursor]);

	const onWheel = useCallback(
		(e: React.WheelEvent) => {
			e.preventDefault();
			const scaleChange = e.deltaY > 0 ? -0.1 : 0.1;
			const newScale = Math.min(Math.max(scale + scaleChange * scale, 0.1), 32);
			zoom(newScale);
		},
		[scale, zoom]
	);

	const onClick = useCallback(() => {
		// Directions for adjacent blocks (up, down, left, right)
		const directions = [
			{ dx: 0, dy: 1 },
			{ dx: 0, dy: -1 },
			{ dx: 1, dy: 0 },
			{ dx: -1, dy: 0 },
		];
		switch (tool) {
			case "magic-wand": {
				const visited = new Set<string>();
				const result: CoordinateArray = [];
				const startBlock = blocks.find((block) => block.x === mouseCoords.x && block.y === mouseCoords.y);
				const startName = startBlock ? startBlock.name : "air";

				function depthFirstSearch(x: number, y: number) {
					const key = `${x},${y}`;
					if (visited.has(key)) return;
					visited.add(key);

					const withinCanvas = x >= canvasSize.minX && x < canvasSize.maxX && y >= canvasSize.minY && y < canvasSize.maxY;
					if (!withinCanvas) return;

					result.push([x, y]);


					for (const { dx, dy } of directions) {
						const newX = x + dx;
						const newY = y + dy;
						const adjacentBlock = blocks.find((b) => b.x === newX && b.y === newY);
						const adjacentName = adjacentBlock ? adjacentBlock.name : "air";

						if (adjacentName === startName) {
							depthFirstSearch(newX, newY);
						}
					}
				}

				depthFirstSearch(mouseCoords.x, mouseCoords.y);
				setSelectionCoords((prev) => {
					if (holdingAltRef.current) {
						// If holding alt, remove new magic wand selection
						return prev.filter(([x, y]) => !result.some(([x2, y2]) => x2 === x && y2 === y));
					} else if (holdingShiftRef.current) {
						// If holding shift, add magic wand selection to existing selection
						const existing = new Set(prev.map(([x, y]) => `${x},${y}`));
						const newCoords = result.filter(([x, y]) => !existing.has(`${x},${y}`));
						return [...prev, ...newCoords];
					}

					// If not holding alt or shift, replace the existing selection with the magic wand selection
					return result;
				});
				break;
			}
			case "paint-bucket": {
				const visited = new Set<string>();
				const startBlock = blocks.find((block) => block.x === mouseCoords.x && block.y === mouseCoords.y);
				const startName = startBlock ? startBlock.name : "air";

				// If the target area is already the selected block, break
				if (startName === selectedBlock) break;

				function floodFill(x: number, y: number) {
					const key = `${x},${y}`;
					if (visited.has(key)) return;
					visited.add(key);

					const withinCanvas = x >= canvasSize.minX && x < canvasSize.maxX && y >= canvasSize.minY && y < canvasSize.maxY;
					if (!withinCanvas) return;

					const block = blocks.find((b) => b.x === x && b.y === y);
					const currentName = block ? block.name : "air";

					// Only fill if the current block name matches the target block name.
					if (currentName !== startName) return;

					// Update block name or push new one
					if (block) {
						block.name = selectedBlock;
					} else {
						blocks.push({ x, y, name: selectedBlock });
					}

					// Recursive
					for (const { dx, dy } of directions) {
						floodFill(x + dx, y + dy);
					}
				}

				floodFill(mouseCoords.x, mouseCoords.y);
				setBlocks([...blocks]);
				break;
			}
			case "eyedropper": {
				const mouseBlock = blocks.find((block) => block.x === mouseCoords.x && block.y === mouseCoords.y);
				if (mouseBlock) setSelectedBlock(mouseBlock.name);
				break;
			}
			case "zoom": {
				const scaleChange = holdingAltRef.current ? -0.1 : 0.1;
				const newScale = Math.min(Math.max(scale + scaleChange * scale, 0.1), 32);
				zoom(newScale);
				break;
			}

			default:
				break;
		}
	}, [tool, holdingAltRef, scale, mouseCoords, blocks, selectedBlock, canvasSize, setSelectionCoords, setBlocks, setSelectedBlock, zoom]);

	const onKeyDown = useCallback(
		async (e: React.KeyboardEvent) => {
			switch (e.key) {
				case "Escape":
					setSelectionLayerBlocks([]);
					break;
				case "Enter":
					selection.confirm(blocks, selectionLayerBlocks, setBlocks, setSelectionLayerBlocks);
					break;
				case " ": // Space
					setDragging(true);
					oldToolRef.current = tool;
					setTool("hand");
					setCssCursor("grabbing");
					break;
				case "Shift":
					holdingShiftRef.current = true;
					break;
				case "Alt":
					holdingAltRef.current = true;
					if (tool === "zoom") setCssCursor("zoom-out");
					break;
				case "Delete": {
					setBlocks((prev) => prev.filter((b) => !selectionCoords.some(([x2, y2]) => x2 === b.x && y2 === b.y)));
					break;
				}
				case "a": {
					if (!e.ctrlKey) return;
					e.preventDefault();

					const newSelection: CoordinateArray = [];

					for (let x = canvasSize.minX; x < canvasSize.maxX; x++) {
						for (let y = canvasSize.minY; y < canvasSize.maxY; y++) {
							newSelection.push([x, y]);
						}
					}

					setSelectionCoords(newSelection);
					break;
				}
				case "c": {
					if (!e.ctrlKey) return;
					clipboard.copy(selectionCoords, blocks);
					break;
				}
				case "v": {
					if (!e.ctrlKey) return;
					clipboard.paste(setSelectionLayerBlocks, setSelectionCoords, setTool);
					break;
				}
				case "1":
					setTool("hand");
					break;
				case "2":
					setTool("move");
					break;
				case "3":
					setTool("rectangle-select");
					break;
				case "4":
					setTool("lasso");
					break;
				case "5":
					setTool("magic-wand");
					break;
				case "6":
					setTool("pencil");
					break;
				case "7":
					setTool("eraser");
					break;
				case "8":
					setTool("paint-bucket");
					break;
				case "9":
					setTool("eyedropper");
					break;
				case "0":
					setTool("zoom");
					break;
				case "ArrowRight": {
					// Debug key combination
					if (!e.altKey && !e.shiftKey) return;

					const newBlocks: Block[] = [];

					Object.keys(blockData).forEach((name, index) => {
						const x = index % 16;
						const y = Math.floor(index / 16);
						newBlocks.push({ name, x, y });
					});

					setBlocks(newBlocks);
					break;
				}
			}
		},
		[
			tool,
			blocks,
			selectionCoords,
			selectionLayerBlocks,
			canvasSize,
			blockData,
			setBlocks,
			setCssCursor,
			setSelectionCoords,
			setSelectionLayerBlocks,
			setTool,
		]
	);

	const onKeyUp = useCallback(
		(e: React.KeyboardEvent) => {
			switch (e.key) {
				case " ": // Space
					setDragging(false);
					setCssCursor("grab");

					if (!oldToolRef.current) return;
					setTool(oldToolRef.current);
					break;
				case "Shift":
					holdingShiftRef.current = false;
					break;
				case "Alt":
					holdingAltRef.current = false;
					if (tool === "zoom") setCssCursor("zoom-in");
					break;
			}
		},
		[setCssCursor, setTool, tool]
	);

	// Tool cursor handler
	useEffect(() => {
		const cursorMapping: Partial<Record<Tool, string>> = {
			hand: "grab",
			move: "move",
			zoom: "zoom-in",
		};

		setCssCursor(cursorMapping[tool] || "crosshair");
	}, [tool]);

	// Resize canvas handler
	useEffect(() => {
		const container = stageContainerRef.current;
		if (!container) return;

		const resizeCanvas = () => {
			setStageSize({
				width: container.offsetWidth,
				height: container.offsetHeight,
			});
		};

		const resizeObserver = new ResizeObserver(resizeCanvas);
		resizeObserver.observe(container);

		resizeCanvas();
		return () => resizeObserver.disconnect();
	}, [stageContainerRef, setStageSize]);

	// Window events handler
	useEffect(() => {
		const onBeforeUnload = (e: BeforeUnloadEvent) => {
			e.preventDefault();
		};

		window.addEventListener("beforeunload", onBeforeUnload);

		return () => {
			window.removeEventListener("beforeunload", onBeforeUnload);
		};
	}, [onKeyDown, onKeyUp]);

	return (
		<div ref={stageContainerRef} style={{ cursor: cssCursor }} className="relative w-full h-full bg-zinc-200 dark:bg-black">
			<Stage
				width={stageSize.width}
				height={stageSize.height}
				tabIndex={0}
				onKeyDown={onKeyDown}
				onKeyUp={onKeyUp}
				onMouseMove={onMouseMove}
				onMouseDown={onMouseDown}
				onMouseUp={onMouseUp}
				onWheel={onWheel}
				onClick={onClick}
				options={{ backgroundAlpha: 0 }}
			>
				<Blocks blocks={visibleBlocks} missingTexture={missingTexture} textures={textures} coords={coords} scale={scale} version={version} />
				{/* Selection layer */}
				<Blocks
					isSelectionLayer
					blocks={selectionLayerBlocks}
					missingTexture={missingTexture}
					textures={textures}
					coords={coords}
					scale={scale}
					version={version}
				/>

				<Container x={coords.x} y={coords.y} scale={scale}>
					{settings.canvasBorder && <CanvasBorder canvasSize={canvasSize} isDark={isDark} />}
					<Cursor mouseCoords={mouseCoords} radius={radius} isDark={isDark} />
					<Selection selection={selectionCoords} coords={coords} scale={scale} isDark={isDark} />
				</Container>

				{settings.grid && (
					<Container filters={[new PIXI.AlphaFilter(0.1)]}>
						<Grid stageSize={stageSize} coords={coords} scale={scale} isDark={isDark} />
					</Container>
				)}
			</Stage>

			<CursorInformation mouseCoords={mouseCoords} />
			<CanvasInformation />

			<SelectionBar />
		</div>
	);
}

export default Canvas;
