package ru.dbotthepony.kstarbound.world

import com.google.gson.JsonElement
import com.google.gson.JsonObject
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap
import it.unimi.dsi.fastutil.ints.IntArraySet
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap
import it.unimi.dsi.fastutil.objects.ObjectArrayList
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.arrays.Object2DArray
import ru.dbotthepony.kommons.collect.filterNotNull
import ru.dbotthepony.kommons.gson.set
import ru.dbotthepony.kommons.util.IStruct2d
import ru.dbotthepony.kommons.util.IStruct2i
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.Starbound
import ru.dbotthepony.kstarbound.defs.world.WorldStructure
import ru.dbotthepony.kstarbound.defs.world.WorldTemplate
import ru.dbotthepony.kstarbound.json.mergeJson
import ru.dbotthepony.kstarbound.math.*
import ru.dbotthepony.kstarbound.network.IPacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.SetPlayerStartPacket
import ru.dbotthepony.kstarbound.util.BlockableEventLoop
import ru.dbotthepony.kstarbound.util.ParallelPerform
import ru.dbotthepony.kstarbound.util.random.random
import ru.dbotthepony.kstarbound.world.api.ICellAccess
import ru.dbotthepony.kstarbound.world.api.AbstractCell
import ru.dbotthepony.kstarbound.world.api.TileView
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
import ru.dbotthepony.kstarbound.world.entities.DynamicEntity
import ru.dbotthepony.kstarbound.world.entities.MovementController
import ru.dbotthepony.kstarbound.world.entities.tile.TileEntity
import ru.dbotthepony.kstarbound.world.physics.CollisionPoly
import ru.dbotthepony.kstarbound.world.physics.CollisionType
import ru.dbotthepony.kstarbound.world.physics.Poly
import java.util.concurrent.CompletableFuture
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.locks.ReentrantLock
import java.util.function.Predicate
import java.util.random.RandomGenerator
import java.util.stream.Stream
import kotlin.math.roundToInt

abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, ChunkType>>(val template: WorldTemplate) : ICellAccess {
	val background = TileView.Background(this)
	val foreground = TileView.Foreground(this)
	val sky = Sky(template.skyParameters)
	val geometry: WorldGeometry = template.geometry

	val nextEntityID = AtomicInteger()

	override fun getCell(x: Int, y: Int): AbstractCell {
		return chunkMap.getCell(x, y)
	}

	override fun setCell(x: Int, y: Int, cell: AbstractCell): Boolean {
		return chunkMap.setCell(x, y, cell, ChunkState.EMPTY)
	}

	override fun setCell(x: Int, y: Int, cell: AbstractCell, chunkState: ChunkState): Boolean {
		return chunkMap.setCell(x, y, cell, chunkState)
	}

	protected open fun onChunkCreated(chunk: ChunkType) { }
	protected open fun onChunkRemoved(chunk: ChunkType) { }

	abstract inner class ChunkMap {
		abstract operator fun get(x: Int, y: Int): ChunkType?
		abstract fun compute(x: Int, y: Int): ChunkType?
		abstract fun chunks(): List<ChunkType>
		abstract fun remove(x: Int, y: Int)

		operator fun get(pos: ChunkPos) = get(pos.x, pos.y)

		fun compute(pos: ChunkPos) = compute(pos.x, pos.y)
		fun remove(pos: ChunkPos) = remove(pos.x, pos.y)

		fun getCell(x: Int, y: Int): AbstractCell {
			val ix = geometry.x.cell(x)
			val iy = geometry.y.cell(y)
			val chunk = get(geometry.x.chunkFromCell(ix), geometry.y.chunkFromCell(iy)) ?: return AbstractCell.NULL
			return chunk.getCell(ix and CHUNK_SIZE_MASK, iy and CHUNK_SIZE_MASK)
		}

		fun getCellDirect(ix: Int, iy: Int): AbstractCell {
			val chunk = get(geometry.x.chunkFromCell(ix), geometry.y.chunkFromCell(iy)) ?: return AbstractCell.NULL
			return chunk.getCell(ix and CHUNK_SIZE_MASK, iy and CHUNK_SIZE_MASK)
		}

		fun setCell(x: Int, y: Int, cell: AbstractCell, state: ChunkState): Boolean {
			val ix = geometry.x.cell(x)
			val iy = geometry.y.cell(y)
			val chunk = get(geometry.x.chunkFromCell(ix), geometry.y.chunkFromCell(iy)) ?: return false
			return chunk.setCell(ix and CHUNK_SIZE_MASK, iy and CHUNK_SIZE_MASK, cell, state)
		}

		fun getCell(position: IStruct2i) = getCell(position.component1(), position.component2())
		fun setCell(position: IStruct2i, cell: AbstractCell) = setCell(position.component1(), position.component2(), cell)

		abstract val size: Int
	}

	// around 30% slower than ArrayChunkMap, but can support insanely large worlds
	inner class SparseChunkMap : ChunkMap() {
		private val map = Long2ObjectOpenHashMap<ChunkType>()
		// see CONCURRENT_SPARSE_CHUNK_MAP
		private val lock = Any()
		private val list = CopyOnWriteArrayList<ChunkType>()

		override fun get(x: Int, y: Int): ChunkType? {
			if (!geometry.x.inBoundsChunk(x) || !geometry.y.inBoundsChunk(y)) return null

			if (CONCURRENT_SPARSE_CHUNK_MAP) {
				synchronized(lock) {
					return map[ChunkPos.toLong(x, y)]
				}
			} else {
				return map[ChunkPos.toLong(x, y)]
			}
		}

		override fun compute(x: Int, y: Int): ChunkType? {
			if (!geometry.x.inBoundsChunk(x) || !geometry.y.inBoundsChunk(y)) return null
			val index = ChunkPos.toLong(x, y)

			if (CONCURRENT_SPARSE_CHUNK_MAP) {
				synchronized(lock) {
					return map[index] ?: chunkFactory(ChunkPos(x, y)).also {
						list.add(it)
						map[index] = it
						onChunkCreated(it)
					}
				}
			} else {
				return map[index] ?: chunkFactory(ChunkPos(x, y)).also {
					list.add(it)
					map[index] = it
					onChunkCreated(it)
				}
			}
		}

		override fun remove(x: Int, y: Int) {
			val index = ChunkPos.toLong(geometry.x.chunk(x), geometry.y.chunk(y))

			if (CONCURRENT_SPARSE_CHUNK_MAP) {
				synchronized(lock) {
					val chunk = map.get(index)

					if (chunk != null) {
						chunk.remove()
						onChunkRemoved(chunk)
						list.add(chunk)
						map.remove(index)
					}
				}
			} else {
				val chunk = map.get(index)

				if (chunk != null) {
					chunk.remove()
					onChunkRemoved(chunk)
					list.add(chunk)
					map.remove(index)
				}
			}
		}

		override fun chunks(): List<ChunkType> {
			return list
		}

		override val size: Int
			get() = map.size
	}

	inner class ArrayChunkMap : ChunkMap() {
		private val map = Object2DArray.nulls<ChunkType>(divideUp(geometry.size.x, CHUNK_SIZE), divideUp(geometry.size.y, CHUNK_SIZE))
		private val list = CopyOnWriteArrayList<ChunkType>()

		override fun compute(x: Int, y: Int): ChunkType? {
			if (!geometry.x.inBoundsChunk(x) || !geometry.y.inBoundsChunk(y)) return null
			return map[x, y] ?: chunkFactory(ChunkPos(x, y)).also {
				list.add(it)
				map[x, y] = it
				onChunkCreated(it)
			}
		}

		override fun get(x: Int, y: Int): ChunkType? {
			if (!geometry.x.inBoundsChunk(x) || !geometry.y.inBoundsChunk(y)) return null
			return map[x, y]
		}

		@Suppress("NAME_SHADOWING")
		override fun remove(x: Int, y: Int) {
			val x = geometry.x.chunk(x)
			val y = geometry.y.chunk(y)
			val chunk = map[x, y]

			if (chunk != null) {
				chunk.remove()
				onChunkRemoved(chunk)
				list.remove(chunk)
				map[x, y] = null
			}
		}

		override fun chunks(): List<ChunkType> {
			return list
		}

		override val size: Int
			get() = list.size
	}

	val chunkMap: ChunkMap = if (geometry.size.x <= 32000 && geometry.size.y <= 32000) ArrayChunkMap() else SparseChunkMap()

	/**
	 * Random generator for in-world events
 	 */
	val random: RandomGenerator = random()

	abstract val isClient: Boolean
	val isServer: Boolean
		get() = !isClient

	// generic lock
	val lock = ReentrantLock()

	val entities = Int2ObjectOpenHashMap<AbstractEntity>()
	val entityList = CopyOnWriteArrayList<AbstractEntity>()
	val entityIndex = EntityIndex(geometry)
	val dynamicEntities = ArrayList<DynamicEntity>()

	var playerSpawnPosition = Vector2d.ZERO
		protected set

	var respawnInWorld = false
		protected set

	var adjustPlayerSpawn = false

	var ticks = 0L
		private set

	abstract val eventLoop: BlockableEventLoop

	open fun broadcast(packet: IPacket) {

	}

	var centralStructure: WorldStructure = WorldStructure()

	val protectedDungeonIDs = IntArraySet()
	protected val worldProperties = JsonObject()

	fun copyProperties(): JsonObject = worldProperties.deepCopy()

	fun updateProperties(properties: JsonObject) {
		mergeJson(worldProperties, properties)
	}

	protected open fun setProperty0(key: String, value: JsonElement) {

	}

	fun setProperty(key: String, value: JsonElement) {
		if (worldProperties[key] == value)
			return

		val copy = value.deepCopy()
		worldProperties[key] = copy
		setProperty0(key, copy)
	}

	open fun setPlayerSpawn(position: Vector2d, respawnInWorld: Boolean) {
		playerSpawnPosition = position
		this.respawnInWorld = respawnInWorld
		broadcast(SetPlayerStartPacket(position, respawnInWorld))
	}

	open fun tick(delta: Double) {
		ticks++

		if (dynamicEntities.size < 128) {
			dynamicEntities.forEach {
				if (!it.isRemote) {
					it.movement.move(delta)
				}
			}
		} else {
			val tasks = ArrayList<CompletableFuture<*>>()
			var batch = ArrayList<MovementController>()

			for (entity in dynamicEntities) {
				batch.add(entity.movement)

				if (batch.size == 32) {
					val b = batch
					tasks.add(CompletableFuture.runAsync(Runnable { b.forEach { it.move(delta) } }, Starbound.EXECUTOR))
					batch = ArrayList()
				}
			}

			if (batch.isNotEmpty()) {
				tasks.add(CompletableFuture.runAsync(Runnable { batch.forEach { it.move(delta) } }, Starbound.EXECUTOR))
			}

			CompletableFuture.allOf(*tasks.toTypedArray()).join()
		}

		entityList.forEach {
			try {
				if (it.isInWorld) // entities might remove other entities during tick
					it.tick(delta)
			} catch (err: Throwable) {
				if (it.isRemote && isServer) {
					LOGGER.error("Exception ticking client spawned entity $it, removing", err)
					it.remove(AbstractEntity.RemovalReason.REMOVED)
				} else {
					LOGGER.error("Exception ticking entity $it", err)
				}
			}
		}

		for (chunk in chunkMap.chunks())
			chunk.tick(delta)

		sky.tick(delta)
	}

	protected abstract fun chunkFactory(pos: ChunkPos): ChunkType

	fun entitiesAtTile(pos: Vector2i, filter: Predicate<TileEntity> = Predicate { true }): List<TileEntity> {
		return entityIndex.query(
			AABBi(pos, pos + Vector2i.POSITIVE_XY).toDoubleAABB(),
			filter = { it is TileEntity && pos in it.occupySpaces && filter.test(it) }
		) as List<TileEntity>
	}

	fun interface CellPredicate {
		fun test(x: Int, y: Int, cell: AbstractCell): Boolean
	}

	fun anyCellSatisfies(aabb: AABBi, predicate: CellPredicate): Boolean {
		for (x in aabb.mins.x .. aabb.maxs.x) {
			for (y in aabb.mins.x .. aabb.maxs.x) {
				val ix = geometry.x.cell(x)
				val iy = geometry.x.cell(y)


				if (predicate.test(ix, iy, chunkMap.getCellDirect(ix, iy))) {
					return true
				}
			}
		}

		return false
	}

	fun anyCellSatisfies(aabb: AABB, predicate: CellPredicate): Boolean {
		for (x in aabb.mins.x.toInt() .. aabb.maxs.x.roundToInt()) {
			for (y in aabb.mins.y.toInt() .. aabb.maxs.y.roundToInt()) {
				val ix = geometry.x.cell(x)
				val iy = geometry.x.cell(y)

				if (predicate.test(ix, iy, chunkMap.getCellDirect(ix, iy))) {
					return true
				}
			}
		}

		return false
	}

	fun anyCellSatisfies(x: Int, y: Int, distance: Int, predicate: CellPredicate): Boolean {
		for (tx in x - distance .. x + distance) {
			for (ty in y - distance .. y + distance) {
				val ix = geometry.x.cell(tx)
				val iy = geometry.x.cell(ty)

				if (predicate.test(ix, iy, chunkMap.getCellDirect(ix, iy))) {
					return true
				}
			}
		}

		return false
	}

	fun queryTileCollisions(aabb: AABB): MutableList<CollisionPoly> {
		val result = ObjectArrayList<CollisionPoly>() // no CME checks
		val tiles = aabb.encasingIntAABB()

		for (x in tiles.mins.x .. tiles.maxs.x) {
			for (y in tiles.mins.y .. tiles.maxs.y) {
				val cx = geometry.x.cell(x)
				val cy = geometry.y.cell(y)

				val chunk = chunkMap[geometry.x.chunkFromCell(cx), geometry.y.chunkFromCell(cy)] ?: continue
				chunk.getCollisions(cx - chunk.pos.tileX, cy - chunk.pos.tileY, result)
			}
		}

		return result
	}

	fun collide(with: Poly, filter: Predicate<CollisionPoly>): Stream<Poly.Penetration> {
		return queryTileCollisions(with.aabb.enlarge(1.0, 1.0)).stream()
			.filter(filter)
			.map { with.intersect(it.poly) }
			.filterNotNull()
	}

	fun polyIntersects(with: Poly, filter: Predicate<CollisionPoly> = Predicate { true }, tolerance: Double = 0.0): Boolean {
		return collide(with, filter).anyMatch { it.penetration >= tolerance }
	}

	fun polyIntersects(with: Poly, filter: Collection<CollisionType>, tolerance: Double = 0.0): Boolean {
		return polyIntersects(with, Predicate { it.type in filter }, tolerance)
	}

	fun gravityAt(pos: IStruct2i): Vector2d {
		return template.worldParameters?.gravity ?: Vector2d.ZERO
	}

	fun gravityAt(pos: IStruct2d): Vector2d {
		return template.worldParameters?.gravity ?: Vector2d.ZERO
	}

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

		/**
		 * Employ locking since concurrent reads can
		 * cause random exceptions in event of concurrent write
		 * (due to hash table resize)
		 *
		 * This obviously seriously impacts performance,
		 * but there is no other way unless we sacrifice concurrent
		 * chunk generation
		 *
		 * On other hand, currently, terrain is the real performance hog
		 * of world generation, everything else is kinda okay to be performed
		 * on main world thread, so concurrent access is not needed for now.
		 *
		 * ArrayChunkMap does ~not need~ needs synchronization too, unless we use CopyOnWriteArrayList
		 * for "existing" chunks list.
		 */
		private const val CONCURRENT_SPARSE_CHUNK_MAP = false
	}
}
