const inputConfig = {
	clickRadius: 10, // Radius to be considered a click (pixels). If farther, turns into a drag
	clickTiming: 500, // Timing window to be considered a click (ms). If longer, turns into a drag
	dClickTiming: 500, // Timing window to be considered a double click (ms).

	keyboardHoldTiming: 100, // Timing window after which to consider holding a key (ms)
};

/**
 * Mouse input processing
 */
// Base object generator functions
function _context_coords() {
	return {
		dragging: {
			left: null,
			middle: null,
			right: null,
		},

		prev: {
			x: 0,
			y: 0,
		},

		pos: {
			x: 0,
			y: 0,
		},
	};
}
function _mouse_observers() {
	return {
		// Simple click handler
		onclick: new Observer(),
		// Double click handler (will still trigger simple click handler as well)
		ondclick: new Observer(),
		// Drag handler
		ondragstart: new Observer(),
		ondrag: new Observer(),
		ondragend: new Observer(),
		// Paint handler (like drag handler, but with no delay); will trigger during clicks too
		onpaintstart: new Observer(),
		onpaint: new Observer(),
		onpaintend: new Observer(),
	};
}

function _context_observers() {
	return {
		onwheel: new Observer(),
		onmousemove: new Observer(),
		left: _mouse_observers(),
		middle: _mouse_observers(),
		right: _mouse_observers(),
	};
}

const mouse = {
	buttons: {
		right: null,
		left: null,
		middle: null,
	},

	// Mouse Actions in Window Coordinates
	window: _context_coords(),

	// Mouse Actions in Canvas Coordinates
	canvas: _context_coords(),

	// Mouse Actions in World Coordinates
	world: _context_coords(),

	listen: {
		window: _context_observers(),
		canvas: _context_observers(),
		world: _context_observers(),
	},
};

function _mouse_state_snapshot() {
	return {
		buttons: window.structuredClone(mouse.buttons),
		window: window.structuredClone(mouse.window),
		canvas: window.structuredClone(mouse.canvas),
		world: window.structuredClone(mouse.world),
	};
}

const _double_click_timeout = {};
const _drag_start_timeout = {};

window.onmousedown = (evn) => {
	const time = new Date();

	// Processes for a named button
	const onhold = (key) => () => {
		if (_double_click_timeout[key]) {
			// ondclick event
			["window", "canvas", "world"].forEach((ctx) =>
				mouse.listen[ctx][key].ondclick.emit({
					target: evn.target,
					buttonId: evn.button,
					x: mouse[ctx].pos.x,
					y: mouse[ctx].pos.y,
					timestamp: new Date(),
				})
			);
		} else {
			// Start timer
			_double_click_timeout[key] = setTimeout(
				() => delete _double_click_timeout[key],
				inputConfig.dClickTiming
			);
		}

		// Set drag start timeout
		_drag_start_timeout[key] = setTimeout(() => {
			["window", "canvas", "world"].forEach((ctx) => {
				mouse.listen[ctx][key].ondragstart.emit({
					target: evn.target,
					buttonId: evn.button,
					x: mouse[ctx].pos.x,
					y: mouse[ctx].pos.y,
					timestamp: new Date(),
				});
				if (mouse[ctx].dragging[key]) mouse[ctx].dragging[key].drag = true;

				delete _drag_start_timeout[key];
			});
		}, inputConfig.clickTiming);

		["window", "canvas", "world"].forEach((ctx) => {
			mouse.buttons[key] = time;
			mouse[ctx].dragging[key] = {target: evn.target};
			Object.assign(mouse[ctx].dragging[key], mouse[ctx].pos);

			// onpaintstart event
			mouse.listen[ctx][key].onpaintstart.emit({
				target: evn.target,
				buttonId: evn.button,
				x: mouse[ctx].pos.x,
				y: mouse[ctx].pos.y,
				timestamp: new Date(),
			});
		});
	};

	// Runs the correct handler
	const buttons = [onhold("left"), onhold("middle"), onhold("right")];

	buttons[evn.button] && buttons[evn.button]();
};

window.onmouseup = (evn) => {
	const time = new Date();

	// Processes for a named button
	const onrelease = (key) => () => {
		["window", "canvas", "world"].forEach((ctx) => {
			const start = {
				x: mouse[ctx].dragging[key].x,
				y: mouse[ctx].dragging[key].y,
			};

			// onclick event
			const dx = mouse[ctx].pos.x - start.x;
			const dy = mouse[ctx].pos.y - start.y;

			if (
				time.getTime() - mouse.buttons[key].getTime() <
					inputConfig.clickTiming &&
				dx * dx + dy * dy < inputConfig.clickRadius * inputConfig.clickRadius
			)
				mouse.listen[ctx][key].onclick.emit({
					target: evn.target,
					buttonId: evn.button,
					x: mouse[ctx].pos.x,
					y: mouse[ctx].pos.y,
					timestamp: new Date(),
				});

			// onpaintend event
			mouse.listen[ctx][key].onpaintend.emit({
				target: evn.target,
				initialTarget: mouse[ctx].dragging[key].target,
				buttonId: evn.button,
				x: mouse[ctx].pos.x,
				y: mouse[ctx].pos.y,
				timestamp: new Date(),
			});

			// ondragend event
			if (mouse[ctx].dragging[key].drag)
				mouse.listen[ctx][key].ondragend.emit({
					target: evn.target,
					initialTarget: mouse[ctx].dragging[key].target,
					buttonId: evn.button,
					x: mouse[ctx].pos.x,
					y: mouse[ctx].pos.y,
					timestamp: new Date(),
				});

			mouse[ctx].dragging[key] = null;
		});

		if (_drag_start_timeout[key] !== undefined) {
			clearTimeout(_drag_start_timeout[key]);
			delete _drag_start_timeout[key];
		}
		mouse.buttons[key] = null;
	};

	// Runs the correct handler
	const buttons = [onrelease("left"), onrelease("middle"), onrelease("right")];

	buttons[evn.button] && buttons[evn.button]();
};

window.onmousemove = (evn) => {
	// Set Window Coordinates
	Object.assign(mouse.window.prev, mouse.window.pos);
	mouse.window.pos = {x: evn.clientX, y: evn.clientY};

	// Set Canvas Coordinates (using overlay canvas as reference)
	if (evn.target.id === "overlayCanvas") {
		Object.assign(mouse.canvas.prev, mouse.canvas.pos);
		mouse.canvas.pos = {x: evn.layerX, y: evn.layerY};
	}

	// Set World Coordinates (For now the same as canvas coords; Will be useful with infinite canvas)
	if (evn.target.id === "overlayCanvas") {
		Object.assign(mouse.world.prev, mouse.world.pos);
		mouse.world.pos = {x: evn.layerX, y: evn.layerY};
	}

	["window", "canvas", "world"].forEach((ctx) => {
		mouse.listen[ctx].onmousemove.emit({
			target: evn.target,
			px: mouse[ctx].prev.x,
			py: mouse[ctx].prev.y,
			x: mouse[ctx].pos.x,
			y: mouse[ctx].pos.y,
			timestamp: new Date(),
		});
		["left", "middle", "right"].forEach((key) => {
			// ondrag event
			if (mouse[ctx].dragging[key] && mouse[ctx].dragging[key].drag)
				mouse.listen[ctx][key].ondrag.emit({
					target: evn.target,
					initialTarget: mouse[ctx].dragging[key].target,
					px: mouse[ctx].prev.x,
					py: mouse[ctx].prev.y,
					x: mouse[ctx].pos.x,
					y: mouse[ctx].pos.y,
					timestamp: new Date(),
				});

			// onpaint event
			if (mouse[ctx].dragging[key])
				mouse.listen[ctx][key].onpaint.emit({
					target: evn.target,
					initialTarget: mouse[ctx].dragging[key].target,
					px: mouse[ctx].prev.x,
					py: mouse[ctx].prev.y,
					x: mouse[ctx].pos.x,
					y: mouse[ctx].pos.y,
					timestamp: new Date(),
				});
		});
	});
};

window.addEventListener(
	"wheel",
	(evn) => {
		evn.preventDefault();
		["window", "canvas", "world"].forEach((ctx) => {
			mouse.listen[ctx].onwheel.emit({
				target: evn.target,
				delta: evn.deltaY,
				deltaX: evn.deltaX,
				deltaY: evn.deltaY,
				deltaZ: evn.deltaZ,
				mode: evn.deltaMode,
				x: mouse[ctx].pos.x,
				y: mouse[ctx].pos.y,
				timestamp: new Date(),
			});
		});
	},
	{passive: false}
);
/**
 * Keyboard input processing
 */
// Base object generator functions

const keyboard = {
	keys: {},

	isPressed(code) {
		return this.keys[key].pressed;
	},

	isHeld(code) {
		return !!this;
	},

	shortcuts: {},
	onShortcut(shortcut, callback) {
		/**
		 * Adds a shortcut handler (shorcut must be in format: {ctrl?: bool, alt?: bool, shift?: bool, key: string (code)})
		 * key must be the "code" parameter from keydown event; A key is "KeyA" for example
		 */
		if (this.shortcuts[shortcut.key] === undefined)
			this.shortcuts[shortcut.key] = [];

		this.shortcuts[shortcut.key].push({
			ctrl: shortcut.ctrl,
			alt: shortcut.alt,
			shift: shortcut.shift,
			id: guid(),
			callback,
		});
	},
	deleteShortcut(id) {
		this.shortcuts.keys().forEach((key) => {
			this.shortcuts[key] = this.shortcuts[key].filter((v) => v.id !== id);
		});
	},

	listen: {
		onkeydown: new Observer(),
		onkeyup: new Observer(),
		onkeyholdstart: new Observer(),
		onkeyholdend: new Observer(),
		onkeyclick: new Observer(),
		onshortcut: new Observer(),
	},
};

window.onkeydown = (evn) => {
	keyboard.listen.onkeydown.emit({
		code: evn.code,
		key: evn.key,
		evn,
	});

	keyboard.keys[evn.code] = {
		pressed: true,
		held: false,
		_hold_to: setTimeout(() => {
			keyboard.keys[evn.code].held = true;
			delete keyboard.keys[evn.code]._hold_to;
			keyboard.listen.onkeyholdstart.emit({
				code: evn.code,
				key: evn.key,
				evn,
			});
		}, inputConfig.keyboardHoldTiming),
	};

	// Process shortcuts
	const callbacks = keyboard.shortcuts[evn.code];

	if (callbacks)
		callbacks.forEach((callback) => {
			if (
				!!callback.ctrl === evn.ctrlKey &&
				!!callback.alt === evn.altKey &&
				!!callback.shift === evn.shiftKey
			) {
				keyboard.listen.onshortcut.emit({
					code: evn.code,
					key: evn.key,
					id: callback.id,
					evn,
				});
				callback.callback(evn);
			}
		});
};

window.onkeyup = (evn) => {
	keyboard.listen.onkeyup.emit({
		code: evn.code,
		key: evn.key,
		evn,
	});
	if (keyboard.keys[evn.code] && keyboard.keys[evn.code].held) {
		keyboard.listen.onkeyholdend.emit({
			code: evn.code,
			key: evn.key,
			evn,
		});
	} else {
		keyboard.listen.onkeyclick.emit({
			code: evn.code,
			key: evn.key,
			evn,
		});
	}

	keyboard.keys[evn.code] = {
		pressed: false,
		held: false,
	};
};