package com.izouma.nineth.aspect;

import com.izouma.nineth.annotations.Debounce;
import com.izouma.nineth.aspect.debounce.DebounceTask;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

@Aspect
@Component
@Slf4j
public class DebounceAspect {

    private DefaultParameterNameDiscoverer nameDiscoverer  = new DefaultParameterNameDiscoverer();
    private HashMap<String, Future<Void>>  debounceStore   = new HashMap<>();
    private ScheduledExecutorService       executorService = Executors.newScheduledThreadPool(10);
    private Map<String, Long>              debounceCounter = new HashMap<>();

    @Pointcut("@annotation(com.izouma.nineth.annotations.Debounce)")
    public void debouncePointCut() {
    }

    @Around(value = "debouncePointCut() && @annotation(debounce)")
    public synchronized void debounce(ProceedingJoinPoint joinPoint, Debounce debounce) {
        ExpressionParser parser = new SpelExpressionParser();
        EvaluationContext context = new StandardEvaluationContext(joinPoint.getSignature());
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        String[] paramNames = nameDiscoverer.getParameterNames(method);
        Object[] args = joinPoint.getArgs();
        for (int i = 0; i < args.length; i++) {
            context.setVariable(paramNames[i], args[i]);
        }
        String key = Optional.ofNullable(parser.parseExpression(debounce.key()).getValue(context)).map(Object::toString)
                .orElse("default");

        Future<Void> future = debounceStore.get(key);
        long lastRun = debounceCounter.getOrDefault(key, 0L);
        if (future != null && !future.isDone()) {
            if (System.currentTimeMillis() - lastRun > debounce.delay()) {
                debounceCounter.put(key, System.currentTimeMillis());
            } else {
                future.cancel(false);
            }
        }
        debounceStore.put(key, executorService.schedule(new DebounceTask(joinPoint, (Void) -> {
            debounceCounter.put(key, System.currentTimeMillis());
            return null;
        }), debounce.delay(), TimeUnit.MILLISECONDS));

    }

}