package com.alterdekim.game.component.rtmp;

import com.alterdekim.game.component.GameServer;
import com.alterdekim.game.message.*;
import com.alterdekim.game.message.amf.*;
import com.alterdekim.game.utils.RTMPUtils;
import com.alterdekim.game.utils.StringUtils;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.*;

@Slf4j
public class ConnectedProcessor extends ConnectionProcessor {
    private ConnectedState state;
    private final List<Byte> message;
    private byte lastStreamId;
    private final GameServer gameServer;
    private Integer playerId;
    private final RTMPSession rtmpSessionInfo;

    public ConnectedProcessor(InputStream inputStream, OutputStream outputStream, Socket sock, GameServer gameServer) {
        super(inputStream, outputStream, sock);
        this.state = ConnectedState.WAIT_FOR_MESSAGE;
        this.message = new ArrayList<>();
        this.gameServer = gameServer;
        this.rtmpSessionInfo = new RTMPSession();
    }

    @Override
    public ConnectionState getState() {
        return ConnectionState.CONNECTED;
    }

    @Override
    public ConnectionState process() throws IOException {
        byte[] b1 = new byte[1]; // this was left as is due to underlying conversion between int and byte types.
        int b = this.getInputStream().read(b1);
        if (b == -1) {
            close(); return ConnectionState.CONNECTED;
        }
        this.state = switch(state) {
            case WAIT_FOR_MESSAGE -> processWaitForMessage(b1[0]);
            case LISTENS_FOR_MESSAGE -> processListensForMessage(b1[0]);
        };
        return ConnectionState.CONNECTED;
    }

    private ConnectedState processWaitForMessage(byte b) {
        try {
            RTMPHeader header = new RTMPHeader();

            header.setChunkType(RTMPHeader.ChunkType.fromByte((byte) ((0xC0 & b) >> 6)));
            header.setChunkStreamId((byte) (b & 0x3F));

            if( header.getChunkStreamId() != ChunkStreamInfo.CONTROL_CHANNEL &&
                header.getChunkStreamId() != ChunkStreamInfo.RTMP_COMMAND_CHANNEL &&
                header.getChunkStreamId() != ChunkStreamInfo.RTMP_STREAM_CHANNEL ) {
                return ConnectedState.WAIT_FOR_MESSAGE;
            }

            switch (header.getChunkType()) {
                case TYPE_0_FULL -> { //  b00 = 12 byte header (full header)
                    // Read bytes 1-3: Absolute timestamp
                    header.setAbsoluteTimestamp(RTMPUtils.readUnsignedInt24(this.getInputStream()));
                    header.setTimestampDelta(0);
                    // Read bytes 4-6: Packet length
                    header.setPacketLength(RTMPUtils.readUnsignedInt24(this.getInputStream()));
                    // Read byte 7: Message type ID
                    byte[] b1 = new byte[1];
                    this.getInputStream().read(b1);
                    header.setMessageType(RTMPHeader.MessageType.valueOf(b1[0]));
                    // Read bytes 8-11: Message stream ID (apparently little-endian order)
                    byte[] messageStreamIdBytes = new byte[4];
                    this.getInputStream().read(messageStreamIdBytes);
                    header.setMessageStreamId(RTMPUtils.toUnsignedInt32LittleEndian(messageStreamIdBytes));
                }
                case TYPE_1_RELATIVE_LARGE -> { // b01 = 8 bytes - like type 0. not including message stream ID (4 last bytes)
                    // Read bytes 1-3: Timestamp delta
                    header.setTimestampDelta(RTMPUtils.readUnsignedInt24(this.getInputStream()));
                    // Read bytes 4-6: Packet length
                    header.setPacketLength(RTMPUtils.readUnsignedInt24(this.getInputStream()));
                    // Read byte 7: Message type ID
                    byte[] b1 = new byte[1];
                    this.getInputStream().read(b1);
                    header.setMessageType(RTMPHeader.MessageType.valueOf(b1[0]));
                    RTMPHeader prevHeader = rtmpSessionInfo.getLastHeader(header.getChunkStreamId());
                    try {
                        header.setMessageStreamId(prevHeader.getMessageStreamId());
                        header.setAbsoluteTimestamp(prevHeader.getAbsoluteTimestamp() + header.getTimestampDelta());
                    } catch (NullPointerException ex) {
                        header.setMessageStreamId(0);
                        header.setAbsoluteTimestamp(header.getTimestampDelta());
                    }
                }
                case TYPE_2_RELATIVE_TIMESTAMP_ONLY -> { // b10 = 4 bytes - Basic Header and timestamp (3 bytes) are included
                    // Read bytes 1-3: Timestamp delta
                    header.setTimestampDelta(RTMPUtils.readUnsignedInt24(this.getInputStream()));
                    RTMPHeader prevHeader = rtmpSessionInfo.getLastHeader(header.getChunkStreamId());
                    header.setPacketLength(prevHeader.getPacketLength());
                    header.setMessageType(prevHeader.getMessageType());
                    header.setMessageStreamId(prevHeader.getMessageStreamId());
                    header.setAbsoluteTimestamp(prevHeader.getAbsoluteTimestamp() + header.getTimestampDelta());
                }
                case TYPE_3_RELATIVE_SINGLE_BYTE -> { // b11 = 1 byte: basic header only
                    RTMPHeader prevHeader = rtmpSessionInfo.getLastHeader(header.getChunkStreamId());
                    header.setTimestampDelta(prevHeader.getTimestampDelta());
                    header.setAbsoluteTimestamp(prevHeader.getAbsoluteTimestamp() + header.getTimestampDelta());
                    header.setPacketLength(prevHeader.getPacketLength());
                    header.setMessageType(prevHeader.getMessageType());
                    header.setMessageStreamId(prevHeader.getMessageStreamId());
                }
            }
            this.message.clear();
            this.lastStreamId = header.getChunkStreamId();
            this.rtmpSessionInfo.chunkStreams.put(this.lastStreamId, header);

            return ConnectedState.LISTENS_FOR_MESSAGE;
        } catch (IOException e) {
            log.error("ConnectedProcessor.processWaitForMessage()", e);
        }
        return ConnectedState.WAIT_FOR_MESSAGE;
    }

    private ConnectedState processListensForMessage(byte b) {
        if(this.message.size() >= this.rtmpSessionInfo.getLastHeader(this.lastStreamId).getPacketLength()) {
            this.processMessage();
            return ConnectedState.WAIT_FOR_MESSAGE;
        }
        if( b == (byte) 0xC3 ) { this.rtmpSessionInfo.getLastHeader(this.lastStreamId).setPacketLength(this.rtmpSessionInfo.getLastHeader(this.lastStreamId).getPacketLength()-1); return ConnectedState.LISTENS_FOR_MESSAGE; }
        this.message.add(b);
        if(this.message.size() >= this.rtmpSessionInfo.getLastHeader(this.lastStreamId).getPacketLength()) {
            this.processMessage();
            return ConnectedState.WAIT_FOR_MESSAGE;
        }
        return ConnectedState.LISTENS_FOR_MESSAGE;
    }

    private void processMessage() {
        switch (this.lastStreamId) {
            case ChunkStreamInfo.CONTROL_CHANNEL -> processControlChannel();
            case ChunkStreamInfo.RTMP_COMMAND_CHANNEL -> processCommandChannel();
            case ChunkStreamInfo.RTMP_STREAM_CHANNEL -> processStreamChannel();
            default -> log.warn("Unknown channel id: {}", this.lastStreamId);
        }
    }

    private void processControlChannel() {
        byte[] bytes = SerializerUtils.bytesToPrimitive(message);
        log.info("ConnectedProcessor.processMessage() controlMessage: {}", StringUtils.bytesToHex(bytes));
    }

    private void processCommandChannel() {
        if( this.rtmpSessionInfo.getLastHeader(this.lastStreamId).getMessageType() != RTMPHeader.MessageType.COMMAND_AMF0 ) {
            log.info("ConnectedProcessor.processMessage() type: {}", this.rtmpSessionInfo.getLastHeader(this.lastStreamId).getMessageType());
            return;
        }
        byte[] bytes = SerializerUtils.bytesToPrimitive(message);
        log.info("ConnectedProcessor.processMessage() messageString: {}", StringUtils.bytesToHex(bytes));
        try {
            List<AMFObject> mr = AMFDeserializer.newInstance(message).deserialize();
            if( !mr.isEmpty() &&
                    mr.get(0).getType() == AMFValueType.STRING &&
                    mr.get(0).toString().equals("connect") ) {
                this.playerId = mr.get(3).getInt();
                this.gameServer.onConnect(this.playerId, mr.get(5).toString(), this);

                this.getOutputStream().write( StringUtils.hexStringToByteArray("020000000000040500000000002625A0020000000000050600000000002625A00202000000000004010000000000001000030000000000F214000000000200075F726573756C74003FF0000000000000030006666D7356657202000E464D532F342C302C302C31313231000C6361706162696C697469657300406FE0000000000000046D6F6465003FF00000000000000000090300056C6576656C0200067374617475730004636F646502001D4E6574436F6E6E656374696F6E2E436F6E6E6563742E53756363657373000B6465736372697074696F6E020015436F6E6E656374696F6E207375636365656465642E000E6F626A656374456E636F64696E670000000000000000000004646174610800000000000776657273696F6E02000A342C302C302C31313231000009000009") );
                this.getOutputStream().flush();
                return;
            }
            this.gameServer.onMessage(this.playerId, mr);
        } catch (Exception e) {
            log.error("ConnectedProcessor.processMessage() error: {}", e.getMessage(), e);
        }
    }

    private void processStreamChannel() {
        log.warn("There's stream message.");
    }

    @Override
    public void write(List<Byte> bytes) throws IOException {
        List<Byte> l = new ArrayList<>(Arrays.asList((byte) 0x03, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x14, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00));
        l.addAll(bytes);
        super.write(l);
    }

    public void write_packet(List<Byte> bytes) throws IOException {
        List<Byte> l = new ArrayList<>(Collections.singletonList((byte) 0x03));
        List<Byte> data = new ArrayList<>();
        int cnt = 0;
        for( int i = 0; i < bytes.size(); i++ ) {
            data.add(bytes.get(i));
            if( i >= 4095 && ((i+1) % 4096 == 0) && (i+1) < bytes.size() ) {
                data.add((byte) 0xC3);
                cnt++;
            }
        }
        l.addAll(int2bytes((int) ((System.currentTimeMillis() - this.rtmpSessionInfo.getLastHeader(this.lastStreamId).getAbsoluteTimestamp()) / 1000L), 3));
        l.addAll(int2bytes(data.size() - cnt, 3));
        l.add((byte) 0x14);
        l.addAll(int2bytes(this.lastStreamId, 4));
        l.addAll(data);
        super.write(l);
    }

    private List<Byte> int2bytes(int u, int n) {
        List<Byte> b = new ArrayList<>();
        for( int i = n-1; i >= 0; i-- ) {
            b.add((byte) ((u >> i*8) & 0xFF));
        }
        return b;
    }

    @Override
    public void close() {
        this.gameServer.onDisconnect(this.playerId);
        super.close();
    }

    private class RTMPSession {

        @Getter
        @Setter
        private int chunkSize = 128;

        private Map<Byte, RTMPHeader> chunkStreams = new HashMap<>();

        public RTMPHeader getLastHeader(byte chunkStreamId) {
            RTMPHeader chunkStreamInfo = chunkStreams.get(chunkStreamId);
            if (chunkStreamInfo == null) {
                chunkStreamInfo = new RTMPHeader();
                chunkStreams.put(chunkStreamId, chunkStreamInfo);
            }
            return chunkStreamInfo;
        }
    }

    private interface ChunkStreamInfo {
        byte RTMP_STREAM_CHANNEL = 0x08;
        byte RTMP_COMMAND_CHANNEL = 0x03;
        byte CONTROL_CHANNEL = 0x02;
    }
}
