/*
 * Copyright (c) "Neo4j"
 * Neo4j Sweden AB [http://neo4j.com]
 *
 * This file is part of Neo4j.
 *
 * Neo4j 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.
 *
 * This program 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 org.neo4j.kernel.impl.api.index;

import java.time.Clock;
import java.util.EnumMap;
import java.util.StringJoiner;
import java.util.concurrent.TimeUnit;
import org.neo4j.internal.helpers.TimeUtil;
import org.neo4j.logging.InternalLog;
import org.neo4j.util.FeatureToggles;
import org.neo4j.util.Preconditions;
import org.neo4j.util.VisibleForTesting;

public class LoggingPhaseTracker implements PhaseTracker {
    private static final String MESSAGE_PREFIX = "TIME/PHASE ";
    static final int PERIOD_INTERVAL = FeatureToggles.getInteger(LoggingPhaseTracker.class, "period_interval", 600);

    private final long periodInterval;
    private final InternalLog log;
    private final Clock clock;

    private final EnumMap<Phase, Logger> times = new EnumMap<>(Phase.class);
    private Phase currentPhase;
    private long timeEnterPhase;
    private boolean stopped;
    private long lastPeriodReport = -1;

    LoggingPhaseTracker(InternalLog log) {
        this(PERIOD_INTERVAL, log, Clock.systemUTC());
    }

    @VisibleForTesting
    LoggingPhaseTracker(long periodIntervalInSeconds, InternalLog log, Clock clock) {
        this.periodInterval = TimeUnit.SECONDS.toMillis(periodIntervalInSeconds);
        this.log = log;
        this.clock = clock;
        for (Phase phase : Phase.values()) {
            times.put(phase, new Logger(phase));
        }
    }

    @Override
    public void enterPhase(Phase phase) {
        if (stopped) {
            throw new IllegalStateException("Trying to report a new phase after phase tracker has been stopped.");
        }
        if (phase != currentPhase) {
            long now = logCurrentTime();
            currentPhase = phase;
            timeEnterPhase = now;

            if (lastPeriodReport == -1) {
                lastPeriodReport = now;
            }

            long millisSinceLastPeriodReport = now - lastPeriodReport;
            if (millisSinceLastPeriodReport >= periodInterval) {
                // Report period
                periodReport(millisSinceLastPeriodReport);
                lastPeriodReport = now;
            }
        }
    }

    @Override
    public void registerTime(Phase phase, long millis) {
        Preconditions.checkState(!stopped, "Trying to report a new phase after phase tracker has been stopped.");
        logTime(phase, millis);
    }

    @Override
    public void stop() {
        stopped = true;
        logCurrentTime();
        currentPhase = null;
        finalReport();
    }

    EnumMap<Phase, Logger> times() {
        return times;
    }

    private void finalReport() {
        log.debug(MESSAGE_PREFIX + mainReportString("Final"));
    }

    private void periodReport(long millisSinceLastPeriodReport) {
        String periodReportString = periodReportString(millisSinceLastPeriodReport);
        String mainReportString = mainReportString("Total");
        log.debug(MESSAGE_PREFIX + mainReportString + ", " + periodReportString);
    }

    private String mainReportString(String title) {
        StringJoiner joiner = new StringJoiner(", ", title + ": ", "");
        times.values().forEach(logger -> reportToJoiner(joiner, logger));
        return joiner.toString();
    }

    private String periodReportString(long millisSinceLastPeriodReport) {
        long secondsSinceLastPeriodReport = TimeUnit.MILLISECONDS.toSeconds(millisSinceLastPeriodReport);
        StringJoiner joiner = new StringJoiner(", ", "Last " + secondsSinceLastPeriodReport + " sec: ", "");
        times.values().stream().map(Logger::period).forEach(period -> {
            reportToJoiner(joiner, period);
            period.reset();
        });
        return joiner.toString();
    }

    private static void reportToJoiner(StringJoiner joiner, Counter counter) {
        if (counter.nbrOfReports > 0) {
            joiner.add(counter.toString());
        }
    }

    private long logCurrentTime() {
        long now = clock.millis();
        if (currentPhase != null) {
            logTime(currentPhase, now - timeEnterPhase);
        }
        return now;
    }

    private void logTime(Phase phase, long timeMillis) {
        Logger logger = times.get(phase);
        logger.log(timeMillis);
    }

    public static class Logger extends Counter {
        final Counter periodCounter;

        private Logger(Phase phase) {
            super(phase);
            periodCounter = new Counter(phase);
            periodCounter.reset();
        }

        @Override
        void log(long timeMillis) {
            super.log(timeMillis);
            periodCounter.log(timeMillis);
        }

        Counter period() {
            return periodCounter;
        }
    }

    public static class Counter {
        private final Phase phase;
        long totalTime;
        long nbrOfReports;
        long maxTime;
        long minTime;

        Counter(Phase phase) {
            this.phase = phase;
        }

        void log(long timeMillis) {
            totalTime += timeMillis;
            nbrOfReports++;
            maxTime = Math.max(maxTime, timeMillis);
            minTime = Math.min(minTime, timeMillis);
        }

        void reset() {
            totalTime = 0;
            nbrOfReports = 0;
            maxTime = Long.MIN_VALUE;
            minTime = Long.MAX_VALUE;
        }

        @Override
        public String toString() {
            StringJoiner joiner = new StringJoiner(", ", phase + "[", "]");
            if (nbrOfReports == 0) {
                addToString("nbrOfReports", nbrOfReports, joiner, false);
            } else if (nbrOfReports == 1) {
                addToString("totalTime", totalTime, joiner, true);
            } else {
                long avgTime = totalTime / nbrOfReports;
                addToString("totalTime", totalTime, joiner, true);
                addToString("avgTime", avgTime, joiner, true);
                addToString("minTime", minTime, joiner, true);
                addToString("maxTime", maxTime, joiner, true);
                addToString("nbrOfReports", nbrOfReports, joiner, false);
            }
            return joiner.toString();
        }

        static void addToString(String name, long measurement, StringJoiner joiner, boolean isTime) {
            String measurementString;
            if (isTime) {
                long timeInNanos = TimeUnit.MILLISECONDS.toNanos(measurement);
                measurementString = TimeUtil.nanosToString(timeInNanos);
            } else {
                measurementString = Long.toString(measurement);
            }
            joiner.add(String.format("%s=%s", name, measurementString));
        }
    }
}
