/**
 * Module exports class Model
 */
module.exports = ModelBaseClass;

/**
 * Module dependencies
 */
 
var util = require('util');
var traverse = require('traverse');
var jutil = require('./jutil');
var List = require('./list');
var Hookable = require('./hooks');
var validations = require('./validations.js');

var BASE_TYPES = ['String', 'Boolean', 'Number', 'Date', 'Text'];

/**
 * Model class - base class for all persist objects
 * provides **common API** to access any database connector.
 * This class describes only abstract behavior layer, refer to `lib/connectors/*.js`
 * to learn more about specific connector implementations
 *
 * `ModelBaseClass` mixes `Validatable` and `Hookable` classes methods
 *
 * @constructor
 * @param {Object} data - initial object data
 */
function ModelBaseClass(data) {
    this._initProperties(data, true);
}

// FIXME: [rfeng] We need to make sure the input data should not be mutated. Disabled cloning for now to get tests passing
function clone(data) {
    /*
    if(!(data instanceof ModelBaseClass)) {
        if(data && (Array.isArray(data) || 'object' === typeof data)) {
            return traverse(data).clone();
        }
    }
    */
    return data;
}
/**
 * Initialize properties
 * @param data
 * @param applySetters
 * @private
 */
ModelBaseClass.prototype._initProperties = function (data, applySetters) {
    var self = this;
    var ctor = this.constructor;
    
    var properties = ctor.properties;
    data = data || {};

    Object.defineProperty(this, '__cachedRelations', {
        writable: true,
        enumerable: false,
        configurable: true,
        value: {}
    });

    Object.defineProperty(this, '__data', {
        writable: true,
        enumerable: false,
        configurable: true,
        value: {}
    });

    Object.defineProperty(this, '__dataWas', {
        writable: true,
        enumerable: false,
        configurable: true,
        value: {}
    });

    if (data['__cachedRelations']) {
        this.__cachedRelations = data['__cachedRelations'];
    }

    // Check if the strict option is set to false for the model
    var strict = ctor.settings.strict;

    for (var i in data) {
        if (i in properties) {
            this.__data[i] = this.__dataWas[i] = clone(data[i]);
        } else if (i in ctor.relations) {
            this.__data[ctor.relations[i].keyFrom] = this.__dataWas[i] = data[i][ctor.relations[i].keyTo];
            this.__cachedRelations[i] = data[i];
        } else {
            if(strict === false) {
                this.__data[i] = this.__dataWas[i] = clone(data[i]);
            } else if(strict === 'throw') {
                throw new Error('Unknown property: ' + i);
            }
        }
    }

    if (applySetters === true) {
        Object.keys(data).forEach(function (attr) {
            if((attr in properties) || (attr in ctor.relations)) {
                self[attr] = self.__data[attr] || data[attr];
            }
        });
    }

    // Set the unknown properties as properties to the object
    if(strict === false) {
        Object.keys(data).forEach(function (attr) {
            if(!(attr in properties)) {
                self[attr] = self.__data[attr] || data[attr];
            }
        });
    }

    ctor.forEachProperty(function (attr) {

        if ('undefined' === typeof self.__data[attr]) {
            self.__data[attr] = self.__dataWas[attr] = getDefault(attr);
        } else {
            self.__dataWas[attr] = self.__data[attr];
        }

    });

    ctor.forEachProperty(function (attr) {

        var type = properties[attr].type;
        
        if (BASE_TYPES.indexOf(type.name) === -1) {
            if (typeof self.__data[attr] !== 'object' && self.__data[attr]) {
                try {
                    self.__data[attr] = JSON.parse(self.__data[attr] + '');
                } catch (e) {
                    self.__data[attr] = String(self.__data[attr]);
                }
            }
            if (type.name === 'Array' || Array.isArray(type)) {
                if(!(self.__data[attr] instanceof List)) {
                    self.__data[attr] = new List(self.__data[attr], type, self);
                }
            }
        }

    });

    function getDefault(attr) {
        var def = properties[attr]['default'];
        if (isdef(def)) {
            if (typeof def === 'function') {
                return def();
            } else {
                return def;
            }
        } else {
            return undefined;
        }
    }

    this.trigger('initialize');
}

/**
 * @param {String} prop - property name
 * @param {Object} params - various property configuration
 */
ModelBaseClass.defineProperty = function (prop, params) {
    this.dataSource.defineProperty(this.modelName, prop, params);
};

ModelBaseClass.whatTypeName = function (propName) {
    var prop = this.properties[propName];
    if(!prop) {
        // The property is not part of the definition
        return null;
    }
    if (!prop.type) {
        throw new Error('Type not defined for property ' + this.modelName + '.' + propName);
        // return null;
    }
    return prop.type.name;
};

ModelBaseClass.prototype.whatTypeName = function (propName) {
    return this.constructor.whatTypeName(propName);
};

/**
 * Return string representation of class
 *
 * @override default toString method
 */
ModelBaseClass.toString = function () {
    return '[Model ' + this.modelName + ']';
};

/**
 * Convert instance to Object
 *
 * @param {Boolean} onlySchema - restrict properties to dataSource only, default false
 * when onlySchema == true, only properties defined in dataSource returned,
 * otherwise all enumerable properties returned
 * @returns {Object} - canonical object representation (no getters and setters)
 */
ModelBaseClass.prototype.toObject = function (onlySchema) {
    var data = {};
    var self = this;

    var schemaLess = this.constructor.settings.strict === false || !onlySchema;
    this.constructor.forEachProperty(function (attr) {
        if (self[attr] instanceof List) {
            data[attr] = self[attr].toObject(!schemaLess);
        } else if (self.__data.hasOwnProperty(attr)) {
            if(self[attr] !== undefined && self[attr]!== null && self[attr].toObject) {
                data[attr] = self[attr].toObject(!schemaLess);
            } else {
                data[attr] = self[attr];
            }
        } else {
            data[attr] = null;
        }
    });

    if (schemaLess) {
        Object.keys(self.__data).forEach(function (attr) {
            if (!data.hasOwnProperty(attr)) {
                var val = self.hasOwnProperty(attr) ? self[attr] : self.__data[attr];
                if(val !== undefined && val!== null && val.toObject) {
                    data[attr] = val.toObject(!schemaLess);
                } else {
                    data[attr] = val;
                }
            }
        });
    }
    return data;
};

// ModelBaseClass.prototype.hasOwnProperty = function (prop) {
//     return this.__data && this.__data.hasOwnProperty(prop) ||
//         Object.getOwnPropertyNames(this).indexOf(prop) !== -1;
// };

ModelBaseClass.prototype.toJSON = function () {
    return this.toObject();
};

ModelBaseClass.prototype.fromObject = function (obj) {
    Object.keys(obj).forEach(function (key) {
        this[key] = obj[key];
    }.bind(this));
};

/**
 * Checks is property changed based on current property and initial value
 *
 * @param {String} attr - property name
 * @return Boolean
 */
ModelBaseClass.prototype.propertyChanged = function propertyChanged(attr) {
    return this.__data[attr] !== this.__dataWas[attr];
};

/**
 * Reset dirty attributes
 *
 * this method does not perform any database operation it just reset object to it's
 * initial state
 */
ModelBaseClass.prototype.reset = function () {
    var obj = this;
    Object.keys(obj).forEach(function (k) {
        if (k !== 'id' && !obj.constructor.dataSource.definitions[obj.constructor.modelName].properties[k]) {
            delete obj[k];
        }
        if (obj.propertyChanged(k)) {
            obj[k] = obj[k + '_was'];
        }
    });
};

ModelBaseClass.prototype.inspect = function () {
    return util.inspect(this.__data, false, 4, true);
};

/**
 * Check whether `s` is not undefined
 * @param {Mixed} s
 * @return {Boolean} s is undefined
 */
function isdef(s) {
    var undef;
    return s !== undef;
}

ModelBaseClass.mixin = function(anotherClass, options) {
    return jutil.mixin(this, anotherClass, options);
}

jutil.mixin(ModelBaseClass, Hookable);
jutil.mixin(ModelBaseClass, validations.Validatable);