/*
 * Copyright (c) 2004-2017, by Alex Leigh.
 * All rights reserved.
 *
 * THIS IS UNPUBLISHED PROPRIETARY SOURCE CODE
 * The copyright notice above does not evidence any
 * actual or intended publication of such source code.
 */

package lc.mecha.util;

import lc.mecha.log.MechaLogger;

import java.time.Duration;
import java.time.Instant;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * A version of a stop-watch geared towards rapidly repeating events.
 * <p>
 * MTS: Unsafe
 *
 * @author Alex Leigh
 */
public final class VelocityWatch {
    private final int rowRate;
    private final MechaLogger log;
    // We use atomics here because they are mutable. Should be an intmap instead, really.
    private SortedMap<String, AtomicInteger> counters = new TreeMap<>();
    private SortedMap<String, AtomicInteger> split = new TreeMap<>();
    private Instant startTime;
    private Instant splitTime;
    private int rowcnt = 0;
    private boolean mute = false;

    public VelocityWatch(MechaLogger logger) {
        this(logger, 100);
    }

    public VelocityWatch(MechaLogger logger, int rowRate) {
        this.log = logger;
        this.rowRate = rowRate;
    }

    public boolean isMute() {
        return mute;
    }

    /**
     * Mute the watch. If the watch is muted, it will not output periodic messages.
     *
     * @param mute
     */
    public void setMute(boolean mute) {
        this.mute = mute;
    }

    public void status() {
        StringAccumulator sa = new StringAccumulator(" ");

        // This case can arise if the watch is never used, and then status() is called.
        if (startTime == null) startTime = Instant.now();

        Duration d = Duration.between(startTime, Instant.now());

        sa.append("Elapsed Time: " + d + " ->");

        for (Map.Entry<String, AtomicInteger> counter : counters.entrySet()) {
            sa.append("[" + counter.getKey() + ": " + counter.getValue() + "]");
        }

        log.info(sa.asString());
    }

    private void msg() {
        StringAccumulator sa = new StringAccumulator(" ");
        Instant now = Instant.now();
        long deltaSec = Duration.between(splitTime, now).getSeconds();

        // We require at least one second to have passed before we'll print a message. This prevents spamming
        // for very high rates.
        if (deltaSec == 0)
            return;

        for (Map.Entry<String, AtomicInteger> counter : counters.entrySet()) {

            AtomicInteger splitCnt = split.get(counter.getKey());
            //  logger.info("splitcnt: {} deltaSec: {}", splitCnt, deltaSec);
            if (splitCnt == null) {
                sa.append("[" + counter.getKey() + ": " + counter.getValue() + " 0/s]");
            } else {
                int delta = splitCnt.get();
                long rate = delta / deltaSec;
                //  logger.info("delta & rate: {} {}", delta, rate);
                sa.append("[" + counter.getKey() + ": " + counter.getValue() + " " + rate + "/s]");
            }
        }

        split.clear();
        splitTime = now;
        if (!mute)
            log.info(sa.asString());
    }

    /**
     * Indicate that the given event has occurred.
     */
    public void event(String evtName) {
        if (startTime == null)
            startTime = splitTime = Instant.now();

        AtomicInteger x = counters.get(evtName);

        // There is a race here
        if (x == null) {
            counters.put(evtName, new AtomicInteger(1));
        } else {
            x.incrementAndGet();
        }

        x = split.get(evtName);
        if (x == null) {
            split.put(evtName, new AtomicInteger(1));
        } else {
            x.incrementAndGet();
        }

        rowcnt++;
        if (rowcnt % 100 == 0) msg();
    }
}
