/*
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 * @copyright Ranides Atterwim
 * @license WTFPL
 * @url http://ranides.net/projects/assira.drafts
 */

package net.ranides.assira.time;

import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;

import net.ranides.assira.collection.maps.IdentMap;

/**
 * Statefull class used for measuring elapsed time.
 *
 * It can be used as stopwatch: it means it has "start" "stop" and "split" functions.
 *
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 */
public final class TimeMetric {
    
    private static final Map<TimeUnit, String> UNITS = new IdentMap<>(
        new TimeUnit[]{TimeUnit.DAYS, TimeUnit.HOURS, TimeUnit.MICROSECONDS, TimeUnit.MILLISECONDS, TimeUnit.MINUTES, TimeUnit.NANOSECONDS, TimeUnit.SECONDS}, 
        new String[]{"d", "h", "us", "ms", "m", "ns", "s"}
    );
    
    private final TimeUnit unit;
    
    private final Deque<Long> stack = new ArrayDeque<>();

    private final boolean threadsafe;

    /**
     * Creates new inactive stopwatch.
     * Returned results are expressed in milliseconds by default.
     */
    public TimeMetric() {
        this(false);
    }

    /**
     * Creates new inactive stopwatch.
     * Returned results are expressed in "unit" by default.
     *
     * Please note, that "unit" does not internal precision, but only default results returned
     * by split or stop
     *
     * @param unit unit
     */
    public TimeMetric(TimeUnit unit) {
        this(false, unit);
    }

    /**
     * Creates new inactive stopwatch.
     * Returned results are expressed in milliseconds by default.
     */
    public TimeMetric(boolean threadsafe) {
        this(threadsafe, TimeUnit.MILLISECONDS);
    }

    /**
     * Creates new inactive stopwatch.
     * Returned results are expressed in "unit" by default.
     *
     * Please note, that "unit" does not internal precision, but only default results returned
     * by split or stop
     *
     * @param unit unit
     */
    public TimeMetric(boolean threadsafe, TimeUnit unit) {
        this.unit = unit;
        this.threadsafe = threadsafe;
        start();
    }

    /**
     * Starts new clock.
     * Previously started, but not stopped measurements are still active.
     *
     * @return this
     */
    public TimeMetric start() {
        return $synchronized(() -> {
            stack.push(System.nanoTime());
            return this;
        });
    }

    /**
     * Stops last measurement and returns elapsed time.
     * You can call "stop" as many times as you called "start". Measurements are stacked.
     *
     * @return elapsed time
     */
    public long stop() {
        return stop(unit);
    }

    /**
     * Stops last measurement and returns elapsed time.
     * You can call "stop" as many times as you called "start". Measurements are stacked.
     *
     * @param unit unit
     * @return elapsed time
     */
    public long stop(TimeUnit unit) {
        return $synchronized(() -> {
            return time(unit, stack.pop());
        });
    }

    /**
     * Returns elapsed time for last measurement and resets its counter to zero.
     * "Split" does not affect measurements other than the last one.
     *
     * @return elapsed time
     */
    public long split() {
        long dt = stop();
        start();
        return dt;
    }

    /**
     * Returns elapsed time for last measurement.
     * It does not change state of clockwatch.
     *
     * @return elapsed time
     */
    public long time() {
        return time(unit);
    }

    /**
     * Returns elapsed time for last measurement.
     * It does not change state of clockwatch.
     *
     * @param unit unit
     * @return elapsed time
     */
    public long time(TimeUnit unit) {
        return $synchronized(() -> {
            if (stack.isEmpty()) {
                throw new IllegalStateException("#start must be called first");
            }
            return time(unit, stack.peek());
        });
    }

    private <T> T $synchronized(Supplier<T> action) {
        if(threadsafe) {
            synchronized (stack) {
                return action.get();
            }
        } else {
            return action.get();
        }
    }


    private static long time(TimeUnit unit, long dt) {
        return unit.convert(System.nanoTime() - dt, TimeUnit.NANOSECONDS);
    }

    @Override
    public String toString() {
        return time() + UNITS.get(unit);
    }
    
}
