import type { VNode } from 'vue';
import type { DropdownButtonItemArgs } from '../dropdown-button/dropdown-button-item';
import type { FormItemWrapperArgs, MarginType } from '../form/form-item-wrapper';
import { Prop, toNative } from 'vue-facing-decorator';
import { globalState } from '../../app/global-state';
import PowerduckState from '../../app/powerduck-state';
import TsxComponent, { Component } from '../../app/vuetsx';
import FormItemWrapper from '../form/form-item-wrapper';
import './css/minicolors.css';
import './css/color-picker.css';

interface ColorPickerArgs extends FormItemWrapperArgs {
	value: string;
	placeholder?: string;
	disabled?: boolean;
	changed: (newValue: string) => void;
}

interface Rgb { r: number; g: number; b: number }
interface Hsv { h: number; s: number; v: number }

const HEX_RE = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i;

const clamp = (
	n: number,
	lo: number,
	hi: number,
): number => Math.max(lo, Math.min(hi, n));

const parseHex = (value: string | null | undefined): Rgb | null => {
	const match = HEX_RE.exec(value?.trim() ?? '');
	if (match == null) {
		return null;
	}

	let hex = match[1];
	if (hex.length === 3) {
		hex = hex.split('').map(c => c + c).join('');
	}

	return {
		r: parseInt(hex.slice(0, 2), 16),
		g: parseInt(hex.slice(2, 4), 16),
		b: parseInt(hex.slice(4, 6), 16),
	};
};

const rgbToHex = ({ r, g, b }: Rgb): string => {
	const toHex = (n: number) => clamp(
		Math.round(n),
		0,
		255,
	).toString(16).padStart(2, '0');
	return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
};

const rgbToHsv = ({ r, g, b }: Rgb): Hsv => {
	const rn = r / 255;
	const gn = g / 255;
	const bn = b / 255;
	const max = Math.max(
		rn,
		gn,
		bn,
	);
	const min = Math.min(
		rn,
		gn,
		bn,
	);
	const d = max - min;
	let h = 0;
	const s = max === 0 ? 0 : d / max;
	const v = max;

	if (d !== 0) {
		switch (max) {
			case rn:
				h = ((gn - bn) / d + (gn < bn ? 6 : 0));
				break;
			case gn:
				h = ((bn - rn) / d + 2);
				break;
			case bn:
				h = ((rn - gn) / d + 4);
				break;
		}
		h /= 6;
	}

	return { h, s, v };
};

const hsvToRgb = ({ h, s, v }: Hsv): Rgb => {
	const i = Math.floor(h * 6);
	const f = h * 6 - i;
	const p = v * (1 - s);
	const q = v * (1 - f * s);
	const t = v * (1 - (1 - f) * s);

	let r = 0;
	let g = 0;
	let b = 0;
	switch (i % 6) {
		case 0:
			r = v;
			g = t;
			b = p;
			break;
		case 1:
			r = q;
			g = v;
			b = p;
			break;
		case 2:
			r = p;
			g = v;
			b = t;
			break;
		case 3:
			r = p;
			g = q;
			b = v;
			break;
		case 4:
			r = t;
			g = p;
			b = v;
			break;
		case 5:
			r = v;
			g = p;
			b = q;
			break;
	}

	return { r: r * 255, g: g * 255, b: b * 255 };
};

type DragKind = 'slider' | 'grid' | null;

@Component
class ColorPickerComponent extends TsxComponent<ColorPickerArgs> implements ColorPickerArgs {
	@Prop() label!: string | VNode;
	@Prop() labelButtons!: DropdownButtonItemArgs[];
	@Prop() subtitle!: string;
	@Prop() value!: string;
	@Prop() placeholder!: string;
	@Prop() disabled?: boolean;
	@Prop() mandatory!: boolean;
	@Prop() wrap!: boolean;
	@Prop() hint: string;
	@Prop() marginType?: MarginType;
	@Prop() appendIcon: string;
	@Prop() prependIcon: string;
	@Prop() appendClicked: () => void;
	@Prop() prependClicked: () => void;
	@Prop() changed: (newValue: string) => void;

	hue: number = 0;
	saturation: number = 0;
	brightness: number = 0;
	hexValue: string = '#000000';
	panelOpen: boolean = false;
	private suppressNextOutsideMousedown = false;

	private dragKind: DragKind = null;
	private moveListener: ((e: MouseEvent | TouchEvent) => void) | null = null;
	private upListener: (() => void) | null = null;
	private outsideListener: ((e: MouseEvent) => void) | null = null;

	mounted(): void {
		this.applyValue(this.value);
		this.outsideListener = (e: MouseEvent) => this.handleOutsideMouseDown(e);
		globalState.addEventListener('mousedown', this.outsideListener);
	}

	beforeUnmount(): void {
		if (this.outsideListener != null) {
			globalState.removeEventListener('mousedown', this.outsideListener);
		}

		this.detachDragListeners();
	}

	private applyValue(raw: string): void {
		const rgb = parseHex(raw) ?? { r: 0, g: 0, b: 0 };
		const hsv = rgbToHsv(rgb);
		this.hue = hsv.h;
		this.saturation = hsv.s;
		this.brightness = hsv.v;
		this.hexValue = rgbToHex(rgb);
	}

	private get pureHueColor(): string {
		const rgb = hsvToRgb({ h: this.hue, s: 1, v: 1 });
		return `rgb(${Math.round(rgb.r)}, ${Math.round(rgb.g)}, ${Math.round(rgb.b)})`;
	}

	private get sliderPickerY(): number {
		const rect = this.$refs.slider as HTMLElement | undefined;
		const h = rect?.clientHeight ?? 150;
		return (1 - this.hue) * h;
	}

	private get gridPickerX(): number {
		const rect = this.$refs.grid as HTMLElement | undefined;
		const w = rect?.clientWidth ?? 150;
		return this.saturation * w;
	}

	private get gridPickerY(): number {
		const rect = this.$refs.grid as HTMLElement | undefined;
		const h = rect?.clientHeight ?? 150;
		return (1 - this.brightness) * h;
	}

	private commitColor(): void {
		const rgb = hsvToRgb({ h: this.hue, s: this.saturation, v: this.brightness });
		this.hexValue = rgbToHex(rgb);
		this.populateValidationDeclaration();
		this.changed?.(this.hexValue);
	}

	private pointerX(e: MouseEvent | TouchEvent): number {
		if ('touches' in e) {
			return e.touches[0]?.clientX ?? (e as TouchEvent).changedTouches?.[0]?.clientX ?? 0;
		}

		return (e as MouseEvent).clientX;
	}

	private pointerY(e: MouseEvent | TouchEvent): number {
		if ('touches' in e) {
			return e.touches[0]?.clientY ?? (e as TouchEvent).changedTouches?.[0]?.clientY ?? 0;
		}

		return (e as MouseEvent).clientY;
	}

	private updateSliderFromEvent(e: MouseEvent | TouchEvent): void {
		const slider = this.$refs.slider as HTMLElement | undefined;
		if (slider == null) {
			return;
		}

		const rect = slider.getBoundingClientRect();
		const y = clamp(
			this.pointerY(e) - rect.top,
			0,
			rect.height,
		);
		this.hue = rect.height === 0 ? 0 : 1 - (y / rect.height);
		this.commitColor();
	}

	private updateGridFromEvent(e: MouseEvent | TouchEvent): void {
		const grid = this.$refs.grid as HTMLElement | undefined;
		if (grid == null) {
			return;
		}

		const rect = grid.getBoundingClientRect();
		const x = clamp(
			this.pointerX(e) - rect.left,
			0,
			rect.width,
		);
		const y = clamp(
			this.pointerY(e) - rect.top,
			0,
			rect.height,
		);
		this.saturation = rect.width === 0 ? 0 : x / rect.width;
		this.brightness = rect.height === 0 ? 0 : 1 - (y / rect.height);
		this.commitColor();
	}

	private startDrag(kind: DragKind, e: MouseEvent | TouchEvent): void {
		if (this.disabled) {
			return;
		}

		e.preventDefault();
		this.dragKind = kind;
		// Apply the initial mousedown position
		if (kind === 'slider') {
			this.updateSliderFromEvent(e);
		}

		if (kind === 'grid') {
			this.updateGridFromEvent(e);
		}

		this.moveListener = (ev) => {
			if (this.dragKind === 'slider') {
				this.updateSliderFromEvent(ev);
			}

			if (this.dragKind === 'grid') {
				this.updateGridFromEvent(ev);
			}
		};
		this.upListener = () => {
			this.detachDragListeners();
		};

		globalState.addEventListener('mousemove', this.moveListener);
		globalState.addEventListener('mouseup', this.upListener);
		globalState.addEventListener(
			'touchmove',
			this.moveListener,
			{ passive: false },
		);
		globalState.addEventListener('touchend', this.upListener);
	}

	private detachDragListeners(): void {
		if (this.moveListener != null) {
			globalState.removeEventListener('mousemove', this.moveListener);
			globalState.removeEventListener('touchmove', this.moveListener);
		}

		if (this.upListener != null) {
			globalState.removeEventListener('mouseup', this.upListener);
			globalState.removeEventListener('touchend', this.upListener);
		}

		this.moveListener = null;
		this.upListener = null;
		this.dragKind = null;
	}

	private handleOutsideMouseDown(e: MouseEvent): void {
		const root = this.$el as HTMLElement | null;
		if (root == null || !this.panelOpen) {
			return;
		}

		// Disabled while the panel was open — clear the flag so it does not pop
		// back open once the picker is enabled again. Rendering already ignores
		// it, so doing this on the next mousedown rather than on the prop change
		// itself costs nothing and keeps the render path free of extra passes.
		if (this.disabled || !root.contains(e.target as Node)) {
			this.panelOpen = false;
		}
	}

	private handleSwatchClick(e: MouseEvent): void {
		e.preventDefault();
		if (this.disabled) {
			return;
		}

		this.panelOpen = !this.panelOpen;
		if (this.panelOpen) {
			(this.$refs.input as HTMLInputElement | undefined)?.focus();
		}
	}

	private handleInputFocus(): void {
		if (this.disabled) {
			return;
		}

		this.panelOpen = true;
	}

	private handleInputBlur(_e: FocusEvent): void {
		// No-op for now — panel close is driven by the outside-mousedown handler
		// rather than blur, since focus may transiently leave the input as the
		// user drags the slider/grid handles inside the same wrapper.
	}

	private handleInputChange(e: Event): void {
		if (this.disabled) {
			return;
		}

		const newValue = (e.target as HTMLInputElement).value;
		this.applyValue(newValue);
		this.populateValidationDeclaration();
		this.changed?.(this.hexValue);
	}

	private renderPicker(h: any): VNode {
		const isOpen = this.panelOpen && !this.disabled;
		const wrapperClass = [
			'minicolors',
			'minicolors-theme-bootstrap',
			'minicolors-position-bottom',
			'minicolors-position-left',
			isOpen ? 'minicolors-focus' : '',
			this.disabled ? 'minicolors-disabled' : '',
		].filter(Boolean).join(' ');

		return (
			<div class={wrapperClass}>
				<input
					ref="input"
					type="text"
					class={`${PowerduckState.getFormControlCssClass()} minicolors-input`}
					value={this.hexValue}
					placeholder={this.placeholder}
					size={7}
					disabled={this.disabled}
					onFocus={() => this.handleInputFocus()}
					onBlur={(e: FocusEvent) => this.handleInputBlur(e)}
					onChange={(e: Event) => this.handleInputChange(e)}
					onInput={(e: Event) => this.handleInputChange(e)}
				/>
				<div
					class="minicolors-panel minicolors-slider-hue"
					style={isOpen ? 'display: block;' : ''}
					onSelectstart={(e: Event) => e.preventDefault()}
				>
					<div
						ref="slider"
						class="minicolors-slider minicolors-sprite"
						onMousedown={(e: MouseEvent) => this.startDrag('slider', e)}
						onTouchstart={(e: TouchEvent) => this.startDrag('slider', e)}
					>
						<div
							class="minicolors-picker"
							style={`top: ${this.sliderPickerY}px;`}
						>
						</div>
					</div>
					<div
						ref="grid"
						class="minicolors-grid minicolors-sprite"
						style={`background-color: ${this.pureHueColor};`}
						onMousedown={(e: MouseEvent) => this.startDrag('grid', e)}
						onTouchstart={(e: TouchEvent) => this.startDrag('grid', e)}
					>
						<div class="minicolors-grid-inner"></div>
						<div
							class="minicolors-picker"
							style={`left: ${this.gridPickerX}px; top: ${this.gridPickerY}px;`}
						>
							<div></div>
						</div>
					</div>
				</div>
				<span
					class="minicolors-swatch minicolors-sprite"
					onClick={(e: MouseEvent) => this.handleSwatchClick(e)}
				>
					<span class="minicolors-swatch-color" style={`background-color: ${this.hexValue};`}></span>
				</span>
			</div>
		);
	}

	render(h: any): VNode {
		return (
			<FormItemWrapper
				label={this.label}
				labelButtons={this.labelButtons}
				subtitle={this.subtitle}
				mandatory={this.mandatory}
				wrap={this.wrap}
				appendIcon={this.appendIcon}
				prependIcon={this.prependIcon}
				hint={this.hint}
				marginType={this.marginType}
				appendClicked={this.appendClicked}
				prependClicked={this.prependClicked}
				validationState={this.validationState}
			>
				{this.renderPicker(h)}
			</FormItemWrapper>
		);
	}
}

const ColorPicker = toNative(ColorPickerComponent);
export default ColorPicker;
