/*
 * Copyright (c) 2022 MiLaboratories Inc.
 *
 * 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.milaboratory.util;

import cc.redberry.pipe.util.CountLimitingOutputPort;
import cc.redberry.pipe.util.CountingOutputPort;

import java.io.PrintStream;
import java.text.DecimalFormat;
import java.util.List;

public class SmartProgressReporter implements Runnable {
    private static final DecimalFormat percentFormat = new DecimalFormat("##.#'%'");
    private final PrintStream stream;
    private final CanReportProgressAndStage reporter;
    private double progressPeriod = 0.10, timePeriod = 120_000;
    private boolean detectStageChange = true;

    public SmartProgressReporter(CanReportProgressAndStage reporter, PrintStream stream) {
        this.stream = stream;
        this.reporter = reporter;
    }

    public SmartProgressReporter(CanReportProgressAndStage reporter) {
        this(reporter, System.out);
    }

    public SmartProgressReporter(final String prefix, final CanReportProgress reporter) {
        this(prefix, reporter, System.out);
    }

    public SmartProgressReporter(final String prefix, final CanReportProgress reporter, PrintStream stream) {
        this.stream = stream;
        this.reporter = new CanReportProgressAndStage() {
            @Override
            public String getStage() {
                return prefix;
            }

            @Override
            public double getProgress() {
                return reporter.getProgress();
            }

            @Override
            public boolean isFinished() {
                return reporter.isFinished();
            }
        };
    }

    public double getProgressPeriod() {
        return progressPeriod;
    }

    public void setProgressPeriod(double progressPeriod) {
        this.progressPeriod = progressPeriod;
    }

    public double getTimePeriod() {
        return timePeriod;
    }

    public void setTimePeriod(double timePeriod) {
        this.timePeriod = timePeriod;
    }

    public boolean isDetectStageChange() {
        return detectStageChange;
    }

    public void setDetectStageChange(boolean detectStageChange) {
        this.detectStageChange = detectStageChange;
    }

    @Override
    public void run() {
        long currentStamp, lastStamp = System.currentTimeMillis(), deltaTime, et;
        double currentProgress, lastProgress = Double.NaN, deltaValue;
        String currentStage, lastStage = null, etStr;
        boolean trigger;
        try {
            while (!reporter.isFinished()) {
                synchronized (reporter) {
                    currentProgress = reporter.getProgress();
                    currentStage = reporter.getStage();
                }
                currentStamp = System.currentTimeMillis();

                deltaValue = currentProgress - lastProgress;
                deltaTime = currentStamp - lastStamp;

                trigger = false;

                if (detectStageChange && !currentStage.equals(lastStage))
                    trigger = true;

                if (Double.isNaN(currentProgress) ^ Double.isNaN(lastProgress))
                    trigger = true;

                if (deltaValue >= progressPeriod
                        || deltaTime >= timePeriod)
                    trigger = true;

                if (deltaValue < 0.0) {
                    deltaValue = Double.NaN;
                    trigger = true;
                }

                long hours, minutes, seconds;

                if (trigger) {

                    if (Double.isNaN(deltaValue) || deltaTime == 0 || deltaValue == 0.0)
                        etStr = "";
                    else {
                        et = (long) ((1.0 - currentProgress) * deltaTime / deltaValue);

                        et /= 1000;
                        hours = et / 3600;
                        et -= hours * 3600;
                        minutes = (et) / 60;
                        et -= minutes * 60;
                        seconds = et;

                        etStr = "  ETA: " + timeString(hours) + ":" + timeString(minutes) + ":" + timeString(seconds);
                    }

                    if (currentStage == null)
                        currentStage = "null";

                    String sProgress;
                    if (Double.isNaN(currentProgress))
                        sProgress = "progress unknown";
                    else
                        sProgress = percentFormat.format(currentProgress * 100.0);

                    stream.println(currentStage + ": " + sProgress + etStr);

                    lastProgress = currentProgress;
                    lastStamp = currentStamp;
                    lastStage = currentStage;
                }

                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
        }
    }

    private static String timeString(long time) {
        String timeStr = Long.toString(time);
        return timeStr.length() < 2 ? ("0" + timeStr) : timeStr;
    }

    public static void startProgressReport(SmartProgressReporter reporter) {
        Thread thread = new Thread(reporter);
        thread.setDaemon(true);
        thread.start();
    }

    public static void startProgressReport(CanReportProgressAndStage reporter, PrintStream stream) {
        startProgressReport(new SmartProgressReporter(reporter, stream));
    }

    public static void startProgressReport(CanReportProgressAndStage reporter) {
        startProgressReport(new SmartProgressReporter(reporter));
    }

    public static void startProgressReport(final String prefix, final CanReportProgress reporter) {
        startProgressReport(new SmartProgressReporter(prefix, reporter));
    }

    public static void startProgressReport(final String prefix, final CanReportProgress reporter, PrintStream stream) {
        startProgressReport(new SmartProgressReporter(prefix, reporter, stream));
    }

    public static CanReportProgress extractProgress(final CountingOutputPort<?> countingOutputPort, final long size) {
        return new CanReportProgress() {
            @Override
            public double getProgress() {
                return 1.0 * countingOutputPort.getCount() / size;
            }

            @Override
            public boolean isFinished() {
                return countingOutputPort.getCount() >= size;
            }
        };
    }

    public static CanReportProgress extractProgress(final CountLimitingOutputPort<?> countLimitingOutputPort) {
        return new CanReportProgress() {
            @Override
            public double getProgress() {
                long limit = countLimitingOutputPort.getLimit();
                long done = limit - countLimitingOutputPort.getElementsLeft();
                return 1.0 * done / limit;
            }

            @Override
            public boolean isFinished() {
                return countLimitingOutputPort.getElementsLeft() == 0;
            }
        };
    }

    public static CanReportProgress combinedProgress(List<CanReportProgress> progresses) {
        return new CanReportProgress() {
            @Override
            public double getProgress() {
                double sum = 0;
                int n = 0;
                for (CanReportProgress progress : progresses) {
                    double p = progress.getProgress();
                    if(!Double.isNaN(p)){
                        sum += p;
                        n++;
                    }
                }
                return sum / n;
            }

            @Override
            public boolean isFinished() {
                return progresses.stream().allMatch(CanReportProgress::isFinished);
            }
        };
    }
}