/* v8 ignore start */

import type {KeyValue} from "../../controller/tstype";
import type {DataType} from "../../zspec/zcl";
import {BuffaloZcl} from "../../zspec/zcl/buffaloZcl";
import type {BuffaloZclDataType} from "../../zspec/zcl/definition/enums";
import type {BuffaloZclOptions} from "../../zspec/zcl/definition/tstype";
import {ClusterId as ZdoClusterId} from "../../zspec/zdo";
import {BuffaloZdo} from "../../zspec/zdo/buffaloZdo";
import type {GenericZdoResponse} from "../../zspec/zdo/definition/tstypes";
import {FRAMES, type ParamsDesc, ZBOSS_COMMAND_ID_TO_ZDO_RSP_CLUSTER_ID} from "./commands";
import {BuffaloZBOSSDataType, type CommandId} from "./enums";

export class ZBOSSBuffaloZcl extends BuffaloZcl {
    // biome-ignore lint/suspicious/noExplicitAny: API
    public override write(type: DataType | BuffaloZclDataType | BuffaloZBOSSDataType, value: any, options: BuffaloZclOptions): void {
        switch (type) {
            case BuffaloZBOSSDataType.EXTENDED_PAN_ID: {
                this.writeBuffer(value, 8);
                break;
            }
            default: {
                super.write(type as DataType | BuffaloZclDataType, value, options);
                break;
            }
        }
    }
    // biome-ignore lint/suspicious/noExplicitAny: API
    public override read(type: DataType | BuffaloZclDataType | BuffaloZBOSSDataType, options: BuffaloZclOptions): any {
        switch (type) {
            case BuffaloZBOSSDataType.EXTENDED_PAN_ID: {
                return this.readBuffer(8);
            }
            default: {
                return super.read(type as DataType | BuffaloZclDataType, options);
            }
        }
    }

    public writeByDesc(payload: KeyValue, params: ParamsDesc[]): number {
        const start = this.getPosition();
        for (const parameter of params) {
            const options: BuffaloZclOptions = {};

            if (parameter.condition && !parameter.condition(payload, this)) {
                continue;
            }
            if (parameter.options) parameter.options(payload, options);

            if (parameter.type === BuffaloZBOSSDataType.LIST_TYPED && parameter.typed) {
                const internalPaload = payload[parameter.name];
                for (const value of internalPaload) {
                    this.writeByDesc(value, parameter.typed);
                }
            } else {
                this.write(parameter.type as DataType, payload[parameter.name], options);
            }
        }
        return this.getPosition() - start;
    }

    public readByDesc(params: ParamsDesc[]): KeyValue {
        const payload: KeyValue = {};

        for (const parameter of params) {
            const options: BuffaloZclOptions = {payload};

            if (parameter.condition && !parameter.condition(payload, this)) {
                continue;
            }
            if (parameter.options) parameter.options(payload, options);

            if (parameter.type === BuffaloZBOSSDataType.LIST_TYPED && parameter.typed) {
                payload[parameter.name] = [];

                if (!this.isMore()) break;

                for (let i = 0; i < (options.length || 0); i++) {
                    const internalPaload = this.readByDesc(parameter.typed);
                    payload[parameter.name].push(internalPaload);
                }
            } else {
                if (!this.isMore()) break;

                payload[parameter.name] = this.read(parameter.type as DataType, options);
            }
        }

        return payload;
    }
}

function getFrameDesc(type: FrameType, key: CommandId): ParamsDesc[] {
    const frameDesc = FRAMES[key];
    if (!frameDesc) throw new Error(`Unrecognized frame type from FrameID ${key}`);
    switch (type) {
        case FrameType.REQUEST:
            return frameDesc.request || [];
        case FrameType.RESPONSE:
            return frameDesc.response || [];
        case FrameType.INDICATION:
            return frameDesc.indication || [];
    }
}

function fixNonStandardZdoRspPayload(clusterId: ZdoClusterId, buffer: Buffer): Buffer {
    switch (clusterId) {
        case ZdoClusterId.NODE_DESCRIPTOR_RESPONSE:
        case ZdoClusterId.POWER_DESCRIPTOR_RESPONSE:
        case ZdoClusterId.ACTIVE_ENDPOINTS_RESPONSE:
        case ZdoClusterId.MATCH_DESCRIPTORS_RESPONSE: {
            // flip nwkAddress from end to start
            return Buffer.concat([buffer.subarray(0, 1), buffer.subarray(-2), buffer.subarray(1, -2)]);
        }

        case ZdoClusterId.SIMPLE_DESCRIPTOR_RESPONSE: {
            // flip nwkAddress from end to start
            // add length after nwkAddress
            // move outClusterCount before inClusterList
            const inClusterListSize = buffer[7] * 2; // uint16
            return Buffer.concat([
                buffer.subarray(0, 1), // status
                buffer.subarray(-2), // nwkAddress
                Buffer.from([buffer.length - 3 /* status + nwkAddress */]),
                buffer.subarray(1, 8), // endpoint>inClusterCount
                buffer.subarray(9, 9 + inClusterListSize), // inClusterList
                buffer.subarray(8, 9), // outClusterCount
                buffer.subarray(9 + inClusterListSize, -2), // outClusterList
            ]);
        }
    }

    return buffer;
}

export function readZBOSSFrame(buffer: Buffer): ZBOSSFrame {
    const buf = new ZBOSSBuffaloZcl(buffer);
    const version = buf.readUInt8();
    const type: FrameType = buf.readUInt8();
    const commandId: CommandId = buf.readUInt16();
    const tsn = type === FrameType.REQUEST || type === FrameType.RESPONSE ? buf.readUInt8() : 0;

    const zdoResponseClusterId =
        type === FrameType.RESPONSE || type === FrameType.INDICATION ? ZBOSS_COMMAND_ID_TO_ZDO_RSP_CLUSTER_ID[commandId] : undefined;

    if (zdoResponseClusterId !== undefined) {
        // FrameType.INDICATION has no tsn (above), no category
        const category = type === FrameType.RESPONSE ? buf.readUInt8() : undefined;
        const zdoPayload = fixNonStandardZdoRspPayload(zdoResponseClusterId, buffer.subarray(type === FrameType.RESPONSE ? 6 : 4));
        const zdo = BuffaloZdo.readResponse(false, zdoResponseClusterId, zdoPayload);

        return {
            version,
            type,
            commandId,
            tsn,
            payload: {
                category,
                zdoClusterId: zdoResponseClusterId,
                zdo,
            },
        };
    }

    return {
        version,
        type,
        commandId,
        tsn,
        payload: readPayload(type, commandId, buf),
    };
}

export function writeZBOSSFrame(frame: ZBOSSFrame): Buffer {
    const buf = new ZBOSSBuffaloZcl(Buffer.alloc(247));
    buf.writeInt8(frame.version);
    buf.writeInt8(frame.type);
    buf.writeUInt16(frame.commandId);
    buf.writeUInt8(frame.tsn);
    writePayload(frame.type, frame.commandId, frame.payload, buf);
    return buf.getWritten();
}

export enum FrameType {
    REQUEST = 0,
    RESPONSE = 1,
    INDICATION = 2,
}

export interface ZBOSSFrame {
    version: number;
    type: FrameType;
    commandId: CommandId;
    tsn: number;
    payload: KeyValue & {zdoCluster?: ZdoClusterId; zdo?: GenericZdoResponse};
}

export function makeFrame(type: FrameType, commandId: CommandId, params: KeyValue): ZBOSSFrame {
    const frameDesc = getFrameDesc(type, commandId);
    const payload: KeyValue = {};
    for (const parameter of frameDesc) {
        // const options: BuffaloZclOptions = {payload};

        if (parameter.condition && !parameter.condition(params, undefined)) {
            continue;
        }

        payload[parameter.name] = params[parameter.name];
    }
    return {
        version: 0,
        type: type,
        commandId: commandId,
        tsn: 0,
        payload: payload,
    };
}

function readPayload(type: FrameType, commandId: CommandId, buffalo: ZBOSSBuffaloZcl): KeyValue {
    const frameDesc = getFrameDesc(type, commandId);
    return buffalo.readByDesc(frameDesc);
}

function writePayload(type: FrameType, commandId: CommandId, payload: KeyValue, buffalo: ZBOSSBuffaloZcl): number {
    const frameDesc = getFrameDesc(type, commandId);
    return buffalo.writeByDesc(payload, frameDesc);
}
