/*
 * This file is part of muCommander, http://www.mucommander.com
 *
 * muCommander 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.
 *
 * muCommander 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 this program.  If not, see <http://www.gnu.org/licenses/>.
 */


package com.mucommander.text;

import java.text.DecimalFormat;
import java.text.NumberFormat;


/**
 * SizeFormat formats byte sizes into localized string representations.
 *
 * @author Maxence Bernard.
 */
public class SizeFormat {

    /** Bitmask for short digits, e.g. "15" */
    public final static int DIGITS_SHORT = 1;
    /** Bitmask for medium digits, e.g. "15,2" */
    public final static int DIGITS_MEDIUM = 2;
    /** Bitmask for full digits, e.g. "15,204,405" */
    public final static int DIGITS_FULL = 4;

    /** Bitmask for no unit string */
    public final static int UNIT_NONE = 0;
    /** Bitmask for short unit string, e.g. "b" */
    public final static int UNIT_SHORT = 8;
    /** Bitmask for short unit string, e.g. "bytes" */
    public final static int UNIT_LONG = 16;

    /** Byte unit */
    public final static int BYTE_UNIT = 0;
    /** Kilobyte unit */
    public final static int KILOBYTE_UNIT = 1;
    /** Megabyte unit */
    public final static int MEGABYTE_UNIT = 2;
    /** Gigabyte unit */
    public final static int GIGABYTE_UNIT = 3;
    /** Terabyte unit */
    public final static int TERABYTE_UNIT = 4;

    /** Bitmask to add '/s' (per second) to the returned String */
    public final static int UNIT_SPEED = 32;
    
    /** Bitmask to include a space character to separate the digits and unit parts */
    public final static int INCLUDE_SPACE = 64;

    /** Bitmask to round any size < 1KB to 1KB (except 0 which will be 0 KB) */
    public final static int ROUND_TO_KB = 128;

    /** One kilobyte: 2^10 */
    private final static int KB_1 = 1024;
    /** Ten kilobytes: (2^10)*10 */
    private final static int KB_10 = 10240;
    /** One megabyte: 2^20 */
    private final static int MB_1 = 1048576;
    /** Ten megabytes: (2^20)*10 */
    private final static int MB_10 = 10485760;
    /** One gigabyte: 2^30 */
    private final static int GB_1 = 1073741824;
    /** Ten gigabytes: (2^10)*10 */
    private final static long GB_10 = 10737418240l;
    /** One terabyte: 2^40 */
    private final static long TB_1 = 1099511627776l;
    /** Ten terabytes: (2^40)*10 */
    private final static long TB_10 = 10995116277760l;

    /** DecimalFormat instance to localize thousands separators */
    private final static DecimalFormat DECIMAL_FORMAT = (DecimalFormat)NumberFormat.getInstance();

    /** Localized decimal separator */
    private final static String DECIMAL_SEPARATOR = ""+DECIMAL_FORMAT.getDecimalFormatSymbols().getDecimalSeparator();


    /////////////////////
    // Dictionary keys //
    /////////////////////

    private final static String BYTE = Translator.get("unit.byte");
    private final static String BYTES = Translator.get("unit.bytes");
    private final static String B = Translator.get("unit.bytes_short");
    private final static String KB = Translator.get("unit.kb");
    private final static String MB = Translator.get("unit.mb");
    private final static String GB = Translator.get("unit.gb");
    private final static String TB = Translator.get("unit.tb");

    private final static String SPEED_KEY = "unit.speed";


    /**
     * Returns a String representation of the given byte size.
     *
     * @param size the size to format
     * @param format format bitmask, see constant fields for allowed values
     * @return a String representation of the given byte size
     */
    public static String format(long size, int format) {
        if(size<0)
            return "?";

        String digitsString;
        String unitString;
		
        // Whether the unit string should be long or not
        boolean unitLong = (format&UNIT_LONG)!=0;
        // Whether the unit string should be short or not
        boolean unitShort = (format&UNIT_SHORT)!=0;
        // Whether the unit string should be short or not
        boolean noUnit = !(unitLong||unitShort);
        // Whether the digits string should be short or not
        boolean digitsShort = (format&DIGITS_SHORT)!=0;
        // Whether any size < 1024 bytes should be rounded to a kilobyte
        boolean roundToKb = (format&ROUND_TO_KB)!=0;
		
        // size < 1KB
        if(size<KB_1) {
            if(roundToKb) {
                // Note: ROUND_TO_KB must have precedence over DIGITS_FULL
                digitsString = size==0?"0":"1";
                unitString = noUnit?"":KB;
            }
            else {
                digitsString = ""+size;
                unitString = unitLong?(size<=1?BYTE:BYTES):unitShort?B:"";
            }
        }
        else if((format&DIGITS_FULL)!=0) {
            // DecimalFormat localizes thousands separators

            // Calls to DecimalFormat must be synchronized.
            // Quote from DecimalFormat's Javadoc: "Decimal formats are generally not synchronized. It is recommended 
            // to create separate format instances for each thread. If multiple threads access a format concurrently,
            // it must be synchronized externally."
            synchronized(DECIMAL_FORMAT) {
                digitsString = DECIMAL_FORMAT.format(size);
            }
            unitString = unitLong?BYTES:unitShort?B:"";
        }
        else {
            // size < 10KB	-> "9.6 KB"
            if(size<KB_10 && !digitsShort) {
                int nKB = (int)size/KB_1;
                digitsString = nKB+DECIMAL_SEPARATOR+(int)((size-nKB*KB_1)/(float)KB_1*10);
                unitString = noUnit?"":KB;
            }
            // size < 1MB -> "436 KB"
            else if(size<MB_1) {
                digitsString = ""+size/KB_1;
                unitString = noUnit?"":KB;
            }
            // size < 10MB -> "4.3 MB"
            else if(size<MB_10 && !digitsShort) {
                int nMB = (int)size/MB_1;
                digitsString = nMB+DECIMAL_SEPARATOR+(int)((size-nMB*MB_1)/(float)MB_1*10);
                unitString = noUnit?"":MB;
            }
            // size < 1GB -> "548 MB"
            else if(size<GB_1) {
                digitsString = ""+size/MB_1;
                unitString = noUnit?"":MB;
            }	
            // size < 10GB -> "4.8 GB"
            else if(size<GB_10 && !digitsShort) {
                long nGB = size/GB_1;
                digitsString = nGB+DECIMAL_SEPARATOR+(int)((size-nGB*GB_1)/(double)GB_1*10);
                unitString = noUnit?"":GB;
            }
            // size < 1TB -> "216 GB"
            else if(size<TB_1) {
                digitsString = ""+size/GB_1;
                unitString = noUnit?"":GB;
            }
            // size < 10TB -> "4.8 TB"
            else if(size<TB_10 && !digitsShort) {
                long nTB = size/TB_1;
                digitsString = nTB+DECIMAL_SEPARATOR+(int)((size-nTB*TB_1)/(double)TB_1*10);
                unitString = noUnit?"":TB;
            }
            else {
                // Will I live long enough to see files that large ??
                digitsString = ""+size/TB_1;
                unitString = noUnit?"":TB;
            }
        }

        // Add localized '/s' to unit string if unit is speed
        if((format&UNIT_SPEED)!=0)
            unitString = Translator.get(SPEED_KEY, unitString);

        return digitsString+((format&INCLUDE_SPACE)!=0?" ":"")+unitString;
    }
    


    public static String getUnitString(int unit, boolean speedUnit) {
        String unitString;

        switch(unit) {
            case BYTE_UNIT:
                unitString = B;
                break;
            case KILOBYTE_UNIT:
                unitString = KB;
                break;
            case MEGABYTE_UNIT:
                unitString = MB;
                break;
            case GIGABYTE_UNIT:
                unitString = GB;
                break;
            case TERABYTE_UNIT:
                unitString = TB;
                break;
            default:
                return "";
        }

        return speedUnit?Translator.get(SPEED_KEY, unitString):unitString;
    }


    /**
     * Returns the size in bytes of the given byte unit, e.g. <code>1024</code> for {@link #KILOBYTE_UNIT}.
     *
     * @param unit a unit constant, see constant fields for allowed values
     * @return the size in bytes of the given byte unit
     */
    public static long getUnitBytes(int unit) {
        long bytes;

        switch(unit) {
            case BYTE_UNIT:
                bytes = 1;
                break;
            case KILOBYTE_UNIT:
                bytes = KB_1;
                break;
            case MEGABYTE_UNIT:
                bytes = MB_1;
                break;
            case GIGABYTE_UNIT:
                bytes = GB_1;
                break;
            case TERABYTE_UNIT:
                bytes = TB_1;
                break;
            default:
                return 0;
        }

        return bytes;
    }
}
