/** var Class = pulp.cls; */
pulp.Modules.cls = 'Class';

(function() { 

  var $p = pulp.base,
    $void = {},
    slice = Array.prototype.slice;

  /**
   * Create a class with 'initialize' as the constructor
   * - the first object may be another class which to use as the parent
   *
   * @name pulp.cls
   * @namespace
   * @requires base
   */
  var Class = pulp.cls = {
    /**
     * @param {Object} [methods1]  The methods of the class (add to class prototype)
     * @param {Object} [methods2]  More methods
     * @param {Object} [methodsN]  As many sets of methods you need
     * @return {Function}
     */
    create: function() {
      var a = slice.call(arguments), cls;
      cls = (typeof a[0] == 'function' && typeof a[0].createSubclass == 'function' ? a.shift() : Class.Base).createSubclass();
      $p.extend.apply(null, [cls.prototype].concat(a));
      return cls;
    },
    /**
     * Create an abstract class (a regular object with methods extend() and alias())
     * @param {Object} [methods1]  The methods of the class
     * @param {Object} [methods2]  More methods
     * @param {Object} [methodsN]  As many sets of methods you need
     * @return {Object}
     */
    createAbstract: function() {
      var abstr = {
        /** @ignore - commented below */
        extend: $p.methodize($p.extend),
        /** @ignore - commented below*/
        alias: function(props) {
          for (var name in props) {
            this[name] = this[props[name]];
          }
        }
      };
      return abstr.extend.apply(slice.call(arguments));
    },
    /**
     * The base object from which all classes inherit
     * @name pulp.cls.Base
     */
    Base: function() {}
  };

  var makeSubclassable = function(parent) {
    // add static methods to the would-be parent class
    $p.extend(parent, /** @scope pulp.cls.Base */{
      subclasses: [],
      _PULP_CLS_ID: $p.getGuid(),
      /**
       * Create a subclass of this class
       * 
       * @param {Object} [methods1]  The methods of the class (add to class prototype)
       * @param {Object} [methods2]  More methods
       * @param {Object} [methodsN]  As many sets of methods you need
       * @return {Object}  The new subclass
       */
      createSubclass: function() {
        var ctor = function() {
          if (arguments[0] !== $void) {
            // if you get an error that "this.initialize" is not set,
            // the function is being used without the "new" operator
            this.initialize.apply(this, slice.call(arguments));
          }
        };
        ctor.prototype = new parent($void);     
        ctor.prototype.constructor = ctor;
        ctor.parent = parent;
        parent.subclasses.push(ctor);
        makeSubclassable(ctor);
        
        if (arguments.length) { // allow subsequent arguments to also extend prototype
          // m = "methods"
          $p.each(slice.call(arguments), function(m) {
            $p.extend(ctor.prototype, m);
          });
        }
        return ctor;
      },
      /**
       * Create instance methods that pass the object instance as the first parameter to the given set of static methods  
       *
       * @param {Object} methods  The set of static methods
       * @param {Object} [prop]  A property of the object instance to use instead of the object instance itself
       * @return {Object}  The class on which the method was called
       * @chainable
       * @example
       *   var MyNode = pulp.cls.create({
       *     initialize: function(id) {
       *       this.element = document.getElementById(id);
       *     }
       *   });
       *   var MyStatic = {
       *     hide: function(element) {
       *       element.style.display = 'none';
       *     }
       *   };
       *   MyNode.addMethods(MyStatic, 'element');
       *   new MyNode('my-id').hide();
       */
      addMethods: function(methods, prop) {
        for (var name in methods) {
          parent.prototype[name] = $p.methodize(methods[name], prop);
        }
        return parent;
      },
      /**
       * Extend the parent class with new properties
       *
       * @param {Object} properties  The properties to which to write to the class (as Class properties or methods)
       * @return {Object}  The class on which the method was called
       * @chainable
       */
      extend: function(properties) {
        return $p.extend(parent, properties);
      },
      /**
       * Extend the class prototype with the given methods (or properties)
       *
       * @param {Object} methods
       * @return {Object}  The class on which the method was called
       * @chainable
       */
      extendPrototype: function(methods) {        
        return $p.extend(parent.prototype, methods);
      },
      /**
       * Create aliases from alias-string pairs (static properties)
       * e.g. myClass.alias({alias: 'original'})
       * @param {Object} props
       * @return {Object}  The class on which the method was called
       * @chainable
       */
      alias: function(props) {
        for (var name in props) {
          parent[name] = prent[props[name]];
        }
        return parent;
      },      
      /**
       * Create aliases from alias-string pairs (prototype properties)
       * e.g. myClass.aliasMethods({alias: 'original'})
       *
       * @param {Object} methods  An object whose keys are aliases and values are names of existing methods
       * @return {Object}  The class on which the method was called
       * @chainable
       */
      aliasMethods: function(methods) {
        for (var alias in methods) {
          parent.prototype[alias] = parent.prototype[methods[alias]];
        }
        return parent;
      },
      /**
       * For each passed name (array or comma-separated string),
       * create a class method that directly calls a method of that name on the raw object
       *
       * @param {Array|String} names
       * @param {Boolean|Function} [wrapperClass]  If true, return the instance; if falsy return the result;
       *                                           if a function, return an instance of the function with the result passed to the constructor
       * @param {Object} [property=raw]  The property of the object to wrap
       * @return {Object}  The class on which the method was called
       * @chainable
       */
      createWrappers: function(names, wrapperClass, property) {
        $p.each(names, function(name) {
          parent.prototype[name] = function() {
            var raw = this[property || 'raw'];
            var result = raw[name].apply(raw, slice.call(arguments));
            if (wrapperClass === true) {
              return this;
            }
            return wrapperClass ? new wrapperClass(result) : result;
          };
        });
        return parent;
      }  
    });
  };
  makeSubclassable(Class.Base);
  
  Class.Base.extendPrototype(/** @lends pulp.cls.Base# */{
    /**
     * The default (empty) constructor
     * @constructs
     * @type {Function}
     */
    initialize: pulp.EmptyFunction,
    /**
     * Call the nearest ancestor's method with the given array of arguments
     * @param {String} method  The name of the parent method
     * @param {Array} [args]  Arguments to pass to the parent method
     * @return {Mixed} The result of the call to the parent function
     */
    applyParent: function(method, args) {
      return this.constructor.parent.prototype[method].apply(this, slice.call(args))
    },
    /**
     * Call the nearest ancestor's method with the given arguments
     * @param {String} method  The name of the parent method
     * @param {Mixed} [arg1]
     * @param {Mixed} [arg2]
     * @param {Mixed} [argN]
     * @return {Mixed} The result of the call to the parent function
     */
    callParent: function() {
      var args = slice.call(arguments);
      return this.constructor.parent.prototype[args.shift()].apply(this, args);
    }    
  });
  
})();