package ru.kirillius.cooler.controller.API;

import ru.kirillius.cooler.controller.Application;
import ru.kirillius.cooler.controller.EventSystem.EventHandler;
import ru.kirillius.cooler.controller.EventSystem.EventListener;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public abstract class MessageSystem {
    private final Map<ActionType, Handler> subscribers = new ConcurrentHashMap<>();
    private volatile long lastReceiveTime = 0;

    protected volatile boolean alive;

    public MessageSystem() {
        if (Application.DEBUG) SubscribeDebug();
        alive = true;
    }

    protected final void SubscribeDebug() {
        addListener(ActionType.ERROR, eventData -> System.err.println("Remote error received: " + ErrorCode.getByValue(eventData.getValue())));
    }

    public final void addListener(ActionType type, Listener listener) {
        if (!subscribers.containsKey(type)) {
            subscribers.put(type, new Handler());
        }

        var handler = subscribers.get(type);
        handler.addListener(listener);
    }

    public final void removeListener(ActionType type, Listener listener) {
        if (!subscribers.containsKey(type)) {
            return;
        }

        var handler = subscribers.get(type);
        handler.removeListener(listener);
    }

    public abstract void sendMessage(Message message);

    public final long getLastReceiveTime() {
        return lastReceiveTime;
    }

    protected final void OnMessageReceived(Message message) {
        lastReceiveTime = System.currentTimeMillis();
        var handler = subscribers.get(message.getActionType());
        if (handler != null) {
            handler.invoke(message);
        }
    }


    public final void close() throws MessageException {
        for (var handler : subscribers.values()) {
            handler.removeAllListeners();
        }
        subscribers.clear();
        OnClose();
        alive = false;
    }

    protected abstract void OnClose() throws MessageException;

    public final Operation setValue(DataType dataType, byte value) {
        var op = new WriteOperation(dataType, value);
        op.start();
        return op;
    }

    public final Operation getValue(DataType dataType) {
        var op = new ReadOperation(dataType);
        op.start();
        return op;
    }

    public final Operation sendCommand(ActionType type) {
        if (type == ActionType.READ || type == ActionType.WRITE)
            throw new UnsupportedOperationException("Action type " + type + " is unsupported");
        var op = new CommandOperation(type);
        op.start();
        return op;
    }

    public interface Listener extends EventListener<Message> {

    }

    private final static class Handler extends EventHandler<Listener, Message> {

    }

    private abstract class AbstractOperation implements Operation {
        private final static long Timeout = 10000L;
        protected volatile boolean complete = false;
        protected volatile boolean successful = false;
        protected ActionType actionType = ActionType.UNKNOWN;
        protected DataType dataType = DataType.NONE;
        protected volatile byte value = 0x0;
        protected Listener listener = null;
        protected EventHandler<EventListener<Operation>, Operation> handler = new EventHandler<>();
        private volatile Thread executor = null;

        @Override
        public byte getValue() {
            return value;
        }

        @Override
        public boolean isComplete() {
            return complete;
        }

        @Override
        public boolean isSuccessful() {
            return successful;
        }

        @Override
        public void start() {
            if (executor != null) throw new IllegalStateException("Operation is started already");
            complete = false;
            executor = new Thread(AbstractOperation.this::run);
            executor.start();
        }

        @Override
        public void interrupt() {
            if (executor != null) executor.interrupt();
            executor = null;
            successful = false;
            complete = true;
        }

        @Override
        public void waitForReturn() {
            while (!complete && executor != null && executor.isAlive()) {
                try {
                    //noinspection BusyWait
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }

        protected void run() {
            if (!alive) {
                complete = true;
                handler.invoke(this);
                return;
            }
            addListener(actionType, listener);
            var start = System.currentTimeMillis();
            do {
                if (System.currentTimeMillis() - start > AbstractOperation.Timeout) {
                    break;
                }
                sendMessage(new Message(actionType, dataType, value));
                try {
                    //noinspection BusyWait
                    Thread.sleep(1000L);
                } catch (InterruptedException e) {
                    return;
                }
            } while (!successful);
            removeListener(actionType, listener);
            complete = true;
            handler.invoke(this);
        }

        @Override
        public void addCompleteListener(EventListener<Operation> listener) {
            handler.addListener(listener);
            if (complete) listener.invoke(this);
        }
    }

    private final class ReadOperation extends AbstractOperation {
        public ReadOperation(DataType dataType) {
            actionType = ActionType.READ;
            this.dataType = dataType;
            listener = eventData -> {
                if (eventData.getDataType() != dataType) return;
                successful = true;
                value = eventData.getValue();
            };
        }
    }

    private final class WriteOperation extends AbstractOperation {
        public WriteOperation(DataType dataType, byte value) {
            this.dataType = dataType;
            actionType = ActionType.WRITE;
            this.value = value;
            listener = eventData -> successful = true;
        }
    }

    private final class CommandOperation extends AbstractOperation {
        public CommandOperation(ActionType actionType) {
            this.actionType = actionType;
            listener = eventData -> {
                if (eventData.getDataType() != dataType) return;
                successful = true;
            };
        }
    }

}
