HTMLView = Marionette.ItemView.extend({
  render: function() {
      this.$el.append(this.options.html);
  },
});

FilteredCompositeView = Marionette.CompositeView.extend( {
    showCollection: function() {
      var ChildView;
      this.collection.each(function(child, index) {
        filterFunc = this.options.filter || this.filter;
        if (filterFunc && !filterFunc(child)) {
            return;
        }
        ChildView = this.getChildView(child);
        this.addChild(child, ChildView, index);
      }, this);

    },
});

SliceSelectorOption = Marionette.ItemView.extend({
    template: "#xos-sliceselector-option",
    tagName: "option",
    attributes: function() {
        if (this.options.selectedID == this.model.get("id")) {
            return { value: this.model.get("id"), selected: 1 };
        } else {
            return { value: this.model.get("id") };
        }
    },
});

SliceSelectorView = FilteredCompositeView.extend({
    template: "#xos-sliceselector-select",
    childViewContainer: "select",
    childView: SliceSelectorOption,
    caption: "Slice",

    events: {"change select": "onSliceChanged"},

    childViewOptions: function() {
        return { selectedID: this.options.selectedID || this.selectedID || null };
    },

    onSliceChanged: function() {
        this.sliceChanged(this.$el.find("select").val());
    },

    sliceChanged: function(id) {
        console.log("sliceChanged " + id);
    },

    templateHelpers: function() { return {caption: this.options.caption || this.caption }; },
});

XOSRouter = Marionette.AppRouter.extend({
        initialize: function() {
            this.routeStack=[];
        },

        onRoute: function(x,y,z) {
             this.routeStack.push(Backbone.history.fragment);
             this.routeStack = this.routeStack.slice(-32);   // limit the size of routeStack to something reasonable
        },

        prevPage: function() {
             return this.routeStack.slice(-1)[0];
        },

        showPreviousURL: function() {
            prevPage = this.prevPage();
            //console.log("showPreviousURL");
            //console.log(this.routeStack);
            if (prevPage) {
                this.navigate("#"+prevPage, {trigger: false, replace: true} );
            }
        },

        navigate: function(href, options) {
            if (options.force) {
                Marionette.AppRouter.prototype.navigate.call(this, "nowhere", {trigger: false, replace: true});
            }
            Marionette.AppRouter.prototype.navigate.call(this, href, options);
        },
    });

// XXX - We import backbone multiple times (BAD!) since the import happens
//   inside of the view's html. The second time it's imported (developer
//   view), it wipes out Backbone.Syphon. So, save it as Backbone_Syphon for
//   now.
Backbone_Syphon = Backbone.Syphon
Backbone_Syphon.InputReaders.register('select', function(el) {
    // Modify syphon so that if a select has "syphonall" in the class, then
    // the value of every option will be returned, regardless of whether of
    // not it is selected.
    if (el.hasClass("syphonall")) {
        result = [];
        _.each(el.find("option"), function(option) {
            result.push($(option).val());
        });
        return result;
    }
    return el.val();
});

XOSApplication = Marionette.Application.extend({
    detailBoxId: "#detailBox",
    errorBoxId: "#errorBox",
    errorCloseButtonId: "#close-error-box",
    successBoxId: "#successBox",
    successCloseButtonId: "#close-success-box",
    errorTemplate: "#xos-error-template",
    successTemplate: "#xos-success-template",
    logMessageCount: 0,

    confirmDialog: function(view, event, callback) {
        $("#xos-confirm-dialog").dialog({
           autoOpen: false,
           modal: true,
           buttons : {
                "Confirm" : function() {
                  $(this).dialog("close");
                  if (event) {
                      view.trigger(event);
                  }
                  if (callback) {
                      callback();
                  }
                },
                "Cancel" : function() {
                  $(this).dialog("close");
                }
              }
            });
        $("#xos-confirm-dialog").dialog("open");
    },

    popupErrorDialog: function(responseText) {
        try {
            parsed_error=$.parseJSON(responseText);
            width=300;
        }
        catch(err) {
            parsed_error=undefined;
            width=640;    // django stacktraces like wide width
        }
        console.log(responseText);
        console.log(parsed_error);

        if (parsed_error && ("error" in parsed_error)) {
            // this error comes from genapi views
            $("#xos-error-dialog").html(templateFromId("#xos-error-response")(parsed_error));
        } else if (parsed_error && ("detail" in parsed_error)) {
            // this error response comes from rest_framework APIException
            parsed_error["error"] = "API Error";
            parsed_error["specific_error"] = parsed_error["detail"];
            parsed_error["reasons"] = [];
            $("#xos-error-dialog").html(templateFromId("#xos-error-response")(parsed_error));
        } else {
            $("#xos-error-dialog").html(templateFromId("#xos-error-rawresponse")({responseText: strip_scripts(responseText)}))
        }

        $("#xos-error-dialog").dialog({
            modal: true,
            width: width,
            buttons: {
                Ok: function() { $(this).dialog("close"); }
            }
        });
    },

    hideLinkedItems: function(result) {
        var index=0;
        while (index<4) {
            this["linkedObjs" + (index+1)].empty();
            index = index + 1;
        }
    },

    hideTabs: function() { $("#tabs").hide(); },
    showTabs: function() { $("#tabs").show(); },

    createListHandler: function(listViewName, collection_name, regionName, title) {
        var app=this;
        return function() {
            listView = new app[listViewName];
            app[regionName].show(listView);
            app.hideLinkedItems();
            $("#contentTitle").html(templateFromId("#xos-title-list")({"title": title}));
            $("#detail").show();
            app.hideTabs();

            listButtons = new XOSListButtonView({linkedView: listView});
            app["rightButtonPanel"].show(listButtons);
        }
    },

    createAddHandler: function(detailName, collection_name, regionName, title) {
        var app=this;
        return function() {
            console.log("addHandler");

            app.hideLinkedItems();
            app.hideTabs();

            model = new xos[collection_name].model();
            detailViewClass = app[detailName];
            detailView = new detailViewClass({model: model, collection:xos[collection_name]});
            app[regionName].show(detailView);

            detailButtons = new XOSDetailButtonView({linkedView: detailView});
            app["rightButtonPanel"].show(detailButtons);
        }
    },

    createAddChildHandler: function(addChildName, collection_name) {
        var app=this;
        return function(parent_modelName, parent_fieldName, parent_id) {
            app.Router.showPreviousURL();
            model = new xos[collection_name].model();
            model.attributes[parent_fieldName] = parent_id;
            model.readOnlyFields.push(parent_fieldName);
            detailViewClass = app[addChildName];
            var detailView = new detailViewClass({model: model, collection:xos[collection_name]});
            detailView.dialog = $("xos-addchild-dialog");
            app["addChildDetail"].show(detailView);
            $("#xos-addchild-dialog").dialog({
               autoOpen: false,
               modal: true,
               width: 640,
               buttons : {
                    "Save" : function() {
                      var addDialog = this;
                      detailView.synchronous = true;
                      detailView.afterSave = function() { console.log("addChild afterSave"); $(addDialog).dialog("close"); }
                      detailView.save();

                      //$(this).dialog("close");
                    },
                    "Cancel" : function() {
                      $(this).dialog("close");
                    }
                  }
                });
            $("#xos-addchild-dialog").dialog("open");
        }
    },

    createDeleteHandler: function(collection_name) {
        var app=this;
        return function(model_id) {
            console.log("deleteCalled");
            collection = xos[collection_name];
            model = collection.get(model_id);
            assert(model!=undefined, "failed to get model " + model_id + " from collection " + collection_name);
            app.Router.showPreviousURL();
            app.deleteDialog(model);
        }
    },

    createDetailHandler: function(detailName, collection_name, regionName, title) {
        var app=this;
        showModelId = function(model_id) {
            $("#contentTitle").html(templateFromId("#xos-title-detail")({"title": title}));

            collection = xos[collection_name];
            model = collection.get(model_id);
            if (model == undefined) {
                app[regionName].show(new HTMLView({html: "failed to load object " + model_id + " from collection " + collection_name}));
            } else {
                detailViewClass = app[detailName];
                detailView = new detailViewClass({model: model});
                app[regionName].show(detailView);
                detailView.showLinkedItems();

                detailButtons = new XOSDetailButtonView({linkedView: detailView});
                app["rightButtonPanel"].show(detailButtons);
            }
        }
        return showModelId;
    },

    /* error handling callbacks */

    hideError: function() {
        if (this.logWindowId) {
        } else {
            $(this.errorBoxId).hide();
            $(this.successBoxId).hide();
        }
    },

    showSuccess: function(result) {
         result["statusclass"] = "success";
         if (this.logTableId) {
             this.appendLogWindow(result);
         } else {
             $(this.successBoxId).show();
             $(this.successBoxId).html(_.template($(this.successTemplate).html())(result));
             var that=this;
             $(this.successCloseButtonId).unbind().bind('click', function() {
                 $(that.successBoxId).hide();
             });
         }
    },

    showError: function(result) {
         result["statusclass"] = "failure";
         if (this.logTableId) {
             this.appendLogWindow(result);
             this.popupErrorDialog(result.responseText);
         } else {
             // this is really old stuff
             $(this.errorBoxId).show();
             $(this.errorBoxId).html(_.template($(this.errorTemplate).html())(result));
             var that=this;
             $(this.errorCloseButtonId).unbind().bind('click', function() {
                 $(that.errorBoxId).hide();
             });
         }
    },

    showInformational: function(result) {
         result["statusclass"] = "inprog";
         if (this.logTableId) {
             return this.appendLogWindow(result);
         } else {
             return undefined;
         }
    },

    appendLogWindow: function(result) {
        // compute a new logMessageId for this log message
        logMessageId = "logMessage" + this.logMessageCount;
        this.logMessageCount = this.logMessageCount + 1;
        result["logMessageId"] = logMessageId;

        logMessageTemplate=$("#xos-log-template").html();
        assert(logMessageTemplate != undefined, "logMessageTemplate is undefined");
        newRow = _.template(logMessageTemplate, result);
        assert(newRow != undefined, "newRow is undefined");

        if (result["infoMsgId"] != undefined) {
            // We were passed the logMessageId of an informational message,
            // and the caller wants us to replace that message with our own.
            // i.e. replace an informational message with a success or an error.
            $("#"+result["infoMsgId"]).replaceWith(newRow);
        } else {
            // Create a brand new log message rather than replacing one.
            logTableBody = $(this.logTableId + " tbody");
            logTableBody.prepend(newRow);
        }

        if (this.statusMsgId) {
            $(this.statusMsgId).html( templateFromId("#xos-status-template")(result) );
        }

        limitTableRows(this.logTableId, 5);

        return logMessageId;
    },

    saveError: function(model, result, xhr, infoMsgId) {
        console.log("saveError");
        result["what"] = "save " + model.modelName + " " + model.attributes.humanReadableName;
        result["infoMsgId"] = infoMsgId;
        this.showError(result);
    },

    saveSuccess: function(model, result, xhr, infoMsgId, addToCollection) {
        console.log("saveSuccess");
        if (model.addToCollection) {
            console.log("addToCollection");
            console.log(model.addToCollection);
            model.addToCollection.add(model);
            model.addToCollection.sort();
            model.addToCollection = undefined;
        }
        result = {status: xhr.xhr.status, statusText: xhr.xhr.statusText};
        result["what"] = "save " + model.modelName + " " + model.attributes.humanReadableName;
        result["infoMsgId"] = infoMsgId;
        this.showSuccess(result);
    },

    destroyError: function(model, result, xhr, infoMsgId) {
        result["what"] = "destroy " + model.modelName + " " + model.attributes.humanReadableName;
        result["infoMsgId"] = infoMsgId;
        this.showError(result);
    },

    destroySuccess: function(model, result, xhr, infoMsgId) {
        result = {status: xhr.xhr.status, statusText: xhr.xhr.statusText};
        result["what"] = "destroy " + model.modelName + " " + model.attributes.humanReadableName;
        result["infoMsgId"] = infoMsgId;
        this.showSuccess(result);
    },

    /* end error handling callbacks */

    destroyModel: function(model) {
         //console.log("destroyModel"); console.log(model);
         this.hideError();
         var infoMsgId = this.showInformational( {what: "destroy " + model.modelName + " " + model.attributes.humanReadableName, status: "", statusText: "in progress..."} );
         var that = this;
         model.destroy({error: function(model, result, xhr) { that.destroyError(model,result,xhr,infoMsgId);},
                        success: function(model, result, xhr) { that.destroySuccess(model,result,xhr,infoMsgId);}});
    },

    deleteDialog: function(model, afterDelete) {
        var that=this;
        assert(model!=undefined, "deleteDialog's model is undefined");
        //console.log("deleteDialog"); console.log(model);
        this.confirmDialog(null, null, function() {
            //console.log("deleteConfirm"); console.log(model);
            modelName = model.modelName;
            that.destroyModel(model);
            if (afterDelete=="list") {
                that.navigate("list", modelName);
            } else if (afterDelete) {
                afterDelete();
            }
        });
    },
});

XOSButtonView = Marionette.ItemView.extend({
            events: {"click button.btn-xos-save-continue": "submitContinueClicked",
                     "click button.btn-xos-save-leave": "submitLeaveClicked",
                     "click button.btn-xos-save-another": "submitAddAnotherClicked",
                     "click button.btn-xos-delete": "deleteClicked",
                     "click button.btn-xos-add": "addClicked",
                     "click button.btn-xos-refresh": "refreshClicked",
                     },

            submitLeaveClicked: function(e) {
                     this.options.linkedView.submitLeaveClicked.call(this.options.linkedView, e);
                     },

            submitContinueClicked: function(e) {
                     this.options.linkedView.submitContinueClicked.call(this.options.linkedView, e);
                     },

            submitAddAnotherClicked: function(e) {
                     this.options.linkedView.submitAddAnotherClicked.call(this.options.linkedView, e);
                     },

            submitDeleteClicked: function(e) {
                     this.options.linkedView.deleteClicked.call(this.options.linkedView, e);
                     },

            addClicked: function(e) {
                     this.options.linkedView.addClicked.call(this.options.linkedView, e);
                     },

            refreshClicked: function(e) {
                     this.options.linkedView.refreshClicked.call(this.options.linkedView, e);
                     },
            });

XOSDetailButtonView = XOSButtonView.extend({ template: "#xos-savebuttons-template" });
XOSListButtonView = XOSButtonView.extend({ template: "#xos-listbuttons-template" });

/* XOSDetailView
      extend with:
         app - MarionetteApplication
         template - template (See XOSHelper.html)
*/

XOSDetailView = Marionette.ItemView.extend({
            tagName: "div",

            viewInitializers: [],

            events: {"click button.btn-xos-save-continue": "submitContinueClicked",
                     "click button.btn-xos-save-leave": "submitLeaveClicked",
                     "click button.btn-xos-save-another": "submitAddAnotherClicked",
                     "click button.btn-xos-delete": "deleteClicked",
                     "change input": "inputChanged"},

            /* inputChanged is watching the onChange events of the input controls. We
               do this to track when this view is 'dirty', so we can throw up a warning
               if the user tries to change his slices without saving first.
            */

            initialize: function() {
                this.on("saveSuccess", this.onSaveSuccess);
                this.synchronous = false;
            },

            onShow: function() {
                _.each(this.viewInitializers, function(initializer) {
                    initializer();
                });
            },

            saveSuccess: function(e) {
                // always called after a save succeeds
            },

            afterSave: function(e) {
                // if this.synchronous, then called after the save succeeds
                // if !this.synchronous, then called after save is initiated
            },

            onSaveSuccess: function(e) {
                this.saveSuccess(e);
                if (this.synchronous) {
                    this.afterSave(e);
                }
            },

            inputChanged: function(e) {
                this.dirty = true;
            },

            submitContinueClicked: function(e) {
                console.log("saveContinue");
                e.preventDefault();
                this.afterSave = function() { };
                this.save();
            },

            submitLeaveClicked: function(e) {
                console.log("saveLeave");
                e.preventDefault();
                if (this.options.noSubmitButton || this.noSubmitButton) {
                    return;
                }
                var that=this;
                this.afterSave = function() {
                    that.app.navigate("list", that.model.modelName);
                }
                this.save();
            },

            submitAddAnotherClicked: function(e) {
                console.log("saveAnother");
                console.log(this);
                e.preventDefault();
                var that=this;
                this.afterSave = function() {
                    console.log("addAnother afterSave");
                    that.app.navigate("add", that.model.modelName);
                }
                this.save();
            },

            save: function() {
                this.app.hideError();
                var data = Backbone_Syphon.serialize(this);
                var that = this;
                var isNew = !this.model.id;

                console.log(data);

                this.$el.find(".help-inline").remove();

                /* although model.validate() is called automatically by
                   model.save, we call it ourselves, so we can throw up our
                   validation error before creating the infoMsg in the log
                */
                errors =  this.model.xosValidate(data);
                if (errors) {
                    this.onFormDataInvalid(errors);
                    return;
                }

                if (isNew) {
                    this.model.attributes.humanReadableName = "new " + this.model.modelName;
                    this.model.addToCollection = this.collection;
                } else {
                    this.model.addToCollection = undefined;
                }

                var infoMsgId = this.app.showInformational( {what: "save " + this.model.modelName + " " + this.model.attributes.humanReadableName, status: "", statusText: "in progress..."} );

                this.model.save(data, {error: function(model, result, xhr) { that.app.saveError(model,result,xhr,infoMsgId);},
                                       success: function(model, result, xhr) { that.app.saveSuccess(model,result,xhr,infoMsgId);
                                                                               that.trigger("saveSuccess");
                                                                             }});
                this.dirty = false;

                if (!this.synchronous) {
                    this.afterSave();
                }
            },

            deleteClicked: function(e) {
                e.preventDefault();
                this.app.deleteDialog(this.model, "list");
            },

            tabClick: function(tabId, regionName) {
                    region = this.app[regionName];
                    if (this.currentTabRegion != undefined) {
                        this.currentTabRegion.$el.hide();
                    }
                    if (this.currentTabId != undefined) {
                        $(this.currentTabId).removeClass('active');
                    }
                    this.currentTabRegion = region;
                    this.currentTabRegion.$el.show();

                    this.currentTabId = tabId;
                    $(tabId).addClass('active');
            },

            showTabs: function(tabs) {
                template = templateFromId("#xos-tabs-template", {tabs: tabs});
                $("#tabs").html(template(tabs));
                var that = this;

                _.each(tabs, function(tab) {
                    var regionName = tab["region"];
                    var tabId = '#xos-nav-'+regionName;
                    $(tabId).bind('click', function() { that.tabClick(tabId, regionName); });
                });

                $("#tabs").show();
            },

            showLinkedItems: function() {
                    tabs=[];

                    tabs.push({name: "details", region: "detail"});

                    makeFilter = function(relatedField, relatedId) {
                        return function(model) { return model.attributes[relatedField] == relatedId; }
                    };

                    var index=0;
                    for (relatedName in this.model.collection.relatedCollections) {
                        var relatedField = this.model.collection.relatedCollections[relatedName];
                        var relatedId = this.model.id;
                        regionName = "linkedObjs" + (index+1);

                        relatedListViewClassName = relatedName + "ListView";
                        assert(this.app[relatedListViewClassName] != undefined, relatedListViewClassName + " not found");
                        relatedListViewClass = this.app[relatedListViewClassName].extend({collection: xos[relatedName],
                                                                                          filter: makeFilter(relatedField, relatedId),
                                                                                          parentModel: this.model});
                        this.app[regionName].show(new relatedListViewClass());
                        if (this.app.hideTabsByDefault) {
                            this.app[regionName].$el.hide();
                        }
                        tabs.push({name: relatedName, region: regionName});
                        index = index + 1;
                    }

                    while (index<4) {
                        this.app["linkedObjs" + (index+1)].empty();
                        index = index + 1;
                    }

                    this.showTabs(tabs);
                    this.tabClick('#xos-nav-detail', 'detail');
              },

            onFormDataInvalid: function(errors) {
                var self=this;
                var markErrors = function(value, key) {
                    var $inputElement = self.$el.find("[name='" + key + "']");
                    var $inputContainer = $inputElement.parent();
                    //$inputContainer.find(".help-inline").remove();
                    var $errorEl = $("<span>", {class: "help-inline error", text: value});
                    $inputContainer.append($errorEl).addClass("error");
                }
                _.each(errors, markErrors);
            },

             templateHelpers: function() { return { modelName: this.model.modelName,
                                                    collectionName: this.model.collectionName,
                                                    addFields: this.model.addFields,
                                                    listFields: this.model.listFields,
                                                    detailFields: this.options.detailFields || this.detailFields || this.model.detailFields,
                                                    fieldDisplayNames: this.options.fieldDisplayNames || this.fieldDisplayNames || this.model.fieldDisplayNames || {},
                                                    foreignFields: this.model.foreignFields,
                                                    detailLinkFields: this.model.detailLinkFields,
                                                    inputType: this.model.inputType,
                                                    model: this.model,
                                                    detailView: this,
                                                    choices: this.options.choices || this.choices || this.model.choices || {},
                                                    helpText: this.options.helpText || this.helpText || this.model.helpText || {},
                                         }},
});

XOSDetailView_sliver = XOSDetailView.extend( {
    events: $.extend(XOSDetailView.events,
        {"change #field_deployment": "onDeploymentChange"}
    ),

    onShow: function() {
        // Note that this causes the selects to be updated a second time. The
        // first time was when the template was originally invoked, and the
        // selects will all have the full unfiltered set of candidates. Then
        // onShow will fire, and we'll update them with the filtered values.
        this.onDeploymentChange();
    },

    onDeploymentChange: function(e) {
        var deploymentID = this.$el.find("#field_deployment").val();

        //console.log("onDeploymentChange");

        filterFunc = function(model) { for (index in xos.siteDeployments.models) {
                                           site_deployment = xos.siteDeployments.models[index];
                                           if (site_deployment.attributes.id == model.attributes.site_deployment) {
                                               return (site_deployment.attributes.deployment == deploymentID);
                                           }
                                        }
                                        return false;
                                        // return (model.attributes.deployment==deploymentID); }
                                      };
        newSelect = idToSelect("node",
                               this.model.attributes.node,
                               this.model.foreignFields["node"],
                               "humanReadableName",
                               false,
                               filterFunc);
        this.$el.find("#field_node").html(newSelect);

        filterFunc = function(model) { for (index in model.attributes.deployments) {
                                          if (model.attributes.deployments[index] == deploymentID) return true;
                                        };
                                        return false;
                                     }
        newSelect = idToSelect("flavor",
                               this.model.attributes.flavor,
                               this.model.foreignFields["flavor"],
                               "humanReadableName",
                               false,
                               filterFunc);
        this.$el.find("#field_flavor").html(newSelect);

        filterFunc = function(model) { for (index in model.attributes.deployments) {
                                           if (model.attributes.deployments[index] == deploymentID) return true;
                                       };
                                       return false;
                                     };
        newSelect = idToSelect("image",
                               this.model.attributes.image,
                               this.model.foreignFields["image"],
                               "humanReadableName",
                               false,
                               filterFunc);
        this.$el.find("#field_image").html(newSelect);
    },
});

/* XOSItemView
      This is for items that will be displayed as table rows.
      extend with:
         app - MarionetteApplication
         template - template (See XOSHelper.html)
*/

XOSItemView = Marionette.ItemView.extend({
             tagName: 'tr',
             className: 'test-tablerow',

             templateHelpers: function() { return { modelName: this.model.modelName,
                                                    collectionName: this.model.collectionName,
                                                    listFields: this.model.listFields,
                                                    addFields: this.model.addFields,
                                                    detailFields: this.model.detailFields,
                                                    foreignFields: this.model.foreignFields,
                                                    detailLinkFields: this.model.detailLinkFields,
                                                    inputType: this.model.inputType,
                                                    model: this.model,
                                         }},
});

/* XOSListView:
      extend with:
         app - MarionetteApplication
         childView - class of ItemView, probably an XOSItemView
         template - template (see xosHelper.html)
         collection - collection that holds these objects
         title - title to display in template
*/

XOSListView = FilteredCompositeView.extend({
             childViewContainer: 'tbody',
             parentModel: null,

             events: {"click button.btn-xos-add": "addClicked",
                      "click button.btn-xos-refresh": "refreshClicked",
                     },

             _fetchStateChange: function() {
                 if (this.collection.fetching) {
                    $("#xos-list-title-spinner").show();
                 } else {
                    $("#xos-list-title-spinner").hide();
                 }
             },

             addClicked: function(e) {
                e.preventDefault();
                this.app.Router.navigate("add" + firstCharUpper(this.collection.modelName), {trigger: true});
             },

             refreshClicked: function(e) {
                 e.preventDefault();
                 this.collection.refresh(refreshRelated=true);
             },

             initialize: function() {
                 this.listenTo(this.collection, 'change', this._renderChildren)
                 this.listenTo(this.collection, 'sort', function() { console.log("sort"); })
                 this.listenTo(this.collection, 'add', function() { console.log("add"); })
                 this.listenTo(this.collection, 'fetchStateChange', this._fetchStateChange);

                 // Because many of the templates use idToName(), we need to
                 // listen to the collections that hold the names for the ids
                 // that we want to display.
                 for (i in this.collection.foreignCollections) {
                     foreignName = this.collection.foreignCollections[i];
                     if (xos[foreignName] == undefined) {
                         console.log("Failed to find xos class " + foreignName);
                     }
                     this.listenTo(xos[foreignName], 'change', this._renderChildren);
                     this.listenTo(xos[foreignName], 'sort', this._renderChildren);
                 }
             },

             getAddChildHash: function() {
                if (this.parentModel) {
                    parentFieldName = this.parentModel.relatedCollections[this.collection.collectionName];
                    parentFieldName = parentFieldName || "unknown";

                    /*parentFieldName = "unknown";

                    for (fieldName in this.collection.foreignFields) {
                        cname = this.collection.foreignFields[fieldName];
                        if (cname = this.collection.collectionName) {
                            parentFieldName = fieldName;
                        }
                    }*/
                    return "#addChild" + firstCharUpper(this.collection.modelName) + "/" + this.parentModel.modelName + "/" + parentFieldName + "/" + this.parentModel.id; // modelName, fieldName, id
                } else {
                    return null;
                }
             },

             templateHelpers: function() {
                return { title: this.title,
                         addChildHash: this.getAddChildHash(),
                         foreignFields: this.collection.foreignFields,
                         listFields: this.collection.listFields,
                         detailLinkFields: this.collection.detailLinkFields, };
             },
});

XOSDataTableView = Marionette.View.extend( {
    el: '<div style="overflow: hidden">' +
        '<h3 class="xos-list-title title_placeholder"></h3>' +
        '<div class="header_placeholder"></div>' +
        '<table></table>' +
        '<div class="footer_placeholder"></div>' +
        '</div>',

    filter: undefined,

     events: {"click button.btn-xos-add": "addClicked",
              "click button.btn-xos-refresh": "refreshClicked",
             },

     _fetchStateChange: function() {
         if (this.collection.fetching) {
            $("#xos-list-title-spinner").show();
         } else {
            $("#xos-list-title-spinner").hide();
         }
     },

     addClicked: function(e) {
        e.preventDefault();
        this.app.Router.navigate("add" + firstCharUpper(this.collection.modelName), {trigger: true});
     },

     refreshClicked: function(e) {
         e.preventDefault();
         this.collection.refresh(refreshRelated=true);
     },


    initialize: function() {
        $(this.el).find(".footer_placeholder").html( xosListFooterTemplate({addChildHash: this.getAddChildHash()}) );
        $(this.el).find(".header_placeholder").html( xosListHeaderTemplate() );

        this.listenTo(this.collection, 'fetchStateChange', this._fetchStateChange);
    },

    render: function() {
        var view = this;
        var fieldDisplayNames = view.options.fieldDisplayNames || view.fieldDisplayNames || {};

        view.columnsByIndex = [];
        view.columnsByFieldName = {};
        _.each(this.collection.listFields, function(fieldName) {
            inputType = view.options.inputType || view.inputType || {};
            mRender = undefined;
            mSearchText = undefined;
            sTitle = fieldName in fieldDisplayNames ? fieldDisplayNames[fieldName] : fieldNameToHumanReadable(fieldName);
            bSortable = true;
            if (fieldName=="backend_status") {
                mRender = function(x,y,z) { return xosBackendStatusIconTemplate(z); };
                sTitle = "";
                bSortable = false;
            } else if (fieldName in view.collection.foreignFields) {
                var foreignCollection = view.collection.foreignFields[fieldName];
                mSearchText = function(x) { return idToName(x, foreignCollection, "humanReadableName"); };
            } else if (inputType[fieldName] == "spinner") {
                mRender = function(x,y,z) { return xosDataTableSpinnerTemplate( {value: x, collectionName: view.collection.collectionName, fieldName: fieldName, id: z.id, app: view.app} ); };
            }
            if ($.inArray(fieldName, view.collection.detailLinkFields)>=0) {
                var collectionName = view.collection.collectionName;
                mRender = function(x,y,z) { return '<a href="#' + collectionName + '/' + z.id + '">' + x + '</a>'; };
            }
            thisColumn = {sTitle: sTitle, bSortable: bSortable, mData: fieldName, mRender: mRender, mSearchText: mSearchText};
            view.columnsByIndex.push( thisColumn );
            view.columnsByFieldName[fieldName] = thisColumn;
        });

        if (!view.noDeleteColumn) {
            deleteColumn = {sTitle: "", bSortable: false, mRender: function(x,y,z) { return xosDeleteButtonTemplate({modelName: view.collection.modelName, id: z.id}); }, mData: function() { return "delete"; }};
            view.columnsByIndex.push(deleteColumn);
            view.columnsByFieldName["delete"] = deleteColumn;
        };

        oTable = $(this.el).find("table").dataTable( {
            "bJQueryUI": true,
            "bStateSave": true,
            "bServerSide": true,
            "bFilter": ! (view.options.disableFilter || view.disableFilter),
            "bPaginate": ! (view.options.disablePaginate || view.disablePaginate),
            "aoColumns": view.columnsByIndex,

            fnServerData: function(sSource, aoData, fnCallback, settings) {
                var compareColumns = function(sortCols, sortDirs, a, b) {
                    a = a[sortCols[0]];
                    b = b[sortCols[0]];
                    result = (a==b) ? 0 : ((a<b) ? -1 : 1);
                    if (sortDirs[0] == "desc") {
                        result = -result;
                    }
                    return result;
                };

                var searchMatch = function(row, sSearch) {
                    for (fieldName in row) {
                        if (fieldName in view.columnsByFieldName) {
                            try {
                                value = row[fieldName].toString();
                            } catch(e) {
                                continue;
                            }
                            if (value.indexOf(sSearch) >= 0) {
                                return true;
                            }
                        }
                    }
                    return false;
                };

                //console.log(aoData);

                // function used to populate the DataTable with the current
                // content of the collection
                var populateTable = function()
                {
                  //console.log("populatetable!");

                  // clear out old row views
                  rows = [];

                  sSearch = null;
                  iDisplayStart = 0;
                  iDisplayLength = 1000;
                  sortDirs = [];
                  sortCols = [];
                  _.each(aoData, function(param) {
                      if (param.name == "sSortDir_0") {
                          sortDirs = [param.value];
                      } else if (param.name == "iSortCol_0") {
                          sortCols = [view.columnsByIndex[param.value].mData];
                      } else if (param.name == "iDisplayStart") {
                          iDisplayStart = param.value;
                      } else if (param.name == "iDisplayLength") {
                          iDisplayLength = param.value;
                      } else if (param.name == "sSearch") {
                          sSearch = param.value;
                      }
                  });

                  aaData = view.collection.toJSON();

                  // apply backbone filtering on the models
                  if (view.filter) {
                      aaData = aaData.filter( function(row) { model = {}; model.attributes = row; return view.filter(model); } );
                  }

                  var totalSize = aaData.length;

                  // turn the ForeignKey fields into human readable things
                  for (rowIndex in aaData) {
                      row = aaData[rowIndex];
                      for (fieldName in row) {
                          if (fieldName in view.columnsByFieldName) {
                              mSearchText = view.columnsByFieldName[fieldName].mSearchText;
                              if (mSearchText) {
                                  row[fieldName] = mSearchText(row[fieldName]);
                              }
                          }
                      }
                  }

                  // apply datatables search
                  if (sSearch) {
                      aaData = aaData.filter( function(row) { return searchMatch(row, sSearch); });
                  }

                  var filteredSize = aaData.length;

                  // apply datatables sort
                  aaData.sort(function(a,b) { return compareColumns(sortCols, sortDirs, a, b); });

                  // slice it for pagination
                  if (iDisplayLength >= 0) {
                      aaData = aaData.slice(iDisplayStart, iDisplayStart+iDisplayLength);
                  }

                  return fnCallback({iTotalRecords: totalSize,
                         iTotalDisplayRecords: filteredSize,
                         aaData: aaData});
                };

                aoData.shift(); // ignore sEcho
                populateTable();

                view.listenTo(view.collection, 'change', populateTable);
                view.listenTo(view.collection, 'add', populateTable);
                view.listenTo(view.collection, 'remove', populateTable);
            },
        } );

        return this;
    },

     getAddChildHash: function() {
        if (this.parentModel) {
            parentFieldName = this.parentModel.relatedCollections[this.collection.collectionName];
            parentFieldName = parentFieldName || "unknown";

            /*parentFieldName = "unknown";

            for (fieldName in this.collection.foreignFields) {
                cname = this.collection.foreignFields[fieldName];
                if (cname = this.collection.collectionName) {
                    parentFieldName = fieldName;
                }
            }*/
            return "#addChild" + firstCharUpper(this.collection.modelName) + "/" + this.parentModel.modelName + "/" + parentFieldName + "/" + this.parentModel.id; // modelName, fieldName, id
        } else {
            return null;
        }
     },

});

idToName = function(id, collectionName, fieldName) {
    return xos.idToName(id, collectionName, fieldName);
};

makeIdToName = function(collectionName, fieldName) {
    return function(id) { return idToName(id, collectionName, fieldName); }
};

/* Constructs lists of <option> html blocks for items in a collection.

   selectedId = the id of an object that should be selected, if any
   collectionName = name of collection
   fieldName = name of field within models of collection that will be displayed
*/

idToOptions = function(selectedId, collectionName, fieldName, filterFunc) {
    result=""
    for (index in xos[collectionName].models) {
        linkedObject = xos[collectionName].models[index];
        linkedId = linkedObject["id"];
        linkedName = linkedObject.attributes[fieldName];
        if (linkedId == selectedId) {
            selected = " selected";
        } else {
            selected = "";
        }
        if ((filterFunc) && (!filterFunc(linkedObject))) {
            continue;
        }
        result = result + '<option value="' + linkedId + '"' + selected + '>' + linkedName + '</option>';
    }
    return result;
};

/* Constructs an html <select> and the <option>s to go with it.

   variable = variable name to return to form
   selectedId = the id of an object that should be selected, if any
   collectionName = name of collection
   fieldName = name of field within models of collection that will be displayed
*/

idToSelect = function(variable, selectedId, collectionName, fieldName, readOnly, filterFunc) {
    if (readOnly) {
        readOnly = " readonly";
    } else {
        readOnly = "";
    }
    result = '<select name="' + variable + '" id="field_' + variable + '"' + readOnly + '>' +
             idToOptions(selectedId, collectionName, fieldName, filterFunc) +
             '</select>';
    return result;
}

choicesToOptions = function(selectedValue, choices) {
    result="";
    for (index in choices) {
        choice = choices[index];
        displayName = choice[0];
        value = choice[1];
        if (value == selectedValue) {
            selected = " selected";
        } else {
            selected = "";
        }
        result = result + '<option value="' + value + '"' + selected + '>' + displayName + '</option>';
    }
    return result;
}

choicesToSelect = function(variable, selectedValue, choices) {
    result = '<select name="' + variable + '" id="field_' + variable + '">' +
             choicesToOptions(selectedValue, choices) +
             '</select>';
    return result;
}
