/*
 * Copyright (C) 2017 Anadea 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.anadeainc.rxbus;

import android.support.annotation.NonNull;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArraySet;

import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.functions.Consumer;
import io.reactivex.internal.functions.ObjectHelper;
import io.reactivex.subjects.PublishSubject;
import io.reactivex.subjects.Subject;

public final class RxBus implements Bus {

    private final ConcurrentMap<Class<?>, CompositeDisposable> OBSERVERS
            = new ConcurrentHashMap<>();
    private final ConcurrentMap<Class<?>, CopyOnWriteArraySet<CustomSubscriber<?>>> SUBSCRIBERS
            = new ConcurrentHashMap<>();

    private final Subject<Object> bus = PublishSubject.create().toSerialized();

    public void register(@NonNull Object observer) {
        ObjectHelper.requireNonNull(observer, "Observer to register must not be null.");

        Class<?> observerClass = observer.getClass();

        if (OBSERVERS.putIfAbsent(observerClass, new CompositeDisposable()) != null)
            throw new IllegalArgumentException("Observer has already been registered.");

        CompositeDisposable composite = OBSERVERS.get(observerClass);

        Set<Class<?>> events = new HashSet<>();

        for (Method method : observerClass.getDeclaredMethods()) {

            if (method.isBridge() || method.isSynthetic())
                continue;

            if (!method.isAnnotationPresent(Subscribe.class))
                continue;

            int mod = method.getModifiers();

            if (Modifier.isStatic(mod) || !Modifier.isPublic(mod))
                throw new IllegalArgumentException("Method " + method.getName() +
                        " has @Subscribe annotation must be public, non-static");

            Class<?>[] params = method.getParameterTypes();

            if (params.length != 1)
                throw new IllegalArgumentException("Method " + method.getName() +
                        " has @Subscribe annotation must require a single argument");

            Class<?> eventClass = params[0];

            if (eventClass.isInterface())
                throw new IllegalArgumentException("Event class must be on a concrete class type.");

            if (!events.add(eventClass))
                throw new IllegalArgumentException("Subscriber for " + eventClass.getSimpleName() +
                        " has already been registered.");

            composite.add(bus.ofType(eventClass)
                    .observeOn(AndroidSchedulers.mainThread())
                    .subscribe(new AnnotatedSubscriber<>(observer, method)));
        }

    }

    public <T> CustomSubscriber<T> obtainSubscriber(@NonNull Class<T> eventClass,
                                                    @NonNull Consumer<T> receiver) {
        ObjectHelper.requireNonNull(eventClass, "Event class must not be null.");
        if (eventClass.isInterface())
            throw new IllegalArgumentException("Event class must be on a concrete class type.");
        ObjectHelper.requireNonNull(receiver, "Receiver must not be null.");
        return new CustomSubscriber<>(eventClass, receiver);
    }

    public <T> void registerSubscriber(@NonNull Object observer, @NonNull CustomSubscriber<T> subscriber) {
        ObjectHelper.requireNonNull(observer, "Observer to register must not be null.");
        ObjectHelper.requireNonNull(subscriber, "Subscriber to register must not be null.");

        SUBSCRIBERS.putIfAbsent(observer.getClass(), new CopyOnWriteArraySet<CustomSubscriber<?>>());
        Set<CustomSubscriber<?>> subscribers = SUBSCRIBERS.get(observer.getClass());
        if (subscribers.contains(subscriber))
            throw new IllegalArgumentException("Subscriber has already been registered.");
        else
            subscribers.add(subscriber);

        Observable<T> observable = bus.ofType(subscriber.getEventClass())
                .observeOn(subscriber.getScheduler() == null ?
                        AndroidSchedulers.mainThread() : subscriber.getScheduler());

        Class<?> observerClass = observer.getClass();

        OBSERVERS.putIfAbsent(observerClass, new CompositeDisposable());
        CompositeDisposable composite = OBSERVERS.get(observerClass);

        composite.add(((subscriber.getFilter() == null) ? observable :
                observable.filter(subscriber.getFilter()))
                .subscribe(subscriber));
    }

    public void unregister(@NonNull Object observer) {
        ObjectHelper.requireNonNull(observer, "Observer to unregister must not be null.");
        CompositeDisposable composite = OBSERVERS.get(observer.getClass());
        ObjectHelper.requireNonNull(composite, "Missing observer, it was registered?");
        composite.dispose();
        OBSERVERS.remove(observer.getClass());

        Set<CustomSubscriber<?>> subscribers = SUBSCRIBERS.get(observer.getClass());
        if (subscribers != null) {
            subscribers.clear();
            SUBSCRIBERS.remove(observer.getClass());
        }
    }

    public void post(@NonNull Object event) {
        ObjectHelper.requireNonNull(event, "Event must not be null.");
        bus.onNext(event);
    }

}