package ru.dbotthepony.kstarbound.json.builder

import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableMap
import com.google.common.collect.Streams
import com.google.gson.Gson
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 it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.gson.consumeNull
import ru.dbotthepony.kstarbound.util.sbIntern
import java.util.*
import java.util.stream.Stream
import kotlin.reflect.KClass
import kotlin.reflect.full.isSuperclassOf

interface IStringSerializable {
	val jsonName: String

	fun match(name: String): Boolean {
		return name == jsonName
	}
}

inline fun <reified T : Enum<T>> EnumAdapter(values: Stream<T> = Arrays.stream(T::class.java.enumConstants), default: T? = null): EnumAdapter<T> {
	return EnumAdapter(T::class, values, default)
}

inline fun <reified T : Enum<T>> EnumAdapter(values: Iterator<T>, default: T? = null): EnumAdapter<T> {
	return EnumAdapter(T::class, Streams.stream(values), default)
}

inline fun <reified T : Enum<T>> EnumAdapter(values: Array<out T>, default: T? = null): EnumAdapter<T> {
	return EnumAdapter(T::class, Arrays.stream(values), default)
}

inline fun <reified T : Enum<T>> EnumAdapter(values: Collection<T>, default: T? = null): EnumAdapter<T> {
	return EnumAdapter(T::class, values.stream(), default)
}

class EnumAdapter<T : Enum<T>>(private val enum: KClass<T>, values: Stream<T> = Arrays.stream(enum.java.enumConstants), val default: T? = null) : TypeAdapter<T?>() {
	constructor(clazz: Class<T>, values: Stream<T> = Arrays.stream(clazz.enumConstants), default: T? = null) : this(clazz.kotlin, values, default)

	constructor(clazz: Class<T>, values: Iterator<T>, default: T? = null) : this(clazz.kotlin, Streams.stream(values), default)
	constructor(clazz: Class<T>, values: Array<out T>, default: T? = null) : this(clazz.kotlin, Arrays.stream(values), default)
	constructor(clazz: Class<T>, values: Collection<T>, default: T? = null) : this(clazz.kotlin, values.stream(), default)

	constructor(clazz: KClass<T>, values: Iterator<T>, default: T? = null) : this(clazz, Streams.stream(values), default)
	constructor(clazz: KClass<T>, values: Array<out T>, default: T? = null) : this(clazz, Arrays.stream(values), default)
	constructor(clazz: KClass<T>, values: Collection<T>, default: T? = null) : this(clazz, values.stream(), default)

	private val values = values.collect(ImmutableList.toImmutableList())
	private val mapping: ImmutableMap<String, T>
	private val areCustom = IStringSerializable::class.java.isAssignableFrom(enum.java)
	private val misses = Collections.synchronizedSet(ObjectOpenHashSet<String>())

	init {
		val builder = Object2ObjectArrayMap<String, T>()

		for (value in this.values) {
			builder[value.name.sbIntern()] = value
			builder[value.name.uppercase().sbIntern()] = value
			builder[value.name.lowercase().sbIntern()] = value

			val spaced = value.name.replace('_', ' ').sbIntern()
			val stitched = value.name.replace("_", "").sbIntern()

			builder[spaced.sbIntern()] = value
			builder[spaced.uppercase().sbIntern()] = value
			builder[spaced.lowercase().sbIntern()] = value

			builder[stitched.sbIntern()] = value
			builder[stitched.uppercase().sbIntern()] = value
			builder[stitched.lowercase().sbIntern()] = value
		}

		mapping = ImmutableMap.copyOf(builder)
	}

	override fun write(out: JsonWriter, value: T?) {
		if (value == null) {
			out.nullValue()
		} else if (value is IStringSerializable) {
			out.value(value.jsonName)
		} else {
			out.value(value.name)
		}
	}

	@Suppress("unchecked_cast")
	override fun read(`in`: JsonReader): T? {
		if (`in`.consumeNull()) {
			return null
		}

		val key = `in`.nextString()

		if (areCustom) {
			for (value in values) {
				if ((value as IStringSerializable).match(key)) {
					return value
				}
			}
		}

		val existing = mapping[key]

		if (existing == null && misses.add(key)) {
			LOGGER.error("$key is not a valid ${enum.qualifiedName} value")
		}

		return existing ?: default
	}

	/**
	 * Возвращает [EnumAdapter] с тем же типом и набором [T], которому запрещено возвращать или принимать null'ы
	 */
	fun neverNull(): TypeAdapter<T> {
		return object : TypeAdapter<T>() {
			override fun write(out: JsonWriter, value: T) {
				return this@EnumAdapter.write(out, value)
			}

			override fun read(`in`: JsonReader): T {
				val key = `in`.nextString()

				if (areCustom) {
					for (value in values) {
						if ((value as IStringSerializable).match(key)) {
							return value
						}
					}
				}

				return mapping[key] ?: default ?: throw JsonSyntaxException("$key is not a valid ${enum.qualifiedName} value")
			}
		}
	}

	private enum class Never

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

		override fun <T : Any?> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
			if (type.rawType.isEnum) {
				val clazz = type.rawType as Class<Never>
				return EnumAdapter(clazz, clazz.enumConstants) as TypeAdapter<T>
			}

			return null
		}
	}
}
