/*
 * Copyright (c) 2016-present, RxJava Contributors.
 *
 * 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.reactivex.rxjava3.internal.util;

import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;

import org.reactivestreams.Subscription;

import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.exceptions.ProtocolViolationException;
import io.reactivex.rxjava3.internal.disposables.DisposableHelper;
import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper;
import io.reactivex.rxjava3.plugins.RxJavaPlugins;

/**
 * Utility class to help report multiple subscriptions with the same
 * consumer type instead of the internal "Disposable already set!" message
 * that is practically reserved for internal operators and indicate bugs in them.
 */
public final class EndConsumerHelper {

    /**
     * Utility class.
     */
    private EndConsumerHelper() {
        throw new IllegalStateException("No instances!");
    }

    /**
     * Ensures that the upstream Disposable is null and returns true, otherwise
     * disposes the next Disposable and if the upstream is not the shared
     * disposed instance, reports a ProtocolViolationException due to
     * multiple subscribe attempts.
     * @param upstream the upstream current value
     * @param next the Disposable to check for nullness and dispose if necessary
     * @param observer the class of the consumer to have a personalized
     * error message if the upstream already contains a non-cancelled Disposable.
     * @return true if successful, false if the upstream was non null
     */
    public static boolean validate(Disposable upstream, Disposable next, Class<?> observer) {
        Objects.requireNonNull(next, "next is null");
        if (upstream != null) {
            next.dispose();
            if (upstream != DisposableHelper.DISPOSED) {
                reportDoubleSubscription(observer);
            }
            return false;
        }
        return true;
    }

    /**
     * Atomically updates the target upstream AtomicReference from null to the non-null
     * next Disposable, otherwise disposes next and reports a ProtocolViolationException
     * if the AtomicReference doesn't contain the shared disposed indicator.
     * @param upstream the target AtomicReference to update
     * @param next the Disposable to set on it atomically
     * @param observer the class of the consumer to have a personalized
     * error message if the upstream already contains a non-cancelled Disposable.
     * @return true if successful, false if the content of the AtomicReference was non null
     */
    public static boolean setOnce(AtomicReference<Disposable> upstream, Disposable next, Class<?> observer) {
        Objects.requireNonNull(next, "next is null");
        if (!upstream.compareAndSet(null, next)) {
            next.dispose();
            if (upstream.get() != DisposableHelper.DISPOSED) {
                reportDoubleSubscription(observer);
            }
            return false;
        }
        return true;
    }

    /**
     * Ensures that the upstream Subscription is null and returns true, otherwise
     * cancels the next Subscription and if the upstream is not the shared
     * cancelled instance, reports a ProtocolViolationException due to
     * multiple subscribe attempts.
     * @param upstream the upstream current value
     * @param next the Subscription to check for nullness and cancel if necessary
     * @param subscriber the class of the consumer to have a personalized
     * error message if the upstream already contains a non-cancelled Subscription.
     * @return true if successful, false if the upstream was non null
     */
    public static boolean validate(Subscription upstream, Subscription next, Class<?> subscriber) {
        Objects.requireNonNull(next, "next is null");
        if (upstream != null) {
            next.cancel();
            if (upstream != SubscriptionHelper.CANCELLED) {
                reportDoubleSubscription(subscriber);
            }
            return false;
        }
        return true;
    }

    /**
     * Atomically updates the target upstream AtomicReference from null to the non-null
     * next Subscription, otherwise cancels next and reports a ProtocolViolationException
     * if the AtomicReference doesn't contain the shared cancelled indicator.
     * @param upstream the target AtomicReference to update
     * @param next the Subscription to set on it atomically
     * @param subscriber the class of the consumer to have a personalized
     * error message if the upstream already contains a non-cancelled Subscription.
     * @return true if successful, false if the content of the AtomicReference was non null
     */
    public static boolean setOnce(AtomicReference<Subscription> upstream, Subscription next, Class<?> subscriber) {
        Objects.requireNonNull(next, "next is null");
        if (!upstream.compareAndSet(null, next)) {
            next.cancel();
            if (upstream.get() != SubscriptionHelper.CANCELLED) {
                reportDoubleSubscription(subscriber);
            }
            return false;
        }
        return true;
    }

    /**
     * Builds the error message with the consumer class.
     * @param consumer the class of the consumer
     * @return the error message string
     */
    public static String composeMessage(String consumer) {
        return "It is not allowed to subscribe with a(n) " + consumer + " multiple times. "
                + "Please create a fresh instance of " + consumer + " and subscribe that to the target source instead.";
    }

    /**
     * Report a ProtocolViolationException with a personalized message referencing
     * the simple type name of the consumer class and report it via
     * RxJavaPlugins.onError.
     * @param consumer the class of the consumer
     */
    public static void reportDoubleSubscription(Class<?> consumer) {
        RxJavaPlugins.onError(new ProtocolViolationException(composeMessage(consumer.getName())));
    }
}
