/*
 * 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.maven.cli.transfer;

import java.io.PrintStream;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.Locale;

import org.apache.commons.lang3.Validate;
import org.eclipse.aether.transfer.AbstractTransferListener;
import org.eclipse.aether.transfer.TransferCancelledException;
import org.eclipse.aether.transfer.TransferEvent;
import org.eclipse.aether.transfer.TransferResource;

/**
 * AbstractMavenTransferListener
 */
public abstract class AbstractMavenTransferListener extends AbstractTransferListener {

    // CHECKSTYLE_OFF: LineLength
    /**
     * Formats file size with the associated <a href="https://en.wikipedia.org/wiki/Metric_prefix">SI</a> prefix
     * (GB, MB, kB) and using the patterns <code>#0.0</code> for numbers between 1 and 10
     * and <code>###0</code> for numbers between 10 and 1000+ by default.
     *
     * @see <a href="https://en.wikipedia.org/wiki/Metric_prefix">https://en.wikipedia.org/wiki/Metric_prefix</a>
     * @see <a href="https://en.wikipedia.org/wiki/Binary_prefix">https://en.wikipedia.org/wiki/Binary_prefix</a>
     * @see <a
     *      href="https://en.wikipedia.org/wiki/Octet_%28computing%29">https://en.wikipedia.org/wiki/Octet_(computing)</a>
     */
    // CHECKSTYLE_ON: LineLength
    // TODO Move me to Maven Shared Utils
    static class FileSizeFormat {
        enum ScaleUnit {
            BYTE {
                @Override
                public long bytes() {
                    return 1L;
                }

                @Override
                public String symbol() {
                    return "B";
                }
            },
            KILOBYTE {
                @Override
                public long bytes() {
                    return 1000L;
                }

                @Override
                public String symbol() {
                    return "kB";
                }
            },
            MEGABYTE {
                @Override
                public long bytes() {
                    return KILOBYTE.bytes() * KILOBYTE.bytes();
                }

                @Override
                public String symbol() {
                    return "MB";
                }
            },
            GIGABYTE {
                @Override
                public long bytes() {
                    return MEGABYTE.bytes() * KILOBYTE.bytes();
                }
                ;

                @Override
                public String symbol() {
                    return "GB";
                }
            };

            public abstract long bytes();

            public abstract String symbol();

            public static ScaleUnit getScaleUnit(long size) {
                Validate.isTrue(size >= 0L, "file size cannot be negative: %s", size);

                if (size >= GIGABYTE.bytes()) {
                    return GIGABYTE;
                } else if (size >= MEGABYTE.bytes()) {
                    return MEGABYTE;
                } else if (size >= KILOBYTE.bytes()) {
                    return KILOBYTE;
                } else {
                    return BYTE;
                }
            }
        }

        private DecimalFormat smallFormat;
        private DecimalFormat largeFormat;

        FileSizeFormat(Locale locale) {
            smallFormat = new DecimalFormat("#0.0", new DecimalFormatSymbols(locale));
            largeFormat = new DecimalFormat("###0", new DecimalFormatSymbols(locale));
        }

        public String format(long size) {
            return format(size, null);
        }

        public String format(long size, ScaleUnit unit) {
            return format(size, unit, false);
        }

        @SuppressWarnings("checkstyle:magicnumber")
        public String format(long size, ScaleUnit unit, boolean omitSymbol) {
            Validate.isTrue(size >= 0L, "file size cannot be negative: %s", size);

            if (unit == null) {
                unit = ScaleUnit.getScaleUnit(size);
            }

            double scaledSize = (double) size / unit.bytes();
            String scaledSymbol = " " + unit.symbol();

            if (omitSymbol) {
                scaledSymbol = "";
            }

            if (unit == ScaleUnit.BYTE) {
                return largeFormat.format(size) + scaledSymbol;
            }

            if (scaledSize < 0.05 || scaledSize >= 10.0) {
                return largeFormat.format(scaledSize) + scaledSymbol;
            } else {
                return smallFormat.format(scaledSize) + scaledSymbol;
            }
        }

        public String formatProgress(long progressedSize, long size) {
            Validate.isTrue(progressedSize >= 0L, "progressed file size cannot be negative: %s", progressedSize);
            Validate.isTrue(
                    size >= 0L && progressedSize <= size || size < 0L,
                    "progressed file size cannot be greater than size: %s > %s",
                    progressedSize,
                    size);

            if (size >= 0L && progressedSize != size) {
                ScaleUnit unit = ScaleUnit.getScaleUnit(size);
                String formattedProgressedSize = format(progressedSize, unit, true);
                String formattedSize = format(size, unit);

                return formattedProgressedSize + "/" + formattedSize;
            } else {
                return format(progressedSize);
            }
        }
    }

    protected PrintStream out;

    protected AbstractMavenTransferListener(PrintStream out) {
        this.out = out;
    }

    @Override
    public void transferInitiated(TransferEvent event) {
        String action = event.getRequestType() == TransferEvent.RequestType.PUT ? "Uploading" : "Downloading";
        String direction = event.getRequestType() == TransferEvent.RequestType.PUT ? "to" : "from";

        TransferResource resource = event.getResource();
        StringBuilder message = new StringBuilder();
        message.append(action).append(' ').append(direction).append(' ').append(resource.getRepositoryId());
        message.append(": ");
        message.append(resource.getRepositoryUrl()).append(resource.getResourceName());

        out.println(message.toString());
    }

    @Override
    public void transferCorrupted(TransferEvent event) throws TransferCancelledException {
        TransferResource resource = event.getResource();
        // TODO This needs to be colorized
        out.println("[WARNING] " + event.getException().getMessage() + " from " + resource.getRepositoryId() + " for "
                + resource.getRepositoryUrl() + resource.getResourceName());
    }

    @Override
    public void transferSucceeded(TransferEvent event) {
        String action = (event.getRequestType() == TransferEvent.RequestType.PUT ? "Uploaded" : "Downloaded");
        String direction = event.getRequestType() == TransferEvent.RequestType.PUT ? "to" : "from";

        TransferResource resource = event.getResource();
        long contentLength = event.getTransferredBytes();
        FileSizeFormat format = new FileSizeFormat(Locale.ENGLISH);

        StringBuilder message = new StringBuilder();
        message.append(action).append(' ').append(direction).append(' ').append(resource.getRepositoryId());
        message.append(": ");
        message.append(resource.getRepositoryUrl()).append(resource.getResourceName());
        message.append(" (").append(format.format(contentLength));

        long duration = System.currentTimeMillis() - resource.getTransferStartTime();
        if (duration > 0L) {
            double bytesPerSecond = contentLength / (duration / 1000.0);
            message.append(" at ").append(format.format((long) bytesPerSecond)).append("/s");
        }

        message.append(')');
        out.println(message.toString());
    }
}
