'use strict';

var lodash = require('lodash');
var isPromise = require('is-promise');

// Returns a lodash chain that calls .value()
// automatically after the first .method()
//
// It also returns a promise or value
//
// For example:
// lowChain(_, array, save).method()
//
// is the same as:
// _.chain(array).method().value()
function lowChain(_, array, save) {
  var chain = _.chain(array);

  _.functionsIn(chain).forEach(function (method) {
    chain[method] = _.flow(chain[method], function (arg) {
      var v = undefined;
      if (arg) {
        v = _.isFunction(arg.value) ? arg.value() : arg;
      }

      var s = save();

      if (s) return s.then(function () {
        return Promise.resolve(v);
      });
      return v;
    });
  });

  return chain;
}

function low(source) {
  var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1];
  var writeOnChange = arguments.length <= 2 || arguments[2] === undefined ? true : arguments[2];

  // Create a fresh copy of lodash
  var _ = lodash.runInContext();

  if (source) {
    if (options.storage) {
      (function () {
        var storage = options.storage;


        if (storage.read) {
          db.read = function () {
            var s = arguments.length <= 0 || arguments[0] === undefined ? source : arguments[0];

            var res = storage.read(s, db.deserialize);

            if (isPromise(res)) {
              return res.then(function (obj) {
                db.object = obj;
                db._checksum = JSON.stringify(db.object);

                return db;
              });
            }

            db.object = res;
            db._checksum = JSON.stringify(db.object);

            return db;
          };
        }

        if (storage.write) {
          db.write = function () {
            var dest = arguments.length <= 0 || arguments[0] === undefined ? source : arguments[0];
            return storage.write(dest, db.object, db.serialize);
          };
        }
      })();
    }

    if (options.format) {
      var format = options.format;

      db.serialize = format.serialize;
      db.deserialize = format.deserialize;
    }
  }

  // Modify value function to call save before returning result
  _.prototype.value = _.wrap(_.prototype.value, function (value) {
    var v = value.apply(this);
    var s = _save();

    if (s) return s.then(function () {
      return Promise.resolve(v);
    });
    return v;
  });

  // Return a promise or nothing in sync mode or if the database hasn't changed
  function _save() {
    if (db.source && db.write && writeOnChange) {
      var str = JSON.stringify(db.object);

      if (str !== db._checksum) {
        db._checksum = str;
        return db.write(db.source, db.object);
      }
    }
  }

  function db(key) {
    if (typeof db.object[key] === 'undefined') {
      db.object[key] = [];
    }
    var array = db.object[key];
    var short = lowChain(_, array, _save);
    short.chain = function () {
      return _.chain(array);
    };
    // Prevents db.write being called when just calling db('foo').value()
    short.value = function () {
      return db.object[key];
    };
    return short;
  }

  // Expose
  db._ = _;
  db.object = {};
  db.source = source;

  // Init
  if (db.read) {
    return db.read();
  } else {
    return db;
  }
}

module.exports = low;