package io.apptik.roxy;


import org.reactivestreams.Processor;
import org.reactivestreams.Publisher;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

import akka.stream.ActorMaterializer;
import akka.stream.javadsl.AsPublisher;
import akka.stream.javadsl.Sink;
import akka.stream.javadsl.Source;

import static io.apptik.roxy.Roxy.TePolicy.PASS;
import static io.apptik.roxy.Roxy.TePolicy.WRAP;

/**
 * Roxy implementation using Akka Stream API
 */
public class AkkaProcProxy extends RSProcProxy<Publisher> {

    private final ActorMaterializer mat;
    private final Processor proc;
    private final TePolicy tePolicy;

    private final Map<Publisher, Subscription> subscriptions = new ConcurrentHashMap<>();
    private final AtomicInteger cnt;

    public AkkaProcProxy(Processor proc, TePolicy tePolicy, ActorMaterializer mat) {
        super(proc, tePolicy);
        this.proc = proc;
        this.tePolicy = tePolicy;
        this.mat = mat;
        cnt = new AtomicInteger(
                mat.settings().initialInputBufferSize()
        );
    }

    @Override
    @SuppressWarnings("unchecked")
    protected Publisher hide(Processor processor) {
        return (Publisher) Source
                .fromPublisher(processor)
                .runWith(Sink.asPublisher(AsPublisher.WITH_FANOUT), mat);
    }

    @Override
    @SuppressWarnings("unchecked")
    protected <T> Publisher<T> filter(Processor processor, final Class<T> filterClass) {
        Source src = Source.fromPublisher(processor)
                .filter(o -> filterClass.isAssignableFrom(o.getClass()));
        return (Publisher<T>) src.runWith(Sink.asPublisher(AsPublisher.WITH_FANOUT), mat);
    }

    @Override
    public void emit(Object event) {
        super.emit(event);
        cnt.decrementAndGet();
    }

    @Override
    public Removable addUpstream(Publisher publisher) {
        publisher.subscribe(new Subscriber() {

            Subscription s;

            @Override
            public void onSubscribe(final Subscription subscription) {
                s = subscription;
                subscriptions.put(publisher, s);
                s.request(cnt.get());
            }

            @Override
            public void onNext(Object o) {
                proc.onNext(o);
                cnt.decrementAndGet();
                if (cnt.compareAndSet(0, mat.settings().maxInputBufferSize())) {
                    try {
                        //todo super hack:
                        // Akka internal subscriber request chunks of data
                        // it might happen that we call onNext where internal subscriber did not
                        // request the next chunk. this is why we wait a little and try to use the
                        // same ratio as internals
                        Thread.sleep(33);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    s.request(mat.settings().maxInputBufferSize());
                }
            }

            @Override
            public void onError(Throwable t) {
                if (tePolicy.equals(WRAP)) {
                    proc.onNext(new Event.ErrorEvent(t));
                    cnt.decrementAndGet();
                    cnt.compareAndSet(0, mat.settings().maxInputBufferSize());
                } else if (tePolicy.equals(PASS)) {
                    proc.onError(t);
                }
            }

            @Override
            public void onComplete() {
                if (tePolicy.equals(WRAP)) {
                    proc.onNext(Event.COMPLETE);
                    cnt.decrementAndGet();
                    cnt.compareAndSet(0, mat.settings().maxInputBufferSize());
                } else if (tePolicy.equals(PASS)) {
                    proc.onComplete();
                }
            }
        });
        return () -> removeUpstream(publisher);
    }
}