package com.jwetherell.algorithms.sequence;

/**
 * In mathematics, the Fibonacci numbers are the numbers in the following integer sequence, called the Fibonacci sequence, and characterized by the fact that every number after the first two is the 
 * sum of the two preceding ones: 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, ...
 * <p>
 * 
 * https://en.wikipedia.org/wiki/Fibonacci_number
 * <br>
 * @author Justin Wetherell <phishman3579@gmail.com>
 */
public class FibonacciSequence {

    private static final double INVERSE_SQUARE_ROOT_OF_5 = 1 / Math.sqrt(5); // Inverse of the square root of 5
    private static final double PHI = (1 + Math.sqrt(5)) / 2; // Golden ratio

    private FibonacciSequence() {} 

    public static final long fibonacciSequenceUsingLoop(int n) {
        final long[] array = new long[n + 1];
        int counter = 0;
        while (counter <= n) {
            long r = 0;
            if (counter > 1) {
                r = array[counter - 1] + array[counter - 2];
            } else if (counter == 1) {
                r = 1;
            }
            // If r goes below zero then we have run out of bits in the long
            if (r < 0)
                throw new IllegalArgumentException("Run out of bits in long, n="+n);
            array[counter] = r;
            counter++;
        }

        return array[n];
    }

    /**
     * Recursion with memoization
     */
    public static final long fibonacciSequenceUsingRecursion(int n) {
        // Using the array to store values already computed
        final long[] array = new long[n + 1];
        return fibonacciSequenceUsingRecursion(array,n);
    }

    private static final long fibonacciSequenceUsingRecursion(long[] array, int n) {
        if (n == 0 || n == 1) 
            return n;

        // If array already has a value then it has previously been computed
        if (array[n] != 0)
            return array[n];

        final String exception = "Run out of bits in long, n="+n;

        final long r1 = fibonacciSequenceUsingRecursion(array, (n - 1));
        array[n-1] = r1; // memoization
        // If r1 goes below zero then we have run out of bits in the long
        if (r1 < 0)
            throw new IllegalArgumentException(exception);

        final long r2 = fibonacciSequenceUsingRecursion(array, (n - 2));
        array[n-2] = r2; // memoization
        // If r2 goes below zero then we have run out of bits in the long
        if (r2 < 0)
            throw new IllegalArgumentException(exception);

        final long r = r1 + r2;
        // If r goes below zero then we have run out of bits in the long
        if (r < 0)
            throw new IllegalArgumentException("Run out of bits in long, n="+n);

        array[n] = r; // memoization

        return r;
    }

    public static final long fibonacciSequenceUsingMatrixMultiplication(int n) {
        // m = [ 1 , 1 ]
        //     [ 1 , 0 ]
        final long[][] matrix = new long[2][2];
        matrix[0][0] = 1;
        matrix[0][1] = 1;
        matrix[1][0] = 1;
        matrix[1][1] = 0;

        long[][] temp = new long[2][2];
        temp[0][0] = 1;
        temp[0][1] = 1;
        temp[1][0] = 1;
        temp[1][1] = 0;

        int counter = n;
        while (counter > 0) {
            temp = multiplyMatrices(matrix, temp);
            // Subtract an additional 1 the first time in the loop because the
            // first multiplication is actually n -= 2 since it multiplying two matrices
            counter -= (counter == n) ? 2 : 1;
        }
        final long r = temp[0][1];
        // If r goes below zero then we have run out of bits in the long
        if (r < 0)
            throw new IllegalArgumentException("Run out of bits in long, n="+n);
        return r;
    }

    private static final long[][] multiplyMatrices(long[][] A, long[][] B) {
        final long a = A[0][0];
        final long b = A[0][1];
        final long c = A[1][0];
        final long d = A[1][1];

        final long e = B[0][0];
        final long f = B[0][1];
        final long g = B[1][0];
        final long h = B[1][1];

        B[0][0] = a * e + b * g;
        B[0][1] = a * f + b * h;
        B[1][0] = c * e + d * g;
        B[1][1] = c * f + d * h;

        return B;
    }

    public static final long fibonacciSequenceUsingBinetsFormula(int n) {
        final long r = (long) Math.floor(Math.pow(PHI, n) * INVERSE_SQUARE_ROOT_OF_5 + 0.5);
        // If r hits max value then we have run out of bits in the long
        if (r == Long.MAX_VALUE)
            throw new IllegalArgumentException("Run out of bits in long, n="+n);
        return r;
    }
}
