package org.jgroups.protocols.tom;

import org.jgroups.Address;
import org.jgroups.Global;
import org.jgroups.Header;
import org.jgroups.util.Bits;
import org.jgroups.util.Util;

import java.io.DataInput;
import java.io.DataOutput;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;

/**
 * The header for the Total Order Anycast (TOA) protocol
 *
 * @author Pedro Ruivo
 * @since 3.1
 */
public class ToaHeader extends Header {

    //type
    public static final byte DATA_MESSAGE = 1 << 0;
    public static final byte PROPOSE_MESSAGE = 1 << 1;
    public static final byte FINAL_MESSAGE = 1 << 2;
    public static final byte SINGLE_DESTINATION_MESSAGE = 1 << 3;

    private byte type = 0;
    private MessageID messageID; //address and sequence number
    private long sequencerNumber;
    private Collection<Address> destinations;

    public ToaHeader() {
    }

    private ToaHeader(MessageID messageID, byte type) {
        this.messageID = messageID;
        this.type = type;
    }

    public MessageID getMessageID() {
        return messageID;
    }

    private ToaHeader setDestinations(Collection<Address> addresses) {
        this.destinations = addresses;
        return this;
    }

    public Collection<Address> getDestinations() {
        return Collections.unmodifiableCollection(destinations);
    }

    public long getSequencerNumber() {
        return sequencerNumber;
    }

    public ToaHeader setSequencerNumber(long sequencerNumber) {
        this.sequencerNumber = sequencerNumber;
        return this;
    }

    public byte getType() {
        return type;
    }

    @Override
    public int size() {
        return (int) (Global.BYTE_SIZE + messageID.serializedSize() + Bits.size(sequencerNumber) +
                Util.size(destinations));
    }

    @Override
    public void writeTo(DataOutput out) throws Exception {
        out.writeByte(type);
        messageID.writeTo(out);
        Bits.writeLong(sequencerNumber, out);
        if (type == DATA_MESSAGE) {
            Util.writeAddresses(destinations, out);
        }
    }

    @Override
    public void readFrom(DataInput in) throws Exception {
        type = in.readByte();
        messageID = new MessageID();
        messageID.readFrom(in);
        sequencerNumber = Bits.readLong(in);
        if (type == DATA_MESSAGE) {
            destinations = (Collection<Address>) Util.readAddresses(in, ArrayList.class);
        }
    }

    @Override
    public String toString() {
        return "ToaHeader{" +
                "type=" + type2String(type) +
                ", message_id=" + messageID +
                ", sequence_number=" + sequencerNumber +
                ", destinations=" + destinations +
                '}';
    }

    public static String type2String(byte type) {
        switch (type) {
            case DATA_MESSAGE:
                return "DATA_MESSAGE";
            case PROPOSE_MESSAGE:
                return "PROPOSE_MESSAGE";
            case FINAL_MESSAGE:
                return "FINAL_MESSAGE";
            case SINGLE_DESTINATION_MESSAGE:
                return "SINGLE_DESTINATION_MESSAGE";
            default:
                return "UNKNOWN";
        }
    }

    public static ToaHeader newDataMessageHeader(MessageID messageID, Collection<Address> destinations) {
        assertMessageIDNotNull(messageID);
        return new ToaHeader(messageID, DATA_MESSAGE).setDestinations(new ArrayList<>(destinations));
    }

    public static ToaHeader newProposeMessageHeader(MessageID messageID, long sequencerNumber) {
        assertMessageIDNotNull(messageID);
        return new ToaHeader(messageID, PROPOSE_MESSAGE).setSequencerNumber(sequencerNumber);
    }

    public static ToaHeader newFinalMessageHeader(MessageID messageID, long sequenceNumber) {
        assertMessageIDNotNull(messageID);
        return new ToaHeader(messageID, FINAL_MESSAGE).setSequencerNumber(sequenceNumber);
    }

    public static ToaHeader createSingleDestinationHeader(MessageID messageID) {
        return new ToaHeader(messageID, SINGLE_DESTINATION_MESSAGE);
    }

    private static void assertMessageIDNotNull(MessageID messageID) {
        if (messageID == null) {
            throw new NullPointerException("The message ID can't be null.");
        }
    }
}

