/**
 * Copyright (c) 2006, Opera Software ASA
 * All rights reserved.
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of Opera Software ASA nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY OPERA SOFTWARE ASA AND CONTRIBUTORS ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL OPERA SOFTWARE ASA AND CONTRIBUTORS BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

/**
 * @fileoverview
 * Generic counter management library.
 *
 * All time calculations are done in UTC, so DST is not a factor.
 * Count-up counters keep counting during computer hibernation mode.
 *
 * @author Magnus Kristiansen
 * @version 1.0
 * @requires Array#getIndexByValue Array.getIndexByValue
 * @requires Array#getIndexByPropValue Array.getIndexByPropValue
 * @requires Array#removeByValue Array.removeByValue
 * @requires Date#shift Date.shift
 */

/**
 * Creates a new Counter object.
 * @class Class for counter management.
 * Creates new counters, manages ticking, saves and loads counter state.
 * @constructor
 * @param {Function} _add Default add-counter callback
 * @param {Function} _remove Default remove-counter callback
 * @param {Function} _tick Default tick callback
 * @param {Function} _alert Default alert callback
 */
function CounterManager( _add, _remove, _tick, _alert ) { // Depends on: Counter, Counter.id, Counter.tick()
	
	/** 
	 * Main list storing all counters
	 * @type Array
	 */
	var allCounters = [];
	/** 
	 * Add a counter to the main list
	 * @param {Counter} counter A counter to add
	 * @type void
	 */
	this.add = function(counter) {
		allCounters.push( counter );
	}
	/** 
	 * Remove a counter from the main list
	 * @param {Counter} counter A counter to remove
	 * @type void
	 */
	this.remove = function(counter) {
		var idx = allCounters.getIndexByValue(counter);
		if (idx != null) allCounters.splice(idx,1);
	}
	/**
	 * Get a counter object by its id
	 * @param {Number} id A counter id
	 * @return The counter in the main list with that id, or null if none found
	 * @type Counter
	 */
	this.get = function(id) {
		var idx = allCounters.getIndexByPropValue(id, 'id');
		return (idx != null) ? allCounters[idx] : null;
	}
	
	/** 
	 * List storing all ticking counters
	 * @type Array
	 */
	var tickingCounters = [];
	/** 
	 * Add a counter to the ticking list
	 * @param {Counter} counter A counter to add
	 * @type void
	 */
	this.start = function(counter) {
		if (! tickingCounters.length) {
			startAll();
		}
		tickingCounters.push( counter );
	}
	/** 
	 * Remove a counter from the ticking list
	 * @param {Counter} counter A counter to remove
	 * @type void
	 */
	this.stop = function(counter) {
		var idx = tickingCounters.getIndexByValue(counter);
		if (idx != null) {
			tickingCounters.splice(idx,1);
			if (! tickingCounters.length) {
				stopAll();
			}
		}
	}
	
	/**
	 * Interval id of the tick interval
	 * @type Number
	 */
	var tickingInterval = 0;
	/** 
	 * Start the tick interval
	 * @type void
	 */
	function startAll() {
		if (tickingInterval) clearInterval( tickingInterval );
		tickingInterval = setInterval( intervalUpdate, 1000 );
	}
	/**
	 * Stop the tick interval
	 * @type void
	 */
	function stopAll() {
		if (tickingInterval) clearInterval( tickingInterval );
		tickingInterval = 0;
	}
	/**
	 * The interval tick handler.
	 *
	 * Calls tick() on all counters in the ticking list
	 * @type void
	 */
	function intervalUpdate() {
		for (var i = tickingCounters.length; i--;) {
			tickingCounters[i].tick();
		}
	}
	
	/**
	 * Make a counter
	 * @param {Boolean} _auto If true, starts the counter immediately
	 * @param {String} _name A name
	 * @param {Function} _alert Alert callback function
	 * @param {Function} _tick Tick callback function
	 * @param {Boolean} _tminus Pre-alert last 10 seconds before alert
	 * @param {Date} _time Time to start at
	 * @param {Number} _size Number of units in interval
	 * @param {String} _unit Type of unit in interval (e.g. 'hours')
	 * @param {Counter} _parent The parent counter, if applicable
	 * @return A counter
	 * @type Counter
	 */
	this.makeCount = function( _mode, _auto, _name, _alert, _tick, _time, _tminus, _size, _unit, _repeat, _parent ) {
		if (typeof _tick != 'function') _tick = default_tick;
		if (typeof _alert != 'function') _alert = default_alert;
		
		var c = new Counter( this, _name, _mode, _time, _alert, _tick, default_add, default_remove, _tminus, _size, _unit, _repeat );
		c.handle_add(_parent);
		if (_auto) c.handle_play();
		return c;
	}
	/**
	 * Make a countup counter
	 * @param {Boolean} _auto If true, starts the counter immediately
	 * @param {String} _name A name
	 * @param {Function} _alert Alert callback function name
	 * @param {Function} _tick Tick callback function name
	 * @param {Date} _time Time to countdown to
	 * @param {Counter} _parent The parent counter, if applicable
	 * @return A countup counter
	 * @type Counter
	 */
	this.makeCountup = function( _auto, _name, _alert, _tick, _time, _parent ) {
		return this.makeCount( false, _auto, _name, _alert, _tick, _time, null, null, null, null, _parent );
	}
	/**
	 * Make a countdown counter
	 * @param {Boolean} _auto If true, starts the counter immediately
	 * @param {String} _name A name
	 * @param {Function} _alert Alert callback function name
	 * @param {Function} _tick Tick callback function name
	 * @param {Date} _time Time to start interval at, null for 'on play'
	 * @param {Boolean} _tminus Pre-alert last 10 seconds before alert
	 * @param {Number} _size Number of units in interval
	 * @param {String} _unit Type of unit in interval (e.g. 'hours')
	 * @param {Number} _repeat Number of repeats
	 * @param {Counter} _parent The parent counter, if applicable
	 * @see Date#shift
	 * @return A countdown counter
	 * @type Counter
	 */
	this.makeCountdown = function( _auto, _name, _alert, _tick, _time, _tminus, _size, _unit, _repeat, _parent ) {
		return this.makeCount( true, _auto, _name, _alert, _tick, _time, _tminus, _size, _unit, _repeat, _parent );
	}
	
	/**
	 * Default alert callback function
	 * @type Function
	 */
	var default_alert = (typeof _alert == 'function') ? _alert : null;
	/**
	 * Default tick callback function
	 * @type Function
	 */
	var default_tick = (typeof _tick == 'function') ? _tick : null;
	/**
	 * Default add-counter callback function
	 * @type Function
	 */
	var default_add = (typeof _add == 'function') ? _add : null;
	/**
	 * Default remove-counter callback function
	 * @type Function
	 */
	var default_remove = (typeof _remove == 'function') ? _remove : null;

	/**
	 * Set default alert callback function
	 * @param {Function} f The new default callback function
	 * @return true if default was set successfully
	 * @type Boolean
	 */
	this.setAlertDefault = function(f) {
		if (typeof f != 'function') return false;
		default_alert = f;
		return true;
	}
	/**
	 * Set default tick callback function
	 * @param {Function} f The new default callback function
	 * @return true if default was set successfully
	 * @type Boolean
	 */
	this.setTickDefault = function(f) {
		if (typeof f != 'function') return false;
		default_tick = f;
		return true;
	}
	/**
	 * Set default add-counter callback function
	 * @param {Function} f The new default callback function
	 * @return true if default was set successfully
	 * @type Boolean
	 */
	this.setAddDefault = function(f) {
		if (typeof f != 'function') return false;
		default_add = f;
		return true;
	}
	/**
	 * Set default remove-counter callback function
	 * @param {Function} f The new default callback function
	 * @return true if default was set successfully
	 * @type Boolean
	 */
	this.setRemoveDefault = function(f) {
		if (typeof f != 'function') return false;
		default_remove = f;
		return true;
	}
}

/**
 * auto_increment field for counter ids
 * @type Number
 */
Counter.prototype.auto_increment = 0;
/**
 * Serializes counter as string
 * @return The serialized counter string
 * @type String
 */
Counter.prototype.toString = function(shallow) {
	if (shallow) return 'Counter : { id:' + this.id + ',name:' + this.name + ' }';
	var r, ret = [];
	for (var p in this) {
		if (! this.hasOwnProperty(p)) continue;
		r = this[p] && this[p].toString(true);
		if (typeof this[p] == 'function') {
			r = r.split('\n',2)[1];
		}
		ret.push( p + " : " + r );
	}
	return 'Counter : {\n' + ret.join('\n') + '\n}';
};
/**
 * Handles counter mode changing to play, invoked by parent
 * @type void
 */
Counter.prototype.handle_play_parent = function() {
	if (this.savestate || !this.is_ticking) return;
	this.handle_play(true);
}
/**
 * Handles counter mode changing to play
 * @param {Boolean} parent_echo True if method was called by parent
 * @type void
 */
Counter.prototype.handle_play = function(parent_echo) {
	if (this.parent && !this.parent.is_counting && !parent_echo) return;
	if (this.is_counting) return;

	if (this.children && this.children.length) {
		for (var i = this.children.length;i--;) {
			this.children[i].handle_play_parent();
		}
	}

	if (this.delta) {
		this.handle_suspend(false);
	} else {
		// If no specified time => now
		if (! this.time) {
			this.time = new Date();
		}
		if (this.interval_size) {
			// If interval and in the past, shift forward
			var d = new Date().getTime();
			while (this.time.getTime() <= d) {
				this.time.shift( this.interval_size, this.interval_unit );
			}
		}
	}

	if (! this.is_ticking) {
		this.cm.start(this);
		this.is_ticking = true;
	}
	this.is_counting = true;
};
/**
 * Handles counter mode changing to pause, invoked by parent
 * @type void
 */
Counter.prototype.handle_pause_parent = function() {
	this.savestate = ! this.is_counting;
	this.handle_pause();
}
/**
 * Handles counter mode changing to pause
 * @type void
 */
Counter.prototype.handle_pause = function() {
	if (! this.is_counting) return;
	
	if (this.children && this.children.length) {
		for (var i = this.children.length;i--;) {
			this.children[i].handle_pause_parent();
		}
	}

	if (! this.deftime) {
		this.handle_suspend(true);
	}
	this.is_counting = false;
};
/**
 * Handles counter mode changing to stop, invoked by parent
 * @type void
 */
Counter.prototype.handle_stop_parent = function() {
	this.savestate = false;
	this.handle_stop();
}
/**
 * Handles counter mode changing to stop
 * @type void
 */
Counter.prototype.handle_stop = function(finish_echo) {
	if (! this.is_ticking) return;
	
	if (this.children && this.children.length) {
		for (var i = this.children.length;i--;) {
			this.children[i].handle_stop_parent();
		}
	}

	if (this.delta) {
		this.delta = null;
	}

	// Restore original repeats for next run
	if (! finish_echo && this.defrepeats) {
		this.repeats = this.defrepeats;
	}
	if (! this.deftime) {
		// If started with no time, return to no-time state
		this.time = null;
	}
	
	this.cm.stop(this);
	this.is_counting = false;
	this.is_ticking = false;
};
/**
 * Handles counter being added
 * @param {Counter} parent The parent, if there is one
 * @type void
 */
Counter.prototype.handle_add = function(parent) {
	if (parent) {
		parent.children.push(this);
		this.parent = parent;
	}
	this.cm.add(this);
	( this.ext_add || this.fallback_add )(this);
};
/**
 * Handles counter being removed, invoked by parent
 * @type void
 */
Counter.prototype.handle_remove_parent = function() {
	this.parent.children.removeByValue(this);
	this.handle_remove();
}
/**
 * Handles counter being removed
 * @type void
 */
Counter.prototype.handle_remove = function() {
	if (this.children && this.children.length) {
		for (var i = this.children.length;i--;) {
			this.children[i].handle_remove_parent();
		}
	}
	
	this.handle_stop();
	this.cm.remove(this);
	( this.ext_remove || this.fallback_remove )(this);
};
/**
 * Handles counter time being suspended
 * @param {Boolean} start Whether suspend mode should start or stop
 * @type void
 */
Counter.prototype.handle_suspend = function(start) {
	if (start) {
		this.delta = (new Date()).getTime() - this.time.getTime();
		this.time = null;
	} else {
		this.time = new Date();
		this.time.setTime( this.time.getTime() - this.delta );
		this.delta = null;
	}
};
/**
 * Handles counter count finishing
 * @type void
 */
Counter.prototype.handle_finish = function() {
	var dorepeat = this.repeats && this.interval_size;
	
	this.handle_stop(dorepeat);
	( this.ext_alert || this.fallback_alert )(this);
	
	if (dorepeat) {
		// If repeats remaining, decrement by one
		if (this.repeats > 0) this.repeats--;
		// Start new repeat
		this.handle_play();
	}
};
/**
 * Alert fallback function
 * @param {Counter} counter The triggered counter
 * @type void
 */
Counter.prototype.fallback_alert = function(counter) {
	opera.postError('Fallback alert handler used for "' + counter.name + '" triggered at ' + (new Date()).toGMTString());
};
/**
 * Tick fallback function
 * @param {Counter} counter The triggered counter
 * @type void
 */
Counter.prototype.fallback_tick = function(counter) {
	opera.postError('Fallback tick handler used for "' + counter.name + '" triggered at ' + (new Date()).toGMTString());
};
/**
 * Add-counter fallback function
 * @param {Counter} counter The triggered counter
 * @type void
 */
Counter.prototype.fallback_add = function(counter) {
	opera.postError('Fallback add handler used for "' + counter.name + '" triggered at ' + (new Date()).toGMTString());
};
/**
 * Remove-counter fallback function
 * @param {Counter} counter The triggered counter
 * @type void
 */
Counter.prototype.fallback_remove = function(counter) {
	opera.postError('Fallback remove handler used for "' + counter.name + '" triggered at ' + (new Date()).toGMTString());
};
/**
 * Handles counter ticking
 * @type void
 */
Counter.prototype.tick = function() {
	if (this.is_down && this.is_counting) {
		// Check if countdowns have finished, handles T-tick
		var d = this.is_down ? new Date() : this.time;
		var t = this.is_down ? this.time : new Date();
		var delta = Math.floor( (t.getTime() - d.getTime()) / 1000 );
		if (delta > 0) {
			if (this.is_tminus && delta < 10) {
				( this.ext_alert || this.fallback_alert )(this, delta);
			}
		} else {
			this.handle_finish();
		}
	}
	( this.ext_tick || this.fallback_tick )(this);
};
/**
 * Creates a new Counter object.
 * Should not be called directly, use {@link CounterManager} makeCount* methods instead.
 *
 * @class Class for counters.
 * Each counter represents one "clock".
 * @constructor
 *
 * @param {String} _name A name
 * @param {Boolean} _down Counting direction, true = down
 * @param {Date} _time Time to countdown to
 * @param {Function} _alert Alert callback function
 * @param {Function} _tick Tick callback function
 * @param {Function} _add Add-counter callback function
 * @param {Function} _remove Remove-counter callback function
 * @param {Boolean} _tminus Pre-alert last 10 seconds before alert
 * @param {Number} _size Number of units in interval
 * @param {String} _unit Type of unit in interval (e.g. 'hours')
 * @param {Number} _repeat Number of repeats
 */
function Counter( _cm, _name, _down, _time, _alert, _tick, _add, _remove, _tminus, _size, _unit, _repeat ) {

	/**
	 * Counter id, autogenerated
	 * @type Number
	 */
	this.id = ++this.constructor.prototype.auto_increment;
	/**
	 * controlling CounterManager
	 * @type CounterManager
	 */
	this.cm = _cm;
	/**
	 * Counter name
	 * @type String
	 */
	this.name = _name;
	/**
	 * Counter target time, if applicable
	 * @type Date
	 */
	this.time = _time;
	/**
	 * Counter target time, if applicable
	 * @type Date
	 */
	this.deftime = _time;

	/**
	 * If counter counts down
	 * @type Boolean
	 */
	this.is_down = _down;
	/**
	 * If counter counts up
	 * @type Boolean
	 */
	this.is_up = ! _down;
	/**
	 * If counter is ticking
	 * @type Boolean
	 */
	this.is_ticking = false;
	/**
	 * If counter is counting
	 * @type Boolean
	 */
	this.is_counting = false;
	/**
	 * If counter counts down 10 last seconds
	 * @type Boolean
	 */
	this.is_tminus = _tminus;
	
	/**
	 * Repeat interval length, if applicable
	 * @type Number
	 */
	this.interval_size = _size;
	/**
	 * Repeat interval unit, if applicable
	 * @type String
	 * @see Date#shift
	 */
	this.interval_unit = _unit;
	/**
	 * Number of repeats, if applicable
	 * @type Number
	 */
	this.repeats = _repeat;
	/**
	 * Number of repeats default, if applicable
	 * @type Number
	 */
	this.defrepeats = _repeat;
	/**
	 * Array of child counters, if applicable
	 * @type Array
	 */
	this.children = [];
	/**
	 * The counter's parent, if applicable
	 * @type Counter
	 */
	this.parent = null;
	/**
	 * Time delta for paused counters
	 * @type Number
	 */
	this.delta = null;
	
	/**
	 * Alert callback function
	 * @type Function
	 */
	this.ext_alert = _alert;
	/**
	 * Tick callback function
	 * @type Function
	 */
	this.ext_tick = _tick;
	/**
	 * Add-counter callback function
	 * @type Function
	 */
	this.ext_add = _add;
	/**
	 * Remove-counter callback function
	 * @type Function
	 */
	this.ext_remove = _remove;
}
