package ru.dbotthepony.kstarbound.defs.world

import com.google.common.collect.ImmutableList
import com.google.gson.JsonArray
import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive
import com.google.gson.TypeAdapter
import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter
import it.unimi.dsi.fastutil.doubles.DoubleArrayList
import it.unimi.dsi.fastutil.ints.IntArrayList
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
import ru.dbotthepony.kommons.gson.JsonArrayCollector
import ru.dbotthepony.kommons.gson.consumeNull
import ru.dbotthepony.kommons.gson.set
import ru.dbotthepony.kommons.gson.value
import ru.dbotthepony.kommons.math.RGBAColor
import ru.dbotthepony.kommons.util.AABBi
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.Globals
import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.util.ListInterner
import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise
import ru.dbotthepony.kstarbound.util.random.nextRange
import ru.dbotthepony.kstarbound.world.WorldGeometry
import ru.dbotthepony.kstarbound.world.terrain.AbstractTerrainSelector
import ru.dbotthepony.kstarbound.world.terrain.TerrainSelectorType
import java.util.random.RandomGenerator
import kotlin.math.roundToInt
import kotlin.properties.Delegates

/**
 * While this class seems redundant, it is not.
 *
 * Every world must be represented by layers and regions,
 * hence this class serves as mutual point of reference between different
 * world types.
 *
 * Layer is a "line" than spans from 0 to world width, and has determined height.
 * Region, on other hand, is 1D section (slice) of layer, representing unique biome.
 * This structure allows to have diverse worlds, but regions are limited to cover only one
 * layer at time.
 *
 * While final region structure can be anything, generated terrestrial worlds
 * regions are generated in following pattern (per layer):
 * `[... [main region 0][sub region 0][main region 0] [main region 1][sub region 1][main region 1] ...]`
 *
 * Sucks that it has to virtually duplicate everything when creating from
 * [TerrestrialWorldParameters], though.
 *
 * World regions terrain selectors and selectors themselves are stored separately,
 * and hence create weird "index" structure in original code. This was done to
 * greatly cut down serialized form size. We, however, don't do this at runtime,
 * and reference biomes/terrain selectors directly.
 */
class WorldLayout {
	var worldSize: Vector2i by Delegates.notNull()
	var regionBlending: Double = 0.0
	var blendNoise: AbstractPerlinNoise? = null
	var blockNoise: BlockNoise? = null
	val terrainSelectors = ListInterner<AbstractTerrainSelector<*>>()
	val biomes = ListInterner<Biome>()

	val playerStartSearchRegions = ArrayList<AABBi>()
	var layers: MutableList<Layer> = ArrayList()
		private set

	private fun optimizeLayers() {
		// replaces arraylist with immutable list
		// which is faster when determining layer weighting
		// because it doesn't check for concurrent modifications
		// TODO: add binary search of layers
		// binary searching layers will save CPU cycles
		// when there are a lot of them
		layers = ImmutableList.copyOf(layers)
	}

	var loopX = true
	var loopY = false

	val worldGeometry by lazy {
		WorldGeometry(worldSize, loopX, loopY)
	}

	private object StartingRegionsToken : TypeToken<ArrayList<AABBi>>()

	@JsonFactory
	data class SerializedLayer(
		val yStart: Int,
		val boundaries: IntArrayList,
		val cells: ImmutableList<JsonObject>,
	)

	inner class Layer(val yStart: Int) : Comparable<Layer> {
		val boundaries = IntArrayList()
		val cells = ArrayList<Region>()

		override fun compareTo(other: Layer): Int {
			return yStart.compareTo(other.yStart)
		}

		fun toJson(isLegacy: Boolean): JsonObject {
			val data = JsonObject()

			data["yStart"] = yStart
			data["boundaries"] = boundaries.stream().map { JsonPrimitive(it) }.collect(JsonArrayCollector)
			data["cells"] = cells.stream().map { it.toJson(isLegacy) }.collect(JsonArrayCollector)

			return data
		}

		fun findContainingCell(x: Int): Pair<Int, Int> {
			if (boundaries.isEmpty) // entire world strip is within single cell
				return 0 to worldGeometry.x.cell(x)

			val wx = worldGeometry.x.cell(x)

			if (wx < boundaries.first()) {
				return 0 to wx
			} else if (wx > boundaries.last()) {
				return boundaries.size to wx
			} else {
				return (boundaries.indexOfLast { it <= wx } + 1) to wx
			}
		}

		fun leftCell(index: Int, x: Int): Pair<Int, Int> {
			if (index == 0) {
				// wrap around
				return (cells.size - 1) to (x + worldSize.x)
			} else {
				// normal left cell
				return (index - 1) to x
			}
		}

		fun rightCell(index: Int, x: Int): Pair<Int, Int> {
			if (index >= cells.size - 1) {
				// wrap around
				return 0 to (x - worldSize.x)
			} else {
				// normal right cell
				return (index + 1) to x
			}
		}

		fun weight(cellIndex: Int, x: Int): Double {
			val xMin = if (cellIndex > 0) boundaries.getInt(cellIndex - 1) else 0
			val xMax = if (cellIndex < boundaries.size) boundaries.getInt(cellIndex) else worldSize.x

			if (x > (xMin + xMax) / 2.0) {
				return (0.5 - (x - xMax) / regionBlending).coerceIn(0.0, 1.0)
			} else {
				return (0.5 - (xMin - x) / regionBlending).coerceIn(0.0, 1.0)
			}
		}
	}

	@JsonFactory
	data class RegionLiquids(
		val caveLiquid: Registry.Ref<LiquidDefinition> = BuiltinMetaMaterials.NO_LIQUID.ref,
		val caveLiquidSeedDensity: Double = 0.0,

		val oceanLiquid: Registry.Ref<LiquidDefinition> = BuiltinMetaMaterials.NO_LIQUID.ref,
		val oceanLiquidLevel: Int = 0,

		val encloseLiquids: Boolean = false,
		val fillMicrodungeons: Boolean = false,
	) {
		fun toLegacy(): RegionLiquidsLegacy {
			return RegionLiquidsLegacy(
				caveLiquid = caveLiquid.value?.liquidId ?: 0,
				caveLiquidSeedDensity = caveLiquidSeedDensity,
				oceanLiquid = oceanLiquid.value?.liquidId ?: 0,
				oceanLiquidLevel = oceanLiquidLevel,
				encloseLiquids = encloseLiquids,
				fillMicrodungeons = fillMicrodungeons,
			)
		}
	}

	@JsonFactory
	data class RegionLiquidsLegacy(
		val caveLiquid: Int = 0,
		val caveLiquidSeedDensity: Double = 0.0,

		val oceanLiquid: Int = 0,
		val oceanLiquidLevel: Int = 0,

		val encloseLiquids: Boolean = false,
		val fillMicrodungeons: Boolean = false,
	)

	@JsonFactory
	data class SerializedRegion(
		val terrainSelectorIndex: Int,
		val foregroundCaveSelectorIndex: Int,
		val backgroundCaveSelectorIndex: Int,

		val blockBiomeIndex: Int,
		val environmentBiomeIndex: Int,

		val subBlockSelectorIndexes: IntArrayList,
		val foregroundOreSelectorIndexes: IntArrayList,
		val backgroundOreSelectorIndexes: IntArrayList,
	) {
		init {
			check(terrainSelectorIndex != -1) { "terrainSelectorIndex is -1" }
			check(foregroundCaveSelectorIndex != -1) { "foregroundCaveSelectorIndex is -1" }
			check(backgroundCaveSelectorIndex != -1) { "backgroundCaveSelectorIndex is -1" }

			check(blockBiomeIndex != -1) { "blockBiomeIndex is -1" }
			check(environmentBiomeIndex != -1) { "environmentBiomeIndex is -1" }

			check(subBlockSelectorIndexes.none { it == -1 }) { "subBlockSelectorIndexes contains -1" }
			check(foregroundOreSelectorIndexes.none { it == -1 }) { "foregroundOreSelectorIndexes contains -1" }
			check(backgroundOreSelectorIndexes.none { it == -1 }) { "backgroundOreSelectorIndexes contains -1" }
		}
	}

	inner class Region(
		val terrainSelector: AbstractTerrainSelector<*>?,
		val foregroundCaveSelector: AbstractTerrainSelector<*>?,
		val backgroundCaveSelector: AbstractTerrainSelector<*>?,

		val blockBiome: Biome?,
		var environmentBiome: Biome?,

		val subBlockSelector: List<AbstractTerrainSelector<*>>,
		val foregroundOreSelector: List<AbstractTerrainSelector<*>>,
		val backgroundOreSelector: List<AbstractTerrainSelector<*>>,

		val regionLiquids: RegionLiquids,
	) {
		val blockBiomeIndex: Int
		var environmentBiomeIndex: Int

		init {
			if (blockBiome == null) {
				blockBiomeIndex = 0
			} else {
				val index = biomes.list.indexOf(blockBiome)
				check(index != -1) { "blockBiome != null but indexOf returned -1" }
				blockBiomeIndex = index + 1
			}

			if (environmentBiome == null) {
				environmentBiomeIndex = 0
			} else {
				val index = biomes.list.indexOf(environmentBiome)
				check(index != -1) { "environmentBiome != null but indexOf returned -1" }
				environmentBiomeIndex = index + 1
			}
		}

		fun toJson(isLegacy: Boolean): JsonObject {
			val data = SerializedRegion(
				terrainSelectorIndex = terrainSelectors.list.indexOf(terrainSelector) + 1,
				foregroundCaveSelectorIndex = terrainSelectors.list.indexOf(foregroundCaveSelector) + 1,
				backgroundCaveSelectorIndex = terrainSelectors.list.indexOf(backgroundCaveSelector) + 1,
				blockBiomeIndex = biomes.list.indexOf(blockBiome) + 1,
				environmentBiomeIndex = biomes.list.indexOf(environmentBiome) + 1,
				subBlockSelectorIndexes = subBlockSelector.stream().mapToInt { terrainSelectors.list.indexOf(it) + 1 }.collect(::IntArrayList, IntArrayList::add, IntArrayList::addAll),
				foregroundOreSelectorIndexes = foregroundOreSelector.stream().mapToInt { terrainSelectors.list.indexOf(it) + 1 }.collect(::IntArrayList, IntArrayList::add, IntArrayList::addAll),
				backgroundOreSelectorIndexes = backgroundOreSelector.stream().mapToInt { terrainSelectors.list.indexOf(it) + 1 }.collect(::IntArrayList, IntArrayList::add, IntArrayList::addAll),
			)

			val liquidData = (if (isLegacy) Starbound.gson.toJsonTree(regionLiquids.toLegacy()) else Starbound.gson.toJsonTree(regionLiquids)) as JsonObject
			val data2 = Starbound.gson.toJsonTree(data) as JsonObject

			for ((k, v) in data2.entrySet()) {
				liquidData[k] = v
			}

			return liquidData
		}
	}

	data class RegionParameters(
		val baseHeight: Int,
		val threatLevel: Double,
		val biomeName: String?,
		val terrainSelector: String?,
		val fgCaveSelector: String?,
		val bgCaveSelector: String?,
		val fgOreSelector: String?,
		val bgOreSelector: String?,
		val subBlockSelector: String?,
		val regionLiquids: RegionLiquids,
	)

	@JsonFactory
	data class SerializedForm(
		val worldSize: Vector2i,
		val regionBlending: Double,
		val blockNoise: BlockNoise? = null,
		val blendNoise: AbstractPerlinNoise? = null,
		val playerStartSearchRegions: List<AABBi>,
		val biomes: List<Biome>,
		val terrainSelectors: List<AbstractTerrainSelector<*>>,
		val layers: JsonArray,
		val loopX: Boolean = true,
		val loopY: Boolean = false,
	)

	fun toJson(): JsonObject {
		return Starbound.gson.toJsonTree(SerializedForm(
			worldSize, regionBlending, blockNoise, blendNoise,
			playerStartSearchRegions, biomes.list, terrainSelectors.list,
			layers = layers.stream().map { it.toJson(Starbound.IS_LEGACY_JSON) }.collect(JsonArrayCollector),
			loopX, loopY
		)) as JsonObject
	}

	fun fromJson(data: JsonObject): WorldLayout {
		val load = Starbound.gson.fromJson(data, SerializedForm::class.java)

		worldSize = load.worldSize
		regionBlending = load.regionBlending
		blockNoise = load.blockNoise
		blendNoise = load.blendNoise
		loopX = load.loopX
		loopY = load.loopY
		playerStartSearchRegions.addAll(load.playerStartSearchRegions)

		load.layers.forEach {
			val datalayer = Starbound.gson.fromJson(it, SerializedLayer::class.java)
			val layer = Layer(datalayer.yStart)
			layer.boundaries.addAll(datalayer.boundaries)

			datalayer.cells.forEach {
				val region = Starbound.gson.fromJson(it, SerializedRegion::class.java)

				load.terrainSelectors.forEach { terrainSelectors.intern(it) }
				load.biomes.forEach { biomes.intern(it) }

				layer.cells.add(Region(
					terrainSelector = load.terrainSelectors.getOrNull(region.terrainSelectorIndex - 1),
					foregroundCaveSelector = load.terrainSelectors.getOrNull(region.foregroundCaveSelectorIndex - 1),
					backgroundCaveSelector = load.terrainSelectors.getOrNull(region.backgroundCaveSelectorIndex - 1),
					blockBiome = load.biomes.getOrNull(region.blockBiomeIndex - 1),
					environmentBiome = load.biomes.getOrNull(region.environmentBiomeIndex - 1),
					subBlockSelector = region.subBlockSelectorIndexes.map { load.terrainSelectors.getOrNull(it - 1) }.filterNotNull(),
					foregroundOreSelector = region.foregroundOreSelectorIndexes.map { load.terrainSelectors.getOrNull(it - 1) }.filterNotNull(),
					backgroundOreSelector = region.backgroundOreSelectorIndexes.map { load.terrainSelectors.getOrNull(it - 1) }.filterNotNull(),
					regionLiquids = Starbound.gson.fromJson(it, RegionLiquids::class.java),
				))
			}
		}

		optimizeLayers()

		return this
	}

	private fun buildRegion(random: RandomGenerator, params: RegionParameters): Region {
		var terrainSelector: AbstractTerrainSelector<*>? = null
		var foregroundCaveSelector: AbstractTerrainSelector<*>? = null
		var backgroundCaveSelector: AbstractTerrainSelector<*>? = null

		val terrainBase = TerrainSelectorParameters(worldSize.x, params.baseHeight.toDouble(), worldHeight = worldSize.y)
		val terrain = terrainBase.withSeed(random.nextLong())
		val fg = terrainBase.withSeed(random.nextLong())
		val bg = terrainBase.withSeed(random.nextLong())

		if (params.terrainSelector != null)
			terrainSelector = terrainSelectors.intern(TerrainSelectorType.named(params.terrainSelector, terrain))
		if (params.fgCaveSelector != null)
			foregroundCaveSelector = terrainSelectors.intern(TerrainSelectorType.named(params.fgCaveSelector, fg))
		if (params.bgCaveSelector != null)
			backgroundCaveSelector = terrainSelectors.intern(TerrainSelectorType.named(params.bgCaveSelector, bg))

		val subBlockSelector = ArrayList<AbstractTerrainSelector<*>>()
		val foregroundOreSelector = ArrayList<AbstractTerrainSelector<*>>()
		val backgroundOreSelector = ArrayList<AbstractTerrainSelector<*>>()

		var biome: Biome? = null

		if (params.biomeName != null) {
			biome = biomes.intern(Registries.biomes.getOrThrow(params.biomeName).value.create(random, params.baseHeight, params.threatLevel))

			if (params.subBlockSelector != null) {
				for (i in 0 until biome.subBlocks.size) {
					subBlockSelector.add(terrainSelectors.intern(TerrainSelectorType.named(params.subBlockSelector, terrainBase.withSeed(random.nextLong()))))
				}

				for ((ore, commonality) in biome.ores) {
					val oreParams = terrainBase.withCommonality(commonality)

					if (params.fgOreSelector != null) {
						foregroundOreSelector.add(terrainSelectors.intern(TerrainSelectorType.named(params.fgOreSelector, oreParams.withSeed(random.nextLong()))))
					}

					if (params.bgOreSelector != null) {
						backgroundOreSelector.add(terrainSelectors.intern(TerrainSelectorType.named(params.bgOreSelector, oreParams.withSeed(random.nextLong()))))
					}
				}
			}
		}

		return Region(
			terrainSelector = terrainSelector,
			foregroundCaveSelector = foregroundCaveSelector,
			backgroundCaveSelector = backgroundCaveSelector,

			subBlockSelector = subBlockSelector,
			foregroundOreSelector = foregroundOreSelector,
			backgroundOreSelector = backgroundOreSelector,

			blockBiome = biome,
			environmentBiome = biome,

			regionLiquids = params.regionLiquids
		)
	}

	fun addLayer(random: RandomGenerator, yStart: Int, params: RegionParameters) {
		val layer = Layer(yStart)
		layer.cells.add(buildRegion(random, params))
		layers.add(layer)
	}

	fun addLayer(
		random: RandomGenerator, yStart: Int, yBase: Int, primaryBiome: String,
		primaryRegionParams: RegionParameters, primarySubRegionParams: RegionParameters,
		secondaryRegions: List<RegionParameters>, secondarySubRegions: List<RegionParameters>,
		secondaryRegionSize: Vector2d, subRegionSize: Vector2d
	) {
		require(secondaryRegions.size == secondarySubRegions.size) { "${secondaryRegions.size} != ${secondarySubRegions.size}" }
		require(subRegionSize.y <= 1.0) { "subRegionSize.y > 1.0: ${subRegionSize.y}" }

		val layer = Layer(yStart)
		val relativeRegionSizes = DoubleArrayList()
		var totalRelativeSize = 0.0

		val primaryEnvironment = buildRegion(random, primaryRegionParams)
		val spawnBiomes = ObjectOpenHashSet<Biome>()

		fun addRegion(params: RegionParameters, subParams: RegionParameters, regionSizeRange: Vector2d) {
			val region = buildRegion(random, params)
			val subRegion = buildRegion(random, params)

			if (!Globals.terrestrialWorlds.useSecondaryEnvironmentBiomeIndex) {
				region.environmentBiome = primaryEnvironment.environmentBiome
				region.environmentBiomeIndex = primaryEnvironment.environmentBiomeIndex
			}

			subRegion.environmentBiome = region.environmentBiome
			subRegion.environmentBiomeIndex = region.environmentBiomeIndex

			if (params.biomeName == primaryBiome && region.blockBiome != null)
				spawnBiomes.add(region.blockBiome)

			if (subParams.biomeName == primaryBiome && subRegion.blockBiome != null)
				spawnBiomes.add(subRegion.blockBiome)

			layer.cells.add(region)
			layer.cells.add(subRegion)
			layer.cells.add(region)

			var regionRelativeSize = random.nextRange(regionSizeRange)
			var subRegionRelativeSize = random.nextRange(subRegionSize)

			totalRelativeSize += regionRelativeSize
			subRegionRelativeSize *= regionRelativeSize
			regionRelativeSize -= subRegionRelativeSize

			relativeRegionSizes.add(regionRelativeSize / 2.0)
			relativeRegionSizes.add(subRegionRelativeSize)
			relativeRegionSizes.add(regionRelativeSize / 2.0)
		}

		// construct list of region cells and relative sizes
		addRegion(primaryRegionParams, primarySubRegionParams, Vector2d.POSITIVE_XY)

		for ((i, region) in secondaryRegions.withIndex()) {
			addRegion(region, secondarySubRegions[i], secondaryRegionSize)
		}

		// construct boundaries based on normalized sizes
		var nextBoundary = random.nextInt(0, worldSize.x)
		layer.boundaries.add(nextBoundary)

		for (i in 0 until relativeRegionSizes.size - 1) {
			nextBoundary += (worldSize.x * relativeRegionSizes.getDouble(i) / totalRelativeSize).roundToInt()
			layer.boundaries.add(nextBoundary)
		}

		// wrap cells + boundaries
		while (layer.boundaries.last() > worldSize.x) {
			layer.cells.add(0, layer.cells.removeLast())
			layer.boundaries.add(0, layer.boundaries.removeLast() - worldSize.x)
		}

		layer.cells.add(0, layer.cells.last())
		val yRange = Globals.worldTemplate.playerStartSearchYRange
		var i = 0
		var lastBoundary = 0

		for (region in layer.cells) {
			nextBoundary = if (i < layer.boundaries.size) layer.boundaries.getInt(i) else worldSize.x

			if (region.blockBiome in spawnBiomes) {
				playerStartSearchRegions.add(AABBi(
					Vector2i(lastBoundary, (yBase - yRange).coerceAtLeast(0)),
					Vector2i(nextBoundary, (yBase + yRange).coerceAtMost(worldSize.y)),
				))
			}

			lastBoundary = nextBoundary
			i++
		}

		layers.add(layer)
	}

	fun finalize(skyColoring: RGBAColor) {
		layers.sort()

		for (biome in biomes) {
			biome.parallax?.fadeToSkyColor(skyColoring)
		}

		optimizeLayers()
	}

	data class RegionWeighting(val weight: Double, val xValue: Int, val region: Region)

	fun getWeighting(x: Int, y: Int): List<RegionWeighting> {
		val weighting = ArrayList<RegionWeighting>()

		fun addLayerWeighting(layer: Layer, x: Int, weightFactor: Double) {
			if (layer.cells.isEmpty())
				return

			val (innerIndex, innerX) = layer.findContainingCell(x)
			var innerWeight = layer.weight(innerIndex, innerX)

			val (leftIndex, leftX) = layer.leftCell(innerIndex, innerX)
			var leftWeight = layer.weight(leftIndex, leftX)

			val (rightIndex, rightX) = layer.leftCell(innerIndex, innerX)
			var rightWeight = layer.weight(rightIndex, rightX)

			val totalWeight = innerWeight + leftWeight + rightWeight
			if (totalWeight <= 0.0)
				return

			val factor = weightFactor / totalWeight
			innerWeight *= factor
			leftWeight *= factor
			rightWeight *= factor

			if (innerWeight > 0.0)
				weighting.add(RegionWeighting(innerWeight, innerX, layer.cells[innerIndex]))

			if (leftWeight > 0.0)
				weighting.add(RegionWeighting(leftWeight, leftX, layer.cells[leftIndex]))

			if (rightWeight > 0.0)
				weighting.add(RegionWeighting(rightWeight, rightX, layer.cells[rightIndex]))
		}

		val yi: Int

		if (y == layers.first().yStart) {
			yi = 0
		} else if (y < layers.first().yStart) {
			return emptyList()
		} else if (y >= layers.last().yStart) {
			yi = layers.size - 1
		} else {
			yi = layers.indexOfFirst { it.yStart >= y } - 1
		}

		val layer = layers[yi]

		if (y - layer.yStart < regionBlending / 2.0) {
			if (yi == 0) {
				addLayerWeighting(layer, x, 1.0)
			} else {
				val weight = 0.5 + (y - layer.yStart) / regionBlending
				addLayerWeighting(layer, x, weight)
				addLayerWeighting(layers[yi - 1], x, 1.0 - weight)
			}
		} else {
			val nextLayer = layers.getOrNull(yi + 1)

			if (nextLayer == null || y <= nextLayer.yStart - (regionBlending / 2.0)) {
				addLayerWeighting(layer, x, 1.0)
			} else {
				val weight = 0.5 + (nextLayer.yStart - y) / regionBlending
				addLayerWeighting(layer, x, 1.0 - weight)
				addLayerWeighting(nextLayer, x, weight)
			}
		}

		// Need to return weighting in order of greatest to least
		weighting.sortWith { o1, o2 ->
			o2.weight.compareTo(o1.weight)
		}

		return weighting
	}

	companion object : TypeAdapter<WorldLayout>() {
		override fun write(out: JsonWriter, value: WorldLayout?) {
			if (value == null)
				out.nullValue()
			else
				out.value(value.toJson())
		}

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

			val params = WorldLayout()
			params.fromJson(Starbound.ELEMENTS_ADAPTER.objects.read(`in`))
			return params
		}
	}
}
