import { CIP } from "../enip";
import { Template } from "../structure";
import type Controller from '../controller'

type tagListTagType = {
    code: number,
    sintPos: number,
    typeName: string,
    structure: boolean,
    arrayDims: number,
    reserved: boolean
}

type tagListTag = {
    id: number,
    name: string,
    type: tagListTagType,
    program: string
}

type tagListTemplates = {
    [index: string]: Template;
}

class TagList {
    tags: tagListTag[];
    templates: tagListTemplates;

    /**
     * TagList Class for retrieving a list of all tags on a PLC
     */
    constructor () {
        this.tags = [];
        this.templates = {};
    }

    /**
     * Generates the CIP message to request a list of tags
     *
     * @param instanceID- instance id to start getting a list of object tags
     * @param program - (optional) name of the program to search 
     * @returns message to be sent to PLC
     */
    _generateListMessageRequest(instanceID: number = 0, program?: string) {

        const { LOGICAL, DATA } = CIP.EPATH.segments;

        let pathArray = [];

        if (program) { pathArray.push(DATA.build("Program:" + program)); }

        pathArray.push(LOGICAL.build(LOGICAL.types.ClassID, 0x6b)); //Symbol Class ID
    
        if (instanceID === 0) {
            pathArray.push(Buffer.from([0x25, 0x00, 0x00, 0x00])); //Start at Instance 0;
        } else {
            pathArray.push( LOGICAL.build(LOGICAL.types.InstanceID, instanceID));
        }

        const requestData = Buffer.from([0x02, 0x00, 0x01, 0x00, 0x02, 0x00]); // 2 Attributes - Attribute 1 and Attribute 2
        const request = CIP.MessageRouter.build( CIP.MessageRouter.services.GET_INSTANCE_ATTRIBUTE_LIST, Buffer.concat(pathArray), requestData);

        return request;
    }

    /**
     * Parse CIP response into tag data
     *
     * @param data - Buffer data to parse
     * @param program - (optional) name of the program tag is from (optional)
     * @returns Last instance id parsed
     */
    _parseAttributeListResponse(data: Buffer, program?: string): number {
        let instanceID: number;
        let pointer = 0;

        while (pointer < data.length) {
            instanceID = data.readUInt32LE(pointer); //Parse instance ID
            pointer += 4;

            const nameLength = data.readUInt16LE(pointer); // Parse tag Name Length
            pointer += 2;

            const tagName = data.slice(pointer, pointer + nameLength).toString(); // Parse tag Name
            pointer += nameLength;

            const tagType = data.readUInt16LE(pointer); // Parse tag type
            pointer += 2;

            const lastTag = this.tags.findIndex(tag => {
                return (tag.id === instanceID && tag.program === program); 
            });

            const tagObj = {
                id: instanceID,
                name: tagName,
                type: this._parseTagType(tagType),
                program: program
            };
            
            if (lastTag !== -1) {
                this.tags[lastTag] = tagObj;
            } else {
                this.tags.push(tagObj);
            }          
        }
        return instanceID; // Return last instance id
    }

    /**
     * Get and store tag type name from code for all tags
     */
    _getTagTypeNames () {
        for (const tag of this.tags) {
            tag.type.typeName = CIP.DataTypes.getTypeCodeString(tag.type.code);
            if(!tag.type.typeName && this.templates[tag.type.code]) {
                tag.type.typeName = this.templates[tag.type.code]._name;
            }
        }
    }

    /**
     * 
     * @param tagType - tag type numerical value
     * @returns tag list type object
     */
    _parseTagType(tagType: number): tagListTagType {

        let typeCode = null;
        let sintPos = null;
        if ((tagType & 0x00ff) === 0xc1) {
            typeCode = 0x00c1;
            sintPos = (tagType & 0x0f00) >> 8;
        } else {
            typeCode = tagType & 0x0fff;
        }
        
        const structure = !!(tagType & 0x8000);
        const reserved = !!(tagType & 0x1000);
        const arrayDims = (tagType & 0x6000) >> 13;
        
        return {
            code: typeCode,
            sintPos: sintPos,
            typeName: null,
            structure: structure,
            arrayDims: arrayDims,
            reserved: reserved
        };
    }

    /**
     * Parse CIP response into tag data
     *
     * @param PLC - Controller to get tags from
     * @param program - (optional) name of the program tag is from (optional)
     * @returns Promise resolves taglist array
     */
    getControllerTags(PLC: Controller, program: string = null): Promise<tagListTag[]> {
        return new Promise( (resolve, reject) => {

            const getListAt = (instanceID = 0) => { // Create function that we can call back in recursion

                const cipData = this._generateListMessageRequest(instanceID, program); // Create CIP Request
        
                PLC.write_cip(cipData); // Write CIP data to PLC
                
                // Response Handler
                PLC.on("Get Instance Attribute List", async (err, data) => {

                    PLC.removeAllListeners("Get Instance Attribute List");  // Make sure we don't handle future calls in this instance

                    // Check For actual error (Skip too much data)
                    if (err && err.generalStatusCode !== 6) {
                        
                        const errData = {
                            func: "getControllerTags",
                            instanceID: instanceID,
                            program: program,
                            cipReq: cipData,
                        };

                        if (Array.isArray(err.ext)) {
                            err.ext.push(errData);
                        } else {
                            err.ext = [errData];
                        }

                        reject(err);
                        return;
                    }

                    // If too much data, call function again starting at last instance + 1
                    if (err && err.generalStatusCode === 6) {

                        const lastInstance = this._parseAttributeListResponse(data, program); // Parse response data
                        getListAt(lastInstance + 1);

                    } else {

                        this._parseAttributeListResponse(data, program); // pArse response data

                        // If program is not defined fetch tags for existing programs
                        if (!program) {
                            for (let prg of this.programs) {
                                await this.getControllerTags(PLC, prg).catch(reject);
                            }

                            await this._getAllTemplates(PLC).catch(reject); // Get All templates for structures
                            
                        } 
                        this._getTagTypeNames();
                        resolve(this.tags);
                    }
                });
            };

            getListAt(0); // Call first time

        });
    }

    /**
     * Gets Controller Program Names
     * 
     * @returns array of program names
     */
    get programs(): string[] {
        return this.tags.filter(tag => tag.name.slice(0, 8) === "Program:").map(tag => {
            return tag.name.slice(8, tag.name.length);
        });
    }

    /**
     * Gets tag info from tag name and program name
     * 
     * @param tagName 
     * @param program 
     * @returns 
     */
    getTag(tagName: string, program: string = null): tagListTag {
        return this.tags.find(tag => tag.name === tagName && tag.program === program);
    }

    /**
     * 
     * @param tagName 
     * @param program 
     * @returns tag template or null if none
     */
    getTemplateByTag(tagName: string, program: string = null): Template {        
        const tagArray = tagName.split(".");
        const tag = this.tags.find(tag => tag.name.toLowerCase().replace(/\[.*/, "") === tagArray[0].toLowerCase().replace(/\[.*/, "") && String(tag.program).toLowerCase() === String(program).toLowerCase());

        if (tag) {
            let finalTemplate = this.templates[tag.type.code];
            let tagArrayPointer = 1;
            while (finalTemplate && tagArrayPointer < tagArray.length) {
                const memberName = String(tagArray[tagArrayPointer]).replace(/\[.*/, ""); //removes array indication
                const nextTag = finalTemplate._members.find(member => member.name === memberName);
                if(nextTag) {
                    finalTemplate = this.templates[nextTag.type.code];
                } else {
                    finalTemplate = null;
                }
                tagArrayPointer++;
            } 
            return finalTemplate;
        } else {
            return null;
        }
        
    }

    /**
     * Get all templates from a PLC
     * 
     * @param PLC 
     * @returns Promise that resolves after all templates are retrieved from PLC
     */
    _getAllTemplates (PLC: Controller): Promise<void> {
        return new Promise (async (resolve, reject) => {
            for (const tag of this.tags) {
                if (tag.type.structure && !this.templates[tag.type.code]) {

                    try {
                        const template = new Template();
                        await template.getTemplate(PLC, tag.type.code);
                        this.templates[tag.type.code] = template;
                    } catch (e) { /* ignore template fetching errors */ }              

                }
            }

            let foundTemplate = true;

            while(foundTemplate) {
                foundTemplate = false;

                for (const temp in this.templates) {
                    for (const member of this.templates[temp]._members) {
                        if (member.type.structure && !this.templates[member.type.code]) {
                            foundTemplate = true;
                            const template = new Template();
                            await template.getTemplate(PLC, member.type.code).catch(reject); 
                            this.templates[member.type.code] = template; 
                        }
                    }
                }
            }

            resolve();
        });
    }
  
}

export default TagList;
export {tagListTag, tagListTemplates, tagListTagType}