import { _decorator, Component, EventTouch, Node, Touch, UITransform, Vec2, Vec3, view } from 'cc';
import { globalEventBus, globalLogger } from '../common/index';
import { PERF_TOUCH_RESPONSE_MAX_MS } from '../common/Constants';
import {
    ControlId,
    DEFAULT_LAYOUT,
    IFloatingLayout,
    ISafeAreaInsets,
    MultiTouchRouter,
    applySafeArea,
    classifyDirection,
    joystickDirection,
} from './InputModel';
import { InputEvents } from './InputEvents';

const { ccclass, property } = _decorator;

/**
 * View component for the floating control layer.
 *
 * Responsibilities (req 1.x, 20.1):
 *  - Subscribe to all touch events on a full-screen UI node.
 *  - Translate device coordinates into landscape-design coordinates.
 *  - Delegate hit-testing / dead-zone / angle classification to `InputModel`
 *    (platform-agnostic, already unit-tested under Jest).
 *  - Emit high-level input events through `globalEventBus`.
 *  - Record touch→response latency to `globalLogger` for QA (req 20.1).
 *
 * IMPORTANT: This class intentionally avoids any gameplay logic so that the
 * player controller (task 4.x) can be swapped / re-tested without touching
 * the input layer.
 */
@ccclass('FloatingControlLayer')
export class FloatingControlLayer extends Component {
    @property({ tooltip: 'The root node of the joystick visual (bg + handle).' })
    public joystickRoot: Node | null = null;

    @property({ tooltip: 'The root node of the jump button visual.' })
    public jumpRoot: Node | null = null;

    @property({ tooltip: 'The root node of the shuriken button visual.' })
    public shurikenRoot: Node | null = null;

    @property({ tooltip: 'The root node of the ninja-sword button visual.' })
    public ninjaSwordRoot: Node | null = null;

    private layout: IFloatingLayout = DEFAULT_LAYOUT;
    private router: MultiTouchRouter = new MultiTouchRouter(DEFAULT_LAYOUT);

    protected onLoad(): void {
        this.applyInitialLayout();
        this.bindTouchEvents();
    }

    protected onDestroy(): void {
        this.unbindTouchEvents();
    }

    /** Public API — called by `UIFlowMgr` when safe-area changes. */
    public updateSafeArea(insets: ISafeAreaInsets): void {
        this.layout = applySafeArea(DEFAULT_LAYOUT, insets);
        this.router = new MultiTouchRouter(this.layout);
        this.syncLayoutToNodes();
    }

    /** Public API — replace the layout (used by the layout-customisation flow, task 3.2). */
    public setLayout(layout: IFloatingLayout): void {
        this.layout = layout;
        this.router = new MultiTouchRouter(layout);
        this.syncLayoutToNodes();
    }

    public getLayout(): IFloatingLayout {
        return this.layout;
    }

    // ------------------------------------------------------------------
    // internals
    // ------------------------------------------------------------------

    private applyInitialLayout(): void {
        // On first frame the engine gives us a visibleSize in real pixels;
        // we derive insets from it so the landscape 960x540 baseline still
        // maps correctly into a notched screen (req 1.7).
        const size = view.getVisibleSize();
        // Heuristic: if the device is wider than 16:9 we add insets on both
        // left/right to keep controls in the safe area.
        const baselineRatio = 16 / 9;
        const actualRatio = size.width / size.height;
        let leftInset = 0;
        let rightInset = 0;
        if (actualRatio > baselineRatio) {
            const extra = (actualRatio - baselineRatio) * size.height;
            leftInset = extra / 2 / (size.width / 960);
            rightInset = leftInset;
        }
        this.updateSafeArea({ left: leftInset, right: rightInset, top: 0, bottom: 0 });
    }

    private bindTouchEvents(): void {
        this.node.on(Node.EventType.TOUCH_START, this.onTouchStart, this);
        this.node.on(Node.EventType.TOUCH_MOVE, this.onTouchMove, this);
        this.node.on(Node.EventType.TOUCH_END, this.onTouchEnd, this);
        this.node.on(Node.EventType.TOUCH_CANCEL, this.onTouchEnd, this);
    }

    private unbindTouchEvents(): void {
        this.node.off(Node.EventType.TOUCH_START, this.onTouchStart, this);
        this.node.off(Node.EventType.TOUCH_MOVE, this.onTouchMove, this);
        this.node.off(Node.EventType.TOUCH_END, this.onTouchEnd, this);
        this.node.off(Node.EventType.TOUCH_CANCEL, this.onTouchEnd, this);
    }

    private onTouchStart(ev: EventTouch): void {
        const t = ev.getUILocation();
        const start = FloatingControlLayer.now();
        const touchId = this.touchId(ev);
        const hit = this.router.begin(touchId, t.x, t.y, start);
        this.recordLatency('input/touchStart', start);
        if (!hit) {
            // Let the touch fall through to the gameplay layer (req 1.3).
            return;
        }
        ev.propagationStopped = true;
        switch (hit) {
            case ControlId.Jump:
                globalEventBus.emit(InputEvents.JumpPressed, {});
                break;
            case ControlId.Shuriken:
                globalEventBus.emit(InputEvents.ShurikenPressed, {});
                break;
            case ControlId.NinjaSword:
                globalEventBus.emit(InputEvents.NinjaSwordPressed, {});
                break;
            case ControlId.Joystick:
                this.broadcastJoystick(t.x, t.y);
                break;
            default:
                break;
        }
    }

    private onTouchMove(ev: EventTouch): void {
        const t = ev.getUILocation();
        const touchId = this.touchId(ev);
        const bound = this.router.move(touchId, t.x, t.y);
        if (bound === ControlId.Joystick) {
            this.broadcastJoystick(t.x, t.y);
        }
    }

    private onTouchEnd(ev: EventTouch): void {
        const touchId = this.touchId(ev);
        const end = FloatingControlLayer.now();
        const bound = this.router.end(touchId);
        if (!bound) return;
        switch (bound) {
            case ControlId.Jump: {
                const slotStart = this.lastStartTs.get(touchId);
                const hold = slotStart !== undefined ? end - slotStart : 0;
                globalEventBus.emit(InputEvents.JumpReleased, { holdMs: hold });
                break;
            }
            case ControlId.Shuriken:
                globalEventBus.emit(InputEvents.ShurikenReleased, {});
                break;
            case ControlId.NinjaSword:
                globalEventBus.emit(InputEvents.NinjaSwordReleased, {});
                break;
            case ControlId.Joystick:
                globalEventBus.emit(InputEvents.JoystickMove, { dx: 0, dy: 0, klass: 'none' });
                break;
            default:
                break;
        }
        this.lastStartTs.delete(touchId);
    }

    private broadcastJoystick(x: number, y: number): void {
        const dir = joystickDirection(this.layout, x, y);
        const klass = classifyDirection(dir);
        globalEventBus.emit(InputEvents.JoystickMove, { dx: dir.x, dy: dir.y, klass });
    }

    /** Mirror layout geometry onto the bound visual nodes. */
    private syncLayoutToNodes(): void {
        this.placeNode(this.joystickRoot, this.layout.joystick.cx, this.layout.joystick.cy);
        this.placeNode(this.jumpRoot, this.layout.jump.cx, this.layout.jump.cy);
        this.placeNode(this.shurikenRoot, this.layout.shuriken.cx, this.layout.shuriken.cy);
        this.placeNode(this.ninjaSwordRoot, this.layout.ninjaSword.cx, this.layout.ninjaSword.cy);
    }

    private placeNode(node: Node | null, cx: number, cy: number): void {
        if (!node) return;
        // Landscape design coordinates: origin at bottom-left of 960x540.
        const worldX = cx - 480;
        const worldY = cy - 270;
        node.setPosition(new Vec3(worldX, worldY, 0));
        const ui = node.getComponent(UITransform);
        if (ui) ui.setAnchorPoint(new Vec2(0.5, 0.5));
    }

    /** Map a Cocos `Touch` object to a stable numeric id we can track. */
    private readonly lastStartTs = new Map<number, number>();
    private touchId(ev: EventTouch): number {
        const touch: Touch | null = ev.touch;
        const id = touch ? touch.getID() : 0;
        if (!this.lastStartTs.has(id)) {
            this.lastStartTs.set(id, FloatingControlLayer.now());
        }
        return id;
    }

    /** Capture input→emit latency into the perf metric store (req 20.1). */
    private recordLatency(name: string, start: number): void {
        const elapsed = FloatingControlLayer.now() - start;
        globalLogger.metric({ name, value: elapsed });
        if (elapsed > PERF_TOUCH_RESPONSE_MAX_MS) {
            globalLogger.warn(
                'Input',
                `latency ${elapsed.toFixed(1)}ms exceeds ${PERF_TOUCH_RESPONSE_MAX_MS}ms target`
            );
        }
    }

    private static now(): number {
        return typeof performance !== 'undefined' && typeof performance.now === 'function'
            ? performance.now()
            : Date.now();
    }
}
