import { createContext, useContext, type ReactNode } from 'react';
import { clamp, getGlobalStates, randomUUID } from '@wener/utils';
import { createStore } from 'zustand';
import { mutative } from 'zustand-mutative';

export const WindowContext = createContext<ReactWindow | undefined>(undefined);

export function useRootWindow(): ReactWindow {
	return getRootWindow();
}

export function useWindow(): ReactWindow {
	let win = useContext(WindowContext);
	if (!win) {
		console.trace(`useWindow used outside of WindowContext, fallback to root window`);
	}
	return win || getRootWindow();
}

export function useWindowContext() {
	let win = useContext(WindowContext);
	if (!win) {
		throw new Error(`useWindowContext must be used within a WindowContext`);
	}
	return win;
}

export function getRootWindow(): ReactRootWindow {
	return getGlobalStates('ReactRootWindow', () => new ReactRootWindow());
}

interface RootWindowState {
	maximized?: ReactWindow;
	windows: ReactWindow[];
}

function createRootWindowStore(init: Partial<RootWindowState> = {}) {
	return createStore(
		mutative<RootWindowState>((setState, getState, store) => {
			return {
				maximized: undefined,
				windows: [],
				...init,
			};
		}),
	);
}

export type RootWindowStore = ReturnType<typeof createRootWindowStore>;

interface WindowBaseState {
	x: number;
	y: number;
	width: number;
	height: number;
	zIndex: number;

	minimized: boolean;
	maximized: boolean;
	canMaximize: boolean;
	canMinimize: boolean;
	canResize: boolean;
	canDrag: boolean;
	canFullscreen: boolean;
	fullscreen: boolean;

	frameless: boolean;

	minWidth?: number;
	minHeight?: number;
	maxWidth?: number;
	maxHeight?: number;

	title?: string;
	icon?: ReactNode;
	render?: () => ReactNode;

	metadata: Record<string, any>;
	attributes: Record<string, any>;
	properties: Record<string, any>;

	windows: ReactWindow[];
}

export interface WindowState extends WindowBaseState {
	windowElement?: HTMLElement | null;
	bodyElement?: HTMLElement | null;
	childrenElement?: HTMLElement | null; // for windows
}

const WindowSizes = {
	xs: { width: 200, height: 200 },
	sm: { width: 400, height: 300 },
	md: { width: 600, height: 400 },
	lg: { width: 800, height: 600 },
	xl: { width: 1000, height: 800 },
	xxl: { width: 1200, height: 800 },
};

const FrameSize = {
	title: 28,
	border: 1,
	width: 2,
	height: 28 + 2,
};

function normalize(init: Partial<WindowBaseState>): WindowBaseState {
	return {
		zIndex: 0,
		minimized: false,
		maximized: false,
		canMaximize: true,
		canMinimize: true,
		canResize: true,
		canDrag: true,
		minWidth: 200,
		minHeight: 200,
		metadata: {},
		attributes: {},
		properties: {},
		frameless: false,
		canFullscreen: true,
		fullscreen: false,

		windows: [],
		...init,
		...normalizeCoordinate(init),
	};
}

function normalizeCoordinate(
	{
		width = WindowSizes.md.width,
		height = WindowSizes.md.height,
		x,
		y,
	}: {
		x?: number;
		y?: number;
		width?: number;
		height?: number;
	},
	{ center }: { center?: boolean } = {},
) {
	const { innerWidth: ww, innerHeight: wh } =
		typeof window === 'undefined'
			? {
					innerWidth: 800,
					innerHeight: 600,
				}
			: window;

	width = clamp(width, WindowSizes.xs.width, ww);
	height = clamp(height, WindowSizes.xs.height, wh);

	let cx = (ww - width) / 2;
	let cy = (wh - height) / 2;

	if (center) {
		x = cx;
		y = cy;
	}

	x = clamp(x || cx, 0, ww - width);
	y = clamp(y || cy, 0, wh - height);
	return {
		x,
		y,
		width,
		height,
	};
}

function createWindowStore(init: Partial<WindowState> = {}) {
	return createStore(
		mutative<WindowState>((setState, getState, store) => {
			return normalize(init);
		}),
	);
}

type WindowStore = ReturnType<typeof createWindowStore>;

export class ReactWindow extends EventTarget {
	public readonly id: string;
	public readonly key: string;
	readonly store: WindowStore;
	readonly parent?: ReactWindow;

	private static MaximizedWindow?: ReactWindow;

	constructor({ id, key, store, parent }: { id: string; key?: string; store?: WindowStore; parent?: ReactWindow }) {
		super();
		this.id = id;
		this.key = key || id;
		this.parent = parent;
		this.store = store || createWindowStore();
	}

	get state() {
		return this.store.getState();
	}

	get body() {
		return this.state.bodyElement;
	}

	setBody = (ref: HTMLElement | null | undefined) => {
		let current = this.state.bodyElement;
		if (current === ref) {
			return;
		}
		// do initial focus
		if (!current) {
			ref?.focus();
		}
		this.store.setState({ bodyElement: ref });
	};

	close = (data?: any) => {
		this.dispatchEvent(new CustomEvent('close', { detail: data }));
	};

	focus = () => {
		let ele = this.state.bodyElement;
		if (!ele) {
			return;
		}
		if (!document.activeElement || !ele?.contains(document.activeElement)) {
			ele?.focus();
			this.dispatchEvent(new Event('focus'));
		}
	};

	minimize = (minimize?: boolean) => {
		let { minimized: current, canMinimize } = this.state;
		if (!canMinimize) {
			return;
		}
		minimize = minimize ?? !current;
		if (minimize === current) {
			return;
		}

		this.store.setState((s) => {
			s.minimized = minimize;
			s.maximized = false;
		});
		this.dispatchEvent(new Event('minimize'));
	};

	maximize = (maximize?: boolean) => {
		const { maximized: current, canMaximize } = this.state;
		if (!canMaximize) {
			return;
		}
		maximize = maximize ?? !current;
		if (maximize === current) {
			return;
		}

		this.store.setState((s) => {
			if (maximize && !s.maximized) {
				s.maximized = true;
				s.minimized = false;
				s.properties['last'] = [s.x, s.y, s.width, s.height];
				s.width = window.innerWidth;
				s.height = window.innerHeight;
				s.x = 0;
				s.y = 0;
				ReactWindow.MaximizedWindow = this;
			} else if (!maximize && s.maximized) {
				s.maximized = false;
				s.minimized = false;
				const [x, y, width, height] = s.properties['last'] ?? [];
				s.width = width;
				s.height = height;
				s.x = x;
				s.y = y;
				Object.assign(s, normalizeCoordinate(s));
				ReactWindow.MaximizedWindow = undefined;
			}
		});

		if (maximize) {
			this.dispatchEvent(new Event('maximize'));
		} else {
			this.dispatchEvent(new Event('restore'));
		}
	};

	center = () => {
		this.store.setState(normalizeCoordinate(this.store.getState(), { center: true }));
	};

	open = (opts: WindowOpenOptions) => {
		getRootWindow().open(opts);
	};
}

class ReactRootWindow extends ReactWindow {
	private zIndex = 1;
	current?: ReactWindow;

	constructor() {
		super({ id: 'root' });
	}

	get top(): ReactWindow | undefined {
		if (this.current) {
			return this.current;
		}
		return this.windows.filter((v) => !v.state.minimized).toSorted((a, b) => a.state.zIndex - b.state.zIndex)[0];
	}

	get windows() {
		return this.state.windows;
	}

	private handleFocusIn = (e: Event, win: ReactWindow) => {
		this.setActive(win);
	};

	private handleFocusOut = (e: Event, win: ReactWindow) => {
		if (win === this.current) {
			this.current = undefined;
		}
	};

	setActive(win: ReactWindow) {
		try {
			const { zIndex } = win.state;
			if (zIndex === this.zIndex) {
				this.current = win;
				win.minimize(false);
				return;
			}
			win.store.setState({ zIndex: ++this.zIndex });
			this.current = win;
		} finally {
			win.focus();
		}
	}

	private find(s: { key?: string }) {
		if (s.key) return this.windows.find((v) => v.key === s.key);
	}

	toggle = (opts: WindowOpenOptions) => {
		let found = this.find(opts);
		if (found) {
			found.close();
			return;
		}
		return this.open(opts);
	};

	open = (opts: WindowOpenOptions) => {
		if (opts.key) {
			let existing = this.windows.find((v) => v.key === opts.key);
			if (existing) {
				this.setActive(existing);
				return existing;
			}
		}

		let id = randomUUID();
		let key = opts.key || id;

		if (!opts.frameless) {
			const { width: fw, height: fh } = FrameSize;
			const wkeys: (keyof WindowBaseState)[] = ['width', 'maxWidth', 'minWidth'];
			const hkeys: (keyof WindowBaseState)[] = ['height', 'maxHeight', 'minHeight'];

			for (let key of wkeys) {
				if (opts[key]) {
					(opts as any)[key] += fw;
				}
			}
			for (let key of hkeys) {
				if (opts[key]) {
					(opts as any)[key] += fh;
				}
			}
		}

		let root = (this.parent || getRootWindow()).store;

		let store = createWindowStore({
			...opts,
			zIndex: this.zIndex++,
		});

		let child = new ReactWindow({
			id,
			key,
			store,
		});

		child.addEventListener('close', () => {
			root.setState((s) => {
				s.windows = s.windows.filter((v) => v !== child);
				this.current === child && (this.current = undefined);
			});
		});

		child.addEventListener('focusin', (e) => this.handleFocusIn(e, child));
		child.addEventListener('focusout', (e) => this.handleFocusOut(e, child));

		root.setState((s) => {
			s.windows.push(child as any);
		});

		this.setActive(child);
		return child;
	};

	close = () => {
		this.windows.forEach((v) => v.close());
	};
}

export interface WindowOpenOptions extends Partial<WindowBaseState> {
	key?: string;
}
