/*
 * Image preloader and magnifier
 * Copyright (C) 2010 H. Nelson
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program. If not, see <http://www.gnu.org/licenses/>
 */

// Refresh rate
var hideandseekTimeValue = 5;

// Number of iteration to perform a complete animation
var hideandseekAnimationValue = 13;

// Margin between the image and the inner border of the window
var hideandseekMargin = 50;

// Increase/decrease the velocity of the alpha animation
// Between 0. and infinity, 1 for a linear fading
var hideandseekOpacityFactor = 2;

loc = new String(document.location);
var path = (loc.match(/wb_admin/)) ? '../' : './';

// Path to the "click to close" image
// If you want none image, set it to null.
var hideandseekCloseImageSrc = path + 'wb_images/icons/close.png';

// Path to the image to be displayed when the user clicks on a
// thumbnail for which the correspondig full resolution image isn't
// loaded yet	
var hideandseekSpinnerSrc = path + 'wb_admin/images/loader.gif';

// CSS Shadow to be set on the full resolution image when it is
// becoming magnified
var hideandseekShadowStyle = '0 10px 30px rgba(0,0,0,.4)';

// Main class to call on each DOM node you want to click on, the image
// located at imageHref will be displayed with the label strToDisplay
function hideandseek (nodeToClick, imageHref, preload, strToDisplay) {
	
	// Set the new coordinates and dimensions to reach
	this.focus = function (e) {
		// We already wait for one image to be loaded
		if (window.hideandseekSpinner.waiting) {
			e.preventDefault();
			return 1;
		}
		
		// If the image isn't loaded yet
		if (bridge.fullWidth == 0) {
			// Show the spinner
			window.hideandseekSpinner.style.left = (window.innerWidth  - window.hideandseekSpinner.fullWidth)  / 2 + window.pageXOffset + 'px';
			window.hideandseekSpinner.style.top  = (window.innerHeight - window.hideandseekSpinner.fullHeight) / 2 + window.pageYOffset + 'px';
			window.hideandseekSpinner.style.display = 'block';
			
			// Wait for the full resolution image to load before we hide the spinner
			bridge.img.addEventListener('load', function () {
				window.hideandseekSpinner.style.display = 'none';
				window.hideandseekSpinner.waiting = false;
				bridge.focus(e);
				return 0;
			}, true);
			
			e.preventDefault();
			
			window.hideandseekSpinner.waiting = true;
			
			return 1;
		}
		
		bridge.focused = true;
		
		// Dimensions to reach are the image's one
		bridge.dow = bridge.fullWidth;
		bridge.doh = bridge.fullHeight;
		
		// Ratio reduced width / full width
		var ratio;
		
		// We don't want the image to exceed the window's width
		if (bridge.fullWidth + 2 * hideandseekMargin > window.innerWidth) {
			bridge.dow = window.innerWidth - 2 * hideandseekMargin;
			ratio = bridge.dow / bridge.fullWidth;
			bridge.doh *= ratio;
			bridge.reduced = true;
			bridge.img.style.cursor = '-moz-zoom-in';
		}
		
		// We don't want the image to exceed the window's height
		else if (bridge.fullHeight + 2 * hideandseekMargin > window.innerHeight) {
			bridge.doh = window.innerHeight - 2 * hideandseekMargin;
			ratio = bridge.doh / bridge.fullHeight;
			bridge.dow *= ratio;
			bridge.reduced = true;
			bridge.img.style.cursor = '-moz-zoom-in';
		}
		
		// Destination to reach
		// We want to *center* the container
		bridge.dox = (window.innerWidth  - bridge.dow) / 2 + window.pageXOffset;
		bridge.doy = (window.innerHeight - bridge.doh) / 2 + window.pageYOffset;
		
		// The distination coordinates are the same if the image is
		// reduced or not
		bridge.computeIncrements();
		
		// Set Z-index
		bridge.cnt.style.zIndex = '200';
		
		// We want the first step not to increment the coordinates
		// nor the dimensions
		bridge.increment = false;
		
		// Display the close image while zooming
		if (bridge.closeImage)
			bridge.closeImage.style.visibility = 'visible';
		
		// Start a new animation if necessary
		if (!bridge.interval)		
			bridge.interval = setInterval(function () {
				bridge.animate(true);
				return 0;
			}, hideandseekTimeValue);
		
		e.preventDefault();
		
		// Do not follow the link
		return 0;
	};
	
	// This function is called when the user clicks on the page
	this.onclickAnyWhere = function (e) {
		if (bridge.fullWidth == 0 || !bridge.focused)
			return 1;
			
		// Don't propagate the event and avoid a re-zoom comportement
		if (e.target == nodeToClick) {
			e.preventDefault();
			e.stopPropagation();
		}
		
		// The image is reduced, resize it to full size
		if (bridge.reduced && e.target == bridge.img) {
			bridge.dow = bridge.fullWidth;
			bridge.doh = bridge.fullHeight;
			bridge.dox = (window.innerWidth  - bridge.dow)  / 2 + window.pageXOffset;
			bridge.doy = (window.innerHeight - bridge.doh)  / 2 + window.pageYOffset;

			bridge.computeIncrements(3);

			bridge.reduced = false;
			bridge.img.style.cursor = 'default';

			// Init a new animation if necessary
			if (!bridge.interval)
				bridge.interval = setInterval(function () {
					bridge.animate(false);
					return 0;
				}, hideandseekTimeValue);
		}
		// Unfocus the instance
		else {
			// Hide the "close" focus
			bridge.focused = false;
		
			// Compute the clickable node's coordinates
			bridge.dox = 0;
			bridge.doy = 0;
			var el = nodeToClick;
			
			do {
				bridge.dox += el.offsetLeft;
				bridge.doy += el.offsetTop;
				el = el.offsetParent;
			}
			while (el && el.tagName != 'HTML');
			
			// Final dimensions of the preview
			// are the clickable element's ones
			bridge.dow = nodeToClick.offsetWidth;
			bridge.doh = nodeToClick.offsetHeight;

			bridge.computeIncrements();

			// Set Z-index
			bridge.cnt.style.zIndex = '100';
			
			// We want the first step not to be incremented
			// only displayed
			bridge.increment = false;

			// Hide the close image while minimizing
			bridge.closeImage.style.visibility = 'hidden';
			
			// Init a new animation if necessary
			if (!bridge.interval)
				bridge.interval = setInterval(function () {
					bridge.animate(true);
					return 0;
				}, hideandseekTimeValue);
		}
		return 0;
	};
	
	// Perform one step of animation
	this.animate = function (useEffects) {
		// First call, only displays, do not increment
		if (!bridge.increment) {
			// Hide the shadow while minimizing for performance issues.
			bridge.cnt.style.boxShadow = 'none';
			bridge.cnt.style.WebkitBoxShadow = 'none';
			bridge.cnt.style.MozBoxShadow = 'none';

			bridge.cnt.style.display = 'block';
			
			bridge.originalDistance = Math.sqrt(
				Math.pow(bridge.dox - bridge.cox, 2) +
				Math.pow(bridge.doy - bridge.coy, 2)
			);
			
			// Will animate on the next iteration
			bridge.increment = true;
		}
		// We can increments values now
		else {
			// X axis
			// The destination is on the right
			if ((bridge.cox <= bridge.dox &&
			    // Will exceed the coordinates
			    bridge.cox + bridge.incx > bridge.dox) ||
			// The destination is on the left
			    (bridge.cox >= bridge.dox &&
			    // Will exceed the coordinates
			     bridge.cox + bridge.incx < bridge.dox)) {
			    // Then directly set the final coordinates
				bridge.cox = bridge.dox;
			}
			// No problem, we just increment
			else
				bridge.cox += bridge.incx;

			// Y axis
			// The destination is under
			if ((bridge.coy <= bridge.doy &&
			    // Will exceed the coordinates
			    bridge.coy + bridge.incy > bridge.doy) ||
			// The destination is above
			    (bridge.coy >= bridge.doy &&
			    // Will exceed the coordinates
			     bridge.coy + bridge.incy < bridge.doy)) {
				// Then directly set the final coordinates
				bridge.coy = bridge.doy;
			}
			// No problem, we just increment
			else
				bridge.coy += bridge.incy;
			
			// Width
			// The destination is bigger
			if ((bridge.cow <= bridge.dow &&
			    // Will exceed the width
			    bridge.cow + bridge.incw > bridge.dow) ||
			// The destination is smaller
			    (bridge.cow >= bridge.dow &&
			    // Will exceed the width
			     bridge.cow + bridge.incw < bridge.dow)) {
				// Then directly set the final width
				bridge.cow = bridge.dow;
			}
			// No problem, we just increment
			else
				bridge.cow += bridge.incw;
			
			// Height
			// The destination is bigger
			if ((bridge.coh <= bridge.doh &&
			    // Will exceed the height
			    bridge.coh + bridge.inch > bridge.doh) ||
			// The destination is smaller
			    (bridge.coh >= bridge.doh &&
			    // Will exceed the height
			     bridge.coh + bridge.inch < bridge.doh)) {
				// Then directly set the final coordinates
				bridge.coh = bridge.doh;
			}
			// No problem, we just increment
			else
				bridge.coh += bridge.inch;
		}

		// All destination offsets/dimensions have been reached
		if (bridge.cox == bridge.dox &&
		    bridge.coy == bridge.doy &&
		    bridge.cow == bridge.dow &&
		    bridge.coh == bridge.doh)
		{
		   	 // We can clear the interval
			clearInterval(bridge.interval);
			bridge.interval = null;
			
			// Display the shadow if we are focusing
			if (bridge.focused) {
				bridge.cnt.style.boxShadow = hideandseekShadowStyle;
				bridge.cnt.style.WebkitBoxShadow = hideandseekShadowStyle;
				bridge.cnt.style.MozBoxShadow = hideandseekShadowStyle;
			}
			// Remove the container from the DOM flow else
			else
				bridge.cnt.style.display = 'none';
		}
		
		// Render positions/dimensions via CSS
		bridge.cnt.style.top  = Math.round(bridge.coy) + 'px';
		bridge.cnt.style.left = Math.round(bridge.cox) + 'px';
		
		bridge.cnt.style.height = Math.round(bridge.coh) + 'px';
		bridge.cnt.style.width  = Math.round(bridge.cow) + 'px';

		// Compute the ratio from the current distance to the destination
		var ratio = 1;
		
		var distance = Math.sqrt(
			Math.pow(bridge.dox - bridge.cox, 2) +
			Math.pow(bridge.doy - bridge.coy, 2)
		);
		
		// The alpha factor is function of the distance traveled
		// But we can't divide by zero
		if (bridge.originalDistance > 0.) {
			// We want the ratio of advancement of the animation
			ratio = Math.round(1000 * distance / bridge.originalDistance) / 1000;
			
			// If we are focusing we want the reverse the ratio
			if (bridge.focused)
				ratio = 1 - ratio;
		}
		
		// Set the alpha factor on the image and adjusts the size of the
		// font for the label
		if (useEffects) {
			// Alpha
			bridge.cnt.style.opacity = Math.round(1000 * Math.pow(ratio, hideandseekOpacityFactor)) / 1000;
			// Font size
			if (bridge.textBox)
				bridge.textBox.style.fontSize = ratio + 'em';
		}
		else {
			bridge.cnt.style.opacity = 1;
			if (bridge.textBox)
				bridge.textBox.style.fontSize = '1em';
		}

		return 0;
	};
	
	// Compute the increments values from the current set of coordinates
	// and dimensions
	this.computeIncrements = function (factor) {
		if (!factor)
			factor = 1;
		
		this.incx = factor * (this.dox - this.cox) / hideandseekAnimationValue;
		this.incy = factor * (this.doy - this.coy) / hideandseekAnimationValue;
		this.incw = factor * (this.dow - this.cow) / hideandseekAnimationValue;
		this.inch = factor * (this.doh - this.coh) / hideandseekAnimationValue;
		return 0;
	};
	
	// Fix the coordinates of the containers while resizing
	this.fixResizing = function () {
		// The container is not focused
		if (!bridge.focused) {
			// Compute the new coordinates of the clickable node
			bridge.cox = 0;
			bridge.coy = 0;
			var el = nodeToClick;
			do {
				bridge.cox += el.offsetLeft;
				bridge.coy += el.offsetTop;
				el = el.offsetParent;
			}
			while (el && el.tagName != 'HTML');
		}
		
		return 0;
	};

	this.loadFullResImage = function () {
		if (bridge.img.src != imageHref)
			bridge.img.src = imageHref;
		return 0;	
	};

	///////////////////////
	// INSTANCE VARIABLES
	///////////////////////
	
	this.interval = null;
	this.el = nodeToClick;
	
	// Global reference for this instance, used to travel through DOM
	// event listeners
	var bridge = this;

	// Define the coordinates of the container
	this.cox = 0;
	this.coy = 0;
	var el = nodeToClick;
	do {
		this.cox += el.offsetLeft;
		this.coy += el.offsetTop;
		el = el.offsetParent;
	}
	while (el && el.tagName != 'HTML');
	
	// ... and the dimensions
	this.cow = nodeToClick.offsetWidth;
	this.coh = nodeToClick.offsetHeight;
	
	// Destination is still unknown
	this.dox = 0;
	this.doy = 0;
	this.dow = 0;
	this.doh = 0;
	
	// Increments are useless for the moment
	this.incx = 0;
	this.incy = 0;
	this.incw = 0;
	this.inch = 0;

	// None instance is focused nor crooped by default
	this.focused = false;
	this.reduced = false;
	
	// Create the DIV container
	this.cnt = document.createElement('DIV');
	
	this.cnt.style.position = 'absolute';
	this.cnt.style.display = 'none';
	this.cnt.style.textAlign = 'center';
	this.cnt.style.backgroundColor = 'transparent';
	
	// Append the container to the body
	document.body.appendChild(this.cnt);
	
	// Create the image that will be resized by the browser
	this.img = document.createElement('IMG');
	
	// Wait for the image to be fully loaded to grab its dimensions
	this.fullWidth  = 0;
	this.fullHeight = 0;
	
	// Bind methods to events
	nodeToClick.addEventListener('click', bridge.focus, true);
	document.body.addEventListener('click', bridge.onclickAnyWhere, true);
	window.addEventListener('resize', bridge.fixResizing, true);
	
	this.img.addEventListener('load', function () {
		// Keep the real dimensions of the image
		bridge.fullWidth  = this.width;
		bridge.fullHeight = this.height;
		// Remove event listeners
		if (!preload) {
			nodeToClick.removeEventListener('mouseover', bridge.loadFullResImage, true);
			nodeToClick.removeEventListener('focus', bridge.loadFullResImage, true);
		}
		return 0;
	}, true);
	
	// Load the image directly
	if (preload)
		this.img.src = imageHref;
	// Load the image when the user mouseovers the node to click on
	// But also onfocus, for mobile devices and accessibility
	else {
		nodeToClick.addEventListener('mouseover', bridge.loadFullResImage, true);
		nodeToClick.addEventListener('focus', bridge.loadFullResImage, true);
	}
	
	// Auto resize it via CSS
	this.img.style.width  = '100%';
	this.img.style.height = '100%';

	this.img.style.cursor = 'pointer';
	
	this.closeImage = null;
	
	// Little close box
	this.closeImage = document.createElement('IMG');
	
	this.closeImage.style.visibility = 'hidden';
	this.closeImage.style.position = 'absolute';
		
	// Center back the image when it's loaded
	this.closeImage.addEventListener('load', function () {
		this.style.left = '-' + Math.round(this.width  / 2) + 'px';
		this.style.top  = '-' + Math.round(this.height / 2) + 'px';
		this.style.visibility = 'visible';
		return 0;
	}, true);

	this.closeImage.src = hideandseekCloseImageSrc;

	// Append the button to the container
	this.cnt.appendChild(this.closeImage);

	// Append the image to the container
	this.cnt.appendChild(this.img);
		
	// Create the text box if necessary
	this.textBox = null;
	
	if (strToDisplay) {
		this.textBox = document.createElement('SPAN');
		// Apply styles to the SPAN element
		this.textBox.style.display = 'inline-block';
		this.textBox.style.padding = '3px 20px';
		this.textBox.style.marginTop = '15px';
		this.textBox.style.color = 'white';
		this.textBox.style.backgroundColor = 'rgba(0,0,0,.7)';

		// Border radius for the text box
		this.textBox.style.borderRadius = '10px';
		this.textBox.style.MozBorderRadius = '10px';
		this.textBox.style.WebkitBorderRadius = '10px';
		
		this.cnt.appendChild(this.textBox);
		this.textBox.appendChild(document.createTextNode(strToDisplay));
	}
	
	return 0;
};

// We want a unique spinner for all the instances of the class
window.hideandseekSpinner = document.createElement('IMG');

window.hideandseekSpinner.src = hideandseekSpinnerSrc;

window.hideandseekSpinner.style.visibility = 'hidden';
window.hideandseekSpinner.style.position = 'absolute';

window.hideandseekSpinner.fullWidth  = 0;
window.hideandseekSpinner.fullHeight = 0;

window.hideandseekSpinner.waiting = false;

// Wait for the spin image to be loaded
//window.hideandseekSpinner.addEventListener('load', function () {
window.load = function () {
	this.fullWidth  = this.width;
	this.fullHeight = this.height;
	
	this.style.display = 'none';
	this.style.visibility = 'visible';
	
	document.body.appendChild(window.hideandseekSpinner);
	
	return 0;
};//, true);
