package ru.dbotthepony.kstarbound.world

import it.unimi.dsi.fastutil.objects.ObjectArrayList
import it.unimi.dsi.fastutil.objects.ObjectArraySet
import ru.dbotthepony.kommons.io.readVector2i
import ru.dbotthepony.kommons.io.writeStruct2i
import ru.dbotthepony.kommons.util.AABB
import ru.dbotthepony.kommons.util.AABBi
import ru.dbotthepony.kommons.util.IStruct2d
import ru.dbotthepony.kommons.util.IStruct2f
import ru.dbotthepony.kommons.util.IStruct2i
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.world.physics.Poly
import java.io.DataInputStream
import java.io.DataOutputStream

@JsonFactory
data class WorldGeometry(val size: Vector2i, val loopX: Boolean = true, val loopY: Boolean = false) {
	constructor(buff: DataInputStream) : this(buff.readVector2i(), buff.readBoolean(), buff.readBoolean())

	init {
		require(size.x > 0) { "Invalid world width: ${size.x}" }
		require(size.y > 0) { "Invalid world height: ${size.y}" }
	}

	val x: CoordinateMapper = if (loopX) CoordinateMapper.Wrapper(size.x) else CoordinateMapper.Clamper(size.x)
	val y: CoordinateMapper = if (loopY) CoordinateMapper.Wrapper(size.y) else CoordinateMapper.Clamper(size.y)

	fun write(buff: DataOutputStream) {
		buff.writeStruct2i(size)
		buff.writeBoolean(loopX)
		buff.writeBoolean(loopY)
	}

	fun wrap(pos: IStruct2i): Vector2i {
		return Vector2i(x.cell(pos.component1()), y.cell(pos.component2()))
	}

	fun wrap(pos: IStruct2d): Vector2d {
		return Vector2d(x.cell(pos.component1()), y.cell(pos.component2()))
	}

	fun chunkFromCell(pos: IStruct2i): ChunkPos {
		return ChunkPos(x.chunkFromCell(pos.component1()), y.chunkFromCell(pos.component2()))
	}

	fun chunkFromCell(x: Int, y: Int): ChunkPos {
		return ChunkPos(this.x.chunkFromCell(x), this.y.chunkFromCell(y))
	}

	fun chunkFromCell(pos: IStruct2f): ChunkPos {
		return ChunkPos(x.chunkFromCell(pos.component1()), y.chunkFromCell(pos.component2()))
	}

	fun chunkFromCell(pos: IStruct2d): ChunkPos {
		return ChunkPos(x.chunkFromCell(pos.component1()), y.chunkFromCell(pos.component2()))
	}

	fun inBoundsCell(pos: IStruct2i): Boolean {
		return x.inBoundsCell(pos.component1()) && y.inBoundsCell(pos.component2())
	}

	fun inBoundsCell(x: Int, y: Int): Boolean {
		return this.x.inBoundsCell(x) && this.y.inBoundsCell(y)
	}

	fun wrap(pos: ChunkPos): ChunkPos {
		val x = this.x.chunk(pos.x)
		val y = this.y.chunk(pos.y)
		if (x == pos.x && y == pos.y) return pos
		return ChunkPos(x, y)
	}

	fun wrapToLong(pos: ChunkPos): Long {
		val x = this.x.chunk(pos.x)
		val y = this.y.chunk(pos.y)
		if (x == pos.x && y == pos.y) return pos.toLong()
		return ChunkPos.toLong(x, y)
	}

	// AABB + offset; offset allows to reconstruct original coordinate
	// as if it weren't wrapped around world edge
	fun split(region: AABBi): Pair<List<AABBi>, Vector2i> {
		val splitX = x.split(region.mins.x, region.maxs.x)
		val splitY = y.split(region.mins.y, region.maxs.y)
		val offset = Vector2i(splitX.offset, splitY.offset)

		if (splitX.rangeB == null && splitY.rangeB == null) {
			// confined
			return listOf(AABBi(Vector2i(splitX.rangeA.x, splitY.rangeA.x), Vector2i(splitX.rangeA.y, splitY.rangeA.y))) to offset
		} else if (splitX.rangeB != null && splitY.rangeB == null) {
			// wrapped around X axis
			val a = AABBi(Vector2i(splitX.rangeA.x, splitY.rangeA.x), Vector2i(splitX.rangeA.y, splitY.rangeA.y))
			val b = AABBi(Vector2i(splitX.rangeB.x, splitY.rangeA.x), Vector2i(splitX.rangeB.y, splitY.rangeA.y))
			return listOf(a, b) to offset
		} else if (splitX.rangeB == null && splitY.rangeB != null) {
			// wrapped around Y axis
			val a = AABBi(Vector2i(splitX.rangeA.x, splitY.rangeA.x), Vector2i(splitX.rangeA.y, splitY.rangeA.y))
			val b = AABBi(Vector2i(splitX.rangeA.x, splitY.rangeB.x), Vector2i(splitX.rangeA.y, splitY.rangeB.y))
			return listOf(a, b) to offset
		} else {
			// wrapped around X and Y axis
			splitX.rangeB!!
			splitY.rangeB!!

			val a = AABBi(Vector2i(splitX.rangeA.x, splitY.rangeA.x), Vector2i(splitX.rangeA.y, splitY.rangeA.y))
			val b = AABBi(Vector2i(splitX.rangeB.x, splitY.rangeA.x), Vector2i(splitX.rangeB.y, splitY.rangeA.y))
			val c = AABBi(Vector2i(splitX.rangeA.x, splitY.rangeB.x), Vector2i(splitX.rangeA.y, splitY.rangeB.y))
			val d = AABBi(Vector2i(splitX.rangeB.x, splitY.rangeB.x), Vector2i(splitX.rangeB.y, splitY.rangeB.y))
			return listOf(a, b, c, d) to offset
		}
	}

	/**
	 * TODO: While for entities it is completely legal to touch world border
	 * (e.g. have coordinate equal to world's size), it is not for tiles
	 *
	 * Currently, this splits rectangles with tile grid's point of view,
	 * where coordinate at exactly world border is treated as "wrap around"
	 */
	fun split(region: AABB): Pair<List<AABB>, Vector2d> {
		val splitX = x.split(region.mins.x, region.maxs.x)
		val splitY = y.split(region.mins.y, region.maxs.y)
		val offset = Vector2d(splitX.offset, splitY.offset)

		if (splitX.rangeB == null && splitY.rangeB == null) {
			// confined
			return listOf(AABB(Vector2d(splitX.rangeA.x, splitY.rangeA.x), Vector2d(splitX.rangeA.y, splitY.rangeA.y))) to offset
		} else if (splitX.rangeB != null && splitY.rangeB == null) {
			// wrapped around X axis
			val a = AABB(Vector2d(splitX.rangeA.x, splitY.rangeA.x), Vector2d(splitX.rangeA.y, splitY.rangeA.y))
			val b = AABB(Vector2d(splitX.rangeB.x, splitY.rangeA.x), Vector2d(splitX.rangeB.y, splitY.rangeA.y))
			return listOf(a, b) to offset
		} else if (splitX.rangeB == null && splitY.rangeB != null) {
			// wrapped around Y axis
			val a = AABB(Vector2d(splitX.rangeA.x, splitY.rangeA.x), Vector2d(splitX.rangeA.y, splitY.rangeA.y))
			val b = AABB(Vector2d(splitX.rangeA.x, splitY.rangeB.x), Vector2d(splitX.rangeA.y, splitY.rangeB.y))
			return listOf(a, b) to offset
		} else {
			// wrapped around X and Y axis
			splitX.rangeB!!
			splitY.rangeB!!

			val a = AABB(Vector2d(splitX.rangeA.x, splitY.rangeA.x), Vector2d(splitX.rangeA.y, splitY.rangeA.y))
			val b = AABB(Vector2d(splitX.rangeB.x, splitY.rangeA.x), Vector2d(splitX.rangeB.y, splitY.rangeA.y))
			val c = AABB(Vector2d(splitX.rangeA.x, splitY.rangeB.x), Vector2d(splitX.rangeA.y, splitY.rangeB.y))
			val d = AABB(Vector2d(splitX.rangeB.x, splitY.rangeB.x), Vector2d(splitX.rangeB.y, splitY.rangeB.y))
			return listOf(a, b, c, d) to offset
		}
	}

	fun region2Chunks(region: AABBi): Set<ChunkPos> {
		if (region.mins == region.maxs)
			return emptySet()

		val result = ObjectArrayList<ChunkPos>()

		for (actualRegion in split(region).first) {
			val xMin = this.x.chunkFromCell(actualRegion.mins.x)
			val xMax = this.x.chunkFromCell(actualRegion.maxs.x)

			val yMin = this.y.chunkFromCell(actualRegion.mins.y)
			val yMax = this.y.chunkFromCell(actualRegion.maxs.y)

			for (x in xMin .. xMax) {
				for (y in yMin .. yMax) {
					result.add(ChunkPos(x, y))
				}
			}
		}

		result.sort()

		val itr = result.iterator()
		var previous: ChunkPos? = null

		for (value in itr) {
			if (value == previous)
				itr.remove()
			else
				previous = value
		}

		return ObjectArraySet.ofUnchecked(*result.toTypedArray())
	}

	fun region2Chunks(region: AABB): Set<ChunkPos> {
		if (region.mins == region.maxs)
			return emptySet()

		val result = ObjectArrayList<ChunkPos>()

		for (actualRegion in split(region).first) {
			val xMin = this.x.chunkFromCell(actualRegion.mins.x)
			val xMax = this.x.chunkFromCell(actualRegion.maxs.x)

			val yMin = this.y.chunkFromCell(actualRegion.mins.y)
			val yMax = this.y.chunkFromCell(actualRegion.maxs.y)

			for (x in xMin .. xMax) {
				for (y in yMin .. yMax) {
					result.add(ChunkPos(x, y))
				}
			}
		}

		result.sort()

		val itr = result.iterator()
		var previous: ChunkPos? = null

		for (value in itr) {
			if (value == previous)
				itr.remove()
			else
				previous = value
		}

		return ObjectArraySet.ofUnchecked(*result.toTypedArray())
	}

	fun rectContains(rect: AABB, point: IStruct2d): Boolean {
		val wrap = wrap(point)
		return split(rect).first.any { it.isInside(wrap) }
	}

	fun diff(a: Vector2i, b: Vector2i): Vector2i {
		return Vector2i(x.diff(a.x, b.x), y.diff(a.y, b.y))
	}

	fun diff(a: Vector2d, b: Vector2d): Vector2d {
		return Vector2d(x.diff(a.x, b.x), y.diff(a.y, b.y))
	}

	fun nearestTo(source: IStruct2i, target: IStruct2i) = Vector2i(x.nearestTo(source.component1(), target.component1()), y.nearestTo(source.component2(), target.component2()))
	fun nearestTo(source: IStruct2d, target: IStruct2d) = Vector2d(x.nearestTo(source.component1(), target.component1()), y.nearestTo(source.component2(), target.component2()))

	fun polyDistance(poly: Poly, point: IStruct2d): Double {
		return poly.distance(nearestTo(poly.centre, point))
	}
}
