/*
 * Copyright © 2018 Apple Inc. and the ServiceTalk project authors
 *
 * 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 io.servicetalk.concurrent.internal;

import io.servicetalk.concurrent.Cancellable;
import io.servicetalk.concurrent.CompletableSource;
import io.servicetalk.concurrent.PublisherSource;
import io.servicetalk.concurrent.PublisherSource.Subscriber;
import io.servicetalk.concurrent.PublisherSource.Subscription;
import io.servicetalk.concurrent.SingleSource;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.atomic.AtomicLongFieldUpdater;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
import javax.annotation.Nullable;

import static io.servicetalk.concurrent.Cancellable.IGNORE_CANCEL;
import static io.servicetalk.concurrent.internal.EmptySubscription.EMPTY_SUBSCRIPTION;
import static java.lang.Math.min;

/**
 * A set of utilities for common {@link Subscriber} tasks.
 */
public final class SubscriberUtils {
    private static final Logger LOGGER = LoggerFactory.getLogger(SubscriberUtils.class);

    private SubscriberUtils() {
        // No instances.
    }

    /**
     * Checks for an already existing {@link Subscription} and if one is given calls {@link Subscription#cancel()} on
     * {@code next} and returns {@code false}.
     *
     * @param existing the existing {@link Subscription} or {@code null} if none exists.
     * @param next the next {@link Subscription} to use.
     * @return {@code true} if no {@link Subscription} exists, {@code false} otherwise.
     */
    public static boolean checkDuplicateSubscription(@Nullable Subscription existing, Subscription next) {
        if (existing != null) {
            next.cancel();
            return false;
        }
        return true;
    }

    /**
     * Returns {@code false} if the requested amount of elements {@code n} is not-positive, {@code true} otherwise.
     *
     * @param n the number of elements to request.
     * @return {@code false} if the requested amount of elements {@code n} is not-positive, {@code true} otherwise.
     */
    public static boolean isRequestNValid(long n) {
        return n > 0;
    }

    /**
     * Create a new exception for an invalid amount of {@link Subscription#request(long)} according to
     * <a href="https://github.com/reactive-streams/reactive-streams-jvm/blob/v1.0.1/README.md#3.9">Reactive Streams,
     * Rule 3.9</a>.
     * @param n the invalid request count.
     * @return The exception which clarifies the invalid behavior.
     */
    public static IllegalArgumentException newExceptionForInvalidRequestN(long n) {
        return new IllegalArgumentException("Rule 3.9 states non-positive request signals are illegal, but got: " + n);
    }

    /**
     * Attempts to increment {@code sourceRequestedUpdater} in order to make it the same as {@code requestNUpdater}
     * while not exceeding the {@code limit}.
     * @param requestNUpdater The total number which has been requested (typically from
     * {@link Subscription#request(long)}).
     * @param sourceRequestedUpdater The total number which has actually been passed to
     * {@link Subscription#request(long)}. This outstanding count
     * {@code sourceRequestedUpdater() - emittedUpdater.get()} should never exceed {@code limit}.
     * @param emittedUpdater The amount of data that has been emitted/delivered by the source.
     * @param limit The maximum outstanding demand from the source at any given time.
     * @param owner The object which all atomic updater parameters are associated with.
     * @param <T> The type of object which owns the atomic updater parameters.
     * @return The amount that {@code sourceRequestedUpdater} was increased by. This value is typically used to call
     * {@link Subscription#request(long)}.
     */
    public static <T> int calculateSourceRequested(final AtomicLongFieldUpdater<T> requestNUpdater,
                                                   final AtomicLongFieldUpdater<T> sourceRequestedUpdater,
                                                   final AtomicLongFieldUpdater<T> emittedUpdater,
                                                   final int limit,
                                                   final T owner) {
        for (;;) {
            final long sourceRequested = sourceRequestedUpdater.get(owner);
            final long requested = requestNUpdater.get(owner);
            if (requested == sourceRequested) {
                return 0;
            }
            final long emitted = emittedUpdater.get(owner);
            // Connected sources (like each Publisher in a group-by) may buffer data before requesting as the peer
            // source could have requested the data. In such cases, the source would drain and then call this method
            // leading to emitted > sourceRequested
            if (emitted > sourceRequested) {
                // sourceRequested ... emitted ...[delta]... requested
                final long delta = requested - emitted;
                final int toRequest = (int) min(limit, delta);
                if (sourceRequestedUpdater.compareAndSet(owner, sourceRequested, emitted + toRequest)) {
                    return toRequest;
                }
            } else {
                // emitted ...[outstanding]... sourceRequested ...[delta]... requested
                final long outstanding = sourceRequested - emitted;
                final long delta = requested - sourceRequested;
                final int toRequest = (int) min(limit - outstanding, delta);
                if (sourceRequestedUpdater.compareAndSet(owner, sourceRequested, sourceRequested + toRequest)) {
                    return toRequest;
                }
            }
        }
    }

    /**
     * There are some scenarios where a completion {@link TerminalNotification} can be overridden with an error if
     * errors are produced asynchronously.
     * <p>
     * This method helps set {@link TerminalNotification} atomically providing such an override.
     *
     * @param toSet {@link TerminalNotification} to set.
     * @param overrideComplete Whether exisiting {@link TerminalNotification#complete()} should be overridden with the
     * {@code toSet}.
     * @param terminalNotificationUpdater {@link AtomicReferenceFieldUpdater} to access the current
     * {@link TerminalNotification}.
     * @param flagOwner instance of {@link R} that owns the current {@link TerminalNotification} field referenced by
     * {@code terminalNotificationUpdater}.
     * @param <R> Type of {@code flagOwner}.
     * @return {@code true} if {@code toSet} is updated as the current {@link TerminalNotification}.
     */
    public static <R> boolean trySetTerminal(TerminalNotification toSet, boolean overrideComplete,
                     AtomicReferenceFieldUpdater<R, TerminalNotification> terminalNotificationUpdater, R flagOwner) {
        for (;;) {
            TerminalNotification curr = terminalNotificationUpdater.get(flagOwner);
            if (curr != null && !overrideComplete) {
                // Once terminated, terminalNotification will never be set back to null.
                return false;
            } else if (curr == null && terminalNotificationUpdater.compareAndSet(flagOwner, null, toSet)) {
                return true;
            } else if (curr != null && curr.cause() == null) {
                // Override complete
                if (terminalNotificationUpdater.compareAndSet(flagOwner, curr, toSet)) {
                    return true;
                }
            } else {
                return false;
            }
        }
    }

    /**
     * Deliver a terminal complete to a {@link Subscriber} that has not yet had
     * {@link Subscriber#onSubscribe(PublisherSource.Subscription)} called.
     * @param subscriber The {@link Subscriber} to terminate.
     * @param <T> The type of {@link Subscriber}.
     */
    public static <T> void deliverCompleteFromSource(Subscriber<T> subscriber) {
        try {
            subscriber.onSubscribe(EMPTY_SUBSCRIPTION);
        } catch (Throwable t) {
            handleExceptionFromOnSubscribe(subscriber, t);
            return;
        }
        safeOnComplete(subscriber);
    }

    /**
     * Invokes {@link SingleSource.Subscriber#onSuccess(Object)} ignoring an occurred exception if any.
     * @param subscriber The {@link SingleSource.Subscriber} that may throw an exception from
     * {@link SingleSource.Subscriber#onSuccess(Object)}.
     * @param value The value to pass to {@link SingleSource.Subscriber#onSuccess(Object)}.
     * @param <T> The type of {@link SingleSource.Subscriber}.
     */
    public static <T> void deliverSuccessFromSource(SingleSource.Subscriber<T> subscriber, @Nullable T value) {
        try {
            subscriber.onSubscribe(IGNORE_CANCEL);
        } catch (Throwable t) {
            handleExceptionFromOnSubscribe(subscriber, t);
            return;
        }
        safeOnSuccess(subscriber, value);
    }

    /**
     * Deliver a terminal complete to a {@link CompletableSource.Subscriber} that has not yet had
     * {@link CompletableSource.Subscriber#onSubscribe(Cancellable)} called.
     * @param subscriber The {@link CompletableSource.Subscriber} to terminate.
     * @param <T> The type of {@link CompletableSource.Subscriber}.
     */
    public static <T> void deliverCompleteFromSource(CompletableSource.Subscriber subscriber) {
        try {
            subscriber.onSubscribe(IGNORE_CANCEL);
        } catch (Throwable t) {
            handleExceptionFromOnSubscribe(subscriber, t);
            return;
        }
        safeOnComplete(subscriber);
    }

    /**
     * Deliver a terminal error to a {@link Subscriber} that has not yet had
     * {@link Subscriber#onSubscribe(PublisherSource.Subscription)} called.
     * @param subscriber The {@link Subscriber} to terminate.
     * @param cause The terminal event.
     * @param <T> The type of {@link Subscriber}.
     */
    public static <T> void deliverErrorFromSource(Subscriber<T> subscriber, Throwable cause) {
        try {
            subscriber.onSubscribe(EMPTY_SUBSCRIPTION);
        } catch (Throwable t) {
            handleExceptionFromOnSubscribe(subscriber, t);
            return;
        }
        safeOnError(subscriber, cause);
    }

    /**
     * Deliver a terminal error to a {@link SingleSource.Subscriber} that has not yet had
     * {@link SingleSource.Subscriber#onSubscribe(Cancellable)} called.
     * @param subscriber The {@link SingleSource.Subscriber} to terminate.
     * @param cause The terminal event.
     * @param <T> The type of {@link SingleSource.Subscriber}.
     */
    public static <T> void deliverErrorFromSource(SingleSource.Subscriber<T> subscriber, Throwable cause) {
        try {
            subscriber.onSubscribe(IGNORE_CANCEL);
        } catch (Throwable t) {
            handleExceptionFromOnSubscribe(subscriber, t);
            return;
        }
        safeOnError(subscriber, cause);
    }

    /**
     * Deliver a terminal error to a {@link CompletableSource.Subscriber} that has not yet had
     * {@link CompletableSource.Subscriber#onSubscribe(Cancellable)} called.
     * @param subscriber The {@link CompletableSource.Subscriber} to terminate.
     * @param cause The terminal event.
     */
    public static void deliverErrorFromSource(CompletableSource.Subscriber subscriber, Throwable cause) {
        try {
            subscriber.onSubscribe(IGNORE_CANCEL);
        } catch (Throwable t) {
            handleExceptionFromOnSubscribe(subscriber, t);
            return;
        }
        safeOnError(subscriber, cause);
    }

    /**
     * Handle the case when a call to {@link Subscriber#onSubscribe(PublisherSource.Subscription)} throws from a source.
     * @param subscriber The {@link Subscriber} that threw an exception from
     * {@link Subscriber#onSubscribe(PublisherSource.Subscription)}.
     * @param cause The exception thrown by {@code subscriber}.
     * @param <T> The type of {@link Subscriber}.
     */
    public static <T> void handleExceptionFromOnSubscribe(Subscriber<T> subscriber, Throwable cause) {
        // The Subscriber violated the spec by throwing from onSubscribe [1]. However we make a best effort to
        // complete the async control flow by calling onError even though the Subscriber state is unknown. The "best
        // effort" may end up violating RFC single terminal signal delivery [2] and concurrency [3] rules, but we are
        // in an invalid state.
        // [1] https://github.com/reactive-streams/reactive-streams-jvm#1.9
        // [2] https://github.com/reactive-streams/reactive-streams-jvm#1.7
        // [3] https://github.com/reactive-streams/reactive-streams-jvm#1.3
        safeOnError(subscriber, cause);
        LOGGER.warn("Unexpected exception from onSubscribe of Subscriber {}.", subscriber, cause);
    }

    /**
     * Handle the case when a call to {@link SingleSource.Subscriber#onSubscribe(Cancellable)} throws from a source.
     * @param subscriber The {@link SingleSource.Subscriber} that threw an exception from
     * {@link SingleSource.Subscriber#onSubscribe(Cancellable)}.
     * @param cause The exception thrown by {@code subscriber}.
     * @param <T> The type of {@link SingleSource.Subscriber}.
     */
    public static <T> void handleExceptionFromOnSubscribe(SingleSource.Subscriber<T> subscriber, Throwable cause) {
        // The Subscriber violated the spec by throwing from onSubscribe [1]. However we make a best effort to
        // complete the async control flow by calling onError even though the Subscriber state is unknown. The "best
        // effort" may end up violating RFC single terminal signal delivery [2] and concurrency [3] rules, but we are
        // in an invalid state.
        // [1] https://github.com/reactive-streams/reactive-streams-jvm#1.9
        // [2] https://github.com/reactive-streams/reactive-streams-jvm#1.7
        // [3] https://github.com/reactive-streams/reactive-streams-jvm#1.3
        safeOnError(subscriber, cause);
        LOGGER.warn("Unexpected exception from onSubscribe of Subscriber {}.", subscriber, cause);
    }

    /**
     * Handle the case when a call to {@link CompletableSource.Subscriber#onSubscribe(Cancellable)} throws from a
     * source.
     * @param subscriber The {@link CompletableSource.Subscriber} that threw an exception from
     * {@link CompletableSource.Subscriber#onSubscribe(Cancellable)}.
     * @param cause The exception thrown by {@code subscriber}.
     */
    public static void handleExceptionFromOnSubscribe(CompletableSource.Subscriber subscriber, Throwable cause) {
        // The Subscriber violated the spec by throwing from onSubscribe [1]. However we make a best effort to
        // complete the async control flow by calling onError even though the Subscriber state is unknown. The "best
        // effort" may end up violating RFC single terminal signal delivery [2] and concurrency [3] rules, but we are
        // in an invalid state.
        // [1] https://github.com/reactive-streams/reactive-streams-jvm#1.9
        // [2] https://github.com/reactive-streams/reactive-streams-jvm#1.7
        // [3] https://github.com/reactive-streams/reactive-streams-jvm#1.3
        safeOnError(subscriber, cause);
        LOGGER.warn("Unexpected exception from onSubscribe of Subscriber {}.", subscriber, cause);
    }

    /**
     * Invokes {@link CompletableSource.Subscriber#onError(Throwable)} ignoring an occurred exception if any.
     * @param subscriber The {@link CompletableSource.Subscriber} that may throw an exception from
     * {@link CompletableSource.Subscriber#onError(Throwable)}.
     * @param cause The occurred {@link Throwable} for {@link CompletableSource.Subscriber#onError(Throwable)}.
     */
    public static void safeOnError(CompletableSource.Subscriber subscriber, Throwable cause) {
        try {
            subscriber.onError(cause);
        } catch (Throwable t) {
            LOGGER.info("Ignoring exception from onError of Subscriber {}.", subscriber, t);
        }
    }

    /**
     * Invokes {@link SingleSource.Subscriber#onError(Throwable)} ignoring an occurred exception if any.
     * @param subscriber The {@link SingleSource.Subscriber} that may throw an exception from
     * {@link SingleSource.Subscriber#onError(Throwable)}.
     * @param cause The occurred {@link Throwable} for {@link SingleSource.Subscriber#onError(Throwable)}.
     * @param <T> The type of {@link SingleSource.Subscriber}.
     */
    public static <T> void safeOnError(SingleSource.Subscriber<T> subscriber, Throwable cause) {
        try {
            subscriber.onError(cause);
        } catch (Throwable t) {
            LOGGER.info("Ignoring exception from onError of Subscriber {}.", subscriber, t);
        }
    }

    /**
     * Invokes {@link PublisherSource.Subscriber#onError(Throwable)} ignoring an occurred exception if any.
     * @param subscriber The {@link PublisherSource.Subscriber} that may throw an exception from
     * {@link PublisherSource.Subscriber#onError(Throwable)}.
     * @param cause The occurred {@link Throwable} for {@link PublisherSource.Subscriber#onError(Throwable)}.
     * @param <T> The type of {@link PublisherSource.Subscriber}.
     */
    public static <T> void safeOnError(PublisherSource.Subscriber<T> subscriber, Throwable cause) {
        try {
            subscriber.onError(cause);
        } catch (Throwable t) {
            LOGGER.info("Ignoring exception from onError of Subscriber {}.", subscriber, t);
        }
    }

    /**
     * Invokes {@link Subscriber#onComplete()} ignoring an occurred exception if any.
     * @param subscriber The {@link PublisherSource.Subscriber} that may throw an exception from
     * {@link Subscriber#onComplete()}.
     * @param <T> The type of {@link PublisherSource.Subscriber}.
     */
    public static <T> void safeOnComplete(PublisherSource.Subscriber<T> subscriber) {
        try {
            subscriber.onComplete();
        } catch (Throwable t) {
            LOGGER.info("Ignoring exception from onComplete of Subscriber {}.", subscriber, t);
        }
    }

    /**
     * Invokes {@link SingleSource.Subscriber#onSuccess(Object)} ignoring an occurred exception if any.
     * @param subscriber The {@link SingleSource.Subscriber} that may throw an exception from
     * {@link SingleSource.Subscriber#onSuccess(Object)}.
     * @param value The value to pass to {@link SingleSource.Subscriber#onSuccess(Object)}.
     * @param <T> The type of {@link SingleSource.Subscriber}.
     */
    public static <T> void safeOnSuccess(SingleSource.Subscriber<T> subscriber, @Nullable T value) {
        try {
            subscriber.onSuccess(value);
        } catch (Throwable t) {
            LOGGER.info("Ignoring exception from onSuccess of Subscriber {}.", subscriber, t);
        }
    }

    /**
     * Invokes {@link CompletableSource.Subscriber#onComplete()} ignoring an occurred exception if any.
     * @param subscriber The {@link CompletableSource.Subscriber} that may throw an exception from
     * {@link CompletableSource.Subscriber#onComplete()}.
     */
    public static void safeOnComplete(CompletableSource.Subscriber subscriber) {
        try {
            subscriber.onComplete();
        } catch (Throwable t) {
            LOGGER.info("Ignoring exception from onComplete of Subscriber {}.", subscriber, t);
        }
    }
}