/* This is a JavaScript module (JSM) to be imported via Components.utils.import() and acts as a singleton.
   Only the following listed symbols will exposed on import, and only when and where imported. */
var EXPORTED_SYMBOLS = ["Flagfox"];

Components.utils.import("resource://flagfox/ipdb.jsm");  // Access IPDB here

var prefService = null;
var ioService = null;
var threadManager = null;

var mainPrefListener = null;
var warningsThisSession = [];

var hotKeys;
var hotClicks;

var actionsListAge = 0;
var actionsSaveIsInProgress = false;

var loaded = false;

//// Main Flagfox object (only thing exported out of this file) /////////////////////////////////////////////////////////////////////////////////////////////////////////
var Flagfox =
{
    strings : null,  // Localized strings (flagfox.properties)

    actions : null,  // Loaded actions (array of {name, template} with optional properties {iconclick, hotkey, show, custom})

    load : function(initWindow)
    {
        if (loaded === null)
            return;
        if (!loaded)
        {
            loaded = true;  // Load once per application session

            try
            {
                var bundles = Components.classes["@mozilla.org/intl/stringbundle;1"]
                                        .getService(Components.interfaces.nsIStringBundleService);
                Flagfox.strings = bundles.createBundle("chrome://flagfox/locale/flagfox.properties");
                if (!Flagfox.strings || !Flagfox.strings.getSimpleEnumeration().hasMoreElements())
                    throw "Could not load localized strings!";

                prefService = Components.classes["@mozilla.org/preferences-service;1"]
                                        .getService(Components.interfaces.nsIPrefBranch)
                                        .QueryInterface(Components.interfaces.nsIPrefService);
                if (!prefService)
                    throw "Can't find preferences service!";

                ioService = Components.classes["@mozilla.org/network/io-service;1"]
                                      .getService(Components.interfaces.nsIIOService);
                threadManager = Components.classes["@mozilla.org/thread-manager;1"]
                                          .getService(Components.interfaces.nsIThreadManager);

                DnsHandler.init();

                ipdb.load();

                Flagfox.loadActions();

                migrateOldPrefs();
                mergeDefaultActionUpdates();

                function onPrefChange(branch,prefName)
                {
                    switch (prefName)
                    {
                        case "actions":
                            if (!actionsSaveIsInProgress)  // Reload if this wasn't our doing (manual pref edit/reset or Mozilla Weave Sync)
                            {
                                Flagfox.loadActions();
                                mergeDefaultActionUpdates();  // Make sure changes to defaults list are handled correctly
                            }
                            return;
                        case "showfavicons":
                            actionsListAge = Date.now();  // All menus will need to be updated
                            return;
                        case "warn.tld":
                        case "warn.proxy":
                            if (!branch.prefHasUserValue(prefName))
                                warningsThisSession = [];  // Reset list on pref reset
                            return;
                    }
                }
                mainPrefListener = new PrefListener("flagfox.",onPrefChange);

                // Show first run page for users installing 4.0 so they can be informed of new features
                if (!safeGetBoolPref("flagfox.hasshown4dot0firstrunpage"))
                {
                    initWindow.setTimeout(function(){
                        initWindow.getBrowser().selectedTab = initWindow.getBrowser().addTab("chrome://flagfox/content/firstrun.xhtml",null,null);
                        prefService.setBoolPref("flagfox.hasshown4dot0firstrunpage",true);
                    }, 1000);  // Need a delay to make sure it's shown after an update under Firefox 3.6 for some reason
                }

                // Check if the IPDB version is getting old and results are beginning to get particularly stale (3 months or more old)
                if (Date.now() - ipdb.lastmodifiedtime >= 7776000000)
                    Flagfox.warning(initWindow, "flagfox.warn.stale", Flagfox.strings.GetStringFromName("stalewarnmessage"));

                // Queue up final shutdown sequence; each window has its own unload sequence as well
                var observerService = Components.classes["@mozilla.org/observer-service;1"]
                                                .getService(Components.interfaces.nsIObserverService);
                var quitEventObserver =
                {
                    observe : function(subject, topic, data)
                    {
                        if (topic != "quit-application")
                            return;
                        mainPrefListener.unregister();
                        ipdb.close();
                        observerService.removeObserver(quitEventObserver, "quit-application");
                    }
                };
                observerService.addObserver(quitEventObserver, "quit-application", false);
            }
            catch (e)
            {
                Flagfox.error("Fatal Flagfox startup error!",e);
                loaded = null;  // Don't do anything more if startup has failed
                return;
            }
        }

        // Load the flag for this window
        try { newFlagInstance(initWindow); }
        catch (e) { Flagfox.error("Error loading icon for window",e); }
    },

    loadActions : function()
    {
        var json = Components.classes["@mozilla.org/dom/json;1"]
                             .createInstance(Components.interfaces.nsIJSON);
        Flagfox.actions = json.decode(getUCharPref(prefService, "flagfox.actions"));
        logMessage("Flagfox actions loaded from JSON pref");

        Flagfox.refreshActions();
    },

    saveActions : function()
    {
        Flagfox.refreshActions();  // Apply any new bindings and resync IDs if any have changed

        var json = Components.classes["@mozilla.org/dom/json;1"]
                             .createInstance(Components.interfaces.nsIJSON);
        actionsSaveIsInProgress = true;
        try
        {
            setUCharPref(prefService, "flagfox.actions", json.encode(Flagfox.actions));
            logMessage("Flagfox actions saved to JSON pref");
        }
        catch (e) { Flagfox.error("Error saving actions",e); }
        actionsSaveIsInProgress = false;
    },

    refreshActions : function()  // Makes changes to actions list take effect; must saveActions() to make changes persist after application close
    {
        actionsListAge = Date.now();  // Make context menus refresh on next open

        hotKeys = [];    // Sparse array
        hotClicks = {};  // Associative array (object)

        if (!Flagfox.actions)
            throw Error("Actions not loaded!");

        for (var i in Flagfox.actions)  // Refresh all keyboard and icon click shortcuts
        {
            var hotkey = Flagfox.actions[i].hotkey;
            if (hotkey)
            {
                var combo = {};
                combo.id = i;
                combo.ctrlKey = hotkey.mods.indexOf("ctrl")!=-1;
                combo.altKey = hotkey.mods.indexOf("alt")!=-1;
                combo.metaKey = hotkey.mods.indexOf("meta")!=-1;
                var shiftKey = hotkey.mods.indexOf("shift")!=-1;
                var charCode = (shiftKey ? hotkey.key.toUpperCase() : hotkey.key.toLowerCase()).charCodeAt(0);
                if (!hotKeys[charCode])
                    hotKeys[charCode] = [];
                hotKeys[charCode].push(combo);
            }
            var hotclick = Flagfox.actions[i].iconclick;
            if (hotclick)
            {
                hotClicks[hotclick] = i;
            }
        }
    },

    setBindingsForAction : function(id,newclick,newhotkey)  // Must saveActions() after setting (which will also refreshActions() to make them active)
    {
        if (!Flagfox.actions)
            throw Error("Actions not loaded!");
        if (!Flagfox.actions[id])
            throw Error("Invalid action: " + id);

        if (newclick == "")
            newclick = undefined;

        // Unset existing bindings first, if needed
        if (newclick)
            for (var i in Flagfox.actions)
                if (Flagfox.actions[i].iconclick == newclick)
                    Flagfox.actions[i].iconclick = undefined;
        if (newhotkey)
            for (var i in Flagfox.actions)
                if (Flagfox.actions[i].hotkey && Flagfox.actions[i].hotkey.key == newhotkey.key && Flagfox.actions[i].hotkey.mods == newhotkey.mods)
                    Flagfox.actions[i].hotkey = undefined;

        // Set new bindings (undefined clears; null leaves alone)
        if (newclick !== null)
            Flagfox.actions[id].iconclick = newclick;
        if (newhotkey !== null)
            Flagfox.actions[id].hotkey = newhotkey;
    },

    getFaviconForTemplate : function(template)
    {
        try
        {
            switch (template.truncateBeforeFirstChar(":"))
            {
                case "copystring":
                    return getIconPath("copy");  // Copy to clipboard ad hoc pseudo-protocol
                case "javascript": case "data":
                    return getIconPath("special/script");
                case "about":  case "chrome":  case "resource":
                    return getIconPath("special/resource");
                case "file":
                    return getIconPath("special/localfile");
            }

            if (template.indexOf("://")==-1)
                template = "http://" + template;
            var uri = ioService.newURI(template, null, null);
            uri.host = uri.host.replace(/\{.*\}\.?/gi,"");  // Clear out any placeholders in the domain name
            uri.path = "favicon.ico";

            if (uri.host == "geotool.flagfox.net")  // May as well cheat here to avoid hitting the server a few million times
                return "chrome://flagfox/content/logo-small.png";

            return uri.spec;  // nsIFaviconService doesn't seem to want to work without a bookmark, but the main cache seems to work fine with it
        }
        catch (e)
        {
            return "";  // Given template probably isn't a valid URL
        }
    },

    getLocalizedNameForAction : function(action)
    {
        try {
            if (!action.custom)
            {
                var stringName = "action." + action.name.replace(/ /g,"_").toLowerCase();
                return Flagfox.strings.GetStringFromName(stringName);
            }
        } catch (e) {}
        return action.name;
    },

    warning : function(window,pref,message)  // Shows a slide-down info bar (max once per session for each unique message)
    {
        if (prefService.getCharPref(pref) == "disabled")  // Valid states are: "enabled", "once", & "disabled"
            return;  // Disabled by user

        var messageID = hashString(message);
        if (warningsThisSession.indexOf(messageID)!=-1)
            return;  // Shown before this session
        warningsThisSession.push(messageID);

        var notificationBox = window.getBrowser().getNotificationBox();

        const XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
        var notification = window.document.createElementNS(XULNS,"notification");
        notification.setAttribute("type","warning");
        notification.setAttribute("priority",notificationBox.PRIORITY_WARNING_MEDIUM);
        notification.setAttribute("value",pref);
        notification.setAttribute("image","chrome://flagfox/content/icons/help.png");
        notification.setAttribute("label",message);

        var checkbox = window.document.createElementNS(XULNS,"checkbox");
        if (prefService.getCharPref(pref) == "once")  // If pref is "once", default to checked
        {
            checkbox.setAttribute("checked",true);
            prefService.setCharPref(pref,"disabled");
        }
        function onCheckboxToggled(event)
        {
            prefService.setCharPref(pref, event.target.checked ? "disabled" : "enabled");
        }
        checkbox.addEventListener("command",onCheckboxToggled,false);
        checkbox.setAttribute("label",Flagfox.strings.GetStringFromName("warnchecklabel"));
        notification.appendChild(checkbox);

        notification.setAttribute("persistence",100);  // Also give it a second of persistence to prevent accidental hide without user interaction
        window.setTimeout(function(){notification.removeAttribute("persistence");},1000);

        notificationBox.appendChild(notification);  // Add and show notification
        if (notificationBox._showNotification)
            notificationBox._showNotification(notification,true);  // Do slide animation (HACK: undocumented method...)

        // Fire event for accessibility APIs
        var event = window.document.createEvent("Events");
        event.initEvent("AlertActive", true, true);
        notification.dispatchEvent(event);
    },

    error : function(message,exception)  // This is more error info than others might do, but users have a bad habit of ignoring you if you don't ask for feedback
    {
        if (!message)
            message = "Unknown error!";

        Components.utils.reportError("Flagfox ERROR: " + message + " \n" + parseException(exception));

        try
        {
            var appInfo = Components.classes["@mozilla.org/xre/app-info;1"]
                                    .getService(Components.interfaces.nsIXULAppInfo)
                                    .QueryInterface(Components.interfaces.nsIXULRuntime);

            // No L10N: We only speak English (well) and thus our forums and the problems reported on them need to be in English. Sorry.
            var outputMsg = "Sorry, Flagfox has encountered a problem.  " +
                            "Please copy the report below and post it on our forums with a detailed explanation " +
                            "of what you were doing at the time so we can attempt to fix your issue.  (English please)\n";

            outputMsg += "\nFlagfox version " + Flagfox.getVersion() + " (" + Flagfox.getDatabaseAge() + ")\n";
            outputMsg += "\nERROR MESSAGE: " + message + "\n";
            if (exception)
            {
                outputMsg += "\nEXCEPTION THROWN: " + exception + "\n";
                if (exception.stack)
                    outputMsg += "\nSTACK TRACE: " + exception.stack;  // ends with "\n"
            }

            try { Components.utils.reportError("Flagfox PREFERENCES DUMP:\n" + dumpPrefs("flagfox.")); }
            catch (prefsDumpError) { outputMsg += "\nEXCEPTION THROWN on preferences dump: " + parseException(prefsDumpError) + "\n"; }

            outputMsg += "\nBROWSER: " + appInfo.vendor + " " + appInfo.name +  " " + appInfo.version + "/" + appInfo.appBuildID
                                       + " (Gecko " + appInfo.platformVersion + "/" + appInfo.platformBuildID + ")"
                                       + " using locale " + Flagfox.getUserAgentLocale()
                                       + " on " + appInfo.OS + " " + appInfo.XPCOMABI + "\n";
            outputMsg += "\nThis error and a Flagfox preferences dump has been sent to Tools -> Error Console. " +
                         "Please report this and any related errors so we can investigate your problem. " +
                         "Conversely, if you don't report this then it probably won't get fixed.\n";

            var prompts = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
                                    .getService(Components.interfaces.nsIPromptService);
            var flags = prompts.BUTTON_POS_0 * prompts.BUTTON_TITLE_OK +
                        prompts.BUTTON_POS_1 * prompts.BUTTON_TITLE_IS_STRING +
                        prompts.BUTTON_POS_0_DEFAULT;
            var button = prompts.confirmEx( null, "Flagfox Error!", outputMsg, flags, "", "Forums", "", null, {} );

            if (button == 1)  // "Forums" button
            {
                // Open forum in new tab (can't open new window; if error is on startup, we could hit another error)
                Flagfox.addTabInCurrentBrowser("http://flagfox.net/reportingbugs");
            }
        }
        catch (e) { Components.utils.reportError("EXCEPTION DURING FLAGFOX ERROR REPORTING: " + parseException(e)); }
    },

    openURL : function(window,url)  // Open URL in a window based on the user's pref
    {
        try
        {
            var openPref = prefService.getCharPref("flagfox.openlinksin");
            if (openPref == "tabBG" || openPref == "tabFG")
            {
                var newTab = window.getBrowser().addTab(url,null,null);
                if (openPref == "tabFG")
                    window.getBrowser().selectedTab = newTab;
            }
            else if (openPref == "currentTab")
            {
                window.content.document.location = url;
            }
            else  // "winBG" || "winFG"
            {
                var newWindow = window.open(url,"_blank");
                if (openPref == "winBG")
                {
                    newWindow.blur();
                    window.focus();
                }
            }
        } catch (e) { Flagfox.error("Failed to open URL: "+url,e); }
    },

    addTabInCurrentBrowser : function(url)  // Add tab to most recent window, regardless of where this function was called from
    {
        var currentWindow = Components.classes["@mozilla.org/appshell/window-mediator;1"]
                                      .getService(Components.interfaces.nsIWindowMediator)
                                      .getMostRecentWindow("navigator:browser");
        currentWindow.focus();
        var currentBrowser = currentWindow.getBrowser();
        currentBrowser.selectedTab = currentBrowser.addTab(url,null,null);
    },

    getVersion : function()  // Returns current Flagfox version number
    {
        return Components.classes["@mozilla.org/extensions/manager;1"]
                         .getService(Components.interfaces.nsIExtensionManager)
                         .getItemForID("{1018e4d6-728f-4b20-ad56-37578a4de76b}")
                         .version;
    },

    getDatabaseAge : function()  // Returns current Flagfox IPDB file age as YYYY-M
    {
        if (!ipdb.lastmodifiedtime)
            return "missing IPDB!";
        var date = new Date(ipdb.lastmodifiedtime);
        return date.getUTCFullYear() + "-" + (date.getUTCMonth()+1);  // JS months are 0-11, just to be confusing
    },

    getUserAgentLocale : function()  // Returns locale this browser is set to fetch content in
    {
        try { return prefService.getComplexValue("general.useragent.locale",Components.interfaces.nsIPrefLocalizedString).data; }
        catch (e) { return prefService.getCharPref("general.useragent.locale"); }
    }
};

//// Flag icon instance closure (one per window) ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
function newFlagInstance(window)
{
    var url = "init";          // The URL of the current page
    var urlIsPortable = true;  // Is the URL not on this computer?
    var host = "";             // The host name of the current URL
    var ip = "";               // The IP address of the current host
    var country = null;        // The country of the current IP address

    var icon = window.document.getElementById("flagfox-icon");
    var menu = window.document.getElementById("flagfox-contextmenu");
    if (!icon || !menu)
    {
        Components.utils.reportError("Flagfox warning: attempted to load into an invalid window");
        return;
    }

    try
    {
        updatePosition();  // Initial placement of flag icon
    }
    catch (e) { return; }

    var menuContentAge = 0;
    var DNSrequest = null;
    var prefListener = new PrefListener("flagfox.",onPrefChange);
    var pollLoopID = window.setInterval(pollWindowURL,250);

    // Go-go gadget events
    icon.addEventListener("click",onIconClick,false);
    icon.addEventListener("mousedown",onIconMouseDown,false);
    icon.addEventListener("mouseover",onIconHover,false);
    menu.addEventListener("command",onMenuCommand,false);
    menu.addEventListener("popupshowing",onMenuShowing,false);
    window.addEventListener("keypress",onKeyPressed,false);
    window.addEventListener("online",onChangedOnlineStatus,false);
    window.addEventListener("offline",onChangedOnlineStatus,false);
    window.addEventListener("unload",unload,false);

    function unload()
    {
        window.removeEventListener("unload",unload,false);
        window.removeEventListener("offline",onChangedOnlineStatus,false);
        window.removeEventListener("online",onChangedOnlineStatus,false);
        window.removeEventListener("keypress",onKeyPressed,false);
        menu.removeEventListener("popupshowing",onMenuShowing,false);
        menu.removeEventListener("command",onMenuCommand,false);
        icon.removeEventListener("mouseover",onIconHover,false);
        icon.removeEventListener("mousedown",onIconMouseDown,false);
        icon.removeEventListener("click",onIconClick,false);
        window.clearInterval(pollLoopID);
        DnsHandler.cancelRequest(DNSrequest);
        prefListener.unregister();
    }

    function pollWindowURL()  // Polling instead of event based to make sure it updates on every page (including error pages)
    {
        try
        {
            if (url != window.content.document.location.href)
                onChangedURL();
        }
        catch (e)
        {
            Components.utils.reportError("Flagfox EXCEPTION: " + parseException(e));
            //Flagfox.error("Error during poll loop!",e);
        }
    }

    function onChangedURL()
    {
        url = window.content.document.location.href;
        urlIsPortable = true;
        host = "";
        ip = "";
        country = null;

        // If we've changed pages before completing a lookup, then abort the old request first
        DnsHandler.cancelRequest(DNSrequest);
        DNSrequest = null;

        if (updateIcon())
            return;

        // Need to look up host
        try { host = window.content.document.location.hostname.cropTrailingChar("."); } catch (e) {}
        if (host == "")
        {
            icon.src = getIconPath("special/unknown");
            icon.tooltipText = Flagfox.strings.GetStringFromName("unknownsite");
            Components.utils.reportError("Flagfox warning: no host returned for \"" + url + "\"");
            return;
        }

        if (!window.navigator.onLine)
        {
            icon.src = getIconPath("special/offline");
            icon.tooltipText = host + " - " + Flagfox.strings.GetStringFromName("offlinemode");
            logMessage("Flagfox is in offline mode");
            return;  // Offline mode or otherwise not connected
        }

        if (DnsHandler.isProxiedDNS(url))
        {
            icon.src = getIconPath("special/anonymous");
            icon.tooltipText = host + " - " + Flagfox.strings.GetStringFromName("nodnserror");
            logMessage("Flagfox is in proxied mode");
            Flagfox.warning(window, "flagfox.warn.proxy", Flagfox.strings.GetStringFromName("proxywarnmessage"));
            return;  // Proxy in use for DNS; can't do a DNS lookup
        }

        // Idealy just hitting the DNS cache here
        DNSrequest = DnsHandler.resolveHost(host, onReturnedIP);

        function onReturnedIP(returnedIP)
        {
            DNSrequest = null;  // Request complete

            if (returnedIP == "FAIL")
            {
                icon.src = getIconPath("special/error");
                icon.tooltipText = host + " - " + Flagfox.strings.GetStringFromName("lookuperror");
                return;  // DNS lookup failed (ip & country stay empty)
            }

            ip = returnedIP;
            country = ipdb.lookupIP(ip);

            updateIcon();

            doTLDCheck();
        }
    }

    function doTLDCheck()  // Explain things to user if this IP is not in the same country as the domain's registration (common user confusion)
    {
        try
        {
            if (!country)
                return;

            var tld = host.truncateAfterLastChar(".");
            if (tld.length != 2)
                return;
            var tldCode = tld.toUpperCase();  // TLD tweaked to search in list

            switch (tldCode)         // Special TLD cases:
            {
                case "UK":
                    tldCode = "GB";  // List uses country code for Great Britan instead of United Kingdom
                    break;
                case "EU":           // Don't tell users European Union TLDs aren't in European countries
                    return;
                case "FM":           // Some countries have TLDs that are frequently sold for other uses as abbreviations or words
                case "TV":
                case "TO":
                case "ME":
                    return;
            }
            switch (country[0])      // Special IP range cases:
            {
                case "EU":           // Don't tell users European Union IPs aren't in European countries
                case "AP":           // Don't tell users Asia/Pacific IPs aren't in Asian countries
                    return;
            }

            var tldCountry = ipdb.countrynames.GetStringFromName(tldCode);  // Throws an exception if not found
            if (tldCountry.length && country[0] != tldCode)
            {
                Flagfox.warning(window, "flagfox.warn.tld", Flagfox.strings.formatStringFromName("tldwarnmessage", [country[1],"."+tld,tldCountry], 3));
            }
        } catch (e) {}
    }

    /* Update the flag icon state (icon & tooltip)
       Returns true if it's done and false if unknown */
    function updateIcon()
    {
        function getServerString()
        {
            if (host == ip)
                return ip;
            if (ip == "")
                return host;
            switch (icon.getAttribute("chromedir"))  // via global.dtd
            {
                default:
                case "ltr": return host + " (" + ip + ")";
                /* I'm forced to work around Mozilla's automatic RTL punctuation flipping bugs here.
                   "host (ip)" shows as "(host (ip". The following should show as "host (ip)" for all instances.
                   There is no way to consistently show "(ip) host" in RTL, as it tries to flip the word order. */
                case "rtl": return "(" + host + " (" + ip;
            }
        }

        switch (window.content.document.location.protocol)
        {
            case "file:":
                urlIsPortable = false;
                icon.src = getIconPath("special/localfile");
                icon.tooltipText = Flagfox.strings.GetStringFromName("localfile");
                return true;  // Done

            case "data:":
                icon.src = getIconPath("special/script");
                icon.tooltipText = Flagfox.strings.GetStringFromName("localfile") + " (" + url.truncateBeforeFirstChar(",") + ")";  // TODO: better string?
                return true;  // Done

            case "about:":  case "chrome:":  case "resource:":
                urlIsPortable = false;
                if (url == "about:blank")
                {   // Blank page gets its own icon and tooltip
                    icon.src = getIconPath("special/blank");
                    icon.tooltipText = Flagfox.strings.GetStringFromName("blankpage");
                }
                else
                {
                    icon.src = getIconPath("special/resource");
                    icon.tooltipText = Flagfox.strings.GetStringFromName("localfile") + " (" + window.content.document.location.protocol + "//)";
                }
                return true;  // Done

            case "view-source:":  // TODO: handle better
                urlIsPortable = false;
            default:
                if (host == "")
                    return false;  // Unknown host -> still need to look up
                if (!country)
                {
                    icon.src = getIconPath("special/unknown");  // Have a host (and ip) but no country -> unknown site
                    icon.tooltipText = getServerString() + " - " + Flagfox.strings.GetStringFromName("unknownsite");
                    return true;  // Done
                }
                switch (country[0])  // IP has been looked up
                {
                    case '-A':  case '-B':  case '-C':
                        urlIsPortable = false;
                        icon.src = getIconPath("special/privateip");
                        break;
                    case '-L':
                        urlIsPortable = false;
                        icon.src = getIconPath("special/localhost");
                        break;
                    case 'A1':  case 'A2':
                        icon.src = getIconPath("special/anonymous");
                        break;
                    default:
                        var set = prefService.getBoolPref("flagfox.usealticons") ? "flagset2/" : "flagset1/" ;
                        icon.src = getIconPath(set + country[0].toLowerCase());
                        break;
                }
                icon.tooltipText = Flagfox.strings.formatStringFromName( "tooltip", [getServerString(),country[1]], 2 );
                return true;  // Done
        }
    }

    function updatePosition()  // Update the position of the flag icon based on current prefs
    {
        try
        {
            function safeGetElementById(id)
            {
                var element = window.document.getElementById(id);
                if (!element)
                    throw Error("Element does not exist: " + id);
                return element;
            }
            function deleteElementById(id)
            {
                var node = window.document.getElementById(id);
                if (!node)
                    return;
                node.parentNode.removeChild(node);
                if (window.document.getElementById(id) != null)
                    throw Error("Failed to delete element: " + id);
            }
            function wrapElementWith(node,type,id)
            {
                var wrapper = window.document.createElement(type);
                wrapper.setAttribute("id",id);
                wrapper.appendChild(node);
                return wrapper;
            }

            /* Rather than detecting where the icon is and move it from there to its new location,
               I move it into a temporary location and then delete both possible previous wrappers.
               As a result, I can simply place the icon in the same generic way every time. */
            var iconBox = safeGetElementById("flagfox-iconbox");
            iconBox.hidden = true;
            safeGetElementById("main-window").appendChild(iconBox);
            if (!iconBox)
                throw Error("Lost flagfox-iconbox in main-window");

            // Cleanup any existing panels
            deleteElementById("flagfox-statusbarpanel");
            deleteElementById("flagfox-addressbarpanel");

            var bar, panel, nextSibling;
            var iconLoc = prefService.getCharPref("flagfox.position.bar");   // What bar is it in?
            var iconPos = prefService.getCharPref("flagfox.position.side");  // Where in that bar is it?
            switch (iconLoc)
            {
                case "statusbar":  // Positions reversed in right-to-left locales
                    bar = safeGetElementById("status-bar");
                    panel = wrapElementWith(iconBox,"statusbarpanel","flagfox-statusbarpanel");
                    if (icon.getAttribute("chromedir") == "rtl")  // via global.dtd
                    {
                        var flip = {};  // Need to flip positioning to match strings
                        flip["LM"]="RM"; flip["L"]="R"; flip["R"]="L"; flip["RM"]="LM";
                        iconPos = flip[iconPos];
                    }
                    switch (iconPos)
                    {
                        case "LM":
                            nextSibling = bar.firstChild;
                            break;
                        case "L":
                            nextSibling = window.document.getElementById("statusbar-display");
                            if (!nextSibling)
                                nextSibling = bar.firstChild;
                            break;
                        case "R":
                            var popBlockBtn = window.document.getElementById("page-report-button");
                            if (!popBlockBtn)
                                popBlockBtn = window.document.getElementById("popupIcon");  // SeaMonkey 2 support (button has different ID)
                            nextSibling = popBlockBtn ? popBlockBtn.nextSibling : null ;
                            break;
                        case "RM":
                            nextSibling = null;
                            break;
                    }
                    break;

                case "addressbar":  // Positions _NOT_ reversed in right-to-left locales
                    bar = safeGetElementById("urlbar-icons");
                    panel = wrapElementWith(iconBox,"box","flagfox-addressbarpanel");
                    switch (iconPos)
                    {
                        case "LM": case "L":       // REQUIRES FF3+
                            nextSibling = bar;     // Will place icon to left of address field
                            bar = bar.parentNode;  // Jump out of normal icon box
                            panel.style.marginLeft = "4px";
                            break;
                        case "R":
                            nextSibling = window.document.getElementById("star-button");  // If not FF3+ this'll be null => "RM"
                            if (nextSibling && nextSibling.parentNode.id != bar.id)
                                nextSibling = null;  // Flock 2 support ("star-button" is not in "urlbar-icons")
                            break;
                        case "RM":
                            nextSibling = null;
                            break;
                    }
                    break;
            }
            if (nextSibling === undefined)
                throw Error("Invalid position: " + iconLoc + "-" + iconPos);

            // Make the placement
            bar.insertBefore(panel,nextSibling);
            iconBox.hidden = false;
        }
        catch (e)
        {
            // This window probably just lacks an address bar or status bar; Flagfox can't work in this window
            if (e.message.substr(0,22) == "Element does not exist" && url == "init")
                Components.utils.reportError("Flagfox warning: attempted to load icon into an unsupported window");
            else
                Flagfox.error("Error setting icon position",e);
            throw null;  // Done error reporting; abort loading if needed
        }
    }

    function updateMenuContent()  // Update actions in context menu based on current prefs
    {
        if (menuContentAge == actionsListAge)  // Only generate if this window's menu is stale
            return;

        if (!Flagfox.actions)
            throw Error("Actions not loaded!");

        var showAllItems = (menuContentAge == -1);  // Set menu age to -1 to show everything at once, regardless of show setting

        var showFavicons = prefService.getBoolPref("flagfox.showfavicons");

        while (menu.firstChild)  // Clear previously generated menu, if one exists
            menu.removeChild(menu.firstChild);

        function newMenuItem(value,label)
        {
            var newElement = window.document.createElement("menuitem");
            newElement.setAttribute("value", value);
            newElement.setAttribute("label", label);
            menu.appendChild(newElement);
            return newElement;
        }

        function newMenuItemForAction(action,id)
        {
            if ( !(action.show || showAllItems) )
                return;

            var newElement = newMenuItem(id, Flagfox.getLocalizedNameForAction(action));

            if (showFavicons)
            {
                newElement.setAttribute("class", "menuitem-iconic");  // Allow icon
                newElement.setAttribute("validate", "never");  // Force usage of cache
                newElement.setAttribute("onerror", "this.image='chrome://mozapps/skin/places/defaultFavicon.png';");
                newElement.setAttribute("image", Flagfox.getFaviconForTemplate(action.template));
            }
        }

        // Generate actions list
        for (var i in Flagfox.actions)
            newMenuItemForAction(Flagfox.actions[i], i);

        menu.appendChild(window.document.createElement("menuseparator"));

        // Add "Options"
        newMenuItem("options", Flagfox.strings.GetStringFromName("options"));

        if (showAllItems)
            menuContentAge = 0;  // All were shown; reset for next open
        else
            menuContentAge = actionsListAge;  // Menu content synced to actions list
    }

    function isActionAllowed(id)  // Is the given action allowed for this current state?
    {
        if (id === undefined || id === null)  // The id may be 0
            return false;
        if (id == "options")
            return true;

        var action = Flagfox.actions[id];
        if (!action)
            throw Error("Invalid action: " + id);

        if (!action.template)
            return false;

        function needs(placeholder)
        {
            return action.template.search( new RegExp(placeholder,"i") ) != -1;  // This is case-insensitive
        }

        if (action.template.substr(0,11) != "copystring:")
        {
            if (!window.navigator.onLine)
                return false;
            if (!urlIsPortable)
            {
                if ( needs("{fullURL}") )  // Don't send local URLs to remote lookups
                    return false;
                if ( (host == ip || host == "localhost") && needs("{(IPaddress|(base)?domainName)}") )  // Don't send local IPs without hostnames to remote lookups
                    return false;
            }
        }
        if ( host == "" && needs("{(base)?domainName}") )
            return false;
        if ( ip == "" && needs("{IPaddress}") && !needs("{(base)?domainName}") )  // Allow optional IP when also using host (i.e. Geotool behind a proxy)
            return false;
        if ( country == null && needs("{country(Code|Name)}") )
            return false;
        // Locale placeholders that should always be valid

        // TODO: care about IPv6 support or lack thereof?
        return true;
    }

    function doAction(id)
    {
        if (!isActionAllowed(id))
            return;

        if (id == "options")
        {
            // Flags from Add-ons Manager + resizable; focus() after open to refocus already open window, if needed
            window.openDialog("chrome://flagfox/content/options.xul", "FlagfoxOptions", "chrome,titlebar,toolbar,centerscreen,resizable").focus();
            return;
        }

        var action = Flagfox.actions[id];
        if (!action)
            throw Error("Invalid action: " + id);

        var locale = Flagfox.getUserAgentLocale().toLowerCase();  // Strangely, Wikipedia "variant" is case-sensitive

        /* Both the full template and parameters need encoding but I can't do encodeURI() with the parameters as that ignores
           certain characters that might cause problems with searches. To prevent double encoding I do encodeURI() first and
           simply search using encoded placeholders. This is case-insensitive and placeholders may be used multiple times. */
        var parsedTemplate = encodeURI(action.template).replace(/%7BfullURL%7D/gi, encodeURIComponent(url))
                                                       .replace(/%7BdomainName%7D/gi, encodeURIComponent(host))
                                                       .replace(/%7BbaseDomainName%7D/gi, encodeURIComponent(getRootDomain(host)))
                                                       .replace(/%7BIPaddress%7D/gi, encodeURIComponent(ip))
                                                       .replace(/%7BcountryName%7D/gi, country ? encodeURIComponent(country[1]) : "")
                                                       .replace(/%7BcountryCode%7D/gi, country ? country[0] : "")
                                                       .replace(/%7Blocale%7D/gi, locale)
                                                       .replace(/%7BbaseLocale%7D/gi, locale.split('-')[0]);  // language-dialect -> language

        if (parsedTemplate.substr(0,11) == "copystring:")  // Copy to clipboard action uses an ad hoc pseudo-protocol
        {
            Components.classes["@mozilla.org/widget/clipboardhelper;1"]
                      .getService(Components.interfaces.nsIClipboardHelper)
                      .copyString( decodeURIComponent(parsedTemplate.slice(11)) );  // No encoding for copying (back/forth rather than duplicating code)
        }
        else  // Lookup URL action
        {
            Flagfox.openURL(window, parsedTemplate);
        }
    }

    function onPrefChange(branch,prefName)
    {
        switch (prefName)
        {
            case "position.bar":
            case "position.side":
                updatePosition();
                return;
            case "usealticons":
                onChangedURL();
                return;
        }
    }

    function onIconClick(event)
    {
        function doClickAction()
        {
            if (event.button == 1 || (event.button == 0 && event.ctrlKey))  // Middle or Left+Ctrl
                var binding = "middleclick";
            else if (event.button == 0)  // Left
                var binding = "click";
            else
                return;
            // Button 2 (Right) shows popup menu via context attribute

            // event.detail for click events is the number of successive clicks thus far
            if (event.detail == 2)
                binding = "double" + binding;
            else if (event.detail == 3)
                binding = "triple" + binding;

            doAction(hotClicks[binding]);
        }

        /* There is a dblclick event, but I can't use that because it's sent out in addition to two click events,
           not instead of. As a result, I just use the click event and detect multiple clicks within a short timeframe.
           (which also allows for triple click detection) The time has to be very short, otherwise when a user does a
           single click action it will still have to wait a while to see if there's going to be a second click. */
        window.clearTimeout(this.clicktimer);
        this.clicktimer = window.setTimeout(doClickAction, 250);
        // Double click = two clicks within 250ms; Triple click = three clicks within 500ms
    }

    function onIconMouseDown(event)  // Handle keyboard modifiers when right-clicking on the icon
    {
        if (event.button == 2 && event.ctrlKey)  // Right+Ctrl
            menuContentAge = -1;  // Show all items at once
    }

    function onIconHover(event)  // Changes mouseover cursor to a hand when there is a click action
    {
        icon.style.cursor = isActionAllowed(hotClicks["click"]) ? "pointer" : "default" ;
    }

    function onMenuCommand(event)
    {
        var action = event.target.value;
        doAction(action);
    }

    function onMenuShowing(event)
    {
        updateMenuContent();  // Update menu, if need be

        var menuItems = menu.getElementsByTagName("menuitem");
        for (var i=0; i < menuItems.length; i++)  // Decide which menu items to grey out if they aren't available
            menuItems[i].setAttribute("disabled", !isActionAllowed( menuItems[i].getAttribute("value") ));  // Need to use attributes here
    }

    /* Listening to every keypress here because dynamically adding to a <keyset> with <keys> being listened to doesn't seem to work well.
       This function only takes under a microsecond to run, so it shouldn't affect performance. */
    function onKeyPressed(event)
    {
        var bindings = hotKeys[event.charCode];  // event.charCode is case-sensitive; don't need to check for shift separately
        for (var i in bindings)
        {
            var binding = bindings[i];
            if (event.ctrlKey == binding.ctrlKey && event.altKey == binding.altKey && event.metaKey == binding.metaKey)
                doAction(binding.id);
        }
    }

    /* The "online" and "offline" events fire many times at once on the window object.
       To avoid redundant updates I just reset the URL and let it update on the next poll.
       In case the menu was opened offline it's reset too to make sure favicons are loaded. */
    function onChangedOnlineStatus(event)
    {
        url = "init";
        menuContentAge = 0;
    }
}

//// DNS handler (does lookups for IP addresses) ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
var DnsHandler =
{
    dns : null,      // DNS service
    proxy : null,    // Proxy service
    thread : null,   // This thread; onLookupComplete() must be called in this thread

    init : function()
    {
        this.dns = Components.classes["@mozilla.org/network/dns-service;1"]
                             .getService(Components.interfaces.nsIDNSService);
        this.proxy = Components.classes["@mozilla.org/network/protocol-proxy-service;1"]
                               .getService(Components.interfaces.nsIProtocolProxyService);
        this.thread = threadManager.currentThread;
        if (!this.thread)
            throw "Could not fetch thread for DNS handler";
    },

    isProxiedDNS : function(url)  // Returns true if the URL is set to have its DNS lookup proxied via SOCKS
    {
        var uri = ioService.newURI(url, null, null);
        var proxyinfo = this.proxy.resolve(uri, 0);  // Finds proxy (shouldn't block thread; we already did this lookup to load the page)
        return (proxyinfo != null) && (proxyinfo.flags & proxyinfo.TRANSPARENT_PROXY_RESOLVES_HOST);
        // "network.proxy.socks_remote_dns" pref must be set to true for Firefox to set TRANSPARENT_PROXY_RESOLVES_HOST flag when applicable
    },

    cancelRequest : function(request)
    {
        try { request.cancel(Components.results.NS_ERROR_ABORT); } catch(e) {}  // calls onLookupComplete() with status=Components.results.NS_ERROR_ABORT
    },

    resolveHost : function(host,returnIP)  // Returns request object
    {
        function fail(reason)
        {
            Components.utils.reportError("Flagfox warning: DNS lookup failure for \"" + host + "\": " + reason);
            returnIP("FAIL");
        }

        var callback =
        {
            onLookupComplete : function(nsrequest, nsrecord, status)
            {
                if (status == Components.results.NS_ERROR_ABORT)
                    return;  // Ignore cancel

                if (status != 0 || !nsrecord || !nsrecord.hasMore())
                {
                    fail( (status==Components.results.NS_ERROR_UNKNOWN_HOST) ? ("Unknown host") : ("status " + status) );
                    return;  // IP not found in DNS
                }

                returnIP(nsrecord.getNextAddrAsString());
            }
        };

        try
        {
            return this.dns.asyncResolve(host, 0, callback, this.thread);
        }
        catch (e)
        {
            fail( "exception " + ((e.name && e.name.length) ? e.name : e) );
            return null;
        }
    }
};

//// Utility functions //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

function getIconPath(filename)
{
    return "chrome://flagfox/content/icons/" + filename + ".png";
}

function logMessage(message)  // Adds an entry to the Error Console with "message" severity
{
    Components.classes["@mozilla.org/consoleservice;1"]
              .getService(Components.interfaces.nsIConsoleService)
              .logStringMessage(message);
}

/* Migration from old Flagfox versions:
     1) Imports old custom action from 3.3.x
     2) Imports old middle click option
     3) Wipes all obsolete Flagfox preferences (prefs that don't have defaults) */
function migrateOldPrefs()
{
    if (!Flagfox.actions)
        throw Error("Actions not loaded!");

    try  // Import flagfox.customlookup.* from Flagfox 3.3.x
    {
        var oldCustomAction = new Object();
        oldCustomAction.name = getUCharPref(prefService, "flagfox.customlookup.name");
        oldCustomAction.template = getUCharPref(prefService, "flagfox.customlookup.url");
        oldCustomAction.show = safeGetBoolPref("flagfox.customlookup.enabled");
        oldCustomAction.custom = true;
        Flagfox.actions.push(oldCustomAction);
        logMessage("Flagfox 3 custom action imported");
        Flagfox.saveActions();
    }
    catch (e) {}  // Throw -> pref doesn't exist (was default or already imported)

    try  // Import flagfox.middleclick
    {
        var oldClickPref = prefService.getCharPref("flagfox.middleclick");
        if (oldClickPref == "CopyIP")
            oldClickPref = "Copy IP";
        else if (oldClickPref == "Custom")
            oldClickPref = oldCustomAction.name;
        for (var i in Flagfox.actions)
            if (Flagfox.actions[i].name == oldClickPref)
            {
                Flagfox.setBindingsForAction(i, "middleclick", null);
                logMessage("Flagfox 3 middleclick option imported");
                Flagfox.saveActions();
                break;
            }
    }
    catch (e) {}  // Throw -> pref doesn't exist (was default or already imported)

    /* Both the user and default branches contain the exact same list of pref names,
       even if any default values don't exist or any user values equal the default. */
    var defaultBranch = prefService.getDefaultBranch("flagfox.");
    var userBranch = prefService.getBranch("flagfox.");
    var prefsList = userBranch.getChildList("",{});
    if (!prefsList.length)
        throw "Could not load Flagfox preferences list";
    for (var i in prefsList)
    {   // Exception means no default exists and this pref is not in use
        try { getGenericPref(defaultBranch,prefsList[i]); }
        catch (e) { userBranch.clearUserPref(prefsList[i]); }
    }
}

/* If the default actions list was updated recently, then those changes should be applied to the users' settings.
   There are three possibilities:
      a) Default action changed -> update it
      b) Default action removed -> delete it
      c) Default action added   -> add it */
function mergeDefaultActionUpdates()
{
    try
    {
        if (!Flagfox.actions)
            throw Error("Actions not loaded!");

        if (!prefService.prefHasUserValue("flagfox.actions"))  // If already using defaults then the new defaults will be used automatically
            return;

        var updatesDone = [];
        var json = Components.classes["@mozilla.org/dom/json;1"]
                             .createInstance(Components.interfaces.nsIJSON);
        var defaultActions = json.decode(getUCharPref(prefService.getDefaultBranch(""), "flagfox.actions"));

        function findDefaultActionByName(actionsList,name)
        {
            for (var i in actionsList)
                if (!actionsList[i].custom && actionsList[i].name == name)
                    return actionsList[i];
            return null;
        }

        if (!safeGetBoolPref("flagfox.hasshown4dot0firstrunpage"))  // Update beta testers to new default shortcut (TODO: remove after no longer needed)
        {
            for (var i in Flagfox.actions)
                if (!Flagfox.actions[i].custom && Flagfox.actions[i].name == "Geotool"
                    && Flagfox.actions[i].hotkey && Flagfox.actions[i].hotkey.key == "f" && Flagfox.actions[i].hotkey.mods == "ctrl shift")
                {
                    updatesDone.push("Geotool hotkey updated to new default");
                    Flagfox.actions[i].hotkey = {mods:"ctrl alt",key:"g"};
                }
        }

        for (var i=Flagfox.actions.length-1; i>=0; i--)  // Need to scan backwards in case of deletion and index change
            if (!Flagfox.actions[i].custom)
            {
                var action = findDefaultActionByName(defaultActions, Flagfox.actions[i].name);
                if (action)
                {
                    if (Flagfox.actions[i].template != action.template)  // Update to template
                    {
                        updatesDone.push("default action template update: " + Flagfox.actions[i].name);
                        Flagfox.actions[i].template = action.template;
                    }
                }
                else  // Old default action
                {
                    updatesDone.push("action is no longer a default: " + Flagfox.actions[i].name);
                    Flagfox.actions.splice(i,1);  // Delete action from main array
/*
                    Flagfox.actions[i].name = Flagfox.getLocalizedNameForAction(Flagfox.actions[i]);  // Save localization (none for custom)
                    Flagfox.actions[i].custom = true;  // Allow user to edit or delete
*/
                }
            }

        for (var i in defaultActions)
            if (!findDefaultActionByName(Flagfox.actions, defaultActions[i].name))  // New default action
            {
                updatesDone.push("new default action added: " + defaultActions[i].name);
                Flagfox.actions.splice(i,0,defaultActions[i]);  // TODO: Check existing shortcuts if I ever add a new default with a shortcut
            }

        if (updatesDone.length)
        {
            logMessage("Flagfox default action list updates applied for version " + Flagfox.getVersion() + ":\n" + updatesDone.join("\n"));
            Flagfox.saveActions();
        }
    }
    catch (e)
    {
        Flagfox.error("Error applying default actions list updates",e);
    }
}

/* Generic pref listener class
    Usage:
        function onPrefChange(branch,prefname)
        {
            switch (prefname)
            {
                case "prefname1":
                    break;
                case "prefname2":
                    break;
            }
        }
        var listener = new PrefListener("branchname.",onPrefChange);
*/
function PrefListener(branchName, onChanged)
{
    var branch = Components.classes["@mozilla.org/preferences-service;1"]
                           .getService(Components.interfaces.nsIPrefService)
                           .getBranch(branchName);
    branch.QueryInterface(Components.interfaces.nsIPrefBranch2);
    branch.addObserver("", this, false);

    this.unregister = function()
    {
        if (branch)
            branch.removeObserver("", this);
        branch = null;
    };

    this.observe = function(subject, topic, data)
    {
        if (topic == "nsPref:changed")
            onChanged(branch, data);
    };
}

function hashString(string)  // Returns a base-64 encoded MD5 hash of a Unicode string
{
    var converter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"]
                              .createInstance(Components.interfaces.nsIScriptableUnicodeConverter);
    converter.charset = "UTF-8";
    var bytes = converter.convertToByteArray(string,{});
    var cryptoHash = Components.classes["@mozilla.org/security/hash;1"]
                               .createInstance(Components.interfaces.nsICryptoHash);
    cryptoHash.init(cryptoHash.MD5);
    cryptoHash.update(bytes,bytes.length);
    return cryptoHash.finish(true);
}

function getRootDomain(hostName)
{
    try
    {
        return Components.classes["@mozilla.org/network/effective-tld-service;1"]
                         .getService(Components.interfaces.nsIEffectiveTLDService)
                         .getBaseDomainFromHost(hostName);
    }
    catch (e)
    {
        return hostName;  // Something wrong with host name or is IP address
    }
}

function parseException(e)  // Returns a string version of an exception object with its stack trace
{
    if (!e)
        return "";
    else if (!e.stack)
        return String(e);
    else
        return String(e) + " \n" + String(e.stack);
}

function dumpPrefs(branchname)
{
    var branch = prefService.getBranch(branchname);
    var prefList = branch.getChildList("",{});
    prefList.sort();
    var output = [];
    for (var i in prefList)
        output.push(branchname + prefList[i] + "=" + getGenericPref(branch,prefList[i]));
    if (!output.length)
        throw "Prefs list is EMPTY!"
    return output.join("\n");
}

function getGenericPref(branch,prefName)  // Get any pref from a branch, even if you don't know its type
{
    switch (branch.getPrefType(prefName))
    {
        case 0:   return "(INVALID!)";                   // PREF_INVALID
        case 32:  return getUCharPref(branch,prefName);  // PREF_STRING
        case 64:  return branch.getIntPref(prefName);    // PREF_INT
        case 128: return branch.getBoolPref(prefName);   // PREF_BOOL
    }
    throw Error("Bad pref type for: " + prefName);
}

function getUCharPref(branch,prefName)  // Unicode getCharPref
{
    return branch.getComplexValue(prefName, Components.interfaces.nsISupportsString).data;
}

function setUCharPref(branch,prefName,text)  // Unicode setCharPref
{
    var string = Components.classes["@mozilla.org/supports-string;1"]
                           .createInstance(Components.interfaces.nsISupportsString);
    string.data = text;
    branch.setComplexValue(prefName, Components.interfaces.nsISupportsString, string);
}

function safeGetBoolPref(pref)  // Returns bool pref value, or false if it does not exist
{
    try { return prefService.getBoolPref(pref); }
    catch (e) { return false; }
}

String.prototype.cropTrailingChar = function(character)  // This does NOT get exposed out of the scope of this JSM file
{
    return (this.charAt(this.length-1)==character) ? this.slice(0,this.length-1) : this.valueOf();
};

String.prototype.truncateAfterLastChar = function(character)  // This does NOT get exposed out of the scope of this JSM file
{
    return this.substring(this.lastIndexOf(character)+1);
};

String.prototype.truncateBeforeFirstChar = function(character)  // This does NOT get exposed out of the scope of this JSM file
{
    return this.substring(0,this.indexOf(character));
};
