/**
 * Mandala is a conceptual visualization that shows the relationships between terms and documents.
 *
 * @example
 *
 *   let config = {
 *     "labels": true,
 *     "query": null,
 *     "stopList": "auto",
 *   };
 *
 *   loadCorpus("austen").tool("Mandala", config);
 *
 * @class Mandala
 * @tutorial mandala
 * @memberof Tools
 */
Ext.define('Voyant.panel.Mandala', {
	extend: 'Ext.panel.Panel',
	mixins: ['Voyant.panel.Panel'],
	alias: 'widget.mandala',
    statics: {
    	i18n: {
    	},
    	api: {
    		/**
			 * @memberof Tools.Mandala
			 * @instance
			 * @property {stopList}
			 * @default
			 */
    		stopList: 'auto',
    		
			/**
			 * @memberof Tools.Mandala
			 * @instance
			 * @property {query}
			 */
    		query: undefined,
    		
			/**
			 * @memberof Tools.Mandala
			 * @instance
			 * @property {Boolean} labels Whether or not labels should be shown.
			 * @default
			 */
    		labels: true
    		
    	},
    	glyph: 'xf1db@FontAwesome'
	},
	
	gutter: 5,
	
	textFont: '12px sans-serif',
	
	config: {
    	options: [{xtype: 'stoplistoption'},{xtype: 'categoriesoption'}]
	},
	
    constructor: function() {

    	this.mixins['Voyant.util.Localization'].constructor.apply(this, arguments);
    	Ext.apply(this, {
    		title: this.localize('title'),
			html: '<div style="text-align: center"><canvas width="800" height="600"></canvas></div>',
			dockedItems: [{
                dock: 'bottom',
                xtype: 'toolbar',
                overflowHandler: 'scroller',
                items: [{
                	text: this.localize('add'),
        			glyph: 'xf067@FontAwesome',
                	handler: function() {
                		this.editMagnet();
                	},
                	scope: this
                },{
                	text: this.localize('clear'),
					glyph: 'xf014@FontAwesome',
                	handler: function() {
                		this.setApiParam('query', undefined);
                		this.updateFromQueries(true);
                		this.editMagnet();
                	},
                	scope: this
	            },{
	                xtype: 'checkbox',
	                boxLabel: this.localize('labels'),
	                listeners: {
	                	render: function(cmp) {
	                		cmp.setValue(this.getApiParam("labels")===true);
	    		        	Ext.tip.QuickTipManager.register({
	    		        		target: cmp.getEl(),
	   		                 	text: this.localize('labelsTip')
	    		        	});
	                		
	                	},
	                	beforedestroy: function(cmp) {
	                		Ext.tip.QuickTipManager.unregister(cmp.getEl());
	                	},
	                    change: function(cmp, val) {
	                    	this.setApiParam('labels', val);
	                    	this.draw();
	                    },
	                    scope: this
	                }
	            }]
    		}]			
    	});
        this.callParent(arguments);
    	this.mixins['Voyant.panel.Panel'].constructor.apply(this, arguments);
    	
    	this.on('boxready', function(cmp) {
			var canvas = this.getTargetEl().dom.querySelector("canvas");
			var me = this;
    		canvas.addEventListener('mousemove', function(evt) {
    			var rect = canvas.getBoundingClientRect(), x =  evt.clientX - rect.left, y = evt.clientY - rect.top,
				change = false, docRadius = parseInt(me.textFont)/2;
    			if (me.documents) {
    				me.documents.forEach(function(doc) {
    					var isHovering = x > doc.x-docRadius && x < doc.x+docRadius && y > doc.y-docRadius && y < doc.y+docRadius;    					
    					if (isHovering!=doc.isHovering) {change = true;}
    					doc.isHovering = isHovering;
    				})
    			}
				radius = parseInt(me.textFont)/2;
				for (term in me.magnets) {
					var isHovering = x > me.magnets[term].x-radius && x < me.magnets[term].x+radius && y > me.magnets[term].y-radius && y < me.magnets[term].y+radius;    					
					if (isHovering!=me.magnets[term].isHovering) {change = true;}
					me.magnets[term].isHovering = isHovering;
				}
				if (change) {
					me.draw();
				}
    	    }, false);
    		canvas.addEventListener('click', function(evt) {
    			var rect = canvas.getBoundingClientRect(), x =  evt.clientX - rect.left, y = evt.clientY - rect.top,
					docRadius = parseInt(me.textFont)/2;
				for (term in me.magnets) {
					if (x > me.magnets[term].x-radius && x < me.magnets[term].x+radius && y > me.magnets[term].y-radius && y < me.magnets[term].y+radius) {
						me.editMagnet(term);
					}
				}
    	    }, false);
    	})
    	
    	this.on('loadedCorpus', function(src, corpus) {
    		this.documents = [];
    		var canvas = this.getTargetEl().dom.querySelector("canvas"), ctx = canvas.getContext("2d"), radius = canvas.width/2;
    		ctx.font = this.textFont;
    		corpus.getDocuments().each(function(document) {
    			var label = document.getTinyTitle();
    			this.documents.push({
    				doc: document,
    				label: label,
    				width: ctx.measureText(label).width,
    				x: radius,
    				y: radius,
    				matches: [],
    				isHovering: false
    			});
    		}, this);
    		this.updateDocs(canvas);
    		this.draw();
    		this.updateFromQueries();
    	}, this);
    	
    	this.on("resize", function() {
    		var canvas = this.getTargetEl().dom.querySelector("canvas"),
    			diam = Math.min(this.getTargetEl().getWidth(), this.getTargetEl().getHeight());
    		canvas.width = diam;
    		canvas.height = diam;
	    	this.updateMagnets();
	    	this.updateDocs();
    		this.draw(canvas)
    	})
    },
    
    editMagnet: function(term) {
    	var me = this, currentTerms = Ext.Array.from(me.getApiParam('query'));
		Ext.create('Ext.window.Window', {
			title: this.localize("EditMagnet"),
			modal: true,
			items: {
				xtype: 'form',
				width: 300,
				items: [{
					xtype: 'querysearchfield',
					corpus: this.getCorpus(),
					store: this.getCorpus().getCorpusTerms({
						proxy: {
							extraParams: {
								stopList: this.getApiParam('stopList')
							}
						}
					}),
					stopList: this.getApiParam('stopList'),
					listeners: {
						afterrender: function(field) {
							if (term) {
								var termObj = new Ext.create("Voyant.data.model.CorpusTerm", {
									term: term
								});
								field.getStore().loadData(termObj, true)
								field.setValue(termObj);
							}
						}
					}
				},{
					xtype: "numberfield",
				    fieldLabel: me.localize('rotateClockwise'),
				    minValue: 0,
				    maxValue: currentTerms.length-1,
				    value: 0,
				    stepValue: 1,
				    width: 200,
				    name: "rotate"
				}],
				buttons: [{
	            	text: this.localize("remove"),
					glyph: 'xf0e2@FontAwesome',
	            	flex: 1,
		            ui: 'default-toolbar',
	        		handler: function(btn) {
	        			var queries = Ext.Array.filter(Ext.Array.from(me.getApiParam('query')), function(query) {
	        				return query!=term
	        			});
	        			me.setApiParam('query', queries);
	        			me.updateFromQueries(queries.length==0);
	        			btn.up('window').close();
	        		},
	        		scope: this
				},{xtype: 'tbfill'}, {
	            	text: this.localize("cancel"),
		            ui: 'default-toolbar',
	                glyph: 'xf00d@FontAwesome',
	        		flex: 1,
	        		handler: function(btn) {
	        			btn.up('window').close();
	        		}
				},{
	            	text: this.localize("update"),
					glyph: 'xf00c@FontAwesome',
	            	flex: 1,
	        		handler: function(btn) {
	        			var val = btn.up('window').down('querysearchfield').getValue().join("|")
	        			if (val) {

	        				// start by updating the term in place
		        			var position = -1;
		        			for (var i=0; i<currentTerms.length; i++) {
		        				if (term==currentTerms[i]) {
		        					position=i;
		        					currentTerms[i]=val;
		        					
				        			// see if we need to shift
				        			var rotate = btn.up('window').down('numberfield').getValue();
				        			if (rotate) {
				        				currentTerms.splice(i, 1);
				        				var newpos = i+rotate;
				        				if (newpos>currentTerms.length) {newpos-=currentTerms.length+1;}
				        				currentTerms.splice(newpos, 0, val);
				        			}
				        			break
				        			
		        				}
		        			}
		        			if (position==-1) { // not sure why it couldn't be found
		        				currentTerms.push(val);
		        			}		        			
	        			}
	        			
	        			me.setApiParam('query', currentTerms);
	        			me.updateFromQueries(currentTerms.length==0);
	        			btn.up('window').close();
	        		},
	        		scope: this
	            }]
			},
			bodyPadding: 5
		}).show()
    },
    
    updateFromQueries: function(allowEmpty) {
		this.magnets = undefined;
		this.documents.forEach(function(doc) {doc.matches=[]})
		this.updateDocs();
		this.draw();
    	if (this.documents) {
    		var params = this.getApiParams();
    		if (!params.query) {params.limit=10;}
        	var queries = Ext.Array.from(this.getApiParam('query'));
        	if (!allowEmpty || queries.length>0) {
        		this.getCorpus().getCorpusTerms().load({
        			params: Ext.apply(params, {withDistributions: true}),
        			callback: function(records) {
        		    	var canvas = this.getTargetEl().dom.querySelector("canvas"), ctx = canvas.getContext("2d");
        		    		diam = canvas.width, rad = diam /2;
        		    	ctx.font = this.textFont;
        		    	var magnets = {};
        		    	for (var i=0, len=records.length; i<len; i++) {
        		    		var term = records[i].getTerm();
        		    		records[i].getDistributions().forEach(function(val, i) {
        		    			if (val>0) {
        		    				this.documents[i].matches.push(term)
        		    			}
        		    		}, this);
        		    		magnets[term] = {
        		    			record: records[i],
        		    			colour: this.getApplication().getColor(i),
        		    			width: ctx.measureText(term).width,
        		    			isHovering: false
        		    		}
        		    	}

        		    	this.magnets = {};
        		    	
        		    	// try ordering by queries
        		    	queries.forEach(function(query) {
        		    		if (magnets[query]) {
            		    		this.magnets[query] = magnets[query]
            		    		delete magnets[query]
        		    		}
        		    	}, this);
        		    	
        		    	// now for any leftovers
        		    	for (term in magnets) {
        		    		this.magnets[term] = magnets[term]
        		    	}
        		    	
        		    	this.setApiParam('query', Object.keys(this.magnets))
        		    	this.updateMagnets();
        		    	this.updateDocs();
        		    	this.draw();
        			},
        			scope: this
        		})
        	}
    	}
    },
    
    updateMagnets: function(canvas) {
    	var canvas = this.getTargetEl().dom.querySelector("canvas"), diam = canvas.width, rad = diam /2;
    	var len = Object.keys(this.magnets || {}).length;
    	var i = 0;
    	for (var term in this.magnets) {
    		Ext.apply(this.magnets[term], {
				x:  rad+((rad-this.gutter-50) * Math.cos(2 * Math.PI * i / len)),
				y:  rad+((rad-this.gutter-50) * Math.sin(2 * Math.PI * i / len))
    		})
    		i++;
    	}
    },
    
    updateDocs: function(canvas) {
    	canvas = canvas ||  this.getTargetEl().dom.querySelector("canvas"), diam = canvas.width, rad = diam /2;
    	var notMatching = [];
    	if (this.documents) {
        	this.documents.forEach(function(doc, i) {
        		if (Ext.Array.from(doc.matches).length==0) {notMatching.push(i);} // will be set around perimeter below
        		else if (Ext.Array.from(doc.matches).length==1) { // try to set it away from magnet
        			var x = (Math.random()*15)+15, y = (Math.random()*15)+15;
        			doc.targetX = this.magnets[doc.matches[0]].x + (Math.round(Math.random())==0 ? x : -x);
        			doc.targetY = this.magnets[doc.matches[0]].y + (Math.round(Math.random())==0 ? y : -y);
        		} else {
        			// determine the weighted position
        			var x = 0, y = 0,
        				vals = doc.matches.map(function(term) {return this.magnets[term].record.getDistributions()[i]}, this),
        				min = Ext.Array.min(vals), max = Ext.Array.max(vals);
        			var weights = 0;
        			doc.matches.forEach(function(term, j) {
        				weight = max==min ? 1 : ((vals[j]-min)+min)/((max-min)+min);
        				weights += weight;
        				x += this.magnets[term].x*weight;
        				y += this.magnets[term].y*weight;
        				
        			}, this)
        			doc.targetX = x/weights
        			doc.targetY = y/weights
        		}
        	}, this);
        	
        	// set around perimeter
        	for (var i=0, len=notMatching.length; i<len; i++) {
        		Ext.apply(this.documents[i], {
    				targetX:  rad+((rad-this.gutter) * Math.cos(2 * Math.PI * i / len)),
    				targetY:  rad+((rad-this.gutter) * Math.sin(2 * Math.PI * i / len))
        		})
        	}
    	}
    },
    
    draw: function(canvas, ctx) {
    	canvas = canvas ||  this.getTargetEl().dom.querySelector("canvas");
    	ctx = ctx || canvas.getContext("2d");
    	ctx.font = this.textFont;
    	var radius = canvas.width/2;
    	ctx.clearRect(0,0,canvas.width,canvas.height);
    	var labels = this.getApiParam('labels');
    	
    	// draw circle
    	ctx.beginPath();
    	ctx.strokeStyle = "rgba(0,0,0,.1)"
        ctx.fillStyle = "rgba(0,0,0,.02)"
    	ctx.arc(radius, radius, radius-this.gutter, 0, 2 * Math.PI, false);
    	ctx.fill();
    	ctx.lineWidth = 2;
    	ctx.stroke();
    	
    	// determine if we're animating a move and need to come back
    	var needRedraw = false;
    	
    	// draw documents
    	
    	if (this.documents && this.documents.length>0) {
    		var needMove = false;
    		
    		var noHovering = Ext.Array.each(this.documents, function(doc) {
    			return !doc.isHovering
    		}, this);
    		
    		if (noHovering===true) {
    			noHovering = Ext.Array.each(Object.keys(this.magnets || {}), function(term) {
        			return !this.magnets[term].isHovering
        		}, this);
    		}
			// go through a first time to draw connecting lines underneath
    		var hoveringTerms = {}; hoveringDocs = [];
    		this.documents.forEach(function(document, j) {
	        	document.matches.forEach(function(term, i) {
	        		ctx.beginPath();
	        		ctx.moveTo(document.x, document.y);
	        		ctx.lineTo(this.magnets[term].x, this.magnets[term].y);
	        		if (noHovering===true) {
		        		ctx.strokeStyle = "rgba("+this.magnets[term].colour.join(",")+",.1)";
	        		} else {
	        			if (document.isHovering || this.magnets[term].isHovering) {
	        				hoveringDocs[j]=true;
	        				hoveringTerms[term]=true;
			        		ctx.strokeStyle = "rgba("+this.magnets[term].colour.join(",")+",.5)";
	        			} else {
		        			ctx.strokeStyle = "rgba(0,0,0,.02)";
	        			}
	        		}
	        		ctx.stroke();
	        	}, this);
    		}, this);
    			
			// now a second time for labels/markers
    		var halfSize = parseInt(this.textFont)/2, height = parseInt(this.textFont)+4;
    		this.documents.forEach(function(document, i) {
    			
    			// draw marker/label
    			if (labels || document.isHovering || hoveringDocs[i]==true) {
	    		    var width = document.width+4;
			        ctx.fillStyle = document.isHovering || hoveringDocs[i]==true || noHovering===true ? "white" : "rgba(255,255,255,.05)"
	    		    ctx.fillRect(document.x-(width/2), document.y-(height/2), width, height);
			        ctx.strokeStyle = document.isHovering || hoveringDocs[i]==true || noHovering===true ? "rgba(0,0,0,.2)" : "rgba(0,0,0,.05)"
	    		    ctx.strokeRect(document.x-(width/2), document.y-(height/2), width, height);
			        ctx.textAlign = "center";
			        ctx.fillStyle = document.isHovering || hoveringDocs[i]==true || noHovering===true ? "rgba(0,0,0,.8)" : "rgba(0,0,0,.05)";
	    		    ctx.fillText(document.label, document.x, document.y);
    			} else {
    		    	ctx.beginPath();
    		        ctx.fillStyle = "rgba(0,0,0,.8)"
    		    	ctx.arc(document.x, document.y, halfSize, 0, 2 * Math.PI);
    	        	ctx.fill();
    		    	ctx.stroke();
    			}
	        	
	        	// determine if we need to move
		    	var dx = Math.abs(document.x - document.targetX), dy = Math.abs(document.y- document.targetY)
		    	if (dx!=0 || dy!=0) {
		    		if (dx<1) {document.x = document.targetX}
		    		else {
		    			dx/=2;
		    			document.x = document.x > document.targetX ? document.x-dx : document.x+dx;
		    		}
		    		if (dy<1) {document.y = document.targetY}
		    		else {
		    			dy/=2;
		    			document.y = document.y > document.targetY ? document.y-dy : document.y+dy;
		    		}
		    		needRedraw = true;
		    	}
    		}, this);
    		
    		// now magnets
    		var i = 0, height = parseInt(this.textFont)+4;
	        ctx.textAlign = "center";
	        ctx.textBaseline="middle";
	        for (var term in this.magnets) {
	        	if (labels || term in hoveringTerms || this.magnets[term].isHovering) {
	    		    var width = this.magnets[term].width+4;
			        ctx.fillStyle = term in hoveringTerms || this.magnets[term].isHovering || noHovering===true ? "white" : "rgba(255,255,255,.05)";
	    		    ctx.fillRect(this.magnets[term].x-(width/2), this.magnets[term].y-(height/2), width, height);
			        ctx.strokeStyle = term in hoveringTerms || this.magnets[term].isHovering || noHovering===true ? "rgb("+this.magnets[term].colour.join(",")+")" : "rgba(0,0,0,.05)";
	    		    ctx.strokeRect(this.magnets[term].x-(width/2), this.magnets[term].y-(height/2), width, height);
			        ctx.textAlign = "center";
			        ctx.fillStyle = term in hoveringTerms || this.magnets[term].isHovering || noHovering===true ?"rgba(0,0,0,.8)" : "rgba(0,0,0,.05)";
	    		    ctx.fillText(term, this.magnets[term].x, this.magnets[term].y);
	        	} else {
			    	ctx.beginPath();
			        ctx.fillStyle = "rgb("+this.magnets[term].colour.join(",")+")"
			        ctx.strokeStyle = "rgb("+this.magnets[term].colour.join(",")+")"
			    	ctx.arc(this.magnets[term].x, this.magnets[term].y, 12, 0, 2 * Math.PI);
		        	ctx.fill();
			    	ctx.stroke();
	        	}
    		}
    	}
    	
		if (needRedraw) {
			var me = this;
			setTimeout(function() {
				me.draw();
			}, 100);
		} else if (this.documents) {
			var minDist = Math.max(radius/this.documents.length, 50), spring = .1
			for (var i=0, len=this.documents.length; i<len; i++) {
				for (var j=0; j<len; j++) {
					if (i<j) {
						
						var dx = this.documents[i].x  - this.documents[j].x,
							dy = this.documents[i].y - this.documents[j].y,
							dist = Math.sqrt(dx * dx + dy * dy);
						if (dist < minDist) {
							var ax = dx * spring, ay = dy * spring;
							this.documents[i].targetX += ax;
							this.documents[j].targetX -= ax;
							this.documents[i].targetY += ay;
							this.documents[j].targetY -= ay;
							needRedraw = true;
						}
					}
				}
			}
			if (needRedraw) {
				var me = this;
				setTimeout(function() {
					me.draw();
				}, 100);
			}
		}
    }
    
});