/*jslint indent: 2, maxlen: 80, nomen: true */
/*global jIO: true */
/**
 * JIO Replicate Revision Storage.
 * It manages storages that manage revisions and conflicts.
 * Description:
 * {
 *     "type": "replicaterevision",
 *     "storage_list": [
 *         <sub storage description>,
 *         ...
 *     ]
 * }
 */
jIO.addStorageType('replicaterevision', function (spec, my) {
  "use strict";
  var that, priv = {};
  spec = spec || {};
  that = my.basicStorage(spec, my);

  priv.storage_list_key = "storage_list";
  priv.storage_list = spec[priv.storage_list_key];
  priv.emptyFunction = function () {};

  that.specToStore = function () {
    var o = {};
    o[priv.storage_list_key] = priv.storage_list;
    return o;
  };

  /**
   * Generate a new uuid
   * @method generateUuid
   * @return {string} The new uuid
   */
  priv.generateUuid = function () {
    var S4 = function () {
      var i, string = Math.floor(
        Math.random() * 0x10000 /* 65536 */
      ).toString(16);
      for (i = string.length; i < 4; i += 1) {
        string = "0" + string;
      }
      return string;
    };
    return S4() + S4() + "-" +
      S4() + "-" +
      S4() + "-" +
      S4() + "-" +
      S4() + S4() + S4();
  };

  /**
   * Create an array containing dictionnary keys
   * @method dictKeys2Array
   * @param  {object} dict The object to convert
   * @return {array} The array of keys
   */
  priv.dictKeys2Array = function (dict) {
    var k, newlist = [];
    for (k in dict) {
      if (dict.hasOwnProperty(k)) {
        newlist.push(k);
      }
    }
    return newlist;
  };

  /**
   * Checks a revision format
   * @method checkRevisionFormat
   * @param  {string} revision The revision string
   * @return {boolean} True if ok, else false
   */
  priv.checkRevisionFormat = function (revision) {
    return (/^[0-9]+-[0-9a-zA-Z_]+$/.test(revision));
  };

  /**
   * Clones an object in deep (without functions)
   * @method clone
   * @param  {any} object The object to clone
   * @return {any} The cloned object
   */
  priv.clone = function (object) {
    var tmp = JSON.stringify(object);
    if (tmp === undefined) {
      return undefined;
    }
    return JSON.parse(tmp);
  };

  /**
   * Like addJob but also return the method and the index of the storage
   * @method send
   * @param  {string} method The request method
   * @param  {number} index The storage index
   * @param  {object} doc The document object
   * @param  {object} option The request object
   * @param  {function} callback The callback. Parameters:
   * - {string} The request method
   * - {number} The storage index
   * - {object} The error object
   * - {object} The response object
   */
  priv.send = function (method, index, doc, option, callback) {
    var wrapped_callback_success, wrapped_callback_error;
    callback = callback || priv.emptyFunction;
    wrapped_callback_success = function (response) {
      callback(method, index, undefined, response);
    };
    wrapped_callback_error = function (err) {
      callback(method, index, err, undefined);
    };
    that.addJob(
      method,
      priv.storage_list[index],
      doc,
      option,
      wrapped_callback_success,
      wrapped_callback_error
    );
  };

  /**
   * Use "send" method to all sub storages.
   * Calling "callback" for each storage response.
   * @method sendToAll
   * @param  {string} method The request method
   * @param  {object} doc The document object
   * @param  {object} option The request option
   * @param  {function} callback The callback. Parameters:
   * - {string} The request method
   * - {number} The storage index
   * - {object} The error object
   * - {object} The response object
   */
  priv.sendToAll = function (method, doc, option, callback) {
    var i;
    for (i = 0; i < priv.storage_list.length; i += 1) {
      priv.send(method, i, doc, option, callback);
    }
  };

  /**
   * Use "send" method to all sub storages.
   * Calling "callback" only with the first response
   * @method sendToAllFastestResponseOnly
   * @param  {string} method The request method
   * @param  {object} doc The document object
   * @param  {object} option The request option
   * @param  {function} callback The callback. Parameters:
   * - {string} The request method
   * - {object} The error object
   * - {object} The response object
   */
  priv.sendToAllFastestResponseOnly = function (method, doc, option, callback) {
    var i, callbackWrapper, error_count, last_error;
    error_count = 0;
    callbackWrapper = function (method, index, err, response) {
      if (err) {
        error_count += 1;
        last_error = err;
        if (error_count === priv.storage_list.length) {
          return callback(method, err, response);
        }
      }
      callback(method, err, response);
    };
    for (i = 0; i < priv.storage_list.length; i += 1) {
      priv.send(method, i, doc, option, callbackWrapper);
    }
  };

  /**
   * Use "sendToAll" method, calling "callback" at the last response with
   * the response list
   * @method sendToAllGetResponseList
   * @param  {string} method The request method
   * @param  {object} doc The document object
   * @param  {object} option The request option
   * @return {function} callback The callback. Parameters:
   * - {string} The request method
   * - {object} The error object
   * - {object} The response object
   */
  priv.sendToAllGetResponseList = function (method, doc, option, callback) {
    var wrapper, callback_count = 0, response_list = [], error_list = [];
    response_list.length = priv.storage_list.length;
    wrapper = function (method, index, err, response) {
      error_list[index] = err;
      response_list[index] = response;
      callback_count += 1;
      if (callback_count === priv.storage_list.length) {
        callback(error_list, response_list);
      }
    };
    priv.sendToAll(method, doc, option, wrapper);
  };

  /**
   * Checks if the sub storage are identical
   * @method check
   * @param  {object} command The JIO command
   */
  that.check = function (command) {
    function callback(err, response) {
      if (err) {
        return that.error(err);
      }
      that.success(response);
    }
    priv.check(
      command.cloneDoc(),
      command.cloneOption(),
      callback
    );
  };

  /**
   * Repair the sub storages to make them identical
   * @method repair
   * @param  {object} command The JIO command
   */
  that.repair = function (command) {
    function callback(err, response) {
      if (err) {
        return that.error(err);
      }
      that.success(response);
    }
    priv.repair(
      command.cloneDoc(),
      command.cloneOption(),
      true,
      callback
    );
  };

  priv.check = function (doc, option, success, error) {
    priv.repair(doc, option, false, success, error);
  };

  priv.repair = function (doc, option, repair, callback) {
    var functions = {};
    callback = callback || priv.emptyFunction;
    option = option || {};
    functions.begin = function () {
    // };
    // functions.repairAllSubStorages = function () {
      var i;
      for (i = 0; i < priv.storage_list.length; i += 1) {
        priv.send(
          repair ? "repair" : "check",
          i,
          doc,
          option,
          functions.repairAllSubStoragesCallback
        );
      }
    };
    functions.repair_sub_storages_count = 0;
    functions.repairAllSubStoragesCallback = function (method,
                                                       index, err, response) {
      if (err) {
        return that.error(err);
      }
      functions.repair_sub_storages_count += 1;
      if (functions.repair_sub_storages_count === priv.storage_list.length) {
        functions.getAllDocuments(functions.newParam(
          doc,
          option,
          repair
        ));
      }
    };
    functions.newParam = function (doc, option, repair) {
      var param = {
        "doc": doc, // the document to repair
        "option": option,
        "repair": repair,
        "responses": {
          "count": 0,
          "list": [
            // 0: response0
            // 1: response1
            // 2: response2
          ],
          "stats": {
            // responseA: [0, 1]
            // responseB: [2]
          },
          "stats_items": [
            // 0: [responseA, [0, 1]]
            // 1: [responseB, [2]]
          ],
          "attachments": {
            // attachmentA : {_id: attachmentA, _revs_info, _mimetype: ..}
            // attachmentB : {_id: attachmentB, _revs_info, _mimetype: ..}
          }
        },
        "conflicts": {
          // revC: true
          // revD: true
        },
        "deal_result_state": "ok",
        "my_rev": undefined
      };
      param.responses.list.length = priv.storage_list.length;
      return param;
    };
    functions.getAllDocuments = function (param) {
      var i, doc = priv.clone(param.doc), option = priv.clone(param.option);
      option.conflicts = true;
      option.revs = true;
      option.revs_info = true;
      for (i = 0; i < priv.storage_list.length; i += 1) {
        // if the document is not loaded
        priv.send("get", i, doc, option, functions.dealResults(param));
      }
      functions.finished_count += 1;
    };
    functions.dealResults = function (param) {
      return function (method, index, err, response) {
        var response_object = {};
        if (param.deal_result_state !== "ok") {
          // deal result is in a wrong state, exit
          return;
        }
        if (err) {
          if (err.status !== 404) {
            // get document failed, exit
            param.deal_result_state = "error";
            callback({
              "status": 40,
              "statusText": "Check Failed",
              "error": "check_failed",
              "message": "An error occured on the sub storage",
              "reason": err.reason
            }, undefined);
            return;
          }
        }
        // success to get the document
        // add the response in memory
        param.responses.count += 1;
        param.responses.list[index] = response;

        // add the conflicting revision for other synchronizations
        functions.addConflicts(param, (response || {})._conflicts);
        if (param.responses.count !== param.responses.list.length) {
          // this is not the last response, wait for the next response
          return;
        }

        // this is now the last response
        functions.makeResponsesStats(param.responses);
        if (param.responses.stats_items.length === 1) {
          // the responses are equals!
          response_object.ok = true;
          response_object.id = param.doc._id;
          if (doc._rev) {
            response_object.rev = doc._rev;
            // "rev": (typeof param.responses.list[0] === "object" ?
            //         param.responses.list[0]._rev : undefined)
          }
          callback(undefined, response_object);
          return;
        }
        // the responses are different
        if (param.repair === false) {
          // do not repair
          callback({
            "status": 41,
            "statusText": "Check Not Ok",
            "error": "check_not_ok",
            "message": "Some documents are different in the sub storages",
            "reason": "Storage contents differ"
          }, undefined);
          return;
        }
        // repair
        functions.getAttachments(param);
      };
    };
    functions.addConflicts = function (param, list) {
      var i;
      list = list || [];
      for (i = 0; i < list.length; i += 1) {
        param.conflicts[list[i]] = true;
      }
    };
    functions.makeResponsesStats = function (responses) {
      var i, str_response;
      for (i = 0; i < responses.count; i += 1) {
        str_response = JSON.stringify(responses.list[i]);
        if (responses.stats[str_response] === undefined) {
          responses.stats[str_response] = [];
          responses.stats_items.push([
            str_response,
            responses.stats[str_response]
          ]);
        }
        responses.stats[str_response].push(i);
      }
    };
    functions.getAttachments = function (param) {
      var response, parsed_response, attachment;
      for (response in param.responses.stats) {
        if (param.responses.stats.hasOwnProperty(response)) {
          parsed_response = JSON.parse(response);
          for (attachment in parsed_response._attachments) {
            if ((parsed_response._attachments).hasOwnProperty(attachment)) {
              functions.get_attachment_count += 1;
              priv.send(
                "get",
                param.responses.stats[response][0],
                {
                  "_id": param.doc._id + "/" + attachment,
                  "_rev": JSON.parse(response)._rev
                },
                param.option,
                functions.getAttachmentsCallback(
                  param,
                  attachment,
                  param.responses.stats[response]
                )
              );
            }
          }
        }
      }
    };
    functions.get_attachment_count = 0;
    functions.getAttachmentsCallback = function (
      param,
      attachment_id,
      index_list
    ) {
      return function (method, index, err, response) {
        if (err) {
          callback({
            "status": 40,
            "statusText": "Check Failed",
            "error": "check_failed",
            "message": "Unable to retreive attachments",
            "reason": err.reason
          }, undefined);
          return;
        }
        functions.get_attachment_count -= 1;
        param.responses.attachments[attachment_id] = response;
        if (functions.get_attachment_count === 0) {
          functions.synchronizeAllSubStorage(param);
          if (param.option.synchronize_conflicts !== false) {
            functions.synchronizeConflicts(param);
          }
        }
      };
    };
    functions.synchronizeAllSubStorage = function (param) {
      var i, j, len = param.responses.stats_items.length;
      for (i = 0; i < len; i += 1) {
        // browsing responses
        for (j = 0; j < len; j += 1) {
          // browsing storage list
          if (i !== j) {
            functions.synchronizeResponseToSubStorage(
              param,
              param.responses.stats_items[i][0],
              param.responses.stats_items[j][1]
            );
          }
        }
      }
      functions.finished_count -= 1;
    };
    functions.synchronizeResponseToSubStorage = function (
      param,
      response,
      storage_list
    ) {
      var i, new_doc, attachment_to_put = [];
      if (response === undefined) {
        // no response to sync
        return;
      }
      new_doc = JSON.parse(response);
      new_doc._revs = new_doc._revisions;
      delete new_doc._rev;
      delete new_doc._revisions;
      delete new_doc._conflicts;
      for (i in new_doc._attachments) {
        if (new_doc._attachments.hasOwnProperty(i)) {
          attachment_to_put.push({
            "_id": i,
            "_mimetype": new_doc._attachments[i].content_type,
            "_revs_info": new_doc._revs_info
          });
        }
      }
      for (i = 0; i < storage_list.length; i += 1) {
        functions.finished_count += attachment_to_put.length || 1;
        priv.send(
          "put",
          storage_list[i],
          new_doc,
          param.option,
          functions.putAttachments(param, attachment_to_put)
        );
      }
      functions.finished_count += 1;
      functions.finished();
    };
    functions.synchronizeConflicts = function (param) {
      var rev, new_doc, new_option;
      new_option = priv.clone(param.option);
      new_option.synchronize_conflict = false;
      for (rev in param.conflicts) {
        if (param.conflicts.hasOwnProperty(rev)) {
          new_doc = priv.clone(param.doc);
          new_doc._rev = rev;
          // no need to synchronize all the conflicts again, do it once
          functions.getAllDocuments(functions.newParam(
            new_doc,
            new_option,
            param.repair
          ));
        }
      }
    };
    functions.putAttachments = function (param, attachment_to_put) {
      return function (method, index, err, response) {
        var i, attachment;
        if (err) {
          return callback({
            "status": 40,
            "statusText": "Check Failed",
            "error": "check_failed",
            "message": "Unable to copy attachments",
            "reason": err.reason
          }, undefined);
        }
        for (i = 0; i < attachment_to_put.length; i += 1) {
          attachment = {
            "_id": param.doc._id,
            "_attachment": attachment_to_put[i]._id,
            "_mimetype": attachment_to_put[i]._mimetype,
            "_revs_info": attachment_to_put[i]._revs_info,
            // "_revs_info": param.responses.list[index]._revs_info,
            "_data": param.responses.attachments[attachment_to_put[i]._id]
          };
          attachment._id += "/" + attachment._attachment;
          delete attachment._attachment;
          priv.send(
            "putAttachment",
            index,
            attachment,
            option,
            functions.putAttachmentCallback(param)
          );
        }
        if (attachment_to_put.length === 0) {
          functions.finished();
        }
      };
    };
    functions.putAttachmentCallback = function (param) {
      return function (method, index, err, response) {
        if (err) {
          return callback(err, undefined);
        }
        functions.finished();
      };
    };
    functions.finished_count = 0;
    functions.finished = function () {
      var response_object = {};
      functions.finished_count -= 1;
      if (functions.finished_count === 0) {
        response_object.ok = true;
        response_object.id = doc._id;
        if (doc._rev) {
          response_object.rev = doc._rev;
        }
        callback(undefined, response_object);
      }
    };
    functions.begin();
  };

  /**
   * The generic method to use
   * @method genericRequest
   * @param  {object} command The JIO command
   * @param  {string} method The method to use
   */
  that.genericRequest = function (command, method) {
    var doc = command.cloneDoc();
    doc._id = doc._id || priv.generateUuid();
    priv.sendToAllFastestResponseOnly(
      method,
      doc,
      command.cloneOption(),
      function (method, err, response) {
        if (err) {
          return that.error(err);
        }
        that.success(response);
      }
    );
  };

  /**
   * Post the document metadata to all sub storages
   * @method post
   * @param  {object} command The JIO command
   */
  that.post = function (command) {
    that.genericRequest(command, "put");
  };

  /**
   * Put the document metadata to all sub storages
   * @method put
   * @param  {object} command The JIO command
   */
  that.put = function (command) {
    that.genericRequest(command, "post");
  };

  /**
   * Put an attachment to a document to all sub storages
   * @method putAttachment
   * @param  {object} command The JIO command
   */
  that.putAttachment = function (command) {
    that.genericRequest(command, "putAttachment");
  };

  /**
   * Get the document from all sub storages, get the fastest.
   * @method get
   * @param  {object} command The JIO command
   */
  that.get = function (command) {
    that.genericRequest(command, "get");
  };

  /**
   * Get the attachment from all sub storages, get the fastest.
   * @method getAttachment
   * @param  {object} command The JIO command
   */
  that.getAttachment = function (command) {
    that.genericRequest(command, "getAttachment");
  };

  /**
   * Remove the document from all sub storages.
   * @method remove
   * @param  {object} command The JIO command
   */
  that.remove = function (command) {
    that.genericRequest(command, "remove");
  };

  /**
   * Remove the attachment from all sub storages.
   * @method remove
   * @param  {object} command The JIO command
   */
  that.removeAttachment = function (command) {
    that.genericRequest(command, "removeAttachment");
  };

  return that;
});