/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
*/
define([
    "underscore", "jquery", "backbone", "brooklyn",
    "view/location-wizard",
    "model/location", "model/entity",
    "text!tpl/catalog/page.html",
    "text!tpl/catalog/details-entity.html",
    "text!tpl/catalog/details-generic.html",
    "text!tpl/catalog/details-location.html",
    "text!tpl/catalog/add-catalog-entry.html",
    "text!tpl/catalog/add-yaml.html",
    "text!tpl/catalog/add-location.html",
    "text!tpl/catalog/nav-entry.html",

    "bootstrap", "jquery-form"
], function(_, $, Backbone, Brooklyn,
        LocationWizard,
        Location, Entity,
        CatalogPageHtml, DetailsEntityHtml, DetailsGenericHtml, LocationDetailsHtml,
        AddCatalogEntryHtml, AddYamlHtml, AddLocationHtml, EntryHtml) {

    // Holds the currently active details type, e.g. applications, policies. Bit of a workaround
    // to share the active view with all instances of AccordionItemView, so clicking the 'reload
    // catalog' button (handled by the parent of the AIVs) does not apply the 'active' class to
    // more than one element.
    var activeDetailsView;

    var CatalogItemDetailsView = Backbone.View.extend({

        events: {
            "click .composer": "composeItem",
            "click .delete": "deleteItem"
        },

        initialize: function() {
            _.bindAll(this);
            this.options.template = _.template(this.options.template || DetailsGenericHtml);
        },

        render: function() {
            if (!this.options.model) {
                return this.renderEmpty();
            } else {
                return this.renderDetails();
            }
        },

        renderEmpty: function(extraMessage) {
            this.$el.html("<div class='catalog-details'>" +
                "<h3>Select an entry on the left</h3>" +
                _.escape(extraMessage ? extraMessage : "") +
                "</div>");
            return this;
        },

        renderDetails: function() {
            var that = this,
                model = this.options.model,
                template = this.options.template;
            var show = function() {
                // Keep the previously open section open between items. Duplication between
                // here and setDetailsView, below. This case handles view refreshes from this
                // view directly (e.g. when indicating an error), below handles keeping the
                // right thing open when navigating from view to view.
                var open = this.$(".in").attr("id");
                var newHtml = $(template({model: model, viewName: that.options.name}));
                $(newHtml).find("#"+open).addClass("in");
                that.$el.html(newHtml);
                // rewire events. previous callbacks are removed automatically.
                that.delegateEvents()
            };

            this.activeModel = model;
            // Load the view with currently available data and refresh once the load is complete.
            // Only refreshes the view if the model changes and the user hasn't selected another
            // item while the load was executing.
            show();
            model.on("change", function() {
                if (that.activeModel.cid === model.cid) {
                    show();
                }
            });
            model.fetch()
                .fail(function(xhr, textStatus, errorThrown) {
                    console.log("error loading", model.id, ":", errorThrown);
                    if (that.activeModel.cid === model.cid) {
                        model.error = true;
                        show();
                    }
                })
                // Runs after the change event fires, or after the xhr completes
                .always(function () {
                    model.off("change");
                });
            return this;
        },

        composeItem: function(event) {
            // TODO we should store `catalogYaml` in the catalog so these can be edited
            // for now locations are edited, but other items are deployed (ugly and inconsistent)
            if (this.options.name == "locations") {
              // old ui has planYaml inside a catalog block
              this.composerEditItem(event, this.options.model.get("catalog"));
            } else {
              // deploy the item
              this.composerDeployItem(event);
            }
        },
        composerDeployItem: function(event) {
            console.log("composer deploy", event.currentTarget);
            Backbone.history.navigate("/v1/editor/app/"+ encodeURIComponent($(event.currentTarget).data("name")),
                {trigger: true});
        },        
        composerEditItem: function(event, catalog) {
            // TODO ideally we get a catalogYaml from the object instead of recreating
            var planYaml = catalog.planYaml;
            
            // remove a brooklyn.locations: - ... prefix which brooklyn may insert for legacy reasons,
            // replace with single blank line
            if (planYaml.trim().startsWith("brooklyn.locations:")) {
              planYaml = planYaml.replace(/^ *brooklyn.locations:( *\n)+/, "")
              planYaml = "\n"+planYaml.replace(/^( *)(-)( *)/, "$1 $3")
            }
            var yaml = "brooklyn.catalog:\n"+
              "  items:\n"+
              "  - id: " + catalog.symbolicName + "\n"+
              "    # NB: the version may need to be increased\n"+
              "    version: " + catalog.version + "\n"+
              (catalog.description ? "    description: " + catalog.description + "\n" : "") +
              (catalog.iconUrl ? "    iconUrl: " + catalog.iconUrl + "\n" : "") +
              // any other fields?  
              "    itemType: location\n"+
              "    item:\n"+
              this.prefixAllLines("      ", planYaml);
            Backbone.history.navigate("/v1/editor/catalog/_/"+ encodeURIComponent(yaml), 
                {trigger: true});
        },
        prefixAllLines: function(prefix, lines) {
            return prefix+lines.split("\n").join("\n"+prefix);
            Backbone.history.navigate("/v1/editor/app/"+ encodeURIComponent($(event.currentTarget).data("name")),
                {trigger: true});
        },
        
        deleteItem: function(event) {
            // Could use wait flag to block removal of model from collection
            // until server confirms deletion and success handler to perform
            // removal. Useful if delete fails for e.g. lack of entitlement.
            var that = this;
            var displayName = $(event.currentTarget).data("name") || "item";
            this.activeModel.destroy({
                success: function() {
                    that.renderEmpty("Deleted " + displayName);
                },
                error: function(info) {
                    that.renderEmpty("Unable to permanently delete " + displayName+". Deletion is temporary, client-side only.");
                }
            });
        }
    });

    var AddCatalogEntryView = Backbone.View.extend({
        template: _.template(AddCatalogEntryHtml),
        events: {
            "click .show-context": "showContext"
        },
        initialize: function() {
            _.bindAll(this);
        },
        render: function (initialView) {
            this.$el.html(this.template());
            if (initialView) {
                if (initialView == "entity") initialView = "yaml";
                
                this.$("[data-context='"+_.escape(initialView)+"']").addClass("active");
                this.showFormForType(initialView)
            }
            return this;
        },
        clearWithHtml: function(template) {
            if (this.contextView) this.contextView.close();
            this.context = undefined;
            this.$(".btn").removeClass("active");
            this.$("#catalog-add-form").html(template);
        },
        beforeClose: function () {
            if (this.contextView) this.contextView.close();
        },
        showContext: function(event) {
            var $event = $(event.currentTarget);
            var context = $event.data("context");
            if (this.context !== context) {
                if (this.contextView) {
                    this.contextView.close();
                }
                this.showFormForType(context)
            }
        },
        showFormForType: function (type) {
            this.context = type;
            if (type == "location") {
                this.contextView = newLocationForm(this, this.options.parent);
                Backbone.history.navigate("/v1/catalog/new/" + encodeURIComponent(type));
                this.$("#catalog-add-form").html(this.contextView.$el);
            }else{
                // go to composer
                Backbone.history.navigate('/v1/editor/catalog/', {trigger: true});
            }
        }
    });

    // Could adapt to edit existing locations too.
    function newLocationForm(addView, addViewParent) {
        return new LocationWizard({
            onLocationCreated: function(wizard, data) {
                addViewParent.loadAccordionItem("locations", data.id, true);
            },
            onFinish: function(wizard, data) {
                addViewParent.loadAccordionItem("locations", data.id);
                addView.clearWithHtml( "Added: "+_.escape(data.id)+". Loading..." );
            }
        }).render();
    }

    var Catalog = Backbone.Collection.extend({
        modelX: Backbone.Model.extend({
          url: function() {
            return "/v1/catalog/" + encodeURIComponent(this.name) + "/" + encodeURIComponent(this.id) + "?allVersions=true";
          }
        }),
        initialize: function(models, options) {
            this.name = options["name"];
            if (!this.name) {
                throw new Error("Catalog collection must know its name");
            }
            //this.model is a constructor so it shouldn't be _.bind'ed to this
            //It actually works when a browser provided .bind is used, but the
            //fallback implementation doesn't support it.
            var that = this; 
            var model = this.model.extend({
              url: function() {
                return "/v1/catalog/" + encodeURIComponent(that.name) + "/" + encodeURIComponent(this.id).split(encodeURIComponent(":")).join("/");
              }
            });
            _.bindAll(this);
            this.model = model;
        },
        url: function() {
            return "/v1/catalog/" + encodeURIComponent(this.name) + "?allVersions=true";
        }
    });

    /** Use to fill single accordion view list. */
    var AccordionItemView = Backbone.View.extend({
        tag: "div",
        className: "accordion-item",
        events: {
            'click .accordion-head': 'toggle',
            'click .accordion-nav-row': 'showDetails'
        },
        bodyTemplate: _.template(
            "<div class='accordion-head capitalized'><%- name %></div>" +
            "<div class='accordion-body' style='display: <%- display %>'></div>"),

        initialize: function() {
            _.bindAll(this);
            this.name = this.options.name;
            if (!this.name) {
                throw new Error("Name should have been given for accordion entry");
            } else if (!this.options.onItemSelected) {
                throw new Error("onItemSelected(model, element) callback should have been given for accordion entry");
            }

            // Generic templates
            this.template = _.template(this.options.template || EntryHtml);

            // Returns template applied to function arguments. Alter if collection altered.
            // Will be run in the context of the AccordionItemView.
            this.entryTemplateArgs = this.options.entryTemplateArgs || function(model, index) {
                return {type: model.getVersionedAttr("type"), id: model.get("id")};
            };

            // undefined argument is used for existing model items
            var collectionModel = this.options.model || Backbone.Model;
            this.collection = this.options.collection || new Catalog(undefined, {
                name: this.name,
                model: collectionModel
            });
            // Refreshes entries list when the collection is synced with the server or
            // any of its members are destroyed.
            this.collection
                .on("sync", this.renderEntries)
                .on("destroy", this.renderEntries);
            this.refresh();
        },

        beforeClose: function() {
            this.collection.off();
        },

        render: function() {
            this.$el.html(this.bodyTemplate({
                name: this.name,
                display: this.options.autoOpen ? "block" : "none"
            }));
            this.renderEntries();
            return this;
        },

        singleItemTemplater: function(isChild, model, index) {
            var args = _.extend({
                    cid: model.cid,
                    isChild: isChild,
                    extraClasses: (activeDetailsView == this.name && model.cid == this.activeCid) ? "active" : ""
                }, this.entryTemplateArgs(model));
            return this.template(args);
        },

        renderEntries: function() {
            var elements = this.collection.map(_.partial(this.singleItemTemplater, false), this);
            this.updateContent(elements.join(''));
        },

        updateContent: function(markup) {
            this.$(".accordion-body")
                .empty()
                .append(markup);
        },

        refresh: function() {
            this.collection.fetch();
        },

        showDetails: function(event) {
            var $event = $(event.currentTarget);
            var cid = $event.data("cid");
            if (activeDetailsView !== this.name || this.activeCid !== cid) {
                activeDetailsView = this.name;
                this.activeCid = cid;
                var model = this.collection.get(cid);
                Backbone.history.navigate("v1/catalog/" + encodeURIComponent(this.name) + "/" + encodeURIComponent(model.id));
                this.options.onItemSelected(activeDetailsView, model, $event);
            }
        },

        toggle: function() {
            var body = this.$(".accordion-body");
            var hidden = this.hidden = body.css("display") == "none";
            if (hidden) {
                body.removeClass("hide").slideDown('fast');
            } else {
                body.slideUp('fast')
            }
        },

        show: function() {
            var body = this.$(".accordion-body");
            var hidden = this.hidden = body.css("display") == "none";
            if (hidden) {
                body.removeClass("hide").slideDown('fast');
            }
        }
    });
    
    var AccordionEntityView = AccordionItemView.extend({
        renderEntries: function() {
            var symbolicNameFn = function(model) {return model.get("symbolicName")};
            var groups = this.collection.groupBy(symbolicNameFn);
            var orderedIds = _.uniq(this.collection.map(symbolicNameFn));

            function getLatestStableVersion(items) {
                //the server sorts items by descending version, snapshots at the back
                return items[0];
            }

            var catalogTree = _.map(orderedIds, function(symbolicName) {
                var group = groups[symbolicName];
                var root = getLatestStableVersion(group);
                var children = _.reject(group, function(model) {return root.id == model.id;});
                return {root: root, children: children};
            });

            var templater = function(memo, item, index) {
                memo.push(this.singleItemTemplater(false, item.root));
                return memo.concat(_.map(item.children, _.partial(this.singleItemTemplater, true), this));
            };

            var elements = _.reduce(catalogTree, templater, [], this);
            this.updateContent(elements.join(''));
        }
    });

    // Controls whole page. Parent of accordion items and details view.
    var CatalogResourceView = Backbone.View.extend({
        tagName:"div",
        className:"container container-fluid",
        entryTemplate:_.template(EntryHtml),

        events: {
            'click .refresh':'refresh',
            'click #add-new-thing': 'createNewThing'
        },

        initialize: function() {
            $(".nav1").removeClass("active");
            $(".nav1_catalog").addClass("active");
            // Important that bind happens before accordion object is created. If it happens after
            // `this' will not be set correctly for the onItemSelected callbacks.
            _.bindAll(this);
            this.accordion = this.options.accordion || {
                "applications": new AccordionEntityView({
                    name: "applications",
                    singular: "application",
                    onItemSelected: _.partial(this.showCatalogItem, DetailsEntityHtml),
                    model: Entity.Model,
                    autoOpen: !this.options.kind || this.options.kind == "applications"
                }),
                "entities": new AccordionEntityView({
                    name: "entities",
                    singular: "entity",
                    onItemSelected: _.partial(this.showCatalogItem, DetailsEntityHtml),
                    model: Entity.Model,
                    autoOpen: this.options.kind == "entities"
                }),
                "policies": new AccordionEntityView({
                    // TODO needs parsing, and probably its own model
                    // but cribbing "entity" works for now 
                    // (and not setting a model can cause errors intermittently)
                    onItemSelected: _.partial(this.showCatalogItem, DetailsEntityHtml),
                    name: "policies",
                    singular: "policy",
                    model: Entity.Model,
                    autoOpen: this.options.kind == "policies"
                }),
                "locations": new AccordionItemView({
                    name: "locations",
                    singular: "location",
                    onItemSelected: _.partial(this.showCatalogItem, LocationDetailsHtml),
                    collection: this.options.locations,
                    autoOpen: this.options.kind == "locations",
                    entryTemplateArgs: function (location, index) {
                        return {
                            type: location.getIdentifierName(),
                            id: location.getLinkByName("self")
                        };
                    }
                })
            };
        },

        beforeClose: function() {
            _.invoke(this.accordion, 'close');
        },

        render: function() {
            this.$el.html(_.template(CatalogPageHtml, {}));
            var parent = this.$(".catalog-accordion-parent");
            _.each(this.accordion, function(child) {
                parent.append(child.render().$el);
            });
            if (this.options.kind === "new") {
                this.createNewThing(this.options.id);
            } else if (this.options.kind && this.options.id) {
                this.loadAccordionItem(this.options.kind, this.options.id)
            } else {
                // Show empty details view to start
                this.setDetailsView(new CatalogItemDetailsView().render());
            }
            return this
        },

        /** Refreshes the contents of each accordion pane */
        refresh: function() {
            _.invoke(this.accordion, 'refresh');
        },

        createNewThing: function (type) {
            // Discard if it's the jquery event object.
            if (!_.isString(type)) {
                type = undefined;
            }
            var viewName = "createNewThing";
            if (!type) {
                Backbone.history.navigate("/v1/catalog/new");
            }
            activeDetailsView = viewName;
            this.$(".accordion-nav-row").removeClass("active");
            var newView = new AddCatalogEntryView({
                parent: this
            }).render(type);
            this.setDetailsView(newView);
        },

        loadAnyAccordionItem: function (id) {
            this.loadAccordionItem("entities", id);
            this.loadAccordionItem("applications", id);
            this.loadAccordionItem("policies", id);
            this.loadAccordionItem("locations", id);
        },

        loadAccordionItem: function (kind, id, noRedirect) {
            if (!this.accordion[kind]) {
                console.error("No accordion for: " + kind);
            } else {
                var accordion = this.accordion[kind];
                var self = this;
                // reset is needed because we rely on server's ordering;
                // without it, server additions are placed at end of list
                accordion.collection.fetch({reset: true})
                    .then(function() {
                        var model = accordion.collection.get(id);
                        if (!model) {
                            // if a version is supplied, try it without a version - needed for locations, navigating after deletion
                            if (id && id.split(":").length>1) {
                                model = accordion.collection.get( id.split(":")[0] );
                            }
                        }
                        if (!model) {
                            // if an ID is supplied without a version, look for first matching version (should be newest)
                            if (id && id.split(":").length==1 && accordion.collection.models) {
                                model = _.find(accordion.collection.models, function(m) { 
                                    return m && m.id && m.id.startsWith(id+":");
                                });
                            }
                        }
                        // TODO could look in collection for any starting with ID
                        if (model && !noRedirect) {
                            Backbone.history.navigate("/v1/catalog/" + encodeURIComponent(kind) + "/" + encodeURIComponent(id));
                            activeDetailsView = kind;
                            accordion.activeCid = model.cid;
                            accordion.options.onItemSelected(kind, model);
                            accordion.show();
                        } else {
                            // catalog item not found, or not found yet (it might be reloaded and another callback will try again)
                        }
                    });
            }
        },

        showCatalogItem: function(template, viewName, model, $target) {
            this.$(".accordion-nav-row").removeClass("active");
            if ($target) {
                $target.addClass("active");
            } else {
                this.$("[data-cid='" + _.escape(model.cid) + "']").addClass("active");
            }
            var newView = new CatalogItemDetailsView({
                model: model,
                template: template,
                name: viewName
            }).render();
            this.setDetailsView(newView)
        },

        setDetailsView: function(view) {
            this.$("#details").html(view.el);
            if (this.detailsView) {
                // Try to re-open sections that were previously visible.
                var openedItem = this.detailsView.$(".in").attr("id");
                if (openedItem) {
                    view.$("#" + openedItem).addClass("in");
                }
                this.detailsView.close();
            }
            this.detailsView = view;
        }
    });
    
    return CatalogResourceView
});
