/*
 * Copyright 2010-2015 Institut Pasteur.
 * 
 * This file is part of Icy.
 * 
 * Icy is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Icy 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 General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with Icy. If not, see <http://www.gnu.org/licenses/>.
 */
package icy.math;

import java.util.concurrent.TimeUnit;

import icy.util.StringUtil;

/**
 * Unit conversion utilities class.
 * 
 * @author Thomas Provoost & Stephane Dallongeville
 */
public class UnitUtil
{
    /**
     * Constants for special characters
     */
    public static final char MICRO_CHAR = '\u00B5';
    public static final String MICRO_STRING = "\u00B5";

    public static enum UnitPrefix
    {
        GIGA, MEGA, KILO, NONE, MILLI, MICRO, NANO, PICO;

        @Override
        public String toString()
        {
            switch (this)
            {
                case GIGA:
                    return "G";
                case KILO:
                    return "k";
                case MEGA:
                    return "M";
                case MILLI:
                    return "m";
                case MICRO:
                    return MICRO_STRING;
                case NANO:
                    return "n";
                case PICO:
                    return "p";
                case NONE:
                    return "";
                default:
                    return "x";
            }
        }
    };

    /**
     * Return the specified value as "bytes" string :<br>
     * 1024 --> "1 KB"<br>
     * 1048576 --> "1 MB"<br>
     * ...<br>
     */
    public static String getBytesString(double value)
    {
        final double absValue = Math.abs(value);

        // TB
        if (absValue > 549755813888d)
            return Double.toString(MathUtil.round(value / 1099511627776d, 1)) + " TB";
        // GB
        else if (absValue > 536870912d)
            return Double.toString(MathUtil.round(value / 1073741824d, 1)) + " GB";
        // MB
        else if (absValue > 524288d)
            return Double.toString(MathUtil.round(value / 1048576d, 1)) + " MB";
        // KB
        else if (absValue > 512d)
            return Double.toString(MathUtil.round(value / 1024d, 1)) + " KB";
        // B
        return Double.toString(MathUtil.round(value, 1)) + " B";
    }

    /**
     * Get the best unit with the given value and {@link UnitPrefix}.<br>
     * By best unit we adapt the output unit so the value stay between 0.1 --> 100 range (for
     * dimension 1).<br>
     * Be careful, this method is supposed to be used with unit in <b>decimal</b>
     * system. For sexagesimal system, please use {@link #getBestTimeUnit(double)} or {@link TimeUnit} methods.<br/>
     * <br/>
     * Example: <code>getBestUnit(0.01, UnitPrefix.MILLI, 1)</code> will return <code>UnitPrefix.MICRO</code><br/>
     * 
     * @param value
     *        : value used to get the best unit.
     * @param currentUnit
     *        : current unit of the value.
     * @param dimension
     *        : current unit dimension.
     * @return Return the best unit
     * @see #getValueInUnit(double, UnitPrefix, UnitPrefix)
     */
    public static UnitPrefix getBestUnit(double value, UnitPrefix currentUnit, int dimension)
    {
        // special case
        if (value == 0d)
            return currentUnit;

        int typeInd = currentUnit.ordinal();
        double v = value;
        final int maxInd = UnitPrefix.values().length - 1;
        final double factor = Math.pow(1000d, dimension);
        final double midFactor = Math.pow(100d, dimension);

        while (((int) v == 0) && (typeInd < maxInd))
        {
            v *= factor;
            typeInd++;
        }
        while (((int) (v / midFactor) != 0) && (typeInd > 0))
        {
            v /= factor;
            typeInd--;
        }

        return UnitPrefix.values()[typeInd];
    }

    /**
     * Get the best unit with the given value and {@link UnitPrefix}. By best unit we adapt the
     * output unit so the value stay between 0.1 --> 100 range (for dimension 1).<br>
     * Be careful, this method is supposed to be used with unit in <b>decimal</b>
     * system. For sexagesimal system, please use {@link #getBestTimeUnit(double)} or {@link TimeUnit} methods.<br/>
     * Example: <code>getBestUnit(0.01, UnitPrefix.MILLI, 1)</code> will return <code>UnitPrefix.MICRO</code><br/>
     * 
     * @param value
     *        : value used to get the best unit.
     * @param currentUnit
     *        : current unit of the value.
     * @return Return the best unit
     * @see #getValueInUnit(double, UnitPrefix, UnitPrefix)
     */
    public static UnitPrefix getBestUnit(double value, UnitPrefix currentUnit)
    {
        return getBestUnit(value, currentUnit, 1);
    }

    /**
     * Return the value from a specific unit to another unit.<br/>
     * Be careful, this method is supposed to be used with unit in <b>decimal</b>
     * system. For sexagesimal system, please use {@link #getBestTimeUnit(double)} or {@link TimeUnit} methods.<br/>
     * <b>Example:</b><br/>
     * <ul>
     * <li>value = 0.01</li>
     * <li>currentUnit = {@link UnitPrefix#MILLI}</li>
     * <li>wantedUnit = {@link UnitPrefix#MICRO}</li>
     * <li>returns: 10</li>
     * </ul>
     * 
     * @param value
     *        : Original value.
     * @param currentUnit
     *        : current unit
     * @param wantedUnit
     *        : wanted unit
     * @param dimension
     *        : unit dimension.
     * @return Return a double value in the <code>wantedUnit</code> unit.
     * @see #getBestUnit(double, UnitPrefix)
     */
    public static double getValueInUnit(double value, UnitPrefix currentUnit, UnitPrefix wantedUnit, int dimension)
    {
        int currentOrdinal = currentUnit.ordinal();
        int wantedOrdinal = wantedUnit.ordinal();
        double result = value;
        final double factor = Math.pow(1000d, dimension);

        while (currentOrdinal < wantedOrdinal)
        {
            result *= factor;
            currentOrdinal++;
        }
        while (currentOrdinal > wantedOrdinal)
        {
            result /= factor;
            currentOrdinal--;
        }

        return result;
    }

    /**
     * Return the value from a specific unit to another unit.<br/>
     * Be careful, this method is supposed to be used with unit in <b>decimal</b>
     * system. For sexagesimal system, please use {@link #getBestTimeUnit(double)} or {@link TimeUnit} methods.<br/>
     * <b>Example:</b><br/>
     * <ul>
     * <li>value = 0.01</li>
     * <li>currentUnit = {@link UnitPrefix#MILLI}</li>
     * <li>wantedUnit = {@link UnitPrefix#MICRO}</li>
     * <li>returns: 10</li>
     * </ul>
     * 
     * @param value
     *        : Original value.
     * @param currentUnit
     *        : current unit
     * @param wantedUnit
     *        : wanted unit
     * @return Return a double value in the <code>wantedUnit</code> unit.
     * @see #getBestUnit(double, UnitPrefix)
     */
    public static double getValueInUnit(double value, UnitPrefix currentUnit, UnitPrefix wantedUnit)
    {
        return getValueInUnit(value, currentUnit, wantedUnit, 1);
    }

    /**
     * This method returns a string containing the value rounded to a specified
     * number of decimals and its best unit prefix. This method is supposed to
     * be used with meters only.
     * 
     * @param value
     *        : value to display
     * @param decimals
     *        : number of decimals to keep
     * @param currentUnit
     *        : current unit prefix (Ex: {@link UnitPrefix#MILLI})
     */
    public static String getBestUnitInMeters(double value, int decimals, UnitPrefix currentUnit)
    {
        UnitPrefix unitPxSize = getBestUnit(value, currentUnit);
        double distanceMeters = getValueInUnit(value, currentUnit, unitPxSize);

        return StringUtil.toString(distanceMeters, decimals) + unitPxSize + "m";
    }

    /**
     * Return the best unit to display the value. The best unit is chosen
     * according to the precision. <br/>
     * <b>Example:</b>
     * <ul>
     * <li>62001 ms -> {@link TimeUnit#MILLISECONDS}</li>
     * <li>62000 ms -> {@link TimeUnit#SECONDS}</li>
     * <li>60000 ms -> {@link TimeUnit#MINUTES}</li>
     * </ul>
     * 
     * @param valueInMs
     *        : value in milliseconds.
     * @return Return a {@link TimeUnit} enumeration value.
     */
    public static TimeUnit getBestTimeUnit(double valueInMs)
    {
        if (valueInMs % 1000 != 0)
            return TimeUnit.MILLISECONDS;
        if (valueInMs % 60000 != 0)
            return TimeUnit.SECONDS;
        if (valueInMs % 3600000 != 0)
            return TimeUnit.MINUTES;

        return TimeUnit.HOURS;
    }

    /**
     * @deprecated Use {@link #getBestTimeUnit(double)} instead.
     */
    @Deprecated
    public static TimeUnit getBestUnit(double valueInMs)
    {
        return getBestTimeUnit(valueInMs);
    }

    /**
     * Display the time with a comma and a given precision.
     * 
     * @param valueInMs
     *        : value in milliseconds
     * @param precision
     *        : number of decimals after comma
     * @return <b>Example:</b> "2.5 h", "1.543 min", "15 ms".
     */
    public static String displayTimeAsStringWithComma(double valueInMs, int precision, TimeUnit unit)
    {
        String result;
        double v = valueInMs;

        switch (unit)
        {
            case DAYS:
                v /= 24d * 60d * 60d * 1000d;
                result = StringUtil.toString(v, precision) + " d";
                break;

            case HOURS:
                v /= 60d * 60d * 1000d;
                result = StringUtil.toString(v, precision) + " h";
                break;

            default:
            case MINUTES:
                v /= 60d * 1000d;
                result = StringUtil.toString(v, precision) + " min";
                break;

            case SECONDS:
                v /= 1000d;
                result = StringUtil.toString(v, precision) + " sec";
                break;

            case MILLISECONDS:
                result = StringUtil.toString(v, precision) + " ms";
                break;

            case NANOSECONDS:
                v *= 1000d;
                result = StringUtil.toString(v, precision) + " ns";
                break;
        }

        return result;
    }

    /**
     * Display the time with a comma and a given precision.
     * 
     * @param valueInMs
     *        : value in milliseconds
     * @param precision
     *        : number of decimals after comma
     * @return <b>Example:</b> "2.5 h", "1.543 min", "15 ms".
     */
    public static String displayTimeAsStringWithComma(double valueInMs, int precision)
    {
        double v = Math.abs(valueInMs);

        if (v >= 24d * 60d * 60d * 1000d)
            return displayTimeAsStringWithComma(valueInMs, precision, TimeUnit.DAYS);
        if (v >= 60d * 60d * 1000d)
            return displayTimeAsStringWithComma(valueInMs, precision, TimeUnit.HOURS);
        else if (v >= 60d * 1000d)
            return displayTimeAsStringWithComma(valueInMs, precision, TimeUnit.MINUTES);
        else if (v >= 1000d)
            return displayTimeAsStringWithComma(valueInMs, precision, TimeUnit.SECONDS);
        else if (v < 1d)
            return displayTimeAsStringWithComma(valueInMs, precision, TimeUnit.MILLISECONDS);
        else
            return displayTimeAsStringWithComma(valueInMs, precision, TimeUnit.NANOSECONDS);
    }

    /**
     * Display the time with all the units.
     * 
     * @param valueInMs
     *        : value in milliseconds
     * @param displayZero
     *        : Even if a unit is not relevant (equals to zero), it will be displayed.
     * @return <b>Example:</b> "2h 3min 40sec 350ms".
     */
    public static String displayTimeAsStringWithUnits(double valueInMs, boolean displayZero)
    {
        String result = "";
        double v = valueInMs;

        if (v >= 24d * 60d * 60d * 1000d)
        {
            result += (int) (v / (24d * 60d * 60d * 1000d)) + "d ";
            v %= 24d * 60d * 60d * 1000d;
        }
        else if (displayZero)
            result += "0d ";
        if (v >= 60d * 60d * 1000d)
        {
            result += (int) (v / (60d * 60d * 1000d)) + "h ";
            v %= 60d * 60d * 1000d;
        }
        else if (displayZero)
            result += "00h ";
        if (v >= 60d * 1000d)
        {
            result += (int) (v / (60d * 1000d)) + "min ";
            v %= 60d * 1000d;
        }
        else if (displayZero)
            result += "00min ";
        if (v >= 1000d)
        {
            result += (int) (v / 1000d) + "sec ";
            v %= 1000d;
        }
        else if (displayZero)
            result += "00sec ";
        if (v != 0d)
            result += StringUtil.toString(v, 2) + "ms";
        else if (displayZero)
            result += "000ms";

        return result;
    }
}