package com.github.faucamp.simplertmp.packets;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;

import com.github.faucamp.simplertmp.Util;
import com.github.faucamp.simplertmp.io.ChunkStreamInfo;

/**
 * User Control message, such as ping
 * 
 * @author francois
 */
public class UserControl extends RtmpPacket {

    /**
     * Control message type
     * Docstring adapted from the official Adobe RTMP spec, section 3.7
     */
    public static enum Type {

        /** 
         * Type: 0
         * The server sends this event to notify the client that a stream has become 
         * functional and can be used for communication. By default, this event
         * is sent on ID 0 after the application connect command is successfully
         * received from the client. 
         * 
         * Event Data:
         * eventData[0] (int) the stream ID of the stream that became functional
         */
        STREAM_BEGIN(0),
        /**
         * Type: 1
         * The server sends this event to notify the client that the playback of
         * data is over as requested on this stream. No more data is sent without
         * issuing additional commands. The client discards the messages received
         * for the stream.
         * 
         * Event Data:
         * eventData[0]: the ID of thestream on which playback has ended.
         */
        STREAM_EOF(1),
        /**
         * Type: 2
         * The server sends this event to notify the client that there is no 
         * more data on the stream. If the server does not detect any message for
         * a time period, it can notify the subscribed clients that the stream is 
         * dry. 
         * 
         * Event Data:
         * eventData[0]: the stream ID of the dry stream. 
         */
        STREAM_DRY(2),
        /**
         * Type: 3
         * The client sends this event to inform the server of the buffer size 
         * (in milliseconds) that is used to buffer any data coming over a stream.
         * This event is sent before the server starts  processing the stream.
         * 
         * Event Data:
         * eventData[0]: the stream ID and 
         * eventData[1]: the buffer length, in milliseconds.
         */
        SET_BUFFER_LENGTH(3),
        /**
         * Type: 4
         * The server sends this event to notify the client that the stream is a
         * recorded stream.
         * 
         * Event Data:
         * eventData[0]: the stream ID of the recorded stream.         
         */
        STREAM_IS_RECORDED(4),
        /**
         * Type: 6
         * The server sends this event to test whether the client is reachable. 
         * 
         * Event Data:
         * eventData[0]: a timestamp representing the local server time when the server dispatched the command. 
         * 
         * The client responds with PING_RESPONSE on receiving PING_REQUEST.
         */
        PING_REQUEST(6),
        /**
         * Type: 7
         * The client sends this event to the server in response to the ping request. 
         * 
         * Event Data:
         * eventData[0]: the 4-byte timestamp which was received with the PING_REQUEST.
         */
        PONG_REPLY(7),
        /**
         * Type: 31 (0x1F)
         * 
         * This user control type is not specified in any official documentation, but
         * is sent by Flash Media Server 3.5. Thanks to the rtmpdump devs for their
         * explanation: 
         * 
         * Buffer Empty (unofficial name): After the server has sent a complete buffer, and
         * sends this Buffer Empty message, it will wait until the play
         * duration of that buffer has passed before sending a new buffer.
         * The Buffer Ready message will be sent when the new buffer starts.
         *
         * (see also: http://repo.or.cz/w/rtmpdump.git/blob/8880d1456b282ee79979adbe7b6a6eb8ad371081:/librtmp/rtmp.c#l2787)         
         */
        BUFFER_EMPTY(31),
        /**
         * Type: 32 (0x20)
         * 
         * This user control type is not specified in any official documentation, but
         * is sent by Flash Media Server 3.5. Thanks to the rtmpdump devs for their
         * explanation: 
         * 
         * Buffer Ready (unofficial name): After the server has sent a complete buffer, and
         * sends a Buffer Empty message, it will wait until the play
         * duration of that buffer has passed before sending a new buffer.
         * The Buffer Ready message will be sent when the new buffer starts.
         * (There is no BufferReady message for the very first buffer;
         * presumably the Stream Begin message is sufficient for that
         * purpose.)
         *
         * (see also: http://repo.or.cz/w/rtmpdump.git/blob/8880d1456b282ee79979adbe7b6a6eb8ad371081:/librtmp/rtmp.c#l2787)         
         */
        BUFFER_READY(32);
        
        private int intValue;
        private static final Map<Integer, Type> quickLookupMap = new HashMap<Integer, Type>();

        static {
            for (Type type : Type.values()) {
                quickLookupMap.put(type.getIntValue(), type);
            }
        }

        private Type(int intValue) {
            this.intValue = intValue;
        }

        public int getIntValue() {
            return intValue;
        }

        public static Type valueOf(int intValue) {
            return quickLookupMap.get(intValue);
        }
    }
    private Type type;
    private int[] eventData;

    public UserControl(RtmpHeader header) {
        super(header);
    }

    public UserControl(ChunkStreamInfo channelInfo) {
        super(new RtmpHeader(channelInfo.canReusePrevHeaderTx(RtmpHeader.MessageType.USER_CONTROL_MESSAGE) ? RtmpHeader.ChunkType.TYPE_2_RELATIVE_TIMESTAMP_ONLY : RtmpHeader.ChunkType.TYPE_0_FULL, ChunkStreamInfo.RTMP_CID_PROTOCOL_CONTROL, RtmpHeader.MessageType.USER_CONTROL_MESSAGE));
    }

    /** Convenience construtor that creates a "pong" message for the specified ping */
    public UserControl(UserControl replyToPing, ChunkStreamInfo channelInfo) {
        this(Type.PONG_REPLY, channelInfo);
        this.eventData = replyToPing.eventData;
    }

    public UserControl(Type type, ChunkStreamInfo channelInfo) {
        this(channelInfo);
        this.type = type;
    }

    public Type getType() {
        return type;
    }

    public void setType(Type type) {
        this.type = type;
    }

    /** 
     * Convenience method for getting the first event data item, as most user control
     * message types only have one event data item anyway
     * This is equivalent to calling <code>getEventData()[0]</code>
     */
    public int getFirstEventData() {
        return eventData[0];
    }

    public int[] getEventData() {
        return eventData;
    }

    /** Used to set (a single) event data for most user control message types */
    public void setEventData(int eventData) {
        if (type == Type.SET_BUFFER_LENGTH) {
            throw new IllegalStateException("SET_BUFFER_LENGTH requires two event data values; use setEventData(int, int) instead");
        }
        this.eventData = new int[]{eventData};
    }

    /** Used to set event data for the SET_BUFFER_LENGTH user control message types */
    public void setEventData(int streamId, int bufferLength) {
        if (type != Type.SET_BUFFER_LENGTH) {
            throw new IllegalStateException("User control type " + type + " requires only one event data value; use setEventData(int) instead");
        }
        this.eventData = new int[]{streamId, bufferLength};
    }

    @Override
    public void readBody(InputStream in) throws IOException {
        // Bytes 0-1: first parameter: ping type (mandatory)
        type = Type.valueOf(Util.readUnsignedInt16(in));
        int bytesRead = 2;
        // Event data (1 for most types, 2 for SET_BUFFER_LENGTH)
        if (type == Type.SET_BUFFER_LENGTH) {
            setEventData(Util.readUnsignedInt32(in), Util.readUnsignedInt32(in));
            bytesRead += 8;
        } else {
            setEventData(Util.readUnsignedInt32(in));
            bytesRead += 4;
        }
        // To ensure some strange non-specified UserControl/ping message does not slip through
        assert header.getPacketLength() == bytesRead;
    }

    @Override
    protected void writeBody(OutputStream out) throws IOException {
        // Write the user control message type
        Util.writeUnsignedInt16(out, type.getIntValue());
        // Now write the event data
        Util.writeUnsignedInt32(out, eventData[0]);
        if (type == Type.SET_BUFFER_LENGTH) {
            Util.writeUnsignedInt32(out, eventData[1]);
        }
    }

    @Override
    protected byte[] array() {
        return null;
    }

    @Override
    protected int size() {
        return 0;
    }

    @Override
    public String toString() {
        return "RTMP User Control (type: " + type + ", event data: " + eventData + ")";
    }
}
