module.exports = function (RED) {
    "use strict";
    var bonescript, adjustName, setPinMode;
    var analogInputPins = ["P9_39", "P9_40", "P9_37", "P9_38", "P9_33", "P9_36", "P9_35"];
    var gpioPins = ["P8_7", "P8_8", "P8_9", "P8_10", "P8_11", "P8_12", "P8_13", "P8_14", "P8_15",
        "P8_16", "P8_17", "P8_18", "P8_19", "P8_26", "P9_11", "P9_12", "P9_13", "P9_14",
        "P9_15", "P9_16", "P9_17", "P9_18", "P9_21", "P9_22", "P9_23", "P9_24", "P9_26",
        "P9_27", "P9_30", "P9_41", "P9_42"];
    var usrLEDs = ["USR0", "USR1", "USR2", "USR3"];
    // Load the hardware library and set up polymorphic functions to suit it. Prefer
    // octalbonescript (faster & less buggy) but drop back to bonescript if not available
    bonescript = require("octalbonescript");
    adjustName = function (pin) {
        if (pin === "P8_7") {
            pin = "P8_07";
        }
        else if (pin === "P8_8") {
            pin = "P8_08";
        }
        else if (pin === "P8_9") {
            pin = "P8_09";
        }
        return pin;
    };
    setPinMode = function (pin, mode, callback) {
        bonescript.pinMode(pin, mode, callback);
    }

    // Node constructor for bbb-analogue-in
    function AnalogueInputNode(n) {
        RED.nodes.createNode(this, n);
        var node = this;

        // Store local copies of the node configuration (as defined in the .html)
        this.topic = n.topic;
        this.pin = n.pin;                               // The Beaglebone Black pin identifying string
        this._pin = adjustName(this.pin);               // Adjusted for Octal if necessary
        this.breakpoints = n.breakpoints;
        this.averaging = n.averaging;
        if (this.averaging) {
            this.averages = 10;
        }
        else {
            this.averages = 1;
        }

        // Variables used for input averaging
        var sum;    // accumulates the input readings to be averaged
        var count;  // keep track of the number of measurements made

        // The callback function for analogRead. Accumulates the required number of
        // measurements, then divides the total number, applies output scaling and
        // sends the result
        var analogReadCallback = function (err, x) {
            sum = sum + Number(x);
            count = count - 1;
            if (count > 0) {
                bonescript.analogRead(node._pin, analogReadCallback);
            }
            else {
                var msg = {};
                msg.topic = node.topic;
                sum = sum/node.averages;
                // i is the index of the first breakpoint where the 'input' value is strictly
                // greater than the measurement (note: a measurement can never be == 1)
                var i = node.breakpoints.map(function (breakpoint) {
                    return sum >= breakpoint.input;
                }).indexOf(false);
                msg.payload = node.breakpoints[i - 1].output + (node.breakpoints[i].output - node.breakpoints[i - 1].output)*
                (sum - node.breakpoints[i - 1].input)/(node.breakpoints[i].input - node.breakpoints[i - 1].input);
                node.send(msg);
            }
        };

        // If we have a valid pin, set the input event handler to Bonescript's analogRead
        if (analogInputPins.indexOf(node.pin) >= 0) {
            node.on("input", function () {
                sum = 0;
                count = node.averages;
                bonescript.analogRead(node._pin, analogReadCallback);
            });
        }
        else {
            node.error("Unconfigured input pin");
        }
    }

    // Node constructor for bbb-discrete-in
    function DiscreteInputNode(n) {
        RED.nodes.createNode(this, n);
        var node = this;

        // Store local copies of the node configuration (as defined in the .html)
        this.topic = n.topic;                           // the topic is not currently used
        this.pin = n.pin;                               // The Beaglebone Black pin identifying string
        this._pin = adjustName(this.pin);               // Adjusted for Octal if necessary
        if (n.activeLow) {                              // Set the 'active' state 0 or 1 as appropriate
            this.activeState = 0;
        }
        else {
            this.activeState = 1;
        }
        this.updateInterval = n.updateInterval*1000;    // How often to send totalActiveTime messages
        this.debounce = n.debounce || null;             // Enable switch contact debouncing algorithm
        if (n.outputOn === "rising") {
            this.activeEdges = [false, true];
        }
        else if (n.outputOn === "falling") {
            this.activeEdges = [true, false];
        }
        else if (n.outputOn === "both") {
            this.activeEdges = [true, true];
        }
        else {
            node.error("Invalid edge type: " + n.outputOn);
        }

        // Working variables
        this.interruptAttached = false; // Flag: should we detach interrupt when we are closed?
        this.intervalId = null;         // Remember the timer ID so we can delete it when we are closed
        this.currentState = 0;          // The pin input state "1" or "0"
        this.lastActiveTime = NaN;      // The date (in ms since epoch) when the pin last went high
        // switch to process.hrtime()
        this.totalActiveTime = 0;       // The total time in ms that the pin has been high (since reset)
        this.starting = true;
        this.debouncing = false;        // True after a change of state while waiting for the 7ms debounce time to elapse
        this.debounceTimer = null;

        // This function is called by the input pin change-of-state interrupt. If
        // debounce is disabled, send the output message. Otherwise, if we are
        // currently debouncing, ignore this interrupt. If we are not debouncing,
        // schedule a re-read of the input pin in 7ms time, and set the debouncing flag
        // Note: if x has an 'attached' field and no 'value' field, the callback is reporting
        // the success or failure of attaching the interrupt - we must handle this
        var interruptCallback = function (err, x) {
            if (x === undefined) {
                if (x.attached === true) {
                    node.interruptAttached = true;
                    node.on("input", inputCallback);
                    node.intervalId = setInterval(timerCallback, node.updateInterval);
                }
                else {
                    node.error("Failed to attach interrupt");
                }
            }
            else if (node.currentState !== Number(x)) {
                if (node.debounce) {
                    if (node.debouncing === false) {
                        node.debouncing = true;
                        node.debounceTimer = setTimeout(function () {
                            bonescript.digitalRead(node._pin, debounceCallback);
                        }, Number(node.debounce));
                    }
                }
                else {
                    sendStateMessage(x);
                }
            }
        };

        // This function is called approx 7ms after a potential change-of-state which is
        // being debounced. Terminate the debounce, and send a message if the state has
        // actually changed
        var debounceCallback = function (err, x) {
            node.debounceTimer = null;
            node.debouncing = false;
            if (x !== undefined && node.currentState !== Number(x)) {
                sendStateMessage(x);
            }
        };

        // This function is called when either the interruptCallback or the debounceCallback
        // have determined we have a 'genuine' change of state. Update the currentState and
        // ActiveTime variables, and send a message on the first output with the new state
        var sendStateMessage = function (x) {
            node.currentState = Number(x);
            var now = Date.now();
            if (node.currentState === node.activeState) {
                node.lastActiveTime = now;
            }
            else if (!isNaN(node.lastActiveTime)) {
                node.totalActiveTime += now - node.lastActiveTime;
            }
            if (node.activeEdges[node.currentState]) {
                var msg = {};
                msg.topic = node.topic;
                msg.payload = node.currentState;
                node.send([msg, null]);
            }
        };

        // This function is called by the timer. It updates the ActiveTime variables, and sends a
        // message on the second output with the latest value of the total active time, in seconds
        var timerCallback = function () {
            if (node.currentState === node.activeState) {
                var now = Date.now();
                node.totalActiveTime += now - node.lastActiveTime;
                node.lastActiveTime = now;
            }
            var msg = {};
            msg.topic = node.topic;
            msg.payload = node.totalActiveTime/1000;
            node.send([null, msg]);
            // Re-synchronise the pin state if we have missed a state change interrupt for some
            // reason, and we are not in the process of debouncing one
            if (node.debouncing === false) {
                bonescript.digitalRead(node._pin, interruptCallback);
            }
        };

        // This function is called when we receive an input message. If the topic contains
        // 'load' (case insensitive) set the totalActiveTime to the numeric value of the
        // payload, if possible. Otherwise clear the totalActiveTime (so we start counting
        // from zero again)
        var inputCallback = function (ipMsg) {
            if (String(ipMsg.topic).search(/load/i) < 0 || isFinite(ipMsg.payload) === false) {
                node.totalActiveTime = 0;
            }
            else {
                node.totalActiveTime = Number(ipMsg.payload);
            }
            if (node.currentState === node.activeState) {
                node.lastActiveTime = Date.now();
            }
            // On startup, send an initial activeTime message, but only send an
            // initial currentState message if we are in both edges active mode
            if (node.starting) {
                node.starting = false;
                var msg;
                if (node.activeEdges[0] && node.activeEdges[1]) {
                    msg = [{topic: node.topic}, {topic: node.topic}];
                    msg[0].payload = node.currentState;
                }
                else {
                    msg = [null, {topic: node.topic}];
                }
                msg[1].payload = node.totalActiveTime;
                node.send(msg);
            }
        };

        // If we have a valid pin, set it as an input and read the (digital) state
        if (gpioPins.indexOf(node.pin) >= 0) {
            // Don't set up interrupts & intervals until after the close event handler has been installed
            bonescript.detachInterrupt(node._pin);
            process.nextTick(function () {
                setPinMode(node._pin, bonescript.INPUT, function (response, pin) {
                    if (!response) {
                        bonescript.digitalRead(node._pin, function (err, x) {
                            // Initialise the currentState and lastActiveTime variables based on the value read
                            node.currentState = Number(x);
                            if (node.currentState === node.activeState) {
                                node.lastActiveTime = Date.now();
                            }
                            // Attempt to attach a change-of-state interrupt handler to the pin. If we succeed,
                            // the input event and interval handlers will be installed by interruptCallback
                            bonescript.attachInterrupt(node._pin, bonescript.CHANGE, interruptCallback);
                            // Send an initial message with the pin state on the first output
                            setTimeout(function () {
                                node.emit("input", {});
                            }, 50);
                        });
                    }
                    else {
                        node.error("Unable to set " + pin + " as input: " + response);
                    }
                });
            });
        }
        else {
            node.error("Unconfigured input pin");
        }
    }

    // Node constructor for bbb-pulse-in
    function PulseInputNode(n) {
        RED.nodes.createNode(this, n);
        var node = this;

        // Store local copies of the node configuration (as defined in the .html)
        this.topic = n.topic;                           // the topic is not currently used
        this.pin = n.pin;                               // The Beaglebone Black pin identifying string
        this._pin = adjustName(this.pin);               // Adjusted for Octal if necessary
        this.updateInterval = n.updateInterval*1000;    // How often to send output messages
        this.countType = n.countType;                   // Sets either 'edge' or 'pulse' counting
        this.countUnit = n.countUnit;                   // Scaling applied to count output
        this.countRate = n.countRate;                   // Scaling applied to rate output

        // Working variables
        this.interruptAttached = false;                 // Flag: should we detach interrupt when we are closed?
        this.intervalId = null;                         // Remember the timer ID so we can delete it when we are closed
        this.pulseCount = 0;                            // (Unscaled) total pulse count
        // Hold the hrtime of the last two pulses (with ns resolution)
        this.pulseTime = [[NaN, NaN], [NaN, NaN]];

        // Called by the edge or pulse interrupt. Record the pulse time and count the pulse
        // Note: if x has an 'attached' field and no 'value' field, the callback is reporting
        // the success or failure of attaching the interrupt - we must handle this
        var interruptCallback = function (x) {
            if (x === undefined) {
                if (x.attached === true) {
                    node.interruptAttached = true;
                    node.on("input", inputCallback);
                    node.intervalId = setInterval(timerCallback, node.updateInterval);
                }
                else {
                    node.error("Failed to attach interrupt");
                }
            }
            else {
                node.pulseTime = [node.pulseTime[1], process.hrtime()];
                node.pulseCount = node.pulseCount + 1;
            }
        };

        // Called when an input message arrives. If the topic contains 'load' (case
        // insensitive) and the payload is a valid number, set the count to that
        // number, otherwise set it to zero
        var inputCallback = function (msg) {
            if (String(msg.topic).search(/load/i) < 0 || isFinite(msg.payload) === false) {
                node.pulseCount = 0;
            }
            else {
                node.pulseCount = Number(msg.payload);
            }
        };

        // Called by the message timer. Send two messages: the scaled pulse count on
        // the first output and the scaled instantaneous pulse rate on the second.
        // The instantaneous pulse rate is the reciprocal of the larger of either the
        // time interval between the last two pulses, or the time interval since the last pulse.
        var timerCallback = function () {
            var now = process.hrtime();
            var lastTime = node.pulseTime[1][0] - node.pulseTime[0][0] + (node.pulseTime[1][1] - node.pulseTime[0][1])/1e9;
            var thisTime = now[0] - node.pulseTime[1][0] + (now[1] - node.pulseTime[1][1])/1e9;
            var msg = [{topic: node.topic}, {topic: node.topic}];
            msg[0].payload = node.countUnit*node.pulseCount;
            // At startup, pulseTime contains NaN's: force the rate output to 0
            msg[1].payload = node.countRate/Math.max(thisTime, lastTime) || 0;
            node.send(msg);
        };

        // If we have a valid pin, set it as an input and read the (digital) state
        if (gpioPins.indexOf(node.pin) >= 0) {
            // Don't set up interrupts & intervals until after the close event handler has been installed
            bonescript.detachInterrupt(node._pin);
            process.nextTick(function () {
                setPinMode(node._pin, bonescript.INPUT, function (response, pin) {
                    if (!response) {
                        bonescript.digitalRead(node._pin, function (err, x) {
                            // Initialise the currentState based on the value read
                            node.currentState = Number(x);
                            // Attempt to attach an interrupt handler to the pin. If we succeed,
                            // set the input event and interval handlers
                            var interruptType;
                            if (node.countType === "pulse") {
                                // interruptType = bonescript.FALLING; <- doesn't work in v0.2.4
                                interruptType = bonescript.RISING;
                            }
                            else {
                                interruptType = bonescript.CHANGE;
                            }
                            // Attempt to attach the required interrupt handler to the pin. If we succeed,
                            // the input event and interval handlers will be installed by interruptCallback
                            bonescript.attachInterrupt(node._pin, interruptType, interruptCallback)
                        });
                    }
                    else {
                        node.error("Unable to set " + pin + " as input: " + response);
                    }
                });
            });
        }
        else {
            node.error("Unconfigured input pin");
        }
    }

    // Node constructor for bbb-discrete-out
    function DiscreteOutputNode(n) {
        RED.nodes.createNode(this, n);
        var node = this;

        // Store local copies of the node configuration (as defined in the .html)
        this.topic = n.topic;                           // the topic is not currently used
        this.pin = n.pin;                               // The Beaglebone Black pin identifying string
        this._pin = adjustName(this.pin);               // Adjusted for Octal if necessary
        this.defaultState = Number(n.defaultState);     // What state to set up as
        this.inverting = n.inverting;
        this.toggle = n.toggle;

        // Working variables
        this.currentState = this.defaultState;

        // If the input message payload is numeric, values > 0.5 are 'true', otherwise use
        // the truthiness of the payload. Apply the inversion flag before setting the output
        var inputCallback = function (msg) {
            var newState;
            if (node.toggle) {
                newState = node.currentState === 0 ? 1 : 0;
            }
            else {
                if (isFinite(Number(msg.payload))) {
                    newState = Number(msg.payload) > 0.5;
                }
                else if (msg.payload) {
                    newState = true;
                }
                else {
                    newState = false;
                }
                if (node.inverting) {
                    newState = !newState;
                }
            }
            bonescript.digitalWrite(node._pin, newState ? 1 : 0, function() {
                node.send({topic: node.topic, payload: newState});
                node.currentState = newState;
            });
        };

        // If we have a valid pin, set it as an output and set the default state
        if (gpioPins.concat(usrLEDs).indexOf(node.pin) >= 0) {
            // Don't set up interrupts & intervals until after the close event handler has been installed
            if (node._pin) { bonescript.detachInterrupt(node._pin); }
            process.nextTick(function () {
                setPinMode(node._pin, bonescript.OUTPUT, function (response, pin) {
                    if (response) {
                        node.error("Unable to set " + pin + " as output: " + response.err);
                    }
                    else {
                        node.on("input", inputCallback);
                        setTimeout(function () {
                            bonescript.digitalWrite(node._pin, node.defaultState, function() {});
                        }, 50);
                    }
                });
            });
        }
        else {
            node.error("Unconfigured output pin");
        }
    }

    // Node constructor for bbb-pulse-out
    function PulseOutputNode(n) {
        RED.nodes.createNode(this, n);
        var node = this;

        // Store local copies of the node configuration (as defined in the .html)
        this.topic = n.topic;                           // the topic is not currently used
        this.pin = n.pin;                               // The Beaglebone Black pin identifying string
        this._pin = adjustName(this.pin);               // Adjusted for Octal if necessary
        this.pulseState = Number(n.pulseState);         // What state the pulse will be..
        this.defaultState = this.pulseState === 1 ? 0 : 1;
        this.retriggerable = n.retriggerable;
        this.pulseTime = n.pulseTime*1000;              // Pulse width in milliseconds

        // Working variables
        this.pulseTimer = null;                         // Non-null while a pulse is being generated

        // Generate a pulse in response to an input message. If the topic includes the text
        // 'time' (case insensitive) and the payload is numeric, use this value as the
        // pulse time. Otherwise use the value from the properties dialog.
        // If the resulting pulse time is < 1ms, do nothing.
        // If the pulse mode is not retriggerable, then if no pulseTimer is active, generate
        // a pulse. If the pulse mode is retriggerable, and a pulseTimer is active, cancel it.
        // If no timer is active, set the pulse output. In both cases schedule a new pulse
        // timer.
        var inputCallback = function (msg) {
            var time = node.pulseTime;
            if (String(msg.topic).search(/time/i) >= 0 && isFinite(msg.payload)) {
                time = msg.payload*1000;
            }
            if (time >= 1) {
                if (node.retriggerable === false) {
                    if (node.pulseTimer === null) {
                        node.pulseTimer = setTimeout(endPulseCallback, time);
                        bonescript.digitalWrite(node._pin, node.pulseState, function() {
                            node.send({topic: node.topic, payload: node.pulseState});
                        });
                    }
                }
                else {
                    if (node.pulseTimer !== null) {
                        clearTimeout(node.pulseTimer);
                    }
                    else {
                        bonescript.digitalWrite(node._pin, node.pulseState, function() {
                            node.send({topic: node.topic, payload: node.pulseState});
                        });
                    }
                    node.pulseTimer = setTimeout(endPulseCallback, time);
                }
            }
        };

        // At the end of the pulse, restore the default state and set the timer to null
        var endPulseCallback = function () {
            node.pulseTimer = null;
            bonescript.digitalWrite(node._pin, node.defaultState, function() {
                node.send({topic: node.topic, payload: node.defaultState});
            });
        };

        // If we have a valid pin, set it as an output and set the default state
        if (gpioPins.concat(usrLEDs).indexOf(node.pin) >= 0) {
            // Don't set up interrupts & intervals until after the close event handler has been installed
            bonescript.detachInterrupt(node._pin);
            process.nextTick(function () {
                setPinMode(node._pin, bonescript.OUTPUT, function (response, pin) {
                    if (!response) {
                        node.on("input", inputCallback);
                        // Set the pin to the default state once the dust settles
                        setTimeout(endPulseCallback, 50);
                    }
                    else {
                        node.error("Unable to set " + pin + " as output: " + response.err);
                    }
                });
            });
        }
        else {
            node.error("Unconfigured output pin");
        }
    }

    // Register the nodes by name. This must be called before overriding any of the Node functions.
    RED.nodes.registerType("bbb-analogue-in", AnalogueInputNode);
    RED.nodes.registerType("bbb-discrete-in", DiscreteInputNode);
    RED.nodes.registerType("bbb-pulse-in", PulseInputNode);
    RED.nodes.registerType("bbb-discrete-out", DiscreteOutputNode);
    RED.nodes.registerType("bbb-pulse-out", PulseOutputNode);

    // On close, detach the interrupt (if we attached one) and clear any active timers
    DiscreteInputNode.prototype.close = function () {
        if (this.interruptAttached) {
            bonescript.detachInterrupt(this._pin);
        }
        if (this.intervalId !== null) {
            clearInterval(this.intervalId);
        }
        if (this.debounceTimer !== null) {
            clearTimeout(this.debounceTimer);
        }
    };

    // On close, detach the interrupt (if we attached one) and clear the interval (if we set one)
    PulseInputNode.prototype.close = function () {
        if (this.interruptAttached) {
            bonescript.detachInterrupt(this._pin);
        }
        if (this.intervalId !== null) {
            clearInterval(this.intervalId);
        }
    };

    // On close, clear an active pulse timer
    PulseOutputNode.prototype.close = function () {
        if (this.pulseTimer !== null) {
            clearTimeout(this.pulseTimer);
        }
    };
};