/*
 * Copyright 2013-2014 Richard M. Hightower
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *  		http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * __________                              _____          __   .__
 * \______   \ ____   ____   ____   /\    /     \ _____  |  | _|__| ____    ____
 *  |    |  _//  _ \ /  _ \ /    \  \/   /  \ /  \\__  \ |  |/ /  |/    \  / ___\
 *  |    |   (  <_> |  <_> )   |  \ /\  /    Y    \/ __ \|    <|  |   |  \/ /_/  >
 *  |______  /\____/ \____/|___|  / \/  \____|__  (____  /__|_ \__|___|  /\___  /
 *         \/                   \/              \/     \/     \/       \//_____/
 *      ____.                     ___________   _____    ______________.___.
 *     |    |____ ___  _______    \_   _____/  /  _  \  /   _____/\__  |   |
 *     |    \__  \\  \/ /\__  \    |    __)_  /  /_\  \ \_____  \  /   |   |
 * /\__|    |/ __ \\   /  / __ \_  |        \/    |    \/        \ \____   |
 * \________(____  /\_/  (____  / /_______  /\____|__  /_______  / / ______|
 *               \/           \/          \/         \/        \/  \/
 */

package org.boon.primitive;

import org.boon.Exceptions;
import org.boon.Universal;
import org.boon.core.reflection.Invoker;

import java.lang.invoke.ConstantCallSite;
import java.lang.invoke.MethodHandle;
import java.lang.reflect.Method;
import java.util.Arrays;

import static org.boon.Exceptions.die;
import static org.boon.Exceptions.handle;

public class Flt {

    public static float[] grow( float[] array, final int size ) {
        Exceptions.requireNonNull( array );

        float[] newArray = new float[ array.length + size ];
        System.arraycopy( array, 0, newArray, 0, array.length );
        return newArray;
    }

    public static float[] grow( float[] array ) {
        Exceptions.requireNonNull( array );

        float[] newArray = new float[ array.length * 2 ];
        System.arraycopy( array, 0, newArray, 0, array.length );
        return newArray;
    }

    public static float[] shrink( float[] array, int size ) {
        Exceptions.requireNonNull( array );

        float[] newArray = new float[ array.length - size ];

        System.arraycopy( array, 0, newArray, 0, array.length - size );
        return newArray;
    }

    public static float[] compact( float[] array ) {
        Exceptions.requireNonNull( array );

        int nullCount = 0;
        for ( float ch : array ) {

            if ( ch == '\0' ) {
                nullCount++;
            }
        }
        float[] newArray = new float[ array.length - nullCount ];

        int j = 0;
        for ( float ch : array ) {

            if ( ch == '\0' ) {
                continue;
            }

            newArray[ j ] = ch;
            j++;
        }
        return newArray;
    }

    /**
     * Creates an array of bytes
     *
     * @param size size of the array you want to make
     * @return
     */
    public static float[] arrayOfFloat( final int size ) {
        return new float[ size ];
    }

    /**
     * @param array
     * @return
     */
    @Universal
    public static float[] array( final float... array ) {
        Exceptions.requireNonNull( array );
        return array;
    }

    @Universal
    public static int lengthOf( float[] array ) {
        return len(array);
    }

    @Universal
    public static int len( float[] array ) {
        return array.length;
    }

    @Universal
    public static float atIndex( final float[] array, final int index ) {
        return idx(array, index);
    }

    @Universal
    public static float idx( final float[] array, final int index ) {
        final int i = calculateIndex( array, index );

        return array[ i ];
    }

    @Universal
    public static void atIndex( final float[] array, int index, float value ) {
        idx (array, index, value);
    }

    @Universal
    public static void idx( final float[] array, int index, float value ) {
        final int i = calculateIndex( array, index );

        array[ i ] = value;
    }

    @Universal
    public static float[] sliceOf( float[] array, int startIndex, int endIndex ) {

        return slc(array, startIndex, endIndex);
    }

    @Universal
    public static float[] slc( float[] array, int startIndex, int endIndex ) {

        final int start = calculateIndex( array, startIndex );
        final int end = calculateEndIndex( array, endIndex );
        final int newLength = end - start;

        if ( newLength < 0 ) {
            throw new ArrayIndexOutOfBoundsException(
                    String.format( "start index %d, end index %d, length %d",
                            startIndex, endIndex, array.length )
            );
        }

        float[] newArray = new float[ newLength ];
        System.arraycopy( array, start, newArray, 0, newLength );
        return newArray;
    }

    @Universal
    public static float[] sliceOf( float[] array, int startIndex ) {

        return slc(array, startIndex);
    }

    @Universal
    public static float[] slc( float[] array, int startIndex ) {

        final int start = calculateIndex( array, startIndex );
        final int newLength = array.length - start;

        if ( newLength < 0 ) {
            throw new ArrayIndexOutOfBoundsException(
                    String.format( "start index %d, length %d",
                            startIndex, array.length )
            );
        }

        float[] newArray = new float[ newLength ];
        System.arraycopy( array, start, newArray, 0, newLength );
        return newArray;
    }

    @Universal
    public static float[] endOfSlice( float[] array, int endIndex ) {
        return slcEnd(array, endIndex);
    }

    @Universal
    public static float[] slcEnd( float[] array, int endIndex ) {

        final int end = calculateEndIndex( array, endIndex );
        final int newLength = end; // +    (endIndex < 0 ? 1 : 0);

        if ( newLength < 0 ) {
            throw new ArrayIndexOutOfBoundsException(
                    String.format( "start index %d, length %d",
                            endIndex, array.length )
            );
        }

        float[] newArray = new float[ newLength ];
        System.arraycopy( array, 0, newArray, 0, newLength );
        return newArray;
    }

    @Universal
    public static boolean in( float value, float[] array ) {
        for ( float currentValue : array ) {
            if ( currentValue == value ) {
                return true;
            }
        }
        return false;
    }

    @Universal
    public static float[] copy( float[] array ) {
        Exceptions.requireNonNull( array );
        float[] newArray = new float[ array.length ];
        System.arraycopy( array, 0, newArray, 0, array.length );
        return newArray;
    }

    @Universal
    public static float[] add( float[] array, float v ) {
        Exceptions.requireNonNull( array );
        float[] newArray = new float[ array.length + 1 ];
        System.arraycopy( array, 0, newArray, 0, array.length );
        newArray[ array.length ] = v;
        return newArray;
    }

    @Universal
    public static float[] add( float[] array, float[] array2 ) {
        Exceptions.requireNonNull( array );
        float[] newArray = new float[ array.length + array2.length ];
        System.arraycopy( array, 0, newArray, 0, array.length );
        System.arraycopy( array2, 0, newArray, array.length, array2.length );
        return newArray;
    }

    @Universal
    public static float[] insert( final float[] array, final int idx, final float v ) {

        if ( idx >= array.length ) {
            return add( array, v );
        }

        final int index = calculateIndex( array, idx );

        //Object newArray = Array.newInstance(array.getClass().getComponentType(), array.length+1);
        float[] newArray = new float[ array.length + 1 ];

        if ( index != 0 ) {
            /* Copy up to the length in the array before the index. */
            /*                 src     sbegin  dst       dbegin   length of copy */
            System.arraycopy( array, 0, newArray, 0, index );
        }

        boolean lastIndex = index == array.length - 1;
        int remainingIndex = array.length - index;

        if ( lastIndex ) {
            /* Copy the area after the insert. Make sure we don't write over the end. */
            /*                 src  sbegin   dst       dbegin     length of copy */
            System.arraycopy( array, index, newArray, index + 1, remainingIndex );

        } else {
            /* Copy the area after the insert.  */
            /*                 src  sbegin   dst       dbegin     length of copy */
            System.arraycopy( array, index, newArray, index + 1, remainingIndex );

        }

        newArray[ index ] = v;
        return newArray;
    }

    @Universal
    public static float[] insert( final float[] array, final int fromIndex, final float[] values ) {
        Exceptions.requireNonNull( array );

        if ( fromIndex >= array.length ) {
            return add( array, values );
        }

        final int index = calculateIndex( array, fromIndex );

        //Object newArray = Array.newInstance(array.getClass().getComponentType(), array.length+1);
        float[] newArray = new float[ array.length + values.length ];

        if ( index != 0 ) {
            /* Copy up to the length in the array before the index. */
            /*                 src     sbegin  dst       dbegin   length of copy */
            System.arraycopy( array, 0, newArray, 0, index );
        }

        boolean lastIndex = index == array.length - 1;

        int toIndex = index + values.length;
        int remainingIndex = newArray.length - toIndex;

        if ( lastIndex ) {
            /* Copy the area after the insert. Make sure we don't write over the end. */
            /*                 src  sbegin   dst       dbegin     length of copy */
            System.arraycopy( array, index, newArray, index + values.length, remainingIndex );

        } else {
            /* Copy the area after the insert.  */
            /*                 src  sbegin   dst       dbegin     length of copy */
            System.arraycopy( array, index, newArray, index + values.length, remainingIndex );

        }

        for ( int i = index, j = 0; i < toIndex; i++, j++ ) {
            newArray[ i ] = values[ j ];
        }
        return newArray;
    }

    /* End universal methods. */
    private static int calculateIndex( float[] array, int originalIndex ) {
        final int length = array.length;

        int index = originalIndex;

        /* Adjust for reading from the right as in
        -1 reads the 4th element if the length is 5
         */
        if ( index < 0 ) {
            index = length + index;
        }

        /* Bounds check
            if it is still less than 0, then they
            have an negative index that is greater than length
         */
         /* Bounds check
            if it is still less than 0, then they
            have an negative index that is greater than length
         */
        if ( index < 0 ) {
            index = 0;
        }
        if ( index >= length ) {
            index = length - 1;
        }
        return index;
    }

    /* End universal methods. */
    private static int calculateEndIndex( float[] array, int originalIndex ) {
        final int length = array.length;

        int index = originalIndex;

        /* Adjust for reading from the right as in
        -1 reads the 4th element if the length is 5
         */
        if ( index < 0 ) {
            index = length + index;
        }

        /* Bounds check
            if it is still less than 0, then they
            have an negative index that is greater than length
         */
         /* Bounds check
            if it is still less than 0, then they
            have an negative index that is greater than length
         */
        if ( index < 0 ) {
            index = 0;
        }
        if ( index > length ) {
            index = length;
        }
        return index;
    }

    /**
     * Checks to see if two arrays are equals
     * @param expected expected array
     * @param got got array
     * @return true if equal or throws exception if not.
     */
    public static boolean equalsOrDie(float[] expected, float[] got) {

        if (expected.length != got.length) {
            die("Lengths did not match, expected length", expected.length,
                    "but got", got.length);
        }

        for (int index=0; index< expected.length; index++) {
            if (expected[index]!= got[index]) {
                die("value at index did not match index", index , "expected value",
                        expected[index],
                        "but got", got[index]);

            }
        }
        return true;
    }

    /**
     * Checks to see if two arrays are equals
     * @param expected expected array
     * @param got got array
     * @return true if equal or false if not.
     */
    public static boolean equals(float[] expected, float[] got) {

        if (expected.length != got.length) {
            return false;
        }

        for (int index=0; index< expected.length; index++) {
            if (expected[index]!= got[index]) {
                return false;
            }
        }
        return true;
    }

    /** Public interface for a very fast reduce by. */
    public static interface ReduceBy {
        double reduce(double sum, float value);
    }

    /**
     * A very fast reduce by.
     * If performance is your thing, this seems to be as fast a plain for loop when benchmarking with JMH.
     *
     * @param array array of items to reduce by
     * @param reduceBy reduceBy interface
     * @return the final value
     */
    public static double reduceBy( final float[] array, ReduceBy reduceBy ) {

        double sum = 0;
        for ( float v : array ) {
            sum = reduceBy.reduce(sum, v);
        }
        return sum;
    }

    /**
     *
     * @param array array of items to reduce by
     * @param start where to start in the array
     * @param length where to end in the array
     * @param reduceBy the function to do the reduce by
     * @return the reduction
     */
    public static double reduceBy( final float[] array, final int start, final int length, ReduceBy reduceBy ) {

        double sum = 0;

        for (int index = start; index < length; index++) {
            float v = array[index];
            sum = reduceBy.reduce(sum, v);
        }
        return sum;
    }

    /**
     *
     * @param array array of items to reduce by
     * @param length where to end in the array
     * @param reduceBy the function to do the reduce by
     * @return the reduction
     */
    public static double reduceBy( final float[] array, final int length, ReduceBy reduceBy ) {

        double sum = 0;

        for (int index = 0; index < length; index++) {
            float v = array[index];
            sum = reduceBy.reduce(sum, v);
        }
        return sum;
    }

    /**
     * Reduce by functional support for int arrays.
     * @param array array of items to reduce by
     * @param object object that contains the reduce by function
     * @param <T> the type of object
     * @return the final reduction
     */
    public  static <T> double reduceBy( final float[] array, T object ) {
        if (object.getClass().isAnonymousClass()) {
            return reduceByR(array, object );
        }

        try {
            ConstantCallSite callSite = Invoker.invokeReducerLongIntReturnLongMethodHandle(object);
            MethodHandle methodHandle = callSite.dynamicInvoker();
            try {

                double sum = 0;
                for ( float v : array ) {
                    sum = (double) methodHandle.invokeExact( sum, v );

                }
                return sum;
            } catch (Throwable throwable) {
                return handle(Long.class, throwable, "Unable to perform reduceBy");
            }
        } catch (Exception ex) {
            return reduceByR(array, object);
        }

    }

    /**
     * Reduce by functional support for int arrays.
     * @param array array of items to reduce by
     * @param object object that contains the reduce by function
     * @param <T> the type of object
     * @return the final reduction
     */
    public static <T> double reduceBy( final float[] array, T object, String methodName ) {

        if (object.getClass().isAnonymousClass()) {
            return reduceByR(array, object, methodName);
        }

        try {
            ConstantCallSite callSite = Invoker.invokeReducerLongIntReturnLongMethodHandle(object, methodName);
            MethodHandle methodHandle = callSite.dynamicInvoker();
            try {

                double sum = 0;
                for ( float v : array ) {
                    sum = (double) methodHandle.invokeExact( sum, v );

                }
                return sum;
            } catch (Throwable throwable) {
                return handle(Long.class, throwable, "Unable to perform reduceBy");
            }
        } catch (Exception ex) {
            return reduceByR(array, object, methodName);
        }

    }

    /**
     * Fallback to reflection if the call-site will not work or did not work
     * @param array array of items to reduce by
     * @param object function object
     * @param <T> type of function object.
     * @return
     */
    private  static <T> double reduceByR( final float[] array, T object ) {
        try {

            Method method = Invoker.invokeReducerLongIntReturnLongMethod(object);

            double sum = 0;
            for ( float v : array ) {
                sum = (double) method.invoke(object, sum, v);

            }
            return sum;

        } catch (Throwable throwable) {
            return handle(Long.class, throwable, "Unable to perform reduceBy");
        }

    }

    /**
     * Reflection based reduce by.
     * @param array array of items to reduce by
     * @param object function
     * @param methodName name of method
     * @param <T> type of function
     * @return reduction
     */
    private  static <T> double reduceByR( final float[] array, T object, String methodName ) {
        try {

            Method method = Invoker.invokeReducerLongIntReturnLongMethod(object, methodName);

            double sum = 0;
            for ( float v : array ) {
                sum = (double) method.invoke(object, sum, v);

            }
            return sum;

        } catch (Throwable throwable) {
            return handle(Long.class, throwable, "Unable to perform reduceBy");
        }

    }

    /**
     * Reflection based reduce by.
     * @param array array of items to reduce by
     * @param object function
     * @param methodName name of method
     * @param <T> type of function
     * @return reduction
     */
    private  static <T> double reduceByR( final float[] array, int length, T object, String methodName ) {
        try {

            Method method = Invoker.invokeReducerLongIntReturnLongMethod(object, methodName);

            double sum = 0;
            for (int index=0; index< length; index++) {
                float v = array[index];
                sum = (double) method.invoke(object, sum, v);

            }
            return sum;

        } catch (Throwable throwable) {
            return handle(Long.class, throwable, "Unable to perform reduceBy");
        }

    }

    /**
     * Reflection based reduce by.
     * @param array array of items to reduce by
     * @param object function
     * @param <T> type of function
     * @return reduction
     */
    private  static <T> double reduceByR( final float[] array, int length, T object ) {
        try {

            Method method = Invoker.invokeReducerLongIntReturnLongMethod(object);

            double sum = 0;
            for (int index=0; index< length; index++) {
                float v = array[index];
                sum = (double) method.invoke(object, sum, v);

            }
            return sum;

        } catch (Throwable throwable) {
            return handle(Long.class, throwable, "Unable to perform reduceBy");
        }

    }

    /**
     * Reduce By
     * @param array array of items to reduce by
     * @param length where to end in the array
     * @param object function
     * @return reduction
     */
    public static double reduceBy( final float[] array,  int length,
                                 Object object ) {

        if (object.getClass().isAnonymousClass()) {
            return reduceByR(array, length, object );
        }

        try {
            ConstantCallSite callSite = Invoker.invokeReducerLongIntReturnLongMethodHandle(object );
            MethodHandle methodHandle = callSite.dynamicInvoker();
            try {

                double sum = 0;
                for (int index=0; index < length; index++) {
                    float v = array[index];
                    sum = (double) methodHandle.invokeExact( sum, v );

                }
                return sum;
            } catch (Throwable throwable) {
                return handle(Long.class, throwable, "Unable to perform reduceBy");
            }
        } catch (Exception ex) {
            return reduceByR(array, length, object );
        }

    }

    /**
     * Reduce By
     * @param array array of items to reduce by
     * @param length where to end in the array
     * @param function function
     * @param function functionName
     * @return reduction
     */
    public static double reduceBy( final float[] array,  int length,
                                 Object function, String functionName ) {

        if (function.getClass().isAnonymousClass()) {
            return reduceByR(array, length, function, functionName );
        }

        try {
            ConstantCallSite callSite = Invoker.invokeReducerLongIntReturnLongMethodHandle(function, functionName );
            MethodHandle methodHandle = callSite.dynamicInvoker();
            try {

                double sum = 0;
                for (int index=0; index < length; index++) {
                    float v = array[index];
                    sum = (double) methodHandle.invokeExact( sum, v );

                }
                return sum;
            } catch (Throwable throwable) {
                return handle(Long.class, throwable, "Unable to perform reduceBy");
            }
        } catch (Exception ex) {
            return reduceByR(array, length, function, functionName );
        }

    }

    /**
     * Reduce By
     * @param array array of items to reduce by
     * @param length where to end in the array
     * @param object function
     * @return reduction
     */
    public static double reduceBy( final float[] array, int start, int length,
                                 Object object ) {

        if (object.getClass().isAnonymousClass()) {
            return reduceByR(array, object );
        }

        try {
            ConstantCallSite callSite = Invoker.invokeReducerLongIntReturnLongMethodHandle(object );
            MethodHandle methodHandle = callSite.dynamicInvoker();
            try {

                double sum = 0;
                for (int index=start; index < length; index++) {
                    float v = array[index];
                    sum = (double) methodHandle.invokeExact( sum, v );

                }
                return sum;
            } catch (Throwable throwable) {
                return handle(Long.class, throwable, "Unable to perform reduceBy");
            }
        } catch (Exception ex) {
            return reduceByR(array, object );
        }

    }

    /**
     * Checks to see if two values are the same
     * @param expected expected value
     * @param got got value
     * @return true if equal throws exception if not equal
     */
    public static boolean equalsOrDie(float expected, float got) {
        if (expected != got) {
            return die(Boolean.class, "Expected was", expected, "but we got ", got);
        }
        return true;
    }

    /**
     * Compares two values
     * @param expected expected value
     * @param got got value
     * @return true or false
     */
    public static boolean equals(float expected, float got) {

        return expected == got;
    }

    /**
     * Sum
     * Provides overflow protection.
     * @param values values in int
     * @return sum
     */
    public static double sum( float[] values ) {
        return sum( values, 0, values.length);
    }

    /**
     * Sum
     * Provides overflow protection.
     * @param values values in int
     * @return sum
     */
    public static float sum( float[] values,  int length ) {
        return sum( values, 0, length);
    }

    /**
     * Sum
     * Provides overflow protection.
     * @param values values in int
     * @return sum
     */
    public static float sum( float[] values, int start, int length ) {
        double sum = 0;
        for (int index = start; index < length; index++ ) {
            sum+= values[index];
        }

        if (sum < Float.MIN_VALUE) {
            die ("overflow the sum is too small", sum);
        }

        if (sum > Float.MAX_VALUE) {
            die ("overflow the sum is too big", sum);
        }

        return (float) sum;

    }

    /**
     * Sum
     * Provides overflow protection.
     * @param values values in int
     * @return sum
     */
    public static double bigSum( float[] values ) {
        return bigSum(values, 0, values.length);
    }

    /**
     * Sum
     * Provides overflow protection.
     * @param values values in int
     * @return sum
     */
    public static double bigSum( float[] values,  int length ) {
        return bigSum(values, 0, length);
    }

    /**
     * Big Sum
     * @param values values in int
     * @return sum
     */
    public static double bigSum( float[] values, int start, int length ) {
        double sum = 0;
        for (int index = start; index < length; index++ ) {
            sum+= values[index];
        }

        return sum;

    }

    /**
     * Max
     * @param values values in int
     * @return max
     */
    public static float max( float[] values, final int start, final int length ) {
        float max = Float.MIN_VALUE;
        for (int index = start; index < length; index++ ) {
            if ( values[index] > max ) {
                max = values[index];
            }
        }

        return max;
    }

    /**
     * max
     * @param values values in int
     * @return max
     */
    public static float max( float[] values ) {
        return max(values, 0, values.length);
    }

    /**
     * max
     * @param values values in int
     * @return max
     */
    public static float max( float[] values, int length ) {
        return max(values, 0, length);
    }

    /**
     * Min
     * @param values values in int
     * @return min
     */
    public static float min( float[] values, final int start, final int length ) {
        float min = Float.MAX_VALUE;
        for (int index = start; index < length; index++ ) {
            if (values[index] < min) min = values[index];
        }
        return min;
    }

    /**
     * Min
     * @param values values in int
     * @return min
     */
    public static float min( float[] values ) {
        return min(values, 0, values.length);
    }

    /**
     * Min
     * @param values values in int
     * @return min
     */
    public static float min( float[] values, int length ) {
        return min(values, 0, length);
    }

    /**
     * Average
     * @param values values in int
     * @return average
     */
    public static float mean( float[] values, final int start, final int length ) {
        return (float) meanDouble(values, start, length);
    }

    /**
     * Average
     * @param values values in int
     * @return average
     */
    public static float mean( float[] values, final int length ) {
        return (float) meanDouble(values, 0, length);
    }

    /**
     * Average
     * @param values values in int
     * @return average
     */
    public static float mean( float[] values ) {

        return (float) meanDouble(values, 0, values.length);
    }

    /**
     * Calculate Variance.
     *
     * @param values values
     * @param start start
     * @param length length
     * @return variance
     */
    public static float variance(float[] values, final int start, final int length) {
        return (float) varianceDouble(values, start, length);
    }

    private static double meanDouble( float[] values, final int start, final int length ) {
        double mean = ((double)bigSum(values, start, length))/ ((double) length);
        return mean;
    }

    /**
     * Calculate Variance.
     *
     * @param values values
     * @param start start
     * @param length length
     * @return variance
     */
    public static double varianceDouble(float[] values, final int start, final int length) {
        double mean = meanDouble(values, start, length);
        double temp = 0;
        for(int index = start; index < length; index++) {
            double a = values[index];
            temp += (mean-a)*(mean-a);
        }
        return temp / length;
    }

    /**
     * Calculate Variance.
     *
     * @param values values
     * @param length length
     * @return variance
     */
    public static float variance(float[] values,  final int length) {
        return (float) varianceDouble(values, 0, length);
    }

    /**
     * Calculate Variance.
     *
     * @param values values
     * @return variance
     */
    public static float variance(float[] values) {
        return (float) varianceDouble(values, 0, values.length);
    }

    /**
     * Calculate standard deviation.
     *
     * @param values values
     * @param start start
     * @param length length
     * @return standard deviation
     */
    public static float standardDeviation(float[] values, final int start, final int length) {
        double variance = varianceDouble(values, start, length);
        return (float)Math.sqrt(variance);
    }

    /**
     * Calculate standard deviation.
     *
     * @param values values
     * @param length length
     * @return standard deviation
     */
    public static float standardDeviation(float[] values,  final int length) {
        double variance = varianceDouble(values, 0, length);
        return (float)Math.sqrt(variance);
    }

    /**
     * Calculate standard deviation.
     *
     * @param values values
     * @return standard deviation
     */
    public static float standardDeviation(float[] values) {
        double variance = varianceDouble(values, 0, values.length);
        return (float)Math.sqrt(variance);
    }

    /**
     * Calculate Median
     *
     * @param start start
     * @param values values
     * @param length length
     * @return median
     */
    public static float median(float[] values, final int start, final int length) {
        float[] sorted = new float[length];
        System.arraycopy(values, start, sorted, 0, length);
        Arrays.sort(sorted);

        if (length % 2 == 0) {
            int middle = sorted.length / 2;
            double median = (sorted[middle-1] + sorted[middle]) / 2.0;
            return (float) median;
        } else {
            return sorted[sorted.length / 2];
        }
    }

    /**
     * Calculate Median
     * @param values values
     * @param length length
     * @return median
     */
    public static float median(float[] values, final int length) {
        return median(values, 0, length);
    }

    /**
     * Calculate Median
     * @param values values
     * @return median
     */
    public static float median(float[] values) {
        return median(values, 0, values.length);
    }

    /**
     * Checks to see if two arrays are equals
     * @param expected expected array
     * @param got got array
     * @return true if equal or false if not.
     */
    public static boolean equals(int start, int end, float[] expected, float[] got) {

        if (expected.length != got.length) {
            return false;
        }

        for (int index=start; index< end; index++) {
            if (expected[index]!= got[index]) {
                return false;
            }
        }
        return true;
    }

    public static int hashCode(float array[]) {
        if (array == null) {
            return 0;
        }

        int result = 1;
        for (float item : array) {

            int bits = Float.floatToIntBits(item);
            result = 31 * result + bits;
        }

        return result;
    }

    public static int hashCode(int start, int end, float array[]) {
        if (array == null) {
            return 0;
        }

        int result = 1;

        for (int index=start; index< end; index++) {

            int bits = Float.floatToIntBits(array[index]);
            result = 31 * result + bits;
        }

        return result;
    }

}


/*
 * Copyright 2013-2014 Richard M. Hightower
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *  		http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * __________                              _____          __   .__
 * \______   \ ____   ____   ____   /\    /     \ _____  |  | _|__| ____    ____
 *  |    |  _//  _ \ /  _ \ /    \  \/   /  \ /  \\__  \ |  |/ /  |/    \  / ___\
 *  |    |   (  <_> |  <_> )   |  \ /\  /    Y    \/ __ \|    <|  |   |  \/ /_/  >
 *  |______  /\____/ \____/|___|  / \/  \____|__  (____  /__|_ \__|___|  /\___  /
 *         \/                   \/              \/     \/     \/       \//_____/
 *      ____.                     ___________   _____    ______________.___.
 *     |    |____ ___  _______    \_   _____/  /  _  \  /   _____/\__  |   |
 *     |    \__  \\  \/ /\__  \    |    __)_  /  /_\  \ \_____  \  /   |   |
 * /\__|    |/ __ \\   /  / __ \_  |        \/    |    \/        \ \____   |
 * \________(____  /\_/  (____  / /_______  /\____|__  /_______  / / ______|
 *               \/           \/          \/         \/        \/  \/
 */

package org.boon.core;

import org.boon.Exceptions;
import org.boon.Str;
import org.boon.core.reflection.FastStringUtils;
import org.boon.json.JsonException;
import org.boon.primitive.CharBuf;
import org.boon.primitive.CharScanner;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.TimeZone;

public class Dates {

    public static final TimeZone GMT = TimeZone.getTimeZone("GMT");
    private static final TimeZone UTC_TIME_ZONE = TimeZone.getTimeZone( "UTC" );
    private static volatile long lastNow;
    private static long MILLI_SECOND = 1;
    private static long SECOND = MILLI_SECOND * 1000;
    private static long MINUTE = 60 * SECOND;
    private static long HOUR = 60 * MINUTE;
    private static long DAY = 24 * HOUR;
    private static long WEEK = 7 * DAY;
    private static long MONTH = ( long ) ( 30.4167 * DAY );
    private static long YEAR = ( long ) ( 365.2425 * DAY );

    public static long utcNow() {
        long now = System.currentTimeMillis();
        Calendar calendar = Calendar.getInstance();
        calendar.setTimeInMillis( now );
        calendar.setTimeZone( UTC_TIME_ZONE );
        long utcNow = calendar.getTime().getTime();
        lastNow = now;
        return utcNow;
    }

    public static long now() {
        return System.currentTimeMillis();
    }

    public static long timeZoneNow(TimeZone timeZone) {
        Calendar calendar = Calendar.getInstance();
        return timeZoneNow(timeZone);
    }

    public static long timeZoneNow(TimeZone timeZone, Calendar calendar) {
        long now = System.currentTimeMillis();
        calendar.setTimeInMillis( now );
        calendar.setTimeZone( timeZone );
        long timeZoneNow = calendar.getTime().getTime();
        return timeZoneNow;
    }

    public static Calendar utcCalendar() {
        Calendar calendar = Calendar.getInstance();
        calendar.setTimeZone( UTC_TIME_ZONE );
        return  calendar;
    }

    public static int durationInHours(long to, long from) {
        long duration = Math.abs ( to - from );

        return  (int) (duration / HOUR);

    }

    public static int durationInMinutes(long to, long from) {
        long duration = Math.abs ( to - from );

        return  (int) (duration / MINUTE);

    }

    public static long durationInSeconds(long to, long from) {
        long duration = Math.abs ( to - from );

        return  (int) (duration / SECOND);

    }

    public static long durationInMilis(long to, long from) {
        long duration = Math.abs ( to - from );

        return  (int) (duration / MILLI_SECOND);

    }

    public static long utcNowFast(Calendar utcCalendar) {
        long now = Sys.time();
        long utcNow = utcCalendar.getTime().getTime();
        lastNow = now;
        return utcNow;
    }

    public static long utc( long time ) {
        Calendar calendar = Calendar.getInstance();
        calendar.setTimeInMillis( time );
        calendar.setTimeZone( UTC_TIME_ZONE );
        long utcNow = calendar.getTime().getTime();
        lastNow = time;
        return utcNow;
    }

    public static long utcFast( long time, Calendar utcCalendar ) {
        long utcNow = utcCalendar.getTime().getTime();
        lastNow = time;
        return utcNow;
    }

    /**
     * For testing only, avoids potential timing issue.
     */
    static long lastNow() {
        return lastNow;
    }

    public static long fromUtcTimeToTimeZone( long utcTime, TimeZone timeZone ) {

        Calendar calendar = Calendar.getInstance( UTC_TIME_ZONE );
        calendar.setTimeInMillis( utcTime );
        calendar.setTimeZone( timeZone );
        return calendar.getTime().getTime();
    }

    public static boolean before( long isThis, long beforeThis ) {
        return isThis < beforeThis;
    }

    public static boolean after( long isThis, long afterThis ) {
        return isThis > afterThis;
    }

    public static long hourDuration (int count ) {
        return count * HOUR;
    }

    public static long minuteDuration (int count ) {
        return count * MINUTE;
    }

    public static long secondDuration (int count ) {
        return count * SECOND;
    }

    public static long dayDuration (int count ) {
        return count * DAY;
    }

    public static long secondsFrom( long time, int seconds ) {
        return time + ( seconds * SECOND );
    }

    public static long minutesFrom( long time, int minutes ) {
        return time + ( minutes * MINUTE );
    }

    public static long hoursFrom( long time, int hours ) {
        return time + ( hours * HOUR );
    }

    public static long daysFrom( long time, int days ) {
        return time + ( days * DAY );
    }

    public static long weeksFrom( long time, int weeks ) {
        return time + ( weeks * WEEK );
    }

    public static long monthsFrom( long time, int months ) {
        return time + ( months * MONTH );
    }

    public static long yearsFrom( long time, int years ) {
        return time + ( years * YEAR );
    }

    public static long utcDate( int year, int month, int day ) {
        Calendar calendar = Calendar.getInstance();

        /* Set to midnight. */
        midnight( calendar );

        /* This might change the date, but when you convert it
        back to the clocktime timezone, it will be correct.
         */
        calendar.setTimeZone( UTC_TIME_ZONE );

        return internalDate( year, month, day, calendar );
    }

    public static long utcDate( int year, int month, int day,
                                int hour, int minute ) {
        Calendar calendar = Calendar.getInstance();
        midnight( calendar );

        /* This might change the date, but when you convert it
        back to the clocktime timezone, it will be correct.
         */
        calendar.setTimeZone( UTC_TIME_ZONE );

        return internalDateLong( year, month, day, hour, minute, calendar );
    }

    private static long internalDateLong( int year, int month, int day, int hour, int minute, Calendar calendar ) {

        return internalDate( year, month, day, hour, minute, calendar ).getTime();

    }

    private static Date internalDate( int year, int month, int day, int hour, int minute, Calendar calendar ) {
        calendar.set( Calendar.YEAR, year );
        calendar.set( Calendar.MONTH, month );
        calendar.set( Calendar.DAY_OF_MONTH, day );
        calendar.set( Calendar.HOUR_OF_DAY, hour );
        calendar.set( Calendar.MINUTE, minute );
        calendar.set( Calendar.SECOND, 0 );
        calendar.set( Calendar.MILLISECOND, 0 );

        return calendar.getTime();
    }

    private static Date internalDate( TimeZone tz, int year, int month, int day, int hour, int minute, int second ) {

        Calendar calendar = Calendar.getInstance();

        calendar.set( Calendar.YEAR, year );
        calendar.set( Calendar.MONTH, month - 1 );
        calendar.set( Calendar.DAY_OF_MONTH, day );
        calendar.set( Calendar.HOUR_OF_DAY, hour );
        calendar.set( Calendar.MINUTE, minute );
        calendar.set( Calendar.SECOND, second );
        calendar.set( Calendar.MILLISECOND, 0 );

        calendar.setTimeZone( tz );

        return calendar.getTime();
    }

    private static Date internalDate( TimeZone tz, int year, int month, int day, int hour,
                                      int minute, int second, int miliseconds ) {

        Calendar calendar = Calendar.getInstance();

        calendar.set( Calendar.YEAR, year );
        calendar.set( Calendar.MONTH, month - 1 );
        calendar.set( Calendar.DAY_OF_MONTH, day );
        calendar.set( Calendar.HOUR_OF_DAY, hour );
        calendar.set( Calendar.MINUTE, minute );
        calendar.set( Calendar.SECOND, second );
        calendar.set( Calendar.MILLISECOND, miliseconds );

        calendar.setTimeZone( tz );

        return calendar.getTime();
    }

    public static long wallTimeDate( int year, int month, int day ) {
        Calendar calendar = Calendar.getInstance();

        /* Set to midnight. */
        midnight( calendar );

        return internalDate( year, month, day, calendar );
    }

    public static long date( int year, int month, int day ) {
        return utcDate( year, month, day );
    }

    public static long date( int year, int month, int day,
                             int hour, int minute ) {
        return utcDate( year, month, day, hour, minute );

    }

    public static long date( TimeZone tz, int year, int month, int day ) {
        Calendar calendar = Calendar.getInstance();

        /* Set to midnight. */
        midnight( calendar );

        calendar.setTimeZone( tz );

        return internalDate( year, month, day, calendar );
    }

    private static long internalDate( int year, int month, int day, Calendar calendar ) {
        calendar.set( Calendar.YEAR, year );
        calendar.set( Calendar.MONTH, month );
        calendar.set( Calendar.DAY_OF_MONTH, day );

        calendar.set( Calendar.HOUR_OF_DAY, 0 );

        calendar.set( Calendar.MINUTE, 0 );

        calendar.set( Calendar.SECOND, 0 );

        calendar.set( Calendar.MILLISECOND, 0 );

        return calendar.getTime().getTime();
    }

    public static long wallTimeDate( int year, int month, int day,
                                     int hour, int minute ) {
        Calendar calendar = Calendar.getInstance();
        midnight( calendar );

        return internalDateLong( year, month, day, hour, minute, calendar );
    }

    public static Date toDate( TimeZone tz, int year, int month, int day,
                               int hour, int minute, int second ) {
        return internalDate( tz, year, month, day, hour, minute, second );
    }

    public static Date toDate( TimeZone tz, int year, int month, int day,
                               int hour, int minute, int second, int miliseconds ) {
        return internalDate( tz, year, month, day, hour, minute, second, miliseconds );
    }

    public static Date toDate( int year, int month, int day,
                               int hour, int minute, int second, int miliseconds ) {
        return internalDate( TimeZone.getDefault(), year, month, day, hour, minute, second, miliseconds );
    }

    public static long date( TimeZone tz, int year, int month, int day,
                             int hour, int minute ) {
        Calendar calendar = Calendar.getInstance();
        midnight( calendar );
        calendar.setTimeZone( tz );

        return internalDateLong( year, month, day, hour, minute, calendar );
    }

    private static void midnight( Calendar calendar ) {
        /* Set to midnight. */
        calendar.set( Calendar.HOUR_OF_DAY, 0 );
        calendar.set( Calendar.MINUTE, 0 );
        calendar.set( Calendar.SECOND, 0 );
        calendar.set( Calendar.MILLISECOND, 0 );
    }

    /**
     * Useful for generating string versions of timestamps
     *
     * @return euro style format.
     */
    public static String euroUTCSystemDateNowString() {
        long now = System.currentTimeMillis();
        return euroUTCSystemDateString( now );
    }

    /**
     * Useful for generated file names and generated work directories.
     *
     * @param timestamp the timestamp
     * @return euro style format.
     */
    public static String euroUTCSystemDateString( long timestamp ) {
        Calendar calendar = Calendar.getInstance();
        calendar.setTimeInMillis( timestamp );
        calendar.setTimeZone( UTC_TIME_ZONE );
        int day = calendar.get( Calendar.DAY_OF_MONTH );
        int month = calendar.get( Calendar.MONTH );
        int year = calendar.get( Calendar.YEAR );
        int hour = calendar.get( Calendar.HOUR_OF_DAY );
        int minute = calendar.get( Calendar.MINUTE );
        int second = calendar.get( Calendar.SECOND );

        CharBuf buf = CharBuf.create( 16 );
        buf.add( Str.zfill ( day, 2 ) ).add( '_' );
        buf.add( Str.zfill( month, 2 ) ).add( '_' );
        buf.add( year ).add( '_' );
        buf.add( Str.zfill( hour, 2 ) ).add( '_' );
        buf.add( Str.zfill( minute, 2 ) ).add( '_' );
        buf.add( Str.zfill( second, 2 ) ).add( "_utc_euro" );

        return buf.toString();
    }

    public static void main( String... args ) {

        Sys.println( euroUTCSystemDateNowString() );

    }

    public static Date year( int year ) {
        Calendar c = Calendar.getInstance();
        c.setTimeZone(GMT);
        c.set( 1970, Calendar.JANUARY, 2, 0, 0, 0 );
        c.set( Calendar.YEAR, year );
        c.set( Calendar.MILLISECOND, 0 );
        return c.getTime();
    }

    public static Date getUSDate( int month, int day, int year ) {
        Calendar c = Calendar.getInstance();
        c.setTimeZone(GMT);
        c.set( year, month - 1, day + 1, 0, 0, 0 );
        c.set( Calendar.MILLISECOND, 0 );
        return c.getTime();
    }

    public static Date getUSDate( int month, int day, int year, int hour, int minute, int second ) {
        Calendar c = Calendar.getInstance();
        c.setTimeZone(GMT);
        c.set( year, month - 1, day + 1, hour, minute, second );
        c.set( Calendar.MILLISECOND, 0 );
        return c.getTime();
    }

    public static Date getEuroDate( int day, int month, int year ) {
        Calendar c = Calendar.getInstance();
        c.setTimeZone(GMT);
        c.set( year, month - 1, day + 1, 0, 0, 0 );
        c.set( Calendar.MILLISECOND, 0 );
        return c.getTime();
    }

    public static Date getEuroDate( int day, int month, int year, int hour, int minute, int second ) {
        Calendar c = Calendar.getInstance();
        c.setTimeZone(GMT);
        c.set( year, month - 1, day + 1, hour, minute, second );
        c.set( Calendar.MILLISECOND, 0 );
        return c.getTime();
    }

    public static Date fromISO8601_( String string ) {

        try {

            return new SimpleDateFormat( "yyyy-MM-dd'T'HH:mm:ssXXX" ).parse( string );
        } catch ( ParseException e ) {
            return Exceptions.handle ( Date.class, "Not a valid ISO8601", e );
        }

    }

    public static Date fromISO8601Jackson_(String string) {

        if (string.length() == 29 && Str.idx(string, -3) == ':') {

            try {

                return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX").parse(string);
            } catch (ParseException e) {
                return Exceptions.handle(Date.class, "Not a valid ISO8601 \"Jackson\" date", e);
            }

        } else {

            try {

                return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ").parse(string);
            } catch (ParseException e) {
                return Exceptions.handle(Date.class, "Not a valid ISO8601 \"Jackson\" date", e);
            }

        }

    }

    public static Date fromJsonDate_( String string ) {

        try {

            return new SimpleDateFormat( "yyyy-MM-dd'T'HH:mm:ss.SSSXXX" ).parse( string );
        } catch ( ParseException e ) {
            return Exceptions.handle( Date.class, "Not a valid JSON date", e );
        }

    }

    public static Date fromJsonDate( String string ) {

        return fromJsonDate( FastStringUtils.toCharArray( string ), 0, string.length() );

    }

    public static Date fromISO8601( String string ) {

        return fromISO8601( FastStringUtils.toCharArray( string ), 0, string.length() );

    }

    public static Date fromISO8601Jackson(String string) {

        return fromISO8601Jackson(FastStringUtils.toCharArray(string), 0, string.length());

    }

    public static Date fromISO8601DateLoose( String string ) {
        return fromISO8601DateLoose( FastStringUtils.toCharArray( string ), 0, string.length() );

    }

    public static Date fromISO8601DateLoose( char [] chars ) {
        return fromISO8601DateLoose( chars, 0, chars.length );

    }

    final static int SHORT_ISO_8601_TIME_LENGTH = "1994-11-05T08:15:30Z".length();
    final static int LONG_ISO_8601_TIME_LENGTH = "1994-11-05T08:15:30-05:00".length();
    final static int LONG_ISO_8601_JACKSON_TIME_LENGTH = "1994-11-05T08:11:22.123-0500".length();
    public final static int JSON_TIME_LENGTH = "2013-12-14T01:55:33.412Z".length();

    /**
     *
     * @param date the timestamp
     * @return json style format.
     */
    public static String jsonDate( Date date ) {
        CharBuf buf = CharBuf.create( JSON_TIME_LENGTH );
        jsonDateChars ( date, buf );
        return buf.toString();
    }

    private final static boolean  isGMT;
    static {
        if (TimeZone.getDefault () == GMT) {
            isGMT = true;
        } else {
            isGMT = false;
        }

    }

    public static void jsonDateChars( Date date, CharBuf buf ) {
        Calendar calendar = Calendar.getInstance();
        calendar.setTimeZone (GMT);
        jsonDateChars( calendar, date, buf );
    }

    public static void jsonDateChars( long milis, CharBuf buf ) {
        Calendar calendar = Calendar.getInstance();
        calendar.setTimeZone (GMT);
        jsonDateChars( calendar, milis, buf );
    }

    public static void jsonDateChars( Calendar calendar, Date date, CharBuf buf ) {
        jsonDateChars(calendar, date.getTime(), buf);
    }

    public static void jsonDateChars( Calendar calendar, long milis, CharBuf buf ) {
        if (isGMT) {
            /* For the Unix admins of the world who avoid it all and just GMT it. */
            fastJsonDateChars ( new Date(milis), buf );
            return;
        }

        calendar.setTimeInMillis( milis );

        int day = calendar.get( Calendar.DAY_OF_MONTH );
        int month = calendar.get( Calendar.MONTH ) +1;
        int year = calendar.get( Calendar.YEAR );
        int hour = calendar.get( Calendar.HOUR_OF_DAY );
        int minute = calendar.get( Calendar.MINUTE );
        int second = calendar.get( Calendar.SECOND );
        int mili = calendar.get( Calendar.MILLISECOND );

        buf.add( '"' );
        buf.add( year ).add( '-' );
        buf.add( Str.zfill( month, 2 ) ).add( '-' );
        buf.add( Str.zfill ( day, 2 ) ).add('T');

        buf.add( Str.zfill( hour, 2 ) ).add( ':' );
        buf.add( Str.zfill( minute, 2 ) ).add( ':' );
        buf.add( Str.zfill( second, 2 ) ).add( "." );
        buf.add( Str.zfill( mili, 3 ) ).add( "Z" );

        buf.add( '"' );

    }

    public static void fastJsonDateChars( Date date, CharBuf buf ) {

        int day = date.getDate ();
        int month = date.getMonth () +1;
        int year = date.getYear () + 1900;
        int hour = date.getHours ();
        int minute = date.getMinutes ();
        int second = date.getSeconds ();
        int offset = date.getTimezoneOffset ();
        int mili = 1;

        buf.add( '"' );
        buf.add( year ).add( '-' );
        buf.add( Str.zfill( month, 2 ) ).add( '-' );
        buf.add( Str.zfill ( day, 2 ) ).add('T');

        buf.add( Str.zfill( hour, 2 ) ).add( ':' );
        buf.add( Str.zfill( minute, 2 ) ).add( ':' );
        buf.add( Str.zfill( second, 2 ) ).add( "." );
        buf.add( Str.zfill( mili, 3 ) ).add( "Z" );

        buf.add( '"' );

    }

    public static Date fromISO8601DateLoose( char[] buffer, int startIndex, int endIndex ) {

        if ( Dates.isISO8601QuickCheck( buffer, startIndex, endIndex ) ) {

            if ( Dates.isJsonDate( buffer, startIndex, endIndex ) ) {
                return Dates.fromJsonDate( buffer, startIndex, endIndex );

            } else if ( Dates.isISO8601( buffer, startIndex, endIndex ) ) {
                return Dates.fromISO8601( buffer, startIndex, endIndex );
            } else {
                try {
                    return looseParse( buffer, startIndex, endIndex );
                } catch ( Exception ex ) {
                    throw new JsonException( "unable to do a loose parse", ex );
                }
            }
        } else {

            try {
                return looseParse( buffer, startIndex, endIndex );
            } catch ( Exception ex ) {
                throw new JsonException( "unable to do a loose parse", ex );
            }
        }

    }

    private static Date looseParse( char[] buffer, int startIndex, int endIndex ) {
        final char[][] parts = CharScanner.splitByCharsNoneEmpty( buffer, startIndex, endIndex, '-', ':', 'T', '.' );
        int year = 0;
        int month = 0;
        int day = 0;

        int hour = 0;
        int minutes = 0;
        int seconds = 0;

        int mili = 0;

        if ( parts.length >= 3 ) {
            year = CharScanner.parseInt( parts[ 0 ] );
            month = CharScanner.parseInt( parts[ 1 ] );
            day = CharScanner.parseInt( parts[ 2 ] );
        }

        if ( parts.length >= 6 ) {
            hour = CharScanner.parseInt( parts[ 3 ] );
            minutes = CharScanner.parseInt( parts[ 4 ] );
            seconds = CharScanner.parseInt( parts[ 5 ] );
        }

        if ( parts.length >= 7 ) {
            mili = CharScanner.parseInt( parts[ 6 ] );
        }

        return toDate( year, month, day, hour, minutes, seconds, mili );
    }

    public static Date fromISO8601Jackson(char[] charArray, int from, int to) {

        try {
            if ( isISO8601Jackson(charArray, from, to) ) {
                int year = CharScanner.parseInt(charArray, from + 0, from + 4);
                int month = CharScanner.parseInt(charArray, from + 5, from + 7);
                int day = CharScanner.parseInt(charArray, from + 8, from + 10);
                int hour = CharScanner.parseInt(charArray, from + 11, from + 13);

                int minute = CharScanner.parseInt(charArray, from + 14, from + 16);

                int second = CharScanner.parseInt(charArray, from + 17, from + 19);
                int millisecond = CharScanner.parseInt(charArray, from + 20, from + 23);

                TimeZone tz = null;

                if ( charArray[ from + 19 ] == 'Z' ) {

                    tz = GMT;

                }

                else {
                    StringBuilder builder = new StringBuilder( 8 );
                    builder.append("GMT");

                    for (int index = from + 23; index < to; index++ ) {
                        if (charArray[index] == ':') {
                            continue;
                        }
                        builder.append( charArray[index] );
                    }
                    String tzStr = builder.toString();
                    tz = TimeZone.getTimeZone(tzStr);
                }
                return toDate( tz, year, month, day, hour, minute, second, millisecond );

            } else {
                return null;
            }
        } catch (Exception ex) {
            return null;
        }

    }

    public static Date fromISO8601( char[] charArray, int from, int to ) {

        try {
        int length = to - from;
        if ( isISO8601( charArray, from, to ) ) {
            int year = CharScanner.parseInt(charArray, from + 0, from + 4);
            int month = CharScanner.parseInt(charArray, from + 5, from + 7);
            int day = CharScanner.parseInt(charArray, from + 8, from + 10);
            int hour = CharScanner.parseInt(charArray, from + 11, from + 13);

            int minute = CharScanner.parseInt(charArray, from + 14, from + 16);

            int second = CharScanner.parseInt(charArray, from + 17, from + 19);
            TimeZone tz = null;

            if ( charArray[ from + 19 ] == 'Z' ) {

                tz = GMT;

            } else {

                StringBuilder builder = new StringBuilder( 9 );
                builder.append( "GMT" );
                builder.append( charArray, from + 19, 6 );
                String tzStr = builder.toString();
                tz = TimeZone.getTimeZone( tzStr );

            }
            return toDate( tz, year, month, day, hour, minute, second );

        } else {
            return null;
        }
        } catch (Exception ex) {
            return null;
        }

    }

    public static Date fromJsonDate( char[] charArray, int from, int to ) {
        try {
        if ( isJsonDate( charArray, from, to ) ) {
            int year = CharScanner.parseInt(charArray, from + 0, from + 4);
            int month = CharScanner.parseInt(charArray, from + 5, from + 7);
            int day = CharScanner.parseInt(charArray, from + 8, from + 10);
            int hour = CharScanner.parseInt(charArray, from + 11, from + 13);

            int minute = CharScanner.parseInt(charArray, from + 14, from + 16);

            int second = CharScanner.parseInt(charArray, from + 17, from + 19);

            int milliseconds = CharScanner.parseInt(charArray, from + 20, from + 23);

            TimeZone tz = GMT;

            return toDate( tz, year, month, day, hour, minute, second, milliseconds );

        } else {
            return null;
        }
        } catch (Exception ex) {
            return null;
        }

    }

    public static boolean isISO8601( String string ) {

        return isISO8601( FastStringUtils.toCharArray( string ) );
    }

    public static boolean isISO8601( char[] charArray ) {
        return isISO8601( charArray, 0, charArray.length );
    }

    public static boolean isISO8601( char[] charArray, int start, int to ) {
        boolean valid = true;
        final int length = to - start;

        if ( length == SHORT_ISO_8601_TIME_LENGTH ) {
            valid &= ( charArray[ start + 19 ] == 'Z' );

        } else if ( length == LONG_ISO_8601_TIME_LENGTH ) {
            valid &= ( charArray[ start + 19 ] == '-' || charArray[ start + 19 ] == '+' );
            valid &= ( charArray[ start + 22 ] == ':' );

        } else {
            return false;
        }

        //  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4
        // "1 9 9 4 - 1 1 - 0 5 T 0 8 : 1 5 : 3 0 - 0 5 : 0 0

        valid &= ( charArray[ start + 4 ] == '-' ) &&
                ( charArray[ start + 7 ] == '-' ) &&
                ( charArray[ start + 10 ] == 'T' ) &&
                ( charArray[ start + 13 ] == ':' ) &&
                ( charArray[ start + 16 ] == ':' );

        return valid;
    }

    public static boolean isISO8601Jackson(char[] charArray, int start, int to) {
        boolean valid = true;
        final int length = to - start;

        if ( length == SHORT_ISO_8601_TIME_LENGTH ) {
            valid &= ( charArray[ start + 19 ] == 'Z' );

        } else if ( length == LONG_ISO_8601_JACKSON_TIME_LENGTH || length == 29) {
            valid &= ( charArray[ start + 23 ] == '-' || charArray[ start + 23 ] == '+' );
        } else {
            return false;
        }

        //  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4
        // "1 9 9 4 - 1 1 - 0 5 T 0 8 : 1 5 : 3 0 - 0 5 : 0 0

        valid &= ( charArray[ start + 4 ] == '-' ) &&
                ( charArray[ start + 7 ] == '-' ) &&
                ( charArray[ start + 10 ] == 'T' ) &&
                ( charArray[ start + 13 ] == ':' ) &&
                ( charArray[ start + 16 ] == ':' );

        return valid;
    }

    public static boolean isISO8601QuickCheck( char[] charArray, int start, int to ) {
        final int length = to - start;

        try {

            if ( length == JSON_TIME_LENGTH || length == LONG_ISO_8601_TIME_LENGTH
                    || length == SHORT_ISO_8601_TIME_LENGTH || ( length >= 17 && ( charArray[ start + 16 ] == ':' ) )
                    ) {
                    return true;
            }

            return false;
        } catch (Exception ex) {
            ex.printStackTrace();
            return false;
        }

    }

    public static boolean isISO8601QuickCheck( char[] charArray ) {
        final int length = charArray.length;

        if ( length == JSON_TIME_LENGTH || length == LONG_ISO_8601_TIME_LENGTH
                || length == SHORT_ISO_8601_TIME_LENGTH || ( length >= 16 && ( charArray[   16 ] == ':' ) )
                ) {

            if ( length >= 16 && ( charArray[  16 ] == ':' ) ) {
                return true;
            }
        }

        return false;

    }

    public static boolean isJsonDate( String str ) {

        return isJsonDate(FastStringUtils.toCharArray(str), 0, str.length());
    }

    public static boolean isJsonDate( char[] charArray, int start, int to ) {
        boolean valid = true;
        final int length = to - start;

        if ( length != JSON_TIME_LENGTH ) {
            return false;
        }

        valid &= ( charArray[ start + 19 ] == '.' );

        if ( !valid ) {
            return false;
        }

        valid &= ( charArray[ start + 4 ] == '-' ) &&
                ( charArray[ start + 7 ] == '-' ) &&
                ( charArray[ start + 10 ] == 'T' ) &&
                ( charArray[ start + 13 ] == ':' ) &&
                ( charArray[ start + 16 ] == ':' );

        return valid;
    }

}