/**
 * 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 MidiOut class
 *     Prepare the MIDI-OUT message and send them to the MIDI-OUT ports.
 */
HUM.midi.MidiOut = class MidiOut {
    /**
    * @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;
        /**
         * Temporary MIDI-OUT ports settings cache; <em>keys</em> are the output port IDs
         * (a little DB where to store the user's settings about the port)
         *
         * @member {Object.<string, HUM.midi.MidiOut#InstrumentSettings>}
         */
        // @old icMIDIoutSettings
        this.settings = {};
        /**
         * Get the "MIDI-OUT Tuning" HTML element and store to global
         *
         * @member {HTMLElement}
         */
        // @old icHTMLmotModalContent
        this.htmlMotModalContent = document.getElementById("HTMLf_motPanelContent"+dhc.id);

        // Tell to the DHC that a new app is using it
        this.dhc.registerApp(this, 'updatesFromDHC', 1);

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

    /**
     * Manage and route an incoming message
     * @param {HUM.DHCmsg} msg - The incoming message
     */
    updatesFromDHC(msg) {

        if (msg.cmd === 'panic') {
            this.allNotesOff('soft');
        }

        if (msg.cmd === 'update') {
            if (msg.type === 'ft') {

                this.updateMIDInoteON('ft');

            } else if (msg.type === 'ht') {

                this.updateMIDInoteON('ht');

            } else if (msg.type === 'ctrlmap') {

            }

        } else if (msg.cmd === 'tone-on') {
            if (msg.type === 'ft') {

                this.midiOut(msg.ctrlNum, msg.xtNum, msg.velocity, 1, "ft", msg.tsnap);
                
                // if (this.dhc.settings.ht.curr_ft !== msg.xtNum) {
                //     this.updateMIDInoteON("ht");
                // }

            } else if (msg.type === 'ht') {
            
                if (msg.xtNum !== 0) {
                    this.midiOut(msg.ctrlNum, msg.xtNum, msg.velocity, 1, "ht", msg.tsnap);
                }
            
            }

        } else if (msg.cmd === 'tone-off') {
            if (msg.type === 'ft') {

                this.midiOut(msg.ctrlNum, msg.xtNum, msg.velocity, 0, "ft");

            } else if (msg.type === 'ht') {

                if (msg.xtNum !== 0) {
                    this.midiOut(msg.ctrlNum, msg.xtNum, msg.velocity, 0, "ht");
                }
            }

        }
    }

    /**
     * Update the MIDI-OUT Tuning UI
     * Create the UI to manage the MIDI port channels assignment
     */
     // @old icUpdateMOT
    updateMidiOutUI() {
        let dhcID = this.dhc.id;
        // Init the container
        this.htmlMotModalContent.innerHTML = "";
        this.midi.port.selectedOutputs.forEach((value, key) => {
            // If it's a new port in this browser section
            if (!this.settings[key]) {
                // Initialize the port with default settings
                this.settings[key] = new this.InstrumentSettings();
                // this.settings[key] = JSON.parse(JSON.stringify(this.dhc.settings.instrument));
            }
            // **CONTAINER**
            // Create the main <div> container
            let divPort = document.createElement('div');
            let divPortSettings = document.createElement('div');
            let tablePort = document.createElement('table');
            let tablePortRowHeader = document.createElement('tr');
            let tablePortRowHeaderBottom = document.createElement('tr');
            let tablePortRowFT = document.createElement('tr');
            let tablePortRowHT = document.createElement('tr');
            let tdLabFT = document.createElement('th');
            let tdLabHT = document.createElement('th');
            // Set ids
            divPort.id = "HTMLf_motPort" + key + "_" + dhcID;
            // Set classes
            divPort.className = "mot-port";
            divPortSettings.className = "mot-pb-settings";
            tablePortRowHeader.className = "mot-pb-row1";
            tablePortRowHT.className = "mot-pb-row2";
            tablePortRowFT.className = "mot-pb-row3";
            tablePortRowHeaderBottom.className = "mot-pb-row4";
            tdLabFT.className = tdLabHT.className = "mot-pb-typeHeader";
            // Write the title (Port Name)
            divPort.innerHTML = "<h2>" + value.name + "</h2>";
            tdLabFT.innerHTML = "FTs";
            tdLabHT.innerHTML = "HTs";
            tdLabFT.rowSpan = "2";
            tdLabHT.rowSpan = "2";
            // First Column
            tablePortRowHeader.appendChild(tdLabHT);
            tablePortRowFT.appendChild(tdLabFT);
            // **CHANNEL CHECKBOXES**
            // Create the single channel checkboxes
            for (let ch = 0; ch < 16; ch++) {
                // Create the HTML elements
                let tdChFT = document.createElement('td');
                let tdChHT = document.createElement('td');
                let tdLabChFT = document.createElement('td');
                let tdLabChHT = document.createElement('td');
                let checkboxFT = document.createElement('input');
                let checkboxHT = document.createElement('input');
                let labelFT = document.createElement("label");
                let labelHT = document.createElement("label");
                // Set the htmlElements
                tdChFT.className = "mot-pb-portChannel_ft";
                tdChHT.className = "mot-pb-portChannel_ht";
                labelFT.setAttribute("for", key + "_" + ch + "_ft" + dhcID);
                labelHT.setAttribute("for", key + "_" + ch + "_ht" + dhcID);
                labelFT.innerHTML = labelHT.innerHTML = ch + 1  ;
                checkboxFT.type = checkboxHT.type = 'checkbox';
                /**
                 * A JSON string containing data about the assignment of a MIDI Channel useful on DOM events.
                 * 
                 * @typedef {string} ChanAssignment
                 *
                 * @property {string}   port - MIDI port
                 * @property {midichan} chan - MIDI channel on that port
                 * @property {tonetype} fn   - Tone type (FT or HT)
                 * @property {tonetype} not  - The opposite tone type (HT or FT) ... don't ask ;)
                 */
                checkboxFT.value = '{ "port": "' + key + '", "chan": '+ ch + ', "fn": "ft", "not": "ht" }';
                checkboxHT.value = '{ "port": "' + key + '", "chan": '+ ch + ', "fn": "ht", "not": "ft" }';
                checkboxFT.name = checkboxHT.name = key + "_" + ch;
                checkboxFT.addEventListener("click", (e) => this.chanSelect(e) );
                checkboxHT.addEventListener("click", (e) => this.chanSelect(e) );
                checkboxFT.id = key + "_" + ch + "_ft"+ "_" + dhcID;
                checkboxHT.id = key + "_" + ch + "_ht"+ "_" + dhcID;
                if (ch === 9) {
                    tdChFT.className = "mot-pb-portChannel_ft gmCh10";
                    tdChHT.className = "mot-pb-portChannel_ht gmCh10";
                    tdLabChFT.className = "gmCh10";
                    tdLabChHT.className = "gmCh10";
                }
                //Inputs and labels in cells
                tdLabChHT.appendChild(labelHT);
                tdLabChFT.appendChild(labelFT);
                tdChHT.appendChild(checkboxHT);
                tdChFT.appendChild(checkboxFT);
                // Cells in rows
                tablePortRowHeader.appendChild(tdLabChHT);
                tablePortRowHeaderBottom.appendChild(tdLabChFT);
                tablePortRowHT.appendChild(tdChHT);
                tablePortRowFT.appendChild(tdChFT);
                // Check the checkboxes if the channel is used
                if (this.settings[key].pb.channels.ft.used.indexOf(ch) > -1) {
                    checkboxFT.checked = true;
                }
                if (this.settings[key].pb.channels.ht.used.indexOf(ch) > -1) {
                    checkboxHT.checked = true;
                }
            }
            // **PORT PARAMETERS**
            // Create the HTML elements
            let tdPortPBrangeHeader = document.createElement('th');
            let tdPortPBdelayHeader = document.createElement('th');
            let tdPortPBrangeFT = document.createElement('td');
            let tdPortPBrangeHT = document.createElement('td');
            let tdPortPBdelayFT = document.createElement('td');
            let tdPortPBdelayHT = document.createElement('td');
            let btnRangeFT = document.createElement('button');
            let btnRangeHT = document.createElement('button');
            let inputRangeFT = document.createElement('input');
            let inputRangeHT = document.createElement('input');
            let inputDelayFT = document.createElement('input');
            let inputDelayHT = document.createElement('input');
            // Set the htmlElements
            tablePort.className = "invisibleTable";
            tdPortPBrangeFT.className = "mot-pb-rangeFT";
            tdPortPBrangeHT.className = "mot-pb-rangeHT";
            tdPortPBdelayFT.className = "mot-pb-delayFT";
            tdPortPBdelayHT.className = "mot-pb-delayHT";
            tdPortPBrangeHeader.innerHTML = "PitchBend Range";
            tdPortPBdelayHeader.innerHTML = "Delay (ms)";
            btnRangeFT.innerHTML = ">Send";
            btnRangeHT.innerHTML = ">Send";
            inputRangeFT.type = inputRangeHT.type = inputDelayFT.type = inputDelayHT.type = "number";
            inputRangeFT.min = inputRangeHT.min = "1";
            inputRangeFT.max = inputRangeHT.max = "12";
            inputRangeFT.step = inputRangeHT.step = "1";
            inputDelayFT.min = inputDelayHT.min = "0";
            inputDelayFT.max = inputDelayHT.max = "20";
            inputDelayFT.step = inputDelayHT.step = "1";
            inputRangeFT.value = this.settings[key].pb.range.ft;
            inputRangeHT.value = this.settings[key].pb.range.ht;
            inputDelayFT.value = this.settings[key].pb.delay.ft;
            inputDelayHT.value = this.settings[key].pb.delay.ht;
            inputRangeFT.addEventListener("input", (e) => {
                this.settings[key].pb.range.ft = e.target.value;
            });
            inputRangeHT.addEventListener("input", (e) => {
                this.settings[key].pb.range.ht = e.target.value;
            });
            inputDelayFT.addEventListener("input", (e) => {
                this.settings[key].pb.delay.ft = e.target.value;
            });
            inputDelayHT.addEventListener("input", (e) => {
                this.settings[key].pb.delay.ht = e.target.value;
            });
            btnRangeFT.addEventListener("click", () => {
                this.sendMIDIoutPBrange(key, "ft");
            });
            btnRangeHT.addEventListener("click", () => {
                this.sendMIDIoutPBrange(key, "ht");
            });
            inputRangeFT.id = "HTMLi_MIDIoutPortPBrangeFT_" + key + "_" + dhcID;
            inputRangeHT.id = "HTMLi_MIDIoutPortPBrangeHT_" + key + "_" + dhcID;
            inputDelayFT.id = "HTMLi_MIDIoutPortPBdelayFT_" + key + "_" + dhcID;
            inputDelayHT.id = "HTMLi_MIDIoutPortPBdelayHT_" + key + "_" + dhcID;
            // **FINAL COMPOSE**
            // Insert each element into its respective htmlNode
            //Inputs in cells
            tdPortPBrangeFT.appendChild(inputRangeFT);
            tdPortPBrangeFT.appendChild(btnRangeFT);
            tdPortPBdelayFT.appendChild(inputDelayFT);
            tdPortPBrangeHT.appendChild(inputRangeHT);
            tdPortPBrangeHT.appendChild(btnRangeHT);
            tdPortPBdelayHT.appendChild(inputDelayHT);
            // Cells in rows
            tablePortRowHeader.appendChild(tdPortPBrangeHeader);
            tablePortRowHeader.appendChild(tdPortPBdelayHeader);
            tablePortRowFT.appendChild(tdPortPBrangeFT);
            tablePortRowFT.appendChild(tdPortPBdelayFT);
            tablePortRowHT.appendChild(tdPortPBrangeHT);
            tablePortRowHT.appendChild(tdPortPBdelayHT);
            tablePortRowHeaderBottom.appendChild(tdPortPBrangeHeader.cloneNode(true));
            tablePortRowHeaderBottom.appendChild(tdPortPBdelayHeader.cloneNode(true));
            // Final Column
            tablePortRowHeader.appendChild(tdLabHT.cloneNode(true));
            tablePortRowFT.appendChild(tdLabFT.cloneNode(true));
            // Rows in <table>
            tablePort.appendChild(tablePortRowHeader);
            tablePort.appendChild(tablePortRowHT);
            tablePort.appendChild(tablePortRowFT);
            tablePort.appendChild(tablePortRowHeaderBottom);
            // Insert each <div> container into its respective htmlNode
            divPortSettings.appendChild(tablePort);
            divPort.appendChild(divPortSettings);
            this.htmlMotModalContent.appendChild(divPort);
        });
    }

    /**
     * What to do if a MIDI channel is selected in the MIDI-OUT PitchBend Method UI
     *
     * @param {Event}          event              - OnClick event on the MIDI-OUT PitchBend Method channel checkboxes
     * @param {Object}         event.target       - The event's target HTML element (could be just a namespace)
     * @param {ChanAssignment} event.target.value - JSON string containing the informations about the assignment of the channel
     */
    // @old icChanSelect
    chanSelect(event) {
        console.log(event);
        // Parse the JSON from the checkbox value
        let chanSet = JSON.parse(event.target.value);
        // Find and index the current channel in the arrays
        let index = this.settings[chanSet.port].pb.channels[chanSet.fn].used.indexOf(chanSet.chan);
        let indexOther = this.settings[chanSet.port].pb.channels[chanSet.not].used.indexOf(chanSet.chan);
        // Get the other channel checkbox
        let targetOther = document.getElementById(chanSet.port + "_" + chanSet.chan + "_" + chanSet.not + "_" + this.dhc.id);

        // If the checkbox is checked
        if (event.target.checked) {
            // Add current channel to the right array
            if (index === -1) {
                this.settings[chanSet.port].pb.channels[chanSet.fn].used.push(chanSet.chan);
            }
            // Remove the current channel from the other array
            if (indexOther > -1) {
                this.settings[chanSet.port].pb.channels[chanSet.not].used.splice(indexOther, 1);
            }
            // Uncheck the other channel checkbox
            if (targetOther.checked) {
                targetOther.checked = false;
            }
        // If the checkbox is not checked
        } else {
            // << ALL NOTES OFF >>
            this.allNotesOffChannel(chanSet.port, chanSet.chan, 'soft');
            // Remove the current channel from the right array
            if (index > -1) {
                this.settings[chanSet.port].pb.channels[chanSet.fn].used.splice(index, 1);
            }
        }
        this.settings[chanSet.port].pb.channels.ft.used.sort((a, b) => {
            return a - b;
        });
        this.settings[chanSet.port].pb.channels.ht.used.sort((a, b) => {
            return a - b;
        });
        // @todo - Send all Note-OFF and re-init the last channel in order to avoid stuck notes
        this.dhc.harmonicarium.components.backendUtils.eventLog("MIDI multichannel polyphony assignment:\n| Output port = " + this.midi.port.selectedOutputs.get(chanSet.port).name + "\n| " + chanSet.fn.toUpperCase() + " selected channels = " + this.settings[chanSet.port].pb.channels[chanSet.fn].used + "\n| " + chanSet.not.toUpperCase() + " selected channels = " + this.settings[chanSet.port].pb.channels[chanSet.not].used + "\n| ---------------------------------------");
    }
    /**
     * Try turning off all currently instruments' playing notes across all output used MIDI ports.
     * 
     * @param {('soft'|'hard')} mode - The way the command must be executed
     */
    allNotesOff(mode) {
        this.midi.port.selectedOutputs.forEach((port, portID) => {
            this.allNotesOffPort(portID, mode);
        });
    }
    /**
     * Try turning off all currently instruments' playing notes on the given MIDI Channel.
     * 
     * @param {string}          portID - The MIDI output Port id
     * @param {('soft'|'hard')} mode   - The way the command must be executed
     */
    allNotesOffPort(portID, mode) {
        // Get all used ports (used + held)
        let ftUsed = this.settings[portID].pb.channels.ft.used,
            htUsed = this.settings[portID].pb.channels.ht.used,
            ftHeld = [],
            htHeld = [];
        for (let held of Object.values(this.settings[portID].pb.channels.ft.held)) {
            ftHeld.push(held.ch);
        }
        for (let held of Object.values(this.settings[portID].pb.channels.ht.held)) {
            htHeld.push(held);
        }
        let channels = [...ftUsed, ...ftHeld, ...htUsed, ...htHeld];
        
        // Send message
        for (let ch of channels) {
            this.allNotesOffChannel(portID, ch, mode);
        }
        
        // // Restore the channel-management arrays
        // this.settings[portID].pb.channels.ft.used = [...ftUsed, ...ftHeld];
        // this.settings[portID].pb.channels.ht.used = [...htUsed, ...htHeld];
        // this.settings[portID].pb.channels.ft.held = [];
        // this.settings[portID].pb.channels.ht.held = [];
    }
    /**
     * Try turning off all currently instruments' playing notes on the given MIDI Channel.
     * 
     * @param {string}          portID  - The MIDI output Port id
     * @param {midichan}        channel - The Channel on that MIDI Port
     * @param {('soft'|'hard')} mode    - The way the command must be executed
     */
    allNotesOffChannel(portID, channel, mode) {
        let midiOutput = this.midi.port.selectedOutputs.get(portID),
            msg;

        if (mode === 'soft') {
            msg = [0xB0 + channel, 0x7B, 0];
            midiOutput.send(msg);

        } else if (mode === 'hard') {
            for (let mnn = 0; mnn <= 127; mnn++) {
                msg = this.makeMIDIoutNoteMsg(channel, 0, mnn, 0);
                midiOutput.send(msg);
            }
        }

        for (let type of ['ft', 'ht']) {
            for (const [ctrlNum, held] of Object.entries(this.settings[portID].pb.channels[type].held)) {
                if (held.ch === channel) {
                    delete this.settings[portID].pb.channels[type].held[ctrlNum];
                    this.settings[portID].pb.channels[type].used.push(held.ch);
                }
            }
        }

    }

    /**
     * Send the MIDI Pitch Bend Sensitivity (range) message over all the ports of a given Tone Type and MIDI Port
     *
     * @param {string}   portID - The MIDI-OUT port on which to send the message
     * @param {tonetype} type   - If the ports to which the message should be sent are assigned to FTs or HTs
     */
    // @old icSendMIDIoutPBrange
    sendMIDIoutPBrange(portID, type) {
        let midiOutput = this.midi.port.selectedOutputs.get(portID);
        // If the user is not playing
        if (Object.keys(this.settings[portID].pb.channels[type].held).length === 0) {
            let chansEventLog = []; 
            for (let i = 0; i < 16; i++) {
                let outMsgsQueue = [];
                if (this.settings[portID].pb.channels[type].used[i] !== undefined) {
                    let ch = this.settings[portID].pb.channels[type].used[i];
                    chansEventLog.push(ch + 1);
                    outMsgsQueue.push([0xB0 + ch, 0x64, 0x00]); // [CC+CHANNEL, CC RPN LSB (100), value (RPN 00)]
                    outMsgsQueue.push([0xB0 + ch, 0x65, 0x00]);  // [CC+CHANNEL, CC RPN MSB (101), value (RPN 00)]
                    // outMsgsQueue.push([0xB0 + ch, 0x26, 0x00]); // [CC+CHANNEL, CC DATA ENTRY LSB, value (not used)]
                    outMsgsQueue.push([0xB0 + ch, 0x06, this.settings[portID].pb.range[type]]); // [CC+CHANNEL, CC DATA ENTRY MSB, value (1-24 semitones)]
                    outMsgsQueue.push([0xB0 + ch, 0x64, 0x7F]); // [CC+CHANNEL, CC RPN LSB (100), value (null == 127)]
                    outMsgsQueue.push([0xB0 + ch, 0x65, 0x7F]); // [CC+CHANNEL, CC RPN MSB (101), value (null == 127)]
                    for (let msg of outMsgsQueue) {
                        midiOutput.send(msg);
                    }
                }
            }
            this.dhc.harmonicarium.components.backendUtils.eventLog("MIDI Control Change message:\n| Output Port = " + midiOutput.name + "\n| " + type.toUpperCase() + " Channels = " + chansEventLog + "\n| Pitch Bend Sensitivity = " + this.settings[portID].pb.range[type] + " semitones e.t. (12-EDO)\n| ---------------------------------------------------");
        // Else, an alert message
        } else {
            alert("Do not play when sending the message!\nTry again.");
        }
    }

    /**
     * Create a MIDI Note ON/OFF message
     *
     * @param  {midichan} ch       - MIDI Channel to which the message should be sent
     * @param  {(0|1)}    state    - Note ON or OFF; 1 is Note-ON, 0 is Note-OFF
     * @param  {midinnum} note     - MIDI Note number (from 0 to 127)
     * @param  {velocity} velocity - MIDI Velocity amount (from 0 to 127)
     *
     * @return {Array} - The MIDI Note ON/OFF message
     */
    // @old icMakeMIDIoutNoteMsg
    makeMIDIoutNoteMsg(ch, state, note, velocity) {
        let msg = [];
        if (state === 1) {
            msg = [0x90 + ch, note, velocity];
        } else if (state === 0) {
            msg = [0x80 + ch, note, velocity];
        }
        return msg;
    }

    /**
     * Create a MIDI Pitch Bend Change message
     *
     * @param  {midichan} ch     - MIDI Channel to which the message should be sent (from 0 to 15)
     * @param  {number}   amount - Pitch Bend amount (from 0 to 16383)
     *
     * @return {Array} - The MIDI Pitch Bend Change message
     */
    // @old icMakeMIDIoutPitchBendMsg
    makeMIDIoutPitchBendMsg(ch, amount) {
        let lsb = amount & 0x7F;
        let msb = amount >> 7;
        let msg = [0xE0 + ch, lsb, msb];
        return msg;
    }

    /**
     * @todo - The voice stealing implementation of the MIDI-OUT has not the same results of the DHC/Synth.
     *         When voices are overloaded on HT and you release a key on the controller there is a different behavior.
     */

    /**
     * Update the frequency of every sill pending Note-ON
     *
     * @param {tonetype} type - If the notes/channels to be updated are the FT or HT ones.
     */
    // @old icUpdateMIDInoteON
    updateMIDInoteON(type) {
        // For each selected MIDI-OUT ports
        this.midi.port.selectedOutputs.forEach((value, portID) => {
            // PitchBend method
            if (this.settings[portID].selected === "pb") {
                let heldChsFT = this.settings[portID].pb.channels.ft.held;
                let heldChsHT = this.settings[portID].pb.channels.ht.held;
                let heldChsKeysFT = Object.keys(heldChsFT);
                let heldChsKeysHT = Object.keys(heldChsHT);
                if (type === "ft") {
                    if (heldChsKeysFT.length > 0) {
                        for (let key of heldChsKeysFT) {
                            // Update only if the original note is not Tsnapped
                            if (!heldChsFT[key].tsnap) {
                                console.log('updateMIDIout ft>ft');
                                let ft = heldChsFT[key].xt;
                                let ftObj = this.dhc.tables.ft[ft];
                                let velocity = heldChsFT[key].vel;
                                this.sendMIDIoutPB(key, ft, ftObj, 64, 0, "ft", portID);
                                this.sendMIDIoutPB(key, ft, ftObj, velocity, 1, "ft", portID);
                            }
                        }
                    }
                    if (heldChsKeysHT.length > 0) {
                        for (let key of heldChsKeysHT) {
                            // Update only if the original note is not Tsnapped
                            if (!heldChsHT[key].tsnap) {
                                console.log('updateMIDIout ft>ht');
                                let ht = heldChsHT[key].xt;
                                let htObj = this.dhc.tables.ht[ht];
                                let velocity = heldChsHT[key].vel;
                                this.sendMIDIoutPB(key, ht, htObj, 64, 0, "ht", portID);
                                this.sendMIDIoutPB(key, ht, htObj, velocity, 1, "ht", portID);
                            }
                        }
                    }
                } else if (type === "ht") {
                    if (heldChsKeysHT.length > 0) {
                        for (let key of heldChsKeysHT) {
                            // Update only if the original note is not Tsnapped
                            if (!heldChsHT[key].tsnap) {
                                console.log('updateMIDIout ht>ht');
                                let ht = heldChsHT[key].xt;
                                let htObj = this.dhc.tables.ht[ht];
                                let velocity = heldChsHT[key].vel;
                                this.sendMIDIoutPB(key, ht, htObj, 64, 0, "ht", portID);
                                this.sendMIDIoutPB(key, ht, htObj, velocity, 1, "ht", portID);
                            }
                        }
                    }
                }
            // MIDI Tuning Standard method
            } else if (this.settings[portID].selected === "mts") {

            }
        });
    }

    /**
     * For each selected MIDI-OUT Port,
     * prepare and send the MIDI-OUT message according to the selected MIDI-OUT Tuning Method of the port
     *
     * @param {midinnum} ctrlNum  - MIDI Note number of the original MIDI-IN message from the controller
     * @param {xtnum}    xtNum    - Outgoing FT or HT relative tone number
     * @param {velocity} velocity - MIDI Velocity amount (from 0 to 127) of the original MIDI-IN message from the controller
     * @param {(0|1)}    state    - Note ON or OFF; 1 is Note-ON, 0 is Note-OFF
     * @param {tonetype} type     - If the outgoing MIDI message is for FTs or HTs
     * @param {boolean=} tsnap    - If the note is managed by Tsnap
     */
    // @old icMIDIout
    midiOut(ctrlNum, xtNum, velocity, state, type, tsnap=false) {
        let xtObj = this.dhc.tables[type][xtNum];
        // For each selected MIDI-OUT ports
        this.midi.port.selectedOutputs.forEach((value, portID) => {
            // PitchBend method
            if (this.settings[portID].selected === "pb") {
                // Check the if Instrument MIDI Note Number is in the range 0-127
                if (Math.trunc(xtObj.mc) <= 127 && Math.trunc(xtObj.mc) >= 0) {
                    // Check if another note with the same ctrlNum came before its respective Note-OFF
                    // @todo - Manage in different way the Double Note-ON: change index ??
                    if (this.settings[portID].pb.channels[type].held[ctrlNum] && state === 1) {
                        this.sendMIDIoutPB(ctrlNum, xtNum, xtObj, 64, 0, type, portID);                
                        this.sendMIDIoutPB(ctrlNum, xtNum, xtObj, velocity, state, type, portID, tsnap);                
                        console.log(type + " MIDI event: Double Note-ON");
                    } else {
                        this.sendMIDIoutPB(ctrlNum, xtNum, xtObj, velocity, state, type, portID, tsnap);                
                    }
                }
            // @todo - MIDI Tuning Standard method
            } else if (this.settings[portID].selected === "mts") {

            }
        });
    }

    /**
     * MIDI-OUT Tuning - PITCHBEND METHOD core
     * The main function to manage the multichannel poly-assignment and send the MIDI messages
     * This is to implement the "MIDI Channel Mode 4" aka "Guitar Mode" for outgoing messages
     *
     * @param {midinnum}                ctrlNum  - MIDI Note number of the original MIDI-IN message from the controller
     * @param {xtnum}                   xt       - Outgoing FT or HT relative tone number
     * @param {HUM.DHC#Xtone} xtObj    - FT or HT object of the outgoing tone
     * @param {velocity}                velocity - MIDI Velocity amount (from 0 to 127) of the original MIDI-IN message from the controller
     * @param {(0|1)}                   state    - Note ON or OFF; 1 is Note-ON, 0 is Note-OFF
     * @param {tonetype}                type     - If the outgoing MIDI message is for FTs or HTs
     * @param {string}                  portID   - ID of the MIDI-OUT Port to send the message to
     * @param {boolean=}                tsnap    - If the note is managed by Tsnap
     */
    // @old icSendMIDIoutPB
    sendMIDIoutPB(ctrlNum, xt, xtObj, velocity, state, type, portID, tsnap=false) {
        // @todo - Some functional Note-OFF must be sent without delay?!?
        let usedChs = this.settings[portID].pb.channels[type].used;
        let heldChs = this.settings[portID].pb.channels[type].held;
        if (usedChs.length > 0 || Object.keys(heldChs).length > 0) {
            // Init local variables
            let heldOrder = this.settings[portID].pb.channels[type].heldOrder;
            let lastCh = this.settings[portID].pb.channels[type].last;
            let currCh =  null;
            let instNoteNumber = Math.trunc(xtObj.mc);
            let cents = xtObj.mc - Math.trunc(xtObj.mc);
            let midiOutput = this.midi.port.selectedOutputs.get(portID);
            if (cents > 0.5) {
                instNoteNumber = Math.trunc(xtObj.mc + 0.5);
                cents -= 1;
            }
            // @todo - Check 8192 or 8191 depending on the +/-amount (since +amount is up to 8191)
            let pbAmount = cents * (8192 / this.settings[portID].pb.range[type]) + 8192;
            let outMsgsQueue = [];
            // ** FTs **
            if (type === "ft") {
                // Note ON
                if (state === 1) {
                    // Get the key array of the held channels
                    let heldChsKey = Object.keys(heldChs);
                    // If there are available used channels
                    if (usedChs.length > 0 ) {
                        // Find the first available used channel bigger than the lastChan
                        for (let ch = lastCh + 1; ch < 17; ch++) {
                            let index = usedChs.indexOf(ch);
                            if (index > -1 ) {
                                // Set the current channel
                                currCh = ch;
                                // If there are held channels
                                if (heldChsKey.length > 0) {
                                    // Restore the held-on-hold channel to the used channel array
                                    this.settings[portID].pb.channels[type].used.push(heldChs[heldChsKey[0]].ch);
                                    // Make the Note-Off to close the previous note on the last channel
                                    outMsgsQueue.push(this.makeMIDIoutNoteMsg(heldChs[heldChsKey[0]].ch, 0, heldChs[heldChsKey[0]].note, 64));
                                    // Delete the held channel
                                    delete this.settings[portID].pb.channels[type].held[heldChsKey[0]];
                                } // else {
                                // }
                                // Remove the found channel from the used channel array in order to avoid over-assignment
                                this.settings[portID].pb.channels[type].used.splice(index, 1);
                                // Sort the array
                                this.settings[portID].pb.channels[type].used.sort((a, b) => {
                                    return a - b;
                                });
                                /**
                                 * Held channel' infos during the multi-channel polyphony routing;
                                 *     a Held Channel is a currently busy channel already occupied by an outgoing tone
                                 * 
                                 * @typedef  {Object} HeldChannel
                                 *
                                 * @property {midichan} ch    - MIDI Channel Number (from 0 to 15)
                                 * @property {midinnum} note  - Final MIDI Note Number on the Instrument
                                 * @property {velocity} vel   - MIDI velocity amount (from 0 to 127)
                                 * @property {xtnum}    xt    - Relative tone number (FT or HT)
                                 * @property {boolean}  tsnap - If the note is managed by Tsnap
                                 */
                                // Store the current channel to the held-on-hold channel var in order to avoid over-assignment
                                this.settings[portID].pb.channels[type].held[ctrlNum] = {ch: currCh, note: instNoteNumber, vel: velocity, xt: xt, tsnap:tsnap};
                                break;
                            } else {
                                currCh = undefined;
                            }
                        }
                        // If the channel hasn't been found
                        // Restart to find the first available used channel from the beginning to the lastChan
                        if (currCh === undefined) {
                            // Set the current channel
                            currCh = usedChs[0];
                            // If there are held channels
                            if (heldChsKey.length > 0) {
                                // Restore the held-on-hold channel to the used channel array
                                this.settings[portID].pb.channels[type].used.push(heldChs[heldChsKey[0]].ch);
                                // Make the Note-Off to close the previous note on the last channel
                                outMsgsQueue.push(this.makeMIDIoutNoteMsg(heldChs[heldChsKey[0]].ch, 0, heldChs[heldChsKey[0]].note, 64));
                                // Delete the held channels
                                delete this.settings[portID].pb.channels[type].held[heldChsKey[0]];
                            }
                            // Remove the first channel from the used channel array in order to avoid over-assignment
                            this.settings[portID].pb.channels[type].used.splice(0, 1);
                            // Sort the array
                            this.settings[portID].pb.channels[type].used.sort((a, b) => {
                                return a - b;
                            });
                            // Store the current channel to the held-on-hold channels array in order to avoid over-assignment
                            this.settings[portID].pb.channels[type].held[ctrlNum] = {ch: currCh, note: instNoteNumber, vel: velocity, xt: xt, tsnap:tsnap};
                        }
                        // STRANGE: If there aren't available channels
                        if (currCh === undefined) {
                            console.log(type + " MIDI event: Strange event #1");
                        }
                    // If there are NO available used channels
                    } else {
                        console.log(type + " MIDI event: Channels overload");
                        // Use the held channel
                        currCh = heldChs[heldChsKey[0]].ch;
                        // For every held channels
                        for (let key of Object.keys(this.settings[portID].pb.channels[type].held)) {
                            let heldChan = this.settings[portID].pb.channels[type].held[key];
                            // If the held channel is the current channel needed
                            if (heldChan.ch === currCh) {
                                // Make the Note-Off to close the previous Note-On this channel
                                outMsgsQueue.push(this.makeMIDIoutNoteMsg(heldChan.ch, 0, heldChan.note, 64));
                                // Remove the current channel from the held-channels array
                                delete this.settings[portID].pb.channels[type].held[key];
                            }
                        }
                        // Re-store the current channel to the held-on-hold channels array in order to avoid over-assignment
                        this.settings[portID].pb.channels[type].held[ctrlNum] = {ch: currCh, note: instNoteNumber, vel: velocity, xt: xt, tsnap:tsnap};
                    }
                    // Store the current channel on the global slot for polyphony handling
                    this.settings[portID].pb.channels[type].last = currCh;
                    // Make the PitchBend
                    outMsgsQueue.push(this.makeMIDIoutPitchBendMsg(currCh, pbAmount));
                    // Make the Note-On
                    outMsgsQueue.push(this.makeMIDIoutNoteMsg(currCh, 1, instNoteNumber, velocity));
                // Note OFF
                } else if (state === 0) {
                    if (this.settings[portID].pb.channels[type].held[ctrlNum]) {
                        // console.log("== Note OFF == : " + ctrlNum);
                        // Get the current channel to close (to send Note OFF)
                        currCh = this.settings[portID].pb.channels[type].held[ctrlNum].ch;
                        // Restore the held-on-hold channel to the used channel array
                        this.settings[portID].pb.channels[type].used.push(currCh);
                        // Sort the array
                        this.settings[portID].pb.channels[type].used.sort((a, b) => {
                            return a - b;
                        });
                        // Make the Note-Off to close the channel
                        outMsgsQueue.push(this.makeMIDIoutNoteMsg(currCh, 0, this.settings[portID].pb.channels[type].held[ctrlNum].note, velocity));
                        // Remove the current channel from the held-channels array
                        delete this.settings[portID].pb.channels[type].held[ctrlNum];
                    }                
                }
            // ** HTs **
            } else if (type === "ht") {
                // Note ON
                if (state === 1) {
                    // If there are available used channels
                    if (usedChs.length > 0 ) {
                        // Find the first available used channel bigger than the lastChan
                        for (let ch = lastCh + 1; ch < 17; ch++) {
                            let index = usedChs.indexOf(ch);
                            // If there are held channels
                            if (index > -1 ) {
                                // Set the current channel
                                currCh = ch;
                                // Remove the found channel from the used channel array in order to avoid over-assignment
                                this.settings[portID].pb.channels[type].used.splice(index, 1);
                                // Add the current channel at the end of the heldOrder to maintain the assignment order
                                this.settings[portID].pb.channels[type].heldOrder.push(currCh);
                                // Store the current channel to the held-on-hold channels array in order to avoid over-assignment
                                this.settings[portID].pb.channels[type].held[ctrlNum] = {ch: currCh, note: instNoteNumber, vel: velocity, xt: xt, tsnap:tsnap};
                                break;
                            } else {
                                currCh = undefined;
                            }
                        }
                        // If the channel hasn't been found
                        // Restart to find the first available used channel from the beginning to the lastChan
                        if (currCh === undefined) {
                            // Set the current channel
                            currCh = usedChs[0];
                            // Remove the first channel from the used channel array in order to avoid over-assignment
                            this.settings[portID].pb.channels[type].used.splice(0, 1);
                            // Add the current channel at the end of the heldOrder to maintain the assignment order
                            this.settings[portID].pb.channels[type].heldOrder.push(currCh); 
                            // Store the current channel to the held-on-hold channels array in order to avoid over-assignment
                            this.settings[portID].pb.channels[type].held[ctrlNum] = {ch: currCh, note: instNoteNumber, vel: velocity, xt: xt, tsnap:tsnap};
                        }
                        // STRANGE: If there aren't available channels
                        if (currCh === undefined) {
                            console.log(type + " MIDI event: Strange event #1");
                        }
                    // If there are NO available used channels
                    } else {
                        console.log(type + " MIDI event: Channels overload");
                        // Use the oldest held channel
                        currCh = heldOrder[0];
                        // Put the current channel at the end of heldOrder array to maintain the assignment order
                        this.settings[portID].pb.channels[type].heldOrder.splice(0, 1);
                        this.settings[portID].pb.channels[type].heldOrder.push(currCh);
                        // Remove the current channel from the held-channels array
                        for (let key of Object.keys(this.settings[portID].pb.channels[type].held)) {
                            let heldChan = this.settings[portID].pb.channels[type].held[key];
                            if (heldChan.ch === currCh) {
                                // Make the Note-Off to close the previous Note-On this channel
                                outMsgsQueue.push(this.makeMIDIoutNoteMsg(heldChan.ch, 0, heldChan.note, 64));
                                delete this.settings[portID].pb.channels[type].held[key];
                            }
                        }
                        // Re-store the current channel to the held-on-hold channels array in order to avoid over-assignment
                        this.settings[portID].pb.channels[type].held[ctrlNum] = {ch: currCh, note: instNoteNumber, vel: velocity, xt: xt, tsnap:tsnap};
                    }
                    // Store the current channel on the global slot for polyphony handling
                    this.settings[portID].pb.channels[type].last = currCh;
                    // Make the PitchBend
                    outMsgsQueue.push(this.makeMIDIoutPitchBendMsg(currCh, pbAmount));
                    // Make the Note-On
                    outMsgsQueue.push(this.makeMIDIoutNoteMsg(currCh, 1, instNoteNumber, velocity));
                // Note OFF
                } else if (state === 0) {
                    if (this.settings[portID].pb.channels[type].held[ctrlNum]) {
                        // Get the current channel to close (to send Note OFF)
                        currCh = this.settings[portID].pb.channels[type].held[ctrlNum].ch;
                        // Restore the held-on-hold channel to the used channel array
                        this.settings[portID].pb.channels[type].used.push(currCh);
                        // Sort the array
                        this.settings[portID].pb.channels[type].used.sort((a, b) => {
                            return a - b;
                        });
                        // Make the Note-Off to close the channel
                        outMsgsQueue.push(this.makeMIDIoutNoteMsg(currCh, 0, this.settings[portID].pb.channels[type].held[ctrlNum].note, velocity));
                        // Remove the current channel from the held-channels array
                        delete this.settings[portID].pb.channels[type].held[ctrlNum];
                        let index = this.settings[portID].pb.channels[type].heldOrder.indexOf(currCh);
                        this.settings[portID].pb.channels[type].heldOrder.splice(index, 1);
                    }                
                }
            }
            // Send the outgoing MIDI messages
            for (let msg of outMsgsQueue) {
                // If it's a Note-OFF
                if ((msg[0] >> 4 === 9 && msg[2] === 0) || (msg[0] >> 4 === 8)) {
                    document.getElementById(portID + "_" + (msg[0] & 0xf) + "_ft" + "_" + this.dhc.id).disabled = false;
                    document.getElementById(portID + "_" + (msg[0] & 0xf) + "_ht" + "_" + this.dhc.id).disabled = false;
                // If it's a Note-ON
                } else if (msg[0] >> 4 === 9) {
                    document.getElementById(portID + "_" + (msg[0] & 0xf) + "_ft" + "_" + this.dhc.id).disabled = true;
                    document.getElementById(portID + "_" + (msg[0] & 0xf) + "_ht" + "_" + this.dhc.id).disabled = true;
                }
                // If is a Note-ON or Note-OFF message
                if (msg[0] >> 4 === 9 || msg[0] >> 4 === 8) {
                    // @todo - Check native delay method
                    // midiOutput.send(msg, window.performance.now() + this.settings[portID].pb.delay[type]);
                    setTimeout(() => {
                        midiOutput.send(msg);
                    }, this.settings[portID].pb.delay[type]);
                // Else, if it's a PitchBend message 
                } else {
                    midiOutput.send(msg);
                }
            }
        }
    }

};


/**
 * Default port settings for MIDI-OUT tuning methods; each out port has its own settings
 */            
HUM.midi.MidiOut.prototype.InstrumentSettings = class {
    constructor() {
        /**
        * Pitch Bend method settings namespace
        *
        * @member {Object}
        *
        * @property {Object}                         channels              - FTs & HTs multichannel polyphony management
        * @property {Object}                         channels.ft           - Multichannel polyphony for FTs
        * @property {Array.<midichan>}               channels.ft.used      - Sorted array containing the FT used channel numbers
        * @property {Object.<midinnum, HeldChannel>} channels.ft.held      - Object containing the FT busy channel; key is the original Controller MIDI Note number. Init value must be an empty Object.
        * @property {midichan}                       channels.ft.last      - Number of the last held FT channel. Init value must be a -1.
        * @property {Object}                         channels.ht           - Multichannel polyphony for HTs
        * @property {Array.<midichan>}               channels.ht.used      - Sorted array containing the HT used channel numbers
        * @property {Object.<midinnum, HeldChannel>} channels.ht.held      - Object containing the HT busy channels; keys are the original Controller MIDI Note number. Init value must be an empty Object.
        * @property {Array.<midichan>}               channels.ht.heldOrder - Array of channel numbers, sorted according to the held order. Init value must be an empty Array.
        * @property {midichan}                       channels.ht.last      - Number of the last held HT channel. Init value must be a -1.
        * @property {Object}                         range                 - Namespace for pitchBend sensitivity settings
        * @property {number}                         range.ft              - PitchBend sensitivity for FT channels
        * @property {number}                         range.ht              - PitchBend sensitivity for HT channels
        * @property {Object}                         delay                 - Namespace for setting the delay between the PitchBend and Note-ON messages
        * @property {number}                         delay.ft              - Delay for FT channels (milliseconds)
        * @property {number}                         delay.ht              - Delay for HT channels (milliseconds)
        * @property {Object}                         voicestealing         - Namespace for voice stealing management ON/OFF (now stealing is always ON)
        * @property {boolean}                        voicestealing.ft      - Voice stealing ON/OFF for FT channels
        * @property {boolean}                        voicestealing.ht      - Voice stealing ON/OFF for HT channels
        * @property {boolean}                        gm                    - General MIDI ON/OFF (when 'true', avoid channel 10) - `CURRENTLY NOT IMPLEMENTED`
        */
        this.pb = {
            channels: {
                ft: {
                    used:[0, 1, 2],
                    held: {},
                    last: -1
                },
                ht: {
                    used:[3, 4, 5, 6, 7],
                    held: {},
                    heldOrder: [],
                    last: -1
                }
            },
            range: {
                ft: 2,
                ht: 2
            },
            delay: {
                ft: 5,
                ht: 5
            },
            voicestealing: { // @todo - Voice stealing management ON/OFF
                ft: true,
                ht: true
            },
            gm: undefined
        };
        /**
        * MIDI Tuning Standard method settings namespace - `CURRENTLY NOT IMPLEMENTED`
        *
        * @member {Object}
        */
        this.mts = {}; // @todo - MIDI Tuning Standard method
        /**
        * Selected MIDI-OUT Tuning Method for this port;
        *     `'pb'` is PitchBend method, `'mts'` is MIDI Tuning Standard method
        *
        * @member {('pb'|'mts')}
        */
        this.selected = "pb";
    }
};