package org.vaadin.stefan.fullcalendar.dataprovider;

import com.vaadin.flow.function.SerializableConsumer;
import com.vaadin.flow.shared.Registration;
import lombok.Getter;
import org.vaadin.stefan.fullcalendar.Entry;
import org.vaadin.stefan.fullcalendar.FullCalendar;
import org.vaadin.stefan.fullcalendar.dataprovider.EntriesChangeEvent.EntriesChangeListener;
import org.vaadin.stefan.fullcalendar.dataprovider.EntryRefreshEvent.EntryRefreshListener;

import java.util.*;
import java.util.function.Consumer;

/**
 * Abstract base implementation of the {@link EntryProvider} interface.
 * @author Stefan Uebe
 */
public abstract class AbstractEntryProvider<T extends Entry> implements EntryProvider<T> {

    private final Map<Class<?>, List<SerializableConsumer<?>>> listeners = new HashMap<>();

    @Getter
    private FullCalendar calendar;

    @Override
    public void refreshAll() {
        fireEvent(new EntriesChangeEvent<>(this));
    }

    @Override
    public void refreshItem(T item) {
        fireEvent(new EntryRefreshEvent<>(this, item));
    }

    /**
     * Registers a new listener with the specified activation method to listen
     * events generated by this component. If the activation method does not
     * have any arguments the event object will not be passed to it when it's
     * called.
     *
     * @param eventType the type of the listened event. Events of this type or its
     *                  subclasses activate the listener.
     * @param method    the consumer to receive the event.
     * @param <E>       the event type
     * @return a registration for the listener
     */
    protected <E> Registration addListener(Class<E> eventType, SerializableConsumer<E> method) {
        List<SerializableConsumer<?>> list = listeners.computeIfAbsent(eventType, key -> new ArrayList<>());

        return Registration.addAndRemove(list, method);
    }

    @Override
    public Registration addEntriesChangeListener(EntriesChangeListener<T> listener) {
        return addListener(EntriesChangeEvent.class, listener::onDataChange);
    }

    @Override
    public Registration addEntryRefreshListener(EntryRefreshListener<T> listener) {
        return addListener(EntryRefreshEvent.class, listener::onDataRefresh);
    }

    /**
     * Sends the event to all listeners.
     *
     * @param event the Event to be sent to all listeners.
     */
    @SuppressWarnings({"unchecked", "rawtypes"})
    protected void fireEvent(EventObject event) {
        listeners.entrySet().stream()
                .filter(entry -> entry.getKey().isAssignableFrom(event.getClass()))
                .forEach(entry -> {
                    for (Consumer consumer : entry.getValue()) {
                        consumer.accept(event);
                    }
                });
    }

    /**
     * Sets the calendar. Throws an exception, when there is already a calendar set and it is not the
     * same instance as the given one.
     *
     * @param calendar calendar to set
     * @throws UnsupportedOperationException when setting another calendar
     */
    @Override
    public void setCalendar(FullCalendar calendar) {
        if (this.calendar != null && calendar != null && this.calendar != calendar) {
            throw new UnsupportedOperationException("Calendar must be set only once. Please create a new instance instead.");
        }

        this.calendar = calendar;
    }
}
