text: var assert = require('assert');
  var util = require('util');
  var EventEmitter = require('events').EventEmitter;
  var traverse = require('traverse');
  var ModelBaseClass = require('./model');
  var ModelBuilder = require('./model-builder');
  
  /**
   * Model definition
   */
  module.exports = ModelDefinition;
  
  /**
   * Constructor for ModelDefinition
   * @param {ModelBuilder} modelBuilder A model builder instance
   * @param {String|Object} name The model name or the schema object
   * @param {Object} properties The model properties, optional
   * @param {Object} settings The model settings, optional
   * @returns {ModelDefinition}
   * @constructor
   *
   */
  function ModelDefinition(modelBuilder, name, properties, settings) {
    if (!(this instanceof ModelDefinition)) {
      // Allow to call ModelDefinition without new
      return new ModelDefinition(modelBuilder, name, properties, settings);
    }
    this.modelBuilder = modelBuilder || ModelBuilder.defaultInstance;
    assert(name, 'name is missing');
  
    if (arguments.length === 2 && typeof name === 'object') {
      var schema = name;
      this.name = schema.name;
      this.rawProperties = schema.properties || {}; // Keep the raw property definitions
      this.settings = schema.settings || {};
    } else {
      assert(typeof name === 'string', 'name must be a string');
      this.name = name;
      this.rawProperties = properties || {}; // Keep the raw property definitions
      this.settings = settings || {};
    }
    this.relations = [];
    this.properties = null;
    this.build();
  }
  
  util.inherits(ModelDefinition, EventEmitter);
  
  // Set up types
  require('./types')(ModelDefinition);
  
  /**
   * Return table name for specified `modelName`
   * @param {String} connectorType The connector type, such as 'oracle' or 'mongodb'
   */
  ModelDefinition.prototype.tableName = function (connectorType) {
    var settings = this.settings;
    if (settings[connectorType]) {
      return settings[connectorType].table || settings[connectorType].tableName || this.name;
    } else {
      return this.name;
    }
  };
  
  /**
   * Return column name for specified modelName and propertyName
   * @param {String} connectorType The connector type, such as 'oracle' or 'mongodb'
   * @param propertyName The property name
   * @returns {String} columnName
   */
  ModelDefinition.prototype.columnName = function (connectorType, propertyName) {
    if (!propertyName) {
      return propertyName;
    }
    this.build();
    var property = this.properties[propertyName];
    if (property && property[connectorType]) {
      return property[connectorType].column || property[connectorType].columnName || propertyName;
    } else {
      return propertyName;
    }
  };
  
  /**
   * Return column metadata for specified modelName and propertyName
   * @param {String} connectorType The connector type, such as 'oracle' or 'mongodb'
   * @param propertyName The property name
   * @returns {Object} column metadata
   */
  ModelDefinition.prototype.columnMetadata = function (connectorType, propertyName) {
    if (!propertyName) {
      return propertyName;
    }
    this.build();
    var property = this.properties[propertyName];
    if (property && property[connectorType]) {
      return property[connectorType];
    } else {
      return null;
    }
  };
  
  /**
   * Return column names for specified modelName
   * @param {String} connectorType The connector type, such as 'oracle' or 'mongodb'
   * @returns {String[]} column names
   */
  ModelDefinition.prototype.columnNames = function (connectorType) {
    this.build();
    var props = this.properties;
    var cols = [];
    for (var p in props) {
      if (props[p][connectorType]) {
        cols.push(property[connectorType].column || props[p][connectorType].columnName || p);
      } else {
        cols.push(p);
      }
    }
    return cols;
  };
  
  /**
   * Find the ID properties sorted by the index
   * @returns {Object[]} property name/index for IDs
   */
  ModelDefinition.prototype.ids = function () {
    if (this._ids) {
      return this._ids;
    }
    var ids = [];
    this.build();
    var props = this.properties;
    for (var key in props) {
      var id = props[key].id;
      if (!id) {
        continue;
      }
      if (typeof id !== 'number') {
        id = 1;
      }
      ids.push({name: key, id: id, property: props[key]});
    }
    ids.sort(function (a, b) {
      return a.key - b.key;
    });
    this._ids = ids;
    return ids;
  };
  
  /**
   * Find the ID column name
   * @param {String} modelName The model name
   * @returns {String} columnName for ID
   */
  ModelDefinition.prototype.idColumnName = function (connectorType) {
    return this.columnName(connectorType, this.idName());
  };
  
  /**
   * Find the ID property name
   * @returns {String} property name for ID
   */
  ModelDefinition.prototype.idName = function () {
    var id = this.ids()[0];
    if (this.properties.id && this.properties.id.id) {
      return 'id';
    } else {
      return id && id.name;
    }
  };
  
  /**
   * Find the ID property names sorted by the index
   * @returns {String[]} property names for IDs
   */
  ModelDefinition.prototype.idNames = function () {
    var ids = this.ids();
    var names = ids.map(function (id) {
      return id.name;
    });
    return names;
  };
  
  /**
   *
   * @returns {{}}
   */
  ModelDefinition.prototype.indexes = function () {
    this.build();
    var indexes = {};
    if (this.settings.indexes) {
      for (var i in this.settings.indexes) {
        indexes[i] = this.settings.indexes[i];
      }
    }
    for (var p in this.properties) {
      if (this.properties[p].index) {
        indexes[p + '_index'] = this.properties[p].index;
      }
    }
    return indexes;
  };
  
  /**
   * Build a model definition
   * @param {Boolean} force Forcing rebuild
   */
  ModelDefinition.prototype.build = function (forceRebuild) {
    if (forceRebuild) {
      this.properties = null;
      this.relations = [];
      this._ids = null;
    }
    if (this.properties) {
      return this.properties;
    }
    this.properties = {};
    for (var p in this.rawProperties) {
      var prop = this.rawProperties[p];
      var type = this.modelBuilder.resolveType(prop);
      if (typeof type === 'string') {
        this.relations.push({
          source: this.name,
          target: type,
          type: Array.isArray(prop) ? 'hasMany' : 'belongsTo',
          as: p
        });
      } else {
        var typeDef = {
          type: type
        };
        if (typeof prop === 'object' && prop !== null) {
          for (var a in prop) {
            // Skip the type property but don't delete it Model.extend() shares same instances of the properties from the base class
            if (a !== 'type') {
              typeDef[a] = prop[a];
            }
          }
        }
        this.properties[p] = typeDef;
      }
    }
    return this.properties;
  };
  
  /**
   * Define a property
   * @param {String} propertyName The property name
   * @param {Object} propertyDefinition The property definition
   */
  ModelDefinition.prototype.defineProperty = function (propertyName, propertyDefinition) {
    this.rawProperties[propertyName] = propertyDefinition;
    this.build(true);
  };
  
  function isModelClass(cls) {
    if (!cls) {
      return false;
    }
    return cls.prototype instanceof ModelBaseClass;
  }
  
  ModelDefinition.prototype.toJSON = function (forceRebuild) {
    if (forceRebuild) {
      this.json = null;
    }
    if (this.json) {
      return json;
    }
    var json = {
      name: this.name,
      properties: {},
      settings: this.settings
    };
    this.build(forceRebuild);
  
    var mapper = function (val) {
      if (val === undefined || val === null) {
        return val;
      }
      if ('function' === typeof val.toJSON) {
        // The value has its own toJSON() object
        return val.toJSON();
      }
      if ('function' === typeof val) {
        if (isModelClass(val)) {
          if (val.settings && val.settings.anonymous) {
            return val.definition && val.definition.toJSON().properties;
          } else {
            return val.modelName;
          }
        }
        return val.name;
      } else {
        return val;
      }
    };
    for (var p in this.properties) {
      json.properties[p] = traverse(this.properties[p]).map(mapper);
    }
    this.json = json;
    return json;
  };
  