package ru.dbotthepony.kstarbound.world

import ru.dbotthepony.kommons.util.AABB
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.defs.tile.ARTIFICIAL_DUNGEON_ID
import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition
import ru.dbotthepony.kstarbound.defs.tile.TileDefinition
import ru.dbotthepony.kstarbound.defs.tile.TileModifierDefinition
import ru.dbotthepony.kstarbound.defs.tile.isEmptyModifier
import ru.dbotthepony.kstarbound.defs.tile.isEmptyTile
import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyLiquid
import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyModifier
import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyTile
import ru.dbotthepony.kstarbound.defs.tile.orEmptyModifier
import ru.dbotthepony.kstarbound.defs.tile.supportsModifier
import ru.dbotthepony.kstarbound.io.readNullable
import ru.dbotthepony.kstarbound.io.readNullableFloat
import ru.dbotthepony.kstarbound.server.world.ServerWorld
import ru.dbotthepony.kstarbound.world.api.TileColor
import ru.dbotthepony.kstarbound.world.entities.DynamicEntity
import ru.dbotthepony.kstarbound.world.entities.tile.TileEntity
import java.io.DataInputStream
import java.io.DataOutputStream
import java.util.function.Predicate

sealed class TileModification {
	object Invalid : TileModification() {
		override fun write(stream: DataOutputStream, isLegacy: Boolean) {
			stream.writeByte(0)
		}

		override fun apply(
			world: World<*, *>,
			position: Vector2i,
			allowEntityOverlap: Boolean,
		) {
			// do nothing
		}

		override fun allowed(
			world: World<*, *>,
			position: Vector2i,
			allowEntityOverlap: Boolean,
			perhaps: Boolean
		): Boolean {
			return true
		}
	}

	abstract fun write(stream: DataOutputStream, isLegacy: Boolean)
	abstract fun allowed(world: World<*, *>, position: Vector2i, allowEntityOverlap: Boolean, perhaps: Boolean = false): Boolean
	abstract fun apply(world: World<*, *>, position: Vector2i, allowEntityOverlap: Boolean)

	data class PlaceMaterial(val isBackground: Boolean, val material: Registry.Ref<TileDefinition>, val hueShift: Float?) : TileModification() {
		constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readBoolean(), if (isLegacy) Registries.tiles.ref(stream.readUnsignedShort()) else TODO(), if (isLegacy) stream.readNullable { stream.readUnsignedByte() / 255f * 360f } else stream.readNullableFloat())

		override fun write(stream: DataOutputStream, isLegacy: Boolean) {
			stream.writeByte(1)
			stream.writeBoolean(isBackground)

			if (isLegacy) {
				val id = material.key.right.orNull() ?: material.entry?.id ?: throw IllegalStateException("Can't network $material over legacy protocol because it has no pre-defined ID")

				if (id !in 0 .. 65535) {
					throw IllegalStateException("Can't network $material over legacy protocol because its pre-defined ID is bigger than uint16_t")
				}

				stream.writeShort(id)

				stream.writeBoolean(hueShift != null)

				if (hueShift != null) {
					stream.writeByte((hueShift * 360f / 255f).toInt())
				}
			} else {
				// registries name->id mapping should be networked on join
				TODO()

				// stream.writeNullableFloat(hueShift)
			}
		}

		override fun allowed(
			world: World<*, *>,
			position: Vector2i,
			allowEntityOverlap: Boolean,
			perhaps: Boolean
		): Boolean {
			if (material.isEmptyTile || material.value!!.isMeta)
				return false

			if (isBackground && material.value!!.foregroundOnly)
				return false

			val (x, y) = position
			val cell = world.getCell(x, y)
			val tile = cell.tile(isBackground)

			if (tile.material.isNotEmptyTile)
				return false

			if (!isBackground) {
				val rect = AABB(Vector2d(x.toDouble(), y.toDouble()), Vector2d(x + 1.0, y + 1.0))

				if (world.entityIndex.any(rect, Predicate { it is TileEntity && position in it.occupySpaces })) {
					return false
				}

				if (!allowEntityOverlap && world.entityIndex.any(rect, Predicate { it.collisionArea.intersect(rect) })) {
					return false
				}
			}

			return perhaps ||
				world.geometry.x.cell(x - 1) == x ||
				world.geometry.y.cell(x + 1) == x ||
				world.geometry.y.cell(y - 1) == y ||
				world.geometry.y.cell(y + 1) == y ||
				world.anyCellSatisfies(x, y, 1) { tx, ty, tcell ->
					tx != x && ty != y && (tcell.foreground.material.value.isConnectable || tcell.background.material.value.isConnectable)
				}
		}

		override fun apply(
			world: World<*, *>,
			position: Vector2i,
			allowEntityOverlap: Boolean,
		) {
			val material = material.entry!!
			val cell = world.getCell(position).mutable()
			val tile = cell.tile(isBackground)
			tile.material = material
			tile.hueShift = hueShift ?: world.template.cellInfo(position).blockBiome?.hueShift(material) ?: 0f
			tile.color = TileColor.DEFAULT

			if (material.isEmptyTile) {
				// remove modifier if removing tile
				tile.modifier = BuiltinMetaMaterials.EMPTY_MOD
				tile.modifierHueShift = 0f
			} else if (isBackground && cell.liquid.isInfinite) {
				cell.liquid.isInfinite = false
				cell.liquid.pressure = 1f
			} else if (!isBackground && material.value.blocksLiquidFlow) {
				cell.liquid.reset()
			}

			cell.dungeonId = ARTIFICIAL_DUNGEON_ID
			world.setCell(position, cell.immutable())
		}
	}

	data class PlaceModifier(val isBackground: Boolean, val modifier: Registry.Ref<TileModifierDefinition>, val hueShift: Float?) : TileModification() {
		constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readBoolean(), if (isLegacy) Registries.tileModifiers.ref(stream.readUnsignedShort()) else TODO(), if (isLegacy) stream.readNullable { stream.readUnsignedByte() / 255f * 360f } else stream.readNullableFloat())

		override fun write(stream: DataOutputStream, isLegacy: Boolean) {
			stream.writeByte(2)
			stream.writeBoolean(isBackground)

			if (isLegacy) {
				val id = modifier.key.right.orNull() ?: modifier.entry?.id ?: throw IllegalStateException("Can't network $modifier over legacy protocol because it has no pre-defined ID")

				if (id !in 0 .. 65535) {
					throw IllegalStateException("Can't network $modifier over legacy protocol because its pre-defined ID is bigger than uint16_t")
				}

				stream.writeShort(id)

				stream.writeBoolean(hueShift != null)

				if (hueShift != null) {
					stream.writeByte((hueShift * 360f / 255f).toInt())
				}
			} else {
				// registries name->id mapping should be networked on join
				TODO()

				// stream.writeNullableFloat(hueShift)
			}
		}

		override fun allowed(
			world: World<*, *>,
			position: Vector2i,
			allowEntityOverlap: Boolean,
			perhaps: Boolean
		): Boolean {
			val cell = world.getCell(position)
			val tile = cell.tile(isBackground)
			return modifier.isNotEmptyModifier &&
				!modifier.value!!.isMeta &&
				tile.modifier.isEmptyModifier &&
				tile.material.supportsModifier(modifier)
		}

		override fun apply(world: World<*, *>, position: Vector2i, allowEntityOverlap: Boolean) {
			val cell = world.getCell(position).mutable()
			val tile = cell.tile(isBackground)
			tile.modifier = modifier.orEmptyModifier
			world.setCell(position, cell)
		}
	}

	data class Paint(val isBackground: Boolean, val color: TileColor) : TileModification() {
		constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readBoolean(), TileColor.entries[stream.readUnsignedByte()])

		override fun write(stream: DataOutputStream, isLegacy: Boolean) {
			stream.writeByte(3)
			stream.writeBoolean(isBackground)
			stream.writeByte(color.ordinal)
		}

		override fun allowed(
			world: World<*, *>,
			position: Vector2i,
			allowEntityOverlap: Boolean,
			perhaps: Boolean
		): Boolean {
			val tile = world.getCell(position).tile(isBackground)
			val material = tile.material.value
			return tile.hueShift != 0f || tile.color != this.color && material.renderParameters.multiColored
		}

		override fun apply(world: World<*, *>, position: Vector2i, allowEntityOverlap: Boolean) {
			val cell = world.getCell(position).mutable()
			val tile = cell.tile(isBackground)
			tile.hueShift = 0f
			tile.color = this.color
			world.setCell(position, cell)
		}
	}

	data class Pour(val state: Registry.Ref<LiquidDefinition>, val level: Float) : TileModification() {
		constructor(stream: DataInputStream, isLegacy: Boolean) : this(if (isLegacy) Registries.liquid.ref(stream.readUnsignedByte()) else TODO(), stream.readFloat())

		override fun write(stream: DataOutputStream, isLegacy: Boolean) {
			stream.writeByte(4)

			if (isLegacy) {
				val id = state.key.right.orNull() ?: state.entry?.id ?: throw IllegalStateException("Can't network $state over legacy protocol because it has no pre-defined ID")

				if (id !in 0..255) {
					throw IllegalStateException("Can't network $state over legacy protocol because its pre-defined ID is bigger than uint8_t")
				}

				stream.writeByte(id)
			} else {
				// registries name->id mapping should be networked on join
				TODO()
			}

			stream.writeFloat(level)
		}

		override fun allowed(
			world: World<*, *>,
			position: Vector2i,
			allowEntityOverlap: Boolean,
			perhaps: Boolean
		): Boolean {
			if (state.isEmpty)
				return false

			val cell = world.getCell(position)

			if (cell.liquid.state.isNotEmptyLiquid && cell.liquid.isInfinite)
				return false // it makes no sense to try to pour liquid into infinite source

			if (cell.liquid.state.isNotEmptyLiquid && cell.liquid.state != state.entry)
				return false // it makes also makes no sense to magically replace liquid what is already there

			// while checks above makes vanilla client look stupid when it tries to pour liquids into other
			// liquids, we must think better than vanilla client.

			return !cell.foreground.material.value.collisionKind.isSolidCollision
		}

		override fun apply(world: World<*, *>, position: Vector2i, allowEntityOverlap: Boolean) {
			if (state.isEmpty)
				return

			val state = state.entry!!
			val cell = world.getCell(position).mutable()

			if (cell.liquid.state == state) {
				cell.liquid.level += level
			} else {
				cell.liquid.reset()
				cell.liquid.state = state
				cell.liquid.level = level
				cell.liquid.pressure = 1f
			}

			world.setCell(position, cell)
		}
	}

	companion object {
		fun read(stream: DataInputStream, isLegacy: Boolean): TileModification {
			return when (val type = stream.readUnsignedByte()) {
				0 -> Invalid
				1 -> PlaceMaterial(stream, isLegacy)
				2 -> PlaceModifier(stream, isLegacy)
				3 -> Paint(stream, isLegacy)
				4 -> Pour(stream, isLegacy)
				else -> throw IllegalArgumentException("Unknown tile modification type $type!")
			}
		}
	}
}
