// @ts-check
/* global $bottom, $canvas, $left, $right, $top, get_direction */

import { $ToolWindow } from "./$ToolWindow.js";
// import { get_direction } from "./app-localization.js";
import { $G, E } from "./helpers.js";

// Segments here represent UI components as far as a layout algorithm is concerned,
// line segments in one dimension (regardless of whether that dimension is vertical or horizontal),
// with a reference to the UI component DOM element so it can be updated.

function get_segments(component_area_el, pos_axis, exclude_component_el) {
	const $other_components = $(component_area_el).find(".component").not(exclude_component_el);
	return $other_components.toArray().map((component_el) => {
		const segment = { element: component_el };
		if (pos_axis === "top") {
			segment.pos = component_el.offsetTop;
			segment.length = component_el.clientHeight;
		} else if (pos_axis === "left") {
			segment.pos = component_el.offsetLeft;
			segment.length = component_el.clientWidth;
		} else if (pos_axis === "right") {
			segment.pos = component_area_el.scrollWidth - component_el.offsetLeft;
			segment.length = component_el.clientWidth;
		}
		return segment;
	});
}

function adjust_segments(segments, total_available_length) {
	segments.sort((a, b) => a.pos - b.pos);

	// Clamp
	for (const segment of segments) {
		segment.pos = Math.max(segment.pos, 0);
		segment.pos = Math.min(segment.pos, total_available_length - segment.length);
	}

	// Shove things downwards to prevent overlap
	for (let i = 1; i < segments.length; i++) {
		const segment = segments[i];
		const prev_segment = segments[i - 1];
		const overlap = prev_segment.pos + prev_segment.length - segment.pos;
		if (overlap > 0) {
			segment.pos += overlap;
		}
	}

	// Clamp
	for (const segment of segments) {
		segment.pos = Math.max(segment.pos, 0);
		segment.pos = Math.min(segment.pos, total_available_length - segment.length);
	}

	// Shove things upwards to get things back on screen
	for (let i = segments.length - 2; i >= 0; i--) {
		const segment = segments[i];
		const prev_segment = segments[i + 1];
		const overlap = segment.pos + segment.length - prev_segment.pos;
		if (overlap > 0) {
			segment.pos -= overlap;
		}
	}
}

function apply_segments(component_area_el, pos_axis, segments) {
	// Since things aren't positioned absolutely, calculate space between
	let length_before = 0;
	for (const segment of segments) {
		segment.margin_before = segment.pos - length_before;
		length_before = segment.length + segment.pos;
	}

	// Apply to the DOM
	for (const segment of segments) {
		component_area_el.appendChild(segment.element);
		$(segment.element).css(`margin-${pos_axis}`, segment.margin_before);
	}
}

/**
 * @param {string} title
 * @param {string} className
 * @param {"tall" | "wide"} orientation
 * @param {JQuery<HTMLElement>} $el
 * @returns {JQuery<HTMLDivElement> & I$Component}
 */
function $Component(title, className, orientation, $el) {
	// A draggable widget that can be undocked into a window
	const $c = /** @type {JQuery<HTMLDivElement> & I$Component} */ ($(E("div")).addClass("component"));
	$c.addClass(className);
	$c.addClass(orientation);
	$c.append($el);
	$c.css("touch-action", "none");

	const $w = $ToolWindow($c);
	$w.title(title);
	$w.hide();
	$w.$content.addClass({
		tall: "vertical",
		wide: "horizontal",
	}[orientation]);

	// Nudge the Colors component over a tiny bit
	if (className === "colors-component" && orientation === "wide") {
		$c.css("position", "relative");
		$c.css(`margin-${get_direction() === "rtl" ? "right" : "left"}`, "3px");
	}

	let iid;
	if ($("body").hasClass("eye-gaze-mode")) {
		// @TODO: don't use an interval for this!
		iid = setInterval(() => {
			const scale = 3;
			$c.css({
				transform: `scale(${scale})`,
				transformOrigin: "0 0",
				marginRight: $c[0].scrollWidth * (scale - 1),
				marginBottom: $c[0].scrollHeight * (scale - 1),
			});
		}, 200);
	}

	let ox, oy;
	let ox2, oy2;
	let w, h;
	let pos = 0;
	let pos_axis;
	let last_docked_to_pos;
	let $last_docked_to;
	let $dock_to;
	let $ghost;

	if (orientation === "tall") {
		pos_axis = "top";
	} else if (get_direction() === "rtl") {
		pos_axis = "right";
	} else {
		pos_axis = "left";
	}

	/**
	 * @param {JQuery<HTMLElement>} $dock_to
	 */
	const dock_to = ($dock_to) => {
		$w.hide();

		// must get layout state *before* changing it
		const segments = get_segments($dock_to[0], pos_axis, $c[0]);

		// so we can measure clientWidth/clientHeight
		$dock_to.append($c);

		segments.push({
			element: $c[0],
			pos: pos,
			length: $c[0][pos_axis === "top" ? "clientHeight" : "clientWidth"],
		});

		const total_available_length = pos_axis === "top" ? $dock_to.height() : $dock_to.width();
		// console.log("before adjustment", JSON.stringify(segments, (_key,val)=> (val instanceof Element) ? val.className : val));
		adjust_segments(segments, total_available_length);
		// console.log("after adjustment", JSON.stringify(segments, (_key,val)=> (val instanceof Element) ? val.className : val));

		apply_segments($dock_to[0], pos_axis, segments);

		// Save where it's now docked to
		$last_docked_to = $dock_to;
		last_docked_to_pos = pos;
	};
	/**
	 * @param {number} x
	 * @param {number} y
	 */
	const undock_to = (x, y) => {
		const component_area_el = $c.closest(".component-area")[0];
		// must get layout state *before* changing it
		const segments = get_segments(component_area_el, pos_axis, $c[0]);

		$c.css("position", "relative");
		$c.css(`margin-${pos_axis}`, "");

		// Put the component in the window
		$w.$content.append($c);
		// Show and position the window
		$w.show();
		$w.css({
			left: x,
			top: y,
		});

		const total_available_length = pos_axis === "top" ? $(component_area_el).height() : $(component_area_el).width();
		// console.log("before adjustment", JSON.stringify(segments, (_key,val)=> (val instanceof Element) ? val.className : val));
		adjust_segments(segments, total_available_length);
		// console.log("after adjustment", JSON.stringify(segments, (_key,val)=> (val instanceof Element) ? val.className : val));
		apply_segments(component_area_el, pos_axis, segments);
	};

	$w.on("window-drag-start", (e) => {
		e.preventDefault();
	});
	const imagine_window_dimensions = () => {
		const prev_window_shown = $w.is(":visible");
		$w.show();
		let $spacer;
		let { offsetLeft, offsetTop } = $c[0];
		if ($c.closest(".tool-window").length == 0) {
			const styles = getComputedStyle($c[0]);
			$spacer = $(E("div")).addClass("component").css({
				width: styles.width,
				height: styles.height,
				// don't copy margin, margin is actually used for positioning the components in the docking areas
				// don't copy padding, padding changes based on whether the component is in a window in modern theme
				// let padding be influenced by CSS
			});
			$w.append($spacer);
			({ offsetLeft, offsetTop } = $spacer[0]);
		}
		const rect = $w[0].getBoundingClientRect();
		if ($spacer) {
			$spacer.remove();
		}
		if (!prev_window_shown) {
			$w.hide();
		}
		const w_styles = getComputedStyle($w[0]);
		offsetLeft += parseFloat(w_styles.borderLeftWidth);
		offsetTop += parseFloat(w_styles.borderTopWidth);
		return { rect, offsetLeft, offsetTop };
	};
	const imagine_docked_dimensions = ($dock_to = (pos_axis === "top" ? $left : $bottom)) => {
		if ($c.closest(".tool-window").length == 0) {
			return { rect: $c[0].getBoundingClientRect() };
		}
		const styles = getComputedStyle($c[0]);
		const $spacer = $(E("div")).addClass("component").css({
			width: styles.width,
			height: styles.height,
			flex: "0 0 auto",
		});
		$dock_to.prepend($spacer);
		const rect = $spacer[0].getBoundingClientRect();
		if ($spacer) {
			$spacer.remove();
		}
		return { rect };
	};
	const render_ghost = (e) => {

		const { rect } = $dock_to ? imagine_docked_dimensions($dock_to) : imagine_window_dimensions();

		// Make sure these dimensions are odd numbers
		// so the alternating pattern of the border is unbroken
		w = (~~(rect.width / 2)) * 2 + 1;
		h = (~~(rect.height / 2)) * 2 + 1;

		if (!$ghost) {
			$ghost = $(E("div")).addClass("component-ghost dock");
			$ghost.appendTo("body");
		}
		const inset = $dock_to ? 0 : 3;
		$ghost.css({
			position: "absolute",
			display: "block",
			width: w - inset * 2,
			height: h - inset * 2,
			left: e.clientX + ($dock_to ? ox : ox2) + inset,
			top: e.clientY + ($dock_to ? oy : oy2) + inset,
		});

		if ($dock_to) {
			$ghost.addClass("dock");
		} else {
			$ghost.removeClass("dock");
		}
	};
	$c.add($w.$titlebar).on("pointerdown", (e) => {
		// Only start a drag via a left click directly on the component element or titlebar
		if (e.button !== 0) { return; }
		const validTarget =
			$c.is(e.target) ||
			(
				$(e.target).closest($w.$titlebar).length > 0 &&
				$(e.target).closest("button").length === 0
			);
		if (!validTarget) { return; }
		// Don't allow dragging in eye gaze mode
		if ($("body").hasClass("eye-gaze-mode")) { return; }

		const docked = imagine_docked_dimensions();
		const rect = $c[0].getBoundingClientRect();
		ox = rect.left - e.clientX;
		oy = rect.top - e.clientY;
		ox = -Math.min(Math.max(-ox, 0), docked.rect.width);
		oy = -Math.min(Math.max(-oy, 0), docked.rect.height);

		const { offsetLeft, offsetTop } = imagine_window_dimensions();
		ox2 = rect.left - offsetLeft - e.clientX;
		oy2 = rect.top - offsetTop - e.clientY;

		$("body").addClass("dragging");
		$("body").css({ cursor: "default" }).addClass("cursor-bully");

		$G.on("pointermove", drag_update_position);
		$G.one("pointerup", (e) => {
			$G.off("pointermove", drag_update_position);
			drag_onpointerup(e);
			$("body").removeClass("dragging");
			$("body").css({ cursor: "" }).removeClass("cursor-bully");
			$canvas.trigger("pointerleave"); // prevent magnifier preview showing until you move the mouse
		});

		render_ghost(e);
		drag_update_position(e);

		// Prevent text selection anywhere within the component
		e.preventDefault();
	});
	const drag_update_position = (e) => {

		$ghost.css({
			left: e.clientX + ox,
			top: e.clientY + oy,
		});

		$dock_to = null;

		const { width, height } = imagine_docked_dimensions().rect;
		const dock_ghost_left = e.clientX + ox;
		const dock_ghost_top = e.clientY + oy;
		const dock_ghost_right = dock_ghost_left + width;
		const dock_ghost_bottom = dock_ghost_top + height;
		const q = 5;
		if (orientation === "tall") {
			pos_axis = "top";
			if (dock_ghost_left - q < $left[0].getBoundingClientRect().right) {
				$dock_to = $left;
			}
			if (dock_ghost_right + q > $right[0].getBoundingClientRect().left) {
				$dock_to = $right;
			}
		} else {
			pos_axis = get_direction() === "rtl" ? "right" : "left";
			if (dock_ghost_top - q < $top[0].getBoundingClientRect().bottom) {
				$dock_to = $top;
			}
			if (dock_ghost_bottom + q > $bottom[0].getBoundingClientRect().top) {
				$dock_to = $bottom;
			}
		}

		if ($dock_to) {
			const dock_to_rect = $dock_to[0].getBoundingClientRect();
			pos = (
				pos_axis === "top" ? dock_ghost_top : pos_axis === "right" ? dock_ghost_right : dock_ghost_left
			) - dock_to_rect[pos_axis];
			if (pos_axis === "right") {
				pos *= -1;
			}
		}

		render_ghost(e);

		e.preventDefault();
	};

	const drag_onpointerup = (e) => {

		$w.hide();

		// If the component is docked to a component area (a side)
		if ($c.parent().is(".component-area")) {
			// Save where it's docked so we can dock back later
			$last_docked_to = $c.parent();
			if ($dock_to) {
				last_docked_to_pos = pos;
			}
		}

		if ($dock_to) {
			// Dock component to $dock_to
			dock_to($dock_to);
		} else {
			undock_to(e.clientX + ox2, e.clientY + oy2);
		}

		$ghost?.remove();
		$ghost = null;

		$G.trigger("resize");
	};

	$c.dock = ($dock_to) => {
		pos = last_docked_to_pos ?? 0;
		dock_to($dock_to ?? $last_docked_to);
	};
	$c.undock_to = undock_to;

	$c.show = () => {
		$($c[0]).show(); // avoid recursion
		if ($.contains($w[0], $c[0])) {
			$w.show();
		}
		return $c;
	};
	$c.hide = () => {
		$c.add($w).hide();
		return $c;
	};
	$c.toggle = () => {
		if ($c.is(":visible")) {
			$c.hide();
		} else {
			$c.show();
		}
		return $c;
	};
	$c.destroy = () => {
		$w.close();
		$c.remove();
		clearInterval(iid);
	};

	$w.on("close", (e) => {
		e.preventDefault();
		$w.hide();
	});

	return $c;
}

export { $Component };

