/*
 * 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.
 */
package com.facebook.presto.cli;

import com.google.common.primitives.Ints;
import io.airlift.units.DataSize;
import io.airlift.units.Duration;

import java.math.RoundingMode;
import java.text.DecimalFormat;

import static com.google.common.base.Preconditions.checkState;
import static com.google.common.base.Strings.repeat;
import static io.airlift.units.DataSize.Unit.BYTE;
import static java.lang.Math.max;
import static java.lang.Math.min;
import static java.lang.String.format;
import static java.util.concurrent.TimeUnit.SECONDS;

public final class FormatUtils
{
    private FormatUtils() {}

    public static String formatCount(long count)
    {
        double fractional = count;
        String unit = "";
        if (fractional > 1000) {
            fractional /= 1000;
            unit = "K";
        }
        if (fractional > 1000) {
            fractional /= 1000;
            unit = "M";
        }
        if (fractional > 1000) {
            fractional /= 1000;
            unit = "B";
        }
        if (fractional > 1000) {
            fractional /= 1000;
            unit = "T";
        }
        if (fractional > 1000) {
            fractional /= 1000;
            unit = "Q";
        }

        return format("%s%s", getFormat(fractional).format(fractional), unit);
    }

    public static String formatCountRate(double count, Duration duration, boolean longForm)
    {
        double rate = count / duration.getValue(SECONDS);
        if (Double.isNaN(rate) || Double.isInfinite(rate)) {
            rate = 0;
        }

        String rateString = formatCount((long) rate);
        if (longForm) {
            if (rateString.endsWith(" ")) {
                rateString = rateString.substring(0, rateString.length() - 1);
            }
            rateString += "/s";
        }
        return rateString;
    }

    public static String formatDataSize(DataSize size, boolean longForm)
    {
        double fractional = size.toBytes();
        String unit = null;
        if (fractional >= 1024) {
            fractional /= 1024;
            unit = "K";
        }
        if (fractional >= 1024) {
            fractional /= 1024;
            unit = "M";
        }
        if (fractional >= 1024) {
            fractional /= 1024;
            unit = "G";
        }
        if (fractional >= 1024) {
            fractional /= 1024;
            unit = "T";
        }
        if (fractional >= 1024) {
            fractional /= 1024;
            unit = "P";
        }

        if (unit == null) {
            unit = "B";
        }
        else if (longForm) {
            unit += "B";
        }

        return format("%s%s", getFormat(fractional).format(fractional), unit);
    }

    public static String formatDataRate(DataSize dataSize, Duration duration, boolean longForm)
    {
        double rate = dataSize.toBytes() / duration.getValue(SECONDS);
        if (Double.isNaN(rate) || Double.isInfinite(rate)) {
            rate = 0;
        }

        String rateString = formatDataSize(new DataSize(rate, BYTE), false);
        if (longForm) {
            if (!rateString.endsWith("B")) {
                rateString += "B";
            }
            rateString += "/s";
        }
        return rateString;
    }

    private static DecimalFormat getFormat(double value)
    {
        DecimalFormat format;
        if (value < 10) {
            // show up to two decimals to get 3 significant digits
            format = new DecimalFormat("#.##");
        }
        else if (value < 100) {
            // show up to one decimal to get 3 significant digits
            format = new DecimalFormat("#.#");
        }
        else {
            // show no decimals -- we have enough digits in the integer part
            format = new DecimalFormat("#");
        }

        format.setRoundingMode(RoundingMode.HALF_UP);
        return format;
    }

    public static String pluralize(String word, int count)
    {
        if (count != 1) {
            return word + "s";
        }
        return word;
    }

    public static String formatTime(Duration duration)
    {
        int totalSeconds = Ints.saturatedCast(duration.roundTo(SECONDS));
        int minutes = totalSeconds / 60;
        int seconds = totalSeconds % 60;

        return format("%s:%02d", minutes, seconds);
    }

    /**
     * Format an indeterminate progress bar: [       <=>       ]
     */
    public static String formatProgressBar(int width, int tick)
    {
        int markerWidth = 3; // must be odd >= 3 (1 for each < and > marker, the rest for "="

        int range = width - markerWidth; // "lower" must fall within this range for the marker to fit within the bar
        int lower = tick % range;

        if (((tick / range) % 2) == 1) { // are we going or coming back?
            lower = range - lower;
        }

        return repeat(" ", lower) +
                "<" + repeat("=", markerWidth - 2) + ">" +
                repeat(" ", width - (lower + markerWidth));
    }

    public static String formatProgressBar(int width, int complete, int running, int total)
    {
        if (total == 0) {
            return repeat(" ", width);
        }

        int pending = max(0, total - complete - running);

        // compute nominal lengths
        int completeLength = min(width, ceil(complete * width, total));
        int pendingLength = min(width, ceil(pending * width, total));

        // leave space for at least one ">" as long as running is > 0
        int minRunningLength = (running > 0) ? 1 : 0;
        int runningLength = max(min(width, ceil(running * width, total)), minRunningLength);

        // adjust to fix rounding errors
        if (((completeLength + runningLength + pendingLength) != width) && (pending > 0)) {
            // sacrifice "pending" if we're over the max width
            pendingLength = max(0, width - completeLength - runningLength);
        }
        if ((completeLength + runningLength + pendingLength) != width) {
            // then, sacrifice "running"
            runningLength = max(minRunningLength, width - completeLength - pendingLength);
        }
        if (((completeLength + runningLength + pendingLength) > width) && (complete > 0)) {
            // finally, sacrifice "complete" if we're still over the limit
            completeLength = max(0, width - runningLength - pendingLength);
        }

        checkState((completeLength + runningLength + pendingLength) == width,
                "Expected completeLength (%s) + runningLength (%s) + pendingLength (%s) == width (%s), was %s for complete = %s, running = %s, total = %s",
                completeLength, runningLength, pendingLength, width, completeLength + runningLength + pendingLength, complete, running, total);

        return repeat("=", completeLength) + repeat(">", runningLength) + repeat(" ", pendingLength);
    }

    /**
     * Ceiling of integer division
     */
    private static int ceil(int dividend, int divisor)
    {
        return ((dividend + divisor) - 1) / divisor;
    }
}
