/*
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 * @copyright Ranides Atterwim
 * @license WTFPL
 * @url http://ranides.net/projects/assira
 */
package net.ranides.assira.events;

import java.util.List;

import net.ranides.assira.concurrent.SwingInvoker;
import net.ranides.assira.functional.special.AnyFunction;
import net.ranides.assira.functional.special.CancelableConsumer;
import net.ranides.assira.reflection.IClass;
import net.ranides.assira.reflection.IClasses;
import net.ranides.assira.reflection.IMethod;
import net.ranides.assira.concurrent.TaskInvoker;

/**
 * This class allows you to create event listeners in declarative way by passing objects
 * with annotated methods.
 *
 * Every method annotated by {@link EventHandler} will be invoked if specific event is sent to
 * EventObserver. You can attach your EventObserver to any EventDispatcher, or you can attach
 * specific methods to specific named routers at declaration stage.
 *
 * All bindings to named dispatcher are created at construction time.
 * You should check if all necessary routers are already created before obtaining instance of
 * EventObserver.
 *
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 */
public class EventObserver implements EventListener<Event> {

    private static final IClasses EVENT = IClass.typeinfo(Event.class).collect();
    private final List<IHandler> listeners;

    private EventObserver(Object delegator) {
        listeners = IClass.typefor(delegator)
            .methods()
            .require(EventHandler.class)
            .require(IClass.VOID)
            .matches(EVENT)
            .list(m -> newListener(delegator, m));
    }

    /**
     * Creates new EventObserver which can be attached to event dispatchers.
     *
     * Every method annotated in "delegator" will be executed if event of compatible type is fired.
     *
     * Methods could declare named event dispatcher: then bindings to specified dispatchers
     * will be created automatically by this method.
     *
     * @param delegator delegator
     * @return EventObserver
     * @throws IllegalStateException if some named router does not exist yet
     */
    public static EventObserver newInstance(Object delegator) {
        return new EventObserver(delegator);
    }

    private static IHandler newListener(Object delegator, IMethod m) {
        EventHandler info = m.annotations().first(EventHandler.class).get();
        IClass<?> type = m.arguments().first().get().type();
        String router = info.router();
        switch(router) {
            case "":
                return new ThisHandler(type, m.bind(delegator));
            case EventHandler.UI:
                return new UIHandler(type, false, m.bind(delegator));
            case EventHandler.UI_WAIT:
                return new UIHandler(type, true, m.bind(delegator));
            case EventHandler.LATER:
                return new LaterHandler(type, m.bind(delegator), info.delay());
            case EventHandler.LAZY:
                return new LazyHandler(type, m.bind(delegator), info.delay());
            default:
                return new NamedHandler(router, type, m.bind(delegator));
        }

    }

    @Override
    public void handleEvent(Event event) {
        for(IHandler handler : listeners) {
            if(handler.matches(event)) {
                handler.call(event);
            }
        }
    }
    
    private static abstract class IHandler {
        
        private final IClass<?> type;

        public IHandler(IClass<?> type) {
            this.type = type;
        }

        public boolean matches(Event event) {
            return type.isInstance(event);
        }
        
        public abstract void call(Event event);
        
    }
    
    private static final class ThisHandler extends IHandler {
        
        private final AnyFunction<?> handle;

        public ThisHandler(IClass<?> type, AnyFunction<?> handle) {
            super(type);
            this.handle = handle;
        }
        
        @Override
        public void call(Event event) {
            handle.call(event);
        }
        
    }

    private static final class LaterHandler extends IHandler {

        private final AnyFunction<?> handle;
        private final int delay;

        public LaterHandler(IClass<?> type, AnyFunction<?> handle, int delay) {
            super(type);
            this.handle = handle;
            this.delay = delay;
        }

        @Override
        public void call(Event event) {
            TaskInvoker.later(delay, () -> handle.call(event));
        }

    }

    private static final class LazyHandler extends IHandler {

        private final CancelableConsumer<Object> handle;

        public LazyHandler(IClass<?> type, AnyFunction<?> handle, int delay) {
            super(type);
            this.handle = TaskInvoker.lazy(delay, event -> handle.call(event));
        }

        @Override
        public void call(Event event) {
            handle.accept(event);
        }

    }
    
    private static final class UIHandler extends IHandler {
        
        private final boolean sync;
        
        private final AnyFunction<?> handle;

        public UIHandler(IClass<?> type, boolean sync, AnyFunction<?> handle) {
            super(type);
            this.handle = handle;
            this.sync = sync;
        }
        
        @Override
        public void call(Event event) {
            SwingInvoker.run(sync, () -> handle.call(event));
        }
        
    }
    
    private static final class NamedHandler extends IHandler {
        
        private final EventRouter dispatcher;
        
        public NamedHandler(String name, IClass<?> type, AnyFunction<?> handle) {
            super(type);
            this.dispatcher = EventDispatcher.find(name).orElseThrow(() -> {
                return new IllegalStateException("There is no EventDispatcher with name: " + name);
            });
            this.dispatcher.addEventListener(TaggedEvent.class, new TaggedListener(this, handle));
        }
        
        @Override
        public void call(Event event) {
            this.dispatcher.signalEvent(new TaggedEvent(this, event));
        }
        
    }
    
    private static final class TaggedListener implements EventListener<TaggedEvent> {
        
        private final Object tag;
        
        private final AnyFunction<?> handle;

        public TaggedListener(Object tag, AnyFunction<?> handle) {
            this.tag = tag;
            this.handle = handle;
        }
        
        @Override
        public void handleEvent(TaggedEvent event) {
            if(event.tag == tag) {
                handle.call(event.src);
            }
        }
        
    }
    
    private static final class TaggedEvent implements Event {
    
        public final Object tag;
        
        public final Event src;

        public TaggedEvent(Object tag, Event event) {
            this.tag = tag;
            this.src = event;
        }
        
    }
    
}
