const commands = {
    NOP: 0x00,
    ListServices: 0x04,
    ListIdentity: 0x63,
    ListInterfaces: 0x64,
    RegisterSession: 0x65, // Begin Session Command
    UnregisterSession: 0x66, // Close Session Command
    SendRRData: 0x6f, // Send Unconnected Data Command
    SendUnitData: 0x70, // Send Connnected Data Command
    IndicateStatus: 0x72,
    Cancel: 0x73
};

// region Validation Helper Functions

/**
 * Parses Encapulation Status Code to Human Readable Error Message.
 *
 * @param status - Status Code
 * @returns Human Readable Error Message
 */
const parseStatus = (status: number): string => {
    if (typeof status !== "number") throw new Error("parseStatus accepts type <Number> only!");

    /* eslint-disable indent */
    switch (status) {
        case 0x00:
            return "SUCCESS";
        case 0x01:
            return "FAIL: Sender issued an invalid ecapsulation command.";
        case 0x02:
            return "FAIL: Insufficient memory resources to handle command.";
        case 0x03:
            return "FAIL: Poorly formed or incorrect data in encapsulation packet.";
        case 0x64:
            return "FAIL: Originator used an invalid session handle.";
        case 0x65:
            return "FAIL: Target received a message of invalid length.";
        case 0x69:
            return "FAIL: Unsupported encapsulation protocol revision.";
        default:
            return `FAIL: General failure <${status}> occured.`;
    }
    /* eslint-enable indent */
};

/**
 * Checks if Command is a Valid Encapsulation Command
 *
 * @param cmd - Encapsulation command
 * @returns test result
 */
const validateCommand = (cmd: number): boolean => {
    for (let key of Object.keys(commands)) {
        if (cmd === commands[key]) return true;
    }
    return false;
};
// endregion

// region Compact Packet Format

type CommonPacketData = {
    TypeID: number, 
    data: Buffer,   
    length: number 
}

let CPF = {
    ItemIDs:{
        Null: 0x00,
        ListIdentity: 0x0c,
        ConnectionBased: 0xa1,
        ConnectedTransportPacket: 0xb1,
        UCMM: 0xb2,
        ListServices: 0x100,
        SockaddrO2T: 0x8000,
        SockaddrT2O: 0x8001,
        SequencedAddrItem: 0x8002
    },

    /**
     * Checks if Command is a Valid Encapsulation Command
     *
     * @param cmd - Encapsulation command
     * @returns test result
     */
    isCmd: (cmd: number): boolean => {
        for (let key of Object.keys(CPF.ItemIDs)) {
            if (cmd === CPF.ItemIDs[key]) return true;
        }
        return false; 
    },

    /**
     * Builds a Common Packet Formatted Buffer to be
     * Encapsulated.
     *
     * @param dataItems - Array of CPF Data Items
     * @returns CPF Buffer to be Encapsulated
     */
    build: (dataItems: CommonPacketData[]): Buffer => {
        // Write Item Count and Initialize Buffer
        let buf = Buffer.alloc(2);
        buf.writeUInt16LE(dataItems.length, 0);

        for (let item of dataItems) {
            const { TypeID, data } = item;

            if (!CPF.isCmd(TypeID)) throw new Error("Invalid CPF Type ID!");

            let buf1 = Buffer.alloc(4);
            let buf2 = Buffer.from(data);

            buf1.writeUInt16LE(TypeID, 0);
            buf1.writeUInt16LE(buf2.length, 2);

            buf = buf2.length > 0 ? Buffer.concat([buf, buf1, buf2]) : Buffer.concat([buf, buf1]);
        }

        return buf;
    },

    /**
    * Parses Incoming Common Packet Formatted Buffer
    * and returns an Array of Objects.
    *
    * @param {Buffer} buf - Common Packet Formatted Data Buffer
    * @returns {Array} Array of Common Packet Data Objects
    */
    parse: (buf: Buffer): CommonPacketData[] => {
        const itemCount = buf.readUInt16LE(0);

        let ptr = 2;
        let arr: CommonPacketData[] = [];

        for (let i = 0; i < itemCount; i++) {
            // Get Type ID
            const TypeID = buf.readUInt16LE(ptr);
            ptr += 2;

            // Get Data Length
            const length = buf.readUInt16LE(ptr);
            ptr += 2;

            // Get Data from Data Buffer
            const data = Buffer.alloc(length);
            buf.copy(data, 0, ptr, ptr + length);

            // Append Gathered Data Object to Return Array
            arr.push({ TypeID, length, data});

            ptr += length;
        }

        return arr;
    }
};

// endregion

// region Header Assemble Method Definitions

type EncapsulationData = {
    commandCode: number,    // Ecapsulation Command Code
    command: string,        // Encapsulation Command String Interpretation
    length: number,         // Length of Encapsulated Data
    session: number,        // Session ID
    statusCode: number,     // Status Code
    status: string,         // Status Code String Interpretation
    options: number,        // Options (Typically 0x00)
    data: Buffer            // Encapsulated Data Buffer
}

let header = {
    /**
     * Builds an ENIP Encapsolated Packet
     *
     * @param cmd - Command to Send
     * @param session - Session ID
     * @param data - Data to Send
     * @returns  Generated Buffer to be Sent to Target
     */
    build: (cmd: number, session: number = 0x00, data: Buffer | [] = []): Buffer => {
        // Validate requested command
        if (!validateCommand(cmd)) throw new Error("Invalid Encapsulation Command!");

        const buf = Buffer.from(data);
        
        // Initialize header buffer to appropriate length
        let header = Buffer.alloc(24 + buf.length, 0);

        // Build header from encapsulation data
        header.writeUInt16LE(cmd, 0);
        header.writeUInt16LE(buf.length, 2);
        header.writeUInt32LE(session, 4);       
        buf.copy(header, 24);

        return header;
    },
    
    /**
     * Parses an Encapsulated Packet Received from ENIP Target
     *
     * @param buf - Incoming Encapsulated Buffer from Target
     * @returns Parsed Encapsulation Data Object
     */
    parse: (buf: Buffer): EncapsulationData => {
        if (!Buffer.isBuffer(buf)) throw new Error("header.parse accepts type <Buffer> only!");

        const received = {
            commandCode: buf.readUInt16LE(0),
            command: null,
            length: buf.readUInt16LE(2),
            session: buf.readUInt32LE(4),
            statusCode: buf.readUInt32LE(8),
            status: null,
            options: buf.readUInt32LE(20),
            data: null
        };

        // Get Returned Encapsulated Data
        let dataBuffer = Buffer.alloc(received.length);
        buf.copy(dataBuffer, 0, 24);

        received.data = dataBuffer;
        received.status = parseStatus(received.statusCode);

        for (let key of Object.keys(commands)) {
            if (received.commandCode === commands[key]) {
                received.command = key;
                break;
            }
        }

        return received;
    },
    

};

// endregion

// region Common Command Helper Build Funtions

/**
 * Returns a Register Session Request String
 *
 * @returns register session buffer
 */
const registerSession = (): Buffer => {
    const { RegisterSession } = commands;
    const { build } = header;
    const cmdBuf = Buffer.alloc(4);
    cmdBuf.writeUInt16LE(0x01, 0); // Protocol Version (Required to be 1)
    cmdBuf.writeUInt16LE(0x00, 2); // Opton Flags (Reserved for Future List)

    // Build Register Session Buffer and return it
    return build(RegisterSession, 0x00, cmdBuf);
};

/**
 * Returns an Unregister Session Request String
 *
 * @param session - Encapsulation Session ID
 * @returns unregister session buffer
 */
const unregisterSession = (session: number):Buffer => {
    const { UnregisterSession } = commands;
    const { build } = header;

    // Build Unregister Session Buffer
    return build(UnregisterSession, session);
};

/**
 * Returns a UCMM Encapsulated Packet String
 *
 * @param session - Encapsulation Session ID
 * @param data - Data to be Sent via UCMM
 * @param timeout - Timeout (sec)
 * @returns UCMM Encapsulated Message Buffer
 */
const sendRRData = (session: number, data: Buffer, timeout: number = 10): Buffer => {
    const { SendRRData } = commands;

    let timeoutBuf = Buffer.alloc(6);
    timeoutBuf.writeUInt32LE(0x00, 0); // Interface Handle ID (Shall be 0 for CIP)
    timeoutBuf.writeUInt16LE(timeout, 4); // Timeout (sec)

    // Enclose in Common Packet Format
    let buf = CPF.build([
        { TypeID: CPF.ItemIDs.Null, data: Buffer.from([]), length: undefined},
        { TypeID: CPF.ItemIDs.UCMM, data: data, length: undefined}
    ]);

    // Join Timeout Data with
    buf = Buffer.concat([timeoutBuf, buf]);

    // Build SendRRData Buffer
    return header.build(SendRRData, session, buf);
};

/**
 * Returns a Connected Message Datagram (Transport Class 3) String
 *
 * @param {number} session - Encapsulation Session ID
 * @param {Buffer} data - Data to be Sent via Connected Message
 * @param {number} ConnectionID - Connection ID from FWD_OPEN
 * @param {number} SequenceNumber - Sequence Number of Datagram
 * @returns Connected Message Datagram Buffer
 */
const sendUnitData = (session: number, data: Buffer, ConnectionID: number, SequnceNumber: number): Buffer => {
    const { SendUnitData } = commands;

    let timeoutBuf = Buffer.alloc(6);
    timeoutBuf.writeUInt32LE(0x00, 0); // Interface Handle ID (Shall be 0 for CIP)
    timeoutBuf.writeUInt16LE(0x00, 4); // Timeout (sec) (Shall be 0 for Connected Messages)

    // Enclose in Common Packet Format
    const seqAddrBuf = Buffer.alloc(4);
    seqAddrBuf.writeUInt32LE(ConnectionID, 0);
    const seqNumberBuf = Buffer.alloc(2);
    seqNumberBuf.writeUInt16LE(SequnceNumber, 0);
    const ndata = Buffer.concat([
        seqNumberBuf,
        data
    ]);

    let buf = CPF.build([
        {
            TypeID: CPF.ItemIDs.ConnectionBased,
            data: seqAddrBuf,
            length: undefined
        },
        {
            TypeID: CPF.ItemIDs.ConnectedTransportPacket,
            data: ndata,
            length: undefined
        }
    ]);

    // Join Timeout Data with
    buf = Buffer.concat([timeoutBuf, buf]);

    // Build SendRRData Buffer
    return header.build(SendUnitData, session, buf);
};
// endregion

export {
    header,
    CPF,
    validateCommand,
    commands,
    parseStatus,
    registerSession,
    unregisterSession,
    sendRRData,
    sendUnitData,
    EncapsulationData,
    CommonPacketData
};
