package ru.dbotthepony.kstarbound.defs.world

import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableSet
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 it.unimi.dsi.fastutil.ints.IntArrayList
import ru.dbotthepony.kommons.gson.contains
import ru.dbotthepony.kommons.gson.get
import ru.dbotthepony.kommons.gson.getArray
import ru.dbotthepony.kommons.gson.getObject
import ru.dbotthepony.kommons.gson.set
import ru.dbotthepony.kommons.io.readCollection
import ru.dbotthepony.kommons.io.writeBinaryString
import ru.dbotthepony.kommons.io.writeCollection
import ru.dbotthepony.kommons.util.Either
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.collect.WeightedList
import ru.dbotthepony.kstarbound.defs.PerlinNoiseParameters
import ru.dbotthepony.kstarbound.defs.dungeon.DungeonDefinition
import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition
import ru.dbotthepony.kstarbound.defs.tile.isEmptyLiquid
import ru.dbotthepony.kstarbound.fromJson
import ru.dbotthepony.kstarbound.io.readInternedString
import ru.dbotthepony.kstarbound.io.readVector2d
import ru.dbotthepony.kstarbound.io.writeStruct2d
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.json.getAdapter
import ru.dbotthepony.kstarbound.json.mergeJson
import ru.dbotthepony.kstarbound.json.pairAdapter
import ru.dbotthepony.kstarbound.json.readJsonElement
import ru.dbotthepony.kstarbound.json.stream
import ru.dbotthepony.kstarbound.json.writeJsonElement
import ru.dbotthepony.kstarbound.util.binnedChoice
import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise
import ru.dbotthepony.kstarbound.util.random.nextRange
import ru.dbotthepony.kstarbound.util.random.random
import java.io.DataInputStream
import java.io.DataOutputStream
import java.util.random.RandomGenerator
import kotlin.properties.Delegates

class TerrestrialWorldParameters : VisitableWorldParameters() {
	@JsonFactory
	data class Generic(
		// TODO: while engine supports arbitrary gravity direction,
		// TODO: json files don't have the same support level
		val gravityRange: Vector2d,
		val dayLengthRange: Vector2d,
		val threatRange: Vector2d = Vector2d(1.0, 1.0),
		val size: Vector2i,
		val overrideTech: ImmutableSet<String>? = null,
		val globalDirectives: ImmutableSet<String>? = null,
		val disableDeathDrops: Boolean = false,
		val beamUpRule: BeamUpRule = BeamUpRule.SURFACE,
		val worldEdgeForceRegions: WorldEdgeForceRegion = WorldEdgeForceRegion.TOP,

		val blockNoise: BlockNoiseConfig? = null,
		val blendNoise: PerlinNoiseParameters? = null,
		val blendSize: Double,
	)

	@JsonFactory
	data class Region(
		val biome: String,

		val blockSelector: String,
		val fgCaveSelector: String,
		val bgCaveSelector: String,
		val fgOreSelector: String,
		val bgOreSelector: String,
		val subBlockSelector: String,

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

		val encloseLiquids: Boolean,
		val fillMicrodungeons: Boolean,
	) {
		constructor(stream: DataInputStream) : this(
			stream.readInternedString(),

			stream.readInternedString(),
			stream.readInternedString(),
			stream.readInternedString(),
			stream.readInternedString(),
			stream.readInternedString(),
			stream.readInternedString(),

			Registries.liquid.ref(stream.readUnsignedByte()),
			stream.readFloat().toDouble(),

			Registries.liquid.ref(stream.readUnsignedByte()),
			stream.readInt(),

			stream.readBoolean(),
			stream.readBoolean(),
		)

		fun write(stream: DataOutputStream) {
			stream.writeBinaryString(biome)

			stream.writeBinaryString(blockSelector)
			stream.writeBinaryString(fgCaveSelector)
			stream.writeBinaryString(bgCaveSelector)
			stream.writeBinaryString(fgOreSelector)
			stream.writeBinaryString(bgOreSelector)
			stream.writeBinaryString(subBlockSelector)

			stream.writeByte(caveLiquid.entry?.id ?: 0)
			stream.writeFloat(caveLiquidSeedDensity.toFloat())
			stream.writeByte(oceanLiquid.entry?.id ?: 0)
			stream.writeInt(oceanLiquidLevel)

			stream.writeBoolean(encloseLiquids)
			stream.writeBoolean(fillMicrodungeons)
		}
	}

	@JsonFactory
	data class Layer(
		val layerMinHeight: Int,
		val layerBaseHeight: Int,

		val dungeons: ImmutableSet<Registry.Ref<DungeonDefinition>>,
		val dungeonXVariance: Int,

		val primaryRegion: Region,
		val primarySubRegion: Region,

		val secondaryRegions: ImmutableList<Region>,
		val secondarySubRegions: ImmutableList<Region>,

		val secondaryRegionSizeRange: Vector2d,
		val subRegionSizeRange: Vector2d,
	) {
		constructor(stream: DataInputStream) : this(
			stream.readInt(),
			stream.readInt(),
			ImmutableSet.copyOf(stream.readCollection { Registries.dungeons.ref(readInternedString()) }),
			stream.readInt(),

			Region(stream),
			Region(stream),

			ImmutableList.copyOf(stream.readCollection { Region(this) }),
			ImmutableList.copyOf(stream.readCollection { Region(this) }),

			stream.readVector2d(true),
			stream.readVector2d(true),
		)

		fun write(stream: DataOutputStream) {
			stream.writeInt(layerMinHeight)
			stream.writeInt(layerBaseHeight)

			stream.writeCollection(dungeons) { writeBinaryString(it.key.left()) }
			stream.writeInt(dungeonXVariance)

			primaryRegion.write(stream)
			primarySubRegion.write(stream)

			stream.writeCollection(secondaryRegions) { it.write(this) }
			stream.writeCollection(secondarySubRegions) { it.write(this) }

			stream.writeStruct2d(secondaryRegionSizeRange, true)
			stream.writeStruct2d(subRegionSizeRange, true)
		}
	}

	override fun fromJson(data: JsonObject) {
		super.fromJson(data)

		val read = Starbound.gson.fromJson(data, StoreData::class.java)

		primaryBiome = read.primaryBiome
		surfaceLiquid = read.surfaceLiquid?.map({ Registries.liquid.ref(it) }, { Registries.liquid.ref(it) }) ?: BuiltinMetaMaterials.NO_LIQUID.ref
		sizeName = read.sizeName
		hueShift = read.hueShift
		skyColoring = read.skyColoring
		dayLength = read.dayLength
		blockNoiseConfig = read.blockNoise
		blendNoiseConfig = read.blendNoise
		blendSize = read.blendSize
		spaceLayer = read.spaceLayer
		atmosphereLayer = read.atmosphereLayer
		surfaceLayer = read.surfaceLayer
		subsurfaceLayer = read.subsurfaceLayer
		undergroundLayers = read.undergroundLayers
		coreLayer = read.coreLayer
	}

	override fun toJson(data: JsonObject, isLegacy: Boolean) {
		super.toJson(data, isLegacy)

		val primarySurfaceLiquid: Either<Int, String>?

		// original engine operate on liquids solely with IDs
		// and we also need to network this json to legacy clients.
		// what a shame :JC:.
		if (isLegacy) {
			primarySurfaceLiquid = this.surfaceLiquid.entry?.id?.let { Either.left(it) } ?: Either.left(0)
		} else {
			primarySurfaceLiquid = this.surfaceLiquid.entry?.let { Either.right(it.key) } ?: this.surfaceLiquid.key.swap()
		}

		val store = StoreData(
			primaryBiome = primaryBiome,
			surfaceLiquid = primarySurfaceLiquid,
			sizeName = sizeName,
			hueShift = hueShift,
			skyColoring = skyColoring,
			dayLength = dayLength,
			blockNoise = blockNoiseConfig,
			blendNoise = blendNoiseConfig,
			blendSize = blendSize,
			spaceLayer = spaceLayer,
			atmosphereLayer = atmosphereLayer,
			surfaceLayer = surfaceLayer,
			subsurfaceLayer = subsurfaceLayer,
			undergroundLayers = undergroundLayers,
			coreLayer = coreLayer,
		)

		for ((k, v) in (Starbound.gson.toJsonTree(store) as JsonObject).entrySet()) {
			data[k] = v
		}
	}

	@JsonFactory
	data class StoreData(
		val primaryBiome: String,
		val surfaceLiquid: Either<Int, String>? = null,
		val sizeName: String,
		val hueShift: Double,
		val skyColoring: SkyColoring,
		val dayLength: Double,
		val blockNoise: BlockNoiseConfig? = null,
		val blendNoise: PerlinNoiseParameters? = null,
		val blendSize: Double,
		val spaceLayer: Layer,
		val atmosphereLayer: Layer,
		val surfaceLayer: Layer,
		val subsurfaceLayer: Layer,
		val undergroundLayers: List<Layer>,
		val coreLayer: Layer,
	)

	var primaryBiome: String by Delegates.notNull()
		private set
	var surfaceLiquid: Registry.Ref<LiquidDefinition> = BuiltinMetaMaterials.NO_LIQUID.ref
		private set
	var sizeName: String by Delegates.notNull()
		private set
	var hueShift: Double by Delegates.notNull()
		private set
	var skyColoring: SkyColoring by Delegates.notNull()
		private set
	var dayLength: Double by Delegates.notNull()
		private set
	var blockNoiseConfig: BlockNoiseConfig? = null
		private set
	var blendNoiseConfig: PerlinNoiseParameters? = null
		private set
	var blendSize: Double by Delegates.notNull()
		private set

	var spaceLayer: Layer by Delegates.notNull()
		private set
	var atmosphereLayer: Layer by Delegates.notNull()
		private set
	var surfaceLayer: Layer by Delegates.notNull()
		private set
	var subsurfaceLayer: Layer by Delegates.notNull()
		private set
	var undergroundLayers: List<Layer> by Delegates.notNull()
		private set
	var coreLayer: Layer by Delegates.notNull()
		private set

	val layers: List<Layer> get() {
		val layers = ArrayList<Layer>()

		layers.add(spaceLayer)
		layers.add(atmosphereLayer)
		layers.add(surfaceLayer)
		layers.addAll(undergroundLayers)
		layers.add(coreLayer)

		return layers
	}

	override val type: VisitableWorldParametersType
		get() = VisitableWorldParametersType.TERRESTRIAL

	override fun read0(stream: DataInputStream) {
		super.read0(stream)

		primaryBiome = stream.readInternedString()
		surfaceLiquid = Registries.liquid.ref(stream.readUnsignedByte())
		sizeName = stream.readInternedString()
		hueShift = stream.readFloat().toDouble()
		skyColoring = SkyColoring.read(stream, true)
		dayLength = stream.readFloat().toDouble()
		blendSize = stream.readFloat().toDouble()

		blockNoiseConfig = Starbound.gson.fromJson(stream.readJsonElement())
		blendNoiseConfig = Starbound.gson.fromJson(stream.readJsonElement())

		spaceLayer = Layer(stream)
		atmosphereLayer = Layer(stream)
		surfaceLayer = Layer(stream)
		subsurfaceLayer = Layer(stream)
		undergroundLayers = stream.readCollection { Layer(this) }
		coreLayer = Layer(stream)
	}

	override fun write0(stream: DataOutputStream) {
		super.write0(stream)

		stream.writeBinaryString(primaryBiome)
		stream.writeByte(surfaceLiquid.entry?.id ?: 0)
		stream.writeBinaryString(sizeName)
		stream.writeFloat(hueShift.toFloat())
		skyColoring.write(stream, true)
		stream.writeFloat(dayLength.toFloat())
		stream.writeFloat(blendSize.toFloat())

		Starbound.legacyJson {
			stream.writeJsonElement(Starbound.gson.toJsonTree(blockNoiseConfig))
			stream.writeJsonElement(Starbound.gson.toJsonTree(blendNoiseConfig))
		}

		spaceLayer.write(stream)
		atmosphereLayer.write(stream)
		surfaceLayer.write(stream)
		subsurfaceLayer.write(stream)
		stream.writeCollection(undergroundLayers) { it.write(this) }
		coreLayer.write(stream)
	}

	// why
	override fun createLayout(random: RandomGenerator): WorldLayout {
		val layout = WorldLayout()
		layout.worldSize = worldSize

		fun addLayer(layer: Layer) {
			fun bake(region: Region): WorldLayout.RegionParameters {
				return WorldLayout.RegionParameters(
					layer.layerBaseHeight,
					threatLevel,
					region.biome,
					region.blockSelector,
					region.fgCaveSelector,
					region.bgCaveSelector,
					region.fgOreSelector,
					region.bgOreSelector,
					region.subBlockSelector,
					WorldLayout.RegionLiquids(
						region.caveLiquid,
						region.caveLiquidSeedDensity,
						region.oceanLiquid,
						region.oceanLiquidLevel,
						region.encloseLiquids,
						region.fillMicrodungeons,
					)
				)
			}

			val primaryRegionParams = bake(layer.primaryRegion)
			val primarySubRegionParams = bake(layer.primarySubRegion)
			val secondary = layer.secondaryRegions.map { bake(it) }
			val secondarySubRegions = layer.secondarySubRegions.map { bake(it) }

			layout.addLayer(
				random, layer.layerMinHeight, layer.layerBaseHeight,
				primaryBiome,
				primaryRegionParams, primarySubRegionParams,
				secondary, secondarySubRegions,
				layer.secondaryRegionSizeRange, layer.subRegionSizeRange)
		}

		addLayer(coreLayer)
		undergroundLayers.asReversed().forEach { addLayer(it) }
		addLayer(subsurfaceLayer)
		addLayer(surfaceLayer)
		addLayer(atmosphereLayer)
		addLayer(spaceLayer)

		layout.regionBlending = blendSize
		layout.blockNoise = blockNoiseConfig?.build(random)
		layout.blendNoise = blendNoiseConfig?.let { AbstractPerlinNoise.of(it).also { it.init(random.nextLong()) } }

		layout.finalize(skyColoring.mainColor)

		return layout
	}

	companion object {
		private val biomePairs by lazy { Starbound.gson.pairAdapter<Double, JsonArray>() }
		private val vectors2d by lazy { Starbound.gson.getAdapter(Vector2d::class.java) }
		private val vectors2i by lazy { Starbound.gson.getAdapter(Vector2i::class.java) }
		private val dungeonPools by lazy { Starbound.gson.getAdapter<WeightedList<Registry.Ref<DungeonDefinition>>>() }

		fun generate(typeName: String, sizeName: String, seed: Long): TerrestrialWorldParameters {
			return generate(typeName, sizeName, random(seed))
		}

		fun generate(typeName: String, sizeName: String, random: RandomGenerator): TerrestrialWorldParameters {
			val config = Globals.terrestrialWorlds.planetDefaults.deepCopy()
			mergeJson(config, Globals.terrestrialWorlds.planetSizes[sizeName] ?: throw NoSuchElementException("Unknown world size name $sizeName"))
			mergeJson(config, Globals.terrestrialWorlds.planetTypes[typeName] ?: throw NoSuchElementException("Unknown world type name $typeName"))

			val params = Starbound.gson.fromJson(config, Generic::class.java)

			val threadLevel = random.nextRange(params.threatRange)
			val layers = config.getObject("layers")

			fun readRegion(regionConfig: JsonObject, layerBaseHeight: Int): Region {
				val biome = regionConfig["biome"]
					.asJsonArray
					.stream()
					.map { biomePairs.fromJsonTree(it) }
					.binnedChoice(threadLevel)
					.map { it.stream().map { it.asString }.toList() }
					.orElse(listOf())
					.random(random)

				val blockSelector = regionConfig.getArray("blockSelector").random(random).asString
				val fgCaveSelector = regionConfig.getArray("fgCaveSelector").random(random).asString
				val bgCaveSelector = regionConfig.getArray("bgCaveSelector").random(random).asString
				val fgOreSelector = regionConfig.getArray("fgOreSelector").random(random).asString
				val bgOreSelector = regionConfig.getArray("bgOreSelector").random(random).asString
				val subBlockSelector = regionConfig.getArray("subBlockSelector").random(random).asString

				// original engine here translates liquid name into liquid id
				// we, however, do not, since we prefer to operate on registry names and not registry ids
				val caveLiquid: String?
				val caveLiquidSeedDensity: Double
				val oceanLiquid: String?
				val oceanLiquidLevel: Int

				// "can be empty for no liquid"
				// yeah thanks fucking fuck
				// I hope whoever designed original loading code will have
				// their software blown up by C++'s "feature" to implicitly construct classes/structs/values
				// out of thin air
				if ("caveLiquid" in regionConfig) {
					caveLiquid = regionConfig.getArray("caveLiquid").random(random) { JsonPrimitive("") }.asString.let { if (it.isBlank()) null else it }
					val range = Starbound.gson.fromJson(regionConfig["caveLiquidSeedDensityRange"], Vector2d::class.java)
					caveLiquidSeedDensity = if (range.x == range.y) range.x else random.nextDouble(range.x, range.y)
				} else {
					caveLiquid = null
					caveLiquidSeedDensity = 0.0
				}

				if ("oceanLiquid" in regionConfig) {
					oceanLiquid = regionConfig.getArray("oceanLiquid").random(random) { JsonPrimitive("") }.asString.let { if (it.isBlank()) null else it }
					oceanLiquidLevel = regionConfig.get("oceanLevelOffset", 0) + layerBaseHeight
				} else {
					oceanLiquid = null
					oceanLiquidLevel = 0
				}

				val encloseLiquids = regionConfig.get("encloseLiquids", false)
				val fillMicrodungeons = regionConfig.get("fillMicrodungeons", false)

				return Region(
					biome = biome,
					blockSelector = blockSelector,
					fgCaveSelector = fgCaveSelector,
					bgCaveSelector = bgCaveSelector,
					fgOreSelector = fgOreSelector,
					bgOreSelector = bgOreSelector,
					subBlockSelector = subBlockSelector,
					caveLiquid = caveLiquid?.let { Registries.liquid.ref(it) } ?: BuiltinMetaMaterials.NO_LIQUID.ref,
					caveLiquidSeedDensity = caveLiquidSeedDensity,
					oceanLiquid = oceanLiquid?.let { Registries.liquid.ref(it) } ?: BuiltinMetaMaterials.NO_LIQUID.ref,
					oceanLiquidLevel = oceanLiquidLevel,
					encloseLiquids = encloseLiquids,
					fillMicrodungeons = fillMicrodungeons,
				)
			}

			fun makeRegion(name: String, baseHeight: Int): Pair<Region, Region> {
				val primaryRegionJson = Globals.terrestrialWorlds.regionDefaults.deepCopy()
				mergeJson(primaryRegionJson, Globals.terrestrialWorlds.regionTypes[name]!!)

				val region = readRegion(primaryRegionJson, baseHeight)
				val subRegionList = primaryRegionJson.getArray("subRegion")

				val subRegion = readRegion(if (subRegionList.isEmpty) {
					primaryRegionJson
				} else {
					val result = Globals.terrestrialWorlds.regionDefaults.deepCopy()
					mergeJson(result, Globals.terrestrialWorlds.regionTypes[subRegionList.random(random).asString]!!)
					result
				}, baseHeight)

				return region to subRegion
			}

			fun readLayer(layerName: String): Layer? {
				if (layerName !in layers)
					return null

				val layerConfig = mergeJson(config.getObject("layerDefaults").deepCopy(), layers.getObject(layerName))

				if (!layerConfig.get("enabled", false))
					return null

				val layerLevel = layerConfig["layerLevel"].asInt
				val baseHeight = layerConfig["baseHeight"].asInt

				val (primary, subPrimary) = makeRegion(layerConfig.getArray("primaryRegion").random(random).asString, baseHeight)

				val secondaryRegionCountRange = vectors2i.fromJsonTree(layerConfig["secondaryRegionCount"])
				var secondaryRegionCount = random.nextRange(secondaryRegionCountRange)

				val secondaryRegionList = layerConfig.getArray("secondaryRegions")
				val secondary = ArrayList<Pair<Region, Region>>()

				if (!secondaryRegionList.isEmpty && secondaryRegionCount > 0) {
					val indices = IntArrayList(secondaryRegionList.size())
					indices.addAll(0 until secondaryRegionList.size())
					val shuffled = IntArrayList(secondaryRegionList.size())

					while (indices.isNotEmpty()) {
						val v = indices.random(random)
						shuffled.add(v)
						indices.removeInt(indices.indexOf(v))
					}

					while (secondaryRegionCount-- > 0 && shuffled.isNotEmpty()) {
						val regionName = secondaryRegionList[shuffled.removeInt(0)].asString
						secondary.add(makeRegion(regionName, baseHeight))
					}
				}

				val secondaryRegionSizeRange = vectors2d.fromJsonTree(layerConfig["secondaryRegionSize"])
				val subRegionSizeRange = vectors2d.fromJsonTree(layerConfig["subRegionSize"])

				val dungeonPool = dungeonPools.fromJsonTree(layerConfig.get("dungeons"))
				val dungeonCountRange = if ("dungeonCountRange" in layerConfig) vectors2i.fromJsonTree(layerConfig["dungeonCountRange"]) else Vector2i.ZERO
				val dungeonCount = random.nextRange(dungeonCountRange)
				val dungeons = dungeonPool.sample(dungeonCount, random)
				val dungeonXVariance = layerConfig.get("dungeonXVariance", 0)

				return Layer(
					layerLevel,
					baseHeight,
					ImmutableSet.copyOf(dungeons),
					dungeonXVariance,
					primary,
					subPrimary,
					secondary.stream().map { it.first }.collect(ImmutableList.toImmutableList()),
					secondary.stream().map { it.second }.collect(ImmutableList.toImmutableList()),
					secondaryRegionSizeRange,
					subRegionSizeRange
				)
			}

			val surfaceLayer = readLayer("surface") ?: throw NoSuchElementException("No such layer 'surface', this should never happen")
			val primaryBiome = Registries.biomes.getOrThrow(surfaceLayer.primaryRegion.biome)

			val parameters = TerrestrialWorldParameters()

			parameters.threatLevel = threadLevel
			parameters.typeName = typeName
			parameters.worldSize = params.size
			parameters.gravity = Vector2d(y = random.nextRange(params.gravityRange))
			parameters.airless = primaryBiome.value.airless
			parameters.environmentStatusEffects = primaryBiome.value.statusEffects
			parameters.overrideTech = params.overrideTech
			parameters.globalDirectives = params.globalDirectives
			parameters.beamUpRule = params.beamUpRule
			parameters.disableDeathDrops = params.disableDeathDrops
			parameters.worldEdgeForceRegions = params.worldEdgeForceRegions
			parameters.weatherPool = primaryBiome.value.weather.stream().binnedChoice(threadLevel).get().random(random).value ?: throw NullPointerException("No weather pool")
			parameters.primaryBiome = primaryBiome.key
			parameters.sizeName = sizeName
			parameters.hueShift = primaryBiome.value.hueShift(random)

			parameters.surfaceLiquid = surfaceLayer.primaryRegion.oceanLiquid

			if (parameters.surfaceLiquid.isEmptyLiquid)
				parameters.surfaceLiquid = surfaceLayer.primaryRegion.caveLiquid

			parameters.skyColoring = primaryBiome.value.skyColoring(random)
			parameters.dayLength = random.nextRange(params.dayLengthRange)

			parameters.blockNoiseConfig = params.blockNoise
			parameters.blendNoiseConfig = params.blendNoise
			parameters.blendSize = params.blendSize

			parameters.spaceLayer = readLayer("space") ?: throw NoSuchElementException("No such terrain layer 'space'")
			parameters.atmosphereLayer = readLayer("atmosphere") ?: throw NoSuchElementException("No such terrain layer 'atmosphere'")
			parameters.surfaceLayer = surfaceLayer
			parameters.subsurfaceLayer = readLayer("subsurface") ?: throw NoSuchElementException("No such terrain layer 'subsurface'")
			parameters.coreLayer = readLayer("core") ?: throw NoSuchElementException("No such terrain layer 'core'")

			val undergroundLayers = ArrayList<Layer>()
			var ulayer = readLayer("underground${undergroundLayers.size + 1}")

			while (ulayer != null) {
				undergroundLayers.add(ulayer)
				ulayer = readLayer("underground${undergroundLayers.size + 1}")
			}

			parameters.undergroundLayers = undergroundLayers

			return parameters
		}
	}
}
