import "./index.scss";

import { createSettingsStore } from "@olenbetong/appframe-react";

import clsx from "clsx";
import type React from "react";
import { Children, forwardRef, useEffect, useRef, useState } from "react";

const article = globalThis.af?.article?.id ?? globalThis.window?.location.pathname.substring(1).split("/")[0];
const settingsStore = createSettingsStore(`AfSplitContainers.${globalThis.af?.userSession?.login ?? "anonymous"}`);

type SplitContainerSettings = {
	layoutVersion: string | number;
	fixedSize: number;
};

const settingsCache: Record<string, SplitContainerSettings> = {};

async function persistState(key: string, layoutVersion: string | number, fixedSize: number | null | undefined) {
	if (fixedSize != null) {
		let settings = {
			layoutVersion,
			fixedSize,
		};

		settingsCache[key] = settings;
		await settingsStore.setItem<SplitContainerSettings>(key, settings);
	}
}

async function getSettings(key: string, layoutVersion: string | number): Promise<number | null> {
	let settings = settingsCache[key] ?? (await settingsStore.getItem<SplitContainerSettings>(key));

	if (settings && settings.layoutVersion === layoutVersion) {
		return settings.fixedSize;
	}

	return null;
}

// Load all article grid layouts even if the grid is not mounted yet.
// This ways the persisted state can be restored faster after the initial
// page load.
settingsStore.keys().then((keys) =>
	Promise.allSettled(
		keys
			.filter((key) => key.startsWith(article))
			.map((key) =>
				settingsStore.getItem<SplitContainerSettings>(key).then((settings) => {
					if (settings && !settingsCache[key]) {
						settingsCache[key] = settings;
					}
				}),
			),
	),
);

type DividerProps = {
	className?: string;
	direction: "horizontal" | "vertical";
	isDragging: boolean;
	min: number;
	max: number;
	size: number;
};

const Divider = forwardRef<HTMLDivElement, DividerProps>(function Divider(props: DividerProps, ref) {
	return (
		// oxlint-disable-next-line jsx-a11y/prefer-tag-over-role -- NO!
		<div
			className={clsx("ObSplitContainer-divider", props.className)}
			tabIndex={0}
			ref={ref}
			role="separator"
			aria-orientation={props.direction}
			aria-valuemin={props.min}
			aria-valuemax={props.max}
			aria-valuenow={props.size}
		>
			<span>•••</span>
		</div>
	);
});

type SplitRootProps = React.HTMLProps<HTMLDivElement> & {
	className?: string;
	children: React.ReactNode;
	direction: "horizontal" | "vertical";
};

function SplitRoot({ className, direction, ...props }: SplitRootProps) {
	return <div className={clsx(className, "ObSplitContainer-root", direction)} {...props} />;
}

type SplitPanelProps = {
	children: React.ReactNode;
	className?: string;
	style?: React.CSSProperties;
} & (
	| {
			fixed: true;
			size: number;
	  }
	| {
			fixed: false;
			size?: number;
	  }
);

function SplitPanel(props: SplitPanelProps) {
	let style = {
		flex: props.fixed ? `0 0 ${Math.floor(props.size)}px` : "1 1 100%",
		...props.style,
	};

	return (
		<div className={clsx(props.className, "ObSplitContainer-panel")} style={style}>
			{props.children}
		</div>
	);
}

export type SplitContainerProps = {
	className?: string;
	panelClassName?: string;
	children: React.ReactNode[];
	direction?: "horizontal" | "vertical";
	fixedPanel?: 1 | 2;
	defaultFixedSize?: number;
	min?: number;
	max?: number;
	/**
	 * When the fixed size is persisted, changing the layoutVersion will force clients
	 * to reset to the default value.
	 */
	layoutVersion?: string | number;
	/**
	 * If you set an id, the fixed size will be persisted, and restored after page load
	 */
	id?: string;
};

export function SplitContainer({
	className,
	children,
	direction = "vertical",
	fixedPanel = 1,
	panelClassName,
	defaultFixedSize = 400,
	layoutVersion = 1,
	min = 0,
	max,
	id,
}: SplitContainerProps) {
	let [fixedSize, setFixedSize] = useState<number>(defaultFixedSize);
	let [isDragging, setIsDragging] = useState(false);
	let dividerRef = useRef<HTMLDivElement>(null);

	if (Children.count(children) !== 2) {
		throw Error("SplitContainer requires exactly 2 children");
	}

	let containerRef = useRef<HTMLDivElement>(null);
	let [containerSize, setContainerSize] = useState<number>(0);

	useEffect(() => {
		if (!containerRef.current) return;

		let resizeObserver = new ResizeObserver((entries) => {
			for (let entry of entries) {
				if (entry.contentRect) {
					setContainerSize(direction === "horizontal" ? entry.contentRect.width : entry.contentRect.height);
				}
			}
		});

		resizeObserver.observe(containerRef.current);
		return () => resizeObserver.disconnect();
	}, [direction]);

	useEffect(() => {
		if (id) {
			let key = `${article}.${id}`;

			getSettings(key, layoutVersion).then((data: number | null) => {
				if (data) {
					// Persisted state exists, and matches the current layout version,
					// so restore the state.
					setFixedSize(data);

					return Promise.resolve();
				}
			});
		}
	}, [id, layoutVersion]);

	let childrenArray = Children.toArray(children);

	useEffect(() => {
		let divider = dividerRef.current;
		if (divider) {
			let initialPosition: { clientX: number; clientY: number; size: number } = {
				clientX: 0,
				clientY: 0,
				size: 0,
			};
			let movingPanel: HTMLElement = (
				fixedPanel === 1 ? divider.previousElementSibling : divider.nextElementSibling
			) as HTMLElement;

			function clampSize(size: number) {
				let adjustedContainerSize = containerSize - 10; // make sure divider will not be stuck outside the container
				let maxSize = Math.min(max ?? adjustedContainerSize, adjustedContainerSize);
				if (size < min) {
					size = min;
				} else if (size > maxSize) {
					size = maxSize;
				}
				return size;
			}

			function getNewSize(event: PointerEvent) {
				let deltaSize =
					direction === "horizontal"
						? event.clientX - initialPosition.clientX
						: event.clientY - initialPosition.clientY;

				deltaSize *= fixedPanel === 1 ? -1 : 1;

				return clampSize(initialPosition.size - deltaSize);
			}

			function handleFixedSizeChanged(size: React.SetStateAction<number>) {
				setFixedSize((current) => {
					let newValue: number = clampSize(typeof size === "function" ? size(current) : size);

					if (id) {
						let key = `${article}.${id}`;
						persistState(key, layoutVersion, newValue);
					}

					return newValue;
				});
			}

			function handlePointerUp(event: PointerEvent) {
				let newSize = getNewSize(event);
				handleFixedSizeChanged(newSize);
				setIsDragging(false);

				movingPanel.style.flex = `0 0 ${Math.floor(newSize)}px`;
				window.removeEventListener("pointermove", handlePointerMove);
			}

			function handlePointerMove(event: PointerEvent) {
				event.preventDefault();

				let newSize = getNewSize(event);
				let divider = dividerRef.current;
				if (divider) {
					divider.setAttribute("aria-valuenow", String(newSize));
				}
				movingPanel.style.flex = `0 0 ${newSize}px`;
			}

			function handlePointerDown(event: PointerEvent) {
				setIsDragging(true);

				let rect = movingPanel.getBoundingClientRect();
				let size = direction === "horizontal" ? rect.width : rect.height;
				initialPosition = {
					clientX: event.clientX,
					clientY: event.clientY,
					size,
				};

				window.addEventListener("pointermove", handlePointerMove);
				window.addEventListener("pointerup", handlePointerUp, { once: true });
			}

			function handleKeyDown(event: KeyboardEvent) {
				let reduce = true;
				let shouldHandle = false;
				if (!event.altKey && !event.ctrlKey && !event.metaKey) {
					if (direction === "horizontal" && ["ArrowLeft", "ArrowRight"].includes(event.key)) {
						shouldHandle = true;
						reduce =
							(fixedPanel === 1 && event.key === "ArrowLeft") || (fixedPanel === 2 && event.key === "ArrowRight");
					} else if (direction === "vertical" && ["ArrowUp", "ArrowDown"].includes(event.key)) {
						shouldHandle = true;
						reduce = (fixedPanel === 1 && event.key === "ArrowUp") || (fixedPanel === 2 && event.key === "ArrowDown");
					}
				}

				if (shouldHandle) {
					event.preventDefault();
					event.stopPropagation();

					let delta = event.shiftKey ? 25 : 5;

					handleFixedSizeChanged((size) => size + (reduce ? -delta : delta));
				}
			}

			divider.addEventListener("pointerdown", handlePointerDown);
			divider.addEventListener("keydown", handleKeyDown);

			return () => {
				divider?.removeEventListener("pointerdown", handlePointerDown);
				divider?.removeEventListener("keydown", handleKeyDown);
			};
		}
	}, [direction, fixedPanel, layoutVersion, id, min, max, containerSize]);

	return (
		<SplitRoot direction={direction} className={className} ref={containerRef}>
			<SplitPanel className={clsx(isDragging && "dragging", panelClassName)} fixed={fixedPanel === 1} size={fixedSize}>
				{childrenArray[0]}
			</SplitPanel>
			<Divider
				ref={dividerRef}
				direction={direction}
				isDragging={isDragging}
				size={fixedSize}
				min={min}
				max={max ?? containerSize}
			/>
			<SplitPanel className={clsx(isDragging && "dragging", panelClassName)} fixed={fixedPanel === 2} size={fixedSize}>
				{childrenArray[1]}
			</SplitPanel>
		</SplitRoot>
	);
}
