package org.gnunet.mq;


import org.gnunet.util.GnunetMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.LinkedList;

/**
 * General-purpose message queue
 */
public abstract class MessageQueue {
    /**
     * Class logger.
     */
    private static final Logger logger = LoggerFactory
            .getLogger(MessageQueue.class);
    /**
     * Envelopes queued for sending.
     */
    private LinkedList<Envelope> queuedEnvelopes = new LinkedList<Envelope>();
    /**
     * Envelopes queued for sending.  The preferred envelopes will always be sent before
     * the envelopes in 'queuedEnvelopes'.
     */
    private LinkedList<Envelope> preferredQueuedEnvelopes = new LinkedList<Envelope>();
    /**
     * Envelope we are currently in the process of submitting.
     * While the 'currentEnvelope' is not null, no other envelope
     * can be transmitted.
     */
    protected Envelope currentEnvelope;

    /**
     * Are we ready to submit messages, or should we wait and queue further submit
     * requests?
     */
    private boolean readyForSubmit;

    /**
     * Submit a message. Once the message can't be canceled,
     * reportMessageSent must be called.
     * @param ev envelope to submit
     */
    protected abstract void submit(Envelope ev);

    /**
     * Cancel a message that already has been submitted.
     */
    protected abstract void retract();

    public void send(GnunetMessage.Body body) {
        send(new Envelope(body));
    }

    public void sendPreferred(GnunetMessage.Body body) {
        sendPreferred(new Envelope(body));
    }

    public void send(Envelope ev) {
        logger.debug("message queue {}: queueing message", this.getClass().getName());
        queuedEnvelopes.addLast(ev);
        trySubmitNext();
    }

    public void sendPreferred(Envelope ev) {
        logger.debug("message queue {}: queueing preferred message", this.getClass().getName());
        preferredQueuedEnvelopes.addLast(ev);
        trySubmitNext();
    }

    private Envelope pollNextEnvelope() {
        if (!preferredQueuedEnvelopes.isEmpty())
            return preferredQueuedEnvelopes.removeFirst();
        if (!queuedEnvelopes.isEmpty())
            return queuedEnvelopes.removeFirst();
        return null;
    }

    protected void trySubmitNext() {
        if (currentEnvelope != null || !readyForSubmit) {
            logger.debug("message queue {}: not submitting (not ready)", this.getClass().getName());
            return;
        }
        Envelope ev = pollNextEnvelope();
        if (ev == null) {
            logger.debug("message queue {}: not submitting (nothing to send)", this.getClass().getName());
            return;
        }
        currentEnvelope = ev;
        readyForSubmit = false;
        submit(currentEnvelope);
    }

    protected void reportReadyForSubmit() {
        if (readyForSubmit) {
            throw new AssertionError("message queue reported 'ready for submit' twice");
        }
        logger.debug("message queue {} ready for submit", this.getClass().getName());
        readyForSubmit = true;
        trySubmitNext();
    }

    /**
     * Report to the message queue that the message has been sent irrevocably.
     * Only up to calling reportMessageSend, a message can be canceled.
     */
    protected void reportMessageSent() {
        if (null == currentEnvelope)
            throw new AssertionError();
        currentEnvelope.invokeSentNotification();
        currentEnvelope = null;
        trySubmitNext();
    }

    /**
     * Cancel sending an envelope. The envelope must be queued in this message queue.
     *
     * @param ev the envelope to onCancel
     */
    void cancelEnvelope(Envelope ev) {
        if (ev == currentEnvelope) {
            retract();
            trySubmitNext();
        } else {
            queuedEnvelopes.remove(ev);
            preferredQueuedEnvelopes.remove(ev);
        }
    }
}
