var PioneerDDJSB = function () {};

/*
    Find the latest code at https://github.com/jardiacaj/mixxx

    This mapping for the Pioneer DDJ-SB was made by Joan Ardiaca Jové (joan.ardiaca@gmail.com)
    basing on the work of wingcom (wwingcomm@gmail.com, https://github.com/wingcom/Mixxx-Pioneer-DDJ-SB).
    which in turn was based on the work of Hilton Rudham (https://github.com/hrudham/Mixxx-Pioneer-DDJ-SR).

    Just as wingcom's and Rudham's work, this mapping is pusblished under the MIT license.
 */

///////////////////////////////////////////////////////////////
//                       USER OPTIONS                        //
///////////////////////////////////////////////////////////////

// If true the sync button blinks with the beat, if false led is lit when sync is enabled.
PioneerDDJSB.blinkingSync = false;

// If true, the vinyl button activates slip. Vinyl mode is then activated by using shift.
// Allows toggling slip faster, but is counterintuitive.
PioneerDDJSB.invertVinylSlipButton = false;

// Sets the jogwheels sensitivity. 1 is default, 2 is twice as sensitive, 0.5 is half as sensitive.
PioneerDDJSB.jogwheelSensitivity = 1.0;

// Sets how much more sensitive the jogwheels get when holding shift.
// Set to 1 to disable jogwheel sensitivity increase when holding shift.
PioneerDDJSB.jogwheelShiftMultiplier = 20;

///////////////////////////////////////////////////////////////
//                      INIT & SHUTDOWN                      //
///////////////////////////////////////////////////////////////


PioneerDDJSB.init = function (id) {
    PioneerDDJSB.scratchSettings = {
        'alpha': 1.0 / 8,
        'beta': 1.0 / 8 / 32,
        'jogResolution': 720,
        'vinylSpeed': 33 + 1/3,
        'safeScratchTimeout': 20
    };

    PioneerDDJSB.channelGroups = {
        '[Channel1]': 0x00,
        '[Channel2]': 0x01,
        '[Channel3]': 0x02,
        '[Channel4]': 0x03
    };

    PioneerDDJSB.samplerGroups = {
        '[Sampler1]': 0x00,
        '[Sampler2]': 0x01,
        '[Sampler3]': 0x02,
        '[Sampler4]': 0x03
    };

    PioneerDDJSB.fxGroups = {
        '[EffectRack1_EffectUnit1]': 0x00,
        '[EffectRack1_EffectUnit2]': 0x01
    };

    PioneerDDJSB.fxControls = {
        'group_[Channel1]_enable': 0x00,
        'group_[Channel3]_enable': 0x00,
        'group_[Headphone]_enable': 0x01,
        'group_[Channel2]_enable': 0x02,
        'group_[Channel4]_enable': 0x02
    };

    PioneerDDJSB.shiftPressed = false;

    PioneerDDJSB.chFaderStart = [
        null,
        null
    ];

    PioneerDDJSB.fxButtonPressed = [
        [false, false, false],
        [false, false, false]
    ];

    // used for soft takeover workaround
    PioneerDDJSB.fxParamsActiveValues = [
        [0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0]
    ];

    PioneerDDJSB.scratchMode = [false, false, false, false];

    PioneerDDJSB.ledGroups = {
        'hotCue': 0x00,
        'autoLoop': 0x10,
        'manualLoop': 0x20,
        'sampler': 0x30
    };

    PioneerDDJSB.nonPadLeds = {
        'headphoneCue': 0x54,
        'shiftHeadphoneCue': 0x68,
        'cue': 0x0C,
        'shiftCue': 0x48,
        'keyLock': 0x1A,
        'shiftKeyLock': 0x60,
        'play': 0x0B,
        'shiftPlay': 0x47,
        'vinyl': 0x17,
        'shiftVinyl': 0x4E,
        'sync': 0x58,
        'shiftSync': 0x5C
    };

    PioneerDDJSB.loopIntervals = [1, 2, 4, 8, 16, 32, 64];

    PioneerDDJSB.looprollIntervals = [1/16, 1/8, 1/4, 1/2, 1, 2, 4, 8];

    PioneerDDJSB.setAllSoftTakeover(false);
    PioneerDDJSB.bindNonDeckControlConnections(false);
    PioneerDDJSB.initDeck('[Channel1]');
    PioneerDDJSB.initDeck('[Channel2]');
};

PioneerDDJSB.shutdown = function () {
    PioneerDDJSB.bindAllControlConnections(true);
    PioneerDDJSB.setAllSoftTakeover(true);
    PioneerDDJSB.bindDeckControlConnections('[Channel3]', true);
    PioneerDDJSB.bindDeckControlConnections('[Channel4]', true);
};


///////////////////////////////////////////////////////////////
//                      CONTROL BINDING                      //
///////////////////////////////////////////////////////////////


PioneerDDJSB.bindSamplerControlConnections = function (samplerGroup, isUnbinding) {
    engine.connectControl(samplerGroup, 'duration', 'PioneerDDJSB.samplerLeds', isUnbinding);
};

PioneerDDJSB.bindDeckControlConnections = function (channelGroup, isUnbinding) {
    var i,
        index,
        controlsToFunctions = {
            'play': 'PioneerDDJSB.playLeds',
            'pfl': 'PioneerDDJSB.headphoneCueLed',
            'keylock': 'PioneerDDJSB.keyLockLed',
            'slip_enabled': 'PioneerDDJSB.slipLed',
            'quantize': 'PioneerDDJSB.quantizeLed',
            'loop_in': 'PioneerDDJSB.loopInLed',
            'loop_out': 'PioneerDDJSB.loopOutLed',
            'filterLowKill': 'PioneerDDJSB.lowKillLed',
            'filterMidKill': 'PioneerDDJSB.midKillLed',
            'filterHighKill': 'PioneerDDJSB.highKillLed',
            'mute': 'PioneerDDJSB.muteLed',
            'loop_enabled': 'PioneerDDJSB.loopExitLed',
            'loop_double': 'PioneerDDJSB.loopDoubleLed',
            'loop_halve': 'PioneerDDJSB.loopHalveLed'
        };

    if (PioneerDDJSB.blinkingSync) {
        controlsToFunctions.beat_active = 'PioneerDDJSB.syncLed';
    } else {
        controlsToFunctions.sync_enabled = 'PioneerDDJSB.syncLed';
    }

    for (i = 1; i <= 8; i++) {
        controlsToFunctions['hotcue_' + i + '_enabled'] = 'PioneerDDJSB.hotCueLeds';
    }

    for (index in PioneerDDJSB.loopIntervals) {
        controlsToFunctions['beatloop_' + PioneerDDJSB.loopIntervals[index] + '_enabled'] = 'PioneerDDJSB.beatloopLeds';
    }

    for (index in PioneerDDJSB.looprollIntervals) {
        controlsToFunctions['beatlooproll_' + PioneerDDJSB.looprollIntervals[index] + '_activate'] = 'PioneerDDJSB.beatlooprollLeds';
    }

    script.bindConnections(channelGroup, controlsToFunctions, isUnbinding);

    for (fxUnitIndex = 1; fxUnitIndex <= 2; fxUnitIndex++) {
        engine.connectControl('[EffectRack1_EffectUnit' + fxUnitIndex + ']', 'group_' + channelGroup + '_enable', 'PioneerDDJSB.fxLeds', isUnbinding);
        if (!isUnbinding) {
            engine.trigger('[EffectRack1_EffectUnit' + fxUnitIndex + ']', 'group_' + channelGroup + '_enable');
        }
    }
};

PioneerDDJSB.bindNonDeckControlConnections = function (isUnbinding) {
    var samplerIndex,
        fxUnitIndex;

    for (samplerIndex = 1; samplerIndex <= 4; samplerIndex++) {
        PioneerDDJSB.bindSamplerControlConnections('[Sampler' + samplerIndex + ']', isUnbinding);
    }

    for (fxUnitIndex = 1; fxUnitIndex <= 2; fxUnitIndex++) {
        engine.connectControl('[EffectRack1_EffectUnit' + fxUnitIndex + ']', 'group_[Headphone]_enable', 'PioneerDDJSB.fxLeds', isUnbinding);
    }
};

PioneerDDJSB.bindAllControlConnections = function (isUnbinding) {
    var samplerIndex,
        fxUnitIndex,
        channelIndex;

    for (samplerIndex = 1; samplerIndex <= 4; samplerIndex++) {
        PioneerDDJSB.bindSamplerControlConnections('[Sampler' + samplerIndex + ']', isUnbinding);
    }

    for (fxUnitIndex = 1; fxUnitIndex <= 2; fxUnitIndex++) {
        engine.connectControl('[EffectRack1_EffectUnit' + fxUnitIndex + ']', 'group_[Headphone]_enable', 'PioneerDDJSB.fxLeds', isUnbinding);
    }

    for (channelIndex = 1; channelIndex <= 2; channelIndex++) {
        PioneerDDJSB.bindDeckControlConnections('[Channel' + channelIndex + ']', isUnbinding);
    }
};

PioneerDDJSB.setDeckSoftTakeover = function (channel, isUnbinding) {
    engine.softTakeover(channel, "volume", !isUnbinding);
    engine.softTakeover(channel, "rate", !isUnbinding);
    engine.softTakeover(channel, "pregain", !isUnbinding);
    engine.softTakeover(channel, "filterHigh", !isUnbinding);
    engine.softTakeover(channel, "filterMid", !isUnbinding);
    engine.softTakeover(channel, "filterLow", !isUnbinding);
    engine.softTakeover("[QuickEffectRack1_" + channel + "]", "super1", !isUnbinding);
};

PioneerDDJSB.setAllSoftTakeover = function (isUnbinding) {
    var channelIndex;
    for (channelIndex = 1; channelIndex <= 4; channelIndex++) {
        PioneerDDJSB.setDeckSoftTakeover('[Channel' + channelIndex + ']', isUnbinding);
    }
};


///////////////////////////////////////////////////////////////
//                       DECK SWITCHING                      //
///////////////////////////////////////////////////////////////

PioneerDDJSB.deckSwitchTable = {
    '[Channel1]': '[Channel1]',
    '[Channel2]': '[Channel2]'
};

PioneerDDJSB.initDeck = function (group) {
    PioneerDDJSB.bindDeckControlConnections(group, false);
    PioneerDDJSB.nonPadLedControl(group, PioneerDDJSB.nonPadLeds.shiftKeyLock, PioneerDDJSB.channelGroups[group] > 1);
    PioneerDDJSB.triggerVinylLed(PioneerDDJSB.channelGroups[group]);
};

PioneerDDJSB.deckToggleButton = function (channel, control, value, status, group) {
    var deckNumber = PioneerDDJSB.channelGroups[PioneerDDJSB.deckSwitchTable[group]] + 1;

    if (value) {
        // unbind current deck
        PioneerDDJSB.bindDeckControlConnections('[Channel' + deckNumber + ']', true);

        if (deckNumber <= 2) {
            deckNumber += 2;
        } else {
            deckNumber -= 2;
        }

        PioneerDDJSB.deckSwitchTable[group] = '[Channel' + deckNumber + ']';
        PioneerDDJSB.initDeck(PioneerDDJSB.deckSwitchTable[group]);
        PioneerDDJSB.setDeckSoftTakeover(PioneerDDJSB.deckSwitchTable[group], true);
        PioneerDDJSB.setDeckSoftTakeover(PioneerDDJSB.deckSwitchTable[group]);
    }
};


///////////////////////////////////////////////////////////////
//            HIGH RESOLUTION MIDI INPUT HANDLERS            //
///////////////////////////////////////////////////////////////

PioneerDDJSB.highResMSB = {
    '[Channel1]': {},
    '[Channel2]': {},
    '[Channel3]': {},
    '[Channel4]': {}
};

PioneerDDJSB.tempoSliderMSB = function (channel, control, value, status, group) {
    PioneerDDJSB.highResMSB[group].tempoSlider = value;
};

PioneerDDJSB.tempoSliderLSB = function (channel, control, value, status, group) {
    var fullValue = (PioneerDDJSB.highResMSB[group].tempoSlider << 7) + value;
    engine.setValue(
        PioneerDDJSB.deckSwitchTable[group],
        'rate',
        ((0x4000 - fullValue) - 0x2000) / 0x2000
    );
};

PioneerDDJSB.gainKnobMSB = function (channel, control, value, status, group) {
    PioneerDDJSB.highResMSB[group].gainKnob = value;
};

PioneerDDJSB.gainKnobLSB = function (channel, control, value, status, group) {
    var fullValue = (PioneerDDJSB.highResMSB[group].gainKnob << 7) + value;
    engine.setValue(
        PioneerDDJSB.deckSwitchTable[group],
        'pregain',
        script.absoluteNonLin(fullValue, 0.0, 1.0, 4.0, 0, 0x3FFF)
    );
};

PioneerDDJSB.filterHighKnobMSB = function (channel, control, value, status, group) {
    PioneerDDJSB.highResMSB[group].filterHigh = value;
};

PioneerDDJSB.filterHighKnobLSB = function (channel, control, value, status, group) {
    var fullValue = (PioneerDDJSB.highResMSB[group].filterHigh << 7) + value;
    engine.setValue(
        PioneerDDJSB.deckSwitchTable[group],
        'filterHigh',
        script.absoluteNonLin(fullValue, 0.0, 1.0, 4.0, 0, 0x3FFF)
    );
};

PioneerDDJSB.filterMidKnobMSB = function (channel, control, value, status, group) {
    PioneerDDJSB.highResMSB[group].filterMid = value;
};

PioneerDDJSB.filterMidKnobLSB = function (channel, control, value, status, group) {
    var fullValue = (PioneerDDJSB.highResMSB[group].filterMid << 7) + value;
    engine.setValue(
        PioneerDDJSB.deckSwitchTable[group],
        'filterMid',
        script.absoluteNonLin(fullValue, 0.0, 1.0, 4.0, 0, 0x3FFF))
    ;
};

PioneerDDJSB.filterLowKnobMSB = function (channel, control, value, status, group) {
    PioneerDDJSB.highResMSB[group].filterLow = value;
};

PioneerDDJSB.filterLowKnobLSB = function (channel, control, value, status, group) {
    var fullValue = (PioneerDDJSB.highResMSB[group].filterLow << 7) + value;
    engine.setValue(
        PioneerDDJSB.deckSwitchTable[group],
        'filterLow',
        script.absoluteNonLin(fullValue, 0.0, 1.0, 4.0, 0, 0x3FFF)
    );
};

PioneerDDJSB.deckFaderMSB = function (channel, control, value, status, group) {
    PioneerDDJSB.highResMSB[group].deckFader = value;
};

PioneerDDJSB.deckFaderLSB = function (channel, control, value, status, group) {
    var fullValue = (PioneerDDJSB.highResMSB[group].deckFader << 7) + value;
    if (PioneerDDJSB.shiftPressed &&
        engine.getValue(PioneerDDJSB.deckSwitchTable[group], 'volume') === 0 &&
        fullValue !== 0 &&
        engine.getValue(PioneerDDJSB.deckSwitchTable[group], 'play') === 0
    ) {
        PioneerDDJSB.chFaderStart[channel] = engine.getValue(PioneerDDJSB.deckSwitchTable[group], 'playposition');
        engine.setValue(PioneerDDJSB.deckSwitchTable[group], 'play', 1);
    }
    else if (
        PioneerDDJSB.shiftPressed &&
        engine.getValue(PioneerDDJSB.deckSwitchTable[group], 'volume') !== 0 &&
        fullValue === 0 &&
        engine.getValue(PioneerDDJSB.deckSwitchTable[group], 'play') === 1 &&
        PioneerDDJSB.chFaderStart[channel] !== null
    ) {
        engine.setValue(PioneerDDJSB.deckSwitchTable[group], 'play', 0);
        engine.setValue(PioneerDDJSB.deckSwitchTable[group], 'playposition', PioneerDDJSB.chFaderStart[channel]);
        PioneerDDJSB.chFaderStart[channel] = null;
    }
    engine.setValue(PioneerDDJSB.deckSwitchTable[group], 'volume', fullValue / 0x3FFF);
};

PioneerDDJSB.filterKnobMSB = function (channel, control, value, status, group) {
    PioneerDDJSB.highResMSB[group].filterKnob = value;
};

PioneerDDJSB.filterKnobLSB = function (channel, control, value, status, group) {
    var fullValue = (PioneerDDJSB.highResMSB[group].filterKnob << 7) + value;
    if (PioneerDDJSB.shiftPressed) {
        engine.setValue(
            PioneerDDJSB.deckSwitchTable[group],
            'pregain',
            script.absoluteNonLin(fullValue, 0.0, 1.0, 4.0, 0, 0x3FFF)
        );
    } else {
        engine.setValue('[QuickEffectRack1_' + PioneerDDJSB.deckSwitchTable[group] + ']', 'super1', fullValue / 0x3FFF);
    }
};


///////////////////////////////////////////////////////////////
//           SINGLE MESSAGE MIDI INPUT HANDLERS              //
///////////////////////////////////////////////////////////////

PioneerDDJSB.shiftButton = function (channel, control, value, status, group) {
    PioneerDDJSB.shiftPressed = (value == 0x7F);
    for (index in PioneerDDJSB.chFaderStart) {
        PioneerDDJSB.chFaderStart[index] = null;
    }
};

PioneerDDJSB.playButton = function (channel, control, value, status, group) {
    if (value) {
        script.toggleControl(PioneerDDJSB.deckSwitchTable[group], 'play');
    }
};

PioneerDDJSB.headphoneCueButton = function (channel, control, value, status, group) {
    if (value) {
        script.toggleControl(PioneerDDJSB.deckSwitchTable[group], 'pfl');
    }
};

PioneerDDJSB.hotCueButtons = function (channel, control, value, status, group) {
    var hotCueIndex = (control >= 0x40 ? control - 0x40 + 5 : control + 1);
    engine.setValue(PioneerDDJSB.deckSwitchTable[group], 'hotcue_' + hotCueIndex + '_activate', value);
};

PioneerDDJSB.clearHotCueButtons = function (channel, control, value, status, group) {
    var hotCueIndex = (control >= 0x48 ? control - 0x48 + 5 : control - 7);
    if (value) {
        engine.setValue(PioneerDDJSB.deckSwitchTable[group], 'hotcue_' + hotCueIndex + '_clear', 1);
    }
};

PioneerDDJSB.cueButton = function (channel, control, value, status, group) {
    engine.setValue(PioneerDDJSB.deckSwitchTable[group], 'cue_default', value);
};

PioneerDDJSB.beatloopButtons = function (channel, control, value, status, group) {
    var index = (control <= 0x13 ? control - 0x10 : control - 0x14);
    if (value) {
        engine.setValue(
            PioneerDDJSB.deckSwitchTable[group],
            'beatloop_' + PioneerDDJSB.loopIntervals[index] + '_toggle',
            1
        );
    }
};

PioneerDDJSB.beatloopRollButtons = function (channel, control, value, status, group) {
    var index = (control <= 0x53 ? control - 0x50 : control - 0x54);
    engine.setValue(
        PioneerDDJSB.deckSwitchTable[group],
        'beatlooproll_' + PioneerDDJSB.looprollIntervals[index] + '_activate',
        value
    );
};

PioneerDDJSB.vinylButton = function (channel, control, value, status, group) {
    if (PioneerDDJSB.invertVinylSlipButton) {
        PioneerDDJSB.toggleSlip(channel, control, value, status, group);
    } else {
        PioneerDDJSB.toggleScratch(channel, control, value, status, group);
    }
};

PioneerDDJSB.slipButton = function (channel, control, value, status, group) {
    if (PioneerDDJSB.invertVinylSlipButton) {
        PioneerDDJSB.toggleScratch(channel, control, value, status, group);
    } else {
        PioneerDDJSB.toggleSlip(channel, control, value, status, group);
    }
};

PioneerDDJSB.toggleSlip = function (channel, control, value, status, group) {
    if (value) {
        script.toggleControl(PioneerDDJSB.deckSwitchTable[group], 'slip_enabled');
    }
};

PioneerDDJSB.keyLockButton = function (channel, control, value, status, group) {
    if (value) {
        script.toggleControl(PioneerDDJSB.deckSwitchTable[group], 'keylock');
    }
};

PioneerDDJSB.loopInButton = function (channel, control, value, status, group) {
    engine.setValue(PioneerDDJSB.deckSwitchTable[group], 'loop_in', value ? 1 : 0);
};

PioneerDDJSB.loopOutButton = function (channel, control, value, status, group) {
    engine.setValue(PioneerDDJSB.deckSwitchTable[group], 'loop_out', value ? 1 : 0);
};

PioneerDDJSB.loopExitButton = function (channel, control, value, status, group) {
    if (value) {
        engine.setValue(PioneerDDJSB.deckSwitchTable[group], 'reloop_exit', 1);
    }
};

PioneerDDJSB.loopHalveButton = function (channel, control, value, status, group) {
    engine.setValue(PioneerDDJSB.deckSwitchTable[group], 'loop_halve', value ? 1 : 0);
};

PioneerDDJSB.loopDoubleButton = function (channel, control, value, status, group) {
    engine.setValue(PioneerDDJSB.deckSwitchTable[group], 'loop_double', value ? 1 : 0);
};

PioneerDDJSB.loopMoveBackButton = function (channel, control, value, status, group) {
    if (value) {
        engine.setValue(PioneerDDJSB.deckSwitchTable[group], 'loop_move', -1);
    }
};

PioneerDDJSB.loopMoveForwardButton = function (channel, control, value, status, group) {
    if (value) {
        engine.setValue(PioneerDDJSB.deckSwitchTable[group], 'loop_move', 1);
    }
};

PioneerDDJSB.loadButton = function (channel, control, value, status, group) {
    if (value) {
        engine.setValue(PioneerDDJSB.deckSwitchTable[group], 'LoadSelectedTrack', 1);
    }
};

PioneerDDJSB.reverseRollButton = function (channel, control, value, status, group) {
    engine.setValue(PioneerDDJSB.deckSwitchTable[group], 'reverseroll', value);
};

PioneerDDJSB.brakeButton = function (channel, control, value, status, group) {
    script.brake(channel, control, value, status, PioneerDDJSB.deckSwitchTable[group]);
};

PioneerDDJSB.syncButton = function (channel, control, value, status, group) {
    if (value) {
        script.toggleControl(PioneerDDJSB.deckSwitchTable[group], 'sync_enabled');
    }
};

PioneerDDJSB.quantizeButton = function (channel, control, value, status, group) {
    if (value) {
        script.toggleControl(PioneerDDJSB.deckSwitchTable[group], 'quantize');
    }
};

PioneerDDJSB.lowKillButton = function (channel, control, value, status, group) {
    engine.setValue(PioneerDDJSB.deckSwitchTable[group], 'filterLowKill', value ? 1 : 0);
};

PioneerDDJSB.midKillButton = function (channel, control, value, status, group) {
    engine.setValue(PioneerDDJSB.deckSwitchTable[group], 'filterMidKill', value ? 1 : 0);
};

PioneerDDJSB.highKillButton = function (channel, control, value, status, group) {
    engine.setValue(PioneerDDJSB.deckSwitchTable[group], 'filterHighKill', value ? 1 : 0);
};

PioneerDDJSB.muteButton = function (channel, control, value, status, group) {
    engine.setValue(PioneerDDJSB.deckSwitchTable[group], 'mute', value);
};


///////////////////////////////////////////////////////////////
//                          LED HELPERS                      //
///////////////////////////////////////////////////////////////

PioneerDDJSB.deckConverter = function (group) {
    var index;

    if (typeof group === "string") {
        for (index in PioneerDDJSB.deckSwitchTable) {
            if (group === PioneerDDJSB.deckSwitchTable[index]) {
                return PioneerDDJSB.channelGroups[group] % 2;
            }
        }
        return null;
    }
    return group % 2;
};

PioneerDDJSB.fxLedControl = function (deck, ledNumber, shift, active) {
    var fxLedsBaseChannel = 0x94,
        fxLedsBaseControl = (shift ? 0x63 : 0x47),
        midiChannelOffset = PioneerDDJSB.deckConverter(deck);

    if (midiChannelOffset !== null) {
        midi.sendShortMsg(
            fxLedsBaseChannel + midiChannelOffset,
            fxLedsBaseControl + ledNumber,
            active ? 0x7F : 0x00
        );
    }
};

PioneerDDJSB.padLedControl = function (deck, groupNumber, shiftGroup, ledNumber, shift, active) {
    var padLedsBaseChannel = 0x97,
        padLedControl = (shiftGroup ? 0x40 : 0x00) + (shift ? 0x08 : 0x00) + (+groupNumber) + (+ledNumber),
        midiChannelOffset = PioneerDDJSB.deckConverter(deck);

    if (midiChannelOffset !== null) {
        midi.sendShortMsg(
            padLedsBaseChannel + midiChannelOffset,
            padLedControl,
            active ? 0x7F : 0x00
        );
    }
};

PioneerDDJSB.nonPadLedControl = function (deck, ledNumber, active) {
    var nonPadLedsBaseChannel = 0x90,
        midiChannelOffset = PioneerDDJSB.deckConverter(deck);

    if (midiChannelOffset !== null) {
        midi.sendShortMsg(
            nonPadLedsBaseChannel + midiChannelOffset,
            ledNumber,
            active ? 0x7F : 0x00
        );
    }
};


///////////////////////////////////////////////////////////////
//                             LEDS                          //
///////////////////////////////////////////////////////////////

PioneerDDJSB.fxLeds = function (value, group, control) {
    var deck = PioneerDDJSB.fxGroups[group],
        ledNumber = PioneerDDJSB.fxControls[control];

    PioneerDDJSB.fxLedControl(deck, ledNumber, false, value);
    PioneerDDJSB.fxLedControl(deck, ledNumber, true, value);
};

PioneerDDJSB.headphoneCueLed = function (value, group, control) {
    PioneerDDJSB.nonPadLedControl(group, PioneerDDJSB.nonPadLeds.headphoneCue, value);
    PioneerDDJSB.nonPadLedControl(group, PioneerDDJSB.nonPadLeds.shiftHeadphoneCue, value);
};

PioneerDDJSB.keyLockLed = function (value, group, control) {
    PioneerDDJSB.nonPadLedControl(group, PioneerDDJSB.nonPadLeds.keyLock, value);
};

PioneerDDJSB.playLeds = function (value, group, control) {
    PioneerDDJSB.nonPadLedControl(group, PioneerDDJSB.nonPadLeds.play, value);
    PioneerDDJSB.nonPadLedControl(group, PioneerDDJSB.nonPadLeds.shiftPlay, value);
    PioneerDDJSB.nonPadLedControl(group, PioneerDDJSB.nonPadLeds.cue, value);
    PioneerDDJSB.nonPadLedControl(group, PioneerDDJSB.nonPadLeds.shiftCue, value);
};

PioneerDDJSB.slipLed = function (value, group, control) {
    var led = (PioneerDDJSB.invertVinylSlipButton ? PioneerDDJSB.nonPadLeds.vinyl : PioneerDDJSB.nonPadLeds.shiftVinyl);
    PioneerDDJSB.nonPadLedControl(group, led, value);
};

PioneerDDJSB.quantizeLed = function (value, group, control) {
    PioneerDDJSB.nonPadLedControl(group, PioneerDDJSB.nonPadLeds.shiftSync, value);
};

PioneerDDJSB.syncLed = function (value, group, control) {
    PioneerDDJSB.nonPadLedControl(group, PioneerDDJSB.nonPadLeds.sync, value);
};

PioneerDDJSB.loopInLed = function (value, group, control) {
    PioneerDDJSB.padLedControl(group, PioneerDDJSB.ledGroups.manualLoop, false, 0, false, value);
};

PioneerDDJSB.loopOutLed = function (value, group, control) {
    PioneerDDJSB.padLedControl(group, PioneerDDJSB.ledGroups.manualLoop, false, 1, false, value);
};

PioneerDDJSB.loopExitLed = function (value, group, control) {
    PioneerDDJSB.padLedControl(group, PioneerDDJSB.ledGroups.manualLoop, false, 2, false, value);
};

PioneerDDJSB.loopHalveLed = function (value, group, control) {
    PioneerDDJSB.padLedControl(group, PioneerDDJSB.ledGroups.manualLoop, false, 3, false, value);
};

PioneerDDJSB.loopDoubleLed = function (value, group, control) {
    PioneerDDJSB.padLedControl(group, PioneerDDJSB.ledGroups.manualLoop, false, 3, true, value);
};

PioneerDDJSB.lowKillLed = function (value, group, control) {
    PioneerDDJSB.padLedControl(group, PioneerDDJSB.ledGroups.manualLoop, true, 0, false, value);
};

PioneerDDJSB.midKillLed = function (value, group, control) {
    PioneerDDJSB.padLedControl(group, PioneerDDJSB.ledGroups.manualLoop, true, 1, false, value);
};

PioneerDDJSB.highKillLed = function (value, group, control) {
    PioneerDDJSB.padLedControl(group, PioneerDDJSB.ledGroups.manualLoop, true, 2, false, value);
};

PioneerDDJSB.muteLed = function (value, group, control) {
    PioneerDDJSB.padLedControl(group, PioneerDDJSB.ledGroups.manualLoop, true, 3, false, value);
};

PioneerDDJSB.samplerLeds = function (value, group, control) {
    var sampler = PioneerDDJSB.samplerGroups[group],
        channel;

    for (channel = 0; channel < 2; channel++) {
        PioneerDDJSB.padLedControl(channel, PioneerDDJSB.ledGroups.sampler, false, sampler, false, value);
        PioneerDDJSB.padLedControl(channel, PioneerDDJSB.ledGroups.sampler, false, sampler, true, value);
        PioneerDDJSB.padLedControl(channel, PioneerDDJSB.ledGroups.sampler, true, sampler, false, value);
        PioneerDDJSB.padLedControl(channel, PioneerDDJSB.ledGroups.sampler, true, sampler, true, value);
    }
};

PioneerDDJSB.beatloopLeds = function (value, group, control) {
    var index,
        padNum,
        shifted;

    for (index in PioneerDDJSB.loopIntervals) {
        if (control === 'beatloop_' + PioneerDDJSB.loopIntervals[index] + '_enabled') {
            padNum = index % 4;
            shifted = (index >= 4);
            PioneerDDJSB.padLedControl(group, PioneerDDJSB.ledGroups.autoLoop, false, padNum, shifted, value);
        }
    }
};

PioneerDDJSB.beatlooprollLeds = function (value, group, control) {
    var index,
        padNum,
        shifted;

    for (index in PioneerDDJSB.looprollIntervals) {
        if (control === 'beatlooproll_' + PioneerDDJSB.looprollIntervals[index] + '_activate') {
            padNum = index % 4;
            shifted = (index >= 4);
            PioneerDDJSB.padLedControl(group, PioneerDDJSB.ledGroups.autoLoop, true, padNum, shifted, value);
        }
    }
};

PioneerDDJSB.hotCueLeds = function (value, group, control) {
    var shiftedGroup = false,
        padNum = null,
        hotCueNum;

    for (hotCueNum = 1; hotCueNum <= 8; hotCueNum++) {
        if (control === 'hotcue_' + hotCueNum + '_enabled') {
            padNum = (hotCueNum - 1) % 4;
            shiftedGroup = (hotCueNum > 4);
            PioneerDDJSB.padLedControl(group, PioneerDDJSB.ledGroups.hotCue, shiftedGroup, padNum, false, value);
            PioneerDDJSB.padLedControl(group, PioneerDDJSB.ledGroups.hotCue, shiftedGroup, padNum, true, value);
        }
    }
};


///////////////////////////////////////////////////////////////
//                          JOGWHEELS                        //
///////////////////////////////////////////////////////////////

// Work out the jog-wheel change / delta
PioneerDDJSB.getJogWheelDelta = function (value) {
    // The Wheel control centers on 0x40; find out how much it's moved by.
    return value - 0x40;
};

PioneerDDJSB.jogRingTick = function (channel, control, value, status, group) {
    PioneerDDJSB.pitchBendFromJog(PioneerDDJSB.deckSwitchTable[group], PioneerDDJSB.getJogWheelDelta(value));
};

PioneerDDJSB.jogRingTickShift = function (channel, control, value, status, group) {
    PioneerDDJSB.pitchBendFromJog(
        PioneerDDJSB.deckSwitchTable[group],
        PioneerDDJSB.getJogWheelDelta(value) * PioneerDDJSB.jogwheelShiftMultiplier
    );
};

PioneerDDJSB.jogPlatterTick = function (channel, control, value, status, group) {
    var deck = PioneerDDJSB.channelGroups[PioneerDDJSB.deckSwitchTable[group]];
    if (PioneerDDJSB.scratchMode[deck]) {
        engine.scratchTick(deck + 1, PioneerDDJSB.getJogWheelDelta(value));
    } else {
        PioneerDDJSB.pitchBendFromJog(PioneerDDJSB.deckSwitchTable[group], PioneerDDJSB.getJogWheelDelta(value));
    }
};

PioneerDDJSB.jogPlatterTickShift = function (channel, control, value, status, group) {
    var deck = PioneerDDJSB.channelGroups[PioneerDDJSB.deckSwitchTable[group]];
    if (PioneerDDJSB.scratchMode[deck]) {
        engine.scratchTick(deck + 1, PioneerDDJSB.getJogWheelDelta(value));
    } else {
        PioneerDDJSB.pitchBendFromJog(
            PioneerDDJSB.deckSwitchTable[group],
                PioneerDDJSB.getJogWheelDelta(value) * PioneerDDJSB.jogwheelShiftMultiplier
        );
    }
};

PioneerDDJSB.jogTouch = function (channel, control, value, status, group) {
    var deck = PioneerDDJSB.channelGroups[PioneerDDJSB.deckSwitchTable[group]];
    if (PioneerDDJSB.scratchMode[deck]) {
        if (value) {
            engine.scratchEnable(
                deck + 1,
                PioneerDDJSB.scratchSettings.jogResolution,
                PioneerDDJSB.scratchSettings.vinylSpeed,
                PioneerDDJSB.scratchSettings.alpha,
                PioneerDDJSB.scratchSettings.beta,
                true
            );
        } else {
            engine.scratchDisable(deck + 1, true);
        }
    }
};

PioneerDDJSB.toggleScratch = function (channel, control, value, status, group) {
    var deck = PioneerDDJSB.channelGroups[PioneerDDJSB.deckSwitchTable[group]];
    if (value) {
        PioneerDDJSB.scratchMode[deck] = !PioneerDDJSB.scratchMode[deck];
        PioneerDDJSB.triggerVinylLed(deck);
        if (!PioneerDDJSB.scratchMode[deck]) {
            engine.scratchDisable(deck + 1, true);
        }
    }
};

PioneerDDJSB.triggerVinylLed = function (deck) {
    var led = (PioneerDDJSB.invertVinylSlipButton ? PioneerDDJSB.nonPadLeds.shiftVinyl : PioneerDDJSB.nonPadLeds.vinyl);
    PioneerDDJSB.nonPadLedControl(deck % 2, led, PioneerDDJSB.scratchMode[deck]);
};

PioneerDDJSB.pitchBendFromJog = function (channel, movement) {
    var group = (typeof channel === "string" ? channel : '[Channel' + channel + 1 + ']');
    engine.setValue(group, 'jog', movement / 5 * PioneerDDJSB.jogwheelSensitivity);
};


///////////////////////////////////////////////////////////////
//                        ROTARY SELECTOR                    //
///////////////////////////////////////////////////////////////
// Handles the rotary selector for choosing tracks, library items, crates, etc.

PioneerDDJSB.getRotaryDelta = function (value) {
    var delta = 0x40 - Math.abs(0x40 - value),
        isCounterClockwise = value > 0x40;

    if (isCounterClockwise) {
        delta *= -1;
    }
    return delta;
};

PioneerDDJSB.rotarySelector = function (channel, control, value, status) {
    var delta = PioneerDDJSB.getRotaryDelta(value);
    engine.setValue('[Playlist]', 'SelectTrackKnob', delta);
};

PioneerDDJSB.shiftedRotarySelector = function (channel, control, value, status) {
    var delta = PioneerDDJSB.getRotaryDelta(value),
        f = (delta > 0 ? 'SelectNextPlaylist' : 'SelectPrevPlaylist');

    engine.setValue('[Playlist]', f, Math.abs(delta));
};

PioneerDDJSB.rotarySelectorShiftedClick = function (channel, control, value, status) {
    if (value) {
        engine.setValue('[Playlist]', 'ToggleSelectedSidebarItem', 1);
    }
};


///////////////////////////////////////////////////////////////
//                             FX                            //
///////////////////////////////////////////////////////////////

PioneerDDJSB.fxKnobMSB = [0, 0];
PioneerDDJSB.fxKnobShiftedMSB = [0, 0];

PioneerDDJSB.fxButton = function (channel, control, value, status, group) {
    var deck = channel - 4,
        button = control - 0x47,
        channel = PioneerDDJSB.deckSwitchTable['[Channel' + (button === 0 ? 1 : 2) + ']'];

    PioneerDDJSB.fxButtonPressed[deck][button] = (value === 0x7F);

    if (button === 1) {
        engine.trigger(group, 'group_[Headphone]_enable');
    } else {
        engine.trigger(group, 'group_' + channel + '_enable');
    }
};

PioneerDDJSB.fxButtonShifted = function (channel, control, value, status, group) {
    var button = control - 0x63,
        channel = PioneerDDJSB.deckSwitchTable['[Channel' + (button === 0 ? 1 : 2) + ']'];

    if (value) {
        if (button === 1) {
            script.toggleControl(group, 'group_[Headphone]_enable');
        } else {
            script.toggleControl(group, 'group_' + channel + '_enable');
        }
    }
};

PioneerDDJSB.fxKnobShiftedMSB = function (channel, control, value, status) {
    PioneerDDJSB.fxKnobShiftedMSB[channel - 4] = value;
};

PioneerDDJSB.fxKnobShiftedLSB = function (channel, control, value, status) {
    var deck = channel - 4,
        fullValue = (PioneerDDJSB.fxKnobShiftedMSB[deck] << 7) + value;

    if (PioneerDDJSB.softTakeoverEmulation(deck, 4, PioneerDDJSB.fxKnobShiftedMSB[deck])) {
        engine.setValue('[EffectRack1_EffectUnit' + (deck + 1) + ']', 'super1', fullValue / 0x3FFF);
    }
};

PioneerDDJSB.fxKnobMSB = function (channel, control, value, status) {
    PioneerDDJSB.fxKnobMSB[channel - 4] = value;
};

PioneerDDJSB.fxKnobLSB = function (channel, control, value, status) {
    var deck = channel - 4,
        anyButtonPressed = false,
        fullValue = (PioneerDDJSB.fxKnobMSB[deck] << 7) + value,
        parameter;

    for (parameter = 0; parameter < 3; parameter++) {
        if (PioneerDDJSB.fxButtonPressed[deck][parameter]) {
            anyButtonPressed = true;
        }
    }

    if (!anyButtonPressed) {
        if (PioneerDDJSB.softTakeoverEmulation(deck, 3, PioneerDDJSB.fxKnobMSB[deck])) {
            engine.setValue('[EffectRack1_EffectUnit' + (deck + 1) + ']', 'mix', fullValue / 0x3FFF);
        }
    } else {
        for (parameter = 0; parameter < 3; parameter++) {
            if (PioneerDDJSB.fxButtonPressed[deck][parameter] && PioneerDDJSB.softTakeoverEmulation(deck, parameter, PioneerDDJSB.fxKnobMSB[deck])) {
                engine.setParameter(
                    '[EffectRack1_EffectUnit' + (deck + 1) + '_Effect1]',
                    'parameter' + (parameter + 1),
                    fullValue / 0x3FFF
                );
            }
        }
    }
};

PioneerDDJSB.softTakeoverEmulation = function (deck, index, currentValue) {
    var deltaToActive = currentValue - PioneerDDJSB.fxParamsActiveValues[deck][index];

    if (Math.abs(deltaToActive) < 15) {
        PioneerDDJSB.fxParamsActiveValues[deck][index] = currentValue;
        return true;
    }
    return false;
};