import * as EthernetIP from "../enip"; 
import dgram, { Socket } from "dgram";
import { EventEmitter } from "events";

const listIdentityRequest = EthernetIP.encapsulation.header.build(EthernetIP.encapsulation.commands.ListIdentity);

type browserDevice = {
    EncapsulationVersion: number,
    socketAddress: {
        sin_family: number,
        sin_port: number,
        sin_addr: string,
        sin_zero: Buffer
    },
    vendorID: number,
    deviceType: number,
    productCode: number,
    revision: string,
    status: number,
    serialNumber: string,
    productName: string,
    state: number,
    timestamp: number
}

export declare interface Browser extends EventEmitter {
    socket: Socket;
    originatorIPaddress: string;
    autoBrowse: boolean;
    updateRate: number;
    disconnectMultiplier: number;
    deviceList: browserDevice[];
    updateInterval: NodeJS.Timer;
    on(event: 'New Device', listener: (device: browserDevice) => {}): this;
    on(event: 'Broadcast Request', listener: () => {}): this;
    on(event: 'Device Disconnected', listener: (device: browserDevice) => {}): this;
    on(event: 'Device List Updated', listener: (deviceList: browserDevice[]) => {}): this;
    on(event: string, listener: Function): this;
}

export class Browser extends EventEmitter{
    constructor(originatorPort: number=51687, originatorIPaddress: string="0.0.0.0", autoBrowse: boolean=true, updateRate: number=3000, disconnectMultiplier: number=4) {
        super();
        this.socket = dgram.createSocket("udp4");
        this.originatorIPaddress = originatorIPaddress;
        this.autoBrowse = autoBrowse;
        this.updateRate = updateRate;
        this.disconnectMultiplier = disconnectMultiplier;
        this.deviceList = [];

        this.socket.bind(originatorPort, originatorIPaddress, () => {
            this.socket.setBroadcast(true);
            if(this.autoBrowse) this.start();
        });

        this._setupSocketEvents();

        this.updateInterval = null;

    }

    start() {
        if (this.updateInterval) {
            clearInterval(this.updateInterval);
        }

        this.deviceList.forEach( (dev, i) => {
            this.deviceList[i].timestamp = Date.now();
        });

        this.updateInterval = setInterval(() => {
            this.checkStatus();
            this.socket.send(listIdentityRequest, 44818, "255.255.255.255", (e) => {
                if (e) throw e;
                this.emit("Broadcast Request");
            });
        }, this.updateRate);
    }

    stop() {
        clearInterval(this.updateInterval);
    }

    checkStatus() {

        let deviceDisconnected = false;
        this.deviceList.forEach(device => {
            if ( (Date.now() - device.timestamp) > (this.updateRate * this.disconnectMultiplier) ) {
                this.emit("Device Disconnected", device);
                deviceDisconnected = true;
            }
        });

        this.deviceList = this.deviceList.filter(device => (Date.now() - device.timestamp) <= (this.updateRate * this.disconnectMultiplier));
        if (deviceDisconnected) this.emit("Device List Updated", this.deviceList);

    }

    _setupSocketEvents() {
        this.socket.on("message", msg => {
            const device = this._parseListIdentityResponse(msg);            
            if(Object.keys(device).length !== 0)
                this._addDevice(device);    // Device is added only if device is not empty object
        });
   
    }

    _parseListIdentityResponse(msg) {

        let response: browserDevice = {
            EncapsulationVersion: undefined,
            socketAddress: {
                sin_family: undefined,
                sin_port: undefined,
                sin_addr: undefined,
                sin_zero: undefined
            },
            vendorID: undefined,
            deviceType: undefined,
            productCode: undefined,
            revision: undefined,
            status: undefined,
            serialNumber: undefined,
            productName: undefined,
            state: undefined,
            timestamp: undefined
        };

        const messageData = EthernetIP.encapsulation.header.parse(msg);
        
        // Check if messageData is not undefined
        if (messageData !== undefined) {
            // Check if messageData.data is not undefined
            if (messageData.data !== undefined) {
                // Check if messageData.data has a congruent length
                if (messageData.data.length >= 2) {            
                    const cpf = EthernetIP.encapsulation.CPF.parse(messageData.data);
                    // Check if cpf is not undefined
                    if (cpf !== undefined) {
                        // Check if cpf is an array
                        if (Array.isArray(cpf)) {
                            // Check if cpf[0] is not undefined
                            if (cpf[0] !== undefined) {
                                const data = cpf[0].data;
                                let ptr = 0;
                                
                                response.EncapsulationVersion = data.readUInt16LE(ptr);
                                ptr += 2;
                                response.socketAddress.sin_family = data.readUInt16BE(ptr);
                                ptr += 2;
                                response.socketAddress.sin_port = data.readUInt16BE(ptr);
                                ptr += 2;
                                response.socketAddress.sin_addr = data.readUInt8(ptr).toString() + "." + data.readUInt8(ptr + 1).toString() + "." + data.readUInt8(ptr + 2).toString() + "." + data.readUInt8(ptr + 3).toString();
                                ptr += 4;
                                response.socketAddress.sin_zero = data.slice(ptr, ptr + 8);
                                ptr += 8;
                                response.vendorID = data.readUInt16LE(ptr);
                                ptr += 2;
                                response.deviceType = data.readUInt16LE(ptr);
                                ptr += 2;
                                response.productCode = data.readUInt16LE(ptr);
                                ptr += 2;
                                response.revision = data.readUInt8(ptr).toString() + "." + data.readUInt8(ptr + 1).toString();
                                ptr += 2;
                                response.status = data.readUInt16LE(ptr);
                                ptr += 2;
                                response.serialNumber = "0x" + data.readUInt32LE(ptr).toString(16);
                                ptr += 4;
                                response.productName = data.slice(ptr + 1, ptr + 1 + data.readUInt8(ptr)).toString();
                                ptr += (1 + data.readUInt8(ptr));
                                response.state = data.readUInt8(ptr);
                            }
                        }
                    }
                }
            }
        }

        return response;
    }

    _addDevice(device) {
        const index = this.deviceList.findIndex(item => item.socketAddress.sin_addr === device.socketAddress.sin_addr);
    
        device.timestamp = Date.now();
    
        if(index > -1) {
            this.deviceList[index] = device;
        } else {
            this.deviceList.push(device);
            this.emit("New Device", device);
            this.emit("Device List Updated", this.deviceList);
        }
    }

}

export default Browser;
