package theorycrafter.storage

import eve.data.*
import eve.data.utils.ValueByEnum
import eve.data.utils.forEach
import eve.data.utils.mapValues
import eve.data.utils.valueByEnum
import theorycrafter.fitting.*
import theorycrafter.fitting.utils.AppBugException
import theorycrafter.isFlagship
import theorycrafter.ui.fiteditor.requiredSubsystemByKind
import theorycrafter.utils.StoredCollection
import theorycrafter.utils.addNotNull
import theorycrafter.utils.nulls
import theorycrafter.utils.with
import java.io.DataInput
import java.io.DataOutput
import kotlin.experimental.and


/**
 * The data of a serialized representation of a fit.
 */
class StoredFit(


    /**
     * The fit id; `null` if it hasn't been written to disk yet.
     */
    id: Int? = null,


    /**
     * The UTC time when this fit was created.
     */
    val creationTime: Long = System.currentTimeMillis(),


    /**
     * The UTC time when this fit was last modified.
     */
    val lastModifiedTime: Long = creationTime,


    /**
     * The name of the fit.
     */
    val name: String,


    /**
     * The fit's tags.
     */
    val tags: List<String> = emptyList(),


    /**
     * The id of the skill set to use with this fit.
     * A `null` value indicates the fit uses the default skill set.
     */
    val skillSetId: Int?,


    /**
     * The item id of the ship type.
     */
    val shipTypeId: Int,


    /**
     * The security status of the ship's pilot.
     */
    val securityStatus: Float?,


    /**
     * Whether the fit is a flagship in tournaments.
     */
    val isFlagship: Boolean,


    /**
     * The tactical mode.
     */
    val tacticalMode: StoredTacticalMode?,


    /**
     * The subsystems.
     */
    val subsystems: ValueByEnum<SubsystemType.Kind, StoredSubsystem>?,


    /**
     * The high-slot rack. A `null` item indicates an empty slot.
     */
    val highSlotRack: List<StoredModule?>,


    /**
     * The med-slot rack. A `null` item indicates an empty slot.
     */
    val medSlotRack: List<StoredModule?>,


    /**
     * The low-slot rack. A `null` item indicates an empty slot.
     */
    val lowSlotRack: List<StoredModule?>,


    /**
     * The rigs rack. A `null` item indicates an empty slot.
     */
    val rigs: List<StoredModule?>,


    /**
     * The fitted drone groups.
     */
    val droneGroups: List<StoredDroneGroup>,


    /**
     * The loaded cargo items.
     */
    val cargoItems: List<StoredCargoItem>,


    /**
     * The fitted implants.
     */
    val implants: List<StoredImplant>,


    /**
     * The fitted boosters.
     */
    val boosters: List<StoredBooster>,


    /**
     * The applied environments.
     */
    val environments: List<StoredEnvironment>,


    /**
     * The command effects that give this fit bonuses.
     */
    val commandEffects: List<StoredRemoteEffect>,


    /**
     * The hostile remote effects on this fit.
     */
    val hostileEffects: List<StoredRemoteEffect>,


    /**
     * The friendly remote effects on this fit.
     */
    val friendlyEffects: List<StoredRemoteEffect>,


    /**
     * The module effects on this fit.
     */
    val moduleEffects: List<StoredModule>,


    /**
     * The drone effects on this fit.
     */
    val droneEffects: List<StoredDroneGroup>


) {


    /**
     * The id of the fit on disk; `null` if it hasn't been saved to disk yet.
     */
    var id: Int? = id
        set(value) {
            if (field != null)
                throw IllegalStateException("Fit $this already has an id")
            field = value
        }


    /**
     * Specifies how to determine [creationTime] and [lastModifiedTime] when creating a copy of an existing [StoredFit].
     */
    enum class FitTimes(val resetCreationTime: Boolean, val updateLastModifiedTime: Boolean) {

        /** Both [creationTime] and [lastModifiedTime] are set to the current time. */
        NewFit(resetCreationTime = true, updateLastModifiedTime = true),

        /** [creationTime] is unchanged, [lastModifiedTime] is set to the current time. */
        ModifiedFit(resetCreationTime = false, updateLastModifiedTime = true),

        /** Neither [creationTime] nor [lastModifiedTime] are changed.*/
        Unchanged(resetCreationTime = false, updateLastModifiedTime = false),

    }


    /**
     * Returns a copy of this [StoredFit] with the given changes.
     */
    fun copy(
        fitTimes: FitTimes,
        id: Int? = this.id,
        name: String = this.name,
        tags: List<String> = this.tags,
        skillSetId: Int? = this.skillSetId,
        commandEffects: List<StoredRemoteEffect> = this.commandEffects,
        hostileEffects: List<StoredRemoteEffect> = this.hostileEffects,
        friendlyEffects: List<StoredRemoteEffect> = this.friendlyEffects,
    ): StoredFit {
        val currentTime = System.currentTimeMillis()
        return StoredFit(
            id = id,
            creationTime = if (fitTimes.resetCreationTime) currentTime else creationTime,
            lastModifiedTime = if (fitTimes.updateLastModifiedTime) currentTime else lastModifiedTime,
            name = name,
            tags = tags,
            skillSetId = skillSetId,
            shipTypeId = shipTypeId,
            securityStatus = securityStatus,
            isFlagship = isFlagship,
            tacticalMode = tacticalMode,
            subsystems = subsystems,
            highSlotRack = highSlotRack,
            medSlotRack = medSlotRack,
            lowSlotRack = lowSlotRack,
            rigs = rigs,
            droneGroups = droneGroups,
            cargoItems = cargoItems,
            implants = implants,
            boosters = boosters,
            environments = environments,
            commandEffects = commandEffects,
            hostileEffects = hostileEffects,
            friendlyEffects = friendlyEffects,
            moduleEffects = moduleEffects,
            droneEffects = droneEffects
        )
    }


    /**
     * Returns the ids of fits on which this fit depends to the given set.
     */
    fun getDependencyFitIds(): Iterable<Int> {
        return buildList(commandEffects.size + hostileEffects.size + friendlyEffects.size) {
            for (effect in commandEffects)
                add(effect.fitId)
            for (effect in hostileEffects)
                add(effect.fitId)
            for (effect in friendlyEffects)
                add(effect.fitId)
        }
    }


    /**
     * The tactical mode data we store on disk.
     */
    class StoredTacticalMode(val itemId: Int) {

        constructor(tacticalModeType: TacticalModeType): this(tacticalModeType.itemId)

    }


    /**
     * The subsystem data we store on disk.
     */
    class StoredSubsystem(val itemId: Int) {

        constructor(subsystemType: SubsystemType): this(subsystemType.itemId)

    }


    /**
     * The mutation data we store on disk.
     */
    class StoredMutation(
        val mutaplasmidId: Int,
        val name: String,
        val attributeIdsAndValues: List<Pair<Int, Double>>
    ) {


        constructor(mutation: Mutation) : this(
            mutaplasmidId = mutation.mutaplasmid.id,
            name = mutation.name,
            attributeIdsAndValues = mutation.mutatedAttributesAndValues().map { (attribute, value) ->
                Pair(attribute.id, value)
            }
        )


    }



    /**
     * The module's data we store on disk.
     */
    class StoredModule(
        val itemId: Int,
        val enabled: Boolean = true,  // For rigs
        val state: Module.State,
        val chargeId: Int?,
        val mutation: StoredMutation?,
        val extraAttributes: List<ExtraAttribute<Module>> = emptyList()
    ) {

        constructor(module: Module): this(
            itemId = module.type.itemId,
            enabled = module.enabledState.value,
            state = module.state,
            chargeId = module.loadedCharge?.type?.itemId,
            mutation = module.type.mutation?.let { StoredMutation(it) },
            extraAttributes = ExtraAttributes.forModule(module),
        )

    }


    /**
     * The drone group's data we store on disk.
     */
    class StoredDroneGroup(
        val itemId: Int,
        size: Int,
        val active: Boolean,
        val mutation: StoredMutation?
    ) {

        val size = size.coerceIn(1..127)  // The value is stored in 7 bits

        constructor(droneGroup: DroneGroup): this(
            itemId = droneGroup.type.itemId,
            size = droneGroup.size,
            active = droneGroup.active,
            mutation = droneGroup.type.mutation?.let { StoredMutation(it) }
        )

    }


    /**
     * The cargo item group's data we store on disk.
     */
    class StoredCargoItem(
        val itemId: Int,
        val amount: Int
    ) {

        constructor(cargoItem: CargoItem): this(cargoItem.type.itemId, cargoItem.amount)

    }


    /**
     * The implant's data we store on disk.
     */
    class StoredImplant(
        val itemId: Int,
        val enabled: Boolean
    ) {

        constructor(implant: Implant): this(implant.type.itemId, implant.enabled)

    }


    /**
     * The booster's data we store on disk.
     */
    class StoredBooster(
        val itemId: Int,
        val enabled: Boolean,
        val activeSideEffectPenalizedAttributeIds: List<Int>
    ) {

        constructor(booster: Booster): this(
            itemId = booster.type.itemId,
            enabled = booster.enabled,
            activeSideEffectPenalizedAttributeIds = booster.type.sideEffects
                .filter { booster.isSideEffectActive(it) }
                .map { it.penalizedAttribute.id }
        )

    }


    /**
     * The environment data we store on disk.
     */
    class StoredEnvironment(
        val itemId: Int,
        val enabled: Boolean,
    ) {

        constructor(environment: Environment): this(
            itemId = environment.type.itemId,
            enabled = environment.enabled
        )

    }


    /**
     * The remote effect data we store on disk.
     */
    class StoredRemoteEffect(


        /**
         * The id of the source (affecting) fit.
         */
        val fitId: Int,


        /**
         * Whether the effect is enabled.
         */
        val enabled: Boolean


    )


    override fun toString(): String {
        return "StoredFit($name, id=$id)"
    }


    companion object {


        /**
        * Creates a new, empty [StoredFit] with the given name, for the given ship.
         */
        fun newEmpty(
            name: String,
            tags: List<String>,
            skillSetId: Int?,
            shipType: ShipType,
            tacticalMode: TacticalModeType?,
            subsystems: ValueByEnum<SubsystemType.Kind, SubsystemType>?,
        ) = StoredFit(
            name = name,
            tags = tags,
            skillSetId = skillSetId,
            shipTypeId = shipType.itemId,
            securityStatus = null,
            isFlagship = false,
            tacticalMode = tacticalMode?.let(::StoredTacticalMode),
            subsystems = subsystems?.mapValues(StoredFit::StoredSubsystem),
            highSlotRack = nulls(size = shipType.fitting.slots.high),
            medSlotRack = nulls(size = shipType.fitting.slots.med),
            lowSlotRack = nulls(size = shipType.fitting.slots.low),
            rigs = nulls(size = shipType.fitting.slots.rig),
            droneGroups = emptyList(),
            cargoItems = emptyList(),
            implants = emptyList(),
            boosters = emptyList(),
            environments = emptyList(),
            commandEffects = emptyList(),
            hostileEffects = emptyList(),
            friendlyEffects = emptyList(),
            moduleEffects = emptyList(),
            droneEffects = emptyList()
        )


        /**
         * Creates a new [StoredFit] with the given properties.
         *
         * This function is mainly for importing fits (e.g. from EFT).
         */
        fun new(
            name: String,
            tags: List<String> = emptyList(),
            shipType: ShipType,
            tacticalMode: StoredTacticalMode?,
            subsystems: ValueByEnum<SubsystemType.Kind, StoredSubsystem>?,
            highSlotRack: List<StoredModule?>,
            medSlotRack: List<StoredModule?>,
            lowSlotRack: List<StoredModule?>,
            rigs: List<StoredModule?>,
            droneGroups: List<StoredDroneGroup>,
            cargoItems: List<StoredCargoItem>,
            implants: List<StoredImplant>,
            boosters: List<StoredBooster>,
        ) = StoredFit(
            name = name,
            tags = tags,
            skillSetId = null,
            shipTypeId = shipType.itemId,
            securityStatus = null,
            isFlagship = false,
            tacticalMode = tacticalMode,
            subsystems = subsystems,
            highSlotRack = highSlotRack,
            medSlotRack = medSlotRack,
            lowSlotRack = lowSlotRack,
            rigs = rigs,
            droneGroups = droneGroups,
            cargoItems = cargoItems,
            implants = implants,
            boosters = boosters,
            environments = emptyList(),
            commandEffects = emptyList(),
            hostileEffects = emptyList(),
            friendlyEffects = emptyList(),
            moduleEffects = emptyList(),
            droneEffects = emptyList()
        )


        /**
         * Returns a new [StoredFit] from an engine [Fit], which presumably came from [this].
         * This allows the modifications made to the [Fit] to be stored back to disk.
         */
        fun StoredFit.withUpdatesFrom(
            fit: Fit,
            fitTimes: FitTimes,
            fitIdByFit: (Fit) -> Int,
        ): StoredFit {

            fun Fit.Modules.storedModules(slotType: ModuleSlotType): List<StoredModule?> {
                val relevantSlotCount = relevantSlotCount(slotType)
                return slotsInRack(slotType)
                    .take(relevantSlotCount)
                    .map { it?.let(::StoredModule) }
            }

            fun List<RemoteEffect>.toStored() = map {
                StoredRemoteEffect(
                    fitId = fitIdByFit(it.source),
                    enabled = it.enabledState.value
                )
            }

            val subsystems = fit.requiredSubsystemByKind?.mapValues {
                StoredSubsystem(it.type)
            }

            val currentTime = System.currentTimeMillis()

            return StoredFit(
                id = id,
                creationTime = if (fitTimes.resetCreationTime) currentTime else creationTime,
                lastModifiedTime = if (fitTimes.updateLastModifiedTime) currentTime else lastModifiedTime,
                name = name,
                tags = tags,
                skillSetId = skillSetId,
                shipTypeId = fit.ship.type.itemId,
                securityStatus = fit.ship.pilotSecurityStatus?.value?.toFloat(),
                isFlagship = fit.isFlagship,
                tacticalMode = fit.tacticalMode?.type?.let(::StoredTacticalMode),
                subsystems = subsystems,
                highSlotRack = fit.modules.storedModules(ModuleSlotType.HIGH),
                medSlotRack = fit.modules.storedModules(ModuleSlotType.MEDIUM),
                lowSlotRack = fit.modules.storedModules(ModuleSlotType.LOW),
                rigs = fit.modules.storedModules(ModuleSlotType.RIG),
                droneGroups = fit.drones.all.map(::StoredDroneGroup),
                cargoItems = fit.cargohold.contents.map(::StoredCargoItem),
                implants = fit.implants.fitted.map(::StoredImplant),
                boosters = fit.boosters.fitted.map(::StoredBooster),
                environments = fit.environments.map(::StoredEnvironment),
                commandEffects = fit.commandEffects.toStored(),
                hostileEffects = fit.hostileEffects.filter { !it.isByAuxiliaryFit }.toStored(),
                friendlyEffects = fit.friendlyEffects.filter { !it.isByAuxiliaryFit }.toStored(),
                moduleEffects = fit.auxiliaryFit?.modules?.all?.map(::StoredModule) ?: emptyList(),
                droneEffects = fit.auxiliaryFit?.drones?.all?.map(::StoredDroneGroup) ?: emptyList()
            )
        }


    }


    /**
     * A [StoredCollection.Serializer] for [StoredFit]s.
     */
    object Serializer : StoredCollection.Serializer<StoredFit> {


        /**
        * Packed module flags.
         */
        @JvmInline
        private value class ModuleFlags(val value: Byte) {


            /**
             * Whether the module is enabled.
             */
            fun enabled() = (value and 0b1).toInt() != 0


            /**
             * The module state encoded in the flags.
             */
            fun moduleState(): Module.State = when ((value.toInt() shr 1) and 0b11) {
                0 -> Module.State.OFFLINE
                1 -> Module.State.ONLINE
                2 -> Module.State.ACTIVE
                3 -> Module.State.OVERLOADED
                else -> throw AppBugException("Module state value not in range")
            }


            /**
             * Whether the module has a loaded charge.
             */
            fun hasLoadedCharge() = (value.toInt() shr 3) != 0


            companion object {

                /**
                 * The [ModuleFlags] of a [StoredModule].
                 */
                fun ofModule(module: StoredModule): ModuleFlags {
                    val enabledFlag = if (module.enabled) 0b1 else 0b0
                    val stateFlags = when (module.state) {
                        Module.State.OFFLINE -> 0b00
                        Module.State.ONLINE -> 0b01
                        Module.State.ACTIVE -> 0b10
                        Module.State.OVERLOADED -> 0b11
                    } shl 1
                    val hasLoadedChargeFlag = (if (module.chargeId != null) 0b1 else 0b0) shl 3
                    val flags = enabledFlag or stateFlags or hasLoadedChargeFlag
                    return ModuleFlags(flags.toByte())
                }


            }


        }


        /**
         * Reads module flags.
         */
        private fun DataInput.readModuleFlags() = ModuleFlags(readByte())


        /**
         * Writes module flags.
         */
        private fun DataOutput.writeModuleFlags(moduleFlags: ModuleFlags) = writeByte(moduleFlags.value.toInt())


        /**
         * Reads a tactical mode.
         */
        private fun DataInput.readTacticalMode() = readOptionalEveId()?.let { StoredTacticalMode(it) }


        /**
         * Writes a tactical mode.
         */
        private fun DataOutput.writeTacticalMode(tacticalMode: StoredTacticalMode?) =
            writeOptionalEveId(tacticalMode?.itemId)


        /**
         * Reads the subsystems.
         */
        private fun DataInput.readSubsystems(): ValueByEnum<SubsystemType.Kind, StoredSubsystem>?{
            val hasSubsystems = readBoolean()
            if (!hasSubsystems)
                return null

            return valueByEnum {
                val itemId = readEveId()
                StoredSubsystem(itemId = itemId)
            }
        }


        /**
         * Writes the subsystems.
         */
        private fun DataOutput.writeSubsystems(subsystems: ValueByEnum<SubsystemType.Kind, StoredSubsystem>?){
            writeBoolean(subsystems != null)
            subsystems?.forEach { _, storedSubsystem ->
                writeEveId(storedSubsystem.itemId)
            }
        }


        /**
         * Reads a rack of modules.
         */
        private fun DataInput.readModuleRack(formatVersion: Int): List<StoredModule?> {
            val count = readUnsignedByte()
            return List(count) { readModule(formatVersion) }
        }


        /**
         * Writes a rack of modules.
         */
        private fun DataOutput.writeModuleRack(modules: List<StoredModule?>) {
            writeByte(modules.size)
            for (module in modules)
                writeModule(module)
        }


        /**
         * Reads a single module.
         */
        private fun DataInput.readModule(formatVersion: Int): StoredModule? {
            val id = readOptionalEveId()
            return if (id == null)
                null
            else {
                val flags = readModuleFlags()
                val chargeId = if (flags.hasLoadedCharge()) readEveId() else null
                val mutation = if (formatVersion >= 4) readMutation() else null
                val extraAttributes = readExtraModuleAttributes(formatVersion)
                StoredModule(
                    itemId = id,
                    enabled = flags.enabled(),
                    state = flags.moduleState(),
                    chargeId = chargeId,
                    mutation = mutation,
                    extraAttributes = extraAttributes
                )
            }
        }


        /**
         * Writes a single module.
         */
        private fun DataOutput.writeModule(module: StoredModule?) {
            writeOptionalEveId(module?.itemId)
            if (module == null)
                return
            writeModuleFlags(ModuleFlags.ofModule(module))
            if (module.chargeId != null)
                writeEveId(module.chargeId)
            writeMutation(module.mutation)
            writeExtraModuleAttributes(module.extraAttributes)
        }


        /**
         * Reads a mutation.
         */
        private fun DataInput.readMutation(): StoredMutation? {
            if (!readBoolean())
                return null
            val mutaplasmidId = readInt()
            val name = readUTF()
            val attributeIdsAndValues = List(readUnsignedShort()) {
                val attrId = readInt()
                val value = readDouble()
                Pair(attrId, value)
            }
            return StoredMutation(
                mutaplasmidId = mutaplasmidId,
                name = name,
                attributeIdsAndValues = attributeIdsAndValues
            )
        }


        /**
         * Writes a mutation.
         */
        private fun DataOutput.writeMutation(mutation: StoredMutation?) {
            writeBoolean(mutation != null)
            if (mutation == null)
                return
            writeInt(mutation.mutaplasmidId)
            writeUTF(mutation.name)
            writeShort(mutation.attributeIdsAndValues.size)
            for ((attrId, value) in mutation.attributeIdsAndValues) {
                writeInt(attrId)
                writeDouble(value)
            }
        }


        /**
        * Reads a module's extra attributes.
         */
        private fun DataInput.readExtraModuleAttributes(formatVersion: Int): List<ExtraAttribute<Module>> {
            val count = readUnsignedShort()
            return List(count) {
                val id = readInt()
                val reader = ExtraAttributes.readerForModuleById(id)
                reader(this, formatVersion)
            }
        }


        /**
         * Writes a module's extra attributes.
         */
        private fun DataOutput.writeExtraModuleAttributes(attributes: List<ExtraAttribute<Module>>) {
            writeShort(attributes.size)
            for (attribute in attributes) {
                writeInt(attribute.id)
                attribute.write(this)
            }
        }


        /**
         * Reads a list of drone groups.
         */
        private fun DataInput.readDroneGroups(formatVersion: Int): List<StoredDroneGroup> {
            val count = readUnsignedByte()
            return List(count) {
                val itemId = readEveId()
                val sizeAndActive = readUnsignedByte()
                val mutation = if (formatVersion >= 4) readMutation() else null
                StoredDroneGroup(
                    itemId = itemId,
                    size = sizeAndActive and 0x7f,
                    active = (sizeAndActive and 0x80) != 0,
                    mutation = mutation
                )
            }
        }


        /**
         * Writes the drone groups.
         */
        private fun DataOutput.writeDroneGroups(droneGroups: List<StoredDroneGroup>) {
            writeByte(droneGroups.size)
            for (droneGroup in droneGroups) {
                writeEveId(droneGroup.itemId)
                if ((droneGroup.size < 0) || (droneGroup.size > 127))
                    throw IllegalStateException("Drone group size (${droneGroup.size}) out of range")
                writeByte(
                    ((if (droneGroup.active) 1 else 0) shl 7) or
                            droneGroup.size
                )
                writeMutation(droneGroup.mutation)
            }
        }


        /**
         * Reads a list of cargo items.
         */
        private fun DataInput.readCargoItems(): List<StoredCargoItem> {
            val count = readUnsignedShort()
            return List(count) {
                val itemId = readEveId()
                val amount = readInt()
                StoredCargoItem(
                    itemId = itemId,
                    amount = amount
                )
            }
        }


        /**
         * Writes a list of cargo items.
         */
        private fun DataOutput.writeCargoItems(cargoItems: List<StoredCargoItem>) {
            writeShort(cargoItems.size)
            for (cargoItem in cargoItems) {
                writeEveId(cargoItem.itemId)
                writeInt(cargoItem.amount)
            }
        }


        /**
         * Reads a list of implants.
         */
        private fun DataInput.readImplants(): List<StoredImplant> {
            val count = readUnsignedByte()
            return List(count) {
                val itemId = readEveId()
                val enabled = readBoolean()
                StoredImplant(itemId = itemId, enabled = enabled)
            }
        }


        /**
         * Writes a list of implants.
         */
        private fun DataOutput.writeImplants(implants: List<StoredImplant>) {
            writeByte(implants.size)
            for (implant in implants){
                writeEveId(implant.itemId)
                writeBoolean(implant.enabled)
            }
        }


        /**
         * Reads a list of boosters.
         */
        private fun DataInput.readBoosters(formatVersion: Int): List<StoredBooster> {
            fun readAttributeIds() = List(readUnsignedByte()) { readEveId() }

            val count = readUnsignedByte()
            return List(count) {
                val itemId = readEveId()
                val enabled = readBoolean()
                val activeSideEffectAttributeIds = if (formatVersion >= 6) readAttributeIds() else emptyList()
                StoredBooster(itemId, enabled, activeSideEffectAttributeIds)
            }
        }


        /**
         * Writes a list of boosters.
         */
        private fun DataOutput.writeBoosters(boosters: List<StoredBooster>) {
            writeByte(boosters.size)
            for (booster in boosters){
                writeEveId(booster.itemId)
                writeBoolean(booster.enabled)
                booster.activeSideEffectPenalizedAttributeIds.let {
                    writeByte(it.size)
                    for (attributeId in it)
                        writeEveId(attributeId)
                }
            }
        }


        /**
         * Reads a list of environments.
         */
        private fun DataInput.readEnvironments(): List<StoredEnvironment> {
            val count = readUnsignedByte()
            return List(count) {
                val itemId = readEveId()
                val enabled = readBoolean()
                StoredEnvironment(itemId, enabled)
            }
        }


        /**
         * Writes a list of environments.
         */
        private fun DataOutput.writeEnvironments(environments: List<StoredEnvironment>) {
            writeByte(environments.size)
            for (environment in environments) {
                writeEveId(environment.itemId)
                writeBoolean(environment.enabled)
            }
        }


        /**
        * Reads a list of remote effects.
         */
        private fun DataInput.readRemoteEffects(): List<StoredRemoteEffect> {
            val count = readUnsignedByte()
            return buildList(count) {
                repeat(count) {
                    val fitId = readInt()
                    val enabled = readBoolean()
                    add(StoredRemoteEffect(fitId = fitId, enabled = enabled))
                }
            }
        }


        /**
         * Writes a list of remote effects.
         */
        private fun DataOutput.writeRemoteEffects(remoteEffects: List<StoredRemoteEffect>) {
            writeByte(remoteEffects.size)
            for (commandEffect in remoteEffects) {
                writeInt(commandEffect.fitId)
                writeBoolean(commandEffect.enabled)
            }
        }


        /**
         * Reads the module effects on a fit.
         */
        private fun DataInput.readModuleEffects(formatVersion: Int): List<StoredModule> {
            val count = readUnsignedByte()
            return List(count) { readModule(formatVersion)!! }
        }


        /**
         * Writes the module effects on a fit.
         */
        private fun DataOutput.writeModuleEffects(modules: List<StoredModule>) {
            writeByte(modules.size)
            for (module in modules)
                writeModule(module)
        }


        /**
         * Reads the drone effects on a fit.
         */
        private fun DataInput.readDroneEffects(formatVersion: Int): List<StoredDroneGroup> {
            return readDroneGroups(formatVersion)
        }


        /**
         * Writes the drone effects on a fit.
         */
        private fun DataOutput.writeDroneEffects(droneGroups: List<StoredDroneGroup>) {
            writeDroneGroups(droneGroups)
        }


        /**
         * Reads the tags of a fit.
         */
        private fun DataInput.readTags(): List<String> {
            val count = readUnsignedByte()
            return List(count) {
                readUTF()
            }
        }


        /**
         * Writes the tags of a fit.
         */
        private fun DataOutput.writeTags(tags: List<String>) {
            writeByte(tags.size)
            for (tag in tags) {
                writeUTF(tag)
            }
        }


        // This shouldn't actually ever be called because the fits are serialized by the FitRepoItem serializer
        // (in FittingRepository.kt)
        override val itemFormatVersion: Int
            get() = error("StoredFit format version is determined by the FitRepoItem serializer")


        /**
         * Reads a single [StoredFit].
         */
        override fun readItem(input: DataInput, formatVersion: Int): StoredFit = with(input) {
            val id = readInt()
            val creationTime = if (formatVersion >= 10) readLong() else System.currentTimeMillis()
            val lastModifiedTime = if (formatVersion >= 10) readLong() else creationTime

            StoredFit(
                id = id,
                creationTime = creationTime,
                lastModifiedTime = lastModifiedTime,
                name = readUTF(),
                tags = if (formatVersion >= 11) readTags() else emptyList(),
                skillSetId = if (formatVersion >= 5) readOptionalEveId() else null,
                shipTypeId = readEveId(),
                securityStatus = if (formatVersion >= 9) readOptionalSecurityStatus() else null,
                isFlagship = if (formatVersion >= 12) readBoolean() else false,
                tacticalMode = readTacticalMode(),
                subsystems = readSubsystems(),
                highSlotRack = readModuleRack(formatVersion),
                medSlotRack = readModuleRack(formatVersion),
                lowSlotRack = readModuleRack(formatVersion),
                rigs = readModuleRack(formatVersion),
                droneGroups = readDroneGroups(formatVersion),
                cargoItems = readCargoItems(),
                implants = readImplants(),
                boosters = readBoosters(formatVersion),
                environments = if (formatVersion >= 8) readEnvironments() else emptyList(),
                commandEffects = if (formatVersion >= 2) readRemoteEffects() else emptyList(),
                hostileEffects = if (formatVersion >= 3) readRemoteEffects() else emptyList(),
                friendlyEffects = if (formatVersion >= 3) readRemoteEffects() else emptyList(),
                moduleEffects = if (formatVersion >= 3) readModuleEffects(formatVersion) else emptyList(),
                droneEffects = if (formatVersion >= 3) readDroneEffects(formatVersion) else emptyList()
            )
        }


        /**
         * Writes a single [StoredFit].
         */
        override fun writeItem(output: DataOutput, item: StoredFit) {
            val fitId = item.id ?: throw IllegalArgumentException("Fit has not been assigned an id")

            with(output, item) {
                writeInt(fitId)
                writeLong(creationTime)
                writeLong(lastModifiedTime)
                writeUTF(name)
                writeTags(tags)
                writeOptionalEveId(skillSetId)
                writeEveId(shipTypeId)
                writeOptionalSecurityStatus(securityStatus)
                writeBoolean(isFlagship)
                writeTacticalMode(tacticalMode)
                writeSubsystems(subsystems)
                writeModuleRack(highSlotRack)
                writeModuleRack(medSlotRack)
                writeModuleRack(lowSlotRack)
                writeModuleRack(rigs)
                writeDroneGroups(droneGroups)
                writeCargoItems(cargoItems)
                writeImplants(implants)
                writeBoosters(boosters)
                writeEnvironments(environments)
                writeRemoteEffects(commandEffects)
                writeRemoteEffects(hostileEffects)
                writeRemoteEffects(friendlyEffects)
                writeModuleEffects(moduleEffects)
                writeDroneEffects(droneEffects)
            }
        }


    }


}


/**
 * Encapsulates functions related to reading/writing extra attributes.
 */
private object ExtraAttributes {


    /**
     * The id for [SpoolupCyclesExtraAttribute].
     */
    const val SPOOLUP_CYCLES = 1


    /**
     * The id for [AdaptationCyclesExtraAttribute].
     */
    const val ADAPTATION_CYCLES = 2


    /**
     * Maps extra module attribute ids to functions that read them.
     */
    private val MODULE_READ_FUNCTION_BY_ID: Map<Int, (DataInput, Int) -> ExtraAttribute<Module>> = mapOf(
        SPOOLUP_CYCLES to SpoolupCyclesExtraAttribute::read,
        ADAPTATION_CYCLES to AdaptationCyclesExtraAttribute::read
    )


    /**
     * Returns the reader function for the extra module attribute with the given id.
     */
    fun readerForModuleById(id: Int) = MODULE_READ_FUNCTION_BY_ID[id]
        ?: throw CorruptStoredFit("Unknown extra module attribute id: $id")


    /**
     * Returns the extra attributes for the given module.
     */
    fun forModule(module: Module): List<ExtraAttribute<Module>> = buildList(0) {
        addNotNull(SpoolupCyclesExtraAttribute.forModule(module))
        addNotNull(AdaptationCyclesExtraAttribute.forModule(module))
    }


}


/**
 * The interface for an extra attribute of some item.
 */
sealed interface ExtraAttribute<T : Any> {


    /**
     * The identifier of the extra attribute, when serialized.
     */
    val id: Int


    /**
     * Writes this extra attribute to disk (excluding the id).
     */
    fun write(output: DataOutput)


    /**
     * Applies this extra attribute to the target item.
     */
    context(FittingEngine.ModificationScope)
    fun applyTo(target: T)


}


/**
 * The extra attribute for the number of spoolup cycles (of triglavian modules).
 */
class SpoolupCyclesExtraAttribute(private val cycles: SpoolupCycles): ExtraAttribute<Module> {


    override val id: Int
        get() = ExtraAttributes.SPOOLUP_CYCLES


    override fun write(output: DataOutput) {
        output.writeDouble(
            when (cycles) {
                is SpoolupCycles.Maximum -> MAX_COUNT
                is SpoolupCycles.Number -> cycles.value
            }
        )
    }


    context(FittingEngine.ModificationScope)
    override fun applyTo(target: Module) {
        recomputePropertyValues()  // To first apply any bonuses to max spools
        target.setSpoolupCycles(cycles.valueFor(target))
    }


    companion object {


        /**
         * The value of spoolup cycles written to disk indicating the actual number of spoolup cycles should be the
         * maximum of the module.
         */
        private const val MAX_COUNT = -1.0


        /**
         * The [SpoolupCyclesExtraAttribute] which sets the number of spoolup cycles to the maximum of the module.
         */
        val MAX = SpoolupCyclesExtraAttribute(SpoolupCycles.Maximum)


        /**
        * Reads a [SpoolupCyclesExtraAttribute] from the given input.
         */
        fun read(input: DataInput, formatVersion: Int): SpoolupCyclesExtraAttribute {
            val value = if (formatVersion < 7) {
                input.readInt()
                MAX_COUNT  // Ignore the value in previous versions, as it can be wrong, and just use the maximum
            } else
                input.readDouble()

            return SpoolupCyclesExtraAttribute(
                if (value == MAX_COUNT)
                    SpoolupCycles.Maximum
                else
                    SpoolupCycles.Number(value)
            )
        }


        /**
         * Returns the [SpoolupCyclesExtraAttribute] for the given module; `null` if not applicable.
         */
        fun forModule(module: Module): SpoolupCyclesExtraAttribute? {
            val cyclesValue = module.spoolupCycles?.value ?: return null
            val maxCycles = module.maxSpoolupCycles ?: return null
            return SpoolupCyclesExtraAttribute(
                if (cyclesValue == maxCycles)
                    SpoolupCycles.Maximum
                else
                    SpoolupCycles.Number(cyclesValue)
            )
        }


    }


}


/**
 * The extra attribute for the number of adaptation cycles (of e.g. Reactive Armor Hardener).
 */
class AdaptationCyclesExtraAttribute(private val cycleCount: Int): ExtraAttribute<Module> {


    override val id: Int
        get() = ExtraAttributes.ADAPTATION_CYCLES


    override fun write(output: DataOutput) {
        output.writeInt(cycleCount)
    }


    context(FittingEngine.ModificationScope)
    override fun applyTo(target: Module) {
        target.setAdaptationCycles(cycleCount)
    }


    companion object {


        /**
         * The [AdaptationCyclesExtraAttribute] which sets the number of adaptation cycles to the maximum.
         */
        val MAX = AdaptationCyclesExtraAttribute(AdaptationCycles.Maximum.cycleCount)


        /**
         * Reads a [SpoolupCyclesExtraAttribute] from the given input.
         */
        fun read(input: DataInput, @Suppress("unused") formatVersion: Int): AdaptationCyclesExtraAttribute {
            val cycleCount = input.readInt()
            return AdaptationCyclesExtraAttribute(cycleCount)
        }


        /**
         * Returns the [SpoolupCyclesExtraAttribute] for the given module; `null` if not applicable.
         */
        fun forModule(module: Module): AdaptationCyclesExtraAttribute? {
            val cycleCount = module.adaptationCycles?.value ?: return null
            return AdaptationCyclesExtraAttribute(cycleCount)
        }


    }


}


/**
 * The exception thrown when the fit data is unexpected in some way.
 */
class CorruptStoredFit(message: String): IllegalArgumentException(message)
