var safeRequire = require('../utils').safeRequire;

/**
 * Module dependencies
 */
var cradle = safeRequire('cradle');

/**
 * Private functions for internal use
 */
function CradleAdapter(client) {
    this._models = {};
    this.client = client;
}

function createdbif(client, callback) {
    client.exists(function (err, exists) {
        if(err) callback(err);
        if (!exists) { client.create(function() { callback(); }); }
        else { callback(); }
    });
}

function naturalize(data, model) {
    data.nature = model;
    //TODO: maybe this is not a really good idea
    if(data.date) data.date = data.date.toString();
    return data;
}
function idealize(data) {
    data.id = data._id;
    return data;
}
function stringify(data) {
    return data ? data.toString() : data 
}

function errorHandler(callback, func) {
    return function(err, res) {
        if (err) {
            console.log('cradle', err);
            callback(err);
        } else {
            if(func) {
                func(res, function(res) {
                    callback(null, res);
                });
            } else {
                callback(null, res);
            }
        }
    }
};

function synchronize(functions, args, callback) {
    if(functions.length === 0) callback();
    if(functions.length > 0 && args.length === functions.length) {
        functions[0](args[0][0], args[0][1], function(err, res) {
            if(err) callback(err);
            functions.splice(0, 1);
            args.splice(0, 1);
            synchronize(functions, args, callback);
        });
    }
};

function applyFilter(filter) {
    if (typeof filter.where === 'function') {
        return filter.where;
    }
    var keys = Object.keys(filter.where);
    return function (obj) {
        var pass = true;
        keys.forEach(function (key) {
            if (!test(filter.where[key], obj[key])) {
                pass = false;
            }
        });
        return pass;
    }

    function test(example, value) {
        if (typeof value === 'string' && example && example.constructor.name === 'RegExp') {
            return value.match(example);
        }
        // not strict equality
        return example == value;
    }
}

function numerically(a, b) {
   return a[this[0]] - b[this[0]];
}

function literally(a, b) {
   return a[this[0]] > b[this[0]];
}

function filtering(res, model, filter, instance) {

   if(model) {
      if(filter == null) filter = {};
      if(filter.where == null) filter.where = {};
      filter.where.nature = model;
   }
   // do we need some filtration?
   if (filter.where) {
      res = res ? res.filter(applyFilter(filter)) : res;
   }

   // do we need some sorting?
   if (filter.order) {
      var props = instance[model].properties;
      var allNumeric = true;
      var orders = filter.order;
      var reverse = false;
      if (typeof filter.order === "string") {
         orders = [filter.order];
      }

      orders.forEach(function (key, i) {
         var m = key.match(/\s+(A|DE)SC$/i);
         if (m) {
            key = key.replace(/\s+(A|DE)SC/i, '');
            if (m[1] === 'DE') reverse = true;
         }
         orders[i] = key;
         if (props[key].type.name !== 'Number') {
            allNumeric = false;
         }
      });
      if (allNumeric) {
         res = res.sort(numerically.bind(orders));
      } else {
         res = res.sort(literally.bind(orders));
      }
      if (reverse) res = res.reverse();
   }
   return res;
}

/**
 * Connection/Disconnection
 */
exports.initialize = function(dataSource, callback) {
    if (!cradle) return;

    // when using cradle if we dont wait for the dataSource to be connected, the models fails to load correctly.
    dataSource.waitForConnect = true;
    if (!dataSource.settings.url) {
        var host = dataSource.settings.host || 'localhost';
        var port = dataSource.settings.port || '5984';
        var options = dataSource.settings.options || {
           cache: true,
           raw: false
        };
        if (dataSource.settings.username) {
            options.auth = {};
            options.auth.username = dataSource.settings.username;
            if (dataSource.settings.password) {
                options.auth.password = dataSource.settings.password;
            }
        }
        var database = dataSource.settings.database || 'loopback-datasource-juggler';

        dataSource.settings.host = host;
        dataSource.settings.port = port;
        dataSource.settings.database = database;
        dataSource.settings.options = options;
    }
    dataSource.client = new(cradle.Connection)(dataSource.settings.host, dataSource.settings.port,dataSource.settings.options).database(dataSource.settings.database);

    createdbif(
       dataSource.client,
       errorHandler(callback, function() {
          dataSource.connector = new CradleAdapter(dataSource.client);
          process.nextTick(callback);
       }));
};

CradleAdapter.prototype.disconnect = function() {
};

/**
 * Write methods
 */
CradleAdapter.prototype.define = function(descr) {
    this._models[descr.model.modelName] = descr;
};

CradleAdapter.prototype.create = function(model, data, callback) {
    this.client.save(
       stringify(data.id),
       naturalize(data, model),
       errorHandler(callback, function(res, cb) {
          cb(res.id);
       })
    );
};

CradleAdapter.prototype.save = function(model, data, callback) {
   this.client.save(
       stringify(data.id),
       naturalize(data, model),
       errorHandler(callback)
   )
};

CradleAdapter.prototype.updateAttributes = function(model, id, data, callback) {
    this.client.merge(
       stringify(id),
       data,
       errorHandler(callback, function(doc, cb) {
          cb(idealize(doc));
       })
    );
};

CradleAdapter.prototype.updateOrCreate = function(model, data, callback) {
   this.client.get(
      stringify(data.id),
      function (err, doc) {
         if(err) {
            this.create(model, data, callback);
         } else {
            this.updateAttributes(model, data.id, data, callback);
         }
      }.bind(this)
   )
};

/**
 * Read methods
 */
CradleAdapter.prototype.exists = function(model, id, callback) {
    this.client.get(
       stringify(id), 
       errorHandler(callback, function(doc, cb) {
          cb(!!doc);   
       })
    );
};

CradleAdapter.prototype.find = function(model, id, callback) {
    this.client.get(
       stringify(id),
       errorHandler(callback, function(doc, cb) {
          cb(idealize(doc));
       })
    );
};

CradleAdapter.prototype.count = function(model, callback, where) {
    this.models(
       model,
       {where: where},
       callback,
       function(docs, cb) {
          cb(docs.length);
       }
    );
};

CradleAdapter.prototype.models = function(model, filter, callback, func) {
    var limit = 200;
    var skip  = 0;
    if (filter != null) {
        limit = filter.limit || limit;
        skip  = filter.skip ||skip;
    }

    var self = this;

    self.client.save('_design/'+model, {
        views : {
            all : {
                map : 'function(doc) { if (doc.nature == "'+model+'") { emit(doc._id, doc); } }'
            }
        }
    }, function() {
        self.client.view(model+'/all', {include_docs:true, limit:limit, skip:skip}, errorHandler(callback, function(res, cb) {
            var docs = res.map(function(doc) {
                return idealize(doc);
            });
            var filtered = filtering(docs, model, filter, this._models)

            func ? func(filtered, cb) : cb(filtered);
        }.bind(self)));
    });
};

CradleAdapter.prototype.all = function(model, filter, callback) {
   this.models(
       model,
       filter,
       callback
   );
};

/**
 * Detroy methods
 */
CradleAdapter.prototype.destroy = function(model, id, callback) {
    this.client.remove(
       stringify(id),
       function (err, doc) {
         callback(err);
       }
    );
};

CradleAdapter.prototype.destroyAll = function(model, callback) {
   this.models(
       model,
       null,
       callback,
       function(docs, cb) {
          var docIds = docs.map(function(doc) {
             return doc.id;                     
          });
          this.client.get(docIds, function(err, res) {
             if(err) cb(err);

             var funcs = res.map(function(doc) {
                return this.client.remove.bind(this.client);
             }.bind(this));

             var args = res.map(function(doc) {
                return [doc._id, doc._rev];
             });

             synchronize(funcs, args, cb);
          }.bind(this));
       }.bind(this)
   );
};