/* Author: John Van Enk
 * Company: DornerWorks Ltd.
 * On Behalf Of: Espec North America, Inc.
 */

if (undefined === window.console) {
    window.console = { log : function () { } };
}

var __espec_const = {
    monitor_timeout : 10,
    graph_extents : {
        low : 0.0,
        high : 1.0
    }
};

var __espec_state = {
    monitor : undefined,
    graph_window : undefined,
    graph : undefined,
    last_popup : undefined
};

function getTextContent(obj) {
    if (obj.innerText) {
        return obj.innerText;
    } else if (obj.textContent) {
        return obj.textContent;
    } else {
        throw("Unable to use getTextContent with this browser.");
    }
}

/* Collection of functions to drive operations between the 
 * web page and the server device. */
function isDST() {
    var d = new Date();
    var dY = d.getFullYear();
    var d1 = new Date(dY, 0, 1, 0, 0, 0, 0);
    var d2 = new Date(dY, 6, 1, 0, 0, 0, 0);
    var d1a = new Date(d1.toUTCString().replace(" GMT", ""));
    var d2a = new Date(d2.toUTCString().replace(" GMT", ""));
    var o1 = (d1 - d1a) / 3600000;
    var o2 = (d2 - d2a) / 3600000;
    var rV = 0;
    if (o1 !== o2) {
        d.setHours(0);
        d.setMinutes(0);
        d.setSeconds(0);
        d.setMilliseconds(0);

        var da = new Date(d.toUTCString().replace(" GMT", ""));
        var o3 = (d - da) / 3600000;
        rV = (o3 === o1) ? 0 : 1;
    }
    return rV;
}

function formatComponentDate(year, month, day, hour, minute, second) {
    var d = new Date(year, month - 1,
                     day, hour,
                     minute, second);
    return formatDate(d);
}

function formatDate(d) {
    // I'm remided how much I dislke the Date object...
    var days = [
        "Sunday",   "Monday",
        "Tuesday",  "Wednesday",
        "Thursday", "Friday",
        "Saturday"
    ];

    var months = [
        "Jan", "Feb", "Mar",
        "Apr", "May", "Jun",
        "Jul", "Aug", "Sep",
        "Oct", "Nov", "Dec"
    ];

    var h = d.getHours();
    var m = d.getMinutes();

    return days[d.getDay()] + ", " +
           months[d.getMonth()] + " " +
           d.getDate() + " " +
           d.getFullYear() + " " +
           (h < 10 ? "0" : "") + h + ":" +
           (m < 10 ? "0" : "") + m;
}

function popWindow(elem,expire,onclose) {
    /* Places a window on the screen with child element elem. */
    var body = $$('body')[0];

    var closeButton = new Element('button', {'type': 'button'}).
        update("Close Window");

    var innerDiv = new Element('div').update(elem);
    innerDiv.style.marginBottom = "0.5em";

    var box = new Element('div').update(innerDiv);
    box.insert(closeButton);

    var viewport = document.viewport.getScrollOffsets();
    var view_left = viewport[0];
    var view_top = viewport[1];

    if (__espec_state.last_popup === undefined) {
        __espec_state.last_popup = { pleft : view_left + 15, ptop : view_top + 15 };
    } else {
        __espec_state.last_popup.pleft += 15;
        __espec_state.last_popup.ptop  += 15;
    }

    box.addClassName("especPopupBox");
    box.style.border = "5px solid #999999";
    box.style.color = "#000000";
    box.style.position = "absolute";
    box.style.left = (__espec_state.last_popup.pleft) + "px";
    box.style.top =  (__espec_state.last_popup.ptop)  + "px";
    box.style.background = "#CCCCCC";
    box.style.padding = "1em";

    var remove = function (evt) {
        if (undefined !== onclose) {
            onclose();
        }

        box.remove();
    }

    closeButton.observe('click',remove);

    if(undefined !== expire || expire >= 0) {
        var ms = parseInt(expire,10);
        if (! isNaN(ms)) {
            remove.delay(ms);
        }
    }

    body.insert(box);
}

function popAlarm(msg) {
    var html = "<div style=\'text-align:center\'><h3>Alarm</h3><div>" + msg + "</div></div>";
    popWindow(html);
}

/* Utility class to interact with the server */
var Espec = function () {

    /**
     * Function (private) _is_error
     *
     * Determines whether or not the returned
     * JSON is an error message.
     *
     * Parameters:
     *      r - The response json.
     */
    function _is_error(r) {
        if (undefined === r.error) {
            return false;
        } else {
            return true;
        }
    }

    /**
     * Function (private) _is_exception
     * 
     * Determines whether or not the returned
     * JSON is a Ruby Exception message.
     *
     * Parameters:
     *      r - The response json.
     */
    function _is_exception(r) {
        if (undefined === r.error || undefined === r.error.exception) {
            return false;
        } else {
            return true;
        }
    }

    /**
     * Function (private) _handle_errors
     *
     * Checks if a response is an error
     * and does something appropriate to
     * handle it.
     *
     * Parameters:
     *      r - The response json.
     */
    function _handle_errors(r) {
        if (false === _is_error(r)) {
            return false;
        }

        if (_is_exception(r)) {
            var errString = "Ruby Exception:\n" + r.error.exception;
            errString += "\n";

            $A(r.error.backtrace).each(function (v) {
                errString += "\t" + v + "\n";
            });

            alert(errString);
        }
        else {
            alert(r.error);
        }

        return true;
    }

    /**
     * Function (private) _version
     *
     * Returns a text version of our current
     * script version.
     */
    function _version() {
        return { version : ["0.0.1"] };
    }

    /**
     * Function (public) _load -> load
     *
     * Loads the parameters passed into it.
     *
     * Parameters
     *      parms - An array of key/arguments pairs.
     *              Parameters are represented as
     *              follows:
     *                  { key : [arg1, arg2, ...] }
     *      success - Function which the response
     *              JSON is passed to on success.
     *      error_handler - A function to replace
     *              the default error handler.
     */
    function _load(parms, success, error_handler) {
        if (undefined === error_handler) {
            error_handler = _handle_errors;
        }

        parms = $H(parms);
        var p_hash = new Hash();

        /* Add each parameter to the request hash. */
        parms.each(function (p) {
            var h = new Hash();
            h.set(p.key, p.value.toJSON());

            p_hash = p_hash.merge(h);
        });

        var a = new Ajax.Request("app/app.app", {
            method: 'get',
            parameters: p_hash,
            evalJSON: true,
            onSuccess: function (t) {
                if (_is_error(t.responseJSON)) {
                    error_handler(t.responseJSON);
                } else {
                    success(t.responseJSON);
                }
            },
            onFailure: error_handler
        });
    }

    /**
     * Function: (public) _alarms -> alarms
     *
     * Fetches a list of all the alarms.
     */
    function _alarms(as) {
        var news = _handle_new_alarms(as);

        if (0 < news.length) {
            var list = news.map(function (a) {
                return "<div>ID: " + a.id + " -- Message: " + a.text + "</div>";
            });

            popAlarm(list.join("\n"));
        }
    }

    function _save_alarm_cookies(alarms) {
        var date = new Date();

        // Hold the cookie for 30 minutes.
        date.setTime(date.getTime() + (30 * 60 * 1000));
        var expires = "; expires=" + date.toGMTString();
        var _expired_ = "; expires=Thu, 01-Jan-1970 00:00:01 GMT";
        var name = "alarms";

        var value = alarms.uniq().sort().join(",");
        var olds = _load_alarm_cookies().uniq().sort().join(",");

        if (value !== olds) {
            if (value === "") {
                document.cookie = name + "=" + _expired_ + "; path=/";
            } else {
                document.cookie = name + "=" + value + expires + "; path=/";
            }
        } else {
            // No change necessary
        }
    }

    function _load_alarm_cookies() {
        var c = document.cookie;
        var fields = c.split("; ");
        var alarms = fields.map(function (f) {
            var p = f.split("=");
            if (p[0] == "alarms") {
                return p[1].split(",").map(function (v) {
                    var i = parseInt(v, 10);
                    if (isNaN(i)) {
                        return undefined;
                    } else {
                        return i;
                    }
                }).compact();
            } else {
                return undefined;
            }
        }).compact();

        // There's only going to be one "alarms" variable, so we take
        // the first index (if it exists).
        if (alarms.length > 0) {
            return alarms[0];
        } else {
            return [];
        }
    }

    function _handle_new_alarms(current) {
        var news = new Array();
        var old = _load_alarm_cookies();
        var c_ids = current.map(function(c) { return c.id; });

        /* Remove alarms that aren't occuring any more */
        old = old.map(function (c) {
            if (-1 == c_ids.indexOf(c)) {
                return undefined;
            } else {
                return c;
            }
        }).compact();

        /* Add alarms that are new and need to cause alerts. */
        current.each(function (c) {
            if (-1 == old.indexOf(c.id)) {
                // This is a new one
                news.push(c);
                old.push(c.id);
            }
        });

        _save_alarm_cookies(old);

        return news;
    }

    return {
        alarms : _alarms,
        load : _load
    };
}();

/**
 * Object to facilitate changing data on
 * the page.
 */
var Mutator = function () {
    /**
     * Function: (public) _fill
     * 
     * Parameters:
     *      handlers - A name -> function mapping.
     *      loadables - An array of loadable nodes.
     *      data - An Object containing all the needed data.
     */
    function _fill(handlers, loadables, data) {
        var hs = $H(handlers);

        loadables.each(function (e) {
            var h = hs.get(e.id).handler;
            if (undefined === h) {
                alert("DEBUG: No handler defined for " + e.id);
            } else {
                e.update(h(data));
            }
        });
    }

    /* Expose public methods. */
    return {
        fill : _fill
    };
}();

/* Utility class to assemble and retrieve data. */
var App = function () {

    function _get_step_timer_cookie() {
        var fields = document.cookie.split("; ");
        var timer = fields.map(function (f) {
            var p = f.split('=');
            if (p[0] == "step_timer") {
                var vals = p[1].split(",");
                vals = [parseInt(vals[0], 10),vals[1]];
                return vals;
            } else {
                return undefined;
            }
        }).compact();

        if (timer.length > 0) {
            return timer[0];
        } else {
            return undefined;
        }
    }

    function _update_step_timer_cookie(tmr_val) {
        var date_ref = new Date();
        var date_set = new Date();

        var name = "step_timer";

        var old = _get_step_timer_cookie();

        // Only update if we don't have a cookie or
        // if the value doesn't match our own.
        if (undefined === old || old[1] !== tmr_val) {
            // Hold the cookie for 30 minutes
            date_set.setTime(date_ref.getTime() + (30 * 60 * 1000));
            var expires = "; expires=" + date_set.toGMTString();
            var val = date_ref.getTime() + "," + tmr_val;
            document.cookie = name + "=" + val + expires + "; path=/";
        }
    }

    /* Returns whether the program is paused and updates the cookie
     * for the future. */
    function _is_program_paused(tmr_val) {
        var date_ref = new Date();

        var old = _get_step_timer_cookie();

        var oneAndHalfMinutes = 90000; // milliseconds

        _update_step_timer_cookie(tmr_val);

        if (undefined === old) {
            return false;
        } else if (date_ref.getTime() - old[0] > oneAndHalfMinutes && tmr_val == old[1]) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * Variable: (private) _Handlers
     *
     * A hash of functions to be used to
     * set specific fields in the pages.
     *
     * TODO: Add dependency lists.
     */
    var _Handlers = {
        app_chamber_name : {
            depends : [{ 'chamber_name' : [] }],
            handler : function (r) {
                /* There's probably a better place for this. */
                document.title = r.chamber_name;

                return r.chamber_name;
            }
        },
        app_side_status : {
            depends : [{ 'mode' : [] },{ 'alarms' : [] },{ 'prgm_mon' : [] }],
            handler : function (r) {
                var p;

                if ("CONT_NOT_READY_2" === r.prgm_mon.chamber_error) {
                    p = "NONE";
                } else if( undefined !== r.prgm_mon.chamber_error) {
                    p = "**CHAMBER ERROR**";
                } else {
                    p = r.prgm_mon.timeRemaining;
                }

                var is_paused = _is_program_paused(p);

                if (r.alarms.count > 0) {
                    return "Current Status: <span style='color:red'>ALARM</span>";
                } else {
                    if (r.mode === "RUN") {
                        if (is_paused) {
                            return "Current Status: Program Paused";
                        } else {
                            return "Current Status: Program Running";
                        }
                    } else {
                        return "Current Status: " + r.mode;
                    }
                }
            }
        },
        app_side_program : {
            depends: [{ 'prgm_set' : []}],
            handler: function (r) {
                var p;

                if ("CONT_NOT_READY_2" === r.prgm_set.chamber_error) {
                    p = "NONE";
                } else if (undefined !== r.prgm_set.chamber_error) {
                    p = "**CHAMBER ERROR**";
                } else {
                    var num = parseInt(r.prgm_set.number.substring(4), 10);
                    var lnk = "<a href='#none' " +
                              "onclick='window.open(\"/editprog.html?program=" + num + "\",\"editprog\",\"width=950,height=600,scrollbars=yes\");'" +
                              "style='float:none' >";
                    var lnk_end = "</a>";

                    p = lnk + r.prgm_set.name + lnk_end;
                }


                return "Program: " + p;
            }
        },
        app_side_prod_temp : {
            depends: [{ 'temp_ptc' : [] }, {'config' : [] }],
            handler: function (r) {
                var t;

                if (r.config.ptc) {
                    if (r.temp_ptc.status === "OFF") {
                        t = "OFF";
                    } else {
                        t = r.temp_ptc.mon_prod_temp + "&deg;C";
                    }
                } else {
                    t = "Option not installed.";
                }

                return "Product Temperature: " + t;
            }
        },
        app_side_prod_status : {
            depends: [{ 'temp_ptc' : [] }, {'config' : [] }],
            handler: function (r) {
                var t = "Product Control: ";

                if (r.config.ptc) {
                    t += r.temp_ptc.status;
                } else {
                    t += "Not Installed";
                }

                return t;
            }
        },
        app_side_prod_setpoint : {
            depends: [{ 'temp_ptc' : [] }, {'config' : [] }],
            handler: function (r) {
                var t;

                if (r.config.ptc) {
                    if (r.temp_ptc.status === "OFF") {
                        t = "OFF";
                    } else {
                        t = r.temp_ptc.target_prod_temp + "&deg;C";
                    }
                } else {
                    t = "Option not installed.";
                }

                return "Product Setpoint: " + t;
            }
        },
        app_side_air_temp : {
            depends: [{ 'temp' : [] }, { 'temp_ptc' : [] }, {'config' : [] }],
            handler: function (r) {
                var t = undefined;
                if (r.config.ptc) {
                    t = r.temp_ptc.mon_air_temp;
                } else {
                    t = r.temp.monitored;
                }
                return "Air Temperature: " + t + "&deg;C";
            }
        },
        app_side_air_setpoint : {
            depends: [{ 'temp' : [] }],
            handler : function (r) {
                var t = undefined;
                if (r.config.ptc) {
                    t = r.temp_ptc.target_air_temp;
                } else {
                    t = r.temp.target;
                }
                return "Air Setpoint: " + t + "&deg;C";
            }
        },
        app_side_humidity : {
            depends: [{ 'humi' : [] }, {'config' : [] }],
            handler: function (r) {
                var h ;

                if (r.config.humidity) {
                    h = r.humi.monitored + "%";
                } else {
                    h = "Option not installed.";
                }

                return "Humidity: " + h;
            }
        },
        app_side_humidity_setpoint : {
            depends: [{ 'humi' : [] }, {'config' : [] }],
            handler: function (r) {
                var h;

                if (r.config.humidity) {
                    h = r.humi.target + "%";

                    if(h == "-1%") {
                        h = "OFF";
                    }
                } else {
                    h = "Option not installed.";
                }

                return "Humidity Setpoint: " + h;
            }
        },
        app_side_alarm_status : {
            depends: [{ 'alarms' : [] }],
            handler: function (r) {
                var ac = r.alarms.count + " alarm" +
                    (r.alarms.count !== 1 ? "s" : "");

                Espec.alarms(r.alarms.alarms);

                return "Alarm Status: " + ac;
            }
        },
        app_side_alarms : {
            depends: [{ 'alarms' : [] }],
            handler: function (r) {
                /* For some reason, the following blows up with "Unknown runtime error" in IE */
                /*
                var as = "";
                $A(r.alarms.alarms).each(function (a) {
                    // Need flashyer alarm status
                    as += "<p>" + a.text + " [" + a.id + "]</p>";
                });
                return as;
                */

                function mkText(a) {
                    return a.text + " [" + a.id + "]";
                }

                return $A(r.alarms.alarms).map(mkText).join("<br />");
            }
        },
        app_side_keylock : {
            depends: [{'keyprotect' : [] }],
            handler: function (r) {
                if (r.keyprotect == "OFF") {
                    return "Chamber Keylock: " + r.keyprotect;
                } else {
                    return "Chamber Keylock: <span style='color:red; font-weight:bold'>" + r.keyprotect + "</span>";
                }
            }
        },
        app_side_date : {
            depends: [{ 'time' : [] }],
            handler: function (r) {
                return formatComponentDate(r.time.year, r.time.month, r.time.day,
                                           r.time.hour, r.time.minute, r.time.second);
            }
        },
        app_side_time_signals : {
            depends: [{ 'relay' : [] }, { 'time_signals' : []}],
            handler: function (r) {
                var t = "";

                if (r.relay.count > 0) {
                    $A(r.relay.signals).each(function (s) {
                        if (undefined === r.time_signals.names[s.toString()]) {
                            t += "'Signal " + s + "' is on<br />";
                        } else {
                            t += "'" + r.time_signals.names[s.toString()] + "' is on<br />";
                        }
                    });
                } else {
                    t = "(none)";
                }

                return t;
            }
        },
        app_side_set : {
            depends: [{ 'set' : [] }],
            handler: function (r) {
                var t = "Refrigeration Control: ";

                switch(r.set) {
                case 'REF0': t +=   "0%"; break;
                case 'REF1': t +=  "20%"; break;
                case 'REF3': t +=  "50%"; break;
                case 'REF6': t += "100%"; break;
                case 'REF9': t += "Auto"; break;
                }

                return t;
            }
        },
        app_program_current_step : {
            depends: [{ 'prgm_mon' : [] }],
            handler: function(r) {

                var p;
                if ("CONT_NOT_READY_2" === r.prgm_mon.chamber_error) {
                    p = "NONE";
                } else if( undefined !== r.prgm_mon.chamber_error) {
                    p = "**CHAMBER ERROR**";
                } else {
                    p = r.prgm_mon.currentStep;
                }

                return "Current Step: " + p;
            }
        },
        app_program_end_step : {
            depends: [{ 'current_prgm_data_all' : [] }],
            handler: function(r) {
                var p;

                if ("CONT_NOT_READY_2" === r.current_prgm_data_all.chamber_error) {
                    p = "NONE";
                } else if( undefined !== r.current_prgm_data_all.chamber_error) {
                    p = "**CHAMBER ERROR**";
                } else {
                    p = r.current_prgm_data_all.program_steps.length;
                }

                return "End Step: " + p;
            }
        },
        app_program_current_step_remaining_time : {
            depends: [{ 'prgm_mon' : [] }],
            handler: function(r) {
                var p;

                if ("CONT_NOT_READY_2" === r.prgm_mon.chamber_error) {
                    p = "NONE";
                } else if (undefined !== r.prgm_mon.chamber_error) {
                    p = "**CHAMBER ERROR**";
                } else {
                    p = r.prgm_mon.timeRemaining;
                }

                return "Current Step Remaining Time: " + p;
            }
        },
        app_program_total_run_time : {
            depends: [{ 'current_prgm_data_all' : [] }],
            handler: function(r) {
                var b1 = ("CONT_NOT_READY_2" === r.current_prgm_data_all.chamber_error);
                var b2 = ( undefined !== r.current_prgm_data_all.chamber_error);
                if (b1 || b2) {
                    return "Total Run Time: N/A";
                }

                var steps = r.current_prgm_data_all.program_steps;
                var total = 0;

                var times = new Array(steps.length);
                steps.each(function (s) {
                    times[s.step - 1] = (s.hours * 60) + s.minutes;
                });

                var total_with_ctrs = function (a,b,times) {
                    var total = 0;

                    // Grab each step
                    times.each(function(t) {
                        total += t;
                    });

                    if (a.cycles > 0) {
                        for (var i = (a.start - 1); i <= (a.end - 1); i++) {
                            // Add the number of times * length of each of a's steps
                            total += a.cycles * times[i];
                        }
                    }
                    if (b.cycles > 0) {
                        for (var i = (b.start - 1); i <= (b.end - 1); i++) {
                            // Add the number of times * length of each of b's steps
                            total += b.cycles * times[i];
                        }
                    }

                    return total;
                };

                var total = total_with_ctrs(r.current_prgm_data_all.program_info.a_count,
                                            r.current_prgm_data_all.program_info.b_count,
                                            times);

                var hours = Math.floor(total / 60);
                var minutes = total - (hours * 60);

                return "Total Run Time: " + hours + ":" +
                       (minutes >= 10 ? minutes : "0" + minutes);
            }
        },
        app_program_schedule_test_end : {
            depends: [{ 'current_prgm_data_all' : [] },
                      { 'time' : [] },
                      { 'prgm_mon' : [] }],
            handler: function(r) {
                var b1 = ("CONT_NOT_READY_2" === r.current_prgm_data_all.chamber_error);
                var b2 = ( undefined !== r.current_prgm_data_all.chamber_error);
                if (b1 || b2) {
                    return "Schedule Test End: N/A";
                }

                var current = r.prgm_mon.currentStep;
                var a = r.current_prgm_data_all.program_info.a_count;
                var rem_a = r.prgm_mon.aLeft;
                var b = r.current_prgm_data_all.program_info.b_count;
                var rem_b = r.prgm_mon.bLeft;

                var steps = r.current_prgm_data_all.program_steps;
                var l = new Array(steps.length);

                var dur_a = 0;
                var dur_b = 0;
                steps.each(function (s) {
                    var dur = s.minutes + (s.hours * 60);
                    l[s.step - 1] = dur;

                    if (a.start <= s.step && s.step <= a.end) {
                        dur_a += dur;
                    } else if (b.start <= s.step && s.step <= b.end) {
                        dur_b += dur;
                    }

                });

                var remaining = 0;
                // Grab the rest of the cycle we're currently in
                if (a.cycles > 0 && a.start <= current && current < a.end) {
                    // Notice we make sure we're not on the last element of the cycle
                    for(var i = (current - 1); i <= (a.end - 1); i++) {
                        remaining += l[i];
                    }
                } else if (b.cycles > 0 && b.start <= current && current < b.end) {
                    // Notice we make sure we're not on the last element of the cycle
                    for(var i = (current - 1); i <= (b.end - 1); i++) {
                        remaining += l[i];
                    }
                }

                // Add the remainder of the cycles
                remaining += (rem_a * dur_a) + (rem_b * dur_b);

                var lp = new Array(l.length);
                for(var i = 0; i < l.length; i++) {
                    lp[i] = l[i];

                    var ip = i + 1;
                    // Member of a?
                    if (a.cycles > 0 && a.start <= ip && ip <= a.end) {
                        lp[i] = undefined;
                    }

                    // Member of b?
                    if (b.cycles > 0 && b.start <= ip && ip <= b.end) {
                        lp[i] = undefined;
                    }

                    // Done or in progress?
                    if (i <= (current - 1)) {
                        lp[i] = undefined;
                    }
                }
                lp = lp.compact();

                lp.each(function (v) {
                    remaining += v;
                });

                var tmpRem = r.prgm_mon.timeRemaining;
                tmpRem = $A(tmpRem.split(':')).map(function (r) {
                    return parseInt(r, 10);
                });

                remaining += tmpRem[0] * 60;
                remaining += tmpRem[1];

                var tmpDate = new Date(r.time.year, r.time.month - 1, r.time.day,
                                       r.time.hour, r.time.minute, r.time.second);

                tmpDate.setTime(tmpDate.getTime() + (remaining * (1000 * 60)));

                return "Schedule Test End: " + formatDate(tmpDate);
            }
        },
        app_program_end_mode : {
            depends: [{ 'prgm_set' : [] }],
            handler: function(r) {
                var p;

                if ("CONT_NOT_READY_2" === r.prgm_set.chamber_error) {
                    p = "NONE";
                } else if (undefined !== r.prgm_set.chamber_error) {
                    p = "**CHAMBER ERROR**";
                } else {
                    p = r.prgm_set.end_mode;
                }

                return "End Mode: " + p;
            }
        }
    };

    /**
     * Function (public) _refresh -> refresh
     *
     * Refreshes all elements on the page with the
     * .app class. Note: if a handler isn't available
     * for a specific app'ed element, it will be 
     * treated as an error.
     *
     * Parameters
     *      loadables (optional) - a list of elements
     *          to update. they must have id's matching
     *          a defined handler. By default, it looks
     *          for all elements in the 'app' class.
     */
    function _refresh(loadables) {
        if (undefined === loadables) {
            loadables = $$('.app');
        }

        /* Breaking the steps makes it a 
         * bit easier to debug this. */
        var s1 = loadables.map(function (l) {
            return l.id;
        }).uniq();

        var s2 = s1.map(function (i) {
            return $A(_Handlers[i].depends);
        }).flatten();

        var s3 = s2.map(function (d) {
            return $H(d).toJSON();
        }).uniq();

        var to_load = new Hash();
        s3.each(function (j) {
            to_load = to_load.merge(j.evalJSON());
        });

        Espec.load(to_load.toObject(), function (r) {
            var m = Mutator.fill(_Handlers, loadables, r);
        });
    }

    /**
     * Function (public) _startMonitor
     *
     * Starts a monitoring loop to keep the status
     * bar continually up to date.
     */
    function _startMonitor() {
        if (undefined === __espec_state.monitor) {
            var f =_refresh.wrap(function(proceed) {
                proceed();
                __espec_state.monitor = f.delay(
                    __espec_const.monitor_timeout);
            });

            f();
        } else {
            // It's already running... don't start it again.
        }
    }

    /**
     * Function (public) _stopMonitor
     *
     * Stops the monitoring.
     */
    function _stopMonitor() {
        if (undefined === __espec_state.monitor) {
            return;
        } else {
            window.clearTimeout(__espec_state.monitor);
            __espec_state.monitor = undefined;
        }
    }

    function _parseLog(log) {
        return log.map(function (l) {
            h = $H(l)
            cmd = h.values()[0][0];
            ret = h.values()[0][1];

            if(ret.substr(0,3) == "NA:") {
                return "When executing '" + cmd + "', the chamber returned the error '" + ret + "'."; 
            } else {
                return undefined;
            }
        }).compact();
    }

    return {
        refresh : _refresh,
        startMonitor : _startMonitor,
        stopMonitor : _stopMonitor,
        parseLog : _parseLog
    };
}();

var AppGraph = function () {

    function _dateFormatter(n) {
        var tzOffset = new Date().getTimezoneOffset();

        // The date is stored on the device *as if* the chamber
        // was giving it GMT. It's loaded out and sent to us the
        // same way. When we get it, we need to promote it to
        // local time (hence, we add, rather than subtract the 
        // time zone offset).
        var d = new Date((n * 1000) + (tzOffset * 60 * 1000));
        var m = 1 + d.getMonth();
        var a = d.getDate();
        var h = d.getHours();
        var i = d.getMinutes();

        return (m < 10 ? "0" : "") + m + "/" +
               (a < 10 ? "0" : "") + a + " " +
               (h < 10 ? "0" : "") + h + ":" +
               (i < 10 ? "0" : "") + i;
    }

    function _processData(datum, datom_extents) {
        function do_scale(d, de, ne) {
            var x = d[0];
            var y = d[1];
            var el = ne.low;
            var eh = ne.high;
            var vl = de.low;
            var vh = de.high;

            // Normalize the y value to the extents we provide.
            var ny = el + (((y - vl) / (vh - vl)) * (eh - el));

            return [x,ny];
        }

        var scaledDatum = $A(datum).map(function (d) {
            return do_scale(d,datom_extents,__espec_const.graph_extents);
        });

        return scaledDatum;
    }

    /**
     * _draw_timeline expects its argument 'data' to be
     * an array of [timestamp,state] pairs.
     * 
     * The general type is: [[Int,String]].
     */
    function _draw_timeline(data) {
        var cwidth = __espec_state.graph.canvasWidth;
        var poffset = __espec_state.graph.plotOffset.left;

        /*
        $('timeline').style.width =  (cwidth - poffset) + "px";
        */
        $('timeline').style.marginLeft = __espec_state.graph.plotOffset.left + "px";
        $('timeline-label').style.marginLeft = __espec_state.graph.plotOffset.left + "px";

        var alarm = [];
        var constant = [];
        var program = [];
        var hold = [];
        var off = [];

        var last = null;
        data.each(function (sample) {
            var i = sample[0];
            var mode = sample[1];

            switch(mode) {
            case "ALARM": // ??
                // alarm[alarm.length - 1][1] = 1;
                alarm.push([i,1]);
                break;
            case "CONSTANT":
                // constant[constant.length - 1][1] = 1;
                constant.push([i,1]);
                break;
            case "RUN":
                // program[program.length - 1][1] = 1;
                program.push([i,1]);
                break;
            case "STANDBY": // ?? 
                // hold[hold.length - 1][1] = 1;
                hold.push([i,1]);
                break;
            case "OFF":
                // off[off.length - 1][1] = 1;
                off.push([i,1]);
                break;
            }
            i += 1;
        });

        var sets = [{data:alarm, color:'#B22222', lines:{fill:true}},
                    {data:constant, color: '#006400', lines:{fill:true}},
                    {data:program, color: '#0000FF', lines:{fill:true}},
                    {data:hold, color: '#FFD700', lines:{fill:true}},
                    {data:off, color: '#000000', lines:{fill:true}}];

        Flotr.draw(
            $('timeline'), sets,
            {points: {show:true, lineWidth: 3, radius: 1 },
             yaxis: {noTicks: 0, min: 0, max: 2},
             xaxis: {noTicks: 0, min : __espec_state.graph_window.x.low, max : __espec_state.graph_window.x.high},
             shadowSize: 0}
        );
    }

    function _draw(dataSets, timeline) {
        var dataSets_ = dataSets.map(function (d) {
            return { data : _processData(d.data, d.extents),
                     label : d.label,
                     points : {show : false},
                     lines : {show : true} };
        });

        function yTickFormatter (v) {
            var usedPostfixes = [];
            var textVal = dataSets.map(function (d) {
                if (-1 === usedPostfixes.indexOf(d.postfix)) {
                    usedPostfixes.push(d.postfix);
                    var r = d.extents.high - d.extents.low;

                    var value = (v * r) + d.extents.low;
                    return [value.toFixed(2) + d.postfix];
                } else {
                    return [];
                }
            }).flatten().join("/");

            return textVal;
        }

        var yaxis_opt;
        var xaxis_opt;

        yaxis_opt = {
            min : __espec_const.graph_extents.low,
            max : __espec_const.graph_extents.high,
            tickFormatter : yTickFormatter
        };

        if (undefined === __espec_state.graph_window) {
            /*
            yaxis_opt = {
                min : __espec_const.graph_extents.low,
                max : __espec_const.graph_extents.high,
                tickFormatter : yTickFormatter
            };
            */

            xaxis_opt = {
                tickFormatter : _dateFormatter
            };
        } else {
            /*
            yaxis_opt = {
                min : __espec_state.graph_window.y.low,
                max : __espec_state.graph_window.y.high,
                tickFormatter : yTickFormatter
            };
            */

            xaxis_opt = {
                min : __espec_state.graph_window.x.low,
                max : __espec_state.graph_window.x.high,
                tickFormatter :  _dateFormatter
            };
        }

        var options = {
            yaxis: yaxis_opt,
            xaxis: xaxis_opt,
            legend: {
                show: true,
                noColumns: 2,
                position: 'se',
                margin: 5,
                backgroundColor: '#CCCCCC',
                backgroundOpacity: 0.55
            },
            selection: {
                mode: 'xy'
            },
            shadowSize: 0
        };

        __espec_state.graph = Flotr.draw($('graph'), dataSets_, options);
        x = __espec_state.graph.axes.x;
        y = __espec_state.graph.axes.y;

        __espec_state.graph_window = {
            x : {
                low : x.min,
                high : x.max
            },
            y : {
                low : x.min,
                high : x.max
            }
        };

        $('graph_download_link').href = "/app/app.app?csv_graph_start=" + __espec_state.graph_window.x.low +
                                        "&csv_graph_end=" + __espec_state.graph_window.x.high;
        $('all_download_link').href = "/app/app.app?csv_graph_start=0" +
                                        "&csv_graph_end=" + (Math.pow(2,31) - 1);

        if (undefined !== timeline) {
            _draw_timeline(timeline);
        }
    }

    function _shift(amount) {
        if (undefined === __espec_state.graph_window) {
            return;
        }

        var win = __espec_state.graph_window;

        var shiftBy = (win.x.high - win.x.low) * amount;
        win.x.low = win.x.low + shiftBy;
        win.x.high = win.x.high + shiftBy;

        __espec_state.graph_window = win;
    }

    function _zoom(amount) {
        if (undefined === __espec_state.graph_window) {
            return;
        }

        var win = __espec_state.graph_window;
        var width = (win.x.high - win.x.low);
        var xMid = (win.x.high - (width / 2));
        var height = (win.y.high - win.y.low);
        var yMid = (win.y.high - (height / 2));

        var xDelta = width * (amount / 2);
        var yDelta = height * (amount / 2);

        win.x.low = xMid - xDelta;
        win.x.high = xMid + xDelta;
        win.y.low = yMid - yDelta;
        win.y.high = yMid + yDelta;

        if (win.y.high > __espec_const.graph_extents.high) {
            win.y.high = 1.0;
        }

        if (win.y.low < __espec_const.graph_extents.low) {
            win.y.low = 0.0;
        }

        __espec_state.graph_window = win;
    }

    return {
        draw : _draw,
        draw_timeline: _draw_timeline,
        shift : _shift,
        zoom : _zoom
    };
}();

/* Manage the time signals, and their naming. */
var TimeSignals = function () {
    function _set_signal_name(id) {
        var name = window.prompt("Enter the new name for time signal " + id + ":");

        if (null == name || name.length <= 0) {
            alert("Name must be at least one character long.");
        } else {
            $('sig_label_' + id).update(name); // Set the label so we see what it is

            /* Save the new name to the server immediately. */
            var ps = new Object();
            ps['sig_name_' + id] = name;
            ps['save'] = 'save';

            var r = new Ajax.Request('/app/app.app', {
                method: 'post',
                parameters: ps
            });
        }
    }

    function _create_signal_checkboxes(elem,sigs,checks,active,edit) {
        var templateString = (checks ? '<input type="checkbox" class="sig_on" name="sig_#{id}" id="sig_#{id}" value="ON" #{checked} /> ' : '') +
                             '<span class="sig_name" id="sig_label_#{id}" style="cursor:pointer" >#{name}</span> <br />';
        var sig_template = new Template(templateString);

        $R(1,sigs.count).toArray().each(function (e) {
            var name = "Signal " + e;

            if (undefined !== sigs.names[e]) {
                name = sigs.names[e];
            }

            var checked = "";

            if (undefined !== active) {
                if (-1 !== active.indexOf(e)) {
                    checked = "CHECKED";
                }
            }

            // Create the dom element
            elem.insert(sig_template.evaluate({
                "id" : e,
                "name" : name,
                "checked" : checked
            }));

            // We don't always want to allow the box to be editable.
            if (edit) {
                // Register the click event to change the name
                $('sig_label_' + e).observe('click', function (evt) {
                    _set_signal_name(e);
                });
            }
        });
    }

    function _editable(elem,sigs,active,checks) {
        var c = (checks === true ? true : false);
        return _create_signal_checkboxes(elem,sigs,c,active,true);
    }

    function _constant(elem,sigs,active) {
        return _create_signal_checkboxes(elem,sigs,true,active,false);
    }

    return {
        editable : _editable,
        constant : _constant
    };
}();

Ajax.Responders.register({
    onException: function(request,exception) {
        (function () {
            throw exception;
        }).defer();
    }
});

