/* 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 FlagfoxVersion = "4.0.x";  // Fetched on startup; value here is a fallback

var mainPrefListener = null;
var warningsThisSession = [];

var hotKeys;
var hotClicks;

var actionsListAge = 0;
var actionsSaveIsInProgress = false;

var loadStatus = 0;
var ready = false;

//// Initial startup sequence ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
function startup()
{
    try
    {
        switch (loadStatus)
        {
            default:
                return;

            // First startup
            case 0:
                loadStatus = 1;  // Started startup

                if (checkForAbsoluteIncompatability())  // Check if user has ignored the listed compatability range past its known breaking point
                {
                    loadStatus = -1;
                    return;  // No-go
                }

                const id = "{1018e4d6-728f-4b20-ad56-37578a4de76b}";  // Flagfox ID
                const filename = "db.dat";                            // IPDB filename

                if ("@mozilla.org/extensions/manager;1" in Components.classes)  // Gecko 1.9.x
                {
                    var ExtensionManager = Components.classes["@mozilla.org/extensions/manager;1"]
                                                     .getService(Components.interfaces.nsIExtensionManager);
                    FlagfoxVersion = ExtensionManager.getItemForID(id)
                                                     .version;
                    var file = ExtensionManager.getInstallLocation(id)
                                               .getItemLocation(id);
                    file.append(filename);
                    ipdb.load(file);
                    startup();  // Continue startup
                }
                else  // Gecko 2.0+
                {
                    Components.utils.import("resource://gre/modules/AddonManager.jsm");
                    AddonManager.getAddonByID(id, function(addon) {
                        try
                        {
                            FlagfoxVersion = addon.version;
                            var file = addon.getResourceURI(filename)  // After API change on 2010-6-22 (Mozilla bug 563262)
                                            .QueryInterface(Components.interfaces.nsIFileURL)
                                            .file;
                            ipdb.load(file);
                            startup();  // Continue startup
                        }
                        catch (e)  // This callback is outside of the exception catching for startup()
                        {
                            Flagfox.error("Fatal Flagfox startup error!",e);
                            loadStatus = -1;
                        }
                    });
                }
                return;

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

                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 Flagfox 4.0 so they can be informed of new features
                showPageOnceEver("chrome://flagfox/content/firstrun.xhtml", "flagfox.hasshown4dot0firstrunpage");

                /* Show a warning page to anyone still running Firefox 3.0.x which is EOL or Firefox 3.5.x which will soon be EOL.
                   Flagfox 4.1 drops support for Firefox 3.0.x and 3.5.x support will be dropped eventually too. Users need to update.
                   Unfortunately, Mozilla only has its big loud out-of-date security warning on the en-US versions of the first run pages.
                   This is dumb on the part of whomever implemented Mozilla's warning, but I don't see a better warning page to use instead.
                   Whatever... Flagfox has a translate action built in for the readers who need it. I'd rather have the big warning icon than a translation. */
                if (appInfo.ID == "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}" /*Firefox*/)
                {
                    if (versionComparator.compare(appInfo.version,"3.5") < 0 /*version<3.5 => version=3.0.x*/)
                        showPageOnceEver("http://www.mozilla.com/en-US/firefox/3.0/firstrun/", "flagfox.hasshownFirefoxEOLwarning");
                    else if (versionComparator.compare(appInfo.version,"3.6") < 0 /*version<3.6 => version=3.5.x*/)
                        showPageOnceEver("http://www.mozilla.com/en-US/firefox/3.5/firstrun/", "flagfox.hasshownFirefoxEOLwarning");
                }

                // 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(getCurrentWindow(), "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);

                loadStatus = 2;  // Loaded...
                ready = true;    // and ready to go
                return;
        }
    }
    catch (e)
    {
        Flagfox.error("Fatal Flagfox startup error!",e);
        loadStatus = -1;
    }
}

//// 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(window)
    {
        if (loadStatus == 0)  // Startup if not done yet
            startup();

        if (loadStatus == -1)  // Halt if startup was aborted
            return;

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

    loadActions : function()
    {
        Flagfox.actions = JSON.parse(getUCharPref(prefService, "flagfox.actions"));
        consoleService.logStringMessage("Flagfox actions loaded from JSON pref");

        Flagfox.refreshActions();
    },

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

        actionsSaveIsInProgress = true;
        try
        {
            setUCharPref(prefService, "flagfox.actions", JSON.stringify(Flagfox.actions));
            consoleService.logStringMessage("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 = {};
        hotClicks = {};

        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)
            {
                function hasMod(mod) { return hotkey.mods.indexOf(mod)!=-1; }
                var charCode = (hasMod("shift") ? hotkey.key.toUpperCase() : hotkey.key.toLowerCase()).charCodeAt(0);
                if (!hotKeys[charCode])
                    hotKeys[charCode] = {};
                hotKeys[charCode][getModsCode(hasMod("ctrl"),hasMod("alt"),hasMod("meta"))] = i;
            }
            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 == "geo.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)
                return Flagfox.strings.GetStringFromName( "action." + action.name.replace(/ /g,"_").toLowerCase() );
        } 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
        {
            // 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, the Flagfox extension has encountered a problem. " +
                            "The following error output and a Flagfox preferences dump has been sent to Tools -> Error Console.\n" +
                            "\n--------------------------------------------------\n";

            outputMsg += "FLAGFOX VERSION: " + Flagfox.version + " (" + Flagfox.getIPDBversion() + ")\n";
            outputMsg += "\nERROR MESSAGE: " + message + "\n";
            if (exception)
            {
                outputMsg += "\nEXCEPTION THROWN: " + exception + "\n";
                if (exception.stack)
                    outputMsg += "\nSTACK TRACE: " + cleanExceptionStack(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;

            outputMsg += "\n--------------------------------------------------\n" +
                         "\nSelect and copy the error report above. In order to fix this problem for you and others, please read and follow the " +
                         "troubleshooting and bug reporting instructions on the Flagfox support forums. Please post an abundance of information with any " +
                         "error reports, namely what you were doing at the time that may have triggered this. (English please)\n";

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

            if (button == 0)  // "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 browser = window.getBrowser();
                try { window.TreeStyleTabService.readyToOpenChildTab(browser.selectedTab); } catch (e) {}  // Support for Tree Style Tab extension
                if (versionComparator.compare(appInfo.platformVersion,"1.9.2") >= 0)
                    var newTab = browser.addTab(url, {ownerTab:browser.selectedTab, relatedToCurrent:true});  // Add tab as child in Firefox 3.6+
                else
                    var newTab = browser.addTab(url, null, null, null, browser.selectedTab);  // Firefox 3.0 - 3.5 (TODO: drop with dropped Firefox 3.0/3.5 support)
                if (openPref == "tabFG")
                    browser.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 = getCurrentWindow();
        currentWindow.focus();
        var currentBrowser = currentWindow.getBrowser();
        currentBrowser.selectedTab = currentBrowser.addTab(url,null,null);
    },

    getIPDBversion : function()  // Returns current Flagfox IPDB file modification date 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"); }
    },

    get version() { return FlagfoxVersion; }
};

//// 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()
    {
        if (!ready)
            return;  // Startup not finished; wait until it is before proceeding

        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");
            consoleService.logStringMessage("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");
            consoleService.logStringMessage("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":
                case "LY":
                    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
                        iconPos = { LM:"RM", L:"R", R:"L", RM:"LM" }[iconPos];  // Need to flip positioning to match strings
                    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 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);

        function getParameterValue(parameter)
        {
            switch (parameter.slice(3,-3).toLowerCase())
            {
                case "fullurl":  // Some templates will need the URL variable to be encoded and others will need it to not be
                    var urlParameterPos = action.template.search(/\{fullURL\}/i);
                    var charBeforeURL = action.template[urlParameterPos-1];
                    if (charBeforeURL == '=' || charBeforeURL == ':')
                        return encodeURIComponent(url);
                    return url;

                case "basedomainname":
                    try {
                        return encodeURIComponent(Components.classes["@mozilla.org/network/effective-tld-service;1"]
                                                            .getService(Components.interfaces.nsIEffectiveTLDService)
                                                            .getBaseDomainFromHost(host));
                    } catch (e) {}  // Throws if something is wrong with host name or is IP address; fall-through and use full host name

                case "domainname":
                    return encodeURIComponent(host);

                case "ipaddress":
                    return encodeURIComponent(ip);

                case "countrycode":
                    return country[0];

                case "countryname":
                    return encodeURIComponent(country[1]);

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

                case "baselocale":
                    return Flagfox.getUserAgentLocale().split('-')[0];  // language-dialect -> language

                default:
                    return parameter;  // Don't know what it is; leave it alone
            }
        }

        /* 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(/%7B[^%]+%7D/g, getParameterValue);

        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
        {
            if (action.template.substr(0,23) == "http://geo.flagfox.net/")  // Identify this Flagfox version to Geotool for abuse prevention purposes
                setGeotoolCookie();
            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 around a microsecond or less to run so it shouldn't affect performance.
       event.charCode is case-sensitive so I don't need to check for shift separately. */
    function onKeyPressed(event)
    {
        var boundKey = hotKeys[event.charCode];
        if (boundKey)
            doAction( boundKey[getModsCode(event.ctrlKey,event.altKey,event.metaKey)] );
    }

    /* 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 =
{
    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 = proxyService.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 dnsService.asyncResolve(host, 0, callback, threadManager.currentThread);
        }
        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";
}

// Automatically generate and cache IPDB hash on-demand
defineLazyGetter("IPDBhash", function() {
    return ipdb.generateQuickHash(3);
});

/* Set secret decoder ring for Geotool to try to reduce crippling server abuse from other sources.
   This gives up-to-date Flagfox users an all-access pass and restricts everyone else via a captcha at certain times.
   This does not, however, allow for infinite requests. Geotool will still auto-block after many excessive requests.
   This only identifies the Flagfox version. All users on all systems will get the same cookie for the same Flagfox version.
   No information that would identify this computer, profile, or user is sent and it is only sent to the Geotool server. */
function setGeotoolCookie()
{
    const expiry = (Date.now()/1000) + 600;  // Set 10 minute expiration time (automatically reset as needed on each call)
    const values = [
        ["Flagfox-version", FlagfoxVersion],                // Flagfox extension version string
        ["Flagfox-IPDBversion", Flagfox.getIPDBversion()],  // Flagfox IP location database version string (year and month)
        ["Flagfox-IPDBhash", IPDBhash]                      // 6 char base-36 quick hash of this version's IP location database file (used to verify versions)
    ];
    values.forEach(function(value) {
        cookieManager.add("geo.flagfox.net","/",value[0],value[1],false,true,false,expiry);  // HttpOnly mode on: only accessible by Geotool server, not client scripts
    });
}

/* Migration from old Flagfox versions:
     1) Imports old custom action from Flagfox 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);
        consoleService.logStringMessage("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);
                consoleService.logStringMessage("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 defaultActions = JSON.parse(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;
        }

        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)
            {
                if (Flagfox.actions[i].show && Flagfox.actions[i].name == "tr.im URL")  // tr.im died; replace with bit.ly as new default
                {
                    updatesDone.push("default action \"bit.ly URL\" replaces now defunct \"tr.im URL\" in default menu");
                    try { findDefaultActionByName(Flagfox.actions,"bit.ly URL").show = true; } catch (e) {}
                }  // tr.im will be deleted down below

                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
                }
            }

        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)
        {
            consoleService.logStringMessage("Flagfox default action list updates applied for version " + Flagfox.version + ":\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 = prefService.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 getModsCode(ctrl,alt,meta)  // Convert boolean triplet into an integer
{
    var code = 0;
    if (ctrl)
        code |= 1;
    if (alt)
        code |= 2;
    if (meta)
        code |= 4;
    return code;
}

function getCurrentWindow()
{
    return Components.classes["@mozilla.org/appshell/window-mediator;1"]
                     .getService(Components.interfaces.nsIWindowMediator)
                     .getMostRecentWindow("navigator:browser");
}

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" + cleanExceptionStack(e.stack);
}

/* For some stupid reason any resource:// URLs are converted into full file:// URL paths in exception stacks.
   This function converts them back to make the stack shorter and more readable.
   TODO: Someday use chrome:// loaded JSM and not need this (Gecko 2.0+ only) */
function cleanExceptionStack(stack)
{
    try
    {
        const shortPath = "resource://flagfox/";
        const longPath = ioService.newChannel(shortPath,null,null).URI.spec;
        return stack.replace(new RegExp(longPath,"ig"), shortPath);
    }
    catch (e)
    {
        return stack;
    }
}

/* The install.rdf file contains the listed application compatibility ranges which each addon supports. Remote support version bumps can be done via AMO.
   As newly developed versions of applications get released, some users disable compatibility checking and force installation before the support is updated.
   Often that works fine; sometimes it'll cause errors. Sadly, not all users can figure this out anymore, even many Minefield users.
   This function checks to make sure we're running inside the hard known compatibility range and tells the user essentially
   "I told you so" if it's completely incompatible. This is preferable to an error message that they won't understand. */
function checkForAbsoluteIncompatability()
{
    // Hard minimum of Gecko 1.9.0 for Flagfox 4.0.x; JavaScript module support needed (so... this won't even run if it's older)
    const MAX_GECKO_VER = "2.0b6";  // Hard maximum of Gecko 2.0b6 for Flagfox 4.0.x; extractionless XPI installation broke compatibility -> Needs Flagfox 4.1

    if (versionComparator.compare(appInfo.platformVersion,MAX_GECKO_VER) > 0)  // Too new to work at all
    {
        Components.utils.reportError("Flagfox INCOMPATIBILITY ERROR: Gecko version " + appInfo.platformVersion + " NOT supported!");

        var title = "Flagfox 4.0.x is NOT compatible with " + appInfo.vendor + " " + appInfo.name + " " + appInfo.version + "!";
        var msg = "You have forced the installation/running of Flagfox 4.0.x by overriding normal compatibility checking. This doesn't always work well and " +
                  "this time it didn't. Flagfox 4.0.x is NOT compatible with the Mozilla code-base this application is running: Gecko " + appInfo.platformVersion +
                  "\n\nYou need to install a newer Flagfox version that supports your application." +
                  "\n\nClose this dialog to go to the Flagfox versions list page on Mozilla's Add-ons site and install " +
                  "the latest Flagfox version with added compatibility for newer Firefox and SeaMonkey versions.";
        var prompts = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
                                .getService(Components.interfaces.nsIPromptService);
        prompts.alert(null,title,msg);
        Flagfox.addTabInCurrentBrowser("https://addons.mozilla.org/addon/5791/versions/");  // URL redirects to application and locale specific version
        return true;  // Ain't gonna work
    }

    return false;  // Normal compatibility checking will be used; proceed as normal
}

function showPageOnceEver(url,pref)  // Will show the given URL in a new tab, only once ever, and use the given pref to remember
{
    if (!safeGetBoolPref(pref))
    {
        getCurrentWindow().setTimeout(function(){
            Flagfox.addTabInCurrentBrowser(url);
            prefService.setBoolPref(pref,true);
        }, 1000);  // Need a delay to make sure it's shown after an update under Firefox 3.6+ for some reason
    }
}

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));
};

//// Services (each fetched once on first use) //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

function defineLazyGetter(getterName, getterFunction)
{
    this.__defineGetter__(getterName, function() {
        delete this[getterName];
        return this[getterName] = getterFunction.apply(this);
    });
}

defineLazyGetter("prefService", function() {
    return Components.classes["@mozilla.org/preferences-service;1"]
                     .getService(Components.interfaces.nsIPrefBranch)
                     .QueryInterface(Components.interfaces.nsIPrefService);
});
defineLazyGetter("ioService", function() {
    return Components.classes["@mozilla.org/network/io-service;1"]
                     .getService(Components.interfaces.nsIIOService);
});
defineLazyGetter("threadManager", function() {
    return Components.classes["@mozilla.org/thread-manager;1"]
                     .getService(Components.interfaces.nsIThreadManager);
});
defineLazyGetter("dnsService", function() {
    return Components.classes["@mozilla.org/network/dns-service;1"]
                     .getService(Components.interfaces.nsIDNSService);
});
defineLazyGetter("proxyService", function() {
    return Components.classes["@mozilla.org/network/protocol-proxy-service;1"]
                     .getService(Components.interfaces.nsIProtocolProxyService);
});
defineLazyGetter("consoleService", function() {
    return Components.classes["@mozilla.org/consoleservice;1"]
                     .getService(Components.interfaces.nsIConsoleService);
});
defineLazyGetter("cookieManager", function() {
    return Components.classes["@mozilla.org/cookiemanager;1"]
                     .getService(Components.interfaces.nsICookieManager2);
});
defineLazyGetter("appInfo", function() {
    return Components.classes["@mozilla.org/xre/app-info;1"]
                     .getService(Components.interfaces.nsIXULAppInfo)
                     .QueryInterface(Components.interfaces.nsIXULRuntime);
});
defineLazyGetter("versionComparator", function() {
    return Components.classes["@mozilla.org/xpcom/version-comparator;1"]
                     .getService(Components.interfaces.nsIVersionComparator);
});

if (typeof JSON == "undefined")  // Add support for ES5 native JSON API under Gecko 1.9.0
{
    defineLazyGetter("JSON", function() {
        let nsIJSON = Components.classes["@mozilla.org/dom/json;1"]
                                .createInstance(Components.interfaces.nsIJSON);
        return { parse : nsIJSON.decode, stringify : nsIJSON.encode };
    });
}
