define(['jquery', 'underscore', 'backbone', "views/CitationView", "views/ProvStatementView"], 				
	function($, _, Backbone, CitationView, ProvStatement) {
	'use strict';

	
	var ProvChartView = Backbone.View.extend({
		initialize: function(options){
			if((typeof options === "undefined") || !options) var options = {};
			
			this.parentView    = options.parentView    || null;
			this.sources 	   = options.sources       || null;
			this.derivations   = options.derivations   || null;
			this.context 	   = options.context       || null;
			this.contextEl     = options.contextEl     || $("body");
			this.packageModel  = options.packageModel  || null;
			this.nodeHeight    = options.nodeHeight    || 67; 	  //Pixel height of the node including padding and margins
			this.pointerHeight = options.pointerHeight || 15;     //Pixel height of the pointer/arrow image
			this.offsetTop     = options.offsetTop     || this.nodeHeight; //The top margin of the chart, in pixels
			this.title 		   = options.title         || "";
			this.editor 	   = options.editor        || false;
			this.editorType    = options.editorType    || null;

			//For Sources charts
			if(!this.derivations && this.sources){
				this.type 		    = "sources";
				this.provEntities   = this.sources;
				
				//Find the number of sources and programs
				var sources = [], programs = [];
				_.each(this.sources, function(model){
					if(model.get("type") == "program")
						programs.push(model);
					else
						sources.push(model);
				});
				
				this.sources = sources;
				this.programs = programs;
				
				this.numSources      = this.sources.length;
				this.numPrograms     = this.programs.length;
				this.numProvEntities = this.numSources;
				this.numPrograms     = this.programs.length;
				this.numDerivations = 0;
			}
			
			//For Derivations charts
			if(!this.sources && this.derivations){
				this.type 	   	     = "derivations";
				this.provEntities = this.derivations;
				
				//Find the number of derivations and programs
				var derivations = [], programs = [];
				_.each(this.derivations, function(model){
					if(model.get("type") == "program")
						programs.push(model);
					else
						derivations.push(model);
				});
				
				this.derivations  = derivations;
				this.programs     = programs;
				
				this.numDerivations  = this.derivations.length;
				this.numProvEntities = this.numDerivations;
				this.numPrograms     = this.programs.length;
				this.numSources      = 0;
			}
			
			//For empty editor charts
			if(this.editor && !this.provEntities.length){
				this.type = options.editorType || null;
				this.sources = [];
				this.derivations = [];
				this.programs = [];
				this.provEntities = [];
				this.numDerivations = 0;
				this.numSources = 0;
				this.numProvEntities = 0;
				this.className += " editor empty";
			}
			
			//Add the chart type to the class list
			this.className = this.className + " " + this.type;
			
			//Create a title
			if((this.context.get("type") == "program") && (this.type == "derivations")){
				this.title = this.numProvEntities + " outputs";				
			}
			else if((this.context.get("type") == "program") && (this.type == "sources")){
				this.title = this.numProvEntities + " inputs";				
			}
			else
				this.title 	   = this.numProvEntities + " " + this.type;
			
			//The default height of the chart when all nodes are visible/expanded
			this.height = (this.numProvEntities * this.nodeHeight);

		},
		
		tagName: "aside",
		
		className: "prov-chart",
		
		events: {
			"click .expand-control"   : "expandNodes",
			"click .collapse-control" : "collapseNodes",
			"click .preview"         : "previewData"
		},
		
		subviews: new Array(),
		
		render: function(){
			//Nothing to do if there are no entities and it isn't an editor
			if(!this.numProvEntities && !this.numPrograms && !this.editor) return false;
			
			var view = this;
			
			//Are there any programs?
			if(this.programs.length && !this.editor){
				this.$el.append($(document.createElement("div")).addClass(this.type + "-programs programs"));
			}
			
			var position = 0,
				programPosition = 0;
			_.each(this.provEntities, function(entity, i){
				
				//Create the HTML node and line connecter
				if(entity.type == "Package")
					view.$el.append(view.createNode(entity, position, _.find(entity.get("members"), function(member){ return member.get("formatType") == "METADATA"; })));	
				else{
					//Find the id of the metadata that documents this object
					var metadataID = entity.get("isDocumentedBy"),
						metadata = null;
					
					if(Array.isArray(metadataID))
						metadataID = metadataID[0];
					
					if(metadataID){
						//The metadata doc for this object may be in the same package as the context of this prov chart
						metadata = _.find(view.packageModel.get("members"), function(member){ return member.get("id") == metadataID });
					}
					
					if(!metadata){
					//Or it may be in any of the other packages related to that package
						var potentialMatch;
						_.each(view.packageModel.get("relatedModels"), function(model){
							potentialMatch = _.find(model.get("members"), function(member){ return member.get("id") == metadataID });
							if(potentialMatch)
								metadata = potentialMatch;
						});
					}

					//Programs will be positioned at a different point in the graph
					if(entity.get("type") == "program"){
						//Find the program position
						this.$(".programs").append(this.createNode(entity, programPosition, metadata));
					}
					else
						this.$el.append(view.createNode(entity, position, metadata));						
				}
				
				//Derivation charts have a pointer for each node
				if(view.type == "derivations" && (this.numDerivations > 0 || this.editor))
					view.$el.append(view.createConnecter(position));
				
				//Source charts have a connector for each node and one pointer
				if(view.type == "sources" && (this.numSources > 0 || this.editor))
					view.$el.append(view.createConnecter(position));
				
				//Bump the position for non-programs only
				if(entity.get("type") == "program")
					programPosition++;
				else
					position++;
				
			}, this);	
			
			//If we are drawing a blank editor
			if(this.editor){
				this.$el.append(this.createEditorNode());
				
				//Derivation charts have a pointer for each node
				if(this.type == "derivations") this.$el.append(this.createConnecter(this.numProvEntities));
				//Source charts have a connector for each node and one pointer
				if(this.type == "sources")	this.$el.append(this.createConnecter(this.numProvEntities));
			}
			
			//Move the last-viewed prov node to the top of the chart so it is always displayed first
			if(this.$(".node.previous").length > 0)
				this.switchNodes(this.$(".node.previous").first(), this.$(".node").first());
	
			//Add classes
			this.$el.addClass(this.className);
			if(this.numPrograms > 0) this.$el.addClass("has-programs");
			if(this.numDerivations == 1 && !this.numPrograms) this.$el.addClass("one-derivation");
			
			//Specify classes for the context element (e.g. entity details container)
			var contextClasses = this.type == "sources" ? "hasProvLeft" : "hasProvRight";
			
			if(this.numPrograms > 0 && this.type == "sources"){
				contextClasses += " hasProgramsLeft";
			}
			else if(this.numPrograms > 0 && this.type == "derivations"){
				contextClasses += " hasProgramsRight";
			}
			
			$(this.contextEl).addClass(contextClasses);
			
			//If it's a derivation chart, add a connector line
			if(this.type == "derivations" && !this.numPrograms) this.$el.append(this.createPointer());
			//If it's a sources chart, add a pointer arrow
			if((this.type == "sources") && !this.numPrograms) this.$el.append(this.createPointer());
			
			//Charts with programs need an extra connecter
			if(this.programs.length && (this.numSources || this.numDerivations)) 
				this.$(".programs").append(this.createConnecter());
			
			if(this.$(".collapsed").length){
				var expandIcon   = $(document.createElement("i")).addClass("icon icon-expand-alt"),
				    collapseIcon = $(document.createElement("i")).addClass("icon icon-collapse-alt");

				this.$el.addClass("expand-collapse")
				        .append($(document.createElement("a"))
				        		          .addClass("expand-control")
				        		          .text("view more ")
				        		          .append(expandIcon))
				        .append($(document.createElement("a"))
				        		          .addClass("collapse-control")
				        		          .text("view less ")
				        		          .append(collapseIcon));
				this.collapseNodes(false);
			}
			else
				this.$el.css("height", this.height - this.offsetTop);
			
			//Lastly, add the title
			this.$el.prepend($(document.createElement("h3")).addClass("title").text(this.title));
						
			return this;
		},
		
		createNode: function(provEntity, position, metadata){
			//What kind of icon will visually represent this object type?
			var icon = "",
				type = null;
			
			if(provEntity.type == "SolrResult"){
				type = provEntity.getType();
				
				if(type == "data")
					icon = "icon-table";
				else if(type == "metadata")
					icon = "icon-file-text";
				else if (type == "image")
					icon = "icon-picture";
				else if (type == "PDF")
					icon = "icon-file pdf";
			}
			else if(provEntity.type == "Package"){
				icon = "icon-folder-open",
				type = "package";
			}
			
			if(!type){
				type = "data";
				icon = "icon-table";
			}
			
			//Get the name of this object
			var name = provEntity.get("fileName") || provEntity.get("id") || type;
			var id   = provEntity.get("id");
			
			//Get the top CSS style of this node based on its position in the chart and determine if it vertically overflows past its context element
			if(provEntity.get("type") == "program"){
				var distanceFromMiddle = (position * this.nodeHeight) - (this.nodeHeight/2),
					operator           = distanceFromMiddle > 0 ? "+" : "-",
				    top                = "calc(50% " + operator + " " + Math.abs(distanceFromMiddle).toString() + "px)",
					isCollapsed        = "expanded";
			}
			else{
				var top = (position * this.nodeHeight) - (this.nodeHeight/2),
					isCollapsed = ((top + this.nodeHeight + this.offsetTop) > $(this.contextEl).outerHeight()) ? "collapsed" : "expanded";					
			}

			if(provEntity.get("type") != "program"){
				//Create a DOM element to represent the node	
				var nodeEl = $(document.createElement("div")).css("top", top);;
			}
			else{
				//Create an SVG drawing for the program arrow shape
				var svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"),
					nodeEl = $(document.createElementNS("http://www.w3.org/2000/svg", "polygon"))
					    		 .attr("points", "2,20 2,48 17,48 17,67 67,33.5 17,2 17,20");

				//Set a viewBox, height, width, and top position
				svg.setAttribute("viewBox", "0 0 " + this.nodeHeight + " " + this.nodeHeight);
				svg.setAttribute("class", "popover-this");
				$(svg).attr("width", this.nodeHeight + "px").attr("height", this.nodeHeight + "px").css("top", top);
				
				//Create the code icon
				var iconEl = $(document.createElementNS("http://www.w3.org/2000/svg", "text"))
							.text("\u{F121}")
							.attr("class", "icon icon-foo program-icon pointer");
				
				//Create a group element to contain the icon
				var g = $(document.createElementNS("http://www.w3.org/2000/svg", "g"))
						.attr("transform", "translate(18,43)")
						.attr("class", "popover-this program-icon pointer");
				
				//Glue it all together
				$(g).append(iconEl);
				$(svg).append(nodeEl, g);
				
			}
			
			//Add classes via .attr() so it works for SVG, too
			var currentClasses = $(nodeEl).attr("class") || "";
			$(nodeEl).attr("class", currentClasses + " " + type + " node pointer popover-this " + isCollapsed)
					 .attr("tabindex", 0)
					 //Reference the id of the data object
					 .attr("data-id", provEntity.get("id"));
			
			//Display images in the prov chart node
			
			if(type == "image"){
				$(nodeEl).css("background-image", "url('" + provEntity.get("url") + "')");
			}
			//Create an icon inside the node for other format types
			else{
				var iconEl = $(document.createElement("i"))
							 .addClass(icon + " icon");
				//Put the icon in the node
				$(nodeEl).append(iconEl);		
			}
		
			//The placement and title of the popover depends on what type of chart this is
			if(this.type == "derivations"){
				var placement = "left";
				var title = "Derived " + type;
			}
			else{
				var placement = "right";		
				var title = "Source " + type;
			}
						
			if(metadata) var citationModel = metadata;
			else var citationModel = provEntity;
			
			var relatedModels = this.packageModel.get("relatedModels");
			
			//The citation
			var createLink = true;
			if((provEntity.get("id") == appModel.get("pid")) || (citationModel.get("id") == appModel.get("pid")))
				createLink = false;
			
			var citationHeader = $(document.createElement("h6")).addClass("subtle").text("Citation");
			var citationEl = new CitationView({
				model: citationModel,
				createLink: createLink
			}).render().el;
			
			//The title
			var titleEl = $(document.createElement("span")).append($(document.createElement("i")).addClass(icon + " icon-on-left"), title);
			
			//The name
			if(name)
				var nameEl = $(document.createElement("h5")).addClass("name").text(name);

			//The View link
			var arrowIcon = $(document.createElement("i")).addClass("icon-double-angle-right icon-on-right");
			if(_.contains(this.packageModel.get("memberIds"), provEntity.get("id")))
				var linkEl = $(document.createElement("a")).attr("href", "#view/" + provEntity.get("id")).addClass("btn preview").attr("data-id", provEntity.get("id")).text("View").append(arrowIcon);
			else
				var linkEl = $(document.createElement("a")).attr("href", "#view/" + provEntity.get("id")).addClass("btn").text("View").append(arrowIcon);
			
			//The provenance statements
			var provStatementView = new ProvStatement({
				model            : provEntity, 
				relatedModels    : relatedModels,
				currentlyViewing : this.context,
				parentView       : this
				});
			var provStatementEl = provStatementView.render().el;
			this.subviews.push(provStatementView);
			
			//Glue all the parts together
			var headerContainer = $(document.createElement("div")).addClass("well header").append(citationHeader, citationEl, linkEl);
			var popoverContent = $(document.createElement("div")).append(headerContainer, provStatementEl).attr("data-id", provEntity.get("id"));
			
			//Add the name of the data object to the popover
			if(name)
				$(headerContainer).prepend(nameEl);
			
			//Display images in the prov chart node popover 
			if(type == "image"){
				var img = $(document.createElement("img")).attr("src", provEntity.get("url")).addClass("thumbnail");
				$(citationEl).after(img);
			}

			//Mark the node that was last viewed, if any
			if(appModel.get("previousPid") == provEntity.get("id")){
				$(nodeEl).addClass("previous");
				$(citationEl).before($(document.createElement("h7")).text("Last viewed"));
			}
			
			//Get the id->class name map for unique node colors
			var classMap = this.parentView.classMap || null;
			
			//Add a popover to the node that will show the citation for this dataset and a provenance statement
			var view = this,
				popoverTriggerEl = (provEntity.get("type") == "program") ? $(nodeEl).add(g) : nodeEl;
			
			$(nodeEl).popover({
				html: true,
				placement: placement,
				trigger: "click",
				container: this.el,
				title: titleEl,
				content: function(){ 
					//Find the unique class name associated with this ID
					if(classMap){
						var allProvLinks = $(popoverContent).find(".provenance-statement .node-link[data-id]");
						_.each(allProvLinks, function(provlink, i, allProvLinks){
							var id      = $(provlink).attr("data-id"),
								mapItem = _.findWhere(classMap, {id: id});
						
							if(typeof mapItem !== "undefined"){
								var className = mapItem.className,
									matchingProvLinks = $(allProvLinks).filter("[data-id='" + id + "']");
								if(matchingProvLinks.length > 0)
									$(matchingProvLinks).addClass(className);	
							}
						});					
					}
					
					return 	popoverContent;
				}
			}).on("show.bs.popover", function(){
				//Close the last open node popover
				$(".popover-this.active").popover("hide");
								
				//Toggle the active class
				if($(this).parent("svg").length) 
						$(this).attr("class", $(this).attr("class") + " active");
				else
					$(this).toggleClass("active");
				
			}).on("hide.bs.popover", function(){				
				//Toggle the active class
				if($(this).parent("svg").length) 
					$(this).attr("class", $(this).attr("class").replace(" active", " "));
				else
					$(this).toggleClass("active");
			});
			
			/*
			 * Set a separate event listener on the program icon since it is overlapped with the program arrow
			 */
			if(provEntity.get("type") == "program"){
				$(g).on("click", function(){
					var programNode = $(this).prev("polygon"),
						isOpen = $(programNode).attr("class").indexOf("active") > -1;
					
						if(isOpen)
							$(programNode).popover("hide");
						else
							$(programNode).popover("show");
				});
			}
			
			// If the prov statement views in the popover content have an expand collapse list view, then we want to delegate events 
			//  again when the popover is done displaying. This is because the ExpandCollapseList view hides/shows DOM elements, and each time
			// the DOM elements are hidden, their events are detached.
			if(provStatementView.subviews.length > 0){
				//Get the ExpandCollapseList views
				var expandCollapseLists = _.where(provStatementView.subviews, {name: "ExpandCollapseList"});
				if(expandCollapseLists.length > 0){
					//When the popover is *done* displaying
					$(nodeEl).on("shown.bs.popover", function(){
						//Delegate the events of each of the ExpandCollapseList views
				  	    _.each(expandCollapseLists, function(subview){
							subview.delegateEvents(subview.events);
						});
		 			});
				}
			}
			
			//If this node is rendered as an SVG, return that. Otherwise return the node element created.
			return (typeof svg != "undefined")? svg : nodeEl;
		},
		
		createEditorNode: function(){
			//Get the top CSS style of this node based on its position in the chart and determine if it vertically overflows past its context element
			var position = this.numProvEntities,
				top = (position * this.nodeHeight) - (this.nodeHeight/2),
				isCollapsed = ((top + this.nodeHeight + this.offsetTop) > $(this.contextEl).outerHeight()) ? "collapsed" : "expanded";			
			
			//Create a DOM element to represent the node	
			var nodeEl = $(document.createElement("div"))
						 .addClass(this.type + " node pointer popover-this editor " + isCollapsed)
						 .attr("tabindex", 0)
						 .attr("data-id", "")
						 .css("top", top);
			
			//Create the plus icon
			var iconEl = $(document.createElement("i"))
						 .addClass("editor icon icon-plus");
			
			//Put the icon in the node
			$(nodeEl).append(iconEl);
			
			return nodeEl;
		},
		
		createConnecter: function(position){
			if(typeof position == "undefined"){
				var top = "50%",
					isCollapsed = "";
			}
			else{
				var top = this.nodeHeight * position,
				    isCollapsed = ((top + (this.nodeHeight/2) + this.offsetTop) > $(this.contextEl).outerHeight()) ? "collapsed" : "expanded";			
			}			
			
			return $(document.createElement("div")).addClass("connecter " + isCollapsed).css("top", top);
		},
		
		createPointer: function(position){			
			var pointer =  $(document.createElement("img")).attr("src", "./img/arrow.gif").addClass("prov-pointer");
			
			if(typeof position !== "undefined"){
				var top = ((this.nodeHeight * position) - (this.pointerHeight/2)),
					isCollapsed = ((top + (this.nodeHeight/2) + this.offsetTop) > $(this.contextEl).outerHeight()) ? "collapsed" : "expanded";
				
				$(pointer).css("top", top + "px").addClass(isCollapsed);
			}
			
			return pointer;
		},
		
		/*
		 * Displays the nodes that are collapsed/hidden - not all provenance charts will have collapsed nodes
		 */
		expandNodes: function(){
			//Change the context element (accompanying metadata section) and the chart itself to the full expanded height
			$(this.contextEl).height(this.height + this.offsetTop);
			this.$el.height(this.height - this.offsetTop);
			
			//Hide the expand control and show the hidden nodes
			this.$(".expand-control").fadeOut();
			this.$(".collapse-control").fadeIn();
			this.$(".collapsed").fadeIn();
		},
		
		collapseNodes: function(scroll){			
			//Fit the context element to its contents
			$(this.contextEl).height("auto");
			
			//Use the last expanded/visible connecter element to determine the chart height
			var lastConnecter = _.last(this.$(".connecter.expanded"));				
			
			if(typeof lastConnecter !== "undefined") 
				this.$el.height(parseInt(lastConnecter.style.top));
			else
				this.$el.height(this.height);
			
			//Find the pointer and move to the half-way point of the chart height
			this.$(".prov-pointer").css("top", "50%");
			
			//Hide the expand control and show the hidden nodes
			this.$(".expand-control").fadeIn();
			this.$(".collapse-control").css("display", "none");
			
			//Fade out the collapsed elements and scroll the page back up to the chart since when
			//the elements collapse the user may be left several hundred pixels downpage
			var chartEl = this.$el,
				i = 0,
				numAnimations = this.$(".collapsed").length;
			
		},
		
		switchNodes: function(nodeA, nodeB){
			if(nodeA == nodeB) return;
			
			var oldPosition =  $(nodeA).css("top");
			var isCollapsed =  $(nodeA).hasClass("collapsed");
			
			$(nodeA).css("top", (this.nodeHeight/2) * -1).removeClass("collapsed");
			$(nodeB).first().css("top", oldPosition);
			if(isCollapsed) $(nodeB).first().addClass("collapsed");
		},
		
		/*
		 * Will show a preview of the data for the currently active node
		 */
		previewData: function(e){
			//Don't go anywhere yet...
			e.preventDefault();
			
			//If this prov chart has a parent view with a previewData function, then execute that
			if(this.parentView && this.parentView.previewData && this.parentView.previewData(e)){
					
				//Trigger a click on the active node to deactivate it
				this.$(".node.active").click();
				
				//Exit
				return;
			}
			
			//Get the target of the click
			var button = $(e.target);
			if(!$(button).hasClass("preview")) 
				button = $(button).parents("a.preview");
			if(button.length < 1) 
				button = $(button).parents("[href]");
			
			//Trigger a click on the active node to deactivate it
			this.$(".node.active").click();
			
			//navigate to the link href
			window.location = $(button).attr("href");
		},
		
		onClose: function() {			
			this.remove();			
		}
		
	});
	
	return ProvChartView;
});