import {Direction, Foundation, DataType, BuffaloZclDataType, FrameControl} from './definition';
import * as Utils from './utils';
import BuffaloZcl from './buffaloZcl';
import {TsType as BuffaloTsType} from '../buffalo';
import * as TsType from './tstype';
import {TsType as DefinitionTsType, FrameType} from './definition';

const MINIMAL_FRAME_LENGTH = 3;

// eslint-disable-next-line
type ZclPayload = any;

interface ZclHeader {
    frameControl: FrameControl;
    manufacturerCode: number;
    transactionSequenceNumber: number;
    commandIdentifier: number;
}

const ListTypes: number[] = [
    BuffaloZclDataType.LIST_UINT8,
    BuffaloZclDataType.LIST_UINT16,
    BuffaloZclDataType.LIST_UINT24,
    BuffaloZclDataType.LIST_UINT32,
    BuffaloZclDataType.LIST_ZONEINFO,
];

class ZclFrame {
    public readonly Header: ZclHeader;
    public readonly Payload: ZclPayload;
    public readonly Cluster: TsType.Cluster;

    private constructor(header: ZclHeader, payload: ZclPayload, cluster: TsType.Cluster) {
        this.Header = header;
        this.Payload = payload;
        this.Cluster = cluster;
    }

    /**
     * Creating
     */
    public static create(
        frameType: FrameType, direction: Direction, disableDefaultResponse: boolean, manufacturerCode: number,
        transactionSequenceNumber: number, commandKey: number | string, clusterID: number,
        payload: ZclPayload, reservedBits = 0
    ): ZclFrame {
        const cluster = Utils.getCluster(clusterID, manufacturerCode != null ? manufacturerCode : null);
        let command: TsType.Command = null;
        if (frameType === FrameType.GLOBAL) {
            command = Utils.getGlobalCommand(commandKey);
        } else {
            command = direction === Direction.CLIENT_TO_SERVER ?
                cluster.getCommand(commandKey) : cluster.getCommandResponse(commandKey);
        }

        const header: ZclHeader = {
            frameControl: {
                reservedBits, frameType, direction, disableDefaultResponse,
                manufacturerSpecific: manufacturerCode !== null,
            },
            transactionSequenceNumber,
            manufacturerCode,
            commandIdentifier: command.ID,
        };

        return new ZclFrame(header, payload, cluster);
    }

    public toBuffer(): Buffer {
        const buffalo = new BuffaloZcl(Buffer.alloc(250));
        this.writeHeader(buffalo);

        if (this.Header.frameControl.frameType === FrameType.GLOBAL) {
            this.writePayloadGlobal(buffalo);
        } else if (this.Header.frameControl.frameType === FrameType.SPECIFIC) {
            this.writePayloadCluster(buffalo);
        } else {
            throw new Error(`Frametype '${this.Header.frameControl.frameType}' not valid`);
        }

        return buffalo.getBuffer().slice(0, buffalo.getPosition());
    }

    private writeHeader(buffalo: BuffaloZcl): void {
        const frameControl = (
            (this.Header.frameControl.frameType & 0x03) |
            (((this.Header.frameControl.manufacturerSpecific ? 1 : 0) << 2) & 0x04) |
            ((this.Header.frameControl.direction << 3) & 0x08) |
            (((this.Header.frameControl.disableDefaultResponse ? 1 : 0) << 4) & 0x10) |
            ((this.Header.frameControl.reservedBits << 5) & 0xE0)
        );

        buffalo.writeUInt8(frameControl);

        if (this.Header.frameControl.manufacturerSpecific) {
            buffalo.writeUInt16(this.Header.manufacturerCode);
        }

        buffalo.writeUInt8(this.Header.transactionSequenceNumber);
        buffalo.writeUInt8(this.Header.commandIdentifier);
    }

    private writePayloadGlobal(buffalo: BuffaloZcl): void {
        const command = Object.values(Foundation).find((c): boolean => c.ID === this.Header.commandIdentifier);

        if (command.parseStrategy === 'repetitive') {
            for (const entry of this.Payload) {
                for (const parameter of command.parameters) {
                    const options: TsType.BuffaloZclOptions = {};

                    if (!ZclFrame.conditionsValid(parameter, entry, null)) {
                        continue;
                    }

                    if (parameter.type === BuffaloZclDataType.USE_DATA_TYPE && typeof entry.dataType === 'number') {
                        // We need to grab the dataType to parse useDataType
                        options.dataType = DataType[entry.dataType];
                    }

                    const typeStr = ZclFrame.getDataTypeString(parameter.type);
                    buffalo.write(typeStr, entry[parameter.name], options);
                }
            }
        } else if (command.parseStrategy === 'flat') {
            for (const parameter of command.parameters) {
                buffalo.write(DataType[parameter.type], this.Payload[parameter.name], {});
            }
        } else {
            /* istanbul ignore else */
            if (command.parseStrategy === 'oneof') {
                /* istanbul ignore else */
                if (command === Foundation.discoverRsp) {
                    buffalo.writeUInt8(this.Payload.discComplete);

                    for (const entry of this.Payload.attrInfos) {
                        for (const parameter of command.parameters) {
                            buffalo.write(DataType[parameter.type], entry[parameter.name], {});
                        }
                    }
                }
            }
        }
    }

    private writePayloadCluster(buffalo: BuffaloZcl): void {
        const command = this.Header.frameControl.direction === Direction.CLIENT_TO_SERVER ?
            this.Cluster.getCommand(this.Header.commandIdentifier) :
            this.Cluster.getCommandResponse(this.Header.commandIdentifier);

        for (const parameter of command.parameters) {
            if (!ZclFrame.conditionsValid(parameter, this.Payload, null)) {
                continue;
            }

            if (!this.Payload.hasOwnProperty(parameter.name)) {
                throw new Error(`Parameter '${parameter.name}' is missing`);
            }

            const typeStr = ZclFrame.getDataTypeString(parameter.type);
            buffalo.write(typeStr, this.Payload[parameter.name], {});
        }
    }

    /**
     * Parsing
     */
    public static fromBuffer(clusterID: number, buffer: Buffer): ZclFrame {
        if (buffer.length < MINIMAL_FRAME_LENGTH) {
            throw new Error("ZclFrame length is lower than minimal length");
        }

        const buffalo = new BuffaloZcl(buffer);
        const header = this.parseHeader(buffalo);
        const cluster = Utils.getCluster(
            clusterID,
            header.frameControl.manufacturerSpecific ? header.manufacturerCode : null
        );
        const payload = this.parsePayload(header, cluster, buffalo);

        return new ZclFrame(header, payload, cluster);
    }

    private static parseHeader(buffalo: BuffaloZcl): ZclHeader {
        const frameControlValue = buffalo.readUInt8();
        const frameControl = {
            frameType: frameControlValue & 0x03,
            manufacturerSpecific: ((frameControlValue >> 2) & 0x01) === 1,
            direction: (frameControlValue >> 3) & 0x01,
            disableDefaultResponse: ((frameControlValue >> 4) & 0x01) === 1,
            reservedBits: frameControlValue >> 5,
        };

        let manufacturerCode = null;
        if (frameControl.manufacturerSpecific) {
            manufacturerCode = buffalo.readUInt16();
        }

        const transactionSequenceNumber = buffalo.readUInt8();
        const commandIdentifier = buffalo.readUInt8();

        return {frameControl, transactionSequenceNumber, manufacturerCode, commandIdentifier};
    }

    private static parsePayload(header: ZclHeader, cluster: TsType.Cluster, buffalo: BuffaloZcl): ZclPayload {
        if (header.frameControl.frameType === FrameType.GLOBAL) {
            return this.parsePayloadGlobal(header, buffalo);
        } else if (header.frameControl.frameType === FrameType.SPECIFIC) {
            return this.parsePayloadCluster(header, cluster, buffalo);
        } else {
            throw new Error(`Unsupported frameType '${header.frameControl.frameType}'`);
        }
    }

    private static parsePayloadCluster(header: ZclHeader, cluster: TsType.Cluster,  buffalo: BuffaloZcl): ZclPayload {
        const command = header.frameControl.direction === Direction.CLIENT_TO_SERVER ?
            cluster.getCommand(header.commandIdentifier) : cluster.getCommandResponse(header.commandIdentifier);
        const payload: ZclPayload = {};

        for (const parameter of command.parameters) {
            const options: BuffaloTsType.Options = {payload};

            if (ListTypes.includes(parameter.type)) {
                const lengthParameter = command.parameters[command.parameters.indexOf(parameter) - 1];
                const length = payload[lengthParameter.name];

                /* istanbul ignore else */
                if (typeof length === 'number') {
                    options.length = length;
                }
            }

            const typeStr = ZclFrame.getDataTypeString(parameter.type);
            payload[parameter.name] = buffalo.read(typeStr, options);
        }

        return payload;
    }

    private static parsePayloadGlobal(header: ZclHeader, buffalo: BuffaloZcl): ZclPayload {
        const command = Object.values(Foundation).find((c): boolean => c.ID === header.commandIdentifier);

        if (command.parseStrategy === 'repetitive') {
            const payload = [];

            while (buffalo.getPosition() < buffalo.getBuffer().length) {
                const entry: {[s: string]: BuffaloTsType.Value} = {};

                for (const parameter of command.parameters) {
                    const options: TsType.BuffaloZclOptions = {};

                    if (!this.conditionsValid(parameter, entry, buffalo.getBuffer().length - buffalo.getPosition())) {
                        continue;
                    }

                    if (parameter.type === BuffaloZclDataType.USE_DATA_TYPE && typeof entry.dataType === 'number') {
                        // We need to grab the dataType to parse useDataType
                        options.dataType = DataType[entry.dataType];

                        if (entry.dataType === DataType.charStr && entry.hasOwnProperty('attrId')) {
                            // For Xiaomi struct parsing we need to pass the attributeID.
                            options.attrId = entry.attrId;
                        }
                    }

                    const typeStr = DataType[parameter.type] != null ?
                        DataType[parameter.type] : BuffaloZclDataType[parameter.type];
                    entry[parameter.name] = buffalo.read(typeStr, options);

                    // TODO: not needed, but temp workaroudn to make payload equal to that of zcl-packet
                    if (parameter.type === BuffaloZclDataType.USE_DATA_TYPE && entry.dataType === DataType.struct) {
                        entry['structElms'] = entry.attrData;
                        entry['numElms'] = entry.attrData.length;
                    }
                }

                payload.push(entry);
            }

            return payload;
        } else if (command.parseStrategy === 'flat') {
            const payload: {[s: string]: BuffaloTsType.Value} = {};

            for (const parameter of command.parameters) {
                payload[parameter.name] = buffalo.read(DataType[parameter.type], {});
            }

            return payload;
        } else {
            /* istanbul ignore else */
            if (command.parseStrategy === 'oneof') {
                /* istanbul ignore else */
                if (command === Foundation.discoverRsp) {
                    const payload: {[s: string]: BuffaloTsType.Value} = {};
                    payload.discComplete = buffalo.readUInt8();
                    payload.attrInfos = [];

                    while (buffalo.getPosition() < buffalo.getBuffer().length) {
                        const entry: {[s: string]: BuffaloTsType.Value} = {};

                        for (const parameter of command.parameters) {
                            entry[parameter.name] = buffalo.read(DataType[parameter.type], {});
                        }

                        payload.attrInfos.push(entry);
                    }

                    return payload;
                }
            }
        }
    }

    /**
     * Utils
     */
    private static getDataTypeString(dataType: DataType | BuffaloZclDataType): string {
        return DataType[dataType] != null ? DataType[dataType] : BuffaloZclDataType[dataType];
    }

    private static conditionsValid(
        parameter: DefinitionTsType.ParameterDefinition,
        entry: ZclPayload,
        remainingBufferBytes: number
    ): boolean {
        if (parameter.conditions) {
            const failedCondition = parameter.conditions.find((condition): boolean => {
                if (condition.type === 'statusEquals') {
                    return entry.status !== condition.value;
                } else if (condition.type == 'statusNotEquals') {
                    return entry.status === condition.value;
                } else if (condition.type == 'directionEquals') {
                    return entry.direction !== condition.value;
                } else if (remainingBufferBytes !== null && condition.type == 'minimumRemainingBufferBytes') {
                    return remainingBufferBytes < condition.value;
                } else  {
                    /* istanbul ignore else */
                    if (condition.type == 'dataTypeValueTypeEquals') {
                        return Utils.IsDataTypeAnalogOrDiscrete(entry.dataType) !== condition.value;
                    }
                }
            });

            if (failedCondition) {
                return false;
            }
        }

        return true;
    }

    public isSpecific(): boolean {
        return this.Header.frameControl.frameType === FrameType.SPECIFIC;
    }

    public isGlobal(): boolean {
        return this.Header.frameControl.frameType === FrameType.GLOBAL;
    }

    // List of clusters is not completed, feel free to add more.
    public isCluster(clusterName: 'genTime' | 'genAnalogInput' | 'genBasic' | 'genGroups' | 'ssIasZone'): boolean {
        return this.Cluster.name === clusterName;
    }

    // List of commands is not completed, feel free to add more.
    public isCommand(
        commandName: 'read' | 'report' | 'readRsp' | 'remove' | 'add' | 'write' | 'enrollReq' | 'configReport'
    ): boolean {
        return this.getCommand().name === commandName;
    }

    public getCommand(): TsType.Command {
        let command: TsType.Command = null;
        if (this.Header.frameControl.frameType === FrameType.GLOBAL) {
            command = Utils.getGlobalCommand(this.Header.commandIdentifier);
        } else {
            command = this.Header.frameControl.direction === Direction.CLIENT_TO_SERVER ?
                this.Cluster.getCommand(this.Header.commandIdentifier) :
                this.Cluster.getCommandResponse(this.Header.commandIdentifier);
        }
        return command;
    }
}

export default ZclFrame;
