package ru.dbotthepony.kstarbound.defs.dungeon

import com.google.common.collect.ImmutableList
import com.google.gson.Gson
import com.google.gson.JsonArray
import com.google.gson.JsonObject
import com.google.gson.JsonSyntaxException
import com.google.gson.TypeAdapter
import com.google.gson.annotations.JsonAdapter
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor
import ru.dbotthepony.kstarbound.defs.`object`.ObjectDefinition
import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition
import ru.dbotthepony.kstarbound.defs.tile.NO_DUNGEON_ID
import ru.dbotthepony.kstarbound.defs.tile.TileDefinition
import ru.dbotthepony.kstarbound.defs.tile.TileModifierDefinition
import ru.dbotthepony.kstarbound.defs.tile.isRealModifier
import ru.dbotthepony.kstarbound.defs.tile.orEmptyLiquid
import ru.dbotthepony.kstarbound.defs.tile.orEmptyModifier
import ru.dbotthepony.kstarbound.defs.tile.orEmptyTile
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.json.builder.JsonIgnore
import ru.dbotthepony.kstarbound.util.random.random
import ru.dbotthepony.kstarbound.world.Direction
import ru.dbotthepony.kstarbound.world.api.AbstractLiquidState
import ru.dbotthepony.kstarbound.world.api.MutableLiquidState
import ru.dbotthepony.kstarbound.world.api.TileColor

@JsonAdapter(DungeonBrush.Adapter::class)
abstract class DungeonBrush {
	enum class Phase {
		CLEAR,
		DUNGEON_ID, // move dungeon id to very top so it takes effect right away
		PLACE_WALLS,
		PLACE_MODS,
		PLACE_OBJECTS,
		PLACE_BIOME_TREES,
		PLACE_BIOME_ITEMS,
		DO_WIRING,
		DROP_ITEMS,
		PLACE_NPCS;
	}

	class Adapter(gson: Gson) : TypeAdapter<DungeonBrush>() {
		override fun write(out: JsonWriter, value: DungeonBrush) {
			TODO("Not yet implemented")
		}

		override fun read(`in`: JsonReader): DungeonBrush {
			val read = Starbound.ELEMENTS_ADAPTER.arrays.read(`in`)
			// don't delegate to EnumAdapter since this can have bad consequences
			// such as defaulting to CLEAR action and printing a warning in console
			val type = DungeonBrushType.entries.firstOrNull { it.jsonName == read[0].asString } ?: throw NoSuchElementException("Unknown brush type ${read[0].asString}!")

			try {
				return type.createLegacy(read)
			} catch (err: Throwable) {
				throw JsonSyntaxException("Reading dungeon brush $type", err)
			}
		}
	}

	abstract fun execute(x: Int, y: Int, phase: Phase, world: DungeonWorld)

	object Invalid : DungeonBrush() {
		override fun execute(x: Int, y: Int, phase: Phase, world: DungeonWorld) {
			// do nothing
		}
	}

	// in original engine this brush is non-deterministic.
	data class Random(val children: ImmutableList<DungeonBrush>) : DungeonBrush() {
		override fun execute(x: Int, y: Int, phase: Phase, world: DungeonWorld) {
			if (children.isEmpty())
				return

			children.random(world.random).execute(x, y, phase, world)
		}
	}

	object Clear : DungeonBrush() {
		override fun execute(x: Int, y: Int, phase: Phase, world: DungeonWorld) {
			if (phase !== Phase.CLEAR)
				return

			// TODO: delete objects too?
			world.setLiquid(x, y, AbstractLiquidState.EMPTY)
			world.setForeground(x, y, BuiltinMetaMaterials.EMPTY)
			world.setForeground(x, y, BuiltinMetaMaterials.EMPTY_MOD)
			world.setBackground(x, y, BuiltinMetaMaterials.EMPTY)
			world.setBackground(x, y, BuiltinMetaMaterials.EMPTY_MOD)

			world.setDungeonID(x, y)
		}

		override fun toString(): String {
			return "Clear"
		}
	}

	// In original engine due to oversight in Brush::parse and parseFrontBrush
	// detailed
	data class Tile(
		val isBackground: Boolean,
		val material: Registry.Ref<TileDefinition> = BuiltinMetaMaterials.EMPTY.ref,
		val modifier: Registry.Ref<TileModifierDefinition> = BuiltinMetaMaterials.EMPTY_MOD.ref,
		val hueShift: Float = 0f,
		val modHueShift: Float = 0f,
		val color: TileColor = TileColor.DEFAULT,
	) : DungeonBrush() {
		constructor(isBackground: Boolean, data: JsonData) : this(
			isBackground,
			data.material, data.modifier,
			data.modHueShift, data.hueShift,
			TileColor.entries.firstOrNull { it.lowercase == data.colorVariant.lowercase() } ?: TileColor.entries[data.colorVariant.toIntOrNull() ?: throw JsonSyntaxException("Invalid color variant: ${data.colorVariant}")]
		)

		constructor(isBackground: Boolean, material: Registry.Ref<TileDefinition>, data: TiledData) : this(
			isBackground, material,
			data.modifier, data.hueshift,
			data.modhueshift,
			TileColor.entries.firstOrNull { it.lowercase == data.colorVariant.lowercase() } ?: TileColor.entries[data.colorVariant.toIntOrNull() ?: throw JsonSyntaxException("Invalid color variant: ${data.colorVariant}")]
		)

		@JsonFactory
		data class JsonData(
			val material: Registry.Ref<TileDefinition> = BuiltinMetaMaterials.EMPTY.ref,
			val modifier: Registry.Ref<TileModifierDefinition> = BuiltinMetaMaterials.EMPTY_MOD.ref,
			val hueShift: Float = 0f,
			val modHueShift: Float = 0f,
			val colorVariant: String = "0", // HOLY FUCKING SHIT
		)

		@JsonFactory
		data class TiledData(
			val modifier: Registry.Ref<TileModifierDefinition> = BuiltinMetaMaterials.EMPTY_MOD.ref,
			val hueshift: Float = 0f,
			val modhueshift: Float = 0f,
			val colorVariant: String = "0", // HOLY FUCKING SHIT
		)

		override fun execute(x: Int, y: Int, phase: Phase, world: DungeonWorld) {
			if (phase !== Phase.PLACE_WALLS)
				return

			if (isBackground)
				world.setBackground(x, y, material.orEmptyTile, hueShift, color)
			else
				world.setForeground(x, y, material.orEmptyTile, hueShift, color)

			if (!isBackground && material.orEmptyTile.value.collisionKind.isSolidCollision) {
				world.setLiquid(x, y, AbstractLiquidState.EMPTY)
			}

			if (modifier.isRealModifier) {
				if (isBackground)
					world.setBackground(x, y, modifier.orEmptyModifier, modHueShift)
				else
					world.setForeground(x, y, modifier.orEmptyModifier, modHueShift)
			}
		}
	}

	data class WorldObject(
		val obj: Registry.Ref<ObjectDefinition>,
		val direction: Direction = Direction.LEFT,
		val parameters: JsonObject = JsonObject(),
	) : DungeonBrush() {
		constructor(obj: Registry.Ref<ObjectDefinition>, extra: Extra) : this(obj, extra.direction, extra.parameters)

		@JsonFactory
		data class Extra(
			val direction: Direction = Direction.LEFT,
			val parameters: JsonObject = JsonObject(),
		)

		override fun execute(x: Int, y: Int, phase: Phase, world: DungeonWorld) {
			if (phase == Phase.PLACE_OBJECTS && obj.isPresent) {
				world.placeObject(x, y, obj.entry!!, direction, parameters)
			}
		}
	}

	data class Vehicle(val vehicle: String, val parameters: JsonObject = JsonObject()) : DungeonBrush() {
		override fun execute(x: Int, y: Int, phase: Phase, world: DungeonWorld) {
			LOGGER.warn("NYI: Vehicle at $x, $y")
		}
	}

	object BiomeItems : DungeonBrush() {
		override fun execute(x: Int, y: Int, phase: Phase, world: DungeonWorld) {
			if (phase === Phase.PLACE_BIOME_ITEMS) {
				world.placeBiomeItems(x, y)
			}
		}

		override fun toString(): String {
			return "Biome item"
		}
	}

	object BiomeTree : DungeonBrush() {
		override fun execute(x: Int, y: Int, phase: Phase, world: DungeonWorld) {
			if (phase === Phase.PLACE_BIOME_TREES) {
				world.placeBiomeTree(x, y)
			}
		}

		override fun toString(): String {
			return "Biome tree"
		}
	}

	data class DropItem(val item: ItemDescriptor, val randomize: Boolean) : DungeonBrush() {
		override fun execute(x: Int, y: Int, phase: Phase, world: DungeonWorld) {
			if (phase === Phase.DROP_ITEMS) {
				if (randomize) {
					world.dropRandomizedItem(x, y, item)
				} else {
					world.dropItem(x, y, item)
				}
			}
		}
	}

	data class NPC(val data: JsonObject) : DungeonBrush() {
		override fun execute(x: Int, y: Int, phase: Phase, world: DungeonWorld) {
			if (phase === Phase.PLACE_NPCS) {
				LOGGER.warn("NYI: NPC at $x, $y")
			}
		}
	}

	data class Stagehand(val data: JsonObject) : DungeonBrush() {
		override fun execute(x: Int, y: Int, phase: Phase, world: DungeonWorld) {
			if (phase === Phase.PLACE_NPCS) {
				LOGGER.warn("NYI: Stagehand at $x, $y")
			}
		}
	}

	@JsonFactory
	data class Surface(val variant: Int = 0, val mod: Registry.Ref<TileModifierDefinition> = BuiltinMetaMaterials.EMPTY_MOD.ref, @JsonIgnore val isBackground: Boolean = false) : DungeonBrush() {
		val material = BuiltinMetaMaterials.BIOME_META_MATERIALS.getOrNull(variant) ?: throw IllegalArgumentException("Invalid biome metamaterial $variant")

		override fun execute(x: Int, y: Int, phase: Phase, world: DungeonWorld) {
			if (phase == Phase.PLACE_WALLS) {
				if (!isBackground) // TODO: is this intentional?
					world.setForeground(x, y, material)

				world.setBackground(x, y, material)
			} else if (phase == Phase.PLACE_MODS) {
				if (mod.isRealModifier) {
					if (isBackground)
						world.setBackground(x, y, mod.orEmptyModifier)
					else
						world.setForeground(x, y, mod.orEmptyModifier)
				} else if (!isBackground && world.needsForegroundBiomeMod(x, y)) {
					world.setForeground(x, y, BuiltinMetaMaterials.BIOME_MOD)
				} else if (isBackground && world.needsBackgroundBiomeMod(x, y)) {
					world.setBackground(x, y, BuiltinMetaMaterials.BIOME_MOD)
				}
			}
		}
	}

	data class Liquid(val liquid: Registry.Ref<LiquidDefinition>, val level: Float = 1f, val isInfinite: Boolean = false) : DungeonBrush() {
		override fun execute(x: Int, y: Int, phase: Phase, world: DungeonWorld) {
			if (phase == Phase.PLACE_WALLS) {
				world.requestLiquid(x, y, MutableLiquidState(liquid.orEmptyLiquid, level, 1f, isInfinite))
			}
		}
	}

	@JsonFactory
	data class Wire(val group: String, val local: Boolean = false) : DungeonBrush() {
		override fun execute(x: Int, y: Int, phase: Phase, world: DungeonWorld) {
			if (phase === Phase.DO_WIRING) {
				world.placeWiring(x, y, group, local)
			}
		}
	}

	object info_player_start : DungeonBrush() {
		override fun execute(x: Int, y: Int, phase: Phase, world: DungeonWorld) {
			if (phase === Phase.PLACE_NPCS) {
				world.playerStart = Vector2d(x.toDouble(), y.toDouble())
			}
		}

		override fun toString(): String {
			return "info_player_start"
		}
	}

	data class DungeonID(val id: Int, val perTile: Boolean = false) : DungeonBrush() {
		init {
			require(id in 0 .. NO_DUNGEON_ID) { "Dungeon ID out of range: $id" }
		}

		override fun execute(x: Int, y: Int, phase: Phase, world: DungeonWorld) {
			if (phase == Phase.DUNGEON_ID) {
				if (perTile)
					world.setDungeonID(x, y, id)
				else
					world.setDungeonID(id)
			}
		}
	}

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