<!--
Reusable "Ship Class design area": the five design blocks (drive,
armament, weapons, shields, cargo) plus the four tech levels they are
built with. Tech and MAT locks follow the same idiom as goal-seek
locks below the design area — by default the value renders as plain
text with an open padlock; clicking it overrides (input + closed
padlock). Reserved space for the padlock keeps the column width
stable as the lock state toggles. A block claimed by an active
goal-seek lock renders read-only with its own lock marker. The
component is presentational — the parent owns the state and the
calculator math — so the ship-group upgrade flow can reuse it later.
-->
<script lang="ts">
	import { tick } from "svelte";
	import { i18n, type TranslationKey } from "$lib/i18n/index.svelte";
	import {
		shipClassFieldErrors,
		type ShipClassValueInvalidReason,
	} from "$lib/util/ship-class-validation";
	import type { ClaimedInput } from "./calc-model";

	export interface DesignBlocksState {
		drive: number;
		armament: number;
		weapons: number;
		shields: number;
		cargo: number;
	}
	export interface TechState {
		drive: number;
		weapons: number;
		shields: number;
		cargo: number;
	}
	export type TechKey = keyof TechState;

	type Props = {
		blocks: DesignBlocksState;
		// Blocks after goal-seek: the claimed block carries its solved
		// value, which is what the read-only computed cell displays.
		resolved: DesignBlocksState;
		techs: TechState;
		techOverridden: Record<TechKey, boolean>;
		// Lower bound for the tech inputs: the player's current tech on
		// this turn. A design cannot be built with tech below the player's
		// own level, so we surface that as a per-field validation error.
		techFloor: TechState;
		computedInput?: ClaimedInput | null;
		blocksReadonly?: boolean;
		// Formatter applied to the read-only tech value and to the
		// resolved (goal-seek) ship-block value. Same `fmt` as the
		// rest of the calculator, passed in so the design area stays
		// presentational and the parent owns the rounding policy.
		formatNumber: (value: number) => string;
		onTechInput: (key: TechKey) => void;
		onResetTech: (key: TechKey) => void;
	};
	let {
		blocks = $bindable(),
		resolved,
		techs = $bindable(),
		techOverridden,
		techFloor,
		computedInput = null,
		blocksReadonly = false,
		formatNumber,
		onTechInput,
		onResetTech,
	}: Props = $props();

	const VALUE_REASON_KEY: Record<ShipClassValueInvalidReason, TranslationKey> = {
		drive_value: "game.calculator.invalid.drive_value",
		armament_value: "game.calculator.invalid.armament_value",
		armament_not_integer: "game.calculator.invalid.armament_not_integer",
		weapons_value: "game.calculator.invalid.weapons_value",
		shields_value: "game.calculator.invalid.shields_value",
		cargo_value: "game.calculator.invalid.cargo_value",
		armament_weapons_pair: "game.calculator.invalid.armament_weapons_pair",
		all_zero: "game.calculator.invalid.all_zero",
	};

	// Per-block validity (independent of which one failed first) so every
	// invalid input is highlighted, not only the first.
	const blockErrors = $derived(shipClassFieldErrors(blocks));

	function blockError(key: keyof DesignBlocksState): string {
		const reason = blockErrors[key];
		return reason === undefined ? "" : i18n.t(VALUE_REASON_KEY[reason]);
	}
	function techError(key: TechKey): string {
		const value = techs[key];
		if (value < 0) return i18n.t("game.calculator.invalid.tech_value");
		if (value < techFloor[key]) {
			return i18n.t("game.calculator.invalid.tech_below_current");
		}
		return "";
	}

	// Smart step on the four ship-class blocks (drive, weapons, shields,
	// cargo): values must be 0 or ≥ 1 per `pkg/calc/validator.go`, so the
	// native 0.01 step would produce invalid intermediates like 0.01.
	// Up: 0 jumps straight to 1; otherwise +0.1. Down: 1 collapses to 0;
	// otherwise −0.1 down to 1, clamped at 0. Armament uses a plain
	// integer step (±1, clamped at 0) so it follows the same
	// JS-driven idiom and we can hide the native spinner uniformly.
	function bumpBlock(value: number, dir: 1 | -1): number {
		if (dir === 1) {
			if (value < 1) return 1;
			return Math.round((value + 0.1) * 10) / 10;
		}
		if (value <= 1) return 0;
		return Math.round((value - 0.1) * 10) / 10;
	}
	function bumpArmament(value: number, dir: 1 | -1): number {
		const next = Math.trunc(value) + dir;
		return next < 0 ? 0 : next;
	}
	function onBlockKey(
		event: KeyboardEvent,
		key: keyof DesignBlocksState,
		smart: boolean,
	): void {
		const dir = event.key === "ArrowUp" ? 1 : event.key === "ArrowDown" ? -1 : 0;
		if (dir === 0) return;
		event.preventDefault();
		blocks[key] = smart
			? bumpBlock(blocks[key], dir)
			: bumpArmament(blocks[key], dir);
	}
	// Tech / modernization-target inputs all use the same ±0.001 step
	// with a per-row floor; lifted into a helper so the parent can
	// reuse it (modernization area in `calculator-tab`).
	function bumpTech(event: KeyboardEvent, key: TechKey): void {
		const dir = event.key === "ArrowUp" ? 1 : event.key === "ArrowDown" ? -1 : 0;
		if (dir === 0) return;
		event.preventDefault();
		const current = techs[key];
		const next = Math.round((current + dir * 0.001) * 1000) / 1000;
		const floor = techFloor[key];
		techs[key] = next < floor ? floor : next;
	}
	// Refuse a fourth decimal as typing happens: keeps the calculator
	// from ever displaying a >3-decimal fraction the user could not
	// have intended (the calculator math is `Ceil3`-rounded for display
	// anyway). Pairs with `bind:value` — `apply` overwrites the bound
	// state when Svelte's own bind handler has already read the
	// over-precise number.
	function capDecimals(event: Event, apply: (next: number) => void): void {
		const el = event.currentTarget as HTMLInputElement;
		const txt = el.value;
		const dot = txt.indexOf(".");
		if (dot < 0 || txt.length - dot - 1 <= 3) return;
		el.value = txt.slice(0, dot + 4);
		apply(el.valueAsNumber);
	}

	const BLOCK_ROWS: {
		key: keyof DesignBlocksState;
		label: () => string;
		tech: TechKey | null;
		smartStep: boolean;
	}[] = [
		{ key: "drive", label: () => i18n.t("game.calculator.field.drive"), tech: "drive", smartStep: true },
		{ key: "armament", label: () => i18n.t("game.calculator.field.armament"), tech: null, smartStep: false },
		{ key: "weapons", label: () => i18n.t("game.calculator.field.weapons"), tech: "weapons", smartStep: true },
		{ key: "shields", label: () => i18n.t("game.calculator.field.shields"), tech: "shields", smartStep: true },
		{ key: "cargo", label: () => i18n.t("game.calculator.field.cargo"), tech: "cargo", smartStep: true },
	];

	const techInputEls: Partial<Record<TechKey, HTMLInputElement>> = {};

	async function activateTechOverride(key: TechKey): Promise<void> {
		onTechInput(key);
		await tick();
		techInputEls[key]?.focus();
		techInputEls[key]?.select();
	}
</script>

<div class="design" data-testid="calculator-design-area">
	<div class="cols">
		<span></span>
		<span class="col-head">{i18n.t("game.calculator.col.ship")}</span>
		<span class="col-head">{i18n.t("game.calculator.col.tech")}</span>
	</div>
	{#each BLOCK_ROWS as row (row.key)}
		{@const isComputed = computedInput === row.key}
		<div class="row">
			<span class="label">{row.label()}</span>
			{#if isComputed}
				<input
					class="ship no-spin"
					type="number"
					step="any"
					readonly
					value={formatNumber(resolved[row.key])}
					data-computed="true"
					data-testid={`calculator-block-${row.key}`}
					title={i18n.t("game.calculator.lock.reset")}
				/>
			{:else}
				<input
					class="ship no-spin"
					type="number"
					step="any"
					min="0"
					bind:value={blocks[row.key]}
					readonly={blocksReadonly}
					aria-invalid={blockError(row.key) !== "" ? "true" : "false"}
					title={blockError(row.key)}
					data-testid={`calculator-block-${row.key}`}
					onkeydown={(e) => onBlockKey(e, row.key, row.smartStep)}
					oninput={(e) => capDecimals(e, (v) => (blocks[row.key] = v))}
				/>
			{/if}
			{#if row.tech !== null}
				{@const techKey = row.tech}
				<span class="tech-cell">
					{#if techOverridden[techKey]}
						<input
							bind:this={techInputEls[techKey]}
							class="tech no-spin"
							type="number"
							step="any"
							min={techFloor[techKey]}
							bind:value={techs[techKey]}
							aria-invalid={techError(techKey) !== "" ? "true" : "false"}
							title={techError(techKey)}
							data-testid={`calculator-tech-${techKey}`}
							onkeydown={(e) => bumpTech(e, techKey)}
							oninput={(e) => capDecimals(e, (v) => (techs[techKey] = v))}
						/>
						<button
							type="button"
							class="lock active"
							title={i18n.t("game.calculator.tech.reset")}
							aria-label={i18n.t("game.calculator.tech.reset")}
							data-testid={`calculator-tech-reset-${techKey}`}
							onclick={() => onResetTech(techKey)}
						>
							🔒
						</button>
					{:else}
						<span
							class="tech-val"
							data-testid={`calculator-tech-value-${techKey}`}
						>
							{formatNumber(techs[techKey])}
						</span>
						<button
							type="button"
							class="lock"
							title={i18n.t("game.calculator.tech.override")}
							aria-label={i18n.t("game.calculator.tech.override")}
							data-testid={`calculator-tech-override-${techKey}`}
							onclick={() => void activateTechOverride(techKey)}
						>
							🔓
						</button>
					{/if}
				</span>
			{:else}
				<span></span>
			{/if}
		</div>
	{/each}
</div>

<style>
	.design {
		display: flex;
		flex-direction: column;
		gap: 0.3rem;
	}
	.cols,
	.row {
		display: grid;
		grid-template-columns: 4.5rem 1fr 1fr;
		align-items: center;
		gap: 0.35rem;
	}
	.col-head {
		color: var(--color-text-muted);
		font-size: 0.7rem;
		text-align: center;
		text-transform: lowercase;
	}
	.label {
		color: var(--color-text-muted);
		font-size: 0.8rem;
	}
	input {
		font-family: var(--font-mono);
		font-size: 0.8rem;
		width: 100%;
		min-width: 0;
		padding: 0.2rem 0.35rem;
		background: var(--color-bg);
		color: var(--color-text);
		border: 1px solid var(--color-border);
		border-radius: 3px;
		font-variant-numeric: tabular-nums;
		text-align: right;
	}
	/* Hide native spinners across the design area — the row drives
	   every numeric edit through ArrowUp/ArrowDown so the column
	   width stays stable and the inputs read consistently. */
	input.no-spin::-webkit-inner-spin-button,
	input.no-spin::-webkit-outer-spin-button {
		-webkit-appearance: none;
		margin: 0;
	}
	input.no-spin {
		-moz-appearance: textfield;
		appearance: textfield;
	}
	input[data-computed="true"],
	input[readonly] {
		color: var(--color-accent);
		background: var(--color-surface-raised);
	}
	input[aria-invalid="true"] {
		border-color: var(--color-danger);
	}
	.tech-cell {
		display: flex;
		align-items: center;
		gap: 0.2rem;
	}
	.tech-val {
		flex: 1;
		min-width: 0;
		font-family: var(--font-mono);
		font-size: 0.8rem;
		font-variant-numeric: tabular-nums;
		text-align: right;
		padding: 0.2rem 0.35rem;
	}
	.lock {
		flex: none;
		padding: 0;
		font-size: 0.7rem;
		line-height: 1;
		background: transparent;
		border: 0;
		cursor: pointer;
		opacity: 0.5;
	}
	.lock.active,
	.lock:hover {
		opacity: 1;
	}
</style>
