import ByteBuf from "./util/ByteBuf.js";
import PacketRegistry from "./PacketRegistry.js";
import ProtocolState from "./ProtocolState.js";
import {require} from "../../../../Start.js";

export default class NetworkManager {

    static DEBUG = false;
    static MAX_COMPRESSION = 2097152;

    constructor(minecraft) {
        this.minecraft = minecraft;
        this.socket = null;
        this.connected = false;
        this.networkHandler = null;

        this.registry = new PacketRegistry();
        this.protocolState = ProtocolState.HANDSHAKE;

        this.queue = [];

        this.pako = require("pako");
        this.compressionThreshold = 0;

        this.carryBuffer = [];
    }

    setNetworkHandler(networkHandler) {
        this.networkHandler = networkHandler;
    }

    connect(address, port, proxy) {
        this.socket = new WebSocket("ws://" + proxy.address + ":" + proxy.port);
        this.socket.binaryType = "arraybuffer";

        this.socket.onopen = e => this._onOpen(e);
        this.socket.onclose = e => this._onClose(e);
        this.socket.onmessage = e => this._onMessage(e);
        this.socket.onerror = e => this._onError(e);

        this.address = address;
        this.port = port;
    }

    _onOpen() {
        this.connected = true;

        // Send proxy handshake
        this.sendProxyPacket(0, {
            "host": this.address,
            "port": this.port,
        });

        // Handle connect event
        this.networkHandler.onConnect();

        // Flush packet queue
        this.flushPacketQueue();
    }

    sendProxyPacket(id, payload) {
        let object = {
            "id": id,
            "payload": payload
        };
        this.socket.send(JSON.stringify(object));
    }

    sendPacket(packet) {
        if (this.connected) {
            this._sendPacketImmediately(packet);
        } else {
            this.queue.push(packet);
        }
    }

    _sendPacketImmediately(packet) {
        // Switch packet state
        let packetState = this.registry.getPacketState(packet);
        if (packetState !== this.protocolState) {
            if (packetState === null) {
                console.error("[Network] Tried to send unknown packet: " + packet.constructor.name);
                return;
            }

            this.setState(packetState);
        }

        // Packet Codec
        let buffer = new ByteBuf();
        buffer.writeByte(this.registry.getClientBoundPacketId(this.protocolState, packet));
        packet.write(buffer);
        buffer.setPosition(0);

        // Packet Compression
        if (this.compressionThreshold !== 0) {
            let length = buffer.length();
            if (length > this.compressionThreshold) {
                let compressed = this.pako.deflate(buffer.getArray(), {
                    chunkSize: 8192
                });

                buffer = new ByteBuf();
                buffer.writeVarInt(length);
                buffer.write(compressed);
            } else {
                let copy = buffer.getArray();
                buffer = new ByteBuf();
                buffer.writeVarInt(0);
                buffer.write(copy);
            }
            buffer.setPosition(0);
        }

        // Packet Sizer
        let wrapper = new ByteBuf();
        wrapper.writeVarInt(buffer.length());
        wrapper.write(buffer.getArray());

        // Packet Compression
        if (this.isEncrypted) {
            wrapper = new ByteBuf(this.encryption.encrypt(wrapper.getArray()));
        }

        // Send chunk
        this.socket.send(wrapper.getArray());

        if (NetworkManager.DEBUG) {
            console.log("[Network] [OUT] " + packet.constructor.name);
        }
    }

    _onMessage(event) {
        try {
            let data = new Uint8Array(event.data);

            // Packet Compression
            if (this.isEncrypted) {
                data = this.decryption.decrypt(data);
            }

            // Packet Sizer

            let bufferIn = new ByteBuf(new Int8Array([]));
            bufferIn.write(this.carryBuffer);
            bufferIn.write(data);
            bufferIn.setPosition(0);
            this.carryBuffer = [];
            while (bufferIn.readableBytes() > 0) {
                let three = [0, 0, 0];
                let start = bufferIn.getPosition();
                for (let i = 0; i < three.length; i++) {
                    three[i] = bufferIn.readByte();
                    if (three[i] >= 0) {
                        let length = new ByteBuf(three).readVarInt();
                        if (length === 0) {
                            throw new Error("Empty Packet!");
                        }

                        if (bufferIn.readableBytes() < length) {
                            bufferIn.setPosition(start);
                            this.carryBuffer = bufferIn.getSlicedArray();
                            return;
                        } else {
                            this.handlePacket(new ByteBuf(bufferIn.getSlicedArray(length)));
                            bufferIn.skipBytes(length);
                        }
                        break;
                    }
                }
            }
        } catch (e) {
            console.error(e);
            console.log(e.stack);
        }
    }

    handlePacket(buffer) {
        // Packet Compression
        if (this.compressionThreshold !== 0) {
            let uncompressedLength = buffer.readVarInt();

            if (uncompressedLength !== 0) {
                if (uncompressedLength < this.compressionThreshold) {
                    throw new Error("Badly compressed packet - size of " + uncompressedLength + " is below server threshold of " + this.compressionThreshold);
                }
                if (uncompressedLength > NetworkManager.MAX_COMPRESSION) {
                    throw new Error("Badly compressed packet - size of " + uncompressedLength + " is larger than protocol maximum of " + NetworkManager.MAX_COMPRESSION);
                }

                // Decompress
                buffer = new ByteBuf(this.pako.inflate(new Uint8Array(buffer.getSlicedArray()), {
                    chunkSize: 8192
                }));

                if (buffer.length() !== uncompressedLength) {
                    throw new Error("Badly compressed packet - decompressed size of " + buffer.length() + " is not equal to original size of " + uncompressedLength);
                }
            }
        }

        // Packet Codec
        let packetId = buffer.readByte(); // Read packet id
        let clazz = this.registry.getServerBoundById(this.protocolState, packetId);
        if (clazz === null) {
            if (NetworkManager.DEBUG) {
                console.log("[Network] [IN] Unknown packet id: " + packetId + " (0x" + packetId.toString(16) + ")");
            }
            return;
        } else {
            if (NetworkManager.DEBUG) {
                console.log("[Network] [IN] " + clazz.name);
            }
        }

        let packet = new clazz;
        packet.read(buffer, buffer.length);
        packet.handle(this.networkHandler);
    }

    _onError(event) {
        console.error("[Network] Error: " + event.data);
    }

    _onClose(event) {
        if (this.connected) {
            this.networkHandler.onDisconnect();
        }

        this.connected = false;
    }

    close() {
        this.connected = false;
        this.socket.close();
        this.networkHandler.onDisconnect();
    }

    isConnected() {
        return this.connected;
    }

    flushPacketQueue() {
        this.queue.forEach(packet => this.sendPacket(packet));
        this.queue = [];
    }

    enableEncryption(secretKey) {
        this.isEncrypted = true;
        this.decryption = new (require("aesjs").ModeOfOperation).cfb(secretKey, secretKey, 1);
        this.encryption = new (require("aesjs").ModeOfOperation).cfb(secretKey, secretKey, 1);
    }

    setState(packetState) {
        console.log("[Network] Switching protocol state from " + this.protocolState.getName() + " to " + packetState.getName());
        this.protocolState = packetState;
    }

    getState() {
        return this.protocolState;
    }

    setCompressionThreshold(threshold) {
        console.log("[Network] Set compression threshold to " + threshold);

        if (threshold >= 0) {
            this.compressionThreshold = threshold;
        } else {
            this.compressionThreshold = 0;
        }
    }
}