/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.
 */
package org.apache.brooklyn.util.text;

import java.util.Formattable;
import java.util.FormattableFlags;
import java.util.Formatter;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.annotation.Nullable;

import com.google.common.base.Function;

/**
 * A formatter to pretty-print numeric values representing sizes in byes.
 * <p>
 * The {@link ByteSizeStrings#builder()} presents a fluent interface to create
 * various configurations of formatting. The defaults produce metric units in
 * multiples of 1000 bytes at a precision of three significant figures. This is
 * the way disk space is normally measured, for example {@literal 128.1GB}.
 * <p>
 * Alternatively the {@link ByteSizeStrings#iso()} convenience method produces
 * ISO standard units in multiples of 1024 bytes, with the same precision as the
 * metric output. This is how RAM is normally measured, for example {@literal 12.4MiB}
 * or {@literal 1.04GiB}.
 * <p>
 * Finally, the {@link ByteSizeStrings#java()} convenience method will produce
 * strings suitable for use with a Java command line, as part of the {@code -Xms}
 * or {@code -Xmx} options. These output integer values only, so values up to
 * 10GB will be reported in MB to preserve accuracy. For size values over 1000GB,
 * the output will still be formatted as GB but rounded to a mutiple of 1000.
 * <p>
 * The class is immutable and thread safe once built and a single instance of
 * the three pre-defined configurations is created and returned buy the methods
 * described above.
 *
 * @see Strings#makeSizeString(long)
 * @see Strings#makeISOSizeString(long)
 * @see Strings#makeJavaSizeString(long)
 */
public class ByteSizeStrings implements Function<Long, String> {

    /**
     * Configures and builds a {@link ByteSizeStrings} formatter.
     */
    public static class Builder {

        private String suffixBytes = "B";
        private String suffixKilo = "kB";
        private String suffixMega = "MB";
        private String suffixGiga = "GB";
        private String suffixTera = "TB";
        private boolean addSpace = true;
        private int bytesPerMetricUnit = 1000;
        private int maxLen = 4;
        private int precision = 3;
        private int lowerLimit = 1;

        /**
         * The suffix to use when printing bytes.
         */
        public Builder suffixBytes(String suffixBytes) { this.suffixBytes = suffixBytes; return this; }

        /**
         * The suffix to use when printing Kilobytes.
         */
        public Builder suffixKilo(String suffixKilo) { this.suffixKilo = suffixKilo; return this; }

        /**
         * The suffix to use when printing Megabytes.
         */
        public Builder suffixMega(String suffixMega) { this.suffixMega = suffixMega; return this; }

        /**
         * The suffix to use when printing Gigabytes.
         */
        public Builder suffixGiga(String suffixGiga) { this.suffixGiga = suffixGiga; return this; }

        /**
         * The suffix to use when printing Terabytes.
         */
        public Builder suffixTera(String suffixTera) { this.suffixTera = suffixTera; return this; }

        /**
         * Whether to add a space between the value and the unit suffix.
         * <p>
         * Defaults is {@literal true} for '5 MiB' output.
         */
        public Builder addSpace(boolean addSpace) { this.addSpace = addSpace; return this; }
        public Builder addSpace() { this.addSpace = true; return this; }
        public Builder noSpace() { this.addSpace = false; return this; }

        /**
         * The number of bytes per metric usnit, usually either 1000 or 1024.
         * <p>
         * Used to determine when to use the next suffix string.
         */
        public Builder bytesPerMetricUnit(int bytesPerMetricUnit) { this.bytesPerMetricUnit = bytesPerMetricUnit; return this; }

        /**
         * The maximum length of the printed number.
         *
         * @see Strings#makeRealString(double, int, int, int, double, boolean)
         */
        public Builder maxLen(int maxLen) { this.maxLen = maxLen; return this; }

        /**
         * The number of digits accuracy desired in the printed number.
         *
         * @see Strings#makeRealString(double, int, int, int, double, boolean)
         */
        public Builder precision(int precision) { this.precision = precision; return this; }

        /**
         * Prints using a lower suffix until the size is greater than this limit multiplied
         * by bytes per metric unit, when the next highest suffix will be used.§
         * <p>
         * If this has the value 5 then sizes up to 5000 will be printed as bytes, and over 5000
         * as Kilobytes.
         */
        public Builder lowerLimit(int lowerLimit) { this.lowerLimit = lowerLimit; return this; }

        /**
         * Returns an immutable {@link ByteSizeStrings} formatter using the builder configuration.
         */
        public ByteSizeStrings build() {
            String space = addSpace ? " " : "";
            return new ByteSizeStrings(space + suffixBytes, space + suffixKilo, space + suffixMega, space + suffixGiga,
                    space + suffixTera, bytesPerMetricUnit, maxLen, precision, lowerLimit);
        }

    }

    /**
     * Returns a builder for a {@link ByteSizeStrings} formatter.
     */
    public static Builder builder() { return new Builder(); }

    /**
     * Format byte sizes suitable for Java {@code -Xms} arguments.
     */
    public static final ByteSizeStrings java() { return JAVA; }

    private static final ByteSizeStrings JAVA = ByteSizeStrings.builder()
                .suffixBytes("")
                .suffixKilo("k")
                .suffixMega("m")
                .suffixGiga("g")
                .suffixTera("000g") // Java has no Tera suffix
                .noSpace()
                .bytesPerMetricUnit(1024)
                .maxLen(6)
                .precision(0)
                .lowerLimit(10)
                .build();

    /**
     * Formats byte sizes using ISO standard suffixes and binary multiples of 1024
     */
    public static ByteSizeStrings iso() { return ISO; }

    private static ByteSizeStrings ISO = ByteSizeStrings.builder()
                .suffixBytes("B")
                .suffixKilo("KiB")
                .suffixMega("MiB")
                .suffixGiga("GiB")
                .suffixTera("TiB")
                .bytesPerMetricUnit(1024)
                .build();

    /**
     * Default byte size formatter using metric multiples of 1000.
     */
    public static ByteSizeStrings metric() { return METRIC; }

    private static ByteSizeStrings METRIC = ByteSizeStrings.builder().build();

    private String suffixBytes;
    private String suffixKilo;
    private String suffixMega;
    private String suffixGiga;
    private String suffixTera;
    private int bytesPerMetricUnit;
    private int maxLen;
    private int precision;
    private int lowerLimit;

    /**
     * For use by the {@link Builder} only.
     */
    private ByteSizeStrings(String suffixBytes, String suffixKilo, String suffixMega, String suffixGiga,
            String suffixTera, int bytesPerMetricUnit, int maxLen, int precision, int lowerLimit) {
        this.suffixBytes = suffixBytes;
        this.suffixKilo = suffixKilo;
        this.suffixMega = suffixMega;
        this.suffixGiga = suffixGiga;
        this.suffixTera = suffixTera;
        this.bytesPerMetricUnit = bytesPerMetricUnit;
        this.maxLen = maxLen;
        this.precision = precision;
        this.lowerLimit = lowerLimit;
    }

    /** @deprecated Use {@link ByteSizeStrings#builder()} */
    @Deprecated
    public ByteSizeStrings() { }

    /** @deprecated Use {@link ByteSizeStrings.Builder#suffixBytes(String)} */
    @Deprecated
    public void setSuffixBytes(String suffixBytes) {
        this.suffixBytes = suffixBytes;
    }

    /** @deprecated Use {@link ByteSizeStrings.Builder#suffixKilo(String)} */
    @Deprecated
    public void setSuffixKilo(String suffixKilo) {
        this.suffixKilo = suffixKilo;
    }

    /** @deprecated Use {@link ByteSizeStrings.Builder#suffixMega(String)} */
    @Deprecated
    public void setSuffixMega(String suffixMega) {
        this.suffixMega = suffixMega;
    }

    /** @deprecated Use {@link ByteSizeStrings.Builder#suffixGiga(String)} */
    @Deprecated
    public void setSuffixGiga(String suffixGiga) {
        this.suffixGiga = suffixGiga;
    }

    /** @deprecated Use {@link ByteSizeStrings.Builder#suffixTera(String)} */
    @Deprecated
    public void setSuffixTera(String suffixTera) {
        this.suffixTera = suffixTera;
    }

    /** @deprecated Use {@link ByteSizeStrings.Builder#bytesPerMetricUnit(int)} */
    @Deprecated
    public void setBytesPerMetricUnit(int bytesPerMetricUnit) {
        this.bytesPerMetricUnit = bytesPerMetricUnit;
    }

    /** @deprecated Use {@link ByteSizeStrings.Builder#maxLen(int)} */
    @Deprecated
    public void setMaxLen(int maxLen) {
        this.maxLen = maxLen;
    }

    /** @deprecated Use {@link ByteSizeStrings.Builder#precision(int)} */
    @Deprecated
    public void setPrecision(int precision) {
        this.precision = precision;
    }

    /** @deprecated Use {@link ByteSizeStrings.Builder#lowerLimit(int)} */
    @Deprecated
    public void setLowerLimit(int lowerLimit) {
        this.lowerLimit = lowerLimit;
    }

    /**
     * Format the {@literal size} bytes as a String.
     */
    public String makeSizeString(long size) {
        return makeSizeString(size, precision);
    }

    /**
     * Format the {@literal size} bytes as a String with the given precision.
     */
    public String makeSizeString(long size, int precision) {
        long t = size;
        if (t==0) return "0"+suffixBytes;
        if (t<0) return "-"+makeSizeString(-t);
        long b = t%bytesPerMetricUnit;
        t = t/bytesPerMetricUnit;
        long kb = t%bytesPerMetricUnit;
        t = t/bytesPerMetricUnit;
        long mb = t%bytesPerMetricUnit;
        t = t/bytesPerMetricUnit;
        long gb = t%bytesPerMetricUnit;
        t = t/bytesPerMetricUnit;
        long tb = t;

        if (tb>lowerLimit)
            return Strings.makeRealString(tb + (1.0*gb/bytesPerMetricUnit), -1, precision, 0) + suffixTera;
        if (gb>lowerLimit)
            return Strings.makeRealString((tb*bytesPerMetricUnit) + gb + (1.0*mb/bytesPerMetricUnit), maxLen, precision, 0) + suffixGiga;
        if (mb>lowerLimit)
            return Strings.makeRealString((gb*bytesPerMetricUnit) + mb + (1.0*kb/bytesPerMetricUnit), maxLen, precision, 0) + suffixMega;
        if (kb>lowerLimit)
            return Strings.makeRealString((mb*bytesPerMetricUnit) + kb + (1.0*b/bytesPerMetricUnit), maxLen, precision, 0) + suffixKilo;
        return (kb*bytesPerMetricUnit) + b + suffixBytes;
    }

    /**
     * Returns a {@link Formattable} object that can be used with {@link String#format(String, Object...)}.
     * <p>
     * When used as the argument for a {@literal %s} format string element, the {@literal bytes} value
     * will be formatted using the current {@link ByteSizeStrings} values, or if the alternative
     * flag is set (using the {@literal %#s} format string) it will use the {@link ByteSizeStrings#metric()}
     * formatter. Finally, the precision of the formatted value can be adjusted using format string
     * argumenbts like {@literal %.6s}.
     *
     * @see http://docs.oracle.com/javase/7/docs/api/java/util/Formatter.html#syntax
     */
    public Formattable formatted(final long bytes) {
        return new Formattable() {
            @Override
            public void formatTo(Formatter formatter, int flags, int width, int precision) {
                boolean alternate = (flags & FormattableFlags.ALTERNATE) == FormattableFlags.ALTERNATE;
                ByteSizeStrings strings = alternate ? ByteSizeStrings.metric() : ByteSizeStrings.this;
                if (precision != -1) {
                    formatter.format("%s", strings.makeSizeString(bytes, precision));
                } else {
                    formatter.format("%s", strings.makeSizeString(bytes));
                }
            }
        };
    }

    /**
     * A {@link Function} implementation that formats its input using the current {@link ByteSizeStrings} values.
     */
    @Override
    @Nullable
    public String apply(@Nullable Long input) {
        if (input == null) return null;
        return makeSizeString(input);
    }

    public static long parse(String sizeString) {
        return parse(sizeString, null);
    }
    public static long parse(String sizeString, String defaultUnits) {
        return parse(sizeString, defaultUnits, null);
    }
    /** parses the given string as a byte size string, e.g. "4gb"
     * @param sizeString string to parse
     * @param defaultUnit optional units to append if a number (no units) are supplied
     * @param bytesMode optional evaluation mode to force 1024 or 1000 as the interpretation of the unit prefix;
     *   if omitted, it will depend on the units supplied,
     *   1000 for {@link #metric()} (e.g. "1kB"), and   
     *   1024 for {@link #java()} (e.g. "1k") and {@link #iso()} (e.g. "1KiB")
     * @return number of bytes represented by this string
     */
    public static long parse(String sizeStringOriginal, String defaultUnit, ByteSizeStrings bytesMode) {
        String sizeString = sizeStringOriginal.trim();
        String units;
        Matcher matcher = Pattern.compile("[A-Za-z]+").matcher(sizeString);
        if (!matcher.find()) {
            if (defaultUnit==null) {
                throw new IllegalArgumentException("Cannot parse '"+sizeStringOriginal+"' as a size string");
            }
            units = defaultUnit;
        } else {
            units = matcher.group();
            int unitsIndex = sizeString.indexOf(units);
            if (sizeString.length() > unitsIndex+units.length()) {
                throw new IllegalArgumentException("Cannot parse '"+sizeStringOriginal+"' as a size string");
            }
            sizeString = sizeString.substring(0, unitsIndex).trim();
        }
        
        int exponent = -1;
        ByteSizeStrings matchedMode = null;
        for (ByteSizeStrings mode: new ByteSizeStrings[] { ISO, JAVA, METRIC } ) {
            matchedMode = mode;
            if (units.equalsIgnoreCase(mode.suffixBytes.trim())) { exponent = 0; break; }
            if (units.equalsIgnoreCase(mode.suffixKilo.trim())) { exponent = 1; break; }
            if (units.equalsIgnoreCase(mode.suffixMega.trim())) { exponent = 2; break; }
            if (units.equalsIgnoreCase(mode.suffixGiga.trim())) { exponent = 3; break; }
            if (units.equalsIgnoreCase(mode.suffixTera.trim())) { exponent = 4; break; }
        }
        
        if (exponent==-1) {
            // did not match; try other standard ones 
            if (units.equalsIgnoreCase("t")) { 
                exponent = 4;
                matchedMode = java();
            } else {
                throw new IllegalArgumentException("Cannot parse '"+sizeStringOriginal+"' as a size string (as '"+sizeString+"' "+units+")");
            }
        }
        
        double base = Double.parseDouble(sizeString.trim());
        
        if (bytesMode==null) bytesMode=matchedMode;
        
        while (exponent>0) {
            base *= bytesMode.bytesPerMetricUnit;
            exponent--;
        }
        
        return (long)base;
    }
    
}
