import { DEFAULT_LAYOUT, IFloatingLayout, IHitRect } from './InputModel';
import { StorageMgr, globalStorageMgr } from '../common/StorageMgr';
import { STORAGE_KEY } from '../common/Constants';

/**
 * Persisted representation of the user's custom control layout.
 *
 * We intentionally persist **deltas** on top of the design-baseline
 * `DEFAULT_LAYOUT` rather than absolute positions:
 *
 *   - Keeps old save data forward-compatible when we retune the baseline.
 *   - Keeps the stored blob under ~100 bytes (well within the 17.x budget).
 *
 * Requirement traceability:
 *  - req 1.6  — long-press customisation mode stores this payload.
 *  - req 17.2 — persists across sessions.
 *  - req 17.6 — any parse failure must fall back to the default, not crash.
 */
export interface ILayoutDelta {
    /** Offset applied on top of the default rect, in landscape design px. */
    joystickOffset: { dx: number; dy: number };
    jumpOffset: { dx: number; dy: number };
    shurikenOffset: { dx: number; dy: number };
    ninjaSwordOffset: { dx: number; dy: number };
    /** Multipliers applied to default `w`/`h`. Clamped to 0.7 — 1.4. */
    buttonSizeScale: number;
    /** UI opacity 0.3 — 1.0 (req 1.1 default 0.7). */
    opacity: number;
}

export const DEFAULT_LAYOUT_DELTA: ILayoutDelta = {
    joystickOffset: { dx: 0, dy: 0 },
    jumpOffset: { dx: 0, dy: 0 },
    shurikenOffset: { dx: 0, dy: 0 },
    ninjaSwordOffset: { dx: 0, dy: 0 },
    buttonSizeScale: 1.0,
    opacity: 0.7,
};

/** Numeric clamps enforced on any delta the user (or stale storage) gives us. */
export const LAYOUT_DELTA_BOUNDS = {
    offsetPxMax: 240,
    sizeScaleMin: 0.7,
    sizeScaleMax: 1.4,
    opacityMin: 0.3,
    opacityMax: 1.0,
} as const;

/** Clamp + sanitise a raw delta object received from storage. */
export function sanitiseLayoutDelta(raw: Partial<ILayoutDelta> | null | undefined): ILayoutDelta {
    if (!raw || typeof raw !== 'object') {
        return { ...DEFAULT_LAYOUT_DELTA };
    }
    const clamp = (v: number, lo: number, hi: number): number => {
        if (typeof v !== 'number' || Number.isNaN(v)) return (lo + hi) / 2;
        return v < lo ? lo : v > hi ? hi : v;
    };
    const clampOffset = (o?: { dx?: number; dy?: number }) => ({
        dx: clamp(o?.dx ?? 0, -LAYOUT_DELTA_BOUNDS.offsetPxMax, LAYOUT_DELTA_BOUNDS.offsetPxMax),
        dy: clamp(o?.dy ?? 0, -LAYOUT_DELTA_BOUNDS.offsetPxMax, LAYOUT_DELTA_BOUNDS.offsetPxMax),
    });
    return {
        joystickOffset: clampOffset(raw.joystickOffset),
        jumpOffset: clampOffset(raw.jumpOffset),
        shurikenOffset: clampOffset(raw.shurikenOffset),
        ninjaSwordOffset: clampOffset(raw.ninjaSwordOffset),
        buttonSizeScale: clamp(
            raw.buttonSizeScale ?? 1,
            LAYOUT_DELTA_BOUNDS.sizeScaleMin,
            LAYOUT_DELTA_BOUNDS.sizeScaleMax
        ),
        opacity: clamp(raw.opacity ?? 0.7, LAYOUT_DELTA_BOUNDS.opacityMin, LAYOUT_DELTA_BOUNDS.opacityMax),
    };
}

/** Apply a sanitised delta on top of the baseline default layout. */
export function applyLayoutDelta(baseline: IFloatingLayout, delta: ILayoutDelta): IFloatingLayout {
    const offsetRect = (r: IHitRect, off: { dx: number; dy: number }): IHitRect => ({
        cx: r.cx + off.dx,
        cy: r.cy + off.dy,
        w: r.w * delta.buttonSizeScale,
        h: r.h * delta.buttonSizeScale,
    });
    return {
        joystick: offsetRect(baseline.joystick, delta.joystickOffset),
        jump: offsetRect(baseline.jump, delta.jumpOffset),
        shuriken: offsetRect(baseline.shuriken, delta.shurikenOffset),
        ninjaSword: offsetRect(baseline.ninjaSword, delta.ninjaSwordOffset),
        joystickDeadzone: baseline.joystickDeadzone,
        opacity: delta.opacity,
    };
}

/**
 * Thin adapter over `StorageMgr` that handles the `kl_control_layout` key.
 * The adapter always produces a valid `IFloatingLayout` — even when the
 * underlying storage is corrupted (req 17.6).
 */
export class LayoutCustomizer {
    constructor(
        private readonly baseline: IFloatingLayout = DEFAULT_LAYOUT,
        private readonly storage: StorageMgr = globalStorageMgr
    ) {}

    /** Load the saved delta (or default) and produce the concrete layout. */
    public loadLayout(): { layout: IFloatingLayout; delta: ILayoutDelta } {
        const raw = this.storage.get<Partial<ILayoutDelta> | null>(STORAGE_KEY.ControlLayout, null);
        const delta = sanitiseLayoutDelta(raw);
        return { layout: applyLayoutDelta(this.baseline, delta), delta };
    }

    /** Persist the given delta after sanitising it. */
    public saveDelta(delta: ILayoutDelta): void {
        this.storage.set(STORAGE_KEY.ControlLayout, sanitiseLayoutDelta(delta));
    }

    /** Reset the layout back to the landscape default. */
    public reset(): void {
        this.storage.remove(STORAGE_KEY.ControlLayout);
    }
}
