var toolbox = {};

toolbox.getMainCollection = function (response) {
  return _.without(_.keys(response), 'links', 'linked', 'meta')[0];
};

// from http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript
toolbox.generateUUID = function() {
  var d = new Date().getTime();
  var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
    var r = (d + Math.random()*16)%16 | 0;
    d = Math.floor(d/16);
    return (c=='x' ? r : (r&0x7|0x8)).toString(16);
  });
  return uuid;
};

toolbox.batchRequest = function(klass, ids, amount, success) {
  // batch requests to avoid GET length limits
  var grouped;

  var batch_size = 75;

  if (ids.length <= batch_size) {
    grouped = _.groupBy(ids, function(element, index){
      return Math.floor(index/batch_size);
    });
  } else {
    grouped = {1: ids};
  }

  return( _.map(grouped, function(id_group, index) {
    var linkedClass = klass;

    if (id_group.length > 0) {
      linkedClass = linkedClass.extend({
        url: function() { return(klass.prototype.url.call() + "/" + id_group.join(',')); }
      });
    }

    var linkedCollection = new linkedClass();
    return linkedCollection.fetch({'reset' : true, 'success' : success});
  }));
};

Backbone.JSONAPIModel = Backbone.Model.extend({

  initialize: function() {
    var _persisted;

    this.isPersisted = function () {
      if ( !_.isUndefined(_persisted) && !_.isNull(_persisted) ) {
        return(_persisted);
      } else {
        return(!_.isNull(this.id));
      }
    };
    this.setPersisted = function(pers) {
      _persisted = pers;
    };
  },

  isNew: function() {
    return(!this.isPersisted());
  },

  // the following two methods, and batchRequest, should be folded into
  // Collection.fetch (and Model.fetch?)
  resolveLink: function(name, klass, superset) {
    if ( _.isUndefined(this.get('links')[name]) ) {
      this.linked[name] = new klass();
    } else {
      context = this;
      var records = superset.filter(function(obj) {
        return(context.get('links')[name].indexOf(obj.get('id')) > -1);
      });
      if ( _.isUndefined(this.linked[name]) ) {
        this.linked[name] = new klass();
      }
      this.linked[name].add(records);
    }
  },

  resolveLinks: function(name_klass_h) {
    var context = this;

    return _.flatten( _.map(name_klass_h, function(klass, name) {
      if ( _.isUndefined(context.linked) ) {
        context.linked = {};
      }

      var links_name = context.get('links')[name];
      if ( !_.isUndefined(links_name) && !_.isEmpty(links_name) ) {

        var success = function(resultCollection, response, options) {
          context.resolveLink(name, klass, resultCollection);
        };

        return toolbox.batchRequest(klass, links_name, 75, success);
      } else {
        return($.when(context.resolveLink(name, klass, new Array())));
      }
    }));
  },

  parse: function (response) {
    if (response === undefined) {
      return;
    }
    if (response._alreadyBBJSONAPIParsed) {
      delete response._alreadyBBJSONAPIParsed;
      return response;
    }
    var mainCollection = toolbox.getMainCollection(response);
    var obj = response[mainCollection][0];

    return obj;
  },

  // post: function(urlType, attrs, options) {
  //   if ( _.isUndefined(options) || _.isNull(options) ) {
  //     options = {};
  //   }

  //   postData = {}
  //   postData[urlType] = [attrs];

  //   return(this.save({}, _.extend(options, {
  //     contentType: 'application/vnd.api+json',
  //     data: JSON.stringify(postData)
  //   })));
  // },

  // NB: should not be exported
  savePatch: function(attrs, patch, options) {
    if ( _.isUndefined(options) || _.isNull(options) ) {
      options = {};
    }
    return(this.save(attrs, _.extend(options, {
      data: JSON.stringify(patch),
      patch: true,
      contentType: 'application/json-patch+json'
    })));
  },

  patch: function(urlType, attrs, options) {
    if (attrs == null) {
      attrs = {};
    }

    var context = this;

    var patch = _.inject(attrs, function(memo, val, key) {
      // skip if not a simple attribute value
      if ( (key == 'links') || _.isObject(val) || _.isArray(val) ) {
        return memo;
      }

      memo.push({
        op: 'replace',
        path: '/' + urlType + '/0/' + key,
        value: val
      });

      return memo;
    }, new Array());

    this.savePatch(attrs, patch, options);
  },

  // singular operation only -- TODO batch up and submit en masse
  addLinked: function(urlType, type, obj, options) {
    var id = obj.get('id');

    var patch = [{
      op: 'add',
      path: '/' + urlType + '/0/links/' + type + '/-',
      value: id
    }];

    this.savePatch({}, patch, {}, options);

    if ( _.isUndefined(this.get('links')[type]) ) {
      this.get('links')[type] = new Array();
    }
    this.get('links')[type].push(id);
    this.linked[type].add(obj);
  },

  // singular operation only -- TODO batch up and submit en masse
  removeLinked: function(urlType, type, obj, options) {
    var id = obj.get('id');

    var patch = [{
      op: 'remove',
      path: '/' + urlType + '/0/links/' + type + '/' + id,
    }];

    this.savePatch({}, patch, {}, options);

    if ( _.isUndefined(this.get('links')[type]) ) {
      this.get('links')[type] = new Array();
    }
    this.get('links')[type] = _.without(this.get('links')[type], id);
    this.linked[type].remove(obj);
  },

  urlRoot: function() { return(flapjack.api_url + this.name); },

  // can only be called from inside a 'change' event
  setDirty: function() {
    if ( !this.hasChanged() ) {
      return;
    }

    this.dirty = true;
    if ( _.isUndefined(this.clean) ) {
      this.clean = {};
    }
    var context = this;

    // foreach changed attribute, merge previous value into a 'clean' hash
    // (only if the key isn't already in there)
    _.each(_.without(_.keys(this.changedAttributes()), _.keys(this.clean)), function(key) {
      context.clean[key] = context.previous(key);
    });
  },

  // can only be called from outside a change event
  revertClean: function() {
    if ( !_.isUndefined(this.clean) ) {
      this.set(this.clean, {silent: true});
    }
    this.clean = {};
    this.dirty = false;
  },

  sync: function(method, model, options) {
    if ( method == 'create' ) {
      // patch sets its own content type, get and delete don't have body content
      data                    = {};
      data[model.name]        = [options.attrs || model.toJSON(options)];
      options['data']         = JSON.stringify(data)
      options['contentType']  = 'application/vnd.api+json';
    }
    var succ = function(data, response, opts) {
      model.clean = {};
      model.dirty = false;
    };
    if ( _.isUndefined(options['success']) ) {
      options['success'] = succ;
    } else {
      options['success'] = [options['success'], succ];
    }

    Backbone.sync(method, model, options);
  }

});


Backbone.JSONAPICollection = Backbone.Collection.extend({

  resolveLinks: function(name_klass_h) {
    if ( _.isUndefined(this.linked) ) {
      this.linked = {};
    }

    var context = this;

    _.each(name_klass_h, function(klass, name) {

      if ( !_.isUndefined(context.links[name]) && !_.isEmpty(context.links[name]) ) {
        context.linked[name] = new klass();

        var success = function(resultCollection, response, options) {
          context.forEach(function(obj, index) {
            if ( _.isUndefined(obj.linked) ) {
              obj.linked = {};
            }
            obj.resolveLink(name, klass, resultCollection);
          });
        }

        toolbox.batchRequest(klass, context.links[name], 75, success);
      }
    });
  },

  parse: function (response) {
    if (response === undefined) {
      return;
    }

    this.links = {};
    var context = this;

    var mainCollection = toolbox.getMainCollection(response);
    var objects = _.map(response[mainCollection], function (obj) {
      _.each(obj.links, function(ids, name) {
        if ( _.isUndefined(context.links[name]) ) {
          context.links[name] = new Array();
        }
        if ( _.isArray(ids) ) {
          context.links[name] = context.links[name].concat(ids);
        }
      });

      obj._alreadyBBJSONAPIParsed = true;
      return obj;
    });

    this.links = _.reduce(this.links, function(memo, ids, name) {
      memo[name] = _.uniq(ids);
      return(memo);
    }, {});

    return objects;
  }

});