package ru.dbotthepony.kstarbound.network

import io.netty.buffer.ByteBuf
import io.netty.channel.ChannelHandler.Sharable
import io.netty.channel.ChannelHandlerContext
import io.netty.channel.ChannelInboundHandlerAdapter
import io.netty.channel.ChannelOutboundHandlerAdapter
import io.netty.channel.ChannelPromise
import it.unimi.dsi.fastutil.bytes.ByteArrayList
import org.apache.logging.log4j.LogManager

const val MAX_DATAGRAM_SIZE = 0xFFFFFF
private val LOGGER = LogManager.getLogger()

@Sharable
object DatagramEncoder : ChannelOutboundHandlerAdapter() {
	override fun write(ctx: ChannelHandlerContext, msg: Any, promise: ChannelPromise) {
		if (msg is ByteBuf) {
			if (msg.readableBytes() >= MAX_DATAGRAM_SIZE) {
				LOGGER.error("Outgoing packet is too big: ${msg.readableBytes()} bytes (max: $MAX_DATAGRAM_SIZE bytes)")
			} else {
				val size = msg.readableBytes()
				msg.ensureWritable(size + 3)

				for (i in msg.readableBytes() - 1 downTo  0)
					msg.setByte(i + 3, msg.getByte(i).toInt())

				val old = msg.writerIndex()
				msg.writerIndex(0)
				msg.writeMedium(size)
				msg.writerIndex(old + 3)

				ctx.write(msg, promise)
			}
		} else {
			super.write(ctx, msg, promise)
		}
	}
}

class DatagramDecoder : ChannelInboundHandlerAdapter() {
	private val pendingBytes = ByteArrayList()
	private var messageSize = 0
	private var discarding = false

	override fun channelRead(ctx: ChannelHandlerContext, msg: Any) {
		if (msg is ByteBuf) {
			try {
				while (msg.readableBytes() > 0) {
					if (messageSize == 0) {
						messageSize = msg.readUnsignedMedium()

						if (messageSize >= MAX_DATAGRAM_SIZE) {
							LOGGER.error("Incoming packet is too big: $messageSize bytes (max: $MAX_DATAGRAM_SIZE bytes), it will be discarded.")
							discarding = true
						}
					}

					while (messageSize > 0 && msg.readableBytes() > 0) {
						if (!discarding) pendingBytes.add(msg.readByte())
						messageSize--
					}

					if (messageSize == 0) {
						if (discarding) {
							discarding = false
						} else {
							val alloc = ctx.alloc().buffer(pendingBytes.size)

							for (i in pendingBytes.indices)
								alloc.writeByte(pendingBytes.getByte(i).toInt())

							pendingBytes.clear()

							ctx.fireChannelRead(alloc)
						}
					}
				}
			} finally {
				msg.release()
			}
		} else {
			super.channelRead(ctx, msg)
		}
	}
}
