package ru.dbotthepony.kstarbound.defs.image

import com.google.gson.JsonSyntaxException
import com.google.gson.TypeAdapter
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.AssetPath
import ru.dbotthepony.kommons.gson.consumeNull
import ru.dbotthepony.kstarbound.util.AssetPathStack
import ru.dbotthepony.kstarbound.util.SBPattern

// TODO: While this class creates proper interface and provides caching for
// image/sprite resolution, it, however, does not match original engine behavior.
// Original engine first resolves entire path as pattern (through String.replaceTags)
// and only then determine what is image path, what is sprite and do we have render directives.
// tl;dr: for next string <image>.png:thing.<frame><directives>
// and next inputs: image=/something frame=4 directives=?hueshift=4
// old and new engines will disagree what to do
// old engine will first resolve tags: /something.png:thing.4?hueshift=4
// and then parse the string (image = /something.png, sprite = thing.4, directives = hueshift=4)
// our engine, however, will first parse the string, and then resolve tags, resulting into
// sprite being "thing.4?hueshift=4" and directives being empty
class SpriteReference private constructor(
	val raw: AssetPath,
	val imagePath: SBPattern,
	val spritePath: SBPattern?,
	val renderDirectives: SBPattern?,
	val image: Image?,
) {
	val sprite by lazy {
		if (image == null)
			null
		else if (spritePath == null)
			image.first
		else
			image[spritePath.value ?: return@lazy null]
	}

	fun with(values: (String) -> String?): SpriteReference {
		val imagePath = this.imagePath.with(values)
		val spritePath = this.spritePath?.with(values)
		val renderDirectives = this.renderDirectives?.with(values)

		if (imagePath != this.imagePath || spritePath != this.spritePath || renderDirectives != this.renderDirectives) {
			if (imagePath != this.imagePath) {
				val resolved = imagePath.value

				if (resolved == null)
					return SpriteReference(raw, imagePath, spritePath, renderDirectives, null)
				else
					return SpriteReference(raw, imagePath, spritePath, renderDirectives, Image.get(resolved))
			} else {
				return SpriteReference(raw, imagePath, spritePath, renderDirectives, image)
			}
		}

		return this
	}

	fun with(values: Map<String, String>): SpriteReference {
		return with(values::get)
	}

	override fun equals(other: Any?): Boolean {
		return other is SpriteReference && other.imagePath == imagePath && other.spritePath == spritePath && other.image == image
	}

	override fun hashCode(): Int {
		return imagePath.hashCode() * 31 + spritePath.hashCode()
	}

	override fun toString(): String {
		val image = imagePath.value ?: imagePath.raw
		val sprite = spritePath?.value ?: spritePath?.raw ?: ""
		val directives = renderDirectives?.value ?: renderDirectives?.raw ?: ""

		if (sprite == "" && directives == "") {
			return image
		} else if (sprite != "" && directives == "") {
			return "$image:$sprite"
		} else {
			return "$image:$sprite?$directives"
		}
	}

	companion object : TypeAdapter<SpriteReference>() {
		val NEVER = SpriteReference(AssetPath("", ""), SBPattern.EMPTY, null, null, null)

		private val strings by lazy { Starbound.gson.getAdapter(String::class.java) }

		override fun write(out: JsonWriter, value: SpriteReference?) {
			out.value(value?.raw?.fullPath)
		}

		@JvmStatic
		fun create(path: String): SpriteReference {
			if (path == "")
				return NEVER

			val split = path.lowercase().split(':')

			if (split.size > 2) {
				throw JsonSyntaxException("Ambiguous image reference: $path")
			}

			val imagePath: SBPattern

			val renderDirectives: SBPattern?
			val spritePath: SBPattern?

			if (split.size == 2) {
				imagePath = SBPattern.of(split[0])

				spritePath = if ('?' in split[1]) {
					// has render directives
					val sprite = split[1].substringBefore('?')
					val directives = split[1].substringAfter('?')
					renderDirectives = SBPattern.of(directives)
					SBPattern.of(sprite)
				} else {
					// no render directives
					renderDirectives = null
					SBPattern.of(split[1])
				}
			} else {
				imagePath = if ('?' in split[0]) {
					// has render directives
					val image = split[0].substringBefore('?')
					val directives = split[0].substringAfter('?')
					renderDirectives = SBPattern.of(directives)
					SBPattern.of(image)
				} else {
					// no render directives
					renderDirectives = null
					SBPattern.of(split[0])
				}

				spritePath = null
			}

			if (imagePath.isPlainString) {
				val remapped = AssetPathStack.remap(imagePath.value!!)
				return SpriteReference(AssetPath(path, AssetPathStack.remap(path)), SBPattern.raw(remapped), spritePath, renderDirectives, Image.get(remapped))
			} else {
				return SpriteReference(AssetPath(path, path), imagePath, spritePath, renderDirectives, null)
			}
		}

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

			return create(strings.read(`in`))
		}
	}
}
