@file:Suppress("DuplicatedCode")

import org.yaml.snakeyaml.LoaderOptions
import org.yaml.snakeyaml.Yaml
import org.yaml.snakeyaml.constructor.SafeConstructor
import org.yaml.snakeyaml.nodes.CollectionNode
import org.yaml.snakeyaml.nodes.MappingNode
import org.yaml.snakeyaml.nodes.SequenceNode
import java.io.DataOutputStream
import java.io.File
import java.io.FileOutputStream
import java.io.OutputStream
import java.net.URI
import java.net.URL
import java.nio.channels.Channels
import java.nio.file.Files
import java.util.Locale
import java.util.zip.ZipFile
import javax.imageio.ImageIO


/**
 * Wraps an output stream into a [DataOutputStream].
 */
fun OutputStream.data() = DataOutputStream(this)


/**
 * Runs the given code, printing the given message before.
 */
internal fun <T> runPrintingMessage(
    message: String,
    block: () -> T
): T {
    println(message)
    return block()
}


/**
 * Imports the EVE Static Data Dump from the given file.
 */
@Suppress("UNUSED_VARIABLE", "unused")
fun importSde(


    /**
     * The SDE file.
     */
    sdeFile: File,


    /**
     * The directory of the EVE client cache, e.g. `~/Library/Application Support/EVE Online/SharedCache`.
     *
     * This is where market group icons are taken from. The item type icons are downloaded on-the-fly from
     * `https://images.evetech.net/types/${iconId}/icon`.
     */
    eveCacheDir: File,


    /**
     * The path to where the output `eve_data.dat` file should be written.
     */
    outputFile: File,


    /**
     * The root directory where the icons should be written.
     */
    iconsOutputDir: File


) {
    val originalEveData = EveData.load(sdeFile)
    val eveData = runPrintingMessage("Fixing Eve data") {
        fixup(originalEveData)
    }
    runPrintingMessage("Validating Eve data") {
        validate(eveData)
    }

    class TypeInfo(
        val type: Type,
        val group: Group,
        val attributes: Map<String, DogmaAttributeValue>,
        val effects: List<DogmaEffect>
    ) {
        val name = type.name
        override fun toString() = name
    }

    fun Collection<Type>.itemTree() = this
        .mapNotNull {
            val dogmaType = eveData.dogmaTypeById[it.id] ?: return@mapNotNull null
            val group = eveData.group(it.groupId)
            TypeInfo(
                type = it,
                group = group,
                attributes = dogmaType.attributeValues.associateBy { attr -> eveData.dogmaAttribute(attr.attributeId).name },
                effects = dogmaType.effectRefs.map { effectRef -> eveData.dogmaEffect(effectRef.effectId) }
            )
        }
        .associateBy { it.name }


    val categories = eveData.knownCategories
    val shipTree = eveData.typesIn(categories.ship).itemTree()
    val skillTree = eveData.typesIn(categories.skill).itemTree()
    val moduleTree = eveData.typesIn(categories.module).itemTree()
    val chargeTree = eveData.typesIn(categories.charge).itemTree()
    val droneTree = eveData.typesIn(categories.drone).itemTree()
    val implantTree = eveData.typesIn(categories.implant).filter {
        eveData.dogmaType(it).hasAttribute(eveData.knownAttributes.implantness)
    }.itemTree()
    val boosterTree = eveData.typesIn(categories.implant).filter {
        eveData.dogmaType(it).hasAttribute(eveData.knownAttributes.boosterness)
    }.itemTree()
    val warfareBuffs = eveData.typesIn(categories.warfareBuffs).itemTree()
    val tacticalModeTree = eveData.typesIn(categories.tacticalMode).itemTree()
    val subsystemTree = eveData.typesIn(categories.subsystem).itemTree()
    val envTree = eveData.relevantEnvironmentTypes().itemTree()
    val allTree = eveData.types.itemTree()

    val modifierOperationToEffects = mutableMapOf<Int?, MutableSet<DogmaEffect>>()
    eveData.dogmaEffects.forEach { effect ->
        effect.modifiers.forEach { modifier ->
            modifierOperationToEffects.putIfAbsent(modifier.operation, mutableSetOf())
            modifierOperationToEffects.getValue(modifier.operation).add(effect)
        }
    }

    val effectToTypeInfo = mutableMapOf<String, MutableSet<TypeInfo>>()
    for (dogmaType in eveData.dogmaTypes) {
        val type = eveData.typeById[dogmaType.id] ?: continue
        val group = eveData.group(type.groupId)
        val effects = dogmaType.effectRefs.map { effectRef -> eveData.dogmaEffect(effectRef.effectId) }
        val typeInfo = TypeInfo(
            type = type,
            group = group,
            attributes = dogmaType.attributeValues.associateBy { attr -> eveData.dogmaAttribute(attr.attributeId).name },
            effects = effects
        )

        effects.forEach {
            effectToTypeInfo.putIfAbsent(it.name, mutableSetOf())
            effectToTypeInfo.getValue(it.name).add(typeInfo)
        }
    }

    val attributeToTypeInfo = mutableMapOf<Int, MutableSet<TypeInfo>>()
    for (dogmaType in eveData.dogmaTypes) {
        val type = eveData.typeById[dogmaType.id] ?: continue
        val group = eveData.group(type.groupId)
        val effects = dogmaType.effectRefs.map { effectRef -> eveData.dogmaEffect(effectRef.effectId) }
        val typeInfo = TypeInfo(
            type = type,
            group = group,
            attributes = dogmaType.attributeValues.associateBy { attr -> eveData.dogmaAttribute(attr.attributeId).name },
            effects = effects
        )

        dogmaType.attributeValues.forEach { attrValue ->
            attributeToTypeInfo.putIfAbsent(attrValue.attributeId, mutableSetOf())
            attributeToTypeInfo.getValue(attrValue.attributeId).add(typeInfo)
        }
    }

    fun effectsAffectingAttribute(attrId: Int): Set<DogmaEffect> {
        val result = mutableSetOf<DogmaEffect>()
        for (effect in eveData.dogmaEffects) {
            if (effect.modifiers.any { (it.modifiedAttributeId == attrId) || (it.modifyingAttributeId == attrId) })
                result.add(effect)
        }
        return result
    }

    data class EffectAndModules(
        val effect: DogmaEffect,
        val modules: List<TypeInfo>
    ) {
        override fun toString() = effect.name
    }

    val modulesByEffect = eveData.dogmaEffects.associateWith { effect ->
        moduleTree.values.filter { typeInfo -> typeInfo.effects.any { it.id == effect.id } }
    }
    val effectsByCategory = mutableMapOf<Int, MutableList<EffectAndModules>>()
    for (effect in eveData.dogmaEffects)
        effectsByCategory.computeIfAbsent(effect.category) { mutableListOf() }
            .add(EffectAndModules(effect, modulesByEffect.getOrElse(effect, ::emptyList)))

    fun Iterable<TypeInfo>.funcToEffects(): Map<String, Collection<DogmaEffect>> {
        return this.flatMap { it.effects.flatMap { effect -> effect.modifiers.map { mod -> mod.func to effect } } }
            .groupBy(
                keySelector = { it.first }, valueTransform = { it.second }
            ).mapValues { it.value.toSet().toList() }
    }

    val funcToModuleEffects = moduleTree.values.funcToEffects()
    val funcToSkillEffects = skillTree.values.funcToEffects()
    val funcToShipEffects = shipTree.values.funcToEffects()

    val funcDomainPairsToEffects = allTree.values.flatMap {
        it.effects.flatMap { effect ->
            effect.modifiers.map { mod -> (mod.func to mod.domain) to effect }
        }
    }.groupBy(
        keySelector = { it.first },
        valueTransform = { it.second }
    ).mapValues { it.value.toSet().toList() }

    val airBoosters = boosterTree.values.filter { it.name.contains("AIR") }

    outputFile.parentFile.mkdirs()
    outputFile.outputStream().buffered().data().use {
        it.apply {
            writeMetaGroups(eveData)
            writeMarketGroups(eveData)
            writeRaces(eveData)
            writeAttributes(eveData)
            writeEffects(eveData)
            writeCategories(eveData)
            writeGroups(eveData)
            writeCharacter(eveData)
            writeWarfareBuffs(eveData)
            writeSkills(eveData)
            writeShips(eveData)
            writeModules(eveData)
            writeCharges(eveData)
            writeDrones(eveData)
            writeImplants(eveData)
            writeBoosters(eveData)
            writeSubsystems(eveData)
            writeTacticalModes(eveData)
            writeEnvironments(eveData)
            writeMaterials(eveData)
            writeMiscTypes(eveData)
            writeMutaplasmids(eveData)
            writeAbyssalItemReplacements(eveData)
        }
    }

    updateItemTypeIcons(eveData, targetDir = File(iconsOutputDir, "items"))
    updateMarketIcons(eveData, eveCacheDir = eveCacheDir, targetDir = File(iconsOutputDir, "market"))
}


/**
 * Warns if the size of the image at the given file is smaller than the given dimensions.
 */
private fun warnIfImageIsSmallerThan(file: File, minWidth: Int, minHeight: Int) {
    val image = ImageIO.read(file)
    if ((image.width < minWidth) || (image.height < minHeight)) {
        val path = file.parentFile.name + File.separator + file.name
        System.err.println(
            "Image $path dimensions are ${image.width}x${image.height}, which is less than ${minWidth}x${minHeight}"
        )
    }
}


/**
 * Updates the icons for item types.
 */
internal fun updateItemTypeIcons(eveData: EveData, targetDir: File) = with (eveData) {
    println("Checking item type icons...")

    val typeByIconId = types.associateBy { it.iconId }
    val iconIdByFilename = typeByIconId.keys.filterNotNull().associateBy {
        "$it.png"
    }

    val existingFiles = targetDir.listFiles()?.filter { it.name.endsWith(".png") } ?: emptyList()
    for (file in existingFiles) {
        if (file.name !in iconIdByFilename.keys) {
//            println("${file.name} can be deleted")
            println("Deleting item type icon ${file.name}")
            file.delete()
        }
    }

    val fileNames = existingFiles.mapTo(mutableSetOf()){ it.name }
    for ((fileName, iconId) in iconIdByFilename) {
        if (fileName in fileNames)
            continue

        val typeId = fileName.substringBefore(".").toInt()
        val item = typeByIconId[typeId]!!
        println("Icon $fileName for \"${item.name}\" is missing; downloading")
        kotlin.runCatching {
            val outputFile = File(targetDir, fileName)
            downloadFile(
                url = URI("https://images.evetech.net/types/${iconId}/icon").toURL(),
//                url = URI("https://images.eve-kill.com/types/${iconId}/icon?imagetype=png").toURL(),
                outputFile = outputFile
            )
            warnIfImageIsSmallerThan(outputFile, minWidth = 64, minHeight = 64)
        }.onFailure {
//            System.err.println(it.message)
            throw it
        }
    }

    // Load all icons and group their names by their content
    val iconIdsByFileContent = mutableMapOf<IconFileContent, MutableList<Int>>()
    for ((iconFileName, iconId) in iconIdByFilename) {
        val file = File(targetDir, iconFileName)
        if (!file.exists())
            continue
        val fileContent = IconFileContent(file.readBytes())
        iconIdsByFileContent.getOrPut(fileContent, ::mutableListOf).add(iconId)
    }
    if (iconIdsByFileContent.values.any { it.size > 1 }) {
        println("Detected duplicate icons; add these remappings to RemappedIcons and re-run this task:")
        for (fileList in iconIdsByFileContent.values) {
            if (fileList.size <= 1)
                continue

            // Pick
            fileList.sort()
            val target = fileList.first()
            for (source in fileList.slice(1..fileList.lastIndex)) {
                println("$source to $target,")
            }
        }
    }

    println("Done.")
}


/**
 * Updates the icons for market types.
 */
internal fun updateMarketIcons(
    eveData: EveData,
    eveCacheDir: File,
    targetDir: File
) = with (eveData) {
    println("Checking market group icons...")

    if (!targetDir.isDirectory && !targetDir.mkdirs())
        error("Unable to create directory $targetDir")

    fun iconFilename(iconId: Int) = "${iconId}.png"

    val resourceIndexFilename = File(eveCacheDir, "index_tranquility.txt").useLines { lines ->
        val line = lines.find { it.contains("resfileindex.txt") }!!
        line.split(",")[1]
    }

    val eveResourcesDir = File(eveCacheDir, "ResFiles")
    val filenameByResourceUrl = File(eveResourcesDir, resourceIndexFilename).useLines { lines ->
        lines.associate {
            val (resourceUrl, filename) = it.split(",")
            resourceUrl to filename
        }
    }

    for (marketGroup in marketGroups) {
        val iconId = marketGroup.iconId ?: continue
        val targetIconFile = File(targetDir, iconFilename(iconId))
        if (targetIconFile.isFile)
            continue

        val resourceUrl = eveData.iconInfoById[iconId]?.iconFile?.lowercase()
            ?: error("No icon info specified for icon $iconId (market group $marketGroup")

        println("Missing icon $iconId for market group $marketGroup; copying from EVE cache")
        val sourceFilename = filenameByResourceUrl[resourceUrl] ?: error("Icon $resourceUrl missing in EVE cache")
        val sourceIconFile = File(eveResourcesDir, sourceFilename)
        if (!sourceIconFile.isFile) {
            println("Market icon $iconId (file $sourceIconFile) for market group $marketGroup doesn't exist; " +
                    "either load it in-game, or add it to MissingMarketIconIds, and re-run this task")
            continue
        }

        Files.copy(sourceIconFile.toPath(), targetIconFile.toPath())

        warnIfImageIsSmallerThan(targetIconFile, minWidth = 64, minHeight = 64)
    }

    val neededIconFilenames = marketGroups.mapNotNullTo(mutableSetOf()) { marketGroup ->
        marketGroup.iconId?.let { iconFilename(it) }
    }
    val existingFiles = targetDir.listFiles()?.filter { it.name.endsWith(".png") } ?: emptyList()
    for (file in existingFiles) {
        if (file.name !in neededIconFilenames) {
//            println("${file.name} can be deleted")
            println("Deleting market group icon ${file.name}")
            file.delete()
        }
    }

    println("Done.")
}


/**
 * Wraps the contents of an icon file.
 */
private class IconFileContent(val data: ByteArray) {

    override fun equals(other: Any?): Boolean {
        if (other !is IconFileContent)
            return false

        return data.contentEquals(other.data)
    }

    override fun hashCode(): Int {
        return data.contentHashCode()
    }

}


/**
 * Downloads the content of the given URL into the given file.
 */
private fun downloadFile(url: URL, outputFile: File) {
    url.openStream().use {
        Channels.newChannel(it).use { rbc ->
            FileOutputStream(outputFile).use { fos ->
                fos.channel.transferFrom(rbc, 0, Long.MAX_VALUE)
            }
        }
    }
}


/**
 * Writes a list of items into the given output stream using the given function to write each.
 */
private inline fun <T> DataOutputStream.writeList(items: Collection<T>, itemWriter: DataOutputStream.(T) -> Unit) {
    writeInt(items.size)
    items.forEach {
        itemWriter.invoke(this, it)
    }
}


/**
 * Writes a list of items into the given output stream using the given function to write each.
 * Also, prints the number of items in the list and a random element from it.
 */
private inline fun <T> DataOutputStream.writeListPrintStats(
    items: Collection<T>,
    typeName: String,
    itemToString: (T) -> String,
    crossinline itemWriter: DataOutputStream.(T) -> Unit
) {
    runPrintingMessage("Writing ${items.size} $typeName (e.g. ${itemToString(items.random())})..."){
        writeList(items, itemWriter)
    }
}


/**
 * Writes a list of [NamedEntity] items into the given output stream using the given function to write each.
 * Also, prints the number of items in the list and the name of a random element from it.
 */
private inline fun <T: NamedEntity> DataOutputStream.writeListPrintStats(
    items: Collection<T>,
    typeName: String,
    crossinline itemWriter: DataOutputStream.(T) -> Unit
) {
    writeListPrintStats(items, typeName, itemToString = { it.name }, itemWriter)
}


/**
 * Writes an optional id.
 * If the value is null, 0 is written, as actual ids can't be 0.
 */
private fun DataOutputStream.writeOptionalId(id: Int?) = writeInt(id ?: 0)


/**
 * Writes an  id.
 */
private fun DataOutputStream.writeId(id: Int) = writeInt(id)


/**
 * Writes a string.
 */
private fun DataOutputStream.writeString(s: String) = writeUTF(s)


/**
 * Writes an optional string.
 */
private fun DataOutputStream.writeOptionalString(s: String?) {
    writeBoolean(s != null)
    if (s != null)
        writeString(s)
}


/**
 * Writes the [MetaGroup]s.
 */
private fun DataOutputStream.writeMetaGroups(eveData: EveData){
    writeListPrintStats(eveData.metaGroups, "meta groups"){ metaGroup ->
        writeId(metaGroup.id)
        writeString(metaGroup.name)
    }
}


/**
 * Writes the [MarketGroup]s.
 */
private fun DataOutputStream.writeMarketGroups(eveData: EveData){
    writeListPrintStats(eveData.marketGroups, "market groups"){ marketGroup ->
        writeId(marketGroup.id)
        writeString(marketGroup.name)
        writeOptionalId(marketGroup.iconId)
        writeOptionalId(marketGroup.parentGroupId)
    }
}


/**
 * Writes the [Race]s.
 */
private fun DataOutputStream.writeRaces(eveData: EveData){
    writeListPrintStats(eveData.races, "races"){ race ->
        writeId(race.id)
        writeString(race.name)
    }
}


/**
 * Writes the [DogmaAttribute]s.
 */
private fun DataOutputStream.writeAttributes(eveData: EveData){
    writeListPrintStats(eveData.dogmaAttributes, "dogma attributes") { attribute ->
        writeId(attribute.id)
        writeString(attribute.name)
        writeOptionalString(attribute.displayName)
        writeByte(attribute.unitId ?: 0)
        writeByte(flagsToByte(
            flag0 = !attribute.stackable,
            flag1 = attribute.highIsGood != null,
            flag2 = attribute.highIsGood ?: false,
            flag3 = attribute.range != null
        ))
        attribute.range?.let {
            writeDouble(it.start)
            writeDouble(it.endInclusive)
        }
    }
}


/**
 * The types of affected items.
 */
enum class AffectedItemsType(val value: Byte){
    NONE(0), // No affected items, either because we don't care about this effect, or we're just not implementing it yet
    AFFECTING_ITEM(1),  // The affected item is the affecting item itself
    SHIP(2),  // The affected item is the ship
    CHARACTER(3),  // The affected item is the character
    MODULES(4),  // The affected items are things fitted to the ship (modules, rigs, but not drones, probes)
    IMPLANTS_BOOSTERS(5),  // The affected items are things "fitted" to the character (implants, boosters)
    LAUNCHABLES(6),  // The affected items are charges and drones
    LAUNCHER(7),  // The affected item is the module into which the charge is loaded
    WARFARE_BUFFS(100)  // The affected item is the synthetic Warfare Buffs item
}


/**
 * An additional filter on the affected items.
 */
enum class AffectedItemsFilter(val value: Byte){
    ALL(1),  // No filter - all the items are affected
    MATCH_REQUIRED_SKILL(2),  // Item must require the skillTypeId of the modifier
    MATCH_GROUP(3),  // Item's groupId must match the modifier's groupId
}


/**
 * Returns the type of items affected by the given modifier and the type of filter on them.
 *
 * - `func=ItemModifier, domain=itemID`:
 *   The affecting item itself, e.g. skill bonus per skill level.
 *
 * - `func=ItemModifier, domain=shipID`:
 *   The ship, e.g. Mechanics skill increasing ship structure.
 *
 * - `func=ItemModifier, domain=charID`:
 *   The character, e.g.
 *     - Drones skill increasing max controlled drones
 *     - BCS module increasing `missileDMGBonus` attribute (not sure why it's a character attribute).
 *
 * - `func=LocationModifier, domain=shipID`:
 *   All fitted modules, e.g.
 *     - Red Giant has `systemOverloadRange` effect, which affects all modules that have the `overloadRangeBonus`, which
 *       includes all modules whose overload increases their range (warp disruptors, warp scramblers, stasis webifiers,
 *       grapplers).
 *     - Subsystems reducing heat damage to all modules.
 *
 * - `func=LocationRequiredSkillModifier, domain=shipID`:
 *   Fitted modules that require the `skillTypeId` of the modifier, e.g.
 *     - Rapid Launch skill affecting all modules requiring the Missile Launcher Operation skill.
 *     - Thalia bonus to Remote Armor Repairer amount
 *
 * - `func=LocationRequiredSkillModifier, domain=charID`:
 *   Fitted implants or boosters that require the `skillTypeId` of the modifier, e.g.
 *     - Asklepian implants set bonus affecting implants requiring the Cybernetics skill.
 *   Pirate implant set bonuses appear to be the only case here. Not sure why this is used instead of just using
 *   `LocationGroupModifier` for all implant set bonuses.
 *
 * - `func=LocationGroupModifier, domain=shipID`:
 *   Fitted modules that have the `groupId` of the modifier, e.g.
 *     - Electronic Warfare skill reducing capacitor need for ewar modules.
 *     - Augoror bonus to Remote Armor Repairer faloff.
 *
 * - `func=LocationGroupModifier, domain=charID`:
 *   Fitted implants or boosters that require the groupId of the modifier, e.g.
 *     - Talisman implants set bonus affecting implants within the group.
 *     - Biology skill increasing attribute booster duration.
 *
 * - `func=OwnerRequiredSkillModifier, domain=charID`:
 *   Non-module items fitted to the ship (missiles, drones, fighters, probes, bombs) that require the `skillTypeId` of
 *   the modifier, e.g.
 *     - Crow's bonus to missile max velocity per Caldari Frigate skill level (`shipMissileVelocityCF`) affects all such
 *       items that require the Missile Launcher Operation skill (which, contrary to Crow's description, includes all
 *       missiles, not just light missiles and rockets).
 *     - Armageddon's bonus to drone damage per Amarr Battleship skill level (`shipBonusDroneDamageMultiplierAB`)
 *       affects all such items that require the Drones skill (which is all drones).
 *     - Manticore's bonus to kinetic bomb damage per Covert Ops skill level (`eliteBonusCoverOpsBombKineticDmg1`)
 *       affects all such items that require the Bomb Deployment skill (which is all bombs).
 */
private fun DogmaModifier.affectedItemsAndFilter(): Pair<AffectedItemsType, AffectedItemsFilter>{
    val unhandled = AffectedItemsType.NONE to AffectedItemsFilter.ALL
    return when (func){
        ModifierFunc.ItemModifier -> when (domain){
            ModifierDomain.itemID -> AffectedItemsType.AFFECTING_ITEM to AffectedItemsFilter.ALL
            ModifierDomain.shipID -> AffectedItemsType.SHIP to AffectedItemsFilter.ALL
            ModifierDomain.charID -> AffectedItemsType.CHARACTER to AffectedItemsFilter.ALL
            ModifierDomain.warfareBuffsID -> AffectedItemsType.WARFARE_BUFFS to AffectedItemsFilter.ALL
            ModifierDomain.otherID -> AffectedItemsType.LAUNCHER to AffectedItemsFilter.ALL
            else -> unhandled
        }
        ModifierFunc.LocationModifier -> when (domain){
            ModifierDomain.shipID -> AffectedItemsType.MODULES to AffectedItemsFilter.ALL
            else -> unhandled
        }
        ModifierFunc.LocationRequiredSkillModifier -> when (domain){
            ModifierDomain.shipID -> AffectedItemsType.MODULES to AffectedItemsFilter.MATCH_REQUIRED_SKILL
            ModifierDomain.charID -> AffectedItemsType.IMPLANTS_BOOSTERS to AffectedItemsFilter.MATCH_REQUIRED_SKILL
            else -> unhandled
        }
        ModifierFunc.LocationGroupModifier -> when (domain){
            ModifierDomain.shipID -> AffectedItemsType.MODULES to AffectedItemsFilter.MATCH_GROUP
            ModifierDomain.charID -> AffectedItemsType.IMPLANTS_BOOSTERS to AffectedItemsFilter.MATCH_GROUP
            else -> unhandled
        }
        ModifierFunc.OwnerRequiredSkillModifier -> when (domain){
            ModifierDomain.charID -> AffectedItemsType.LAUNCHABLES to AffectedItemsFilter.MATCH_REQUIRED_SKILL
            else -> unhandled
        }
        else -> unhandled
    }
}


/**
 * Converts a set of flags to a [Byte].
 */
private fun flagsToByte(
    flag0: Boolean = false,
    flag1: Boolean = false,
    flag2: Boolean = false,
    flag3: Boolean = false,
    flag4: Boolean = false,
    flag5: Boolean = false,
    flag6: Boolean = false,
    flag7: Boolean = false,
): Int {
    fun Boolean.toInt() = if (this) 1 else 0
    return (
            (flag0.toInt() shl 0) or
            (flag1.toInt() shl 1) or
            (flag2.toInt() shl 2) or
            (flag3.toInt() shl 3) or
            (flag4.toInt() shl 4) or
            (flag5.toInt() shl 5) or
            (flag6.toInt() shl 6) or
            (flag7.toInt() shl 7)
    )
}


/**
 * Returns the effect flags (a byte) we write for the given effect.
 */
private fun effectFlags(dogmaEffect: DogmaEffect): Int {
    return with(dogmaEffect) {
        flagsToByte(
            flag0 = isOffensive,
            flag1 = isAssistive,
            flag2 = isDamage
        )
    }
}


/**
 * Writes dogma effects.
 */
private fun DataOutputStream.writeEffects(eveData: EveData) {
    writeListPrintStats(eveData.dogmaEffects, "dogma effects") { effect ->
        writeId(effect.id)
        writeString(effect.name)
        writeByte(effect.category)
        writeByte(effectFlags(effect))
        writeByte(if (effect.conditionAttributeId == null) 0 else 1)
        if (effect.conditionAttributeId != null) {
            writeId(effect.conditionAttributeId)
            writeInt(effect.conditionAttributeValue!!)
        }
        writeList(effect.modifiers) { modifier ->
            writeOptionalId(modifier.modifiedAttributeId)
            writeOptionalId(modifier.modifyingAttributeId)
            writeOptionalId(modifier.attenuatingAttributeId)
            modifier.affectedItemsAndFilter().let { (itemsType, itemsFilter) ->
                writeByte(itemsType.value.toInt())
                writeByte(itemsFilter.value.toInt())
            }
            writeInt(modifier.operation ?: -255)
            writeOptionalId(modifier.groupId)
            writeOptionalId(modifier.skillTypeId)
        }
    }
}


/**
 * Writes the item type categories.
 */
private fun DataOutputStream.writeCategories(eveData: EveData){
    writeListPrintStats(eveData.categories, "categories"){ category ->
        writeId(category.id)
        writeString(category.name)
    }
}


/**
 * Writes the item type groups.
 */
private fun DataOutputStream.writeGroups(eveData: EveData){
    writeListPrintStats(eveData.groups, "groups"){ group ->
        writeId(group.id)
        writeId(group.categoryId)
        writeString(group.name)
    }
}


/**
 * Writes a [DogmaAttributeValue].
 */
private fun DataOutputStream.writeAttributeValue(attrValue: DogmaAttributeValue){
    writeInt(attrValue.attributeId)
    writeDouble(attrValue.value)
}


/**
 * Writes a [DogmaEffectRef]]
 */
private fun DataOutputStream.writeEffectRef(effectRef: DogmaEffectRef){
    writeInt(effectRef.effectId)
}


/**
 * Writes an Eve type's basic data, common among all the types we write.
 */
private fun DataOutputStream.writeTypeData(type: Type, dogmaType: DogmaType?) {
    writeInt(type.id)
    writeInt(type.groupId)
    writeString(type.name)
    writeOptionalString(type.description)
    writeDouble(type.volume!!)  // Verified by Validations
    writeOptionalId(type.marketGroupId)
    writeOptionalId(type.raceId)
    writeOptionalId(type.iconId)
    if (dogmaType != null) {
        writeList(dogmaType.attributeValues, DataOutputStream::writeAttributeValue)
        writeList(dogmaType.effectRefs, DataOutputStream::writeEffectRef)
    }
}


/**
 * Writes the data of all the types in a given category.
 */
private fun DataOutputStream.writeTypeDataInCategory(
    eveData: EveData,
    category: Category,
    noDogma: Boolean = false,
    extraItemDataWriter: (DataOutputStream.(Type, DogmaType?) -> Unit)? = null
){
    with(eveData){
        val types = typesIn(category)
        val typeName = category.name.lowercase(Locale.ROOT).let {
            if (it.endsWith("y"))
                it.substring(0, it.length-1) + "ies"
            else
                it + "s"
        }
        writeListPrintStats(types, typeName){ type ->
            val dogmaType = if (noDogma) null else dogmaType(type)
            writeTypeData(type, dogmaType)
            extraItemDataWriter?.invoke(this@writeTypeDataInCategory, type, dogmaType)
        }
    }
}


/**
 * Writes the character type.
 */
private fun DataOutputStream.writeCharacter(eveData: EveData){
    val character = eveData.typeById[EveData.CHARACTER_TYPE_ID] ?: throw InvalidSdeException("Missing 'Character' type")
    val dogmaCharacter = eveData.dogmaType(character)
    writeTypeData(character, dogmaCharacter)
}


/**
 * Writes the warfare buffs type.
 */
private fun DataOutputStream.writeWarfareBuffs(eveData: EveData){
    val warfareBuffs = eveData.typeById[eveData.knownTypes.warfareBuffs.id]
        ?: throw InvalidSdeException("Missing 'Warfare Buffs' type")
    val dogmaWarfareBuffs = eveData.dogmaType(warfareBuffs)
    writeTypeData(warfareBuffs, dogmaWarfareBuffs)
}


/**
 * The regex for the "showinfo" tags.
 */
private val SHOW_INFO_TAG_REGEX = "<a href=showinfo:\\d+>([^<]*)</a>".toRegex()


/**
 * Strips "showinfo" tags from the given string.
 */
private fun String.stripShowInfoTags() = replace(SHOW_INFO_TAG_REGEX){
    it.groupValues[1]
}


/**
 * Fixes the bonus value string.
 */
private fun String.fixBonusValue() = removeSuffix(".0")


/**
 * Writes a single [Bonus].
 */
private fun DataOutputStream.writeBonus(bonus: Bonus) {
    writeString(bonus.text.stripShowInfoTags())

    writeBoolean(bonus.value != null)
    if (bonus.value != null) {
        writeString(bonus.value.fixBonusValue())
        writeByte(
            when (bonus.unit) {
                BonusUnit.METERS -> 1
                BonusUnit.KILOGRAMS -> 2
                BonusUnit.MULTIPLY -> 3
                BonusUnit.PERCENT -> 4
                BonusUnit.NUMBER -> 5
                BonusUnit.OTHER -> throw InvalidSdeException("Unknown bonus unit")
                null -> throw InvalidSdeException("Bonus unit should not be null when value is not null")
            }
        )
    }
}


/**
 * Writes the ship types.
 */
private fun DataOutputStream.writeShips(eveData: EveData) {

    fun writeBonuses(bonuses: List<Bonus>) {
        writeList(bonuses.sortedBy { it.importance }, DataOutputStream::writeBonus)
    }

    writeTypeDataInCategory(eveData, eveData.knownCategories.ship) { ship, _ ->
        val traits = eveData.traitsByShipId[ship.id]

        val perSkillLevel = traits?.perSkillLevel ?: emptyMap()
        writeList(perSkillLevel.entries){ (skillId, bonuses) ->
            writeId(skillId)
            writeBonuses(bonuses)
        }

        writeBonuses(traits?.role ?: emptyList())
        writeBonuses(traits?.misc ?: emptyList())
    }
}


/**
 * Returns the module flags (a byte) we write for the given module.
 */
private fun moduleFlags(eveData: EveData, dogmaModule: DogmaType): Int {
    val effects = eveData.dogmaEffects(dogmaModule.effectRefs)
    val knownEffects = eveData.knownEffects
    val onlineEffect = knownEffects.online

    val isProjected = effects.any { it.category == EffectCategory.projected }
    val isActivable = isProjected || effects.any {
        (it.category == EffectCategory.active) and (it != onlineEffect) // For some reason even passive modules have the 'online' effect
    }
    val isOverloadable = effects.any { it.category == EffectCategory.overloaded }
    val takesTurretHardpoint = effects.contains(knownEffects.turretFitted)
    val takesLauncherHardpoint = effects.contains(knownEffects.launcherFitted)

    return flagsToByte(
        flag0 = isActivable,
        flag1 = isOverloadable,
        flag2 = isProjected,
        flag3 = takesTurretHardpoint,
        flag4 = takesLauncherHardpoint
    )
}


/**
 * Returns the value we write to indicate the module's slot type; null if the module does not have a slot.
 */
internal fun moduleSlotType(knownEffects: EveData.KnownEffects, dogmaModule: DogmaType): Int?{
    for (effectRef in dogmaModule.effectRefs){
        when (effectRef.effectId){
            knownEffects.highSlot.id -> return 1
            knownEffects.medSlot.id -> return 2
            knownEffects.lowSlot.id -> return 3
            knownEffects.rigSlot.id -> return 4
        }
    }

    return null
}


/**
 * Writes the module types.
 */
private fun DataOutputStream.writeModules(eveData: EveData) {
    writeTypeDataInCategory(eveData, eveData.knownCategories.module){ module, dogmaModule ->
        dogmaModule!!
        writeInt(module.variationParentTypeID!!)  // Fixups assigns variationParentTypeID to all modules
        writeByte(moduleSlotType(eveData.knownEffects, dogmaModule)!!)  // Fixups filters out modules without a slot
        writeByte(moduleFlags(eveData, dogmaModule))
    }
}


/**
 * Writes the charge types.
 */
private fun DataOutputStream.writeCharges(eveData: EveData) {
    writeTypeDataInCategory(eveData, eveData.knownCategories.charge)
}


/**
 * Writes the skill types.
 */
private fun DataOutputStream.writeSkills(eveData: EveData) {
    writeTypeDataInCategory(eveData, eveData.knownCategories.skill)
}


/**
 * Writes the drone types.
 */
private fun DataOutputStream.writeDrones(eveData: EveData) {
    writeTypeDataInCategory(eveData, eveData.knownCategories.drone){ drone, _ ->
        writeInt(drone.variationParentTypeID!!)  // Fixups assigns variationParentTypeID to all drones
    }
}


/**
 * Writes the implant types.
 */
private fun DataOutputStream.writeImplants(eveData: EveData) {
    with(eveData){
        // The implant category includes boosters
        val implants = typesIn(knownCategories.implant).filter {
            dogmaType(it).hasAttribute(knownAttributes.implantness)
        }
        writeListPrintStats(implants, "implants"){ implant ->
            val dogmaImplant = dogmaType(implant)
            writeTypeData(implant, dogmaImplant)
            writeInt(implant.variationParentTypeID!!)  // Fixups assigns variationParentTypeID to all implants
            writeByte(dogmaImplant.getAttributeValue(knownAttributes.implantness)!!.toInt())  // Implant slot
        }
    }
}


/**
 * The information we write about a booster's side effect.
 */
private class BoosterSideEffect(
    val effectId: Int,
    val penalizedAttributeId: Int,
    val penalizingAttributeId: Int
)


/**
 * Returns the list of the booster's side effects.
 */
private fun DogmaType.boosterSideEffects(eveData: EveData, typeName: String): List<BoosterSideEffect> {
    val attributeValueById = attributeValues.associateBy { it.attributeId }
    fun DogmaModifier.isPenalty(effect: DogmaEffect): Boolean {
        val modifiedAttributeId = this.modifiedAttributeId ?: return false
        val modifiedAttributeHighIsGood = eveData.dogmaAttribute(modifiedAttributeId).highIsGood ?: return false
        val modifyingAttributeValue = attributeValueById[modifyingAttributeId]!!.value
        val modifiedValueIncreases = when (operation) {
            ModifierOperation.add,
            ModifierOperation.addPercent -> modifyingAttributeValue > 0.0
            ModifierOperation.postMultiply -> modifyingAttributeValue > 1.0
            else -> throw IllegalStateException("Unexpected booster $typeName.${effect.name} operation: $operation")
        }
        return modifiedValueIncreases != modifiedAttributeHighIsGood
    }

    val effects = effectRefs.map { eveData.dogmaEffect(it.effectId) }
    val penaltyEffects = effects.filter { effect -> effect.modifiers.all { it.isPenalty(effect) } }

    return penaltyEffects
        .map { effect ->
            val modifiedAttributeIds = effect.modifiers.mapNotNull { it.modifiedAttributeId }.toSet()
            if (modifiedAttributeIds.size != 1)
                throw IllegalStateException("Booster $typeName, effect $effect has a penalty effect with more than one modified attribute")

            val modifyingAttributeIds = effect.modifiers.mapNotNull { it.modifyingAttributeId }.toSet()
            if (modifyingAttributeIds.size != 1)
                throw IllegalStateException("Booster $typeName, effect $effect has a penalty effect with more than one modifying attribute")

            BoosterSideEffect(
                effectId = effect.id,
                penalizedAttributeId = modifiedAttributeIds.first(),
                penalizingAttributeId = modifyingAttributeIds.first()
            )
        }
        .sortedBy { it.penalizedAttributeId }
}


/**
 * Writes the booster types.
 */
private fun DataOutputStream.writeBoosters(eveData: EveData) {
    with(eveData){
        // The implant category includes boosters
        val implants = typesIn(knownCategories.implant).filter {
            dogmaType(it).hasAttribute(knownAttributes.boosterness)
        }
        writeListPrintStats(implants, "boosters"){ booster ->
            val dogmaBooster = dogmaType(booster)
            writeTypeData(booster, dogmaBooster)
            writeInt(booster.variationParentTypeID!!)  // Fixups assigns variationParentTypeID to all boosters
            writeByte(dogmaBooster.getAttributeValue(knownAttributes.boosterness)!!.toInt())  // Booster slot
            writeList(dogmaBooster.boosterSideEffects(eveData, booster.name)) {
                writeId(it.effectId)
                writeId(it.penalizedAttributeId)
                writeId(it.penalizingAttributeId)
            }
        }
    }
}


/**
 * Writes the subsystem types.
 */
private fun DataOutputStream.writeSubsystems(eveData: EveData) {
    writeTypeDataInCategory(eveData, eveData.knownCategories.subsystem)
}


/**
 * Writes the material types.
 */
private fun DataOutputStream.writeMaterials(eveData: EveData) {
    writeTypeDataInCategory(eveData, eveData.knownCategories.material, noDogma = true)
}


/**
 * Writes miscellaneous types we only need to be able to place them in the cargohold.
 */
private fun DataOutputStream.writeMiscTypes(eveData: EveData) {
    with(eveData) {
        val types = typesIn(knownCategories.deployable) +
            typesIn(knownGroups.jumpFilaments, knownGroups.triglavianSpaceFilaments)
        writeListPrintStats(types, "misc. items") { type ->
            val dogmaType = dogmaType(type.id)
            writeTypeData(type, dogmaType)
        }
    }
}


/**
 * Writes the tactical mode types.
 */
private fun DataOutputStream.writeTacticalModes(eveData: EveData) {
    writeTypeDataInCategory(eveData, eveData.knownCategories.tacticalMode)
}


/**
 * Writes the environments.
 */
private fun DataOutputStream.writeEnvironments(eveData: EveData) = with(eveData) {
    writeListPrintStats(eveData.relevantEnvironmentTypes(), "environments") { type ->
        val dogmaType = dogmaType(type)
        writeTypeData(type, dogmaType)
        writeInt(type.variationParentTypeID!!)  // Fixups assigns variationParentTypeID to all environments
    }
}


/**
 * Writes the mutaplasmids.
 */
private fun DataOutputStream.writeMutaplasmids(eveData: EveData) {
    writeList(eveData.mutaplasmids) { mutaplasmid ->
        val type = eveData.type(mutaplasmid.id)
        writeId(mutaplasmid.id)
        writeString(type.name)
        writeList(mutaplasmid.targetTypeIds, DataOutputStream::writeId)
        writeList(mutaplasmid.mutations) {
            writeId(it.attributeId)
            writeDouble(it.min)
            writeDouble(it.max)
            writeByte(
                when (it.highIsGood) {
                    null -> 0
                    true -> 1
                    false -> 2
                }
            )
        }
    }
}


/**
 * Write the map of abyssal type names to their "parent" types (should be the Tech 1 counterpart).
 *
 * These are the item types that will be used when e.g. importing a module with that name, without mutation info.
 */
private fun DataOutputStream.writeAbyssalItemReplacements(eveData: EveData) {
    writeList(eveData.abyssalNameToReplacementTypeId.entries) { (name, parentTypeId) ->
        writeString(name)
        writeId(parentTypeId)
    }
}


/**
 * The exception we throw when we encounter something unexpected in the SDE.
 */
class InvalidSdeException(description: String) : RuntimeException(description)


/**
 * A mapping from language code to a translation of some text into that language.
 */
private typealias TranslatedText = Map<String, String>


/**
 * Generic YAML node.
 */
internal open class YamlNode(val properties: Map<Any, Any>){


    /**
     * Returns the value of the given property, of the given type.
     * Throws a [InvalidSdeException] if the value is not of the correct type.
     * Note that if type is non-optional type (subtype of [Any]) and the value is missing, a [InvalidSdeException] will
     * be thrown. If the type is an optional type, however, null will be returned.
     */
    private inline fun <reified T> get(key: String): T{
        return when (val value = properties[key]) {
            is T -> value
            null -> throw InvalidSdeException("Missing '${key}' property type on\n$this")
            else -> throw InvalidSdeException("Wrong '${key}' property type ($value) on\n$this\nActual type: ${value.javaClass}")
        }
    }


    /**
     * Returns the child [YamlNode] with the given key, or `null` if it is missing.
     */
    fun childOrNull(key: String): YamlNode? = get<Map<Any, Any>?>(key)?.let(::YamlNode)


    /**
     * Returns the value of the given [Int] property. The property must be present.
     */
    fun int(key: String): Int = get<Int>(key)


    /**
     * Returns the value of the given [Int] property, or `null` if it is missing.
     */
    fun intOrNull(key: String): Int? = get<Int?>(key)


    /**
     * Returns the value of the given [Double] property. The property must be present.
     */
    fun double(key: String): Double = get<Double>(key)


    /**
     * Returns the value of the given [Double] property, or `null` if it is missing.
     */
    fun doubleOrNull(key: String): Double? = get<Double?>(key)


    /**
     * Returns the value of the given [Boolean] property. The property must be present.
     */
    fun boolean(key: String): Boolean = get<Boolean>(key)


    /**
     * Returns the value of the given [Boolean] property or `null` if it's missing.
     */
    fun booleanOrNull(key: String): Boolean? = get<Boolean?>(key)


    /**
     * Returns the value of the given [String] property. The property must be present.
     */
    fun string(key: String): String = get<String>(key)


    /**
     * Returns the value of the given [String] property or `null` if it's missing.
     */
    fun stringOrNull(key: String): String? = get<String?>(key)


    /**
     * Returns the value of the given [Int] or [Double] property. The property must be present.
     */
    fun number(key: String): Number = get<Number>(key)


    /**
     * Returns the value of the given [Int] or [Double] property, or `null` if it is missing.
     */
    fun numberOrNull(key: String): Number? = get<Number?>(key)


    /**
     * Returns the child list node mapped with the given key. The node must be present.
     */
    fun childList(key: String): List<YamlNode> = get<List<Map<Any, Any>>>(key).map(::YamlNode)


    /**
     * Returns the child list node mapped with the given key, or null if the property is missing.
     */
    fun childListOrNull(key: String): List<YamlNode>? = get<List<Map<Any, Any>>?>(key)?.map(::YamlNode)


    /**
     * Returns the child list node mapped with the given key. The node must be present.
     */
    fun childWithIdList(key: String): List<YamlNodeWithId> = get<Map<Int, Any>>(key).map {
        @Suppress("UNCHECKED_CAST")
        YamlNodeWithId(it.key, it.value as Map<Any, Any>)
    }


    /**
     * Returns the child list of ints. The node must be present.
     */
    fun intList(key: String): List<Int> = get<List<Int>>(key)


    /**
     * Returns the translations of the property with the given name. The property must be present.
     */
    private fun translatedText(key: String): TranslatedText = get<TranslatedText>(key)


    /**
     * Returns the English translation of the property with the given name. The property must be present.
     */
    fun textEn(key: String): String = translatedText(key)["en"]!!


    /**
     * Returns the translations of the property with the given name, or null if the property is missing.
     */
    private fun translatedTextOrNull(key: String): TranslatedText? = get<TranslatedText?>(key)


    /**
     * Returns the English translation of the property with the given name, or null if the property or
     * translation are missing.
     */
    fun textEnOrNull(key: String): String? = translatedTextOrNull(key)?.get("en")


    override fun toString() = properties.toString()


}


/**
 * [DogmaEffect] category values.
 */
@Suppress("unused")
internal object EffectCategory {
    const val always = 0
    const val active = 1
    const val projected = 2
    const val passive = 4
    const val overloaded = 5
}



/**
 * [DogmaModifier] domain values.
 */
@Suppress("unused")
internal object ModifierDomain{
    const val itemID = "itemID"
    const val shipID = "shipID"
    const val charID = "charID"
    const val targetID = "targetID"
    const val warfareBuffsID = "warfareBuffsID"
    const val otherID = "otherID"
}


/**
 * [DogmaModifier] func values.
 */
@Suppress("unused")
internal object ModifierFunc{
    const val ItemModifier = "ItemModifier"
    const val LocationRequiredSkillModifier = "LocationRequiredSkillModifier"
    const val LocationGroupModifier = "LocationGroupModifier"
    const val LocationModifier = "LocationModifier"
    const val OwnerRequiredSkillModifier = "OwnerRequiredSkillModifier"
    const val EffectStopper = "EffectStopper"
}


/**
 * [DogmaModifier] operation values.
 */
@Suppress("unused")
internal object ModifierOperation {
    const val preMultiply = 0
    const val add = 2
    const val subtract = 3
    const val postMultiply = 4
    const val postDivide = 5
    const val addPercent = 6
    const val setValue = 7
    const val setMaxAbsValue = 100  // Added for command bursts; sets only if the new value has a larger absolute value
    // Added for speedFactor of propulsion, whose value is a percentage, but it should be multiplied by, not added
    const val multiplyPercent = 101
    const val coerceAtLeast = 102
    const val coerceAtMost = 103
    const val none = 104 // No operation, the modifier is needed only to establish a dependency between properties
}


/**
 * Unit ids.
 */
internal object UnitIds {
    const val MultiplierDisplayedAsAddedPercentage = 109
}


/**
 * A node that maps an integer id to some entity.
 * All the files in the 'fsd' directory appear to contain entities like this.
 */
internal class YamlNodeWithId(
    val id: Int,
    properties: Map<Any, Any>
): YamlNode(properties)


private fun simpleToString(typeName: String, name: String?, vararg properties: Pair<String, Any?>) = buildString {
    append(typeName)
    append("(")
    var first = true
    if (name != null) {
        append(name)
        first = false
    }
    properties.forEach {
        val (key, value) = it
        if (value != null){
            if (!first)
                append(", ")
            append(key)
            append("=")
            append(value)
            append("")
            first = false
        }
    }
    append(")")
}


/**
 * Base class for entities that have an integer id.
 */
internal open class EntityWithId(val id: Int){


    override fun equals(other: Any?): Boolean {
        if (this === other)
            return true
        if (javaClass != other?.javaClass)
            return false

        other as EntityWithId

        return id == other.id
    }


    override fun hashCode(): Int {
        return id
    }


}


/**
 * Interface for entities that have a `name` [String] property.
 */
internal interface NamedEntity{
    val name: String
}


/**
 * Meta groups, e.g. "Tech I", "Storyline" etc.
 */
internal class MetaGroup(
    id: Int,
    override val name: String,
): EntityWithId(id), NamedEntity{

    constructor(node: YamlNodeWithId): this(
        id = node.id,
        name = node.textEn("name")
    )

    override fun toString() = simpleToString(
        typeName = "MetaGroup",
        name = name,
        "id" to id,
    )

}


/**
 * Market groups, e.g. "Standard Frigates" or "Signal Amplifiers"
 */
internal class MarketGroup(
    id: Int,
    override val name: String,
    val iconId: Int?,
    val parentGroupId: Int?
): EntityWithId(id), NamedEntity{


    constructor(node: YamlNodeWithId): this(
        id = node.id,
        name = node.textEn("name"),
        iconId = node.intOrNull("iconID"),
        parentGroupId = node.intOrNull("parentGroupID")
    )


    fun copy(
        id: Int = this.id,
        name: String = this.name,
        iconId: Int? = this.iconId,
        parentGroupId: Int? = this.parentGroupId
    ) = MarketGroup(
        id = id,
        name = name,
        iconId = iconId,
        parentGroupId = parentGroupId
    )


    override fun toString() = simpleToString(
        typeName = "MarketGroup",
        name = name,
        "parentGroupId" to parentGroupId
    )

}


/**
 * A pilot or ship race.
 */
internal class Race(
    id: Int,
    override val name: String
): EntityWithId(id), NamedEntity{

    constructor(node: YamlNodeWithId): this(
        id = node.id,
        name = node.textEn("name")
    )

    override fun toString() = simpleToString(
        typeName = "Race",
        name = name,
        "id" to id
    )

}


/**
 * The units for the [Bonus.value].
 */
enum class BonusUnit {

    METERS,  // e.g. the bonus to drone control range for the Ishtar
    KILOGRAMS,  // e.g. the added mass of an Entosis Link
    MULTIPLY,  // e.g. the penalty to Entosis Link duration on dreads
    PERCENT, // e.g. the bonus to armor resistances on the Vengeance (nearly all bonuses use this)
    NUMBER, // e.g. the bonus to ship warp core strength on Titans
    OTHER; // Some other unit we are not interested in

    companion object {

        fun decode(unitId: Int) = when (unitId) {
            1 -> METERS
            2 -> KILOGRAMS
            104 -> MULTIPLY
            105 -> PERCENT
            139 -> NUMBER
            else -> OTHER
        }

    }

}


/**
 * Describes a ship bonus.
 */
internal class Bonus(
    val text: String,  // The text of the bonus, e.g. "bonus to whatever"
    val value: String?,  // The value of the bonus, e.g. "5" for a "5% bonus to whatever"
    val unit: BonusUnit?,  // The unit of the bonus value
    val importance: Int  // The "importance" of the bonus, determining the order the bonuses should be displayed
) {

    constructor(node: YamlNode): this(
        text = node.textEn("bonusText"),
        value = node.numberOrNull("bonus")?.toString(),  // Could get an inexact value here due to conversion from string and back
        unit = node.intOrNull("unitID")?.let { BonusUnit.decode(it) },
        importance = node.int("importance")
    )

    override fun toString() = simpleToString(
        typeName = "Bonus",
        name = null,
        "value" to value,
        "unit" to unit,
        "text" to text,
        "importance" to importance
    )

}


/**
 * Describes a ship's traits (bonuses and special abilities).
 *
 * Note that this is just the description, the bonuses themselves are implemented via effects/modifiers.
 */
internal class ShipTraits(
    val shipId: Int,
    val perSkillLevel: Map<Int, List<Bonus>>?,
    val role: List<Bonus>?,
    val misc: List<Bonus>?,
) {

    @Suppress("UNCHECKED_CAST")
    constructor(node: YamlNodeWithId): this(
        shipId = node.id,
        perSkillLevel = node.childOrNull("types")?.let { perSkillBonusNode ->
            perSkillBonusNode.properties.entries.associate { (skillId, bonuses) ->
                val skillIdValue = when (skillId) {
                    is Int -> skillId
                    is String -> Integer.parseInt(skillId)
                    else -> throw InvalidSdeException("Unexpected skillId value: $skillId")
                }
                skillIdValue to (bonuses as List<Map<Any, Any>>).map { Bonus(YamlNode(it)) }
            }
        },
        role = node.childListOrNull("roleBonuses")?.map(::Bonus),
        misc = node.childListOrNull("miscBonuses")?.map(::Bonus)
    )

}



/**
 * A category of entities, e.g. "Module".
 * This is the most top-level grouping. Each category contains groups.
 *
 * Categories come from `categoryIDs.yaml`.
 */
internal class Category(
    id: Int,
    override val name: String,
) : EntityWithId(id), NamedEntity {


    constructor(node: YamlNodeWithId) : this(
        id = node.id,
        name = node.textEn("name"),
    )


    override fun toString() = simpleToString(
        typeName = "Category",
        name = name,
        "id" to id,
    )

}


/**
 * A group of entities, e.g. "Propulsion Module".
 * This is the mid-level grouping - each group belongs to a category and contains types.
 *
 * Groups come from `groupIDs.yaml`.
 */
internal class Group(
    id: Int,
    override val name: String,
    val categoryId: Int
) : EntityWithId(id), NamedEntity {


    constructor(node: YamlNodeWithId) : this(
        id = node.id,
        name = node.textEn("name"),
        categoryId = node.int("categoryID")
    )


    fun copy(
        id: Int = this.id,
        name: String = this.name,
        categoryId: Int = this.categoryId
    ) = Group(
        id = id,
        name = name,
        categoryId = categoryId
    )


    override fun toString() = simpleToString(
        typeName = "Group",
        name = name,
        "id" to id,
        "categoryId" to categoryId,
    )


}


/**
 * A type of entities, e.g. "1MN Afterburner II".
 * This is the most bottom-level grouping - each type belongs to a group.
 *
 * Types come from `typeIDs.yaml`.
 */
internal class Type(
    id: Int,
    override val name: String,
    val groupId: Int,
    val metaGroupId: Int?,
    val capacity: Double?,  // Ship cargo or module capacity
    val mass: Double?,  // Ship mass
    val radius: Double?,  // Ship ball radius
    val volume: Double?,  // Item volume, in m^3
    val variationParentTypeID: Int?,  // The id of the parent of the variations of this type
    val marketGroupId: Int?,  // The id of this item's most specific market group
    val raceId: Int?,  // The id of the item's race, if any
    val iconId: Int?,  // The id of the item's icon, if any
    val description: String?,
    val published: Boolean,
): EntityWithId(id), NamedEntity {


    constructor(node: YamlNodeWithId) : this(
        id = node.id,
        name = node.textEn("name"),
        groupId = node.int("groupID"),
        metaGroupId = node.numberOrNull("metaGroupID")?.toInt(),
        capacity = node.doubleOrNull("capacity"),
        mass = node.doubleOrNull("mass"),
        radius = node.doubleOrNull("radius"),
        volume = node.doubleOrNull("volume"),
        variationParentTypeID = node.intOrNull("variationParentTypeID"),
        marketGroupId = node.intOrNull("marketGroupID"),
        raceId = node.intOrNull("raceID"),
        iconId = null,  // No idea what the iconID in the YAML refers to; we calculate our in fixup
        description = node.textEnOrNull("description"),
        published = node.boolean("published"),
    )

    /**
     * Returns a [Type] that is like this one, but with the given values changed.
     */
    fun copy(
        id: Int = this.id,
        name: String = this.name,
        groupId: Int = this.groupId,
        metaGroupId: Int? = this.metaGroupId,
        capacity: Double? = this.capacity,
        mass: Double? = this.mass,
        radius: Double? = this.radius,
        volume: Double? = this.volume,
        variationParentTypeID: Int? = this.variationParentTypeID,
        marketGroupId: Int? = this.marketGroupId,
        raceId: Int? = this.raceId,
        iconId: Int? = this.iconId,
        description: String? = this.description,
        published: Boolean = this.published,
    ) = Type(
        id = id,
        name = name,
        groupId = groupId,
        metaGroupId = metaGroupId,
        capacity = capacity,
        mass = mass,
        radius = radius,
        volume = volume,
        variationParentTypeID = variationParentTypeID,
        marketGroupId = marketGroupId,
        raceId = raceId,
        iconId = iconId,
        description = description,
        published = published,
    )


    override fun toString() = simpleToString(
        typeName = "Type",
        name = name,
        "id" to id,
        "groupId" to groupId,
        "metaGroupId" to metaGroupId,
        "capacity" to capacity,
        "mass" to mass,
        "volume" to volume,
        "variationParentTypeID" to variationParentTypeID,
        "published" to published
    )

}


/**
 * A dogma attribute and its value in a concrete instance.
 */
internal class DogmaAttributeValue(
    val attributeId: Int,
    val value: Double,
){


    constructor(node: YamlNode): this(
        attributeId = node.int("attributeID"),
        value = node.double("value")
    )


    /**
     * Returns a [DogmaAttributeValue] that is like this one, but with the given values changed.
     */
    @Suppress("unused")
    fun copy(
        attributeId: Int = this.attributeId,
        value: Double = this.value,
    ) = DogmaAttributeValue(
        attributeId = attributeId,
        value = value,
    )


    override fun toString() = simpleToString(
        typeName = "AttributeValue",
        name = null,
        "attributeId" to attributeId,
        "value" to value,
    )

}


/**
 * A reference to a dogma effect.
 */
internal class DogmaEffectRef(val effectId: Int){

    constructor(node: YamlNode): this(node.int("effectID"))

    override fun toString() = simpleToString(
        typeName = "EffectRef",
        name = null,
        "effectId" to effectId,
    )

}


/**
 * A Dogma type.
 *
 * Dogma types come from `typeDogma.yaml`.
 */
internal class DogmaType(
    id: Int,
    val attributeValues: List<DogmaAttributeValue>,
    val effectRefs: List<DogmaEffectRef>
): EntityWithId(id){


    constructor(node: YamlNodeWithId) : this(
        id = node.id,
        attributeValues = node.childList("dogmaAttributes").map(::DogmaAttributeValue),
        effectRefs = node.childListOrNull("dogmaEffects")?.map(::DogmaEffectRef) ?: emptyList()
    )


    /**
     * Returns whether this [DogmaType] has the given attribute.
     */
    fun hasAttribute(attribute: DogmaAttribute): Boolean = getAttributeValue(attribute) != null


    /**
     * Returns the value of the given attribute in this [DogmaType], or `null` if none.
     */
    fun getAttributeValue(attribute: DogmaAttribute): Double? =
        attributeValues.find { it.attributeId == attribute.id }?.value


    /**
     * Returns whether this [DogmaType] has the given effect.
     */
    fun hasEffect(effect: DogmaEffect) = effectRefs.any { it.effectId == effect.id }


    /**
     * Returns a [DogmaType] that is like this one, but with the given values changed.
     */
    fun copy(
        id: Int = this.id,
        attributeValues: List<DogmaAttributeValue> = this.attributeValues,
        effectRefs: List<DogmaEffectRef> = this.effectRefs
    ) = DogmaType(
        id = id,
        attributeValues = attributeValues,
        effectRefs = effectRefs
    )


    override fun toString() = simpleToString(
        typeName = "DogmaType",
        name = null,
        "id" to id,
    )


}


/**
 * Dogma effects.
 *
 * Dogma effects come from `dogmaEffects.yaml`
 */
internal class DogmaEffect(
    id: Int,
    override val name: String,
    val category: Int,
    val isOffensive: Boolean,
    val isAssistive: Boolean,
    val isDamage: Boolean,
    val durationAttributeId: Int?,
    val rangeAttributeId: Int?,
    val falloffAttributeId: Int?,
    val trackingSpeedAttributeId: Int?,
    val conditionAttributeId: Int?,
    val conditionAttributeValue: Int?,
    val modifiers: List<DogmaModifier>,
) : EntityWithId(id), NamedEntity {


    init {
        if ((conditionAttributeId != null) != (conditionAttributeValue != null))
            throw IllegalArgumentException("conditionAttributeId and conditionAttributeValue must be either null together or non-null together")
    }


    constructor(node: YamlNodeWithId): this(
        id = node.id,
        name = node.stringOrNull("name") ?: "Unnamed",
        category = node.int("effectCategoryID"),
        isOffensive = node.boolean("isOffensive"),
        isAssistive = node.boolean("isAssistance"),
        isDamage = false,
        durationAttributeId = node.intOrNull("durationAttributeID"),
        rangeAttributeId = node.intOrNull("rangeAttributeID"),
        falloffAttributeId = node.intOrNull("falloffAttributeID"),
        trackingSpeedAttributeId = node.intOrNull("trackingSpeedAttributeID"),
        conditionAttributeId = null,
        conditionAttributeValue = null,
        modifiers = node.childListOrNull("modifierInfo")?.map(::DogmaModifier) ?: emptyList()
    )

    /**
     * Returns a [DogmaEffect] that is like this one, but with the given values changed.
     */
    fun copy(
        id: Int = this.id,
        name: String = this.name,
        category: Int = this.category,
        isOffensive: Boolean = this.isOffensive,
        isAssistive: Boolean = this.isAssistive,
        isDamage: Boolean = this.isDamage,
        durationAttributeId: Int? = this.durationAttributeId,
        rangeAttributeId: Int? = this.rangeAttributeId,
        falloffAttributeId: Int? = this.falloffAttributeId,
        trackingSpeedAttributeId: Int? = this.trackingSpeedAttributeId,
        conditionAttributeId: Int? = this.conditionAttributeId,
        conditionAttributeValue: Int? = this.conditionAttributeValue,
        modifiers: List<DogmaModifier> = this.modifiers,
    ) = DogmaEffect(
        id = id,
        name = name,
        category = category,
        isOffensive = isOffensive,
        isAssistive = isAssistive,
        isDamage = isDamage,
        durationAttributeId = durationAttributeId,
        rangeAttributeId = rangeAttributeId,
        falloffAttributeId = falloffAttributeId,
        trackingSpeedAttributeId = trackingSpeedAttributeId,
        conditionAttributeId = conditionAttributeId,
        conditionAttributeValue = conditionAttributeValue,
        modifiers = modifiers,
    )


    override fun toString() = simpleToString(
        typeName = "DogmaEffect",
        name = name,
        "id" to id,
        "mods" to modifiers.joinToString(";"){ "${it.modifyingAttributeId}->${it.modifiedAttributeId}" },
    )

}


/**
 * Dogma attributes.
 *
 * Dogma attributes come from `dogmaAttributes.yaml`
 */
internal class DogmaAttribute(
    id: Int,
    override val name: String,
    val displayName: String?,
    val stackable: Boolean,
    val highIsGood: Boolean?,
    val range: ClosedRange<Double>? = null,
    val unitId: Int?,
) : EntityWithId(id), NamedEntity {

    constructor(node: YamlNodeWithId): this(
        id = node.id,
        name = node.string("name"),
        displayName = node.textEnOrNull("displayName"),
        stackable = node.boolean("stackable"),
        highIsGood = node.booleanOrNull("highIsGood"),
        unitId = node.intOrNull("unitID")
    )

    /**
     * Returns a [DogmaAttribute] that is like this one, but with the given values changed.
     */
    fun copy(
        id: Int = this.id,
        name: String = this.name,
        displayName: String? = this.displayName,
        stackable: Boolean = this.stackable,
        highIsGood: Boolean? = this.highIsGood,
        range: ClosedRange<Double>? = this.range,
        unitId: Int? = this.unitId
    ) = DogmaAttribute(
        id = id,
        name = name,
        displayName = displayName,
        stackable = stackable,
        highIsGood = highIsGood,
        range = range,
        unitId = unitId
    )


    override fun toString() = simpleToString(
        typeName = "DogmaAttribute",
        name = name,
        "id" to id,
    )

}


/**
 * Dogma modifiers are part of dogma effects.
 *
 * Dogma modifiers come from `dogmaEffects.yaml`.
 */
internal class DogmaModifier(
    val domain: String,
    val func: String,
    val modifiedAttributeId: Int?,
    val modifyingAttributeId: Int?,
    val attenuatingAttributeId: Int? = null,  // For implementing ewar resistances
    val operation: Int?,
    val groupId: Int? = null,
    val skillTypeId: Int? = null,
) {

    constructor(node: YamlNode): this(
        domain = node.string("domain"),
        func = node.string("func"),
        modifiedAttributeId = node.intOrNull("modifiedAttributeID"),
        modifyingAttributeId = node.intOrNull("modifyingAttributeID"),
        attenuatingAttributeId = null,
        operation = node.intOrNull("operation"),
        groupId = node.intOrNull("groupID"),
        skillTypeId = node.intOrNull("skillTypeID"),
    )

    /**
     * Returns a [DogmaModifier] that is like this one, but with the given values changed.
     */
    fun copy(
        domain: String = this.domain,
        func: String = this.func,
        modifiedAttributeId: Int? = this.modifiedAttributeId,
        modifyingAttributeId: Int? = this.modifyingAttributeId,
        attenuatingAttributeId: Int? = this.attenuatingAttributeId,
        operation: Int? = this.operation,
        groupId: Int? = this.groupId,
        skillTypeId: Int? = this.skillTypeId,
    ) = DogmaModifier(
        domain = domain,
        func = func,
        modifiedAttributeId = modifiedAttributeId,
        modifyingAttributeId = modifyingAttributeId,
        attenuatingAttributeId = attenuatingAttributeId,
        operation = operation,
        groupId = groupId,
        skillTypeId = skillTypeId
    )


    override fun toString() = simpleToString(
        typeName = "DogmaModifier",
        name = null,
        "domain" to domain,
        "func" to func,
        "modifiedAttributeId" to modifiedAttributeId,
        "modifyingAttributeId" to modifyingAttributeId,
        "operation" to operation,
        "groupId" to groupId,
        "skillTypeId" to skillTypeId
    )

}


/**
 * Encapsulates how the mutaplasmid can modify a single attribute.
 */
internal class AttributeMutation(
    val attributeId: Int,
    val min: Double,
    val max: Double,
    val highIsGood: Boolean? = null
) {

    constructor(node: YamlNodeWithId): this(
        attributeId = node.id,
        min = node.double("min"),
        max = node.double("max"),
        highIsGood = node.booleanOrNull("highIsGood")
    )

}


/**
 * Encapsulates a single mutaplasmid.
 */
internal class Mutaplasmid(
    id: Int,
    val targetTypeIds: List<Int>,
    val mutations: List<AttributeMutation>
) : EntityWithId(id) {

    constructor(node: YamlNodeWithId): this(
        id = node.id,
        // Not sure why inputOutputMapping is a list
        targetTypeIds = node.childList("inputOutputMapping").single().intList("applicableTypes"),
        mutations = node.childWithIdList("attributeIDs").map(::AttributeMutation)
    )

    /**
     * Returns a [Mutaplasmid] that is like this one, but with the given values changed.
     */
    fun copy(
        targetTypeIds: List<Int> = this.targetTypeIds,
        mutations: List<AttributeMutation> = this.mutations
    ) = Mutaplasmid(
        id = id,
        targetTypeIds = targetTypeIds,
        mutations = mutations
    )

}



/**
 * Icon information.
 *
 * This comes from `iconIDs.yaml`.
 */
internal class IconInfo(
    id: Int,
    val iconFile: String
): EntityWithId(id) {


    constructor(node: YamlNodeWithId): this(
        id = node.id,
        iconFile = node.string("iconFile")
    )


    override fun toString() = simpleToString(
        typeName = "Icon",
        name = null,
        "iconFile" to iconFile
    )


}


/**
 * Bundles together all the data loaded from the SDE.
 */
@Suppress("MemberVisibilityCanBePrivate")
internal class EveData(
    val metaGroups: List<MetaGroup>,
    val marketGroups: List<MarketGroup>,
    val races: List<Race>,
    val categories: List<Category>,
    val groups: List<Group>,
    val types: List<Type>,
    val shipTraits: List<ShipTraits>,
    val dogmaTypes: List<DogmaType>,
    val dogmaEffects: List<DogmaEffect>,
    val dogmaAttributes: List<DogmaAttribute>,
    val mutaplasmids: List<Mutaplasmid>,
    val abyssalNameToReplacementTypeId: Map<String, Int>,
    val icons: List<IconInfo>
) {

    private val metaGroupsByName = metaGroups.associateBy { it.name }
    private val marketGroupById = marketGroups.associateBy { it.id }
    private val categoryByName = categories.associateBy { it.name }
    private val categoryById = categories.associateBy { it.id }
    private val groupById = groups.associateBy { it.id }
    private val groupByName = groups.associateBy { it.name }
    val traitsByShipId = shipTraits.associateBy { it.shipId }
    val typeById = types.associateBy { it.id }
    val dogmaTypeById = dogmaTypes.associateBy { it.id }
    private val dogmaEffectById = dogmaEffects.associateBy { it.id }
    private val dogmaEffectByName = dogmaEffects.associateBy { it.name }
    private val dogmaAttributeById = dogmaAttributes.associateBy { it.id }
    private val dogmaAttributeByName = dogmaAttributes.associateBy { it.name }
    val iconInfoById = icons.associateBy { it.id }

    val knownMetaGroups = KnownMetaGroups()
    val knownCategories = KnownCategories()
    val knownGroups = KnownGroups()
    val knownTypes = KnownTypes()
    val knownAttributes = KnownAttributes()
    val knownEffects = KnownEffects()
    val knownMarketGroups = KnownMarketGroups()


    /**
     * Returns the [MetaGroup] with the given name; throws [InvalidSdeException] if none found.
     */
    fun metaGroup(name: String) = metaGroupsByName[name] ?: throw InvalidSdeException("Missing '$name' meta group")


    /**
     * Returns the [MarketGroup] with the given ID; throws [InvalidSdeException] if none found.
     */
    fun marketGroup(id: Int) = marketGroupById[id] ?: throw InvalidSdeException("Missing market group with ID $id")


    /**
     * Returns the [Type] with the given id.
     */
    fun type(id: Int) = typeById[id] ?: throw InvalidSdeException("Missing type with id $id")


    /**
     * Returns all the [Type]s in the given categories.
     */
    fun typesInCategories(categories: Collection<Category>): Collection<Type> {
        val categoryIds = categories.map { it.id}.toSet()
        val groupIds = groups.filter { it.categoryId in categoryIds }.map { it.id }.toSet()
        return types.filter { it.groupId in groupIds }
    }


    /**
     * Returns all the [Type]s in the given categories.
     */
    fun typesIn(vararg category: Category): Collection<Type> = typesInCategories(category.toList())


    /**
     * Returns all the [Type]s in the given groups.
     */
    fun typesInGroups(groups: Collection<Group>): Collection<Type> {
        val groupIds = groups.map { it.id }.toSet()
        return types.filter { it.groupId in groupIds }
    }


    /**
     * Returns all the [Type]s in the given group.
     */
    fun typesIn(vararg group: Group): Collection<Type> = typesInGroups(group.toList())


    /**
     * Returns the [Category] with the given name; throws [InvalidSdeException] if none found.
     */
    fun category(name: String) = categoryByName[name] ?: throw InvalidSdeException("Missing '$name' category")


    /**
     * Returns the [Category] with the given id; throws [InvalidSdeException] if none found.
     */
    fun category(id: Int) = categoryById[id] ?: throw InvalidSdeException("Missing category with id $id")


    /**
     * Returns the [Group] with the given id; throws [InvalidSdeException] if none found.
     */
    fun group(id: Int) = groupById[id] ?: throw InvalidSdeException("Missing group with id $id")


    /**
     * Returns the [Group] with the given name; throws [InvalidSdeException] if none found.
     */
    fun group(name: String) = groupByName[name] ?: throw InvalidSdeException("Missing '$name' group")


    /**
     * Returns the [DogmaType] with the given id; throws [InvalidSdeException] if none found.
     */
    fun dogmaType(id: Int) = dogmaTypeById[id] ?: throw InvalidSdeException("Missing dogma type with id $id")


    /**
     * Returns the [DogmaType] corresponding to the given [Type]; throws [InvalidSdeException] if none found.
     */
    fun dogmaType(type: Type) = dogmaType(type.id)


    /**
     * Returns the [DogmaEffect] with the given id; throws [InvalidSdeException] if none found.
     */
    fun dogmaEffect(id: Int) =
        dogmaEffectById[id] ?: throw InvalidSdeException("Missing dogma effect with id $id")


    /**
     * Returns the list of [DogmaEffect]s referenced by the given [DogmaEffectRef]s.
     */
    fun dogmaEffects(refs: Collection<DogmaEffectRef>) = refs.map { dogmaEffect(it.effectId) }


    /**
     * Returns the [DogmaAttribute] with the given id; throws [InvalidSdeException] if none found.
     */
    fun dogmaAttribute(id: Int) =
        dogmaAttributeById[id] ?: throw InvalidSdeException("Missing dogma attribute with id $id")


    /**
     * Returns the [DogmaAttribute] with the given name; throws [InvalidSdeException] if none found.
     */
    fun dogmaAttribute(name: String) =
        dogmaAttributeByName[name] ?: throw InvalidSdeException("Missing '$name' dogma attribute")


    /**
     * Returns the [DogmaEffect] with the given name; throws [InvalidSdeException] if none found.
     */
    fun dogmaEffect(name: String) =
        dogmaEffectByName[name] ?: throw InvalidSdeException("Missing '$name' dogma effect")


    /**
     * Attributes we're referencing directly.
     */
    inner class KnownAttributes{


        @Suppress("SameParameterValue")
        private fun indexedAttributes(nameFormat: String): List<DogmaAttribute>{
            return generateSequence(1){ it + 1 }
                .map { index -> nameFormat.format(index) }
                .map { name -> dogmaAttributeByName[name] }
                .takeWhile { it != null }
                .filterNotNull()
                .toList()
        }


        private fun damageTypeAttributes(nameFormat: String, capitalizeFirstLetter: Boolean): List<DogmaAttribute>{
            return listOf("em", "thermal", "kinetic", "explosive")
                .map { if (capitalizeFirstLetter) it.replaceFirstChar(Char::uppercaseChar) else it }
                .map { nameFormat.format(Locale.ROOT, it) }
                .map { dogmaAttribute(it) }
        }


        private fun sensorTypeAttributes(nameFormat: String): List<DogmaAttribute>{
            return listOf("radar", "magnetometric", "gravimetric", "ladar")
                .map { it.replaceFirstChar(Char::uppercaseChar) }
                .map { nameFormat.format(Locale.ROOT, it) }
                .map { dogmaAttribute(it) }
        }


        val capacity = dogmaAttribute("capacity")
        val mass = dogmaAttribute("mass")
        val radius = dogmaAttribute("radius")
        val metaGroupId = dogmaAttribute("metaGroupID")

        val techLevel = dogmaAttribute("techLevel")
        val metaLevelOld = dogmaAttribute("metaLevelOld")

        val skillLevel = dogmaAttribute("skillLevel")

        val shieldResonance = damageTypeAttributes("shield%sDamageResonance", capitalizeFirstLetter = true)
        val armorResonance = damageTypeAttributes("armor%sDamageResonance", capitalizeFirstLetter = true)
        val structureResonance = damageTypeAttributes("%sDamageResonance", capitalizeFirstLetter = false)
        // These are used in Damage Control modules to specify the bonus to hull resistances
        val hullDamageResonance = damageTypeAttributes("hull%sDamageResonance", capitalizeFirstLetter = true)

        val shieldHp = dogmaAttribute("shieldCapacity")
        val shieldHpBonus = dogmaAttribute("shieldHpBonus")
        val armorHp = dogmaAttribute("armorHP")
        val armorHpBonus = dogmaAttribute("armorHpBonus")
        val duration = dogmaAttribute("duration")
        val capacitorNeed = dogmaAttribute("capacitorNeed")

        val calibration = dogmaAttribute("upgradeCapacity")
        val turretHardpoints = dogmaAttribute("turretSlotsLeft")
        val launcherHardpoints = dogmaAttribute("launcherSlotsLeft")
        val rigSlots = dogmaAttribute("rigSlots")
        val wrongRigSlots = dogmaAttribute("upgradeSlotsLeft")
        val highSlots = dogmaAttribute("hiSlots")
        val medSlots = dogmaAttribute("medSlots")
        val lowSlots = dogmaAttribute("lowSlots")
        val scanResolution = dogmaAttribute("scanResolution")
        val maxTargetRange = dogmaAttribute("maxTargetRange")
        val droneCapacity = dogmaAttribute("droneCapacity")
        val droneBandwidth = dogmaAttribute("droneBandwidth")
        val massAddition = dogmaAttribute("massAddition")
        val energyWarfareResistance = dogmaAttribute("energyWarfareResistance")
        val weaponDisruptionResistance = dogmaAttribute("weaponDisruptionResistance")
        val sensorDampenerResistance = dogmaAttribute("sensorDampenerResistance")
        val targetPainterResistance = dogmaAttribute("targetPainterResistance")
        val ecmResistance = dogmaAttribute("ECMResistance")
        val stasisWebifierResistance = dogmaAttribute("stasisWebifierResistance")
        val remoteRepairImpedance = dogmaAttribute("remoteRepairImpedance")
        val remoteAssistanceImpedance = dogmaAttribute("remoteAssistanceImpedance")
        val signatureRadius = dogmaAttribute("signatureRadius")
        val signatureRadiusBonus = dogmaAttribute("signatureRadiusBonus")
        val speedFactor = dogmaAttribute("speedFactor")
        val agility = dogmaAttribute("agility")
        val speedBoostFactor = dogmaAttribute("speedBoostFactor")
        val signatureRadiusBonusPercent = dogmaAttribute("signatureRadiusBonusPercent")  // MJD and MFJG
        val resistanceKiller = dogmaAttribute("resistanceKiller")

        val maxRange = dogmaAttribute("maxRange")
        val falloff = dogmaAttribute("falloff")
        val falloffEffectiveness = dogmaAttribute("falloffEffectiveness")

        val maxTargetRangeBonus = dogmaAttribute("maxTargetRangeBonus")
        val scanResolutionBonus = dogmaAttribute("scanResolutionBonus")

        val implantVelocityBonus = dogmaAttribute("implantBonusVelocity")  // Used by overdrives, nanofibers etc., in addition to implants

        val maxRangeBonus = dogmaAttribute("maxRangeBonus")
        val falloffBonus = dogmaAttribute("falloffBonus")
        val trackingSpeedBonus = dogmaAttribute("trackingSpeedBonus")
        val missileVelocityBonus = dogmaAttribute("missileVelocityBonus")
        val explosionDelayBonus = dogmaAttribute("explosionDelayBonus")
        val aoeVelocityBonus = dogmaAttribute("aoeVelocityBonus")
        val aoeCloudSizeBonus = dogmaAttribute("aoeCloudSizeBonus")
        val scanStrengthBonus = sensorTypeAttributes("scan%sStrengthBonus")
        val scanStrength = sensorTypeAttributes("scan%sStrength")

        val rigSize = dogmaAttribute("rigSize")

        val canFitShipGroups = indexedAttributes("canFitShipGroup%02d")

        val chargeGroups = indexedAttributes("chargeGroup%d")
        val damage = damageTypeAttributes("%sDamage", capitalizeFirstLetter = false)  // Damage done by ammo
        val damageMultiplier = dogmaAttribute("damageMultiplier")  // Turret/drone attribute
        val missileDamageMultiplier = dogmaAttribute("missileDamageMultiplier")  // Missile/Bomb launcher attribute
        val damageMultiplierBonusPerCycle = dogmaAttribute("damageMultiplierBonusPerCycle")  // Entropic disintegrators
        val damageMultiplierBonusMax = dogmaAttribute("damageMultiplierBonusMax") // Entropic disintegrators
        val repairMultiplierBonusPerCycle = dogmaAttribute("repairMultiplierBonusPerCycle")  // Mutadaptive repairers
        val repairMultiplierBonusMax = dogmaAttribute("repairMultiplierBonusMax")  // Mutadaptive repairers

        val armorDamageAmount = dogmaAttribute("armorDamageAmount")  // Armor repairers (including remote)

        val damageMultiplierBonus = dogmaAttribute("damageMultiplierBonus")
        val rofBonus = dogmaAttribute("rofBonus")

        val speed = dogmaAttribute("speed")  // This is actually a module duration attribute

        // The capacitor increase provided by Cap Booster charges
        val capacitorBonus = dogmaAttribute("capacitorBonus")
        val capRechargeBonus = dogmaAttribute("capRechargeBonus")
        val capRechargeRate = dogmaAttribute("rechargeRate")  // Capacitor recharge rate

        val maxActiveDrones = dogmaAttribute("maxActiveDrones")  // CharacterType attribute

        // The fixup attribute whose value is the id of the item's duration attribute
        val durationAttributeId by lazy { dogmaAttribute(FixupAttrs.durationAttributeId.name) }

        val maxVelocity = dogmaAttribute("maxVelocity")  // Missile, ship, drone attribute
        val speedLimit = dogmaAttribute("speedLimit")  // Ship attribute
        val flightTime = dogmaAttribute("explosionDelay")  // Missile attribute
        val explosionRadius = dogmaAttribute("aoeCloudSize")  // Missile attribute
        val explosionVelocity = dogmaAttribute("aoeVelocity")  // Missile attribute

        val chargeSize = dogmaAttribute("chargeSize")

        val maxGroupActive = dogmaAttribute("maxGroupActive")

        val baseWarpSpeed = dogmaAttribute("baseWarpSpeed")
        val warpCapacitorNeed = dogmaAttribute("warpCapacitorNeed")

        val ecmJamDuration = dogmaAttribute("ecmJamDuration")  // ECM drones, Burst Jammer

        val warfareBuffIds = indexedAttributes("warfareBuff%dID")
        val warfareBuffValues = indexedAttributes("warfareBuff%dValue")

        val implantness = dogmaAttribute("implantness")  // Marks implants, and specifies the implant slot
        val boosterness = dogmaAttribute("boosterness")  // Marks boosters, and specifies the booster slot
        val boosterDuration = dogmaAttribute("boosterDuration")
        val boosterLastInjectionDatetime = dogmaAttribute("boosterLastInjectionDatetime")

        // Subsystems
        val highSlotModifier = dogmaAttribute("hiSlotModifier")
        val medSlotModifier = dogmaAttribute("medSlotModifier")
        val lowSlotModifier = dogmaAttribute("lowSlotModifier")
        val turretHardPointModifier = dogmaAttribute("turretHardPointModifier")
        val launcherHardPointModifier = dogmaAttribute("launcherHardPointModifier")

        // Environment effects
        val structureResistanceBonus = damageTypeAttributes("%sDamageResistanceBonus", capitalizeFirstLetter = false)
        val armorResistanceBonus = damageTypeAttributes("armor%sDamageResistanceBonus", capitalizeFirstLetter = true)
        val shieldResistanceBonus = damageTypeAttributes("shield%sDamageResistanceBonus", capitalizeFirstLetter = true)

        // Ship security status effects
        val inverseCappedSecStatus = dogmaAttribute("inverseCappedSecStatus")

        // Super Weapons
        val damageDelayDuration = dogmaAttribute("damageDelayDuration")  // Doomsdays
        val doomsdayWarningDuration = dogmaAttribute("doomsdayWarningDuration")  // Lances, Reapers, etc.
    }


    /**
     * Effects we're referencing directly.
     */
    inner class KnownEffects{
        val highSlot = dogmaEffect("hiPower")
        val medSlot = dogmaEffect("medPower")
        val lowSlot = dogmaEffect("loPower")
        val rigSlot = dogmaEffect("rigSlot")
        val online = dogmaEffect("online")

        val moduleBonusMicrowarpdrive = dogmaEffect("moduleBonusMicrowarpdrive")
        val moduleBonusAfterburner = dogmaEffect("moduleBonusAfterburner")
        val microJumpDrive = dogmaEffect("microJumpDrive")
        val microJumpPortalDrive = dogmaEffect("microJumpPortalDrive")

        val emergencyHullEnergizer = dogmaEffect("emergencyHullEnergizer")

        val useMissiles = dogmaEffect("useMissiles")  // Empty effect that marks missile and bomb launchers
        val targetAttack = dogmaEffect("targetAttack")  // Empty effect that marks energy weapons and drones
        val projectileFired = dogmaEffect("projectileFired") // Empty effect that marks projectile and hybrid turrets
        val chainLightning = dogmaEffect("ChainLightning") // Empty effect that marks Vorton projectors
        val targetDisintegratorAttack = dogmaEffect("targetDisintegratorAttack")  // Empty effect that marks entropic disintegrators
        val mutadaptiveArmorRepairer = dogmaEffect("ShipModuleRemoteArmorMutadaptiveRepairer")  // Empty effect that marks mutadaptive remote repairers
        val empWave = dogmaEffect("empWave")  // An empty effect that appears to mark smartbombs
        val barrage = dogmaEffect("barrage")  // An empty effect on projectile turrets that duplicates projectileFired and doesn't do anything
        val superWeapons = dogmaEffects
            .filter { it.name.startsWith("superWeapon") }  // Doomsdays, e.g. superWeaponGallente
            .also { if (it.size != 4) throw IllegalStateException("Expected exactly 4 doomsdays, but got ${it.size}") }
        val doomsdayBeamDOT = dogmaEffect("doomsdayBeamDOT")  // Lances
        val doomsdaySlash = dogmaEffect("doomsdaySlash")  // Reapers
        val doomsdayConeDOT = dogmaEffect("doomsdayConeDOT")  // Bosonic Field Generator
        val debuffLance = dogmaEffect("debuffLance")  // Disruptive Lances
        val doomsdayHOG = dogmaEffect("doomsdayHOG")  // Gravitational Transportation Field Oscillator
        val doomsdayAOEECM = dogmaEffect("doomsdayAOEECM")  // ECM Jammer Burst Projector
        val doomsdayAOEWeb = dogmaEffect("doomsdayAOEWeb")  // Stasis Webification Burst Projector
        val doomsdayAOENeut = dogmaEffect("doomsdayAOENeut")  // Energy Neutralization Burst Projector
        val doomsdayAOEBubble = dogmaEffect("doomsdayAOEBubble")  // Warp Disruption Burst Projector
        val doomsdayAOEDamp = dogmaEffect("doomsdayAOEDamp")  // Sensor Dampening Burst Projector
        val doomsdayAOEPaint = dogmaEffect("doomsdayAOEPaint")  // Target Illumination Burst Projector
        val doomsdayAOETrack = dogmaEffect("doomsdayAOETrack")  // Weapon Disruption Burst Projector

        val turretFitted = dogmaEffect("turretFitted")  // An empty effect marking a module as taking a turret hardpoint
        val launcherFitted = dogmaEffect("launcherFitted")  // An empty effect marking a module as taking a missile launcher hardpoint

        val sensorBoosterActivePercentage = dogmaEffect("sensorBoosterActivePercentage")  // Sensor boosters
        val gunneryMaxRangeFalloffTrackingSpeedBonus = dogmaEffect("gunneryMaxRangeFalloffTrackingSpeedBonus")  // Tracking Computer

        // Effect that boosts sensor strength based on these attributes:
        // scanGravimetricStrengthPercent, scanLadarStrengthPercent, scanMagnetometricStrengthPercent, scanRadarStrengthPercent
        // The effect doesn't appear to be used anywhere in the SDE, but we're stealing it for the sensor boosting
        // effect by entosis links.
        val scanStrengthBonusPercentActivate = dogmaEffect("scanStrengthBonusPercentActivate")

        val deepcoreMiningBonus = dogmaEffect("deepcoreMiningBonus")

        val adaptiveArmorHardener = dogmaEffect("adaptiveArmorHardener")  // Reactive Armor Hardener

        // Missile explosion radius penalty
        val boosterMissileExplosionCloudPenaltyFixed = dogmaEffect("boosterMissileExplosionCloudPenaltyFixed")

        // Subsystems
        val slotModifier = dogmaEffect("slotModifier")
        val hardPointModifier = dogmaEffect("hardPointModifierEffect")

        // Remote effects
        val remoteWebifierFalloff = dogmaEffect("remoteWebifierFalloff")  // Stasis webifiers, grapplers
        val remoteWebifierEntity = dogmaEffect("remoteWebifierEntity")  // SW- drones
        val remoteSensorDampFalloff = dogmaEffect("remoteSensorDampFalloff")  // Remote sensor dampeners
        val remoteSensorDampEntity = dogmaEffect("remoteSensorDampEntity")  // SD- drones
        val remoteTargetPaintFalloff = dogmaEffect("remoteTargetPaintFalloff")  // Target painters
        val remoteTargetPaintEntity = dogmaEffect("remoteTargetPaintEntity")  // TP- drones
        val shipModuleTrackingDisruptor = dogmaEffect("shipModuleTrackingDisruptor")  // Tracking disruptors
        val npcEntityWeaponDisruptor = dogmaEffect("npcEntityWeaponDisruptor")  // TD- drones
        val shipModuleGuidanceDisruptor = dogmaEffect("shipModuleGuidanceDisruptor")  // Guidance disruptors
        val remoteSensorBoostFalloff = dogmaEffect("remoteSensorBoostFalloff")  // Remote sensor boosters
        val shipModuleRemoteTrackingComputer = dogmaEffect("shipModuleRemoteTrackingComputer")  // Remote Tracking Comp

        // Warp Disruption Field Generator modules
        val maxRangeHiddenPreAssignmentWarpScrambleRange = dogmaEffect("maxRangeHiddenPreAssignmentWarpScrambleRange")

        // Core Defense Operation Solidifier rigs
        val shieldBoosterDurationBonusShieldSkills = dogmaEffect("shieldBoosterDurationBonusShieldSkills")

        val moduleBonusBastionModule = dogmaEffect("moduleBonusBastionModule")  // Bastion

        val ecmBurstJammer = dogmaEffect("ECMBurstJammer")  // Burst Jammer
    }


    /**
     * Meta groups we're referencing directly.
     */
    inner class KnownMetaGroups{
        val tech1 = metaGroup("Tech I")
    }


    /**
     * Categories we're referencing directly.
     */
    inner class KnownCategories{
        val ship = category("Ship")
        val module = category("Module")
        val charge = category("Charge")
        val skill = category("Skill")
        val drone = category("Drone")
        val implant = category("Implant")  // Includes boosters too :facepalm:
        val subsystem = category("Subsystem")
        val material = category("Material")
        val deployable = category("Deployable")
        val warfareBuffs by lazy { category(FixupCategories.warfareBuff.id) }
        val tacticalMode by lazy { category(FixupCategories.tacticalMode.id) }

        val all: Collection<Category> by lazy{
            setOf(ship, module, charge, skill, drone, implant, subsystem, material, warfareBuffs, tacticalMode)
        }
    }


    /**
     * Groups we're referencing directly.
     */
    inner class KnownGroups {

        val capacitorBooster = group("Capacitor Booster")
        val ancillaryArmorRepairer = group("Ancillary Armor Repairer")
        val ancillaryRemoteArmorRepairer = group("Ancillary Remote Armor Repairer")
        val ancillaryShieldBooster = group("Ancillary Shield Booster")
        val ancillaryRemoteShieldBooster = group("Ancillary Remote Shield Booster")
        val missileSkills = group("Missiles")
        val covertOpsShips = group("Covert Ops")
        val entosisLink = group("Entosis Link")
        val scanProbeLauncher = group("Scan Probe Launcher")
        val interdictionSphereLauncher = group("Interdiction Sphere Launcher")
        val surveyProbeLauncher = group("Survey Probe Launcher")
        val breacherPodLauncher = group("Breacher Pod Launchers")
        val interdictionProbe = group("Interdiction Probe")
        val scannerProbe = group("Scanner Probe")
        val surveyProbe = group("Survey Probe")
        val commandBursts = group("Command Burst")
        val shieldCommandBurstCharges = group("Shield Command Burst Charges")
        val armorCommandBurstCharges = group("Armor Command Burst Charges")
        val skirmishCommandBurstCharges = group("Skirmish Command Burst Charges")
        val informationCommandBurstCharges = group("Information Command Burst Charges")
        val miningForemanBurstCharges = group("Mining Foreman Burst Charges")
        val ecm = group("ECM")
        val warpScrambler = group("Warp Scrambler")
        val stasisWeb = group("Stasis Web")
        val propulsionModule = group("Propulsion Module")
        val projectileWeapon = group("Projectile Weapon")
        val energyNosferatu = group("Energy Nosferatu")
        val shipModifiers = group("Ship Modifiers")  // Tactical modes
        val tacticalDestroyers = group("Tactical Destroyer")
        val jumpFilaments = group("Jump Filaments")
        val triglavianSpaceFilaments = group("Triglavian Space Filaments")
        val booster = group("Booster")
        val superWeapon = group("Super Weapon")
        val burstJammer = group("Burst Jammer")
        val precursorWeapon = group("Precursor Weapon")
        val reactiveArmorHardener = group("Armor Resistance Shift Hardener")

        // Environments
        val effectBeacon = group("Effect Beacon")
        val abyssalEnvironment = group("Abyssal Environment")
    }


    /**
     * [Type]s we're referencing directly.
     */
    inner class KnownTypes{

        val warfareBuffs: Type by lazy{ type(FixupTypes.warfareBuffs.id) }

        val armorCommandBurst = type(42526)
        val shieldCommandBurst = type(42529)
        val informationCommandBurst = type(42527)
        val skirmishCommandBurst = type(42530)
        val miningForemanBurst = type(42528)

        val shieldOperation = type(3416)
        val shieldEmissionSystems = type(3422)
        val capitalShieldEmissionSystems = type(24571)

        val repairSystems = type(3393)
        val remoteArmorRepairSystems = type(16069)
        val capitalRemoteArmorRepairSystems = type(24568)

        val sensorLinking = type(3433)
        val targetPainting = type(19921)
        val weaponDisruption = type(3434)

        val missileLauncherOperation = type(3319)
        val gunnery = type(3300)

    }


    /**
     * Market groups we're referencing directly.
     */
    @Suppress("unused")
    inner class KnownMarketGroups {

        val stasisGrapplers = marketGroup(2154)
        val interdictionSphereLaunchers = marketGroup(1937)
        val warpDisruptionFieldGenerators = marketGroup(1085)
        val warpScramblers = marketGroup(1936)
        val boosters = marketGroup(977)

    }



    companion object{


        /**
         * The id of the "Character" type.
         */
        const val CHARACTER_TYPE_ID = 1373


        /**
         * Loads the [EveData] from the files in the given (extracted SDE) directory.
         */
        fun load(file: File): EveData = ZipFile(file).use { zipFile ->
            val marketGroups = runPrintingMessage("Loading market groups...") {
                loadEntities(zipFile, "marketGroups.yaml", ::MarketGroup)
            }

            val metaGroups = runPrintingMessage("Loading meta groups...") {
                loadEntities(zipFile, "metaGroups.yaml", ::MetaGroup)
            }

            val races = runPrintingMessage("Loading races...") {
                loadEntities(zipFile, "races.yaml", ::Race)
            }

            val categories = runPrintingMessage("Loading categories...") {
                loadEntities(zipFile, "categories.yaml", ::Category)
            }

            val groups = runPrintingMessage("Loading groups...") {
                loadEntities(zipFile, "groups.yaml", ::Group)
            }

            val types = runPrintingMessage("Loading types...") {
                loadEntities(zipFile, "types.yaml", ::Type)
            }

            val traits = runPrintingMessage("Loading traits...") {
                loadEntities(zipFile, "typeBonus.yaml", ::ShipTraits)
            }

            val dogmaTypes = runPrintingMessage("Loading dogma types...") {
                loadEntities(zipFile, "typeDogma.yaml", ::DogmaType)
            }

            val dogmaEffects = runPrintingMessage("Loading dogma effects...") {
                loadEntities(zipFile, "dogmaEffects.yaml", ::DogmaEffect)
            }

            val dogmaAttributes = runPrintingMessage("Loading dogma attributes...") {
                loadEntities(zipFile, "dogmaAttributes.yaml", ::DogmaAttribute)
            }

            val mutaplasmids = runPrintingMessage("Loading mutaplasmids...") {
                loadEntities(zipFile, "dynamicItemAttributes.yaml", ::Mutaplasmid)
            }

            val icons = runPrintingMessage("Loading icon names...") {
                loadEntities(zipFile, "icons.yaml", ::IconInfo)
            }

            return EveData(
                marketGroups = marketGroups,
                metaGroups = metaGroups,
                races = races,
                categories = categories,
                groups = groups,
                types = types,
                shipTraits = traits,
                dogmaTypes = dogmaTypes,
                dogmaEffects = dogmaEffects,
                dogmaAttributes = dogmaAttributes,
                mutaplasmids = mutaplasmids,
                abyssalNameToReplacementTypeId = emptyMap(),
                icons = icons
            )
        }


    }


}


/**
 * The [Yaml] instance we use to parse YAML.
 */
private val yaml = Yaml(object: SafeConstructor(LoaderOptions()){

    // Overriding these to avoid the call to newInstance significantly improves the performance, as newInstance
    // with note.type == Object always fails anyway.

    override fun newMap(node: MappingNode): Map<Any?, Any?>? {
        return createDefaultMap(node.value.size)
    }

    override fun newSet(node: CollectionNode<*>): Set<Any?>? {
        return createDefaultSet(node.value.size)
    }

    override fun newList(node: SequenceNode): List<Any?>? {
        return createDefaultList(node.value.size)
    }

})


/**
 * Returns the entries in the given SDE YAML file as a sequence.
 * Each entry is expected to be a number (id) mapped to a YAML node:
 * ```
 * 123:
 *     prop1: ...
 *     prop2: ...
 * ```
 * We do this instead of passing the entire file to the YAML parser because if we do, it will load
 * the entire thing into memory.
 */
private fun nodesWithIds(zipFile: ZipFile, entryName: String): Sequence<YamlNodeWithId> {
    fun String.toYamlNode(): YamlNodeWithId {
        val itemById: Map<Int, Map<Any, Any>> = yaml.load(this)
        val entry = itemById.entries.first()
        return YamlNodeWithId(entry.key, entry.value)
    }

    return sequence {
        val buffer = StringBuilder()
        val entry = zipFile.getEntry(entryName) ?: error("No entry with name $entryName found")
        zipFile.getInputStream(entry).reader(Charsets.UTF_8).useLines { lineSequence ->
            for (line in lineSequence) {
                if (line.isNotEmpty() && line[0].isDigit() && buffer.isNotBlank()) {
                    yield(buffer.toString())
                    buffer.clear()
                }

                buffer.appendLine(line)
            }

            // Don't forget the last node
            if (buffer.isNotBlank())
                yield(buffer.toString())
        }
    }.map { it.toYamlNode() }
}


/**
 * Loads entries from the given SDE YAML file, converting them to entities via the given
 * constructor.
 */
private fun <T> loadEntities(
    zipFile: ZipFile,
    entryName: String,
    constructor: (YamlNodeWithId) -> T,
): List<T> {
    return nodesWithIds(zipFile, entryName)
        .map(constructor)
        .toList()
}
