//##############################################################################
// The $class Library, version 1.5
// Copyright 2006, Jeff Lau
// License: http://creativecommons.org/licenses/LGPL/2.1/
// Contact: jlau@uselesspickles.com
// Web:     www.uselesspickles.com
//##############################################################################
// CUSTOMIZE FOR PORTABILITY:
//
//   Portions of the $class library depend on having a reference to the global
// object. By default, the $class library is configured to work with web
// browsers by expecting the global object to be named "self". To configure
// the $class library to work in other environments, create a global variable
// named "GLOBAL_NAMESPACE_OBJECT_NAME" that contains a String representing the
// name of an object that refers to the global object. This variable must be
// defined before making any calls to the $class library.
//
// Example:
//   var GLOBAL_NAMESPACE_OBJECT_NAME = "global";
//
//##############################################################################

$class_library = {
  version: "1.5",

  _getGlobalObjectName: function() {
    try {
      this._globalObjectName = GLOBAL_NAMESPACE_OBJECT_NAME;
    } catch (error) {
      this._globalObjectName = "self";
    }

    this._getGlobalObjectName = Function("return this._globalObjectName");

    return this._globalObjectName;
  },

  _getGlobalObject: function() {
    this._globalObject = eval("(" + this._getGlobalObjectName() + ")");
    this._getGlobalObject = Function("return this._globalObject");

    return this._globalObject;
  },

  _copyObject: function(obj1, obj2) {
    var result = (arguments.length == 2 && obj1) ? obj1 : {};
    var source = ((arguments.length == 2) ? obj2 : obj1) || {};

    for (var i in source) {
      result[i] = source[i];
    }

    return result;
  },
  
  _surrogateCtor: function() {
  }
};

//##############################################################################

function $package(name) {
  // if being called as a constructor...
  if (this instanceof $package) {
    this._name = name;
    return;
  }

  if (!name) {
    $package._currentPackage = null;
    return;
  }

  var components = name.split(".");
  var context = $class_library._getGlobalObject();

  for (var i = 0; i < components.length; ++i) {
    var nextContext = context[components[i]];

    if (!nextContext) {
      nextContext = new $package(components.slice(0, i + 1).join("."));
      context[components[i]] = nextContext;
    }

    context = nextContext;
  }

  $package._currentPackage = context;
}

$package.prototype = {
  getName: function() {
    return this._name;
  },

  toString: function() {
    return "[$package " + this.getName() + "]";
  }
};

//##############################################################################

function $class(name, descriptor) {
  // if being called as a constructor...
  if (this instanceof $class) {
    this._name = name;
    this._isNative = descriptor.isNative;
    this._ctor = descriptor.ctor;
    this._baseCtor = descriptor.baseCtor || (this._ctor == Object ? null : Object);
    this._interfaces = {};

    // if not creating $class for Object...
    if (this._ctor != Object) {
      // if inheriting something other than Object...
      if (this._baseCtor != Object) {
        // if this $class object is being created internally by the $class function...
        if (descriptor.calledFrom$class) {
          // a 'surrogate' constructor is used to create inheritance relationship
          // without actually invoking the base class's constructor code
          $class_library._surrogateCtor.prototype = this._baseCtor.prototype;
          this._ctor.prototype = new $class_library._surrogateCtor();
          this._ctor.prototype.constructor = this._ctor;
        }

        // inherit info about the base class
        this._interfaces = $class_library._copyObject(this._baseCtor.$class._interfaces);
      }
      
      // store this class info on the prototype
      this._ctor.prototype._$class = this;
    }

    return;
  }

  if ($package._currentPackage) {
    name = $package._currentPackage.getName() + "." + name;
  }

  var baseCtor = descriptor.$extends || Object;
  var ctorName = name.replace(/[^.]*\./g, "");
  var ctor = descriptor.$constructor || descriptor[ctorName];

  var uses$base = /\bthis\.\$base\b/;
  var isTrivialCtor = /^\s*function[^(]*\([^)]*\)[^{]*\{\s*(return)?\s*;?\s*\}\s*$/;

  if (!ctor) {
    ctor = new Function();
  }
  
  if (isTrivialCtor.test(ctor)) {
    ctor._$class_isTrivialCtor = true;
    
    if (baseCtor != Object) {
      ctor._$class_relevantImplementation = baseCtor._$class_relevantImplementation || baseCtor;
    }
  }

  if (uses$base.test(ctor)) {
    ctor = $class._wrapExtendedMethod(ctor, baseCtor);
  } else if (!baseCtor._$class_isTrivialCtor) {
    ctor = $class._wrapExtendedCtor(ctor, baseCtor);
  }
  
  ctor.$class = new $class(name, {ctor:ctor, baseCtor:baseCtor, calledFrom$class:true});

  // implement interfaces
  if (descriptor.$implements != null) {
    var ifaces = descriptor.$implements;

    // convert to an array if it is a single object
    if (!(ifaces instanceof Array)) {
      ifaces = [ifaces];
    }

    for (var i = 0, ifacesLength = ifaces.length; i < ifacesLength; ++i) {
      $class_library._copyObject(ctor.$class._interfaces, ifaces[i]._interfaces);
    }
  }

  var specialProperties = {$constructor:true,$extends:true,$implements:true,$static:true};
  specialProperties[ctorName] = true;

  // process all properties in the class descriptor
  for (var propertyName in descriptor) {
    // skip over special properties
    if (specialProperties.hasOwnProperty(propertyName)) {
      continue;
    }

    var value = descriptor[propertyName];

    if (value instanceof $class._ModifiedProperty) {
      // In the release version, only the static modifier results in a
      // $class._ModifiedProperty object, so there is no need to check
      // the modifier value
      ctor[propertyName] = value.value;
      continue;
    }

    if (value instanceof Function && uses$base.test(value)) {
      // wrap the method to give it access to the special $base property
      value = $class._wrapExtendedMethod(value, ctor.prototype[propertyName]);
    }

    ctor.prototype[propertyName] = value;
  }

  // set default toString method
  if (ctor.prototype.toString == Object.prototype.toString) {
    ctor.prototype.toString = new Function(
      "return \"[object \" + $class.typeOf(this) + \"]\";"
    );
  }

  // store the constructor so it is accessible by the specified name
  eval(name + " = ctor;");

  //  call the static initializer
  if (descriptor.$static) {
    descriptor.$static.call(ctor);
  }
}

//##############################################################################

$class.prototype = {
  getName: function() {
    return this._name;
  },

  isNative: function() {
    return this._isNative;
  },

  getConstructor: function() {
    return this._ctor;
  },

  getSuperclass: function() {
    return this._baseCtor ? this._baseCtor.$class : null;
  },

  isInstance: function(obj) {
    return $class.instanceOf(obj, this._ctor);
  },

  implementsInterface: function(iface) {
    return this._interfaces.hasOwnProperty(iface.getName());
  },

  toString: function() {
    return "[$class " + this._name + "]";
  }
};

//##############################################################################

$class._wrapExtendedMethod = function(method, baseMethod) {
  var result = new Function(
    "var m=arguments.callee;" +
    "var h=this.$base;" +
    "this.$base=m._$class_baseMethod;" +
    "try{return m._$class_wrappedMethod.apply(this,arguments);}" +
    "finally{this.$base=h;}"
  );

  result._$class_wrappedMethod = method;
  result._$class_baseMethod = baseMethod._$class_relevantImplementation || baseMethod;
  result.toString = $class._wrappedMethod_toString;

  return result;
};

//##############################################################################

$class._wrapExtendedCtor = function(method, baseMethod) {
  var result = new Function(
    "arguments.callee._$class_baseMethod.apply(this,arguments);" +
    (method._$class_isTrivialCtor ?
      "" : // no need to call an empty function
      "arguments.callee._$class_wrappedMethod.apply(this,arguments);")
  );

  result._$class_wrappedMethod = method;
  result._$class_baseMethod = baseMethod._$class_relevantImplementation || baseMethod;
  result.toString = $class._wrappedMethod_toString;

  if (method._$class_isTrivialCtor) {
    result._$class_relevantImplementation = result._$class_baseMethod;
  }

  return result;
};

//##############################################################################

$class._wrappedMethod_toString = function() {
  return String(this._$class_wrappedMethod);
};

//##############################################################################

$class._ModifiedProperty = function(modifier, value) {
  this.modifier = modifier;
  this.value = value;
};

//##############################################################################

$class.resolve = function(qualifiedName) {
  try {
    return eval("(" + $class_library._getGlobalObjectName() + "." + qualifiedName + ")");
  } catch(error) {
    return undefined;
  }
};

//##############################################################################

$class.implementationOf = function(obj, iface) {
  return $class.getClass(obj).implementsInterface(iface);
};

//##############################################################################

$class.instanceOf = function(obj, type) {
  if (type instanceof $interface) {
    return $class.implementationOf(obj, type);
  }

  switch (typeof obj) {
    case "object":
      return (obj instanceof type) ||
             // special case for null
             (obj === null && type == Null) ||
             // allow RegExp to be considered an instance of Function
             (obj instanceof RegExp) && (type == Function);

    case "number":
      return (type == Number);

    case "string":
      return (type == String);

    case "boolean":
      return (type == Boolean);

    case "function":
      return (type == Function) ||
             // see if it's really a RegExp (because typeof identifies regular
             // expressions as functions in Firefox)
             (obj instanceof RegExp) && (type == RegExp);

    case "undefined":
      return (type == Undefined);
  }

  return false;
};

//##############################################################################

$class.typeOf = function(obj) {
  return $class.getClass(obj).getName();
};

//##############################################################################

$class.getClass = function(obj) {
  if (obj == null) {
    if (obj === undefined) {
      return Undefined.$class;
    }

    return Null.$class;
  }

  return obj._$class || Object.$class;
};

//##############################################################################

$class.instantiate = function(ctor, args) {
  if (ctor.$class && ctor.$class.isNative()) {
    return ctor.apply($class_library._getGlobalObject(), args);
  } else {
    $class_library._surrogateCtor.prototype = ctor.prototype;
    var result = new $class_library._surrogateCtor();
    ctor.apply(result, args);

    return result;
  }
};

//##############################################################################

function $interface(name, descriptor) {
  // if being called as a constructor...
  if (this instanceof $interface) {
    this._name = name;
    this._methods = {};
    this._methodsArray = null;
    this._interfaces = {};
    this._interfaces[name] = this;
    return;
  }

  if ($package._currentPackage) {
    name = $package._currentPackage.getName() + "." + name;
  }

  var iface = new $interface(name);

  // extend interfaces
  if (descriptor.$extends != null) {
    var ifaces = descriptor.$extends;

    // convert to an array if it is a single object
    if (!(ifaces instanceof Array)) {
      ifaces = [ifaces];
    }

    for (var i = 0, ifacesLength = ifaces.length; i < ifacesLength; ++i) {
      $class_library._copyObject(iface._methods, ifaces[i]._methods);
      $class_library._copyObject(iface._interfaces, ifaces[i]._interfaces);
    }
  }

  // process all properties in the descriptor
  for (var propertyName in descriptor) {
    // skip over the special properties
    if (propertyName == "$extends") {
      continue;
    }

    var value = descriptor[propertyName];

    if (value instanceof $class._ModifiedProperty) {
      iface[propertyName] = value.value;
    } else {
      iface._methods[propertyName] = iface._name;
    }
  }

  // store the interface so it can be referenced by the specified name
  eval(name + " = iface;");
}

//##############################################################################

$interface.prototype = {
  getName: function() {
    return this._name;
  },
  
  hasMethod: function(methodName) {
    return Boolean(this._methods[methodName]);
  },

  getMethods: function() {
    if (!this._methodsArray) {
      this._methodsArray = [];
      
      for (var methodName in this._methods) {
        this._methodsArray.push(methodName);
      }
    }
    
    return this._methodsArray;
  },

  toString: function() {
    return "[$interface " + this._name + "]";
  }
};

//##############################################################################

function $abstract(method) {
  // abstract modifier always ignored in release version
  return method;
}

function $static(value) {
  return new $class._ModifiedProperty("static", value);
}

function $final(method) {
  // final modifier always ignored in release version
  return method;
}

//##############################################################################

$package.$class   = new $class("$package",   {ctor:$package});
$class.$class     = new $class("$class",     {ctor:$class});
$interface.$class = new $class("$interface", {ctor:$interface});

Object.$class   = new $class("Object",   {isNative:true, ctor:Object});
Object._$class_isTrivialCtor = true;

Array.$class    = new $class("Array",    {isNative:true, ctor:Array});
String.$class   = new $class("String",   {isNative:true, ctor:String});
Number.$class   = new $class("Number",   {isNative:true, ctor:Number});
Boolean.$class  = new $class("Boolean",  {isNative:true, ctor:Boolean});
Function.$class = new $class("Function", {isNative:true, ctor:Function});
RegExp.$class   = new $class("RegExp",   {isNative:true, ctor:RegExp, baseCtor:Function});
Date.$class     = new $class("Date",     {isNative:true, ctor:Date});

Error.$class          = new $class("Error",          {isNative:true, ctor:Error});
EvalError.$class      = new $class("EvalError",      {isNative:true, ctor:EvalError,      baseCtor:Error});
RangeError.$class     = new $class("RangeError",     {isNative:true, ctor:RangeError,     baseCtor:Error});
ReferenceError.$class = new $class("ReferenceError", {isNative:true, ctor:ReferenceError, baseCtor:Error});
SyntaxError.$class    = new $class("SyntaxError",    {isNative:true, ctor:SyntaxError,    baseCtor:Error});
TypeError.$class      = new $class("TypeError",      {isNative:true, ctor:TypeError,      baseCtor:Error});
URIError.$class       = new $class("URIError",       {isNative:true, ctor:URIError,       baseCtor:Error});

//##############################################################################

$class("Undefined", {
  Undefined: $final(function() {
    throw new Error("Attempted instantiation of the Undefined class.");
  })
});

$class("Null", {
  Null: $final(function() {
    throw new Error("Attempted instantiation of the Null class.");
  })
});

//##############################################################################

$class("Exception", {
  $extends: Error,

  Exception: function(message) {
    this.message = String(message);
    this.name = $class.typeOf(this);
  },

  getName: $final(function() {
    return this.name;
  }),

  getMessage: $final(function() {
    return this.message;
  }),

  toString: $final(function() {
    return "[error " + this.getName() + "] " + this.getMessage();
  })
});

//##############################################################################
