(function(){
    ////
    // Set up namespaces
    var Stitsh = this.Stitsh = {};

    var _ = this._;
    var $ = this.jQuery;
    var Backbone = this.Backbone;
    
    ////
    // Utilities

  // Helper function to escape a string for HTML rendering.
  var escapeHTML = function(string) {
    return string.replace(/&(?!\w+;)/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
  };    

    
    ////
    // Models
    
    
    ////
    // Model
    // base for all other models
    
    // Product model

    //var Product = Stitsh.Product = BaseModel.create();
    
    ////
    // Tag model

    var Tag = Stitsh.Tag = Backbone.Model.extend({
        getProduct: function () {
            throw "Not done yet!";
            //return Product.findOrFetch(this.product);
        },
        // Naming the constructor gives the objects a name in the
        // debugger
        constructor: function Tag () {
            Backbone.Model.prototype.constructor.apply(this, arguments);
        },
        // url: function () {
        //     return "/tagging/tag/" + this.get("id") + "/";
        // }
    });

    ////
    // TagCollection

    var TagCollection = Stitsh.TagCollection = Backbone.Collection.extend({
        model: Tag,
        // Naming the constructor gives the objects a name in the
        // debugger        
        constructor: function TagCollection () {
            Backbone.Collection.prototype.constructor.apply(this, arguments);
            
        },
        initialize: function (models, options) {
            
        },
    });
    
    var Photo = Stitsh.Photo = Backbone.Model.extend({
        defaults: {
            "product": {},
        },
        // Naming the constructor gives the objects a name in the
        // debugger        
        constructor: function Photo () {
            _.bindAll(this, "tags_url");
            this.tags = new TagCollection();
            this.tags.url = this.tags_url;
            Backbone.Model.prototype.constructor.apply(this, arguments);
        }, 
        initialize: function () {
        },
        url: function () {
            return "/tagging/photo/" + this.id; 
        },
        tags_url: function () {
            return this.url() + "/tags";
        },
        set: function (attrs, options) {
            // Look for tags and load the collection if they are there
            if (attrs.attributes) attrs = attrs.attributes;
            if ("tags" in attrs) {
                if(attrs.tags.length > 0) {
                    if (typeof attrs.tags[0] == "string") {
                        // We will probably want tags sent just as IDs
                        // in some cases, not done yet though.
                        throw "Not yet supported!";
                    } else {
                        this.tags.refresh(attrs.tags, options);
                    }
                }
                // We don't want to apply the normal logic to the tags attribute
                delete attrs["tags"];
            }
            Backbone.Model.prototype.set.call(this, attrs, options);
        },
    });


    ////
    // Photo view

    var DisplayOverlayView = Stitsh.DisplayOverlayView = Backbone.View.extend({
        className: "stitsh-overlay",
        template:  _.template('<div class="inner">' + 
                      '<img class="image" src="<%= product.image %>"/>' + 
                      '<div class="details">' + 
                       '<div class="title"><%= product.name %></div>' +
                        '<% _.each(product.sources, function (source) { %>' +
                         '<div class="from"><%= source.store.name %> <%= source.currency.symbol %><%= source.price %></div>' +
                        '<% });  %>' +
                      '</div>' +
                      '<div class-"clear">&nbsp;</div>' +
                     '</div>'),
        initialize: function (options) {
            var view = this;
            this.controller = options.controller;
            this.controller.bind("focusTag",function (tag) {
                view.display(tag);
            });
            this.controller.bind("unfocusTag",function (tag){
                view.hide();
            });
            $(this.el).hide();
        },
        render: function () {
            $(this.el).html(this.template(this.model.toJSON()));
        },
        setTag: function (tag) {
            this.model = tag;
            this.render();
        },
        // Set the tag then animate in the overlay
        display: function (tag) {
            this.setTag(tag);
            var parentPos = $(this.el).parent().position();
            $(this.el).show();
            $(this.el).stop();
            $(this.el).css({"left": 0,
                            "top": -$(this.el).height(),
                            "width": $(this.el).parent().width()})
            $(this.el).animate({"top": 0, "opacity": "0.6"}, 200);            
        },
        hide: function () {
            $(this.el).animate({"top": -$(this.el).height(), "opacity": "0"}, 200);            
        }
    });

    ////
    // Tag view

    var TagView = Stitsh.TagView = Backbone.View.extend({
        events: {},
        className: "stitsh-hotspot",
        events: {"mouseenter": "enterHotSpot",
                 "mouseleave": "leaveHotSpot",
                 "click": "clickHotSpot",
                 "mousedown": "startMoveOrResize",
                },
        resizeArea: 15, // In pixels, the area round the edge of the
                        // tag which can be used to resize it (by
                        // clicking and dragging)
        constructor: function TagView (options) {
            Backbone.View.prototype.constructor.apply(this,arguments);
        },
        initialize: function (options) {
            _.bindAll(this,"render", "updateMoveOrResize", "completeMoveOrResize");

            this.controller = options.controller;
            
            if (options.parent) {
                $(options.parent.el).append($(this.el));
            }
            if (options.photo) this.photo = options.photo;
            this.model.bind("change:bounds", this.render);
            this.dragstart = null;

            $(window).mousemove(this, this.updateMoveOrResize);
            $(window).mouseup(this, this.completeMoveOrResize);
            
            this.render();
        },
        render: function ( ) {
            var $el = $(this.el);
            var tag = this.model;
            var width = $(this.el).parent().width();
            var height = $(this.el).parent().height();            
            var bounds = tag.get("bounds");
            $el.css({left:   bounds["left"]*width, ///+parentPos.left,
                     top:    bounds["top"]*height, //+parentPos.top,
                     height: bounds["height"]*height,
                     width:  bounds["width"]*width});
        },
        enterHotSpot: function (event) {
            this.controller.focusTag(this.model);
        },
        leaveHotSpot: function (event) {
            this.controller.unfocusTag();
        },
        clickHotSpot: function (event) {
            if (event.which != 1) return; // only the primary mouse button
            if (this.hasMoved) return; // We don't want to do the
                                       // click stuff if a move or
                                       // resize has happened
            if (this.controller.mode == "edit") {
                this.controller.editTag(this.model);
            } else {
                // Just go to the first buy link for now.
                window.open(this.model.get("product").sources[0].buy_link);
            }
            
        },
        // Return the area of the tag the mouse is in (either a corner or the center)
        getMouseArea: function (event) {
            var offset = $(this.el).offset();
            function nearBorder (position, size, borderSize) {
                if (position < borderSize) {
                    // left side
                    return -1;
                } else if (position > size-borderSize) {
                    return 1;
                } else {
                    return 0;
                }                
            }
            var horizontal = nearBorder(event.pageX-offset.left, $(this.el).width(), this.resizeArea);
            var vertical = nearBorder(event.pageY-offset.top, $(this.el).height(),this.resizeArea);

            if (horizontal == -1 && vertical == -1) {
                return "nw";
            } else if (horizontal == 1 && vertical == -1) {
                return "ne";
            } else if (horizontal == -1 && vertical == 1) {
                return "sw";
            } else if (horizontal == 1 && vertical == 1) {
                return "se";
            } else {
                return "centre";
            }
        },
        // On mousedown
        startMoveOrResize: function (event) {
            if (event.which != 1) return; // only the primary mouse button
            if (this.controller.mode != "edit") return;

            this.hasMoved = false;
            
            this.dragStart = {x: event.pageX, y: event.pageY};
            this.dragStartTime = (new Date()).getTime();
            this.startBounds = _.extend({width: $(this.el).width(), height: $(this.el).height()},$(this.el).position())

            var offset = $(this.el).offset();
            function nearBorder (position, size, borderSize) {
                if (position < borderSize) {
                    // left side
                    return -1;
                } else if (position > size-borderSize) {
                    return 1;
                } else {
                    return 0;
                }                
            }

            var area = this.getMouseArea(event);
            movementTypes = {"nw": {"left": "x", "top": "y", "width": "-x", "height": "-y"},
                             "ne": {"top": "y", "width": "x", "height": "-y"},
                             "sw": {"left": "x","width": "-x", "height": "y"},
                             "se": {"width": "x", "height": "y"},
                             "centre": {"left":"x", "top": "y"}};
            this.dragDimensions = movementTypes[area];
            
            // we don't want to start drawing a new tag!
            event.stopPropagation();
            event.preventDefault();
        },
        // On mousemove
        updateMoveOrResize: function (event) {
            if (this.controller.mode != "edit") return;

            // Update the cursor icon
            var area = this.getMouseArea(event);
            if (area == "centre") {
                $(this.el).css({cursor: "move"})
            } else {
                $(this.el).css({cursor: area + "-resize"});
            };
            
            
            if (!this.dragStart) return;
            // Ignore very recently started drags, so clicks are fine
            event.stopPropagation();
            event.preventDefault();
            if ((new Date()).getTime()-this.dragStartTime < 300) return;
            this.hasMoved = true;

            this.controller.unfocusTag();
            
            with (this.dragStart) {
                var vec = {x: event.pageX-x, y: event.pageY-y};
            }
            // We specified which demensions we where dragging in the
            // startMoveOrResize
            var newBounds = _.clone(this.startBounds);
            // dragDimensions mentions the negation of x and y which
            // we set up here
            vec["-x"] = -vec["x"];
            vec["-y"] = -vec["y"];
            for (var dim in this.dragDimensions) {
                var val = newBounds[dim] + vec[this.dragDimensions[dim]];
                // Don't allow the height or width to go bellow resizeArea times three
                if ((dim == "height" || dim == "width") && val < this.resizeArea*3) {
                    val = this.resizeArea*3;
                }
                newBounds[dim] = val;
            }
            $(this.el).css(newBounds);
            
        },
        // On mouseup
        completeMoveOrResize: function (event) {
            if (!this.dragStart) return;
            this.dragStart = null;
            event.stopPropagation();
            event.preventDefault();

            // Get coordinates transformed into  0..1 range
            var parentwidth = $(this.el).parent().width();
            var parentheight = $(this.el).parent().height();            
            var bounds = _.extend({width: $(this.el).width(),height: $(this.el).height()},$(this.el).position());
            bounds.width /= parentwidth;
            bounds.height /= parentheight;
            bounds.left /= parentwidth;
            bounds.top /= parentheight;
            this.model.set({bounds:bounds});
            this.model.save();
        }
    });
    

    
    function fillParent (el) {
        var $parentel = $(el).parent();;
        var $parentpost = $parentel.position();
        $(el).css({top: $parentpost.top, left: $parentpost.left, width: $parentel.width(), height: $parentel.height()});
    }

    ////
    // PhotTagsView view
    // Handles the collection of tags in a photo 

    // This view uses the SAME element as it's parent (PhotoView).
    // It's just the bit that handles the hotspot elements
    var PhotoTagsView = Stitsh.PhotoTagsView = Backbone.View.extend({
        constructor: function PhotoDisplayMode () {
            return Backbone.View.prototype.constructor.apply(this, arguments);
        },
        initialize: function (options) {
            _.bindAll(this, "render", "addTag", "removeTag");

            this.controller = options.controller;
            
            $(this.el).css({position:"absolute"})
            
            this.tagviews = [];
            this.model.tags.bind("refresh", this.render);
            this.model.tags.bind("add", this.addTag);
            this.model.tags.bind("remove", this.removeTag);
        },
        render: function () {
            // Remove an existing hotspots
            _.each(this.tagviews, function (view) {view.remove();});
            this.model.tags.each(this.addTag);
        },
        addTag: function (tag) {
            var tagview = new TagView({model: tag, parent: this, photo: this.model, controller: this.controller});
            this.tagviews.push(tagview);
        },
        removeTag: function (tag) {
            var tagview = _.detect(this.tagviews, function (view) {return view.model == tag});
            if (tagview) {
                tagview.remove();
            }
            this.tagviews = _.reject(this.tagviews, function (view) {return view.model==tag;})
        }
        
    });

    ////
    // CreateTagView view
    //
    // handles creating new tags, just the initial drag after which
    // the tag is created and we hand over to tag editing

    // This view uses the SAME element as its parent (PhotoView).
    // It's just the bit that handles creating new tags (the dragged
    // proto-tag bit)
    var CreateTagView = Stitsh.CreateTagView = Backbone.View.extend({
        events: {"mousedown": "startdrag",
                 "mousemove": "updatedrag",
                 "mouseup": "completedrag",
                },
        constructor: function CreateTagView() {
            return Backbone.View.prototype.constructor.apply(this, arguments);
        },
        initialize: function (options) {
            _.bindAll(this,"render","startdrag","updatedrag","completedrag","canceldrag", "changeMode");

            this.controller = options.controller;
            
            $(this.el).css({position:"absolute"})
            // Create an element to be used during drawing of new
            // hotspots. Is shown when the mouse is held down. Once
            // the mouse has been released it is hidden again and an
            // actual hotspot is created in its place.
            this.newHotSpot = this.make("div",{class: "stitsh-new-hotspot", display: "hidden"})
            $(this.newHotSpot).hide();
            $(this.el).append(this.newHotSpot);
            this.dragStart = null;

            // Capture mouseups wherever they occur
            $(window).mouseup(this.canceldrag);

            this.controller.bind("changeMode",this.changeMode);

            this.render();
        },
        render: function () {
            //fillParent(this.el);
        },
        changeMode: function (model, mode) {
            if (mode == "edit") {
            } else if (mode == "display") {
            }
        },        
        // Given a mousemove or mouseup event this will return the
        // bounds of the current drag. Returns null if no drag is
        // current or if the drag is too small to show
        dragbounds: function (event) {
            var offset = $(this.el).offset();
            var bounds = {
                width: (event.pageX-offset.left)-this.dragStart.x,
                height: (event.pageY-offset.top)-this.dragStart.y,
                left: this.dragStart.x,
                top: this.dragStart.y
            };

            if (bounds.width < 10 && bounds.height < 10) return null;
            with(bounds) {
                if (bounds.width < 0) {
                    _.extend(bounds,{left: left+width, width: -width})
                }
                if (bounds.height < 0) {
                    _.extend(bounds,{top: top+height, height: -height})
                }
            }
            return bounds;
        },
        // Handles the mousedown event and starts a drag to create a
        // new tag
        startdrag: function (event) {
            if (this.controller.mode != "edit") return;
            if (event.which != 1) return; // only the primary mouse button
            var offset = $(this.el).offset();
            this.dragStart = {x: event.pageX-offset.left, y: event.pageY-offset.top};
            event.preventDefault();
        },
        // On mousemove updates display of the current dragged area
        updatedrag: function (event) {
            if (!this.dragStart) return;
            var bounds = this.dragbounds(event);
            if (!bounds) return;
            $(this.newHotSpot).css(bounds);
            $(this.newHotSpot).show();
            event.preventDefault();

        },
        // On mouseup complete the drag and create the tag
        completedrag: function (event) {
            if (!this.dragStart) return;
            var bounds = this.dragbounds(event);
            if (!bounds) return;
            var mywidth = $(this.el).width();
            var myheight = $(this.el).height();
            with(bounds) {
                bounds = {top: top/myheight, left: left/mywidth, width: width/mywidth, height: height/myheight};
            }
            var newTag = new Tag({bounds: bounds, product:{}});
            // Add it to the collection (won't save it to the server
            // yet though)
            this.model.tags.add(newTag);
            this.controller.editTag(newTag);
            event.preventDefault();
            // canceldrag will now be called automatically
        },
        canceldrag: function (event) {
            $(this.newHotSpot).hide();
            this.dragStart = null;
            event.preventDefault();
        },
    });

    
    var EditTagOverlayView  = Stitsh.EditTagOverlayView  = Backbone.View.extend({
        className: "stitsh-edit-overlay",
        template: _.template(
            '<div class="stitsh-inner">' +
            '<div class="stitsh-overlay-background"/>' +
            '<div class="stitsh-edit-window">' +
                '<label>Select a product</label>' +
                '<input value="<%= product.name %>"/>' +
                '<button class="stitsh-delete">Delete</button><button class="stitsh-ok">OK</button>' +
            '</div>' +
            '</div>'),
        events: {"click .stitsh-delete": "deleteTag",
                 "click .stitsh-ok": "closeDialog",
                 "click .stitsh-overlay-background": "closeDialog",
                },
        constructor: function EditTagOverlayView  (options) {
            Backbone.View.prototype.constructor.apply(this,arguments);
        },
        initialize: function (options) {
            _.bindAll(this,"render", "productChanged");
            $(this.el).css({"position": "absolute"});
        },
        focus: function () {
            this.$("input").focus();
        },
        render: function () {
            fillParent(this.el);
            $(this.el).html(this.template(this.model.toJSON()));
            fillParent(this.$(".stitsh-inner"));

            // center the edit-window
            $editwin = this.$(".stitsh-edit-window")
            $editwin.css({left: $(this.el).width()/2-$editwin.outerWidth()/2,
                          top: $(this.el).height()/2-$editwin.outerHeight()/2});

            this.$("input").autocomplete(
                "/tagging/product_autocomplete/",
                {
                    formatItem: function(item) {
                        return '<img width="24" height="24" src="' + escapeHTML(item.image) + '"/> ' + escapeHTML(item.name);
                    },
                    parse: function(data) {
                        // jQuery already parses the JSON
                        return _.map(data, function (row) {
                            return {data:row,
                                    value: row.name,
                                    result: row.name };
                        });
                    }
                }).bind("result", this.productChanged);
        },
        deleteTag: function () {
            if (!this.model.isNew()) {
                this.model.destroy();
            }
            this.model.collection.remove(this.model);
            this.remove();
        },
        closeDialog: function () {
            this.remove();
        },
        productChanged: function (event, product) {
            this.model.set({"product": product});
            this.model.save();
        }
    });

    
    ////
    // PhotoView view

    var PhotoView = Stitsh.PhotoView = Backbone.View.extend({
        className: "stitsh-tagger",
        initialize: function (options) {
            _.bindAll(this, "render", "changeMode", "editTag");
            
            this.controller = options.controller;
            
            // Wrap the img inside the newly created div
            this.img = options.img;
            $(this.el).insertBefore(this.img);
            $(this.el).append(this.img);

            this.overlay = new DisplayOverlayView({model: this.model, controller: this.controller});
            $(this.el).append(this.overlay.el);            

            // Create a a div to be shared by the tag views
            this.inner = this.make("div", {"class": "stitsh-inner"})
            $(this.el).append(this.inner);

            // Both of these views use the SAME element
            this.createview = null; // lazy created when we enter edit mode
            this.tagsview = new PhotoTagsView({model: this.model, el: this.inner, controller: this.controller});

            this.controller.bind("changeMode",this.changeMode);

            this.controller.bind("editTag", this.editTag)
            
            this.render();
        },
        render: function () {
            $(this.el).css({"width": this.img.width(), "height": this.img.height()})
            fillParent(this.inner);
            this.tagsview.render();
            if (this.createview) this.createview.render();
            this.overlay.render();
            if (this.edittagview) this.edittagview.render();
        },
        changeMode: function (mode) {
            if (mode == "display") {
                $(this.el).removeClass("stitsh-edit-mode");
                $(this.el).addClass("stitsh-display-mode");
            } else if (mode =="edit") {
                $(this.el).addClass("stitsh-edit-mode");
                $(this.el).removeClass("stitsh-display-mode");
                if (!this.createview) {
                    this.createview = new CreateTagView({model: this.model, el: this.inner, controller: this.controller});
                }
            }
        },
        editTag: function (tag) {
            // If there's an existing tag editing going on (which
            // there shouldn't be, but just in case) let's remove it
            // now
            if (this.edittagview) {
                this.edittagview.remove();
            }
            // Create a new overlay view to edit the tag
            this.edittagview = new EditTagOverlayView({model: tag, controller: this.controller});
            $(this.el).append(this.edittagview.el);
            this.edittagview.render();
            this.edittagview.focus();
        },
    });

    ////
    // Photo Controller
    //
    // This is NOT a backbone controller!
    //
    // Handles widget level state, like edit/display mode and widget
    // level events, like show tag info

    var PhotoController = Stitsh.PhotoController = function PhotoController () {
        this.initialize.apply(this,arguments);
    };

    _.extend(PhotoController.prototype, Backbone.Events, {
        initialize: function (options) {
            img = options.img;
            
            this.mode = options.mode || PhotoController.DISPLAY;
            this.focusedTag = null;

            // Create the model and the view. The view is given this
            // instance as a paramter and will subscribe to events on
            // this controller as it needs them
            
            this.model = new Photo({id: options.photo_id}); 
            this.model.fetch();
            this.view = new PhotoView({model: this.model, controller: this, img: img});

            // Fire off an initial mode change event to get everything set up
            this.trigger("changeMode", this.mode);
        },
        changeMode: function (mode) {
            if (mode != this.mode) {
                this.mode = mode;
                this.trigger("changeMode", mode);
            }
        },
        displayMode: function ( ) {
            this.changeMode(PhotoController.DISPLAY);
        },
        editMode: function ( ) {
            this.changeMode(PhotoController.EDIT);
        },
        focusTag: function (tag) {
            if (this.focusedTag != tag) {
                this.focusedTag = tag;
                this.trigger("focusTag", tag);
            }
        },
        unfocusTag: function () {
            if (this.focusedTag != null) {
                this.focusedTag = null;
                this.trigger("unfocusTag");
            }
        },
        editTag: function (tag) {
            this.trigger("editTag", tag);
        }
    });
    _.extend(PhotoController,{
        EDIT: "edit",
        DISPLAY: "display"
    });

    ////
    // jQuery Plugin

    $.fn.stitshTags = function (options) {
        return this.each(function (i,img) {
            $(img).load(function () {
                new PhotoController(_.defaults({img: $(img), photo_id: $(img).attr("data-photo-id")},options));
            });
        });
    };
    
})();



// CSFR for Django

$(document).ajaxSend(function(event, xhr, settings) {
    function getCookie(name) {
        var cookieValue = null;
        if (document.cookie && document.cookie != '') {
            var cookies = document.cookie.split(';');
            for (var i = 0; i < cookies.length; i++) {
                var cookie = jQuery.trim(cookies[i]);
                // Does this cookie string begin with the name we want?
                if (cookie.substring(0, name.length + 1) == (name + '=')) {
                    cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
                    break;
                }
            }
        }
        return cookieValue;
    }
    function sameOrigin(url) {
        // url could be relative or scheme relative or absolute
        var host = document.location.host; // host + port
        var protocol = document.location.protocol;
        var sr_origin = '//' + host;
        var origin = protocol + sr_origin;
        // Allow absolute or scheme relative URLs to same origin
        return (url == origin || url.slice(0, origin.length + 1) == origin + '/') ||
            (url == sr_origin || url.slice(0, sr_origin.length + 1) == sr_origin + '/') ||
            // or any other URL that isn't scheme relative or absolute i.e relative.
            !(/^(\/\/|http:|https:).*/.test(url));
    }
    function safeMethod(method) {
        return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
    }

    if (!safeMethod(settings.type) && sameOrigin(settings.url)) {
        xhr.setRequestHeader("X-CSRFToken", getCookie('csrftoken'));
    }
});
