
var EDGY = EDGY || {};

EDGY.new_edgynet = function () {
    var new_node = function (node_id, node_name) {
        return {
            id: node_id,
            name: node_name,
            tty_count: 0,
            next_tty_name: function () {
                var tty_count = ++this.tty_count;
                return this.name + " : (" + tty_count + ")";
            }
        };
    };

    var new_load_status_indicator = function () {
        var indicator = {
            timer_ref: null,
            init: function () {
                var first_call = function () {
                    indicator._clear_bg_image();
                    $("#netmap").append($("<p id='load-status'>Initialising .</p>"));
                    var add_dot = function () {
                        var s = $("#load-status").text();
                        $("#load-status").text(s + ".");
                    }; 
                    window.clearInterval(indicator.timer_ref);
                    indicator.timer_ref = window.setInterval(add_dot, 1000);
                };
                // Should be a timeout, but we use interval to make stop() simpler
                indicator.timer_ref = window.setInterval(first_call, 1000);
            },
            finish: function () { 
                this._clear_bg_image();  
                $("#load-status").remove();
                this._stop();
            },
            fail: function () { 
                this._clear_bg_image(); 
                this._stop();
                Err = $("<p id='fail-result'>! Failed to Initialise</p>");
                $("#netmap").append(Err);
            },
            _clear_bg_image: function () {
                $("#netmap").css("background-image", "none");
            },
            _stop: function () { 
                window.clearInterval(this.timer_ref);
            }
        };
        return indicator;
    };

    var edgynet = {
        zone_id: null,
        pubsub: null,
        nodes: [],
        tty_rack: EDGY.new_tty_rack(),
        load_status_indicator: new_load_status_indicator(),
        init: function (extra_init, start_immediately) {
            if (! start_immediately) {
                var task = function () {
                    edgynet.init(extra_init, true);
                }
                // Stall init for half a second for splash screen
                window.setTimeout(task, 500);
                return;
            }
            this.load_status_indicator.init();
            var on_pubsub_ready = function (succeeded, zone_id) {
                if (succeeded) {
                    edgynet.zone_id = zone_id;
                    edgynet.load_status_indicator.finish();
                    if (extra_init) {
                        extra_init();
                    }
                } else {
                    edgynet.load_status_indicator.fail();
                }
            };
            this.pubsub = EDGY.select_pubsub(on_pubsub_ready);
            var on_publish = function (payload) {
                return edgynet.publish(payload);
            };
            EDGY.register_keypress_handler(on_publish);
            var on_add_tty = function (node_id) {
                edgynet.add_tty(node_id);
            };
            this.tty_rack.init(on_add_tty);
        },
        add_node: function (node_id, node_name) {
            var node = new_node(node_id, node_name);
            this.nodes[node_id] = node;
        },
        add_tty: function (node_id) {
            var node = this.nodes[node_id];
            var display_name = node.next_tty_name();
            var tty_id = EDGY.rand_tty_id();
            var chan = "/tty/" + this.zone_id + "/" + node_id + "/" + tty_id + "/";
            var in_channel = chan + "in";
            var out_channel = chan + "out";
            var subscribe = function (callback) {
                return edgynet.pubsub.subscribe(out_channel, this, callback);
            };
            var unsubscribe = function (ref) {
                return edgynet.pubsub.unsubscribe(ref);
            };
            this.tty_rack.add_tty({
                tty_id: tty_id,
                display_name: display_name,
                in_channel: in_channel,
                subscribe: subscribe,
                unsubscribe: unsubscribe
            });
        },
        publish: function (payload) {
            var active_tty = this.tty_rack.active_tty;
            if (payload && active_tty && this.pubsub) {
                this.pubsub.publish(active_tty.in_channel, payload);
                return true;
            } 
            return false;
        }
    };

    return edgynet;
};

EDGY.new_tty_rack = function () {
    var tty_rack = {
        ttys: [],
        active_tty: null,
        widget: $("#tty-rack"),
        init: function (on_add_tty) {
            this.widget.droppable({
                accept: ".mapnode",
                drop: function (ev, ui) {
                    var xhtml_id = ui.draggable.attr("id");
                    var node_id = xhtml_id.match(/node-(.*)$/)[1];
                    on_add_tty(node_id);        
                }
            });
            this.widget.sortable({
                axis: "y",
                delay: 100,
                handle: ".tbar"
            });
        },
        add_tty: function (params) {
            var tty_widget = EDGY.new_tty_widget(this.widget, params);
            var tty = EDGY.new_tty(tty_widget, this, params);
            this.ttys.push(tty);
            tty.activate();
        }
    };

    return tty_rack;
};

EDGY.new_tty_widget = function (container, params) {
    var dom_id = "tty-" + params.tty_id;

    var widget = {
        xhtml_id: function () {
            return "#" + dom_id;
        },
        jnode: function () {
            return $(this.xhtml_id());
        },
        jnode_for: function (what) {
            return $(this.xhtml_id() + " " + what);
        },
        tbar: function () {
            return this.jnode_for(".tbar");
        },
        xbox: function () {
            return this.jnode_for(".tbar .xbox");
        },
        out: function () {
            return this.jnode_for(".out");
        },
        outbox: function () {
            return this.jnode_for(".outbox");
        },
        current_output: function () {
            return this.out().text();
        },
        set_output: function (s) {
            this.out().text(s);
        },
        activate: function () {
            this.jnode().removeClass("tty-inactive");
            // The following is to grab focus back to the document which 
            // is lost when an applet is loaded and when focus is on the 
            // browser tab rather than the page.
            // Note that for focus() to work the element must have a 
            // tabindex attribute (unless it's a button, href, etc).
            $("#netmap").focus();
        },
        deactivate: function () {
            this.jnode().addClass("tty-inactive");
        },
        minimise: function () {
            this.outbox().hide();
        },
        maximise: function () {
            this.outbox().show();
            this.scroll_down();
        },
        scroll_down: function () {
            var from_top = this.outbox().attr("scrollHeight");
            this.outbox().scrollTop(from_top);
        },
        update_output: function (s) {
            if (jQuery.browser.msie) {          // IE Hack
                s = s.replace(/\n/g, "\r");
            }
            var txt = this.current_output();
            for (var i = 0; i < s.length; i++) {
                if (s.charAt(i) === "\b") {
                    // Treat Backspace as a rubout character
                    txt = txt.slice(0, -1);
                } else {
                    txt = txt + s.charAt(i);
                }
            }
            this.set_output(txt);
            this.scroll_down();
        }
    };

    container.prepend($(
        "<li class='tty' id='" + dom_id + "'>" + 
            "<h2 class='tbar'>" + params.display_name + "<span class='xbox'>X</span></h2>" +
            "<div class='outbox'>" +
                "<pre class='out'></pre>" +
                "<span class='cursor'>&nbsp;&nbsp;</span>" +
            "</div>" +
        "</li>"
    ));

    return widget;
};

EDGY.new_tty = function (tty_widget, tty_rack, params) {
    var state = {
        ACTIVE: 0,
        DEACTIVE_OPEN: 1,
        DEACTIVE_CLOSED: 2
    };

    var tty = {
        state: state.ACTIVE,
        rack: tty_rack,
        widget: tty_widget,
        in_channel: params.in_channel,
        subscribe: params.subscribe,
        unsubscribe: params.unsubscribe,
        subscription: null,
        init: function () {
            this.widget.jnode().click(function () {
                tty.on_widget_click();
            });
            this.widget.tbar().dblclick(function () {
                tty.on_tbar_dblclick();
            });
            this.widget.xbox().click(function () {
                tty.on_xbox_click();
            });
            var on_tty_out = function (message) {
                if (message.data === "\x1B!exit;") {      
                    this.do_unsubscribe();
                } else {
                    this.widget.update_output(message.data);
                }
            };
            this.subscription = this.subscribe(on_tty_out);
        },
        activate: function () {
            if (this.rack.active_tty) {
                this.rack.active_tty.deactivate();
            }
            this.rack.active_tty = this;
            this.widget.activate();
            this.state = state.ACTIVE;
        },
        deactivate: function () {
            if (this.rack.active_tty === this) {
                this.rack.active_tty = null;
            }
            this.widget.deactivate();
            this.state = state.DEACTIVE_OPEN;
        },
        do_unsubscribe: function () {
            this.unsubscribe(this.subscription);
            this.widget.jnode().slideUp(200);
        },
        on_widget_click: function () {
            switch (this.state) {
                case state.ACTIVE:
                    break;
                case state.DEACTIVE_OPEN:
                    this.activate();
                    break;
                case state.DEACTIVE_CLOSED:
                    break;
            }
            return false;
        },
        on_tbar_dblclick: function () {
            switch (this.state) {
                case state.ACTIVE:
                    this.deactivate();
                    this.widget.minimise();
                    this.state = state.DEACTIVE_CLOSED;
                    break;
                case state.DEACTIVE_OPEN:
                    this.widget.minimise();
                    this.state = state.DEACTIVE_CLOSED;
                    break;
                case state.DEACTIVE_CLOSED:
                    this.widget.maximise();
                    this.activate();
                    break;
            }
            return false;
        },
        on_xbox_click: function () {
            this.do_unsubscribe();
        }
    };

    tty.init();
    return tty;
};

EDGY.select_pubsub = function (on_ready) {
    var pubsub_transport = EDGY.prefs.which_transport();
    var pubsub = EDGY.new_pubsub(pubsub_transport);
    var on_connect = function (client_id) {
        if (client_id) {
            var on_zone_id = function (message) {
                var zone_id = message.data;
                on_ready(true, zone_id);
            };
            pubsub.subscribe("/service/edgynet-zone-api/id", this, on_zone_id);
            pubsub.publish("/service/edgynet-zone-api/id", "");
        } else {
            // TODO - return a reason for failure
            on_ready(false);
        }
    };
    pubsub.init(on_connect);
    return pubsub;
};

EDGY.prefs = {
    use_java_transport: false,
    config_panel: $("#config-panel"),
    transport_selector: "input[name='transport']",
    init: function () {
        this.use_java_transport = 
                    (document.cookie.indexOf("transport=java_tcp") !== -1);
        $("#config-button").click(function () {
            if (EDGY.prefs.is_config_menu_open()) {
                EDGY.prefs.close_config_menu();
            } else {
                EDGY.prefs.open_config_menu();
            }
            return false;
        });
        $("#save-config-button").click(function () {
            EDGY.prefs.on_save_config();
            return false;
        });
    },
    is_config_menu_open: function () {
        return this.config_panel.css("display") !== "none";
    },
    open_config_menu: function () {
        var transport = this.which_transport();
        $(this.transport_selector).val([transport]);
        this.config_panel.show();
    },
    close_config_menu: function () {
        this.config_panel.hide();
    },
    on_save_config: function () {
        this.close_config_menu();
        var trans = $(this.transport_selector + ":checked").val();
        if (trans !== this.which_transport()) {
            this.use_java_transport = (trans === "java_tcp");
            var next_year = new Date();
            next_year.setFullYear(next_year.getFullYear() + 1);
            var cookie_expiry = next_year.toGMTString();
            document.cookie = "transport=" + trans + ";expires=" + cookie_expiry;
            var s = 
                "The change you have made will not take effect \r\n" +
                "until the demo page has been reloaded.\r\n" +
                "\r\n" +
                "Do you want to reload the page now?";
            if (confirm(s)) {
                window.location.reload();
            }
        }
    },
    which_transport: function () {
        return this.use_java_transport ? "java_tcp" : "comet";
    }
};

EDGY.register_keypress_handler = function (on_publish) {
    var opera_key_consumed = false;             // Hack

    var keypress_escape_seq = function (key_code) {
        var xterm_escape = function (key_code) {
            // see: http://invisible-island.net/xterm/ctlseqs/ctlseqs.html
            //      https://developer.mozilla.org/en/DOM/Event/UIEvent/KeyEvent
            switch (key_code) {
                case 33: return "5~";         // PageUp
                case 34: return "6~";         // PageDown
                case 35: return "4~";         // End
                case 36: return "1~";         // Home
                case 37: return "D";          // Left
                case 38: return "A";          // Up
                case 39: return "C";          // Right
                case 40: return "B";          // Down
                case 45: return "2~";         // Insert
                case 46: return "3~";         // Delete
                case 112: return "11~";       // F1
                case 113: return "12~";       // F2
                case 114: return "13~";       // F3
                case 115: return "14~";       // F4
                case 116: return "15~";       // F5
                case 117: return "17~";       // F6
                case 118: return "18~";       // F7
                case 119: return "19~";       // F8
                case 120: return "20~";       // F9
                case 121: return "21~";       // F10
                case 122: return "23~";       // F11
                case 123: return "24~";       // F12
                default: return "";
            }
        };
        var ESC_CSI = String.fromCharCode(27) + "[";
        var s = xterm_escape(key_code);
        return s ? ESC_CSI + s : "";
    };

    var decode_keycode = function (k) {
        var payload = "";
        if (k === 8) {                  // Backspace -> Del
            payload = String.fromCharCode(127);
        } else if ($.inArray(k, [9, 12, 13, 27]) !== -1) {
            payload = String.fromCharCode(k);  
        } else {
            payload = keypress_escape_seq(k);
        }
        return payload;
    };

    var decode_ctrl_key = function (k) {
        var payload = "";
        k %= 128;           // for IE & webkit browsers - eg CTRL-\
        if (k >= 64 && k <= 126) {
            payload = String.fromCharCode(k % 32);
        } else if (k === 127 || k === 63) {
            payload = String.fromCharCode(127);
        } 
        return payload;
    };

    // For debugging use only (chrome has some problems with JSON.stringify())
    var stringify_obj = function (obj) {
        var s = "";
        for (var i in obj) {
            if (typeof(obj[i]) !== "function") {
                s += i + ": " + obj[i] + "\n";
            }
        }
        return s;
    };

    var gecko_keypress_handler = function (ev) {
        // alert("keypress (gecko): \n" + stringify_obj(ev));
        var payload = "";
        if (ev.keyCode) {
            payload = decode_keycode(ev.keyCode);
        } else if (ev.charCode) {
            payload = ev.ctrlKey ?  
                        decode_ctrl_key(ev.charCode) : 
                        String.fromCharCode(ev.charCode);
        }
        on_publish(payload);
        return false;
    };

    var opera_keydown_handler = function (ev) {
        // alert("keydown (opera): \n" + stringify_obj(ev));
        var SHIFT_KEY = 16, CTRL_KEY = 17;
        var k = ev.keyCode;
        var payload = decode_keycode(k) || (ev.ctrlKey ? decode_ctrl_key(k) : "");
        on_publish(payload);
        opera_key_consumed = 
                (payload || k === SHIFT_KEY || k === CTRL_KEY) ?
                true : false;
        return false;
    };

    var opera_keypress_handler = function (ev) {
        // alert("keypress (opera): \n" + stringify_obj(ev));
        if (! opera_key_consumed) {
            var k = ev.keyCode;
            if (k && !ev.ctrlKey) {
                var payload = String.fromCharCode(k);
                on_publish(payload);
            }
        }
        return false;
    };

    var default_keydown_handler = function (ev) {
        // alert("keydown (default): \n" + stringify_obj(ev));
        var k = ev.keyCode;
        var payload = decode_keycode(k) || (ev.ctrlKey ? decode_ctrl_key(k) : "");
        if (on_publish(payload)) {
            // Returning false here causes some browsers (eg webkit browsers)
            // to prevent a keypress event from firing, so we only return
            // false when the key event will not need further processing.
            return false;
        }
    };

    var default_keypress_handler = function (ev) {
        // alert("keypress (default): \n" + stringify_obj(ev));
        var k = ev.keyCode;
        if (k && !ev.ctrlKey) {
            var payload = String.fromCharCode(k);
            on_publish(payload);
        }
        return false;
    };

    var do_register_keypress_handler = function () {
        if (jQuery.browser.mozilla) {
            $(document).keypress(gecko_keypress_handler);
        } else if (jQuery.browser.opera) {
            $(document).keydown(opera_keydown_handler);
            $(document).keypress(opera_keypress_handler);
        } else {
            // Safari, Chrome & IE
            $(document).keypress(default_keypress_handler);
            $(document).keydown(default_keydown_handler);
        }
    };
    
    do_register_keypress_handler();
}; 


EDGY.rand_tty_id = function () {
    // Generate a 32-bit fixed-width hex string
    var n = Math.floor(Math.random() * 0xffffffff);
    var hex = n.toString(16).toUpperCase();
    var EXPECTED_LEN = 8;
    while (hex.length < EXPECTED_LEN) {
        hex = "0" + hex;
    }
    return hex;
};

EDGY.load_demo = function (edgynet) {
    var create_node = function (node_id, node_name) {
        edgynet.add_node(node_id, node_name);
    };

    var add_mapnode = function (spec) {
        create_node(spec.node_id, spec.node_name);
        var xhtml_id = "node-" + spec.node_id;
        var alt_text = spec.node_name;
        var img = "<img class='mapnode' " +
                        "id='" + xhtml_id + "' " +
                        "src='" + spec.img_src + "' " +
                        "height='" + spec.height + "' " +
                        "width='" + spec.width + "' " +
                        "alt='" + alt_text + "'/>";
        var mapnode = $(img);
        mapnode.css("left", spec.x_position);
        mapnode.css("top", spec.y_position);
        $("#netmap").append(mapnode);
        mapnode.draggable({
            appendTo: "body",
            helper: "clone",
            opacity: 0.6
        });
    };

    var mapnode_spec = function (node_id, node_name, img_src, 
                                 height, width, x_px, y_px) {
        return {
            node_id: node_id,
            node_name: node_name,
            img_src: img_src,
            height: height,
            width: width,
            x_position: x_px,
            y_position: y_px
        };
    };

    var mapnode_router_spec = function (num, node_id, x_px, y_px) {
        var node_name = "Router " + num;
        var img_src = "/images/router" + num + ".png";
        var height = 43;
        var width = 80;
        return mapnode_spec(node_id, node_name, img_src, 
                            height, width, x_px, y_px);
    };

    var mapnode_host_spec = function (num, node_id, x_px, y_px) {
        var node_name = "Host " + num;
        var img_src = "/images/host" + num + ".png";
        var height = 69;
        var width = 80;
        return mapnode_spec(node_id, node_name, img_src, 
                            height, width, x_px, y_px);
    };

    var draw_map_background = function () {
        // Smmother transition if we set bg to white first
        $("#netmap").css("background-color", "white");
        $("#netmap").css("background-image", "url(/images/netmap-bg.jpg)");
    };

    var do_load_demo = function () {
        draw_map_background();
        var specs = [
            mapnode_router_spec(1, "00000001", 335, 57),
            mapnode_router_spec(2, "00000002", 335, 226),
            mapnode_host_spec(1, "00000003", 60, 31),
            mapnode_host_spec(2, "00000004", 610, 31),
            mapnode_host_spec(3, "00000005", 610, 200)
        ];
        $.each(specs, function () {
            add_mapnode(this);
        });
    };

    do_load_demo();
}; 


EDGY.decoration_fixups = function () {
    var align_msg_boxes = function () {
        var msg_boxes = $("#demo .msg-box");
        var highest = 0;
        msg_boxes.each(function () {
            var h = $(this).height();
            highest = h > highest ? h : highest;
        });
        msg_boxes.height(highest);
    };

    align_msg_boxes();
};

$(function () {
    EDGY.decoration_fixups();

    EDGY.prefs.init();
    var edgynet = EDGY.new_edgynet();
    edgynet.init(function () {
        EDGY.load_demo(edgynet);
    });
});


