function HerculesMk2 () {}

HerculesMk2.ledOn = 0x7F;
HerculesMk2.ledOff = 0x00;

HerculesMk2.debug = false;
HerculesMk2.scratchMode = false;
HerculesMk2.decayLast = new Date().getTime();
HerculesMk2.decayInterval = 300;
HerculesMk2.decayRate = 1.5;

HerculesMk2.buttons123Modes = ["kill", "fx", "cue", "loop"];
HerculesMk2.buttons123used = {"[Channel1]": false, "[Channel1]": false};

// TODO HerculesMk2 controls should be divided into groups, then signals 
// should directed to each group without thinking about specific controls 
// to allow for easy rebinding.

HerculesMk2.cueLastDate1 = {"[Channel1]": 0, "[Channel2]" : 0};
HerculesMk2.cueLastDate2 = {"[Channel1]": 0, "[Channel2]" : 0};
HerculesMk2.cueLastDate3 = {"[Channel1]": 0, "[Channel2]" : 0};
HerculesMk2.cueLongTime = 500;

HerculesMk2.controls = {
    "inputs": {
    0x09: { "channel": 1, "name": "cue", "type": "button" },
    0x03: { "channel": 2, "name": "cue", "type": "button" },
    0x08: { "channel": 1, "name": "play", "type": "button" },
    0x02: { "channel": 2, "name": "play", "type": "button" },
    0x07: { "channel": 1, "name": "fx select", "type": "button", 
        "mode": 0 },
    0x01: { "channel": 2, "name": "fx select", "type": "button", 
        "mode": 0 },
    0x0F: { "channel": 1, "name": "fx 1", "type": "button", "used": false },
    0x10: { "channel": 2, "name": "fx 1", "type": "button", "used": false },
    0x0E: { "channel": 1, "name": "fx 2", "type": "button", "used": false },
    0x11: { "channel": 2, "name": "fx 2", "type": "button", "used": false },
    0x0D: { "channel": 1, "name": "fx 3", "type": "button", "used": false },
    0x12: { "channel": 2, "name": "fx 3", "type": "button", "used": false },
    0x1B: { "channel": 1, "name": "mouse", "type": "button" },
    0x1C: { "channel": 2, "name": "mouse", "type": "button" },
    0x34: { "channel": 1, "name": "pitch", "type": "pot" },
    0x35: { "channel": 2, "name": "pitch", "type": "pot" },
    0x36: { "channel": 1, "name": "wheel", "type": "pot" },
    0x37: { "channel": 2, "name": "wheel", "type": "pot" }
    },
    "outputs": {
    0x0F: { "channel": 1, "name": "fx mode", "type": "led" },
    0x10: { "channel": 2, "name": "fx mode", "type": "led" },
    0x0E: { "channel": 1, "name": "cue mode", "type": "led" },
    0x11: { "channel": 2, "name": "cue mode", "type": "led" },
    0x0D: { "channel": 1, "name": "loop mode", "type": "led" },
    0x12: { "channel": 2, "name": "loop mode", "type": "led" },
    0x16: { "channel": 1, "name": "master tempo", "type": "led" },
    0x1A: { "channel": 2, "name": "master tempo", "type": "led" },
    0x0A: { "channel": 1, "name": "auto beat", "type": "led" },
    0x04: { "channel": 2, "name": "auto beat", "type": "led" },
    0x09: { "channel": 1, "name": "cue", "type": "led" },
    0x03: { "channel": 2, "name": "cue", "type": "led" },
    0x00: { "channel": 1, "name": "play blink", "type": "led" },
    0x05: { "channel": 2, "name": "play blink", "type": "led" },
    0x08: { "channel": 1, "name": "play", "type": "led" },
    0x02: { "channel": 2, "name": "play", "type": "led" }
    }
};

HerculesMk2.leds = { 
};

HerculesMk2.init = function (id) { // called when the device is opened & set up
    HerculesMk2.initializeControls();
    
    engine.connectControl("[Channel1]","playposition","HerculesMk2.wheelDecay");
    engine.connectControl("[Channel2]","playposition","HerculesMk2.wheelDecay");
    
    print ("HerculesMk2 id: \""+id+"\" initialized.");
};
    
HerculesMk2.initializeControls = function () {
    for (control in HerculesMk2.controls.outputs) {
    if (HerculesMk2.controls.outputs[control].type == 'led') {
        key = "[Channel" + HerculesMk2.controls.outputs[control].channel + "] " + HerculesMk2.controls.outputs[control].name;
        HerculesMk2.leds[key] = control;
    }
    }

    HerculesMk2.setLeds("on");
    HerculesMk2.setLeds("off");
    
    // Set controls in Mixxx to reflect settings on the device
    midi.sendShortMsg(0xB0,0x7F,0x7F);
};

HerculesMk2.shutdown = function (id) {
    HerculesMk2.setLeds("off");
};

HerculesMk2.getGroup = function (control){ 
    // Get the "group" that used to be provided in group, this is not reusable 
    // across devices and also breaks remapping of these functions to other 
    // buttons.  

    return "[Channel" + HerculesMk2.controls.inputs[control].channel + "]";
};

HerculesMk2.getControl = function (io, channel, name) { 
    // Accept channel in form 'N' or '[ChannelN]'
    channel = channel.replace(/\[Channel(\d)\]/, "$1");

    for (control in HerculesMk2.controls.inputs) {
    if (HerculesMk2.controls.inputs[control].channel == channel && 
        HerculesMk2.controls.inputs[control].name == name
        ) return HerculesMk2.controls.inputs[control];
    }

    print ("HerculesMk2.getControl: Control not found: io=" + io + ": channel=" + channel + ": name=" + name);
};

HerculesMk2.setLeds = function (onOff) {
    for (LED in HerculesMk2.leds) {
    HerculesMk2.setLed(LED,onOff);
    // Seems that if midi messages are sent too quickly, leds don't behave
    // as expected. A pause rectifies this.
    HerculesMk2.pauseScript(10);
    }
};

HerculesMk2.setLed = function (led, onOff) {
    value = onOff=="on" ?  HerculesMk2.ledOn : HerculesMk2.ledOff;
    if (HerculesMk2.debug) print ("HerculesMk2.setLed: Setting " + led + " led " + onOff);
    if (HerculesMk2.debug) print ("HerculesMk2.setLed: midi.sendShortMsg(0xB0," + HerculesMk2.leds[led].toString(16) + "," + value + ")");
    midi.sendShortMsg(0xB0,HerculesMk2.leds[led],value);
    HerculesMk2.controls.outputs[HerculesMk2.leds[led]].isOn = onOff=="on" ? true : false;
};

HerculesMk2.pauseScript = function(ms) {
    startDate = new Date();
    currentDate = null;

    while(currentDate-startDate < ms) currentDate = new Date();
};

HerculesMk2.pfl = function (group, control, value, status) {
    if (value) { // Act on given mode being selected, not deselected. 
    if (HerculesMk2.debug) print("HerculesMk2.pfl: " + control.toString(16) + " " + value.toString(16));
    switch (control) {
    case 0x23: // Headphones Split
    case 0x24: // Headphones Mix
        if (HerculesMk2.debug) print("HerculesMk2.pfl: Mix/Split");
        engine.setValue("[Channel1]", "pfl", 1);
        engine.setValue("[Channel2]", "pfl", 1);
        break;
    case 0x21: // Headphones Deck A
        if (HerculesMk2.debug) print("HerculesMk2.pfl: Deck A");
        engine.setValue("[Channel1]", "pfl", 1);        
        engine.setValue("[Channel2]", "pfl", 0);
        break;
    case 0x22: // Headphones Deck B
        if (HerculesMk2.debug) print("HerculesMk2.pfl: Deck B");
        engine.setValue("[Channel1]", "pfl", 0);        
        engine.setValue("[Channel2]", "pfl", 1);        
    }
    }
};

HerculesMk2.cue = function (group, control, value, status) {
    group = HerculesMk2.getGroup(control);
    
    if ((engine.getValue(group, "duration") == 0) && (value)) { 
    print("No song on " + group);
    return; 
    }
    
    if (value) { // Down
    engine.setValue(group,"cue_default",1);
    HerculesMk2.setLed(group + " cue", "on");
    } else { // Release
    engine.setValue(group,"cue_default",0);
    }
};

HerculesMk2.play = function (group, control, value, status) {
    if (value) { // Only do stuff when play is pushed, not released.
    group = HerculesMk2.getGroup(control);
    
    if (engine.getValue(group, "duration") == 0) { 
        print("No song on " + group);
        return; 
    }
    
    engine.setValue(group,"play", !engine.getValue(group,"play"));
    HerculesMk2.setLed(group + " cue", "off");
    }
};

HerculesMk2.loadSelectedTrack = function (group, control, value, status) {
    if (value) { // Only do stuff when pushed, not released.
    group = HerculesMk2.getGroup(control);
    engine.setValue(group, "LoadSelectedTrack", 1);
    HerculesMk2.setLed(group + " cue", "on");
    }
};

HerculesMk2.buttons123 = function (group, control, value, status) {
    group = HerculesMk2.getGroup(control);

    if (value) { // Button pressed.
    HerculesMk2.controls.inputs[control].isDown = true;
    } else { //Button released.
    HerculesMk2.controls.inputs[control].isDown = false;
    }

    mode = HerculesMk2.getControl("inputs", group, "fx select").mode;
    mode = HerculesMk2.buttons123Modes[mode];
    
    switch (mode) {

    case "kill": // Kill mode
    if (value) { // Button pressed.
        switch (HerculesMk2.controls.inputs[control].name) {
        case "fx 1":
        engine.setValue(group, "filterLowKill", !engine.getValue(group, "filterLowKill"));
        break;
        case "fx 2": 
        engine.setValue(group, "filterMidKill", !engine.getValue(group, "filterMidKill"));
        break;
        case "fx 3":
        engine.setValue(group, "filterHighKill", !engine.getValue(group, "filterHighKill"));
        break;
        } 
    }
    break; // End kill mode

    case "fx": // Fx mode
    // Were only turning off the flanger if any of the 123 buttons are
    // released, not pushed. This is so we can have any of the 123 buttons
    // held down, then use the pitch pot to modify the effect settings
    if (!value) { // Button released.
        if (HerculesMk2.controls.inputs[control].used) {
        // Button was used to turn the pitch control into a modifier 
        // for the effect settings, so don't go on and turn the flanger
        // on/off
        HerculesMk2.controls.inputs[control].used = false;
        return;
        }

        switch (HerculesMk2.controls.inputs[control].name) {
        case "fx 1":
        case "fx 2":
        case "fx 3":
        engine.setValue(group, "flanger", !engine.getValue(group, "flanger"));
        break;
        } 
    }
    break; // End fx mode

    case "cue": 
    if (value) { // onPress
        switch (HerculesMk2.controls.inputs[control].name) {
            case "fx 1":
            HerculesMk2.cueLastDate1[group] = new Date().getTime();
            break;
            case "fx 2":
            HerculesMk2.cueLastDate2[group] = new Date().getTime();
            break;
            case "fx 3":
            HerculesMk2.cueLastDate3[group] = new Date().getTime();
            break;
        }
    } else { // onRelease
        currentDate = new Date().getTime();
        switch (HerculesMk2.controls.inputs[control].name) {
            case "fx 1":
            if (currentDate - HerculesMk2.cueLastDate1[group] < HerculesMk2.cueLongTime) {
                engine.setValue(group,"hotcue_1_activate",1);
                engine.setValue(group,"hotcue_1_activate",0);
            } else {
                engine.setValue(group,"hotcue_1_clear",1);
                engine.setValue(group,"hotcue_1_clear",0);
            }
            break;
            case "fx 2":
            if (currentDate - HerculesMk2.cueLastDate2[group] < HerculesMk2.cueLongTime) {
                engine.setValue(group,"hotcue_2_activate",1);
                engine.setValue(group,"hotcue_2_activate",0);
            } else {
                engine.setValue(group,"hotcue_2_clear",1);
                engine.setValue(group,"hotcue_2_clear",0);
            }
            break;
            case "fx 3":
            if (currentDate - HerculesMk2.cueLastDate3[group] < HerculesMk2.cueLongTime) {
                engine.setValue(group,"hotcue_3_activate",1);
                engine.setValue(group,"hotcue_3_activate",0);
            } else {
                engine.setValue(group,"hotcue_3_clear",1);
                engine.setValue(group,"hotcue_3_clear",0);
            }
            break;
        }

    }
    break;

    case "loop":
        if (value) { // Button pressed.
            switch (HerculesMk2.controls.inputs[control].name) {
            case "fx 1": // "fx 1,2,3" should be globally renamed as said before?...
                // trigger loop in
                engine.setValue(group,"loop_in", !engine.getValue(group,"start")); // Am I correct?
                break;
            case "fx 2":
                // trigger loop out
                engine.setValue(group,"loop_out", !engine.getValue(group,"end")); // Am I correct?
                break;
            case "fx 3":
                // trigger loop exit
                engine.setValue(group,"reloop_exit", !engine.getValue(group,"loop")); // Am I correct?
                break;
           }
        }
    break;

    default: 
    print("HerculesMk2.buttons123: " + mode + " mode unsupported");
    }
};

HerculesMk2.buttons123mode = function (group, control, value, status) {
    group = HerculesMk2.getGroup(control);
    if (value) { // Only do stuff when pushed, not released.
    currentMode = HerculesMk2.controls.inputs[control].mode;
    nextMode = currentMode < HerculesMk2.buttons123Modes.length-1 ? currentMode+1 : 0;
    currentLed = group + " " + HerculesMk2.buttons123Modes[currentMode] + " mode";
    nextLed = group + " " + HerculesMk2.buttons123Modes[nextMode] + " mode";

    sNextMode = HerculesMk2.buttons123Modes[nextMode]; 
    switch (sNextMode) {
    case "kill":
    case "fx": 
        print("HerculesMk2.buttons123mode: Switching to " + sNextMode + " mode");
        break;
    case "cue": 
        print("HerculesMk2.buttons123mode: Switching to " + sNextMode + " mode");
        break;
    case "loop":
        print("HerculesMk2.buttons123mode: Switching to " + sNextMode + " mode");
        break;
    default:
        print("HerculesMk2.buttons123mode: Switching to " + sNextMode + " mode");
    }

    // Only turn on/off leds for non-zero modes as 0 is kill mode which
    // has no corresponding LED. i.e. all LEDs off for kill mode.
    if (currentMode) HerculesMk2.setLed(currentLed, "off");
    // Seems that if midi messages are sent too quickly, leds don't behave
    // as expected. A pause rectifies this.
    HerculesMk2.pauseScript(10);
    if (nextMode) HerculesMk2.setLed(nextLed, "on");

    HerculesMk2.controls.inputs[control].mode = nextMode;
    }
};

HerculesMk2.pitch = function (group, control, value, status) {
    //  7F > 40: CCW Slow > Fast - 127 > 64 
    //  01 > 3F: CW Slow > Fast - 0 > 63

    group = HerculesMk2.getGroup(control);
    pitchControl = HerculesMk2.getControl("inputs", group, "pitch");
    done = false;

    currentMode = HerculesMk2.getControl("inputs", group, "fx select").mode;
    currentMode = HerculesMk2.buttons123Modes[currentMode];

    // If in fx mode and one or more of buttons 123 are pressed, use pitch
    // pot to adjust the relevant flanger parameters instead of changing
    // the rate (as is normal operation for the pitch pot)

    if (currentMode == "fx") { 
    potStep = 25; // How many clicks round the pot from min to max values

    if (HerculesMk2.getControl("inputs", group, "fx 1").isDown) {
        min = 0; max = 1;
        increment = (max-min)/potStep;
        increment = (value <= 0x3F) ? increment : increment * -1;

        currentValue = engine.getValue("[Flanger]", "lfoDepth");
        newValue = currentValue + increment;
        newValue = newValue > max ? max : newValue < min ? min : newValue;
        if (HerculesMk2.debug) print ("HerculesMk2.pitch: value= " + newValue);
        if (newValue != currentValue) 
        engine.setValue("[Flanger]", "lfoDepth", newValue);
        
        HerculesMk2.getControl("inputs", group, "fx 1").used = true;
        done = true;
    } 

    if (HerculesMk2.getControl("inputs", group, "fx 2").isDown) {
        min = 50; max = 10000;
        increment = (max-min)/potStep;
        increment = (value <= 0x3F) ? increment : increment * -1;
    
        currentValue = engine.getValue("[Flanger]", "lfoDelay");
        newValue = currentValue + increment;
        newValue = newValue > max ? max : newValue < min ? min : newValue;
        if (HerculesMk2.debug) print ("HerculesMk2.pitch: value= " + newValue);
        if (newValue != currentValue) 
        engine.setValue("[Flanger]", "lfoDelay", newValue);
        
        HerculesMk2.getControl("inputs", group, "fx 2").used = true;
        done = true;
    }
    
    if (HerculesMk2.getControl("inputs", group, "fx 3").isDown) {
        min = 50000; max = 2000000
        increment = (max-min)/potStep;
        increment = (value <= 0x3F) ? increment : increment * -1;

        currentValue = engine.getValue("[Flanger]", "lfoPeriod");
        newValue = currentValue + increment;
        newValue = newValue > max ? max : newValue < min ? min : newValue;
        if (HerculesMk2.debug) print ("HerculesMk2.pitch: value= " + newValue);
        if (newValue != currentValue) 
        engine.setValue("[Flanger]", "lfoPeriod", newValue);
        
        HerculesMk2.getControl("inputs", group, "fx 3").used = true;
        done = true;
    }
    }

    if (done) return;

    increment = 0.016;
    increment = (value <= 0x3F) ? increment : increment * -1;

    if (HerculesMk2.debug) print ("HerculesMk2.pitch: value=" + value);
    engine.setValue(group, "rate", engine.getValue(group, "rate") + increment);
};

HerculesMk2.jog_wheel = function (group, control, value, status) {
    //  7F > 40: CCW Slow > Fast - 127 > 64 
    //  01 > 3F: CW Slow > Fast - 0 > 63
    group = HerculesMk2.getGroup(control);

    if (HerculesMk2.controls.outputs[HerculesMk2.leds[group + " cue"]].isOn == true) {
    HerculesMk2.setLed(group + " cue", "off");
    }
    
    jogValue = value >=0x40 ? value - 0x80 : value; // -64 to +63, - = CCW, + = CW
    
    if (HerculesMk2.scratchMode) { // do some scratching
    if (HerculesMk2.debug) print("Do scratching value:" + value + " jogValue: " + jogValue );
    engine.setValue(group,"scratch", (engine.getValue(group,"scratch") + (jogValue/64)).toFixed(2));
    } else { // do pitch adjustment
    newValue = jogValue; 
    if (HerculesMk2.debug) print("do pitching adjust " + jogValue + " new Value: " + newValue);
    engine.setValue(group,"jog", newValue);
    }
};
    
HerculesMk2.wheelDecay = function (value) {    
    currentDate = new Date().getTime();
    
    if (currentDate > HerculesMk2.decayLast + HerculesMk2.decayInterval) {
    HerculesMk2.decayLast = currentDate;
    
    if (HerculesMk2.debug) print(" new playposition: " + value + " decayLast: "+ HerculesMk2.decayLast);
    if (HerculesMk2.scratchMode) { // do some scratching
        if (HerculesMk2.debug) print("Scratch deck1: " + engine.getValue("[Channel1]","scratch") + " deck2: "+ engine.getValue("[Channel2]","scratch"));
        
        jog1DecayRate = HerculesMk2.decayRate * (engine.getValue("[Channel1]","play") ? 1 : 5);
        jog1 = engine.getValue("[Channel1]","scratch"); 
        if (jog1 != 0) {
        if (Math.abs(jog1) > jog1DecayRate) {  
            engine.setValue("[Channel1]","scratch", (jog1 / jog1DecayRate).toFixed(2));
        } else {
            engine.setValue("[Channel1]","scratch", 0);
        }
        }
        jog2DecayRate = HerculesMk2.decayRate * (engine.getValue("[Channel2]","play") ? 1 : 5);
        jog2 = engine.getValue("[Channel2]","scratch"); 
        if (jog2 != 0) {
        if (Math.abs(jog2) > jog2DecayRate) {  
            engine.setValue("[Channel2]","scratch", (jog2 / jog2DecayRate).toFixed(2));
        } else {
            engine.setValue("[Channel2]","scratch", 0);
        }
        }
    }
    }
};
