package ru.dbotthepony.kstarbound.defs.dungeon

import com.google.common.collect.ImmutableList
import it.unimi.dsi.fastutil.ints.IntAVLTreeSet
import it.unimi.dsi.fastutil.ints.IntArrayList
import it.unimi.dsi.fastutil.objects.ObjectArrayList
import org.lwjgl.stb.STBImage
import org.lwjgl.system.MemoryUtil
import ru.dbotthepony.kommons.arrays.Object2DArray
import ru.dbotthepony.kommons.math.RGBAColor
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.image.Image
import java.lang.ref.Reference

class ImagePartReader(part: DungeonPart, val images: ImmutableList<Image>) : PartReader(part) {
	override val size: Vector2i
		get() = if (images.isEmpty()) Vector2i.ZERO else images.first().size

	// ObjectArrayList doesn't check for concurrent modifications
	private val layers = ObjectArrayList<Layer>()
	private class Layer(val palette: Array<DungeonTile>, val data: VectorizedBitSet)

	override fun bind(def: DungeonDefinition) {
		check(def.tiles.isNotEmpty) { "Image parts require 'tiles' palette to be present in .dungeon definition" }

		for ((i, image) in images.withIndex()) {
			// go around image cache, since image will be loaded exactly once
			// and then forgotten
			val (bytes, width, height) = Image.readImageDirect(image.source)
			val tileData = IntArray(width * height)

			for (x in 0 until image.width) {
				for (y in 0 until image.height) {
					val offset = (x + y * image.width) * 4

					// flip image as we go
					tileData[x + (image.height - y - 1) * image.width] = bytes[offset].toInt().and(0xFF) or
						bytes[offset + 1].toInt().and(0xFF).shl(8) or
						bytes[offset + 2].toInt().and(0xFF).shl(16) or
						bytes[offset + 3].toInt().and(0xFF).shl(24)
				}
			}

			// determine unique tiles
			val uniqueTiles = IntAVLTreeSet()

			for (y in 0 until image.height) {
				for (x in 0 until image.width) {
					val color = tileData[x + y * image.width]

					if (uniqueTiles.add(color)) {
						if (part.dungeon.tiles[color] == null) {
							val parse = RGBAColor.abgr(color)
							throw IllegalStateException("Unknown tile inside ${image.path} at $x, $y with color [${parse.redInt}, ${parse.greenInt}, ${parse.blueInt}, ${parse.alphaInt}] (index $color)")
						}
					}
				}
			}

			// construct palette
			val indices = IntArrayList(uniqueTiles)
			val palette = Array(indices.size) { part.dungeon.tiles[indices.getInt(it)]!! }
			val data = VectorizedBitSet(32 - Integer.numberOfLeadingZeros(uniqueTiles.size), image.width, image.height)

			// fill data
			for (y in 0 until image.height) {
				for (x in 0 until image.width) {
					val index = indices.indexOf(tileData[x + y * image.width])
					check(index != -1)
					data[x, y] = index
				}
			}

			layers.add(Layer(palette, data))
		}
	}

	override fun <T> walkTiles(callback: TileCallback<T>): KOptional<T> {
		for (layer in layers) {
			// walk bottom-top first, this way we will place bottom objects/tiles before top ones
			for (y in 0 until layer.data.height) {
				for (x in 0 until layer.data.width) {
					val get = callback(x, y, layer.palette[layer.data[x, y]])
					if (get.isPresent) return get
				}
			}
		}

		return KOptional()
	}

	override fun <T> walkTilesAt(x: Int, y: Int, callback: TileCallback<T>): KOptional<T> {
		for (layer in layers) {
			if (x in 0 until layer.data.width && y in 0 until layer.data.height) {
				val get = callback(x, y, layer.palette[layer.data[x, y]])
				if (get.isPresent) return get
			}
		}

		return KOptional()
	}
}
