var XoneK2 = {};

XoneK2.seekRateFast = 3.0;
XoneK2.seekRateSlow = 0.5;

XoneK2.decksInMiddleMidiChannel = 0xE;
XoneK2.effectsInMiddleMidiChannel = 0xD;
XoneK2.decksOnLeftMidiChannel = 0xC;
XoneK2.decksOnRightMidiChannel = 0xB;
XoneK2.fourDecksMidiChannel3124 = 0xA;
XoneK2.fourDecksMidiChannel1234 = 0x9;
XoneK2.fourEffectsMidiChannel3124 = 0x8;
XoneK2.fourEffectsMidiChannel1234 = 0x7;

// The MIDI note offsets for different colors with the layer button is different
// from the rest of the buttons.
XoneK2.layerButtonColors = {
    red: 0x0C,
    amber: 0x10,
    green: 0x14,
}
XoneK2.deckBottomButtonLayers = [
    { name: 'intro_outro', layerButtonNoteNumber: XoneK2.layerButtonColors.amber },
    { name: 'hotcue', layerButtonNoteNumber: XoneK2.layerButtonColors.red },
    { name: 'loop', layerButtonNoteNumber: XoneK2.layerButtonColors.green }, ];

// Multiple K2s/K1s can be connected via X-Link and plugged in with one USB
// cable. The MIDI messages of the controllers can be distinguished by setting
// each one to its own MIDI channel. The XoneK2.controllers array maintains state
// for each controller. This also allows the same mapping to  be loaded for
// different use cases as long as the user sets the appropriate MIDI channel for
// the mapping they want.
XoneK2.controllers = [];
for (var ch = 0; ch <= 0xF; ++ch) {
    XoneK2.controllers[ch] = [];
    XoneK2.controllers[ch].columns = [];
    XoneK2.controllers[ch].isShifted = false;
    XoneK2.controllers[ch].leftEncoderIsPressed = false;
    XoneK2.controllers[ch].rightEncoderIsPressed = false;
    XoneK2.controllers[ch].deckPicked = false;
    // This gets incremented to 0 by the init function calling XoneK2.decksLayerButton
    XoneK2.controllers[ch].deckLayerIndex = -1;
    XoneK2.controllers[ch].focusedEffectUnit = 1;
    XoneK2.controllers[ch].singleEffectUnitModeActive = false;
}

XoneK2.init = function (id) {
    var channel = XoneK2.decksInMiddleMidiChannel;
    XoneK2.controllers[channel].columns[1] = new XoneK2.EffectUnit(1, 1, channel, true);
    XoneK2.controllers[channel].columns[2] = new XoneK2.Deck(2, 1, channel);
    XoneK2.controllers[channel].columns[3] = new XoneK2.Deck(3, 2, channel);
    XoneK2.controllers[channel].columns[4] = new XoneK2.EffectUnit(4, 2, channel, true);
    XoneK2.decksLayerButton(channel, null, null, 0x90 + channel, null);

    channel = XoneK2.effectsInMiddleMidiChannel;
    XoneK2.controllers[channel].columns[1] = new XoneK2.Deck(1, 1, channel);
    XoneK2.controllers[channel].columns[2] = new XoneK2.EffectUnit(2, 1, channel, true);
    XoneK2.controllers[channel].columns[3] = new XoneK2.EffectUnit(3, 2, channel, true);
    XoneK2.controllers[channel].columns[4] = new XoneK2.Deck(4, 2, channel);
    XoneK2.decksLayerButton(channel, null, null, 0x90 + channel, null);

    channel = XoneK2.decksOnLeftMidiChannel;
    XoneK2.controllers[channel].columns[1] = new XoneK2.Deck(1, 1, channel);
    XoneK2.controllers[channel].columns[2] = new XoneK2.Deck(2, 2, channel);
    XoneK2.controllers[channel].columns[3] = new XoneK2.EffectUnit(3, 1, channel, true);
    XoneK2.controllers[channel].columns[4] = new XoneK2.EffectUnit(4, 2, channel, true);
    XoneK2.decksLayerButton(channel, null, null, 0x90 + channel, null);

    channel = XoneK2.decksOnRightMidiChannel;
    XoneK2.controllers[channel].columns[1] = new XoneK2.EffectUnit(1, 1, channel, true);
    XoneK2.controllers[channel].columns[2] = new XoneK2.EffectUnit(2, 2, channel, true);
    XoneK2.controllers[channel].columns[3] = new XoneK2.Deck(3, 1, channel);
    XoneK2.controllers[channel].columns[4] = new XoneK2.Deck(4, 2, channel);
    XoneK2.decksLayerButton(channel, null, null, 0x90 + channel, null);

    channel = XoneK2.fourDecksMidiChannel3124;
    XoneK2.controllers[channel].columns[1] = new XoneK2.Deck(1, 3, channel);
    XoneK2.controllers[channel].columns[2] = new XoneK2.Deck(2, 1, channel);
    XoneK2.controllers[channel].columns[3] = new XoneK2.Deck(3, 2, channel);
    XoneK2.controllers[channel].columns[4] = new XoneK2.Deck(4, 4, channel);
    XoneK2.decksLayerButton(channel, null, null, 0x90 + channel, null);

    channel = XoneK2.fourDecksMidiChannel1234;
    XoneK2.controllers[channel].columns[1] = new XoneK2.Deck(1, 1, channel);
    XoneK2.controllers[channel].columns[2] = new XoneK2.Deck(2, 2, channel);
    XoneK2.controllers[channel].columns[3] = new XoneK2.Deck(3, 3, channel);
    XoneK2.controllers[channel].columns[4] = new XoneK2.Deck(4, 4, channel);
    XoneK2.decksLayerButton(channel, null, null, 0x90 + channel, null);

    channel = XoneK2.fourEffectsMidiChannel3124;
    XoneK2.controllers[channel].columns[1] = new XoneK2.EffectUnit(1, 3, channel);
    XoneK2.controllers[channel].columns[2] = new XoneK2.EffectUnit(2, 1, channel);
    XoneK2.controllers[channel].columns[3] = new XoneK2.EffectUnit(3, 2, channel);
    XoneK2.controllers[channel].columns[4] = new XoneK2.EffectUnit(4, 4, channel);

    channel = XoneK2.fourEffectsMidiChannel1234;
    XoneK2.controllers[channel].columns[1] = new XoneK2.EffectUnit(1, 1, channel);
    XoneK2.controllers[channel].columns[2] = new XoneK2.EffectUnit(2, 2, channel);
    XoneK2.controllers[channel].columns[3] = new XoneK2.EffectUnit(3, 3, channel);
    XoneK2.controllers[channel].columns[4] = new XoneK2.EffectUnit(4, 4, channel);
}

XoneK2.shutdown = function(id) {
    var turnOff = function (component) {
        component.send(0);
    };
    for (var z = 1; z <= 4; z++) {
        XoneK2.controllers[XoneK2.effectsMidiChannel].columns[z].forEachComponent(turnOff);
        XoneK2.controllers[XoneK2.decksMidiChannel].columns[z].forEachComponent(turnOff);
    }
}


XoneK2.decksBottomLeftEncoderPress = function (channel, control, value, status) {
    XoneK2.controllers[channel].leftEncoderIsPressed =  (status & 0xF0) === 0x90;
    if (XoneK2.controllers[channel].isShifted && XoneK2.controllers[channel].leftEncoderIsPressed) {
        script.toggleControl('[Master]', 'headSplit');
    }
};
XoneK2.decksBottomLeftEncoder = function (channel, control, value, status) {
    if (!XoneK2.controllers[channel].isShifted) {
        if (!XoneK2.controllers[channel].leftEncoderIsPressed) {
            var bpm = engine.getValue("[InternalClock]", "bpm");
            if (value === 1) {
                bpm += 0.1;
            } else {
                bpm -= 0.1;
            }
            engine.setValue("[InternalClock]", "bpm", bpm);
        } else {
            var mix = engine.getValue("[Master]", "headMix");
            if (value === 1) {
                mix += 1;
            } else {
                mix -= 1;
            }
            engine.setValue("[Master]", "headMix", mix);
        }
    } else {
        var gain = engine.getValue("[Master]", "headGain");
        if (value === 1) {
            gain += 0.025;
        } else {
            gain -= 0.025;
        }
        engine.setValue("[Master]", "headGain", gain);
    }
};

XoneK2.decksBottomRightEncoderPress = function (channel, control, value, status) {
    XoneK2.controllers[channel].rightEncoderIsPressed = (status & 0xF0) === 0x90;
    if (XoneK2.controllers[channel].rightEncoderIsPressed) {
        for (var x = 1; x <= 4; ++x) {
            var deckColumn = XoneK2.controllers[channel].columns[x];
            if (!(deckColumn instanceof components.Deck)) {
                continue;
            }
            deckColumn.topButtons[3].startDeckPickMode();
        }
    } else {
        for (var x = 1; x <= 4; ++x) {
            var deckColumn = XoneK2.controllers[channel].columns[x];
            if (!(deckColumn instanceof components.Deck)) {
                continue;
            }
            deckColumn.topButtons[3].stopDeckPickMode();
        }

        if (XoneK2.controllers[channel].deckPicked === true) {
            XoneK2.controllers[channel].deckPicked = false;
        } else {
            engine.setValue("[Playlist]", "LoadSelectedIntoFirstStopped", 1);
        }
    }
};
XoneK2.decksBottomRightEncoder = function (channel, control, value, status) {
    if (!XoneK2.controllers[channel].isShifted) {
        var bpm = engine.getValue("[InternalClock]", "bpm");
        if (value === 1) {
            engine.setValue("[Playlist]", "SelectNextTrack", 1);
        } else {
            engine.setValue("[Playlist]", "SelectPrevTrack", 1);
        }
        engine.setValue("[InternalClock]", "bpm", bpm);
    } else {
        var gain = engine.getValue("[Master]", "gain");
        if (value === 1) {
            gain += 0.025;
        } else {
            gain -= 0.025;
        }
        engine.setValue("[Master]", "gain", gain);
    }
};

XoneK2.shiftButton = function (channel, control, value, status) {
    XoneK2.controllers[channel].isShifted = (status & 0xF0) === 0x90;
    if (XoneK2.controllers[channel].isShifted) {
        for (var z = 1; z <= 4; z++) {
            XoneK2.controllers[channel].columns[z].shift();
        }
        midi.sendShortMsg(status, 0x0F, 0x7F);
    } else {
        for (var z = 1; z <= 4; z++) {
            XoneK2.controllers[channel].columns[z].unshift();
        }
        midi.sendShortMsg(status, 0x0F, 0x00);
    }
};

// The Xone K2 uses different control numbers (second MIDI byte) to distinguish between
// different colors for the LEDs. The baseline control number sets the LED to red. Adding
// these offsets to the control number sets the LED to a different color.
XoneK2.color = {
    red: 0,
    amber: 36,
    green: 72
};
components.Component.prototype.color = XoneK2.color.red;
components.Component.prototype.send =  function (value) {
    if (this.midi === undefined || this.midi[0] === undefined || this.midi[1] === undefined) {
        return;
    }
    // The LEDs are turned on with a Note On MIDI message (first nybble of first byte 9)
    // and turned off with a Note Off MIDI message (first nybble of first byte 8).
    if (value > 0) {
        midi.sendShortMsg(this.midi[0] + 0x10, this.midi[1] + this.color, value);
    } else {
        midi.sendShortMsg(this.midi[0], this.midi[1], 0x7F);
    }
};
components.Button.prototype.isPress = function (channel, control, value, status) {
    return (status & 0xF0) === 0x90;
}

XoneK2.setTopEncoderPressMidi = function (topEncoderPressObject, columnNumber, midiChannel) {
    topEncoderPressObject.midi = [0x80 + midiChannel, 0x34 + (columnNumber-1)];
}

XoneK2.setTopButtonsMidi = function (topButtonsObject, columnNumber, midiChannel) {
    for (var c = 1; c <= 3; c++) {
        topButtonsObject[c].midi = [0x80 + midiChannel,
                                    0x30 - (c-1)*4 + (columnNumber-1)];
    }
};

XoneK2.setBottomButtonsMidi = function (bottomButtonsObject, columnNumber, midiChannel) {
    for (var c = 1; c <= 4; c++) {
        bottomButtonsObject[c].midi = [0x80 + midiChannel,
                                       0x24 - (c-1)*4 + (columnNumber-1)];
    }
};

XoneK2.setColumnMidi = function (columnObject, columnNumber, midiChannel) {
    XoneK2.setTopEncoderPressMidi(columnObject.encoderPress, columnNumber, midiChannel);
    XoneK2.setTopButtonsMidi(columnObject.topButtons, columnNumber, midiChannel);
    XoneK2.setBottomButtonsMidi(columnObject.bottomButtons, columnNumber, midiChannel);
};

XoneK2.Deck = function (column, deckNumber, midiChannel) {
    var theDeck = this;

    this.deckString = '[Channel' + deckNumber + ']';

    this.encoder = new components.Encoder({
        unshift: function () {
            this.input = function (channel, control, value, status) {
                direction = (value === 1) ? 1 : -1;
                engine.setValue(this.group, "jog", direction * 3);
            };
        },
        shift: function () {
            this.input = function (channel, control, value, status) {
                direction = (value === 1) ? 1 : -1;
                var gain = engine.getValue(this.group, "pregain");
                engine.setValue(this.group, "pregain", gain + 0.025 * direction);
            };
        },
        supershift: function () {
            this.input = function (channel, control, value, status) {
                direction = (value === 1) ? 1 : -1;
                engine.setValue('[QuickEffectRack1_' + theDeck.deckString + ']', 'chain_selector', direction);
            };
        },
    });

    this.encoderPress = new components.Button({
        outKey: 'sync_enabled',
        unshift: function () {
            this.group = theDeck.deckString;
            this.inKey = 'sync_enabled';
            this.type = components.Button.prototype.types.toggle;
        },
        shift: function () {
            this.group = '[QuickEffectRack1_' + theDeck.deckString + ']';
            this.inKey = 'enabled';
            this.type = components.Button.prototype.types.toggle;
        },
        supershift: function () {
            this.group = theDeck.deckString;
            this.inKey = 'rate_set_zero';
            this.type = components.Button.prototype.types.push;
        },
    });

    this.knobs = new components.ComponentContainer();
    for (var k = 1; k <= 2; k++) {
        this.knobs[k] = new components.Pot({
            group: '[EqualizerRack1_' + this.deckString + '_Effect1]',
            inKey: 'parameter' + (4-k),
        });
    }
    // Low EQ knob. With shift, switches to QuickEffect superknob. Stays as
    // QuickEffect superknob until shift is pressed again to allow using the
    // QuickEffect superknob without having to keep shift held down. This
    // allows using the QuickEffect superknob for a transition while using
    // the other hand for another control.
    this.knobs[3] = new components.Pot({
        hasBeenTurnedSinceShiftToggle: false,
        input: function (channel, control, value, status) {
            components.Pot.prototype.input.call(this, channel, control, value, status);
            this.hasBeenTurnedSinceShiftToggle = true;
        },
        unshift: function() {
            if (!this.hasBeenTurnedSinceShiftToggle) {
                this.disconnect();
                this.group = '[EqualizerRack1_' + theDeck.deckString + '_Effect1]';
                this.inKey = 'parameter1';
                this.connect();
            }
            this.hasBeenTurnedSinceShiftToggle = false;
        },
        shift: function() {
            this.disconnect();
            this.group = '[QuickEffectRack1_' + theDeck.deckString + ']';
            this.inKey = 'super1';
            this.connect();
            this.hasBeenTurnedSinceShiftToggle = false;
        }
    });

    this.fader = new components.Pot({inKey: 'volume'});

    this.topButtons = new components.ComponentContainer();
    this.topButtons[1] = new components.Button({
        unshift: function () {
            this.disconnect();
            this.type = components.Button.prototype.types.toggle;
            this.inKey = 'pfl';
            this.outKey = 'pfl';
            this.color = XoneK2.color.red;
            this.connect();
            this.trigger();
        },
        shift: function () {
            this.disconnect();
            this.type = components.Button.prototype.types.push;
            this.inKey = 'rate_set_zero';
            this.outKey = 'pfl';
            this.color = XoneK2.color.red;
            this.connect();
            this.trigger();
        },
        supershift: function () {
            this.disconnect();
            this.type = components.Button.prototype.types.push;
            this.inKey = 'beats_translate_curpos';
            this.outKey = 'beats_translate_curpos';
            this.color = XoneK2.color.amber;
            this.connect();
            this.trigger();
        },
    });
    this.topButtons[2] = new components.Button({
        unshift: function () {
            this.disconnect();
            this.type = components.Button.prototype.types.push;
            this.inKey = 'cue_default';
            this.outKey = 'cue_indicator';
            this.color = XoneK2.color.red;
            this.connect();
            this.trigger();
        },
        shift: function () {
            this.disconnect();
            this.type = components.Button.prototype.types.push;
            this.inKey = 'start_stop';
            this.outKey = 'cue_indicator';
            this.color = XoneK2.color.red;
            this.connect();
            this.trigger();
        },
        supershift: function () {
            this.disconnect();
            this.type = components.Button.prototype.types.toggle;
            this.inKey = 'keylock';
            this.outKey = 'keylock';
            this.color = XoneK2.color.amber;
            this.connect();
            this.trigger();
        },
    });
    this.topButtons[3] = new components.Button({
        unshift: function () {
            this.disconnect();
            this.inKey = 'play';
            this.outKey = 'play_indicator';
            this.color = XoneK2.color.red;
            this.connect();
            this.trigger();
        },
        shift: function () {
            this.disconnect();
            this.inKey = 'reverse';
            this.outKey = 'play_indicator';
            this.color = XoneK2.color.red;
            this.connect();
            this.trigger();
        },
        supershift: function () {
            this.disconnect();
            this.inKey = 'quantize';
            this.outKey = 'quantize';
            this.color = XoneK2.color.amber;
            this.connect();
            this.trigger();
        },
        startDeckPickMode: function () {
            this.input = function (channel, control, value, status) {
                if (this.isPress(channel, control, value, status)) {
                    engine.setValue(this.group, "LoadSelectedTrack", 1);
                    XoneK2.controllers[channel].deckPicked = true;
                }
            };
        },
        stopDeckPickMode: function () {
            // The inKey and outKey are still set from before startDeckPickMode was
            // called, so all that is needed to get back to that mode is to fall back
            // to the prototype input function.
            this.input = components.Button.prototype.input;
        },
        type: components.Button.prototype.types.toggle,
    });

    // This should not be a ComponentContainer, otherwise strange things will
    // happen when iterating over the Deck with reconnectComponents.
    this.bottomButtonLayers = [];

    var CueAndSeekButton = function (options) {
        if (options.cueName === undefined) {
            print('ERROR! cueName not specified');
        } else if (options.seekRate === undefined) {
            print('ERROR! seekRate not specified');
        }

        this.outKey = options.cueName + '_enabled';
        components.Button.call(this, options);
    };
    CueAndSeekButton.prototype = new components.Button({
        unshift: function () {
            this.inKey = this.cueName + '_activate';
            this.input = components.Button.prototype.input;
            // Avoid log spam on startup
            if (this.group !== undefined) {
                engine.setValue(this.group, 'rateSearch', 0);
            }
        },
        shift: function () {
            this.input = function (channel, control, value, status) {
                if (components.Button.prototype.isPress(channel, control, value, status)) {
                    engine.setValue(this.group, 'rateSearch', this.seekRate);
                } else {
                    engine.setValue(this.group, 'rateSearch', 0);
                }
            };
        },
        supershift: function () {
            this.inKey = this.cueName + '_clear';
            this.input = components.Button.prototype.input;
            engine.setValue(this.group, 'rateSearch', 0);
        }
    });

    this.bottomButtonLayers.intro_outro = new components.ComponentContainer();
    this.bottomButtonLayers.intro_outro[1] = new CueAndSeekButton({
        cueName: "intro_start",
        seekRate: XoneK2.seekRateFast,
        color: XoneK2.color.amber,
    });
    this.bottomButtonLayers.intro_outro[2] = new CueAndSeekButton({
        cueName: "intro_end",
        seekRate: -1 * XoneK2.seekRateFast,
        color: XoneK2.color.amber,
    });
    this.bottomButtonLayers.intro_outro[3] = new CueAndSeekButton({
        cueName: "outro_start",
        seekRate: XoneK2.seekRateSlow,
        color: XoneK2.color.amber,
    });
    this.bottomButtonLayers.intro_outro[4] = new CueAndSeekButton({
        cueName: "outro_end",
        seekRate: -1 * XoneK2.seekRateSlow,
        color: XoneK2.color.amber,
    });


    this.bottomButtonLayers.hotcue = new components.ComponentContainer();
    this.bottomButtonLayers.hotcue[1] = new CueAndSeekButton({
        cueName: "hotcue_1",
        seekRate: XoneK2.seekRateFast,
        color: XoneK2.color.red,
    });
    this.bottomButtonLayers.hotcue[2] = new CueAndSeekButton({
        cueName: "hotcue_2",
        seekRate: -1 * XoneK2.seekRateFast,
        color: XoneK2.color.red,
    });
    this.bottomButtonLayers.hotcue[3] = new CueAndSeekButton({
        cueName: "hotcue_3",
        seekRate: XoneK2.seekRateSlow,
        color: XoneK2.color.red,
    });
    this.bottomButtonLayers.hotcue[4] = new CueAndSeekButton({
        cueName: "hotcue_4",
        seekRate: -1 * XoneK2.seekRateSlow,
        color: XoneK2.color.red,
    });


    this.bottomButtonLayers.loop = new components.ComponentContainer();

    this.bottomButtonLayers.loop[1] = new components.Button({
        outKey: 'loop_enabled',
        unshift: function () {
            this.inKey = 'reloop_toggle';
        },
        shift: function () {
            this.inKey = 'reloop_andstop';
        },
        supershift: function () {
            this.inKey = 'loop_in';
        },
        color: XoneK2.color.red,
    });

    this.bottomButtonLayers.loop[2] = new components.Button({
        unshift: function () {
            this.inKey = 'beatloop_activate';
        },
        shift: function () {
            this.inKey = 'beatlooproll_activate';
        },
        supershift: function () {
            this.inKey = 'loop_out';
        },
        trigger: function() {
            this.send(this.on);
        },
        color: XoneK2.color.green,
    });

    this.bottomButtonLayers.loop[3] = new components.Button({
        unshift: function () {
            this.inKey = 'loop_double';
            this.input = components.Button.prototype.input;
        },
        shift: function () {
            this.inKey = 'beatjump_forward';
            this.input = components.Button.prototype.input;
        },
        supershift: function () {
            this.input = function (channel, control, value, status) {
                if (this.isPress(channel, control, value, status)) {
                    engine.setValue(this.group, 'beatjump_size',
                                    engine.getValue(this.group, 'beatjump_size') * 2);
                }
            };
        },
        trigger: function() {
            this.send(this.on);
        },
        color: XoneK2.color.amber,
    });

    this.bottomButtonLayers.loop[4] = new components.Button({
        unshift: function () {
            this.inKey = 'loop_halve';
            this.input = components.Button.prototype.input;
        },
        shift: function () {
            this.inKey = 'beatjump_backward';
            this.input = components.Button.prototype.input;
        },
        supershift: function () {
            this.input = function (channel, control, value, status) {
                if (this.isPress(channel, control, value, status)) {
                    engine.setValue(this.group, 'beatjump_size',
                                    engine.getValue(this.group, 'beatjump_size') / 2);
                }
            };
        },
        trigger: function() {
            this.send(this.on);
        },
        color: XoneK2.color.amber,
    });

    var setGroup = function (component) {
        if (component.group === undefined) {
            component.group = theDeck.deckString;
        }
    };

    for (var memberName in this.bottomButtonLayers) {
        if (this.bottomButtonLayers.hasOwnProperty(memberName)) {
            XoneK2.setBottomButtonsMidi(this.bottomButtonLayers[memberName], column, midiChannel);
            this.bottomButtonLayers[memberName].forEachComponent(setGroup);
        }
    }

    this.bottomButtons = this.bottomButtonLayers[XoneK2.deckBottomButtonLayers[0].name];

    XoneK2.setColumnMidi(this, column, midiChannel);
    this.reconnectComponents(setGroup);

};
XoneK2.Deck.prototype = new components.Deck();

XoneK2.decksLayerButton = function (channel, control, value, status) {
    if (!XoneK2.controllers[channel].isShifted) {
        // Cycle the deck layers
        if (components.Button.prototype.isPress(channel, control, value, status)) {
            XoneK2.controllers[channel].deckLayerIndex++;
            if (XoneK2.controllers[channel].deckLayerIndex === XoneK2.deckBottomButtonLayers.length) {
                XoneK2.controllers[channel].deckLayerIndex = 0;
            }
            var newLayer = XoneK2.deckBottomButtonLayers[XoneK2.controllers[channel].deckLayerIndex];

            for (var x = 1; x <= 4; ++x) {
                var deckColumn = XoneK2.controllers[channel].columns[x];
                if (!(deckColumn instanceof components.Deck)) {
                    continue;
                }

                deckColumn.bottomButtons.forEachComponent(function (c) {
                    c.disconnect();
                });
                deckColumn.bottomButtons = deckColumn.bottomButtonLayers[newLayer.name];
                deckColumn.bottomButtons.forEachComponent(function (c) {
                    c.connect();
                    c.trigger();
                });
            }
            midi.sendShortMsg(status, newLayer.layerButtonNoteNumber, 0x7F);
        }
    } else {
        if (components.Button.prototype.isPress(channel, control, value, status)) {
            // Activate supershift mode
            var supershift = function (c) {
                if (c.supershift !== undefined) {
                    c.supershift();
                }
            };

            for (var x = 1; x <= 4; ++x) {
                var deckColumn = XoneK2.controllers[channel].columns[x];
                if (!(deckColumn instanceof components.Deck)) {
                    continue;
                }
                deckColumn.forEachComponent(supershift);
                deckColumn.bottomButtons.forEachComponent(supershift);
            }
        } else {
            // Shift button is still held down, so exit supershift mode by going back to
            // plain shift mode
            var shift = function (c) {
                if (c.supershift !== undefined) {
                    c.shift();
                }
            };

            for (var x = 1; x <= 4; ++x) {
                var deckColumn = XoneK2.controllers[channel].columns[x];
                if (!(deckColumn instanceof components.Deck)) {
                    continue;
                }
                deckColumn.forEachComponent(shift);
                deckColumn.bottomButtons.forEachComponent(shift);
            }
        }
    }
};

XoneK2.EffectUnit = function (column, unitNumber, midiChannel, twoDeck) {
    // "library" refers to the Components library
    // This is a private variable rather than a property of XoneK2.EffectUnit
    // so that components.ComponentContainer.prototype.shift/unshift do not
    // call the shift/unshift methods of the components in libraryEffectUnit
    // when a single effect unit is focused.
    var libraryEffectUnit = new components.EffectUnit([unitNumber], false, {
        unfocused: XoneK2.color.red,
        focusChooseMode: XoneK2.color.green,
        focused: XoneK2.color.amber,
    });

    var unitString = '[EffectRack1_EffectUnit' + unitNumber + ']';

    this.hadParametersShowing = engine.getValue(unitString, 'show_parameters');
    this.hadFocusShowing = engine.getValue(unitString, 'show_focus');

    this.fader = libraryEffectUnit.dryWetKnob;

    this.bottomButtons = new components.ComponentContainer();
    var channelString;
    libraryEffectUnit.enableOnChannelButtons.addButton('Channel1');
    this.bottomButtons[1] = libraryEffectUnit.enableOnChannelButtons.Channel1;
    libraryEffectUnit.enableOnChannelButtons.addButton('Channel2');
    this.bottomButtons[2] = libraryEffectUnit.enableOnChannelButtons.Channel2;
    if (twoDeck === true) {
        libraryEffectUnit.enableOnChannelButtons.addButton('Master');
        this.bottomButtons[3] = libraryEffectUnit.enableOnChannelButtons.Master;
        libraryEffectUnit.enableOnChannelButtons.addButton('Headphone');
        this.bottomButtons[4] = libraryEffectUnit.enableOnChannelButtons.Headphone;
    } else {
        this.bottomButtons[3] = new components.Button({
            group: unitString,
            unshift: function () {
                this.disconnect();
                this.inKey = 'group_[Channel3]_enable';
                this.outKey = 'group_[Channel3]_enable';
                this.color = XoneK2.color.red;
                this.connect();
                this.trigger();
            },
            shift: function () {
                this.disconnect();
                this.inKey = 'group_[Master]_enable';
                this.outKey = 'group_[Master]_enable';
                this.color = XoneK2.color.amber;
                this.connect();
                this.trigger();
            },
            type: components.Button.prototype.types.toggle,
        });
        this.bottomButtons[4] = new components.Button({
            group: unitString,
            unshift: function () {
                this.disconnect();
                this.inKey = 'group_[Channel4]_enable';
                this.outKey = 'group_[Channel4]_enable';
                this.color = XoneK2.color.red;
                this.connect();
                this.trigger();
            },
            shift: function () {
                this.disconnect();
                this.inKey = 'group_[Headphone]_enable';
                this.outKey = 'group_[Headphone]_enable';
                this.color = XoneK2.color.amber;
                this.connect();
                this.trigger();
            },
            type: components.Button.prototype.types.toggle,
        });
    }

    this.encoder = new components.Component({
        // TODO: figure out a use for this. Maybe switching chain presets?
        input: function () {},
    });

    this.topButtons = new components.ComponentContainer();
    this.knobs = new components.ComponentContainer();

    this.useLibraryEffectUnit = function () {
        //print('*************************************************** COLUMN '
        //    + column + ' USING LIBRARY UNIT');

        this.knobs.forEachComponent(function (component) {
            component.disconnect();
        });
        this.topButtons.forEachComponent(function (component) {
            component.disconnect();
        });

        this.encoderPress = libraryEffectUnit.effectFocusButton;
        this.knobs = libraryEffectUnit.knobs;
        this.topButtons = libraryEffectUnit.enableButtons;

        engine.setValue(unitString, 'show_focus', this.hadFocusShowing);
        engine.setValue(unitString, 'show_parameters', this.hadParametersShowing);

        XoneK2.setColumnMidi(this, column, midiChannel);
        if (libraryEffectUnit.hasInitialized) {
            libraryEffectUnit.showParametersConnection =
                engine.makeConnection(unitString,
                                      'show_parameters',
                                      libraryEffectUnit.onShowParametersChange.bind(this));

            libraryEffectUnit.knobs.reconnectComponents();
            libraryEffectUnit.enableButtons.reconnectComponents();
            libraryEffectUnit.effectFocusButton.connect();
            libraryEffectUnit.effectFocusButton.trigger();
            libraryEffectUnit.showParametersConnection.trigger();
        } else {
            libraryEffectUnit.init();
        }

    };
    this.useLibraryEffectUnit();

    this.unitFocusButton = new components.Button({
        input: function (channel, control, value, status) {
            if (this.isPress(channel, control, value, status)) {
                if (XoneK2.controllers[channel].focusedEffectUnit === unitNumber) {
                    // Prevent flickering
                    return;
                }

                for (var x = 1; x <= 4; ++x) {
                    var effectUnitColumn = XoneK2.controllers[channel].columns[x];
                    if (!(effectUnitColumn instanceof XoneK2.EffectUnit)) {
                        continue;
                    }

                    XoneK2.controllers[channel].focusedEffectUnit = unitNumber;
                    effectUnitColumn.focusUnit(unitNumber);
                }
            }
        },
        color: XoneK2.color.red,
    });
    XoneK2.setTopEncoderPressMidi(this.unitFocusButton, column, midiChannel);

    this.disconnectShowParameters = function () {
        libraryEffectUnit.showParametersConnection.disconnect();
    };

    this.focusUnit = function (focusedUnitNumber) {
        //print('================================================== COLUMN '
        //    + column + ' FOCUSING UNIT ' + focusedUnitNumber);

        libraryEffectUnit.effectFocusButton.disconnect();
        // The showParametersConnection connection does not belong to any specific
        // Component, so it must be disconnected manually. This script creates
        // objects for every potential layout on different MIDI channels. The
        // showParametersConnection for every XoneK2.EffectUnit must be disconnected
        // or the connections for other MIDI channels will interfere with
        // the MIDI channel actually being used.
        for (var n = 0; n <= 0xF; n++) {
            var col = XoneK2.controllers[n].columns[column];
            if (col instanceof XoneK2.EffectUnit) {
                col.disconnectShowParameters();
            }
        }
        libraryEffectUnit.showParametersConnection.disconnect();
        this.knobs.forEachComponent(function (component) {
            component.disconnect();
        });
        this.topButtons.forEachComponent(function (component) {
            component.disconnect();
        });

        if (!XoneK2.controllers[midiChannel].singleEffectUnitModeActive) {
            this.hadFocusShowing = engine.getValue(unitString, 'show_focus');
            this.hadParametersShowing = engine.getValue(unitString, 'show_parameters');
        }
        engine.setValue(unitString, 'show_focus', 0);
        engine.setValue(unitString, 'show_parameters', focusedUnitNumber === unitNumber);

        this.encoderPress = this.unitFocusButton;
        this.unitFocusButton.send(focusedUnitNumber === unitNumber);

        // The containers must be reassigned to new objects before reassigning
        // the Components within them. Otherwise, the Components in
        // libraryEffectUnit will get reassigned too.
        this.knobs = new components.ComponentContainer();
        this.topButtons = new components.ComponentContainer();
        for (var k = 1; k <= 3; k++) {
            this.knobs[k] = new components.Pot({
                group: '[EffectRack1_EffectUnit' + focusedUnitNumber + '_Effect' + k + ']',
                inKey: 'parameter' + column,
                unshift: function () {
                    this.input = function (channel, control, value, status, group) {
                        this.inSetParameter(this.inValueScale(value));

                        if (this.previousValueReceived === undefined) {
                            engine.softTakeover(this.group, this.inKey, true);
                        }
                        this.previousValueReceived = value;
                    };
                },
                shift: function () {
                    engine.softTakeoverIgnoreNextValue(this.group, this.inKey);
                    this.valueAtLastEffectSwitch = this.previousValueReceived;
                    // Floor the threshold to ensure that every effect can be selected
                    this.changeThreshold = Math.floor(this.max /
                        engine.getValue('[Master]', 'num_effectsavailable'));

                    this.input = function (channel, control, value, status, group) {
                        var change = value - this.valueAtLastEffectSwitch;
                        if (Math.abs(change) >= this.changeThreshold
                            // this.valueAtLastEffectSwitch can be undefined if
                            // shift was pressed before the first MIDI value was received.
                            || this.valueAtLastEffectSwitch === undefined) {
                            engine.setValue(this.group, 'effect_selector', change);
                            this.valueAtLastEffectSwitch = value;
                        }

                        this.previousValueReceived = value;
                    };
                },
            });

            if (column === 1) {
                this.topButtons[k] = new components.Button({
                    group: '[EffectRack1_EffectUnit' + focusedUnitNumber + '_Effect' + k + ']',
                    key: 'enabled',
                    type: components.Button.prototype.types.powerWindow,
                    color: XoneK2.color.amber,
                    outConnect: false, // midi is not defined yet
                });
            } else {
                this.topButtons[k] = new components.Button({
                    group: '[EffectRack1_EffectUnit' + focusedUnitNumber + '_Effect' + k + ']',
                    key: 'button_parameter' + (column-1),
                    type: components.Button.prototype.types.powerWindow,
                    color: XoneK2.color.green,
                    outConnect: false, // midi is not defined yet
                });
            }
        }
        XoneK2.setTopButtonsMidi(this.topButtons, column, midiChannel);
        this.knobs.reconnectComponents();
        this.topButtons.reconnectComponents();
    };
};
XoneK2.EffectUnit.prototype = new components.ComponentContainer();

// This is only used for the 4 effect unit layouts
XoneK2.effectsLayerButton = function (channel, control, value, status) {
    if (components.Button.prototype.isPress(channel, control, value, status)) {
        for (var x = 1; x <= 4; ++x) {
            var effectUnitColumn = XoneK2.controllers[channel].columns[x];
            if (!(effectUnitColumn instanceof XoneK2.EffectUnit)) {
                continue;
            }

            if (XoneK2.controllers[channel].singleEffectUnitModeActive === true) {
                effectUnitColumn.useLibraryEffectUnit();
            } else {
                effectUnitColumn.focusUnit(XoneK2.controllers[channel].focusedEffectUnit);
            }
        }

        XoneK2.controllers[channel].singleEffectUnitModeActive =
            !XoneK2.controllers[channel].singleEffectUnitModeActive;

        if (XoneK2.controllers[channel].singleEffectUnitModeActive) {
            midi.sendShortMsg(status, XoneK2.layerButtonColors.red, 0x7F);
        } else {
            midi.sendShortMsg(status, XoneK2.layerButtonColors.red, 0x00);
        }
    }
};
