/**
 * JIO Revision Storage.
 * It manages document version and can generate conflicts.
 * Description:
 * {
 *     "type": "revision",
 *     "secondstorage": <sub storage description>
 * }
 */
jIO.addStorageType('revision', function (spec, my) {
    "use strict";
    var that, priv = {};
    spec = spec || {};
    that = my.basicStorage(spec, my);

    priv.substorage_key = "secondstorage";
    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;
    };

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

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

    /**
     * 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 i, result, search, revs_info = [];
        result = [];
        // search method fills "result" with the good revs info
        search = function (document_tree) {
            var i;
            if (typeof 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 i, result, search, revs_info = [];
        result = [];
        // search method fills "result" with the winner revs info
        search = function (document_tree, deep) {
            var i;
            if (typeof 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 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 (typeof 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";
            } else {
                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({
                    "rev": doc._rev,
                    "status": "missing",
                    "children": []
                });
                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({
            "rev": next_rev.join('-'),
            "status": flag,
            "children": []
        });

        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 i, 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 i, result, search;
        result = undefined;
        // search method fills "result" with the good revs info
        search = function (document_tree) {
            var i;
            if (typeof 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;
        }
        if (priv.update_doctree_allowed === undefined) {
            priv.update_doctree_allowed = true;
        }
        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;
                    if (priv.update_doctree_allowed) {
                        f.postDocument("put");
                    } else {
                        that.error({
                            "status": 409,
                            "statusText": "Conflict",
                            "error": "conflict",
                            "message": "Cannot update a document",
                            "reason": "Document update conflict"
                        });
                    }
                }, 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) {
            revs_info = priv.postToDocumentTree(doctree, doc);
            doc._id = docid+"."+revs_info[0].rev;
            that.addJob(
                "post",
                priv.substorage,
                doc,
                command.cloneOption(),
                function (response) {
                    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 (response) {
                    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) {
        if (command.cloneDoc()._rev === undefined) {
            priv.update_doctree_allowed = false;
        }
        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);
                        prev_rev = revs_info[0].rev;
                    } 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, new_id;
        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 (response) {
                            // put tree
                            doctree._id = command.getDocId()+
                                priv.doctree_suffix;
                            that.addJob(
                                "put",
                                priv.substorage,
                                doctree,
                                command.cloneOption(),
                                function (response) {
                                    that.success({
                                        "ok":true,
                                        "id":command.getDocId(),
                                        "rev":revs_info[0].rev
                                    });
                                },
                                function (err){
                                    that.error({
                                        "status": 409,
                                        "statusText": "Conflict",
                                        "error": "conflict",
                                        "message": "Document update conflict.",
                                        "reason": "Cannot update document tree"
                                    });
                                    return;
                                }
                            );
                        },
                        function (err) {
                            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 (response) {

                                    // put tree
                                    doctree._id = command.getDocId()+
                                        priv.doctree_suffix;
                                    that.addJob(
                                        "put",
                                        priv.substorage,
                                        doctree,
                                        command.cloneOption(),
                                        function (response) {
                                            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 (err){
                                    that.error({
                                        "status": 409,
                                        "statusText": "Conflict",
                                        "error": "conflict",
                                        "message": "Document update conflict.",
                                        "reason": "Cannot update document"
                                    });
                                    return;
                                }
                            );
                        },
                        function (err) {
                            that.error({
                                "status": 404,
                                "statusText": "Not Found",
                                "error": "not_found",
                                "message": "File not found",
                                "reason": "Document was not found"
                            });
                            return;
                        }
                    );
                }
            } else {
                // keep history = update document tree only
            }
        };
        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;
                } else {
                    // 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 (err) {
                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 (command) {
        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;
});