/*jslint indent: 2, maxlen: 80, sloppy: true, nomen: true */
/*global jIO: true, hex_sha256: true, setTimeout: true */
/**
 * JIO Revision Storage.
 * It manages document version and can generate conflicts.
 * Description:
 * {
 *     "type": "revision",
 *     "sub_storage": <sub storage description>
 * }
 */
jIO.addStorageType('revision', function (spec, my) {
  "use strict";
  var that, priv = {};
  spec = spec || {};
  that = my.basicStorage(spec, my);

  priv.substorage_key = "sub_storage";
  priv.doctree_suffix = ".revision_tree.json";
  priv.substorage = spec[priv.substorage_key];

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

  /**
   * 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);
  };

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

  /**
   * Generates a hash code of a string
   * @method hashCode
   * @param  {string} string The string to hash
   * @return {string} The string hash code
   */
  priv.hashCode = function (string) {
    return hex_sha256(string);
  };

  /**
   * Returns an array version of a revision string
   * @method revisionToArray
   * @param  {string} revision The revision string
   * @return {array} Array containing a revision number and a hash
   */
  priv.revisionToArray = function (revision) {
    if (typeof revision === "string") {
      return [parseInt(revision.split('-')[0], 10),
        revision.split('-')[1]];
    }
    return revision;
  };

  /**
   * Convert the revision history object to an array of revisions.
   * @method revisionHistoryToArray
   * @param  {object} revs The revision history
   * @return {array} The revision array
   */
  priv.revisionHistoryToArray = function (revs) {
    var i, start = revs.start, newlist = [];
    for (i = 0; i < revs.ids.length; i += 1, start -= 1) {
      newlist.push(start + "-" + revs.ids[i]);
    }
    return newlist;
  };

  /**
   * Generates the next revision of [previous_revision]. [string] helps us
   * to generate a hash code.
   * @methode generateNextRev
   * @param  {string} previous_revision The previous revision
   * @param  {object} doc The document metadata
   * @param  {object} revisions The revision history
   * @param  {boolean} deleted_flag The deleted flag
   * @return {array} 0:The next revision number and 1:the hash code
   */
  priv.generateNextRevision = function (previous_revision,
    doc, revisions, deleted_flag) {
    var string = JSON.stringify(doc) + JSON.stringify(revisions) +
      JSON.stringify(deleted_flag ? true : false);
    if (typeof previous_revision === "number") {
      return [previous_revision + 1, priv.hashCode(string)];
    }
    previous_revision = priv.revisionToArray(previous_revision);
    return [previous_revision[0] + 1, priv.hashCode(string)];
  };

  /**
   * 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));
  };

  /**
   * Creates an empty document tree
   * @method createDocumentTree
   * @param  {array} children An array of children (optional)
   * @return {object} The new document tree
   */
  priv.createDocumentTree = function (children) {
    return {
      "children": children || []
    };
  };

  /**
   * Creates a new document tree node
   * @method createDocumentTreeNode
   * @param  {string} revision The node revision
   * @param  {string} status The node status
   * @param  {array} children An array of children (optional)
   * @return {object} The new document tree node
   */
  priv.createDocumentTreeNode = function (revision, status, children) {
    return {
      "rev": revision,
      "status": status,
      "children": children || []
    };
  };

  /**
   * Gets the specific revision from a document tree.
   * @method getRevisionFromDocumentTree
   * @param  {object} document_tree The document tree
   * @param  {string} revision The specific revision
   * @return {array} The good revs info array
   */
  priv.getRevisionFromDocumentTree = function (document_tree, revision) {
    var result, search, revs_info = [];
    result = [];
    // search method fills "result" with the good revs info
    search = function (document_tree) {
      var i;
      if (document_tree.rev !== undefined) {
        // node is not root
        revs_info.unshift({
          "rev": document_tree.rev,
          "status": document_tree.status
        });
        if (document_tree.rev === revision) {
          result = revs_info;
          return;
        }
      }
      // This node has children
      for (i = 0; i < document_tree.children.length; i += 1) {
        // searching deeper to find the good rev
        search(document_tree.children[i]);
        if (result.length > 0) {
          // The result is already found
          return;
        }
        revs_info.shift();
      }
    };
    search(document_tree);
    return result;
  };

  /**
   * Gets the winner revision from a document tree.
   * The winner is the deeper revision on the left.
   * @method getWinnerRevisionFromDocumentTree
   * @param  {object} document_tree The document tree
   * @return {array} The winner revs info array
   */
  priv.getWinnerRevisionFromDocumentTree = function (document_tree) {
    var result, search, revs_info = [];
    result = [];
    // search method fills "result" with the winner revs info
    search = function (document_tree, deep) {
      var i;
      if (document_tree.rev !== undefined) {
        // node is not root
        revs_info.unshift({
          "rev": document_tree.rev,
          "status": document_tree.status
        });
      }
      if (document_tree.children.length === 0 && document_tree.status !==
          "deleted") {
        // This node is a leaf
        if (result.length < deep) {
          // The leaf is deeper than result
          result = [];
          for (i = 0; i < revs_info.length; i += 1) {
            result.push(revs_info[i]);
          }
        }
        return;
      }
      // This node has children
      for (i = 0; i < document_tree.children.length; i += 1) {
        // searching deeper to find the deeper leaf
        search(document_tree.children[i], deep + 1);
        revs_info.shift();
      }
    };
    search(document_tree, 0);
    return result;
  };

  /**
   * Add a document revision branch to the document tree
   * @method updateDocumentTree
   * @param  {object} doctree The document tree object
   * @param  {object|array} revs The revision history object or a revision array
   * @param  {boolean} deleted The deleted flag
   * @param  {array} The document revs_info
   */
  priv.updateDocumentTree = function (doctree, revs, deleted) {
    var revs_info, doctree_iterator, flag, i, rev;
    revs_info = [];
    if (revs.ids) {
      // revs is a revision history object
      revs = priv.revisionHistoryToArray(revs);
    } else {
      // revs is an array of revisions
      revs = priv.clone(revs);
    }
    doctree_iterator = doctree;
    while (revs.length > 0) {
      rev = revs.pop(0);
      revs_info.unshift({
        "rev": rev,
        "status": "missing"
      });
      for (i = 0; i < doctree_iterator.children.length; i += 1) {
        if (doctree_iterator.children[i].rev === rev) {
          doctree_iterator = doctree_iterator.children[i];
          revs_info[0].status = doctree_iterator.status;
          rev = undefined;
          break;
        }
      }
      if (rev) {
        doctree_iterator.children.unshift({
          "rev": rev,
          "status": "missing",
          "children": []
        });
        doctree_iterator = doctree_iterator.children[0];
      }
    }
    flag = deleted === true ? "deleted" : "available";
    revs_info[0].status = flag;
    doctree_iterator.status = flag;
    return revs_info;
  };

  /**
   * Add a document revision to the document tree
   * @method postToDocumentTree
   * @param  {object} doctree The document tree object
   * @param  {object} doc The document object
   * @param  {boolean} set_node_to_deleted Set the revision to deleted
   * @return {array} The added document revs_info
   */
  priv.postToDocumentTree = function (doctree, doc, set_node_to_deleted) {
    var i, revs_info, next_rev, next_rev_str, selectNode, selected_node,
      flag;
    flag = set_node_to_deleted === true ? "deleted" : "available";
    revs_info = [];
    selected_node = doctree;
    selectNode = function (node) {
      var i;
      if (node.rev !== undefined) {
        // node is not root
        revs_info.unshift({
          "rev": node.rev,
          "status": node.status
        });
      }
      if (node.rev === doc._rev) {
        selected_node = node;
        return "node_selected";
      }
      for (i = 0; i < node.children.length; i += 1) {
        if (selectNode(node.children[i]) === "node_selected") {
          return "node_selected";
        }
        revs_info.shift();
      }
    };
    if (typeof doc._rev === "string") {
      // document has a previous revision
      if (selectNode(selected_node) !== "node_selected") {
        // no node was selected, so add a node with a specific rev
        revs_info.unshift({
          "rev": doc._rev,
          "status": "missing"
        });
        selected_node.children.unshift(priv.createDocumentTreeNode(
          doc._rev,
          "missing"
        ));
        selected_node = selected_node.children[0];
      }
    }
    next_rev = priv.generateNextRevision(
      doc._rev || 0,
      doc,
      priv.revsInfoToHistory(revs_info),
      set_node_to_deleted
    );
    next_rev_str = next_rev.join("-");
    // don't add if the next rev already exists
    for (i = 0; i < selected_node.children.length; i += 1) {
      if (selected_node.children[i].rev === next_rev_str) {
        revs_info.unshift({
          "rev": next_rev_str,
          "status": flag
        });
        if (selected_node.children[i].status !== flag) {
          selected_node.children[i].status = flag;
        }
        return revs_info;
      }
    }
    revs_info.unshift({
      "rev": next_rev.join('-'),
      "status": flag
    });

    selected_node.children.unshift(priv.createDocumentTreeNode(
      next_rev.join('-'),
      flag
    ));

    return revs_info;
  };

  /**
   * Gets an array of leaves revisions from document tree
   * @method getLeavesFromDocumentTree
   * @param  {object} document_tree The document tree
   * @param  {string} except The revision to except
   * @return {array} The array of leaves revisions
   */
  priv.getLeavesFromDocumentTree = function (document_tree, except) {
    var result, search;
    result = [];
    // search method fills [result] with the winner revision
    search = function (document_tree) {
      var i;
      if (except !== undefined && except === document_tree.rev) {
        return;
      }
      if (document_tree.children.length === 0 && document_tree.status !==
          "deleted") {
        // This node is a leaf
        result.push(document_tree.rev);
        return;
      }
      // This node has children
      for (i = 0; i < document_tree.children.length; i += 1) {
        // searching deeper to find the deeper leaf
        search(document_tree.children[i]);
      }
    };
    search(document_tree);
    return result;
  };

  /**
   * Check if revision is a leaf
   * @method isRevisionALeaf
   * @param  {string} revision revision to check
   * @param  {array} leaves all leaves on tree
   * @return {boolean} true/false
   */
  priv.isRevisionALeaf = function (document_tree, revision) {
    var result, search;
    result = undefined;
    // search method fills "result" with the good revs info
    search = function (document_tree) {
      var i;
      if (document_tree.rev !== undefined) {
        // node is not root
        if (document_tree.rev === revision) {
          if (document_tree.children.length === 0) {
            // This node is a leaf
            result = true;
            return;
          }
          result = false;
          return;
        }
      }
      // This node has children
      for (i = 0; i < document_tree.children.length; i += 1) {
        // searching deeper to find the good rev
        search(document_tree.children[i]);
        if (result !== undefined) {
          // The result is already found
          return;
        }
      }
    };
    search(document_tree);
    return result || false;
  };

  /**
   * Convert revs_info to a simple revisions history
   * @method revsInfoToHistory
   * @param  {array} revs_info The revs info
   * @return {object} The revisions history
   */
  priv.revsInfoToHistory = function (revs_info) {
    var revisions = {
      "start": 0,
      "ids": []
    }, i;
    if (revs_info.length > 0) {
      revisions.start = parseInt(revs_info[0].rev.split('-')[0], 10);
    }
    for (i = 0; i < revs_info.length; i += 1) {
      revisions.ids.push(revs_info[i].rev.split('-')[1]);
    }
    return revisions;
  };

  /**
   * Returns the revision of the revision position from a revs_info array.
   * @method getRevisionFromPosition
   * @param  {array} revs_info The revs_info array
   * @param  {number} rev_pos The revision position number
   * @return {string} The revision of the good position (empty string if fail)
   */
  priv.getRevisionFromPosition = function (revs_info, rev_pos) {
    var i;
    for (i = revs_info.length - 1; i >= 0; i -= 1) {
      if (priv.revisionToArray(revs_info[i].rev)[0] === rev_pos) {
        return revs_info[i].rev;
      }
    }
    return '';
  };

  /**
   * Post the document metadata and create or update a document tree.
   * Options:
   * - {boolean} keep_revision_history To keep the previous revisions
   *                                   (false by default) (NYI).
   * @method post
   * @param  {object} command The JIO command
   */
  that.post = function (command) {
    var f = {}, doctree, revs_info, doc, docid;
    doc = command.cloneDoc();
    docid = command.getDocId();

    if (typeof doc._rev === "string" && !priv.checkRevisionFormat(doc._rev)) {
      that.error({
        "status": 31,
        "statusText": "Wrong Revision Format",
        "error": "wrong_revision_format",
        "message": "The document previous revision does not match " +
          "^[0-9]+-[0-9a-zA-Z]+$",
        "reason": "Previous revision is wrong"
      });
      return;
    }
    if (typeof docid !== "string") {
      doc._id = priv.generateUuid();
      docid = doc._id;
    }
    f.getDocumentTree = function () {
      var option = command.cloneOption();
      if (option.max_retry === 0) {
        option.max_retry = 3;
      }
      that.addJob(
        "get",
        priv.substorage,
        docid + priv.doctree_suffix,
        option,

        function (response) {

          doctree = response;
          f.postDocument("put");
        },
        function (err) {
          switch (err.status) {
          case 404:
            doctree = priv.createDocumentTree();
            f.postDocument("post");
            break;
          default:
            err.message = "Cannot get document revision tree";
            that.error(err);
            break;
          }
        }
      );
    };
    f.postDocument = function (doctree_update_method) {
      if (doc._revs) {
        revs_info = priv.updateDocumentTree(doctree, doc._revs);
      } else {
        revs_info = priv.postToDocumentTree(doctree, doc);
      }
      doc._id = docid + "." + revs_info[0].rev;
      delete doc._rev;
      delete doc._revs;
      that.addJob(
        "post",
        priv.substorage,
        doc,
        command.cloneOption(),
        function () {
          f.sendDocumentTree(doctree_update_method);
        },
        function (err) {
          switch (err.status) {
          case 409:
            // file already exists
            f.sendDocumentTree(doctree_update_method);
            break;
          default:
            err.message = "Cannot upload document";
            that.error(err);
            break;
          }
        }
      );
    };
    f.sendDocumentTree = function (method) {
      doctree._id = docid + priv.doctree_suffix;
      that.addJob(
        method,
        priv.substorage,
        doctree,
        command.cloneOption(),
        function () {
          that.success({
            "ok": true,
            "id": docid,
            "rev": revs_info[0].rev
          });
        },
        function (err) {
          // xxx do we try to delete the posted document ?
          err.message = "Cannot save document revision tree";
          that.error(err);
        }
      );
    };
    f.getDocumentTree();
  };

  /**
   * Update the document metadata and update a document tree.
   * Options:
   * - {boolean} keep_revision_history To keep the previous revisions
   *                                   (false by default) (NYI).
   * @method put
   * @param  {object} command The JIO command
   */
  that.put = function (command) {
    that.post(command);
  };

  /**
   * Get the document metadata or attachment.
   * Options:
   * - {boolean} revs Add simple revision history (false by default).
   * - {boolean} revs_info Add revs info (false by default).
   * - {boolean} conflicts Add conflict object (false by default).
   * @method get
   * @param  {object} command The JIO command
   */
  that.get = function (command) {
    var f = {}, doctree, revs_info, prev_rev, option;
    option = command.cloneOption();
    if (option.max_retry === 0) {
      option.max_retry = 3;
    }
    prev_rev = command.getOption("rev");
    if (typeof prev_rev === "string") {
      if (!priv.checkRevisionFormat(prev_rev)) {
        that.error({
          "status": 31,
          "statusText": "Wrong Revision Format",
          "error": "wrong_revision_format",
          "message": "The document previous revision does not match " +
            "[0-9]+-[0-9a-zA-Z]+",
          "reason": "Previous revision is wrong"
        });
        return;
      }
    }
    f.getDocumentTree = function () {
      that.addJob(
        "get",
        priv.substorage,
        command.getDocId() + priv.doctree_suffix,
        option,
        function (response) {
          doctree = response;
          if (prev_rev === undefined) {
            revs_info = priv.getWinnerRevisionFromDocumentTree(doctree);
            if (revs_info.length > 0) {
              prev_rev = revs_info[0].rev;
            } else {
              that.error({
                "status": 404,
                "statusText": "Not Found",
                "error": "not_found",
                "message": "Cannot find the document",
                "reason": "Document is deleted"
              });
              return;
            }
          } else {
            revs_info = priv.getRevisionFromDocumentTree(doctree, prev_rev);
          }
          f.getDocument(command.getDocId() + "." + prev_rev,
            command.getAttachmentId());
        },
        function (err) {
          switch (err.status) {
          case 404:
            that.error(err);
            break;
          default:
            err.message = "Cannot get document revision tree";
            that.error(err);
            break;
          }
        }
      );
    };
    f.getDocument = function (docid, attmtid) {
      that.addJob(
        "get",
        priv.substorage,
        docid,
        option,
        function (response) {
          var attmt;
          if (typeof response !== "string") {
            if (attmtid !== undefined) {
              if (response._attachments !== undefined) {
                attmt = response._attachments[attmtid];
                if (attmt !== undefined) {
                  prev_rev = priv.getRevisionFromPosition(
                    revs_info,
                    attmt.revpos
                  );
                  f.getDocument(command.getDocId() + "." + prev_rev + "/" +
                    attmtid);
                  return;
                }
              }
              that.error({
                "status": 404,
                "statusText": "Not Found",
                "error": "not_found",
                "message": "Cannot find the attachment",
                "reason": "Attachment is missing"
              });
              return;
            }
            response._id = command.getDocId();
            response._rev = prev_rev;
            if (command.getOption("revs") === true) {
              response._revisions = priv.revsInfoToHistory(revs_info);
            }
            if (command.getOption("revs_info") === true) {
              response._revs_info = revs_info;
            }
            if (command.getOption("conflicts") === true) {
              response._conflicts = priv.getLeavesFromDocumentTree(
                doctree,
                prev_rev
              );
              if (response._conflicts.length === 0) {
                delete response._conflicts;
              }
            }
          }
          that.success(response);
        },
        function (err) {
          that.error(err);
        }
      );
    };
    if (command.getAttachmentId() && prev_rev !== undefined) {
      f.getDocument(command.getDocId() + "." + prev_rev +
        "/" + command.getAttachmentId());
    } else {
      f.getDocumentTree();
    }
  };

  /**
   * Remove document or attachment.
   * Options:
   * - {boolean} keep_revision_history To keep the previous revisions
   * @method remove
   * @param  {object} command The JIO command
   */
  that.remove = function (command) {
    var f = {}, del_rev, option, new_doc, revs_info;
    option = command.cloneOption();
    if (option.max_retry === 0) {
      option.max_retry = 3;
    }
    del_rev = command.getDoc()._rev;

    f.removeDocument = function (docid, doctree) {
      if (command.getOption("keep_revision_history") !== true) {
        if (command.getAttachmentId() === undefined) {
          // update tree
          revs_info = priv.postToDocumentTree(
            doctree,
            command.getDoc(),
            true
          );
          // remove revision
          that.addJob(
            "remove",
            priv.substorage,
            docid,
            option,
            function () {
              // put tree
              doctree._id = command.getDocId() + priv.doctree_suffix;
              that.addJob(
                "put",
                priv.substorage,
                doctree,
                command.cloneOption(),
                function () {
                  that.success({
                    "ok": true,
                    "id": command.getDocId(),
                    "rev": revs_info[0].rev
                  });
                },
                function () {
                  that.error({
                    "status": 409,
                    "statusText": "Conflict",
                    "error": "conflict",
                    "message": "Document update conflict.",
                    "reason": "Cannot update document tree"
                  });
                  return;
                }
              );
            },
            function () {
              that.error({
                "status": 404,
                "statusText": "Not Found",
                "error": "not_found",
                "message": "File not found",
                "reason": "Document was not found"
              });
              return;
            }
          );
        } else {
          // get previsous document
          that.addJob(
            "get",
            priv.substorage,
            command.getDocId() + "." + del_rev,
            option,
            function (response) {
              // update tree
              revs_info = priv.postToDocumentTree(doctree, command.getDoc());
              new_doc = response;
              delete new_doc._attachments;
              new_doc._id = new_doc._id + "." + revs_info[0].rev;

              // post new document version
              that.addJob(
                "post",
                priv.substorage,
                new_doc,
                command.cloneOption(),
                function () {
                  // put tree
                  doctree._id = command.getDocId() + priv.doctree_suffix;
                  that.addJob(
                    "put",
                    priv.substorage,
                    doctree,
                    command.cloneOption(),
                    function () {
                      that.success({
                        "ok": true,
                        "id": new_doc._id,
                        "rev": revs_info[0].rev
                      });
                    },
                    function (err) {
                      err.message =
                        "Cannot save document revision tree";
                      that.error(err);
                    }
                  );
                },
                function () {
                  that.error({
                    "status": 409,
                    "statusText": "Conflict",
                    "error": "conflict",
                    "message": "Document update conflict.",
                    "reason": "Cannot update document"
                  });
                  return;
                }
              );
            },
            function () {
              that.error({
                "status": 404,
                "statusText": "Not Found",
                "error": "not_found",
                "message": "File not found",
                "reason": "Document was not found"
              });
              return;
            }
          );
        }
      }
    };
    if (typeof del_rev === "string") {
      if (!priv.checkRevisionFormat(del_rev)) {
        that.error({
          "status": 31,
          "statusText": "Wrong Revision Format",
          "error": "wrong_revision_format",
          "message": "The document previous revision does not match " +
            "[0-9]+-[0-9a-zA-Z]+",
          "reason": "Previous revision is wrong"
        });
        return;
      }
    }

    // get doctree
    that.addJob(
      "get",
      priv.substorage,
      command.getDocId() + priv.doctree_suffix,
      option,
      function (response) {
        response._conflicts = priv.getLeavesFromDocumentTree(response);

        if (del_rev === undefined) {
          // no revision provided
          that.error({
            "status": 409,
            "statusText": "Conflict",
            "error": "conflict",
            "message": "Document update conflict.",
            "reason": "Cannot delete a document without revision"
          });
          return;
        }
        // revision provided
        if (priv.isRevisionALeaf(response, del_rev) === true) {
          if (typeof command.getAttachmentId() === "string") {
            f.removeDocument(command.getDocId() + "." + del_rev +
              "/" + command.getAttachmentId(), response);
          } else {
            f.removeDocument(command.getDocId() + "." + del_rev,
              response);
          }
        } else {
          that.error({
            "status": 409,
            "statusText": "Conflict",
            "error": "conflict",
            "message": "Document update conflict.",
            "reason": "Trying to remove non-latest revision"
          });
          return;
        }
      },
      function () {
        that.error({
          "status": 404,
          "statusText": "Not Found",
          "error": "not_found",
          "message": "Document tree not found, please checkdocument ID",
          "reason": "Incorrect document ID"
        });
        return;
      }
    );
  };

  /**
   * Get all documents
   * @method allDocs
   * @param  {object} command The JIO command
   */
  that.allDocs = function () {
    setTimeout(function () {
      that.error({
        "status": 405,
        "statusText": "Method Not Allowed",
        "error": "method_not_allowed",
        "message": "Your are not allowed to use this command",
        "reason": "LocalStorage forbids AllDocs command executions"
      });
    });
  };

  return that;
});