package ru.dbotthepony.kstarbound.defs

import com.google.common.collect.ImmutableList
import com.google.gson.Gson
import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonSyntaxException
import com.google.gson.TypeAdapter
import com.google.gson.TypeAdapterFactory
import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kommons.gson.Vector2dTypeAdapter
import ru.dbotthepony.kommons.gson.consumeNull
import ru.dbotthepony.kommons.math.linearInterpolation
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.json.builder.EnumAdapter

enum class JsonFunctionInterpolation {
	LINEAR {
		override fun interpolate(t: Double, a: Double, b: Double): Double {
			return linearInterpolation(t, a, b)
		}
	};

	abstract fun interpolate(t: Double, a: Double, b: Double): Double
}

enum class JsonFunctionConstraint {
	CLAMP {
		override fun process(x: Double, lower: Double, upper: Double): Double {
			return x.coerceIn(lower, upper)
		}
	};

	abstract fun process(x: Double, lower: Double, upper: Double): Double
}

data class DoubleRange(val start: Double, val end: Double) : Comparable<Double> {
	init {
		require(start <= end) { "start <= end: $start <= $end" }
	}

	val length = end - start

	operator fun contains(value: Double): Boolean {
		return value in start .. end
	}

	fun determine(value: Double): Double {
		if (value < start) {
			return 0.0
		} else if (value > end) {
			return 1.0
		} else {
			return (value - start) / length
		}
	}

	override fun compareTo(other: Double): Int {
		if (other < start) {
			return 1
		} else if (other > end) {
			return -1
		} else {
			return 0
		}
	}
}

class JsonFunction(
	val interpolation: JsonFunctionInterpolation,
	val constraint: JsonFunctionConstraint,
	ranges: Collection<Vector2d>
) {
	val ranges: ImmutableList<Vector2d> = ranges.stream().sorted { o1, o2 -> o1.x.compareTo(o2.x) }.collect(ImmutableList.toImmutableList())
	val lowerBound = this.ranges.first().x
	val upperBound = this.ranges.last().x

	fun evaluate(input: Double): Double {
		val x = constraint.process(input, lowerBound, upperBound)

		when (interpolation) {
			JsonFunctionInterpolation.LINEAR -> {
				if (x <= lowerBound) {
					return ranges[0].y
				} else if (x >= upperBound) {
					return ranges.last().y
				}

				for (i in 0 until ranges.size - 1) {
					val a = ranges[i]
					val b = ranges[i + 1]

					if (x in a.x .. b.x) {
						return linearInterpolation((x - a.x) / (b.x - a.x), a.y, b.y)
					}
				}

				return ranges.last().y
			}
		}
	}

	companion object : TypeAdapter<JsonFunction>() {
		val CONSTRAINT_ADAPTER = EnumAdapter(JsonFunctionConstraint::class)
		val INTERPOLATION_ADAPTER = EnumAdapter(JsonFunctionInterpolation::class)

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

		override fun read(reader: JsonReader): JsonFunction? {
			if (reader.consumeNull())
				return null

			reader.beginArray()

			val interpolation = INTERPOLATION_ADAPTER.read(reader) ?: throw JsonSyntaxException("Missing interpolation")
			val constraint = CONSTRAINT_ADAPTER.read(reader) ?: throw JsonSyntaxException("Missing constraint")

			val ranges = ArrayList<Vector2d>()

			while (reader.peek() != JsonToken.END_ARRAY) {
				ranges.add(Vector2dTypeAdapter.read(reader))
			}

			reader.endArray()

			return JsonFunction(interpolation, constraint, ImmutableList.copyOf(ranges))
		}
	}
}

class Json2Function(
	val interpolation: JsonFunctionInterpolation,
	val constraint: JsonFunctionConstraint,
	xRanges: List<Ranges>, // значения [a, b, c, d]
	yRanges: List<Ranges>, // значения [a, [b, c, d]]
) {
	init {
		require(xRanges.isNotEmpty()) { "X ranges are empty" }
		require(yRanges.isNotEmpty()) { "Y ranges are empty" }
	}

	val xRanges: ImmutableList<Ranges> = xRanges.stream().sorted { o1, o2 -> o1.edge.compareTo(o2.edge) }.collect(ImmutableList.toImmutableList())
	val yRanges: ImmutableList<Ranges> = yRanges.stream().sorted { o1, o2 -> o1.edge.compareTo(o2.edge) }.collect(ImmutableList.toImmutableList())

	data class Ranges(val interpolation: JsonFunctionInterpolation, val edge: Double, val ranges: ImmutableList<DoubleRange>) {
		val step = 1.0 / ranges.size.toDouble()

		fun map(value: Double): Double {
			if (value <= ranges.first().start) {
				return 0.0
			} else if (value >= ranges.last().end) {
				return 1.0
			}

			var i = 0.0

			for (range in ranges) {
				if (value in range) {
					return i + range.determine(value) * step
				} else {
					i += step
				}
			}

			return i
		}

		fun unmap(t: Double): Double {
			if (t <= 0.0) {
				return ranges.first().start
			} else if (t >= 1.0) {
				return ranges.last().end
			}

			val unmap = t / step
			val range = ranges[unmap.toInt()]
			return interpolation.interpolate(unmap % 1.0, range.start, range.end)
		}
	}

	fun evaluate(x: Double, y: Double): Double {
		var xT = 0.0

		if (xRanges.size == 1) {
			xT = xRanges[0].map(x)
		} else {
			var hit = false

			for (i in 1 until xRanges.size) {
				val a = xRanges[i - 1]
				val b = xRanges[i]

				if (x in a.edge .. b.edge) {
					xT = interpolation.interpolate((x - a.edge) / (b.edge - a.edge), a.map(x), b.map(x))
					hit = true
					break
				}
			}

			if (!hit) {
				xT = xRanges.last().map(x)
			}
		}

		if (yRanges.size == 1) {
			return yRanges[0].unmap(xT)
		} else {
			for (i in 1 until yRanges.size) {
				val a = yRanges[i - 1]
				val b = yRanges[i]

				if (y in a.edge .. b.edge) {
					return interpolation.interpolate((y - a.edge) / (b.edge - a.edge), a.unmap(xT), b.unmap(xT))
				}
			}

			return yRanges.last().unmap(xT)
		}
	}

	companion object : TypeAdapterFactory {
		override fun <T : Any?> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
			if (type.rawType == Json2Function::class.java) {
				return object : TypeAdapter<Json2Function>() {
					override fun write(out: JsonWriter, value: Json2Function?) {
						TODO("Not yet implemented")
					}

					override fun read(reader: JsonReader): Json2Function? {
						if (reader.consumeNull())
							return null

						reader.beginArray()

						val interpolation = JsonFunction.INTERPOLATION_ADAPTER.read(reader) ?: throw JsonSyntaxException("Missing interpolation")
						val constraint = JsonFunction.CONSTRAINT_ADAPTER.read(reader) ?: throw JsonSyntaxException("Missing constraint")

						val xRanges = ArrayList<Ranges>()
						val yRanges = ArrayList<Ranges>()

						val elements = Starbound.ELEMENTS_ADAPTER.arrays.read(reader)

						for ((i, row) in elements.withIndex()) {
							if (row !is JsonArray) {
								throw JsonSyntaxException("Invalid function ranges at row $i")
							}

							if (row.size() < 2) {
								throw JsonSyntaxException("Row at $i should have at least 2 elements")
							}

							val edge = row[0].asDouble

							if (row[1] is JsonArray) {
								val ySource = row[1].asJsonArray

								if (ySource.size() < 2) {
									throw JsonSyntaxException("Y ranges at row $i should have at least 2 elements")
								}

								val ranges = ImmutableList.Builder<DoubleRange>()
								var prev = ySource[0].asDouble

								for (elem in 1 until ySource.size()) {
									val v = ySource[elem].asDouble
									ranges.add(DoubleRange(prev, v))
									prev = v
								}

								yRanges.add(Ranges(interpolation, edge, ranges.build()))
							} else {
								if (row.size() < 3) {
									throw JsonSyntaxException("Row at $i should have at least 3 elements")
								}

								val ranges = ImmutableList.Builder<DoubleRange>()
								var prev = row[1].asDouble

								for (elem in 2 until row.size()) {
									val v = row[elem].asDouble
									ranges.add(DoubleRange(prev, v))
									prev = v
								}

								xRanges.add(Ranges(interpolation, edge, ranges.build()))
							}
						}

						reader.endArray()

						if (xRanges.isEmpty()) {
							throw JsonSyntaxException("No X ranges")
						}

						if (yRanges.isEmpty()) {
							throw JsonSyntaxException("No Y ranges")
						}

						val size = xRanges.first().ranges.size
						var broken = xRanges.firstOrNull { it.ranges.size != size }

						if (broken != null) {
							throw JsonSyntaxException("One or more of X ranges are having different size (found ${broken.ranges.size}, expected $size)")
						}

						broken = yRanges.firstOrNull { it.ranges.size != size }

						if (broken != null) {
							throw JsonSyntaxException("One or more of Y ranges are having different size (found ${broken.ranges.size}, expected $size)")
						}

						return Json2Function(interpolation, constraint, xRanges, yRanges)
					}
				} as TypeAdapter<T>
			}

			return null
		}
	}
}

class JsonConfigFunction(val data: ImmutableList<Pair<Double, JsonElement>>) {
	fun evaluate(point: Double): JsonElement? {
		return data.lastOrNull { it.first <= point }?.second
	}

	fun <T> evaluate(point: Double, adapter: TypeAdapter<T>): KOptional<T> {
		val eval = data.lastOrNull { it.first <= point }?.second ?: return KOptional()
		return KOptional(adapter.fromJsonTree(eval))
	}

	class Adapter(gson: Gson) : TypeAdapter<JsonConfigFunction>() {
		val parent = gson.getAdapter(
			TypeToken.getParameterized(
				ImmutableList::class.java,
				TypeToken.getParameterized(Pair::class.java,
					java.lang.Double::class.java,
					JsonElement::class.java
				).type
			)
		) as TypeAdapter<ImmutableList<Pair<Double, JsonElement>>>

		override fun write(out: JsonWriter, value: JsonConfigFunction?) {
			if (value == null)
				out.nullValue()
			else
				parent.write(out, value.data)
		}

		override fun read(`in`: JsonReader): JsonConfigFunction? {
			if (`in`.consumeNull())
				return null

			return JsonConfigFunction(
				parent.read(`in`)
					.stream()
					.sorted { o1, o2 -> o1.first.compareTo(o2.first) }
					.collect(ImmutableList.toImmutableList()))
		}
	}
}
