"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.AtemSocket = void 0;
const eventemitter3_1 = require("eventemitter3");
const atemCommandParser_1 = require("./atemCommandParser");
const exitHook = require("exit-hook");
const commands_1 = require("../commands");
const atem_1 = require("../atem");
const threadedclass_1 = require("threadedclass");
const packetBuilder_1 = require("./packetBuilder");
class AtemSocket extends eventemitter3_1.EventEmitter {
    constructor(options) {
        super();
        this._commandParser = new atemCommandParser_1.CommandParser();
        this._nextPacketTrackingId = 0;
        this._isDisconnecting = false;
        this._port = atem_1.DEFAULT_PORT;
        this._address = options.address;
        this._port = options.port;
        this._debugBuffers = options.debugBuffers;
        this._disableMultithreaded = options.disableMultithreaded;
        this._childProcessTimeout = options.childProcessTimeout;
        this._maxPacketSize = options.maxPacketSize;
    }
    async connect(address, port) {
        this._isDisconnecting = false;
        if (address) {
            this._address = address;
        }
        if (port) {
            this._port = port;
        }
        if (!this._socketProcess) {
            // cache the creation promise, in case `destroy` is called before it completes
            this._creatingSocket = this._createSocketProcess();
            await this._creatingSocket;
            if (this._isDisconnecting || !this._socketProcess) {
                throw new Error('Disconnecting');
            }
        }
        await this._socketProcess.connect(this._address, this._port);
    }
    async destroy() {
        await this.disconnect();
        // Ensure thread creation has finished if it was started
        if (this._creatingSocket)
            await this._creatingSocket.catch(() => null);
        if (this._socketProcess) {
            await threadedclass_1.ThreadedClassManager.destroy(this._socketProcess);
            this._socketProcess = undefined;
        }
        if (this._exitUnsubscribe) {
            this._exitUnsubscribe();
            this._exitUnsubscribe = undefined;
        }
    }
    async disconnect() {
        this._isDisconnecting = true;
        if (this._socketProcess) {
            await this._socketProcess.disconnect();
        }
    }
    get nextPacketTrackingId() {
        if (this._nextPacketTrackingId >= Number.MAX_SAFE_INTEGER) {
            this._nextPacketTrackingId = 0;
        }
        return ++this._nextPacketTrackingId;
    }
    async sendCommands(commands) {
        if (!this._socketProcess)
            throw new Error('Socket process is not open');
        const maxPacketSize = this._maxPacketSize - 12; // MTU minus ATEM header
        const packetBuilder = new packetBuilder_1.PacketBuilder(maxPacketSize, this._commandParser.version);
        for (const cmd of commands) {
            packetBuilder.addCommand(cmd);
        }
        const packets = packetBuilder.getPackets().map((buffer) => ({
            payloadLength: buffer.length,
            payloadHex: buffer.toString('hex'),
            trackingId: this.nextPacketTrackingId,
        }));
        if (this._debugBuffers)
            this.emit('debug', `PAYLOAD PACKETS ${JSON.stringify(packets)}`);
        if (packets.length > 0) {
            await this._socketProcess.sendPackets(packets);
        }
        return packets.map((packet) => packet.trackingId);
    }
    async _createSocketProcess() {
        this._socketProcess = await (0, threadedclass_1.threadedClass)('./atemSocketChild', 'AtemSocketChild', [
            {
                address: this._address,
                port: this._port,
                debugBuffers: this._debugBuffers,
            },
            async () => {
                this.emit('disconnect');
            },
            async (message) => {
                this.emit('info', message);
            },
            async (payload) => {
                this._parseCommands(Buffer.from(payload));
            },
            async (ids) => {
                this.emit('ackPackets', ids.map((id) => id.trackingId));
            }, // onPacketsAcknowledged
        ], {
            instanceName: 'atem-connection',
            freezeLimit: this._childProcessTimeout,
            autoRestart: true,
            disableMultithreading: this._disableMultithreaded,
        });
        threadedclass_1.ThreadedClassManager.onEvent(this._socketProcess, 'restarted', () => {
            this.connect().catch((error) => {
                const errorMsg = `Failed to reconnect after respawning socket process: ${error}`;
                this.emit('error', errorMsg);
            });
        });
        threadedclass_1.ThreadedClassManager.onEvent(this._socketProcess, 'thread_closed', () => {
            this.emit('disconnect');
        });
        this._exitUnsubscribe = exitHook(() => {
            this.destroy().catch(() => null);
        });
    }
    _parseCommands(buffer) {
        const parsedCommands = [];
        while (buffer.length > 8) {
            const length = buffer.readUInt16BE(0);
            const name = buffer.toString('ascii', 4, 8);
            if (length < 8) {
                // Commands are never less than 8, as that is the header
                break;
            }
            const cmdConstructor = this._commandParser.commandFromRawName(name);
            if (cmdConstructor && typeof cmdConstructor.deserialize === 'function') {
                try {
                    const cmd = cmdConstructor.deserialize(buffer.slice(8, length), this._commandParser.version);
                    if (cmd instanceof commands_1.VersionCommand) {
                        // init started
                        this._commandParser.version = cmd.properties.version;
                    }
                    parsedCommands.push(cmd);
                }
                catch (e) {
                    this.emit('error', `Failed to deserialize command: ${cmdConstructor.constructor.name}: ${e}`);
                }
            }
            else {
                this.emit('debug', `Unknown command ${name} (${length}b)`);
            }
            // Trim the buffer
            buffer = buffer.slice(length);
        }
        if (parsedCommands.length > 0) {
            this.emit('receivedCommands', parsedCommands);
        }
        return parsedCommands;
    }
}
exports.AtemSocket = AtemSocket;
//# sourceMappingURL=atemSocket.js.map