import { EventEmitter } from 'events';
import * as crypto from 'crypto';
import { CIP } from '../enip';
const { MessageRouter } = CIP;
const { READ_TAG, WRITE_TAG, READ_MODIFY_WRITE_TAG, READ_TAG_FRAGMENTED, WRITE_TAG_FRAGMENTED } = MessageRouter.services;
import { Types, getTypeCodeString, isValidTypeCode } from '../enip/cip/data-types';
import dateFormat from 'dateformat';
import equals from 'deep-equal';

// Static Class Property - Tracks Instances
let instances = 0;

type tag = {
    name: string,
    type: number,
    bitIndex: number,
    arrayDims: number,
    value: any,
    controllerValue: any,
    path: Buffer,
    program: string,
    stage_write: boolean
};

type tagError = {
    code: number,
    status: any
};

type tagState = {
    tag: tag,
    read_size: number,
    error: tagError,
    timestamp: Date,
    instance: string,
    keepAlive: number
};

export declare interface Tag extends EventEmitter {
    state: tagState;
    on(event: string, listener: Function): this;
    on(event: 'Changed', listener: (this: this, previousValue: any) => {}): this;
    on(event: 'Initialized', listener: (this: this) => {}): this;
    on(event: 'Unknown', listener: (this: this) => {}): this;
    on(event: 'KeepAlive', listener: (this: this) => {}): this;
}

export class Tag extends EventEmitter {
    /**
     * PLC Tag Object
     * 
     * @param tagname - Tagname
     * @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, program: string = null, datatype:number = null, keepAlive: number = 0, arrayDims:number = 0, arraySize:number = 0x01) {
        super();

        if (!Tag.isValidTagname(tagname)) throw new Error("Tagname Must be of Type <string>");
        if (!isValidTypeCode(datatype) && datatype !== null)
            throw new Error("Datatype must be a Valid Type Code <number>");
        if (typeof keepAlive !== "number")
            throw new Error(
                `Tag expected keepAlive of type <number> instead got type <${typeof keepAlive}>`
            );
        if (keepAlive < 0)
            throw new Error(`Tag expected keepAlive to be greater than 0, got ${keepAlive}`);

        // Increment Instances
        instances += 1;

        // Split by "." for memebers
        // Split by "[" or "]" for array indexes
        // Split by "," for array indexes with more than 1 dimension
        // Filter for length > 0 to remove empty elements (happens if tag ends with array index)
        let pathArr = tagname.split(/[.[\],]/).filter(segment => segment.length > 0);

        let bitIndex = null;

        // Check for bit index (tag ends in .int) - this only applies to SINT, INT, DINT or array elements of
        // Split by "." to only check udt members and bit index.
        let memArr = tagname.split(".");
        let isBitIndex = (memArr.length > 1) && !isNaN(Number(memArr[memArr.length - 1]));

        // Check if BIT_STRING data type was passed in
        let isBitString = datatype === Types.BIT_STRING && !isNaN(Number(pathArr[pathArr.length - 1]));

        // Tag can not be both a bit index and BIT_STRING
        if (isBitString && isBitIndex)
            throw "Tag cannot be defined as a BIT_STRING and have a bit index";

        if (isBitString) {
            // BIT_STRING need to be converted to array with bit index
            // tag[x] converts to tag[(x-x%32)/32].x%32
            // e.g. tag[44] turns into tag[1].12
            bitIndex = parseInt(pathArr[pathArr.length - 1]) % 32;
            pathArr[pathArr.length - 1] = (
                (parseInt(pathArr[pathArr.length - 1]) - bitIndex) /
                32
            ).toString();
        } else {
            if (isBitIndex) {
                // normal bit index handling
                bitIndex = parseInt(pathArr.pop());
                if ((bitIndex < 0) || (bitIndex > 31))
                    throw new Error(`Tag bit index must be between 0 and 31, received ${bitIndex}`);
            }
        }

        let bufArr = [];

        // Push Program Path to Buffer if Present
        if (program) bufArr.push(CIP.EPATH.segments.DATA.build(`Program:${program}`));

        // Build EPATH Buffer
        for (let path of pathArr) {
            bufArr.push(CIP.EPATH.segments.DATA.build(path));
        }

        const pathBuf = Buffer.concat(bufArr);

        //buffer for instance id
        let bitIndexBuf = Buffer.alloc(1);
        if (bitIndex === null) bitIndexBuf.writeInt8(32);
        else bitIndexBuf.writeInt8(bitIndex);

        let instanceBuf = Buffer.concat([pathBuf, bitIndexBuf]);

        this.state = {
            tag: {
                name: tagname,
                type: datatype,
                arrayDims: arrayDims,
                bitIndex: bitIndex,
                value: null,
                controllerValue: null,
                path: pathBuf,
                program: program,
                stage_write: false
            },
            read_size: arraySize,
            error: { code: null, status: null },
            timestamp: new Date(),
            instance: hash(instanceBuf),
            keepAlive: keepAlive
        };
    }

    // region Property Accessors
    /**
     * Returns the total number of Tag Instances
     * that have been Created
     *
     * @readonly
     * @static
     * @returns instances
     */
    static get instances(): number {
        return instances;
    }

    /**
     * Returns the Tag Instance ID
     *
     * @readonly
     * @returns Instance ID
     */
    get instance_id(): string {
        return this.state.instance;
    }

    /**
     * Gets Tagname
     *
     * @returns tagname
     */
    get name():string {
        const { program, name } = this.state.tag;

        if (program === null) {
            return name;
        } else {
            return `Program:${program}.${name}`;
        }
    }

    /**
     * Gets Program Name
     *
     * @returns program
     */
    get program():string {
        const { program } = this.state.tag;

        return program
    }

    /**
     * Sets Tagname if Valid
     *
     * @param name - New Tag Name
     */
    set name(name: string) {
        if (!Tag.isValidTagname(name)) throw new Error("Tagname Must be of Type <string>");
        this.state.tag.name = name;
    }

    /**
     * Gets Tag Datatype
     *
     * @returns datatype
     */
    get type(): string {
        return getTypeCodeString(this.state.tag.type);
    }

    /**
     * Gets Tag Bit Index
     * - Returns null if no bit index has been assigned
     *
     * @returns bitIndex
     */
    get bitIndex(): number {
        return this.state.tag.bitIndex;
    }

    /**
     * Sets Tag Datatype if Valid
     *
     * @param type - Valid Datatype Code
     */
    set type(type: string | number) {
        if (typeof type === 'number') {
            if (!isValidTypeCode(type)) throw new Error("Datatype must be a Valid Type Code <number>");
            this.state.tag.type = type;
        } else {
            if (!isValidTypeCode(Types[type])) throw new Error("Datatype must be a Valid Type Code <number>");
            this.state.tag.type = Types[type] 
        }   
    }

    /**
     * Gets Tag Read Size
     *
     * @returns read size
     */
    get read_size() {
        return this.state.read_size;
    }

    /**
     * Sets Tag Read Size
     *
     * @param size - read size
     */
    set read_size(size: number) {
        if (typeof size !== "number")
            throw new Error("Read Size must be a number");
        this.state.read_size = size;
    }

    /**
     * Gets Tag value
     * - Returns null if no value has been read
     *
     * @returns value
     */
    get value(): any {
        if (Array.isArray(this.state.tag.value)) {
            let prevValue = [...this.state.tag.value];
            setTimeout(() => {
                if (!equals(prevValue, this.state.tag.value))
                    this.state.tag.stage_write = true;
            }, 0);
        }
        
        return this.state.tag.value;
    }

    /**
     * Sets Tag Value
     *
     * @param newValue - value
     */
    set value(newValue) {
        if (!equals(newValue, this.state.tag.value))
            this.state.tag.stage_write = true;
        
        this.state.tag.value = newValue;
    }

    /**
     * Sets Controller Tag Value and Emits Changed Event
     *
     * @param new - value
     */
    set controller_value(newValue: any) {
        if (!equals(newValue,this.state.tag.controllerValue)) {
            let lastValue = null;
            lastValue = this.state.tag.controllerValue;
            this.state.tag.controllerValue = newValue;

            const { stage_write } = this.state.tag;
            if (!stage_write) this.state.tag.value = newValue;

            this.state.timestamp = new Date();

            if (lastValue !== null) this.emit("Changed", this, 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);
                }
            }
        }
    }

    /**
     * Sets Controller Tag Value and Emits Changed Event
     *
     * @returns new value
     */
    get controller_value(): any {
        return this.state.tag.controllerValue;
    }

    /**
     * Gets Timestamp in a Human Readable Format
     *
     * @readonly
     * @returns Timestamp formatted as "mm/dd/yyyy-HH:MM:ss.l"
     */
    get timestamp(): string {
        return dateFormat(this.state.timestamp, "mm/dd/yyyy-HH:MM:ss.l");
    }

    /**
     * Gets Javascript Date Object of Timestamp
     *
     * @readonly
     * @returns Date object
     */
    get timestamp_raw(): Date {
        return this.state.timestamp;
    }

    /**
     * Gets Error
     *
     * @readonly
     * @returns error
     */
    get error(): tagError {
        return this.state.error.code ? this.state.error : null;
    }

    /**
     * Returns a Padded EPATH of Tag
     *
     * @readonly
     * @returns Padded EPATH
     */
    get path(): Buffer {
        return this.state.tag.path;
    }

    /**
     * Returns a whether or not a write is staging
     *
     * @returns true or false
     */
    get write_ready(): boolean {
        return this.state.tag.stage_write;
    }
    // endregion

    unknownTag(): void {
        this.state.timestamp = new Date();
        this.emit("Unknown", this);
    }
    // region Public Methods
    
    /**
     * Generates Read Tag Message
     *
     * @param size
     * @returns {buffer} - Read Tag Message Service
     */
    generateReadMessageRequest(size: number = null): Buffer {
        if (size) this.state.read_size = size;

        const { tag } = this.state;

        // Build Message Router to Embed in UCMM
        let buf = Buffer.alloc(2);
        buf.writeUInt16LE(this.state.read_size, 0);

        // Build Current Message
        return MessageRouter.build(READ_TAG, tag.path, buf);
    }

    /**
     * Generates Fragmented Read Tag Message
     *
     * @param offset - offset based on previous message
     * @param size 
     * @returns Read Tag Message Service
     */
    generateReadMessageRequestFrag(offset: number = 0, size: number = null): Buffer {
        if (size) this.state.read_size = size;

        const { tag } = this.state;

        // Build Message Router to Embed in UCMM
        let buf = Buffer.alloc(6);
        buf.writeUInt16LE(this.state.read_size, 0);
        buf.writeUInt32LE(offset, 2);

        // Build Current Message
        return MessageRouter.build(READ_TAG_FRAGMENTED, tag.path, buf);
    }

    /**
     *  Parses Good Read Request Messages
     *
     * @param Data - Returned from Successful Read Tag Request
     */
    parseReadMessageResponse(data: Buffer) {
        // Set Type of Tag Read
        const type = data.readUInt16LE(0);
        this.state.tag.type = type;
        
        if (this.state.tag.bitIndex !== null) 
        {
            this.parseReadMessageResponseValueForBitIndex(data);
        } else if(type === Types.STRUCT) {
            this.controller_value = data.slice(4);
        } else {
            this.parseReadMessageResponseValueForAtomic(data);
        }
    }

    /**
     *  Parses Good Read Request Messages Using A Mask For A Specified Bit Index
     *
     * @param Data - Returned from Successful Read Tag Request
     */
    parseReadMessageResponseValueForBitIndex(data: Buffer) {
        const { tag } = this.state;
        const { SINT, INT, DINT, BIT_STRING, UINT } = Types;

        // Read Tag Value
        /* eslint-disable indent */
        switch (this.state.tag.type) {
            case SINT:
                this.controller_value =
                    (data.readInt8(2) & (1 << tag.bitIndex)) == 0 ? false : true;
                break;
            case UINT:
                this.controller_value =
                    (data.readUInt16LE(2) & (1 << tag.bitIndex)) == 0 ? false : true;
                break;
            case INT:
                this.controller_value =
                    (data.readInt16LE(2) & (1 << tag.bitIndex)) == 0 ? false : true;
                break;
            case DINT:
            case BIT_STRING:
                this.controller_value =
                    (data.readInt32LE(2) & (1 << tag.bitIndex)) == 0 ? false : true;
                break;
            default:
                throw new Error(
                    "Data Type other than SINT, INT, DINT, or BIT_STRING returned when a Bit Index was requested"
                );
        }
        /* eslint-enable indent */
    }

    /**
     *  Parses Good Read Request Messages For Atomic Data Types
     *
     * @param Data - Returned from Successful Read Tag Request
     */
    parseReadMessageResponseValueForAtomic(data: Buffer) {
        const { SINT, INT, DINT, REAL, BOOL, LINT, BIT_STRING, UINT, SHORT_STRING, USINT } = Types;

        const { read_size } = this.state;

        // Read Tag Value
        /* eslint-disable indent */
        switch (this.state.tag.type) {
            case SINT:
                if (data.length > 3) {
                    const array = [];
                    for (let i = 0; i < data.length - 2; i++) {
                        array.push(data.readInt8(i + 2));
                    }
                    this.controller_value = array;
                } else {
                    this.controller_value = data.readInt8(2);
                }
                break;
            case USINT:
                    if (data.length > 3) {
                        const array = [];
                        for (let i = 0; i < data.length - 2; i++) {
                            array.push(data.readUInt8(i + 2));
                        }
                        this.controller_value = array;
                    } else {
                        this.controller_value = data.readUInt8(2);
                    }
                    break;
            case UINT:
                if (data.length > 4) {
                    const array = [];
                    for (let i = 0; i < (data.length - 2) / 2; i++) {
                        array.push(data.readUInt16LE(i * 2 + 2));
                    }
                    this.controller_value = array;
                } else {
                    this.controller_value = data.readUInt16LE(2);
                }
                break;
            case INT:
                if (data.length > 4) {
                    const array = [];
                    for (let i = 0; i < (data.length - 2) / 2; i++) {
                        array.push(data.readInt16LE(i * 2 + 2));
                    }
                    this.controller_value = array;
                } else {
                    this.controller_value = data.readInt16LE(2);
                }
                break;
            case DINT:
                if (data.length > 6) {
                    const array = [];
                    for (let i = 0; i < (data.length - 2) / 4; i++) {
                        array.push(data.readInt32LE(i * 4 + 2));
                    }
                    this.controller_value = array;
                } else {
                    this.controller_value = data.readInt32LE(2);
                }
                break;
            case REAL:
                if (data.length > 6) {
                    const array = [];
                    for (let i = 0; i < (data.length - 2) / 4; i++) {
                        array.push(data.readFloatLE(i * 4 + 2));
                    }
                    this.controller_value = array;
                } else {
                    this.controller_value = data.readFloatLE(2);
                }
                break;
            case BOOL:
                this.controller_value = !!data.readUInt8(2);
                break;
            case BIT_STRING: {
                const array = [];
                for (let b = 0; b < read_size; b++) {
                    for (let i = 0; i < 32; i++) {
                        array.push(!!(data.readUInt32LE(b * 4 + 2) >> i & 0x01));
                    }
                }
                this.controller_value = array;
                break;
            }
            case LINT:
                if(typeof data.writeBigInt64LE !== "function") {
                    throw new Error("This version of Node.js does not support big integers. Upgrade to >= 12.0.0");
                }
                if (data.length > 10) {
                    const array = [];
                    for (let i = 0; i < (data.length - 2) / 8; i++) {
                        array.push(data.readBigInt64LE(i * 8 + 2));
                    }
                    this.controller_value = array;
                } else {
                    this.controller_value = data.readBigInt64LE(2);
                }
                break;
            case SHORT_STRING: // single byte character string
                if (data.length > 3) {
                    const len = data.readUInt8(2);
                    this.controller_value = data.toString('utf8', 3); // use latin1 encoding?
                    if (len !== this.controller_value.length) {
                        throw new Error(` Read from Controller: Short string length mismatch`);
                    }
                } else {
                    //empty string, data[2] will be 0 as well, as it is the string length
                    //return empty string instead of null
                    this.controller_value = '';//null;
                }
                break;
            default:
                const ttt = CIP.DataTypes.getTypeCodeString(this.state.tag.type);
                throw new Error(
                    `Unrecognized Type Passed Read from Controller: ${this.state.tag.type} ${ttt}`
                );
        }
        /* eslint-enable indent */
    }

    /**
     * Generates Write Tag Message
     *
     * @param value - If Omitted, Tag.value will be used
     * @param size
     * @returns Write Tag Message Service
     * @memberof Tag
     */
    generateWriteMessageRequest(value: any = null, size: number = 0x01): Buffer {
        if (value !== null) {
            this.state.tag.value = value;
        } 

        const { tag } = this.state;

        if (tag.type === null)
            throw new Error(
                `Tag ${
                    tag.name
                } has not been initialized. Try reading the tag from the controller first or manually providing a valid CIP datatype.`
            );

        if (tag.bitIndex !== null) return this.generateWriteMessageRequestForBitIndex(tag.value);
        else return this.generateWriteMessageRequestForAtomic(tag.value, size);
    }

    /**
     * Generates Write Tag Message For A Bit Index
     *
     * @param value
     * @returns Write Tag Message Service
     */
    generateWriteMessageRequestForBitIndex(value: number): Buffer {
        const { tag } = this.state;
        const { SINT, INT, DINT, BIT_STRING } = Types;

        // Build Message Router to Embed in UCMM
        let buf = null;

        /* eslint-disable indent */
        switch (tag.type) {
            case SINT:
                buf = Buffer.alloc(4);
                buf.writeInt16LE(1); //mask length
                buf.writeUInt8(value ? 1 << tag.bitIndex : 0, 2); // or mask
                buf.writeUInt8(value ? 255 : 255 & ~(1 << tag.bitIndex), 3); // and mask
                break;
            case INT:
                buf = Buffer.alloc(6);
                buf.writeInt16LE(2); //mask length
                buf.writeUInt16LE(value ? 1 << tag.bitIndex : 0, 2); // or mask
                buf.writeUInt16LE(value ? 65535 : 65535 & ~(1 << tag.bitIndex), 4); // and mask
                break;
            case DINT:
            case BIT_STRING:
                buf = Buffer.alloc(10);
                buf.writeInt16LE(4); //mask length
                buf.writeInt32LE(value ? 1 << tag.bitIndex : 0, 2); // or mask
                buf.writeInt32LE(value ? -1 : -1 & ~(1 << tag.bitIndex), 6); // and mask
                break;
            default:
                throw new Error(
                    "Bit Indexes can only be used on SINT, INT, DINT, or BIT_STRING data types."
                );
        }

        // Build Current Message
        return MessageRouter.build(READ_MODIFY_WRITE_TAG, tag.path, buf);
    }

    /**
     * Generates Write Tag Message For Atomic Types
     *
     * @param value
     * @param size
     * @returns Write Tag Message Service
     */
    generateWriteMessageRequestForAtomic(value: any, size: number) {
        const { tag } = this.state;
        const { BIT_STRING ,SINT, INT, DINT, REAL, BOOL, LINT, SHORT_STRING, USINT } = Types;
        // Build Message Router to Embed in UCMM
        let buf = Buffer.alloc(4);
        let valBuf = null;

        buf.writeUInt16LE(tag.type, 0);

        if (Array.isArray(tag.value)) {
            if (tag.type == BIT_STRING) {
                buf.writeUInt16LE(Math.ceil(tag.value.length / 32), 2);
            } else {
                buf.writeUInt16LE(tag.value.length, 2);
            }
        } else {
            buf.writeUInt16LE(size, 2);
        }

        /* eslint-disable indent */
        switch (tag.type) {
            case SINT:
                if (Array.isArray(tag.value)) {
                    valBuf = Buffer.alloc(tag.value.length);
                    for (var i = 0; i < tag.value.length; i++) {
                        valBuf.writeInt8(tag.value[i], i);
                    }
                } else {
                    valBuf = Buffer.alloc(1);
                    valBuf.writeInt8(tag.value);                    
                }
                buf = Buffer.concat([buf, valBuf]);
                break;
            case USINT:
                    if (Array.isArray(tag.value)) {
                        valBuf = Buffer.alloc(tag.value.length);
                        for (var i = 0; i < tag.value.length; i++) {
                            valBuf.writeUInt8(tag.value[i], i);
                        }
                    } else {
                        valBuf = Buffer.alloc(1);
                        valBuf.writeUInt8(tag.value);                    
                    }
                    buf = Buffer.concat([buf, valBuf]);
                    break;
            case INT:
                if (Array.isArray(tag.value)) {
                    valBuf = Buffer.alloc(2 * tag.value.length);
                    for (let i = 0; i < tag.value.length; i++) {
                        valBuf.writeInt16LE(tag.value[i], i * 2);
                    }
                } else {
                    valBuf = Buffer.alloc(2);
                    valBuf.writeInt16LE(tag.value);                    
                }
                buf = Buffer.concat([buf, valBuf]);
                break;
            case DINT:
                if (Array.isArray(tag.value)) {
                    valBuf = Buffer.alloc(4 * tag.value.length);
                    for (let i = 0; i < tag.value.length; i++) {
                        valBuf.writeInt32LE(tag.value[i], i * 4);
                    }
                } else {
                    valBuf = Buffer.alloc(4);
                    valBuf.writeInt32LE(tag.value);                    
                }
                buf = Buffer.concat([buf, valBuf]);               
                break;
            case REAL:
                if (Array.isArray(tag.value)) {
                    valBuf = Buffer.alloc(4 * tag.value.length);
                    for (let i = 0; i < tag.value.length; i++) {
                        valBuf.writeFloatLE(tag.value[i], i * 4);
                    }
                } else {
                    valBuf = Buffer.alloc(4);
                    valBuf.writeFloatLE(tag.value);                    
                }
                buf = Buffer.concat([buf, valBuf]);
                break;
            case BOOL:
                valBuf = Buffer.alloc(1);
                if (!tag.value) valBuf.writeInt8(0x00);
                else valBuf.writeInt8(0x01);

                buf = Buffer.concat([buf, valBuf]);
                break;
            case LINT:
                valBuf = Buffer.alloc(8);
                if(typeof valBuf.writeBigInt64LE !== "function") {
                    throw new Error("This version of Node.js does not support big integers. Upgrade to >= 12.0.0");
                }
                if (Array.isArray(tag.value)) {
                    valBuf = Buffer.alloc(8 * tag.value.length);
                    for (let i = 0; i < tag.value.length; i++) {
                        if (typeof tag.value[i] != 'bigint') {
                            tag.value[i]= BigInt(tag.value[i]);
                        }

                        valBuf.writeBigInt64LE(tag.value[i], i * 8);
                        
                        
                    }
                } else {
                    valBuf = Buffer.alloc(8);
                    if (typeof tag.value != 'bigint') {
                        tag.value= BigInt(tag.value);
                    }

                    valBuf.writeBigInt64LE(tag.value);
                    
                                        
                }
                buf = Buffer.concat([buf, valBuf]);
                break;
            case SHORT_STRING:
                valBuf = Buffer.alloc(1);
                if (!tag.value || tag.value.length === 0)
                    valBuf.writeInt8(0x00);
                else {
                    const strBuf = Buffer.from(tag.value, 'latin1');
                    const strlen = strBuf.length;
                    if ( strlen > 255) {
                        throw new Error(`String too long to Write to Controller: ${tag.type}, limit 255 bytes, length ${strlen}`);
                    }
                    valBuf = Buffer.alloc(1 + strlen); // 1+ for length byte
                    valBuf.writeUInt8(strlen);//write the length
                    strBuf.copy(valBuf,1,);
                }
                buf = Buffer.concat([buf, valBuf]);
                break;
            case BIT_STRING:     
                valBuf = Buffer.alloc(Math.ceil(tag.value.length / 32) *  4);
                for (let i = 0; i < tag.value.length; i++) {
                    if (tag.value[i] == true) {
                        valBuf[Math.floor(i / 8)] |= 0x01 << Math.floor(i % 8)
                    }      
                }
                buf = Buffer.concat([buf, valBuf]);
                break;
            default:
                throw new Error(`Unrecognized Type to Write to Controller: ${tag.type}`);
        }

        // Build Current Message
        return MessageRouter.build(WRITE_TAG, tag.path, buf);
    }

    /**
     * Generates Write Tag Message Frag
     *
     * @param offset - Offset of data already written
     * @param value - If Omitted, Tag.value will be used
     * @param size
     * @returns Write Tag Message Service
     * @memberof Tag
     */
    generateWriteMessageRequestFrag(offset: number = 0, value: any, size: number = 0x01) {
        const { tag } = this.state;
        const { BIT_STRING, SINT, INT, DINT, REAL, BOOL, LINT, SHORT_STRING    } = Types;
        // Build Message Router to Embed in UCMM
        let buf = Buffer.alloc(8);
        let valBuf = null;

        buf.writeUInt16LE(tag.type, 0);
        buf.writeUInt16LE(this.value.length, 2);
        buf.writeUInt32LE(offset, 4);

        /* eslint-disable indent */
        switch (tag.type) {
            case SINT:
                if (Array.isArray(value)) {
                    valBuf = Buffer.alloc(value.length);
                    for (var i = 0; i < value.length; i++) {
                        valBuf.writeInt8(value[i], i);
                    }
                } else {
                    valBuf = Buffer.alloc(1);
                    valBuf.writeInt8(value);                    
                }
                buf = Buffer.concat([buf, valBuf]);
                break;
            case INT:
                if (Array.isArray(value)) {
                    valBuf = Buffer.alloc(2 * value.length);
                    for (let i = 0; i < value.length; i++) {
                        valBuf.writeInt16LE(value[i], i * 2);
                    }
                } else {
                    valBuf = Buffer.alloc(2);
                    valBuf.writeInt16LE(value);                    
                }
                buf = Buffer.concat([buf, valBuf]);
                break;
            case DINT:
                if (Array.isArray(value)) {
                    valBuf = Buffer.alloc(4 * value.length);
                    for (let i = 0; i < value.length; i++) {
                        valBuf.writeInt32LE(value[i], i * 4);
                    }
                } else {
                    valBuf = Buffer.alloc(4);
                    valBuf.writeInt32LE(value);                    
                }
                buf = Buffer.concat([buf, valBuf]);               
                break;
            case REAL:
                if (Array.isArray(value)) {
                    valBuf = Buffer.alloc(4 * value.length);
                    for (let i = 0; i < value.length; i++) {
                        valBuf.writeFloatLE(value[i], i * 4);
                    }
                } else {
                    valBuf = Buffer.alloc(4);
                    valBuf.writeFloatLE(value);                    
                }
                buf = Buffer.concat([buf, valBuf]);
                break;
            case BOOL:
                valBuf = Buffer.alloc(1);
                if (!value) valBuf.writeInt8(0x00);
                else valBuf.writeInt8(0x01);

                buf = Buffer.concat([buf, valBuf]);
                break;
            case LINT:
                valBuf = Buffer.alloc(8);
                if(typeof valBuf.writeBigInt64LE !== "function") {
                    throw new Error("This version of Node.js does not support big integers. Upgrade to >= 12.0.0");
                }
                if (Array.isArray(value)) {
                    valBuf = Buffer.alloc(8 * value.length);
                    for (let i = 0; i < value.length; i++) {
                        if (typeof value[i] != 'bigint') {
                            value[i] = BigInt(value[i])
                        }
                        valBuf.writeBigInt64LE(value[i], i * 8);
                    }
                } else {
                    valBuf = Buffer.alloc(8);
                    if (typeof value != 'bigint') {
                        value = BigInt(value[i])
                    }
                    valBuf.writeBigInt64LE(value);                    
                }
                buf = Buffer.concat([buf, valBuf]);
                break;
            case SHORT_STRING:
                valBuf = Buffer.alloc(1);
                if (!tag.value || tag.value.length === 0)
                    valBuf.writeInt8(0x00);
                else {
                    const strBuf = Buffer.from(tag.value, 'latin1');
                    const strlen = strBuf.length;
                    if ( strlen > 255) {
                        throw new Error(`String too long to Write to Controller: ${tag.type}, limit 255 bytes, length ${strlen}`);
                    }
                    valBuf = Buffer.alloc(1 + strlen); // 1+ for length byte
                    valBuf.writeUInt8(strlen);//write the length
                    strBuf.copy(valBuf,1,);
                }
                buf = Buffer.concat([buf, valBuf]);
                break;
            default:
                throw new Error(`Unrecognized Type to Write to Controller: ${tag.type}`);
        }

        // Build Current Message
        return MessageRouter.build(WRITE_TAG_FRAGMENTED, tag.path, buf); 
    }

    /**
     * Unstages Value Edit by Updating controllerValue
     * after the Successful Completion of 
     * a Tag Write
     *
     * @memberof Tag
     */
    unstageWriteRequest(): void {
        const { tag } = this.state;
        tag.stage_write = false;
        tag.controllerValue = tag.value;
    }
    // endregion

    /**
     * Determines if a Tagname is Valid
     *
     * @param tagname - Name of PLC tag
     * @returns true or false
     */
    static isValidTagname(tagname: string): boolean {
        if (typeof tagname !== "string") return false;

        // regex components
        const nameRegex = captureIndex => {
            return `(_?[a-zA-Z]|_\\d)(?:(?=(_?[a-zA-Z0-9]))\\${captureIndex})*`;
        };
        const multDimArrayRegex = "(\\[\\d+(,\\d+){0,2}])";
        const arrayRegex = "(\\[\\d+])";
        const bitIndexRegex = "(\\.\\d{1,2})";

        // user regex for user tags
        const userRegex = new RegExp(
            "^(Program:" +
            nameRegex(3) +
            "\\.)?" + // optional program name
            nameRegex(5) +
            multDimArrayRegex +
            "?" + // tag name
            "(\\." +
            nameRegex(10) +
            arrayRegex +
            "?)*" + // option member name
                bitIndexRegex +
                "?$"
        ); // optional bit index
        // full user regex
        // ^(Program:(_?[a-zA-Z]|_\d)(?:(?=(_?[a-zA-Z0-9]))\3)*\.)?(_?[a-zA-Z]|_\d)(?:(?=(_?[a-zA-Z0-9]))\5)*(\[\d+(,\d+){0,2}])?(\.(_?[a-zA-Z]|_\d)(?:(?=(_?[a-zA-Z0-9]))\10)*(\[\d+])?)*(\.\d{1,2})?$

        // module regex for module tags
        const moduleRegex = new RegExp(
            "^" +
            nameRegex(2) + // module name
            "(:\\d{1,2})?" + // optional slot num (not required for rack optimized connections)
            ":[IOC]" + // input/output/config
            "(\\." +
            nameRegex(6) +
            arrayRegex +
            "?)?" + // optional member with optional array index
                bitIndexRegex +
                "?$"
        ); // optional bit index
        // full module regex
        // ^(_?[a-zA-Z]|_\d)(?:(?=(_?[a-zA-Z0-9]))\2)*(:\d{1,2})?:[IOC](\.(_?[a-zA-Z]|_\d)(?:(?=(_?[a-zA-Z0-9]))\6)*(\[\d+])?)?(\.\d{1,2})?$

        if (!userRegex.test(tagname) && !moduleRegex.test(tagname)) return false;

        // check segments
        if (tagname.split(/[:.[\],]/).filter(segment => segment.length > 40).length > 0)
            return false; // check that all segments are <= 40 char

        // passed all tests
        return true;
    }
}

/**
 * Generates Unique ID for Each Instance
 * based on the Generated EPATH
 *
 * @param input - EPATH of Tag
 * @returns hash
 */
const hash = (input: Buffer): string => {
    return crypto
        .createHash("md5")
        .update(input)
        .digest("hex");
};

export default Tag;
