var Utils        = require("./utils")
  , Mixin        = require("./associations/mixin")
  , DaoValidator = require("./dao-validator")
  , DataTypes    = require("./data-types")
  , hstore       = require('./dialects/postgres/hstore')
  , _            = require('lodash')

module.exports = (function() {
  var DAO = function(values, options) {
    this.dataValues                  = {}
    this._previousDataValues         = {}
    this.__options                   = this.__factory.options
    this.options                     = options
    this.hasPrimaryKeys              = this.__factory.options.hasPrimaryKeys
    // What is selected values even used for?
    this.selectedValues              = options.include ? _.omit(values, options.includeNames) : values
    this.__eagerlyLoadedAssociations = []
    this.isNewRecord                 = options.isNewRecord

    initValues.call(this, values, options);
  }

  Utils._.extend(DAO.prototype, Mixin.prototype)

  Object.defineProperty(DAO.prototype, 'sequelize', {
    get: function(){ return this.__factory.daoFactoryManager.sequelize }
  })

  Object.defineProperty(DAO.prototype, 'QueryInterface', {
    get: function(){ return this.sequelize.getQueryInterface() }
  })

  Object.defineProperty(DAO.prototype, 'isDeleted', {
    get: function() {
      return this.Model._timestampAttributes.deletedAt && this.dataValues[this.Model._timestampAttributes.deletedAt] !== null
    }
  })

  Object.defineProperty(DAO.prototype, 'values', {
    get: function() {
      return this.get()
    }
  })

  Object.defineProperty(DAO.prototype, 'isDirty', {
    get: function() {
      return !!this.changed()
    }
  })

  Object.defineProperty(DAO.prototype, 'primaryKeyValues', {
    get: function() {
      var result = {}
        , self   = this

      Utils._.each(this.__factory.primaryKeys, function(_, attr) {
        result[attr] = self.dataValues[attr]
      })

      return result
    }
  })

  Object.defineProperty(DAO.prototype, "identifiers", {
    get: function() {
      var primaryKeys = Object.keys(this.__factory.primaryKeys)
        , result      = {}
        , self        = this

      if (!this.__factory.hasPrimaryKeys) {
        primaryKeys = ['id']
      }

      primaryKeys.forEach(function(identifier) {
        result[identifier] = self.dataValues[identifier]
      })

      return result
    }
  })

  DAO.prototype.getDataValue = function(key) {
    return this.dataValues[key]
  }
  DAO.prototype.setDataValue = function(key, value) {
    this.dataValues[key] = value
  }

  DAO.prototype.get = function (key) {
    if (key) {
      if (this._customGetters[key]) {
        return this._customGetters[key].call(this, key)
      }
      return this.dataValues[key]
    }

    if (this._hasCustomGetters) {
      var values = {}
        , key

      for (key in this._customGetters) {
        if (this._customGetters.hasOwnProperty(key)) {
          values[key] = this.get(key)
        }
      }

      for (key in this.dataValues) {
        if (!values.hasOwnProperty(key) && this.dataValues.hasOwnProperty(key)) {
          values[key] = this.dataValues[key]
        }
      }
      return values
    }
    return this.dataValues
  }
  DAO.prototype.set = function (key, value, options) {
    var values
      , originalValue

    if (typeof key === "object") {
      values = key
      options = value

      options || (options = {})

      if (options.reset) {
        this.dataValues = {}
      }

      // If raw, and we're not dealing with includes, just set it straight on the dataValues object
      if (options.raw && !(this.options && this.options.include) && !this._hasBooleanAttributes) {
        if (Object.keys(this.dataValues).length) {
          this.dataValues = _.extend(this.dataValues, values)
        } else {
          this.dataValues = values
        }
        // If raw, .changed() shouldn't be true
        this._previousDataValues = _.clone(this.dataValues)
      } else {
        // Loop and call set
        for (key in values) {
          this.set(key, values[key], options)
        }

        if (options.raw) {
          // If raw, .changed() shouldn't be true
          this._previousDataValues = _.clone(this.dataValues)
        }
      }
    } else {
      options || (options = {})
      originalValue = this.dataValues[key]

      // If not raw, and there's a customer setter
      if (!options.raw && this._customSetters[key]) {
        this._customSetters[key].call(this, value, key)
      } else {
        // Check if we have included models, and if this key matches the include model names/aliases

        if (this.options && this.options.include && this.options.includeNames.indexOf(key) !== -1) {
          // Pass it on to the include handler
          this._setInclude(key, value, options)
          return
        } else {
          // If not raw, and attribute is not in model definition, return
          if (!options.raw && !this._isAttribute(key)) {
            return;
          }

          // If attempting to set primary key and primary key is already defined, return
          if (this._hasPrimaryKeys && originalValue && this._isPrimaryKey(key)) {
            return
          }

          // If attempting to set generated id and id is already defined, return
          // This is hack since generated id is not in primaryKeys, although it should be
          if (originalValue && key === "id") {
            return
          }

          // If attempting to set read only attributes, return
          if (!options.raw && this._hasReadOnlyAttributes && this._isReadOnlyAttribute(key)) {
            return
          }

          // Convert boolean-ish values to booleans
          if (this._hasBooleanAttributes && this._isBooleanAttribute(key) && value !== null && value !== undefined) {
            value = !!value
          }

          // Convert date fields to real date objects
          if (this._hasDateAttributes && this._isDateAttribute(key) && value !== null && !(value instanceof Date)) {
            value = new Date(value)
          }

          if (originalValue !== value) {
            this._previousDataValues[key] = originalValue
          }
          this.dataValues[key] = value
        }
      }
    }
  }

  DAO.prototype.changed = function(key) {
    if (key) {
      if (this._isDateAttribute(key) && this._previousDataValues[key] && this.dataValues[key]) {
        return this._previousDataValues[key].valueOf() !== this.dataValues[key].valueOf()
      }
      return this._previousDataValues[key] !== this.dataValues[key]
    }
    var changed = Object.keys(this.dataValues).filter(function (key) {
      return this.changed(key)
    }.bind(this))

    return changed.length ? changed : false
  }

  DAO.prototype.previous = function(key) {
    return this._previousDataValues[key]
  }

  DAO.prototype._setInclude = function(key, value, options) {
    if (!Array.isArray(value)) value = [value]
    if (value[0] instanceof DAO) {
      value = value.map(function (instance) {
        return instance.dataValues
      })
    }

    var include = _.find(this.options.include, function (include) {
      return include.as === key || (include.as.slice(0,1).toLowerCase() + include.as.slice(1)) === key
    })
    var association          = include.association
      , self                 = this

    var accessor = Utils._.camelize(key)

    // downcase the first char
    accessor = accessor.slice(0,1).toLowerCase() + accessor.slice(1)

    value.forEach(function(data) {
      var daoInstance = include.daoFactory.build(data, {
          isNewRecord: false,
          isDirty: false,
          include: include.include,
          includeNames: include.includeNames,
          includeMap: include.includeMap,
          includeValidated: true,
          raw: options.raw
        })
        , isEmpty = !Utils.firstValueOfHash(daoInstance.identifiers)

      if (association.isSingleAssociation) {
        accessor = Utils.singularize(accessor, self.sequelize.language)
        self.dataValues[accessor] = isEmpty ? null : daoInstance
        self[accessor] = self.dataValues[accessor]
      } else {
        if (!self.dataValues[accessor]) {
          self.dataValues[accessor] = []
          self[accessor] = self.dataValues[accessor]
        }

        if (!isEmpty) {
          self.dataValues[accessor].push(daoInstance)
        }
      }
    }.bind(this))
  };

  // if an array with field names is passed to save()
  // only those fields will be updated
  DAO.prototype.save = function(fieldsOrOptions, options) {
    if (fieldsOrOptions instanceof Array) {
      fieldsOrOptions = { fields: fieldsOrOptions }
    }

    options = Utils._.extend({}, options, fieldsOrOptions)

    if (!options.fields) {
      options.fields = Object.keys(this.Model.attributes)
    }

    if (options.returning === undefined) {
      if (options.association) {
        options.returning = false
      } else {
        options.returning = true
      }
    }

    var self           = this
      , values         = {}
      , updatedAtAttr  = this.Model._timestampAttributes.updatedAt
      , createdAtAttr  = this.Model._timestampAttributes.createdAt

    if (options.fields) {
      if (updatedAtAttr && options.fields.indexOf(updatedAtAttr) === -1) {
        options.fields.push(updatedAtAttr)
      }

      if (createdAtAttr && options.fields.indexOf(createdAtAttr) === -1 && this.isNewRecord === true) {
        options.fields.push(createdAtAttr)
      }
    }

    return new Utils.CustomEventEmitter(function(emitter) {
      self.hookValidate().error(function(err) {
        emitter.emit('error', err)
      }).success(function() {
        options.fields.forEach(function(field) {
          if (self.dataValues[field] !== undefined) {
            values[field] = self.dataValues[field]
          }
        })

        for (var attrName in self.daoFactory.rawAttributes) {
          if (self.daoFactory.rawAttributes.hasOwnProperty(attrName)) {
            var definition = self.daoFactory.rawAttributes[attrName]
              , isHstore   = !!definition.type && !!definition.type.type && definition.type.type === DataTypes.HSTORE.type
              , isEnum          = definition.type && (definition.type.toString() === DataTypes.ENUM.toString())
              , isMySQL         = ['mysql', 'mariadb'].indexOf(self.daoFactory.daoFactoryManager.sequelize.options.dialect) !== -1
              , ciCollation     = !!self.daoFactory.options.collate && self.daoFactory.options.collate.match(/_ci$/i)
              , valueOutOfScope

            // Unfortunately for MySQL CI collation we need to map/lowercase values again
            if (isEnum && isMySQL && ciCollation && (attrName in values) && values[attrName]) {
              var scopeIndex = (definition.values || []).map(function(d) { return d.toLowerCase() }).indexOf(values[attrName].toLowerCase())
              valueOutOfScope = scopeIndex === -1

              // We'll return what the actual case will be, since a simple SELECT query would do the same...
              if (!valueOutOfScope) {
                values[attrName] = definition.values[scopeIndex]
              }
            }

            if (isHstore) {
              if (typeof values[attrName] === "object") {
                values[attrName] = hstore.stringify(values[attrName])
              }
            }
          }
        }

        if (updatedAtAttr) {
          values[updatedAtAttr] = (
            (
              self.isNewRecord
              && !!self.daoFactory.rawAttributes[updatedAtAttr]
              && !!self.daoFactory.rawAttributes[updatedAtAttr].defaultValue
            )
            ? self.daoFactory.rawAttributes[updatedAtAttr].defaultValue
            : Utils.now(self.sequelize.options.dialect))    
        }
        
        if (self.isNewRecord && createdAtAttr && !values[createdAtAttr]) {
          values[createdAtAttr] = (
            (
              !!self.daoFactory.rawAttributes[createdAtAttr]
              && !!self.daoFactory.rawAttributes[createdAtAttr].defaultValue
            )
            ? self.daoFactory.rawAttributes[createdAtAttr].defaultValue
            : values[updatedAtAttr])
          }

        var query = null
          , args  = []
          , hook  = ''

        if (self.isNewRecord) {
          query         = 'insert'
          args          = [self, self.QueryInterface.QueryGenerator.addSchema(self.__factory), values, options]
          hook          = 'Create'
        } else {
          var identifier = self.__options.hasPrimaryKeys ? self.primaryKeyValues : { id: self.id }

          if (identifier === null && self.__options.whereCollection !== null) {
            identifier = self.__options.whereCollection;
          }

          query         = 'update'
          args          = [self, self.QueryInterface.QueryGenerator.addSchema(self.__factory), values, identifier, options]
          hook          = 'Update'
        }

        // Add the values to the DAO
        self.dataValues = _.extend(self.dataValues, values)

        // Run the beforeCreate / beforeUpdate hook
        self.__factory.runHooks('before' + hook, self, function(err) {
          if (!!err) {
            return emitter.emit('error', err)
          }

          // dataValues might have changed inside the hook, rebuild
          // the values hash
          values = {}

          options.fields.forEach(function(field) {
            if (self.dataValues[field] !== undefined) {
              values[field] = self.dataValues[field]
            }
          })
          args[2] = values

          self.QueryInterface[query].apply(self.QueryInterface, args)
            .proxy(emitter, {events: ['sql']})
            .error(function(err) {
              if (!!self.__options.uniqueKeys && err.code && self.QueryInterface.QueryGenerator.uniqueConstraintMapping.code === err.code) {
                var fields = self.QueryInterface.QueryGenerator.uniqueConstraintMapping.map(err.toString())

                if (fields !== false) {
                  fields = fields.filter(function(f) { return f !== self.daoFactory.tableName; })
                  Utils._.each(self.__options.uniqueKeys, function(value, key) {
                    if (Utils._.isEqual(value.fields, fields) && !!value.msg) {
                      err = value.msg
                    }
                  })
                }
              }

              emitter.emit('error', err)
            })
            .success(function(result) {
              // Transfer database generated values (defaults, autoincrement, etc)
              values = _.extend(values, result.dataValues)

              // Ensure new values are on DAO, and reset previousDataValues
              result.dataValues = _.extend(result.dataValues, values)
              result._previousDataValues = _.clone(result.dataValues)

              self.__factory.runHooks('after' + hook, result, function(err) {
                if (!!err) {
                  return emitter.emit('error', err)
                }
                emitter.emit('success', result)
              })
            })
        })
      })
    }).run()
  }

 /*
  * Refresh the current instance in-place, i.e. update the object with current data from the DB and return the same object.
  * This is different from doing a `find(DAO.id)`, because that would create and return a new object. With this method,
  * all references to the DAO are updated with the new data and no new objects are created.
  *
  * @return {Object}         A promise which fires `success`, `error`, `complete` and `sql`.
  */
  DAO.prototype.reload = function(options) {
    var where = [
      this.QueryInterface.quoteIdentifier(this.Model.getTableName()) + '.' + this.QueryInterface.quoteIdentifier(this.Model.primaryKeyAttributes[0] || 'id') + '=?',
      this.get(this.Model.primaryKeyAttributes[0] || 'id', {raw: true})
    ]

    return new Utils.CustomEventEmitter(function(emitter) {
      this.__factory.find({
        where:   where,
        limit:   1,
        include: this.options.include || null
      }, options)
      .on('sql', function(sql) { emitter.emit('sql', sql) })
      .on('error', function(error) { emitter.emit('error', error) })
      .on('success', function(obj) {
        this.set(obj.dataValues, {raw: true, reset: true})
        this.isDirty = false
        emitter.emit('success', this)
      }.bind(this))
    }.bind(this)).run()
  }

  /*
   * Validate this dao's attribute values according to validation rules set in the dao definition.
   *
   * @return null if and only if validation successful; otherwise an object containing { field name : [error msgs] } entries.
   */
  DAO.prototype.validate = function(object) {
    var validator = new DaoValidator(this, object)
      , errors    = validator.validate()

    return (Utils._.isEmpty(errors) ? null : errors)
  }

  /*
   * Validate this dao's attribute values according to validation rules set in the dao definition.
   *
   * @return CustomEventEmitter with null if validation successful; otherwise an object containing { field name : [error msgs] } entries.
   */
  DAO.prototype.hookValidate = function(object) {
    var validator = new DaoValidator(this, object)

    return validator.hookValidate()
  }

  DAO.prototype.updateAttributes = function(updates, options) {
    if (options instanceof Array) {
      options = { fields: options }
    }

    this.set(updates)
    return this.save(options)
  }

  DAO.prototype.setAttributes = function(updates) {
    this.set(updates)
  }

  DAO.prototype.destroy = function(options) {
    options = options || {}
    options.force = options.force === undefined ? false : Boolean(options.force)

    var self  = this
      , query = null

    return new Utils.CustomEventEmitter(function(emitter) {
      self.daoFactory.runHooks(self.daoFactory.options.hooks.beforeDestroy, self, function(err) {
        if (!!err) {
          return emitter.emit('error', err)
        }

        if (self.Model._timestampAttributes.deletedAt && options.force === false) {
          self.dataValues[self.Model._timestampAttributes.deletedAt] = new Date()
          query = self.save(options)
        } else {
          var identifier = self.__options.hasPrimaryKeys ? self.primaryKeyValues : { id: self.id };
          query = self.QueryInterface.delete(self, self.QueryInterface.QueryGenerator.addSchema(self.__factory.tableName, self.__factory.options.schema), identifier, options)
        }

        query.on('sql', function(sql) {
          emitter.emit('sql', sql)
        })
        .error(function(err) {
          emitter.emit('error', err)
        })
        .success(function(results) {
          self.daoFactory.runHooks(self.daoFactory.options.hooks.afterDestroy, self, function(err) {
            if (!!err) {
              return emitter.emit('error', err)
            }

            emitter.emit('success', results)
          })
        })
      })
    }).run()
  }

  DAO.prototype.increment = function(fields, countOrOptions) {
    Utils.validateParameter(countOrOptions, Object, {
      optional:           true,
      deprecated:         'number',
      deprecationWarning: "Increment expects an object as second parameter. Please pass the incrementor as option! ~> instance.increment(" + JSON.stringify(fields) + ", { by: " + countOrOptions + " })"
    })

    var identifier    = this.__options.hasPrimaryKeys ? this.primaryKeyValues : { id: this.id }
      , updatedAtAttr = this.Model._timestampAttributes.updatedAt
      , values        = {}

    if (countOrOptions === undefined) {
      countOrOptions = { by: 1, transaction: null }
    } else if (typeof countOrOptions === 'number') {
      countOrOptions = { by: countOrOptions, transaction: null }
    }

    countOrOptions = Utils._.extend({
      by:         1,
      attributes: {}
    }, countOrOptions)

    if (Utils._.isString(fields)) {
      values[fields] = countOrOptions.by
    } else if (Utils._.isArray(fields)) {
      Utils._.each(fields, function (field) {
        values[field] = countOrOptions.by
      })
    } else { // Assume fields is key-value pairs
      values = fields
    }

    if (updatedAtAttr && !values[updatedAtAttr]) {
      countOrOptions.attributes[updatedAtAttr] = Utils.now(this.daoFactory.daoFactoryManager.sequelize.options.dialect)
    }

    return this.QueryInterface.increment(this, this.QueryInterface.QueryGenerator.addSchema(this.__factory.tableName, this.__factory.options.schema), values, identifier, countOrOptions)
  }

  DAO.prototype.decrement = function (fields, countOrOptions) {
    Utils.validateParameter(countOrOptions, Object, {
      optional:           true,
      deprecated:         'number',
      deprecationWarning: "Decrement expects an object as second parameter. Please pass the decrementor as option! ~> instance.decrement(" + JSON.stringify(fields) + ", { by: " + countOrOptions + " })"
    })

    if (countOrOptions === undefined) {
      countOrOptions = { by: 1, transaction: null }
    } else if (typeof countOrOptions === 'number') {
      countOrOptions = { by: countOrOptions, transaction: null }
    }

    if (countOrOptions.by === undefined) {
      countOrOptions.by = 1
    }

    if (!Utils._.isString(fields) && !Utils._.isArray(fields)) { // Assume fields is key-value pairs
      Utils._.each(fields, function (value, field) {
        fields[field] = -value
      })
    }

    countOrOptions.by = 0 - countOrOptions.by

    return this.increment(fields, countOrOptions)
  }

  DAO.prototype.equals = function(other) {
    var result = true

    Utils._.each(this.dataValues, function(value, key) {
      if(Utils._.isDate(value) && Utils._.isDate(other[key])) {
        result = result && (value.getTime() == other[key].getTime())
      } else {
        result = result && (value == other[key])
      }
    })

    return result
  }

  DAO.prototype.equalsOneOf = function(others) {
    var result = false
      , self   = this

    others.forEach(function(other) { result = result || self.equals(other) })

    return result
  }

  DAO.prototype.setValidators = function(attribute, validators) {
    this.validators[attribute] = validators
  }

  DAO.prototype.toJSON = function() {
    return this.get();
  }

  // private
  var initValues = function(values, options) {
    // set id to null if not passed as value, a newly created dao has no id
    var defaults = this.hasPrimaryKeys ? {} : { id: null },
        key;

    values = values && _.clone(values) || {}

    if (options.isNewRecord) {
      if (this.hasDefaultValues) {
        Utils._.each(this.defaultValues, function(valueFn, key) {
          if (!defaults.hasOwnProperty(key)) {
            defaults[key] = valueFn()
          }
        })
      }

      if (this.Model._timestampAttributes.createdAt && defaults[this.Model._timestampAttributes.createdAt]) {
        this.dataValues[this.Model._timestampAttributes.createdAt] = Utils.toDefaultValue(defaults[this.Model._timestampAttributes.createdAt]);
        delete defaults[this.Model._timestampAttributes.createdAt];
      }

      if (this.Model._timestampAttributes.updatedAt && defaults[this.Model._timestampAttributes.updatedAt]) {
        this.dataValues[this.Model._timestampAttributes.updatedAt] = Utils.toDefaultValue(defaults[this.Model._timestampAttributes.updatedAt]);
        delete defaults[this.Model._timestampAttributes.updatedAt];
      }

      if (this.Model._timestampAttributes.createdAt && defaults[this.Model._timestampAttributes.deletedAt]) {
        this.dataValues[this.Model._timestampAttributes.deletedAt] = Utils.toDefaultValue(defaults[this.Model._timestampAttributes.deletedAt]);
        delete defaults[this.Model._timestampAttributes.deletedAt];
      }
    }
    if (Object.keys(defaults).length) {
      for (key in defaults) {
        if (!values.hasOwnProperty(key)) {
          values[key] = Utils.toDefaultValue(defaults[key])
        }
      }
    }

    this.set(values, options)
  }

  return DAO
})()
