package theorycrafter

import androidx.compose.runtime.*
import eve.data.*
import eve.data.utils.ValueByEnum
import eve.data.utils.forEach
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import theorycrafter.fitting.*
import theorycrafter.formats.FitsImportResult
import theorycrafter.formats.ImportedRemoteFitEffect
import theorycrafter.storage.*
import theorycrafter.storage.StoredFit.Companion.withUpdatesFrom
import theorycrafter.storage.StoredFit.StoredRemoteEffect
import theorycrafter.ui.fiteditor.defaultInitialSubsystemsOrNull
import theorycrafter.ui.fiteditor.initialTacticalMode
import theorycrafter.utils.bfsSequence
import theorycrafter.utils.forEachNonNullIndexed
import theorycrafter.utils.runCatchingExceptCancellation


/**
 * The part of [TheorycrafterContext] responsible for fit management.
 */
class FitsContext(


    /**
     * The underlying repository.
     */
    private val repo: FittingRepository,


    /**
     * The fitting engine.
     */
    private val fittingEngine: FittingEngine,


    /**
     * The callbacks to invoke when fits change.
     */
    private val changeListener: ChangeListener


) {


    /**
     * The list of fit handles, as a Compose state object.
     */
    private val mutableHandles = repo.fits
        .sortedBy { it.id!! }
        .map { FitHandleImpl(it) }
        .toMutableStateList()


    /**
     * Maps fit handles by their ids.
     */
    private val handleById: MutableMap<Int, FitHandle> =
        mutableHandles
            .associateBy { it.fitId }
            .toMutableMap()


    /**
     * The list of fits handles, as an unmodifiable [List], a Compose state object reflecting the actual list of fits
     * in the repository.
     */
    val handles: List<FitHandle>
        get() = mutableHandles


    /**
     * The mutex that protects loading engine fits.
     */
    private val engineFitLoadingMutex = Mutex()


    /**
     * An int state we bump whenever the fit handles change.
     */
    private var _handlesKey by mutableIntStateOf(0)


    /**
     * A Compose [State] value that changes whenever the fit handles change.
     *
     * This can be observed or used as a key in [remember].
     */
    val handlesKey: Any
        get() = _handlesKey


    /**
     * Maps [FitHandle]s by their [Fit]s.
     */
    private val handleByEngineFit: MutableMap<Fit, FitHandle> = mutableMapOf()


    /**
     * Rewrites and replaces the [FitHandle.storedFit].
     */
    private suspend fun FitHandle.replaceStoredFit(updated: (StoredFit) -> StoredFit) {
        val currentStoredFit = impl.requireStoredFit()
        val updatedStoredFit = updated(currentStoredFit)
        repo.updateFit(currentStoredFit, updatedStoredFit)
        impl.storedFit = updatedStoredFit
    }


    /**
     * Creates a new fit with the given name, for the given ship type, and returns its [FitHandle].
     */
    suspend fun addEmpty(
        shipType: ShipType,
        name: String,
        tags: List<String> = emptyList(),
    ): FitHandle {
        return add(
            StoredFit.newEmpty(
                name = name,
                tags = tags,
                skillSetId = null,
                shipType = shipType,
                tacticalMode = initialTacticalMode(shipType),
                subsystems = shipType.defaultInitialSubsystemsOrNull()
            )
        )
    }


    /**
     * Adds the given fit and returns its [FitHandle].
     */
    suspend fun add(storedFit: StoredFit): FitHandle {
        val fitId = repo.addFit(storedFit)
        return FitHandleImpl(storedFit).also {
            mutableHandles.add(it)
            handleById[fitId] = it
            _handlesKey += 1
            changeListener.onFitAdded(it)
        }
    }


    /**
     * Adds imported [fits] in bulk.
     *
     * Unlike [add], this method also allows adding remote fit effects between the imported fits, which are taken from
     * [importResult] (it's not possible to create a [StoredRemoteEffect] before adding the affecting fit, because it
     * requires its id).
     */
    suspend fun addImportedFits(importResult: FitsImportResult, fits: Collection<StoredFit>) {
        // First, assign ids to all the fits
        val fitById = mutableMapOf<Int, StoredFit>()
        for (addedFit in fits) {
            val id = repo.assignFitId(addedFit)
            fitById[id] = addedFit
        }

        // Then add the remote effects to the fits
        fun buildRemoteEffects(importedEffects: List<ImportedRemoteFitEffect>) =
            buildMap<StoredFit, MutableList<StoredRemoteEffect>> {
                for (commandEffect in importedEffects) {
                    val affectingFit = fitById[commandEffect.affectingFit.id] ?: continue
                    val affectedFit = fitById[commandEffect.affectedFit.id] ?: continue
                    getOrPut(affectedFit, ::mutableListOf).add(
                        StoredRemoteEffect(affectingFit.id!!, commandEffect.enabled)
                    )
                }
            }
        val commandEffectsByAffectedFit = buildRemoteEffects(importResult.commandFitEffects)
        val friendlyEffectsByAffectedFit = buildRemoteEffects(importResult.friendlyFitEffects)
        val hostileEffectsByAffectedFit = buildRemoteEffects(importResult.hostileFitEffects)
        for (fit in fits) {
            fitById[fit.id!!] = fit.copy(
                fitTimes = StoredFit.FitTimes.Unchanged,
                commandEffects = commandEffectsByAffectedFit[fit] ?: emptyList(),
                friendlyEffects = friendlyEffectsByAffectedFit[fit] ?: emptyList(),
                hostileEffects = hostileEffectsByAffectedFit[fit] ?: emptyList(),
            )
        }

        // Now add the fits to the repo
        for (addedFit in fitById.values) {
            val fitId = repo.addFit(addedFit)
            val handle = FitHandleImpl(addedFit)
            mutableHandles.add(handle)
            handleById[fitId] = handle
            changeListener.onFitAdded(handle)
        }

        _handlesKey += 1
    }


    /**
     * Duplicates the given fit, assigning the new fit the given name, and tags.
     *
     * Returns the handle to the new fit.
     */
    suspend fun duplicate(
        fitHandle: FitHandle,
        name: String,
        tags: List<String> = fitHandle.requireStoredFit().tags
    ): FitHandle {
        val newStoredFit = fitHandle.requireStoredFit().copy(
            fitTimes = StoredFit.FitTimes.NewFit,
            id = null,
            name = name,
            tags = tags,
        )
        return add(newStoredFit)
    }


    /**
     * Changes the given fit's name.
     */
    suspend fun setName(handle: FitHandle, name: String) {
        handle.replaceStoredFit {
            it.copy(
                name = name,
                fitTimes = StoredFit.FitTimes.Unchanged
            )
        }

        _handlesKey += 1
        changeListener.onFitNameChanged(handle)
    }


    /**
     * Changes the given fit's tags.
     */
    suspend fun setTags(handle: FitHandle, tags: List<String>) {
        handle.replaceStoredFit {
            it.copy(
                tags = tags,
                fitTimes = StoredFit.FitTimes.Unchanged
            )
        }

        _handlesKey += 1
        changeListener.onFitTagsChanged(handle)
    }


    /**
     * Sets the skill set of the given fits.
     *
     * A `null` [skillSetHandle] indicates the fits should use the default skill set.
     */
    suspend fun setSkillSet(handles: Collection<FitHandle>, skillSetHandle: SkillSetHandle?) {
        // Update the engine fits, if any
        val engineFits = handles.mapNotNull { it.impl.engineFit }
        if (engineFits.isNotEmpty()) {
            val engineSkillSet = TheorycrafterContext.skillSets.engineSkillSetOf(skillSetHandle)
            fittingEngine.modify {
                for (engineFit in engineFits)
                    engineFit.setSkillSet(engineSkillSet)
            }
        }

        // Update the stored fits
        for (handle in handles) {
            handle.replaceStoredFit {
                it.copy(fitTimes = StoredFit.FitTimes.Unchanged, skillSetId = skillSetHandle?.skillSetId)
            }
        }
    }


    /**
     * Sets the skill set of the given fit.
     *
     * A `null` [skillSetHandle] indicates the fit should use the default skill set.
     */
    suspend fun setSkillSet(handle: FitHandle, skillSetHandle: SkillSetHandle?) {
        setSkillSet(listOf(handle), skillSetHandle)
    }


    /**
     * Sets the flagship status of the given fit.
     */
    suspend fun setFlagship(fit: Fit, value: Boolean) {
        fit.isFlagship = value
        save(fit)
    }


    /**
     * Saves the given fit to disk.
     */
    private suspend fun save(fit: Fit) {
        handleOf(fit).replaceStoredFit {
            it.withUpdatesFrom(
                fit = fit,
                fitTimes = StoredFit.FitTimes.ModifiedFit,
                fitIdByFit = { f -> handleByEngineFit[f]!!.fitId }
            )
        }
    }


    /**
     * Deletes the given fits.
     */
    suspend fun delete(handles: Collection<FitHandle>) {
        repo.deleteFits(handles.mapNotNull { it.storedFit })

        mutableHandles.removeAll(handles.toSet())
        for (fitHandle in handles)
            handleById.remove(fitHandle.fitId)

        val deletedEngineFits = handles.mapNotNull{ it.impl.engineFit }.toSet()
        handleByEngineFit.keys.removeAll(deletedEngineFits)

        val affectedFits = mutableSetOf<Fit>()
        fittingEngine.modify {
            for (fit in deletedEngineFits) {
                fit.remove().also {
                    affectedFits.addAll(it)
                }
            }
        }

        for (fitHandle in handles) {
            fitHandle.impl.storedFit = null
            fitHandle.impl.engineFit = null
        }

        _handlesKey += 1
        changeListener.onFitsDeleted(handles, affectedFits)
    }


    /**
     * Returns the engine [Fit] corresponding to the given [FitHandle], loading it into the engine if needed.
     */
    suspend fun engineFitOf(handle: FitHandle): Fit {
        // Avoid locking the mutex if it's already been loaded
        handle.impl.engineFit?.let { return it }

        engineFitLoadingMutex.withLock(owner = null) {
            val fit = handle.impl.engineFit
            if (fit != null)
                return fit

            return handle.impl.requireStoredFit().loadInto(fittingEngine)
        }
    }


    /**
     * Returns the already-loaded [Fit] corresponding to the given [FitHandle], or `null` if it hasn't yet been loaded.
     */
    fun loadedEngineFitOf(handle: FitHandle): Fit? {
        return handle.impl.engineFit
    }


    /**
     * Loads the fit of the given [FitHandle] and returns it as a state.
     */
    @Composable
    fun rememberEngineFitOf(
        handle: FitHandle?,
        onError: (Throwable) -> Unit = { throw it }
    ): Fit? {
        if (handle == null)
            return null

        // If the fit is already loaded, we want to return it immediately, and not even call engineFitOf.
        // If the fit hasn't yet been loaded, we don't want to change the value to `null`, because that
        // would make the fit editor display a `null` value. What we want is to have it display the previous
        // fit until the new one has been loaded.

        var rememberedFit: Fit? by remember { mutableStateOf(null) }

        val loadedFit = TheorycrafterContext.fits.loadedEngineFitOf(handle)
        if (loadedFit != null) {
            rememberedFit = loadedFit
            return loadedFit
        }

        val currentOnError by rememberUpdatedState(onError)
        LaunchedEffect(handle) {
            rememberedFit = runCatchingExceptCancellation {
                TheorycrafterContext.fits.engineFitOf(handle)
            }.onFailure(currentOnError).getOrNull()
        }

        return rememberedFit
    }



    /**
     * Returns the [StoredFit] of the given [FitHandle].
     */
    fun storedFitOf(handle: FitHandle): StoredFit {
        return handle.impl.requireStoredFit()
    }


    /**
     * Returns the [FitHandle] of the given engine [Fit].
     */
    fun handleOf(fit: Fit): FitHandle {
        return handleByEngineFit[fit] ?: error("No fit handle exists for $fit")
    }


    /**
     * Returns the [FitHandle] with the given id, or `null` if no such fit exists.
     */
    fun handleById(id: Int): FitHandle? {
        return handleById[id]
    }


    /**
     * Loads the [StoredFit] into the engine and returns it.
     */
    private suspend fun StoredFit.loadInto(fittingEngine: FittingEngine): Fit {
        val fitId = this.id!!

        val dependencyFitHandles = handleById[fitId]!!.bfsSequence(
            childrenOf = {
                it.impl.requireStoredFit().getDependencyFitIds().mapNotNull { dependencyFitId ->
                    handleById[dependencyFitId]
                }
            },
            initialQueueCapacity = 1
        ).toSet()

        val fitById = mutableMapOf<Int, Fit>()

        // Get the fits that need to be loaded; the rest put into fitById immediately
        val fitHandlesToLoad = mutableListOf<FitHandle>()
        for (fitHandle in dependencyFitHandles) {
            val engineFit = fitHandle.impl.engineFit
            if (engineFit == null)
                fitHandlesToLoad.add(fitHandle)
            else
                fitById[fitHandle.fitId] = engineFit
        }

        // Load all the new dependency fits, but only the self effects
        for (fitHandle in fitHandlesToLoad)
            fitById[fitHandle.fitId] = fitHandle.impl.requireStoredFit().loadSelfEffects(fittingEngine)

        // Load all the cross-fit effects
        for (fitHandle in fitHandlesToLoad) {
            val engineFit = fitById[fitHandle.fitId]!!
            fitHandle.impl.requireStoredFit().loadEffectsFromDependencyFits(engineFit, fittingEngine, fitById)
        }

        // Remember all the new fits we loaded
        for (fitHandle in fitHandlesToLoad) {
            val engineFit = fitById[fitHandle.fitId]!!
            fitHandle.impl.engineFit = engineFit
            handleByEngineFit[engineFit] = fitHandle
        }

        return fitById[fitId]!!
    }


    /**
     * Makes modifications to fits and saves them.
     *
     * Only modifications are allowed. Creating or deleting fits is not allowed via this function.
     */
    suspend fun <T> modifyAndSave(block: FittingEngine.ModificationScope.() -> T): T {
        var modificationScope: FitSavingModificationScope? = null
        try {
            return fittingEngine.modify {
                val scope = FitSavingModificationScope(fittingEngine, this)
                modificationScope = scope
                scope.block()
            }
        } finally {
            modificationScope?.let {
                for (fit in it.modifiedFits)
                    save(fit)
            }
        }
    }


    /**
     * Makes temporary fittings.
     *
     * Modifying any existing fits is not allowed within [block], and all fits created by it are automatically removed
     * when it returns.
     * This function can be used to determine theoretical properties of eve items. For example, it can be used to find
     * the ammo with the highest damage to fit into a weapon.
     */
    suspend fun <T> withTemporaryFit(

        /**
         * A ship to create a temporary fit for, which can be accessed with [block] via [TempFittingScope.tempFit].
         */
        shipType: ShipType,

        /**
         * Code to execute within the temporary fitting scope.
         */
        block: suspend TempFittingScope.() -> T

    ): T {
        val tempFittingScope = makeTempFittingScope(shipType)
        return try {
            tempFittingScope.block()
        } finally {
            tempFittingScope.dispose()
        }
    }


    /**
     * Returns a remembered context for temporary fitting.
     *
     * The given ship is fitted into [TempFittingScope.tempFit].
     */
    @Composable
    fun rememberTempFittingScope(key: Any?, shipType: ShipType): TempFittingScope {
        val context = remember(key, shipType) { makeTempFittingScope(shipType) }
        DisposableEffect(context) {
            onDispose {
                runBlocking {
                    context.dispose()
                }
            }
        }

        return context
    }


    /**
     * Returns a context for temporary fitting. It is the caller's responsibility to dispose of it when done.
     */
    fun makeTempFittingScope(shipType: ShipType): TempFittingScope {
        return TempFittingScope(fittingEngine, shipType)
    }


    /**
     * The interface for listening to fit changes.
     */
    interface ChangeListener {


        /**
         * Invoked when a new fit has been added.
         */
        fun onFitAdded(fitHandle: FitHandle)


        /**
         * Invoked when a fit's name has been changed.
         */
        fun onFitNameChanged(fitHandle: FitHandle)


        /**
         * Invoked when a fit's tags have been changed.
         */
        fun onFitTagsChanged(fitHandle: FitHandle)


        /**
         * Invoked when fits have been deleted.
         *
         * [affectedFits] holds the fits that had been affected by the removal of the deleted fits (because they were
         * the target of a remote effect by a deleted fit).
         */
        fun onFitsDeleted(
            deletedFitHandles: Collection<FitHandle>,
            affectedFits: Collection<Fit>
        )


    }


}


/**
 * A handle to a fit. To obtain the [Fit] this handle is for, use [FitsContext.engineFitOf].
 */
@Stable
sealed interface FitHandle {


    /**
     * The id of the fit.
     */
    val fitId: Int


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


    /**
     * The [StoredFit]; `null` if it has been deleted.
     */
    val storedFit: StoredFit?


    /**
     * Returns the stored fit, throwing an exception if it has been deleted.
     */
    fun requireStoredFit(): StoredFit = storedFit ?: error("Fit has been deleted")


    /**
     * The type of the ship of this fit.
     */
    val shipTypeId: Int
        get() = requireStoredFit().shipTypeId


    /**
     * Whether the fit of this handle has been deleted.
     */
    val isDeleted: Boolean


}


/**
 * The actual implementation of [FitHandle], which allows us to:
 * 1. Hold the underlying [StoredFit], and to replace it when saving the fit on disk (see [FitsContext.save]).
 * 2. Hold the underlying engine [Fit], without a need for an extra map in [FitsContext].
 */
private class FitHandleImpl(storedFit: StoredFit): FitHandle {


    /**
     * The [StoredFit]; `null` when it has been deleted.
     */
    override var storedFit: StoredFit? by mutableStateOf(storedFit)


    /**
     * The engine [Fit].
     */
    var engineFit: Fit? = null


    override val fitId: Int
        get() = requireStoredFit().id!!


    override val name: String
        get() = requireStoredFit().name


    override val isDeleted: Boolean by derivedStateOf(structuralEqualityPolicy()) {
        this.storedFit == null
    }


    override fun toString(): String {
        return storedFit?.let { "$name [id=$fitId]" } ?: "Deleted fit"
    }


}


/**
 * A shortcut for casting [FitHandle] into [FitHandleImpl].
 */
private inline val FitHandle.impl: FitHandleImpl
    get() = this as FitHandleImpl


/**
 * Loads this [StoredFit] into the engine, except for any effects from other fits; returns the resulting engine [Fit].
 */
@Suppress("SuspiciousCollectionReassignment")
private suspend fun StoredFit.loadSelfEffects(fittingEngine: FittingEngine): Fit {
    // This handles correctly with both default and unknown skill set id
    val skillSetHandle = skillSetId?.let { TheorycrafterContext.skillSets.handleByIdOrNull(it) }
    val skillSet = TheorycrafterContext.skillSets.engineSkillSetOf(skillSetHandle)

    val eveData = fittingEngine.eveData
    with(eveData) {
        return fittingEngine.modify {

            fun Fit.setTacticalMode(storedTacticalMode: StoredFit.StoredTacticalMode) {
                setTacticalMode(
                    tacticalModeTypeOrNull(storedTacticalMode.itemId)
                        ?: throw CorruptStoredFit("Unknown tactical mode type id: ${storedTacticalMode.itemId}")
                )
            }

            fun Fit.setSubsystems(subsystems: ValueByEnum<SubsystemType.Kind, StoredFit.StoredSubsystem>) {
                subsystems.forEach { _, storedSubsystem ->
                    val subsystemType = subsystemTypeOrNull(storedSubsystem.itemId)
                        ?: throw CorruptStoredFit("Unknown subsystem type id: ${storedSubsystem.itemId}")
                    setSubsystem(subsystemType)
                }
            }

            fun <T: EveItemType> T.mutatedWith(
                mutation: StoredFit.StoredMutation?,
                mutated: T.(EveData, Mutaplasmid, String, Map<Attribute<*>, Double>) -> T
            ): T {
                if (mutation == null)
                    return this
                val mutaplasmid = mutaplasmid(mutation.mutaplasmidId)
                val name = mutation.name
                val valueByAttribute = mutation.attributeIdsAndValues.associate { (attrId, value) ->
                    attributes[attrId] to value
                }
                return mutated(eveData, mutaplasmid, name, valueByAttribute)
            }

            fun StoredFit.StoredModule.moduleType(): ModuleType {
                val baseType = moduleTypeOrNull(itemId) ?: throw CorruptStoredFit("Unknown module type id: $itemId")
                return baseType.mutatedWith(mutation, ModuleType::mutated)
            }

            fun Module.configureFrom(module: StoredFit.StoredModule) {
                val charge = module.chargeId?.let { id ->
                    chargeTypeOrNull(id) ?: throw CorruptStoredFit("Unknown charge type id: $id")
                }
                setEnabled(module.enabled)
                setState(module.state)
                if (charge != null)
                    setCharge(charge)

                // Add default extra attributes
                var extraAttributes = module.extraAttributes
                if ((spoolupCycles != null) && extraAttributes.none { it is SpoolupCyclesExtraAttribute })
                    extraAttributes += SpoolupCyclesExtraAttribute.MAX
                if ((adaptationCycles != null) && extraAttributes.none { it is AdaptationCyclesExtraAttribute })
                    extraAttributes += AdaptationCyclesExtraAttribute.MAX
                for (extraAttribute in extraAttributes)
                    extraAttribute.applyTo(this)
            }

            fun Fit.addRack(modules: List<StoredFit.StoredModule?>) {
                modules.forEachNonNullIndexed { index, module ->
                    fitModule(module.moduleType(), slotIndex = index).also {
                        it.configureFrom(module)
                    }
                }
            }

            fun StoredFit.StoredDroneGroup.droneType(): DroneType {
                val baseType = droneTypeOrNull(itemId) ?: throw CorruptStoredFit("Unknown drone type id: $itemId")
                return baseType.mutatedWith(mutation, DroneType::mutated)
            }

            fun Fit.addDroneGroup(droneGroup: StoredFit.StoredDroneGroup) {
                addDroneGroup(droneGroup.droneType(), size = droneGroup.size).also {
                    it.setActive(droneGroup.active)
                }
            }

            fun Fit.addCargoItem(cargoItem: StoredFit.StoredCargoItem) {
                val itemType = cargoItemTypeOrNull(cargoItem.itemId)
                    ?: throw CorruptStoredFit("Unknown cargo item type id: ${cargoItem.itemId}")
                addCargoItem(itemType, amount = cargoItem.amount)
            }

            fun Fit.addImplant(implant: StoredFit.StoredImplant) {
                val implantType = implantTypes.getOrNull(implant.itemId)
                    ?: throw CorruptStoredFit("Unknown implant type id: ${implant.itemId}")
                fitImplant(implantType).also {
                    it.setEnabled(implant.enabled)
                }
            }

            fun Fit.addBooster(booster: StoredFit.StoredBooster) {
                val boosterType = boosterTypeOrNull(booster.itemId)
                    ?: throw CorruptStoredFit("Unknown booster type id: ${booster.itemId}")
                fitBooster(boosterType).also {
                    it.setEnabled(booster.enabled)
                    for (sideEffect in it.type.sideEffects) {
                        val active = sideEffect.penalizedAttribute.id in booster.activeSideEffectPenalizedAttributeIds
                        it.setSideEffectActive(sideEffect, active = active)
                    }
                }
            }

            fun Fit.addEnvironment(environment: StoredFit.StoredEnvironment) {
                val envType = environmentTypeOrNull(environment.itemId)
                    ?: throw CorruptStoredFit("Unknown environment type id: ${environment.itemId}")
                addEnvironment(envType).also {
                    it.setEnabled(environment.enabled)
                }
            }

            fun Fit.addModuleEffect(module: StoredFit.StoredModule) {
                addModuleEffect(module.moduleType())?.also {
                    it.configureFrom(module)
                }
            }

            fun Fit.addDroneEffect(droneGroup: StoredFit.StoredDroneGroup) {
                addDroneEffect(droneGroup.droneType(), size = droneGroup.size).also {
                    it.setActive(droneGroup.active)
                }
            }

            val shipType = shipTypeOrNull(shipTypeId) ?: throw CorruptStoredFit("Unknown ship type id: $shipTypeId")

            newFit(shipType, skillSet).also { fit ->
                if (fit.ship.pilotSecurityStatus != null)
                    fit.ship.setPilotSecurityStatus(securityStatus?.toDouble() ?: 0.0)
                fit.isFlagship = isFlagship
                if (shipType.hasTacticalModes && (tacticalMode != null))
                    fit.setTacticalMode(tacticalMode)
                if (shipType.usesSubsystems && (subsystems != null))
                    fit.setSubsystems(subsystems)
                fit.addRack(highSlotRack)
                fit.addRack(medSlotRack)
                fit.addRack(lowSlotRack)
                fit.addRack(rigs)
                for (droneGroup in droneGroups)
                    fit.addDroneGroup(droneGroup)
                for (cargoItem in cargoItems)
                    fit.addCargoItem(cargoItem)
                for (implant in implants)
                    fit.addImplant(implant)
                for (booster in boosters)
                    fit.addBooster(booster)
                for (environment in environments)
                    fit.addEnvironment(environment)
                for (module in moduleEffects)
                    fit.addModuleEffect(module)
                for (droneGroup in droneEffects)
                    fit.addDroneEffect(droneGroup)
            }
        }
    }
}


/**
 * Loads effects other fits have on the fit.
 */
private suspend fun StoredFit.loadEffectsFromDependencyFits(
    fit: Fit,
    fittingEngine: FittingEngine,
    fitById: Map<Int, Fit>
) {
    // In the most common don't bother opening a modification scope
    if (commandEffects.isEmpty() && hostileEffects.isEmpty() && friendlyEffects.isEmpty())
        return

    fittingEngine.modify {
        for (storedCommandEffect in commandEffects) {
            val commandingFit = fitById[storedCommandEffect.fitId] ?: continue  // Ignore stale dependency fits
            val commandEffect = fit.addCommandEffect(commandingFit)
            commandEffect.setEnabled(storedCommandEffect.enabled)
        }
        for (storedHostileEffect in hostileEffects) {
            val hostileFit = fitById[storedHostileEffect.fitId] ?: continue  // Ignore stale dependency fits
            val hostileEffect = fit.addHostileEffect(hostileFit)
            hostileEffect.setEnabled(storedHostileEffect.enabled)
        }
        for (storedFriendlyEffect in friendlyEffects) {
            val friendlyFit = fitById[storedFriendlyEffect.fitId] ?: continue  // Ignore stale dependency fits
            val friendlyEffect = fit.addFriendlyEffect(friendlyFit)
            friendlyEffect.setEnabled(storedFriendlyEffect.enabled)
        }
    }
}


/**
 * A [FittingEngine.ModificationScope] that remembers all the fits that were modified, so that they can be saved later.
 */
private class FitSavingModificationScope(
    private val fittingEngine: FittingEngine,
    private val engineModificationScope: FittingEngine.ModificationScope
) : FittingEngine.ModificationScope {


    /**
     * The fits that have been modified in this scope.
     */
    val modifiedFits = mutableSetOf<Fit>()


    override fun newSkillSet(levelOfSkill: (SkillType) -> Int): SkillSet {
        error("New skill sets should be created via SkillSetsContext")
    }


    override fun SkillSet.setLevels(skillAndLevelList: Collection<Pair<SkillType, Int>>) {
        error("Skill sets should be edited via SkillSetsContext")
    }


    override fun SkillSet.remove() {
        error("Skill sets should be removed via SkillSetsContext")
    }


    override fun newFit(shipType: ShipType, skillSet: SkillSet?): Fit {
        error("New fits should be created via FitsContext.addEmpty")
    }


    override fun Fit.remove(): Collection<Fit> {
        error("Fits should be removed via FitsContext.delete")
    }


    private fun <T> modify(fit: Fit, block: FittingEngine.ModificationScope.() -> T): T {
        modifiedFits.add(fit.auxiliaryFitOf ?: fit)

        return engineModificationScope.block()
    }


    /**
     * Verify that the size of a drone group is in the valid range.
     */
    private fun verifyDroneGroupSize(size: Int) {
        // Not more than 127 because of how we store it on disk
        if (size !in 1..127)
            throw IllegalArgumentException("DroneGroup size must be between 1 and 127")
    }


    override fun Fit.setSkillSet(skillSet: SkillSet) {
        return modify(this) {
            setSkillSet(skillSet)
        }
    }


    override fun Fit.setTacticalMode(tacticalModeType: TacticalModeType): TacticalMode {
        return modify(this) {
            setTacticalMode(tacticalModeType)
        }
    }


    override fun Fit.setSubsystem(subsystemType: SubsystemType): Subsystem {
        return modify(this) {
            setSubsystem(subsystemType)
        }
    }


    override fun Fit.fitModule(moduleType: ModuleType, slotIndex: Int): Module {
        return modify(this) {
            fitModule(moduleType, slotIndex)
        }
    }


    override fun Fit.removeModule(slotType: ModuleSlotType, slotIndex: Int) {
        return modify(this) {
            removeModule(slotType, slotIndex)
        }
    }


    override fun Fit.removeModule(module: Module) {
        return modify(this) {
            removeModule(module)
        }
    }


    override fun Fit.reorderModules(slotType: ModuleSlotType, newOrder: (Int) -> Int) {
        return modify(this) {
            reorderModules(slotType, newOrder)
        }
    }


    override fun Module.setState(newState: Module.State) {
        return modify(fit) {
            setState(newState)
        }
    }


    override fun Module.setCharge(charge: ChargeType): Charge {
        return modify(fit) {
            setCharge(charge)
        }
    }


    override fun Module.removeCharge() {
        return modify(fit) {
            removeCharge()
        }
    }


    override fun Fit.addDroneGroup(droneType: DroneType, size: Int, index: Int?): DroneGroup {
        verifyDroneGroupSize(size)
        return modify(this) {
            addDroneGroup(droneType, size = size, index = index)
        }
    }


    override fun Fit.removeDroneGroup(droneGroup: DroneGroup) {
        return modify(this) {
            removeDroneGroup(droneGroup)
        }
    }


    override fun DroneGroup.setActive(isActive: Boolean) {
        return modify(fit) {
            setActive(isActive)
        }
    }


    override fun DroneGroup.setSize(size: Int) {
        verifyDroneGroupSize(size)
        return modify(fit) {
            setSize(size)
        }
    }


    override fun Fit.addCargoItem(itemType: EveItemType, amount: Int, index: Int?): CargoItem {
        return modify(this) {
            addCargoItem(itemType, amount = amount, index = index)
        }
    }


    override fun Fit.removeCargoItem(cargoItem: CargoItem) {
        return modify(this) {
            removeCargoItem(cargoItem)
        }
    }


    override fun CargoItem.setAmount(amount: Int) {
        return modify(fit) {
            setAmount(amount)
        }
    }


    override fun CargoItem.moveTo(index: Int) {
        return modify(fit) {
            moveTo(index)
        }
    }


    override fun Fit.fitImplant(implantType: ImplantType): Implant {
        return modify(this) {
            fitImplant(implantType)
        }
    }


    override fun Fit.removeImplant(slotIndex: Int) {
        return modify(this) {
            removeImplant(slotIndex)
        }
    }


    override fun Fit.fitBooster(boosterType: BoosterType): Booster {
        return modify(this) {
            fitBooster(boosterType)
        }
    }


    override fun Booster.setSideEffectActive(sideEffect: BoosterType.SideEffect, active: Boolean) {
        return modify(fit) {
            setSideEffectActive(sideEffect, active)
        }
    }


    override fun Fit.removeBooster(slotIndex: Int) {
        return modify(this) {
            removeBooster(slotIndex)
        }
    }


    override fun Fit.addEnvironment(environmentType: EnvironmentType, index: Int?): Environment {
        return modify(this) {
            addEnvironment(environmentType, index)
        }
    }


    override fun Fit.removeEnvironment(environment: Environment) {
        return modify(this) {
            removeEnvironment(environment)
        }
    }


    override fun Fit.addCommandEffect(source: Fit, index: Int?): RemoteEffect {
        return modify(this) {
            addCommandEffect(source, index)
        }
    }


    override fun Fit.removeCommandEffect(effect: RemoteEffect) {
        modify(this) {
            removeCommandEffect(effect)
        }
    }


    override fun Fit.addHostileEffect(source: Fit, index: Int?): RemoteEffect {
        return modify(this) {
            addHostileEffect(source, index)
        }
    }


    override fun Fit.removeHostileEffect(effect: RemoteEffect) {
        modify(this) {
            removeHostileEffect(effect)
        }
    }


    override fun Fit.addFriendlyEffect(source: Fit, index: Int?): RemoteEffect {
        return modify(this) {
            addFriendlyEffect(source, index)
        }
    }


    override fun Fit.removeFriendlyEffect(effect: RemoteEffect) {
        modify(this) {
            removeFriendlyEffect(effect)
        }
    }


    override fun Fit.addModuleEffect(moduleType: ModuleType, index: Int?): Module? {
        return modify(this) {
            addModuleEffect(moduleType, index)
        }
    }


    override fun Fit.removeModuleEffect(module: Module) {
        modify(this) {
            removeModuleEffect(module)
        }
    }


    override fun Fit.addDroneEffect(droneType: DroneType, size: Int, index: Int?): DroneGroup {
        return modify(this) {
            verifyDroneGroupSize(size)
            addDroneEffect(droneType, size = size, index = index)
        }
    }


    override fun Fit.removeDroneEffect(droneGroup: DroneGroup) {
        modify(this) {
            removeDroneEffect(droneGroup)
        }
    }


    override fun <T : Any> Ship.setPinnedPropertyValue(attribute: Attribute<T>, value: T?) {
        return modify(fit) {
            setPinnedPropertyValue(attribute, value)
        }
    }


    override fun <T : Any> Module.setPinnedPropertyValue(attribute: Attribute<T>, value: T?) {
        return modify(fit) {
            setPinnedPropertyValue(attribute, value)
        }
    }


    override fun FitItemWithEnabledState.setEnabled(isEnabled: Boolean) {
        modify(fit) {
            setEnabled(isEnabled)
        }
    }


    override fun EveItemType.setMutatedAttributeValue(attribute: Attribute<*>, value: Double) {
        with(engineModificationScope) {
            setMutatedAttributeValue(attribute, value)
        }

        for (item in fittingEngine.mutatedItemsOf(this)) {
            if (item is FitItem)
                modifiedFits.add(item.fit)
        }
    }


    override fun recomputePropertyValues() {
        with(engineModificationScope) {
            recomputePropertyValues()
        }
    }


}


/**
 * A scope where temporary fittings can be made.
 */
class TempFittingScope(


    /**
     * The fitting engine.
     */
    private val fittingEngine: FittingEngine,


    /**
     * The ship to fit into [tempFit].
     */
    shipType: ShipType


) {


    /**
     * The fits created in this scope.
     */
    private val fits = mutableSetOf<Fit>()


    /**
     * The "main" fit on which temporary fittings will be done.
     */
    val tempFit: Fit


    init {
        tempFit = runBlocking {
            modify {
                newFit(shipType)
            }
        }
    }


    /**
     * A mutex which can be locked if several modifications need to be done atomically (such as fitting and then
     * unfitting a module).
     *
     * Note that this mutex is local to *this* [TempFittingScope], and that it is *not* locked automatically by
     * [modify]. It's completely optional.
     */
    val mutex = Mutex()


    /**
     * Makes modifications to the engine.
     */
    suspend fun <T> modify(block: FittingEngine.ModificationScope.() -> T): T {
        return fittingEngine.modify(silent = true) {
            ModificationScope(this).block()
        }
    }


    /**
     * Removes the fits created in this scope.
     */
    suspend fun dispose() {
        fittingEngine.modify(silent = true) {
            for (fit in fits)
                fit.remove()
        }
    }


    /**
     * Makes [target] a copy of [source] by fitting it with the same modules, drones etc.
     */
    fun FittingEngine.ModificationScope.copyFit(source: Fit, target: Fit) {
        source.tacticalMode?.let {
            target.setTacticalMode(it.type)
        }
        source.subsystemByKind?.values?.let { subsystems ->
            for (subsystem in subsystems) {
                if (subsystem != null) {
                    target.setSubsystem(subsystem.type)
                }
            }
        }
        target.modules.all.toList().forEach { target.removeModule(it) }
        target.drones.all.toList().forEach { target.removeDroneGroup(it) }
        target.implants.fitted.toList().forEach { target.removeImplant(it) }
        target.boosters.fitted.toList().forEach { target.removeBooster(it) }
        target.commandEffects.toList().forEach { target.removeCommandEffect(it) }
        target.friendlyEffects.toList().forEach {
            if (it.isByAuxiliaryFit) {
                for (module in it.affectingModules)
                    target.removeModuleEffect(module)
                for (drone in it.affectingDrones)
                    target.removeDroneEffect(drone)
            } else
                target.removeFriendlyEffect(it)
        }
        target.hostileEffects.toList().forEach {
            if (it.isByAuxiliaryFit) {
                for (module in it.affectingModules)
                    target.removeModuleEffect(module)
                for (drone in it.affectingDrones)
                    target.removeDroneEffect(drone)
            } else
                target.removeHostileEffect(it)
        }
        target.environments.toList().forEach { target.removeEnvironment(it) }
        target.cargohold.contents.toList().forEach { target.removeCargoItem(it) }

        val countByRack = ModuleSlotType.entries.associateWithTo(mutableMapOf()) { 0 }
        source.modules.all.forEach {
            val count = countByRack[it.type.slotType]!!
            val module = target.fitModule(it.type, count)
            it.loadedCharge?.let { charge ->
                module.setCharge(charge.type)
            }
            module.setState(it.state)
            it.spoolupCycles?.let { spoolupCyclesAttr ->
                module.setSpoolupCycles(spoolupCyclesAttr.value)
            }
            it.adaptationCycles?.let { adaptationCyclesAttr ->
                module.setAdaptationCycles(adaptationCyclesAttr.value)
            }
            countByRack[it.type.slotType] = count+1
        }
        source.drones.all.forEach {
            val droneGroup = target.addDroneGroup(it.type, it.size)
            droneGroup.setActive(it.active)
        }
        source.implants.fitted.forEach {
            val implant = target.fitImplant(it.type)
            implant.setEnabled(it.enabled)
        }
        source.boosters.fitted.forEach {
            val booster = target.fitBooster(it.type)
            booster.setEnabled(it.enabled)
            for (sideEffect in it.type.sideEffects) {
                booster.setSideEffectActive(sideEffect, it.isSideEffectActive(sideEffect))
            }
        }
        source.commandEffects.forEach {
            val effect = target.addCommandEffect(it.source)
            effect.setEnabled(it.enabled)
        }
        source.friendlyEffects.forEach {
            if (it.isByAuxiliaryFit) {
                for (module in it.affectingModules) {
                    val newModule = target.addModuleEffect(module.type)!!
                    module.loadedCharge?.let { charge ->
                        newModule.setCharge(charge.type)
                    }
                    newModule.setState(module.state)
                }
                for (drone in it.affectingDrones) {
                    val newDrone = target.addDroneEffect(drone.type, drone.size)
                    newDrone.setActive(drone.active)
                }
            } else {
                val effect = target.addFriendlyEffect(it.source)
                effect.setEnabled(it.enabled)
            }
        }
        source.hostileEffects.forEach {
            if (it.isByAuxiliaryFit) {
                for (module in it.affectingModules) {
                    val newModule = target.addModuleEffect(module.type)!!
                    module.loadedCharge?.let { charge ->
                        newModule.setCharge(charge.type)
                    }
                    newModule.setState(module.state)
                }
                for (drone in it.affectingDrones) {
                    val newDrone = target.addDroneEffect(drone.type, drone.size)
                    newDrone.setActive(drone.active)
                }
            } else {
                val effect = target.addHostileEffect(it.source)
                effect.setEnabled(it.enabled)
            }
        }
        source.environments.forEach {
            val env = target.addEnvironment(it.type)
            env.setEnabled(it.enabled)
        }
        source.cargohold.contents.forEach {
            target.addCargoItem(it.type, it.amount)
        }
    }


    /**
     * Throws an exception if the given fit was not created in this scope.
     */
    private fun checkFit(fit: Fit) {
        if (fits.none { (fit == it) || (fit == it.auxiliaryFit) })
            throw IllegalArgumentException("Attempting to modify an unrelated fit in a temporary fitting scope")
    }


    /**
     * The [FittingEngine.ModificationScope] passed to the action block in [modify].
     */
    private inner class ModificationScope(
        private val engineModificationScope: FittingEngine.ModificationScope
    ) : FittingEngine.ModificationScope {


        override fun newSkillSet(levelOfSkill: (SkillType) -> Int): SkillSet {
            error("Custom skill sets are not supported in temporary modifications")
        }


        override fun SkillSet.setLevels(skillAndLevelList: Collection<Pair<SkillType, Int>>) {
            error("Custom skill sets are not supported in temporary modifications")
        }


        override fun SkillSet.remove() {
            error("Custom skill sets are not supported in temporary modifications")
        }


        override fun newFit(shipType: ShipType, skillSet: SkillSet?): Fit {
            return with(engineModificationScope) {
                newFit(shipType)
            }.also {
                fits.add(it)
            }
        }


        override fun Fit.remove(): Collection<Fit> {
            val fit = this
            checkFit(fit)

            return with(engineModificationScope) {
                fit.remove()
            }.also {
                this@TempFittingScope.fits.remove(fit)
            }
        }


        override fun Fit.setSkillSet(skillSet: SkillSet) {
            checkFit(this)

            return with(engineModificationScope) {
                setSkillSet(skillSet)
            }
        }


        override fun Fit.setTacticalMode(tacticalModeType: TacticalModeType): TacticalMode {
            checkFit(this)

            return with(engineModificationScope) {
                setTacticalMode(tacticalModeType)
            }
        }


        override fun Fit.setSubsystem(subsystemType: SubsystemType): Subsystem {
            checkFit(this)

            return with(engineModificationScope) {
                setSubsystem(subsystemType)
            }
        }


        override fun Fit.fitModule(moduleType: ModuleType, slotIndex: Int): Module {
            checkFit(this)

            return with(engineModificationScope) {
                fitModule(moduleType, slotIndex)
            }
        }


        override fun Fit.removeModule(slotType: ModuleSlotType, slotIndex: Int) {
            checkFit(this)

            return with(engineModificationScope) {
                removeModule(slotType, slotIndex)
            }
        }


        override fun Fit.removeModule(module: Module) {
            checkFit(this)

            return with(engineModificationScope) {
                removeModule(module)
            }
        }


        override fun Fit.reorderModules(slotType: ModuleSlotType, newOrder: (Int) -> Int) {
            checkFit(this)

            return with(engineModificationScope) {
                reorderModules(slotType, newOrder)
            }
        }


        override fun Module.setState(newState: Module.State) {
            checkFit(this.fit)

            return with(engineModificationScope) {
                setState(newState)
            }
        }


        override fun Module.setCharge(charge: ChargeType): Charge {
            checkFit(this.fit)

            return with(engineModificationScope) {
                setCharge(charge)
            }
        }


        override fun Module.removeCharge() {
            checkFit(this.fit)

            return with(engineModificationScope) {
                removeCharge()
            }
        }


        override fun Fit.addDroneGroup(droneType: DroneType, size: Int, index: Int?): DroneGroup {
            checkFit(this)

            return with(engineModificationScope) {
                addDroneGroup(droneType, size = size, index = index)
            }
        }


        override fun Fit.removeDroneGroup(droneGroup: DroneGroup) {
            checkFit(this)

            return with(engineModificationScope) {
                removeDroneGroup(droneGroup)
            }
        }


        override fun DroneGroup.setActive(isActive: Boolean) {
            checkFit(this.fit)

            return with(engineModificationScope) {
                setActive(isActive)
            }
        }


        override fun DroneGroup.setSize(size: Int) {
            checkFit(this.fit)

            return with(engineModificationScope) {
                setSize(size)
            }
        }


        override fun Fit.addCargoItem(itemType: EveItemType, amount: Int, index: Int?): CargoItem {
            checkFit(this)

            return with(engineModificationScope) {
                addCargoItem(itemType, amount = amount, index = index)
            }
        }


        override fun Fit.removeCargoItem(cargoItem: CargoItem) {
            checkFit(this)

            return with(engineModificationScope) {
                removeCargoItem(cargoItem)
            }
        }


        override fun CargoItem.setAmount(amount: Int) {
            checkFit(this.fit)

            return with(engineModificationScope) {
                setAmount(amount)
            }
        }


        override fun CargoItem.moveTo(index: Int) {
            checkFit(this.fit)

            return with(engineModificationScope) {
                moveTo(index)
            }
        }

        override fun Fit.fitImplant(implantType: ImplantType): Implant {
            checkFit(this)

            return with(engineModificationScope) {
                fitImplant(implantType)
            }
        }


        override fun Fit.removeImplant(slotIndex: Int) {
            checkFit(this)

            return with(engineModificationScope) {
                removeImplant(slotIndex)
            }
        }


        override fun Fit.fitBooster(boosterType: BoosterType): Booster {
            checkFit(this)

            return with(engineModificationScope) {
                fitBooster(boosterType)
            }
        }


        override fun Booster.setSideEffectActive(sideEffect: BoosterType.SideEffect, active: Boolean) {
            checkFit(fit)

            return with(engineModificationScope) {
                setSideEffectActive(sideEffect, active)
            }
        }

        override fun Fit.removeBooster(slotIndex: Int) {
            checkFit(this)

            return with(engineModificationScope) {
                removeBooster(slotIndex)
            }
        }


        override fun Fit.addEnvironment(environmentType: EnvironmentType, index: Int?): Environment {
            checkFit(this)

            return with(engineModificationScope) {
                addEnvironment(environmentType, index)
            }
        }


        override fun Fit.removeEnvironment(environment: Environment) {
            checkFit(this)

            return with(engineModificationScope) {
                removeEnvironment(environment)
            }
        }


        override fun Fit.addCommandEffect(source: Fit, index: Int?): RemoteEffect {
            checkFit(this)

            return with(engineModificationScope) {
                addCommandEffect(source, index)
            }
        }


        override fun Fit.removeCommandEffect(effect: RemoteEffect) {
            checkFit(this)

            with(engineModificationScope) {
                removeCommandEffect(effect)
            }
        }


        override fun Fit.addHostileEffect(source: Fit, index: Int?): RemoteEffect {
            checkFit(this)

            return with(engineModificationScope) {
                addHostileEffect(source, index)
            }
        }


        override fun Fit.removeHostileEffect(effect: RemoteEffect) {
            checkFit(this)

            with(engineModificationScope) {
                removeHostileEffect(effect)
            }
        }


        override fun Fit.addFriendlyEffect(source: Fit, index: Int?): RemoteEffect {
            checkFit(this)

            return with(engineModificationScope) {
                addFriendlyEffect(source, index)
            }
        }


        override fun Fit.removeFriendlyEffect(effect: RemoteEffect) {
            checkFit(this)

            with(engineModificationScope) {
                removeFriendlyEffect(effect)
            }
        }


        override fun Fit.addModuleEffect(moduleType: ModuleType, index: Int?): Module? {
            checkFit(this)

            return with(engineModificationScope) {
                addModuleEffect(moduleType, index)
            }
        }


        override fun Fit.removeModuleEffect(module: Module) {
            checkFit(this)

            with(engineModificationScope) {
                removeModuleEffect(module)
            }
        }


        override fun Fit.addDroneEffect(droneType: DroneType, size: Int, index: Int?): DroneGroup {
            checkFit(this)

            return with(engineModificationScope) {
                addDroneEffect(droneType, size = size, index = index)
            }
        }


        override fun Fit.removeDroneEffect(droneGroup: DroneGroup) {
            checkFit(this)

            with(engineModificationScope) {
                removeDroneEffect(droneGroup)
            }
        }


        override fun <T : Any> Ship.setPinnedPropertyValue(attribute: Attribute<T>, value: T?) {
            checkFit(this.fit)

            return with(engineModificationScope) {
                setPinnedPropertyValue(attribute, value)
            }
        }


        override fun <T : Any> Module.setPinnedPropertyValue(attribute: Attribute<T>, value: T?) {
            checkFit(this.fit)

            return with(engineModificationScope) {
                setPinnedPropertyValue(attribute, value)
            }
        }


        override fun FitItemWithEnabledState.setEnabled(isEnabled: Boolean) {
            checkFit(this.fit)

            return with(engineModificationScope) {
                setEnabled(isEnabled)
            }
        }


        override fun EveItemType.setMutatedAttributeValue(attribute: Attribute<*>, value: Double) {
            with(engineModificationScope) {
                setMutatedAttributeValue(attribute, value)
            }
        }


        override fun recomputePropertyValues() {
            with(engineModificationScope) {
                recomputePropertyValues()
            }
        }


    }


}


/**
 * Returns whether the given fit is for a flagship.
 */
var Fit.isFlagship: Boolean
    get() = extras["flagship"] == true
    private set(value) {
        extras["flagship"] = value
    }
