(function(window)
{
    var Gitana = window.Gitana;

    Gitana.Branch = Gitana.AbstractRepositoryObject.extend(
    /** @lends Gitana.Branch.prototype */
    {
        /**
         * @constructs
         * @augments Gitana.AbstractRepositoryObject
         *
         * @class Branch
         *
         * @param {Gitana.Repository} repository
         * @param [Object] object json object (if no callback required for populating)
         */
        constructor: function(repository, object)
        {
            this.base(repository, object);

            this.objectType = function() { return "Gitana.Branch"; };
        },

        /**
         * @OVERRIDE
         */
        getType: function()
        {
            return Gitana.TypedIDConstants.TYPE_BRANCH;
        },

        /**
         * @OVERRIDE
         */
        getUri: function()
        {
            return "/repositories/" + this.getRepositoryId() + "/branches/" + this.getId();
        },

        /**
         * @override
         */
        clone: function()
        {
            return this.getFactory().branch(this.getRepository(), this);
        },

        /**
         * @returns {Boolean} whether this is the master branch
         */
        isMaster: function()
        {
            return (this.getBranchType().toLowerCase() == "master");
        },

        /**
         * @return {String} the type of branch ("master" or "custom")
         */
        getBranchType: function()
        {
            return this.get("type");
        },

        /**
         * @return {String} the tip changeset of the branch
         */
        getTip: function()
        {
            return this.get("tip");
        },

        /**
         * Acquires a list of mount nodes under the root of the repository.
         *
         * @chained node map
         *
         * @public
         *
         * @param [Object] pagination
         */
        listMounts: function(pagination)
        {
            var self = this;

            var params = {};
            if (pagination)
            {
                Gitana.copyInto(params, pagination);
            }

            var uriFunction = function()
            {
                return self.getUri() + "/nodes";
            };

            var chainable = this.getFactory().nodeMap(this);
            return this.chainGet(chainable, uriFunction, params);
        },

        /**
         * Reads a node.
         *
         * @chained node
         *
         * @public
         *
         * @param {String} nodeId the node id
         * @param [String] offset path
         */
        readNode: function(nodeId, path)
        {
            var self = this;

            var uriFunction = function()
            {
                var uri = self.getUri() + "/nodes/" + nodeId;
                if (path) {
                    uri += "?path=" + path;
                }

                return uri;
            };

            var chainable = this.getFactory().node(this);
            return this.chainGet(chainable, uriFunction);
        },

        /**
         * Reads the root node.
         *
         * @chained node
         *
         * @public
         */
        rootNode: function()
        {
            return this.readNode("root");
        },

        /**
         * Create a node
         *
         * @chained node
         *
         * @public
         *
         * @param [Object] object JSON object
         * @param [Object|String] options a JSON object providing the configuration for the create operation.
         *                                If a string, must follow format (<rootNode>/<filePath>)
         */
        createNode: function(object, options)
        {
            var self = this;

            var uriFunction = function()
            {
                return self.getUri() + "/nodes";
            };

            var params = {};

            if (options)
            {
                var rootNodeId = "root"; // default
                var associationType = "a:child"; // default
                var filePath = null;
                var parentFolderPath = null;
                var fileName = null;

                // if they pass in a string instead of an options object, then the string can follow the format
                // (/root/pages/file.txt) where root is the root node to start from
                if (typeof(options) === "string")
                {
                    var rootPrefixedFilePath = options;

                    // filePath should not start with "/"
                    if (Gitana.startsWith(rootPrefixedFilePath, "/")) {
                        rootPrefixedFilePath = rootPrefixedFilePath.substring(1);
                    }

                    if (rootPrefixedFilePath == "") {
                        filePath = "/";
                    } else {
                        var i = rootPrefixedFilePath.indexOf("/");
                        rootNodeId = rootPrefixedFilePath.substring(0, i);
                        filePath = rootPrefixedFilePath.substring(i + 1);
                    }
                }
                else if (typeof(options) === "object")
                {
                    if (options.rootNodeId) {
                        rootNodeId = options.rootNodeId;
                    }
                    if (options.associationType) {
                        associationType = options.associationType;
                    }
                    if (options.fileName) {
                        fileName = options.fileName;
                    }
                    else if (options.filename) {
                        fileName = options.filename;
                    }
                    if (options.parentFolderPath) {
                        parentFolderPath = options.parentFolderPath;
                    }
                    else if (options.folderPath) {
                        parentFolderPath = options.folderPath;
                    }
                    else if (options.folderpath) {
                        parentFolderPath = options.folderpath;
                    }
                    if (options.filePath) {
                        filePath = options.filePath;
                    }
                    else if (options.filepath) {
                        filePath = options.filepath;
                    }
                }

                // plug in the resolved params
                if (rootNodeId) {
                    params.rootNodeId = rootNodeId;
                }
                if (associationType) {
                    params.associationType = associationType;
                }
                if (fileName) {
                    params.fileName = fileName;
                }
                if (filePath) {
                    params.filePath = filePath;
                }
                if (parentFolderPath) {
                    params.parentFolderPath = parentFolderPath;
                }

                // allow custom params to be passed through
                if (options.params) {
                    for (var param in options.params) {
                        params[param] = options.params[param];
                    }
                }
            }

            var chainable = this.getFactory().node(this);
            return this.chainCreate(chainable, object, uriFunction, params);
        },

        /**
         * Searches the branch.
         *
         * @chained node map
         *
         * Config should be:
         *
         *    {
         *       "search": {
         *           ... Elastic Search Config Block
         *       }
         *    }
         *
         * For a full text term search, you can simply provide text in place of a config json object.
         *
         * See the Elastic Search documentation for more advanced examples
         *
         * @public
         *
         * @param search
         * @param [Object] pagination
         */
        searchNodes: function(search, pagination)
        {
            var self = this;

            var params = {};
            if (pagination)
            {
                Gitana.copyInto(params, pagination);
            }

            if (Gitana.isString(search))
            {
                search = {
                    "search": search
                };
            }

            var uriFunction = function()
            {
                return self.getUri() + "/nodes/search";
            };

            var chainable = this.getFactory().nodeMap(this);
            return this.chainPost(chainable, uriFunction, params, search);
        },

        /**
         * Queries for nodes on the branch.
         *
         * Config should be:
         *
         *    {
         *       Gitana query configs
         *    }
         *
         * @chained node map
         *
         * @public
         *
         * @param {Object} query
         * @param [Object] pagination
         */
        queryNodes: function(query, pagination)
        {
            var self = this;

            if (!query) {
                query = {};
            }

            var params = {};
            if (pagination)
            {
                Gitana.copyInto(params, pagination);
            }

            var uriFunction = function()
            {
                return self.getUri() + "/nodes/query";
            };

            var chainable = this.getFactory().nodeMap(this);

            if (!Gitana.PREFER_GET_OVER_POST)
            {
                return this.chainPost(chainable, uriFunction, params, query);
            }
            else
            {
                Gitana.copyInto(params, {
                    "query": JSON.stringify(query)
                });

                return this.chainGet(chainable, uriFunction, params);
            }


        },

        /**
         * Queries for a single matching node to a query on the branch.
         *
         * @chained node
         *
         * @param query
         * @param errHandler
         *
         * @returns Gitana.Node
         */
        queryOne: function(query, errHandler)
        {
            return this.queryNodes(query).keepOne(function(err) {
                if (errHandler)
                {
                    errHandler(err);
                    return false;
                }
            });
        },

        /**
         * Deletes the nodes described the given array of node ids.
         *
         * @hcained branch
         *
         * @param nodeIds
         *
         * @returns Gitana.Branch
         */
        deleteNodes: function(nodeIds)
        {
            var self = this;

            var uriFunction = function()
            {
                return self.getUri() + "/nodes/delete";
            };

            return this.chainPost(this, uriFunction, {}, {
                "_docs": nodeIds
            });
        },

        /**
         * Performs a bulk check of permissions against permissioned objects of type node.
         *
         * Example of checks array:
         *
         * [{
         *    "permissionedId": "<permissionedId>",
         *    "principalId": "<principalId>",
         *    "permissionId": "<permissionId>"
         * }]
         *
         * The callback receives an array of results, example:
         *
         * [{
         *    "permissionedId": "<permissionedId>",
         *    "principalId": "<principalId>",
         *    "permissionId": "<permissionId>",
         *    "result": true
         * }]
         *
         * The order of elements in the array will be the same for checks and results.
         *
         * @param checks
         * @param callback
         */
        checkNodePermissions: function(checks, callback)
        {
            var self = this;

            var uriFunction = function()
            {
                return self.getUri() + "/nodes/permissions/check";
            };

            var object = {
                "checks": checks
            };

            return this.chainPostResponse(this, uriFunction, {}, object).then(function(response) {
                callback.call(this, response["results"]);
            });
        },


        /**
         * Reads the person object for a security user.
         *
         * @chained node
         *
         * @param {Object} user either the user id, user name or the user object
         * @param [Boolean] createIfNotFound whether to create the person object if it isn't found
         */
        readPersonNode: function(user, createIfNotFound)
        {
            var self = this;

            var principalDomainQualifiedId = this.extractPrincipalDomainQualifiedId(user);

            var uriFunction = function()
            {
                var uri = self.getUri() + "/person/acquire?id=" + principalDomainQualifiedId;
                if (createIfNotFound)
                {
                    uri += "&createIfNotFound=" + createIfNotFound;
                }

                return uri;
            };

            var chainable = this.getFactory().node(this, "n:person");
            return this.chainGet(chainable, uriFunction);
        },

        /**
         * Reads the group object for a security group.
         *
         * @chained node
         *
         * @param {Object} group eitehr the group id, group name or the group object
         * @param [Boolean] createIfNotFound whether to create the group object if it isn't found
         */
        readGroupNode: function(group, createIfNotFound)
        {
            var self = this;

            var principalDomainQualifiedId = this.extractPrincipalDomainQualifiedId(group);

            var uriFunction = function()
            {
                var uri = self.getUri() + "/group/acquire?id=" + principalDomainQualifiedId;
                if (createIfNotFound)
                {
                    uri += "&createIfNotFound=" + createIfNotFound;
                }

                return uri;
            };

            var chainable = this.getFactory().node(this, "n:group");
            return this.chainGet(chainable, uriFunction);
        },

        /**
         * Acquire a list of definitions.
         *
         * @chained node map
         *
         * @public
         *
         * @param [String] filter Optional filter of the kind of definition to fetch - "association", "type" or "feature"
         * @param [Object] pagination Optional pagination
         */
        listDefinitions: function(filter, pagination)
        {
            if (filter && typeof(filter) === "object")
            {
                pagination = filter;
                filter = null;
            }

            var self = this;

            var params = {};
            if (pagination)
            {
                Gitana.copyInto(params, pagination);
            }

            var uriFunction = function()
            {
                // uri
                var uri = self.getUri() + "/definitions";
                if (filter)
                {
                    uri = uri + "?filter=" + filter;
                }

                return uri;
            };

            var chainable = this.getFactory().nodeMap(this);
            return this.chainGet(chainable, uriFunction, params);
        },

        /**
         * Reads a definition by qname.
         *
         * @chained definition
         *
         * @public
         *
         * @param {String} qname the qname
         */
        readDefinition: function(qname)
        {
            var self = this;

            var uriFunction = function()
            {
                return self.getUri() + "/definitions/" + qname;
            };

            var chainable = this.getFactory().definition(this);
            return this.chainGet(chainable, uriFunction);
        },

        /**
         * Loads a list of schemas for an optional given type.
         *
         * @chained this
         *
         * @public
         *
         * @param [String] filter Optional filter of the kind of definition to fetch - "association", "type" or "feature"
         * @param {Function} callback
         */
        loadSchemas: function(filter, callback)
        {
            if (typeof(filter) == "function")
            {
                callback = filter;
                filter = null;
            }

            var self = this;

            return this.then(function() {

                var chain = this;

                // call
                var uri = self.getUri() + "/schemas";
                if (filter) {
                    uri += "?filter=" + filter;
                }
                self.getDriver().gitanaGet(uri, null, {}, function(response) {

                    callback.call(chain, response);

                    chain.next();
                });

                // NOTE: we return false to tell the chain that we'll manually call next()
                return false;
            });
        },


        /**
         * Reads a schema by qname.
         *
         * @chained this
         *
         * @public
         *
         * @param {String} qname the qname
         */
        loadSchema: function(qname, callback)
        {
            var self = this;

            return this.then(function() {

                var chain = this;

                // call
                var uri = self.getUri() + "/schemas/" + qname;
                self.getDriver().gitanaGet(uri, null, {}, function(response) {
                    callback.call(chain, response);
                    chain.next();
                });

                // NOTE: we return false to tell the chain that we'll manually call next()
                return false;
            });
        },

        /**
         * Determines an available QName on this branch given some input.
         * This makes a call to the repository and asks it to provide a valid QName.
         *
         * The valid QName is passed as an argument to the next method in the chain.
         *
         * Note: This QName is a recommended QName that is valid at the time of the call.
         *
         * If another thread writes a node with the same QName after this call but ahead of this thread
         * attempting to commit, an invalid qname exception may still be thrown back.
         *
         * @chained this
         *
         * @public
         *
         * @param {Object} object an object with "title" or "description" fields to base generation upon
         */
        generateQName: function(object, callback)
        {
            var self = this;

            return this.then(function() {

                var chain = this;

                // call
                var uri = self.getUri() + "/qnames/generate";
                self.getDriver().gitanaPost(uri, null, object, function(response) {

                    var qname = response["_qname"];

                    callback.call(chain, qname);

                    chain.next();
                });

                // NOTE: we return false to tell the chain that we'll manually call next()
                return false;
            });
        },

        /**
         * Creates an association between the source node and the target node of the given type.
         *
         * @chained branch (this)
         *
         * @param sourceNode
         * @param targetNode
         * @param object (or string identifying type)
         */
        associate: function(sourceNode, targetNode, object)
        {
            // source
            var sourceNodeId = null;
            if (Gitana.isString(sourceNode))
            {
                sourceNodeId = sourceNode;
            }
            else
            {
                sourceNodeId = sourceNode.getId();
            }

            // target
            var targetNodeId = null;
            if (Gitana.isString(targetNode))
            {
                targetNodeId = targetNode;
            }
            else
            {
                targetNodeId = targetNode.getId();
            }

            // make sure we hand back the branch
            var result = this.subchain(this);

            // run a subchain to do the association
            result.subchain(this).then(function() {
                this.readNode(sourceNodeId).associate(targetNodeId, object);
            });

            return result;
        },

        /**
         * Traverses around the given node.
         *
         * Note: This is a helper function provided for convenience that delegates off to the node to do the work.
         *
         * @chained traversal results
         *
         * @param node or node id
         * @param config
         */
        traverse: function(node, config)
        {
            var nodeId = null;
            if (Gitana.isString(node))
            {
                nodeId = node;
            }
            else
            {
                nodeId = node.getId();
            }

            return this.readNode(nodeId).traverse(config);
        },

        //////////////////////////////////////////////////////////////////////////////////////////
        //
        // CONTAINER (a:child) CONVENIENCE FUNCTIONS
        //
        //////////////////////////////////////////////////////////////////////////////////////////

        /**
         * Creates a container node.
         *
         * This is a convenience function that simply applies the container feature to the object
         * ahead of calling createNode.
         *
         * @chained node
         *
         * @public
         *
         * @param [Object] object JSON object
         */
        createContainer: function(object)
        {
            if (!object)
            {
                object = {};
            }

            if (!object["_features"])
            {
                object["_features"] = {};
            }

            object["_features"]["f:container"] = {
                "active": "true"
            };

            return this.createNode(object);
        },


        //////////////////////////////////////////////////////////////////////////////////////////
        //
        // FIND
        //
        //////////////////////////////////////////////////////////////////////////////////////////

        /**
         * Finds nodes within a branch
         *
         * @chained node map
         *
         * Config should be:
         *
         *    {
         *       "query": {
         *           ... Query Block
         *       },
         *       "search": {
         *           ... Elastic Search Config Block
         *       }
         *    }
         *
         * Alternatively, the value for "search" in the JSON block above can simply be text.
         *
         * @public
         *
         * @param {Object} config search configuration
         */
        find: function(config, pagination)
        {
            var self = this;

            var params = {};
            if (pagination)
            {
                Gitana.copyInto(params, pagination);
            }

            var uriFunction = function()
            {
                return self.getUri() + "/nodes/find";
            };

            var chainable = this.getFactory().nodeMap(this);
            return this.chainPost(chainable, uriFunction, params, config);
        },

        /**
         * Another way to access the find() method that is more consistent with the API
         * that would be expected.
         *
         * @param config
         * @param pagination
         * @return {*}
         */
        findNodes: function(config, pagination)
        {
            return this.find(config, pagination);
        },


        ///////////////////////////////////////////////////////////////////////////////////////////////////////
        //
        // NODE LIST
        //
        ///////////////////////////////////////////////////////////////////////////////////////////////////////


        /**
         * List the items in a node list.
         *
         * @chained node map
         *
         * @public
         *
         * @param {String} listKey
         * @param [Object] pagination
         */
        listItems: function(listKey, pagination)
        {
            var self = this;

            var params = {};
            if (pagination)
            {
                Gitana.copyInto(params, pagination);
            }

            var uriFunction = function()
            {
                return self.getUri() + "/lists/" + listKey + "/items";
            };

            var chainable = this.getFactory().nodeMap(this);
            return this.chainGet(chainable, uriFunction, pagination);
        },

        /**
         * Queries for items in a node list.
         *
         * @chained node map
         *
         * @public
         *
         * @param {String} listKey
         * @param {Object} query
         * @param [Object] pagination
         */
        queryItems: function(listKey, query, pagination)
        {
            var self = this;

            var params = {};
            if (pagination)
            {
                Gitana.copyInto(params, pagination);
            }

            var uriFunction = function()
            {
                return self.getUri() + "/lists/" + listKey + "/items/query";
            };

            var chainable = this.getFactory().nodeMap(this);
            return this.chainPost(chainable, uriFunction, params, query);
        },


        ///////////////////////////////////////////////////////////////////////////////////////////////////////
        //
        // UTILITIES
        //
        ///////////////////////////////////////////////////////////////////////////////////////////////////////

        /**
         * Loads all of the definitions, forms and key mappings on this branch.
         *
         * @param filter
         * @param callback
         */
        loadForms: function(filter, callback)
        {
            var self = this;

            return this.then(function() {

                var chain = this;

                // call
                var uri = self.getUri() + "/forms";
                if (filter) {
                    uri += "?filter=" + filter;
                }
                self.getDriver().gitanaGet(uri, null, {}, function(response) {

                    callback.call(chain, response);

                    chain.next();
                });

                // NOTE: we return false to tell the chain that we'll manually call next()
                return false;
            });
        },

        ///////////////////////////////////////////////////////////////////////////////////////////////////////
        //
        // ADMIN
        //
        ///////////////////////////////////////////////////////////////////////////////////////////////////////

        adminRebuildPathIndexes: function()
        {
            var self = this;

            return this.then(function() {

                var chain = this;

                // call
                var uri = self.getUri() + "/admin/paths/index";
                self.getDriver().gitanaPost(uri, null, {}, function(response) {
                    chain.next();
                });

                // NOTE: we return false to tell the chain that we'll manually call next()
                return false;
            });
        },

        adminRebuildSearchIndexes: function()
        {
            var self = this;

            return this.then(function() {

                var chain = this;

                // call
                var uri = self.getUri() + "/admin/search/index";
                self.getDriver().gitanaPost(uri, null, {}, function(response) {
                    chain.next();
                });

                // NOTE: we return false to tell the chain that we'll manually call next()
                return false;
            });
        },

        adminContentMaintenance: function()
        {
            var self = this;

            return this.then(function() {

                var chain = this;

                // call
                var uri = self.getUri() + "/admin/content";
                self.getDriver().gitanaPost(uri, null, {}, function(response) {
                    chain.next();
                });

                // NOTE: we return false to tell the chain that we'll manually call next()
                return false;
            });
        },

        adminUpgradeSchema: function()
        {
            var self = this;

            return this.then(function() {

                var chain = this;

                // call
                var uri = self.getUri() + "/admin/upgradeschema";
                self.getDriver().gitanaPost(uri, null, {}, function(response) {
                    chain.next();
                });

                // NOTE: we return false to tell the chain that we'll manually call next()
                return false;
            });
        },

        createForExport: function(exportId, config, callback)
        {
            var self = this;

            if (!config)
            {
                config = {};
            }

            if (!config.repositoryId)
            {
                config.repositoryId = self.getRepositoryId();
            }
            if (!config.branchId)
            {
                config.branchId = self.getId();
            }
            if (!config.properties)
            {
                config.properties = {};
            }
            if (!config.parentFolderPath)
            {
                config.parentFolderPath = {};
            }

            var uriFunction = function()
            {
                return "/ref/exports/" + exportId + "/generate";
            };

            var params = {};

            return this.chainPostResponse(this, uriFunction, params, config).then(function(response) {
                callback(response);
            });
        },


        //////////////////////////////////////////////////////////////////////////////////////////
        //
        // INFO
        //
        //////////////////////////////////////////////////////////////////////////////////////////

        /**
         * Loads information about the branch.
         *
         * @param callback
         */
        loadInfo: function(callback)
        {
            var uriFunction = function()
            {
                return this.getUri() +  "/info";
            };

            return this.chainGetResponse(this, uriFunction, {}).then(function(response) {
                callback(response);
            });
        },



        //////////////////////////////////////////////////////////////////////////////////////////
        //
        // INDEXES
        //
        //////////////////////////////////////////////////////////////////////////////////////////

        createCustomIndex: function(name, index)
        {
            var self = this;

            var payload = null;
            if (typeof(index) === "undefined")
            {
                payload = name;
            }
            else
            {
                payload = {
                    "name": name,
                    "index": index
                };
            }

            var uriFunction = function()
            {
                return self.getUri() + "/indexes";
            };

            return this.chainPost(this, uriFunction, {}, payload);
        },

        dropCustomIndex: function(name)
        {
            var self = this;

            var uriFunction = function()
            {
                return self.getUri() + "/indexes/" + name;
            };

            return this.chainDelete(this, uriFunction);
        },

        loadCustomIndexes: function(callback)
        {
            var self = this;

            var uriFunction = function()
            {
                return self.getUri() + "/indexes";
            };

            return this.chainGetResponse(this, uriFunction, {}).then(function(response) {
                callback(response);
            });
        },


        //////////////////////////////////////////////////////////////////////////////////////////
        //
        // HISTORY
        //
        //////////////////////////////////////////////////////////////////////////////////////////

        /**
         * Loads the historic changesets for a branch.
         *
         * The config is optional and can specify "root" and "tip" changeset ids.
         *
         * @param config
         * @param callback
         * @returns {*}
         */
        loadHistoryChangesets: function(config, callback)
        {
            var self = this;

            if (typeof(config) === "function") {
                callback = config;
                config = {};
            }

            if (!config) {
                config = {};
            }

            var uriFunction = function()
            {
                return self.getUri() + "/history/changesets";
            };

            var params = {};
            if (config.root) {
                params.root = config.root;
            }
            if (config.tip) {
                params.tip = config.tip;
            }

            return this.chainGetResponse(this, uriFunction, params).then(function(response) {
                callback(response);
            });
        },

        /**
         * Loads the history node differences for a branch.
         *
         * The config is optional and can specify "root" and "tip" changeset ids.
         *
         * @param config
         * @param callback
         * @returns {*}
         */
        loadHistoryNodeDiffs: function(config, callback)
        {
            var self = this;

            if (typeof(config) === "function") {
                callback = config;
                config = {};
            }

            if (!config) {
                config = {};
            }

            var uriFunction = function()
            {
                return self.getUri() + "/history/nodediffs";
            };

            var params = {};
            if (config.root) {
                params.root = config.root;
            }
            if (config.tip) {
                params.tip = config.tip;
            }

            return this.chainGetResponse(this, uriFunction, params).then(function(response) {
                callback(response);
            });
        }

    });

})(window);
