package com.github.davidmoten.rx.internal.operators;

import com.github.davidmoten.rx.util.BackpressureStrategy;
import com.github.davidmoten.util.Preconditions;

import rx.Notification;
import rx.Observable;
import rx.Observable.OnSubscribe;
import rx.Observable.Transformer;
import rx.Subscriber;
import rx.functions.Func0;
import rx.functions.Func1;
import rx.functions.Func2;
import rx.functions.Func3;

public final class TransformerStateMachine<State, In, Out> implements Transformer<In, Out> {

    private final Func0<? extends State> initialState;
    private final Func3<? super State, ? super In, ? super Subscriber<Out>, ? extends State> transition;
    private final Func2<? super State, ? super Subscriber<Out>, Boolean> completion;
    private final BackpressureStrategy backpressureStrategy;
    private final int initialRequest;

    private TransformerStateMachine(Func0<? extends State> initialState,
            Func3<? super State, ? super In, ? super Subscriber<Out>, ? extends State> transition,
            Func2<? super State, ? super Subscriber<Out>, Boolean> completion,
            BackpressureStrategy backpressureStrategy, int initialRequest) {
        Preconditions.checkNotNull(initialState);
        Preconditions.checkNotNull(transition);
        Preconditions.checkNotNull(completion);
        Preconditions.checkNotNull(backpressureStrategy);
        Preconditions.checkArgument(initialRequest > 0, "initialRequest must be greater than zero");
        this.initialState = initialState;
        this.transition = transition;
        this.completion = completion;
        this.backpressureStrategy = backpressureStrategy;
        this.initialRequest = initialRequest;
    }

    public static <State, In, Out> Transformer<In, Out> create(Func0<? extends State> initialState,
            Func3<? super State, ? super In, ? super Subscriber<Out>, ? extends State> transition,
            Func2<? super State, ? super Subscriber<Out>, Boolean> completion,
            BackpressureStrategy backpressureStrategy, int initialRequest) {
        return new TransformerStateMachine<State, In, Out>(initialState, transition, completion,
                backpressureStrategy, initialRequest);
    }

    @Override
    public Observable<Out> call(final Observable<In> source) {
        // use defer so we can have a single state reference for each
        // subscription
        return Observable.defer(new Func0<Observable<Out>>() {
            @Override
            public Observable<Out> call() {
                Mutable<State> state = new Mutable<State>(initialState.call());
                return source.materialize()
                        // do state transitions and emit notifications
                        // use flatMap to emit notification values
                        .flatMap(execute(transition, completion, state, backpressureStrategy),
                                initialRequest)
                        // complete if we encounter an unsubscribed sentinel
                        .takeWhile(NOT_UNSUBSCRIBED)
                        // flatten notifications to a stream which will enable
                        // early termination from the state machine if desired
                        .dematerialize();
            }
        });
    }

    private static <State, Out, In> Func1<Notification<In>, Observable<Notification<Out>>> execute(
            final Func3<? super State, ? super In, ? super Subscriber<Out>, ? extends State> transition,
            final Func2<? super State, ? super Subscriber<Out>, Boolean> completion,
            final Mutable<State> state, final BackpressureStrategy backpressureStrategy) {

        return new Func1<Notification<In>, Observable<Notification<Out>>>() {

            @Override
            public Observable<Notification<Out>> call(final Notification<In> in) {

                Observable<Notification<Out>> o = Observable
                        .create(new OnSubscribe<Notification<Out>>() {

                    @Override
                    public void call(Subscriber<? super Notification<Out>> subscriber) {
                        Subscriber<Out> w = wrap(subscriber);
                        if (in.hasValue()) {
                            state.value = transition.call(state.value, in.getValue(), w);
                            if (!subscriber.isUnsubscribed())
                                subscriber.onCompleted();
                            else {
                                // this is a special error to indicate that the
                                // transition called unsubscribe. It will be
                                // filtered later.
                                subscriber.onNext(UnsubscribedNotificationHolder.<Out>unsubscribedNotification());
                            }
                        } else if (in.isOnCompleted()) {
                            if (completion.call(state.value, w) && !subscriber.isUnsubscribed()) {
                                w.onCompleted();
                            }
                        } else if (!subscriber.isUnsubscribed()) {
                            w.onError(in.getThrowable());
                        }
                    }
                });
                // because the observable we just created does not
                // support backpressure we need to apply a backpressure
                // handling operator. This operator is supplied by the
                // user.
                return applyBackpressure(o, backpressureStrategy);
            }

        };
    }
    
    private static final class UnsubscribedNotificationHolder {
        private static final Notification<Object> INSTANCE = Notification.createOnNext(null);
        
        @SuppressWarnings("unchecked")
        static <T> Notification<T> unsubscribedNotification() {
            return (Notification<T>) INSTANCE;
        }
    }

    private static <Out> Observable<Notification<Out>> applyBackpressure(
            Observable<Notification<Out>> o, final BackpressureStrategy backpressureStrategy) {
        if (backpressureStrategy == BackpressureStrategy.BUFFER)
            return o.onBackpressureBuffer();
        else if (backpressureStrategy == BackpressureStrategy.DROP)
            return o.onBackpressureDrop();
        else if (backpressureStrategy == BackpressureStrategy.LATEST)
            return o.onBackpressureLatest();
        else
            throw new IllegalArgumentException(
                    "backpressure strategy not supported: " + backpressureStrategy);
    }

    private static final Func1<Notification<?>, Boolean> NOT_UNSUBSCRIBED = new Func1<Notification<?>, Boolean>() {

        @Override
        public Boolean call(Notification<?> t) {
            return t != UnsubscribedNotificationHolder.unsubscribedNotification();
        }

    };

    private static final class Mutable<T> {
        T value;

        Mutable(T value) {
            this.value = value;
        }
    }

    private static <Out> NotificationSubscriber<Out> wrap(
            Subscriber<? super Notification<Out>> sub) {
        return new NotificationSubscriber<Out>(sub);
    }

    private static final class NotificationSubscriber<Out> extends Subscriber<Out> {

        private final Subscriber<? super Notification<Out>> sub;

        NotificationSubscriber(Subscriber<? super Notification<Out>> sub) {
            this.sub = sub;
            add(sub);
        }

        @Override
        public void onCompleted() {
            sub.onNext(Notification.<Out> createOnCompleted());
        }

        @Override
        public void onError(Throwable e) {
            sub.onNext(Notification.<Out> createOnError(e));
        }

        @Override
        public void onNext(Out t) {
            sub.onNext(Notification.createOnNext(t));
        }

    }

}

@@ -4509,20 +4509,23 @@
 pecial e
-rror
+mission
  to indi

update comment