'use strict';

var EventBus = require('../core/EventBus.js');

/* ────────────────────────────────────────────────────────────────
   Networking — WebRTC peer connections, rooms, state sync
   
   Sprints 4.1-4.4: RTC connections, NetworkManager,
   state synchronization, lobby system
──────────────────────────────────────────────────────────────── */
/** Default WebSocket-based signaling transport. */
function createWebSocketSignaling() {
    let ws = null;
    const transport = {
        onMessage: null,
        async connect(url) {
            return new Promise((resolve, reject) => {
                ws = new WebSocket(url);
                ws.onopen = () => resolve();
                ws.onerror = () => reject(new Error('WebSocket connection failed'));
                ws.onmessage = (evt) => {
                    var _a;
                    try {
                        const msg = JSON.parse(evt.data);
                        (_a = transport.onMessage) === null || _a === void 0 ? void 0 : _a.call(transport, msg);
                    }
                    catch (_b) {
                        console.debug('Ignoring malformed signaling message');
                    }
                };
                ws.onclose = () => { ws = null; };
            });
        },
        send(msg) {
            ws === null || ws === void 0 ? void 0 : ws.send(JSON.stringify(msg));
        },
        disconnect() {
            ws === null || ws === void 0 ? void 0 : ws.close();
            ws = null;
        },
    };
    return transport;
}
const DEFAULT_RTC_CONFIG = {
    iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
    dataChannelLabel: 'game',
    ordered: false,
    maxRetransmits: 0,
};
class PeerManager {
    constructor(localId, signaling, config) {
        this.events = new EventBus.EventBus();
        this.peers = new Map();
        this.localId = localId;
        this.signaling = signaling;
        this.rtcConfig = { ...DEFAULT_RTC_CONFIG, ...config };
        signaling.onMessage = (msg) => this.handleSignaling(msg);
    }
    get localPeerId() {
        return this.localId;
    }
    get connectedPeers() {
        return Array.from(this.peers.values()).filter(p => p.state === 'connected');
    }
    getPeer(id) {
        return this.peers.get(id);
    }
    /** Initiate connection to a remote peer. */
    async connectToPeer(remotePeerId) {
        if (this.peers.has(remotePeerId))
            return;
        const pc = new RTCPeerConnection({ iceServers: this.rtcConfig.iceServers });
        const info = {
            peerId: remotePeerId,
            displayName: remotePeerId,
            state: 'connecting',
            latencyMs: 0,
            channel: null,
            connection: pc,
        };
        this.peers.set(remotePeerId, info);
        this.events.emit('peer:connecting', remotePeerId);
        // Create data channel
        const channel = pc.createDataChannel(this.rtcConfig.dataChannelLabel, {
            ordered: this.rtcConfig.ordered,
            maxRetransmits: this.rtcConfig.maxRetransmits,
        });
        this.setupChannel(info, channel);
        // ICE candidates
        pc.onicecandidate = (evt) => {
            if (evt.candidate) {
                this.signaling.send({
                    type: 'ice-candidate',
                    from: this.localId,
                    to: remotePeerId,
                    payload: evt.candidate.toJSON(),
                });
            }
        };
        pc.onconnectionstatechange = () => {
            if (pc.connectionState === 'failed' || pc.connectionState === 'disconnected') {
                info.state = 'disconnected';
                this.events.emit('peer:disconnected', remotePeerId);
            }
        };
        const offer = await pc.createOffer();
        await pc.setLocalDescription(offer);
        this.signaling.send({
            type: 'offer',
            from: this.localId,
            to: remotePeerId,
            payload: offer,
        });
    }
    async handleSignaling(msg) {
        if (msg.to && msg.to !== this.localId)
            return;
        if (msg.type === 'offer') {
            const pc = new RTCPeerConnection({ iceServers: this.rtcConfig.iceServers });
            const info = {
                peerId: msg.from,
                displayName: msg.from,
                state: 'connecting',
                latencyMs: 0,
                channel: null,
                connection: pc,
            };
            this.peers.set(msg.from, info);
            pc.ondatachannel = (evt) => {
                this.setupChannel(info, evt.channel);
            };
            pc.onicecandidate = (evt) => {
                if (evt.candidate) {
                    this.signaling.send({
                        type: 'ice-candidate',
                        from: this.localId,
                        to: msg.from,
                        payload: evt.candidate.toJSON(),
                    });
                }
            };
            await pc.setRemoteDescription(new RTCSessionDescription(msg.payload));
            const answer = await pc.createAnswer();
            await pc.setLocalDescription(answer);
            this.signaling.send({
                type: 'answer',
                from: this.localId,
                to: msg.from,
                payload: answer,
            });
        }
        if (msg.type === 'answer') {
            const peer = this.peers.get(msg.from);
            if (peer === null || peer === void 0 ? void 0 : peer.connection) {
                await peer.connection.setRemoteDescription(new RTCSessionDescription(msg.payload));
            }
        }
        if (msg.type === 'ice-candidate') {
            const peer = this.peers.get(msg.from);
            if (peer === null || peer === void 0 ? void 0 : peer.connection) {
                await peer.connection.addIceCandidate(new RTCIceCandidate(msg.payload));
            }
        }
    }
    setupChannel(info, channel) {
        info.channel = channel;
        channel.binaryType = 'arraybuffer';
        channel.onopen = () => {
            info.state = 'connected';
            this.events.emit('peer:connected', info.peerId);
        };
        channel.onclose = () => {
            info.state = 'disconnected';
            this.events.emit('peer:disconnected', info.peerId);
        };
        channel.onmessage = (evt) => {
            this.events.emit('peer:message', { from: info.peerId, data: evt.data });
        };
    }
    /** Send to a specific peer. */
    sendTo(peerId, data) {
        var _a;
        const peer = this.peers.get(peerId);
        if (((_a = peer === null || peer === void 0 ? void 0 : peer.channel) === null || _a === void 0 ? void 0 : _a.readyState) === 'open') {
            peer.channel.send(data);
        }
    }
    /** Broadcast to all connected peers. */
    broadcast(data) {
        var _a;
        for (const peer of this.peers.values()) {
            if (((_a = peer.channel) === null || _a === void 0 ? void 0 : _a.readyState) === 'open') {
                peer.channel.send(data);
            }
        }
    }
    /** Measure latency via ping / pong. */
    ping(peerId) {
        const now = performance.now();
        this.sendTo(peerId, JSON.stringify({ _ping: now }));
    }
    disconnectPeer(peerId) {
        var _a, _b;
        const peer = this.peers.get(peerId);
        if (peer) {
            (_a = peer.channel) === null || _a === void 0 ? void 0 : _a.close();
            (_b = peer.connection) === null || _b === void 0 ? void 0 : _b.close();
            peer.state = 'disconnected';
            this.peers.delete(peerId);
            this.events.emit('peer:disconnected', peerId);
        }
    }
    disconnectAll() {
        for (const id of Array.from(this.peers.keys())) {
            this.disconnectPeer(id);
        }
    }
}
class NetworkManager {
    constructor() {
        this.events = new EventBus.EventBus();
        this._mode = 'offline';
        this._tick = 0;
        this._tickRate = 20; // ticks per second
        this._tickAccumulator = 0;
        this.identities = new Map();
        this.rpcHandlers = new Map();
        this.syncVars = [];
        this._nextNetId = 1;
        this.peers = null;
    }
    get mode() { return this._mode; }
    get tick() { return this._tick; }
    get tickRate() { return this._tickRate; }
    set tickRate(v) { this._tickRate = Math.max(1, v); }
    get isHost() { return this._mode === 'host' || this._mode === 'dedicated'; }
    get isOnline() { return this._mode !== 'offline'; }
    /** Start as host or client. */
    start(mode, peerManager) {
        this._mode = mode;
        this.peers = peerManager;
        this._tick = 0;
        peerManager.events.on('peer:message', (msg) => {
            try {
                const netMsg = JSON.parse(msg.data);
                this.handleMessage(netMsg, msg.from);
            }
            catch (_a) {
                console.debug('Ignoring non-network message from peer');
            }
        });
        this.events.emit('network:started', mode);
    }
    stop() {
        var _a;
        (_a = this.peers) === null || _a === void 0 ? void 0 : _a.disconnectAll();
        this._mode = 'offline';
        this.identities.clear();
        this.syncVars.length = 0;
        this._tick = 0;
        this.events.emit('network:stopped', null);
    }
    /* ── Network Identity ───────────────────────────────────────── */
    registerEntity(entityId, ownerId) {
        const netId = this._nextNetId++;
        const identity = { netId, ownerId, entityId };
        this.identities.set(netId, identity);
        return identity;
    }
    getIdentity(netId) {
        return this.identities.get(netId);
    }
    getEntityIdentity(entityId) {
        for (const id of this.identities.values()) {
            if (id.entityId === entityId)
                return id;
        }
        return undefined;
    }
    /* ── RPC ─────────────────────────────────────────────────────── */
    registerRPC(name, handler) {
        this.rpcHandlers.set(name, handler);
    }
    callRPC(name, args, target = 'all') {
        var _a, _b, _c, _d, _e, _f, _g;
        const msg = {
            type: 'rpc',
            tick: this._tick,
            senderId: (_b = (_a = this.peers) === null || _a === void 0 ? void 0 : _a.localPeerId) !== null && _b !== void 0 ? _b : '',
            payload: { name, args },
        };
        const serialized = JSON.stringify(msg);
        if (target === 'all') {
            // Execute locally too
            (_c = this.rpcHandlers.get(name)) === null || _c === void 0 ? void 0 : _c(msg.senderId, args);
            (_d = this.peers) === null || _d === void 0 ? void 0 : _d.broadcast(serialized);
        }
        else if (target === 'server') {
            if (this.isHost) {
                (_e = this.rpcHandlers.get(name)) === null || _e === void 0 ? void 0 : _e(msg.senderId, args);
            }
            else {
                // Send to host — first connected peer that is host
                (_f = this.peers) === null || _f === void 0 ? void 0 : _f.broadcast(serialized);
            }
        }
        else {
            (_g = this.peers) === null || _g === void 0 ? void 0 : _g.sendTo(target, serialized);
        }
    }
    /* ── SyncVar ────────────────────────────────────────────────── */
    registerSyncVar(def) {
        this.syncVars.push(def);
    }
    /* ── Network Tick ───────────────────────────────────────────── */
    update(dt) {
        if (this._mode === 'offline')
            return;
        this._tickAccumulator += dt;
        const tickInterval = 1 / this._tickRate;
        while (this._tickAccumulator >= tickInterval) {
            this._tickAccumulator -= tickInterval;
            this._tick++;
            this.events.emit('network:tick', this._tick);
        }
    }
    /* ── Spawn / Destroy over network ──────────────────────────── */
    networkSpawn(entityId, prefabName, position) {
        var _a, _b, _c, _d, _e;
        if (!this.isHost)
            return;
        const identity = this.registerEntity(entityId, (_b = (_a = this.peers) === null || _a === void 0 ? void 0 : _a.localPeerId) !== null && _b !== void 0 ? _b : '');
        const msg = {
            type: 'spawn',
            tick: this._tick,
            senderId: (_d = (_c = this.peers) === null || _c === void 0 ? void 0 : _c.localPeerId) !== null && _d !== void 0 ? _d : '',
            payload: { netId: identity.netId, prefabName, position },
        };
        (_e = this.peers) === null || _e === void 0 ? void 0 : _e.broadcast(JSON.stringify(msg));
        this.events.emit('network:spawn', msg.payload);
    }
    networkDestroy(netId) {
        var _a, _b, _c;
        if (!this.isHost)
            return;
        const msg = {
            type: 'destroy',
            tick: this._tick,
            senderId: (_b = (_a = this.peers) === null || _a === void 0 ? void 0 : _a.localPeerId) !== null && _b !== void 0 ? _b : '',
            payload: { netId },
        };
        (_c = this.peers) === null || _c === void 0 ? void 0 : _c.broadcast(JSON.stringify(msg));
        this.identities.delete(netId);
        this.events.emit('network:destroy', netId);
    }
    /* ── Message handling ───────────────────────────────────────── */
    handleMessage(msg, fromPeerId) {
        var _a, _b, _c, _d, _e;
        switch (msg.type) {
            case 'rpc': {
                const payload = msg.payload;
                (_a = this.rpcHandlers.get(payload.name)) === null || _a === void 0 ? void 0 : _a(fromPeerId, payload.args);
                break;
            }
            case 'spawn':
                this.events.emit('network:spawn', msg.payload);
                break;
            case 'destroy':
                this.events.emit('network:destroy', msg.payload.netId);
                break;
            case 'snapshot':
                this.events.emit('network:snapshot', msg.payload);
                break;
            case 'ping': {
                const pong = {
                    type: 'pong', tick: this._tick,
                    senderId: (_c = (_b = this.peers) === null || _b === void 0 ? void 0 : _b.localPeerId) !== null && _c !== void 0 ? _c : '',
                    payload: msg.payload,
                };
                (_d = this.peers) === null || _d === void 0 ? void 0 : _d.sendTo(fromPeerId, JSON.stringify(pong));
                break;
            }
            case 'pong': {
                const sentAt = msg.payload;
                const latency = performance.now() - sentAt;
                const peer = (_e = this.peers) === null || _e === void 0 ? void 0 : _e.getPeer(fromPeerId);
                if (peer)
                    peer.latencyMs = latency;
                this.events.emit('network:latency', { peerId: fromPeerId, latencyMs: latency });
                break;
            }
        }
    }
}
/** Snapshot buffer for interpolation / reconciliation. */
class SnapshotBuffer {
    constructor(maxSize = 60) {
        this.buffer = [];
        this.maxSize = maxSize;
    }
    push(snapshot) {
        this.buffer.push(snapshot);
        if (this.buffer.length > this.maxSize)
            this.buffer.shift();
    }
    /** Get two snapshots bracketing the given server time for interpolation. */
    getInterpolationPair(serverTime) {
        for (let i = 0; i < this.buffer.length - 1; i++) {
            const a = this.buffer[i];
            const b = this.buffer[i + 1];
            if (a.timestamp <= serverTime && b.timestamp >= serverTime) {
                return [a, b];
            }
        }
        return null;
    }
    getLatest() {
        return this.buffer.length > 0 ? this.buffer[this.buffer.length - 1] : null;
    }
    getAtTick(tick) {
        return this.buffer.find(s => s.tick === tick);
    }
    clear() {
        this.buffer.length = 0;
    }
}
/** Linear interpolation between two entity states. */
function interpolateState(a, b, t) {
    return {
        netId: a.netId,
        position: {
            x: a.position.x + (b.position.x - a.position.x) * t,
            y: a.position.y + (b.position.y - a.position.y) * t,
        },
        rotation: a.rotation + (b.rotation - a.rotation) * t,
        velocity: {
            x: a.velocity.x + (b.velocity.x - a.velocity.x) * t,
            y: a.velocity.y + (b.velocity.y - a.velocity.y) * t,
        },
        custom: b.custom,
    };
}
class ClientPrediction {
    constructor() {
        this.pendingInputs = [];
    }
    addInput(input) {
        this.pendingInputs.push(input);
    }
    /** Clear inputs confirmed by server (tick <= serverTick). */
    acknowledge(serverTick) {
        this.pendingInputs = this.pendingInputs.filter(i => i.tick > serverTick);
    }
    /** Re-apply unconfirmed inputs after server correction. */
    getPendingInputs() {
        return this.pendingInputs;
    }
    clear() {
        this.pendingInputs.length = 0;
    }
}
class LobbyManager {
    constructor(localPeerId) {
        this.events = new EventBus.EventBus();
        this._room = null;
        this._players = new Map();
        this._localPeerId = localPeerId;
    }
    get room() { return this._room; }
    get players() { return Array.from(this._players.values()); }
    get isHost() { var _a; return ((_a = this._room) === null || _a === void 0 ? void 0 : _a.hostId) === this._localPeerId; }
    get allReady() { return this.players.every(p => p.ready); }
    /** Create a new room (as host). */
    createRoom(name, maxPlayers, gameMode, isPublic = true) {
        const room = {
            id: `room_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
            name,
            hostId: this._localPeerId,
            maxPlayers,
            currentPlayers: 1,
            isPublic,
            hasPassword: false,
            gameMode,
            metadata: {},
        };
        this._room = room;
        this.addPlayer(this._localPeerId, 'Host');
        this.events.emit('lobby:created', room);
        return room;
    }
    joinRoom(room, displayName) {
        this._room = room;
        this.addPlayer(this._localPeerId, displayName);
        this.events.emit('lobby:joined', room);
    }
    leaveRoom() {
        this._room = null;
        this._players.clear();
        this.events.emit('lobby:left', null);
    }
    addPlayer(peerId, displayName) {
        if (this._room && this._players.size >= this._room.maxPlayers)
            return;
        const player = {
            peerId,
            displayName,
            ready: false,
            team: 0,
            slot: this._players.size,
        };
        this._players.set(peerId, player);
        if (this._room)
            this._room.currentPlayers = this._players.size;
        this.events.emit('lobby:playerJoined', player);
    }
    removePlayer(peerId) {
        this._players.delete(peerId);
        if (this._room)
            this._room.currentPlayers = this._players.size;
        this.events.emit('lobby:playerLeft', peerId);
        // Host migration
        if (this._room && this._room.hostId === peerId) {
            const remaining = this.players;
            if (remaining.length > 0) {
                this._room.hostId = remaining[0].peerId;
                this.events.emit('lobby:hostMigrated', this._room.hostId);
            }
        }
    }
    setReady(peerId, ready) {
        const p = this._players.get(peerId);
        if (p) {
            p.ready = ready;
            this.events.emit('lobby:readyChanged', { peerId, ready });
        }
    }
    setTeam(peerId, team) {
        const p = this._players.get(peerId);
        if (p) {
            p.team = team;
            this.events.emit('lobby:teamChanged', { peerId, team });
        }
    }
    /** Serialize lobby state for broadcasting. */
    toJSON() {
        return {
            room: this._room,
            players: this.players,
        };
    }
    /** Apply state from host. */
    fromJSON(data) {
        this._room = data.room;
        this._players.clear();
        for (const p of data.players) {
            this._players.set(p.peerId, p);
        }
        this.events.emit('lobby:synced', data);
    }
}

exports.ClientPrediction = ClientPrediction;
exports.LobbyManager = LobbyManager;
exports.NetworkManager = NetworkManager;
exports.PeerManager = PeerManager;
exports.SnapshotBuffer = SnapshotBuffer;
exports.createWebSocketSignaling = createWebSocketSignaling;
exports.interpolateState = interpolateState;
//# sourceMappingURL=Networking.js.map
