/*
 * JSFace Object Oriented Programming Library
 * https://github.com/tnhu/jsface
 *
 * Copyright (c) Tan Nhu
 * Licensed under MIT license (https://github.com/tnhu/jsface/blob/master/LICENSE.txt)
 */
(function(context, OBJECT, NUMBER, LENGTH, toString, readyFns, readyCount, undefined, oldClass, jsface) {
  /**
   * Return a map itself or null. A map is a set of { key: value }
   * @param obj object to be checked
   * @return obj itself as a map or false
   */
  function mapOrNil(obj) { return (obj && typeof obj === OBJECT && !(typeof obj.length === NUMBER && !(obj.propertyIsEnumerable(LENGTH))) && obj) || null; }

  /**
   * Return an array itself or null
   * @param obj object to be checked
   * @return obj itself as an array or null
   */
  function arrayOrNil(obj) { return (obj && typeof obj === OBJECT && typeof obj.length === NUMBER && !(obj.propertyIsEnumerable(LENGTH)) && obj) || null; }

  /**
   * Return a function itself or null
   * @param obj object to be checked
   * @return obj itself as a function or null
   */
  function functionOrNil(obj) { return (obj && typeof obj === "function" && obj) || null; }

  /**
   * Return a class itself or null
   * @param obj object to be checked
   * @return obj itself as a class or false
   */
  function classOrNil(obj) { return (functionOrNil(obj) && (obj.prototype && obj === obj.prototype.constructor) && obj) || null; }

  /**
   * Util for extend() to copy a map of { key:value } to an object
   * @param key key
   * @param value value
   * @param ignoredKeys ignored keys
   * @param object object
   */
  function copier(key, value, ignoredKeys, object) {
    if ( !ignoredKeys || !ignoredKeys.hasOwnProperty(key)) {
      object[key] = value;
    }
  }

  /**
   * Extend object from subject, ignore properties in ignoredKeys
   * @param object the child
   * @param subject the parent
   * @param ignoredKeys (optional) keys should not be copied to child
   */
  function extend(object, subject, ignoredKeys) {
    if (arrayOrNil(subject)) {
      for (var len = subject.length; --len >= 0;) { extend(object, subject[len], ignoredKeys); }
    } else {
      ignoredKeys = ignoredKeys || { constructor: 1, $super: 1, prototype: 1, $superp: 1 };

      var iClass     = classOrNil(object),
          isSubClass = classOrNil(subject),
          oPrototype = object.prototype, key, proto;

      // copy static properties and prototype.* to object
      if (mapOrNil(subject) || iClass) {
        for (key in subject) {
          copier(key, subject[key], ignoredKeys, object, iClass, oPrototype);
        }
      }

      if (isSubClass) {
        proto = subject.prototype;
        for (key in proto) {
          copier(key, proto[key], ignoredKeys, object, iClass, oPrototype);
        }
      }

      // prototype properties
      if (iClass && isSubClass) { extend(oPrototype, subject.prototype, ignoredKeys); }
    }
  }

  /**
   * To make object fully immutable, freeze each object inside it.
   * @param object to deep freeze
   */
  function deepFreeze(object) {
    var prop, propKey;
    Object.freeze(object); // first freeze the object
    for (propKey in object) {
      prop = object[propKey];
      if (!object.hasOwnProperty(propKey) || (typeof prop !== 'object') || Object.isFrozen(prop)) {
        // If the object is on the prototype, not an object, or is already frozen,
        // skip it. Note that this might leave an unfrozen reference somewhere in the
        // object if there is an already frozen object containing an unfrozen object.
        continue;
      }

      deepFreeze(prop); // recursively call deepFreeze
    }
  }

  /**
   * Create a class.
   * @param parent parent class(es)
   * @param api class api
   * @return class
   */
  function Class(parent, api) {
    if ( !api) {
      parent = (api = parent, 0); // !api means there's no parent
    }

    // TODO remove $statics, use $static instead
    var clazz, constructor, singleton, statics, key, bindTo, len, i = 0, p,
        ignoredKeys = { constructor: 1, $singleton: 1, $static:1, $statics: 1, prototype: 1, $super: 1, $superp: 1, main: 1, toString: 0 },
        plugins     = Class.plugins,
        rootParent, parentClass, Stub, descriptor;

    api         = (typeof api === "function" ? api() : api) || {};             // execute api if it's a function
    constructor = api.hasOwnProperty("constructor") ? api.constructor : null;  // hasOwnProperty is a must, constructor is special
    singleton   = api.$singleton;
    statics     = api.$statics || api.$static;

    // add plugins' keys into ignoredKeys
    for (key in plugins) { ignoredKeys[key] = 1; }

    // make sure parent be always an array
    parent = !parent || parent instanceof Array ? parent : [ parent ];
    len = parent && parent.length;
    rootParent = parent[0];

    // construct constructor
    clazz = singleton ? function(){} : (constructor ? constructor : function(){
      if (rootParent) {
        rootParent.apply(this, arguments);
      }
    });

    if ( !singleton && len) {
      parentClass = rootParent.prototype && rootParent === rootParent.prototype.constructor && rootParent;

      if ( !parentClass) {
        clazz.prototype = rootParent;
      } else {
        // Constributed by Freddy Snijder (https://github.com/tnhu/jsface/issues/26)
        Stub = function(){};

        Stub.prototype                    = parentClass.prototype;
        Stub.prototype.constructor        = Stub;
        clazz.prototype                   = new Stub();
        clazz.prototype.constructor       = clazz;       // restoring proper constructor for child class
        parentClass.prototype.constructor = parentClass; // restoring proper constructor for parent class
      }
    }

    // determine bindTo: where api should be bound
    bindTo = singleton ? clazz : clazz.prototype;

    // do inherit static properties and extentions (parents other than the first one)
    while (i < len) {
      p = parent[i++];

      // copy static properties
      for (key in p) {
        if ( !ignoredKeys[key]) {
          clazz[key] = p[key];
        }
      }
      if ( !singleton && i !== 0) {
        for (key in p.prototype) {
          if ( !ignoredKeys[key]) {
            descriptor = Object.getOwnPropertyDescriptor(p.prototype, key);                                    // Fix #37
            bindTo[key] = (descriptor && (descriptor.get || descriptor.set)) ? descriptor : p.prototype[key]; 
          }
        }
      }
    }

    // copy properties from api to bindTo
    for (key in api) {
      if ( !ignoredKeys[key]) {
        var prop = api[key];

        if (prop && (prop.get || prop.set)) {                 // check if it is a property descriptor
          prop.enumerable = true;
          Object.defineProperty(bindTo, key, prop);
        } else {
          bindTo[key] = prop;
        }
      }
    }

    // copy static properties from statics to clazz
    for (key in statics) {
      clazz[key] = statics[key];
    }

    // add $super and $superp to refer to parent class and parent.prototype (if applied)
    p = parent && rootParent || parent;
    clazz.$super  = p;
    clazz.$superp = p && p.prototype || p;

    for (key in plugins) { plugins[key](clazz, parent, api); }            // pass control to plugins
    if (typeof api.main === "function") { api.main.call(clazz, clazz); }  // execute main()

    return clazz;
  }

  /* Class plugins repository */
  Class.plugins = {
    $ready: function invoke(clazz, parent, api, loop) {
      var r       = api.$ready,
          len     = parent ? parent.length : 0,
          count   = len,
          _super  = len && parent[0].$super,
          pa, i, entry;

      // find and invoke $ready from parent(s)
      while (len--) {
        for (i = 0; i < readyCount; i++) {
          entry = readyFns[i];
          pa    = parent[len];

          if (pa === entry[0]) {
            entry[1].call(pa, clazz, parent, api);
            count--;
          }

          if ( !count) { break; }
        }
      }

      // call $ready from grandparent(s), if any
      if (_super) {
        invoke(clazz, [ _super ], api, true);
      }

      // in an environment where there are a lot of class creating/removing (rarely)
      // this implementation might cause a leak (saving pointers to clazz and $ready)
      if ( !loop && functionOrNil(r)) {
        r.call(clazz, clazz, parent, api);  // invoke ready from current class
        readyFns.push([ clazz,  r ]);
        readyCount++;
      }
    },

    $const: function (clazz, parent, api) {
      var key,
          consts = api.$const;

      // copy immutable properties from consts to clazz and freeze them recursively
      for (key in consts) {
        Object.defineProperty(clazz, key, { enumerable: true, value: consts[key] }); // enumerable for proper inheritance

        if ((typeof clazz[key] === 'object') && !Object.isFrozen(clazz[key])) {
          deepFreeze(clazz[key]); // if property is an unfrozen object, freeze it recursively
        }
      }
    }
  };

  /* Initialization */
  jsface = {
    Class        : Class,
    extend       : extend,
    mapOrNil     : mapOrNil,
    arrayOrNil   : arrayOrNil,
    functionOrNil: functionOrNil,
    classOrNil   : classOrNil
  };

  if (typeof module !== "undefined" && module.exports) {                       // NodeJS/CommonJS
    module.exports = jsface;
  } else {
    oldClass          = context.Class;                                         // save current Class namespace
    context.Class     = Class;                                                 // bind Class and jsface to global scope
    context.jsface    = jsface;
    jsface.noConflict = function() { context.Class = oldClass; };              // no conflict
  }
})(this, "object", "number", "length", Object.prototype.toString, [], 0);