/*
 * Copyright 2014 Tom <tw201207@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
(function($) {

/**
 * Sets up elem as a slider; returns an access object. Elem must be positioned!
 * Note that the element may contain other elements; this is used for instance
 * for the image diff overlay slider.
 *
 * The styling of the slider is to be done in CSS. Currently recognized options:
 * - initial: <float> clipped to [0..1], default 0
 * - handleClass: <string> to assign to the handle span element created.
 * If no handleClass is specified, a very plain default style is assigned.
 */
function rangeSlider(elem, options) {
	options = $.extend({ initial : 0 }, options || {});
	options.initial = Math.min(1.0, Math.max(0, options.initial));
	
	var $elem = $(elem);
	var $handle = $('<span></span>').css({ position: 'absolute', left: 0, cursor: 'ew-resize' });
	var $root = $(document.documentElement);
	var $doc = $(document);	
	var lastRatio = options.initial;

	/** Mousemove event handler to track the mouse and move the slider. Generates slider:pos events. */
	function track(e) {
		var pos = $elem.offset().left;
		var width = $elem.innerWidth();
		var handleWidth = $handle.outerWidth(false);
		var range = width - handleWidth;
		if (range <= 0) return;
		var delta = Math.min(range, Math.max (0, e.pageX - pos - handleWidth / 2));
		lastRatio = delta / range;
		$handle.css('left', "" + (delta * 100 / width) + '%');
		$elem.trigger('slider:pos', { ratio: lastRatio, handle: $handle[0] });
	}

	/** Mouseup event handler to stop mouse tracking. */
	function end(e) {
		$doc.off('mousemove', track);
		$doc.off('mouseup', end);
		$root.removeClass('no-select');
	}

    /** Snaps the slider to the given ratio and generates a slider:pos event with the new ratio. */
	function setTo(ratio) {
		var w = $elem.innerWidth();
		if (w <= 0 || $elem.is(':hidden')) return;
		lastRatio = Math.min( 1.0, Math.max(0, ratio));
		$handle.css('left', "" + Math.max(0, 100 * (lastRatio * (w - $handle.outerWidth(false))) / w) + '%');
		$elem.trigger('slider:pos', { ratio: lastRatio, handle: $handle[0] });
	}
	
	/**
	 * Moves the slider to the given ratio, clipped to [0..1], in duration milliseconds.
	 * Generates slider:pos events during the animation. If duration <= 30, same as setTo.
	 * Default duration is 500ms. If a callback is given, it's called once the animation
	 * has completed.
	 */
	function moveTo(ratio, duration, callback) {
		ratio = Math.min(1.0, Math.max(0, ratio));
		if (ratio === lastRatio) {
			if (typeof callback == 'function') callback();
			return;
		}
		if (typeof duration == 'undefined') duration = 500;
		if (duration <= 30) {
			 // Cinema is 24 or 48 frames/sec, so 20-40ms per frame. Makes no sense to animate for such a short duration.
			setTo(ratio);
			if (typeof callback == 'function') callback();
		} else {
			var target = ratio * ($elem.innerWidth() - $handle.outerWidth(false));
			if (ratio > lastRatio) target--; else target++;
			$handle.stop().animate({left: target},
				{ 'duration' : duration,
				  'step' : function() {
						lastRatio = Math.min(1.0, Math.max(0, $handle.position().left / ($elem.innerWidth() - $handle.outerWidth(false))));
						$elem.trigger('slider:pos', { ratio : lastRatio, handle : $handle[0] });
					},
				  'complete' : function() { setTo(ratio); if (typeof callback == 'function') callback(); } // Ensure we have again a % value
				}
			);
		}
	}
	
	/**
	 * As moveTo, but determines an appropriate duration in the range [0..maxDuration] on its own,
	 * depending on the distance the handle would move. If no maxDuration is given it defaults
	 * to 1500ms.
	 */
	function moveAuto(ratio, maxDuration, callback) {
		if (typeof maxDuration == 'undefined') maxDuration = 1500;
		var delta = ratio - lastRatio;
		if (delta < 0) delta = -delta;
		var speed = $elem.innerWidth() * delta * 2;
		if (speed > maxDuration) speed = maxDuration;
		moveTo(ratio, speed, callback);
	}

	/** Returns the current ratio. */
	function getValue() {
		return lastRatio;
	}

	$elem.append($handle);
	if (options.handleClass) {
		$handle.addClass(options.handleClass);
	} else { // Provide a default style so that it is at least visible
		$handle.css({ width: '10px', height: '10px', background: 'white', border: '1px solid black' });
	}
	if (options.initial) setTo(options.initial);

	/** Install mousedown handler to start mouse tracking. */
	$handle.on('mousedown', function(e) {
		$root.addClass('no-select');
		$doc.on('mousemove', track);
		$doc.on('mouseup', end);
		e.stopPropagation();
		e.preventDefault();
	});

	return { setRatio: setTo, moveRatio: moveTo, 'moveAuto': moveAuto, getRatio: getValue, handle: $handle[0] };
}

function setup() {
	$('.imgdiff-container').each(function() {
		var $this = $(this);
		var $overlaySlider = $this.find('.imgdiff-ovr-slider').first();
		var $opacitySlider = $this.find('.imgdiff-opa-slider').first();
		var overlayAccess = rangeSlider($overlaySlider, {handleClass: 'imgdiff-ovr-handle'});
		var opacityAccess = rangeSlider($opacitySlider, {handleClass: 'imgdiff-opa-handle'});
		var $img = $('#' + this.id.substr(this.id.indexOf('-')+1)); // Here we change opacity
		var $div = $img.parent(); // This controls visibility: here we change width.
		var blinking = false;
		
		$overlaySlider.on('slider:pos', function(e, data) {
			var pos = $(data.handle).offset().left;
			var imgLeft = $img.offset().left; // Global
			var imgW = $img.outerWidth(true);
			var imgOff = $img.position().left; // From left edge of $div
			if (pos <= imgLeft) {
				$div.width(0);
			} else if (pos <= imgLeft + imgW) {
				$div.width(pos - imgLeft + imgOff);
			} else if ($div.width() < imgW + imgOff) {
				$div.width(imgW + imgOff);
			}
		});
		$overlaySlider.css('cursor', 'pointer');
		$overlaySlider.on('mousedown', function(e) {
			var newRatio = (e.pageX - $overlaySlider.offset().left) / $overlaySlider.innerWidth();
			var oldRatio = overlayAccess.getRatio();
			if (newRatio !== oldRatio) {
				overlayAccess.moveAuto(newRatio);
			}
		});
		
		var autoShowing = false;
		$opacitySlider.on('slider:pos', function(e, data) {
			if ($div.width() <= 0 && !blinking) {
				// Make old image visible in a nice way, *then* adjust opacity
				autoShowing = true;
				overlayAccess.moveAuto(1.0, 500, function() {
					$img.stop().animate(
						{opacity: 1.0 - opacityAccess.getRatio()},
						{duration: 400,
						 complete: function () {
							// In case the opacity handle was moved while we were trying to catch up
							$img.css('opacity', 1.0 - opacityAccess.getRatio());
							autoShowing = false;
						 }
						}
					);
				});
			} else if (!autoShowing) {
				$img.css('opacity', 1.0 - data.ratio);
			}
		});
		$opacitySlider.on('click', function(e) {
			var newRatio = (e.pageX - $opacitySlider.offset().left) / $opacitySlider.innerWidth();
			var oldRatio = opacityAccess.getRatio();
			if (newRatio !== oldRatio) {
				if ($div.width() <= 0) {
					overlayAccess.moveRatio(1.0, 500, function() {opacityAccess.moveAuto(newRatio);}); // Make old image visible in a nice way
				} else {
					opacityAccess.moveAuto(newRatio)
				}
			}
			e.preventDefault();
		});
			
		// Blinking before and after images is a good way for the human eye to catch differences.
		var $blinker = $this.find('.imgdiff-blink');
		var initialOpacity = null;
		$blinker.on('click', function(e) {
			if (blinking) {
				window.clearTimeout(blinking);
				$blinker.children('img').first().css('border', '1px solid transparent');
				opacityAccess.setRatio(initialOpacity);
				blinking = null;
			} else {
				$blinker.children('img').first().css('border', '1px solid #AAA');
				initialOpacity = opacityAccess.getRatio();
				var currentOpacity = 1.0;
				function blink() {
					opacityAccess.setRatio(currentOpacity);
					currentOpacity = 1.0 - currentOpacity;
					// Keep frequeny below 2Hz (i.e., delay above 500ms)
					blinking = window.setTimeout(blink, 600);
				}
				if ($div.width() <= 0) {
					overlayAccess.moveRatio(1.0, 500, blink);
				} else {
					blink();
				}
			}
			e.preventDefault();
		});
		
		// Subtracting before and after images is another good way to detect differences. Result will be
		// black where identical.		
		if (typeof $img[0].style.mixBlendMode != 'undefined') {
			// Feature test: does the browser support the mix-blend-mode CSS property from the Compositing 
			// and Blending Level 1 spec (http://dev.w3.org/fxtf/compositing-1/#mix-blend-mode )?
			// As of 2014-11, only Firefox >= 32 and Safari >= 7.1 support this. Other browsers will have to
			// make do with the blink comparator only.
			var $sub = $this.find('.imgdiff-subtract');
			$sub.css('display', 'inline-block');
			$sub.on('click', function (e) {
				var curr = $img.css('mix-blend-mode');
				if (curr != 'difference') {
					curr = 'difference';
					$sub.children('img').first().css('border', '1px solid #AAA');
					if ($div.width() <= 0) overlayAccess.moveRatio(1.0, 500);
					opacityAccess.setRatio(0);
				} else {
					curr = 'normal';
					$sub.children('img').first().css('border', '1px solid transparent');
					
				}
				$img.css('mix-blend-mode', curr);
				e.preventDefault();
			});
		}
	});
}

$(setup); // Run on jQuery's dom-ready

})(jQuery);