/**
 * Super Fight UI — Terminal takeover for the multiplayer ASCII fighting game.
 *
 * Clears and takes over the outer TUI's alternate screen, intercepts all
 * keyboard input, runs the game loop, and bridges the headless game engine
 * with terminal rendering and network I/O.
 */
import { DAMAGE, GROUND_Y, JUMP_VY, SPRITE_H, TICK_MS, VIEWPORT_H, VIEWPORT_W, applyDamage, checkHit, createPlayer, decodeGameMsg, encodeGameMsg, renderFrame, tickPlayer } from '../../app/collab/super-fight.js';
export class SuperFightUI {
    ui;
    client;
    localPeerId;
    localDisplayName;
    gameState = new Map();
    gameLoop = null;
    removeInputListener = null;
    inputListener = null;
    resizeHandler = null;
    running = false;
    paused = false; // true when terminal too small
    localAction = 'idle';
    /** Set of target IDs we've already registered a hit for this attack */
    hitTargets = new Set();
    titleScreenTimeout = null;
    showingTitle = true;
    killFeed = [];
    /** Tick counter for respawn invincibility flash */
    invincibilityTicks = new Map();
    /** Callback fired when game stops (for cleanup by InteractiveMode) */
    onStop;
    constructor(opts) {
        this.ui = opts.ui;
        this.client = opts.client;
        this.localPeerId = opts.localPeerId;
        this.localDisplayName = opts.localDisplayName;
        // Initialize players from peer list
        let colorIdx = 0;
        // Add local player first
        this.gameState.set(this.localPeerId, createPlayer(this.localDisplayName, colorIdx++, 20));
        // Add remote peers
        for (const [peerId, peer] of opts.peers) {
            if (peerId === this.localPeerId)
                continue;
            this.gameState.set(peerId, createPlayer(peer.displayName, colorIdx++, 20 + colorIdx * 10));
        }
    }
    // ── Lifecycle ──────────────────────────────────────────────────────────
    start() {
        if (this.running)
            return;
        this.running = true;
        // Suspend TUI rendering so resize events don't clobber the game screen
        this.ui.renderSuspended = true;
        // The main TUI already owns the alternate screen; clear it for the game.
        this.ui.terminal.write('\x1b[2J\x1b[H\x1b[?25l');
        // Register input listener (intercepts ALL keys)
        this.inputListener = (data) => this.handleInput(data);
        this.removeInputListener = this.ui.addInputListener(this.inputListener);
        // Listen for resize
        this.resizeHandler = () => this.handleResize();
        process.stdout.on('resize', this.resizeHandler);
        // Check terminal size — if too small, show warning and wait for resize
        if (!this.checkSize()) {
            this.paused = true;
            this.renderSizeWarning();
            // Don't show title screen until terminal is big enough
            return;
        }
        this.showTitleAndStart();
    }
    stop() {
        if (!this.running)
            return;
        this.running = false;
        // Clear title timeout if still showing
        if (this.titleScreenTimeout) {
            clearTimeout(this.titleScreenTimeout);
            this.titleScreenTimeout = null;
        }
        // Stop game loop
        if (this.gameLoop) {
            clearInterval(this.gameLoop);
            this.gameLoop = null;
        }
        // Remove input listener
        if (this.removeInputListener) {
            this.removeInputListener();
            this.removeInputListener = null;
        }
        // Remove resize listener
        if (this.resizeHandler) {
            process.stdout.removeListener('resize', this.resizeHandler);
            this.resizeHandler = null;
        }
        // Broadcast quit
        this.client.sendChat(encodeGameMsg({ type: 'quit' }));
        // Keep the main TUI's alternate screen active and restore its cursor state.
        this.ui.terminal.write('\x1b[?25h');
        // Resume TUI rendering and force a full re-render to restore normal display
        this.ui.renderSuspended = false;
        this.ui.requestRender(true);
        this.onStop?.();
    }
    // ── Game Loop ──────────────────────────────────────────────────────────
    showTitleAndStart() {
        this.showingTitle = true;
        // Clear alternate screen before rendering title
        this.ui.terminal.write('\x1b[2J');
        this.renderTitleScreen();
        this.titleScreenTimeout = setTimeout(() => {
            this.showingTitle = false;
            this.titleScreenTimeout = null;
            // Clear screen before first game frame to remove title remnants
            this.ui.terminal.write('\x1b[2J');
            this.startGameLoop();
        }, 2000);
    }
    startGameLoop() {
        if (!this.running || this.gameLoop)
            return;
        this.gameLoop = setInterval(() => {
            if (this.paused || this.showingTitle)
                return;
            this.tick();
        }, TICK_MS);
    }
    tick() {
        const localPlayer = this.gameState.get(this.localPeerId);
        if (!localPlayer)
            return;
        // Apply local input action
        this.applyLocalInput(localPlayer);
        // Track pre-tick dead state for respawn detection
        const wasDead = new Map();
        for (const [id, p] of this.gameState)
            wasDead.set(id, p.action === 'dead');
        // Physics
        tickPlayer(localPlayer);
        // Detect respawns → start invincibility flash (15 ticks ≈ 1s)
        for (const [id, p] of this.gameState) {
            if (wasDead.get(id) && p.action !== 'dead') {
                this.invincibilityTicks.set(id, 15);
            }
            const remaining = this.invincibilityTicks.get(id);
            if (remaining && remaining > 0) {
                this.invincibilityTicks.set(id, remaining - 1);
            }
            else {
                this.invincibilityTicks.delete(id);
            }
        }
        // Hit detection — check local player's attacks against all remotes
        for (const [peerId, target] of this.gameState) {
            if (peerId === this.localPeerId)
                continue;
            if (this.hitTargets.has(peerId))
                continue; // already hit this attack
            if (this.invincibilityTicks.has(peerId))
                continue; // invincible
            if (checkHit(localPlayer, target)) {
                this.hitTargets.add(peerId);
                // Terminal bell on hit
                this.ui.terminal.write('\x07');
                // Broadcast hit
                this.client.sendChat(encodeGameMsg({ type: 'hit', targetId: peerId, damage: DAMAGE }));
                // Apply locally too
                const killed = applyDamage(target, DAMAGE);
                if (killed) {
                    localPlayer.kills++;
                    this.addKillFeedEntry(localPlayer.displayName, localPlayer.action === 'kick' ? 'kicked' : 'punched', target.displayName);
                }
            }
        }
        // Broadcast local state
        this.client.sendChat(encodeGameMsg({
            type: 'state',
            peerId: this.localPeerId,
            x: localPlayer.x,
            y: localPlayer.y,
            vy: localPlayer.vy,
            action: localPlayer.action,
            facing: localPlayer.facing,
            health: localPlayer.health,
            kills: localPlayer.kills
        }));
        // Render
        this.renderGameFrame();
    }
    // ── Input ──────────────────────────────────────────────────────────────
    handleInput(data) {
        if (!this.running)
            return undefined;
        // Quit — Escape, q, or Ctrl+C
        if (data === '\x1b' || data === 'q' || data === '\x03') {
            this.stop();
            return { consume: true };
        }
        // Movement keys
        switch (data) {
            case '\x1b[A':
            case 'w': // Up / Jump
                this.localAction = 'jump';
                break;
            case '\x1b[B':
            case 's': // Down / Crouch
                this.localAction = 'crouch';
                break;
            case '\x1b[C':
            case 'd': // Right
                this.localAction = 'walk-right';
                break;
            case '\x1b[D':
            case 'a': // Left
                this.localAction = 'walk-left';
                break;
            case 'j':
            case 'z': // Punch
                this.localAction = 'punch';
                this.hitTargets.clear(); // new attack, clear hit tracking
                break;
            case 'k':
            case 'x': // Kick
                this.localAction = 'kick';
                this.hitTargets.clear();
                break;
            default:
                // On key release or unknown key, go idle
                if (data.length === 1 && data >= ' ') {
                    // Ignore other printable chars
                }
                break;
        }
        return { consume: true };
    }
    applyLocalInput(player) {
        if (player.action === 'dead')
            return;
        const onGround = player.y >= GROUND_Y - SPRITE_H;
        switch (this.localAction) {
            case 'jump':
                if (onGround && player.action !== 'jump') {
                    player.vy = JUMP_VY;
                    player.action = 'jump';
                }
                this.localAction = 'idle'; // jump is one-shot
                break;
            case 'crouch':
                if (onGround) {
                    player.action = 'crouch';
                }
                break;
            case 'walk-left':
                if (player.action !== 'punch' && player.action !== 'kick' && player.action !== 'hit') {
                    player.action = 'walk-left';
                    player.facing = 'left';
                }
                break;
            case 'walk-right':
                if (player.action !== 'punch' && player.action !== 'kick' && player.action !== 'hit') {
                    player.action = 'walk-right';
                    player.facing = 'right';
                }
                break;
            case 'punch':
                if (player.actionTimer <= 0) {
                    player.action = 'punch';
                    player.actionTimer = 4; // 4 ticks of punch animation
                }
                this.localAction = 'idle';
                break;
            case 'kick':
                if (player.actionTimer <= 0) {
                    player.action = 'kick';
                    player.actionTimer = 5; // 5 ticks of kick animation
                }
                this.localAction = 'idle';
                break;
            case 'idle':
                // Only go idle if not in an attack/hit animation and on the ground
                if (player.actionTimer <= 0 && player.action !== 'jump' && onGround) {
                    player.action = 'idle';
                }
                break;
        }
    }
    // ── Incoming Network Messages ──────────────────────────────────────────
    /** Called by the chat handler when a game message arrives */
    handleGameMessage(peerId, text) {
        const msg = decodeGameMsg(text);
        if (!msg)
            return;
        switch (msg.type) {
            case 'state':
                this.handleRemoteState(msg.peerId, msg);
                break;
            case 'hit':
                if (msg.targetId === this.localPeerId) {
                    const localPlayer = this.gameState.get(this.localPeerId);
                    if (localPlayer) {
                        applyDamage(localPlayer, msg.damage);
                    }
                }
                break;
            case 'quit':
                this.gameState.delete(peerId);
                break;
        }
    }
    handleRemoteState(peerId, msg) {
        let player = this.gameState.get(peerId);
        if (!player) {
            // New player joined mid-game
            player = createPlayer(peerId, this.gameState.size);
            this.gameState.set(peerId, player);
        }
        player.x = msg.x;
        player.y = msg.y;
        player.vy = msg.vy;
        player.action = msg.action;
        player.facing = msg.facing;
        player.health = msg.health;
        player.kills = msg.kills;
    }
    /** Add a peer that joined the collab room during the game */
    addPeer(peerId, displayName) {
        if (!this.gameState.has(peerId)) {
            this.gameState.set(peerId, createPlayer(displayName, this.gameState.size));
        }
    }
    /** Remove a peer that left the collab room during the game */
    removePeer(peerId) {
        this.gameState.delete(peerId);
    }
    // ── Rendering ──────────────────────────────────────────────────────────
    addKillFeedEntry(killer, verb, victim) {
        this.killFeed.push({ text: `${killer} ${verb} ${victim}!`, time: Date.now() });
        // Keep only last 3
        if (this.killFeed.length > 3)
            this.killFeed.shift();
    }
    tickCount = 0;
    renderGameFrame() {
        this.tickCount++;
        // Invincibility flash: temporarily remove flashing players on odd ticks
        const hidden = new Map();
        for (const [id, ticks] of this.invincibilityTicks) {
            if (ticks > 0 && this.tickCount % 2 === 0) {
                const player = this.gameState.get(id);
                if (player) {
                    hidden.set(id, player);
                    this.gameState.delete(id);
                }
            }
        }
        const lines = renderFrame(this.gameState, VIEWPORT_W, VIEWPORT_H);
        // Restore hidden players
        for (const [id, player] of hidden) {
            this.gameState.set(id, player);
        }
        const now = Date.now();
        // Overlay kill feed at bottom of viewport (above ground)
        this.killFeed = this.killFeed.filter((e) => now - e.time < 5000);
        for (let i = 0; i < this.killFeed.length; i++) {
            const row = GROUND_Y - 2 - (this.killFeed.length - 1 - i);
            if (row >= 1 && row < lines.length) {
                const entry = this.killFeed[i];
                const age = now - entry.time;
                // Fade: bright for first 3s, dim for last 2s
                const color = age < 3000 ? '\x1b[97m' : '\x1b[90m';
                const pad = Math.max(0, VIEWPORT_W - entry.text.length - 4);
                lines[row] = ' '.repeat(pad) + color + ' ' + entry.text + ' \x1b[0m ';
            }
        }
        // Move cursor to home position and overwrite
        let output = '\x1b[H';
        for (let i = 0; i < lines.length; i++) {
            output += lines[i];
            if (i < lines.length - 1)
                output += '\r\n';
        }
        this.ui.terminal.write(output);
    }
    renderTitleScreen() {
        const w = VIEWPORT_W;
        const h = VIEWPORT_H;
        const lines = Array(h).fill(' '.repeat(w));
        // ASCII art title centered
        const title = [
            '  ____  _   _ ____  _____ ____    _____ ___ ____ _   _ _____ _ ',
            ' / ___|| | | |  _ \\| ____|  _ \\  |  ___|_ _/ ___| | | |_   _| |',
            ' \\___ \\| | | | |_) |  _| | |_) | | |_   | | |  _| |_| | | | | |',
            '  ___) | |_| |  __/| |___|  _ <  |  _|  | | |_| |  _  | | | |_|',
            ' |____/ \\___/|_|   |_____|_| \\_\\ |_|   |___\\____|_| |_| |_| (_)'
        ];
        const startRow = Math.floor((h - title.length - 6) / 2);
        const result = [...lines];
        for (let i = 0; i < title.length; i++) {
            const row = startRow + i;
            if (row >= 0 && row < h) {
                const pad = Math.max(0, Math.floor((w - title[i].length) / 2));
                result[row] =
                    ' '.repeat(pad) +
                        '\x1b[93m' +
                        title[i] +
                        '\x1b[0m' +
                        ' '.repeat(Math.max(0, w - pad - title[i].length));
            }
        }
        // Player list below title
        const playerRow = startRow + title.length + 2;
        const players = [...this.gameState.values()];
        let playerLine = '';
        for (const p of players) {
            playerLine += `${p.color}● ${p.displayName}\x1b[0m  `;
        }
        if (playerRow < h) {
            const pad = Math.max(0, Math.floor((w - players.length * 12) / 2));
            result[playerRow] =
                ' '.repeat(pad) + playerLine + ' '.repeat(Math.max(0, w - pad - players.length * 12));
        }
        // "Get ready!" text
        const readyRow = playerRow + 2;
        const readyText = 'Get ready...';
        if (readyRow < h) {
            const pad = Math.floor((w - readyText.length) / 2);
            result[readyRow] =
                ' '.repeat(pad) +
                    '\x1b[97m' +
                    readyText +
                    '\x1b[0m' +
                    ' '.repeat(Math.max(0, w - pad - readyText.length));
        }
        let output = '\x1b[H';
        for (let i = 0; i < result.length; i++) {
            output += result[i];
            if (i < result.length - 1)
                output += '\r\n';
        }
        this.ui.terminal.write(output);
    }
    // ── Terminal Size ──────────────────────────────────────────────────────
    checkSize() {
        return this.ui.terminal.columns >= VIEWPORT_W && this.ui.terminal.rows >= VIEWPORT_H;
    }
    handleResize() {
        if (!this.running)
            return;
        if (this.checkSize()) {
            if (this.paused) {
                this.paused = false;
                // If we never showed the title (was paused from the start), show it now
                if (!this.gameLoop && !this.titleScreenTimeout) {
                    this.showTitleAndStart();
                }
            }
        }
        else {
            this.paused = true;
            this.renderSizeWarning();
        }
    }
    renderSizeWarning() {
        const cols = this.ui.terminal.columns;
        const rows = this.ui.terminal.rows;
        const msg1 = `Resize terminal to at least ${VIEWPORT_W}×${VIEWPORT_H} to play`;
        const msg2 = `Current: ${cols}×${rows}`;
        // Clear screen and center the message
        let output = '\x1b[2J\x1b[H';
        const row1 = Math.max(0, Math.floor(rows / 2) - 1);
        const row2 = row1 + 1;
        if (row1 > 0)
            output += `\x1b[${row1 + 1};1H`;
        const pad1 = Math.max(0, Math.floor((cols - msg1.length) / 2));
        output += ' '.repeat(pad1) + msg1;
        output += `\x1b[${row2 + 1};1H`;
        const pad2 = Math.max(0, Math.floor((cols - msg2.length) / 2));
        output += ' '.repeat(pad2) + '\x1b[90m' + msg2 + '\x1b[0m';
        this.ui.terminal.write(output);
    }
    get isRunning() {
        return this.running;
    }
}
//# sourceMappingURL=super-fight-ui.js.map