package tc.oc.api.minecraft;

import java.io.IOException;
import java.lang.reflect.Method;
import java.util.concurrent.ExecutionException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.inject.Inject;
import javax.inject.Singleton;

import com.google.common.base.Function;
import com.google.common.eventbus.EventBus;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import tc.oc.api.connectable.Connectable;
import tc.oc.api.docs.Server;
import tc.oc.api.docs.virtual.ServerDoc;
import tc.oc.api.exceptions.ApiNotConnected;
import tc.oc.api.message.MessageListener;
import tc.oc.api.message.MessageQueue;
import tc.oc.api.message.types.ModelUpdate;
import tc.oc.api.minecraft.config.MinecraftApiConfiguration;
import tc.oc.api.minecraft.servers.LocalServerDocument;
import tc.oc.api.minecraft.servers.LocalServerReconfigureEvent;
import tc.oc.api.minecraft.servers.StartupServerDocument;
import tc.oc.api.servers.ServerService;
import tc.oc.commons.core.logging.Loggers;
import tc.oc.commons.core.reflect.Methods;
import tc.oc.commons.core.util.MethodHandleInvoker;
import tc.oc.commons.core.util.ProxyUtils;
import tc.oc.minecraft.scheduler.SyncExecutor;

@Singleton
public class MinecraftServiceImpl implements MinecraftService, MessageListener, Connectable {

    private final Logger logger;
    private final EventBus eventBus;
    private final SyncExecutor syncExecutor;
    private final ServerService serverService;
    private final MinecraftApiConfiguration apiConfiguration;
    private final StartupServerDocument startupDocument;
    private final MessageQueue serverQueue;
    private final Server everfreshLocalServer;

    private @Nullable Server server;

    @Inject MinecraftServiceImpl(Loggers loggers,
                                 EventBus eventBus,
                                 SyncExecutor syncExecutor,
                                 ServerService serverService,
                                 MinecraftApiConfiguration apiConfiguration,
                                 MessageQueue serverQueue,
                                 LocalServerDocument localServerDocument,
                                 StartupServerDocument startupDocument) {

        this.logger = loggers.get(getClass());
        this.eventBus = eventBus;
        this.syncExecutor = syncExecutor;
        this.serverService = serverService;
        this.apiConfiguration = apiConfiguration;
        this.serverQueue = serverQueue;

        this.everfreshLocalServer = ProxyUtils.newProxy(Server.class, new MethodHandleInvoker() {
            @Override
            protected Object targetFor(Method method) {
                if(server != null) return server;
                if(Methods.respondsTo(localServerDocument, method)) return localServerDocument;
                throw new ApiNotConnected();
            }
        });
        this.startupDocument = startupDocument;
    }

    @Override
    public boolean listenWhileSuspended() {
        return true;
    }

    private void assertConnected() throws ApiNotConnected {
        if(server == null) {
            throw new ApiNotConnected();
        }
    }

    @Override
    public Server getLocalServer() {
        assertConnected();
        return server;
    }

    @Override
    public boolean isLocalServer(ServerDoc.Identity server) {
        return getLocalServer()._id().equals(server._id());
    }

    /**
     * Return a magic {@link Server} document for the local server that
     * always has the most recent data. If the API is not connected,
     * the fields provided by {@link LocalServerDocument} will work,
     * but trying to read any other field will throw {@link ApiNotConnected}.
     */
    @Override
    public Server everfreshLocalServer() {
        return everfreshLocalServer;
    }

    /**
     * Send a server configuration change to the remote API. The API will respond with
     * the latest version of the server document, and only at that point will the local
     * document be modified. This is done by calling {@link #handleReconfigure},
     * and subclasses can override that method if they want to fire a reconfigure event.
     *
     * This method returns a future that completes after the API responds to the update
     * AND the local server document has been replaced with the result.
     */
    @Override
    public ListenableFuture<Server> updateLocalServer(ServerDoc.Partial update) {
        return Futures.transform(
            serverService.update(apiConfiguration.serverId(), update),
            (Function<? super Server, ? extends Server>) result -> {
                handleLocalReconfigure(result);
                return result;
            },
            syncExecutor
        );
    }

    @HandleMessage
    public void handleReconfigure(ModelUpdate<Server> message) {
        if(server != null && server._id().equals(message.document()._id())) {
            handleLocalReconfigure(message.document());
        }
    }

    protected void handleLocalReconfigure(Server newConfig) {
        Server oldConfig = this.server;
        this.server = newConfig;
        if(logger.isLoggable(Level.FINE)) {
            logger.fine("Local server reconfigured: " + newConfig);
        }
        eventBus.post(new LocalServerReconfigureEvent(oldConfig, newConfig));
    }

    @Override
    public void connect() throws IOException {
        try {
            serverQueue.subscribe(this, syncExecutor);

            handleLocalReconfigure(serverService.update(apiConfiguration.serverId(), startupDocument).get());

            logger.info("Connected to API as server." + getLocalServer()._id());
        } catch (Exception e) {
            this.processIntoIOException(e);
        }
    }

    @Override
    public void disconnect() throws IOException {
        try {
            serverService.update(
                apiConfiguration.serverId(),
                (ServerDoc.Online) () -> false
            ).get();

            serverQueue.unsubscribe(this);
        } catch (Exception e) {
            processIntoIOException(e);
        }
        this.server = null;
    }

    private void processIntoIOException(Throwable t) throws IOException {
        if (t instanceof IOException) throw (IOException)t;
        if (t instanceof ExecutionException) this.processIntoIOException(t.getCause());
        throw new IOException(t);
    }
}
