/*
 This file is part of GNUnet.
 (C) 2013 Christian Grothoff (and other contributing authors)

 GNUnet is free software; you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published
 by the Free Software Foundation; either version 3, or (at your
 option) any later version.

 GNUnet is distributed in the hope that it will be useful, but
 WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with GNUnet; see the file COPYING.  If not, write to the
 Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 Boston, MA 02111-1307, USA.
 */

package org.gnunet.consensus;

import com.google.common.base.Preconditions;
import org.gnunet.consensus.messages.*;
import org.gnunet.mq.Envelope;
import org.gnunet.mq.NotifySentHandler;
import org.gnunet.util.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Multi-peer set reconciliation.
 */
public class Consensus {
    /**
     * Class logger.
     */
    private static final Logger logger = LoggerFactory
            .getLogger(Consensus.class);

    /**
     * Client connected to the consensus service.
     */
    private Client client;

    /**
     * Called when conclude has finished.
     */
    private ConsensusCallback consensusCallback;

    /**
     * Message dispatch for messages from the consensus service.
     */
    private class ConsensusMessageReceiver extends RunaboutMessageReceiver {
        public void visit(ConcludeDoneMessage m) {
            if (null == consensusCallback) {
                logger.error("unexpected conclude done message");
                return;
            }
            consensusCallback.onDone();
        }

        public void visit(NewElementMessage m) {
            ConsensusElement element = new ConsensusElement(m.elementData, m.elementType);
            element.elementType = m.elementType;
            element.data = m.elementData;
            consensusCallback.onElement(element);
        }

        @Override
        public void handleError() {
            System.out.println("Error receiving from consensus service.");
            consensusCallback.onElement(null);
        }
    }

    /**
     * Create a consensus session.  The set being reconciled is initially
     * empty.
     *
     * @param peers array of peers participating in this consensus session
     *              Inclusion of the local peer is optional.
     * @param sessionId session identifier
     *                   Allows a group of peers to have more than consensus session.
     * @param startTime when should the consensus start?
     * @param deadline when should we be done?
     */
    public Consensus(Configuration cfg, PeerIdentity[] peers, HashCode sessionId,
                     AbsoluteTime startTime, AbsoluteTime deadline) {
        client = new Client("consensus", cfg);
        client.installReceiver(new ConsensusMessageReceiver());
        String peersString = "";
        for (PeerIdentity pi : peers)
            peersString += pi + ", ";

        logger.info("starting consensus with {} peers given to consensus ({})", peers.length, peersString);
        JoinMessage m = new JoinMessage();
        m.numPeers = peers.length;
        m.peers = peers;
        m.sessionId = sessionId;
        m.startTime = startTime.asMessage();
        m.deadline = deadline.asMessage();
        client.send(m);
    }

    /**
     * Insert an element into the consensus set.
     *
     * @param element element to insert in the consensus
     */
    public void insertElement(ConsensusElement element) {
        insertElement(element, null);
    }

    /**
     * Insert an element into the consensus set.
     *
     * @param element element to insert in the consensus
     * @param idc called when the element has been sent to the service
     */
    public void insertElement(ConsensusElement element, final InsertDoneCallback idc) {
        InsertElementMessage m = new InsertElementMessage();
        m.elementData = element.data;
        m.elementType = element.elementType;
        Envelope ev = new Envelope(m);
        if (null != idc) {
            ev.notifySent(new NotifySentHandler() {
            @Override
            public void onSent() {
                idc.onInsertDone();
            }
        });
        }
        client.send(ev);
    }

    /**
     * We are done with inserting new elements into the consensus;
     * try to conclude the consensus within a given time window.
     * After conclude has been called, no further elements may be
     * inserted by the client.
     *
     * @param concludeCallback called when the consensus has concluded
     */
    public void conclude(ConsensusCallback concludeCallback) {
        Preconditions.checkNotNull(concludeCallback, "conclude with null callback");
        Preconditions.checkState(null == this.consensusCallback, "called conclude twice");
        this.consensusCallback = concludeCallback;
        ConcludeMessage m = new ConcludeMessage();
        client.send(m);
    }

    /**
     * Destroy a consensus handle.
     * Free all state associated with
     * it, no longer call any of the callbacks.
     */
    public void destroy() {
        client.disconnect();
        client = null;
    }
}
