package ru.dbotthepony.kstarbound.defs

import com.google.common.collect.ImmutableList
import com.google.gson.Gson
import com.google.gson.JsonObject
import com.google.gson.TypeAdapter
import com.google.gson.annotations.JsonAdapter
import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kommons.math.RGBAColor
import ru.dbotthepony.kommons.matrix.Matrix3f
import ru.dbotthepony.kommons.vector.Vector2f
import ru.dbotthepony.kommons.vector.Vector3f
import ru.dbotthepony.kstarbound.world.PIXELS_IN_STARBOUND_UNITf
import ru.dbotthepony.kstarbound.client.StarboundClient
import ru.dbotthepony.kstarbound.client.gl.vertex.GeometryType
import ru.dbotthepony.kstarbound.client.render.IGeometryLayer
import ru.dbotthepony.kstarbound.defs.image.SpriteReference
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kommons.gson.consumeNull
import ru.dbotthepony.kommons.gson.contains
import ru.dbotthepony.kommons.util.AABB
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.math.Line2d

@JsonAdapter(Drawable.Adapter::class)
sealed class Drawable(val position: Vector2f, val color: RGBAColor, val fullbright: Boolean) {
	@JsonFactory
	data class Transformations(val centered: Boolean = false, val rotation: Float = 0f, val mirrored: Boolean = false, val scale: Either<Float, Vector2f> = Either.left(1f))

	class Line(
		val line: Line2d,
		val width: Float,
		position: Vector2f = Vector2f.ZERO,
		color: RGBAColor = RGBAColor.WHITE,
		fullbright: Boolean = false
	) : Drawable(position, color, fullbright) {
		override fun render(client: StarboundClient, layer: IGeometryLayer, x: Float, y: Float) {
			TODO("Not yet implemented")
		}

		override fun flop(): Drawable {
			TODO("Not yet implemented")
		}

		override fun boundingBox(cropImages: Boolean): AABB {
			TODO("Not yet implemented")
		}

		override fun scale(scale: Vector2f): Drawable {
			TODO("Not yet implemented")
		}

		override fun translate(translation: Vector2f): Drawable {
			TODO("Not yet implemented")
		}
	}

	class Poly(
		val vertices: ImmutableList<Vector2f>,
		position: Vector2f = Vector2f.ZERO,
		color: RGBAColor = RGBAColor.WHITE,
		fullbright: Boolean = false
	) : Drawable(position, color, fullbright) {
		override fun render(client: StarboundClient, layer: IGeometryLayer, x: Float, y: Float) {
			TODO("Not yet implemented")
		}

		override fun flop(): Drawable {
			TODO("Not yet implemented")
		}

		override fun boundingBox(cropImages: Boolean): AABB {
			TODO("Not yet implemented")
		}

		override fun scale(scale: Vector2f): Drawable {
			TODO("Not yet implemented")
		}

		override fun translate(translation: Vector2f): Drawable {
			TODO("Not yet implemented")
		}
	}

	class Image(
		val path: SpriteReference,
		val transform: Either<Matrix3f, Transformations>,
		position: Vector2f = Vector2f.ZERO,
		color: RGBAColor = RGBAColor.WHITE,
		fullbright: Boolean = false
	) : Drawable(position, color, fullbright) {
		override fun with(values: (String) -> String?): Image {
			val newPath = path.with(values)

			if (newPath == path) {
				return this
			}

			return Image(newPath, transform, position, color, fullbright)
		}

		override fun flop(): Drawable {
			return Image(path, transform.flatMap({ it.copy().scale(-1f, 1f) }, { it.copy(mirrored = !it.mirrored) }), position, color, fullbright)
		}

		override fun scale(scale: Vector2f): Drawable {
			return Image(path, transform.flatMap({ it.scale(scale) }, { it.copy(scale = Either.right(it.scale.map({ scale * it }, { scale * it }))) }), position, color, fullbright)
		}

		override fun translate(translation: Vector2f): Drawable {
			return Image(path, transform, position + translation, color, fullbright)
		}

		private fun getTransforms(sprite: ru.dbotthepony.kstarbound.defs.image.Image.Sprite) = transform.map({ it.copy() }, {
			val mat = Matrix3f.identity()

			it.scale.map({ mat.scale(it / PIXELS_IN_STARBOUND_UNITf, it / PIXELS_IN_STARBOUND_UNITf) }, { mat.scale(it / PIXELS_IN_STARBOUND_UNITf) })

			if (it.centered) {
				mat.translate(sprite.width / -2f, sprite.height / -2f)
			}

			if (it.rotation != 0f) {
				mat.rotateAroundZ(it.rotation)
			}

			if (it.mirrored) {
				mat.translate(sprite.width.toFloat(), 0f)
				mat.scale(-1f, 1f)
			}

			mat
		})

		// Original engine here calculates bounding box wrong
		// if "cropImages" is false
		override fun boundingBox(cropImages: Boolean): AABB {
			val sprite = path.sprite ?: return AABB.ZERO
			var result: AABB

			if (cropImages) {
				result = AABB(
					Vector2d(sprite.nonEmptyRegion.x.toDouble(), sprite.nonEmptyRegion.y.toDouble()),
					Vector2d(sprite.nonEmptyRegion.z.toDouble(), sprite.nonEmptyRegion.w.toDouble()),
				)
			} else {
				result = AABB(
					Vector2d.ZERO,
					Vector2d(sprite.width.toDouble(), sprite.height.toDouble())
				)
			}

			val transforms = getTransforms(sprite)

			result = result.expand((Vector2f(result.mins.x.toFloat(), result.mins.y.toFloat()) * transforms).toDoubleVector())
			result = result.expand((Vector2f(result.mins.x.toFloat(), result.maxs.y.toFloat()) * transforms).toDoubleVector())
			result = result.expand((Vector2f(result.maxs.x.toFloat(), result.mins.y.toFloat()) * transforms).toDoubleVector())
			result = result.expand((Vector2f(result.maxs.x.toFloat(), result.maxs.y.toFloat()) * transforms).toDoubleVector())

			result += position.toDoubleVector()

			return result
		}

		override fun render(client: StarboundClient, layer: IGeometryLayer, x: Float, y: Float) {
			val sprite = path.sprite ?: return
			val texture = path.image!!.texture
			val program = if (fullbright) client.programs.positionTexture else client.programs.positionTextureLightmap

			val mat = getTransforms(sprite)

			val builder = layer.getBuilder(program.config(texture))

			mat.preTranslate(position.x + x, position.y + y)
			client.stack.last().mulIntoOther(mat)

			builder.mode(GeometryType.QUADS)
			builder.vertex(mat, 0f, 0f).uv(sprite.u0, sprite.v0)
			builder.vertex(mat, 0f + sprite.width, 0f).uv(sprite.u1, sprite.v0)
			builder.vertex(mat, 0f + sprite.width, 0f + sprite.height).uv(sprite.u1, sprite.v1)
			builder.vertex(mat, 0f, 0f + sprite.height).uv(sprite.u0, sprite.v1)
		}
	}

	class Empty(position: Vector2f = Vector2f.ZERO, color: RGBAColor = RGBAColor.WHITE, fullbright: Boolean = false) : Drawable(position, color, fullbright) {
		override fun render(client: StarboundClient, layer: IGeometryLayer, x: Float, y: Float) {}

		override fun flop(): Drawable {
			return this
		}

		override fun boundingBox(cropImages: Boolean): AABB {
			return AABB.ZERO
		}

		override fun scale(scale: Vector2f): Drawable {
			return this
		}

		override fun translate(translation: Vector2f): Drawable {
			return Empty(position + translation, color, fullbright)
		}
	}

	open fun with(values: (String) -> String?): Drawable {
		return this
	}

	abstract fun render(client: StarboundClient = StarboundClient.current(), layer: IGeometryLayer, x: Float = 0f, y: Float = 0f)

	abstract fun boundingBox(cropImages: Boolean = false): AABB

	abstract fun scale(scale: Vector2f): Drawable

	fun scale(scale: Float): Drawable {
		if (scale == 1f)
			return this

		return scale(Vector2f(scale, scale))
	}

	abstract fun translate(translation: Vector2f): Drawable

	/**
	 * mirror along X axis
	 */
	abstract fun flop(): Drawable

	companion object {
		val EMPTY = Empty()
		val CENTERED = Transformations(true)
		private val LOGGER = LogManager.getLogger()
	}

	class Adapter(gson: Gson) : TypeAdapter<Drawable>() {
		private val lines = gson.getAdapter(Line2d::class.java)
		private val vectors = gson.getAdapter(Vector2f::class.java)
		private val vectors3 = gson.getAdapter(Vector3f::class.java)
		private val colors = gson.getAdapter(RGBAColor::class.java)
		private val images = gson.getAdapter(SpriteReference::class.java)
		private val vertices = gson.getAdapter(TypeToken.getParameterized(ImmutableList::class.java, Vector2f::class.java)) as TypeAdapter<ImmutableList<Vector2f>>
		private val transformations = gson.getAdapter(Transformations::class.java)

		override fun write(out: JsonWriter?, value: Drawable?) {
			TODO("Not yet implemented")
		}

		override fun read(`in`: JsonReader): Drawable {
			if (`in`.consumeNull()) {
				return EMPTY
			} else {
				val value = Starbound.ELEMENTS_ADAPTER.objects.read(`in`)!!

				val position = value["position"]?.let { vectors.fromJsonTree(it) } ?: Vector2f.ZERO
				val color = value["color"]?.let { colors.fromJsonTree(it) } ?: RGBAColor.WHITE
				val fullbright = value["fullbright"]?.asBoolean ?: false

				if ("line" in value) {
					return Line(lines.fromJsonTree(value["line"]), value["width"].asFloat, position, color, fullbright)
				} else if ("poly" in value) {
					return Poly(vertices.fromJsonTree(value["poly"]), position, color, fullbright)
				} else if ("image" in value) {
					val image = images.fromJsonTree(value["image"])

					if ("transformation" in value) {
						val mat = Matrix3f.identity()
						val array = value["transformation"].asJsonArray

						// original starbound use GLM, which reflects OpenGL, which in turn make matrices row-major
						val row0 = vectors3.fromJsonTree(array[0])
						val row1 = vectors3.fromJsonTree(array[1])
						val row2 = vectors3.fromJsonTree(array[2])

						mat.r00 = row0.x
						mat.r01 = row0.y
						mat.r02 = row0.z

						mat.r10 = row1.x
						mat.r11 = row1.y
						mat.r12 = row1.z

						mat.r20 = row2.x
						mat.r21 = row2.y
						mat.r22 = row2.z

						return Image(
							image,
							Either.left(mat),
							position,
							color,
							fullbright
						)
					} else {
						return Image(
							image,
							Either.right(transformations.fromJsonTree(value)),
							position,
							color,
							fullbright
						)
					}


				} else {
					return Empty(position, color, fullbright)
				}
			}
		}
	}
}
