/*
 * Copyright 2014 Netflix, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.netflix.prana.http.api;

import io.netty.util.ReferenceCountUtil;
import io.netty.util.ReferenceCounted;
import rx.Observer;
import rx.Subscriber;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.internal.operators.NotificationLite;
import rx.observers.Subscribers;
import rx.subjects.Subject;
import rx.subscriptions.Subscriptions;

import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;

/**
 * A {@link rx.subjects.Subject} implementation for caching {@link io.netty.util.ReferenceCounted} objects which can be disposed if not sent to
 * the sole subscriber of this subject.
 *
 * @author Nitesh Kant
 */
final class UnicastDisposableCachingSubject<T extends ReferenceCounted> extends Subject<T, T> {

    private final State<T> state;

    private UnicastDisposableCachingSubject(State<T> state) {
        super(new OnSubscribeAction<>(state));
        this.state = state;
    }

    public static <T extends ReferenceCounted> UnicastDisposableCachingSubject<T> create() {
        State<T> state = new State<>();
        return new UnicastDisposableCachingSubject<>(state);
    }

    /**
     * Disposes the items held by this subject which were not given out to the lone subscriber.
     *
     * @param disposedElementsProcessor All elements that were disposed are passed to this processor.
     */
    public void dispose(Action1<T> disposedElementsProcessor) {
        if (state.casState(State.STATES.UNSUBSCRIBED, State.STATES.DISPOSED)) {
            _dispose(disposedElementsProcessor);
        } else if (state.casState(State.STATES.SUBSCRIBED, State.STATES.DISPOSED)) {
            state.observerRef.onCompleted(); // Complete the existing subscription in case it is still not unsubscribed.
            _dispose(disposedElementsProcessor);
        }
    }

    private void _dispose(Action1<T> disposedElementsProcessor) {
        Subscriber<T> noOpSub = new PassThruObserver<>(Subscribers.create(disposedElementsProcessor,
                new Action1<Throwable>() {
                    @Override
                    public void call(Throwable throwable) {

                    }
                }),
                state); // Any buffering post buffer draining must not be lying in the buffer
        state.buffer.sendAllNotifications(noOpSub); // It is important to empty the buffer before setting the observer.
        // If not done, there can be two threads draining the buffer
        // (PassThroughObserver on any notification) and this thread.
        state.setObserverRef(noOpSub); // All future notifications are not sent anywhere.
    }

    /**
     * The common state.
     */
    private static final class State<T> {

        /**
         * Following are the only possible state transitions:
         * UNSUBSCRIBED -> SUBSCRIBED
         * UNSUBSCRIBED -> DISPOSED
         */
        private enum STATES {
            UNSUBSCRIBED /*Initial*/, SUBSCRIBED /*Terminal state*/, DISPOSED/*Terminal state*/
        }

        private volatile int state = STATES.UNSUBSCRIBED.ordinal(); /*Values are the ordinals of STATES enum*/

        /**
         * Following Observers are associated with the states:
         * UNSUBSCRIBED => {@link BufferedObserver}
         * SUBSCRIBED => {@link PassThruObserver}
         * DISPOSED => {@link Subscribers#empty()}
         */
        private volatile Observer<? super T> observerRef = new BufferedObserver();

        /**
         * The only buffer associated with this state. All notifications go to this buffer if no one has subscribed and
         * the {@link UnicastDisposableCachingSubject} instance is not disposed.
         */
        private final ByteBufAwareBuffer<T> buffer = new ByteBufAwareBuffer<>();

        /**
         * Field updater for observerRef.
         */
        @SuppressWarnings("rawtypes")
        private static final AtomicReferenceFieldUpdater<State, Observer> OBSERVER_UPDATER
                = AtomicReferenceFieldUpdater.newUpdater(State.class, Observer.class, "observerRef");

        /**
         * Field updater for state.
         */
        @SuppressWarnings("rawtypes")
        private static final AtomicIntegerFieldUpdater<State> STATE_UPDATER
                = AtomicIntegerFieldUpdater.newUpdater(State.class, "state");

        public boolean casState(STATES expected, STATES next) {
            return STATE_UPDATER.compareAndSet(this, expected.ordinal(), next.ordinal());
        }

        public void setObserverRef(Observer<? super T> o) { // Guarded by casState()
            observerRef = o;
        }

        public boolean casObserverRef(Observer<? super T> expected, Observer<? super T> next) {
            return OBSERVER_UPDATER.compareAndSet(this, expected, next);
        }

        /**
         * The default subscriber when the enclosing state is created.
         */
        private final class BufferedObserver extends Subscriber<T> {

            private final NotificationLite<Object> nl = NotificationLite.instance();

            @Override
            public void onCompleted() {
                buffer.add(nl.completed());
            }

            @Override
            public void onError(Throwable e) {
                buffer.add(nl.error(e));
            }

            @Override
            public void onNext(T t) {
                buffer.add(nl.next(t));
            }
        }
    }

    private static final class OnSubscribeAction<T> implements OnSubscribe<T> {

        private final State<T> state;

        public OnSubscribeAction(State<T> state) {
            this.state = state;
        }

        @Override
        public void call(final Subscriber<? super T> subscriber) {
            if (state.casState(State.STATES.UNSUBSCRIBED, State.STATES.SUBSCRIBED)) {

                // drain queued notifications before subscription
                // we do this here before PassThruObserver so the consuming thread can do this before putting itself in
                // the line of the producer
                state.buffer.sendAllNotifications(subscriber);

                // register real observer for pass-thru ... and drain any further events received on first notification
                state.setObserverRef(new PassThruObserver<>(subscriber, state));
                subscriber.add(Subscriptions.create(new Action0() {
                    @Override
                    public void call() {
                        state.setObserverRef(Subscribers.empty());
                    }
                }));
            } else if (State.STATES.SUBSCRIBED.ordinal() == state.state) {
                subscriber.onError(new IllegalStateException("Content can only have one subscription. Use Observable.publish() if you want to multicast."));
            } else if (State.STATES.DISPOSED.ordinal() == state.state) {
                subscriber.onError(new IllegalStateException("Content stream is already disposed."));
            }
        }

    }

    @Override
    public void onCompleted() {
        state.observerRef.onCompleted();
    }

    @Override
    public void onError(Throwable e) {
        state.observerRef.onError(e);
    }

    @Override
    public void onNext(T t) {
        state.observerRef.onNext(t);
    }

    /**
     * This is a temporary observer between buffering and the actual that gets into the line of notifications
     * from the producer and will drain the queue of any items received during the race of the initial drain and
     * switching this.
     * <p/>
     * It will then immediately swap itself out for the actual (after a single notification), but since this is
     * now being done on the same producer thread no further buffering will occur.
     */
    private static final class PassThruObserver<T> extends Subscriber<T> {

        private final Observer<? super T> actual;
        // this assumes single threaded synchronous notifications (the Rx contract for a single Observer)
        private final ByteBufAwareBuffer<T> buffer; // Same buffer instance from the original BufferedObserver.
        private final State<T> state;

        PassThruObserver(Observer<? super T> actual, State<T> state) {
            this.actual = actual;
            buffer = state.buffer;
            this.state = state;
        }

        @Override
        public void onCompleted() {
            drainIfNeededAndSwitchToActual();
            actual.onCompleted();
        }

        @Override
        public void onError(Throwable e) {
            drainIfNeededAndSwitchToActual();
            actual.onError(e);
        }

        @Override
        public void onNext(T t) {
            drainIfNeededAndSwitchToActual();
            actual.onNext(t);
        }

        private void drainIfNeededAndSwitchToActual() {
            buffer.sendAllNotifications(this);
            // now we can safely change over to the actual and get rid of the pass-thru
            // but only if not unsubscribed
            state.casObserverRef(this, actual);
        }
    }

    private static final class ByteBufAwareBuffer<T> {

        private final ConcurrentLinkedQueue<Object> actual = new ConcurrentLinkedQueue<>();
        private final NotificationLite<T> nl = NotificationLite.instance();

        private void add(Object toAdd) {
            ReferenceCountUtil.retain(toAdd); // Released when the notification is sent.
            actual.add(toAdd);
        }

        public void sendAllNotifications(Subscriber<? super T> subscriber) {
            Object notification; // Can be onComplete notification, onError notification or just the actual "T".
            while ((notification = actual.poll()) != null) {
                try {
                    nl.accept(subscriber, notification);
                } finally {
                    ReferenceCountUtil.release(notification); // If it is the actual T for onNext and is a ByteBuf, it will be released.
                }
            }
        }
    }
}