package math;

import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;

import math.equation.Polynomial;
import math.matrix.Vector;
import math.number.BigRational;
import math.number.Rational;

public class Series{
	
	public static ArrayList<Long> factors(long a){
		ArrayList<Long> data = new ArrayList<Long>();
		long i;
        // for each potential factor i
        for(i=2; i*i<=a; i++){
            // if i is a factor of a, repeatedly divide it out
            while(a % i == 0) {
                data.add(i); 
                a /= i;
            }
        }
        if(a>1) data.add(a);
        return data;
	}
	
	/**
	 * Compute Fibonacci number using Dijkstra's recurrence:<br />
	 * F<sub>2n-1</sub> = F<sub>n-1</sub><sup>2</sup> + F<sub>n</sub><sup>2</sup><br />
	 * F<sub>2n</sub> = (2 F<sub>n-1</sub> + F<sub>n</sub>) F<sub>n</sub><br />.
	 * Fibonacci 10 = 55.
	 * Fibonacci 20 = 6765.
	 * @param n
	 * @return Fibonacci number
	 * @see #fibonaccis(int)
	 * @see <a href="http://www.cs.utexas.edu/users/EWD/ewd06xx/EWD654.PDF">http://www.cs.utexas.edu/users/EWD/ewd06xx/EWD654.PDF</a>
	 * @see <a href="http://www.maths.surrey.ac.uk/hosted-sites/R.Knott/Fibonacci/fibFormula.html">http://www.maths.surrey.ac.uk/hosted-sites/R.Knott/Fibonacci/fibFormula.html</a>
	 */
	public static long fibonacci(int n){
		return fibonaccis(n)[n];
	}
	
	/**
	 * Compute Fibonacci numbers using Dijkstra's recurrence:<br />
	 * F<sub>2n-1</sub> = F<sub>n-1</sub><sup>2</sup> + F<sub>n</sub><sup>2</sup><br />
	 * F<sub>2n</sub> = (2 F<sub>n-1</sub> + F<sub>n</sub>) F<sub>n</sub><br />
	 * @param n
	 * @return Fibonacci numbers
	 * @see <a href="http://www.cs.utexas.edu/users/EWD/ewd06xx/EWD654.PDF">http://www.cs.utexas.edu/users/EWD/ewd06xx/EWD654.PDF</a>
	 * @see <a href="http://www.maths.surrey.ac.uk/hosted-sites/R.Knott/Fibonacci/fibFormula.html">http://www.maths.surrey.ac.uk/hosted-sites/R.Knott/Fibonacci/fibFormula.html</a>
	 */
	public static long[] fibonaccis(int n){
        if(n<1 || n>92) throw new RuntimeException("N must be between 1 and 92");
        long[] fib = new long[n+1];
        // base cases
        fib[0] = 0;
        fib[1] = 1;
        // bottom-up dynamic programming
        for(int i = 2; i <= n; i++)
            fib[i] = fib[i-1] + fib[i-2];
		return fib;
	}

    public static BigInteger fibonacci(BigInteger n) {
    	HashMap<BigInteger, BigInteger> cache = new HashMap<BigInteger, BigInteger>();
    	BigInteger ONE  = BigInteger.ONE;
    	BigInteger ZERO = BigInteger.ZERO;
      
        if (n.equals(ZERO)) return ZERO;
        if (n.equals(ONE))  return ONE;
        if (cache.containsKey(n)) return cache.get(n);

        // odd
        if (n.testBit(0)) {
            BigInteger n2 = n.shiftRight(1);
            BigInteger n3 = n2.add(ONE);
            BigInteger result = fibonacci(n2).multiply(fibonacci(n2)).add(fibonacci(n3).multiply(fibonacci(n3)));
            cache.put(n, result);
            return result;
        }

        // even
        else {
            BigInteger n2 = n.shiftRight(1);
            BigInteger n3 = n2.subtract(ONE);
            BigInteger result = fibonacci(n2).multiply(fibonacci(n2).add(fibonacci(n3).add(fibonacci(n3))));
            cache.put(n, result);
            return result;
        }
    }
	
	/**
	 * n-th harmonic number: 1/1 + 1/2 + ... + 1/n.
	 * @param n
	 * @return n-th harmonic number
	 */
	public static Vector harmonics(int n){
		int i;
        double[] data = new double[n];
        for(i=1; i<=n; i++){
        	data[i-1] = 1.0/i;
        }
        return new Vector(data);
	}
	
	/**
	 * n-th harmonic number: 1/1 + 1/2 + ... + 1/n.
	 * @param n
	 * @return n-th harmonic number
	 */
	public static BigRational harmonic(int n){
		int i;
        BigRational sum = BigRational.ZERO;
        for(i=1; i<=n; i++){
            sum = sum.plus(new BigRational(1,i));
        }
        return sum;
	}
	
// Geometric
	
	/**
	 * The sum of a geometric series.
	 * a+ar+ar<sup>2</sup>+ar<sup>3</sup>+&hellip;
	 * @param a start term
	 * @param r common ratio
	 * @return sum
	 */
	public static double geometricSum(double a, double r){
		if(r >= 1) return Double.POSITIVE_INFINITY;
		if(r <=-1) return Double.NaN;
		return a/(1-r);
	}
	
	/**
	 * The sum of a geometric series.
	 * a+ar+ar<sup>2</sup>+ar<sup>3</sup>+&hellip;
	 * @param a start term
	 * @param r common ratio
	 * @return sum
	 */
	public static double geometricSum(Rational a, Rational r){
		if(r.compareTo(new Rational(1)) >= 0) return Double.POSITIVE_INFINITY;
		if(r.compareTo(new Rational(-1))<= 0) return Double.NaN;
		return a.over(new Rational(1).minus(r)).toDouble();
	}
	
	/**
	 * The sum of a geometric series.
	 * a+ar+ar<sup>2</sup>+ar<sup>3</sup>+&hellip;+ar<sup>n-1</sup>.
	 * @param a start term
	 * @param r common ratio
	 * @param n first n terms
	 * @return sum
	 */
	public static double geometricSum(double a, double r, int n){
		return a*(1-Math.pow(r, n))/(1-r);
	}
	
// Taylor 
	/**
	 * sin(x) using Taylor expansion.
	 * sin x = x - x<sup>3</sup>/3! + x<sup>5</sup>/5! - x<sup>7</sup>/7! + ...
	 * <p>
	 * Note: sin(&pi;/6) = sin(0.523598775598299...) = 1/2
	 * <p>
	 * It use identity sin(x) = sin(x + 2 &pi;) to pre-process
	 * x to be between -2 &pi; and 2 &pi;.
	 * @param x
	 * @return sin(x) using Taylor series
	 */
	public static double sinT(double x){
		int i;
        // convert x to an angle between -2 PI and 2 PI
        x = x % (2 * Math.PI);
        // compute the Taylor series approximation
        double term = 1.0; // ith term = x^i / i!
        double sum  = 0.0; // sum of first i terms in taylor series
        for(i=1; term!=0.0; i++){
            term *= (x / i);
            if(i%4 == 1) sum += term;
            if(i%4 == 3) sum -= term;
        }
        return sum;
	}

	
	/**
	 * compute coeffients for Taylor series e<sup>x</sup> = 1 + x + x<sup>2</sup>/2! + x<sup>3</sup>/3! + ...
	 * which ends on the n-th (30th) term.
	 * @return e<sup>x</sup> = 1 + x + x<sup>2</sup>/2! + x<sup>3</sup>/3! + ... + x<sup>n</sup>/n!
	 */
	public static Polynomial eT(){
        return eT(30);
	}
	
	/**
	 * compute coeffients for Taylor series e<sup>x</sup> = 1 + x + x<sup>2</sup>/2! + x<sup>3</sup>/3! + ...
	 * which ends on the n-th term.
	 * @param n
	 * @return e<sup>x</sup> = 1 + x + x<sup>2</sup>/2! + x<sup>3</sup>/3! + ... + x<sup>n</sup>/n!
	 */
	public static Polynomial eT(int n){
		int i;
        double[] p = new double[n];
        p[0] = 1;
        for(i=1; i<n; i++)
            p[i] = p[i-1] / i;
        return new Polynomial(p,false);
	}
	
	/**
	 * compute coeffients for Taylor series e<sup>x</sup> = 1 + x + x<sup>2</sup>/2! + x<sup>3</sup>/3! + ...
	 * which ends on the n-th (30th) term.
	 * @param x
	 * @return e<sup>x</sup> = 1 + x + x<sup>2</sup>/2! + x<sup>3</sup>/3! + ... + x<sup>n</sup>/n!
	 */
	public static double eT(double x){
        return eT().evaluate(x);
	}
	
	/**
	 * Sum s = 1/1 + 1/4 + 1/9 + 1/16 + ...
	 * The series sums to &pi;<sup>2</sup> / 6 so &radic;(6*s) should be an approximation
	 * to &pi; = 3.1415926535897932384626.
	 * @param n
	 * @return the sum s = 1/1 + 1/4 + 1/9 + 1/16 + ... + 1/n<sup>2</sup>
	 */
	public static double sumReciprocalsSquares(int n){
		int i;
		double sum = 0.0;
		for(i=1; i<=n; i++)     
			sum += 1 / (1.0 * i * i);     // good - multiply by 1.0 to cast to double
			// sum += 1.0 / (i * i);      // bad  - overflow if N > 65,535
			// sum += 1 / (i * i);        // bad  - integer division
			// sum += 1.0 / i * i;        // bad  - computes (1.0 / i) * i
		return sum;
	}
	
    public static void main(String[] args) {
    	System.out.println("Factors of "+4444444444l+" = "+factors(4444444444l));
    	System.out.println();
    	System.out.println("Fibonacci:");
        System.out.println(fibonacci(new BigInteger("10")));
        System.out.println(Arrays.toString(fibonaccis(10)));
        System.out.println();
        System.out.println("sin(0.523598775598299) = "+sinT(0.523598775598299));
        
        System.out.println();
        System.out.println(eT().evaluate(0));
        System.out.println(eT().evaluate(1));
        
        System.out.println("\ngeometrix sum:");
        System.out.println("a=1, r=-1 : "+geometricSum(1, -1)+" : "+new Double(geometricSum(1, -1)).equals(Double.NaN));
        System.out.println("a=1/2, r=1/2 : "+geometricSum(1./2, 1./2)+" : "+(geometricSum(1./2, 1./2)==1));
        System.out.println("a=1, r=2/2 : "+geometricSum(1, 2./3)+" : "+(geometricSum(1, 2./3)==3));
        System.out.println("a=1, r=2/3 : "+geometricSum(new Rational(1), new Rational(2,3))+" : "+(geometricSum(new Rational(1), new Rational(2,3))==3));
        System.out.println("a=1, r=1 : "+geometricSum(1, 1)+" : "+(geometricSum(1, 1)==Double.POSITIVE_INFINITY));
        System.out.println("a=1, r=1 : "+geometricSum(new Rational(1), new Rational(1))+" : "+(geometricSum(new Rational(1), new Rational(1))==Double.POSITIVE_INFINITY));
        
        System.out.println();
        System.out.println("Fibonacci(10) = " + fibonacci(10));
        System.out.println("Fibonacci(10) = " + fibonaccis(10));
    }
}
