import "../../utils/patchBigIntSerialization";

import {BuffaloZcl} from "./buffaloZcl";
import {BuffaloZclDataType, DataType, Direction, FrameType, ParameterCondition} from "./definition/enums";
import type {FoundationCommandName} from "./definition/foundation";
import type {Status} from "./definition/status";
import type {BuffaloZclOptions, Cluster, ClusterName, Command, CustomClusters, ParameterDefinition} from "./definition/tstype";
import * as Utils from "./utils";
import {ZclHeader} from "./zclHeader";

// biome-ignore lint/suspicious/noExplicitAny: API
type ZclPayload = any;

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

export class ZclFrame {
    public readonly header: ZclHeader;
    public readonly payload: ZclPayload;
    public readonly cluster: Cluster;
    public readonly command: Command;

    private constructor(header: ZclHeader, payload: ZclPayload, cluster: Cluster, command: Command) {
        this.header = header;
        this.payload = payload;
        this.cluster = cluster;
        this.command = command;
    }

    public toString(): string {
        return JSON.stringify({header: this.header, payload: this.payload, command: this.command});
    }

    /**
     * Creating
     */
    public static create(
        frameType: FrameType,
        direction: Direction,
        disableDefaultResponse: boolean,
        manufacturerCode: number | undefined,
        transactionSequenceNumber: number,
        commandKey: number | string,
        clusterKey: number | string,
        payload: ZclPayload,
        customClusters: CustomClusters,
        reservedBits = 0,
    ): ZclFrame {
        const cluster = Utils.getCluster(clusterKey, manufacturerCode, customClusters);
        const command: Command =
            frameType === FrameType.GLOBAL
                ? Utils.getGlobalCommand(commandKey)
                : direction === Direction.CLIENT_TO_SERVER
                  ? cluster.getCommand(commandKey)
                  : cluster.getCommandResponse(commandKey);

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

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

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

        if (this.header.isGlobal) {
            this.writePayloadGlobal(buffalo);
        } else if (this.header.isSpecific) {
            this.writePayloadCluster(buffalo);
        } else {
            throw new Error(`Frametype '${this.header.frameControl.frameType}' not valid`);
        }

        return buffalo.getWritten();
    }

    private writePayloadGlobal(buffalo: BuffaloZcl): void {
        const command = Utils.getFoundationCommand(this.command.ID);

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

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

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

                    buffalo.write(parameter.type, entry[parameter.name], options);
                }
            }
        } else if (command.parseStrategy === "flat") {
            for (const parameter of command.parameters) {
                buffalo.write(parameter.type, this.payload[parameter.name], {});
            }
        } else {
            if (command.parseStrategy === "oneof") {
                if (Utils.isFoundationDiscoverRsp(command.ID)) {
                    buffalo.writeUInt8(this.payload.discComplete);

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

    private writePayloadCluster(buffalo: BuffaloZcl): void {
        for (const parameter of this.command.parameters) {
            if (!ZclFrame.conditionsValid(parameter, this.payload, undefined)) {
                continue;
            }

            // TODO: biome migration - safer
            if (this.payload[parameter.name] == null) {
                throw new Error(`Parameter '${parameter.name}' is missing`);
            }

            buffalo.write(parameter.type, this.payload[parameter.name], {});
        }
    }

    /**
     * Parsing
     */
    public static fromBuffer(clusterID: number, header: ZclHeader | undefined, buffer: Buffer, customClusters: CustomClusters): ZclFrame {
        if (!header) {
            throw new Error("Invalid ZclHeader.");
        }

        const buffalo = new BuffaloZcl(buffer, header.length);
        const cluster = Utils.getCluster(clusterID, header.manufacturerCode, customClusters);
        const command: Command = header.isGlobal
            ? Utils.getGlobalCommand(header.commandIdentifier)
            : header.frameControl.direction === Direction.CLIENT_TO_SERVER
              ? cluster.getCommand(header.commandIdentifier)
              : cluster.getCommandResponse(header.commandIdentifier);
        const payload = ZclFrame.parsePayload(header, cluster, buffalo);

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

    private static parsePayload(header: ZclHeader, cluster: Cluster, buffalo: BuffaloZcl): ZclPayload {
        if (header.isGlobal) {
            return ZclFrame.parsePayloadGlobal(header, buffalo);
        }

        if (header.isSpecific) {
            return ZclFrame.parsePayloadCluster(header, cluster, buffalo);
        }

        throw new Error(`Unsupported frameType '${header.frameControl.frameType}'`);
    }

    private static parsePayloadCluster(header: ZclHeader, cluster: 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: BuffaloZclOptions = {payload};

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

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

                if (typeof length === "number") {
                    options.length = length;
                }
            }

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

        return payload;
    }

    private static parsePayloadGlobal(header: ZclHeader, buffalo: BuffaloZcl): ZclPayload {
        const command = Utils.getFoundationCommand(header.commandIdentifier);

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

            while (buffalo.isMore()) {
                // biome-ignore lint/suspicious/noExplicitAny: API
                const entry: {[s: string]: any} = {};

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

                    if (!ZclFrame.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 = entry.dataType;

                        if (entry.dataType === DataType.CHAR_STR && entry.attrId === 65281) {
                            // [workaround] parse char str as Xiaomi struct
                            options.dataType = BuffaloZclDataType.MI_STRUCT;
                        }
                    }

                    entry[parameter.name] = buffalo.read(parameter.type, options);

                    // TODO: not needed, but temp workaroudn to make payload equal to that of zcl-packet
                    // XXX: is this still needed?
                    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;
        }

        if (command.parseStrategy === "flat") {
            // biome-ignore lint/suspicious/noExplicitAny: API
            const payload: {[s: string]: any} = {};

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

            return payload;
        }

        if (command.parseStrategy === "oneof") {
            if (Utils.isFoundationDiscoverRsp(command.ID)) {
                // biome-ignore lint/suspicious/noExplicitAny: API
                const payload: {discComplete: number; attrInfos: {[k: string]: any}[]} = {
                    discComplete: buffalo.readUInt8(),
                    attrInfos: [],
                };

                while (buffalo.isMore()) {
                    const entry: (typeof payload.attrInfos)[number] = {};

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

                    payload.attrInfos.push(entry);
                }

                return payload;
            }
        }
    }

    /**
     * Utils
     */

    public static conditionsValid(parameter: ParameterDefinition, entry: ZclPayload, remainingBufferBytes: number | undefined): boolean {
        if (parameter.conditions) {
            for (const condition of parameter.conditions) {
                switch (condition.type) {
                    case ParameterCondition.STATUS_EQUAL: {
                        if ((entry.status as Status) !== condition.value) return false;
                        break;
                    }
                    case ParameterCondition.STATUS_NOT_EQUAL: {
                        if ((entry.status as Status) === condition.value) return false;
                        break;
                    }
                    case ParameterCondition.DIRECTION_EQUAL: {
                        if ((entry.direction as Direction) !== condition.value) return false;
                        break;
                    }
                    case ParameterCondition.BITMASK_SET: {
                        if (condition.reversed) {
                            if ((entry[condition.param] & condition.mask) === condition.mask) return false;
                        } else if ((entry[condition.param] & condition.mask) !== condition.mask) return false;
                        break;
                    }
                    case ParameterCondition.BITFIELD_ENUM: {
                        if (((entry[condition.param] >> condition.offset) & ((1 << condition.size) - 1)) !== condition.value) return false;
                        break;
                    }
                    case ParameterCondition.MINIMUM_REMAINING_BUFFER_BYTES: {
                        if (remainingBufferBytes !== undefined && remainingBufferBytes < condition.value) return false;
                        break;
                    }
                    case ParameterCondition.DATA_TYPE_CLASS_EQUAL: {
                        if (Utils.getDataTypeClass(entry.dataType) !== condition.value) return false;
                        break;
                    }
                    case ParameterCondition.FIELD_EQUAL: {
                        if (entry[condition.field] !== condition.value) return false;
                        break;
                    }
                }
            }
        }

        return true;
    }

    public isCluster(clusterName: FoundationCommandName | ClusterName): boolean {
        return this.cluster.name === clusterName;
    }

    // List of commands is not completed, feel free to add more.
    public isCommand(commandName: FoundationCommandName | "remove" | "add" | "write" | "enrollReq" | "checkin" | "getAlarm" | "arm"): boolean {
        return this.command.name === commandName;
    }
}
