package org.zeromq;

import java.io.Closeable;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import org.zeromq.ZMQ.Socket;
import org.zeromq.proto.ZNeedle;

import zmq.util.Objects;

/**
 * The ZMonitor actor provides an API for obtaining socket events such as
 * connected, listen, disconnected, etc. Socket events are only available
 * for sockets connecting or bound to ipc:// and tcp:// endpoints.
 */
public class ZMonitor implements Closeable
{
    /**
     * High-level representation of an event.
     */
    public static final class ZEvent
    {
        // Enum.values return a different array on each call, so keep it to avoid useless allocations
        private static final Event[] EVENTVALUES = Event.values();

        /**
         * The type of the event. (never null)
         */
        public final Event type;

        /**
         * The numerical code of the event. (useful in case of unrecognized event, in which case type is ALL)
         */
        public final int code;

        /**
         * The address of the source of the event.
         */
        public final String address;

        /**
         * String representation of the event value.
         * Nature of the value depends on the type of event. May be null
         */
        public final String value;

        private ZEvent(ZMsg msg)
        {
            assert (msg != null);
            assert (msg.size() == 1 || msg.size() == 2) : msg.size();

            final ZFrame frame = msg.pop();
            final ZNeedle needle = new ZNeedle(frame);

            type = EVENTVALUES[needle.getNumber1()];
            code = needle.getNumber4();
            address = needle.getString();

            if (msg.isEmpty()) {
                value = null;
            }
            else {
                value = msg.popString();
            }
        }

        @SuppressWarnings("deprecation")
        public ZEvent(ZMQ.Event event)
        {
            code = event.getEvent();
            address = event.getAddress();
            type = Event.findByCode(code);
            Object tryValue = event.resolveValue();
            if (tryValue != null) {
                value = tryValue.toString();
            }
            else {
                value = null;
            }
        }

        @Override
        public String toString()
        {
            return "ZEvent [type=" + type + ", code=" + code + ", address=" + address + ", value=" + value + "]";
        }
    }

    /**
     * Enumerates types of monitoring events.
     */
    public enum Event
    {
        CONNECTED(ZMQ.EVENT_CONNECTED),
        CONNECT_DELAYED(ZMQ.EVENT_CONNECT_DELAYED),
        CONNECT_RETRIED(ZMQ.EVENT_CONNECT_RETRIED),
        LISTENING(ZMQ.EVENT_LISTENING),
        BIND_FAILED(ZMQ.EVENT_BIND_FAILED),
        ACCEPTED(ZMQ.EVENT_ACCEPTED),
        ACCEPT_FAILED(ZMQ.EVENT_ACCEPT_FAILED),
        CLOSED(ZMQ.EVENT_CLOSED),
        CLOSE_FAILED(ZMQ.EVENT_CLOSE_FAILED),
        DISCONNECTED(ZMQ.EVENT_DISCONNECTED),
        MONITOR_STOPPED(ZMQ.EVENT_MONITOR_STOPPED),
        HANDSHAKE_FAILED_NO_DETAIL(ZMQ.HANDSHAKE_FAILED_NO_DETAIL),
        HANDSHAKE_SUCCEEDED(ZMQ.HANDSHAKE_SUCCEEDED),
        HANDSHAKE_FAILED_PROTOCOL(ZMQ.HANDSHAKE_FAILED_PROTOCOL),
        HANDSHAKE_FAILED_AUTH(ZMQ.HANDSHAKE_FAILED_AUTH),
        HANDSHAKE_PROTOCOL(ZMQ.EVENT_HANDSHAKE_PROTOCOL),
        ALL(ZMQ.EVENT_ALL);

        private static final Map<Integer, Event> MAP = new HashMap<>(Event.values().length);
        static {
            for (Event e : Event.values()) {
                MAP.put(e.code, e);
            }
        }

        private final int code;

        Event(int code)
        {
            this.code = code;
        }

        /**
         * Find the {@link Event} associated with the numerical event code.
         * @param event the numerical event code
         * @return the found {@link Event}
         */
        public static Event findByCode(int event)
        {
            return MAP.getOrDefault(event, ALL);
        }
    }

    /**
     * The code returned by handshake events, as generated by <code>eventHandshakeXXX</code>.
     *
     */
    public enum ProtocolCode
    {
        ZMQ_PROTOCOL_ERROR_ZMTP_UNSPECIFIED(zmq.ZMQ.ZMQ_PROTOCOL_ERROR_ZMTP_UNSPECIFIED),
        ZMQ_PROTOCOL_ERROR_ZMTP_UNEXPECTED_COMMAND(zmq.ZMQ.ZMQ_PROTOCOL_ERROR_ZMTP_UNEXPECTED_COMMAND),
        ZMQ_PROTOCOL_ERROR_ZMTP_INVALID_SEQUENCE(zmq.ZMQ.ZMQ_PROTOCOL_ERROR_ZMTP_INVALID_SEQUENCE),
        ZMQ_PROTOCOL_ERROR_ZMTP_KEY_EXCHANGE(zmq.ZMQ.ZMQ_PROTOCOL_ERROR_ZMTP_KEY_EXCHANGE),
        ZMQ_PROTOCOL_ERROR_ZMTP_MALFORMED_COMMAND_UNSPECIFIED(zmq.ZMQ.ZMQ_PROTOCOL_ERROR_ZMTP_MALFORMED_COMMAND_UNSPECIFIED),
        ZMQ_PROTOCOL_ERROR_ZMTP_MALFORMED_COMMAND_MESSAGE(zmq.ZMQ.ZMQ_PROTOCOL_ERROR_ZMTP_MALFORMED_COMMAND_MESSAGE),
        ZMQ_PROTOCOL_ERROR_ZMTP_MALFORMED_COMMAND_HELLO(zmq.ZMQ.ZMQ_PROTOCOL_ERROR_ZMTP_MALFORMED_COMMAND_HELLO),
        ZMQ_PROTOCOL_ERROR_ZMTP_MALFORMED_COMMAND_INITIATE(zmq.ZMQ.ZMQ_PROTOCOL_ERROR_ZMTP_MALFORMED_COMMAND_INITIATE),
        ZMQ_PROTOCOL_ERROR_ZMTP_MALFORMED_COMMAND_ERROR(zmq.ZMQ.ZMQ_PROTOCOL_ERROR_ZMTP_MALFORMED_COMMAND_ERROR),
        ZMQ_PROTOCOL_ERROR_ZMTP_MALFORMED_COMMAND_READY(zmq.ZMQ.ZMQ_PROTOCOL_ERROR_ZMTP_MALFORMED_COMMAND_READY),
        ZMQ_PROTOCOL_ERROR_ZMTP_MALFORMED_COMMAND_WELCOME(zmq.ZMQ.ZMQ_PROTOCOL_ERROR_ZMTP_MALFORMED_COMMAND_WELCOME),
        ZMQ_PROTOCOL_ERROR_ZMTP_INVALID_METADATA(zmq.ZMQ.ZMQ_PROTOCOL_ERROR_ZMTP_INVALID_METADATA),
        ZMQ_PROTOCOL_ERROR_ZMTP_CRYPTOGRAPHIC(zmq.ZMQ.ZMQ_PROTOCOL_ERROR_ZMTP_CRYPTOGRAPHIC),
        ZMQ_PROTOCOL_ERROR_ZMTP_MECHANISM_MISMATCH(zmq.ZMQ.ZMQ_PROTOCOL_ERROR_ZMTP_MECHANISM_MISMATCH),
        ZMQ_PROTOCOL_ERROR_ZAP_UNSPECIFIED(zmq.ZMQ.ZMQ_PROTOCOL_ERROR_ZAP_UNSPECIFIED),
        ZMQ_PROTOCOL_ERROR_ZAP_MALFORMED_REPLY(zmq.ZMQ.ZMQ_PROTOCOL_ERROR_ZAP_MALFORMED_REPLY),
        ZMQ_PROTOCOL_ERROR_ZAP_BAD_REQUEST_ID(zmq.ZMQ.ZMQ_PROTOCOL_ERROR_ZAP_BAD_REQUEST_ID),
        ZMQ_PROTOCOL_ERROR_ZAP_BAD_VERSION(zmq.ZMQ.ZMQ_PROTOCOL_ERROR_ZAP_BAD_VERSION),
        ZMQ_PROTOCOL_ERROR_ZAP_INVALID_STATUS_CODE(zmq.ZMQ.ZMQ_PROTOCOL_ERROR_ZAP_INVALID_STATUS_CODE),
        ZMQ_PROTOCOL_ERROR_ZAP_INVALID_METADATA(zmq.ZMQ.ZMQ_PROTOCOL_ERROR_ZAP_INVALID_METADATA),
        ZMQ_PROTOCOL_ERROR_WS_UNSPECIFIED(zmq.ZMQ.ZMQ_PROTOCOL_ERROR_WS_UNSPECIFIED);

        private static final Map<Integer, ProtocolCode> MAP = new HashMap<>(ProtocolCode.values().length);

        static {
            for (ProtocolCode e : ProtocolCode.values()) {
                MAP.put(e.code, e);
            }
        }

        private final int code;

        ProtocolCode(int code)
        {
            this.code = code;
        }

        /**
         * Find the {@link ProtocolCode} associated with the numerical error code send with <code>eventHandshakeXXX</code>.
         * @param code the numerical error code
         * @return the found {@link ProtocolCode}
         */
        public static ProtocolCode findByCode(int code)
        {
            if (MAP.containsKey(code)) {
                return MAP.get(code);
            }
            else {
                throw new IllegalArgumentException("Protocol code unknown: " + code);
            }
        }
    }

    private boolean started;

    /**
     * Starts the monitoring.
     * Event types have to be added before the start, or they will take no effect.
     * @return this instance.
     */
    public final ZMonitor start()
    {
        if (started) {
            System.out.println("ZMonitor: Unable to start while already started.");
            return this;
        }
        agent.send(START);
        agent.recv();
        started = true;
        return this;
    }

    /**
     * Stops the monitoring and closes the actor.
     * When returning from that call, ZMonitor will be no more active.
     */
    @Override
    public final void close()
    {
        destroy();
    }

    /**
     * Stops the monitoring and closes the actor.
     * When returning from that call, ZMonitor will be no more active.
     */
    public final void destroy()
    {
        agent.send(CLOSE);
        exit.awaitSilent();
        agent.close();
    }

    /**
     * Sets verbosity of the monitor.
     * @param verbose true for monitor to be verbose, otherwise false.
     * @return this instance.
     */
    public final ZMonitor verbose(boolean verbose)
    {
        if (started) {
            System.out.println("ZMonitor: Unable to change verbosity while already started.");
            return this;
        }
        agent.send(VERBOSE, true);
        agent.send(Boolean.toString(verbose));
        agent.recv();
        return this;
    }

    /**
     * Adds event types to monitor.
     * @param events the types of events to monitor.
     * @return this instance.
     */
    public final ZMonitor add(Event... events)
    {
        if (started) {
            System.out.println("ZMonitor: Unable to add events while already started.");
            return this;
        }
        ZMsg msg = new ZMsg();
        msg.add(ADD_EVENTS);
        for (Event evt : events) {
            msg.add(evt.name());
        }
        agent.send(msg);
        agent.recv();
        return this;
    }

    /**
     * Removes event types from monitor.
     * @param events the types of events to stop monitoring.
     * @return this instance.
     */
    public final ZMonitor remove(Event... events)
    {
        if (started) {
            System.out.println("ZMonitor: Unable to remove events while already started.");
            return this;
        }
        ZMsg msg = new ZMsg();
        msg.add(REMOVE_EVENTS);
        for (Event evt : events) {
            msg.add(evt.name());
        }
        agent.send(msg);
        agent.recv();
        return this;
    }

    /**
     * Gets the next event, blocking for it until available.
     * @return the next monitored event or null if closed.
     */
    public final ZEvent nextEvent()
    {
        return nextEvent(true);
    }

    /**
     * Gets the next event, blocking for it until available if requested.
     * @param wait true to block until next event is available, false to immediately return with null if there is no event.
     * @return the next event or null if there is currently none.
     */
    public final ZEvent nextEvent(boolean wait)
    {
        if (!started) {
            System.out.println("ZMonitor: Start before getting events.");
            return null;
        }
        ZMsg msg = agent.recv(wait);
        if (msg == null) {
            return null;
        }
        return new ZEvent(msg);
    }

    /**
     * Gets the next event, blocking for it until available if requested.
     * @param timeout the time in milliseconds to wait for a message before returning null, -1 to block.
     * @return the next event or null if there is currently none after the specified timeout.
     */
    public final ZEvent nextEvent(int timeout)
    {
        if (!started) {
            System.out.println("ZMonitor: Start before getting events.");
            return null;
        }
        ZMsg msg = agent.recv(timeout);
        if (msg == null) {
            return null;
        }
        return new ZEvent(msg);
    }

    private static final String START         = "START";
    private static final String CLOSE         = "CLOSE";
    private static final String VERBOSE       = "VERBOSE";
    private static final String ADD_EVENTS    = "ADD_EVENTS";
    private static final String REMOVE_EVENTS = "REMOVE_EVENTS";

    private final ZAgent     agent;
    private final ZStar.Exit exit;

    /**
     * Creates a monitoring actor for the given socket.
     * @param ctx the context relative to this actor. Not null.
     * @param socket the socket to monitor for events. Not null.
     */
    public ZMonitor(ZContext ctx, Socket socket)
    {
        Objects.requireNonNull(ctx, "ZMonitor works only with a supplied context");
        Objects.requireNonNull(socket, "Socket has to be supplied");
        final MonitorActor actor = new MonitorActor(socket);
        final ZActor zactor = new ZActor(ctx, actor, UUID.randomUUID().toString());

        agent = zactor.agent();
        exit = zactor.exit();

        // wait for the start of the actor
        agent.recv().destroy();
    }

    private static class MonitorActor extends ZActor.SimpleActor
    {
        private static final String ERROR = "ERROR";
        private static final String OK    = "OK";

        private final Socket monitored; // the monitored socket
        private final String address;   // the address where events will be collected

        private Socket  monitor; // the monitoring socket
        private int     events;  // the events to monitor
        private boolean verbose;

        public MonitorActor(ZMQ.Socket socket)
        {
            assert (socket != null);
            this.monitored = socket;
            this.address = String.format("inproc://zmonitor-%s-%s", socket.hashCode(), UUID.randomUUID());
        }

        @Override
        public String premiere(Socket pipe)
        {
            return "ZMonitor-" + monitored.toString();
        }

        @Override
        public List<Socket> createSockets(ZContext ctx, Object... args)
        {
            monitor = ctx.createSocket(SocketType.PAIR);
            assert (monitor != null);

            return Collections.singletonList(monitor);
        }

        @Override
        public void start(Socket pipe, List<Socket> sockets, ZPoller poller)
        {
            pipe.send("STARTED");
        }

        @Override
        public boolean stage(Socket socket, Socket pipe, ZPoller poller, int evts)
        {
            final zmq.ZMQ.Event event = zmq.ZMQ.Event.read(socket.base());
            assert (event != null);
            final int code = event.event;
            final String address = event.addr;
            assert (address != null);
            final Event type = Event.findByCode(code);
            assert (type != null);
            final ZMsg msg = new ZMsg();

            final ZFrame frame = new ZFrame(new byte[1 + 4 + address.length() + 4]);
            final ZNeedle needle = new ZNeedle(frame);
            needle.putNumber1(type.ordinal());
            needle.putNumber4(code);
            needle.putString(address);

            msg.add(frame);

            final Object value = event.arg;
            if (value != null) {
                msg.add(value.toString());
            }
            return msg.send(pipe, true);
        }

        @Override
        public boolean backstage(ZMQ.Socket pipe, ZPoller poller, int evts)
        {
            final String command = pipe.recvStr();
            if (command == null) {
                System.out.printf("ZMonitor: Closing monitor %s : No command%n", monitored);
                return false;
            }
            switch (command) {
            case VERBOSE:
                verbose = Boolean.parseBoolean(pipe.recvStr());
                return pipe.send(OK);
            case ADD_EVENTS:
                return addEvents(pipe);
            case REMOVE_EVENTS:
                return removeEvents(pipe);
            case START:
                return start(poller, pipe);
            case CLOSE:
                return close(poller, pipe);
            default:
                System.out.printf("ZMonitor: Closing monitor %s : Unknown command %s%n", monitored, command);
                pipe.send(ERROR);
                return false;
            }
        }

        private boolean addEvents(Socket pipe)
        {
            final ZMsg msg = ZMsg.recvMsg(pipe);
            if (msg == null) {
                return false; // interrupted
            }
            for (ZFrame frame : msg) {
                final String evt = frame.getString(ZMQ.CHARSET);
                final Event event = Event.valueOf(evt);
                if (verbose) {
                    System.out.printf("ZMonitor: Adding" + " event %s%n", event);
                }
                events |= event.code;
            }
            return pipe.send(OK);
        }

        private boolean removeEvents(Socket pipe)
        {
            final ZMsg msg = ZMsg.recvMsg(pipe);
            if (msg == null) {
                return false; // interrupted
            }
            for (ZFrame frame : msg) {
                final String evt = frame.getString(ZMQ.CHARSET);
                final Event event = Event.valueOf(evt);
                if (verbose) {
                    System.out.printf("ZMonitor: Removing" + " event %s%n", event);
                }
                events &= ~event.code;
            }
            return pipe.send(OK);
        }

        private boolean start(ZPoller poller, Socket pipe)
        {
            boolean rc = true;
            String err = "";
            if (rc) {
                rc = monitored.monitor(address, events);
                err = "Unable to monitor socket " + monitored;
            }
            if (rc) {
                err = "Unable to connect monitoring socket " + monitor;
                rc = monitor.connect(address);
            }
            if (rc) {
                err = "Unable to poll monitoring socket " + monitor;
                rc = poller.register(monitor, ZPoller.IN);
            }
            log("tart", rc, err);
            if (rc) {
                return pipe.send(OK);
            }
            pipe.send(ERROR);
            return false;
        }

        private boolean close(ZPoller poller, Socket pipe)
        {
            boolean rc = poller.unregister(monitor);
            String err = "Unable to unregister monitoring socket " + monitor;
            if (rc) {
                err = "Unable to stop monitor socket " + monitored;
                rc = monitored.monitor(null, events);
            }
            log("top", rc, err);

            if (verbose) {
                System.out.printf("ZMonitor: Closing monitor %s%n", monitored);
            }
            pipe.send(rc ? OK : ERROR);
            return false;
        }

        private void log(String action, boolean rc, String err)
        {
            if (verbose) {
                if (rc) {
                    System.out.printf("ZMonitor: S%s monitor for events %s on %s%n", action, events, monitored);
                }
                else {
                    System.out.printf(
                                      "ZMonitor: Unable to s%s monitor for events %s (%s) on %s%n",
                                      action,
                                      events,
                                      err,
                                      monitored);
                }
            }
        }
    }
}
