/* global controller */

/** Common HID script debugging function. Just to get logging with 'HID' prefix. */
var HIDDebug = function(message) {
    print("HID " + message);
};

/** HID Bit Vector Class
 *
 * Collection of bits in one parsed packet field. These objects are
 * created by HIDPacket addControl and addOutput and should not be
 * created manually. */
var HIDBitVector = function() {
    this.size = 0;
    this.bits = {};
};

/** Return bit offset based on bitmask */
HIDBitVector.prototype.getOffset = function(bitmask) {
    for (var i = 0; i < 32; i++)
        if ((1 & bitmask >> i) !== 0)
            return i;
    return 0;
};

/** Add a control bitmask to the HIDBitVector */
HIDBitVector.prototype.addBitMask = function(group, name, bitmask) {
    var bit = {};
    bit.type = "button";
    bit.packet = undefined;
    bit.id = group + "." + name;
    bit.group = group;
    bit.name = name;
    bit.mapped_group = undefined;
    bit.mapped_name = undefined;
    bit.bitmask = bitmask;
    bit.bitmask = bitmask;
    bit.bit_offset = this.getOffset(bitmask);
    bit.callback = undefined;
    bit.value = undefined;
    bit.auto_repeat = undefined;
    bit.auto_repeat_interval = undefined;
    this.bits[bit.id] = bit;
};

/** Add a Output control bitmask to the HIDBitVector */
HIDBitVector.prototype.addOutputMask = function(group, name, bitmask) {
    var bit = {};
    bit.type = "output";
    bit.packet = undefined;
    bit.id = group + "." + name;
    bit.group = group;
    bit.name = name;
    bit.mapped_group = undefined;
    bit.mapped_name = undefined;
    bit.bitmask = bitmask;
    bit.bit_offset = this.getOffset(bitmask);
    bit.callback = undefined;
    bit.value = undefined;
    bit.toggle = undefined;
    this.bits[bit.id] = bit;
};

/** HID Modifiers object
 *
 * Wraps all defined modifiers to one object with uniform API.
 * Don't call directly, this is available as HIDController.modifiers */
var HIDModifierList = function() {
    this.modifiers = Object();
    this.callbacks = Object();
};

/** Add a new modifier to controller. */
HIDModifierList.prototype.add = function(name) {
    if (name in this.modifiers) {
        HIDDebug("Modifier already defined: " + name);
        return;
    }
    this.modifiers[name] = undefined;
};

/** Set modifier value */
HIDModifierList.prototype.set = function(name, value) {
    if (!(name in this.modifiers)) {
        HIDDebug("Unknown modifier: " + name);
        return;
    }
    this.modifiers[name] = value;
    if (name in this.callbacks) {
        var callback = this.callbacks[name];
        callback(value);
    }
};

/** Get modifier value */
HIDModifierList.prototype.get = function(name) {
    if (!(name in this.modifiers)) {
        HIDDebug("Unknown modifier: " + name);
        return false;
    }
    return this.modifiers[name];
};

/** Set modifier callback (update function after modifier state changes) */
HIDModifierList.prototype.setCallback = function(name, callback) {
    if (!(name in this.modifiers)) {
        HIDDebug("Unknown modifier: " + name);
        return;
    }
    this.callbacks[name] = callback;
};

/**
 * HID Packet object
 *
 * One HID input/output packet to register to HIDController
 * @param name     name of packet
 * @param reportId report ID of the packet. If the device only uses
 *          one report type, this must be 0.
 * @param callback function to call when this packet type is input
 *          and is received. If packet callback is set, the
 *          packet is not parsed by delta functions.
 *          callback is not meaningful for output packets
 * @param header   (optional) list of bytes to match from beginning
 *          of packet. Do NOT put the report ID in this; use
 *          the reportId parameter instead. */
var HIDPacket = function(name, reportId, callback, header) {
    this.name = name;
    this.header = header;
    this.callback = callback;

    this.reportId = 0;
    if (reportId !== undefined) {
        this.reportId = reportId;
    }

    this.groups = {};

    // Size of various 'pack' values in bytes
    this.packSizes = {b: 1, B: 1, h: 2, H: 2, i: 4, I: 4};
    this.signedPackFormats = ["b", "h", "i"];
};

/** Pack a field value to the packet.
 * Can only pack bits and byte values, patches welcome. */
HIDPacket.prototype.pack = function(data, field) {
    var value;
    if (!(field.pack in this.packSizes)) {
        HIDDebug("ERROR parsing packed value: invalid pack format " + field.pack);
        return;
    }
    var bytes = this.packSizes[field.pack];
    var signed = false;
    if (this.signedPackFormats.indexOf(field.pack) !== -1)
        signed = true;

    if (field.type === "bitvector") {
        // TODO - fix multi byte bit vector outputs
        if (bytes > 1) {
            HIDDebug("ERROR: packing multibyte bit vectors not yet supported");
            return;
        }
        for (var bit_id in field.value.bits) {
            var bit = field.value.bits[bit_id];
            data[field.offset] = data[field.offset] | bit.value;
        }
        return;
    }

    value = (field.value !== undefined) ? field.value : 0;

    if (value < field.min || value > field.max) {
        HIDDebug("ERROR " + field.id + " packed value out of range: " + value);
        return;
    }

    for (var byte_index = 0; byte_index < bytes; byte_index++) {
        var index = field.offset + byte_index;
        if (signed) {
            if (value >= 0) {
                data[index] = (value >> (byte_index * 8)) & 255;
            } else {
                data[index] = 255 - ((-(value + 1) >> (byte_index * 8)) & 255);
            }
        } else {
            data[index] = (value >> (byte_index * 8)) & 255;
        }
    }

};

/** Parse and return field value matching the 'pack' field from field attributes.
 * Valid values are:
 *  b       signed byte
 *  B       unsigned byte
 *  h       signed short
 *  H       unsigned short
 *  i       signed integer
 *  I       unsigned integer */
HIDPacket.prototype.unpack = function(data, field) {
    var value = 0;

    if (!(field.pack in this.packSizes)) {
        HIDDebug("ERROR parsing packed value: invalid pack format " + field.pack);
        return;
    }
    var bytes = this.packSizes[field.pack];
    var signed = false;
    if (this.signedPackFormats.indexOf(field.pack) !== -1)
        signed = true;

    for (var field_byte = 0; field_byte < bytes; field_byte++) {
        if (data[field.offset + field_byte] === 255 && field_byte === 4)
            value += 0;
        else
            value += data[field.offset + field_byte] * Math.pow(2, (field_byte * 8));
    }
    if (signed) {
        var max_value = Math.pow(2, bytes * 8);
        var split = max_value / 2 - 1;
        if (value > split)
            value = value - max_value;
    }
    return value;
};

/** Find HID packet group matching name.
 * Create group if create is true */
HIDPacket.prototype.getGroup = function(name, create) {
    if (this.groups === undefined)
        this.groups = {};
    if (name in this.groups)
        return this.groups[name];
    if (!create)
        return undefined;
    this.groups[name] = {};
    return this.groups[name];
};

/** Lookup HID packet field matching given offset and pack type
 * Returns undefined if no patching field can be found. */
HIDPacket.prototype.getFieldByOffset = function(offset, pack) {
    if (!(pack in this.packSizes)) {
        HIDDebug("Unknown pack string " + pack);
        return undefined;
    }
    var end_offset = offset + this.packSizes[pack];
    var group;
    var field;
    for (var group_name in this.groups) {
        group = this.groups[group_name];
        for (var field_id in group) {
            field = group[field_id];
            // Same field offset
            if (field.offset === offset)
                return field;
            // 7-8 8-9
            // Offset for smaller packet inside multibyte field
            if (field.offset < offset && field.end_offset >= end_offset)
                return field;
            // Packet offset starts inside field, may overflow
            if (field.offset < offset && field.end_offset > offset)
                return field;
            // Packet start before field, ends or overflows field
            if (field.offset > offset && field.offset < end_offset)
                return field;
        }
    }
    return undefined;
};

/** Return a field by group and name from the packet,
 * Returns undefined if field could not be found */
HIDPacket.prototype.getField = function(group, name) {
    var field_id = group + "." + name;
    if (!(group in this.groups)) {
        HIDDebug("PACKET " + this.name + " group not found " + group);
        return undefined;
    }

    var control_group = this.groups[group];
    if (field_id in control_group)
        return control_group[field_id];

    // Lookup for bit fields in bitvector matching field name
    for (var group_name in this.groups) {
        control_group = this.groups[group_name];
        for (var field_name in control_group) {
            var field = control_group[field_name];
            if (field === undefined || field.type !== "bitvector")
                continue;
            for (var bit_name in field.value.bits) {
                var bit = field.value.bits[bit_name];
                if (bit.id === field_id) {
                    return field;
                }
            }
        }
    }
    // Field not found
    return undefined;
};

/** Return reference to a bit in a bitvector field */
HIDPacket.prototype.lookupBit = function(group, name) {
    var field = this.getField(group, name);
    if (field === undefined) {
        HIDDebug("Bitvector match not found: " + group + "." + name);
        return undefined;
    }
    var bit_id = group + "." + name;
    for (var bit_name in field.value.bits) {
        var bit = field.value.bits[bit_name];
        if (bit.id === bit_id)
            return bit;
    }
    HIDDebug("BUG: bit not found after successful field lookup");
    return undefined;
};

/** Remove a control registered. Normally not needed */
HIDPacket.prototype.removeControl = function(group, name) {
    var control_group = this.getGroup(group);
    if (!(name in control_group)) {
        HIDDebug("Field not in control group " + group + ": " + name);
        return;
    }
    delete control_group[name];
};

/** Register a numeric value to parse from input packet
 *
 * @param group     control group name
 * @param name      name of the field
 * @param offset    field offset inside packet (bytes)
 * @param pack      control packing format for unpack(), one of b/B, h/H, i/I
 * @param bitmask   bitmask size, undefined for byte(s) controls
 *           NOTE: Parsing bitmask with multiple bits is not supported yet.
 * @param isEncoder indicates if this is an encoder which should be wrapped and delta reported
 * @param callback callback function for the control
 */
HIDPacket.prototype.addControl = function(group, name, offset, pack, bitmask, isEncoder, callback) {
    var control_group = this.getGroup(group, true);
    var bitvector = undefined;
    if (control_group === undefined) {
        HIDDebug("ERROR creating HID packet group " + group);
        return;
    }
    if (!(pack in this.packSizes)) {
        HIDDebug("Unknown pack value " + pack);
        return;
    }

    var field = this.getFieldByOffset(offset, pack);
    if (field !== undefined) {
        if (bitmask === undefined) {
            HIDDebug("ERROR registering offset " + offset + " pack " + pack);
            HIDDebug(
                "ERROR trying to overwrite non-bitmask control " + group + " " + name
            );
            return;
        }
        bitvector = field.value;
        bitvector.addBitMask(group, name, bitmask);
        if (callback !== undefined) {
            if (typeof callback !== "function") {
                HIDDebug("ERROR callback provided for " + group + "." + name + " is not a function.");
                return;
            }
            this.setCallback(group, name, callback);
        }
        return;
    }

    field = {};
    field.packet = undefined;
    field.id = group + "." + name;
    field.group = group;
    field.name = name;
    field.mapped_group = undefined;
    field.mapped_name = undefined;
    field.pack = pack;
    field.offset = offset;
    field.end_offset = offset + this.packSizes[field.pack];
    field.bitmask = bitmask;
    field.isEncoder = isEncoder;
    field.callback = undefined;
    field.soft_takeover = false;
    field.ignored = false;
    field.auto_repeat = undefined;
    field.auto_repeat_interval = undefined;

    var packet_max_value = Math.pow(2, this.packSizes[field.pack] * 8);
    if (this.signedPackFormats.indexOf(pack) !== -1) {
        field.min = 0 - (packet_max_value / 2) + 1;
        field.max = (packet_max_value / 2) - 1;
    } else {
        field.min = 0;
        field.max = packet_max_value - 1;
    }

    if (bitmask === undefined || bitmask === packet_max_value) {
        field.type = "control";
        field.value = undefined;
        field.delta = 0;
        field.mindelta = 0;
    } else {
        if (this.signedPackFormats.indexOf(pack) !== -1) {
            HIDDebug("ERROR registering bitvector: signed fields not supported");
            return;
        }
        // Create a new bitvector field and add the bit to that
        // TODO - accept controls with bitmask < packet_max_value
        var field_name = "bitvector_" + offset;
        field.type = "bitvector";
        field.name = field_name;
        field.id = group + "." + field_name;
        bitvector = new HIDBitVector(field.max);
        bitvector.size = field.max;
        bitvector.addBitMask(group, name, bitmask);
        field.value = bitvector;
        field.delta = undefined;
        field.soft_takeover = undefined;
        field.mindelta = undefined;
    }

    // Add the new field to the packet
    control_group[field.id] = field;

    if (callback !== undefined) {
        if (typeof callback !== "function") {
            HIDDebug("ERROR callback provided for " + group + "." + name + " is not a function.");
            return;
        }
        this.setCallback(group, name, callback);
    }
};

/** Register a Output control field or Output control bit to output packet
 * Output control field:
 *    Output field with no bitmask, controls Output with multiple values
 * Output control bit:
 *    Output with with bitmask, controls Output with a single bit
 *
 * It is recommended to define callbacks after packet creation with
 * setCallback instead of adding it directly here. But you can do it.
 *
 * @param offset the offset of the byte(s) to use, starting at 1
 *          (0 is automatically populated with the reportId)
 * @param pack control packing format for pack(), one of b/B, h/H, i/I */
HIDPacket.prototype.addOutput = function(group, name, offset, pack, bitmask, callback) {
    var control_group = this.getGroup(group, true);
    var field;
    var bitvector = undefined;
    var field_id = group + "." + name;

    if (control_group === undefined) {
        return;
    }
    if (!(pack in this.packSizes)) {
        HIDDebug("ERROR: unknown Output control pack value " + pack);
        return;
    }

    // Adjust offset by 1 because the reportId was previously considered part of the payload
    // but isn't anymore and we can't be bothered to adjust every single script manually
    offset -= 1;

    // Check if we are adding a Output bit to existing bitvector
    field = this.getFieldByOffset(offset, pack);
    if (field !== undefined) {
        if (bitmask === undefined) {
            HIDDebug(
                "ERROR: overwrite non-bitmask control " + group + "." + name
            );
            return;
        }
        bitvector = field.value;
        bitvector.addOutputMask(group, name, bitmask);
        return;
    }

    field = {};
    field.id = field_id;
    field.group = group;
    field.name = name;
    field.mapped_group = undefined;
    field.mapped_name = undefined;
    field.pack = pack;
    field.offset = offset;
    field.end_offset = offset + this.packSizes[field.pack];
    field.bitmask = bitmask;
    field.callback = callback;
    field.toggle = undefined;

    var packet_max_value = Math.pow(2, this.packSizes[field.pack] * 8);
    if (this.signedPackFormats.indexOf(pack) !== -1) {
        field.min = 0 - (packet_max_value / 2) + 1;
        field.max = (packet_max_value / 2) - 1;
    } else {
        field.min = 0;
        field.max = packet_max_value - 1;
    }
    if (bitmask === undefined || bitmask === packet_max_value) {
        field.type = "output";
        field.value = undefined;
        field.delta = undefined;
        field.mindelta = undefined;
    } else {
        // Create new Output bitvector control field, add bit to it
        // rewrite name to use bitvector instead
        var field_name = "bitvector_" + offset;
        field.type = "bitvector";
        field.id = group + "." + field_name;
        field.name = field_name;
        bitvector = new HIDBitVector();
        bitvector.size = field.max;
        bitvector.addOutputMask(group, name, bitmask);
        field.value = bitvector;
        field.delta = undefined;
        field.mindelta = undefined;
    }

    // Add Output to HID packet
    control_group[field.id] = field;
};

/** Register a callback to field or a bit vector bit.
 * Does not make sense for Output fields but you can do that. */
HIDPacket.prototype.setCallback = function(group, name, callback) {
    var field = this.getField(group, name);
    var field_id = group + "." + name;
    if (callback === undefined) {
        HIDDebug("Callback to add was undefined for " + field_id);
        return;
    }
    if (field === undefined) {
        HIDDebug("setCallback: field for " + field_id + " not found"
        );
        return;
    }
    if (field.type === "bitvector") {
        for (var bit_id in field.value.bits) {
            var bit = field.value.bits[bit_id];
            if (bit_id !== field_id)
                continue;
            bit.callback = callback;
            return;
        }
        HIDDebug("ERROR: BIT NOT FOUND " + field_id);
    } else {
        field.callback = callback;
    }
};

/** Set 'ignored' flag for field to given value (true or false)
 * If field is ignored, it is not reported in 'delta' objects. */
HIDPacket.prototype.setIgnored = function(group, name, ignored) {
    var field = this.getField(group, name);
    if (field === undefined) {
        HIDDebug("ERROR setting ignored flag for " + group + " " + name);
        return;
    }
    field.ignored = ignored;
};

/** Adjust field's minimum delta value.
 * Input value changes smaller than this are not reported in delta */
HIDPacket.prototype.setMinDelta = function(group, name, mindelta) {
    var field = this.getField(group, name);
    if (field === undefined) {
        HIDDebug("ERROR adjusting mindelta for " + group + " " + name);
        return;
    }
    if (field.type === "bitvector") {
        HIDDebug("ERROR setting mindelta for bitvector packet does not make sense");
        return;
    }
    field.mindelta = mindelta;
};

/** Parse bitvector field values, returning object with the named bits set.
 * Value must be a valid unsigned byte to parse, with enough bits.
 * Returns list of modified bits (delta) */
HIDPacket.prototype.parseBitVector = function(field, value) {
    var bits = {};
    var bit;
    var new_value;
    for (var bit_id in field.value.bits) {
        bit = field.value.bits[bit_id];
        new_value = (bit.bitmask & value) >> bit.bit_offset;
        if (bit.value !== undefined && bit.value !== new_value)
            bits[bit_id] = bit;
        bit.value = new_value;
    }
    return bits;
};

/** Parse input packet fields from data.
 * Data is expected to be a Packet() received from HID device.
 * Returns list of changed fields with new value.
 * BitVectors are returned as bits you can iterate separately. */
HIDPacket.prototype.parse = function(data) {
    var field_changes = {};
    var group;
    var group_name;
    var field;
    var field_id;

    for (group_name in this.groups) {
        group = this.groups[group_name];
        for (field_id in group) {
            field = group[field_id];
            if (field === undefined)
                continue;

            var value = this.unpack(data, field);
            if (value === undefined) {
                HIDDebug("Error parsing packet field value for " + field_id);
                return;
            }

            if (field.type === "bitvector") {
                // Bitvector deltas are checked in parseBitVector
                var changed_bits = this.parseBitVector(field, value);
                for (var bit_name in changed_bits)
                    field_changes[bit_name] = changed_bits[bit_name];

            } else if (field.type === "control") {
                if (field.value === value && field.mindelta !== undefined)
                    continue;
                if (field.ignored || field.value === undefined) {
                    field.value = value;
                    continue;
                }
                var change;
                if (field.isEncoder) {
                    if (field.value === field.max && value === field.min) {
                        change = 1;
                        field.delta = 1;
                    } else if (value === field.max && field.value === field.min) {
                        change = 1;
                        field.delta = -1;
                    } else {
                        change = 1;
                        field.delta = value - field.value;
                    }
                    field.value = value;
                } else {
                    change = Math.abs(value - field.value);
                    field.delta = value - field.value;
                }
                if (field.mindelta === undefined || change > field.mindelta) {
                    field_changes[field.id] = field;
                    field.value = value;
                }
            }
        }
    }
    return field_changes;
};

/** Send this HID packet to device.
 * First the header bytes are copied to beginning of packet, then
 * field object values are packed to the HID packet according to the
 * field type. */
HIDPacket.prototype.send = function(debug) {
    var data = [];

    if (this.header !== undefined) {
        for (var header_byte = 0; header_byte < this.header.length; header_byte++) {
            data[header_byte] = this.header[header_byte];
        }
    }

    for (var group_name in this.groups) {
        var group = this.groups[group_name];
        for (var field_name in group) {
            this.pack(data, group[field_name]);
        }
    }

    if (debug) {
        var packet_string = "";
        for (var d in data) {
            if (data[d] < 0x10) {
                // Add padding for bytes smaller than 10
                packet_string += "0";
            }
            packet_string += data[d].toString(16) + " ";
        }
        HIDDebug("Sending packet with Report ID " + this.reportId + ": " + packet_string);
    }
    controller.send(data, data.length, this.reportId);
};

/**
 * HID Controller Class
 *
 * HID Controller with packet parser
 * Global attributes include:
 *
 * initialized          by default false, you should set this to true when
 *                      controller is found and everything is OK
 * activeDeck           by default undefined, used to map the virtual deck
 *                      names 'deck','deck1' and 'deck2' to actual [ChannelX]
 * isScratchEnabled     set to true, when button 'jog_touch' is active
 * buttonStates         valid state values for buttons, should contain fields
 *                      released (default 0) and pressed (default 1)
 * LEDColors            possible Output colors named, must contain 'off' value
 * deckOutputColors        Which colors to use for each deck. Default 'on' for first
 *                      four decks. Values are like {1: 'red', 2: 'green' }
 *                      and must reference valid OutputColors fields.
 * OutputUpdateInterval    By default undefined. If set, it's a value for timer
 *                      executed every n ms to update Outputs with updateOutputs()
 * modifiers            Reference to HIDModifierList object
 * toggleButtons        List of button names you wish to act as 'toggle', i.e.
 *                      pressing the button and releasing toggles state of the
 *                      control and does not set it off again when released.
 *
 * Scratch variables (initialized with 'common' defaults, you can override):
 * scratchintervalsPerRev   Intervals value for scratch_enable
 * scratchRPM               RPM value for scratch_enable
 * scratchAlpha             Alpha value for scratch_enable
 * scratchBeta              Beta value for scratch_enable
 * scratchRampOnEnable      If 'ramp' is used when enabling scratch
 * scratchRampOnDisable     If 'ramp' is used when disabling scratch
 */
var HIDController = function() {
    this.initialized = false;
    this.activeDeck = undefined;

    this.InputPackets = {};
    this.OutputPackets = {};
    // Default input control packet name: can be modified for controllers
    // which can swap modes (wiimote for example)
    this.defaultPacket = "control";

    // Callback functions called by deck switching. Undefined by default
    this.disconnectDeck = undefined;
    this.connectDeck = undefined;

    // Scratch parameter defaults for this.scratchEnable function
    // override for custom control
    this.isScratchEnabled = false;
    this.scratchintervalsPerRev = 128;
    this.scratchRPM = 33 + 1 / 3;
    this.scratchAlpha = 1.0 / 8;
    this.scratchBeta = this.scratchAlpha / 32;
    this.scratchRampOnEnable = false;
    this.scratchRampOnDisable = false;

    // Button states available
    this.buttonStates = {released: 0, pressed: 1};
    // Output color values to send
    this.LEDColors = {off: 0x0, on: 0x7f};
    // Toggle buttons
    this.toggleButtons = ["play", "pfl", "keylock", "quantize", "reverse", "slip_enabled",
        "group_[Channel1]_enable", "group_[Channel2]_enable",
        "group_[Channel3]_enable", "group_[Channel4]_enable"];

    // Override to set specific colors for multicolor button Output per deck
    this.deckOutputColors = {1: "on", 2: "on", 3: "on", 4: "on"};
    // Mapping of automatic deck switching with deckSwitch function
    this.virtualDecks = ["deck", "deck1", "deck2", "deck3", "deck4"];
    this.deckSwitchMap = {1: 2, 2: 1, 3: 4, 4: 3, undefined: 1};

    // Standard target groups available in mixxx. This is used by
    // HID packet parser to recognize group parameters we should
    // try sending to mixxx.
    this.valid_groups = [
        "[Channel1]", "[Channel2]", "[Channel3]", "[Channel4]",
        "[Sampler1]", "[Sampler2]", "[Sampler3]", "[Sampler4]",
        "[Sampler5]", "[Sampler6]", "[Sampler7]", "[Sampler8]",
        "[Master]", "[PreviewDeck1]", "[Effects]", "[Playlist]", "[Flanger]",
        "[Microphone]", "[EffectRack1_EffectUnit1]", "[EffectRack1_EffectUnit2]",
        "[EffectRack1_EffectUnit3]", "[EffectRack1_EffectUnit4]",
        "[InternalClock]"];

    // Set to value in ms to update Outputs periodically
    this.OutputUpdateInterval = undefined;

    this.modifiers = new HIDModifierList();
    this.scalers = {};
    this.timers = {};

    this.auto_repeat_interval = 100;
};

/** Function to close the controller object cleanly */
HIDController.prototype.close = function() {
    for (var name in this.timers) {
        var timer = this.timers[name];
        if (timer === undefined)
            continue;
        engine.stopTimer(timer);
        this.timers[name] = undefined;
    }
};

/** Initialize our packet data and callbacks. This does not seem to
 * work when executed from here, but we keep a stub just in case. */
HIDController.prototype.initializePacketData = function() {
};

/** Return deck number from deck name. Deck name can't be virtual deck name
 * in this function call. */
HIDController.prototype.resolveDeck = function(group) {
    if (group === undefined)
        return undefined;
    var result = group.match(/\[Channel[0-9]+\]/);
    if (!result)
        return undefined;
    var str = group.replace(/\[Channel/, "");
    return str.substring(0, str.length - 1);
};

/** Return the group name from given deck number. */
HIDController.prototype.resolveDeckGroup = function(deck) {
    if (deck === undefined)
        return undefined;
    return "[Channel" + deck + "]";
};

/** Map virtual deck names to real deck group. If group is already
 * a real mixxx group value, just return it as it without mapping. */
HIDController.prototype.resolveGroup = function(group) {
    var channel_name = /\[Channel[0-9]+\]/;
    if (group !== undefined && group.match(channel_name))
        return group;
    if (this.valid_groups.indexOf(group) !== -1) {
        return group;
    }
    if (group === "deck" || group === undefined) {
        if (this.activeDeck === undefined)
            return undefined;
        return "[Channel" + this.activeDeck + "]";
    }
    if (this.activeDeck === 1 || this.activeDeck === 2) {
        if (group === "deck1") return "[Channel1]";
        if (group === "deck2") return "[Channel2]";
    }
    if (this.activeDeck === 3 || this.activeDeck === 4) {
        if (group === "deck1") return "[Channel3]";
        if (group === "deck2") return "[Channel4]";
    }
    return undefined;
};

/** Find Output control matching give group and name
 * Returns undefined if output field can't be found. */
HIDController.prototype.getOutputField = function(m_group, m_name) {
    for (var packet_name in this.OutputPackets) {
        var packet = this.OutputPackets[packet_name];
        for (var group_name in packet.groups) {
            var group = packet.groups[group_name];
            for (var field_name in group) {
                var field = group[field_name];
                if (field.type === "bitvector") {
                    for (var bit_id in field.value.bits) {
                        var bit = field.value.bits[bit_id];
                        if (bit.mapped_group === m_group && bit.mapped_name === m_name)
                            return bit;
                        if (bit.group === m_group && bit.name === m_name)
                            return bit;
                    }
                    continue;
                }
                if (field.mapped_group === m_group && field.mapped_name === m_name)
                    return field;
                if (field.group === m_group && field.name === m_name)
                    return field;
            }
        }
    }
    return undefined;
};

/** Find input packet matching given name.
 * Returns undefined if input packet name is not registered. */
HIDController.prototype.getInputPacket = function(name) {
    if (!(name in this.InputPackets))
        return undefined;
    return this.InputPackets[name];
};

/** Find output packet matching given name
 * Returns undefined if output packet name is not registered. */
HIDController.prototype.getOutputPacket = function(name) {
    if (!(name in this.OutputPackets))
        return undefined;
    return this.OutputPackets[name];
};

/** Set input packet callback afterwards */
HIDController.prototype.setPacketCallback = function(packet, callback) {
    var input_packet = this.getInputPacket(packet);
    input_packet.callback = callback;
};

/** Register packet field's callback.
 * If packet has callback, it is still parsed but no field processing is done,
 * callback is called directly after unpacking fields from packet. */
HIDController.prototype.setCallback = function(packet, group, name, callback) {
    var input_packet = this.getInputPacket(packet);
    if (input_packet === undefined) {
        HIDDebug("Input packet not found " + packet);
        return;
    }
    input_packet.setCallback(group, name, callback);
};

/** Register scaling function for a control name
 * This does not check if given control name is valid */
HIDController.prototype.setScaler = function(name, callback) {
    if (name in this.scalers)
        return;
    this.scalers[name] = callback;
};

/** Lookup scaling function for control
 * Returns undefined if function is not registered. */
HIDController.prototype.getScaler = function(name, _callback) {
    if (!(name in this.scalers))
        return undefined;
    return this.scalers[name];
};

/** Change type of a previously defined field to modifier and register it */
HIDController.prototype.linkModifier = function(group, name, modifier) {
    var packet = this.getInputPacket(this.defaultPacket);
    if (packet === undefined) {
        HIDDebug(
            "ERROR creating modifier: input packet " + this.defaultPacket + " not found"
        );
        return;
    }
    var bit_id = group + "." + name;
    var field = packet.lookupBit(group, name);
    if (field === undefined) {
        HIDDebug("BIT field not found: " + bit_id);
        return;
    }
    field.group = "modifiers";
    field.name = modifier;
    this.modifiers.set(modifier);
};

/** TODO - implement unlinking of modifiers */
HIDController.prototype.unlinkModifier = function(_group, _name, _modifier) {
    HIDDebug("Unlinking of modifiers not yet implemented");
};

/** Link a previously declared HID control to actual mixxx control */
HIDController.prototype.linkControl = function(group, name, m_group, m_name, callback) {
    var field;
    var packet = this.getInputPacket(this.defaultPacket);
    if (packet === undefined) {
        HIDDebug("ERROR creating modifier: input packet " + this.defaultPacket + " not found");
        return;
    }
    field = packet.getField(group, name);
    if (field === undefined) {
        HIDDebug("Field not found: " + group + "." + name);
        return;
    }
    if (field.type === "bitvector") {
        field = packet.lookupBit(group, name);
        if (field === undefined) {
            HIDDebug("bit not found: " + group + "." + name);
            return;
        }
    }
    field.mapped_group = m_group;
    field.mapped_name = m_name;
    if (callback !== undefined)
        field.callback = callback;
};

/** TODO - implement unlinking of controls */
HIDController.prototype.unlinkControl = function(_group, _name) {

};

/** Register HID input packet type to controller.
 * Input packets can be responses from device to queries, or control
 * data details. The default control data packet must be named in
 * variable this.defaultPacket to allow automatic processing. */
HIDController.prototype.registerInputPacket = function(packet) {
    // Find modifiers and other special cases from packet fields
    for (var group_name in packet.groups) {
        var group = packet.groups[group_name];
        for (var field_name in group) {
            var field = group[field_name];
            field.packet = packet;
            if (field.type === "bitvector") {
                for (var bit_id in field.value.bits) {
                    var bit = field.value.bits[bit_id];
                    bit.packet = packet;
                    if (bit.group === "modifiers")
                        this.modifiers.add(bit.name);
                }
            } else {
                if (field.group === "modifiers")
                    this.modifiers.add(field.name);
            }
        }
    }
    this.InputPackets[packet.name] = packet;
};

/** Register HID output packet type to controller
 * There are no special Output control output packets, just register Outputs to any
 * valid packet and we detect them here.
 * This module only supports sending bitvector values and byte fields to device.
 * If you need other data structures, patches are welcome, or you can just do it
 * manually in your script without registering the packet. */
HIDController.prototype.registerOutputPacket = function(packet) {
    this.OutputPackets[packet.name] = packet;
    // Link packet to all fields
    for (var group_name in packet.groups) {
        var group = packet.groups[group_name];
        for (var field_name in group) {
            var field = group[field_name];
            field.packet = packet;
            if (field.type === "bitvector") {
                for (var bit_id in field.value.bits) {
                    var bit = field.value.bits[bit_id];
                    bit.packet = packet;
                }
            }
        }
    }
};

/** Parse a received input packet fields with "unpack" calls to fields
 * Calls packet callback and returns, if packet callback was defined
 * Calls processIncomingPacket and processes automated events there.
 * If defined, calls processDelta for results after processing automated fields */
HIDController.prototype.parsePacket = function(data, length) {
    var packet;
    var changed_data;
    if (this.InputPackets === undefined) {
        return;
    }
    for (var name in this.InputPackets) {
        packet = this.InputPackets[name];

        // When the device uses multiple report types with report IDs, hidapi
        // prepends the report ID to the data sent to Mixxx. If the device
        // only has a single report type, the HIDPacket constructor sets the
        // reportId as 0. In this case, hidapi only sends the data of the
        // report to Mixxx without a report ID.
        if (packet.reportId !== 0 && packet.reportId !== data[0]) {
            continue;
        }

        if (packet.header !== undefined) {
            for (var header_byte = 0; header_byte < packet.header.length; header_byte++) {
                if (packet.header[header_byte] !== data[header_byte]) {
                    packet = undefined;
                    break;
                }
            }
            if (packet === undefined)
                continue;
        }
        changed_data = packet.parse(data);
        if (packet.callback !== undefined) {
            packet.callback(packet, changed_data);
            return;
        }
        // Process named group controls
        if (packet.name === this.defaultPacket)
            this.processIncomingPacket(packet, changed_data);
        // Process generic changed_data packet, if callback is defined
        if (this.processDelta !== undefined)
            this.processDelta(packet, changed_data);
        if (this.postProcessDelta !== undefined)
            this.postProcessDelta(packet, changed_data);
        return;
    }
    HIDDebug("Received unknown packet of " + length + " bytes");
    for (var i in data) HIDDebug("BYTE " + data[i]);
};

/** Process the modified field values (delta) from input packet fields for
 * input control packet, if packet name is in this.defaultPacket.
 *
 * Button field processing:
 * - Sets modifiers from buttons
 * - Calls button callbacks, if defined
 * - Finally tries to run matching engine.setValue() function for buttons
 *   in default mixxx groups, honoring toggleButtons and other button
 *   details. Not done if a callback was defined for button.
 *
 * Control field processing
 * - Calls scaling functions for control fields, if defined for field.
 *   Scaling function for encoders (isEncoder attribute is true) scales
 *   field delta instead of raw value.
 * - Calls callback functions for control fields, if defined for field
 * - Finally tries run matching engine.setValue() function for control
 *   fields in default mixxx groups. Not done if a callback was defined. */
HIDController.prototype.processIncomingPacket = function(packet, delta) {
    var field;
    for (var name in delta) {
        if (this.ignoredControlChanges !== undefined
            && this.ignoredControlChanges.indexOf(name) !== -1)
            continue;
        field = delta[name];
        if (field.type === "button")
            this.processButton(field);
        else if (field.type === "control")
            this.processControl(field);
        else
            HIDDebug("Unknown field " + field.name + " type " + field.type);
    }
};

/** Get active group for this field */
HIDController.prototype.getActiveFieldGroup = function(field) {
    if (field.mapped_group !== undefined) {
        return this.resolveGroup(field.mapped_group);
    }
    var group = field.group;
    if (group === undefined) {
        if (this.activeDeck !== undefined)
            return "[Channel" + this.activeDeck + "]";
    }
    if (this.valid_groups.indexOf(group) !== -1) {
        //HIDDebug("Resolving group " + group);
        return this.resolveGroup(group);
    }
    return group;
};

/** Get active control name from field */
HIDController.prototype.getActiveFieldControl = function(field) {
    if (field.mapped_name !== undefined)
        return field.mapped_name;
    return field.name;
};

/** Process given button field, triggering events */
HIDController.prototype.processButton = function(field) {
    var group = this.getActiveFieldGroup(field);
    var control = this.getActiveFieldControl(field);

    if (group === undefined) {
        HIDDebug("processButton: Could not resolve group from "
            + field.group + " " + field.mapped_group + " "
            + field.name + " " + field.mapped_name
        );
        return;
    }

    if (group === "modifiers") {
        if (field.value !== 0)
            this.modifiers.set(control, true);
        else
            this.modifiers.set(control, false);
        return;
    }
    if (field.auto_repeat) {
        var timer_id = "auto_repeat_" + field.id;
        if (field.value) {
            this.startAutoRepeatTimer(timer_id, field.auto_repeat_interval);
        } else {
            this.stopAutoRepeatTimer(timer_id);
        }
    }
    if (field.callback !== undefined) {
        field.callback(field);
        return;
    }
    if (control === "jog_touch") {
        if (group !== undefined) {
            if (field.value === this.buttonStates.pressed)
                this.enableScratch(group, true);
            else
                this.enableScratch(group, false);
        }
        return;
    }
    if (this.toggleButtons.indexOf(control) !== -1) {
        if (field.value === this.buttonStates.released)
            return;
        if (engine.getValue(group, control)) {
            if (control === "play")
                engine.setValue(group, "stop", true);
            else
                engine.setValue(group, control, false);
        } else {
            engine.setValue(group, control, true);
        }
        return;
    }
    if (field.auto_repeat && field.value === this.buttonStates.pressed) {
        HIDDebug("Callback for " + field.group);
        engine.setValue(group, control, field.auto_repeat(field));
    } else if (engine.getValue(group, control) === false) {
        engine.setValue(group, control, true);
    } else {
        engine.setValue(group, control, false);
    }
};

/** Process given control field, triggering events */
HIDController.prototype.processControl = function(field) {
    var value;
    var group = this.getActiveFieldGroup(field);
    var control = this.getActiveFieldControl(field);

    if (group === undefined) {
        HIDDebug("processControl: Could not resolve group from "
            + field.group + " " + field.mapped_group + " "
            + field.name + " " + field.mapped_name
        );
        return;
    }

    if (field.callback !== undefined) {
        value = field.callback(field);
        return;
    }
    if (group === "modifiers") {
        this.modifiers.set(control, field.value);
        return;
    }
    if (control === "jog_wheel") {
        // Handle jog wheel scratching transparently
        this.jog_wheel(field);
        return;
    }
    // Call value scaler if defined and send mixxx signal
    value = field.value;
    var scaler = this.getScaler(control);
    if (field.isEncoder) {
        var field_delta = field.delta;
        if (scaler !== undefined)
            field_delta = scaler(group, control, field_delta);
        engine.setValue(group, control, field_delta);
    } else {
        if (scaler !== undefined) {
            value = scaler(group, control, value);
            // See the Traktor S4 script for how to use this.  If the scaler function has this
            // parameter set to true, we use the effects-engine setParameter call instead of
            // setValue.
            if (scaler.useSetParameter) {
                engine.setParameter(group, control, value);
                return;
            }
        }
        engine.setValue(group, control, value);
    }
};

/** Toggle control state from toggle button */
HIDController.prototype.toggle = function(group, control, value) {
    if (value === this.buttonStates.released)
        return;
    var status = engine.getValue(group, control) !== true;
    engine.setValue(group, control, status);
};

/** Toggle play/pause state */
HIDController.prototype.togglePlay = function(group, field) {
    if (field.value === this.buttonStates.released)
        return;
    var status = !(engine.getValue(group, "play"));
    if (!status)
        engine.setValue(group, "stop", true);
    else
        engine.setValue(group, "play", true);
};

/** Processing of the 'jog_touch' special button name, which is used to detect
 * when scratching should be enabled.
 * Deck is resolved from group with 'resolveDeck'
 *
 * Enabling scratching (press 'jog_touch' button)
 * Sets the internal 'isScratchEnabled' attribute to true, and calls scratchEnable
 * with the scratch attributes (see class definition)
 *
 * Disabling scratching (release 'jog_touch' button)
 * Sets the internal 'isScratchEnabled attribute to false, and calls scratchDisable
 * to end scratching mode */
HIDController.prototype.enableScratch = function(group, status) {
    var deck = this.resolveDeck(group);
    if (status) {
        this.isScratchEnabled = true;
        engine.scratchEnable(deck,
            this.scratchintervalsPerRev,
            this.scratchRPM,
            this.scratchAlpha,
            this.scratchBeta,
            this.rampedScratchEnable
        );
        if (this.enableScratchCallback !== undefined) this.enableScratchCallback(true);
    } else {
        this.isScratchEnabled = false;
        engine.scratchDisable(deck, this.rampedScratchDisable);
        if (this.enableScratchCallback !== undefined) this.enableScratchCallback(false);
    }
};

/** Default jog scratching function. Used to handle jog move events from special
 * input control field called 'jog_wheel'. Handles both 'scratch' and 'jog' mixxx
 * functions, depending on isScratchEnabled value above (see enableScratch())
 *
 * Since most controllers require value scaling for jog and scratch functions,
 * you are warned if following scaling function names are not registered:
 *
 * jog
 *      Scaling function from 'jog_wheel' for rate bend events with mixxx 'jog'
 *      function. Should return value range suitable for 'jog', whatever you
 *      wish it to do.
 * jog_scratch
 *      Scaling function from 'jog_wheel' for scratch movements with mixxx
 *      'scratchTick' function. Should return -1,0,1 or small ranges of integers
 *      both negative and positive values.
 */
HIDController.prototype.jog_wheel = function(field) {
    var scaler = undefined;
    var active_group = this.getActiveFieldGroup(field);
    var value = undefined;
    if (field.isEncoder)
        value = field.delta;
    else
        value = field.value;
    if (this.isScratchEnabled) {
        var deck = this.resolveDeck(active_group);
        if (deck === undefined)
            return;
        scaler = this.getScaler("jog_scratch");
        if (scaler !== undefined)
            value = scaler(active_group, "jog_scratch", value);
        else
            HIDDebug("WARNING non jog_scratch scaler, you likely want one");
        engine.scratchTick(deck, value);
    } else {
        if (active_group === undefined)
            return;
        scaler = this.getScaler("jog");
        if (scaler !== undefined)
            value = scaler(active_group, "jog", value);
        else
            HIDDebug("WARNING non jog scaler, you likely want one");
        engine.setValue(active_group, "jog", value);
    }
};

HIDController.prototype.stopAutoRepeatTimer = function(timer_id) {
    if (this.timers[timer_id]) {
        engine.stopTimer(this.timers[timer_id]);
        delete this.timers[timer_id];
    } else {
        //HIDDebug("No such autorepeat timer: " + timer_id);
    }
};

/** Toggle field autorepeat on or off */
HIDController.prototype.setAutoRepeat = function(group, name, callback, interval) {
    var packet = this.getInputPacket(this.defaultPacket);
    var field = packet.getField(group, name);
    if (field === undefined) {
        HIDDebug("setAutoRepeat: field not found " + group + "." + name);
        return;
    }
    field.auto_repeat = callback;
    if (interval)
        field.auto_repeat_interval = interval;
    else
        field.auto_repeat_interval = controller.auto_repeat_interval;
    if (callback)
        callback(field);
};

/** Callback for auto repeat timer to send again the values for
 * buttons and controls marked as 'auto_repeat'
 * Timer must be defined from actual controller side, because of
 * callback call namespaces and 'this' reference */
HIDController.prototype.autorepeatTimer = function() {
    var group_name;
    var group;
    var field;
    var field_name;
    var bit_name;
    var bit;
    var packet = this.InputPackets[this.defaultPacket];
    for (group_name in packet.groups) {
        group = packet.groups[group_name];
        for (field_name in group) {
            field = group[field_name];
            if (field.type !== "bitvector") {
                if (field.auto_repeat)
                    this.processControl(field);
                continue;
            }
            for (bit_name in field.value.bits) {
                bit = field.value.bits[bit_name];
                if (bit.auto_repeat)
                    this.processButton(bit);
            }
        }
    }
};

/** Toggle active deck and update virtual output field control mappings */
HIDController.prototype.switchDeck = function(deck) {
    var packet;
    var field;
    var controlgroup;
    if (deck === undefined) {
        if (this.activeDeck === undefined) {
            deck = 1;
        } else {
            // This is unusable: num_decks has always minimum 4 decks
            // var totalDecks = engine.getValue("[Master]","num_decks");
            // deck = (this.activeDeck+1) % totalDecks;
            deck = this.deckSwitchMap[this.activeDeck];
            if (deck === undefined)
                deck = 1;
        }
    }
    var new_group = this.resolveDeckGroup(deck);
    HIDDebug("Switching to deck " + deck + " group " + new_group);
    if (this.disconnectDeck !== undefined)
        this.disconnectDeck();
    for (var packet_name in this.OutputPackets) {
        packet = this.OutputPackets[packet_name];
        for (var group_name in packet.groups) {
            var group = packet.groups[group_name];
            for (var field_name in group) {
                field = group[field_name];
                if (field.type === "bitvector") {
                    for (var bit_id in field.value.bits) {
                        var bit = field.value.bits[bit_id];
                        if (this.virtualDecks.indexOf(bit.mapped_group) === -1)
                            continue;
                        controlgroup = this.resolveGroup(bit.mapped_group);
                        engine.connectControl(controlgroup, bit.mapped_name, bit.mapped_callback, true);
                        engine.connectControl(new_group, bit.mapped_name, bit.mapped_callback);
                        var value = engine.getValue(new_group, bit.mapped_name);
                        HIDDebug("BIT " + bit.group + "." + bit.name + " value " + value);
                        if (value)
                            this.setOutput(
                                bit.group, bit.name,
                                this.LEDColors[this.deckOutputColors[deck]]
                            );
                        else
                            this.setOutput(
                                bit.group, bit.name,
                                this.LEDColors.off
                            );
                    }
                    continue;
                }
                // Only move outputs of virtual decks
                if (this.virtualDecks.indexOf(field.mapped_group) === -1)
                    continue;
                controlgroup = this.resolveGroup(field.mapped_group);
                engine.connectControl(controlgroup, field.mapped_name, field.mapped_callback, true);
                engine.connectControl(new_group, field.mapped_name, field.mapped_callback);
                value = engine.getValue(new_group, field.mapped_name);
                if (value)
                    this.setOutput(
                        field.group, field.name,
                        this.LEDColors[this.deckOutputColors[deck]]
                    );
                else
                    this.setOutput(
                        field.group, field.name,
                        this.LEDColors.off
                    );
            }
        }
    }
    this.activeDeck = deck;
    if (this.connectDeck !== undefined)
        this.connectDeck();
};

/** Link a virtual HID Output to mixxx control */
HIDController.prototype.linkOutput = function(group, name, m_group, m_name, callback) {
    var controlgroup;
    var field = this.getOutputField(group, name);
    if (field === undefined) {
        HIDDebug("Linked output not found: " + group + "." + name);
        return;
    }
    if (field.mapped_group !== undefined) {
        HIDDebug("Output already linked: " + field.mapped_group);
        return;
    }
    controlgroup = this.resolveGroup(m_group);
    field.mapped_group = m_group;
    field.mapped_name = m_name;
    field.mapped_callback = callback;
    engine.connectControl(controlgroup, m_name, callback);
    if (engine.getValue(controlgroup, m_name))
        this.setOutput(m_group, m_name, "on");
    else
        this.setOutput(m_group, m_name, "off");
};

/** Unlink a virtual HID Output from mixxx control */
HIDController.prototype.unlinkOutput = function(group, name, callback) {
    var field = this.getOutputField(group, name);
    var controlgroup;
    if (field === undefined) {
        HIDDebug("Unlinked output not found: " + group + "." + name);
        return;
    }
    if (field.mapped_group === undefined || field.mapped_name === undefined) {
        HIDDebug("Unlinked output not mapped: " + group + "." + name);
        return;
    }
    controlgroup = this.resolveGroup(field.mapped_group);
    engine.connectControl(controlgroup, field.mapped_name, callback, true);
    field.mapped_group = undefined;
    field.mapped_name = undefined;
    field.mapped_callback = undefined;
};

/** Set output state to given value */
HIDController.prototype.setOutput = function(group, name, value, send_packet) {
    var field = this.getOutputField(group, name);
    if (field === undefined) {
        HIDDebug("setOutput: unknown field: " + group + "." + name);
        return;
    }
    field.value = value << field.bit_offset;
    field.toggle = value << field.bit_offset;
    if (send_packet)
        field.packet.send();
};

/** Set Output to toggle between two values. Reset with setOutput(name,'off') */
HIDController.prototype.setOutputToggle = function(group, name, toggle_value) {
    var field = this.getOutputField(group, name);
    if (field === undefined) {
        HIDDebug("setOutputToggle: unknown field " + group + "." + name);
        return;
    }
    field.value = toggle_value << field.bit_offset;
    field.toggle = toggle_value << field.bit_offset;
    field.packet.send();
};
