"use strict";
/* eslint-disable @typescript-eslint/no-unused-vars */
/**
 * WASM Socket Manager
 *
 * Manages UDP and TCP sockets for WASM plugins that need raw network access
 * (e.g., radar plugins, NMEA receivers, etc.)
 *
 * Uses Node.js dgram and net modules, bridged to WASM via FFI
 */
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.tcpSocketManager = exports.socketManager = void 0;
const dgram = __importStar(require("dgram"));
const net = __importStar(require("net"));
const debug_1 = __importDefault(require("debug"));
const debug = (0, debug_1.default)('signalk:wasm:sockets');
/**
 * Socket Manager - singleton for managing plugin sockets
 */
class SocketManager {
    sockets = new Map();
    nextSocketId = 1;
    /**
     * Create a new UDP socket
     * @param pluginId - Plugin that owns the socket
     * @param type - Socket type: 'udp4' or 'udp6'
     * @returns Socket ID, or -1 on error
     */
    createSocket(pluginId, type = 'udp4') {
        try {
            const socketId = this.nextSocketId++;
            const socket = dgram.createSocket({
                type,
                reuseAddr: true // Allow multiple plugins to bind to same port
            });
            const managed = {
                socket,
                pluginId,
                bound: false,
                bindPromise: null,
                receiveBuffer: [],
                maxBufferSize: 1000, // Max buffered datagrams
                multicastGroups: new Set(),
                pendingOptions: []
            };
            // Set up message handler to buffer incoming data
            socket.on('message', (msg, rinfo) => {
                if (managed.receiveBuffer.length >= managed.maxBufferSize) {
                    // Drop oldest message if buffer full
                    managed.receiveBuffer.shift();
                }
                managed.receiveBuffer.push({
                    data: Buffer.from(msg), // Copy the buffer
                    address: rinfo.address,
                    port: rinfo.port,
                    timestamp: Date.now()
                });
            });
            socket.on('error', (err) => {
                debug(`[${pluginId}] Socket ${socketId} error: ${err.message}`);
            });
            socket.on('close', () => {
                debug(`[${pluginId}] Socket ${socketId} closed`);
                this.sockets.delete(socketId);
            });
            this.sockets.set(socketId, managed);
            debug(`[${pluginId}] Created socket ${socketId} (${type})`);
            return socketId;
        }
        catch (error) {
            debug(`Failed to create socket: ${error}`);
            return -1;
        }
    }
    /**
     * Bind socket to a port
     * @param socketId - Socket to bind
     * @param port - Port number (0 for any available port)
     * @param address - Address to bind to (optional, defaults to all interfaces)
     * @returns 0 on success, -1 on error
     */
    bind(socketId, port, address) {
        const managed = this.sockets.get(socketId);
        if (!managed) {
            debug(`Socket ${socketId} not found`);
            return Promise.resolve(-1);
        }
        // Store the promise so setBroadcast etc. can wait for it
        managed.bindPromise = new Promise((resolve) => {
            try {
                managed.socket.bind(port, address, () => {
                    managed.bound = true;
                    const addr = managed.socket.address();
                    debug(`[${managed.pluginId}] Socket ${socketId} bound to ${addr.address}:${addr.port}`);
                    // Apply any pending socket options now that we're bound
                    for (const option of managed.pendingOptions) {
                        try {
                            if (option.type === 'broadcast') {
                                managed.socket.setBroadcast(option.value);
                                debug(`[${managed.pluginId}] Applied deferred setBroadcast(${option.value})`);
                            }
                            else if (option.type === 'multicastTTL') {
                                managed.socket.setMulticastTTL(option.value);
                                debug(`[${managed.pluginId}] Applied deferred setMulticastTTL(${option.value})`);
                            }
                            else if (option.type === 'multicastLoopback') {
                                managed.socket.setMulticastLoopback(option.value);
                                debug(`[${managed.pluginId}] Applied deferred setMulticastLoopback(${option.value})`);
                            }
                            else if (option.type === 'joinMulticast') {
                                const { multicastAddress, interfaceAddress } = option.value;
                                if (interfaceAddress) {
                                    managed.socket.addMembership(multicastAddress, interfaceAddress);
                                }
                                else {
                                    managed.socket.addMembership(multicastAddress);
                                }
                                managed.multicastGroups.add(multicastAddress);
                                debug(`[${managed.pluginId}] Applied deferred joinMulticast(${multicastAddress})`);
                            }
                            else if (option.type === 'leaveMulticast') {
                                const { multicastAddress, interfaceAddress } = option.value;
                                if (interfaceAddress) {
                                    managed.socket.dropMembership(multicastAddress, interfaceAddress);
                                }
                                else {
                                    managed.socket.dropMembership(multicastAddress);
                                }
                                managed.multicastGroups.delete(multicastAddress);
                                debug(`[${managed.pluginId}] Applied deferred leaveMulticast(${multicastAddress})`);
                            }
                        }
                        catch (optionError) {
                            debug(`[${managed.pluginId}] Error applying deferred option ${option.type}: ${optionError}`);
                        }
                    }
                    managed.pendingOptions = [];
                    resolve(0);
                });
            }
            catch (error) {
                debug(`[${managed.pluginId}] Bind error: ${error}`);
                resolve(-1);
            }
        });
        return managed.bindPromise;
    }
    /**
     * Join a multicast group
     * @param socketId - Socket to use
     * @param multicastAddress - Multicast group address (e.g., "239.254.2.0")
     * @param interfaceAddress - Interface address to use (optional)
     * @returns 0 on success, -1 on error
     */
    joinMulticast(socketId, multicastAddress, interfaceAddress) {
        const managed = this.sockets.get(socketId);
        if (!managed) {
            debug(`Socket ${socketId} not found`);
            return -1;
        }
        // If socket is not yet bound, defer the multicast join until bind completes
        if (!managed.bound) {
            debug(`[${managed.pluginId}] Deferring joinMulticast(${multicastAddress}) until socket is bound`);
            managed.pendingOptions.push({
                type: 'joinMulticast',
                value: { multicastAddress, interfaceAddress }
            });
            return 0;
        }
        try {
            if (interfaceAddress) {
                managed.socket.addMembership(multicastAddress, interfaceAddress);
            }
            else {
                managed.socket.addMembership(multicastAddress);
            }
            managed.multicastGroups.add(multicastAddress);
            debug(`[${managed.pluginId}] Socket ${socketId} joined multicast ${multicastAddress}`);
            return 0;
        }
        catch (error) {
            debug(`[${managed.pluginId}] Join multicast error: ${error}`);
            return -1;
        }
    }
    /**
     * Leave a multicast group
     * @param socketId - Socket to use
     * @param multicastAddress - Multicast group address
     * @param interfaceAddress - Interface address (optional)
     * @returns 0 on success, -1 on error
     */
    leaveMulticast(socketId, multicastAddress, interfaceAddress) {
        const managed = this.sockets.get(socketId);
        if (!managed) {
            debug(`Socket ${socketId} not found`);
            return -1;
        }
        // If socket is not yet bound, defer the multicast leave until bind completes
        if (!managed.bound) {
            debug(`[${managed.pluginId}] Deferring leaveMulticast(${multicastAddress}) until socket is bound`);
            managed.pendingOptions.push({
                type: 'leaveMulticast',
                value: { multicastAddress, interfaceAddress }
            });
            return 0;
        }
        try {
            if (interfaceAddress) {
                managed.socket.dropMembership(multicastAddress, interfaceAddress);
            }
            else {
                managed.socket.dropMembership(multicastAddress);
            }
            managed.multicastGroups.delete(multicastAddress);
            debug(`[${managed.pluginId}] Socket ${socketId} left multicast ${multicastAddress}`);
            return 0;
        }
        catch (error) {
            debug(`[${managed.pluginId}] Leave multicast error: ${error}`);
            return -1;
        }
    }
    /**
     * Set socket options
     */
    setMulticastTTL(socketId, ttl) {
        const managed = this.sockets.get(socketId);
        if (!managed)
            return -1;
        // If socket is not yet bound, defer the option
        if (!managed.bound) {
            debug(`[${managed.pluginId}] Deferring setMulticastTTL(${ttl}) until socket is bound`);
            managed.pendingOptions.push({ type: 'multicastTTL', value: ttl });
            return 0;
        }
        try {
            managed.socket.setMulticastTTL(ttl);
            return 0;
        }
        catch (error) {
            debug(`[${managed.pluginId}] setMulticastTTL error: ${error}`);
            return -1;
        }
    }
    setMulticastLoopback(socketId, enabled) {
        const managed = this.sockets.get(socketId);
        if (!managed)
            return -1;
        // If socket is not yet bound, defer the option
        if (!managed.bound) {
            debug(`[${managed.pluginId}] Deferring setMulticastLoopback(${enabled}) until socket is bound`);
            managed.pendingOptions.push({ type: 'multicastLoopback', value: enabled });
            return 0;
        }
        try {
            managed.socket.setMulticastLoopback(enabled);
            return 0;
        }
        catch (error) {
            debug(`[${managed.pluginId}] setMulticastLoopback error: ${error}`);
            return -1;
        }
    }
    setBroadcast(socketId, enabled) {
        const managed = this.sockets.get(socketId);
        if (!managed)
            return -1;
        // If socket is not yet bound, defer the option
        if (!managed.bound) {
            debug(`[${managed.pluginId}] Deferring setBroadcast(${enabled}) until socket is bound`);
            managed.pendingOptions.push({ type: 'broadcast', value: enabled });
            return 0;
        }
        try {
            managed.socket.setBroadcast(enabled);
            return 0;
        }
        catch (error) {
            debug(`[${managed.pluginId}] setBroadcast error: ${error}`);
            return -1;
        }
    }
    /**
     * Send data via UDP
     * @param socketId - Socket to use
     * @param data - Data to send
     * @param address - Destination address
     * @param port - Destination port
     * @returns Bytes sent, or -1 on error
     */
    send(socketId, data, address, port) {
        return new Promise((resolve) => {
            const managed = this.sockets.get(socketId);
            if (!managed) {
                debug(`Socket ${socketId} not found`);
                resolve(-1);
                return;
            }
            managed.socket.send(data, port, address, (err, bytes) => {
                if (err) {
                    debug(`[${managed.pluginId}] Send error: ${err}`);
                    resolve(-1);
                }
                else {
                    resolve(bytes);
                }
            });
        });
    }
    /**
     * Receive data from buffer (non-blocking)
     * @param socketId - Socket to receive from
     * @returns Buffered datagram, or null if buffer empty
     */
    receive(socketId) {
        const managed = this.sockets.get(socketId);
        if (!managed) {
            debug(`Socket ${socketId} not found`);
            return null;
        }
        return managed.receiveBuffer.shift() || null;
    }
    /**
     * Get number of buffered datagrams
     */
    getBufferedCount(socketId) {
        const managed = this.sockets.get(socketId);
        return managed ? managed.receiveBuffer.length : 0;
    }
    /**
     * Close a socket
     * @param socketId - Socket to close
     */
    close(socketId) {
        const managed = this.sockets.get(socketId);
        if (!managed) {
            debug(`Socket ${socketId} not found`);
            return;
        }
        try {
            // Leave all multicast groups first
            for (const group of managed.multicastGroups) {
                try {
                    managed.socket.dropMembership(group);
                }
                catch (e) {
                    // Ignore errors when leaving groups during close
                }
            }
            managed.socket.close();
            this.sockets.delete(socketId);
            debug(`[${managed.pluginId}] Socket ${socketId} closed`);
        }
        catch (error) {
            debug(`[${managed.pluginId}] Close error: ${error}`);
        }
    }
    /**
     * Close all sockets for a plugin (cleanup on plugin stop)
     */
    closeAllForPlugin(pluginId) {
        const toClose = [];
        for (const [id, managed] of this.sockets) {
            if (managed.pluginId === pluginId) {
                toClose.push(id);
            }
        }
        for (const id of toClose) {
            this.close(id);
        }
        debug(`[${pluginId}] Closed ${toClose.length} sockets`);
    }
    /**
     * Get socket statistics
     */
    getStats() {
        const socketsPerPlugin = {};
        for (const managed of this.sockets.values()) {
            socketsPerPlugin[managed.pluginId] =
                (socketsPerPlugin[managed.pluginId] || 0) + 1;
        }
        return {
            totalSockets: this.sockets.size,
            socketsPerPlugin
        };
    }
}
// Export singleton instance
exports.socketManager = new SocketManager();
/**
 * TCP Socket Manager - manages TCP connections for WASM plugins
 *
 * Key differences from UDP:
 * - Connection-oriented (connect before send)
 * - Line-buffered receive (splits on \r\n or \n)
 */
class TcpSocketManager {
    sockets = new Map();
    nextSocketId = 1;
    /**
     * Create a new TCP socket
     * @param pluginId - Plugin that owns the socket
     * @returns Socket ID, or -1 on error
     */
    createSocket(pluginId) {
        try {
            const socketId = this.nextSocketId++;
            const socket = new net.Socket();
            const managed = {
                socket,
                pluginId,
                connected: false,
                connecting: false,
                receiveBuffer: [],
                rawBuffer: [],
                partialLine: '',
                maxBufferSize: 1000,
                error: null,
                useLineBuffering: true // Default to line buffering
            };
            // Set up data handler
            socket.on('data', (data) => {
                if (managed.useLineBuffering) {
                    // Line-buffered mode for text protocols
                    managed.partialLine += data.toString();
                    // Split on line endings (\r\n or \n)
                    const lines = managed.partialLine.split(/\r?\n/);
                    // Last element is either empty (if data ended with newline) or partial
                    managed.partialLine = lines.pop() || '';
                    // Add complete lines to buffer
                    for (const line of lines) {
                        if (line.length > 0) {
                            if (managed.receiveBuffer.length >= managed.maxBufferSize) {
                                managed.receiveBuffer.shift(); // Drop oldest
                            }
                            managed.receiveBuffer.push(line);
                        }
                    }
                }
                else {
                    // Raw mode for binary protocols
                    if (managed.rawBuffer.length >= managed.maxBufferSize) {
                        managed.rawBuffer.shift(); // Drop oldest
                    }
                    managed.rawBuffer.push(Buffer.from(data));
                }
            });
            socket.on('connect', () => {
                managed.connected = true;
                managed.connecting = false;
                managed.error = null;
                debug(`[${pluginId}] TCP socket ${socketId} connected`);
            });
            socket.on('error', (err) => {
                managed.error = err.message;
                managed.connected = false;
                managed.connecting = false;
                debug(`[${pluginId}] TCP socket ${socketId} error: ${err.message}`);
            });
            socket.on('close', () => {
                managed.connected = false;
                managed.connecting = false;
                this.sockets.delete(socketId);
                debug(`[${pluginId}] TCP socket ${socketId} closed`);
            });
            socket.on('end', () => {
                managed.connected = false;
                debug(`[${pluginId}] TCP socket ${socketId} ended by remote`);
            });
            this.sockets.set(socketId, managed);
            debug(`[${pluginId}] Created TCP socket ${socketId}`);
            return socketId;
        }
        catch (error) {
            debug(`Failed to create TCP socket: ${error}`);
            return -1;
        }
    }
    /**
     * Connect to a remote host
     * @param socketId - Socket to connect
     * @param address - Remote host address
     * @param port - Remote port
     * @returns 0 if connection initiated, -1 on error
     */
    connect(socketId, address, port) {
        const managed = this.sockets.get(socketId);
        if (!managed) {
            debug(`TCP socket ${socketId} not found`);
            return -1;
        }
        if (managed.connected || managed.connecting) {
            debug(`[${managed.pluginId}] TCP socket ${socketId} already connected/connecting`);
            return -1;
        }
        try {
            managed.connecting = true;
            managed.error = null;
            managed.socket.connect(port, address);
            debug(`[${managed.pluginId}] TCP socket ${socketId} connecting to ${address}:${port}`);
            return 0;
        }
        catch (error) {
            managed.connecting = false;
            managed.error = String(error);
            debug(`[${managed.pluginId}] TCP connect error: ${error}`);
            return -1;
        }
    }
    /**
     * Check if socket is connected
     * @param socketId - Socket to check
     * @returns 1 if connected, 0 if not, -1 if socket not found
     */
    isConnected(socketId) {
        const managed = this.sockets.get(socketId);
        if (!managed) {
            return -1;
        }
        return managed.connected ? 1 : 0;
    }
    /**
     * Send data over TCP
     * @param socketId - Socket to use
     * @param data - Data to send
     * @returns Bytes sent, or -1 on error
     */
    send(socketId, data) {
        return new Promise((resolve) => {
            const managed = this.sockets.get(socketId);
            if (!managed) {
                debug(`TCP socket ${socketId} not found`);
                resolve(-1);
                return;
            }
            if (!managed.connected) {
                debug(`[${managed.pluginId}] TCP socket ${socketId} not connected`);
                resolve(-1);
                return;
            }
            managed.socket.write(data, (err) => {
                if (err) {
                    debug(`[${managed.pluginId}] TCP send error: ${err}`);
                    resolve(-1);
                }
                else {
                    resolve(data.length);
                }
            });
        });
    }
    /**
     * Receive a complete line (non-blocking)
     * @param socketId - Socket to receive from
     * @returns Complete line without line ending, or null if no complete line available
     */
    receiveLine(socketId) {
        const managed = this.sockets.get(socketId);
        if (!managed) {
            debug(`TCP socket ${socketId} not found`);
            return null;
        }
        return managed.receiveBuffer.shift() || null;
    }
    /**
     * Receive raw data (non-blocking)
     * @param socketId - Socket to receive from
     * @returns Raw data buffer, or null if no data available
     */
    receiveRaw(socketId) {
        const managed = this.sockets.get(socketId);
        if (!managed) {
            debug(`TCP socket ${socketId} not found`);
            return null;
        }
        return managed.rawBuffer.shift() || null;
    }
    /**
     * Set buffering mode
     * @param socketId - Socket to configure
     * @param lineBuffering - true for line-buffered (text), false for raw (binary)
     * @returns 0 on success, -1 on error
     */
    setLineBuffering(socketId, lineBuffering) {
        const managed = this.sockets.get(socketId);
        if (!managed) {
            return -1;
        }
        managed.useLineBuffering = lineBuffering;
        debug(`[${managed.pluginId}] TCP socket ${socketId} buffering mode: ${lineBuffering ? 'line' : 'raw'}`);
        return 0;
    }
    /**
     * Get number of buffered items (lines or raw chunks)
     */
    getBufferedCount(socketId) {
        const managed = this.sockets.get(socketId);
        if (!managed)
            return 0;
        return managed.useLineBuffering
            ? managed.receiveBuffer.length
            : managed.rawBuffer.length;
    }
    /**
     * Get last error message
     */
    getError(socketId) {
        const managed = this.sockets.get(socketId);
        return managed ? managed.error : null;
    }
    /**
     * Close a TCP socket
     * @param socketId - Socket to close
     */
    close(socketId) {
        const managed = this.sockets.get(socketId);
        if (!managed) {
            debug(`TCP socket ${socketId} not found`);
            return;
        }
        try {
            managed.socket.destroy();
            this.sockets.delete(socketId);
            debug(`[${managed.pluginId}] TCP socket ${socketId} closed`);
        }
        catch (error) {
            debug(`[${managed.pluginId}] TCP close error: ${error}`);
        }
    }
    /**
     * Close all TCP sockets for a plugin
     */
    closeAllForPlugin(pluginId) {
        const toClose = [];
        for (const [id, managed] of this.sockets) {
            if (managed.pluginId === pluginId) {
                toClose.push(id);
            }
        }
        for (const id of toClose) {
            this.close(id);
        }
        debug(`[${pluginId}] Closed ${toClose.length} TCP sockets`);
    }
    /**
     * Get TCP socket statistics
     */
    getStats() {
        const socketsPerPlugin = {};
        for (const managed of this.sockets.values()) {
            socketsPerPlugin[managed.pluginId] =
                (socketsPerPlugin[managed.pluginId] || 0) + 1;
        }
        return {
            totalSockets: this.sockets.size,
            socketsPerPlugin
        };
    }
}
// Export TCP socket manager singleton
exports.tcpSocketManager = new TcpSocketManager();
//# sourceMappingURL=socket-manager.js.map