import { Chip } from "./chips/chip";
import { Circuit } from "./circuit";
import { ControlledPin } from "./pins/controlledPin";
import { Pin } from "./pins/pin";
import { State } from "./_utils/state";
import { Wire } from "./wires/wire";
import { Vector2 } from "@prozilla-os/core";
import { CONTROLLER, PIN, WIRE } from "../constants/logicSim.const";

export class InputHandler {
	circuit!: Circuit;
	canvas!: HTMLCanvasElement;

	mousePosition = Vector2.ZERO;

	isPlacing = false;
	snapping = false;
	placingOffset = Vector2.ZERO;
	previousPlacement!: Vector2 | null;
	placingWire!: Wire | null;
	placingChip!: Chip | null;
	placingPin!: ControlledPin | null;

	constructor(circuit: Circuit) {
		Object.assign(this, { circuit });
	}

	setMousePosition(event: MouseEvent) {
		const rect = this.canvas.getBoundingClientRect();
		this.mousePosition.x = event.clientX - rect.left;
		this.mousePosition.y = event.clientY - rect.top;
	}

	init() {
		this.canvas = this.circuit.canvas;
		this.mousePosition = Vector2.ZERO;

		// TO DO: add support for touch devices

		this.canvas.addEventListener("mousemove", this.onMouseMove);
		this.canvas.addEventListener("mouseup", this.onMouseUp);
		this.canvas.addEventListener("contextmenu", this.onMouseUp);
		this.canvas.addEventListener("mousedown", this.onMouseDown);

		window.addEventListener("keydown", this.onKeyDown);
		window.addEventListener("keyup", this.onKeyUp);
	}

	cleanup() {
		this.canvas.removeEventListener("mousemove", this.onMouseMove);
		this.canvas.removeEventListener("mouseup", this.onMouseUp);
		this.canvas.removeEventListener("contextmenu", this.onMouseUp);
		this.canvas.removeEventListener("mousedown", this.onMouseDown);

		window.removeEventListener("keydown", this.onKeyDown);
		window.removeEventListener("keyup", this.onKeyUp);
	}

	reset() {
		this.placingWire = null;
		this.placingChip = null;
		this.placingPin = null;
		this.previousPlacement = null;
		this.placingOffset = Vector2.ZERO;
		this.isPlacing = false;
	}

	onMouseMove = (event?: MouseEvent) => {
		if (event != null)
			this.setMousePosition(event);

		if (this.placingWire != null) {
			this.updateWirePlacement();
			return;
		}

		if (this.placingChip != null) {
			this.updateChipPlacement();
			return;
		}

		const isHoveringPinHandle = (pin: ControlledPin) => {
			const top = pin.position.y - CONTROLLER.radius;
			const bottom = pin.position.y + CONTROLLER.radius;

			return (this.mousePosition.y > top && this.mousePosition.y < bottom);
		};

		if (this.placingPin != null) {
			let invalidPlacement = this.mousePosition.x > CONTROLLER.handleTrackWidth && this.mousePosition.x < this.circuit.size.x - CONTROLLER.handleTrackWidth;

			if (invalidPlacement) {
				this.cancelPinPlacement();
				return;
			}

			if (this.placingPin.isInput) {
				this.circuit.inputPins.forEach((pin, index) => {
					if (invalidPlacement || index == this.circuit.inputPins.length - 1)
						return;

					if (isHoveringPinHandle(pin))
						invalidPlacement = true;
				});
			} else {
				this.circuit.outputPins.forEach((pin, index) => {
					if (invalidPlacement || index == this.circuit.outputPins.length - 1)
						return;

					if (isHoveringPinHandle(pin))
						invalidPlacement = true;
				});
			}

			if (invalidPlacement) {
				this.cancelPinPlacement();
			} else {
				this.updatePinPlacement();
			}
		} else {
			if (this.mousePosition.x < CONTROLLER.handleTrackWidth) {
				let invalidPlacement = false;

				this.circuit.inputPins.forEach((pin) => {
					if (invalidPlacement)
						return;

					if (isHoveringPinHandle(pin))
						invalidPlacement = true;
				});

				if (!invalidPlacement)
					this.startPinPlacement(true);
			} else if (this.mousePosition.x > this.circuit.size.x - CONTROLLER.handleTrackWidth) {
				let invalidPlacement = false;

				this.circuit.outputPins.forEach((pin) => {
					if (invalidPlacement)
						return;

					if (isHoveringPinHandle(pin))
						invalidPlacement = true;
				});

				if (!invalidPlacement)
					this.startPinPlacement(false);
			}
		}
	};

	onClickPin(pin: Pin) {
		if (this.placingWire != null) {
			this.endWirePlacement(pin);
		} else {
			this.startWirePlacement(pin);
		}
	}

	onMouseUp = (event: MouseEvent) => {
		event.preventDefault();
		this.setMousePosition(event);

		if (event.button === 2) {
			if (this.placingWire != null)
				this.cancelWirePlacement();
			if (this.placingChip != null)
				this.cancelChipPlacement();
		} else if (event.button === 0) {
			let eventComplete = false;

			this.circuit.inputPins.forEach((pin: Pin) => {
				if (this.mousePosition.getDistance(pin.position.x - CONTROLLER.pinOffset, pin.position.y) <= CONTROLLER.radius) {
					pin.setState(State.invert(pin.state));
					eventComplete = true;
				} else if (this.mousePosition.getDistance(pin.position.x, pin.position.y) <= PIN.radius) {
					this.onClickPin(pin);
					eventComplete = true;
				}
			});
	
			if (eventComplete)
				return;
	
			this.circuit.outputPins.forEach((pin: Pin) => {
				if (this.mousePosition.getDistance(pin.position.x, pin.position.y) <= PIN.radius) {
					this.onClickPin(pin);
					eventComplete = true;
				}
			});
	
			if (eventComplete)
				return;

			this.circuit.chips.forEach((chip) => {
				chip.inputPins.concat(chip.outputPins).forEach((pin) => {
					if (this.mousePosition.getDistance(pin.position.x, pin.position.y) <= PIN.radius) {
						this.onClickPin(pin);
						eventComplete = true;
					}
				});
			});

			if (eventComplete)
				return;
	
			if (this.placingWire != null)
				this.anchorWirePlacement();

			if (this.placingChip != null)
				this.endChipPlacement();

			if (this.placingPin != null)
				this.endPinPlacement();
		}
	};

	onMouseDown = (event: MouseEvent) => {
		event.preventDefault();
		this.setMousePosition(event);

		if (event.button !== 0 || this.isPlacing)
			return;

		this.circuit.chips.forEach((chip, index) => {
			if (!this.isPlacing && this.circuit.isPointInsideRect(chip, this.mousePosition)) {
				let hoveringPin = false;

				chip.inputPins.concat(chip.outputPins).forEach((pin) => {
					if (pin.position.getDistance(this.mousePosition.x, this.mousePosition.y) <= PIN.radius)
						hoveringPin = true;
				});

				if (!hoveringPin)
					this.editChipPlacement(chip, index);
			}
		});
	};

	onKeyDown = (event: KeyboardEvent) => {
		switch (event.key) {
			case "Shift":
				event.preventDefault();
				this.snapping = true;
				this.onMouseMove();
				break;
			case "Backspace":
			case "Delete":
			case "Escape":
				event.preventDefault();
				if (this.placingWire != null)
					this.cancelWirePlacement();
				if (this.placingChip != null)
					this.cancelChipPlacement();
				break;
		}
	};

	onKeyUp = (event: KeyboardEvent) => {
		switch (event.key) {
			case "Shift":
				event.preventDefault();
				this.snapping = false;
				this.onMouseMove();
				break;
		}
	};

	startWirePlacement(pin: Pin) {
		const isInputPin = pin.isPointingRight;

		// Start wire placement
		const inputPin = isInputPin ? pin : undefined;
		const outputPin = !isInputPin ? pin : undefined;
		const anchorPoint = this.mousePosition.clone;

		this.placingWire = new Wire(this.circuit, "red", inputPin, outputPin, [anchorPoint]);

		if (!isInputPin)
			this.placingWire.placedBackwards = true;

		this.circuit.wires.push(this.placingWire);
	}

	snapWireHorizontally(lastAnchorPoint: Vector2, previousAnchorPoint: Vector2) {
		lastAnchorPoint.x = this.mousePosition.x;
		lastAnchorPoint.y = previousAnchorPoint.y;

		// Snapping horizontal wire to other wires
		let anchorPoints: Vector2[] = [];

		this.circuit.wires.forEach((wire, index) => {
			if (index < this.circuit.wires.length - 1)
				anchorPoints = anchorPoints.concat(wire.anchorPoints);
		});

		let closestPositionX: number | undefined;
		let closestDistance: number | undefined;

		anchorPoints.forEach((point) => {
			const distance = Math.abs(this.mousePosition.x - point.x);

			if (closestDistance == null || closestDistance > distance) {
				closestPositionX = point.x;
				closestDistance = distance;
			}
		});

		if (closestDistance != null && closestPositionX != null && closestDistance < WIRE.snappingSensitivity)
			lastAnchorPoint.x = closestPositionX;
	}

	snapWireVertically(lastAnchorPoint: Vector2, previousAnchorPoint: Vector2) {
		lastAnchorPoint.x = previousAnchorPoint.x;
		lastAnchorPoint.y = this.mousePosition.y;

		// Snapping vertical wire to pins
		let pins: Pin[];

		if (!this.placingWire?.placedBackwards) {
			pins = this.circuit.outputPins;

			this.circuit.chips.forEach((chip) => {
				pins = pins.concat(chip.inputPins);
			});
		} else {
			pins = this.circuit.inputPins;

			this.circuit.chips.forEach((chip) => {
				pins = pins.concat(chip.outputPins);
			});
		}

		let closestPositionY: number | undefined;
		let closestDistance: number | undefined;

		pins.forEach((pin) => {
			const distance = Math.abs(this.mousePosition.y - pin.position.y);

			if (closestDistance == null || closestDistance > distance) {
				closestPositionY = pin.position.y;
				closestDistance = distance;
			}
		});

		if (closestDistance != null && closestPositionY != null && closestDistance < WIRE.snappingSensitivity)
			lastAnchorPoint.y = closestPositionY;
	}

	updateWirePlacement() {
		const anchorCount = this.placingWire?.anchorPoints.length;
		if (anchorCount == null) return;
		const lastAnchorPoint = this.placingWire?.anchorPoints[anchorCount - 1];
		if (lastAnchorPoint == null) return;

		if (!this.snapping) {
			lastAnchorPoint.x = this.mousePosition.x;
			lastAnchorPoint.y = this.mousePosition.y;
		} else {
			// Wire snapping
			let previousAnchorPoint: Vector2 | undefined;

			if (anchorCount >= 2) {
				previousAnchorPoint = this.placingWire?.anchorPoints[anchorCount - 2];
			} else if (!this.placingWire?.placedBackwards) {
				previousAnchorPoint = this.placingWire?.inputPin.position;
			} else {
				previousAnchorPoint = this.placingWire?.outputPin.position;
			}

			if (previousAnchorPoint == null) return;
	
			const deltaX = Math.abs(this.mousePosition.x - previousAnchorPoint.x);
			const deltaY = Math.abs(this.mousePosition.y - previousAnchorPoint.y);
	
			if (deltaX > deltaY) {
				this.snapWireHorizontally(lastAnchorPoint, previousAnchorPoint);
			} else {
				this.snapWireVertically(lastAnchorPoint, previousAnchorPoint);
			}
		}
	}

	anchorWirePlacement() {
		this.placingWire?.anchorPoints.push(this.mousePosition.clone);
	}

	cancelWirePlacement() {
		this.placingWire = null;
		this.isPlacing = false;
		this.circuit.wires.pop();
	}

	endWirePlacement(pin: Pin) {
		const isInputPin = pin.isPointingRight;

		if (this.placingWire == null) return;

		let correctPlacement = false;
		if (!isInputPin && !this.placingWire.placedBackwards) {
			this.placingWire.outputPin = pin;
			correctPlacement = true;
		} else if (isInputPin && this.placingWire.placedBackwards) {
			this.placingWire.inputPin = pin;
			correctPlacement = true;
		}

		if (correctPlacement) {
			this.placingWire.anchorPoints.pop();
			this.placingWire.inputPin.addOutputWire(this.placingWire);
			this.placingWire.inputPin.update();

			this.placingWire = null;
			this.isPlacing = false;
		}
	}

	startChipPlacement(chip: Chip) {
		const newChip = new Chip(this.circuit, chip.name, chip.color, false, chip.inputCount, chip.outputCount);
		newChip.setLogic(chip.logic);
		newChip.position = new Vector2(
			this.mousePosition.x - newChip.size.x / 2,
			this.mousePosition.y - newChip.size.y / 2
		);

		this.placingChip = newChip;
		this.isPlacing = true;
		this.circuit.chips.push(newChip);
	}

	editChipPlacement(chip: Chip, index: number) {
		this.placingOffset = new Vector2(
			(chip.position.x + chip.size.x / 2) - this.mousePosition.x,
			(chip.position.y + chip.size.y / 2) - this.mousePosition.y
		);
		this.previousPlacement = chip.position.clone;
		this.circuit.chips.push(this.circuit.chips.splice(index, 1)[0]);

		this.placingChip = chip;
		this.isPlacing = true;
	}

	updateChipPlacement() {
		if (this.placingChip == null) return;
		this.placingChip.position.x = this.mousePosition.x - this.placingChip.size.x / 2 + this.placingOffset.x;
		this.placingChip.position.y = this.mousePosition.y - this.placingChip.size.y / 2 + this.placingOffset.y;
	}

	cancelChipPlacement() {
		if (this.placingChip == null) return;

		if (this.previousPlacement != null) {
			this.placingChip.position = this.previousPlacement;
			this.previousPlacement = null;
		} else {
			this.circuit.chips.pop();
		}

		this.placingChip = null;
		this.isPlacing = false;
	}

	endChipPlacement() {
		this.placingChip = null;
		this.isPlacing = false;
		this.placingOffset = Vector2.ZERO;
	}

	startPinPlacement(isInput: boolean) {
		const newPin = new ControlledPin(this.circuit, "PIN", isInput);

		newPin.position.x = CONTROLLER.handleTrackWidth + CONTROLLER.pinOffset + CONTROLLER.radius;
		newPin.position.y = this.mousePosition.y;

		if (isInput) {
			this.circuit.inputPins.push(newPin);
		} else {
			newPin.position.x = this.circuit.size.x - newPin.position.x;
			this.circuit.outputPins.push(newPin);
		}

		this.placingPin = newPin;
	}

	updatePinPlacement() {
		if (this.placingPin != null)
			this.placingPin.position.y = this.mousePosition.y;
	}

	cancelPinPlacement() {
		if (this.placingPin?.isInput) {
		 	this.circuit.inputPins.pop();
		} else {
			this.circuit.outputPins.pop();
		}

		this.placingPin = null;
	}

	endPinPlacement() {
		this.placingPin = null;
	}

	isPlacingPin(pin: Pin, index: number) {
		if (!pin.isControlled || this.placingPin == null || this.placingPin.isInput != pin.isInput)
			return false;

		if (pin.isInput) {
			return index == this.circuit.inputPins.length - 1;
		} else {
			return index == this.circuit.outputPins.length - 1;
		}
	}
}