import { CIP } from "../enip";
const { MessageRouter } = CIP;
const { WRITE_TAG, WRITE_TAG_FRAGMENTED } = MessageRouter.services;
import Tag from "../tag";
import Template from "./template";
import { bufferToString, stringToBuffer } from "../utilities";
import equals from "deep-equal";
import type TagList from '../tag-list'

export declare interface Structure extends Tag {
    _valueObj: any;
    _taglist: any;
    _template: Template;
}

export class Structure extends Tag {
    
    /**
     * Structure Class to handle structure tags
     * 
     * @param tagname - Tagname
     * @param taglist - Tag list of PLC. Needs to be retrieved first.
     * @param program - Program name. Leave undefined for Controller scope
     * @param datatype - Data type code if it needs to be explicitly defined (Not commonly used)
     * @param keepAlive - Time interval in mS to set stage_write to true to keep connection alive.  0 = disabled.
     * @param arrayDims - Dimensions of an array tag
     * @param arraySize - Size of array
     */
    constructor (tagname: string, taglist: TagList, program: string = null, datatype: number = null, keepAlive: number = 0, arrayDims: number = 0, arraySize: number = 0x01) {
        super(tagname, program, datatype, keepAlive, arrayDims, arraySize);
        this._valueObj = null;
        this._taglist = taglist;
        this._template = taglist.getTemplateByTag(tagname, program);
        if (this._template) super.type = CIP.DataTypes.Types.STRUCT;
    }

    /**
     * Gets structure tag value as an object containing all members 
     */
    get value (): any {
        if (!this._template) {
            return super.value;
        } else {
            if (super.value) {
                if (this._valueObj) {
                    return this._valueObj;
                } else {
                    this._valueObj = this.parseValue(super.value);
                    return this._valueObj;
                }               
            } else {
                return null;
            }
        }
    }

    /**
     * Parses either single or array of structures
     * 
     * @param data - tag value as a data buffer
     * @returns tag value as an object containing all members
     */
    parseValue (data: Buffer): any {
        if (this.state.tag.arrayDims > 0) {
            return this._parseReadDataArray(data);
        } else {
            return this._parseReadData(data, this._template);
        }
        
    }

    /**
     * Sets structure tag value as an object containing all members 
     */
    set value (newValue: any) {
        if (!this._template) {
            super.value = newValue;
        } else {
            if (this.state.tag.arrayDims > 0) {
                super.value = this._parseWriteDataArray(newValue);
                this._valueObj = this.parseValue(super.value);
            } else {
                super.value = this._parseWriteData (newValue, this._template);
                this._valueObj = this.parseValue(super.value);
            }
        }
    }

    /**
     * Write current value as object to value as buffer
     */
    writeObjToValue(): void {
        if (this.state.tag.arrayDims > 0) {
            super.value = this._parseWriteDataArray(this._valueObj);
        } else {
            super.value = this._parseWriteData (this._valueObj, this._template);
        }             
    }

    /**
     * Generates write message to write current structure value to PLC
     * 
     * @param value 
     * @param size 
     * @returns Write message request
     */
    generateWriteMessageRequest(value = null, size: number = 0x01): Buffer {
        const { STRUCT } = CIP.DataTypes.Types;
        
        if(!this._template) {
            return super.generateWriteMessageRequest(value, size);
        } else {
            const { tag } = this.state;
            const buf = Buffer.alloc(6);
            buf.writeUInt16LE(STRUCT, 0);
            buf.writeUInt16LE(this._template._attributes.StructureHandle, 2);
            if (Array.isArray(this.value)) {
                buf.writeUInt16LE(this.value.length, 4);
            } else {
                buf.writeUInt16LE(size, 4);
            }        
            
            return MessageRouter.build(WRITE_TAG, tag.path, Buffer.concat([buf, super.value]));  
        }
    }

    /**
     * Generates write message to write current structure value to PLC
     * 
     * @param offset - Offset of data already written 
     * @param value - Fragment of value of structure as a buffer
     * @param size - size of the data
     * @returns message
     */
    generateWriteMessageRequestFrag(offset: number = 0, value: Buffer = null, size: number = 0x01) {
        const { STRUCT } = CIP.DataTypes.Types;

        if(!this._template) {
            return super.generateWriteMessageRequest(value, size);
        } else {
            const { tag } = this.state;
            const buf = Buffer.alloc(10);
            buf.writeUInt16LE(STRUCT, 0);
            buf.writeUInt16LE(this._template._attributes.StructureHandle, 2);
            if (Array.isArray(this.value)) {
                buf.writeUInt16LE(this.value.length, 4);
            } else {
                buf.writeUInt16LE(size, 4);
            }
            buf.writeUInt32LE(offset, 6);

            return MessageRouter.build(WRITE_TAG_FRAGMENTED, tag.path, Buffer.concat([buf, value]));  
        }
    }

    /**
     * Parse structure data read from PLC
     * 
     * @param data - data from PLC
     * @param template - Template that forms the data structure
     * @returns Structure tag value as an object
     */
    _parseReadData (data: Buffer, template: Template): any {
        if (template._members.length === 2 && template._members[0].name === "LEN" && template._members[1].name === "DATA")
            return bufferToString(data);

        let structValues = {};
        
        const {SINT, INT, DINT, REAL, LINT, BIT_STRING, BOOL, STRUCT } = CIP.DataTypes.Types;

        template._members.forEach(member => {
            
            /* eslint-disable indent */
            switch (member.type.structure ? STRUCT : member.type.code) {
                case SINT:
                    if (member.type.arrayDims > 0) {
                        structValues[member.name] = [];
                        for (let i = 0; i < member.info; i++) {
                            structValues[member.name].push(data.readUInt8(member.offset + i));
                        }
                    } else {
                        structValues[member.name] = data.readUInt8(member.offset);
                    } 
                    break;
                case INT:
                    if (member.type.arrayDims > 0) {
                        let array = [];
                        for (let i = 0; i < member.info * 2; i+=2) {
                            array.push(data.readInt16LE(member.offset + i));
                        }
                        structValues[member.name] = array;
                    } else {
                        structValues[member.name] = data.readInt16LE(member.offset);
                    }
                    break;
                case DINT:
                    if (member.type.arrayDims > 0) {
                        let array = [];
                        for (let i = 0; i < member.info * 4; i+=4) {
                            array.push(data.readInt32LE(member.offset + i));
                        }
                        structValues[member.name] = array;
                    } else {
                        structValues[member.name] = data.readInt32LE(member.offset);
                    }
                    break;
                case REAL:
                    if (member.type.arrayDims > 0) {
                        let array = [];
                        for (let i = 0; i < member.info * 4; i+=4) {
                            array.push(data.readFloatLE(member.offset + i));
                        }
                        structValues[member.name] = array;
                    } else {
                        structValues[member.name] = data.readFloatLE(member.offset);
                    }
                    break;
                case LINT:
                    if (member.type.arrayDims > 0) {
                        let array = [];
                        for (let i = 0; i < member.info * 8; i+=8) {
                            array.push(data.readBigInt64LE(member.offset + i));
                        }
                        structValues[member.name] = array;
                    } else {
                        structValues[member.name] = data.readBigInt64LE(member.offset);
                    }
                    break;
                case BIT_STRING:
                    if (member.type.arrayDims > 0) {
                        let array = [];
                        for (let i = 0; i < member.info * 4; i+=4) {
                            let bitString32bitValue = data.readUInt32LE(member.offset + i);
                            for (let j = 0; j < 32; j++) {
                                array.push(!!(bitString32bitValue >> j & 0x01));
                            }
                        }
                        structValues[member.name] = array;
                    } else {
                        structValues[member.name] = data.readUInt32LE(member.offset);
                    }
                    break;
                case BOOL:
                    structValues[member.name] = !!(data.readUInt8(member.offset) & (1 << member.info));
                    break;
                case STRUCT: {
                    const memberTemplate = this._taglist.templates[member.type.code];
                    const memberStructSize = memberTemplate._attributes.StructureSize;
                    if (member.type.arrayDims > 0) { 
                        let array = [];
                        for (let i = 0; i < member.info * memberStructSize; i+=memberStructSize) {
                            array.push(this._parseReadData(data.subarray(member.offset + i), memberTemplate));
                        }
                        structValues[member.name] = array;
                    } else {
                        structValues[member.name] = this._parseReadData(data.subarray(member.offset), memberTemplate);
                    }
                    break;
                }
                default:
                    throw new Error(
                        "Data Type other than SINT, INT, DINT, LINT, BOOL, STRUCT or BIT_STRING returned "
                    );
            }
            /* eslint-enable indent */   
        });
        return structValues;
    }

    /**
     * Parses and array of structure tag data read from PLC
     * @param data - data from PLC 
     * @returns Array of structure tag values as objects
     */
    _parseReadDataArray(data: Buffer): any[] {
        let array = [];
        for (let i = 0; i < data.length; i+=this._template._attributes.StructureSize) {
            array.push(this._parseReadData(data.subarray(i),this._template));
        }
        return array;
    }

    /**
     * Creates data to send to PLC to write structure value
     * 
     * @param structValues - Structure tag value as an object / string 
     * @param template - Template of Structure tag
     * @returns Data to be sent to PLC
     */
    _parseWriteData (structValues: any, template: Template): Buffer {
        if (template._members.length === 2 && template._members[0].name === "LEN" && template._members[1].name === "DATA")
            return stringToBuffer(structValues, template._attributes.StructureSize);

        const data = Buffer.alloc(template._attributes.StructureSize);

        const {SINT, INT, DINT, REAL, LINT, BIT_STRING, BOOL, STRUCT } = CIP.DataTypes.Types;
        
        template._members.forEach(member => {
            /* eslint-disable indent */
            switch (member.type.structure ? STRUCT : member.type.code) {
                case SINT:
                    if (member.type.arrayDims > 0) {
                        for (let i = 0; i < member.info; i++) {
                            data.writeUInt8(structValues[member.name][i], member.offset + i);
                        }
                    } else {
                        data.writeUInt8(structValues[member.name], member.offset);
                    } 
                    break;
                case INT:
                    if (member.type.arrayDims > 0) {
                        for (let i = 0; i < member.info; i++) {
                            data.writeInt16LE(structValues[member.name][i], member.offset + (i * 2));
                        }
                    } else {
                        data.writeInt16LE(structValues[member.name],member.offset);
                    }
                    break;
                case DINT:
                    if (member.type.arrayDims > 0) {
                        for (let i = 0; i < member.info; i++) {
                            data.writeInt32LE(structValues[member.name][i], member.offset + (i * 4));
                        }
                    } else {
                        data.writeInt32LE(structValues[member.name],member.offset);
                    }
                    break;
                case REAL:
                    if (member.type.arrayDims > 0) {
                        for (let i = 0; i < member.info; i++) {
                            data.writeFloatLE(structValues[member.name][i], member.offset + (i * 4));
                        }
                    } else {
                        data.writeFloatLE(structValues[member.name],member.offset);
                    }
                    break;
                case LINT:
                    if (member.type.arrayDims > 0) {
                        for (let i = 0; i < member.info; i++) {
                            if (typeof structValues[member.name][i] == 'bigint') {
                                data.writeBigInt64LE(structValues[member.name][i], member.offset + (i * 8));
                            } else {
                                data.writeBigInt64LE(BigInt(structValues[member.name][i]), member.offset + (i * 8));
                            }
                            
                        }
                    } else {
                        if (typeof structValues[member.name] == 'bigint') {
                            data.writeBigInt64LE(structValues[member.name], member.offset + 8);
                        } else {
                            data.writeBigInt64LE(BigInt(structValues[member.name]), member.offset +  8);
                        }
                    }
                    break;
                case BIT_STRING:
                    if (member.type.arrayDims > 0) {
                        for (let i = 0; i < member.info; i++) {
                            let bitString32bitValue = 0;
                            for (let j = i*32; j < (i+1)*32; j++) {
                                if (j > structValues[member.name].length) break;
                                bitString32bitValue |= (structValues[member.name][j] & 1) << j;
                            }
                            data.writeUInt32LE(bitString32bitValue >>> 0, member.offset + (i * 4));
                        }
                    } else {
                        data.writeUInt32LE(structValues[member.name],member.offset);
                    }
                    break;
                case BOOL:
                    if (structValues[member.name]) {
                        data.writeUInt8(data.readUInt8(member.offset) | 1<<member.info, member.offset);
                    } else {
                        data.writeUInt8(data.readUInt8(member.offset) & ~(1<<member.info), member.offset);
                    }
                    break;
                case STRUCT: {
                    const memberTemplate = this._taglist.templates[member.type.code];
                    const memberStructSize = memberTemplate._attributes.StructureSize;
                    if (member.type.arrayDims > 0) { 
                        for (let i = 0; i < member.info; i++) {
                            const templateData = this._parseWriteData(structValues[member.name][i], memberTemplate);
                            for (let pairs of templateData.entries()) {
                                data[member.offset + (i * memberStructSize) + pairs[0]] = pairs[1];
                            }
                        }
                    } else {
                        const templateData = this._parseWriteData(structValues[member.name], memberTemplate);
                        for (let pairs of templateData.entries()) {
                            data[member.offset + pairs[0]] = pairs[1];
                        }
                    }
                    break;
                }
                default:
                    throw new Error(
                        "Data Type other than SINT, INT, DINT, LINT, BOOL, STRUCT or BIT_STRING returned "
                    );
            }
            /* eslint-enable indent */   
        });
        return data;
    }

    /**
     * Creates data to send to PLC to write and array of structure values 
     * 
     * @param newValue - array of sture values that are objects / strings
     * @returns data message to be sent to PLC
     */
    _parseWriteDataArray (newValue: any[]) {
        let buf = Buffer.alloc(0);

        newValue.forEach(value => {
            buf = Buffer.concat([buf, this._parseWriteData(value, this._template)]);
        });
        
        return buf;
    }
    
    /**
     * Get current value on the PLC controller
     */
    get controller_value(): Buffer {
        return this.state.tag.controllerValue;
    }

    /**
     *  Set controller value and update object value
     *  @param newValue - Structure tag value as a buffer
     */
    set controller_value(newValue: any) {
        if (!equals(newValue, this.state.tag.controllerValue)) {
            let lastValue = null;
            if(this.state.tag.controllerValue !== null) 
                lastValue = Buffer.from(this.state.tag.controllerValue);
                   
            this.state.tag.controllerValue = Buffer.from(newValue);

            const { stage_write } = this.state.tag;
            if (!stage_write) {
                this.state.tag.value = newValue;
                this._valueObj = this.parseValue(super.value);
            }

            this.state.timestamp = new Date();


            if (lastValue !== null) this.emit("Changed", this, this.parseValue(lastValue));
            else this.emit("Initialized", this);
        } else {
            if (this.state.keepAlive > 0) {
                const now = new Date();
                if (now.getTime() - this.state.timestamp.getTime() >= this.state.keepAlive * 1000) {
                    this.state.tag.controllerValue = newValue;

                    const { stage_write } = this.state.tag;
                    if (!stage_write) this.state.tag.value = newValue;
                    this.state.timestamp = now;

                    this.emit("KeepAlive", this);
                }
            }
        }
    }

}



export { Template};