package io.smallrye.mutiny.operators.multi;

import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;

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

import io.smallrye.mutiny.Multi;
import io.smallrye.mutiny.helpers.ParameterValidation;
import io.smallrye.mutiny.helpers.Subscriptions;
import io.smallrye.mutiny.operators.AbstractMulti;
import io.smallrye.mutiny.operators.multi.processors.UnicastProcessor;
import io.smallrye.mutiny.subscription.MultiSubscriber;
import io.smallrye.mutiny.subscription.SerializedSubscriber;
import io.smallrye.mutiny.subscription.SwitchableSubscriptionSubscriber;

/**
 * Retries a source when a companion stream signals an item in response to the main's failure event.
 * <p>
 * If the companion stream signals when the main source is active, the repeat
 * attempt is suppressed and any terminal signal will terminate the main source with the same signal immediately.
 *
 * @param <T> the type of item
 */
public final class MultiRetryWhenOp<T> extends AbstractMultiOperator<T, T> {

    private final Function<? super Multi<Throwable>, ? extends Publisher<?>> triggerStreamFactory;

    public MultiRetryWhenOp(Multi<? extends T> upstream,
            Function<? super Multi<Throwable>, ? extends Publisher<?>> triggerStreamFactory) {
        super(upstream);
        this.triggerStreamFactory = ParameterValidation.nonNull(triggerStreamFactory, "triggerStreamFactory");
    }

    private static <T> void subscribe(MultiSubscriber<? super T> downstream,
            Function<? super Multi<Throwable>, ? extends Publisher<?>> triggerStreamFactory,
            Multi<? extends T> upstream) {
        TriggerSubscriber other = new TriggerSubscriber();
        Subscriber<Throwable> signaller = new SerializedSubscriber<>(other.processor);
        signaller.onSubscribe(Subscriptions.empty());
        MultiSubscriber<T> serialized = new SerializedSubscriber<>(downstream);

        RetryWhenOperator<T> operator = new RetryWhenOperator<>(upstream, serialized, signaller);
        other.operator = operator;

        serialized.onSubscribe(operator);
        Publisher<?> publisher;

        try {
            publisher = triggerStreamFactory.apply(other);
            if (publisher == null) {
                throw new NullPointerException("The stream factory returned `null`");
            }
        } catch (Throwable e) {
            downstream.onFailure(e);
            return;
        }

        publisher.subscribe(other);

        if (!operator.isCancelled()) {
            upstream.subscribe(operator);
        }
    }

    @Override
    public void subscribe(MultiSubscriber<? super T> downstream) {
        subscribe(downstream, triggerStreamFactory, upstream);
    }

    static final class RetryWhenOperator<T> extends SwitchableSubscriptionSubscriber<T> {

        private final Publisher<? extends T> upstream;
        private final AtomicInteger wip = new AtomicInteger();
        private final Subscriber<Throwable> signaller;
        private final Subscriptions.DeferredSubscription arbiter = new Subscriptions.DeferredSubscription();

        long produced;

        RetryWhenOperator(Publisher<? extends T> upstream, MultiSubscriber<? super T> downstream,
                Subscriber<Throwable> signaller) {
            super(downstream);
            this.upstream = upstream;
            this.signaller = signaller;
        }

        @Override
        public void cancel() {
            if (!isCancelled()) {
                arbiter.cancel();
                super.cancel();
            }

        }

        public void setWhen(Subscription w) {
            arbiter.set(w);
        }

        @Override
        public void onItem(T t) {
            downstream.onItem(t);
            produced++;
        }

        @Override
        public void onFailure(Throwable t) {
            long p = produced;
            if (p != 0L) {
                produced = 0;
                emitted(p);
            }
            arbiter.request(1);
            signaller.onNext(t);
        }

        @Override
        public void onCompletion() {
            arbiter.cancel();
            downstream.onComplete();
        }

        void resubscribe() {
            if (wip.getAndIncrement() == 0) {
                do {
                    if (isCancelled()) {
                        return;
                    }

                    upstream.subscribe(this);

                } while (wip.decrementAndGet() != 0);
            }
        }

        void whenFailure(Throwable failure) {
            super.cancel();
            downstream.onFailure(failure);
        }

        void whenComplete() {
            super.cancel();
            downstream.onComplete();
        }
    }

    @SuppressWarnings({ "SubscriberImplementation" })
    static final class TriggerSubscriber extends AbstractMulti<Throwable>
            implements Multi<Throwable>, Subscriber<Object> {
        RetryWhenOperator<?> operator;
        private final Processor<Throwable, Throwable> processor = UnicastProcessor.<Throwable> create().serialized();

        @Override
        public void onSubscribe(Subscription s) {
            operator.setWhen(s);
        }

        @Override
        public void onNext(Object t) {
            operator.resubscribe();
        }

        @Override
        public void onError(Throwable t) {
            operator.whenFailure(t);
        }

        @Override
        public void onComplete() {
            operator.whenComplete();
        }

        @Override
        public void subscribe(Subscriber<? super Throwable> actual) {
            processor.subscribe(actual);
        }
    }

}