"use strict";
////////////////////////////////////////////////////////////////////////
// JSHint configuration                                               //
////////////////////////////////////////////////////////////////////////
/* global engine                                                      */
/* global print                                                       */
/* global midi                                                        */
/* global SCS3M:true                                                  */
/* jshint -W097                                                       */
/* jshint -W084                                                       */
/* jshint laxbreak: true                                              */
////////////////////////////////////////////////////////////////////////

// manually test messages
// amidi -p hw:1 -S F00001601501F7 # flat mode
// amidi -p hw:1 -S 900302 # 90: note on, 03: id of a touch button, 02: red LED

SCS3M = {
    // The device remembers the selected EQ/FX mode per deck
    // and switches to that mode on deck-switch. Set this to
    // false if you prefer the mode to stay the same on
    // deck-switch.
    eqModePerDeck: true,
};

SCS3M.init = function(id) {
    this.device = this.Device();
    this.agent = this.Agent(this.device);
    this.agent.start();
};

SCS3M.shutdown = function() {
    SCS3M.agent.stop();
};

SCS3M.receive = function(channel, control, value, status) {
    SCS3M.agent.receive(status, control, value);
};

/* Midi map of the SCS.3m device
 *
 * Thanks to Sean M. Pappalardo for the original SCS3 mappings.
 */
SCS3M.Device = function() {
    var NoteOn = 0x90;
    var NoteOff = 0x80;
    var CC = 0xB0;
    var CM = 0xBF; /* this is used for slider mode changes (absolute/relative, sending a control change on channel 16!?) */

    var black = 0x00;
    var blue = 0x01;
    var red = 0x02;
    var purple = blue | red;

    function Logo() {
        var id = 0x69;
        return {
            on: [NoteOn, id, 0x01],
            off: [NoteOn, id, 0x00]
        };
    }

    function Meter(id, lights) {
        function plain(value) {
            if (value <= 0.0) return 1;
            if (value >= 1.0) return lights;
            return 1 + Math.round(value * (lights - 1));
        }

        function clamped(value) {
            if (value <= 0.0) return 1;
            if (value >= 1.0) return lights;
            return Math.round(value * (lights - 2) + 1.5);
        }

        function zeroclamped(value) {
            if (value <= 0.0) return 0;
            if (value >= 1.0) return lights;
            return Math.round(value * (lights - 1) + 0.5);
        }
        return {
            needle: function(value) {
                return [CC, id, plain(value)];
            },
            centerbar: function(value) {
                return [CC, id, 0x14 + clamped(value)];
            },
            bar: function(value) {
                return [CC, id, 0x28 + zeroclamped(value)];
            },
            expand: function(value) {
                return [CC, id, 0x3C + zeroclamped(value)];
            },
        };
    }

    function Slider(id, lights, fields) {
        var touchfields = {};
        for (var fieldn in fields) {
            touchfields[fieldn] = {
                touch: [NoteOn, fields[fieldn]],
                release: [NoteOff, fields[fieldn]],
            };
        }
        return {
            meter: Meter(id, lights),
            slide: [CC, id],
            mode: {
                absolute: [[CM, id, 0x70], [CM, id, 0x7F]],
                relative: [[CM, id, 0x71], [CM, id, 0x7F]],
            },
            field: touchfields,
        };
    }

    function Light(id) {
        return {
            black: [NoteOn, id, black],
            blue: [NoteOn, id, blue],
            red: [NoteOn, id, red],
            purple: [NoteOn, id, purple],
        };
    }

    function Touch(id) {
        return {
            light: Light(id),
            touch: [NoteOn, id],
            release: [NoteOff, id]
        };
    }

    function Side(side) {
        function either(left, right) {
            return ('left' === side) ? left : right;
        }

        function Deck() {
            // The left deck button has a higher address than the right button,
            // for all the other controls the left one has a lower address.
            // I wonder why.
            var id = either(0x10, 0x0F);
            return {
                light: function(bits) {
                    return [NoteOn, id, (bits[0] ? 1 : 0) | (bits[1] ? 2 : 0)];
                },
                touch: [NoteOn, id],
                release: [NoteOff, id]
            };
        }

        function Pitch() {
            return Slider(either(0x00, 0x01), 7, {
                left:   either(0x51, 0x54),
                middle: either(0x52, 0x55),
                right:  either(0x53, 0x56),
            });
        }

        function Eq() {
            return {
                low: Slider(either(0x02, 0x03), 7),
                mid: Slider(either(0x04, 0x05), 7),
                high: Slider(either(0x06, 0x07), 7),
            };
        }

        function Modes() {
            return {
                fx: Touch(either(0x0A, 0x0B)),
                eq: Touch(either(0x0C, 0x0D))
            };
        }

        function Gain() {
            return Slider(either(0x08, 0x09), 7);
        }

        function Touches() {
            return [
                Touch(either(0x00, 0x01)),
                Touch(either(0x02, 0x03)),
                Touch(either(0x04, 0x05)),
                Touch(either(0x06, 0x07)),
            ];
        }

        function Phones() {
            return Touch(either(0x08, 0x09));
        }

        return {
            deck: Deck(),
            pitch: Pitch(),
            eq: Eq(),
            modes: Modes(),
            gain: Gain(),
            touches: Touches(),
            phones: Phones(),
            meter: Meter(either(0x0C, 0x0D), 7)
        };
    }

    return {
        factory: [0xF0, 0x00, 0x01, 0x60, 0x40, 0xF7],
        flat: [0xF0, 0x00, 0x01, 0x60, 0x15, 0x01, 0xF7],
        lightsoff: [CC, 0x7B, 0x00],
        logo: Logo(),
        left: Side('left'),
        right: Side('right'),
        master: Touch(0x0E),
        crossfader: Slider(0x0A, 11)
    };
};

// debugging helper
var printmess = function(message, text) {
    var i;
    var s = '';

    for (i in message) {
        s = s + ('0' + message[i].toString(16)).slice(-2);
    }
    print("Midi " + s + (text ? ' ' + text : ''));
};

SCS3M.Agent = function(device) {
    // Cache last sent bytes to avoid sending duplicates.
    // The second byte of each message (controller id) is used as key to hold
    // the last sent message for each controller.
    var last = {};

    // Queue of messages to send delayed after modeset()
    var loading = false;
    var throttling = false;
    var pipe = [];

    // Handlers for received messages
    var receivers = {};

    // Connected engine controls
    var watched = {};

    function clear() {
        receivers = {};
        pipe = [];

        // I'd like to disconnect everything on clear, but that doesn't work when using closure callbacks, I guess I'd have to pass the callback function as string name
        // I'd have to invent function names for all handlers
        // Instead I'm not gonna bother and just let the callbacks do nothing
        for (var ctrl in watched) {
            if (watched.hasOwnProperty(ctrl)) {
                watched[ctrl] = [];
            }
        }
    }

    // This function receives Midi messages from the SCS.3m
    // See the XML mapping for all the messages caught
    function receive(type, control, value) {
        var address = (type << 8) + control;
        var handler = receivers[address];
        if (handler) {
            handler(value);
        }
    }

    // Register a handler to listen for messages
    // control: an array with at least two message bytes (type and control id)
    // handler: callback function that will be called each time a value is received
    function expect(control, handler) {
        var address = (control[0] << 8) + control[1];
        receivers[address] = handler;
    }

    function watchRegister(channel, control, handler) {
        // Indirection through a registry that keeps all watched controls
        var ctrl = channel + control;

        if (!watched[ctrl]) {
            watched[ctrl] = [];
            engine.connectControl(channel, control, function(value, group, control) {
                var handlers = watched[ctrl];
                for (var i in handlers) {
                    handlers[i]();
                }
            });
        }

        watched[ctrl].push(handler);
    }

    // Register a handler for changes in engine values
    // This is an abstraction over engine.getParameter()
    function watch(channel, control, handler) {
        watchRegister(channel, control, function() {
            handler(engine.getParameter(channel, control));
        });

        if (loading) {
            // ugly UGLY workaround
            // The device does not light meters again if they haven't changed from last value before resetting flat mode
            // so we send each control some bullshit values which causes awful flicker during startup
            // The trigger will then set things straight
            handler(100);
            handler(-100);
        }

        engine.trigger(channel, control);
    }

    // Register a handler for multiple engine values. It will be called
    // everytime one of the values changes.
    // controls: list of channel/control pairs to watch
    // handler: will receive list of control values as parameter in same order
    function watchmulti(controls, handler) {
        var values = [];
        var watchControl = function(controlpos, controlgroup) {
            var channel = controlgroup[0];
            var control = controlgroup[1];
            values[controlpos] = engine.getParameter(channel, control);
            watchRegister(channel, control, function() {
                values[controlpos] = engine.getParameter(channel, control);
                handler(values);
            });
        };

        for (var i in controls) {
            watchControl(i, controls[i]);
        }
        handler(values);
    }


    // Send MIDI message to device
    // Param message: list of three MIDI bytes
    // Param force: send value regardless of last recorded state
    // Param extra: do not record message as last state
    // Returns whether the massage was sent
    // False is returned if the message was sent before.
    function send(message, force, extra) {
        if (!message){
            print("SCS3 warning: send function received invalid message");
            return; // :-(
        }

        var address = (message[0] << 8) + message[1];

        if (!force && last[address] === message[2]) {
            return false; // Not repeating same message
        }

        midi.sendShortMsg(message[0], message[1], message[2]);

        // Record message as sent, unless it as was a mode setting termination message
        if (!extra) {
            last[address] = message[2];
        }
        return true;
    }

    // Wrapper function for send() that delays messages after modesetting
    function tell(message) {
        if (throttling) {
            pipe.push(message);
            return;
        }

        send(message);
    }

    // Send modeset messages to the device
    //
    // messages: list of one or two messages to send
    //
    // Either provide a pair of messages to set a slider to a different
    // mode, or send just one long message in the list. Transmission of
    // subsequent messages will be delayed to give the device some time to apply
    // the changes.
    function modeset(messages) {
        var sent = true;

        // Modeset messages are comprised of the actual modeset message and
        // a termination message that must be sent after.
        var message = messages[0];

        if (message) {
            if (message.length > 3) {
                midi.sendSysexMsg(message, message.length);
            } else {
                sent = send(message);
                if (sent && messages[1]) {
                    // Only send termination message when modeset message was sent
                    send(messages[1], true, true);
                }
            }
        }

        if (sent) {
            // after modesetting, we have to wait for the device to settle
            if (!throttling) {
                throttling = engine.beginTimer(20, flushModeset);
            }
        }
    }

    var flushModeset = function() {
        var message;

        // Now we can flush the rest of the messages.
        // On init, some controls are left unlit if the messages are sent
        // without delay. The causes are unclear. Sending only a few messages
        // per tick seems to work ok.
        var limit = 5; // Determined experimentally
        while (pipe.length) {
            message = pipe.shift();
            send(message);
            if (loading && limit-- < 1) return;
        }

        if (throttling) engine.stopTimer(throttling);
        throttling = false;
        loading = false;
    };

    // Map engine values in the range [0..1] to lights
    // translator maps from [0..1] to a midi message (three bytes)
    function patch(translator) {
        return function(value) {
            tell(translator(value));
        };
    }

    // Cut off at 0.01 because it drops off very slowly
    function vupatch(translator) {
        return function(value) {
            value = value * 1.01 - 0.01;
            tell(translator(value));
        };
    }

    // accelerate away from 0.5 so that small changes become visible faster
    function offcenter(translator) {
        return function(value) {
            // If you want to adjust it, fiddle with the exponent (second argument to pow())
            return translator(Math.pow(Math.abs(value - 0.5) * 2, 0.6) / (value < 0.5 ? -2 : 2) + 0.5);
        };
    }

    function binarylight(off, on) {
        return function(value) {
            tell(value ? on : off);
        };
    }

    // absolute control
    function set(channel, control) {
        return function(value) {
            engine.setParameter(channel, control,
                value / 127
            );
        };
    }

    function setconst(channel, control, value) {
        return function() {
            engine.setParameter(channel, control, value);
        };
    }

    function reset(channel, control) {
        return function() {
            engine.reset(channel, control);
        };
    }

    // relative control
    function budge(channel, control, factor) {
        if (factor === undefined) factor = 1;
        var mult = factor / 128;

        return function(offset) {
            engine.setValue(channel, control,
                engine.getValue(channel, control) + (offset - 64) * mult
            );
        };
    }

    // switch
    function toggle(channel, control) {
        return function() {
            engine.setValue(channel, control, !engine.getValue(channel, control));
        };
    }

    function Switch() {
        var engaged = false;

        return {
            'change': function(state) {
                state = !!(state);
                var changed = engaged !== state;
                engaged = state;
                return changed;
            },
            'engage': function() {
                engaged = true;
            },
            'cancel': function() {
                engaged = false;
            },
            'toggle': function() {
                engaged = !engaged;
            },
            'engaged': function() {
                return engaged;
            },
            'choose': function(off, on) {
                return engaged ? on : off;
            }
        };
    }

    function HoldLimit(limit) {
        var start = false;

        var early = function() {
            return limit > new Date() - start;
        };

        return {
            'hold': function() {
                start = new Date();
            },

            'releaseTrigger': function(onEarly) {
                return function() {
                    if (!start) return;
                    if (early()) {
                        onEarly();
                    }
                    start = false;
                };
            },

            'early': function() {
                if (!start) return;
                return early();
            },

            'late': function() {
                if (!start) return;
                return early();
            },

            'held': function() {
                return !!start;
            },

            'choose': function(normalVal, heldVal) {
                return start ? heldVal : normalVal;
            }
        };
    }

    // HoldDelayedSwitches can be engaged, and they can be held.
    // A switch that is held for less than 200 ms will toggle.
    // After 200ms it will enter held-mode.
    function HoldDelayedSwitch() {
        var sw = Switch();

        var held = false;
        var heldBegin = false;

        sw.hold = function(onHeld) {
            return function() {
                heldBegin = true;
                var switchExpire = engine.beginTimer(200, function() {
                    engine.stopTimer(switchExpire);
                    if (heldBegin) {
                        heldBegin = false;
                        held = true;
                        if (onHeld) onHeld();
                    }
                });
            };
        };

        sw.release = function() {
            if (heldBegin) sw.toggle();
            held = false;
            heldBegin = false;
        };

        sw.held = function() {
            return held;
        };

        return sw;
    }

    function Multiswitch(preset, initialSubMode) {
        var engaged = preset;
        var subMode = initialSubMode;
        return {
            'engage': function(pos) {
                engaged = pos;
                if (engaged !== preset) {
                    subMode = pos;
                }
            },
            'engageSub': function() {
                engaged = subMode;
            },
            'cancel': function() {
                engaged = preset;
            },
            'engaged': function(pos) {
                return engaged === pos;
            },
            'choose': function(pos, off, on) {
                return (engaged === pos) ? on : off;
            }
        };
    }

    var master = Switch(); // Whether master key is held
    var deck = {
        left: HoldDelayedSwitch(), // off: channel1, on: channel3
        right: HoldDelayedSwitch() // off: channel2, on: channel4
    };

    var overlayA = Multiswitch('eq', 0);
    var overlayB = Multiswitch('eq', SCS3M.eqModePerDeck ? 1 : 0);
    var overlayC;
    var overlayD;
    if (SCS3M.eqModePerDeck) {
        overlayC = Multiswitch('eq', 2);
        overlayD = Multiswitch('eq', 3);
    } else {
        overlayC = overlayA;
        overlayD = overlayB;
    }

    var overlay = {
        left: [overlayA, overlayC],
        right: [overlayB, overlayD],
    };

    var eqheld = {
        left: Switch(),
        right: Switch()
    };

    var fxHeld = {
        left: HoldLimit(200),
        right: HoldLimit(200)
    };

    var touchheld = {
        left: Multiswitch('none'),
        right: Multiswitch('none')
    };

    function remap() {
        clear();
        patchage();
    }

    // Remap for chainig with handlers
    function repatch(handler) {
        return function(value) {
            var ret = handler(value);
            remap();
            return ret;
        };
    }

    function patchage() {

        function Side(side) {
            var part = device[side];
            var deckside = deck[side];

            // Switch deck/channel when button is touched
            expect(part.deck.touch, deckside.hold(remap));
            expect(part.deck.release, repatch(deckside.release));

            function either(left, right) {
                return (side === 'left') ? left : right;
            }

            var channelno = deck[side].choose(either(1, 2), either(3, 4));
            var channel = '[Channel' + channelno + ']';
            var effectchannel = '[QuickEffectRack1_' + channel + ']';
            var eqsideheld = eqheld[side];
            var touchsideheld = touchheld[side];
            var sideoverlay = overlay[side][deckside.choose(0, 1)];

            // Light the corresponding deck (channel 1: A, channel 2: B, channel 3: C, channel 4: D)
            // Make the lights blink on each beat
            function beatlight(translator, activepos, held) {
                return function(bits) {
                    bits = bits.slice(); // clone
                    if (held) {
                        // When the switch his held, light both LED
                        // turn them off when beat is active
                        bits[0] = !bits[0];
                        bits[1] = !bits[1];
                    } else {
                        // Invert the bit for the light that should be on
                        bits[activepos] = !bits[activepos];
                    }
                    return translator(bits);
                };
            }
            watchmulti([
                ['[Channel' + either(1, 2) + ']', 'beat_active'],
                ['[Channel' + either(3, 4) + ']', 'beat_active'],
            ], patch(beatlight(part.deck.light, deckside.choose(0, 1), deckside.held())));

            if (!master.engaged()) {
                if (sideoverlay.engaged('eq')) {
                    modeset(part.pitch.mode.relative);
                    expect(part.pitch.slide, eqsideheld.choose(
                        budge(effectchannel, 'super1', 0.5),
                        reset(effectchannel, 'super1')
                    ));
                    watch(effectchannel, 'super1', offcenter(patch(part.pitch.meter.centerbar)));
                }
            }

            if (sideoverlay.engaged('eq')) {
                var eff = "[EqualizerRack1_" + channel + "_Effect1]";
                var op = eqsideheld.choose(set, reset);
                expect(part.eq.low.slide, op(eff, 'parameter1'));
                expect(part.eq.mid.slide, op(eff, 'parameter2'));
                expect(part.eq.high.slide, op(eff, 'parameter3'));

                watch(eff, 'parameter1', patch(offcenter(part.eq.low.meter.centerbar)));
                watch(eff, 'parameter2', patch(offcenter(part.eq.mid.meter.centerbar)));
                watch(eff, 'parameter3', patch(offcenter(part.eq.high.meter.centerbar)));
            }

            expect(part.modes.eq.touch, repatch(function() {
                eqsideheld.engage();
                sideoverlay.cancel();
            }));
            expect(part.modes.eq.release, repatch(eqsideheld.cancel));
            tell(part.modes.eq.light[eqsideheld.choose(sideoverlay.choose('eq', 'blue', 'red'), 'purple')]);

            var fxHeldSide = fxHeld[side];

            var fxMap = function(tnr) {
                var softbutton = part.touches[tnr];
                var fxchannel = channel;
                if (master.engaged()) {
                    fxchannel = either('[Headphone]', '[Master]');
                }
                var effectunit = '[EffectRack1_EffectUnit' + (tnr + 1) + ']';
                var effectunit_enable = 'group_' + fxchannel + '_enable';
                var effectunit_effect = '[EffectRack1_EffectUnit' + (tnr + 1) + '_Effect1]';

                if (fxHeldSide.held() || master.engaged()) {
                    expect(softbutton.touch, toggle(effectunit, effectunit_enable));
                } else {
                    expect(softbutton.touch, repatch(function() {
                        sideoverlay.engage(tnr);
                        touchsideheld.engage(tnr);
                    }));
                }
                expect(softbutton.release, repatch(touchsideheld.cancel));

                if (sideoverlay.engaged(tnr)) {
                    watch(effectunit, effectunit_enable, binarylight(
                        softbutton.light.blue,
                        softbutton.light.purple)
                    );
                } else {
                    watch(effectunit, effectunit_enable, binarylight(
                        softbutton.light.black,
                        softbutton.light.red)
                    );
                }

                if (sideoverlay.engaged(tnr)) {
                    // Select effect by touching top slider when button is held
                    // Otherwise the top slider controls effect wet/dry
                    if (touchsideheld.engaged(tnr)) {
                        tell(part.pitch.meter.expand(0.3));
                        expect(
                            part.pitch.field.left.touch,
                            setconst(effectunit, 'chain_selector', 1)
                        );
                        expect(
                            part.pitch.field.right.touch,
                            setconst(effectunit, 'chain_selector', -1)
                        );
                    } else {
                        modeset(part.pitch.mode.absolute);
                        expect(part.pitch.slide, eqsideheld.choose(
                            set(effectunit, 'mix'),
                            reset(effectunit, 'mix')
                        ));
                        watch(effectunit, 'mix', patch(part.pitch.meter.bar));
                    }

                    expect(part.eq.high.slide, fxHeldSide.choose(
                        set(effectunit_effect, 'parameter3'),
                        reset(effectunit_effect, 'parameter3')
                    ));
                    expect(part.eq.mid.slide, fxHeldSide.choose(
                        set(effectunit_effect, 'parameter2'),
                        reset(effectunit_effect, 'parameter2')
                    ));
                    expect(part.eq.low.slide, fxHeldSide.choose(
                        set(effectunit_effect, 'parameter1'),
                        reset(effectunit_effect, 'parameter1')
                    ));
                    watch(effectunit_effect, 'parameter3', patch(part.eq.high.meter.needle));
                    watch(effectunit_effect, 'parameter2', patch(part.eq.mid.meter.needle));
                    watch(effectunit_effect, 'parameter1', patch(part.eq.low.meter.needle));
                }
            };

            for (var tnr = 0; tnr < 4; tnr++) {
                fxMap(tnr);
            }

            expect(part.modes.fx.touch,
                repatch(fxHeldSide.hold)
            );
            expect(part.modes.fx.release,
                repatch(fxHeldSide.releaseTrigger(sideoverlay.engageSub))
            );
            tell(part.modes.fx.light[fxHeldSide.choose(
                sideoverlay.choose('eq', 'red', 'blue'),
                'purple'
            )]);


            if (!master.engaged()) {
                if (deckside.held()) {
                    modeset(part.gain.mode.relative);
                    expect(part.gain.slide, eqsideheld.choose(
                        budge(channel, 'pregain'),
                        reset(channel, 'pregain')
                    ));
                    watch(channel, 'pregain', patch(offcenter(part.gain.meter.needle)));
                } else {
                    modeset(part.gain.mode.absolute);
                    expect(part.gain.slide, set(channel, 'volume'));
                    watch(channel, 'volume', patch(part.gain.meter.bar));
                }
            }

            watch(channel, 'pfl', binarylight(part.phones.light.blue, part.phones.light.red));
            expect(part.phones.touch, toggle(channel, 'pfl'));

            if (deckside.held()) {
                expect(device.crossfader.slide, set(channel, "playposition"));
                watch(channel, "playposition", patch(device.crossfader.meter.needle));
            }

            if (!master.engaged()) {
                watch(channel, 'VuMeter', vupatch(part.meter.bar));
            }
        }

        // Light the logo and let it go out to signal an overload
        watch("[Master]", 'audio_latency_overload', binarylight(
            device.logo.on,
            device.logo.off
        ));

        Side('left');
        Side('right');

        tell(device.master.light[master.choose('blue', 'purple')]);
        expect(device.master.touch, repatch(master.engage));
        expect(device.master.release, repatch(master.cancel));
        if (master.engaged()) {
            modeset(device.left.pitch.mode.absolute);
            watch("[Master]", "headMix", patch(device.left.pitch.meter.centerbar));
            expect(device.left.pitch.slide,
                eqheld.left.engaged() ? reset('[Master]', 'headMix') : set('[Master]', 'headMix')
            );

            modeset(device.right.pitch.mode.absolute);
            watch("[Master]", "balance", patch(device.right.pitch.meter.centerbar));
            expect(device.right.pitch.slide,
                eqheld.right.engaged() ? reset('[Master]', 'balance') : set('[Master]', 'balance')
            );

            modeset(device.left.gain.mode.relative);
            watch("[Master]", "headVolume", patch(device.left.gain.meter.centerbar));
            expect(device.left.gain.slide, budge('[Master]', 'headVolume'));

            modeset(device.right.gain.mode.relative);
            watch("[Master]", "volume", patch(device.right.gain.meter.centerbar));
            expect(device.right.gain.slide, budge('[Master]', 'volume'));

            watch("[Master]", "VuMeterL", vupatch(device.left.meter.bar));
            watch("[Master]", "VuMeterR", vupatch(device.right.meter.bar));
        }

        if (deck.left.held() || deck.right.held()) {
            // Needledrop handled in Side()
        } else {
            expect(device.crossfader.slide, set("[Master]", "crossfader"));
            watch("[Master]", "crossfader", patch(device.crossfader.meter.centerbar));
        }

        // Communicate currently selected channel of each deck so SCS3d can read it
        // THIS USES A CONTROL FOR ULTERIOR PURPOSES AND IS VERY NAUGHTY INDEED
        engine.setValue('[PreviewDeck1]', 'quantize',
            0x4 // Setting bit three communicates that we're sending deck state
            | deck.left.engaged() // left side is in bit one
            | deck.right.engaged() << 1 // right side bit two
        );
        watch('[PreviewDeck1]', 'quantize', function(deckState) {
            var changed = deck.left.change(deckState & 1) || deck.right.change(deckState & 2);
            if (changed) repatch(function() {})();
        });
    }

    return {
        start: function() {
            loading = true;
            modeset([device.flat]);
            patchage();
        },
        receive: receive,
        stop: function() {
            clear();
            tell(device.lightsoff);
            send(device.logo.on, true);
        }
    };
};
