package ru.dbotthepony.kstarbound.defs.dungeon

import com.google.common.collect.ImmutableSet
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.gson.contains
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.tile.isEmptyLiquid
import ru.dbotthepony.kstarbound.defs.tile.isEmptyTile
import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyLiquid
import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyTile
import ru.dbotthepony.kstarbound.defs.tile.isObjectTile
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
import ru.dbotthepony.kstarbound.json.stream
import ru.dbotthepony.kstarbound.server.world.ServerWorld

@JsonAdapter(DungeonRule.Adapter::class)
abstract class DungeonRule {
	enum class Type(override val jsonName: String, val factory: (JsonArray) -> DungeonRule) : IStringSerializable {
		NOOP("", { Noop }) {
			override fun readTiled(json: JsonObject, flipX: Boolean, flipY: Boolean): DungeonRule? {
				return null
			}
		},

		MUST_CONTAIN_LIQUID("worldGenMustContainLiquid", { MustContainLiquid }) {
			override fun readTiled(json: JsonObject, flipX: Boolean, flipY: Boolean): DungeonRule? {
				if ("worldGenMustContainLiquid" in json)
					return MustContainLiquid

				return null
			}
		},

		MUST_NOT_CONTAIN_LIQUID("worldGenMustNotContainLiquid", { MustNotContainLiquid }) {
			override fun readTiled(json: JsonObject, flipX: Boolean, flipY: Boolean): DungeonRule? {
				if ("worldGenMustNotContainLiquid" in json)
					return MustNotContainLiquid

				return null
			}
		},

		HAVE_SOLID_FOREGROUND("worldGenMustContainSolidForeground", { HaveSolidForeground }) {
			override fun readTiled(json: JsonObject, flipX: Boolean, flipY: Boolean): DungeonRule? {
				if (json["layer"].asString == "front" && "worldGenMustContainSolid" in json)
					return HaveSolidForeground

				return null
			}
		},

		HAVE_EMPTY_FOREGROUND("worldGenMustContainAirForeground", { HaveEmptyForeground }) {
			override fun readTiled(json: JsonObject, flipX: Boolean, flipY: Boolean): DungeonRule? {
				if (json["layer"].asString == "front" && "worldGenMustContainAir" in json)
					return HaveEmptyForeground

				return null
			}
		},

		HAVE_SOLID_BACKGROUND("worldGenMustContainSolidBackground", { HaveSolidBackground }) {
			override fun readTiled(json: JsonObject, flipX: Boolean, flipY: Boolean): DungeonRule? {
				if (json["layer"].asString == "back" && "worldGenMustContainSolid" in json)
					return HaveSolidBackground

				return null
			}
		},

		HAVE_EMPTY_BACKGROUND("worldGenMustContainAirBackground", { HaveEmptyBackground }) {
			override fun readTiled(json: JsonObject, flipX: Boolean, flipY: Boolean): DungeonRule? {
				if (json["layer"].asString == "back" && "worldGenMustContainAir" in json)
					return HaveEmptyBackground

				return null
			}
		},

		ALLOW_OVERDRAWING("allowOverdrawing", { AllowOverdrawing }) {
			override fun readTiled(json: JsonObject, flipX: Boolean, flipY: Boolean): DungeonRule? {
				if ("allowOverdrawing" in json)
					return AllowOverdrawing

				return null
			}
		},

		IGNORE_PART_MAXIMUM("ignorePartMaximumRule", { IgnorePartMaximum }) {
			override fun readTiled(json: JsonObject, flipX: Boolean, flipY: Boolean): DungeonRule? {
				return null
			}
		},

		MAX_SPAWN_COUNT("maxSpawnCount", ::MaxSpawnCount) {
			override fun readTiled(json: JsonObject, flipX: Boolean, flipY: Boolean): DungeonRule? {
				return null
			}
		},

		DO_NOT_CONNECT_TO_PART("doNotConnectToPart", ::DoNotConnectToPart) {
			override fun readTiled(json: JsonObject, flipX: Boolean, flipY: Boolean): DungeonRule? {
				return null
			}
		},

		DO_NOT_COMBINE_WITH("doNotCombineWith", ::DoNotCombineWith) {
			override fun readTiled(json: JsonObject, flipX: Boolean, flipY: Boolean): DungeonRule? {
				return null
			}
		};

		abstract fun readTiled(json: JsonObject, flipX: Boolean = false, flipY: Boolean = false): DungeonRule?
	}

	open val requiresSolid: Boolean
		get() = false

	open val requiresLiquid: Boolean
		get() = false

	open val requiresOpen: Boolean
		get() = false

	open val allowOverdrawing: Boolean
		get() = false

	open val ignorePartMaximum: Boolean
		get() = false

	open fun doesNotConnectToPart(name: String): Boolean {
		return false
	}

	open fun checkTileCanPlace(x: Int, y: Int, world: DungeonWorld): Boolean {
		return true
	}

	open fun checkTileCanPlace(x: Int, y: Int, world: ServerWorld): Boolean {
		return true
	}

	open fun checkPartCombinationsAllowed(placements: Map<String, Int>): Boolean {
		return true
	}

	open fun allowSpawnCount(currentCount: Int): Boolean {
		return true
	}

	object Noop : DungeonRule()

	object MustContainLiquid : DungeonRule() {
		override val requiresLiquid: Boolean
			get() = true

		override fun checkTileCanPlace(x: Int, y: Int, world: DungeonWorld): Boolean {
			val cell = world.parent.template.cellInfo(x, y)
			return cell.oceanLiquid.isNotEmptyLiquid && cell.oceanLiquidLevel > y
		}

		override fun toString(): String {
			return "Must contain liquid"
		}
	}

	object MustNotContainLiquid : DungeonRule() {
		override fun checkTileCanPlace(x: Int, y: Int, world: DungeonWorld): Boolean {
			val cell = world.parent.template.cellInfo(x, y)
			return cell.oceanLiquid.isEmptyLiquid || cell.oceanLiquidLevel <= y
		}

		override fun toString(): String {
			return "Must not contain liquid"
		}
	}

	object HaveSolidForeground : DungeonRule() {
		override val requiresSolid: Boolean
			get() = true

		override fun checkTileCanPlace(x: Int, y: Int, world: DungeonWorld): Boolean {
			if (world.markSurfaceLevel != null)
				return y < world.markSurfaceLevel

			val cell = world.parent.getCell(x, y)

			if (cell.foreground.material.isObjectTile && world.isClearingTileEntityAt(x, y))
				return false

			return cell.foreground.material.isNotEmptyTile && !world.isClearingTileEntityAt(x, y)
		}

		override fun checkTileCanPlace(x: Int, y: Int, world: ServerWorld): Boolean {
			val cell = world.getCell(x, y)
			return cell.foreground.material.isNotEmptyTile
		}

		override fun toString(): String {
			return "Solid foreground"
		}
	}

	object HaveEmptyForeground : DungeonRule() {
		override val requiresOpen: Boolean
			get() = true

		override fun checkTileCanPlace(x: Int, y: Int, world: DungeonWorld): Boolean {
			if (world.markSurfaceLevel != null)
				return y >= world.markSurfaceLevel

			val cell = world.parent.getCell(x, y)
			return cell.foreground.material.isEmptyTile || cell.foreground.material.isObjectTile && world.isClearingTileEntityAt(x, y)
		}

		override fun checkTileCanPlace(x: Int, y: Int, world: ServerWorld): Boolean {
			val cell = world.getCell(x, y)
			return cell.foreground.material.isEmptyTile
		}

		override fun toString(): String {
			return "Empty foreground"
		}
	}

	object HaveSolidBackground : DungeonRule() {
		override val requiresSolid: Boolean
			get() = true

		override fun checkTileCanPlace(x: Int, y: Int, world: DungeonWorld): Boolean {
			if (world.markSurfaceLevel != null)
				return y < world.markSurfaceLevel

			val cell = world.parent.getCell(x, y)

			if (cell.background.material.isObjectTile && world.isClearingTileEntityAt(x, y))
				return false

			return cell.background.material.isNotEmptyTile
		}

		override fun checkTileCanPlace(x: Int, y: Int, world: ServerWorld): Boolean {
			val cell = world.getCell(x, y)
			return cell.background.material.isNotEmptyTile
		}

		override fun toString(): String {
			return "Solid background"
		}
	}

	object HaveEmptyBackground : DungeonRule() {
		override val requiresOpen: Boolean
			get() = true

		override fun checkTileCanPlace(x: Int, y: Int, world: DungeonWorld): Boolean {
			if (world.markSurfaceLevel != null)
				return y >= world.markSurfaceLevel

			val cell = world.parent.getCell(x, y)
			return cell.background.material.isEmptyTile || cell.background.material.isObjectTile && world.isClearingTileEntityAt(x, y)
		}

		override fun checkTileCanPlace(x: Int, y: Int, world: ServerWorld): Boolean {
			val cell = world.getCell(x, y)
			return cell.background.material.isEmptyTile
		}

		override fun toString(): String {
			return "Empty background"
		}
	}

	object AllowOverdrawing : DungeonRule() {
		override val allowOverdrawing: Boolean
			get() = true

		override fun toString(): String {
			return "Allow overdrawing"
		}
	}

	object IgnorePartMaximum : DungeonRule() {
		override val ignorePartMaximum: Boolean
			get() = true

		override fun toString(): String {
			return "Ignore part maximum"
		}
	}

	data class MaxSpawnCount(val count: Int) : DungeonRule() {
		constructor(json: JsonArray) : this(json[1].asJsonArray[0].asInt)

		override fun allowSpawnCount(currentCount: Int): Boolean {
			return currentCount < count
		}

		override fun toString(): String {
			return "Max spawn count = $count"
		}
	}

	data class DoNotConnectToPart(val parts: ImmutableSet<String>) : DungeonRule() {
		constructor(json: JsonArray) : this(json[1].asJsonArray.stream().map { it.asString }.collect(ImmutableSet.toImmutableSet()))

		override fun doesNotConnectToPart(name: String): Boolean {
			return name in parts
		}

		override fun toString(): String {
			return "Do not connect to $parts"
		}
	}

	data class DoNotCombineWith(val parts: ImmutableSet<String>) : DungeonRule() {
		constructor(json: JsonArray) : this(json[1].asJsonArray.stream().map { it.asString }.collect(ImmutableSet.toImmutableSet()))

		override fun checkPartCombinationsAllowed(placements: Map<String, Int>): Boolean {
			return placements.keys.none { it in parts }
		}

		override fun toString(): String {
			return "Do not combine with $parts"
		}
	}

	class Adapter(gson: Gson) : TypeAdapter<DungeonRule>() {
		private val types = gson.getAdapter(Type::class.java)

		override fun write(out: JsonWriter, value: DungeonRule) {
			throw UnsupportedOperationException("Dungeon Rules can't be serialized at this moment")
		}

		override fun read(`in`: JsonReader): DungeonRule {
			val read = Starbound.ELEMENTS_ADAPTER.arrays.read(`in`)

			if (read.isEmpty) {
				throw JsonSyntaxException("Empty rule")
			}

			val type = types.fromJsonTree(read[0])
			return type.factory(read)
		}
	}

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