package lsr.common;

import java.io.DataInputStream;
import java.io.IOException;
import java.io.Serializable;
import java.nio.ByteBuffer;
import java.util.Arrays;

/**
 * Represents the request of user which will be inserted into state machine
 * after deciding it. After executing this request, <code>Reply</code> message
 * is generated.
 * 
 * @see Reply
 */
public final class ClientRequest implements Serializable {
    /*
     * The Request class should be final. The custome deserialization does not
     * respect class hierarchy, so any class derived from request would be
     * deserialized as the base Request class, which could cause bugs if we rely
     * on type information in the code.
     */
    private static final long serialVersionUID = 1L;

    /** Represents the NOP request */
    public static final ClientRequest NOP = new ClientRequest(RequestId.NOP, new byte[0]);

    private final RequestId requestId;
    private final byte[] value;

    /**
     * Creates new <code>Request</code>.
     * 
     * @param requestId - id of this request. Must not be null.
     * @param value - the value of request. Must not be null (but may be empty).
     */
    public ClientRequest(RequestId requestId, byte[] value) {
        assert requestId != null : "Request ID cannot be null";
        assert value != null : "Value cannot be null";
        this.requestId = requestId;
        this.value = value;
    }

    /**
     * Reads a request from the given <code>ByteBuffer</code> and advances the
     * position on the buffer.
     * 
     * @param buffer - the byte buffer with serialized request
     * @return deserialized request from input byte buffer
     */
    public static ClientRequest create(ByteBuffer buffer) {
        long clientId = buffer.getLong();
        int sequenceId = buffer.getInt();
        RequestId requestId = new RequestId(clientId, sequenceId);

        byte[] value = new byte[buffer.getInt()];
        buffer.get(value);
        return new ClientRequest(requestId, value);
    }
    
    /** For use of ForwardedRequest class */
    public static ClientRequest create(DataInputStream input) throws IOException {
        long clientId = input.readLong();
        int sequenceId = input.readInt();
        RequestId requestId = new RequestId(clientId, sequenceId);

        byte[] value = new byte[input.readInt()];
        input.readFully(value);
        return new ClientRequest(requestId, value);
    }

    /**
     * Returns the id of this request.
     * 
     * @return id of request
     */
    public RequestId getRequestId() {
        return requestId;
    }

    /**
     * Returns the value held by this request.
     * 
     * @return the value of this request
     */
    public byte[] getValue() {
        return value;
    }

    /**
     * The size of the request after serialization in bytes.
     * 
     * @return the size of the request in bytes
     */
    public int byteSize() {
        return 8 + 4 + 4 + value.length;
    }

    /**
     * Writes a message to specified byte buffer. The number of elements
     * remaining in specified buffer should be greater or equal than
     * <code>byteSize()</code>.
     * 
     * @param bb - the byte buffer to write message to
     */
    public void writeTo(ByteBuffer bb) {
        bb.putLong(requestId.getClientId());
        bb.putInt(requestId.getSeqNumber());
        bb.putInt(value.length);
        bb.put(value);
    }

    /**
     * Creates a byte array with the binary representation of the request.
     * 
     * @return
     */
    public byte[] toByteArray() {
        ByteBuffer byteBuffer = ByteBuffer.allocate(byteSize());
        writeTo(byteBuffer);
        return byteBuffer.array();
    }

    public boolean equals(Object obj) {
        if (!(obj instanceof ClientRequest)) {
            return false;
        }
        ClientRequest request = (ClientRequest) obj;
        if (requestId.equals(request.requestId)) {
            assert Arrays.equals(value, request.value) : "Critical: identical RequestID, different value";
            return true;
        }
        return false;
    }

    @Override
    public int hashCode() {
        return requestId.hashCode();
    }

    public String toString() {
        return "id=" + requestId;
    }

    public boolean isNop() {
        return requestId.isNop();
    }
}
