﻿// Slide.Show, version 1.0
// Copyright © Vertigo Software, Inc.
// This source is subject to the Microsoft Public License (Ms-PL).
// See http://www.microsoft.com/resources/sharedsource/licensingbasics/publiclicense.mspx.
// All other rights reserved.

/// <reference path="SlideShow.js" />

/*******************************************
 * class: SlideShow.SlideViewer
 *******************************************/
SlideShow.SlideViewer = function(control, parent, options)
{
	/// <summary>Displays slide images.</summary>
	/// <param name="control">The Slide.Show control.</param>
	/// <param name="parent">The parent control.</param>
	/// <param name="options">The options for the control.</param>
	
	if (options.cacheWindowSize < 3)
		throw new Error("Invalid option: cacheWindowSize (must be at least 3)");
	
	var xaml = '<Canvas xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Name="SlideViewer" Visibility="Collapsed"><Canvas.Clip><RectangleGeometry x:Name="TransitionClip" /></Canvas.Clip><Image x:Name="BufferImage" Visibility="Collapsed" /></Canvas>';
	
	SlideShow.SlideViewer.base.constructor.call(this, control, parent, xaml);
	
	SlideShow.merge(this.options,
	{
		top: 0,
		left: 0,
		right: 0,
		bottom: 42,
		cacheWindowSize: 3,
		slideImage: {}
	});
	
	this.setOptions(options);
	
	this.cache = [];
	this.allowSlideChange = false;
	this.transitionNextImage = true;
	this.transitionClip = this.root.findName("TransitionClip");
	this.bufferImage = this.root.findName("BufferImage");
	
	this.control.addEventListener("dataLoad", SlideShow.createDelegate(this, this.onControlDataLoad));
	this.bufferImage.addEventListener("DownloadProgressChanged", SlideShow.createDelegate(this, this.onBufferImageDownloadProgressChanged));
	this.bufferImage.addEventListener("ImageFailed", SlideShow.createDelegate(this, this.onBufferImageDownloadFailed));
};

SlideShow.extend(SlideShow.UserControl, SlideShow.SlideViewer,
{	
	loadCache: function()
	{
		/// <summary>Clears the image cache and loads it with new images.</summary>
		
		for (var i = this.cache.length - 1; i >= 0; i--)
			this.removeCacheImage(i);
		
		this.cache = [];
		this.cacheSize = Math.floor(this.options.cacheWindowSize / 2);
	
		for (var offset = -this.cacheSize; offset < this.cacheSize; offset++)
		{
			var dataIndex = this.getDataIndexByOffset(offset);
			
			if (dataIndex != null)
				this.cache.push(this.addCacheImage(dataIndex));
		}
	},
	
	addCacheImage: function(index)
	{
		/// <summary>Adds an image to the cache for the specified index in the current album.</summary>
		/// <param name="index">The index of the image to add.</param>
		/// <returns>The added image.</returns>
		
		var image;
		
		if (this.control.isSlideIndexValid(this.currentAlbumIndex, index))
		{
			image = this.control.host.content.createFromXaml('<Image Visibility="Collapsed" />');
			image.source = this.control.data.album[this.currentAlbumIndex].slide[index].image;
			this.root.children.add(image);
		}
		
		return image;
	},
	
	removeCacheImage: function(index)
	{
		/// <summary>Removes the image at the specified index from the cache.</summary>
		/// <param name="index">The index of the image to remove.</param>
		
		this.root.children.remove(this.cache[index]);
		this.cache[index] = null;
		this.cache.splice(index, 1);
	},

	getDataIndexByOffset: function(offset)
	{
		/// <summary>Gets the slide index using an offset from the current slide index.</summary>
		/// <param name="offset">A positive or negative offset from the current slide index.</param>
		/// <returns>The slide index.</returns>
		
		var count = 0;
		
		if (this.control.isSlideIndexValid(this.currentAlbumIndex, 0))
			count = this.control.data.album[this.currentAlbumIndex].slide.length;
		
		if (count > 0)
		{
			var index = this.currentSlideIndex + offset;
			var mod = index % count;
			return (mod < 0) ? count + mod : mod;
		}
		
		return null;
	},
	
	loadImageByOffset: function(offset, useTransition)
	{
		/// <summary>Loads an image using an offset from the current slide index.</summary>
		/// <param name="offset">A positive or negative offset from the current slide index.</param>
		/// <param name="useTransition">A value indicating whether or not a transition should be used.</param>
		
		this.currentSlideIndex = this.getDataIndexByOffset(offset);
		this.loadImage(useTransition);
	},
	
	loadImage: function(transitionNextImage)
	{
		/// <summary>Loads the image at the current slide index and fires the "slideLoading" event.</summary>
		/// <param name="transitionNextImage">Specifies whether or not to use a transition for the slide.</param>
		
		this.allowSlideChange = true;
		this.transitionNextImage = transitionNextImage;
		
		var bufferSource = this.getCurrentImageSource();
		
		if (bufferSource)
			this.bufferImage.source = bufferSource;
		else
			this.changeImage("");
		
		this.fireEvent("slideLoading", this.getCurrentSlideData());
	},
	
	getCurrentImageSource: function()
	{
		/// <summary>Gets the source for the current image.</summary>
		/// <returns>The current image source.</returns>
		
		this.adjustCacheWindow();
		
		if (this.cache.length > 0)
			return this.cache[this.cacheSize].source;
		
		return "";
	},
	
	getCurrentSlideData: function()
	{
		/// <summary>Gets the data for the current slide.</summary>
		/// <returns>The current slide data.</returns>
		
		if (this.control.isSlideIndexValid(this.currentAlbumIndex, this.currentSlideIndex))
			return this.control.data.album[this.currentAlbumIndex].slide[this.currentSlideIndex];
		
		return null;
	},
	
	adjustCacheWindow: function()
	{
		/// <summary>Adjusts the cache window to the new slide index.</summary>
		
		var currentSlide = this.getCurrentSlideData();
		
		if (currentSlide)
		{
			var cacheIndex;
			
			for (var i = 0, j = this.cache.length; i < j; i++)
			{
				if (this.cache[i].source == currentSlide.image)
				{
					cacheIndex = i;
					break;
				}
			}

			if (cacheIndex == null)
			{
				this.loadCache();
			}
			else
			{
				var dataIndex, image;
				var offset = cacheIndex - this.cacheSize;
				var absOffset = Math.abs(offset);
				
				if (offset < 0)
				{
					for (var k = 1; k <= absOffset; k++)
					{
						dataIndex = this.getDataIndexByOffset(0 - k);
						image = this.addCacheImage(dataIndex);
						this.cache.unshift(image);
						this.removeCacheImage(this.cache.length - 1);
					}
				}
				else if (offset > 0)
				{
					for (var l = 1; l <= absOffset; l++)
					{
						dataIndex = this.getDataIndexByOffset(l);
						image = this.addCacheImage(dataIndex);
						this.cache.push(image);
						this.removeCacheImage(0);
					}
				}
			}
		}
		else
		{
			this.loadCache();
		}
	},
	
	changeImage: function(toImageSource)
	{
		/// <summary>Changes the image to the new slide.</summary>
		/// <param name="toImageSource">Specifies the source for the next slide image.</param>
		
		this.initializeSlideImages();
		
		if (this.slideImage1.root.visibility == "Visible")
		{
			this.fromImage = this.slideImage1;
			this.toImage = this.slideImage2;
		}
		else
		{
			this.fromImage = this.slideImage2;
			this.toImage = this.slideImage1;
		}
		
		this.toImage.setSource(toImageSource);
		
		this.currentTransition = null;
		
		if (this.transitionNextImage)
		{
			var transitionData = this.control.getSlideTransitionData(this.currentAlbumIndex, this.currentSlideIndex);
			this.currentTransition = this.control.createObjectInstanceFromConfig(transitionData);
		}
		else
		{
			this.currentTransition = this.control.createObjectInstanceFromConfig({ type: "NoTransition" });
			this.transitionNextImage = true;
		}
		
		this.currentTransition.addEventListener("complete", SlideShow.createDelegate(this, this.onSlideChange));
		this.currentTransition.begin(this.fromImage, this.toImage);
	},
	
	initializeSlideImages: function()
	{
		/// <summary>Initializes slide images for use in the next transition.</summary>
	
		if (this.slideImage1 == null || this.slideImage2 == null)
		{
			this.slideImage1 = new SlideShow.SlideImage(this.control, this, this.options.slideImage);
			this.slideImage2 = new SlideShow.SlideImage(this.control, this, this.options.slideImage);
			this.slideImage1.options.visibility = "Collapsed";
		}
		else
		{
			var slideImage1 = new SlideShow.SlideImage(this.control, this, this.options.slideImage);
			slideImage1.options.visibility = this.slideImage1.root.visibility;
			slideImage1.setSource(this.slideImage1.image.source);
			
			var slideImage2 = new SlideShow.SlideImage(this.control, this, this.options.slideImage);
			slideImage2.options.visibility = this.slideImage2.root.visibility;
			slideImage2.setSource(this.slideImage2.image.source);
			
			this.slideImage1.dispose();
			this.slideImage2.dispose();
			
			this.slideImage1 = slideImage1;
			this.slideImage2 = slideImage2;
		}
		
		this.slideImage1.render();
		this.slideImage2.render();
	},
	
	onControlDataLoad: function(sender, e)
	{
		/// <summary>Initializes the current album and slide indexes and loads the first available slide.</summary>
		/// <param name="sender">The event source.</param>
		/// <param name="e">The event arguments.</param>
		
		if (this.control.isAlbumIndexValid(0))
		{
			if (this.control.data.startalbumindex || this.control.data.startslideindex)
			{
				this.currentAlbumIndex = this.control.data.startalbumindex || 0;
				this.currentSlideIndex = this.control.data.startslideindex || 0;
				this.loadImage(true);
			}
			else
			{
				for (var i = 0, j = this.control.data.album.length; i < j; i++)
				{
					var slides = this.control.data.album[i].slide;
					
					if (slides && slides.length > 0)
					{
						this.currentAlbumIndex = i;
						this.currentSlideIndex = 0;
						this.loadImage(true);
						break;
					}
				}
			}
		}
	},
	
	onBufferImageDownloadProgressChanged: function(sender, e)
	{
		/// <summary>Handles the event fired while a slide image is downloading.</summary>
		/// <param name="sender">The event source.</param>
		/// <param name="e">The event arguments.</param>
		
		if (sender.downloadProgress == 1 && this.allowSlideChange)
		{
			this.allowSlideChange = false;
			this.changeImage(sender.source);
		}
		
		this.fireEvent("downloadProgressChanged", sender.downloadProgress);
	},
	
	onBufferImageDownloadFailed: function(sender, e)
	{
		/// <summary>Handles the event fired when a slide image failed to download.</summary>
		/// <param name="sender">The event source.</param>
		/// <param name="e">The event arguments.</param>
		
		throw new Error("Image download failed: " + sender.source);
	},
	
	onSlideChange: function(sender, e)
	{
		/// <summary>Fires the slideChange event when a transition between slides is complete.</summary>
		/// <param name="sender">The event source.</param>
		/// <param name="e">The event arguments.</param>
		
		this.fireEvent("slideChange", this.getCurrentSlideData());
	},
	
	onSizeChanged: function()
	{
		/// <summary>Handles the event fired when the control is resized.</summary>
		
		SlideShow.SlideViewer.base.onSizeChanged.call(this);
		this.transitionClip.rect = this.root["Canvas.Left"] + "," + this.root["Canvas.Top"] + "," + this.root.width + "," + this.root.height;
	}
});

/*******************************************
 * class: SlideShow.SlideImage
 *******************************************/
SlideShow.SlideImage = function(control, parent, options)
{
	/// <summary>Displays slide images within the SlideViewer module.</summary>
	/// <param name="control">The Slide.Show control.</param>
	/// <param name="parent">The parent control.</param>
	/// <param name="options">The options for the control.</param>
	
	var xaml = '<Canvas xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Name="SlideImage" Visibility="Collapsed"><Image x:Name="Image" /></Canvas>';
	
	SlideShow.SlideImage.base.constructor.call(this, control, parent, xaml);
	
	SlideShow.merge(this.options,
	{
		top: 0,
		left: 0,
		right: 0,
		bottom: 0,
		stretch: "Uniform"
	});
	
	this.setOptions(options);
	
	this.image = this.root.findName("Image");
};

SlideShow.extend(SlideShow.UserControl, SlideShow.SlideImage,
{
	render: function()
	{
		/// <summary>Renders the image using the current options.</summary>
		
		SlideShow.SlideImage.base.render.call(this);
		
		this.image.stretch = this.options.stretch;
	},
	
	setSource: function(source)
	{
		/// <summary>Sets the image source.</summary>
		/// <param name="source">The image source.</param>
		
		this.image.source = source;
	},
	
	onSizeChanged: function()
	{
		/// <summary>Handles the event fired when the control is resized.</summary>
		
		SlideShow.SlideImage.base.onSizeChanged.call(this);
		
		this.image.width = this.root.width;
		this.image.height = this.root.height;
		
		this.fireEvent("sizeChange");
	}
});

/*******************************************
 * class: SlideShow.SlideShowNavigation
 *******************************************/
SlideShow.SlideShowNavigation = function(control, parent, options)
{
	/// <summary>Provides basic slideshow navigation.</summary>
	/// <param name="control">The Slide.Show control.</param>
	/// <param name="parent">The parent control.</param>
	/// <param name="options">The options for this control.</param>
	
	var xaml =
		'<Canvas xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Name="SlideShowNavigation" Visibility="Collapsed">' +
		'	<Rectangle x:Name="Background">' +
		'		<Rectangle.Fill>' +
		'			<LinearGradientBrush StartPoint="0.477254,1.16548" EndPoint="0.477254,0.0426189">' +
		'				<LinearGradientBrush.GradientStops>' +
		'					<GradientStop x:Name="BackgroundColor1" Offset="0.232877" />' +
		'					<GradientStop x:Name="BackgroundColor2" Offset="0.987288" />' +
		'				</LinearGradientBrush.GradientStops>' +
		'			</LinearGradientBrush>' +
		'		</Rectangle.Fill>' +
		'	</Rectangle>' +
		'</Canvas>';
	
	SlideShow.SlideShowNavigation.base.constructor.call(this, control, parent, xaml);
	
	SlideShow.merge(this.options,
	{
		width: 66,
		height: 20,
		radius: 2,
		stroke: "#7FF19B77",
		strokeThickness: 1,
		backgroundColor1: "#7F2D12",
		backgroundColor2: "#E9A16B",
		playTimerInterval: 3000,
		enablePlayOnLoad: true,
		loopAlbum: false,
		playButton:
		{
			left: "50%",
			radius: 0,
			strokeThickness: 0,
			backgroundColor1: "Transparent",
			backgroundColor2: "Transparent",
			pathData: "M 100.048,736.889L 98.5482,736.889C 97.9959,736.889 97.5482,736.441 97.5482,735.889L 97.5482,727.889C 97.5482,727.337 97.9959,726.889 98.5482,726.889L 100.048,726.889C 100.6,726.889 101.048,727.337 101.048,727.889L 101.048,735.889C 101.048,736.441 100.6,736.889 100.048,736.889 Z M 106.922,736.889L 105.422,736.889C 104.87,736.889 104.422,736.441 104.422,735.889L 104.422,727.889C 104.422,727.337 104.87,726.889 105.422,726.889L 106.922,726.889C 107.475,726.889 107.922,727.337 107.922,727.889L 107.922,735.889C 107.922,736.441 107.475,736.889 106.922,736.889 Z",
			pathWidth: 10,
			pathHeight: 12,
			pathStretch: "Fill",
			pathFill: "#F2C29F",
			pathFillDisabled: "#D3895A"
		},
		previousButton:
		{
			radius: 0,
			strokeThickness: 0,
			backgroundColor1: "Transparent",
			backgroundColor2: "Transparent",
			pathData: "F1 M6.0000005,1.473075E-06 L6.0000005,10.000002 5.9604696E-07,5.0000014 6.0000005,1.473075E-06 z M-1.3709068E-06,4.0019114E-07 L-1.3709068E-06,10.000006 -6.0000029,5.0000029 -1.3709068E-06,4.0019114E-07 Z",
			pathWidth: 10,
			pathHeight: 8,
			pathFill: "#F2C29F",
			pathFillDisabled: "#D3895A"
		},
		nextButton:
		{
			right: 0,
			radius: 0,
			strokeThickness: 0,
			backgroundColor1: "Transparent",
			backgroundColor2: "Transparent",
			pathData: "F1 M-5.9999976,1.0552602E-05 L-1.9073478E-06,4.9999938 -1.9073478E-06,1.8062785E-05 5.9999981,5.0000033 -1.9073478E-06,10.000018 -1.9073478E-06,5.0000024 -5.9999976,9.9999857 -5.9999976,1.0552602E-05 Z",
			pathWidth: 10,
			pathHeight: 8,
			pathFill: "#F2C29F",
			pathFillDisabled: "#D3895A"
		}
	});
	
	this.setOptions(options);
	
	if (!this.options.enablePlayOnLoad)
		this.options.playButton.pathData = "F1 M 101.447,284.834L 101.447,274.714L 106.906,279.774L 101.447,284.834 Z";
	
	this.playButton = new SlideShow.PathButton(this.control, this, this.options.playButton);
	
	if (this.options.enablePreviousSlide)
	{
		this.previousButton = new SlideShow.PathButton(this.control, this, this.options.previousButton);
		this.previousButton.addEventListener("click", SlideShow.createDelegate(this, this.onPreviousClick));
	}
	
	if (this.options.enableNextSlide)
	{
		this.nextButton = new SlideShow.PathButton(this.control, this, this.options.nextButton);
		this.nextButton.addEventListener("click", SlideShow.createDelegate(this, this.onNextClick));
	}
	
	this.mode = "Pause";
	this.background = this.root.findName("Background");
	this.backgroundColor1 = this.root.findName("BackgroundColor1");
	this.backgroundColor2 = this.root.findName("BackgroundColor2");
	
	this.playButton.addEventListener("click", SlideShow.createDelegate(this, this.onPlayClick));
	this.control.root.addEventListener("KeyUp", SlideShow.createDelegate(this, this.onControlKeyPressed));
	this.control.addEventListener("dataLoad", SlideShow.createDelegate(this, this.onControlDataLoad));	
};

SlideShow.extend(SlideShow.SlideNavigation, SlideShow.SlideShowNavigation,
{
	render: function()
	{
		/// <summary>Renders the control using the current options.</summary>

		SlideShow.SlideShowNavigation.base.render.call(this);
		
		this.background.width = this.options.width;
		this.background.height = this.options.height;
		this.background.radiusX = this.options.radius;
		this.background.radiusY = this.options.radius;
		this.background.stroke = this.options.stroke;
		this.background.strokeThickness = this.options.strokeThickness;
		this.backgroundColor1.color = this.options.backgroundColor1;
		this.backgroundColor2.color = this.options.backgroundColor2;
	},
	
	play: function()
	{
		/// <summary>Plays the slideshow.</summary>
		
		if (this.mode != "Play")
		{
			this.mode = "Play";
			this.startPlayTimer();
			this.playButton.setPath("M 100.048,736.889L 98.5482,736.889C 97.9959,736.889 97.5482,736.441 97.5482,735.889L 97.5482,727.889C 97.5482,727.337 97.9959,726.889 98.5482,726.889L 100.048,726.889C 100.6,726.889 101.048,727.337 101.048,727.889L 101.048,735.889C 101.048,736.441 100.6,736.889 100.048,736.889 Z M 106.922,736.889L 105.422,736.889C 104.87,736.889 104.422,736.441 104.422,735.889L 104.422,727.889C 104.422,727.337 104.87,726.889 105.422,726.889L 106.922,726.889C 107.475,726.889 107.922,727.337 107.922,727.889L 107.922,735.889C 107.922,736.441 107.475,736.889 106.922,736.889 Z", 12); // FIXME: 12?
		}
	},
	
	pause: function()
	{
		/// <summary>Pauses the slideshow.</summary>
		
		if (this.mode != "Pause")
		{
			this.mode = "Pause";
			this.stopPlayTimer();
			this.playButton.setPath("F1 M 101.447,284.834L 101.447,274.714L 106.906,279.774L 101.447,284.834 Z");
		}
	},
	
	togglePlayMode: function()
	{
		/// <summary>Toggles the slideshow between play and pause mode.</summary>
	
		if (this.mode == "Pause")
			this.play();
		else
			this.pause();
	},
	
	startPlayTimer: function()
	{
		/// <summary>Starts the play timer.</summary>
		
		if (!this.playTimerId)
			this.playTimerId = window.setTimeout(SlideShow.createDelegate(this, this.showNextSlide), this.options.playTimerInterval);
	},
	
	stopPlayTimer: function()
	{
		/// <summary>Stops the play timer.</summary>
		
		if (this.playTimerId)
		{
			window.clearTimeout(this.playTimerId);
			this.playTimerId = null;
		}
	},
	
	onPreviousClick: function(sender, e) 
	{
		/// <summary>Handles the event fired when the previous button is clicked.</summary>
		/// <param name="sender">The event source.</param>
		/// <param name="e">The event arguments.</param>
		
		this.showPreviousSlide();
		this.fireEvent("previousClick");
	},
	
	onPlayClick: function(sender, e)
	{
		/// <summary>Handles the event fired when the play button is clicked.</summary>
		/// <param name="sender">The event source.</param>
		/// <param name="e">The event arguments.</param>
		
		this.togglePlayMode();
		this.fireEvent("playClick");
	},
	
	onNextClick: function(sender, e) 
	{
		/// <summary>Handles the event fired when the next button is clicked.</summary>
		/// <param name="sender">The event source.</param>
		/// <param name="e">The event arguments.</param>
		
		this.showNextSlide();
		this.fireEvent("nextClick");
	},
	
	onControlModulesLoad: function(sender, e)
	{
		/// <summary>Completes initialization after all modules have loaded.</summary>
		/// <param name="sender">The event source.</param>
		/// <param name="e">The event arguments.</param>
		
		SlideShow.SlideShowNavigation.base.onControlModulesLoad.call(this, sender, e);
		this.slideViewer.addEventListener("slideLoading", SlideShow.createDelegate(this, this.onSlideLoading));
		this.slideViewer.addEventListener("slideChange", SlideShow.createDelegate(this, this.onSlideChange));
	},
	
	onControlDataLoad: function(sender, e)
	{
		/// <summary>Completes initialization after all data has loaded.</summary>
		/// <param name="sender">The event source.</param>
		/// <param name="e">The event arguments.</param>

		if (this.options.enablePlayOnLoad)
			this.play();

		this.slideViewer.loadImageByOffset(0, this.options.enableTransitionOnNext);
	},
	
	onSlideLoading: function(sender, e)
	{
		/// <summary>Handles the event fired when the next slide is loading. Resets the play timeout while the next slide downloads.</summary>
		/// <param name="sender">The event source.</param>
		/// <param name="e">The event arguments.</param>
		
		if (this.mode == "Play")
			this.stopPlayTimer();
		
		if (this.options.enablePreviousSlide)
		{
			if (this.slideExistsByOffset(-1))
				this.previousButton.enable();
			else
				this.previousButton.disable();
		}

		if (this.options.enableNextSlide)
		{
			if (this.slideExistsByOffset(1))
				this.nextButton.enable();
			else
				this.nextButton.disable();
		}
		
		if (this.slideExistsByOffset(1))
		{
			this.playButton.enable();
		}
		else
		{
			this.pause();
			this.playButton.disable();
		}
	},
	
	onSlideChange: function(sender, e) 
	{
		/// <summary>Handles the event fired when the current slide changes.</summary>
		/// <param name="sender">The event source.</param>
		/// <param name="e">The event arguments.</param>
		
		if (this.mode == "Play")
			this.startPlayTimer();
	},
	
	onControlKeyPressed: function(sender, e)	
	{
		/// <summary>Handles the event fired when a key is pressed.</summary>
		/// <param name="sender">The event source.</param>
		/// <param name="e">The event arguments.</param>
		
		switch (e.key)
		{
			case 9:
				this.togglePlayMode();
				break;
				
			case 14:
				this.showPreviousSlide();
				break;
				
			case 16:
				this.showNextSlide();
				break;
		}
	}
});