/**selectable.js
 * Depends on: 
 * 	builder.js (from script.aculo.us)
 * 	prototype.js (from prototype.conio.net)
 * 
 * Selectable is a library for displaying and editing selection markers, or placements,
 * on an image.  There are two namespaces (and classes) defined by the library:
 * 
 * Selectable: represents the image upon which placements are displayed/edited.
 * Selected: represents each placement.
 */
var Selected = Class.create();
Selected.INITIALIZED = 0;
Selected.REGISTERED = 2;
Selected.IDLE = 4; /* Must have screen-based coordinates before transitioning to this phase */
Selected.MOVE_PENDING = 5;
Selected.MOVING = 6;
Selected.SIZE_PENDING = 7;
Selected.SIZING = 8;
Selected.DESELECTED = 0;
Selected.SELECTED = 1;
Selected.DESELECTION_PENDING = 2;

Selected.register = function(id, selectableID, pUL, pLR, lbl) {
	var selectable = Selectable.findByID(selectableID);
	var s =	new Selected(id, selectable);
	selectable.selecteds.set(id, s);
	s.selUL = pUL;
	s.selLR = pLR;
	s.state = Selected.REGISTERED;
	s.setLabel(lbl)
	return s;
};

Selected.prototype = {
	initialize: function(id, selectable) {
		this.id = id || 'new';
		this.selUL = [];
		this.selLR = [];
		this.anchorPoint = [];
		this.selPoint2 = [];
		this.deselect_pending = false;
		this.selected = false;
		this.state = Selected.INITIALIZED;
		this.selectable = selectable;
		/* Define event handlers *by name* to allow stopObserving to de-register */
		this.mouseDownHandler = this.mouseDown.bindAsEventListener(this);
		this.mouseUpHandler = this.mouseUp.bindAsEventListener(this);
		this.mouseDragHandler = this.mouseDrag.bindAsEventListener(this);
		this.stopEventHandler = this.stopEvent.bindAsEventListener(this);
		
		var cssPrefix = this.selectable.options.cssPrefix;
		this.handles = [];
		if (!this.selectable.options.readOnly) {
			this.handleN	= Builder.node( 'div', { 'id':'N', 'class': cssPrefix + 'handle' } );
			this.handleNE	= Builder.node( 'div', { 'id':'NE', 'class': cssPrefix + 'handle' } );
			this.handleE	= Builder.node( 'div', { 'id':'E', 'class': cssPrefix + 'handle' } );
			this.handleSE	= Builder.node( 'div', { 'id':'SE', 'class': cssPrefix + 'handle' } );
			this.handleS	= Builder.node( 'div', { 'id':'S', 'class': cssPrefix + 'handle' } );
			this.handleSW	= Builder.node( 'div', { 'id':'SW', 'class': cssPrefix + 'handle' } );
			this.handleW	= Builder.node( 'div', { 'id':'W', 'class': cssPrefix + 'handle' } );
			this.handleNW	= Builder.node( 'div', { 'id':'NW', 'class': cssPrefix + 'handle' } );
			this.handles = [this.handleN,this.handleNE,this.handleE,this.handleSE,this.handleS,this.handleSW,this.handleW,this.handleNW];
			var handleHandler = this.mouseDownHandler;
			this.handles.each(function(h) {	
					Event.observe(h, 'mousedown', handleHandler, false);
				}
			);
		}
		this.element = Builder.node('div', {'id': cssPrefix + this.id, 'class': cssPrefix + 'selected'}, this.handles);
		this.selectable.overlay.appendChild(this.element);
		this.labelElement = this.element.appendChild(Builder.node('p', {id: (cssPrefix + this.id +'_label')}, ""));
		Element.hide(this.element);
	},
	
	toString: function() {
		return this.id.toString();
	},
	
	stopEvent: function(e) {
		Event.stop(e);
	},
	
	link: function(id, lbl) {
		this.id = id;
		this.element.id = this.selectable.options.cssPrefix + this.id;
		this.selectable.selecteds.set(this.id, this);
		this.setLabel(lbl)
		this.selectable.selecteds.unset('');
		this.selectable.activeSelected = null;
	},
	
	setLabel: function(lbl) {
		this.labelElement.innerHTML = lbl;
	},

	activate: function(){
		Element.show(this.element);
		this.state = Selected.IDLE;
		Event.observe(this.element, 'mousedown', this.mouseDownHandler, false);
		Event.observe(this.element, 'mouseover', this.stopEventHandler, false);
		Event.observe(this.element, 'mouseout', this.stopEventHandler, false);
	},

	deactivate: function(deselect){
		if (deselect && this.selected) { this.deselect(true); }
		if (!this.selected) {Element.hide(this.element);}
		if (this.state > Selected.IDLE) {this.state = Selected.IDLE;}
		Event.stopObserving(this.element, 'mousedown', this.mouseDownHandler, false);
		Event.stopObserving(this.element, 'mouseover', this.stopEventHandler, false);
		Event.stopObserving(this.element, 'mouseout', this.stopEventHandler, false);
	},

	position: function(pA, pB) { /* second argument is optional.  If missing, update this.anchorPoint */
		if (pB) {
			/* Squirrel away the first point to short-circuit subsequent updates from intitial drag-build. */
			this.anchorPoint = pB;
			this.boundPosition(this.anchorPoint);
		}
		this.boundPosition(pA);
		this.selUL[0] = Math.min(this.anchorPoint[0], pA[0]);
		this.selUL[1] = Math.min(this.anchorPoint[1], pA[1]);
		this.selLR[0] = Math.max(this.anchorPoint[0], pA[0]);
		this.selLR[1] = Math.max(this.anchorPoint[1], pA[1]);

		var w = this.selLR[0] - this.selUL[0];
		var h = this.selLR[1] - this.selUL[1];

		this.element.style.left = (this.selUL[0] + 'px');
		this.element.style.top = (this.selUL[1] + 'px');
		this.element.style.width = (w + 'px');
		this.element.style.height = (h + 'px');
	},
	
	boundPosition: function(p) {
		p[0] = Math.max(Math.min(p[0], this.selectable.canvasDimensions[0] - 1), 0);
		p[1] = Math.max(Math.min(p[1], this.selectable.canvasDimensions[1] - 1), 0);
	},

	resize: function(factorX, factorY) {
		var newUL = [this.selUL[0]*factorX, this.selUL[1]*factorY];
		var newLR = [this.selLR[0]*factorX, this.selLR[1]*factorY];
		this.position(newUL, newLR);
	},

	select: function(callback) {
		Element.addClassName(this.element, this.selectable.options.cssPrefix + 'selectedSelected');
		this.selected = true;
		if (callback) { this.selectable.options.onSelectSelected(this);}
	},
	
	deselect: function(callback) {
		Element.removeClassName(this.element, this.selectable.options.cssPrefix + 'selectedSelected');
		this.selected = false;
		if (callback) {this.selectable.options.onDeselectSelected(this);}
	},
	
	destroy: function() {
		Element.remove(this.element);
		this.selectable.selecteds.unset(this.id);
	},
	
	mouseDown: function(e) {
		el = Event.element(e);
		if ((el != this.element) && (!Element.hasClassName(el, this.selectable.options.cssPrefix + 'handle'))) return;
		if (this.selected) {
			this.deselect_pending = true;
		} else {
			this.deselect_pending = false;
			this.selectable.selecteds.each(function(s) {
				if (s.value.selected) {
					s.value.deselect(true);
				}
			});
		};
		switch(this.state) {
			case(Selected.IDLE) :
				this.dragReference = this.selectable.canvasPointer(e);
				if (Element.hasClassName(Event.element(e), this.selectable.options.cssPrefix + 'handle')) {
					this.state = Selected.SIZE_PENDING;
					this.sizing = Event.element(e).id;
					switch(this.sizing) {
						case('NW')	:
						case('N')	:
							this.anchorPoint = this.selLR;
							break;
						case('NE')	:
						case('E')	:
							this.anchorPoint = [this.selUL[0], this.selLR[1]];
							break;						
						case('SE')	:
						case('S')	:
							this.anchorPoint = this.selUL;
							break;						
						case('SW')	:
						case('W')	:
							this.anchorPoint = [this.selLR[0], this.selUL[1]];
							break;						
					}
				} else {
					this.state = Selected.MOVE_PENDING;					
				}
				Event.observe(this.selectable.overlay, 'mouseup', this.mouseUpHandler, false);
				if (!this.selectable.options.readOnly) {Event.observe(this.selectable.overlay, 'mousemove', this.mouseDragHandler, false);}
				break;
		}
		// Stop event from bubbling up and starting another drag.
    	Event.stop(e);
	},
	
	mouseDrag: function(e) {
		var newReference = this.selectable.canvasPointer(e);
		var delta = [this.dragReference[0] - newReference[0], this.dragReference[1] - newReference[1]];
		var distance = Math.sqrt(delta[0]*delta[0] + delta[1]*delta[1]);
		if (distance == 0) return;
		this.deselect_pending = false;
		this.dragReference = newReference;
		switch(this.state) {
			case(Selected.IDLE)		:
				log.error('IDLE in drag');
				break;
			case(Selected.SIZE_PENDING)		:
				this.state = Selected.SIZING;
			case(Selected.SIZING)	:
				var newP;
				switch(this.sizing) {
					case('NW')	:
						newP = [this.selUL[0] - delta[0], this.selUL[1] - delta[1]];
						break;
					case('N')	:
						newP = [this.selUL[0], this.selUL[1] - delta[1]];
						break;
					case('NE')	:
						newP = [this.selLR[0] - delta[0], this.selUL[1] - delta[1]];
						break;
					case('E')	:
						newP = [this.selLR[0] - delta[0], this.selUL[1]];
						break;
					case('SE')	:
						newP = [this.selLR[0] - delta[0], this.selLR[1] - delta[1]];
						break;
					case('S')	:
						newP = [this.selLR[0], this.selLR[1] - delta[1]];
						break;
					case('SW')	:
						newP = [this.selUL[0] - delta[0], this.selLR[1] - delta[1]];
						break;
					case('W')	:
						newP = [this.selUL[0] - delta[0], this.selLR[1]];
						break;
				}
				this.position(newP);
				break;
			case(Selected.MOVE_PENDING)		:
				this.state = Selected.MOVING;
			case(Selected.MOVING)	:
				var newUL = [this.selUL[0] - delta[0], this.selUL[1] - delta[1]];
				var newLR = [this.selLR[0] - delta[0], this.selLR[1] - delta[1]];
				this.position(newUL, newLR);
				break;
		}	
    	Event.stop(e);
	},
	
	mouseUp: function(e) {
		el = Event.element(e);
		if ((el != this.element) && (!Element.hasClassName(el, this.selectable.options.cssPrefix + 'handle'))) return;
		if (this.deselect_pending) {
			this.deselect(true);
		} else {
			this.select(true);
		}
		switch(this.state) {
			case(Selected.IDLE) :
				log.error('IDLE at mouse up');
				break;
			case(Selected.MOVING) :
			case(Selected.SIZING) :
				this.selectable.options.onUpdateSelected(this);
			case(Selected.SIZE_PENDING) :
			case(Selected.MOVE_PENDING) :
				this.state = Selected.IDLE;
				Event.stopObserving(this.selectable.overlay, 'mouseup', this.mouseUpHandler);
				Event.stopObserving(this.selectable.overlay, 'mousemove', this.mouseDragHandler);
				break;
		}
		// Stop event from bubbling up and starting another drag.
    	Event.stop(e);
	}
};

var Selectable = Class.create();
Selectable.selectables = $H({});
Selectable.findByID = function(id) {
	return Selectable.selectables.get(id);
};

Selectable.IDLE = 0;
Selectable.SELECTING = 1;

Selectable.prototype = {
	initialize: function(id, options) {
		this.options = Object.extend(
			{
				// * @var string * the path of the stylesheet to dynamically load */
				stylesheet: '/stylesheets/selectable.css',
				// prefix for CSS class names
				cssPrefix: 'sel_',
				// can new selecteds be created or existing selecteds edited? */
				readOnly: false,
				// activate/deactivate selecteds on mouseover/mouseout */
				autoActivate: true,
				// callback when a new selected is created */				
				onCreateSelected: Prototype.emptyFunction,
				// callback when an existing selected is edited */
				onUpdateSelected: Prototype.emptyFunction,
				// callback when an existing selected is deleted */
				onSelectSelected: Prototype.emptyFunction
			}, 
			options || {}
		);
		
		this.img = $(id);	/* @var obj * The img node to attach to */
		this.selecteds = $H({});	/* array of child selecteds */
		this.state = Selectable.IDLE;	/* State machine starting point */
		this.canvasDimensions = [1.0, 1.0];
		/** fetch the stylesheet */
		var stylesheet = Builder.node('link',
			{
				'href': this.options.stylesheet,
				'rel': 'stylesheet',
				'type': 'text/css',
				'media': 'screen'
			}
		);
		document.getElementsByTagName('head')[0].appendChild(stylesheet);

		/** Whether the user is on a webKit browser */
		this.isWebKit = /Konqueror|Safari|KHTML/.test( navigator.userAgent );
		/** Whether the user is on IE */
		this.isIE = /MSIE/.test( navigator.userAgent );
		/** Whether the user is on Opera below version 9 */
		this.isOpera8 = /Opera\s[1-8]/.test( navigator.userAgent );
		
		Element.addClassName(this.img, this.options.cssPrefix + 'img');

		/**Create an overlay for managing events in the same screen geography as the image
		 * The overlay is simply a div positioned exactly on top of the image.  The overlay
		 * contains a div for each selected and can thus subscribe to their 'bubbled' up events.
		 * It is inserted at the same parent as the image itself.  No changes to the parentage of
		 * image are made.
		 */
		this.overlay = Builder.node('div',
			{
				'class': this.options.cssPrefix + 'overlay',
				'id': this.options.cssPrefix + 'overlay'
			}
		);
		this.overlay = Element.extend(this.overlay);

		/* Define event handlers *by name* to allow stopObserving to de-register */
		this.mouseDownHandler = this.mouseDown.bindAsEventListener(this);
		this.mouseUpHandler = this.mouseUp.bindAsEventListener(this);
		this.mouseMoveHandler = this.mouseMove.bindAsEventListener(this);
		this.mouseOutHandler = this.mouseOut.bindAsEventListener(this);
		this.mouseOverHandler = this.mouseOver.bindAsEventListener(this);
		this.resizeHandler = this.resize.bindAsEventListener(this);

		/* Register this Selectable by the image id */
		Selectable.selectables.set(this.img.id, this);

		/* Load the event observers etc. when everything has stabilized. */
		Event.observe(window, 'load', this.load.bindAsEventListener(this));
		return this;
	},
	
	load: function() {
		this.overlay = this.img.offsetParent.appendChild(this.overlay);
		this.resize();
		/* Now that they are resized, REGISTERED selected are promoted... */
		this.selecteds.each(function(s) {s.value.state = Selected.IDLE;});
		// add event observers
		Event.observe(window, 'resize', this.resizeHandler);
		if (this.options.autoActivate) {Event.observe(this.overlay, 'mouseover', this.mouseOverHandler);}
	},
	
	unload: function() {
		if (this.options.autoActivate) {Event.stopObserving(this.overlay, 'mouseover', this.mouseOverHandler);}
		Event.stopObserving(window, 'resize', this.resizeHandler);		
		this.overlay.remove();
	},
	
	activate: function() {
		this.selecteds.each(function(s) { s.value.activate(); });
		if (!this.options.readOnly) {Event.observe(this.overlay, 'mousedown', this.mouseDownHandler, false);}
		this.overlay.style.cursor = "crosshair";
	},
	
	deactivate: function(deselect) {
		this.overlay.style.cursor = "default";		
		if (!this.options.readOnly) {Event.stopObserving(this.overlay, 'mousedown', this.mouseDownHandler);}		
		this.selecteds.each(function(s) { s.value.deactivate(deselect); });
	},
	
	mouseDown: function(e) {
//		log.debug('down on selectable');
		if (Event.element(e) != this.overlay) return;
		switch(this.state) {
			case(Selectable.IDLE) :
				if (!this.activeSelected) { 
					this.activeSelected = new Selected(null, this);
					this.selecteds.set('', this.activeSelected);
				}
				var p = this.canvasPointer(e);
				this.activeSelected.position(p, p);
				this.state = Selectable.SELECTING;
				this.activeSelected.activate();  /* Don't activate too early or IE will trigger a resize before we are ready */
				this.selecteds.each(function(s) {
						if (s.value.selected) {
							s.value.deselect(true);
						}
					}
				);
				Event.observe(this.overlay, 'mouseup', this.mouseUpHandler, false);
				Event.observe(this.overlay, 'mousemove', this.mouseMoveHandler, false);
				break;
		}
		// Stop even from bubbling up to where Firefox starts an impossible drag.
    	Event.stop(e);
	},
	
	mouseUp: function(e) {
		switch(this.state) {
			case(Selectable.SELECTING) :
				this.activeSelected.select(false);
				this.activeSelected.position(this.canvasPointer(e));
				this.state = Selectable.IDLE;
				Event.stopObserving(this.overlay, 'mouseup', this.mouseUpHandler);
				Event.stopObserving(this.overlay, 'mousemove', this.mouseMoveHandler);
				this.options.onCreateSelected(this.activeSelected);
				break;
		}
	},
	
	mouseOver: function(e) {
		var te = this.isIE? e.toElement : e.target;
		if (!(te && Element.childOf(te, this.overlay))) {
			this.activate();
			Event.stopObserving(this.overlay, 'mouseover', this.mouseOverHandler);
			Event.observe(this.overlay, 'mouseout', this.mouseOutHandler);
		}
		Event.stop(e);
	},
	
	mouseOut: function(e) {
		var te = this.isIE? e.toElement : e.relatedTarget;
//		var fe = this.isIE? e.fromElement : e.Target;
//		var pe = Event.element(e);
//		feID = fe? (Element.childOf(fe, this.overlay)? 'child' : (fe.id? fe.id :'unknown')) : 'null';
//		teID = te? (Element.childOf(te, this.overlay)? 'child' : (te.id? te.id :'unknown')) : 'null';
//		peID = pe? (Element.childOf(pe, this.overlay)? 'child' : (pe.id? pe.id :'unknown')) : 'null';
//		log.debug(e.type + ' ' + feID + '->' + teID + ' [' + peID + ']');
		if (!(te && Element.childOf(te, this.overlay))) {
			switch(this.state) {
				case(Selectable.SELECTING) :
					this.mouseUp(e);
					break;
			}
			this.deactivate();
			Event.stopObserving(this.overlay, 'mouseout', this.mouseOutHandler);
			Event.observe(this.overlay, 'mouseover', this.mouseOverHandler);
		}
		Event.stop(e);
	},
	
	mouseMove: function(e) {
		switch(this.state) {
			case(Selectable.SELECTING) :
				this.activeSelected.position(this.canvasPointer(e));
				break;
		}
	},
	
	resize: function(e) {
		/* Position the overlay (absolutely) within its container */
		this.overlay.style.left = Position.positionedOffset(this.img)[0] + 'px';
		this.overlay.style.top = Position.positionedOffset(this.img)[1] + 'px';
		this.overlay.style.width = this.img.width + 'px';
		this.overlay.style.height = this.img.height + 'px';

		/**Determine the canvas position for translating mouse clicks.  It needs to be calculated 
		 * late to support fluid pages and resizing.
		 */
		this.canvasUL = [Position.cumulativeOffset(this.img)[0], Position.cumulativeOffset(this.img)[1]];
		var factorX = this.img.width/this.canvasDimensions[0];
		var factorY = this.img.height/this.canvasDimensions[1];
		this.canvasDimensions = [this.img.width, this.img.height];
		this.selecteds.each(function(s) {
				/* rescale selecteds to actual canvas size */
				s.value.resize(factorX, factorY);
			}
		);
	},
	
	/** Extracts pointer coordinates from event and translates them into 
	 * canvas-relative coordinates.
	 */
	canvasPointer: function(e) {
		return [Event.pointerX(e) - this.canvasUL[0], Event.pointerY(e) - this.canvasUL[1]];
	},
	
	toString: function() {
		return this.img.id;
	},
	
	destroy: function() {
		this.selecteds.each(function(s) {s.value.destroy();});
		Element.remove(this.overlay);
		Selectable.selectables.unset(this.img.id);
	},

	findSelectedByID: function(id) {
		return this.selecteds.get(id);
	}
};
