/******************************************************************************
    Core.js
    Copyright (c) 2006-2008 Motorola, Inc.
    All rights reserved

    Compat: IE6 IE7 FF2 Safari2 Rhino (??? = FF1 FF15 Safari1 Konq)

******************************************************************************/
var undefined;

function $nop ()
{
    // do nothing
}

/*
If we don't have Firebug or FirebugLite, generate stubs to make things OK.
*/
if (!window || !("console" in window))
    console = { log:$nop, debug:$nop, info:$nop, warn:$nop, error:$nop, /*assert:$nop,*/
                dir:$nop, dirxml:$nop, group:$nop, groupEnd:$nop, time:$nop, open:$nop,
                timeEnd:$nop, count:$nop, trace:$nop, profile:$nop, profileEnd:$nop };

/**
This method displays a panic message and throws an exception.
*/
function $panic (msg)
{
    console.error(msg);
    console.trace();
    console.open();

    throw new Error(msg);
}

if (netopia)
    $panic("Multiple includes for core.js!");

/**
Returns a function that will invoke this function with a specified "this" value.
Any parameters passed to this method beyond the first are inserted at the front
of calls made to the target method.
*/
Function.prototype.bind = function (obj/*, ... */)
{
    if (arguments.length < 2)
        return this.xbind(obj);

    return this.xbind(obj, netopia.core.arrayDup(arguments, 1));
};

/**
This method binds an object with arguments on the head or tail of a call. The
actual signatures are:

    1. fn.xbind(obj, [ ... ], [ ... ]);  // obj, head, tail
    2. fn.xbind(obj, [ ... ]);           // obj, head
    3. fn.xbind([ ... ], [ ... ]);       // head, tail
    4. fn.xbind(obj);                    // obj
    5. fn.xbind([ ... ]);                // head

It is important to remember that head and tail (if present) MUST be arrays! It
is also important that obj NOT be an array (unless you pass all 3 args).
*/
Function.prototype.xbind = function (obj, head, tail)
{
    if (obj && (arguments.length < 3) && (obj instanceof Array))
        switch (arguments.length)
        {
         case 2: tail = head;  // distinguish forms 2 & 3  (fall on purpose)
         case 1: head = obj; obj = null; break; // distinguish forms 4 & 5
        }

    var f, method = this;

    if (netopia.core.arrayLen(head) + netopia.core.arrayLen(tail) == 0)
        f = function () { return method.apply(obj, arguments); };
    else
        f = function () {
                var args = netopia.core.arrayCat(head, arguments, tail);
                return method.apply(obj, args);
            };

    f.methodName = this.getName();
    f.methodNameSuffix = "~bound";
    f.kMethodName = f.getName();
    return f;
};

/**
Returns a function that will invoke this function but with no parameters. This
is often used in conjunction with bind and methods that have a "..." style. For
example, it can be the case that the caller (such as an event dispatcher for an
HTML Element) might pass parameters that would only confuse the target method.
If no parameters are passed to this method, the return value of the target will
be returned to the ultimate caller of the returned function. Otherwise, the
first argument to this method will be returned to the ultimate caller.

@param this The Function object to be called.
@param ret The optional return value of the sealed method.
@return The function object that will call this function and return ret.
*/
Function.prototype.seal = function (ret)
{
    var method = this;

    if (arguments.length == 0)
        return function () { return method(); };

    return function () { method(); return ret; };
};

Function.prototype.getName = function ()
{
    if (this.kMethodName)
        return this.kMethodName;
    if (this.methodName)
    {
        var name = this.methodName;
        if (this.klass && this.klass.constructor.kClassName)   //@@@
            name = this.klass.constructor.kClassName + (name.length ? "." + name : "");   //@@@
        if (this.methodNameSuffix)
            name += this.methodNameSuffix;
        return name;
    }
    if (this.name) // in FireFox, Function objects have a name property...
        return this.name;

    var name = this.toString().match(/function\s*(\w*)/)[1];
    return name || "~anonymous~";
};

/**
This class encapsulates the mechanics of a namespace.
*/
function $Namespace (parent, name)
{
    this.mChildren = {};
    this.mName = name;
    this.mParent = parent;
    this.mFullName = parent ? (parent.mFullName + "." + name) : name;
}

$Namespace.hasOwnProp = function (obj, name)
{
    if (obj['hasOwnProperty'])
        return obj.hasOwnProperty(name);

    return obj[name] !== undefined;
}

$Namespace.prototype =
{

    add : function (stuff)
    {
        for (var n in stuff)
            if ($Namespace.hasOwnProp(stuff, n))
            {
                var v = stuff[n];
                if (this[n])
                    $panic("Duplicate symbol " + this.mFullName + "." + n);
                this[n] = v;

                if (v instanceof Function)
                {
                    var nm = this.mFullName + "." + n;
                    if (v.kClassName && v.kClassName == "@")
                    {
                        v.kClassName = v.kMethodName = nm;

                        for (var mem in v.prototype)
                        {
                            if (!$Namespace.hasOwnProp(v.prototype, mem))
                                continue;
                            var memval = v.prototype[mem];
                            if (memval instanceof Function)
                                memval.kMethodName = memval.getName();
                        }
                    }
                    else if (!v.methodName)
                        v.methodName = nm;

                    v.kMethodName = v.getName();
                }
            }
    },

    create : function (name)
    {
        var ns = this.mChildren[name];
        if (!ns)
        {
            ns = new $Namespace(this, name);
            this.mChildren[name] = ns;
            this[name] = ns;
        }
        return ns;
    },

    forEach : function (fn)
    {
        for (var c in this.mChildren)
            if ($Namespace.hasOwnProp(this.mChildren, c))
                if (! fn(this.mChildren[c]))
                    break;
    },
   
    stringify : function ()
    {
        return this.mFullName;
    }
};

//=============================================================================
// Create the netopia namespace

var netopia = new $Namespace(null, "netopia");

netopia.create("core").add({

_extendMarker : { },
_loadedPackages : { },
_mostRecentPkg : "",

arrayLen : function (array)
{
    if (!array)
        return 0;
    return array.length || 0;
},

arrayCat : function () // array1, array2, ...
{
    var i, j = 0, n = arguments.length, len = 0;
    var arrays = new Array(n);

    for (i = 0; i < n; ++i)
    {
        var a = netopia.core.makeArray(arguments[i]);
        var k = netopia.core.arrayLen(a);
        if (!k)
            continue;
        len += k;
        arrays[j++] = a;
    }

    var k = 0, ret = new Array(len);
    for (i = 0; i < j; ++i)
        for (var a = arrays[i], x = 0, c = a.length; c--; ) // a.length != 0
            ret[k++] = a[x++];

    return ret;
},

arrayDup : function (src, first, num)
{
    if (!src)
        return [ ];
    if (src.toArray)
    {
        src = src.toArray();
        if (!first && !num)
            return src;
    }
    var i = first || 0, n = src.length;
    if (num)
        n = Math.min(i + num, n);
    var r = new Array(Math.max(n - i,0));
    for (var j = 0; i < n; ++i, ++j)
        r[j] = src[i];
    return r;
},

copyProps : function (dest, src, deepSrc)
{
    var integral = netopia.core.isIntegral;

    for (var prop in src)
        if (deepSrc || netopia.core.hasOwnProp(src, prop))
        {
            var v = src[prop];
            var t = dest[prop];

            if (integral(v) || integral(t) || !netopia.core.hasOwnProp(dest, prop))
                dest[prop] = v;
            else
                netopia.core.copyProps(t, v, deepSrc);
        }

    return dest;
},

findMember : function (of, what)
{
    for (var n in of)
        if (of[n] == what)
            return n;
    return undefined;
},

isEmpty : function (str)
{
    return !str || str.length < 1 || str == " ";
},

isIntegral : function (x)
{
    if (!Function.prototype.kIntegral)
    {
        var types = [ Function, Boolean, Date, Number, RegExp, String, Error,
                      EvalError, RangeError, ReferenceError, SyntaxError,
                      TypeError, URIError ];

        for (var i = 0; i < types.length; ++i)
            types[i].prototype.kIntegral = true;
    }

    return x ? x.kIntegral !== undefined : true;
},

makeArray : function (x)
{
    if (!x)
        return [ ];
    if (x.toArray)
        return x.toArray();
    if (x.length !== undefined) // a little Duck Typing
        return x;
    return [x];
},

pushBack : function (to, v)
{
    if (!to)
        return v;
    if (!(to instanceof Array))
        return [to, v];
    to[to.length] = v;
    return to;
},

// Expose as a "public" API (since $Namespace.hasOwnProp is "private").
hasOwnProp : $Namespace.hasOwnProp,

/**
Throws an exception with a given msg and type (default is Error). If the error
is catastrophic or unexpected, consider using $panic instead.
*/
raise : function (msg, type)
{
    var T = (type || Error);
    var e = new T(msg);
    e.message = msg; // just in case...
    throw e;
},

_setupFunc : function (fn, klass, name, suffix)
{
    if (!(fn instanceof Function))
        return null;
    fn.methodName = name;
    if (suffix)
        fn.methodNameSuffix = suffix;
    fn.klass = klass;
    fn.kPriority = fn.kPriority || 0;
    if (!netopia.core.hasOwnProp(klass, name))
        return null;
    var ret = klass[name];
    return (ret instanceof Function) ? ret : null;
},

tee : function ()
{
    var funcs = [];
    for (var i = 0; i < arguments.length; ++i)
        if (arguments[i])
            funcs.push(arguments[i]);

    return function ()
    {
        var ret;
        for (var i = 0; i < funcs.length; ++i)
            ret = funcs[i].apply(this, arguments);
        return ret;
    };
},

/**
These methods effectively become static methods on all classes.
*/
kClassMethods :
{
    /**
    Applies a set of mixin members to the given class (klass). The descr
    argument is used for diagnostic purposes (for method names).
    */
    mixin : function (descr, members)
    {
        var suffix = descr ? (":" + descr) : null;

        for (var name in members)
        {
            if (!netopia.core.hasOwnProp(members, name))
                continue;

            var fn = members[name];
            var trg = fn.kStaticMember ? this : this.prototype;
            var cfn = netopia.core._setupFunc(fn, trg, name, suffix);

            if (cfn && cfn.kPriority > fn.kPriority)
            {
                while (cfn.callChainNext && cfn.callChainNext.kPriority > fn.kPriority)
                    cfn = cfn.callChainNext;
                fn.callChainNext = cfn.callChainNext;
                cfn.callChainNext = fn;
            }
            else
            {
                fn.callChainNext = cfn;
                trg[name] = fn;
            }
        }
    }
}

}); // netopia.core

//-------------------------------------------
// Class emulation / Pseudo Keywords:

/**
Returns true if base is a base of derived, false if not.
*/
function $baseof (base, derived)
{
    if (!derived)
        return false;
    if (derived.klass)
        derived = derived.klass;
    if (base.prototype == derived)
        return true;
    return derived instanceof base;
}

/**
Returns a new object using the given object as its prototype chain.
*/
function $chain (object, add)
{
    function T () { }
    T.prototype = object;
    var r = new T();
    if (add)
        netopia.core.copyProps(r, add);
    return r;
}

/**
Creates a "class" using the given prototype object. Any $static members are moved
from the given object to the class (aka "constructor function"). At this time the
name of the class is not known, so the kClassName property is set to a special
marker value to be fixed by $Namespace.add.

This is used internally by netopia.core.Object and $extends, but is not otherwise
to be used. Use "$extends (netopia.core.Object, { ... })" instead.
*/
function $class (proto)
{
    var klass = function (ext, what)
    {
        if (ext == netopia.core._extendMarker)
            netopia.core.copyProps(this, what);
        else
        {
            var fn = this.ctor;
            if (fn)
                fn.apply(this, arguments);
        }
    };

    var shadowCtors = 0;
    
    for (var name1 in proto)
    {
        if (name1 == "constructor" && netopia.core.hasOwnProp(proto, "constructor")) shadowCtors += 1;
    }
    
    for (var name in proto)
    {
        if (shadowCtors > 0)
        {
            if ( name == "constructor")
            {
                shadowCtors -= 1; 
            }
            continue;
        }
        
        if (netopia.core.hasOwnProp(proto, name))
        {
             var mem = proto[name], statik = (mem && mem.kStaticMember);
            if (statik)
                delete proto[name];
            netopia.core._setupFunc(mem, (statik ? klass : proto), name);
            if (statik)
                klass[name] = mem;
        }
    }
    //@@@
    proto.constructor = klass;
    klass.prototype = proto;
    klass.klass = proto;
    klass.kClassName = "@"; // set properly by $Namespace.add
    netopia.core.copyProps(klass, netopia.core.kClassMethods);

    return klass;
}

/**
Invokes the destroy method on the given object (if one is given). Any exceptions
thrown by the destroy method will be swallowed.
*/
function $destroy (obj)
{
    if (obj && obj.destroy && obj.destroy instanceof Function)
        try
        {
            obj.destroy();
        }
        catch (e)
        {
            console.error("Error in obj.destroy call (ignoring)!");
        }

    return undefined;
}

/**
Makes a "class" that extends a given class (klass) with more properties (members).
As with $class, the kClassName property is not yet known (set by $Namespace.add).

Example:

    namespace.foo.add({

    Derived : $extends (Base,
    {
    })

    });
*/
function $extends (klass, members)
{
    var proto = new klass(netopia.core._extendMarker, members);

    var klass2 = $class(proto);
    klass2.superKlass = klass;
    return klass2;
}

/**
This method simply returns its argument. In some cases, JavaScript does not
correctly recognize an object literal as such when it is being declared in a
return statement. There may be other places this can happen, but the way out is
to force the grammer into recognizing the object literal by passing it to a
function that simply returns it.
*/
function $literal (obj)
{
    return obj;
}

/**
Registers a given package (must not already be registered).
*/
function $package (pkg)
{
    //console.log("Package: " + pkg);
    var s = pkg.toLowerCase();

    if (netopia.core._loadedPackages[s])
        $panic("Duplicate package (" + pkg + ")");

    netopia.core._loadedPackages[s] = true;
    netopia.core._mostRecentPkg = pkg;
}

/**
Sets the priority of the given function object (fn) in the mixin call chain.
*/
function $priority (prio, fn)
{
    fn.kPriority = prio || 0;
    return fn;
}

/**
Checks that a required package is already registered.
*/
function $requires (pkg)
{
    var s = pkg.toLowerCase();

    if (netopia.core._loadedPackages[s])
        return;

    s = "Required package not available (" + pkg + ") for " +
              netopia.core._mostRecentPkg + ".\n\nGot:";
    for (var n in netopia.core._loadedPackages)
        s += "\n  ->" + n;

    $panic(s);
}

/**
Creates a class that can only be instanced once. The instance is created on first
call to the returned object's getInstance method.
*/
function $singleton (klass, members)
{
    var instance, klass2 = $extends(klass, members || { });

    return $literal(
    {
        getInstance : function ()
        {
            if (!instance)
                instance = new klass2();
            return instance;
        }
    });
}

/**
Marks the given object as a static member. This mark is interpreted by $class,
$extends and mixin such that the property is placed on the constructor/class.

Example:

    namespace.foo.add({

    Derived : $extends (Base,
    {
        bar : $static(function (x)
        {
        })
    })

    });
*/
function $static (mem)
{
    mem.kStaticMember = true;
    return mem;
}

/**
Finds and returns the super method to call. The arg parameter is typically the
"arguments" object of the caller, but can alternatively be the function object
for which the super is desired. The typical syntax is:

    $super(arguments).call(this, x, y);
*/
function $super (arg)
{
    var fn = (arg.callee || arg), f = fn.callChainNext;
    if (f)
        return f;
    var c = fn.klass.constructor.superKlass; //@@@
    if (c && (f = c.prototype[fn.methodName]) != null)
        return f;
    return $nop;
};

//=============================================================================

$package("netopia/core.html");

netopia.core.add({

/**
This is the base class for all objects (other than $Namespace).
ctor problem on Safar 2.0.1 and before
*/
Object : $class(
{
    ctor : function ()
    {
        $super(arguments).call(this);
    },

    destroy : function ()
    {
        $super(arguments).call(this);
    },

    include : function (src)
    {
        return netopia.core.copyProps(this, src);
    },

    stringify : function ()
    {
        return "{" + netopia.core.stringifyMembers(this) + "}";
    }
})

}); // netopia.core

/* A test and debug function. This can be 
   used to validate that constructor chain for
   the current object.
*/ 

function validateCtor(marker)
{
    var result = (netopia.core.Object.klass.constructor == netopia.core.Object) ? "succeeds": "fails";
    if (result != "succeeds")
       alert(marker+": validator "+result +"\n"+
           "comparing "+
           typeof netopia.core.Object.klass.constructor +" to "+
           typeof netopia.core.Object +"\n\n"+
           netopia.core.Object.klass.constructor.valueOf()+"\n\n"+
           netopia.core.Object.valueOf());
        
}

//--------------------------------------------------
// Geometry

netopia.core.add({

Dim : $extends (netopia.core.Object,
{
    ctor : function (w, h)
    {
        $super(arguments).call(this);                
        this.h = h || 0;
        this.w = w || 0;
    },

    stringify : function ()
    {
        return this.w + "x" + this.h;
    }
})

}); // netopia.core
netopia.core.add({

Rect : $extends (netopia.core.Dim,
{
    ctor : function (x, y, w, h)
    {
        $super(arguments).call(this, w, h);
        this.x = x;
        this.y = y;
        this.x2 = x + w;
        this.y2 = y + h;
    },

    contains : function (x, y)
    {
        return this.x <= x && x < this.x2 &&
               this.y <= y && y < this.y2;
    },

    containsPt : function (pt)
    {
        return this.contains(pt.x, pt.y);
    },

    stringify : function ()
    {
        return $super(arguments).call(this) + "@" + this.x + "," + this.y;
    }
})

}); // netopia.core

//-------------------------------------------
// String object methods

netopia.core.copyProps(String.prototype,
{
    capitalize : function ()
    {
        return this.charAt(0).toUpperCase() + this.substring(1);
    },

    camelize : function (splitChar)
    {
        var parts = this.split(splitChar || "-");
        var ret = parts[0], n = parts.length;

        for (var i = 1; i < n; ++i)
            ret += parts[i].capitalize();

        return ret;
    },

    endsWith : function (s)
    {
        if (this.length < s.length)
            return false;
        return this.right(s.length) == s;
    },

    equalsIgnoreCase : function (s)
    {
        var lhs = this.toLowerCase(), rhs = s.toLowerCase();
        return lhs == rhs;
    },

    left : function (n)
    {
        var i = Math.min(n, this.length);
        return this.substring(i);
    },

    right : function (n)
    {
        var k = this.length, i = Math.min(n, k);
        return this.substring(k - i);
    },

    startsWith : function (s)
    {
        if (this.length < s.length)
            return false;
        return this.left(s.length) == s;
    }
});

