package ru.dbotthepony.kstarbound.client

import io.netty.bootstrap.Bootstrap
import io.netty.channel.Channel
import io.netty.channel.ChannelHandlerContext
import io.netty.channel.ChannelInitializer
import io.netty.channel.local.LocalAddress
import io.netty.channel.local.LocalChannel
import io.netty.channel.socket.nio.NioSocketChannel
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.network.Connection
import ru.dbotthepony.kstarbound.network.ConnectionSide
import ru.dbotthepony.kstarbound.network.ConnectionType
import ru.dbotthepony.kstarbound.network.IClientPacket
import ru.dbotthepony.kstarbound.network.packets.ClientContextUpdatePacket
import ru.dbotthepony.kstarbound.network.packets.ProtocolRequestPacket
import ru.dbotthepony.kstarbound.network.packets.serverbound.ClientDisconnectRequestPacket
import ru.dbotthepony.kstarbound.network.packets.serverbound.WorldClientStateUpdatePacket
import java.net.SocketAddress

// clientside part of connection
class ClientConnection(val client: StarboundClient, type: ConnectionType) : Connection(ConnectionSide.CLIENT, type) {
	private fun sendHello(asLegacy: Boolean = false) {
		isLegacy = asLegacy

		if (asLegacy) {
			channel.write(ProtocolRequestPacket(Starbound.LEGACY_PROTOCOL_VERSION))
		} else {
			channel.write(ProtocolRequestPacket(Starbound.NATIVE_PROTOCOL_VERSION))
		}

		channel.flush()
	}

	fun enqueue(task: StarboundClient.() -> Unit) {
		client.mailbox.execute { task.invoke(client) }
	}

	override fun toString(): String {
		val channel = if (hasChannel) channel.remoteAddress().toString() else "<no channel>"
		return "ClientConnection[ID=$connectionID channel=$channel]"
	}

	override fun channelRead(ctx: ChannelHandlerContext, msg: Any) {
		if (msg is IClientPacket) {
			try {
				msg.play(this)
			} catch (err: Throwable) {
				LOGGER.error("Failed to read incoming packet $msg", err)
				disconnect(err.toString())
			}
		} else {
			LOGGER.error("Unknown incoming packet type $msg")
			disconnect("Unknown incoming packet type $msg")
		}
	}

	private var clientStateNetVersion = 0L

	fun tick() {
		if (!pendingDisconnect && isConnected) {
			val entries = rpc.write()

			if (entries != null) {
				channel.write(ClientContextUpdatePacket(entries, KOptional(), KOptional()))
			}

			val (data, new) = client2serverGroup.write(clientStateNetVersion)

			if (data.isNotEmpty())
				channel.write(WorldClientStateUpdatePacket(data))

			clientStateNetVersion = new
		}
	}

	private var pendingDisconnect = false

	fun disconnectNow() {
		pendingDisconnect = false

		if (channel.isOpen) {
			channel.close()
		}
	}

	override fun disconnect(reason: String) {
		if (pendingDisconnect)
			return

		if (channel.isOpen) {
			pendingDisconnect = true
			send(ClientDisconnectRequestPacket)
		} else {
			disconnectNow()
		}
	}

	override fun onChannelClosed() {
		super.onChannelClosed()

		if (pendingDisconnect) {
			disconnectNow()
		}
	}

	fun bootstrap(address: SocketAddress = channel.remoteAddress(), asLegacy: Boolean = false) {
		LOGGER.info("Trying to connect to remote server at $address with ${if (asLegacy) "legacy" else "native"} protocol")

		Bootstrap()
			.group(NIO_POOL)
			.channel(NioSocketChannel::class.java)
			.handler(object : ChannelInitializer<Channel>() { override fun initChannel(ch: Channel) { bind(ch) } })
			.connect(address)
			.syncUninterruptibly()

		sendHello(asLegacy)
	}

	companion object {
		private val LOGGER = LogManager.getLogger()

		fun connectToLocalServer(client: StarboundClient, address: LocalAddress): ClientConnection {
			LOGGER.info("Trying to connect to local server at $address")
			val connection = ClientConnection(client, ConnectionType.MEMORY)

			Bootstrap()
				.group(NIO_POOL)
				.channel(LocalChannel::class.java)
				.handler(object : ChannelInitializer<Channel>() { override fun initChannel(ch: Channel) { connection.bind(ch) } })
				.connect(address)
				.syncUninterruptibly()

			connection.sendHello()

			return connection
		}

		fun connectToLocalServer(client: StarboundClient, address: Channel): ClientConnection {
			return connectToLocalServer(client, address.localAddress() as LocalAddress)
		}

		fun connectToRemoteServer(client: StarboundClient, address: SocketAddress): ClientConnection {
			val connection = ClientConnection(client, ConnectionType.NETWORK)
			connection.bootstrap(address)
			return connection
		}
	}
}