/***************************************************************
 *  Copyright notice
 *
 *  (c) 2010 Benjamin Bergmann <b.bergmann@exinit.de>
 *  All rights reserved
 *
 *  This script is part of the TYPO3 project. The TYPO3 project 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 2 of the License, or
 *  (at your option) any later version.
 *
 *  The GNU General Public License can be found at
 *  http://www.gnu.org/copyleft/gpl.html.
 *
 *  This script 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.
 *
 *  This copyright notice MUST APPEAR in all copies of the script!
 ***************************************************************/

/**
 * The FX base class for the Exinit FX package
 *
 * @version 1.0.0
 * @license http://opensource.org/licenses/gpl-license.php GNU Public License, version 2
 */
var ExinitFX = new Class({

	Implements: [Events, Options, Chain],
	
	items: [],
	startIndex: 1,
	currentIndex: 0,
	prevIndex: 0,
	totalItems: 0,
	autoPlay: false,
	direction: 1,
	timer: null,
	processor: null,
	
	// the default options object
	options: {
		ID: 0,
		dom: false,
		domSelector: '',
		enableFX: true, // enable effects in generally
		controlFX: true, // enable effects for controls
		itemsVisible: 1, // number of items visible at once
		itemsToProcess: 1, // number of items that slide at once */
		height: null,
		width: 800,
		slideVertical: false, // vertical sliding, if false, sliding is horizontal
		showPrevNext: false, // show previous/next controls
		showTabs: false, // show tabs
		showAnnounce: false, // show Announce
		showPlayPause: false, // show Play/Pause button
		showCounter: false, // show counter
		duration: 300, // transition duration
		direction: 1, // sliding direction ( 1: enter from left/top; -1:enter from right/bottom )
		autoTrigger: false, // auto trigger on/off
		triggerInterval: 5000, // auto trigger interval in milliseconds
		mouseWheelNav: false, // enable mouse wheel navigation
		startIndex: null,
		transition: Fx.Transitions.linear, // transition
		additionalPrevNextButtons: false,
		additionalNextButtonClass: 'exinitFXadditionalNext',
		additionalPrevButtonClass: 'exinitFXadditionalPrev'
	},
	ID: 0,
	overallContainer: 'exinitFXouter', // outer container ID
	processedContainer: 'exinitFXinner', // ID of the container to be processed e.g. by sliders 
	itemsContainer: 'exinitFXitems', // items container ID
	
	itemsSelector: 'exinitFXitem', // CSS selector for items
	
	// classes for several dynamic elements
	navigation: {
		next: 'exinitFXnext', // forward button CSS class
		prev: 'exinitFXprev' // back button CSS class
	},
	tabsContainerClass: 'exinitFXtabsContainer',
	tabsULClass: 'exinitFXtabs',
	counterClass: 'exinitFXcounter',
	announceOuterContainerClass: 'exinitFXannounceOuter',
	announceOuter2ContainerClass: 'exinitFXannounceOuter2',
	announceContainerClass: 'exinitFXannounce',
	announce: {
		next: 'exinitFXnextTitle', // next title button CSS class
		prev: 'exinitFXprevTitle' // prev title button CSS class
	},
	playPauseContainerClass: 'exinitFXpausePlayContainer',
	playPauseClass: 'exinitFXpausePlay',
	headers: null,

	effects: {},

	controlFXduration: 100,
	
	initialize: function(options) {
		// overwrite default options with incoming parameters
		this.setOptions(options);
		if(this.options.ID) {
			this.ID = this.options.ID;
		}
		
		// overwrite default container ids with options
		if(this.options.overallContainer) {
			this.overallContainer = this.options.overallContainer;
		}
		if(this.options.processedContainer) {
			this.processedContainer = this.options.processedContainer;
		}
		if(this.options.itemsContainer) {
			this.itemsContainer = this.options.itemsContainer;
		}
		// if items should be selected out of the dom, we have to grab them...
		if(this.options.dom) {
			this.grabItemsFromDom();
		}
		// all items are identified on CSS selector (itemsSelector)
		if($(this.itemsContainer)) {
			this.items = $(this.itemsContainer).getElements('.' + this.itemsSelector);
		}
		// if nothing to process: return
		// TODO: must I, miss Sophie?
		if (this.items.length <= this.options.itemsVisible) {
			return;
		}
		// add the onChange event to this
//		this.addEvent('change', this.onChange);
		// set the default current index
		this.currentIndex = 0;
		// the configured direction to process
		this.direction = this.options.direction;
		// set start index
		if (this.options.startIndex && this.options.startIndex > 0 && this.options.startIndex < this.items.length) {
			this.startIndex = this.options.startIndex;
		}
		// set the number of items to process
		if (this.options.itemsToProcess == 1) {
			this.options.itemsToProcess = 1;
		}
		// set the control FX duration
		if(this.options.controlFXduration) {
			this.controlFXduration = this.options.controlFXduration;
		}

		// generate some ids
		this.nextID = this.navigation.next + '_' + this.ID;
		this.prevID = this.navigation.prev + '_' + this.ID;
		this.nextTitleID = this.announce.next + '_' + this.ID;
		this.prevTitleID = this.announce.prev + '_' + this.ID;
		this.counterID = this.counterClass + '_' + this.ID;
		
		// initialize element dimensions
		this.initializeDimensions();
		// initialize the headers
		this.initializeHeaders();
		// initialize the controls
		this.initializeControls();
		// initialize the item dimensions
		this.initializeItems();
		// resizes the container div's according to the number of itemsVisible items
		this.initializeContainer();
		// start local initialization
		this.start();
		// start index via URL hash overrides configured value
		var hash = window.location.hash.substring(1); // the hash
		if(hash) {
			var hashparts = hash.split("_");
			if(hashparts.length == 2 && hashparts[0] == this.ID) {
				this.startIndex = hashparts[1];
			}
		}
		// process to the configured start index, if it is greater than 1
		if (this.startIndex > 1) {
			this.process(this.startIndex - 1, true, true);
		}
		// autoplay or pause?
		if (this.options.autoTrigger) {
			this.autoPlay = true;
			this.play(true);
		}
		this.callChain();
	},
	
	/**
	 * Local initialization.
	 * Has to be overwritten in inheriting FX classes.
	 */
	start: function() {
		// implement this in inherited classes
	},
	
	/**
	 * onChange event handler.
	 * The change event is fired after processing.
	 * Can be implemented in inheriting classes.
	 */
	//onChange: function() {
		// implement this in inherited classes
	//},
	
	/**
	 * Process to previous item
	 * @param boolean manual manual trigger flag
	 */
	previous: function(manual) {
		this.processor = 'previous';
		var index = (this.currentIndex - this.options.itemsToProcess + 1 > 0 ? this.currentIndex - this.options.itemsToProcess : this.items.length - this.options.itemsToProcess);
		this.process(index, manual);
		return false;
	},
	
	/**
	 * Process to next item
	 * @param boolean manual manual trigger flag
	 */
	next: function(manual) {
		this.processor = 'next';
		var index = (this.currentIndex + this.options.itemsToProcess - 1 < this.items.length - 1 ? this.currentIndex + this.options.itemsToProcess : 0);
		this.process(index, manual);
		return false;
	},
	
	/**
	 * Process to explicit index
	 * @param int to the element number (1,2,3,...) to scroll to
	 */
	scrollTo: function(to) {
		this.processor = 'scrollTo';
		this.process((to - 1), true);
		return false;
	},
	
	/**
	 * This is called before processing.
	 * It stops the auto trigger timer.
	 * @param boolean manual flag for manual triggering the effect.
	 */
	preProcess: function(index, manual) {
		if (manual) {
			this.stop();
		}
		this.prevIndex = this.currentIndex;
		this.currentIndex = index;
		this.previousIndex = this.currentIndex + (this.currentIndex > 0 ? -1 : this.items.length - 1);
		this.nextIndex = this.currentIndex + (this.currentIndex < this.items.length - 1 ? 1 : 1 - this.items.length);
		this.processControlEffects(0);
	},
	
	/**
	 * Do the FX processing to the item with the given index
	 * @param int index
	 * @param boolean manual
	 * @param boolean noFX
	 */
	process: function(index, manual, noFX) {
		if (!noFX || noFX == 'undefined' || noFX == null) {
			noFX = !this.options.enableFX;
		}
		if (index != this.currentIndex) {
			this.chain(function() {
				this.preProcess(index, manual);
				this.callChain();
			});
			if(this.options.controlFX) {
				this.wait(this.controlFXduration);
			}
			this.chain(function() {
				this.doProcess(noFX);
				this.callChain();
			});
			this.wait(this.options.duration);
			this.chain(function() {
				this.postProcess(manual);
			});
			this.callChain();
		}
	},
	
	/**
	 * Local FX processing.
	 * Has to be implemented in inheriting classes.
	 * @param boolean noFX flag to skip FX processing, just switch to current element.
	 */
	doProcess: function(noFX) {
		// implement this in inheriting classes
	},
	
	/**
	 * This is called after processing.
	 * It restarts the auto trigger timer to fix the trigger interval.
	 * @param boolean manual flag for manual triggering the effect.
	 */
	postProcess: function(manual) {
		if (manual) {
			this.stop();
		}
		this.update();
		if (manual && this.autoPlay) {
			this.play(true);
		}
		this.processControlEffects(1);
	},

	processControlEffects: function(to) {
		if(this.options.controlFX && this.effects) {
			if(this.effects[this.nextID]) {
				this.effects[this.nextID].start(to);
			}
			if(this.effects[this.prevID]) {
				this.effects[this.prevID].start(to);
			}
			if(this.effects[this.nextTitleID]) {
				this.effects[this.nextTitleID].start(to);
			}
			if(this.effects[this.prevTitleID]) {
				this.effects[this.prevTitleID].start(to);
			}
			if(this.effects[this.counterID]) {
				this.effects[this.counterID].start(to);
			}
		}
	},

	update: function() {
		if (this.options.showTabs) {
			this.updateTabs();
		}
		if (this.options.showCounter) {
			this.updateCounter();
		}
		if (this.options.showAnnounce) {
			this.updateAnnounce();
		}
	},
	
	play: function(wait) {
		this.stop();
		this.isPlaying = true;
		// trigger effect, if we do not have to wait.
		if (!wait) {
			switch (this.direction) {
				case 1:
					this.next(false);
					break;
				case -1:
					this.previous(false);
					break;
			}
		}
		// set up the timer for auto trigger
		switch (this.direction) {
			case 1:
				this.timer = this.next.periodical(this.options.triggerInterval, this, false);
				break;
			case -1:
				this.timer = this.previous.periodical(this.options.triggerInterval, this, false);
				break;
		}
	},
	
	stop: function() {
		this.isPlaying = false;
		$clear(this.timer);
	},

	initializeDimensions: function() {
		this.itemWidth = parseInt(this.options.width);
		if(typeof(this.itemWidth) == 'number' && this.itemWidth + '' == 'NaN') {
			this.itemWidth = 0;
			this.items.each(function(item) {
				var iWidth = parseInt(item.getStyle('width'));
				if (iWidth > this.itemWidth) {
					this.itemWidth = iWidth;
				}
			}.bind(this));
			this.options.width = this.itemWidth;
		}
		this.itemHeight = parseInt(this.options.height);
		if(typeof(this.itemHeight) == 'number' && this.itemHeight + '' == 'NaN') {
			this.itemHeight = 0;
			this.items.each(function(item) {
				var iHeight = parseInt(item.getStyle('height'));
				if (iHeight > this.itemHeight) {
					this.itemHeight = iHeight;
				}
			}.bind(this));
			this.options.height = this.itemHeight;
		}
	},

	initializeHeaders: function() {
		this.headers = new Array();
		var tmpHeaders = window['headers_' + this.ID];
		this.items.each(function(item, i) {
			if(tmpHeaders && tmpHeaders[i]) {
				this.headers[i] = tmpHeaders[i];
			} else if (item.getElement('h1')) {
				this.headers[i] = item.getElement('h1').get('text');
			} else {
				this.headers[i] = '';
			}
		}.bind(this));
	},

	initializeControls: function() {
		// initialize previous / next buttons
		if (this.options.showPrevNext) {
			this.initializePrevNextButtons();
		}

		if(this.options.additionalPrevNextButtons) {
			this.initializeAdditionalPrevNextButtons();
		}

		// initialize tabs
		if (this.options.showTabs) {
			this.initializeTabs();
		}

		// initialize counter
		if (this.options.showCounter) {
			this.initializeCounter();
		}

		// initialize announce
		if (this.options.showAnnounce) {
			this.initializeAnnounce();
		}

		// initialize play / pause button
		if (this.options.showPlayPause) {
			this.initializePlayPauseButton();
		}
	},
	
	initializeItems: function() {
		var styles = new Hash();
		styles.include('width', this.itemWidth);
		styles.include('height', this.itemHeight);

		this.items.each(function(item) {
			item.setStyles(styles);
			if (this.options.autoTrigger) {
				this.assignAutoTriggerEvents(item);
			}
		}.bind(this));
	},
	
	/* 
	 * sets the containers width to leave visible only the specified number of items
	 */
	initializeContainer: function() {
		var overallStyles = new Hash();
		overallStyles.include('position', 'relative');
		overallStyles.include('width', this.itemWidth);
		$(this.overallContainer).setStyles(overallStyles);
		
		var processedStyles = new Hash();
		processedStyles.include('position', 'relative');
		processedStyles.include('overflow', 'hidden');
		processedStyles.include('width', this.itemWidth);
		processedStyles.include('height', this.itemHeight);
		$(this.processedContainer).setStyles(processedStyles);
		
		var itemStyles = new Hash();
		itemStyles.include('position', 'relative');
		itemStyles.include('width', this.itemWidth);
		itemStyles.include('height', this.itemHeight);
		$(this.itemsContainer).setStyles(itemStyles);
	},
	
	/* 
	 * adds next/previous buttons
	 */
	initializePrevNextButtons: function() {
		if (this.items.length <= this.options.itemsVisible) {
			return;
		}
		// create button divs
		this.prevButton = new Element('div', {'id': this.prevID, 'class': this.navigation.prev});
		this.nextButton = new Element('div', {'id': this.nextID, 'class': this.navigation.next});
		// inject them and add click event
		if (this.prevButton) {
			this.prevButton.inject($(this.overallContainer), 'bottom');
			this.prevButton.addEvent('click', this.previous.pass(true, this));
			this.effects[this.prevID] = new Fx.Tween(this.prevButton, {property:'opacity', duration: this.controlFXduration});
		}
		if (this.nextButton) {
			this.nextButton.inject($(this.overallContainer), 'top');
			this.nextButton.addEvent('click', this.next.pass(true, this));
			this.effects[this.nextID] = new Fx.Tween(this.nextButton, {property:'opacity', duration: this.controlFXduration});
		}

	},

	/*
	 * initializes additional next/previous buttons
	 */
	initializeAdditionalPrevNextButtons: function() {
		if (this.items.length <= this.options.itemsVisible) {
			return;
		}
		// get button divs
		this.additionalPrevButtons = $$('.' + this.options.additionalPrevButtonClass);
		this.additionalNextButtons = $$('.' + this.options.additionalNextButtonClass);
		// add click event
		for (var i = 0; i < this.additionalPrevButtons.length; i++) {
			this.additionalPrevButtons[i].addEvent('click', this.previous.pass(true, this));
		}
		for (var j = 0; j < this.additionalPrevButtons.length; j++) {
			this.additionalNextButtons[j].addEvent('click', this.next.pass(true, this));
		}
	},
	
	initializeTabs: function() {
		this.tabsContainer = new Element('div', {'class': this.tabsContainerClass});
		if(this.tabsContainer) {
			this.tabsContainer.inject($(this.overallContainer), 'top');
			this.tabsUL = new Element('ul', {'class': this.tabsULClass});
			if(this.tabsUL) {
				this.tabsUL.inject(this.tabsContainer, 'top');
				var i = 0;
				this.tabs = [];
				this.items.each(function(item) {
					this.tabs[i] = new Element('li');
					if(i == 0) {
						this.tabs[i].addClass('first');
					} else if(i == this.items.length - 1) {
						this.tabs[i].addClass('last');
					}
					this.tabs[i].inject(this.tabsUL, 'bottom');
					
					var anchor = new Element('a', {'href': this.getPanelLink(i)});
					anchor.set('html', this.headers[i]);
					anchor.inject(this.tabs[i], 'top');
					anchor.addEvent('click', this.scrollTo.pass(i+1, this));
					
					i++;
				}.bind(this));
				this.updateTabs();
				
				var breaker = new Element('div', {'style': 'padding: 0; margin: 0; height: 0px; clear: both;'});
				breaker.inject(this.tabsContainer, 'bottom');
			}
		}
	},
	
	initializeCounter: function() {
		this.counter = new Element('div', {
			'id': this.counterID,
			'class': this.counterClass
		});
		if(this.counter) {
			this.counter.inject($(this.overallContainer), 'bottom');
			this.effects[this.counterID] = new Fx.Tween(this.counter, {property:'opacity', duration: this.controlFXduration});
			this.updateCounter(this.startIndex);
		}
	},
	
	initializeAnnounce: function() {
		// create container element for announces 
		this.announceOuterContainer = new Element('div', {'class': this.announceOuterContainerClass});
		if (this.announceOuterContainer) {
			this.announceOuterContainer.inject($(this.overallContainer), 'bottom');
			
			this.announceOuter2Container = new Element('div', {'class': this.announceOuter2ContainerClass});
			if (this.announceOuter2Container) {
				this.announceOuter2Container.inject(this.announceOuterContainer, 'bottom');
				
				this.announceContainer = new Element('div', {'class': this.announceContainerClass});
				if (this.announceContainer) {
					// and inject container into overall container
					this.announceContainer.inject(this.announceOuter2Container, 'bottom');
					// create next title element
					this.nextTitle = new Element('div', {
						'id': this.nextTitleID,
						'class': this.announce.next
					});
					if (this.nextTitle) {
						// inject it into container
						this.nextTitle.inject(this.announceContainer, 'top');
						// and add click event
						this.nextTitle.addEvent('click', this.next.pass(true, this));
						this.effects[this.nextTitleID] = new Fx.Tween(this.nextTitle, {property:'opacity', duration: this.controlFXduration});
					}
					// create next title element
					this.prevTitle = new Element('div', {
						'id': this.prevTitleID,
						'class': this.announce.prev
					});
					if (this.prevTitle) {
						// inject it into container
						this.prevTitle.inject(this.announceContainer, 'bottom');
						// and add click event
						this.prevTitle.addEvent('click', this.previous.pass(true, this));
						this.effects[this.prevTitleID] = new Fx.Tween(this.prevTitle, {property:'opacity', duration: this.controlFXduration});
					}
					// update announce titles
					this.updateAnnounce();
				}
			}
		}
	},
	
	initializePlayPauseButton: function() {
		// create container element for Play/Pause button  
		this.pausePlayContainer = new Element('div', {'class': this.playPauseContainerClass});
		if (this.pausePlayContainer) {
			// and inject container into overall container
			this.pausePlayContainer.inject($(this.overallContainer), 'bottom');
			// create button element
			this.pausePlay = new Element('div', {'class': this.playPauseClass});
			if (this.pausePlay) {
				// inject it into container
				this.pausePlay.inject(this.pausePlayContainer, 'top');
				// and add click event
				if (this.options.autoTrigger) {
					this.pausePlay.addClass('play').set('title', 'Playing. Click to pause.');
				} else {
					this.pausePlay.addClass('play pause').set('title', 'Paused. Click to start.');
				}
				this.pausePlay.addEvent('click', function(event) {
					new Event(event).stop();
					// isPlaying stores whether our ExinitFX instance is playing or not
					if (this.isPlaying) {
						// if this is playing, we'll need to stop it on click
						this.pausePlay.addClass('pause').set('title', 'Paused. Click to start.');
						this.autoPlay = false;
						this.stop();
					} else {
						// ... and if is off, we need to start it
						this.pausePlay.removeClass('pause').set('title', 'Playing. Click to pause.');
						this.autoPlay = true;
						this.play(true);
					}
				}.bind(this));
			}
		}
	},

	assignAutoTriggerEvents: function(item) {
		if(item) {
			item.addEvent('mouseenter', function() {
				if (this.autoPlay) {
					this.stop(true);
				}
			}.bind(this));
			
			item.addEvent('mouseleave', function() {
				if(this.autoPlay) {
					this.play(true);
				}
			}.bind(this));
		}
	},
	
	updateTabs: function() {
		var i = 0;
		this.tabs.each(function(tab) {
			if (i == this.currentIndex) {
				tab.addClass('current');
			} else {
				tab.removeClass('current');
			}
			i++;
		}.bind(this));
	},
	
	updateCounter: function() {
		if (this.counter) {
			this.counter.set('html', ((this.currentIndex + 1) + ' / ' + this.items.length));
		}
	},
	
	updateAnnounce: function() {
		// calculate the previous slide index and the next
		var prev = this.currentIndex - 1 < 0 ? this.items.length - 1 : this.currentIndex - 1;
		var next = this.currentIndex + 1 >= this.items.length ? 0 : this.currentIndex + 1;
		if (this.prevTitle) {
			this.prevTitle.set('html', '&laquo; ' + this.headers[prev]);
		}
		if (this.nextTitle) {
			this.nextTitle.set('html', this.headers[next] + ' &raquo;');
		}
	},
	
	getPanelLink: function(index) {
		return '#' + this.ID + '_' + (index+1);
	},

	grabItemsFromDom: function() {
		var container = $$(this.options.selector);
		if(container) {
			var items = container[0].getChildren();
			if(items) {
				for(var i = 0; i < items.length; i++) {
					var item = new Element('div', {'class': this.itemsSelector});
					var removedElement = items[i].dispose();
					removedElement.inject(item, 'top');
					item.inject($(this.itemsContainer), 'bottom');
				}
			}
			container.dispose();
		}
	}
});
