import {Subsystem, Type, MaxDataSize} from '../unpi/constants';
import {Frame as UnpiFrame} from '../unpi';
import Definition from './definition';
import BuffaloZnp from './buffaloZnp';
import ParameterType from './parameterType';
import {MtParameter, MtCmd, ZpiObjectPayload, MtType, BuffaloZnpOptions} from './tstype';

const BufferAndListTypes = [
    ParameterType.BUFFER, ParameterType.BUFFER8, ParameterType.BUFFER16,
    ParameterType.BUFFER18, ParameterType.BUFFER32, ParameterType.BUFFER42,
    ParameterType.BUFFER100, ParameterType.LIST_UINT16, ParameterType.LIST_ROUTING_TABLE,
    ParameterType.LIST_BIND_TABLE, ParameterType.LIST_NEIGHBOR_LQI, ParameterType.LIST_NETWORK,
    ParameterType.LIST_ASSOC_DEV, ParameterType.LIST_UINT8,
];

class ZpiObject {
    public readonly subsystem: Subsystem;
    public readonly command: string;
    public readonly commandID: number;
    public readonly payload: ZpiObjectPayload;
    public readonly type: Type;

    private readonly parameters: MtParameter[];

    private constructor(
        type: Type, subsystem: Subsystem, command: string, commandID: number, payload: ZpiObjectPayload,
        parameters: MtParameter[],
    ) {
        this.subsystem = subsystem;
        this.command = command;
        this.commandID = commandID;
        this.payload = payload;
        this.type = type;
        this.parameters = parameters;
    }

    public static createRequest(subsystem: Subsystem, command: string, payload: ZpiObjectPayload): ZpiObject {
        if (!Definition[subsystem]) {
            throw new Error(`Subsystem '${subsystem}' does not exist`);
        }

        const cmd = Definition[subsystem].find((c: MtCmd): boolean => c.name === command);

        if (!cmd) {
            throw new Error(`Command '${command}' from subsystem '${subsystem}' not found`);
        }

        return new ZpiObject(cmd.type, subsystem, command, cmd.ID, payload, cmd.request);
    }

    public toUnpiFrame(): UnpiFrame {
        const buffer = this.createPayloadBuffer();
        return new UnpiFrame(this.type, this.subsystem, this.commandID, buffer);
    }

    public static fromUnpiFrame(frame: UnpiFrame): ZpiObject {
        const cmd = Definition[frame.subsystem].find((c: MtCmd): boolean => c.ID === frame.commandID);

        if (!cmd) {
            throw new Error(`CommandID '${frame.commandID}' from subsystem '${frame.subsystem}' not found`);
        }

        const parameters = frame.type === Type.SRSP ? cmd.response : cmd.request;

        if (parameters === undefined) {
            /* istanbul ignore next */
            throw new Error(
                `CommandID '${frame.commandID}' from subsystem '${frame.subsystem}' cannot be a ` +
                `${frame.type === Type.SRSP ? 'response' : 'request'}`
            );
        }

        const payload = this.readParameters(frame.data, parameters);
        return new ZpiObject(frame.type, frame.subsystem, cmd.name, cmd.ID, payload, parameters);
    }

    private static readParameters(buffer: Buffer, parameters: MtParameter[]): ZpiObjectPayload {
        const buffalo = new BuffaloZnp(buffer);
        const result: ZpiObjectPayload = {};

        for (const parameter of parameters) {
            const options: BuffaloZnpOptions = {};

            if (BufferAndListTypes.includes(parameter.parameterType)) {
                // When reading a buffer, assume that the previous parsed parameter contains
                // the length of the buffer
                const lengthParameter = parameters[parameters.indexOf(parameter) - 1];
                const length: MtType = result[lengthParameter.name];

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

                if (parameter.parameterType === ParameterType.LIST_ASSOC_DEV) {
                    // For LIST_ASSOC_DEV, we also need to grab the startindex which is right before the length
                    const startIndexParameter = parameters[parameters.indexOf(parameter) - 2];
                    const startIndex: MtType = result[startIndexParameter.name];

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

            result[parameter.name] = buffalo.read(ParameterType[parameter.parameterType], options);
        }

        return result;
    }

    private createPayloadBuffer(): Buffer {
        const buffalo = new BuffaloZnp(Buffer.alloc(MaxDataSize));

        for (const parameter of this.parameters) {
            const value = this.payload[parameter.name];
            buffalo.write(ParameterType[parameter.parameterType], value, {});
        }

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

    public isResetCommand(): boolean {
        return (this.command === 'resetReq' && this.subsystem === Subsystem.SYS) ||
            (this.command === 'systemReset' && this.subsystem === Subsystem.SAPI);
    }
}

export default ZpiObject;