/***********************************************************************
* retro-220/webUI B220PanelUtil.js
************************************************************************
* Copyright (c) 2017, Paul Kimpel.
* Licensed under the MIT License, see
*       http://www.opensource.org/licenses/mit-license.php
************************************************************************
* JavaScript object definition for the Burroughs 220 Emulator Control
* Panel utility constructors:
*   NeonLamp
*   NeonLampBox
*   ColoredLamp
*   ToggleSwitch
*   ThreeWaySwitch
*   OrganSwitch
*   BlackControlKnob
*   PanelRegister
************************************************************************
* 2017-01-01  P.Kimpel
*   Original version, from retro-205 D205PanelUtil.js.
***********************************************************************/

/***********************************************************************
*  Panel Neon Lamp                                                     *
***********************************************************************/
function NeonLamp(parent, x, y, id) {
    /* Constructor for the neon lamp objects used within panels. x & y are the
    coordinates of the lamp within its containing element; id is the DOM id */

    this.state = 0;                     // current lamp state, 0=off
    this.topCaptionDiv = null;          // optional top caption element
    this.bottomCaptionDiv = null;       // optional bottom caption element

    // visible DOM element
    this.element = document.createElement("div");
    this.element.id = id;
    this.element.className = NeonLamp.lampClass;
    if (x !== null) {
        this.element.style.left = x.toString() + "px";
    }
    if (y !== null) {
        this.element.style.top = y.toString() + "px";
    }

    if (parent) {
        parent.appendChild(this.element);
    }
}

/**************************************/

NeonLamp.topCaptionClass = "neonLampTopCaption";
NeonLamp.bottomCaptionClass = "neonLampBottomCaption";
NeonLamp.lampClass = "neonLamp";
NeonLamp.litClass = "neonLamp neonLit";
NeonLamp.lampLevels = 6;
NeonLamp.levelClass = [                 // css class names for the lamp levels
            NeonLamp.lampClass,
            NeonLamp.litClass + "1",
            NeonLamp.litClass + "2",
            NeonLamp.litClass + "3",
            NeonLamp.litClass + "4",
            NeonLamp.litClass + "5",
            NeonLamp.litClass];

/**************************************/
NeonLamp.prototype.addEventListener = function addEventListener(eventName, handler, useCapture) {
    /* Sets an event handler whenever the image element is clicked */

    this.element.addEventListener(eventName, handler, useCapture);
};

/**************************************/
NeonLamp.prototype.set = function set(state) {
    /* Changes the visible state of the lamp according to the value of "state", 0-1 */
    var newState = Math.max(Math.min(Math.round(state*NeonLamp.lampLevels + 0.4999), NeonLamp.lampLevels), 0);

    if (this.state ^ newState) {         // the state has changed
        this.state = newState;
        this.element.className = NeonLamp.levelClass[newState];
    }
};

/**************************************/
NeonLamp.prototype.flip = function flip() {
    /* Complements the visible state of the lamp */

    this.set(1.0 - this.state/NeonLamp.lampLevels);
};

/**************************************/
NeonLamp.prototype.setCaption = function setCaption(caption, atBottom) {
    /* Establishes an optional caption at the top or bottom of a single lamp.
    Returns the caption element */
    var e = (atBottom ? this.bottomCaptionDiv : this.topCaptionDiv);

    if (e) {
        e.textContent = caption;
    } else {
        e = document.createElement("div");
        if (atBottom) {
            this.bottomCaptionDiv = e;
            e.className = NeonLamp.bottomCaptionClass;
        } else {
            this.topCaptionDiv = e;
            e.className = NeonLamp.topCaptionClass;
        }
        e.appendChild(document.createTextNode(caption));
        this.element.appendChild(e);
    }
    return e;
};

/***********************************************************************
*  Panel Neon Lamp Box                                                 *
***********************************************************************/
function NeonLampBox(parent, x, y, id, caption) {
    /* Constructor for the neon lamp-in-a-box objects used within panels.
    x & y are the coordinates of the box within its containing element;
    id is the DOM id */

    // visible DOM element
    this.element = document.createElement("div");
    this.element.id = id;
    this.element.className = NeonLampBox.lampBoxClass;
    if (x !== null) {
        this.element.style.left = x.toString() + "px";
    }
    if (y !== null) {
        this.element.style.top = y.toString() + "px";
    }

    this.lamp = new NeonLamp(this.element, 3, 3, id + "_Lamp");

    this.button = document.createElement("div");
    this.button.id = id + "_LampBtn";
    this.button.className = NeonLampBox.lampButtonClass;
    this.button.textContent = caption;
    this.element.appendChild(this.button);

    if (parent) {
        parent.appendChild(this.element);
    }
}

/**************************************/

NeonLampBox.lampBoxClass = "lampBox";
NeonLampBox.lampButtonClass = "lampButton";

/**************************************/
NeonLampBox.prototype.addEventListener = function addEventListener(eventName, handler, useCapture) {
    /* Sets an event handler whenever the image element is clicked */

    this.element.addEventListener(eventName, handler, useCapture);
};

/**************************************/
NeonLampBox.prototype.set = function set(state) {
    /* Changes the visible state of the lamp according to the value of "state", 0-1 */

    this.lamp.set(state);
};

/**************************************/
NeonLampBox.prototype.flip = function flip() {
    /* Complements the visible state of the lamp */

    this.lamp.flip();
};

/**************************************/
NeonLampBox.prototype.setCaption = function setCaption(caption) {
    /* Establishes an optional caption on the button for a single lamp */

    this.button.textContent = caption;
};


/***********************************************************************
*  Panel Colored Lamp                                                  *
***********************************************************************/
function ColoredLamp(parent, x, y, id, offClass, onClass) {
    /* Constructor for the colored lamp objects used within panels. x & y are
    the coordinates of the lamp within its containing element; id is the DOM id */

    this.state = 0;                     // current lamp state, 0=off
    this.topCaptionDiv = null;          // optional top caption element
    this.bottomCaptionDiv = null;       // optional bottom caption element
    this.lampClass = offClass;          // css styling for an "off" lamp
    this.litClass =                     // css styling for an "on" lamp
                offClass + " " + onClass;
    this.levelClass = [                 // css class names for the lamp levels
            offClass,
            this.litClass + "1",
            this.litClass + "2",
            this.litClass + "3",
            this.litClass + "4",
            this.litClass + "5",
            this.litClass];

    // visible DOM element
    this.element = document.createElement("div");
    this.element.id = id;
    this.element.className = offClass;
    if (x !== null) {
        this.element.style.left = x.toString() + "px";
    }
    if (y !== null) {
        this.element.style.top = y.toString() + "px";
    }

    if (parent) {
        parent.appendChild(this.element);
    }
}

/**************************************/

ColoredLamp.lampLevels = 6;
ColoredLamp.topCaptionClass = "coloredLampTopCaption";
ColoredLamp.bottomCaptionClass = "coloredLampBottomCaption";

/**************************************/
ColoredLamp.prototype.addEventListener = function addEventListener(eventName, handler, useCapture) {
    /* Sets an event handler whenever the image element is clicked */

    this.element.addEventListener(eventName, handler, useCapture);
};

/**************************************/
ColoredLamp.prototype.set = function set(state) {
    /* Changes the visible state of the lamp according to the value of "state", 0-1 */
    var newState = Math.max(Math.min(Math.round(state*ColoredLamp.lampLevels + 0.4999), ColoredLamp.lampLevels), 0);

    if (this.state != newState) {       // the state has changed
        this.state = newState;
        this.element.className = this.levelClass[newState];
    }
};

/**************************************/
ColoredLamp.prototype.flip = function flip() {
    /* Complements the visible state of the lamp */

    this.set(ColoredLamp.lampLevels - this.state);
};

/**************************************/
ColoredLamp.prototype.setCaption = function setCaption(caption, atBottom) {
    /* Establishes an optional caption at the top or bottom of a single lamp.
    Returns the caption element */
    var e = (atBottom ? this.bottomCaptionDiv : this.topCaptionDiv);

    if (e) {
        e.textContent = caption;
    } else {
        e = document.createElement("div");
        if (atBottom) {
            this.bottomCaptionDiv = e;
            e.className = ColoredLamp.bottomCaptionClass;
        } else {
            this.topCaptionDiv = e;
            e.className = ColoredLamp.topCaptionClass;
        }
        e.appendChild(document.createTextNode(caption));
        this.element.appendChild(e);
    }
    return e;
};


/***********************************************************************
*  Panel Toggle Switch                                                 *
***********************************************************************/
function ToggleSwitch(parent, x, y, id, offImage, onImage) {
    /* Constructor for the toggle switch objects used within panels. x & y are
    the coordinates of the switch within its containing element; id is the DOM id */

    this.state = 0;                     // current switch state, 0=off
    this.topCaptionDiv = null;          // optional top caption element
    this.bottomCaptionDiv = null;       // optional bottom caption element
    this.offImage = offImage;           // image used for the off state
    this.onImage = onImage;             // image used for the on state

    // visible DOM element
    this.element = document.createElement("img");
    this.element.id = id;
    this.element.src = offImage;
    if (x !== null) {
        this.element.style.left = x.toString() + "px";
    }
    if (y !== null) {
        this.element.style.top = y.toString() + "px";
    }

    if (parent) {
        parent.appendChild(this.element);
    }
}

/**************************************/

ToggleSwitch.topCaptionClass = "toggleSwitchTopCaption";
ToggleSwitch.bottomCaptionClass = "toggleSwitchBottomCaption";

/**************************************/
ToggleSwitch.prototype.addEventListener = function addEventListener(eventName, handler, useCapture) {
    /* Sets an event handler whenever the image element is clicked */

    this.element.addEventListener(eventName, handler, useCapture);
};

/**************************************/
ToggleSwitch.prototype.set = function set(state) {
    /* Changes the visible state of the switch according to the low-order
    bit of "state" */
    var newState = state & 1;

    if (this.state ^ newState) {         // the state has changed
        this.state = newState;
        this.element.src = (newState ? this.onImage : this.offImage);
    }
};

/**************************************/
ToggleSwitch.prototype.flip = function flip() {
    /* Complements the visible state of the switch */
    var newState = this.state ^ 1;

    this.state = newState;
    this.element.src = (newState ? this.onImage : this.offImage);
};

/**************************************/
ToggleSwitch.prototype.setCaption = function setCaption(caption, atBottom) {
    /* Establishes an optional caption at the top or bottom of a single switch.
    Returns the caption element */
    var e = (atBottom ? this.bottomCaptionDiv : this.topCaptionDiv);

    if (e) {
        e.textContent = caption;
    } else {
        e = document.createElement("div");
        if (atBottom) {
            this.bottomCaptionDiv = e;
            e.className = ToggleSwitch.bottomCaptionClass;
        } else {
            this.topCaptionDiv = e;
            e.className = ToggleSwitch.topCaptionClass;
        }
        e.appendChild(document.createTextNode(caption));
        this.element.appendChild(e);
    }
    return e;
};


/***********************************************************************
*  Panel Three-way Toggle Switch                                       *
***********************************************************************/
function ThreeWaySwitch(parent, x, y, id, offImage, onImage1, onImage2) {
    /* Constructor for the three-way toggle switch objects used within panels.
    x & y are the coordinates of the switch within its containing element;
    id is the DOM id */

    this.state = 0;                     // current switch state, 0=off, 1=down, 2=up)
    this.topCaptionDiv = null;          // optional top caption element
    this.bottomCaptionDiv = null;       // optional bottom caption element
    this.offImage = offImage;           // image used for the off state
    this.onImage1 = onImage1;           // image used for the lower on state
    this.onImage2 = onImage2;           // image used for the upper on state

    // visible DOM element
    this.element = document.createElement("img");
    this.element.id = id;
    this.element.src = offImage;
    if (x !== null) {
        this.element.style.left = x.toString() + "px";
    }
    if (y !== null) {
        this.element.style.top = y.toString() + "px";
    }

    if (parent) {
        parent.appendChild(this.element);
    }
}

/**************************************/

ThreeWaySwitch.topCaptionClass = "ToggleSwitchTopCaption";
ThreeWaySwitch.bottomCaptionClass = "ToggleSwitchBottomCaption";

/**************************************/
ThreeWaySwitch.prototype.addEventListener = function addEventListener(eventName, handler, useCapture) {
    /* Sets an event handler whenever the image element is clicked */

    this.element.addEventListener(eventName, handler, useCapture);
};

/**************************************/
ThreeWaySwitch.prototype.set = function set(state) {
    /* Changes the visible state of the switch according to the value
    of "state" */

    if (this.state != state) {          // the state has changed
        switch (state) {
        case 1:                         // down position
            this.state = 1;
            this.element.src = this.onImage1;
            break;
        case 2:                         // up position
            this.state = 2;
            this.element.src = this.onImage2;
            break;
        default:                        // middle position
            this.state = 0;
            this.element.src = this.offImage;
            break;
        } // switch state
    }
};

/**************************************/
ThreeWaySwitch.prototype.flip = function flip() {
    /* Increments the visible state of the switch */

    this.set(this.state+1);
};

/**************************************/
ThreeWaySwitch.prototype.setCaption = function setCaption(caption, atBottom) {
    /* Establishes an optional caption at the top or bottom of a single switch.
    Returns the caption element */
    var e = (atBottom ? this.bottomCaptionDiv : this.topCaptionDiv);

    if (e) {
        e.textContent = caption;
    } else {
        e = document.createElement("div");
        if (atBottom) {
            this.bottomCaptionDiv = e;
            e.className = ThreeWaySwitch.bottomCaptionClass;
        } else {
            this.topCaptionDiv = e;
            e.className = ThreeWaySwitch.topCaptionClass;
        }
        e.appendChild(document.createTextNode(caption));
        this.element.appendChild(e);
    }
    return e;
};


/***********************************************************************
*  Panel Organ Switch                                                  *
***********************************************************************/
function OrganSwitch(parent, x, y, id, offImage, onImage, momentary) {
    /* Constructor for the organ switch objects used within panels. x & y are
    the coordinates of the switch within its containing element; id is the DOM id */

    this.state = 0;                     // current switch state, 0=off
    this.topCaptionDiv = null;          // optional top caption element
    this.bottomCaptionDiv = null;       // optional bottom caption element
    this.offImage = offImage;           // image used for the off state
    this.onImage = onImage;             // image used for the on state
    this.momentary = momentary || false;// true if the switch is only momentary-on

    // visible DOM element
    this.element = document.createElement("img");
    this.element.id = id;
    this.element.src = offImage;
    if (x !== null) {
        this.element.style.left = x.toString() + "px";
    }
    if (y !== null) {
        this.element.style.top = y.toString() + "px";
    }

    if (parent) {
        parent.appendChild(this.element);
    }
}

/**************************************/

OrganSwitch.topCaptionClass = "OrganSwitchTopCaption";
OrganSwitch.bottomCaptionClass = "OrganSwitchBottomCaption";
OrganSwitch.momentaryPeriod = 200;      // time for momentary switch to bounce back, ms

/**************************************/
OrganSwitch.prototype.addEventListener = function addEventListener(eventName, handler, useCapture) {
    /* Sets an event handler whenever the image element is clicked */

    this.element.addEventListener(eventName, handler, useCapture);
};

/**************************************/
OrganSwitch.prototype.set = function set(state) {
    /* Changes the visible state of the switch according to the low-order
    bit of "state" */
    var newState = state & 1;

    if (this.state ^ newState) {         // the state has changed
        this.state = newState;
        this.element.src = (newState ? this.onImage : this.offImage);
        if (this.momentary && newState) {
            setCallback(null, this, OrganSwitch.momentaryPeriod, this.set, 0);
        }
    }
};

/**************************************/
OrganSwitch.prototype.flip = function flip() {
    /* Complements the visible state of the switch */

    this.set(this.state ^ 1);
};

/**************************************/
OrganSwitch.prototype.setCaption = function setCaption(caption, atBottom) {
    /* Establishes an optional caption at the top or bottom of a single switch.
    Returns the caption element */
    var e = (atBottom ? this.bottomCaptionDiv : this.topCaptionDiv);

    if (e) {
        e.textContent = caption;
    } else {
        e = document.createElement("div");
        if (atBottom) {
            this.bottomCaptionDiv = e;
            e.className = OrganSwitch.bottomCaptionClass;
        } else {
            this.topCaptionDiv = e;
            e.className = OrganSwitch.topCaptionClass;
        }
        e.appendChild(document.createTextNode(caption));
        this.element.appendChild(e);
    }
    return e;
};


/***********************************************************************
*  Black Control Knob                                                  *
***********************************************************************/
function BlackControlKnob(parent, x, y, id, initial, positions) {
    /* Constructor for the black control knob objects used within panels. x & y are
    the coordinates of the knob within its containing element; id is the DOM id;
    initial is the 0-relative index indicating the default position of the switch;
    positions is an array indicating the angular position (in degrees, where 0
    is straight up) of each of the knob's positions */

    this.position = 0;                  // current knob position
    this.direction = 1;                 // rotate knob clockwise(1), counter-clockwise(-1)
    this.topCaptionDiv = null;          // optional top caption element
    this.bottomCaptionDiv = null;       // optional bottom caption element
    this.positions = positions;         // array of knob position angles

    // visible DOM element
    this.element = document.createElement("canvas");
    this.element.id = id;
    this.element.width = BlackControlKnob.size;
    this.element.height = BlackControlKnob.size;
    this.element.className = BlackControlKnob.className;
    if (x !== null) {
        this.element.style.left = x.toString() + "px";
    }
    if (y !== null) {
        this.element.style.top = y.toString() + "px";
    }

    if (parent) {
        parent.appendChild(this.element);
    }

    this.set(initial);                  // set to its initial position
}

/**************************************/

BlackControlKnob.topCaptionClass = "blackControlKnobTopCaption";
BlackControlKnob.bottomCaptionClass = "blackControlKnobBottomCaption";
BlackControlKnob.className = "blackControlKnob1";
BlackControlKnob.canvasColor = "transparent";
BlackControlKnob.size = 64;             // width/height in pixels

/**************************************/
BlackControlKnob.prototype.addEventListener = function addEventListener(eventName, handler, useCapture) {
    /* Sets an event handler whenever the canvas element is clicked */

    this.element.addEventListener(eventName, handler, useCapture);
};

/**************************************/
BlackControlKnob.prototype.set = function set(position) {
    /* Changes the visible state of the knob according to the position index */
    var dc = this.element.getContext("2d");
    var degrees = Math.PI/180;
    var fullCircle = 360*degrees;
    var halfSize = Math.floor(BlackControlKnob.size/2);
    var quarterSize = Math.floor(BlackControlKnob.size/4);
    var silverSkirt;

    if (position < 0) {
        this.position = 0;
        this.direction = 1;
    } else if (position < this.positions.length) {
        this.position = position;
    } else {
        this.position = this.positions.length-1;
        this.direction = -1;
    }

    dc.save();
    dc.translate(halfSize+0.5, halfSize+0.5);   // move origin to the center

    dc.fillStyle = BlackControlKnob.canvasColor;// fill in the panel background (aids antialiasing)
    dc.fillRect(-halfSize, -halfSize, BlackControlKnob.size, BlackControlKnob.size);

    silverSkirt = dc.createRadialGradient(0, 0, halfSize, 0, 0, quarterSize);
    silverSkirt.addColorStop(0.5, "#FFF");
    silverSkirt.addColorStop(1, "#CCC");

    dc.beginPath();                             // draw the outer skirt of the knob
    dc.arc(0, 0, halfSize-1, 0, fullCircle, false);
    dc.fillStyle = silverSkirt;
    dc.fill();

    dc.beginPath();                             // draw the central knob
    dc.arc(0, 0, quarterSize, 0, fullCircle, false);
    dc.fillStyle = "#000";
    dc.fill();

    dc.beginPath();                             // draw the inset on top of the knob
    dc.arc(0, 0, quarterSize-4, 0, fullCircle, false);
    dc.fillStyle = "#333";
    dc.fill();

    dc.save();                                  // draw the knob indicator
    dc.rotate(this.positions[this.position]*degrees);
    dc.beginPath();
    dc.moveTo(0, 1-halfSize);
    dc.lineTo(-quarterSize/4, -halfSize+quarterSize/2);
    dc.lineTo(quarterSize/4, -halfSize+quarterSize/2);
    dc.closePath();
    dc.fillStyle = "#000";
    dc.fill();
    dc.restore();                               // undo the rotation
    dc.restore();                               // undo the translation
};

/**************************************/
BlackControlKnob.prototype.step = function step() {
    /* Steps the knob to its next position. If it is at the last position, steps it
    to the first position */
    var position = this.position+this.direction;

    if (position < 0) {
        this.direction = 1;
        this.set(1);
    } else if (position < this.positions.length) {
        this.set(position);
    } else {
        this.direction = -1;
        this.set(this.positions.length-2);
    }
};

/**************************************/
BlackControlKnob.prototype.setCaption = function setCaption(caption, atBottom) {
    /* Establishes an optional caption at the top or bottom of a single switch.
    Returns the caption element */
    var e = (atBottom ? this.bottomCaptionDiv : this.topCaptionDiv);

    if (e) {
        e.textContent = caption;
    } else {
        e = document.createElement("div");
        if (atBottom) {
            this.bottomCaptionDiv = e;
            e.className = BlackControlKnob.bottomCaptionClass;
        } else {
            this.topCaptionDiv = e;
            e.className = BlackControlKnob.topCaptionClass;
        }
        e.appendChild(document.createTextNode(caption));
        this.element.appendChild(e);
    }
    return e;
};


/***********************************************************************
*  Panel Register                                                      *
***********************************************************************/
function PanelRegister(parent, bits, rows, idPrefix, caption) {
    /* Constructor for the register objects used within panels:
        parent:the DOM element (usually a <div>) within which the register will be built
        bits:   number of bits in register
        rows:   number of rows used to display the bit lamps
        caption:optional caption displayed at the bottom of the register
    */
    var cols = Math.floor((bits+rows-1)/rows);
    var b;
    var cx;
    var cy;
    var e;

    this.element = parent;              // containing element for the panel
    this.bits = bits;                   // number of bits in the register
    this.caption = caption || "";       // panel caption
    this.lastValue = 0;                 // prior register value
    this.lamps = new Array(bits);       // bit lamps

    cx = cols*PanelRegister.hSpacing + PanelRegister.hOffset;
    for (b=0; b<bits; b++) {
        if (b%rows == 0) {
            cy = (rows-1)*PanelRegister.vSpacing + PanelRegister.vOffset;
            cx -= PanelRegister.hSpacing;
        } else {
            cy -= PanelRegister.vSpacing;
        }

        this.lamps[b] = new NeonLampBox(parent, cx, cy, idPrefix + b.toString());
    }

    this.captionDiv = document.createElement("div");
    this.captionDiv.className = PanelRegister.captionClass;
    this.captionDiv.style.top = "0";
    this.captionDiv.style.left = "0";
    if (caption) {
        e = document.createElement("span");
        e.className = PanelRegister.captionSpanClass;
        e.appendChild(document.createTextNode(caption.substring(0, 1)));
        this.captionDiv.appendChild(e);
    }
    parent.appendChild(this.captionDiv);

    this.leftClearBar = document.createElement("div");
    this.leftClearBar.id = idPrefix + "LeftClear";
    this.leftClearBar.className = PanelRegister.regClearBarClass;
    this.leftClearBar.style.left = "0";
    parent.appendChild(this.leftClearBar);

    this.rightClearBar = document.createElement("div");
    this.rightClearBar.id = idPrefix + "RightClear";
    this.rightClearBar.className = PanelRegister.regClearBarClass;
    this.rightClearBar.style.right = "0";
    parent.appendChild(this.rightClearBar);

    e = document.createElement("div");
    e.className = PanelRegister.regSpacerClass;
    e.style.top = "0";
    e.style.right = "0";
    parent.appendChild(e);

    e = document.createElement("div");
    e.className = PanelRegister.regSpacerClass;
    e.style.bottom = "0";
    e.style.left = "0";
    parent.appendChild(e);

    e = document.createElement("div");
    e.className = PanelRegister.regSpacerClass;
    e.style.bottom = "0";
    e.style.right = "0";
    parent.appendChild(e);
}

/**************************************/

PanelRegister.hSpacing = 24;            // horizontal lamp spacing, pixels
PanelRegister.hOffset = 24;             // horizontal lamp offset within container
PanelRegister.vSpacing = 32;            // vertical lamp spacing, pixels
PanelRegister.vOffset = 0;              // vertical lamp offset within container
PanelRegister.lampDiameter = 20;        // lamp outer diameter, pixels
PanelRegister.panelClass = "panelRegister";
PanelRegister.captionClass = "panelRegCaption";
PanelRegister.regSpacerClass = "panelRegSpacer";
PanelRegister.regClearBarClass = "panelRegClearBar";
PanelRegister.captionSpanClass = "panelRegSpan";
PanelRegister.boxCaptionClass = "boxCaption";

/**************************************/
PanelRegister.prototype.addEventListener = function addEventListener(eventName, handler, useCapture) {
    /* Sets an event handler whenever the parent element is clicked */

    this.element.addEventListener(eventName, handler, useCapture);
};

/**************************************/
PanelRegister.prototype.xCoord = function xCoord(col) {
    /* Returns the horizontal lamp coordinate in pixels */

    return ((col-1)*PanelRegister.hSpacing + PanelRegister.hOffset);
};

/**************************************/
PanelRegister.prototype.yCoord = function yCoord(row) {
    /* Returns the vertical lamp coordinate in pixels */

    return ((row-1)*PanelRegister.vSpacing + PanelRegister.vOffset);
};

/**************************************/
PanelRegister.prototype.panelWidth = function panelWidth(cols) {
    /* Returns the width of a register panel in pixels */

    return (cols-1)*PanelRegister.hSpacing + PanelRegister.hOffset*2 + PanelRegister.lampDiameter;
};

/**************************************/
PanelRegister.prototype.panelHeight = function panelHeight(rows) {
    /* Returns the height of a register panel in pixels */

    return (rows-1)*PanelRegister.vSpacing + PanelRegister.vOffset*2 + PanelRegister.lampDiameter;
};

/**************************************/
PanelRegister.prototype.drawBar = function drawBar(col, rows, styles) {
    /* Creates a bar along the left border of a group of lamps in a register.
    styles is a JS object with name/value pairs to set additional styles for 
    the bar's <div> element */
    var bar = document.createElement("div");
    var e = "";

    bar.style.position = "absolute";
    bar.style.left = (this.xCoord(col) - (PanelRegister.hSpacing-PanelRegister.lampDiameter)/2 + 1).toString() + "px";
    bar.style.width = "2px";
    bar.style.top = this.yCoord(1).toString() + "px";
    bar.style.height = (this.yCoord(rows+1) - this.yCoord(1)).toString() + "px";
    if (styles) {
        for (e in styles) {
            bar.style[e] = styles[e];
        }
    }
    
    this.element.appendChild(bar);
    return bar;
};

/**************************************/
PanelRegister.prototype.drawBox = function drawBox(col, lamps, rows, leftStyle, rightStyle) {
    /* Creates a box centered around a specified group of lamps in a register.
    leftStyle and rightStyle specify the left and right borders of the box using
    standard CSS border syntax. Returns the box element */
    var box = document.createElement("div");

    box.style.position = "absolute";
    box.style.left = (this.xCoord(col) - (PanelRegister.hSpacing-PanelRegister.lampDiameter)/2 + 1).toString() + "px";
    box.style.width = (PanelRegister.hSpacing*lamps - 2).toString() + "px";
    box.style.top = this.yCoord(1).toString() + "px";
    box.style.height = (this.yCoord(rows+1) - this.yCoord(1)).toString() + "px";
    box.style.borderLeft = leftStyle;
    box.style.borderRight = rightStyle;
    box.appendChild(document.createTextNode("\xA0"));
    this.element.appendChild(box);
    return box;
};

/**************************************/
PanelRegister.prototype.setBoxCaption = function setBoxCaption(box, caption) {
    /* Establishes an optional caption for register lamp box.
    Returns the caption element */
    var e = box.captionDiv;

    if (e) {
        e.textContent = caption;
    } else {
        box.captionDiv = e = document.createElement("div");
        e.className = PanelRegister.boxCaptionClass;
        e.appendChild(document.createTextNode(caption));
        box.appendChild(e);
    }
    return e;
};

/**************************************/
PanelRegister.prototype.update = function update(value) {
    /* Update the register lamps from the value of the parameter. This routine
    compares the value of the register that was previously updated against the new
    one in an attempt to minimize the number of lamp flips that need to be done */
    var bitBase = 0;
    var bitNr;
    var lastMask;
    var lastValue = this.lastValue;
    var thisMask;
    var thisValue = Math.floor(Math.abs(value)) % 0x100000000000;

    if (thisValue != lastValue) {
        this.lastValue = thisValue;     // save it for next time
        do {
            // Loop through the masks 30 bits at a time so we can use Javascript bit ops
            bitNr = bitBase;
            lastMask = lastValue % 0x40000000;              // get the low-order 30 bits
            lastValue = (lastValue-lastMask)/0x40000000;    // shift the value right 30 bits
            thisMask = thisValue % 0x40000000;              // ditto for the second value
            thisValue = (thisValue-thisMask)/0x40000000;

            lastMask ^= thisMask;       // determine which bits have changed
            while (lastMask) {
                if (lastMask & 0x01) {
                    this.lamps[bitNr].set(thisMask & 0x01);
                }
                if (++bitNr <= this.bits) {
                    lastMask >>>= 1;
                    thisMask >>>= 1;
                } else {
                    thisValue = thisMask = 0;
                    lastValue = lastMask = 0;
                    break;              // out of inner while loop
                }
            }

            bitBase += 30;
        } while (thisValue || lastValue);
    }
};

/**************************************/
PanelRegister.prototype.updateLampGlow = function updateLampGlow(glow) {
    /* Update the register lamps from the bitwise intensity values in "glow" */
    var bitNr;

    for (bitNr=this.bits-1; bitNr>=0; --bitNr) {
        this.lamps[bitNr].set(glow[bitNr]);
    }
};
