/**
 * This file is part of HARMONICARIUM, a web app which allows users to play
 * the Harmonic Series dynamically by changing its fundamental tone in real-time.
 * It is available in its latest version from:
 * https://github.com/IndustrieCreative/Harmonicarium
 * 
 * @license
 * Copyright (C) 2017-2020 by Walter Mantovani (http://armonici.it).
 * Written by Walter Mantovani.
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 * 
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/* globals HUM */

"use strict";

/** The MidiPorts class */
HUM.midi.MidiPorts = class {
    /**
    * @param {HUM.DHC}          dhc  - The DHC instance to which it belongs
    * @param {HUM.midi.MidiHub} midi - The MidiHub instance to which it belongs
    */
    constructor(dhc, midi) {
        /**
        * The DHC instance
        *
        * @member {HUM.DHC}
        */
        this.dhc = dhc;
        /**
        * The MidiHub instance
        *
        * @member {HUM.midi.MidiHub}
        */
        this.midi = midi;
        /*==============================================================================*
         * MAIN MIDI OBJECTS
         *==============================================================================*/
        /**
         * The global MIDIAccess object
         * 
         * @see {@link https://webaudio.github.io/web-midi-api/#MIDIAccess|Web MIDI API specs} for 'MIDIAccess'
         *
         * @member {MIDIAccess}
         */
        // @old icMidi
        this.midiAccess = null;

        /**
         * Namespace for WebMidiLink
         *
         * @member {Object}
         * 
         * @property {HUM.midi.WebMidiLinkIn}                   input   - The WebMidiLinkIn instance
         * @property {Object.<string, HUM.midi.WebMidiLinkOut>} outputs - An array containing all the WebMidiLinkOut instances; the <em>key</em> is the {@link HUM.midi.WebMidiLinkOut#id}
         * @property {number}                                   outQty  - How many WebMidiLinkOut instances must be created (integer)
         */
        this.webMidi = {
            input: new HUM.midi.WebMidiLinkIn(this.dhc, this.midi),
            outputs: {},
            outQty: 3,
        };

        /**
         * The global map of selected MIDI outputs
         *
         * @member {Map.<string, MIDIPort>}
         */
        // @old icSelectedOutputs
        this.selectedOutputs = new Map();

        /**
         * Data structure to keep track of how many ports are available and how many are used.
         *     Just used to inform the user about the current MIDI port situation and give the right advices.
         *
         * @member {Object}
         * 
         * @property {Object} availablePort        - Available ports namespace
         * @property {number} availablePort.input  - Number of available input ports
         * @property {number} availablePort.output - Number of available output ports
         * @property {Object} openPort             - Open ports namespace
         * @property {number} openPort.input       - Number of open input ports
         * @property {number} openPort.output      - Number of open output ports
         */
        // @old icAtLeastOneMidi
        this.atLeastOneMidi = {
            availablePort: {
                input: 0,
                output: 0
            },
            openPort: {
                input: 0,
                output: 0
            }
        };
        /**
         * UI HTML elements
         *
         * @member {Object}
         * 
         * @property {Object.<string, HTMLElement>} fn  - Functional UI elements
         * @property {Object.<string, HTMLElement>} in  - Input UI elements
         * @property {Object.<string, HTMLElement>} out - Output UI elements
         */
        this.uiElements = {
            fn: {
                motPanelClose: document.getElementById(`HTMLf_motPanelClose${dhc.id}`),
                motPanelModal: document.getElementById(`HTMLf_motPanelModal${dhc.id}`),
                motPanelModalShow: document.getElementById(`HTMLf_motPanelModalShow${dhc.id}`),
            },
            in: {

            },
            out: {
                /**
                 * The UI HTML elements that contain the MIDI-IN checkboxes (need to be global ??)
                 *
                 * @type {Object}
                 */
                // @old icHTMLelementInputs
                inputPorts: document.getElementById(`HTMLo_inputPorts${dhc.id}`),
                /**
                 * The UI HTML elements that contain the MIDI-OUT checkboxes (need to be global ??)
                 *
                 * @type {Object}
                 */
                // @old icHTMLelementOutputs
                outputPorts: document.getElementById(`HTMLo_outputPorts${dhc.id}`),
            },
        };

        // Request MIDI Access
        if (navigator.requestMIDIAccess) {
            navigator.requestMIDIAccess().then(this._onMidiInit.bind(this), this._onMidiReject.bind(this));
        } else {
            // If MIDIAccess does not exist
            // @see - https://webaudiodemos.appspot.com/namm/#/11
            this.dhc.harmonicarium.components.backendUtils.eventLog("Unfortunately, your browser does not seem to support Web MIDI API.");
            this._postRequestMIDI();
        }

        // =======================
    } // end class Constructor
    // ===========================

    /**
     * Inintialize the WebMidiLink Output and make accessible the UI modal panel 
     */
    _postRequestMIDI() {
        this._initWebMidiLinkOut();
        // Button to open the MIDI settings
        this.uiElements.fn.motPanelModalShow.addEventListener("click", () => this.openMidiPanel() );
    }

    /**
     * Inintialize the WebMidiLink Output
     */
    _initWebMidiLinkOut() {
        for (let key = 0; key < this.webMidi.outQty; key++) {
            // Create a new WebMidiLink output port
            let id = `webmidilink_out_${key}`;
            let webMidiLinkPort = new HUM.midi.WebMidiLinkOut(key, id, this.dhc, this.midi);
            this.webMidi.outputs[id] = webMidiLinkPort;
            
            // Create the checkbox for the new port
            this.createPortCheckbox(webMidiLinkPort, this.uiElements.out.outputPorts);
            this.portLogger(webMidiLinkPort);
        }
    }

    /**
     * What to do on MIDI Access error, if MIDIAccess exist but there is another kind of problem
     *
     * @see {@link https://webaudio.github.io/web-midi-api/#extensions-to-the-navigator-interface|Web MIDI API specs}
     *
     * @param  {DOMException} error - Possible error
     */
    // @old icOnMidiReject
    _onMidiReject(error) {
        this.dhc.harmonicarium.components.backendUtils.eventLog("Failed to get MIDI access because: " + error);
        this._postRequestMIDI();
    }

    /**
     * What to do on MIDI Access, when MIDI is initialized
     *
     * @param {MIDIAccess} midiAccess - The MIDIAccess object; see the {@link https://webaudio.github.io/web-midi-api/#MIDIAccess|Web MIDI API specs}
     */
    // @old icOnMidiInit
    _onMidiInit(midiAccess) {
        this.dhc.harmonicarium.components.backendUtils.eventLog("Luckily, your browser seems to support the Web MIDI API!");
        // Store in the global ??(in real usage, would probably keep in an object instance)??
        this.midiAccess = midiAccess;
        // UI INITIALIZATION
        // Create for the first time the HTML Input and Output ports selection boxes
        // Log the available ports on the Event Log
        this.midiAccess.inputs.forEach((value) => {
            this.createPortCheckbox(value, this.uiElements.out.inputPorts);
            this.portLogger(value);
        });
        this.midiAccess.outputs.forEach((value) => {
            this.createPortCheckbox(value, this.uiElements.out.outputPorts);
            this.portLogger(value);
        });

        // When the state or an attribute of any port changes
        // Execute the Midi State Refresh function with the Event as argument
        this.midiAccess.onstatechange = (e) => this.midiStateRefresh(e);
        // Check the MIDI-IN ports available
        this.checkAtLeastOneMidi("io", false);

        this._postRequestMIDI();
    }

    /*==============================================================================*
     * MIDI PORTS HW/UI HANDLING 
     *==============================================================================*/
    /**
     * Open the MIDI I/O modal panel on UI
     */
     // @old icOpenMidiPanel
    openMidiPanel() {
        // Get the modal element
        let modal = this.uiElements.fn.motPanelModal;
        // Get the <span> element that closes the modal element
        let close = this.uiElements.fn.motPanelClose;
        
        // let span = document.getElementsByClassName("modalOverlay_close")[0];
        // When the user clicks the button, open the modal element
        modal.style.display = "block";
        // When the user clicks on <span> (x), close the modal element
        close.onclick = () => {
            modal.style.display = "none";
        };
        // When the user clicks anywhere outside of the modal element, close it
        window.onclick = (event) => {
            if (event.target == modal) {
                modal.style.display = "none";
            }
        };
    }

    /**
     * Log on the Event Log the informations about a single input or output port
     *
     * @param {MIDIPort} midiPort - The MIDI port
     */
    // @old icPortLogger
    portLogger(midiPort) {
        // let icPortInfos = icPort.state + " " + icPort.type + " port | id: " + icPort.id + " | name: " + icPort.name + " | manufacturer: " + icPort.manufacturer + " | version:" + icPort.version + " | connection: " + icPort.connection;
        let portInfos = midiPort.type + " port: " + midiPort.name + " | " + midiPort.state + ": " + midiPort.connection;
        this.dhc.harmonicarium.components.backendUtils.eventLog(portInfos);
    }

    /**
     * Create a single checkbox and its label in a div.
     * Assign the onclick event to the the checkbox.
     *
     * @param {MIDIPort}    midiPort    - The MIDI port; see the {@link https://webaudio.github.io/web-midi-api/#MIDIPort|Web MIDI API specs}
     * @param {HTMLElement} htmlElement - The 'div' containers of the ports on UI ('this.uiElements.out.inputPorts' or 'this.uiElements.out.outputPorts')
     */
    // @old icCreatePortCheckbox
    createPortCheckbox(midiPort, htmlElement) {
        let dhcID = this.dhc.id;
        // DIV
        // Create the <div> container
        let portSelectorDiv = document.createElement('div');
        // <div> container: set ID
        portSelectorDiv.id = `midiPortDiv${midiPort.id}_${dhcID}`;
        // <div> container: set CLASS
        portSelectorDiv.className = 'IOcheckbox';
        // Insert the <div> container into the htmlElement
        htmlElement.appendChild(portSelectorDiv);

        // INPUT
        // Create the <input> element
        let portSelectorInput = document.createElement('input');
        // <input> element: set CHECKBOX TYPE
        portSelectorInput.type = 'checkbox';
        // <input> checkbox: set VALUE
        portSelectorInput.value = midiPort.id;
        // <input> checkbox: set CLASS
        portSelectorInput.className = midiPort.type;
        // <input> checkbox: set NAME (useful to avoid port loops in this.portSelect())
        portSelectorInput.name = midiPort.name;
        // <input> checkbox: set the onclick function
        portSelectorInput.addEventListener("click", (e) => this.portSelect(e) );
        // <input> checkbox: set ID
        portSelectorInput.id = midiPort.id + "_" + dhcID;
        // Insert the <input> checkbox into the <div> container
        portSelectorDiv.appendChild(portSelectorInput);

        // LABEL
        // Create the <label> element
        let portSelectorLabel = document.createElement("label");
        // <label> element: set FOR
        portSelectorLabel.setAttribute("for", midiPort.id + "_" + dhcID);
        // <label> element: set TEXT CONTENT
        portSelectorLabel.innerHTML = midiPort.name;
        // Insert the <label> element into the <div> container
        portSelectorDiv.appendChild(portSelectorLabel);
    }

    /**
     * If an Input port has been selected, open that port and start to listen from it.
     * If an Output port has been selected, start to send MIDI messages to that port.
     * The function is invoked when a HTML checkbox has been clicked.
     * 
     * @param {Event}   event                - OnClick event on the MIDI I/O Ports checkboxes
     * @param {Object}  event.target         - The event's target HTML element (could be just a namespace)
     * @param {boolean} event.target.checked - Checkbox checked or not
     */
    // @old icPortSelect
    portSelect(event) {
        let elem = event.target;
        let portID = elem.value;
        // let alterPortType = elem.className === "input" ? "outputs" : "inputs";
        // If the port is selected
        if (elem.checked) {
            switch (elem.className) {
                // If the port is an input, open that port and listen from it
                case "input":
                    this.midiAccess.inputs.get(portID).onmidimessage = (midievent) => this.midi.in.midiMessageReceived(midievent);
                    this.atLeastOneMidi.openPort.input++;
                    break;
                // If the port is an output, map it on the this.selectedOutputs global object/map
                case "output":
                    // WebMidiLink MIDI ports
                    if (portID.indexOf('webmidilink') > -1) {
                        this.selectedOutputs.set(portID, this.webMidi.outputs[portID]);
                        this.webMidi.outputs[portID].openPort();
                    // System MIDI ports
                    } else {
                        this.selectedOutputs.set(this.midiAccess.outputs.get(portID).id, this.midiAccess.outputs.get(portID));
                    }
                    this.atLeastOneMidi.openPort.output++;
                    this.midi.out.updateMidiOutUI();
                    break;
                // Debug
                default:
                    console.log("The 'class' attribute of the I/O checkbox on the UI has an unexpected value: " + elem.className);
                    break;
            }
        // If the port in unselected
        } else {
            switch (elem.className) {
                // If the port is an input, close that port
                case "input":
                    // << ALL NOTES OFF TO DHC >>
                    this.dhc.panic();
                    // Close port
                    this.midiAccess.inputs.get(portID).close();
                    this.atLeastOneMidi.openPort.input--;
                    break;
                // If the port is an output, remove it on the this.selectedOutputs global object/map
                case "output":
                    // << ALL NOTES OFF TO MIDI OUT >>
                    this.midi.out.allNotesOffPort(portID, 'soft');
                    // WebMidiLink MIDI ports
                    if (portID.indexOf('webmidilink') > -1) {
                        this.webMidi.outputs[portID].closePort();
                        this.selectedOutputs.delete(portID);
                    // System MIDI ports
                    } else {
                        this.selectedOutputs.delete(this.midiAccess.outputs.get(portID).id);
                    }
                    this.atLeastOneMidi.openPort.output--;
                    this.midi.out.updateMidiOutUI();
                    break;
                // Debug
                default:
                    console.log("The 'class' attribute of the I/O checkbox on the UI has an unexpected value: " + elem.className);
                    break;
            }
        }
        // Prevent set input<>output on the same port in order to avoid MIDI loops
        // this.midiAccess[alterPortType].forEach((value, key, map) => {
        //     if (value.name === elem.name) {
        //         // Disable the other chackbox with same Port Name
        //         document.getElementById(key).disabled = elem.checked;
        //     }
        // });
    }

    /**
     * Midi State Refresh for hot (un)plugging
     * Update the informations about the state of the MIDI ports/devices in the HTML UI
     *
     * @param  {MIDIConnectionEvent} event      - Event from MidiAccess.onstatechange; see the {@link https://webaudio.github.io/web-midi-api/#MIDIConnectionEvent|Web MIDI API specs}
     * @param  {MIDIPort}            event.port - The MIDI Port; see the {@link https://webaudio.github.io/web-midi-api/#MIDIPort|Web MIDI API specs}
     */
    // @old icMidiStateRefresh
    midiStateRefresh(event) {
        let dhcID = this.dhc.id,
            midiPort = event.port,
            htmlElement = null,
            inputPortSelectorDiv = document.getElementById(`midiPortDiv${midiPort.id}_${dhcID}`),
            inputPortSelector = document.getElementById(midiPort.id + "_" + dhcID);
        // Print information about the (dis)connected MIDI controller
        this.portLogger(midiPort);
        switch (midiPort.state) {
            // If the port that generated the event is disconnected: delete port
            case "disconnected":
                if (midiPort.type === "input") {
                    // << ALL NOTES OFF TO DHC >>
                    this.dhc.panic();
                    if (inputPortSelector.checked === true) {
                        this.atLeastOneMidi.openPort.input--;
                    }
                    // Check the available MIDI-IN ports
                    this.checkAtLeastOneMidi("i", false);
                } else if (midiPort.type === "output") {
                    if (inputPortSelector.checked === true) {
                        this.atLeastOneMidi.openPort.output--;
                    }
                    // Check the available MIDI-OUT ports
                    this.checkAtLeastOneMidi("o", false);
                }
                // Remove the checkbox
                inputPortSelectorDiv.remove();
                break;
            // If the port that generated the event is connected: add port
            case "connected":
                // If the checkbox does not exist already
                if (!inputPortSelectorDiv) {
                    switch (midiPort.type) {
                        // If the port is an input
                        case "input":
                            // Select the element containing the input checkboxes
                            htmlElement = this.uiElements.out.inputPorts;
                            break;
                        // If the port is an output
                        case "output":
                            // Select the element containing the output checkboxes
                            htmlElement = this.uiElements.out.outputPorts;
                            break;
                        // Debug
                        default:
                            console.log("The '.type' of the port has an unexpected value: " + midiPort.type);
                            break;
                    }
                    // Add the new checkbox to the HTML UI using the global function
                    this.createPortCheckbox(midiPort, htmlElement);
                }
                break;
            // Debug
            default:
                console.log("The '.state' of the port has an unexpected value: " + midiPort.state);
                break;
        }
    }
    /**
     * Check if there is at least one available MIDI port (Input or Output)
     * Check if there is at least one open MIDI port (Input or Output)
     *
     * @param  {('i'|'o'|'io')}  xPut   - Check for Input ('i'), Output port ('o') or both ('io')
     * @param  {boolean}         isOpen - false: Check if there are ports selected and used by the user
     *                                    true:  Check if there are available ports
     */
    // @old icCheckAtLeastOneMidi
     checkAtLeastOneMidi(xPut, isOpen) {
        this.atLeastOneMidi.availablePort.input = this.midiAccess.inputs.size;
        this.atLeastOneMidi.availablePort.output = this.midiAccess.outputs.size;
        let msg = "";
        if (isOpen === false) {
            switch (xPut) {
                case "io":
                    if (this.atLeastOneMidi.availablePort.input === 0 && this.atLeastOneMidi.availablePort.output === 0) {
                        msg = "NO MIDI-IN/OUT PORTS AVAILABLE.\nTo best use this software, connect:\n– MIDI Controller >> incoming MIDI port\n– MIDI Instrument >> outgoing MIDI port";
                        this.dhc.harmonicarium.components.backendUtils.eventLog(msg);
                        // alert(msg);
                        break;
                    }
                    /* falls through */
                case "i":
                    if (this.atLeastOneMidi.availablePort.input === 0) {
                        msg = "NO MIDI-IN PORTS AVAILABLE!\nTo best use this software, an Input MIDI Controller is recommended.\nIn order to connect a MIDI Controller, at least one MIDI-IN port is required.";
                        this.dhc.harmonicarium.components.backendUtils.eventLog(msg);
                        // alert(msg);
                        break;
                    }
                    /* falls through */
                case "o":
                    if (this.atLeastOneMidi.availablePort.output === 0) {
                        msg = "NO MIDI-OUT PORTS AVAILABLE!\nIn order to retune and play a MIDI Instrument, an Output MIDI Port is required.";
                        this.dhc.harmonicarium.components.backendUtils.eventLog(msg);
                        // alert(msg);
                    }
                    break;
            }
        // @todo - isOpen NOT USED YET
        //Use isOpen to check if the user selected an output port
        } else if (isOpen === true) {
            switch (xPut) {
                case "i":
                    if (this.atLeastOneMidi.openPort.input === 0) {
                        msg = "You have to select at least one MIDI-IN port.";
                        this.dhc.harmonicarium.components.backendUtils.eventLog(msg);
                        alert(msg);
                    }
                    break;
                case "o":
                    if (this.atLeastOneMidi.openPort.output === 0) {
                        msg = "You have to select at least one MIDI-OUT port.";
                        this.dhc.harmonicarium.components.backendUtils.eventLog(msg);
                        alert(msg);
                    }
                    break;
                case "io":
                    if (this.atLeastOneMidi.openPort.input === 0 && this.atLeastOneMidi.openPort.output === 0) {
                        msg = "You have to select at least one MIDI-IN and one MIDI-OUT port.";
                        this.dhc.harmonicarium.components.backendUtils.eventLog(msg);
                        alert(msg);
                    }
                    break;
            }
        }
    }
};