/**
 * Denon MC3000 controller script for v1.10.0
 *
 * Written by Bertrand Espern 2012
 *
 * 2012/04/30 V0.99 : first "good" version approuved and tested by me
 *
 * Special Thanks to the Programmers of Mixx and all the contributors
 *
 * Inspired primarily from Numark Total Control script file 
 *
 **/

function mc3000(){}

// ----------   Global variables    ----------

mc3000.deck2ch = [0, 2];

mc3000.nearEnd = [0, 0];

mc3000.oldloopstart = [0, 0];

mc3000.loophotcue = [0, 0];
mc3000.loophotcuebeatnb = [8, 8];

mc3000.hotcues = {23: 1, 24: 2, 25: 3, 32: 4, 72:5, 73:6, 74:7, 75:8};

mc3000.leds = {vinylmode: 6, keylock: 8, sync: 9, cue:38, play: 39, fxon1: 90, fxon2: 91,
cue1: 17, cue2: 19, cue3: 21, cue4: 23, cue5: 48, cue6: 50, cue7: 52, cue8: 54,
loopin: 36, loopout: 64, autoloop: 43, pfl1: 69, pfl2: 81, pfl3: 75, pfl4: 87,
efx1_1: 92, efx2_1: 93, efx3_1: 94, efx4_1: 95, efx1_2: 96, efx2_2: 97, efx3_2: 98, efx4_2: 99,
autoloop_dimmer: 83};

mc3000.state = {"shift1" : false, "shift2" : false, "alshift1" : false, "alshift2" : false, "pfl1" : 0, "pfl2" : 0 }; 

mc3000.vumetermaxled = [1 ,1];

mc3000.scratch =[false, false];
mc3000.scratchpressed = [false, false];
mc3000.alpha =  (1.0/8);
mc3000.beta = mc3000.alpha/32;
mc3000.jog_sensitivity = 2.0;
mc3000.scratch_sensitivity = 1;

// BeatLoop 2 4 8 16 to 1 2 3 4 
mc3000.efx2no = [4,8,16,2];

// ----------   Functions    ----------

// called when the MIDI device is opened & set up
mc3000.init = function(id) {	

	mc3000.id = id;

	var i=0;
	for (i=1;i<=8;i++) {
		engine.connectControl("[Channel1]","hotcue_"+i+"_enabled","mc3000.hotcueSetLed");
		engine.connectControl("[Channel2]","hotcue_"+i+"_enabled","mc3000.hotcueSetLed");
	}

	for (i=1;i<=2;i++) {
		engine.connectControl("[Channel"+i+"]", "keylock", "mc3000.keylockSetLed");
		engine.connectControl("[Channel"+i+"]", "pfl", "mc3000.pflSetLed");
		engine.connectControl("[Channel"+i+"]", "play", "mc3000.playSetLed");
		engine.connectControl("[Channel"+i+"]", "loop_start_position", "mc3000.loopStartSetLed");
		engine.connectControl("[Channel"+i+"]", "loop_end_position", "mc3000.loopEndSetLed");
		engine.connectControl("[Channel"+i+"]", "loop_enabled", "mc3000.loopEnableSetLed");

		engine.connectControl("[Channel"+i+"]", "beatloop_2_enabled", "mc3000.beatLoopXSetLed");
		engine.connectControl("[Channel"+i+"]", "beatloop_4_enabled", "mc3000.beatLoopXSetLed");
		engine.connectControl("[Channel"+i+"]", "beatloop_8_enabled", "mc3000.beatLoopXSetLed");
		engine.connectControl("[Channel"+i+"]", "beatloop_16_enabled", "mc3000.beatLoopXSetLed");
		// PERFORMANCE EATER ?
		engine.connectControl("[Channel"+i+"]", "playposition", "mc3000.playPositionSetLed");
		engine.connectControl("[Channel"+i+"]", "PeakIndicator", "mc3000.peakIndicatorSetLed");
		engine.connectControl("[Channel"+i+"]", "VuMeter", "mc3000.vuMeterSetLeds");
		//engine.connectControl("[Channel"+i+"]", "beat_active", "mc3000.beatActiveSetLed");
	}

	mc3000.allLedOff();
}

// Called when the MIDI device is closed
mc3000.shutdown = function(id) {
	mc3000.allLedOff();
}

// === MISC TO MANAGE LEDS ===

mc3000.allLedOff = function () {
	// All leds off for deck 1 and 2
	for (var led in mc3000.leds) {
		mc3000.setled(1,mc3000.leds[led],0);
		mc3000.setled(2,mc3000.leds[led],0);	
	}
	// Pfl leds off for deck 1 and 2 are special
	mc3000.setled2(1,mc3000.leds["pfl1"],0);
	mc3000.setled2(2,mc3000.leds["pfl2"],0);
}

mc3000.setled = function(deck,led,status) {
	var ledStatus = 75; // Default OFF
	switch (status) {
    		case  0 : ledStatus = 75; break; // OFF
		case false : ledStatus = 75; break; // OFF 
		case true  : ledStatus = 74; break; // ON
    		case  1 : ledStatus = 74; break; // ON
    		case  2 : ledStatus = 76; break; // BLINK
	}
	midi.sendShortMsg(0xB0+mc3000.deck2ch[deck-1], ledStatus, led);
}

mc3000.setled2 = function(deck,led,status) {
	//var ledStatus = status==1 ? 80 : 81;
	midi.sendShortMsg(0xB0+mc3000.deck2ch[deck-1], status==1 ? 80 : 81, led);
}

// === MISC COMMON ===

mc3000.groupToDeck = function(group) {
	var matches = group.match(/^\[Channel(\d+)\]$/);
	if (matches == null) {
		return -1;
	} else {
		return matches[1];
	}
}

mc3000.loop2NoEfx = function(nbloop) {
	if (nbloop==1) nbloop=16;
	return Math.log(nbloop)/Math.log(2); //2 4 8 16 -> 1 2 3 4	
}

mc3000.samplesPerBeat = function(group) {
	var sampleRate = engine.getValue(group, "track_samplerate");
	var channels = 2;
	var bpm = engine.getValue(group, "file_bpm");
	return channels * sampleRate * 60 / bpm;
}

// Pitch Rate MSB/LSB
mc3000.rate = function(channel, control, value, status, group) {
	// 0..16383 -> -1..1
	var MSB = value;
	var LSB = control;
	var num14 = (MSB << 7) | LSB;  // Construct the 14-bit number
	var nrate = (num14-8192)/8192;
	engine.setValue(group, "rate", nrate);
}

// === MASTER ===
mc3000.micVolume = function(channel, control, value, status, group) {
	engine.setValue("[Microphone]", "volume", value/127);
}
mc3000.headMix = function(channel, control, value, status, group) {
	engine.setValue("[Master]", "headMix", (value-64)/64);
}
mc3000.headVolume = function(channel, control, value, status, group) {
	engine.setValue("[Master]", "headVolume", value/127*5);
}
mc3000.masterBalance = function(channel, control, value, status, group) {
	engine.setValue("[Master]", "balance", (value-64)/64);
}
mc3000.masterVolume = function(channel, control, value, status, group) {
	engine.setValue("[Master]", "volume", value/127*5);
}

// === PLAYLIST ===
mc3000.selectKnob = function(channel, control, value, status, group) {
	var offset = value==0 ? 250:-250;

	// AUTOLOOP + SelectKnob ? -> MOVE LOOP
	if (mc3000.state["alshift1"] || mc3000.state["alshift2"]){
		if (mc3000.state["alshift1"]) mc3000.moveLoop("[Channel1]",offset);
		if (mc3000.state["alshift2"]) mc3000.moveLoop("[Channel2]",offset);
		return;
	}
	
	// SHIFT + SelectKnob ? -> MOVE CUE
	if (mc3000.state["shift1"] || mc3000.state["shift2"]) {
		if (mc3000.state["shift1"]) {
			var curpos = engine.getValue("[Channel1]","cue_point");
	 		engine.setValue("[Channel1]","cue_point",curpos+offset);
		}
		if (mc3000.state["shift2"]) {
			var curpos = engine.getValue("[Channel2]","cue_point");
	 		engine.setValue("[Channel2]","cue_point",curpos+offset);
		}
		return;
	} 

	// NORMAL MODE - NEXT/PREV TRACK
	if (value == 0) { 
		engine.setValue(group, "SelectNextTrack", 1);
	} else {
		engine.setValue(group, "SelectPrevTrack", 1);
	}
}

// === KILL ===
mc3000.killMid = function(channel, control, value, status, group) {
	engine.setValue(group, "filterMidKill", !engine.getValue(group, "filterMidKill"));
}

// BT USED FOR BASS IS USED FOR HIGH if SHIFT
mc3000.killBass = function(channel, control, value, status, group) {
	if (mc3000.state["shift1"] || mc3000.state["shift2"]) {
		engine.setValue(group, "filterHighKill", !engine.getValue(group, "filterHighKill"));
	}else{
		engine.setValue(group, "filterLowKill", !engine.getValue(group, "filterLowKill"));
	}
}
// === FLANGER ===
// Flanger knob Depth, Delay, Period
mc3000.lfoDepth = function(channel, control, value, status, group) {
// 0..127 -> 0..1
	engine.setValue(group, "lfoDepth", value/127);
}
mc3000.lfoDelay = function(channel, control, value, status, group) {
// 0..127 -> 50..10000
	engine.setValue(group, "lfoDelay", 50+(value/127)*9950);
}
mc3000.lfoPeriod = function(channel, control, value, status, group) {
// 0..127 -> 50000..2000000
	engine.setValue(group, "lfoPeriod", 50000+(value/127)*1950000);
}

// === CUE AND HOTCUE ===
mc3000.cue_default = function(channel, control, value, status, group) {
	var isPressed = status==0x90 ? 1 : 0;
	engine.setValue(group,"cue_default", isPressed);
	mc3000.setled(mc3000.groupToDeck(group),mc3000.leds["cue"],isPressed);
}


mc3000.hotcueset = function(channel, control, value, status, group) {
	var deck=mc3000.groupToDeck(group);
	var nocue=mc3000.hotcues[control];
	var is_play = engine.getValue(group,"play")==1 ? true: false;	

	if (status !== 0x90) {    // Is Relesead ?
	 	if (mc3000.state["alshift"+deck] || mc3000.state["shift"+deck]) return; // DO NOTHING ON RELEASE IF ALSHIFT OR SHIFT
		engine.setValue(group,"hotcue_"+nocue+"_activate",0);
		return;
	}

	// AUTOLOOP + HOTCUE MODE ?
	if (mc3000.state["alshift"+deck]) {
		if (engine.getValue(group,"hotcue_"+nocue+"_enabled")) {
			mc3000.setLoopAtHotCue(group,nocue,mc3000.loophotcuebeatnb[deck-1]);
			if (!is_play) engine.setValue(group, "reloop_exit", 1);
			// TO AVOID DIRECT LOOP AFTER RELEASED AUTOLOOP
			mc3000.oldloopstart[deck-1] = engine.getValue(group, "loop_start_position");
		}
		return;
	}

	// WITH SHIFT ?
	if (mc3000.state["shift"+deck]) {
        	engine.setValue(group,"hotcue_"+nocue+"_clear",1);
		return;
	}

	if (is_play) {
		// LOOP HOTCUE OR NORMAL HOTCUE ?
		if (mc3000.loophotcue[deck-1]==nocue) {
  			mc3000.loophotcue[deck-1]=0; 
			engine.setValue(group, "reloop_exit", 1);
		} else {
			engine.setValue(group,"hotcue_"+nocue+"_activate",1);
		}
	} else {
			engine.setValue(group,"hotcue_"+nocue+"_activate",1);
	}
}

// ==== Set a loop at the hotcue_X_position
mc3000.setLoopAtHotCue = function (group,nohotcue,nbbeat) {
	var position = engine.getValue(group,"hotcue_"+nohotcue+"_position");
	var deck = mc3000.groupToDeck(group);

	// Old HotCue Led Off if exist
	if (mc3000.loophotcue[deck-1]>0) mc3000.hotcueSetLed(0,group,"hotcue_"+mc3000.loophotcue[deck-1]);

	// if loop set, exit from loop	
	if (engine.getValue(group, "loop_enabled")) engine.setValue(group, "reloop_exit", 1);

	// Set new loop_start
	engine.setValue(group, "loop_start_position", position);
	// Delete loop_end	
	engine.setValue(group, "loop_end_position", -1);
	// Set Loop End
	engine.setValue(group, "loop_end_position", position+(mc3000.samplesPerBeat(group)*nbbeat));
	
	mc3000.loophotcue[deck-1]=nohotcue;	
	
	// New HotCue Led Blink
	mc3000.hotcueSetLed (2,group,"hotcue_"+nohotcue);
}

mc3000.shift = function(channel, control, value, status, group) {
	mc3000.state["shift"+mc3000.groupToDeck(group)] = status==0x90 ? true : false; // 1 if pressed
}

mc3000.vinylmode = function(channel, control, value, status, group) {
	var deck = mc3000.groupToDeck(group);
	mc3000.scratch[deck-1] = !mc3000.scratch[deck-1];
}

// FX ON 1 to toggle flanger ON/OFF
mc3000.fxon1 = function(channel, control, value, status, group) {
	var newValue = !engine.getValue(group, "flanger");
	engine.setValue(group, "flanger", newValue);
	mc3000.setled(mc3000.groupToDeck(group),mc3000.leds["fxon1"],newValue);
}

// FX ON 2 to toggle quantize / or grid adjust if shift
mc3000.fxon2 = function(channel, control, value, status, group) {
	if (mc3000.state["shift"+mc3000.groupToDeck(group)]) {
		engine.setValue(group, "beats_translate_curpos",true);
	}else{
		var newValue = !engine.getValue(group, "quantize");
		engine.setValue(group, "quantize", newValue);
		mc3000.setled(mc3000.groupToDeck(group),mc3000.leds["fxon2"],newValue);
	}
}

// JOG with WHEELTOUCH
mc3000.wheelTouch = function (channel, control, value, status, group) {
	var deck = mc3000.groupToDeck(group);
	if (mc3000.scratch[deck-1]) {
		if (status==0x90) {    // Is pressed ?
			engine.scratchEnable(deck, 100, 330, mc3000.alpha, mc3000.beta); // 128, 33+1/3 aussi
		} else { 
			engine.scratchDisable(deck);
		}
		mc3000.scratchpressed[deck-1] = status==0x90 ? true : false;
	}
}
 
// The wheel that actually controls the scratching
mc3000.jogWheel = function (channel, control, value, status, group) {
	var deck = mc3000.groupToDeck(group);
	var adjustedJog = value - 64; // Control centers on 0x40 (64)

	if (mc3000.scratch[deck-1]) {
		if (mc3000.scratchpressed[deck-1]) engine.scratchTick(deck,adjustedJog/mc3000.scratch_sensitivity);
	} else {
		var gammaInputRange = 64;	// Max jog speed
		var maxOutFraction = 0.5;	// Where on the curve it should peak; 0.5 is half-way
		var sensitivity = 0.5;		// Adjustment gamma 0.5 def
		var gammaOutputRange = 3;	// Max rate change
		adjustedJog = gammaOutputRange * adjustedJog / (gammaInputRange * maxOutFraction);
		engine.setValue(group, "jog", adjustedJog/mc3000.jog_sensitivity);
	}
}

// RATE TEMP UP if PLAY else FWD / END
mc3000.bendUp = function(channel, control, value, status, group) {
	var isPressed = status==0x90 ? 1 : 0;
	if (engine.getValue(group,"play") == 1) {
		engine.setValue(group, "rate_temp_up", isPressed);		
	}else{
		if ((mc3000.state["shift"+mc3000.groupToDeck(group)]) && isPressed) {
			engine.setValue(group,"end",1);
		} else {
			engine.setValue(group, "fwd",isPressed);
		}
	}
}

// RATE TEMP DOWN if PLAY else BACK / START
mc3000.bendDown = function(channel, control, value, status, group) {
	var isPressed = status==0x90 ? 1 : 0;
	if (engine.getValue(group,"play") == 1) {
		engine.setValue(group, "rate_temp_down", isPressed);
	}else{
		if ((mc3000.state["shift"+mc3000.groupToDeck(group)]) && isPressed) {
			engine.setValue(group,"start",1);
		} else {
			engine.setValue(group, "back", isPressed);
		}
	}
}

// === LOOP CONTROL ===

mc3000.efx_beatLoopX = function(channel, control, value, status, group) {
	var deck = mc3000.groupToDeck(group);
	var nbbeat = mc3000.efx2no[(control-18)-((deck-1)*64)];

	if (mc3000.state["alshift"+deck]) {	
		mc3000.setled(deck,mc3000.leds["efx"+mc3000.loop2NoEfx(mc3000.loophotcuebeatnb[deck-1])+"_"+deck],0);
		mc3000.setled(deck,mc3000.leds["efx"+mc3000.loop2NoEfx(nbbeat)+"_"+deck],2);
		mc3000.loophotcuebeatnb[deck-1]=nbbeat;
	} else {
		engine.setValue(group, "beatloop_"+nbbeat+"_activate",1);
	}

}	

// Shift and autoloop : delete loop point ELSE BeatLoopAtHotCue
mc3000.autoLoop = function(channel, control, value, status, group) {
	var deck = mc3000.groupToDeck(group);
	var i=0;	
	if (status==0x90) {
		// LIGHT LED EFX + CUE  EN MODE CUE LOOP
		// EFX = BEATLOOP NB
		for (i=1;i<=4;i++) mc3000.setled(deck,mc3000.leds["efx"+i+"_"+deck],0);
		mc3000.setled(deck,mc3000.leds["efx"+mc3000.loop2NoEfx(mc3000.loophotcuebeatnb[deck-1])+"_"+deck],2);
		
		for (i=1;i<=8;i++) mc3000.hotcueSetLed(0,group,"hotcue_"+i);
		if (mc3000.loophotcue[deck-1] > 0) mc3000.hotcueSetLed(2,group,"hotcue_"+mc3000.loophotcue[deck-1]);
		print ("hotcue_"+mc3000.loophotcue[deck-1]);
		
		// DELETE LOOP POINTS IF SHIFT
		if (mc3000.state["shift"+deck]) {
			engine.setValue(group, "reloop_exit", 1);
			engine.setValue(group, "loop_end_position", -1);
			engine.setValue(group, "loop_start_position", -1);
		}
		// INIT TO PREPARE MOVELOOP IF NEEDED
		mc3000.oldloopstart[deck-1] = engine.getValue(group, "loop_start_position");

	} else { //released
		// RESTORE LED STATE EFX + CUE_X
		var beatp=0;
		for (i=1;i<=4;i++) {
			 beatp=Math.pow(2,i);
			 mc3000.setled(deck,mc3000.leds["efx"+i+"_"+deck],engine.getValue(group,"beatloop_"+beatp+"_enabled"));
		}
		for (i=1;i<=8;i++) {
			if (engine.getValue(group,"hotcue_"+i+"_enabled")) { 
				mc3000.hotcueSetLed(1,group,"hotcue_"+i);
			} else {
				mc3000.hotcueSetLed(0,group,"hotcue_"+i);
			}
		}

		// IF MOVE LOOP APPLIED
		if (mc3000.oldloopstart[deck-1] != engine.getValue(group, "loop_start_position")) {
			engine.setValue(group, "reloop_exit", 1);
		}
				
	}

	mc3000.state["alshift"+deck] = status==0x90 ? true : false ; // true if pressed
}

mc3000.loopCutM = function(channel, control, value, status, group) {
	var start = engine.getValue(group, "loop_start_position");
	var end = engine.getValue(group, "loop_end_position");
	if ((start != -1) && (end != -1)) engine.setValue(group, "loop_halve", 1);
}

mc3000.loopCutP = function(channel, control, value, status, group) {
	var start = engine.getValue(group, "loop_start_position");
	var end = engine.getValue(group, "loop_end_position");
	if ((start != -1) && (end != -1)) engine.setValue(group, "loop_double", 1);
}

mc3000.loopIn = function(channel, control, value, status, group) {
	if (engine.getValue(group, "loop_enabled")) engine.setValue(group, "reloop_exit", 1);
	engine.setValue(group, "loop_in", 1);
	engine.setValue(group, "loop_end_position", -1);
}

mc3000.loopOut = function(channel, control, value, status, group) {
	var start = engine.getValue(group, "loop_start_position");
	var end = engine.getValue(group, "loop_end_position");
	if (start != -1) {
		if (end != -1) {
			engine.setValue(group, "reloop_exit", 1);
		} else {
			engine.setValue(group, "loop_out", 1);
		}
	}
}


// === EXPERIMENTAL ===
// CF MODE for test 
mc3000.cfMode = function(channel, control, value, status, group) {
	// BACKLOOP de -4 ?
	print ("BackLoop ?");
	engine.setValue("[Channel1]", "beatloop_4", 1);
}
mc3000.moveLoop = function (group,offset) {
		var start = engine.getValue(group, "loop_start_position");
		var end = engine.getValue(group, "loop_end_position");

		if ((start != -1) && (end != -1)) {
			engine.setValue(group, "loop_start_position", start+offset);
			engine.setValue(group, "loop_end_position", end+offset);
		}
}

// === SET LED FUNCTIONS ===

mc3000.hotcueSetLed = function(value, group, control) {
	mc3000.setled(mc3000.groupToDeck(group),mc3000.leds["cue"+control[7]],value);
}

mc3000.pflSetLed = function(value,group) {
	mc3000.setled2(mc3000.groupToDeck(group),mc3000.leds["pfl"+mc3000.groupToDeck(group)],value);
}

mc3000.playSetLed = function(value,group) {
	mc3000.setled(mc3000.groupToDeck(group),mc3000.leds["play"],value);
}

mc3000.keylockSetLed = function(value,group) {
	mc3000.setled(mc3000.groupToDeck(group),mc3000.leds["keylock"],value);
}

mc3000.loopStartSetLed = function (value, group) {
	mc3000.setled(mc3000.groupToDeck(group),mc3000.leds["loopin"],value == -1 ? false: true);
}

mc3000.loopEndSetLed = function (value, group) {
	mc3000.setled(mc3000.groupToDeck(group),mc3000.leds["loopout"],value == -1 ? false: true);
}

mc3000.loopEnableSetLed = function(value, group, control) {
	mc3000.setled(mc3000.groupToDeck(group),mc3000.leds["autoloop"],value);
}

mc3000.beatLoopXSetLed = function(value, group, control) {
	var deck = mc3000.groupToDeck(group);	
	mc3000.setled(deck,mc3000.leds["efx"+mc3000.loop2NoEfx(control[9])+"_"+deck],value);
}

mc3000.beatActiveSetLed = function (value,group){
	mc3000.setled(mc3000.groupToDeck(group),mc3000.leds["sync"],value);
}

mc3000.peakIndicatorSetLed = function (value, group) {
	mc3000.setled(mc3000.groupToDeck(group),mc3000.leds["sync"],value);
}

mc3000.vuMeterSetLeds = function (value,group){
	var deck = mc3000.groupToDeck(group);
	var maxled = Math.round ((value-0.07)*7);
	var curled = mc3000.vumetermaxled[deck-1];
	if (maxled !== curled) {
		var i=0; var offset = deck == 1 ? 9: 41;
		if (maxled > curled) {
			for (i=curled+1;i<=maxled;i++) midi.sendShortMsg(0xB0+mc3000.deck2ch[deck-1], 80, i+offset);
		}else{
			for (i=maxled+1;i<=curled;i++) midi.sendShortMsg(0xB0+mc3000.deck2ch[deck-1], 81, i+offset);
		}
		mc3000.vumetermaxled[deck-1] = maxled;
	}		 
}

mc3000.playPositionSetLed = function (playposition, group) {
	var nearEnd = 0;

	if (playposition < 0.80) { nearEnd = 0; }
	if (playposition > 0.80) { nearEnd = 2; } // The song is going to end
	if (playposition == 1) { nearEnd = 0; }	

	if (nearEnd !== mc3000.nearEnd[mc3000.groupToDeck(group)-1]) {
		mc3000.setled(mc3000.groupToDeck(group),mc3000.leds["sync"],nearEnd);
		mc3000.nearEnd[mc3000.groupToDeck(group)-1] = nearEnd;
	}
}


