/*  PrototypeX for Prototype, version 0.1
 *  (c) 2008 Thierry Jossermoz (except when stated otherwise)
 *
 *  PrototypeX is freely distributable under the terms of an MIT-style license.
 *
 *--------------------------------------------------------------------------*/


Object.extend(Prototype, {
  globalScope: (function() { return this; })(),
  
  options: {
    debugMode: false
  },
  
  findScriptByName: function(scriptName) {
    return $$('script').find(function(s) {
      return s.src && s.src.match(new RegExp(scriptName + ".js"));
    });
  },
  getScriptPath: function(scriptName) {
    if (scriptElement = this.findScriptByName(scriptName))
      return scriptElement.src.match(new RegExp("(.*)/" + scriptName + ".js"))[1];
  },
  getScriptParams: function(scriptName) {
    if (scriptElement = this.findScriptByName(scriptName))
      return scriptElement.src.toQueryParams();
  }
});

Object.extend(Prototype.options, Prototype.getScriptParams('prototypeX'));

Object.extend(Object, {
  isInterface: function(object) {
    if (Object.inspect(object).startsWith('#<Interface'))
      return true;
    return false;
  },
  isAbstract: function(object) {
    if (Object.inspect(object).startsWith('#<Class.Abstract'))
      return true;
    return false;
  },
  isInstanceOf: function(object, klass) {
    if (object instanceof klass)
      return true;
    if (Object.isInterface(klass))
      try { Object.ensureImplements(object, klass); return true; } catch(e) {}
    return false;
  },
  ensureImplements: function(object, interfaces) {
    Interface.check(object, interfaces);
  },
  ensureConcretize: function(object, abstrakt) {
    Class.Abstract.check(object, abstrakt);
  }
});

Object.extend(Array.prototype, {
  add: function(item) {
    if (this.indexOf(item) === -1)
      this.push(item);
  },
  remove: function(item) {
    if ((index = this.indexOf(item)) > -1)
      this.splice(index, 1); 
  }
});

Object.extend(Hash.prototype, {
  isset: function(key) {
    return this._object[key] !== Object.prototype[key];
  }
});

/**
 * Library
 */

var Library = (function() {
  var _registered = $H();
  return {
    register: function(name, path) {
      _registered.set(name, path);
    },
    isRegistered: function(name) {
      return _registered.isset(name);
    },
    getPath: function(name) {
      return _registered.get(name);
    }
  }
})();


/**
 * Module
 *
 * @todo: Add better support for failed required
 */

var Module = (function() {
  
  var _ModuleManager = {
    _required: $A(),
    _expected: $H(),
    _declared: $H(),

    _addExpected: function(name, module) {
      // create if not existing
      if (!this._expected.isset(name)) {
        this._expected.set(name, $A());
      }
      // push module to waiting list
      this._expected.get(name).add(module);
    },
    _removeExpected: function(name) {
      if (this._expected.isset(name)) {
        this._expected.get(name).clone().each(function(waitingModule) {
          // waiting module remove declared module from its expected dependencies list
          waitingModule.expectedDependencies.remove(name);

          // declared module remove the waiting module from its waiting list
          this._expected.get(name).remove(waitingModule);

          // remove from expected list if no more module waiting
          if (this._expected.get(name).size() == 0)
            this._expected.unset(name);

          // declare waiting module if no more dependency expected
          if (waitingModule.expectedDependencies.size() == 0)
            this._addDeclared(waitingModule);

        }, this);
      }
    },
    
    _addWaiting: function(module) {
      // module register itself to the waiting list of each dependency
      module.expectedDependencies.each(function(dependency) {
        this._addExpected(dependency, module);
      }, this);
    },

    _addDeclared: function(module) {
      this._declared.set(module.name, module);
      // dependencies are loaded, module declaration function can be executed safely
      module.callback();

      // dispatch module:loaded event
      document.fire('module:loaded', {module: module.name});

      // inform modules in waiting list
      this._removeExpected(module.name);
    },

    addRequired: function(name) {
      this._required.add(name);
    },
    isRequired: function(name) {
      return this._required.member(name);
    },
    isDeclared: function(name) {
      return this._declared.get(name) ? true : false;
    },

    addLoaded: function(module) {
      // remove declared modules from expected dependencies
      this._declared.keys().each(function(name) {
        module.expectedDependencies.remove(name);
      }, this);
      // if expected dependencies already loaded, declare, else add to waiting list
      if (module.expectedDependencies.size() == 0)
        this._addDeclared(module);
      else
        this._addWaiting(module);
    }
  };

  var _Module = function(name, dependencies, callback) {
    this.name = name;
    this.dependencies = dependencies;
    this.expectedDependencies = dependencies.clone();
    this.callback = callback;
    return this;
  };

  // dom is treated as a module
  _ModuleManager.addRequired('dom');
  document.observe('dom:loaded', function() { 
    _ModuleManager.addLoaded(new _Module('dom', [], Prototype.emptyFunction));
  });

  return {
    namespace: function(namespace, context) {
      var names = namespace.split('.'), current = names.shift();
      context = context || Prototype.globalScope;
      context[current] = context[current] || new Object();
      if (names.size() > 0) this.namespace(names.join('.'), context[current]);
    },

    create: function(name) {
      var options = Object.extend({
        dependencies: [],
        declare: Prototype.emptyFunction
      }, arguments[1] || {});
      // append dom to dependencies and add module to loaded list
      _ModuleManager.addLoaded(new _Module(name, options.dependencies.concat('dom'), options.declare));

      options.dependencies.each(this.require, this);
    },

    isDeclared: function(name) {
      return _ModuleManager.isDeclared(name);
    },

    require: function(name, enforce) {
      if (_ModuleManager.isRequired(name) && Object.isUndefined(enforce))
        return;

      _ModuleManager.addRequired(name);

      var moduleName = name.split('.'), libraryName = moduleName.shift();

      if (!Library.isRegistered(libraryName))
        throw("Library '" + libraryName + "' not registered. Use 'Library.register(name, path)'.");

      var that = this;
      new Ajax.Request(Library.getPath(libraryName) + '/' + moduleName.join('/') + '.js', {
        method: 'get',
        // auto eval JS on success which execute Module.create
        evalJS: true,
        onFailure: function() {
          // retry after 500ms
          window.setTimeout(function() {
            that.require(name, true);
          }, 500);
        }
      });
    }
  };
})();

/**
 * Theme
 */

var Theme = (function() {
  var classNames = [];

  if(Prototype.Browser.IE) {
    classNames.push('ie');
    classNames.push('ie' + parseInt(/MSIE ([^\;]*);/.exec(navigator.userAgent)[1]));
  } else if (Prototype.Browser.Gecko) {
    classNames.push('firefox');
  } else if (Prototype.Browser.Opera) {
    classNames.push('opera');
  } else if (Prototype.Browser.WebKit) {
    classNames.push('safari');
  } else if (Prototype.Browser.MobileSafari) {
    classNames.push('mobileSafari');
  }
  Element.addClassName($$('html')[0], classNames.join(' '));

  var _ThemeManager = {
    _declared: $A(),
    addDeclared: function(name) {
      this._declared.add(name);
    },
    isDeclared: function(name) {
      return this._declared.member(name);
    }
  };

  return {
    require: function(name) {
      if (_ThemeManager.isDeclared(name))
        return;
      _ThemeManager.addDeclared(name);
      
      var themeName = name.split('.'), libraryName = themeName.shift();

      if (!Library.isRegistered(libraryName))
        throw("Library '" + libraryName + "' not registered. Use 'Library.register(name, path)'.");

      document.write('<style type="text/css">@import url("' + Library.getPath(libraryName) + '/' + themeName.join('/') + '/' + themeName.last() + '.css");</style>');
    }
  };
})();

Class = Object.extend(Class, {
  // Prototype's Class.create function modified to support Class.Abstract and Interfaces (but fully compatible)
  create: function() {
    var parent = null, interfaces = [], properties = $A(arguments);
      
    if (Object.isFunction(properties[0]) && !Object.isInterface(properties[0]))
      parent = properties.shift();
  
    if (Object.isArray(properties[0]) || Object.isInterface(properties[0]))
      interfaces = [].concat(properties.shift());
  
    function klass() {
      this.initialize.apply(this, arguments);
    }
  
    Object.extend(klass, Class.Methods);
    klass.superclass = parent;
    klass.subclasses = [];
    klass.interfaces = interfaces;
  
    if (parent) {
      var subclass = function() { };
      subclass.prototype = parent.prototype;
      klass.prototype = new subclass;
      parent.subclasses.push(klass);
  
      if (parent.interfaces && parent.interfaces.length > 0)
        klass.interfaces = klass.interfaces.concat(parent.interfaces).uniq()
    }
  
    for (var i = 0; i < properties.length; i++)
      klass.addMethods(properties[i]);
  
    if (!klass.prototype.initialize)
      klass.prototype.initialize = Prototype.emptyFunction;
  
    klass.prototype.constructor = klass;
  
    if (klass.interfaces.length > 0)
      Object.ensureImplements(klass.prototype, klass.interfaces);
  
    if (Object.isAbstract(parent))  
      Object.ensureConcretize(klass.prototype, klass.superclass);
  
    return klass;
  },

  Abstract: {
    create: function() {
      var parent = null, interfaces = [], properties = $A(arguments);
      
      if (Object.isFunction(properties[0]) && !Object.isInterface(properties[0]))
        parent = properties.shift();
      
      if (Object.isArray(properties[0]) || Object.isInterface(properties[0]))
        interfaces = [].concat(properties.shift());
      
      function klass() {
        throw("Cannot instantiate abstract class");
      }
      
      Object.extend(klass, Class.Methods);
      klass.superclass = parent;
      klass.subclasses = [];
      klass.interfaces = interfaces;
  
      if (parent) {
        subclass = function() { };
        subclass.prototype = parent.prototype;
        klass.prototype = new subclass;
        parent.subclasses.push(klass);
        if (parent.interfaces && parent.interfaces.length > 0)
          klass.interfaces = klass.interfaces.concat(parent.interfaces).uniq();
      }
        
      for (var i = 0; i < properties.length; i++)
        klass.addMethods(properties[i]);
      
      klass.prototype.constructor = klass;
  
      klass.inspect = function() {
        var description = '#<Class.Abstract:' + Object.toJSON(klass.prototype) + '>';
        klass.inspect = function() { return description; };
        return klass.inspect();
      }
      return klass;
    },
    method: function(arity) {
      if (Object.isArray(arity) || Object.isUndefined(arity))
        arity = arity.length || 0;
      
      if (!Object.isNumber(arity))
        throw("Invalid arity for abstract method");
    
      function abstraktMethod() {
        throw("Cannot call abstract method");
      }
      abstraktMethod.inspect = function() {
        return '#<Abstract.Class.method:{arity:' + abstraktMethod.arity + '}>';
      }
      abstraktMethod.arity = arity;
    
      return abstraktMethod;
    },
    attribute: function() {
      function abstraktAttribute() {
        throw("Cannot access abstract attribute");
      }
      abstraktAttribute.inspect = function() {
        return '#<Abstract.Class.attribute:{}>';
      }
      return abstraktAttribute;
    },
    check: function(object, abstrakt) {
      if (!object instanceof abstrakt)
        throw("object is not an instance of " + Object.inspect(abstrakt));

      if (!Prototype.options.debugMode)
        return;

      for(var property in abstrakt.prototype) {
        if (Object.isAbstract(abstrakt.prototype[property])) {
          if (Object.isAbstract(object[property]))
            throw("Abstract property '" + property + "' defined as " + Object.inspect(object[property]) + " has not been implemented. Please implement the property or declare the class as being abstract.");
          else if (Object.isFunction(object[property]) && !abstrakt.prototype[property].arity)
            throw("Abstract attribute '" + property + "' defined as " + Object.inspect(object[property]) + " has not been implemented");
          else if (Object.isFunction(object[property]) && object[property].argumentNames().length !== abstrakt.prototype[property].arity)
            throw("Abstract method '" + property + "' defined as " + Object.inspect(object[property]) + " has been implemented with an invalid arity of " + object[property].argumentNames().length);
        }
      }
    }
  }
});

var Interface = {
  create: function() {
    var parent = null, properties = $A(arguments);
    
    if (Object.isInterface(properties[0]))
      parent = properties.shift();
    
    function klass() { 
      throw('Cannot instantiate interface');
    };

    klass.superclass = parent;
    klass.subclasses = [];

    if (parent) {
      var subclass = function() { };
      subclass.prototype = parent.prototype;
      klass.prototype = new subclass;
      parent.subclasses.push(klass);
    }

    for (var i = 0; i < properties.length; i++)
      for (var property in properties[i]) {
        var arity = null;
        if (Object.isArray(properties[i][property]))
          arity = properties[i][property].length
        else if (Object.isNumber(properties[i][property]))
          arity = properties[i][property];
        else
          throw('Invalid arity for method ' + property);

        klass.prototype[property] = arity;
      }

    klass.inspect = function() {
      var description = '#<Interface:' + Object.toJSON(klass.prototype) + '>';
      klass.inspect = function() { return description; };
      return klass.inspect();
    }
    
    return klass;
  },
  check: function(object, interfaces) {
    interfaces = [].concat(interfaces);
    
    if (object.constructor && object.constructor.interfaces) {
      var declared = object.constructor.interfaces;
      if (!interfaces.all(function(i) { return declared.indexOf(i) > -1 }))
        throw("The provided class does not implement the specified interface");
    }
          
    if (!Prototype.options.debugMode)
      return;
          
    for (var i = 0; i < interfaces.length; i++) {
      if (Object.isInterface(interfaces[i])) {
        for(var property in interfaces[i].prototype) {
          var error = null;
          if (!object[property])
            error = "Method '" + property + "' not found";
          else if (!Object.isFunction(object[property]))
            error = "'" + property + "' is not a function";
          else if (!(object[property].argumentNames().length == interfaces[i].prototype[property]))
            error = "Method '" + property + "' has an arity of " + object[property].argumentNames().length + " but " + interfaces[i].prototype[property] + " expected";
          if (error)
            throw("The provided class does not implement " + interfaces[i].inspect() + '. ' + error);
        }
      }
    }
  }
}

var Cookie = {
  set: function(name, value, expire, path) {
    var expirationDate = new Date();
    expirationDate.setDate(expirationDate.getDate()+(expire === true?365:(expire === -1?'-1':expire)));
    document.cookie = name + '=' + escape(value)
                    + (expire? '; expires=' + expirationDate.toGMTString() : '')
                    + (path ? '; path=' + path : '; path=/'); 
  },
  get: function(name) {
    if (this.isset(name)) {
      var start = document.cookie.indexOf(name + '=') + name.length + 1;
      var end = document.cookie.indexOf(';', start);
      if (end === -1) end = document.cookie.length;
      return unescape(document.cookie.substring(start, end));
    }
    return null;
  },
  unset: function(name) {
    this.set(name, null, -1);
  },
  isset: function(name) {
    return document.cookie.indexOf(name + '=') !== -1;
  }
}