// Copyright (c) 2009-2015 Kris Maglione <maglione.k@gmail.com>
//
// This work is licensed for reuse under an MIT license. Details are
// given in the LICENSE.txt file included with this file.
"use strict";

var { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;

var Cs = new Proxy(Components.stack, {
    get: function Cs_get(target, prop) {
        return Components.stack.caller[prop];
    }
});

function module(url) {
    let obj = {};
    Cu.import(url, obj);
    return obj;
}

var { XPCOMUtils } = module("resource://gre/modules/XPCOMUtils.jsm");
var { OS } = module("resource://gre/modules/osfile.jsm");
try {
    var { ctypes } = module("resource://gre/modules/ctypes.jsm");
}
catch (e) {}

let objproto = Object.prototype;
var { __lookupGetter__, __lookupSetter__, __defineGetter__, __defineSetter__,
      hasOwnProperty, propertyIsEnumerable } = objproto;

var hasOwnProp = Function.call.bind(hasOwnProperty);

hasOwnProperty = hasOwnProp;
propertyIsEnumerable = Function.call.bind(propertyIsEnumerable);

function require(module_, target) {
    if (/^[A-Za-z0-9]+:/.test(module_))
        return module(module_);
    return JSMLoader.load(module_, target);
}

function lazyRequire(module, names, target) {
    for (let name of names)
        memoize(target || this, name, name => require(module)[name]);
}

var jsmodules = { lazyRequire: lazyRequire };
jsmodules.jsmodules = jsmodules;

function toString() {
    return "[module-global " + this.NAME + "]";
}

function objToString(obj) {
    try {
        return objproto.toString.call(obj);
    }
    catch (e) {
        var type_ = typeof obj;
        if (e == null)
            type_ = String(e);
        return "[non-object " + type_ + "]";
    }
}

var use = {};
var loaded = {};
var currentModule;
var global = this;
function defineModule(name, params, module) {
    if (!module)
        module = this;

    module.NAME = name;
    module.EXPORTED_SYMBOLS = params.exports || [];
    if (!~module.EXPORTED_SYMBOLS.indexOf("File"))
        delete module.File;

    defineModule.loadLog.push("[Begin " + name + "]");
    defineModule.prefix += "  ";

    for (let mod of (params.require || []))
        require(mod, module);

    module._lastModule = currentModule;
    currentModule = module;
    module.startTime = Date.now();
}

defineModule.loadLog = [];
Object.defineProperty(defineModule.loadLog, "push", {
    value: function (val) {
        val = defineModule.prefix + val;
        if (true)
            defineModule.dump(val + "\n");
        this[this.length] = Date.now() + " " + val;
    }
});
defineModule.prefix = "";
defineModule.dump = function dump_(...args) {
    try {
        let msg = args.map(function (msg) {
            if (loaded.util && typeof msg == "object")
                msg = util.objectToString(msg);
            return msg;
        }).join(", ");
        dump(String.replace(msg, /\n?$/, "\n")
                   .replace(/^./gm, JSMLoader.name + ": $&"));
    }
    catch (e) {
        let msg = "Error dumping object: " + e + "\n" + (e && e.stack || Error.stack);
        dump(String.replace(msg, /\n?$/, "\n")
                   .replace(/^./gm, JSMLoader.name + ": $&"));
    }
};
defineModule.modules = [];
defineModule.time = function time(major, minor, func, self, ...args) {
    let time = Date.now();
    if (typeof func !== "function")
        func = self[func];

    try {
        var res = func.apply(self, args);
    }
    catch (e) {
        loaded.util && util.reportError(e);
    }

    JSMLoader.times.add(major, minor, Date.now() - time);
    return res;
};

function endModule() {
    defineModule.prefix = defineModule.prefix.slice(0, -2);
    defineModule.loadLog.push("(End   " + currentModule.NAME + ")");

    loaded[currentModule.NAME] = 1;
    require(currentModule.NAME, jsmodules);
    currentModule = currentModule._lastModule;
}

function require_(obj, name, from, targetName) {
    try {
        if (arguments.length === 1)
            [obj, name] = [{}, obj];

        let caller = Components.stack.caller;

        if (!loaded[name])
            defineModule.loadLog.push((from || "require") + ": loading " + name +
                                      " into " + (targetName || obj.NAME || caller.filename + ":" + caller.lineNumber));

        JSMLoader.load(name + ".jsm", obj);

        if (!loaded[name] && obj != jsmodules)
            JSMLoader.load(name + ".jsm", jsmodules);

        return obj;
    }
    catch (e) {
        defineModule.dump("loading " + JSON.stringify(name + ".jsm") + "\n");
        if (loaded.util)
            util.reportError(e);
        else
            defineModule.dump("    " + (e.filename || e.fileName) + ":" + e.lineNumber + ": " + e + "\n");
    }
}

defineModule("base", {
    // sed -n 's/^(const|var|function) ([a-zA-Z0-9_]+).*/	"\2",/p' base.jsm | sort | fmt
    exports: [
        "Ary",
        "Cc",
        "Ci",
        "Class",
        "Cr",
        "Cs",
        "Cu",
        "ErrorBase",
        "Finished",
        "JSMLoader",
        "Module",
        "OS",
        "RealSet",
        "Set",
        "Struct",
        "StructBase",
        "TextEncoder",
        "TextDecoder",
        "Timer",
        "UTF8",
        "XPCOM",
        "XPCOMShim",
        "XPCOMUtils",
        "apply",
        "array",
        "bind",
        "call",
        "callable",
        "ctypes",
        "curry",
        "defineModule",
        "deprecated",
        "endModule",
        "hasOwnProperty",
        "hasOwnProp",
        "identity",
        "isArray",
        "isGenerator",
        "isObject",
        "isString",
        "isSubclass",
        "isinstance",
        "iter",
        "iterOwnProperties",
        "keys",
        "literal",
        "loadPolyfill",
        "memoize",
        "module",
        "octal",
        "properties",
        "require",
        "set",
        "update",
        "values",
    ]
});

function loadPolyfill(obj) {
    JSMLoader.loadSubScript("resource://dactyl/polyfill.jsm",
                            obj);
}
loadPolyfill(this);

this.lazyRequire("cache", ["cache"]);
this.lazyRequire("config", ["config"]);
this.lazyRequire("messages", ["_", "Messages"]);
this.lazyRequire("promises", ["Task", "promises"]);
this.lazyRequire("services", ["services"]);
this.lazyRequire("storage", ["File"]);
this.lazyRequire("util", ["FailedAssertion", "util"]);

let literal_ = function literal(comment) {
    if (comment)
        return /^function.*?\/\*([^]*)\*\/(?:\/\* use strict \*\/)\s*\S$/.exec(comment)[1];

    let { caller } = Components.stack;
    while (caller && caller.language != 2)
        caller = caller.caller;

    // Immediate caller is the `deprecate` helper.
    caller = caller.caller;

    let file = caller.filename.replace(/.* -> /, "");
    let key = "literal:" + file + ":" + caller.lineNumber;
    return cache.get(key, function() {
        let source = literal.files[file] || File.readURL(file);
        literal.files[file] = source;

        let match = RegExp("(?:.*\\n){" + (caller.lineNumber - 1) + "}" +
                           ".*literal\\(/\\*([^]*?)\\*/\\)").exec(source);
        return match[1];
    });
};
literal_.files = {};
literal_.locations = {};

// This needs to happen after `files` and `locations` have been defined
// as properties of the real `literal` function.
var literal = deprecated("template strings", literal_);
literal.files = literal_.files;
literal.locations = literal_.locations;

function apply(obj, meth, args) {
    // The function's own apply method breaks in strange ways
    // when using CPOWs.
    if (callable(meth))
        return Function.apply.call(meth, obj, args);
    return Function.apply.call(obj[meth], obj, args);
}

/**
 * Iterates over the names of all of the top-level properties of an
 * object or, if prototypes is given, all of the properties in the
 * prototype chain below the top.
 *
 * @param {object} obj The object to inspect.
 * @param {boolean} properties Whether to inspect the prototype chain
 * @default false
 * @returns {Generator}
 */
function prototype(obj) {
    return obj.__proto__ ||
           Object.getPrototypeOf(obj) ||
           XPCNativeWrapper.unwrap(obj).__proto__ ||
           Object.getPrototypeOf(XPCNativeWrapper.unwrap(obj));
}

function* properties(obj, prototypes) {
    let orig = obj;
    let seen = new RealSet(["dactylPropertyNames"]);

    try {
        if ("dactylPropertyNames" in obj && !prototypes)
            for (let key of obj.dactylPropertyNames)
                if (key in obj && !seen.add(key))
                    yield key;
    }
    catch (e) {}

    function props(obj) {
        // Grr.
        try {
            return Object.getOwnPropertyNames(obj);
        }
        catch (e) {
            if (e.result === Cr.NS_ERROR_FAILURE) {
                // This is being thrown for PDF.js content windows,
                // currently.
                let filter = function filter(prop) {
                    try {
                        return prop in obj;
                    }
                    catch (e) {
                        util.reportError("Filtering properties for " +
                                         JSON.stringify(obj) + ", " +
                                         "error checking presence of " +
                                         JSON.stringify(prop) + ": " + e);
                    }
                    return false;
                };

                let keys_ = function* (obj) {
                    for (let key in object)
                        yield key;
                };

                return [...new RealSet(
                    [...keys_(obj),
                     ...Object.getOwnPropertyNames(Cu.waiveXrays(obj))
                              .filter(filter)])
                ];
            }
            else if (!e.stack) {
                throw Error(e);
            }
            throw e;
        }
    }

    for (; obj; obj = prototypes && prototype(obj)) {
        for (let key of props(obj))
            if (!prototypes || !seen.add(key) && obj != orig)
                yield key;
    }
}

function* iterOwnProperties(obj) {
    for (let prop of properties(obj))
        yield [prop, Object.getOwnPropertyDescriptor(obj, prop)];
}

function deprecated(alternative, fn) {
    if (isObject(fn))
        return Class.Property(iter(fn).map(([k, v]) => [k,
                                                        callable(v) ? deprecated(alternative, v)
                                                                    : v])
                                      .toObject());

    let name,
        func = callable(fn) ? fn
                            : function () { return apply(this, fn, arguments); };

    function deprecatedMethod() {
        let obj = !this                      ? "" :
                  this.className             ? this.className + "#" :
                  this.constructor.className ? this.constructor.className + "#" :
                      "";

        deprecated.warn(func,
                        obj + (fn.realName || fn.name || name || "").replace(/__/g, "."),
                        alternative);
        return func.apply(this, arguments);
    }
    if (func.name)
        deprecatedMethod.realName = func.name;

    return callable(fn) ? deprecatedMethod : Class.Property({
        get: function () { return deprecatedMethod; },
        init: function (prop) { name = prop; }
    });
}
deprecated.warn = function warn(func, name, alternative, frame) {
    if (!func.seenCaller)
        func.seenCaller = new RealSet([
            "resource://dactyl/javascript.jsm",
            "resource://dactyl/util.jsm"
        ]);

    if (!(loaded.util && util && loaded.config && config.protocolLoaded)) {
        dump("DACTYL: deprecated method called too early [" + [name, alternative] + "]:\n" + Error().stack + "\n\n");
        return;
    }

    frame = frame || Components.stack.caller.caller;

    let filename = util.fixURI(frame.filename || "unknown");
    if (!func.seenCaller.add(filename))
        util.dactyl(func).warn([util.urlPath(filename), frame.lineNumber, " "].join(":")
                                   + _("warn.deprecated", name, alternative));
};

/**
 * Iterates over all of the top-level, iterable property names of an
 * object.
 *
 * @param {object} obj The object to inspect.
 * @returns {Iter}
 */
function keys(obj) {
    if (isinstance(obj, ["Map"]))
        return iter(obj.keys());

    return iter(function* () {
        for (let k in obj)
            if (hasOwnProp(obj, k))
                yield k;
    }());
}

/**
 * Iterates over all of the top-level, iterable property values of an
 * object.
 *
 * @param {object} obj The object to inspect.
 * @returns {Iter}
 */
function values(obj) {
    if (isinstance(obj, ["Map"]))
        return iter(obj.values());

    if (isinstance(obj, ["Generator", "Iterator", Iter]))
        return iter(obj);

    if (Symbol.iterator in obj)
        return iter(obj[Symbol.iterator]());

    return iter(function* () {
        for (let k in obj)
            if (hasOwnProp(obj, k))
                yield obj[k];
    }());
}

var RealSet = Set;
let Set_add = RealSet.prototype.add;

Object.defineProperty(RealSet.prototype, "add", {
    configurable: true,
    writable: true,
    value: function RealSet_add(val) {
        let res = this.has(val);
        Set_add.apply(this, arguments);
        return res;
    }
});

Object.defineProperty(RealSet.prototype, "difference", {
    configurable: true,
    writable: true,
    value: function RealSet_difference(set) {
        return new RealSet(Array.from(this).filter(i => !set.has(i)));
    }
});

Object.defineProperty(RealSet.prototype, "intersection", {
    configurable: true,
    writable: true,
    value: function RealSet_intersection(set) {
        return new RealSet(Array.from(this).filter(i => set.has(i)));
    }
});

Object.defineProperty(RealSet.prototype, "union", {
    configurable: true,
    writable: true,
    value: function RealSet_union(set) {
        let res = new RealSet(this);
        for (let item of set)
            res.add(item);
        return res;
    }
});

/**
 * Utility for managing sets of strings. Given an array, returns an
 * object with one key for each value thereof.
 *
 * @param {[string]} ary @optional
 * @returns {object}
 */
this.Set = deprecated("RealSet", function Set(ary) {
    let obj = {};
    if (ary)
        for (let val of ary)
            obj[val] = true;
    return obj;
});
/**
 * Adds an element to a set and returns true if the element was
 * previously contained.
 *
 * @param {object} set The set.
 * @param {string} key The key to add.
 * @returns boolean
 */
Set.add = deprecated("RealSet#add",
    curry(function Set__add(set, key) {
        if (isinstance(set, ["Set"])) {
            let res = set.has(key);
            set.add(key);
            return res;
        }

        let res = this.has(set, key);
        set[key] = true;
        return res;
    }));
/**
 * Returns true if the given set contains the given key.
 *
 * @param {object} set The set.
 * @param {string} key The key to check.
 * @returns {boolean}
 */
Set.has = deprecated("hasOwnProperty or Set#has",
    curry(function Set__has(set, key) {
        if (isinstance(set, ["Set"]))
            return set.has(key);

        return hasOwnProp(set, key) &&
               propertyIsEnumerable(set, key);
    }));
/**
 * Returns a new set containing the members of the first argument which
 * do not exist in any of the other given arguments.
 *
 * @param {object} set The set.
 * @returns {object}
 */
Set.subtract = deprecated("RealSet#difference",
    function set_subtract(set) {
        set = update({}, set);
        for (let i = 1; i < arguments.length; i++)
            for (let k of keys(arguments[i]))
                delete set[k];
        return set;
    });

/**
 * Removes an element from a set and returns true if the element was
 * previously contained.
 *
 * @param {object} set The set.
 * @param {string} key The key to remove.
 * @returns boolean
 */
Set.remove = deprecated("RealSet#delete",
    curry(function Set__remove(set, key) {
        if (isinstance(set, ["Set"]))
            return set.delete(key);

        let res = set.has(set, key);
        delete set[key];
        return res;
    }));

function set() {
    deprecated.warn(set, "set", "Set");
    return Set.apply(this, arguments);
}
Object.keys(Set).forEach(function (meth) {
    set[meth] = function proxy() {
        deprecated.warn(proxy, "set." + meth, "Set." + meth);
        return apply(Set, meth, arguments);
    };
});

/**
 * Curries a function to the given number of arguments. Each
 * call of the resulting function returns a new function. When
 * a call does not contain enough arguments to satisfy the
 * required number, the resulting function is another curried
 * function with previous arguments accumulated.
 *
 *     function foo(a, b, c) [a, b, c].join(" ");
 *     curry(foo)(1, 2, 3) -> "1 2 3";
 *     curry(foo)(4)(5, 6) -> "4 5 6";
 *     curry(foo)(7)(8)(9) -> "7 8 9";
 *
 * @param {function} fn The function to curry.
 * @param {integer} length The number of arguments expected.
 *     @default fn.length
 *     @optional
 * @param {object} self The 'this' value for the returned function. When
 *     omitted, the value of 'this' from the first call to the function is
 *     preserved.
 *     @optional
 */
function curry(fn, length, self, acc) {
    if (length == null)
        length = fn.length;
    if (length == 0)
        return fn;

    // Close over function with 'this'
    function close(self, fn) {
        return (...args) => fn.apply(self, args);
    }

    if (acc == null)
        acc = [];

    function curried(...args) {
        // The curried result should preserve 'this'
        if (args.length == 0)
            return close(self || this, curried);

        args = acc.concat(args);

        if (args.length >= length)
            return fn.apply(self || this, args);

        return curry(fn, length, self || this, args);
    }
    curried.realName = fn.realName || fn.name;
    return curried;
}

var bind = function bind(meth, self, ...args) {
    let func = callable(meth) ? meth : self[meth];

    return apply(func, "bind", [self].concat(args));
};

/**
 * Returns true if both arguments are functions and
 * (targ() instanceof src) would also return true.
 *
 * @param {function} targ
 * @param {function} src
 * @returns {boolean}
 */
function isSubclass(targ, src) {
    return src === targ ||
        targ && typeof targ === "function" && targ.prototype instanceof src;
}

/**
 * Returns true if *object* is an instance of *interfaces*. If *interfaces* is
 * an array, returns true if *object* is an instance of any element of
 * *interfaces*. If *interfaces* is the object form of a primitive type,
 * returns true if *object* is a non-boxed version of the type, i.e., if
 * (typeof object == "string"), isinstance(object, String) is true. Finally, if
 * *interfaces* is a string, returns true if ({}.toString.call(object) ==
 * "[object <interfaces>]").
 *
 * @param {object} object The object to check.
 * @param {constructor|[constructor|string]} interfaces The types to check *object* against.
 * @returns {boolean}
 */
var isinstance_types = {
    boolean: Boolean,
    string: String,
    function: Function,
    number: Number,
    symbol: Symbol
};
function isinstance(object, interfaces) {
    if (object == null)
        return false;

    return Array.concat(interfaces).some(function isinstance_some(iface) {
        if (typeof iface === "string") {
            if (objToString(object) === "[object " + iface + "]")
                return true;
        }
        else if (typeof object === "object" && "isinstance" in object && object.isinstance !== isinstance) {
            if (object.isinstance(iface))
                return true;
        }
        else {
            if (object instanceof iface)
                return true;
            var type = isinstance_types[typeof object];
            if (type && isSubclass(iface, type))
                return true;
        }
        return false;
    });
}

/**
 * Returns true if obj is a non-null object.
 */
function isObject(obj) {
    return typeof obj === "object" && obj != null || obj instanceof Ci.nsISupports;
}

/**
 * Returns true if and only if its sole argument is an
 * instance of the builtin Array type. The array may come from
 * any window, frame, namespace, or execution context, which
 * is not the case when using (obj instanceof Array).
 */
var isArray =
    // This is bloody stupid.
    function isArray(val) {
        return Array.isArray(val) || val && val.constructor && val.constructor.name === "Array";
    }

/**
 * Returns true if and only if its sole argument is an
 * instance of the builtin Generator type. This includes
 * functions containing the 'yield' statement and generator
 * statements such as (x for (x in obj)).
 */
function isGenerator(val) {
    return objToString(val) == "[object Generator]";
}

/**
 * Returns true if and only if its sole argument is a String,
 * as defined by the builtin type. May be constructed via
 * String(foo) or new String(foo) from any window, frame,
 * namespace, or execution context, which is not the case when
 * using (obj instanceof String) or (typeof obj == "string").
 */
function isString(val) {
    return objToString(val) == "[object String]";
}

/**
 * Returns true if and only if its sole argument may be called
 * as a function. This includes classes and function objects.
 *
 * Excludes callable DOM objects: https://bugzil.la/268945
 *
 * @param {*} val
 * @returns {boolean}
 */
function callable(val) {
    return typeof val === "function" && !(val instanceof Ci.nsIDOMElement);
}

function call(fn, self, ...args) {
    fn.apply(self, args);
    return fn;
}

/**
 * Returns *val*.
 *
 * @param {*} val
 * @returns {*}
 */
function identity(val) { return val; }

/**
 * Memoizes an object property value.
 *
 * @param {object} obj The object to add the property to.
 * @param {string} key The property name.
 * @param {function} getter The function which will return the initial
 * value of the property.
 */
function memoize(obj, key, getter) {
    if (arguments.length == 1) {
        let res = update(Object.create(obj), obj);
        for (let prop of Object.getOwnPropertyNames(obj)) {
            let get = __lookupGetter__.call(obj, prop);
            if (get)
                memoize(res, prop, get);
        }
        return res;
    }

    try {
        Object.defineProperty(obj, key, {
            configurable: true,
            enumerable: true,

            get: function g_replaceProperty() {
                try {
                    Class.replaceProperty(this.instance || this, key, null);
                    return Class.replaceProperty(this.instance || this, key, getter.call(this, key));
                }
                catch (e) {
                    util.reportError(e);
                }
            },

            set: function s_replaceProperty(val) {
                Class.replaceProperty(this.instance || this, key, val);
            }
        });
    }
    catch (e) {
        obj[key] = getter.call(obj, key);
    }
}

/**
 * Updates an object with the properties of another object. Getters
 * and setters are copied as expected. Moreover, any function
 * properties receive new 'supercall' and 'superapply' properties,
 * which will call the identically named function in target's
 * prototype.
 *
 *    let a = { foo: function (arg) "bar " + arg }
 *    let b = { __proto__: a }
 *    update(b, { foo: function foo() foo.supercall(this, "baz") });
 *
 *    a.foo("foo") -> "bar foo"
 *    b.foo()      -> "bar baz"
 *
 * @param {Object} target The object to update.
 * @param {Object} src The source object from which to update target.
 *    May be provided multiple times.
 * @returns {Object} Returns its updated first argument.
 */
function update(target) {
    for (let i = 1; i < arguments.length; i++) {
        let src = arguments[i];

        Object.getOwnPropertyNames(src || {}).forEach(function (k) {
            let desc = Object.getOwnPropertyDescriptor(src, k);
            if (desc.value instanceof Class.Property)
                desc = desc.value.init(k, target) || desc.value;

            try {
                if (callable(desc.value) &&
                        Cu.getClassName(desc.value, true) != "Proxy" &&
                        Object.getPrototypeOf(target)) {

                    let func = desc.value.wrapped || desc.value;
                    if (!func.superapply) {
                        func.__defineGetter__("super", function get_super() {
                            return Object.getPrototypeOf(target)[k];
                        });

                        func.superapply = function superapply(self, args) {
                            let meth = Object.getPrototypeOf(target)[k];
                            return meth && meth.apply(self, args);
                        };

                        func.supercall = function supercall(self, ...args) {
                            return func.superapply(self, args);
                        };
                    }
                }

                if (k.startsWith("@@") && k.slice(2) in Symbol)
                    k = Symbol[k.slice(2)];

                Object.defineProperty(target, k, desc);
            }
            catch (e) {
                dump("Hmm... " + e + "\n" + (e && e.stack || new Error().stack) + "\n");
            }
        });
    }
    return target;
}

/**
 * @constructor Class
 *
 * Constructs a new Class. Arguments marked as optional must be
 * either entirely elided, or they must have the exact type
 * specified.
 *
 * @param {string} name The class's as it will appear when toString
 *     is called, as well as in stack traces.
 *     @optional
 * @param {function} base The base class for this module. May be any
 *     callable object.
 *     @optional
 *     @default Class
 * @param {Object} prototype The prototype for instances of this
 *     object. The object itself is copied and not used as a prototype
 *     directly.
 * @param {Object} classProperties The class properties for the new
 *     module constructor. More than one may be provided.
 *     @optional
 *
 * @returns {function} The constructor for the resulting class.
 */
function Class(...args) {

    if (isString(args[0]))
        var name = args.shift();
    var superclass = Class;
    if (callable(args[0]))
        superclass = args.shift();

    var Constructor = eval(String.replace('\n\
        (function constructor(PARAMS) {                      \n\
            var self = Object.create(Constructor.prototype); \n\
            self.instance = self;                            \n\
            self.globalInstance = self;                      \n\
                                                             \n\
            if ("_metaInit_" in self && self._metaInit_)     \n\
                self._metaInit_.apply(self, arguments);      \n\
                                                             \n\
            var res = self.init.apply(self, arguments);      \n\
            return res !== undefined ? res : self;           \n\
        })',
        "constructor", (name || superclass.className).replace(/\W/g, "_"))
            .replace("PARAMS",
                     /^function .*?\((.*?)\)/
                        .exec(args[0] && args[0].init || Class.prototype.init)[1]
                        .replace(/\b(self|res|Constructor)\b/g, "$1_")));

    Constructor.className = name || superclass.className || superclass.name;

    if ("init" in superclass.prototype)
        Constructor.__proto__ = superclass;
    else {
        let superc = superclass;
        superclass = function Shim() {};
        Class.extend(superclass, superc, {
            init: superc
        });
        superclass.__proto__ = superc;
    }

    Class.extend(Constructor, superclass, args[0]);
    memoize(Constructor, "bound", Class.makeClosure);
    if (Iter && Ary) // Hack. :/
        Object.defineProperty(Constructor, "closure",
                              deprecated("bound", { get: function closure() { return this.bound; } }));
    update(Constructor, args[1]);

    Constructor.__proto__ = superclass;

    args.slice(2).forEach(function (obj) {
        if (callable(obj))
            obj = obj.prototype;
        update(Constructor.prototype, obj);
    });
    return Constructor;
}

Class.objectGlobal = object => {
    try {
        return Cu.getGlobalForObject(object);
    }
    catch (e) {
        return null;
    }
};

/**
 * @class Class.Property
 * A class which, when assigned to a property in a Class's prototype
 * or class property object, defines that property's descriptor
 * rather than its value. If the desc object has an init property, it
 * will be called with the property's name before the descriptor is
 * assigned.
 *
 * @param {Object} desc The property descriptor.
 */
Class.Property = function Property(desc) {
    return update(Object.create(Property.prototype),
                  desc || { configurable: true, writable: true });
};
Class.Property.prototype.init = () => {};
/**
 * Extends a subclass with a superclass. The subclass's
 * prototype is replaced with a new object, which inherits
 * from the superclass's prototype, {@see update}d with the
 * members of *overrides*.
 *
 * @param {function} subclass
 * @param {function} superclass
 * @param {Object} overrides @optional
 */
Class.extend = function extend(subclass, superclass, overrides) {
    subclass.superclass = superclass;

    subclass.prototype = Object.create(superclass.prototype);
    update(subclass.prototype, overrides);
    subclass.prototype.constructor = subclass;
    subclass.prototype._class_ = subclass;

    if (superclass.prototype.constructor === objproto.constructor)
        superclass.prototype.constructor = superclass;
};

/**
 * Memoizes the value of a class property to the value returned by
 * the passed function the first time the property is accessed.
 *
 * @param {function(string)} getter The function which returns the
 *      property's value.
 * @returns {Class.Property}
 */
Class.Memoize = function Memoize(getter, wait) {
    return Class.Property({
        configurable: true,
        enumerable: true,
        init: function (key) {
            let done = false;

            if (wait)
                // Crazy, yeah, I know. -- Kris
                this.get = function replace() {
                    let obj = this.instance || this;
                    Object.defineProperty(obj, key,  {
                        configurable: true, enumerable: false,
                        get: function get() {
                            util.waitFor(() => done);
                            return this[key];
                        }
                    });

                    Task.spawn(function* () {
                        let wait;
                        for (var res of getter.call(obj)) {
                            if (wait !== undefined)
                                yield promises.sleep(wait);
                            wait = res;
                        }
                        Class.replaceProperty(obj, key, res);
                        done = true;
                    });

                    return this[key];
                };
            else
                this.get = function g_Memoize() {
                    let obj = this.instance || this;
                    try {
                        Class.replaceProperty(obj, key, null);
                        return Class.replaceProperty(obj, key, getter.call(this, key));
                    }
                    catch (e) {
                        if (loaded.util)
                            util.reportError(e);
                        else
                            defineModule.dump("    " + (e.filename || e.fileName) + ":" + e.lineNumber + ": " + e + "\n" + (e.stack || Error().stack) + "\n");
                    }
                };

            this.set = function s_Memoize(val) {
                return Class.replaceProperty(this.instance || this, key, val);
            };
        }
    });
};

/**
 * Updates the given object with the object in the target class's
 * prototype.
 */
Class.Update = function Update(obj) {
    return Class.Property({
        configurable: true,
        enumerable: true,
        writable: true,
        init: function (key, target) {
            this.value = update({}, target[key], obj);
        }
    });
};

Class.replaceProperty = function replaceProperty(obj, prop, value) {
    Object.defineProperty(obj, prop, { configurable: true, enumerable: true, value: value, writable: true });
    return value;
};
Class.toString = function toString() {
    return "[class " + this.className + "]";
};
Class.prototype = {
    /**
     * Initializes new instances of this class. Called automatically
     * when new instances are created.
     */
    init: function c_init() {},

    get instance() { return {}; },
    set instance(val) { Class.replaceProperty(this, "instance", val); },

    withSavedValues: function withSavedValues(names, callback, self) {
        let vals = names.map(name => this[name]);
        try {
            return callback.call(self || this);
        }
        finally {
            names.forEach((name, i) => { this[name] = vals[i]; });
        }
    },

    toString: function C_toString() {
        if (this.toStringParams)
            var params = "(" + this.toStringParams.map(m => (isArray(m)  ? "[" + m + "]" :
                                                             isString(m) ? JSON.stringify(m) : String(m)))
                                   .join(", ") + ")";
        return "[instance " + this.constructor.className + (params || "") + "]";
    },

    /**
     * Executes *callback* after *timeout* milliseconds. The value of
     * 'this' is preserved in the invocation of *callback*.
     *
     * @param {function} callback The function to call after *timeout*
     * @param {number} timeout The time, in milliseconds, to wait
     *     before calling *callback*.
     * @returns {nsITimer} The timer which backs this timeout.
     */
    timeout: function timeout(callback, timeout) {
        let timeout_notify = timer => {
            if (this.stale ||
                    util.rehashing && !isinstance(Cu.getGlobalForObject(callback), ["BackstagePass"]))
                return;
            this.timeouts.splice(this.timeouts.indexOf(timer), 1);
            try {
                callback.call(this);
            }
            catch (e) {
                try {
                    util.dump("Error invoking timer callback registered at " +
                              [frame.filename, frame.lineNumber, ""].join(":"));
                    util.reportError(e);
                }
                catch (e) {
                    Cu.reportError(e);
                }
            }
        };
        let frame = Components.stack.caller;
        let timer = services.Timer(timeout_notify, timeout || 0, services.Timer.TYPE_ONE_SHOT);
        this.timeouts.push(timer);
        return timer;
    },
    timeouts: [],

    /**
     * Updates this instance with the properties of the given objects.
     * Like the update function, but with special semantics for
     * localized properties.
     */
    update: function update() {
        // XXX: Duplication.

        for (let i = 0; i < arguments.length; i++) {
            let src = arguments[i];
            Object.getOwnPropertyNames(src || {}).forEach(k => {
                if (k.startsWith("@@") && k.slice(2) in Symbol)
                    k = Symbol[k.slice(2)];

                let desc = Object.getOwnPropertyDescriptor(src, k);
                if (desc.value instanceof Class.Property)
                    desc = desc.value.init(k, this) || desc.value;

                if (typeof desc.value === "function") {
                    let func = desc.value.wrapped || desc.value;
                    if (!func.superapply) {
                        func.__defineGetter__("super", () => Object.getPrototypeOf(this)[k]);

                        func.superapply = function superapply(self, args) {
                            let meth = Object.getPrototypeOf(self)[k];
                            return meth && meth.apply(self, args);
                        };

                        func.supercall = function supercall(self, ...args) {
                            return func.superapply(self, args);
                        };
                    }
                }

                try {
                    if (k.startsWith("@@") && k.slice(2) in Symbol)
                        k = Symbol[k.slice(2)];

                    if ("value" in desc && (this.localizedProperties.has(k) || this.magicalProperties.has(k)))
                        this[k] = desc.value;
                    else
                        Object.defineProperty(this, k, desc);
                }
                catch (e) {}
            }, this);
        }
        return this;
    },

    localizedProperties: new RealSet,
    magicalProperties: new RealSet
};

/*
Object.defineProperty(Class.prototype, Symbol.iterator, {
    get: function () this.__iterator__,
    set: function (val) { this.__iterator__ = val },
    configurable: true
});
*/

for (let name of properties(Class.prototype)) {
    let desc = Object.getOwnPropertyDescriptor(Class.prototype, name);
    desc.enumerable = false;
    Object.defineProperty(Class.prototype, name, desc);
}

var closureHooks = {
    get(target, prop) {
        if (prop in target._closureCache)
            return target._closureCache[prop];

        let p = target[prop];
        if (callable(p))
            return target._closureCache[prop] = p.bind(target);
        return p;
    },
};

Class.makeClosure = function makeClosure() {
    this._closureCache = Object.create(null);

    return new Proxy(this, closureHooks);
};
memoize(Class.prototype, "bound", Class.makeClosure);

/**
 * A base class generator for classes which implement XPCOM interfaces.
 *
 * @param {nsIIID|[nsIJSIID]} interfaces The interfaces which the class
 *      implements.
 * @param {Class} superClass A super class. @optional
 * @returns {Class}
 */
function XPCOM(interfaces, superClass) {
    interfaces = Array.concat(interfaces);

    let shim = XPCOMShim(interfaces);

    let base = { QueryInterface: XPCOMUtils.generateQI(interfaces) };

    for (let [k, v] of iter(shim)) {
        if (v === undefined || callable(v))
            base[k] = stub;
        else
            base[k] = v;
    }

    let res = Class("XPCOM(" + interfaces + ")",
                    superClass || Class,
                    base);

    return res;
}

function XPCOMShim(interfaces) {
    let ip = services.InterfacePointer({
        QueryInterface: function (iid) {
            if (iid.equals(Ci.nsISecurityCheckedComponent))
                throw Cr.NS_ERROR_NO_INTERFACE;
            return this;
        },
        getHelperForLanguage: function () { return null; },
        getInterfaces: function (count) { count.value = 0; }
    });
    return (interfaces || []).reduce((shim, iface) => shim.QueryInterface(Ci[iface]),
                                     ip.data);
}
let stub = Class.Property({
    configurable: true,
    enumerable: false,
    value: function stub() { return null; },
    writable: true
});

/**
 * An abstract base class for classes that wish to inherit from Error.
 */
var ErrorBase = Class("ErrorBase", Error, {
    level: 2,
    init: function EB_init(message, level=0) {
        let error = Error(message);
        update(this, error);
        this.stack = error.stack;
        this.message = message;

        let frame = Components.stack;
        for (let i = 0; i < this.level + level; i++) {
            frame = frame.caller;
            this.stack = this.stack.replace(/^.*\n/, "");
        }
        this.fileName = frame.filename;
        this.lineNumber = frame.lineNumber;
    },
    toString: function () { return String(this.message); }
});

/**
 * An Error subclass to throw in order to stop sourcing a plugin without
 * printing a stack trace.
 */
var Finished = Class("Finished", ErrorBase);

/**
 * Constructs a new Module class and instantiates an instance into the current
 * module global object.
 *
 * @param {string} name The name of the instance.
 * @param {Object} prototype The instance prototype.
 * @param {Object} classProperties Properties to be applied to the class constructor.
 * @returns {Class}
 */
function Module(name, prototype, ...args) {
    try {
        let init = callable(prototype) ? 2 : 1;
        let proto = callable(prototype) ? args[0] : prototype;

        proto._metaInit_ = function () {
            module.prototype._metaInit_ = null;
            currentModule[name.toLowerCase()] = this;
        };

        const module = Class.apply(Class, [name, prototype, ...args.slice(0, init)]);
        let instance = module();
        module.className = name.toLowerCase();

        instance.INIT = update(Object.create(Module.INIT),
                               args[init] || {});

        currentModule[module.className] = instance;
        defineModule.modules.push(instance);
        return module;
    }
    catch (e) {
        if (typeof e === "string")
            e = Error(e);

        dump(e.fileName + ":" + e.lineNumber + ": " + e + "\n" + (e.stack || Error().stack));
    }
}
Module.INIT = {
    init: function Module_INIT_init(dactyl, modules, window) {
        let args = arguments;

        let locals = [];
        for (let local = this.Local; local; local = local.super)
            locals.push(local);

        if (locals.length) {
            let module = this, objs = {};
            for (let i in locals) {
                module = objs[i] = Object.create(module);
                module.modules = modules;
            }
            module.isLocalModule = true;

            modules.jsmodules[this.constructor.className] = module;
            locals.reverse().forEach((fn, i) => {
                update(objs[i], fn.apply(module, args));
            });

            memoize(module, "closure", Class.makeClosure);
            module.instance = module;
            module.init();

            if (module.signals)
                modules.dactyl.registerObservers(module);
        }
    }
};

/**
 * @class Struct
 *
 * Creates a new Struct constructor, used for creating objects with
 * a fixed set of named members. Each argument should be the name of
 * a member in the resulting objects. These names will correspond to
 * the arguments passed to the resultant constructor. Instances of
 * the new struct may be treated very much like arrays, and provide
 * many of the same methods.
 *
 *     const Point = Struct("x", "y", "z");
 *     let p1 = Point(x, y, z);
 *
 * @returns {function} The constructor for the new Struct.
 */
function Struct(...args) {
    if (/^[A-Z]/.test(args[0]))
        var className = args.shift();

    const Struct = Class(className || "Struct", StructBase, {
        length: args.length,
        members: Ary.toObject(args.map((v, k) => [v, k])),
    });
    args.forEach(function (name, i) {
        Struct.prototype.__defineGetter__(name, function () {
            return this[i];
        });
        Struct.prototype.__defineSetter__(name, function (val) {
            this[i] = val;
        });
    });
    return Struct;
}
var StructBase = Class("StructBase", Array, {
    init: function struct_init() {
        for (let i = 0; i < arguments.length; i++)
            if (arguments[i] != undefined)
                this[i] = arguments[i];
    },

    get toStringParams() { return this; },

    clone: function struct_clone() {
        return this.constructor.apply(null, this.slice());
    },

    bound: Class.Property(Object.getOwnPropertyDescriptor(Class.prototype, "bound")),
    closure: Class.Property(Object.getOwnPropertyDescriptor(Class.prototype, "closure")),

    get: function struct_get(key, val) {
        return this[this.members[key]];
    },
    set: function struct_set(key, val) {
        return this[this.members[key]] = val;
    },

    toObject: function struct_toObject() {
        return Ary.toObject(Object.keys(this.members).map(k => [k, this[k]]));
    },

    toString: function struct_toString() {
        return Class.prototype.toString.apply(this, arguments);
    },

    // Iterator over our named members
    "@@iterator": function* struct__iterator__() {
        for (let k of keys(this.members))
            yield this[k];
    }
}, {
    fromArray: function fromArray(ary) {
        if (!(ary instanceof this))
            ary.__proto__ = this.prototype;
        return ary;
    },

    /**
     * Sets a lazily constructed default value for a member of
     * the struct. The value is constructed once, the first time
     * it is accessed and memoized thereafter.
     *
     * @param {string} key The name of the member for which to
     *     provide the default value.
     * @param {function} val The function which is to generate
     *     the default value.
     */
    defaultValue: function defaultValue(key, val) {
        let i = this.prototype.members[key];
        this.prototype.__defineGetter__(i, function () {
            return this[i] = val.call(this);
        });
        this.prototype.__defineSetter__(i, function (value) {
            return Class.replaceProperty(this, i, value);
        });
        return this;
    },

    localize: function localize(key, defaultValue) {
        let i = this.prototype.members[key];
        Object.defineProperty(this.prototype, i, Messages.Localized(defaultValue).init(key, this.prototype));
        return this;
    }
});

var Timer = Class("Timer", {
    init: function init(minInterval, maxInterval, callback, self=this) {
        this._timer = services.Timer();
        this.callback = callback;
        this.self = self;
        this.minInterval = minInterval;
        this.maxInterval = maxInterval;
        this.doneAt = 0;
        this.latest = 0;
    },

    notify: function notify(timer, force) {
        try {
            if (!loaded || loaded.util && util.rehashing || typeof util === "undefined" || !force && this.doneAt == 0)
                return;

            this._timer.cancel();
            this.latest = 0;
            // minInterval is the time between the completion of the command and the next firing
            this.doneAt = Date.now() + this.minInterval;

            this.callback.call(this.self, this.arg);
        }
        catch (e) {
            if (typeof util === "undefined")
                dump(JSMLoader.name + ": " + e + "\n" + (e.stack || Error().stack));
            else
                util.reportError(e);
        }
        finally {
            this.doneAt = Date.now() + this.minInterval;
        }
    },

    tell: function tell(arg) {
        if (arguments.length > 0)
            this.arg = arg;

        let now = Date.now();
        if (this.doneAt == -1)
            this._timer.cancel();

        let timeout = this.minInterval;
        if (now > this.doneAt && this.doneAt > -1)
            timeout = 0;
        else if (this.latest)
            timeout = Math.min(timeout, this.latest - now);
        else
            this.latest = now + this.maxInterval;

        this._timer.initWithCallback(this, Math.max(timeout, 0), this._timer.TYPE_ONE_SHOT);
        this.doneAt = -1;
    },

    reset: function reset() {
        this._timer.cancel();
        this.doneAt = 0;
    },

    flush: function flush(force) {
        if (this.doneAt == -1 || force)
            this.notify(null, true);
    }
});

/**
 * Idempotent function which returns the UTF-8 encoded value of an
 * improperly-decoded string.
 *
 * @param {string} str
 * @returns {string}
 */
function UTF8(str) {
    try {
        return decodeURIComponent(escape(str));
    }
    catch (e) {
        return str;
    }
}

var octal = deprecated("octal integer literals", function octal(decimal) { return parseInt(decimal, 8); });

/**
 * Iterates over an arbitrary object. The following iterator types are
 * supported, and work as a user would expect:
 *
 *  • nsIDOMNodeIterator
 *  • mozIStorageStatement
 *
 * Additionally, the following array-like objects yield a tuple of the
 * form [index, element] for each contained element:
 *
 *  • nsIDOMHTMLCollection
 *  • nsIDOMNodeList
 *
 * and the following likewise yield one element of the form
 * [name, element] for each contained element:
 *
 *  • nsIDOMNamedNodeMap
 *
 * Duck typing is implemented for any other type. If the object
 * contains the "enumerator" property, iter is called on that. If the
 * property is a function, it is called first. If it contains the
 * property "getNext" along with either "hasMoreItems" or "hasMore", it
 * is iterated over appropriately.
 *
 * For all other cases, this function behaves exactly like the Iterator
 * function.
 *
 * @param {object} obj
 * @param {nsIJSIID} iface The interface to which to query all elements.
 * @returns {Generator}
 */
function iter(obj, iface) {
    if (arguments.length == 2 && iface instanceof Ci.nsIJSIID)
        return iter(obj).map(item => item.QueryInterface(iface));

    let args = arguments;
    let res;

    if (args.length > 1)
        res = (function* () {
            for (let i = 0; i < args.length; i++)
                for (let j of iter(args[i]))
                    yield j;
        })();
    else if (isinstance(obj, ["Array", StructBase]))
        res = obj.entries();
    else if (Symbol.iterator in obj)
        res = obj[Symbol.iterator]();
    else if (isinstance(obj, [Ci.nsIDOMHTMLCollection, Ci.nsIDOMNodeList]))
        res = Array.from(obj).entries();
    else if (ctypes && ctypes.CData && obj instanceof ctypes.CData) {
        while (obj.constructor instanceof ctypes.PointerType)
            obj = obj.contents;
        if (obj.constructor instanceof ctypes.ArrayType)
            res = Array.from(obj).entries();
        else if (obj.constructor instanceof ctypes.StructType)
            res = (function* () {
                for (let prop of obj.constructor.fields) {
                    let [name, type] = Iterator(prop).next();
                    yield [name, obj[name]];
                }
            })();
        else
            return iter({});
    }
    else if (Ci.nsIDOMNamedNodeMap && obj instanceof Ci.nsIDOMNamedNodeMap ||
             Ci.nsIDOMMozNamedAttrMap && obj instanceof Ci.nsIDOMMozNamedAttrMap)
        res = (function* () {
            for (let i = 0; i < obj.length; i++)
                yield [obj.name, obj];
        })();
    else if (obj instanceof Ci.mozIStorageStatement)
        res = (function* (obj) {
            while (obj.executeStep())
                yield obj.row;
            obj.reset();
        })(obj);
    else if ("getNext" in obj) {
        if ("hasMoreElements" in obj)
            res = (function* () {
                while (obj.hasMoreElements())
                    yield obj.getNext();
            })();
        else if ("hasMore" in obj)
            res = (function* () {
                while (obj.hasMore())
                    yield obj.getNext();
            })();
    }
    else if ("enumerator" in obj) {
        if (callable(obj.enumerator))
            return iter(obj.enumerator());
        return iter(obj.enumerator);
    }

    if (res === undefined)
        res = Object.entries(obj).values();

    return Iter(res);
}
update(iter, {
    toArray: deprecated("Array.from", function toArray(iter) {
        return Array.from(iter);
    }),

    // See Ary.prototype for API docs.
    toObject: function toObject(iter) {
        let obj = {};
        for (let [k, v] of iter)
            if (v instanceof Class.Property)
                Object.defineProperty(obj, k, v.init(k, obj) || v);
            else
                obj[k] = v;
        return obj;
    },

    compact: function* compact(iter) {
        for (let item of iter)
            if (item != null)
                yield item;
    },

    every: function every(iter, pred, self) {
        pred = pred || identity;
        for (let elem of iter)
            if (!pred.call(self, elem))
                return false;
        return true;
    },

    some: function every(iter, pred, self) {
        pred = pred || identity;
        for (let elem of iter)
            if (pred.call(self, elem))
                return true;
        return false;
    },

    filter: function* filter(iter, pred, self) {
        for (let elem of iter)
            if (pred.call(self, elem))
                yield elem;
    },

    /**
     * Iterates over an iterable object and calls a callback for each
     * element.
     *
     * @param {object} iter The iterator.
     * @param {function} fn The callback.
     * @param {object} self The this object for *fn*.
     */
    forEach: function forEach(iter, func, self) {
        for (let val of iter)
            func.call(self, val);
    },

    indexOf: function indexOf(iter, elem) {
        let i = 0;
        for (let item of iter) {
            if (item == elem)
                return i;
            i++;
        }
        return -1;
    },

    /**
     * Returns the array that results from applying *func* to each property of
     * *obj*.
     *
     * @param {Object} obj
     * @param {function} func
     * @returns {Array}
     */
    map: function* map(iter, func, self) {
        for (let i of iter)
            yield func.call(self, i);
    },

    /**
     * Returns the nth member of the given array that matches the
     * given predicate.
     */
    nth: function nth(iter, pred, n, self) {
        if (typeof pred === "number")
            [pred, n] = [() => true, pred]; // Hack.

        for (let elem of iter)
            if (pred.call(self, elem) && n-- === 0)
                return elem;
        return undefined;
    },

    /**
     * Analog of Array.find method. Returns the first item in the
     * iterator for which `pred` returns true.
     */
    find: function find(iter, pred, self) {
        for (let elem of iter)
            if (pred.call(self, elem))
                return elem;
        return undefined;
    },

    sort: function sort(iter, fn, self) {
        return Ary(iter).sort(fn, self);
    },

    uniq: function* uniq(iter) {
        let seen = new RealSet;
        for (let item of iter)
            if (!seen.add(item))
                yield item;
    },

    /**
     * Zips the contents of two iterables. The resulting iterable is the length
     * of iter1, with any shortcomings of iter2 replaced with null strings.
     *
     * @param {Iterable} iter1
     * @param {Iterable} iter2
     * @returns {Generator}
     */
    zip: function* zip(iter1, iter2) {
        let i = iter2[Symbol.iterator]();
        for (let x of iter1) {
            let y = i.next().value;
            yield [x, y === undefined ? "" : y];
        }
    }
});

var Iter = Class("Iter", {
    init(iter) {
        this.iter = iter;
        if (!(Symbol.iterator in iter) && "__iterator__" in iter)
            this.iter = iter.__iterator__();

        if (this.iter.finalize)
            this.finalize = function finalize() {
                return apply(this.iter, "finalize", arguments);
            };
    },

    next() { return this.iter.next() },

    send() { return apply(this.iter, "send", arguments) },

    "@@iterator": function () { return this.iter },

    __iterator__: function () {
        Cu.reportError(
            "Dear sir or madam,\n" +
            "You are attempting to use this iterator object as a\n" +
            "deprecated, pre-ES6 iterator, which is known to cause\n" +
            "major problems. Please use it in a for-of loop instead.\n\n" +
            "Since I don't know how far back in the call chain you\n" +
            "originate, I'm just going to give you an entire stack.\n" +
            "Here you go:\n\n" + Error().stack);

        throw Error("Iter object used as a pre-ES6 iterator");
    }
});
iter.Iter = Iter;

function arrayWrap(fn) {
    function wrapper() {
        let res = fn.apply(this, arguments);
        if (isArray(res))
            return Ary(res);

        if (isObject(res) && Symbol.iterator in res)
            return iter(res);
        return res;
    }
    wrapper.wrapped = fn;
    return wrapper;
}

/**
 * Array utility methods.
 */
var Ary = Class("Ary", Array, {
    init: function (ary) {
        if (Symbol.iterator in ary && !isArray(ary))
            ary = Array.from(ary);

        return new Proxy(ary, {
            get: function array_get(target, prop) {
                if (prop == "array")
                    return target;

                if (hasOwnProp(Ary, prop) && callable(Ary[prop]))
                    return arrayWrap(Ary[prop].bind(Ary, target));

                let p = target[prop];

                if (typeof prop == "symbol" ||
                    /^\d+$/.test(prop) ||
                    prop == "toString" ||
                    prop == "toSource" ||
                    !callable(p))
                    return p;

                return arrayWrap(p);
            }
        });
    }
}, {
    /**
     * Converts an array to an object. As in lisp, an assoc is an
     * array of key-value pairs, which maps directly to an object,
     * as such:
     *    [["a", "b"], ["c", "d"]] -> { a: "b", c: "d" }
     *
     * @param {[Array]} assoc
     * @... {string} 0 - Key
     * @...          1 - Value
     */
    toObject: function toObject(assoc) {
        let obj = {};
        assoc.forEach(function ([k, v]) {
            if (v instanceof Class.Property)
                Object.defineProperty(obj, k, v.init(k, obj) || v);
            else
                obj[k] = v;
        });
        return obj;
    },

    /**
     * Compacts an array, removing all elements that are null or undefined:
     *    ["foo", null, "bar", undefined] -> ["foo", "bar"]
     *
     * @param {Array} ary
     * @returns {Array}
     */
    compact(ary) { return ary.filter(item => item != null) },

    /**
     * Returns true if each element of ary1 is equal to the
     * corresponding element in ary2.
     *
     * @param {Array} ary1
     * @param {Array} ary2
     * @returns {boolean}
     */
    equals(ary1, ary2) {
        return (ary1.length === ary2.length &&
                Array.every(ary1, (e, i) => e === ary2[i]));
    },

    /**
     * Flattens an array, such that all elements of the array are
     * joined into a single array:
     *    [["foo", ["bar"]], ["baz"], "quux"] -> ["foo", ["bar"], "baz", "quux"]
     *
     * @param {Array} ary
     * @returns {Array}
     */
    flatten(ary) {
        if (ary.length)
            return [].concat(...ary);
        return [];
    },

    /**
     * Returns an Iterator for an array's values.
     *
     * @param {Array} ary
     * @returns {Iterator(Object)}
     */
    iterValues: deprecated("Array#values", function* iterValues(ary) {
        for (let i = 0; i < ary.length; i++)
            yield ary[i];
    }),

    /**
     * Returns an Iterator for an array's indices and values.
     *
     * @param {Array} ary
     * @returns {Iterator([{number}, {Object}])}
     */
    iterItems: deprecated("Array#entries", function* iterItems(ary) {
        let length = ary.length;
        for (let i = 0; i < length; i++)
            yield [i, ary[i]];
    }),

    /**
     * Returns the nth member of the given array that matches the
     * given predicate.
     */
    nth: function nth(ary, pred, n, self) {
        for (let elem of ary)
            if (pred.call(self, elem) && n-- === 0)
                return elem;
        return undefined;
    },

    /**
     * Filters out all duplicates from an array. If *unsorted* is false, the
     * array is sorted before duplicates are removed.
     *
     * @param {Array} ary
     * @param {boolean} unsorted
     * @returns {Array}
     */
    uniq: function uniq(ary, unsorted) {
        let res = [];
        if (unsorted) {
            for (let item of ary)
                if (res.indexOf(item) == -1)
                    res.push(item);
        }
        else {
            for (let item of ary.sort()) {
                if (item != last || !res.length)
                    res.push(item);
                var last = item;
            }
        }
        return res;
    },

    /**
     * Zips the contents of two arrays. The resulting array is the length of
     * ary1, with any shortcomings of ary2 replaced with null strings.
     *
     * @param {Array} ary1
     * @param {Array} ary2
     * @returns {Array}
     */
    zip: function zip(ary1, ary2) {
        let res = [];
        for (let [i, item] of iter(ary1))
            res.push([item, i in ary2 ? ary2[i] : ""]);
        return res;
    }
});

let iterProto = Iter.prototype;
Object.keys(iter).forEach(function (k) {
    iterProto[k] = function (...args) {
        let res = iter[k](this, ...args);

        if (k == "toArray")
            return res;

        if (isObject(res) && Symbol.iterator in res)
            return Iter(res[Symbol.iterator]());

        return res;
    };
});

Object.keys(Ary).forEach(function (k) {
    if (!(k in iterProto))
        iterProto[k] = function (...args) {
            let res = Ary[k]([...this], ...args);

            if (isArray(res))
                return Ary(res);

            if (isObject(res) && Symbol.iterator in res)
                return Iter(res[Symbol.iterator]());
            return res;
        };
});

Object.getOwnPropertyNames(Array.prototype).forEach(function (k) {
    if (!(k in iterProto) && callable(Array.prototype[k]))
        iterProto[k] = function (...args) {
            let res = [...this][k](...args);

            if (isArray(res))
                return Ary(res);

            return res;
        };
});

Object.defineProperty(Class.prototype, "closure",
                      deprecated("bound", { get: function closure() { return this.bound; } }));

if (false)
    var array = Class("array", Ary, {
        init: deprecated("Ary", function init() { init.superapply(arguments); })
    });
else
    array = Ary;

endModule();

// catch(e){dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack);}

// vim: set fdm=marker sw=4 sts=4 ts=8 et ft=javascript:
