package ru.dbotthepony.kstarbound.world

import it.unimi.dsi.fastutil.objects.ObjectArrayList
import ru.dbotthepony.kommons.arrays.Boolean2DArray
import ru.dbotthepony.kommons.arrays.Object2DArray
import ru.dbotthepony.kommons.util.AABB
import ru.dbotthepony.kommons.util.AABBi
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.network.LegacyNetworkCellState
import ru.dbotthepony.kstarbound.server.world.ServerChunk
import ru.dbotthepony.kstarbound.world.api.AbstractCell
import ru.dbotthepony.kstarbound.world.api.ICellAccess
import ru.dbotthepony.kstarbound.world.api.ImmutableCell
import ru.dbotthepony.kstarbound.world.api.OffsetCellAccess
import ru.dbotthepony.kstarbound.world.api.TileView
import ru.dbotthepony.kstarbound.world.physics.CollisionPoly
import ru.dbotthepony.kstarbound.world.physics.CollisionType
import ru.dbotthepony.kstarbound.world.physics.getBlockPlatforms
import ru.dbotthepony.kstarbound.world.physics.getBlocksMarchingSquares
import java.util.BitSet
import java.util.concurrent.CopyOnWriteArraySet
import kotlin.math.max
import kotlin.math.min

/**
 * Чанк мира
 *
 * Хранит в себе тайлы и ентити внутри себя
 *
 * Считается, что один тайл имеет форму квадрата и сторона квадрата примерно равна полуметру,
 * что будет называться Starbound Unit
 *
 * Весь игровой мир будет измеряться в Starbound Unit'ах
 */
abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType, This>>(
	val world: WorldType,
	val pos: ChunkPos,
) : ICellAccess {
	var changeset = 0
		private set

	var tileChangeset = 0
		private set
	var liquidChangeset = 0
		private set
	var cellChangeset = 0
		private set

	var foregroundChangeset = 0
		private set
	var backgroundChangeset = 0
		private set

	abstract val state: ChunkState

	val width = (world.geometry.size.x - pos.tileX).coerceAtMost(CHUNK_SIZE)
	val height = (world.geometry.size.y - pos.tileY).coerceAtMost(CHUNK_SIZE)

	// local cells' tile access
	val localBackgroundView = TileView.Background(this)
	val localForegroundView = TileView.Foreground(this)

	// relative world cells access (accessing 0, 0 will lookup cell in world, relative to this chunk)
	val worldView = OffsetCellAccess(world, pos.x * CHUNK_SIZE, pos.y * CHUNK_SIZE)
	val worldBackgroundView = TileView.Background(worldView)
	val worldForegroundView = TileView.Foreground(worldView)

	val aabb = AABBi(pos.tile, pos.tile + Vector2i(width, height))
	val aabbd = aabb.toDoubleAABB()

	protected val cells = lazy {
		Object2DArray(width, height, AbstractCell.NULL)
	}

	protected val backgroundHealth = HashMap<Vector2i, TileHealth.Tile>()
	protected val foregroundHealth = HashMap<Vector2i, TileHealth.Tile>()
	protected val collisionCacheDirty = Boolean2DArray.allocate(width, height)
	protected val collisionCache by lazy {
		Object2DArray(width, height) { _, _ -> ObjectArrayList<CollisionPoly>(0) }
	}

	init {
		for (x in 0 until width)
			for (y in 0 until height)
				collisionCacheDirty[x, y] = true
	}

	private var hasDirtyCollisions = false

	// bulk mark collision dirty of neighbour chunks as well as ours
	protected fun signalChunkContentsUpdated() {
		val signalPositions = ArrayList<Vector2i>()

		for (x in 1 .. 2) {
			for (y in 1 .. 2) {
				signalPositions.add(pos.tile + Vector2i(width + x, height + y))
				signalPositions.add(pos.tile + Vector2i(width, height + y))
				signalPositions.add(pos.tile + Vector2i(width + x, height))

				signalPositions.add(pos.tile + Vector2i(-x, -y))
				signalPositions.add(pos.tile + Vector2i(0, -y))
				signalPositions.add(pos.tile + Vector2i(-x, 0))
			}
		}

		for (pos in signalPositions) {
			val actualCellPosition = world.geometry.wrap(pos)
			val chunk = world.chunkMap[world.geometry.chunkFromCell(actualCellPosition)] ?: continue

			chunk.hasDirtyCollisions = true
			chunk.collisionCacheDirty[actualCellPosition.x - chunk.pos.tileX, actualCellPosition.y - chunk.pos.tileY] = true
		}

		hasDirtyCollisions = true
		backgroundHealth.clear()
		foregroundHealth.clear()

		for (x in 0 until width) {
			for (y in 0 until height) {
				collisionCacheDirty[x, y] = true
			}
		}
	}

	private val collisionsLock = Any()

	fun getCollisions(x: Int, y: Int, target: MutableCollection<CollisionPoly>) {
		if (hasDirtyCollisions) {
			synchronized(collisionsLock) {
				if (hasDirtyCollisions) {
					var minX = width
					var minY = height
					var maxX = 0
					var maxY = 0

					for (x in 0 until width) {
						for (y in 0 until height) {
							if (collisionCacheDirty[x, y]) {
								minX = min(minX, x)
								minY = min(minY, y)
								maxX = max(maxX, x)
								maxY = max(maxY, y)
							}
						}
					}

					for (x in minX .. maxX) {
						for (y in minY .. maxY) {
							if (collisionCacheDirty[x, y]) {
								collisionCache[x, y].clear()
								getBlocksMarchingSquares(pos.tileX + x, pos.tileY + y, world.foreground, CollisionType.DYNAMIC, collisionCache[x, y])
								getBlockPlatforms(pos.tileX + x, pos.tileY + y, world.foreground, CollisionType.PLATFORM, collisionCache[x, y])
								collisionCacheDirty[x, y] = false
							}
						}
					}
				}
			}

			hasDirtyCollisions = false
		}

		target.addAll(collisionCache[x, y])
	}

	fun loadCells(source: Object2DArray<out AbstractCell>) {
		val ours = cells.value

		for (x in 0 until width) {
			for (y in 0 until height) {
				ours[x, y] = source[x, y].immutable()
			}
		}

		signalChunkContentsUpdated()
	}

	fun copyCells(): Object2DArray<ImmutableCell> {
		if (cells.isInitialized()) {
			val cells = cells.value
			return Object2DArray(CHUNK_SIZE, CHUNK_SIZE) { x, y -> cells[x, y] }
		} else {
			return Object2DArray(CHUNK_SIZE, CHUNK_SIZE, AbstractCell.NULL)
		}
	}

	override fun getCell(x: Int, y: Int): AbstractCell {
		return if (cells.isInitialized()) cells.value[x, y] else AbstractCell.NULL
	}

	final override fun setCell(x: Int, y: Int, cell: AbstractCell): Boolean {
		val old = if (cells.isInitialized()) cells.value[x, y] else AbstractCell.NULL
		val new = cell.immutable()

		if (old != new) {
			cells.value[x, y] = new
			hasDirtyCollisions = true
			collisionCacheDirty[x, y] = true

			for (xoff in -2 .. 2) {
				for (yoff in -2 .. 2) {
					val actualCellPosition = world.geometry.wrap(pos.tile + Vector2i(x + xoff, y + yoff))
					val chunk = world.chunkMap[world.geometry.chunkFromCell(actualCellPosition)] ?: continue

					chunk.hasDirtyCollisions = true
					chunk.collisionCacheDirty[actualCellPosition.x - chunk.pos.tileX, actualCellPosition.y - chunk.pos.tileY] = true
				}
			}

			if (old.foreground != new.foreground) {
				foregroundHealth.remove(Vector2i(x, y))
				foregroundChanges(x, y, new)
			}

			if (old.background != new.background) {
				backgroundHealth.remove(Vector2i(x, y))
				backgroundChanges(x, y, new)
			}

			if (old.liquid != new.liquid) {
				liquidChanges(x, y, new)
			}

			cellChanges(x, y, new)
		}

		return true
	}

	protected open fun foregroundChanges(x: Int, y: Int, cell: ImmutableCell) {
		tileChangeset++
		foregroundChangeset++
	}

	protected open fun backgroundChanges(x: Int, y: Int, cell: ImmutableCell) {
		tileChangeset++
		backgroundChangeset++
	}

	protected open fun liquidChanges(x: Int, y: Int, cell: ImmutableCell) {
		liquidChangeset++
	}

	protected open fun cellChanges(x: Int, y: Int, cell: ImmutableCell) {
		changeset++
		cellChangeset++
	}

	protected inline fun forEachNeighbour(block: (This) -> Unit) {
		world.chunkMap[pos.left]?.let(block)
		world.chunkMap[pos.right]?.let(block)
		world.chunkMap[pos.top]?.let(block)
		world.chunkMap[pos.bottom]?.let(block)
		world.chunkMap[pos.topLeft]?.let(block)
		world.chunkMap[pos.topRight]?.let(block)
		world.chunkMap[pos.bottomLeft]?.let(block)
		world.chunkMap[pos.bottomRight]?.let(block)
	}

	override fun toString(): String {
		return "${this::class.simpleName}(pos=$pos, world=$world)"
	}

	open fun remove() {

	}

	open fun tick(delta: Double) {

	}
}
