/* eslint-disable no-control-regex */
import koffi from "koffi";

import { Device, VMLibrary, VoiceMeeterTypes } from "../types/voicemeeter-types";
import { BusProperties, MacroButtonModes, StripProperties } from "./constants";
import DLLHandler from "./dll-handler";

/**
 * @ignore
 */
let libVM: VMLibrary;
/**
 * @ignore
 */
let instance: Voicemeeter;

export default class Voicemeeter {
    /**
     * Initializes the voice meeter dll connection.
     * This call is neccessary to use the api. It returns a promise with a VoiceMeeter instance
     */
    static init = async (): Promise<Voicemeeter> => {
        const dllPath = await DLLHandler.getDLLPath();

        return new Promise((resolve: (instance: Voicemeeter) => any) => {
            if (!instance) {
                instance = new Voicemeeter();
            }
            const lib = koffi.load(`${dllPath}/VoicemeeterRemote64.dll`);
            libVM = {
                VBVMR_Login: lib.func("long __stdcall VBVMR_Login(void)"),
                VBVMR_Logout: lib.func("long __stdcall VBVMR_Logout(void)"),
                VBVMR_RunVoicemeeter: lib.func("long __stdcall VBVMR_RunVoicemeeter(long mode)"),
                VBVMR_IsParametersDirty: lib.func("long __stdcall VBVMR_IsParametersDirty(void)"),
                VBVMR_GetLevel: lib.func("long __stdcall VBVMR_GetLevel(long type, long channel, _Out_ float* value)"),
                VBVMR_GetParameterFloat: lib.func("long __stdcall VBVMR_GetParameterFloat(const char* param, _Out_ float* value)"),
                VBVMR_GetParameterStringA: lib.func("long __stdcall VBVMR_GetParameterStringA(const char* param, _Out_ char* value)"),
                VBVMR_SetParameters: lib.func("long __stdcall VBVMR_SetParameters(const char* param)"),
                VBVMR_Output_GetDeviceNumber: lib.func("long __stdcall VBVMR_Output_GetDeviceNumber(void)"),
                VBVMR_Output_GetDeviceDescA: lib.func(
                    "long __stdcall VBVMR_Output_GetDeviceDescA(long index, _Out_ long* type, _Out_ char* name, _Out_ char* hardwareId)"
                ),
                VBVMR_Input_GetDeviceNumber: lib.func("long __stdcall VBVMR_Input_GetDeviceNumber(void)"),
                VBVMR_Input_GetDeviceDescA: lib.func(
                    "long __stdcall VBVMR_Input_GetDeviceDescA(long index, long* type, char* name, char* hardwareId)"
                ),
                VBVMR_GetVoicemeeterType: lib.func("long __stdcall VBVMR_GetVoicemeeterType(_Out_ long* type)"),
                VBVMR_GetVoicemeeterVersion: lib.func("long __stdcall VBVMR_GetVoicemeeterVersion(_Out_ long* version)"),
                VBVMR_MacroButton_IsDirty: lib.func("long __stdcall VBVMR_MacroButton_IsDirty(void)"),
                VBVMR_MacroButton_GetStatus: lib.func(
                    "long __stdcall VBVMR_MacroButton_GetStatus(long nuLogicalButton, _Out_ float* pValue, long bitmode)"
                ),
                VBVMR_MacroButton_SetStatus: lib.func(
                    "long __stdcall VBVMR_MacroButton_SetStatus(long nuLogicalButton, float fValue, long bitmode)"
                ),
            };

            instance.isInitialised = true;
            resolve(instance);
        });
    };

    private isInitialised = false;
    private isConnected = false;
    private outputDevices: Device[] = [];
    private inputDevices: Device[] = [];
    private version = "";
    private type: VoiceMeeterTypes;
    private eventPool = [] as Array<() => void>;
    private stringParameters = ["Label", "FadeTo", "FadeBy", "AppGain", "AppMute", "name", "ip"];
    private timerInterval: NodeJS.Timeout;

    /**
     * Starts a connection to VoiceMeeter
     */
    public connect = (): { success: boolean; message: string; code: number } | never => {
        if (!this.isInitialised) {
            throw new Error("Await the initialisation before connect");
        }
        if (this.isConnected) {
            // If already connected, still return success status instead of throwing
            return { success: true, message: "Already connected.", code: 0 };
        }

        const loginResult = libVM.VBVMR_Login();

        switch (loginResult) {
            case 0: {
                // Complete success, Voicemeeter is running
                this.isConnected = true;
                this.type = this.getVoicemeeterType();
                this.version = this.getVoicemeeterVersion();
                this.timerInterval = setInterval(this.checkPropertyChange, 10);
                return { success: true, message: "Successfully connected to VoiceMeeter (VM Running).", code: 0 };
            }
            case 1: {
                // Connected successfully but Voicemeeter application is not running
                // Pipe is established, can wait for VM to start
                this.isConnected = true;
                this.type = undefined;
                this.version = "Unknown";
                this.timerInterval = setInterval(this.checkPropertyChange, 10);
                return { success: true, message: "Connected to VoiceMeeter API (VM App Not Running).", code: 1 };
            }
            case -1: {
                // Failed to get client (unexpected error) - throw error
                this.isConnected = false;
                throw new Error("VoiceMeeter connection failed: Unable to get client (Unexpected error -1).");
            }
            case -2: {
                // Unexpected login (logout should have been executed first) - throw error
                this.isConnected = false;
                throw new Error("VoiceMeeter connection failed: Unexpected login (Expected logout first -2).");
            }
            default: {
                // Unknown return value - throw error
                this.isConnected = false;
                throw new Error(`VoiceMeeter connection failed with unknown error code: ${loginResult}.`);
            }
        }
    };

    /**
     * Getter $outputDevices
     * @return {Device[] }
     */
    public get $outputDevices(): Device[] {
        return this.outputDevices;
    }

    /**
     * Getter $inputDevices
     * @return {Device[] }
     */
    public get $inputDevices(): Device[] {
        return this.inputDevices;
    }

    /**
     * Getter $version
     * @return {string }
     */
    public get $version(): string {
        return this.version;
    }

    /**
     * Getter $type
     * @return {VoiceMeeterTypes}
     */
    public get $type(): VoiceMeeterTypes {
        return this.type;
    }

    /**
     * Terminates the connection to VoiceMeeter
     */
    public disconnect = () => {
        if (!this.isConnected) {
            throw new Error("Not connected ");
        }
        try {
            if (libVM.VBVMR_Logout() === 0) {
                clearInterval(this.timerInterval);
                this.isConnected = false;
                return;
            }
            throw new Error("Disconnect failed");
        } catch {
            throw new Error("Disconnect failed");
        }
    };

    /**
     * Updates all input and ouput devices
     */
    public updateDeviceList = () => {
        if (!this.isConnected) {
            throw new Error("Not connected ");
        }
        this.outputDevices = [];
        this.inputDevices = [];
        const outputDeviceNumber = libVM.VBVMR_Output_GetDeviceNumber();
        for (let i = 0; i < outputDeviceNumber; i++) {
            const hardwareIdPtr = Buffer.alloc(256);
            const namePtr = Buffer.alloc(256);
            const typePtr = [0];

            libVM.VBVMR_Output_GetDeviceDescA(i, typePtr, namePtr, hardwareIdPtr);
            this.outputDevices.push({
                name: namePtr.toString().replaceAll(/\u0000+$/g, ""),
                hardwareId: hardwareIdPtr.toString().replaceAll(/\u0000+$/g, ""),
                type: typePtr[0],
            });
        }

        const inputDeviceNumber = libVM.VBVMR_Input_GetDeviceNumber();
        for (let i = 0; i < inputDeviceNumber; i++) {
            const hardwareIdPtr = Buffer.alloc(256);
            const namePtr = Buffer.alloc(256);
            const typePtr = [0];

            libVM.VBVMR_Input_GetDeviceDescA(i, typePtr, namePtr, hardwareIdPtr);
            this.inputDevices.push({
                name: namePtr.toString().replaceAll(/\u0000+$/g, ""),
                hardwareId: hardwareIdPtr.toString().replaceAll(/\u0000+$/g, ""),
                type: typePtr[0],
            });
        }
    };

    /**
     * Returns wheter a parameter has been changed
     */
    public isParametersDirty = () => {
        return libVM.VBVMR_IsParametersDirty();
    };

    /**
     * Gets a bus parameter.
     * @param  {number} index Index of the bus
     * @param  {BusProperties} property Property which should be get
     */

    public getBusParameter = (index: number, property: BusProperties) => {
        return this.getParameter("Bus", index, property);
    };

    /**
     * Gets a strip parameter
     * @param  {number} index Index of the strip
     * @param  {StripProperties} property Property which should be get
     */
    public getStripParameter = (index: number, property: StripProperties) => {
        return this.getParameter("Strip", index, property);
    };

    /**
     * Sets a parameter of a strip.
     * @param  {number} index Strip number
     * @param  {StripProperties} property Propertyname which should be changed
     * @param  {any} value Property value
     */
    public setStripParameter = (index: number, property: StripProperties, value: any) => {
        return this.setParameter("Strip", index, property, value);
    };

    /**
     * Sets a parameter of a bus.
     * @param  {number} index Bus number
     * @param  {StripProperties} property Propertyname which should be changed
     * @param  {any} value Property value
     */
    public setBusParameter = (index: number, property: BusProperties, value: any) => {
        return this.setParameter("Bus", index, property, value);
    };

    /**
     * @param  {()=>any} fn Function which should be called if something changes
     */
    public attachChangeEvent = (fn: () => any) => {
        this.eventPool.push(fn);
    };
    /**
     * @param parameterName Name of the parameter that should be get
     * @returns {any} Parameter value
     */
    public getOption = (parameterName: string) => {
        if (!this.isConnected) {
            throw new Error("Not correct connected ");
        }
        // Some parameters return string values and require some post-processing, this checks for those parameters
        if (this.stringParameters.some((str) => parameterName.includes(str))) {
            const strPtr = Buffer.alloc(512);
            libVM.VBVMR_GetParameterStringA(parameterName, strPtr);
            return [...String.fromCharCode.apply(null, strPtr)]
                .filter((e: string) => {
                    return e !== "\0";
                })
                .join("");
        }
        const valuePtr = [0];
        libVM.VBVMR_GetParameterFloat(parameterName, valuePtr);
        return valuePtr[0];
    };
    /**
     * Sets an option.
     * @param {string} option Option to set
     */
    public setOption = (option: string) => {
        const script = Buffer.alloc(option.length + 1);
        script.fill(0).write(option);
        libVM.VBVMR_SetParameters(script);
        return new Promise((resolve) => {
            setTimeout(resolve, 200);
        });
    };

    /**
     * Checks if any macro button has unsaved changes
     */
    public isMacroButtonDirty = () => {
        return libVM.VBVMR_MacroButton_IsDirty();
    };

    /**
     * Gets the status of a specific macro button
     * @param {number} buttonIndex - The index of the macro button
     * @param {number} [bitmode=0] - Bit mode parameter (optional, defaults to MacroButtonModes.DEFAULT)
     * @returns {number} The current status value of the macro button
     * @throws {Error} Throws an error if failed to get the button status
     */
    public getMacroButtonStatus = (buttonIndex: number, bitmode: number = MacroButtonModes.DEFAULT): number => {
        const valuePtr = [0];
        const result = libVM.VBVMR_MacroButton_GetStatus(buttonIndex, valuePtr, bitmode);
        if (result === 0) {
            return valuePtr[0];
        }
        throw new Error(`Failed to get macro button ${buttonIndex} status`);
    };

    /**
     * Sets the status of a specific macro button
     * @param {number} buttonIndex - The index of the macro button
     * @param {number} value - The status value to set
     * @param {number} [bitmode=0] - Bit mode parameter (optional, defaults to MacroButtonModes.DEFAULT)
     * @throws {Error} Throws an error if failed to set the button status
     */
    public setMacroButtonStatus = (buttonIndex: number, value: number, bitmode: number = MacroButtonModes.DEFAULT): void => {
        const result = libVM.VBVMR_MacroButton_SetStatus(buttonIndex, value, bitmode);
        if (result !== 0) {
            throw new Error(`Failed to set macro button ${buttonIndex} status`);
        }
    };

    /**
     * Checks whether properties has been changed and calls all event listeners
     */
    private checkPropertyChange = () => {
        let hasChanges = false;

        if (this.isParametersDirty() === 1) {
            hasChanges = true;
        }

        if (this.isMacroButtonDirty() === 1) {
            hasChanges = true;
        }

        if (hasChanges) {
            for (const eventListener of this.eventPool) {
                eventListener();
            }
        }
    };

    /**
     * Gets installed voicemeeter type.
     * Means Voicemeeter(normal,banana,potato)
     */
    private getVoicemeeterType = (): VoiceMeeterTypes => {
        const typePtr = [0];
        if (libVM.VBVMR_GetVoicemeeterType(typePtr) !== 0) {
            throw new Error("running failed");
        }

        switch (typePtr[0]) {
            case 1: {
                // Voicemeeter
                return "voicemeeter";
            }
            case 2: {
                // Voicemeeter Banana
                return "voicemeeterBanana";
            }
            case 3: {
                // Voicemeeter Potato
                return "voicemeeterPotato";
            }
            default: {
                throw new Error("Voicemeeter seems not to be installed");
            }
        }
    };

    /**
     * Returns the installed voicemeeter version
     */
    private getVoicemeeterVersion = () => {
        const versionPtr = [0];
        if (libVM.VBVMR_GetVoicemeeterVersion(versionPtr) !== 0) {
            throw new Error("running failed");
        }
        // For info on this see: https://github.com/mirror/equalizerapo/blob/53d885f7f1a097b457e17a5206b7d60f647877a8/VoicemeeterClient/VoicemeeterRemote.h#L122-L135
        const version =
            `${(versionPtr[0] & 0xff_00_00_00) >> 24}` +
            `.${(versionPtr[0] & 0x00_ff_00_00) >> 16}` +
            `.${(versionPtr[0] & 0x00_00_ff_00) >> 8}` +
            `.${versionPtr[0] & 0x00_00_00_ff}`;
        return version;
    };

    /**
     * Gets a parameter of voicemeeter
     * @param  {'Strip'|'Bus'} selector Strip or Bus
     * @param  {number} index Number of strip or bus
     * @param  {StripProperties|BusProperties} property Property which should be read
     */
    private getParameter = (selector: "Strip" | "Bus", index: number, property: StripProperties | BusProperties) => {
        const parameterName = `${selector}[${index}].${property}`;
        return this.getOption(parameterName);
    };

    /**
     * Sets a parameter of a bus or Strip
     * @param  {'Strip'|'Bus'} selector
     * @param  {number} index Number of strip or bus
     * @param  {StripProperties|BusProperties} property Propertyname which should be changed
     * @param  {any} value Property value
     */
    private setParameter = (
        selector: "Strip" | "Bus",
        index: number,
        property: StripProperties | BusProperties,
        value: any
    ): Promise<any> => {
        if (!this.isConnected) {
            throw new Error("Not connected ");
        }
        const scriptString = `${selector}[${index}].${property}=${value};`;
        return this.setOption(scriptString);
    };
    /**
     * Gets realtime audio level see the VoicemeeterRemote API: [VoicemeeterRemote.h GetLevel](https://github.com/mirror/equalizerapo/blob/7aece1b788fce5aa11873f3842a0d01f7c78454b/VoicemeeterClient/VoicemeeterRemote.h#L284),
     * for more details about the parameters
     * @param {0|1|2|3} type 0 = pre fader input levels. 1 = post fader input levels. 2= post Mute input levels. 3= output levels
     * @param channel audio channel zero based index
     * @returns {float} Current audio level
     */
    public getLevel = (type: 0 | 1 | 2 | 3, channel: number) => {
        const levelPtr = [0];
        libVM.VBVMR_GetLevel(type, channel, levelPtr);
        return levelPtr[0];
    };
}
