// Copyright (c) 2022 Robin Davies
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
// the Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


import {PiPedalArgumentError} from "./PiPedalError";
import Units from './Units';

interface Deserializable<T> {
    deserialize(input: any): T;
}


export class  Port implements Deserializable<Port> {
    deserialize(input: any): Port {
        this.port_index = input.port_index;
        this.symbol = input.symbol;
        this.name = input.name;
        this.min_value = input.min_value;
        this.max_value = input.max_value;
        this.default_value = input.default_value;
        this.scale_points = ScalePoint.deserialize_array(input.scale_points);
        this.is_input = input.is_input;
        this.is_output = input.is_output;
        this.is_control_port = input.is_control_port;
        this.is_audio_port = input.is_audio_port;
        this.is_atom_port = input.is_atom_port;
        this.is_valid = input.is_valid;
        this.supports_midi = input.supports_midi;
        this.supports_time_position = input.supports_time_position;
        this.port_group = input.port_group;
        return this;
    }

    static EmptyPorts: Port[] = [];

    static deserialize_array(input: any): Port[] {
        let result: Port[] = [];
        for (let i = 0; i < input.length; ++i)
        {
            result[i] = new Port().deserialize(input[i]);
        }
        return result;
    }
    port_index:      number = -1;
    symbol:          string = "";
    name:            string = "";
    min_value:       number = 0;
    max_value:       number = 1;
    default_value:   number = 0.5;
    scale_points:    ScalePoint[] = [];
    is_input:        boolean = false;
    is_output:       boolean = false
    is_control_port: boolean = false;
    is_audio_port:   boolean = false;
    is_atom_port:    boolean = false;
    is_valid:        boolean = false;
    supports_midi:   boolean = false;
    supports_time_position: boolean = false;
    port_group:      string = "";
}

export class PortGroup {
    deserialize(input: any): PortGroup {
        this.symbol = input.symbol;
        this.name = input.name;
        return this;
    }
    static deserialize_array(input: any) : PortGroup[] {
        let result: PortGroup[] = [];
        for (let i = 0; i < input.length; ++i)
        {
            result.push(new PortGroup().deserialize(input[i]));
        }
        return result;
    }

    symbol: string = "";
    name: string = "";
};

export class  Lv2Plugin implements Deserializable<Lv2Plugin> {
    deserialize(input: any): Lv2Plugin
    {
        this.uri = input.uri;
        this.name = input.name;
        this.plugin_class = input.plugin_class;
        this.supported_features = input.supported_features;
        this.required_features = input.required_features;
        this.optional_features = input.optional_features;
        this.author_name = input.author_name;
        this.author_homepage = input.author_homepage;
        this.comment = input.comment;
        this.ports=  Port.deserialize_array(input.ports);
        this.port_groups = PortGroup.deserialize_array(input.port_groups);
        return this;
    }
    static EmptyFeatures: string[] = [];

    uri:                string = "";
    name:               string = "";
    plugin_class:       string = "";
    supported_features: string[] = Lv2Plugin.EmptyFeatures;
    required_features:  string[] = Lv2Plugin.EmptyFeatures;
    optional_features:  string[] = Lv2Plugin.EmptyFeatures;
    author_name:        string = "";
    author_homepage:    string = "";
    comment:            string = "";
    ports:              Port[] = Port.EmptyPorts;
    port_groups: PortGroup[] = [];
}


export class  ScalePoint implements Deserializable<ScalePoint> {
    deserialize(input: any): ScalePoint {
        this.value = input.value;
        this.label = input.label;
        return this;
    }
    static deserialize_array(input: any): ScalePoint[]
    {
        let result: ScalePoint[] = [];
        for (let i = 0; i < input.length; ++i)
        {
            result[i] = new ScalePoint().deserialize(input[i]);
        }
        return result;
    }
    value: number = 0;
    label: string = "";
}

export enum PluginType {
    // Reserved types used in pedalboards.
    None="",
    InvalidPlugin= "InvalidPlugin",
    
    Plugin = "Plugin",
    AllpassPlugin = "AllpassPlugin",
    AmplifierPlugin = "AmplifierPlugin",
    AnalyserPlugin = "AnalyserPlugin",
    BandpassPlugin = "BandpassPlugin",
    ChorusPlugin = "ChorusPlugin",
    CombPlugin = "CombPlugin",
    CompressorPlugin = "CompressorPlugin",
    ConstantPlugin = "ConstantPlugin",
    ConverterPlugin = "ConverterPlugin",
    DelayPlugin = "DelayPlugin",
    DistortionPlugin = "DistortionPlugin",
    DynamicsPlugin = "DynamicsPlugin",
    EQPlugin = "EQPlugin",
    EnvelopePlugin = "EnvelopePlugin",
    ExpanderPlugin = "ExpanderPlugin",
    FilterPlugin = "FilterPlugin",
    FlangerPlugin = "FlangerPlugin",
    FunctionPlugin = "FunctionPlugin",
    GatePlugin = "GatePlugin",
    GeneratorPlugin = "GeneratorPlugin",
    HighpassPlugin = "HighpassPlugin",
    InstrumentPlugin = "InstrumentPlugin",
    LimiterPlugin = "LimiterPlugin",
    LowpassPlugin = "LowpassPlugin",
    MixerPlugin = "MixerPlugin",
    ModulatorPlugin = "ModulatorPlugin",
    MultiEQPlugin = "MultiEQPlugin",
    OscillatorPlugin = "OscillatorPlugin",
    ParaEQPlugin = "ParaEQPlugin",
    PhaserPlugin = "PhaserPlugin",
    PitchPlugin = "PitchPlugin",
    ReverbPlugin = "ReverbPlugin",
    SimulatorPlugin = "SimulatorPlugin",
    SpatialPlugin = "SpatialPlugin",
    SpectralPlugin = "SpectralPlugin",
    UtilityPlugin = "UtilityPlugin",
    WaveshaperPlugin = "WaveshaperPlugin",

}

export enum ControlType {
    Dial,
    OnOffSwitch,
    Toggle,
    Select
}

export class  UiControl implements Deserializable<UiControl> {
    deserialize(input: any): UiControl
    {
        this.symbol = input.symbol;
        this.name = input.name;
        this.index = input.index;
        this.min_value = input.min_value;
        this.max_value = input.max_value;
        this.default_value = input.default_value;
        this.is_logarithmic = input.is_logarithmic;
        this.display_priority = input.display_priority;
        this.range_steps = input.range_steps;
        this.integer_property = input.integer_property;
        this.enumeration_property = input.enumeration_property;
        this.toggled_property = input.toggled_property;
        this.trigger = input.trigger;
        this.not_on_gui = input.not_on_gui;
        this.scale_points = ScalePoint.deserialize_array(input.scale_points);
        this.port_group = input.port_group;
        this.units = input.units as Units;

        this.controlType = ControlType.Dial;
        if (this.enumeration_property && this.scale_points.length === 2)
        {
            this.controlType = ControlType.Toggle;
        } else {
            if (this.min_value === 0 && this.max_value === 1)
            {
                if (this.toggled_property || this.integer_property)
                {
                    this.controlType = ControlType.OnOffSwitch;
                }
            }
        }
        if (this.controlType === ControlType.Dial && this.enumeration_property)
        {
            this.controlType = ControlType.Select;
        }

        return this;

    }

    controlType: ControlType = ControlType.Dial; // non-serializable.

    static deserialize_array(input: any): UiControl[] {
        let result: UiControl[] = [];
        for (let i = 0; i < input.length; ++i)
        {
            result[i] = new UiControl().deserialize(input[i]);
        }
        return result;
    }
    symbol: string = "";
    name:   string = "";
    index: number = -1;
    min_value: number = 0;
    max_value: number = 1;
    default_value:number = 0.5;
    is_logarithmic: boolean = false;
    display_priority: number = -1;
    range_steps: number = 0;
    integer_property:boolean = false;
    enumeration_property: boolean = false;
    trigger: boolean = false;
    not_on_gui: boolean = false;
    toggled_property: boolean = false;
    scale_points: ScalePoint[] = [];
    port_group: string = "";
    units: Units = Units.none;

    // Return the value of the closest scale_point.
    clampSelectValue(value: number): number{
        if (this.scale_points.length !== 0)
        {
            let minError = 1.0E100;
            let bestValue = value;
            for (let i = 0; i < this.scale_points.length; ++i)
            {
                let error = Math.abs(this.scale_points[i].value-value);
                if (error < minError)
                {
                    minError = error;
                    bestValue = this.scale_points[i].value;
                }
            }
            return bestValue;
        } else {
            return value;
        }
    }
    isOnOffSwitch() : boolean {
        return this.controlType === ControlType.OnOffSwitch;
    }

    isAbToggle(): boolean {
        return this.controlType === ControlType.Toggle;
    }
    isSelect() : boolean {
        return this.controlType === ControlType.Select;
    }

    isDial() : boolean {
        return this.controlType === ControlType.Dial;
    }

    valueToRange(value: number): number {
        if (this.toggled_property) return value === 0 ? 0: 1;

        if (this.integer_property || this.enumeration_property) {
            value = Math.round(value);
        }
        let range = (value - this.min_value) / (this.max_value - this.min_value);
        if (range > 1) range = 1;
        if (range < 0) range = 0;

        if (this.range_steps !== 0) {
            range = Math.round(range*this.range_steps)/this.range_steps;
        }

        return range;
    }

    rangeToValue(range: number) : number {
        if (range < 0) range = 0;
        if (range > 1) range = 1;

        if (this.toggled_property) return range === 0? 0: 1;
        if (this.range_steps !== 0) {
            range = Math.round(range * this.range_steps) / this.range_steps;
        }
        let value = range * (this.max_value - this.min_value) + this.min_value;
        if (this.integer_property || this.enumeration_property) {
            value = Math.round(value);
        }
        return value;
    }
    clampValue(value: number): number {
        return this.rangeToValue(this.valueToRange(value));
    }

    formatDisplayValue(value: number): string {
        if (this.integer_property || this.enumeration_property) {
            value = Math.round(value);
        }

        for (let i = 0; i < this.scale_points.length; ++i)
        {
            let scalePoint = this.scale_points[i];
            if (scalePoint.value === value)
            {
                return scalePoint.label;
            }
        }
        let text = this.formatShortValue(value);

        switch (this.units) {
            case Units.bpm:
                text += "bpm";
                break;
            case Units.cent:
                text += "cents";
                break;
            case Units.cm:
                text += "cm";
                break;
            case Units.db:
                text += "dB";
                break;
            case Units.hz:
                text += "Hz";
                break;
            case Units.khz:
                text += "kHz";
                break;
            case Units.km:
                text += "km";
                break;
            case Units.m:
                text += "m";
                break;
            case Units.mhz:
                text += "MHz";
                break;
            case Units.min:
                text += "min";
                break;
            case Units.ms:
                text += "ms";
                break;
            case Units.pc:
                text += "%";
                break;
            case Units.s:
                text += "s";
                break;
            // Midinote: not handled.
            // semitone12TET not handled.



        }
        return text;
    }
    formatShortValue(value: number): string
    {
        if (this.enumeration_property) {
            for (let i = 0; i < this.scale_points.length; ++i) {
                let scale_point = this.scale_points[i];
                if (scale_point.value === value) {
                    return scale_point.label;
                }
            }
            return "#invalid";
        } else if (this.integer_property) {
            return value.toFixed(0);
        } else {
            if (value >= 100 || value <= -100) {
                return value.toFixed(0);
            }
            if (value >= 10 || value <= -10) {
                return value.toFixed(1);
            }
            return value.toFixed(2);
        }
    }

    
}

export class UiPlugin implements Deserializable<UiPlugin> {
    deserialize(input: any): UiPlugin 
    {
        this.uri = input.uri;
        this.name = input.name;
        this.plugin_type = input.plugin_type as PluginType;
        this.plugin_display_type = input.plugin_display_type;
        this.author_name = input.author_name;
        this.author_homepage = input.author_homepage;
        this.audio_inputs = input.audio_inputs;
        this.audio_outputs = input.audio_outputs;
        this.has_midi_input = input.has_midi_input;
        this.has_midi_output = input.has_midi_output;
        this.description = input.description;
        this.controls = UiControl.deserialize_array(input.controls);
        this.port_groups = PortGroup.deserialize_array(input.port_groups);
        return this;

    }
    static deserialize_array(input: any): UiPlugin[] {
        let result: UiPlugin[] = [];
        for (let i = 0; i < input.length; ++i)
        {
            result[i] = new UiPlugin().deserialize(input[i]);
        }
        return result;
    }
    getControl(key: string): UiControl | undefined {
        for (let i = 0; i < this.controls.length; ++i)
        {
            let control = this.controls[i];
            if (control.symbol === key)
            {
                return control;
            }
        } 
        return undefined;

    }
    getPortGroup(symbol: string): PortGroup
    {
        for (let i = 0; i < this.port_groups.length; ++i)
        {
            let port_group = this.port_groups[i];
            if (port_group.symbol === symbol)
            {
                return port_group;
            }
        }
        throw new PiPedalArgumentError("Port group not found.");
    }

    uri:                 string = "";
    name:                string = "";
    plugin_type:         PluginType = PluginType.InvalidPlugin;
    plugin_display_type: string = "";
    author_name:         string = "";
    author_homepage:     string = "";
    audio_inputs:        number = 0;
    audio_outputs:       number = 0;
    has_midi_input:      number = 0;
    has_midi_output:     number = 0;
    description: string  = "";
    controls:            UiControl[] = [];
    port_groups:  PortGroup[] = [];
}

