/*
 *  Satpathy.Financial.js
 *
 *  Copyright (C) 2005 Gautam Satpathy
 *  gautam@satpathy.in
 *  www.satpathy.in
 *
 *  This program is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public License
 *  as published by the Free Software Foundation; either version 2
 *  of the License, or (at your option) any later version.
 *
 *  This program 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 Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 *  Converted to Javascript by Ruslan Gainutdinov <contact@ruslan.org>
 */

var GoalSeekStatus = {};

GoalSeekStatus.GOAL_SEEK_OK = 1;
GoalSeekStatus.GOAL_SEEK_ERROR = 2;

GoalSeekStatus.create = function ( st, value ) {
	var v = {};
	v.seekStatus = st ? st : GoalSeekStatus.GOAL_SEEK_ERROR;
	v.returnData = value;
	return v;		
}

var GoalSeek =  {};

GoalSeek.debug = true ;

GoalSeek.update_data = function ( x, y, data ) {
	if (y > 0) {
		if (data.havexpos) {
			if (data.havexneg) {
				/*
				 *  When we have pos and neg, prefer the new point only
				 *  if it makes the pos-neg x-internal smaller.
				 */
				if (Math.abs(x - data.xneg) < Math.abs(data.xpos - data.xneg)) {
					data.xpos = x;
					data.ypos = y;
				}
			}
			else if (y < data.ypos) {
				/* We have pos only and our neg y is closer to zero.  */
				data.xpos = x;
				data.ypos = y;
			}
		}
		else {
			data.xpos = x;
			data.ypos = y;
			data.havexpos = true  ;
		}
		return false  ;
	}
	else if (y < 0) {
		if (data.havexneg) {
			if (data.havexpos) {
				/*
				 * When we have pos and neg, prefer the new point only
				 * if it makes the pos-neg x-internal smaller.
				 */
				if (Math.abs(x - data.xpos) < Math.abs(data.xpos - data.xneg)) {
					data.xneg = x;
					data.yneg = y;
				}
			}
			else if (-y < -data.yneg) {
				/* We have neg only and our neg y is closer to zero.  */
				data.xneg = x;
				data.yneg = y;
			}

		}
		else {
			data.xneg = x;
			data.yneg = y;
			data.havexneg = true;
		}
		return false  ;
	}
	else {
		/* Lucky guess...  */
		data.have_root = true  ;
		data.root = x  ;
		return true  ;
	}
}

/*
 *  Calculate a reasonable approximation to the derivative of a function
 *  in a single point.
 */
GoalSeek.fake_df = function (f, x, xstep, data, userData) {
	var          xl ;
	var          xr ;
	var          yl ;
	var          yr ;
	var          dfx ;
	var  status = GoalSeekStatus.create();

	if ( GoalSeek.debug ) {
		GoalSeek.log("fake_df (x = " + x +", xstep = " + xstep + ")") ;
	}

	xl = x - xstep;
	if (xl < data.xmin)
		xl = x;

	xr = x + xstep;
	if (xr > data.xmax)
		xr = x;

	if (xl == xr) {
		if ( GoalSeek.debug ) {
			GoalSeek.log("==> xl == xr") ;
		}
		return GoalSeekStatus.create( GoalSeekStatus.GOAL_SEEK_ERROR, null ) ;
	}

	status = f.f( xl, userData ) ; //yl, userData ) ;
	if ( status.seekStatus != GoalSeekStatus.GOAL_SEEK_OK ) {
		if ( GoalSeek.debug ) {
			GoalSeek.log("==> failure at xl\n") ;
		}
		return status;
	}
	yl = status.returnData ;
	if ( GoalSeek.debug ) {
		GoalSeek.log("==> xl = " + xl + " ; yl =" + yl) ;
	}

	status = f.f( xr, userData ) ;  //yr, userData ) ;
	if (status.seekStatus != GoalSeekStatus.GOAL_SEEK_OK) {
		if ( GoalSeek.debug ) {
			GoalSeek.log("==> failure at xr") ;
		}
		return status;
	}
	yr = status.returnData ;
	if ( GoalSeek.debug ) {
		GoalSeek.log("==> xr = " + xr + " ; yr = " + yr) ;
	}

	dfx = (yr - yl) / (xr - xl) ;
	if ( GoalSeek.debug ) {
		GoalSeek.log("==> " + dfx) ;
	}

	return !isFinite(dfx) ?
	       GoalSeekStatus.create(GoalSeekStatus.GOAL_SEEK_ERROR, null) :
	       GoalSeekStatus.create(GoalSeekStatus.GOAL_SEEK_OK, dfx) ;
}

/**
 *  Initialize a GoalSeekData object.
 */
GoalSeek.goal_seek_initialize = function ( data ) 	{
	if (!data) {
		data = {};
	}
	data.havexpos = data.havexneg = data.have_root = false;
	data.xpos = data.xneg = data.root = NaN ; //gnm_nan;
	data.ypos = data.yneg = NaN ; //gnm_nan ;
	data.xmin = -1e10;
	data.xmax = +1e10;
	data.precision = 1e-10;
	return data;
}

/**
 *  Seek a goal (root) using Newton's iterative method.
 *
 *  The supplied function must (should) be continously differentiable in
 *  the supplied interval.  If NULL is used for `df', this function will
 *  estimate the derivative.
 *
 *  This method will find a root rapidly provided the initial guess, x0,
 *  is sufficiently close to the root.  (The number of significant digits
 *  (asympotically) goes like i^2 unless the root is a multiple root in
 *  which case it is only like c*i.)
 */
GoalSeek.goalSeekNewton = function (f, df, data, userData, x0 ) {
	var iterations;
	var precision = data.precision / 2;

	if ( data.have_root )   {
		return GoalSeekStatus.create( GoalSeekStatus.GOAL_SEEK_OK, data.root ) ;
	}

	if ( GoalSeek.debug ) {
		GoalSeek.log("goalSeekNewton") ;
	}

	for (iterations = 0; iterations < 20; iterations++) {
		var x1 ;
        var y0 ;
        var df0 ;
        var stepsize ;
		var status = GoalSeekStatus.create();
    	if ( GoalSeek.debug ) {
			GoalSeek.log("goalSeekNewton - x0 = " + x0 + ", (i = " + iterations + " )") ;
		}
		//  Check whether we have left the valid interval.
		if ( x0 < data.xmin || x0 > data.xmax ) {
			return GoalSeekStatus.create( GoalSeekStatus.GOAL_SEEK_ERROR, null );
		}
		status = f.f(x0, userData ) ;
		if ( status.seekStatus != GoalSeekStatus.GOAL_SEEK_OK )   {
			return status ;
		}

		y0 = status.returnData ;
		if ( GoalSeek.debug ) {
			GoalSeek.log("   y0 = " + y0) ;
		}
		if (GoalSeek.update_data (x0, y0, data) )    {
			return GoalSeekStatus.create( GoalSeekStatus.GOAL_SEEK_OK, data.root ) ;
        }

		if ( df != null ) {
			status = df.f( x0, userData ) ;
		}
		else {
			var xstep;
			if ( Math.abs(x0) < 1e-10 ) {
				if (data.havexneg && data.havexpos)
					xstep = Math.abs(data.xpos - data.xneg) / 1e6;
				else
					xstep = (data.xmax - data.xmin) / 1e6;
			}
			else    {
				xstep = Math.abs(x0) / 1e6;
			}
			status = GoalSeek.fake_df(f, x0, xstep, data, userData) ;
		}
		if ( status.seekStatus != GoalSeekStatus.GOAL_SEEK_OK )    {
			return status;
		}

		df0 = status.returnData ;
		//  If we hit a flat spot, we are in trouble.
		if ( df0 == 0 ) {
			return GoalSeekStatus.create(GoalSeekStatus.GOAL_SEEK_ERROR, null);
		}

		/*
		 * Overshoot slightly to prevent us from staying on
		 * just one side of the root.
		 */
		x1 = x0 - 1.000001 * y0 / df0;
		stepsize = Math.abs(x1 - x0) / (Math.abs(x0) + Math.abs(x1)) ;
		if ( GoalSeek.debug ) {
			GoalSeek.log("   df0 = " + df0) ;
			GoalSeek.log("   ss = " + stepsize) ;
		}

		x0 = x1;

		if ( stepsize < precision ) {
			data.root = x0;
			data.have_root = true;
			return GoalSeekStatus.create( GoalSeekStatus.GOAL_SEEK_OK, data.root ) ;
		}
	}

	return new GoalSeekStatus( GoalSeekStatus.GOAL_SEEK_ERROR, null ) ;
}

/**
 *  Log a message to the console.
 *
 *  @param message
 */
GoalSeek.log = function ( message ) {
	if (window.console && window.console.log) {
		console.log( message ) ;
	}
}

Satpathy = {};

Satpathy.Financial = {};

Satpathy.Financial.debug = GoalSeek.debug;

Satpathy.Financial.log = function ( message ) {
	if (window.console && window.console.log) {
		console.log( message ) ;
	}
}

Satpathy.Financial.XIRRNPV_iterator = function (rate, list) {	
	var sum = 0;
	var n = list.length;
	
	for ( var i = 0; i < n; i++ ) {
		var d = list[i].time - list[0].time;
		
		if ( d < 0 )  {
			return GoalSeekStatus.create( GoalSeekStatus.GOAL_SEEK_ERROR, null);
		}
		
	    sum += list[i].amount / Math.pow(rate, d / 365.0);
	}
	
	return GoalSeekStatus.create( GoalSeekStatus.GOAL_SEEK_OK, sum );
}

// Normalize date to double
Satpathy.Financial.convertDate = function (d2) {
	//FIXME: Somehow differs from Java version???
	var d1 = new Date("Dec, 30, 1899 00:00:00");
	// d1.setFullYear(1900);
	var diff = 0;
	// diff = Calendar.getDaysDiff(d1, d2);
	diff = (d1.getTime() / 1000.0 - d2.getTime() / 1000.0);
	diff = diff / (60 * 60 * 24);
	Satpathy.Financial.log("Convert date: " + d1 + ", " + d2 + ": " + diff); 
	return Math.abs(diff);
}

Satpathy.Financial.normalizeDate = function (pay, firstTime) {
	// no normalization here needed
}

Satpathy.Financial._xirr = function (list, guess) {
	var data ;
	var  status ;
	var          result ;
	var          rate0 ;
	var             n ;
	var             d_n ;

	data = GoalSeek.goal_seek_initialize() ;
	data.xmin   = -1;
	data.xmax   = Math.min( 1000, data.xmax ) ;
	rate0       = guess ;

	var func = {};
	func.f = Satpathy.Financial.XIRRNPV_iterator;	
	status = GoalSeek.goalSeekNewton(func, null, data, list, rate0);

	if (status.seekStatus == GoalSeekStatus.GOAL_SEEK_OK)  {
		result = status.returnData;
	} else {
		result = NaN ;
	}

	if (Satpathy.Financial.debug) {
		Satpathy.Financial.log( "XIRR Result - " + result ) ;
	}
	
	return (result != NaN) ? (result - 1) : result ;
}

Satpathy.Financial.xirr = function(dates, values, guess) {
	var v = [];
	if (!guess) {
		guess = 0.3;
	}

	for (var i = 0; i < dates.length; i++) {
		var p = {};
		p.amount = values[i];
		var dt = dates[i];		
		if (!dt) {
		    (window.error || alert)("Date at index: " + i + " is null");
		    return NaN;
		}
		p.date = dt;
		p.time = Satpathy.Financial.convertDate(dt);		
		
		v[v.length] = p;
	}
	
	if (Satpathy.Financial.debug) {
		for (var i = 0; i < v.length; i++) {
			Satpathy.Financial.log(i + ";" + v[i].date + ";" + v[i].time + ";" + v[i].amount);
		}
	}
	
	return Satpathy.Financial._xirr(v, guess);
}

