package ru.yandex.qe.dispenser.domain.pefomance;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;

import com.google.common.base.Stopwatch;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


@Aspect
public class TimingAspect {
    private static final Logger LOG = LoggerFactory.getLogger(TimingAspect.class);
    private final ThreadLocal<AtomicInteger> counterContainer = new ThreadLocal<>();
    private final ThreadLocal<List<Result>> resultsContainer = new ThreadLocal<>();

    @Around("execution(public * *(..)) && @annotation(timing)")
    public Object around(final @NotNull ProceedingJoinPoint jp, final @NotNull Timing timing) throws Throwable {
        final AtomicInteger counter = getFromTl(counterContainer, AtomicInteger::new);
        final int level = counter.getAndIncrement();

        final Stopwatch stopwatch = Stopwatch.createStarted();
        try {
            return jp.proceed();
        } finally {
            final long duration = stopwatch.elapsed(TimeUnit.MILLISECONDS);
            final List<Result> results = getFromTl(resultsContainer, ArrayList::new);
            results.add(0, new Result(!timing.key().isEmpty() ? timing.key() : jp.getSignature().toLongString(), duration, level));
            if (counter.decrementAndGet() == 0) {
                if (!timing.root()) {
                    LOG.info("NOT ROOOT");
                    final StringBuilder sb = new StringBuilder();
                    results.forEach(r -> {
                        sb
                                .append(r.level)
                                .append(":")
                                .append(r.key)
                                .append(":")
                                .append(r.time)
                                .append("\t");
                    });
                    LOG.info("{}", sb);
                }
                if (timing.root() && timing.threshold() < duration) {
                    final StringBuilder sb = new StringBuilder();
                    results.forEach(r -> {
                        sb
                                .append(r.level)
                                .append(":")
                                .append(r.key)
                                .append(":")
                                .append(r.time)
                                .append("\t");
                    });
                    LOG.info("{}", sb);
                }
                results.clear();
            }

        }
    }

    private static <T> T getFromTl(final ThreadLocal<T> tl, final Supplier<T> supplier) {
        T obj = tl.get();
        if (obj == null) {
            obj = supplier.get();
            tl.set(obj);
        }
        return obj;
    }

    public static class Result {
        final String key;
        final long time;
        final int level;

        private Result(final String key, final long time, final int level) {
            this.key = key;
            this.time = time;
            this.level = level;
        }
    }
}
