/* -------------------------------------------------------------------------- */
/** 
 *    @fileoverview
 *       Smooth Scroller
 *
 *    @version rev012.2007-10-17
 *    @requires common.js
 */
/* -------------------------------------------------------------------------- */

var BA_SMOOTHSCROLL_AS;



/* -------------------- Settings for BASmoothScrollAutoSetup -------------------- */

var BA_SMOOTHSCROLL_AS_ENABLED           = true;
var BA_SMOOTHSCROLL_AS_FIND_ATONCE       = false;
var BA_SMOOTHSCROLL_AS_USE_POSTPROCESS   = true;
var BA_SMOOTHSCROLL_AS_OFFSET_X          = 0;
var BA_SMOOTHSCROLL_AS_OFFSET_Y          = 0;
var BA_SMOOTHSCROLL_AS_DURATION          = 1000;
var BA_SMOOTHSCROLL_AS_INTERVAL          = 10;
var BA_SMOOTHSCROLL_AS_IGNORE_NODE_CNAME = 'noSmoothScroll'

var BA_SMOOTHSCROLL_AS_EASING_FUNC       = null;   // function(t, b, c, d) { ... };

var BA_SMOOTHSCROLL_AS_POSTPROCESS_FUNC  = function(x, y) {
	if (BA.ua.isGecko || BA.ua.isWinIE || (BA.ua.isSafari && BA.ua.revision > 522)) {
		location.href = BA_SMOOTHSCROLL_AS.fromNode.getAttributeBA('href');
	}
}



/* -------------------- Constructor : BASmoothScroll inherits BAObservable -------------------- */
/**
 * provide smooth scroll behavior for the pages.
 * @class smooth scroller for the page
 * @constructor
 * @param {Number}   offsetX     X-distance from original scroll destination position (px)
 * @param {Number}   offsetY     Y-distance from original scroll destination position (px)
 * @param {Number}   duration    animation duration (ms)
 * @param {Number}   interval    easing interval (ms)
 * @param {Function} function    easing function
 */
/*!
 *  arguments of callback 'onStart(x, y)' :
 *      {Number} x    current scroll position (X-coordinate) (px)
 *      {Number} y    current scroll position (Y-coordinate) (px)
 * 
 *  arguments of callback 'onScroll(x, y)' :
 *      {Number} x    current scroll position (X-coordinate) (px)
 *      {Number} y    current scroll position (Y-coordinate) (px)
 * 
 *  arguments of callback 'onComplete(x, y)' :
 *      {Number} x    current scroll position (X-coordinate) (px)
 *      {Number} y    current scroll position (Y-coordinate) (px)
 * 
 *  arguments of callback 'onAbort(x, y)' :
 *      {Number} x    current scroll position (X-coordinate) (px)
 *      {Number} y    current scroll position (Y-coordinate) (px)
 */
function BASmoothScroll(offsetX, offsetY, duration, interval, func) {
	/** X-distance from original scroll destination position (px)
	    @type Number */
	this.offsetX     = (typeof offsetX == 'number') ? offsetX  : 0;
	/** Y-distance from original scroll destination position (px)
	    @type Number */
	this.offsetY     = (typeof offsetY == 'number') ? offsetY  : 0;
	/** animation duration time (ms).
	    @type Number @private */
	this.duration    = (duration > 0) ? duration : 1500;
	/** easing interval seconds (ms).
	    @type Number @private */
	this.interval    = (interval > 0) ? interval : 5;
	/** associative array of coordinate { X, Y } of scroll starting position (px).
	    @type Object @private */
	this.fromPos     = { X : 0, Y : 0 };
	/** associative array of coordinate { X, Y } of scroll destination posision (px).
	    @type Object @private */
	this.toPos       = { X : 0, Y : 0 };
	/** animation interval timer.
	    @type BASetInterval @private */
	this.easingTimer = null;
	/** animation elapsed timer.
	    @type BATimer @private */
	this.elapseTimer = null;

	if (BA.env.isDOMReady) {
		this.setEasingFunc(func);
	}
}

BASmoothScroll.prototype = new BAObservable;

/**
 * scroll to the specified coordinate.
 * @param {Number} x    X-coordinate of the scroll destination (px)
 * @param {Number} y    Y-coordinate of the scroll destination (px)
 */
BASmoothScroll.prototype.scrollTo = function(x, y) {
	if (isNaN(x) || isNaN(y)) {
		throw 'BASmoothScroll.scrollTo: arguments must be a number';
	} else {
		this.abort();
		var geom = BAGetGeometry();
		var maxX = (geom.pageW > geom.windowW) ? geom.pageW - geom.windowW : 0;
		var maxY = (geom.pageH > geom.windowH) ? geom.pageH - geom.windowH : 0;
		var posX = Math.round((x + this.offsetX) * geom.zoom);
		var posY = Math.round((y + this.offsetY) * geom.zoom);
		this.toPos = {
			X : (posX < 0) ? 0 : (posX > maxX) ? maxX : posX,
			Y : (posY < 0) ? 0 : (posY > maxY) ? maxY : posY
		}
		this.start();
	}
}

/**
 * scroll to the position of the specified element node.
 * @param {BAElement} node    element node as scroll destination
 */
BASmoothScroll.prototype.scrollToNode = function(node) {
	if (!node || node.instanceOf != 'BAElement') {
		throw 'BASmoothScroll.scrollToNode: first argument must be a BAElement node';
	} else {
		var nodePos = node.getAbsoluteOffsetBA();
		this.scrollTo(nodePos.X, nodePos.Y);
	}
}

/**
 * start scrolling.
 * @private
 */
BASmoothScroll.prototype.start = function() {
	this.clearTimer();

	var geom     = BAGetGeometry();
	this.fromPos = {
		X : geom.scrollX,
		Y : geom.scrollY
	};
	if (this.fromPos.X != this.toPos.X || this.fromPos.Y != this.toPos.Y) {
		this.elapseTimer = new BATimer;
		this.easingTimer = new BASetInterval(this.scrollProcess, this.interval, this);
		this.doCallBack('onStart'   , this.fromPos.X, this.fromPos.Y);
	} else {
		this.doCallBack('onComplete', geom.scrollX, geom.scrollY);
	}
}

/**
 * scrolling process.
 * @private
 */
BASmoothScroll.prototype.scrollProcess = function() {
	var elapse    = Math.min(this.elapseTimer.getTime(), this.duration);
	var distance  = {
		X : this.toPos.X - this.fromPos.X,
		Y : this.toPos.Y - this.fromPos.Y
	};
	var factor    = {
		X : (distance.X > 0) ? 1 : -1,
		Y : (distance.Y > 0) ? 1 : -1
	};
	var newPos    = {
		X : this.fromPos.X + this.easingFunc(elapse, 0, Math.abs(distance.X), this.duration) * factor.X,
		Y : this.fromPos.Y + this.easingFunc(elapse, 0, Math.abs(distance.Y), this.duration) * factor.Y
	};
	var completed = {
		X : Boolean(Math.abs(this.toPos.X - newPos.X) < 1),
		Y : Boolean(Math.abs(this.toPos.Y - newPos.Y) < 1)
	};
	if (elapse < this.duration && (!completed.X || !completed.Y)) {
		this.realScrollTo(newPos.X, newPos.Y);
		this.doCallBack('onScroll', newPos.X, newPos.Y);
	} else {
		this.realScrollTo(this.toPos.X, this.toPos.Y);
		this.doCallBack('onScroll', this.toPos.X, this.toPos.Y);
		this.stop();
	}
}

/**
 * scrolling real process.
 * @param {Number} x    X-coordinate of the scroll destination (px)
 * @param {Number} y    Y-coordinate of the scroll destination (px)
 * @private
 */
BASmoothScroll.prototype.realScrollTo = function(x, y) {
	window.scrollTo(x, y);
}

/**
 * stop scrolling.
 * @private
 */
BASmoothScroll.prototype.stop = function() {
	if (this.elapseTimer) {
		this.clearTimer();
		var geom = BAGetGeometry();
		this.doCallBack('onComplete', geom.scrollX, geom.scrollY);
	}
}

/**
 * abort scrolling.
 */
BASmoothScroll.prototype.abort = function() {
	if (this.elapseTimer) {
		this.clearTimer();
		var geom = BAGetGeometry();
		this.doCallBack('onAbort'   , geom.scrollX, geom.scrollY);
	}
}

/**
 * set easing function.
 * @param {Function} func    easing function.
 */
BASmoothScroll.prototype.setEasingFunc = function(func) {
	if (typeof func == 'function' && typeof func(0, 0, 0, 0) == 'number') {
		this.easingFunc = func;
	}
}

/**
 * easing function - this can be replaced by using {@link #setEasingFunc}.
 * @param {Number} t    current time (seconds/milliseconds) (required)
 * @param {Number} b    beginning value (required)
 * @param {Number} c    change in value (required)
 * @param {Number} d    duration (seconds/milliseconds) (required)
 * @return solution value of the easing equation
 * @type Number
 * @private
 */
BASmoothScroll.prototype.easingFunc = function(t, b, c, d) {
	// quartic easing out
	var ts = (t /= d) * t;
	var tc = ts * t;
	return b + c * (tc * ts + -5 * ts * ts + 10 * tc -10 * ts + 5 * t);
}

/**
 * clear animation timers.
 * @private
 */
BASmoothScroll.prototype.clearTimer = function() {
	if (this.elapseTimer) {
		this.elapseTimer = null;
	}
	if (this.easingTimer) {
		this.easingTimer.clearTimer();
		this.easingTimer = null;
	}
}






/* -------------------- Constructor : BASmoothScrollField inherits BASmoothScroll -------------------- */
/**
 * provide smooth scroll behavior for the scrollable part domain. (*** MacIE not work ***)
 * @class smooth scroller for the page
 * @constructor
 * @param {BAElement} node        element node that constitutes scroll domain (required)
 * @param {Number}    offsetX     X-distance from original scroll destination position (px)
 * @param {Number}    offsetY     Y-distance from original scroll destination position (px)
 * @param {Number}    duration    animation duration (ms)
 * @param {Number}    interval    easing interval (ms)
 * @param {Function}  function    easing function
 */
/*!
 *  arguments of callback 'onStart(x, y)' :
 *      {Number} x    current scroll position (X-coordinate) (px)
 *      {Number} y    current scroll position (Y-coordinate) (px)
 * 
 *  arguments of callback 'onScroll(x, y)' :
 *      {Number} x    current scroll position (X-coordinate) (px)
 *      {Number} y    current scroll position (Y-coordinate) (px)
 * 
 *  arguments of callback 'onComplete(x, y)' :
 *      {Number} x    current scroll position (X-coordinate) (px)
 *      {Number} y    current scroll position (Y-coordinate) (px)
 */
function BASmoothScrollField(node, offsetX, offsetY, duration, interval, func) {
	if (!node || node.instanceOf != 'BAElement') {
		throw 'BASmoothScrollField: first argument must be a BAElement node.';
	}

	/** element node that constitutes scroll domain
	    @type BAElement @const */
	this.node    = node;
	/** X-distance from original scroll destination position (px)
	    @type Number */
	this.offsetX     = (typeof offsetX == 'number') ? offsetX  : 0;
	/** Y-distance from original scroll destination position (px)
	    @type Number */
	this.offsetY     = (typeof offsetY == 'number') ? offsetY  : 0;
	/** animation duration time (ms).
	    @type Number @private */
	this.duration    = (duration > 0) ? duration : 1500;
	/** easing interval seconds (ms).
	    @type Number @private */
	this.interval    = (interval > 0) ? interval : 5;
	/** associative array of coordinate { X, Y } of scroll starting position (px).
	    @type Object @private */
	this.fromPos     = { X : 0, Y : 0 };
	/** associative array of coordinate { X, Y } of scroll destination posision (px).
	    @type Object @private */
	this.toPos       = { X : 0, Y : 0 };
	/** animation interval timer.
	    @type BASetInterval @private */
	this.easingTimer = null;
	/** animation elapsed timer.
	    @type BATimer @private */
	this.elapseTimer = null;

	if (BA.env.isDOMReady) {
		this.setEasingFunc(func);
	}
}

BASmoothScrollField.prototype = new BASmoothScroll;

/**
 * scroll to the specified coordinate.
 * @param {Number} x    X-coordinate of the scroll destination (px)
 * @param {Number} y    Y-coordinate of the scroll destination (px)
 */
BASmoothScrollField.prototype.scrollTo = function(x, y) {
	if (isNaN(x) || isNaN(y)) {
		throw 'BASmoothScrollField.scrollTo: arguments must be a number';
	} else {
		this.abort();
		var geom = BAGetGeometry();
		var maxX = this.node.scrollWidth  - this.node.offsetWidth ; if (maxX < 0) maxX = 0;
		var maxY = this.node.scrollHeight - this.node.offsetHeight; if (maxY < 0) maxY = 0;
		var posX = Math.round((x + this.offsetX) * geom.zoom);
		var posY = Math.round((y + this.offsetY) * geom.zoom);
		this.toPos = {
			X : (posX < 0) ? 0 : (posX > maxX) ? maxX : posX,
			Y : (posY < 0) ? 0 : (posY > maxY) ? maxY : posY
		}
		this.start();
	}
}

/**
 * scroll to the position of the specified element node.
 * @param {BAElement} node    element node as scroll destination
 */
BASmoothScrollField.prototype.scrollToNode = function(node) {
	if (!node || node.instanceOf != 'BAElement') {
		throw 'BASmoothScrollField.scrollToNode: first argument must be a BAElement node';
	} else {
		var fieldPos = this.node.getAbsoluteOffsetBA();
		var nodePos  =      node.getAbsoluteOffsetBA();
		this.scrollTo(nodePos.X - fieldPos.X, nodePos.Y - fieldPos.Y);
	}
}

/**
 * start scrolling.
 * @private
 */
BASmoothScrollField.prototype.start = function() {
	this.clearTimer();

	this.fromPos = {
		X : this.node.scrollLeft,
		Y : this.node.scrollTop
	};
	if (this.fromPos.X != this.toPos.X || this.fromPos.Y != this.toPos.Y) {
		this.elapseTimer = new BATimer;
		this.easingTimer = new BASetInterval(this.scrollProcess, this.interval, this);
		this.doCallBack('onStart', this.fromPos.X, this.fromPos.Y);
	} 
}

/**
 * scrolling real process.
 * @param {Number} x    X-coordinate of the scroll destination (px)
 * @param {Number} y    Y-coordinate of the scroll destination (px)
 * @private
 */
BASmoothScrollField.prototype.realScrollTo = function(x, y) {
	this.node.scrollLeft = x;
	this.node.scrollTop  = y;
}






/* -------------------- Function : BASmoothScrollAutoSetup inherit BASmoothScroll -------------------- */
/**
 * provide smooth scroll behavior for the pages with auto setup event handlers.
 * (this is usually used as a single object 'BA_SMOOTHSCROLL_AS'.)
 * @class smooth scroller for the page with auto setup event handlers
 * @constructor
 */
function BASmoothScrollAutoSetup() {
	/** X-distance from original scroll destination position (px)
	    @type Number */
	this.offsetX     = (BA_SMOOTHSCROLL_AS_OFFSET_X > 0) ? BA_SMOOTHSCROLL_AS_OFFSET_X : 0;
	/** Y-distance from original scroll destination position (px)
	    @type Number */
	this.offsetY     = (BA_SMOOTHSCROLL_AS_OFFSET_Y > 0) ? BA_SMOOTHSCROLL_AS_OFFSET_Y : 0;
	/** animation duration time (ms).
	    @type Number @private */
	this.duration    = (BA_SMOOTHSCROLL_AS_DURATION > 0) ? BA_SMOOTHSCROLL_AS_DURATION : 1500;
	/** easing interval seconds (ms).
	    @type Number @private */
	this.interval    = (BA_SMOOTHSCROLL_AS_INTERVAL > 0) ? BA_SMOOTHSCROLL_AS_INTERVAL : 5;
	/** associative array of coordinate { X, Y } of scroll starting position (px).
	    @type Object @private */
	this.fromPos     = { X : 0, Y : 0 };
	/** associative array of coordinate { X, Y } of scroll destination posision (px).
	    @type Object @private */
	this.toPos       = { X : 0, Y : 0 };
	/** animation interval timer.
	    @type BASetInterval @private */
	this.easingTimer = null;
	/** animation elapsed timer.
	    @type BATimer @private */
	this.elapseTimer = null;
	/** anchor element node which started scroll.
	    @type BAElement */
	this.fromNode = null;

	if (BA.env.isDOMReady) {
		this.setEasingFunc(BA_SMOOTHSCROLL_AS_EASING_FUNC);
		this.init();
	}
}

BASmoothScrollAutoSetup.prototype = new BASmoothScroll;

/**
 * create scrolling destination nodes, setup event handlers.
 * @private
 */
BASmoothScrollAutoSetup.prototype.init = function() {
	// setup event handlers
	document.addEventListenerBA('click'     , this.abort, this);
	document.addEventListenerBA('mousewheel', this.abort, this);

	if (BA_SMOOTHSCROLL_AS_FIND_ATONCE) {
		var nodes = BAConcatNodeList(['a', 'area'].map(function(tagName) { return document.getElementsByTagNameBA(tagName) }));
		nodes.forEach(function(node) {
			node.addEventListenerBA('click', function(e) {
				if (!this.timer) {
					this.startByAnchor(e.currentTarget, e);
				}
			}, this);
		}, this);
	} else {
		document.addEventListenerBA('click', function(e) {
			if (!this.timer) {
				var node = e.target;
				if (['a', 'area'].indexOf(node.nodeName.toLowerCase()) == -1) {
					node = node.getAncestorsByTagNameBA('a')[0] || node.getAncestorsByTagNameBA('area')[0];
				}
				if (node) {
					this.startByAnchor(node, e);
				}
			}
		}, this);
	}

	// setup callback funcs
	if (BA_SMOOTHSCROLL_AS_USE_POSTPROCESS) {
		this.addCallBack('onComplete', BA_SMOOTHSCROLL_AS_POSTPROCESS_FUNC);
	}
}

/**
 * @param {BAElement} anchor    anchor element node which is clicked to start scroll. (required)
 * @param {Event}     e         event object.
 * @private
 */
BASmoothScrollAutoSetup.prototype.startByAnchor = function(anchor, e) {
	var flagID = this.getInternalLinkFragmentID(anchor);
	if (flagID) {
		var target = document.getElementByIdBA(flagID) || document.getElementsByNameBA(flagID)[0];
		if (target && this.validateAnchor(anchor)) {
			anchor.blur();
			if (e) {
				e.preventDefault();
				e.stopPropagation();
			}
			this.fromNode = anchor;
			this.scrollToNode(target);
		}
	}
}

/**
 * get fragment-id of link target of the anchor that links to a same page.
 * @param {BAElement} node    anchor element node (a, area) (required)
 * @return fragment-id, if the anchor links to a same page. not then returns null string.
 * @type String
 * @private
 */
BASmoothScrollAutoSetup.prototype.getInternalLinkFragmentID = function(node) {
	var ret = '';
	if (node && typeof node.nodeType == 'number') {
		var href = node.getAttributeBA('href');
		if (href && href.match(/#/)) {
			href = href.split('#');
			if (!href[0] || href[0] == location.href.split('#')[0]) {
				ret = href[1];
			}
		}
	}
	return ret;
}

/**
 * validate the clicked anchor whether it should start smooth scroll.
 * @param {BAElement} node    anchor element node (a, area) (required)
 * @return true if it should start smooth scroll
 * @type Boolean
 * @private
 */
BASmoothScrollAutoSetup.prototype.validateAnchor = function (node) {
	return !node.hasClassNameBA(BA_SMOOTHSCROLL_AS_IGNORE_NODE_CNAME);
}






/* -------------------- Main : register start-up -------------------- */

if (typeof BA == 'object' && BA.ua.isDOMReady && BA_SMOOTHSCROLL_AS_ENABLED) {
	BAAddOnload(function() {
		BA_SMOOTHSCROLL_AS = BASingleton(BASmoothScrollAutoSetup);
	});
}

