package theorycrafter

import androidx.compose.runtime.*
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.WindowPosition
import androidx.compose.ui.window.WindowState
import eve.data.SensorType
import eve.data.SensorType.*
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.serialization.*
import kotlinx.serialization.builtins.IntArraySerializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.Json
import theorycrafter.TheorycrafterContext.settings
import theorycrafter.fits.FitHandle
import theorycrafter.formats.EftExportOptions
import theorycrafter.tournaments.TournamentDescriptor
import theorycrafter.tournaments.TournamentDescriptorById
import theorycrafter.ui.graphs.AmmoSelection
import theorycrafter.ui.graphs.AmmoSelection.CurrentlyLoaded
import theorycrafter.utils.onSet
import java.io.File
import java.io.IOException
import java.util.concurrent.locks.ReentrantReadWriteLock
import kotlin.concurrent.read
import kotlin.concurrent.write
import kotlin.math.roundToInt
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KMutableProperty0
import kotlin.reflect.KProperty
import kotlin.time.Duration.Companion.minutes
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid


/**
 * The application settings.
 *
 * This is a live object backed by a file, which it updates as settings change.
 */
class TheorycrafterSettings private constructor(


    /**
     * The settings themselves.
     */
    private val storedSettings: StoredSettings,


    /**
     * The file where the settings are stored; a `null` value indicates it should not be written to disk.
     */
    private val file: File?


) {


    /**
     * Whether this instance has been closed.
     */
    @Volatile
    private var closed = false


    /**
     * A channel to trigger the saving of settings.
     *
     * Created lazily to speed up startup (the class-loading takes some time).
     */
    private val writeTriggerChannel by lazy { Channel<Unit>(Channel.CONFLATED) }


    /**
     * A lock for accessing the settings.
     */
    private val lock = ReentrantReadWriteLock()


    init {
        @OptIn(ExperimentalCoroutinesApi::class)
        val coroutineScope = CoroutineScope(Dispatchers.IO.limitedParallelism(1))
        coroutineScope.launch {
            delay(1000)  // Avoid immediately creating the channel
            for (trigger in writeTriggerChannel) {
                delay(100)  // Buffer some writes
                store()
            }
        }
    }


    /**
     * The settings of the main window.
     */
    val mainWindow = FitWindowSettings(storedSettings.mainWindow)


    /**
     * The settings of secondary fit windows.
     */
    val secondaryFitWindows = SecondaryFitWindowsSettings(storedSettings.secondaryFitWindows)


    /**
     * The settings of the market tree window.
     */
    val marketTreeWindow = WindowSettingsWithOpenState(storedSettings.marketTreeWindow)


    /**
     * The settings of the graphs.
     */
    val graphs = GraphSettings(storedSettings.graphs)


    /**
     * The settings of the tournament window.
     */
    val tournamentWindow = TournamentWindowSettings(storedSettings.tournamentWindow)


    /**
     * The settings of the doctrines window.
     */
    val doctrinesWindow = WindowSettingsWithOpenState(storedSettings.doctrinesWindow)


    /**
     * The id of the default skill set.
     */
    var defaultSkillSetId: Int by storedSetting(storedSettings::defaultSkillSetId)


    /**
     * The settings for the "Pack cargo for battle" dialog.
     */
    val packForBattleDialogSettings = PackForBattleDialogSettings(storedSettings.packForBattleDialogSettings)


    /**
     * The id of the active tournament; `null` if none.
     */
    var activeTournamentId: String? by mutableStateOfStoredSetting(storedSettings::activeTournamentId)


    /**
     * Whether to suggest adding the active tournament's [TournamentDescriptor.suggestedFitTag] as a tag to newly
     * created fits.
     */
    var suggestTournamentFitTagForNewFits =
        mutableStateOfStoredSetting(storedSettings::suggestTournamentFitTagForNewFits)


    /**
     * Returns the tags to suggest (or directly apply) to a new fit, based on the active tournament and settings.
     */
    fun newFitTags(): List<String> {
        val tournamentTag = TheorycrafterContext.tournaments.activeTournamentDescriptor?.suggestedFitTag
        return if (settings.suggestTournamentFitTagForNewFits.value && (tournamentTag != null))
            listOf(tournamentTag)
        else
            emptyList()
    }


    /**
     * Whether the user has checked to not be asked about clearing a non-current tournament from being active.
     */
    val doNotAskAboutNonCurrentButActiveTournament =
        mutableStateOfStoredSetting(storedSettings::doNotAskAboutNonCurrentButActiveTournament)


    /**
     * The current color theme style.
     */
    var colorThemeStyle: ColorThemeStyle by mutableStateOfStoredSetting(storedSettings::colorThemeStyle)


    /**
     * The UI scale factor, as a percentage.
     */
    var uiScaleFactorPct: Int by mutableStateOfStoredSetting(storedSettings::uiScalePct)


    /**
     * The values selected initially in the fit export options dialog.
     */
    var fitExportOptionsDialogValues: FitExportOptionSettings
        by mutableStateOfStoredSetting(storedSettings::fitExportOptionsDialogValues)


    /**
     * The fit export options used when
     */
    var regularFitExportOptions: FitExportOptionSettings
        by mutableStateOfStoredSetting(storedSettings::regularFitExportOptions)


    /**
     * The composition export options saved in settings.
     */
    var compositionExportOptions: CompositionExportOptionSettings
            by mutableStateOfStoredSetting(storedSettings::compositionExportOptions)


    /**
     * The composition export templates saved in settings.
     */
    var compositionExportTemplates: CompositionExportTemplatesSettings
            by mutableStateOfStoredSetting(storedSettings::compositionExportTemplates)


    /**
     * The pinned fit searches.
     */
    var pinnedFitSearches: List<String>
        by mutableStateOfStoredSetting(storedSettings::pinnedFitSearches)


    /**
     * The simulated annealing configuration for the fit optimizer.
     */
    val fitOptimizer: FitOptimizerSettings = FitOptimizerSettings(storedSettings.fitOptimizer)


    /**
     * The settings for EVE item prices.
     */
    val prices: EveItemPricesSettings = EveItemPricesSettings(storedSettings.prices)


    /**
     * The settings related to new releases.
     */
    val theorycrafterReleases: TheorycrafterReleasesSettings =
        TheorycrafterReleasesSettings(storedSettings.theorycrafterReleases)


    /**
     * The analytics settings.
     */
    val analytics: AnalyticsSettings = AnalyticsSettings(storedSettings.analytics)


    /**
     * Writes the settings to disk.
     */
    private fun store() {
        val data = lock.read {
            Encoder.encodeToString(storedSettings)
        }

        try {
            file?.writeText(data)
        } catch (e: IOException) {
            System.err.println("Error writing settings")
            e.printStackTrace()
        }
    }


    /**
     * Throws an [IllegalStateException] if this instance is closed.
     */
    private fun checkNotClosed() {
        check(!closed) { "This Settings instance is closed" }
    }


    /**
     * Closes this instance.
     */
    @Suppress("unused")
    fun close() {
        closed = true
        writeTriggerChannel.close()
    }


    /**
     * Returns a property delegate that controls access to the given stored setting.
     * Reads are protected by a read lock.
     * Writes are protected by a corresponding write lock, and the settings are saved when written.
     */
    private fun <T> storedSetting(target: KMutableProperty0<T>) = object: ReadWriteProperty<Any?, T> {

        override fun getValue(thisRef: Any?, property: KProperty<*>): T {
            checkNotClosed()
            return lock.read {
                target.get()
            }
        }

        override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
            checkNotClosed()
            return lock.write {
                target.set(value)
            }.also {
                writeTriggerChannel.trySend(Unit)
            }
        }

    }


    /**
     * Returns a compose state that also updates the given setting when written to.
     *
     * This both controls access to the stored setting and presents it as Compose state.
     */
    fun <T> mutableStateOfStoredSetting(target: KMutableProperty0<T>): MutableState<T> = object: MutableState<T> {


        /**
         * A property for getting the setting.
         */
        private var setting by storedSetting(target)


        /**
         * The backing [MutableState].
         *
         * Created lazily to speed up settings creation on startup.
         */
        private val mutableState: MutableState<T> by lazy { mutableStateOf(setting) }


        override var value: T
            get() = mutableState.value
            set(value) {
                mutableState.value = value
                setting = value
            }

        override fun component1(): T = mutableState.component1()

        override fun component2(): (T) -> Unit = {
            value = it
        }

    }


    companion object {


        /**
         * The JSON encoder with which we write the settings.
         */
        @OptIn(ExperimentalSerializationApi::class)
        private val Encoder = Json {
            encodeDefaults = true
            ignoreUnknownKeys = true
            isLenient = true
            prettyPrint = true
            prettyPrintIndent = "\t"
            allowTrailingComma = true
        }


        /**
         * Loads the settings from disk.
         */
        fun load(): TheorycrafterSettings {
            val file = File(USER_FILES_DIR, "settings.json")
            if (!file.exists())
                return TheorycrafterSettings(StoredSettings(), file)

            return try {
                val data = file.readText()
                val storedSettings = Encoder.decodeFromString<StoredSettings>(data)
                TheorycrafterSettings(storedSettings, file)
            } catch (_: IOException) {
                TheorycrafterSettings(StoredSettings(), file)
            }
        }


        /**
         * Returns an empty [TheorycrafterSettings] for use in tests. It doesn't actually write to disk.
         */
        fun forTest(): TheorycrafterSettings {
            return TheorycrafterSettings(
                storedSettings = StoredSettings(
                    defaultSkillSetId = BuiltInSkillSets.AllLevel5.skillSetId,
                    activeTournamentId = null,
                    fitExportOptionsDialogValues = FitExportOptionSettings(EftExportOptions.IncludeAll),
                    regularFitExportOptions = FitExportOptionSettings(EftExportOptions.IncludeAll)
                ),
                file = null
            )
        }


    }


    /**
     * Groups the settings of a window.
     */
    @Stable
    open inner class WindowSettings(storedWindowSettings: StoredWindowSettings) {


        /**
         * The bounds of the window.
         */
        private var bounds: StoredWindowBounds? by storedSetting(storedWindowSettings::bounds)


        /**
         * The position of the window.
         */
        val position: WindowPosition?
            get() = bounds?.position


        /**
         * The size of the window.
         */
        val size: DpSize?
            get() = bounds?.size


        /**
         * Sets the bounds of the window specified by the given [WindowState].
         */
        fun setBounds(windowState: WindowState) {
            if (!windowState.position.isSpecified)
                return

            bounds = with(Density(1f)) {
                StoredWindowBounds(
                    x = windowState.position.x.roundToPx(),
                    y = windowState.position.y.roundToPx(),
                    width = windowState.size.width.roundToPx(),
                    height = windowState.size.height.roundToPx()
                )
            }
        }


    }


    /**
     * Groups the settings of a fit (main or secondary) window.
     */
    inner class FitWindowSettings(storedWindowSettings: StoredFitWindowSettings): WindowSettings(storedWindowSettings) {


        /**
         * The id of the fit displayed in the window.
         */
        private var fitId: Int? by storedSetting(storedWindowSettings::fitId)


        /**
         * The [theorycrafter.fits.FitHandle] of the fit displayed in the window.
         *
         * A null value means:
         * - For the main window: no fit is displayed
         * - For secondary windows: that window is not displayed
         */
        var fitHandle: FitHandle?
            get() = fitId?.let { TheorycrafterContext.fits.handleById(it) }
            set(value) {
                fitId = value?.fitId
            }


        /**
         * A non-null [FitHandle] of the fit displayed in the window.
         */
        fun requireFitHandle() = fitHandle ?: error("Fit handle must be non-null")


    }


    /**
     * The settings of secondary fit windows.
     */
    inner class SecondaryFitWindowsSettings(
        private val storedSettings: MutableList<StoredFitWindowSettings>
    ): AbstractList<FitWindowSettings>() {


        /**
         * The [FitWindowSettings] wrapping each stored settings.
         */
        private val windowsSettings = storedSettings.mapTo(mutableListOf(), ::FitWindowSettings)


        override val size: Int
            get() = lock.read { windowsSettings.size }


        override operator fun get(index: Int) = lock.read { windowsSettings[index] }


        /**
         * Called when a new secondary fit window is opened. Returns the settings it should use.
         */
        fun onWindowAdded(fitHandle: FitHandle): FitWindowSettings {
            return (
                // Existing settings
                lock.read {
                    windowsSettings.find { it.fitHandle == null }?.also {
                        it.fitHandle = fitHandle
                    }
                }
                ?:
                // New settings
                lock.write {
                    val storedWindowSettings = StoredFitWindowSettings(fitId = fitHandle.fitId).also {
                        storedSettings.add(it)
                    }
                    FitWindowSettings(storedWindowSettings).also {
                        windowsSettings.add(it)
                    }
                }.also {
                    writeTriggerChannel.trySend(Unit)
                }
            )
        }


        /**
         * Called when a secondary fit window is closed.
         */
        fun onWindowRemoved(windowSettings: FitWindowSettings) {
            // It's already protected by a write lock
            windowSettings.fitHandle = null
        }


    }


    /**
     * The settings of a window that has no state other than being open and its bounds.
     */
    @Stable
    inner class WindowSettingsWithOpenState(
        storedWindowSettings: StoredWindowSettingsWithOpenState
    ): WindowSettings(storedWindowSettings) {

        /**
         * Whether the window is open.
         */
        var open: Boolean by storedSetting(storedWindowSettings::open)

    }


    /**
     * Groups the settings of the tournament window.
     */
    @Stable
    inner class TournamentWindowSettings(
        storedWindowSettings: StoredTournamentWindowSettings
    ): WindowSettings(storedWindowSettings) {


        /**
         * The id of the tournament displayed in the window.
         */
        private var tournamentId: String? by storedSetting(storedWindowSettings::tournamentId)


        /**
         * The tournament displayed in the window.
         *
         * A `null` value means the window is now visible.
         */
        var tournamentDescriptor: TournamentDescriptor?
            get() = tournamentId?.let { TournamentDescriptorById[it] }
            set(value) {
                tournamentId = value?.id
            }


    }


    @Stable
    inner class PackForBattleDialogSettings(
        storedSettings: StoredPackForBattleDialogSettings
    ) {


        /**
         * The duration of battle to pack for.
         */
        var battleDuration by mutableStateOfStoredSetting(storedSettings::battleDuration)


        /**
         * Whether the tech 1 ammo category is selected.
         */
        val selectTech1Ammo = mutableStateOfStoredSetting(storedSettings::selectTech1Ammo)


        /**
         * Whether the faction ammo category is selected.
         */
        val selectFactionAmmo = mutableStateOfStoredSetting(storedSettings::selectFactionAmmo)


        /**
         * Whether the pirate ammo category is selected.
         */
        val selectPirateAmmo = mutableStateOfStoredSetting(storedSettings::selectPirateAmmo)


        /**
         * Whether the Tech 2 ammo category is selected.
         */
        val selectTech2Ammo = mutableStateOfStoredSetting(storedSettings::selectTech2Ammo)


        /**
         * Whether the scripts category is selected.
         */
        val selectScripts = mutableStateOfStoredSetting(storedSettings::selectScripts)


        /**
         * Whether the command burst category is selected.
         */
        val selectCommandBurstCharges = mutableStateOfStoredSetting(storedSettings::selectCommandBurstCharges)


        /**
         * Whether the cap boosters category is selected.
         */
        val selectCapBoosters = mutableStateOfStoredSetting(storedSettings::selectCapBoosters)


        /**
         * Whether the interdiction probes category is selected.
         */
        val selectInterdictionProbes = mutableStateOfStoredSetting(storedSettings::selectInterdictionProbes)


        /**
         * Whether the boosters category is selected.
         */
        val selectBoosters = mutableStateOfStoredSetting(storedSettings::selectBoosters)


        /**
         * Whether the implants category is selected.
         */
        val selectImplants = mutableStateOfStoredSetting(storedSettings::selectImplants)


    }


    /**
     * Groups the settings of graphs.
     */
    @Stable
    inner class GraphSettings(
        private val graphSettings: StoredGraphSettings
    ): WindowSettings(graphSettings) {


        /**
         * The settings of the damage graph pane.
         */
        val damage = DamageGraphSettings()


        /**
         * Whether to include repair bot repairs in the remote repairs graph.
         */
        val includeRepairBots: MutableState<Boolean> =
            mutableStateOfStoredSetting(graphSettings.remoteRepairs::includeDrones)


        /**
         * The settings of the energy neutralization graph pane.
         */
        val neutralization = NeutralizationGraphSettings()


        /**
         * The settings of the ECM effectiveness graph pane.
         */
        val ecm = EcmGraphSettings()


        /**
         * Whether to include web drones in the webification graph.
         */
        val includeWebDrones: MutableState<Boolean> =
            mutableStateOfStoredSetting(graphSettings.webification::includeDrones)


        /**
         * Whether to include sensor dampening drones in the targeting range dampening graph.
         */
        val includeTargetingRangeDampeningDrones: MutableState<Boolean> =
            mutableStateOfStoredSetting(graphSettings.targetRangeDampening::includeDrones)


        /**
         * Whether to include sensor dampening drones in the scan resolution dampening graph.
         */
        val includeScanResolutionDampeningDrones: MutableState<Boolean> =
            mutableStateOfStoredSetting(graphSettings.scanResDampening::includeDrones)


        /**
         * Whether to include tracking disruption drones in the turret range disruption graph.
         */
        val includeTurretRangeDisruptingDrones: MutableState<Boolean> =
            mutableStateOfStoredSetting(graphSettings.turretRangeDisruption::includeDrones)


        /**
         * Whether to include target painting drones in the target painting graph.
         */
        val includeTargetPaintingDrones: MutableState<Boolean> =
            mutableStateOfStoredSetting(graphSettings.targetPainting::includeDrones)


        /**
         * The scan resolution of the virtual fit in the lock time graph.
         */
        val virtualFitScanResolution: MutableState<Double> =
            mutableStateOfStoredSetting(graphSettings.lockTime::virtualFitScanResolution)


        /**
         * The generic effect graph settings.
         */
        val genericEffect = GenericEffectGraphSettings()


        /**
        * Groups the damage graph settings.
         */
        @Stable
        inner class DamageGraphSettings {

            /**
             * Whether to show volley damage; otherwise show damage-per-second.
             */
            val showVolley: MutableState<Boolean> =
                mutableStateOfStoredSetting(graphSettings.damage::showVolley)

            /**
             * Whether to include drone damage in the graph.
             */
            val includeDrones: MutableState<Boolean> =
                mutableStateOfStoredSetting(graphSettings.damage::includeDrones)

            /**
             * Whether to include wrecking shots in the damage calculations.
             */
            val includeWreckingShots: MutableState<Boolean> =
                mutableStateOfStoredSetting(graphSettings.damage::includeWreckingShots)

            /**
             * The ammo selection to load to determine damage.
             */
            val ammoSelection: MutableState<AmmoSelection> =
                mutableStateOfStoredSetting(graphSettings.damage::ammoSelection)

            /**
             * The signature radius of the virtual target.
             */
            val virtuaTargetSignatureRadius: MutableState<Double> =
                mutableStateOfStoredSetting(graphSettings.damage::vtSigRadius)

            /**
             * The max. velocity of the virtual target.
             */
            val virtualTargetMaxVelocity: MutableState<Double> =
                mutableStateOfStoredSetting(graphSettings.damage::vtMaxVelocity)

        }



        /**
         * Groups energy neutralization graph settings.
         */
        inner class NeutralizationGraphSettings {

            /**
             * Whether to include energy vampire drones in the graph.
             */
            val includeDrones: MutableState<Boolean> =
                mutableStateOfStoredSetting(graphSettings.neutralization::includeDrones)

            /**
             * Whether to include nosferatus in the graph.
             */
            var includeNosferatus: MutableState<Boolean> =
                mutableStateOfStoredSetting(graphSettings.neutralization::includeNosferatus)

        }


        /**
         * Groups ECM effectiveness graph settings.
         */
        inner class EcmGraphSettings {

            /**
             * Whether to include ECM drones in the graph.
             */
            val includeDrones: MutableState<Boolean> =
                mutableStateOfStoredSetting(graphSettings.ecm::includeDrones)

            /**
             * Whether to show the chance to jam in the graph; otherwise show the percentage of time the target will
             * spend jammed.
             */
            val showChanceToJam: MutableState<Boolean> =
                mutableStateOfStoredSetting(graphSettings.ecm::showChanceToJam)

            /**
             * The sensor type of the virtual target.
             */
            val virtualTargetSensorType: MutableState<SensorType> =
                mutableStateOfStoredSetting(graphSettings.ecm::vtSensorType)

            /**
             * The sensor strength of the virtual target.
             */
            val virtualTargetSensorStrength: MutableState<Double> =
                mutableStateOfStoredSetting(graphSettings.ecm::vtSensorStrength)

        }


        /**
         * Groups generic effect graph settings.
         */
        inner class GenericEffectGraphSettings {

            /**
             * The strength of the effect.
             */
            val effectStrength: MutableState<Double> =
                mutableStateOfStoredSetting(graphSettings.genericEffect::effect)

            /**
             * The optimal range of the effect.
             */
            val optimalRange: MutableState<Double> =
                mutableStateOfStoredSetting(graphSettings.genericEffect::optimal)

            /**
             * The falloff range of the effect.
             */
            val falloffRange: MutableState<Double> =
                mutableStateOfStoredSetting(graphSettings.genericEffect::falloff)

        }


    }


    /**
     * Groups the settings related to new releases.
     */
    inner class TheorycrafterReleasesSettings(
        private val storedSettings: StoredTheorycrafterReleasesSettings
    ) {


        /**
         * Whether to notify the user when a new version is released.
         */
        var notifyOnNewRelease: Boolean by mutableStateOfStoredSetting(storedSettings::notifyOnNewRelease)


        /**
         * The user's chosen upgrade lane.
         */
        var upgradeLane: ReleaseType by mutableStateOfStoredSetting(storedSettings::upgradeLane)


        /**
         * The mapping from [ReleaseType.id] to the version code of the latest release of that type.
         */
        private var latestReleaseVersionCode by storedSetting(storedSettings::latestReleaseVersionCode)


        /**
         * Returns the release type to which we should upgrade, based on the user's [upgradeLane], and its version code.
         */
        fun latestUpgradeableReleaseTypeAndVersionCode(): Pair<ReleaseType, Int>? {
            return ReleaseType.entries
                .takeWhile { it <= upgradeLane }
                .mapNotNull { releaseType ->
                    val versionCode = latestReleaseVersionCode[releaseType.id] ?: return@mapNotNull null
                    releaseType to versionCode
                }
                .maxByOrNull { (_, versionCode) -> versionCode }
        }


        /**
         * Returns the release type to which we should upgrade, based on the user's [upgradeLane].
         */
        fun latestUpgradeableReleaseType(): ReleaseType? =
            latestUpgradeableReleaseTypeAndVersionCode()?.first


        /**
         * Updates the known latest releases from the information in the given [TheorycrafterReleases].
         */
        fun updateReleaseVersionCodes(releases: TheorycrafterReleases) {
            latestReleaseVersionCode = ReleaseType.entries.mapNotNull { releaseType ->
                releases[releaseType]?.versionCode?.let {
                    releaseType.id to it
                }
            }.toMap()
        }


        /**
         * The version code of the latest release the user has asked to not be notified about.
         */
        var latestSkippedVersionCode by mutableStateOfStoredSetting(storedSettings::latestSkippedVersionCode)


        /**
         * Resets the [latestSkippedVersionCode] to not skip any versions.
         */
        fun resetLatestSkippedVersionCode() {
            latestSkippedVersionCode = 0
        }


    }


    /**
     * Groups the settings related to the fit optimizer.
     */
    inner class FitOptimizerSettings(
        private val storedSettings: StoredFitOptimizerSettings
    ) {


        /**
         * The number of optimizers to run concurrently.
         */
        var concurrentExecutions: Int by mutableStateOfStoredSetting(storedSettings::concurrentExecutions)


        /**
         * The temperature cooling rate.
         */
        var coolingRate: Double by mutableStateOfStoredSetting(storedSettings::coolingRate)


        /**
         * The number of simulated annealing iterations to run per temperature value.
         */
        var iterationsPerTemperature: Int by mutableStateOfStoredSetting(storedSettings::iterationsPerTemperature)


        /**
         * The price limit (in ISK) when optimizing fits.
         */
        var priceLimit: Double by mutableStateOfStoredSetting(storedSettings::priceLimit)


        /**
         * Whether the ISK limit is enabled.
         */
        var priceLimitEnabled: Boolean by mutableStateOfStoredSetting(storedSettings::priceLimitEnabled)


    }


    /**
     * Groups the settings related to EVE item prices.
     */
    inner class EveItemPricesSettings(
        private val storedSettings: StoredEveItemPricesSettings
    ) {


        /**
         * The state for whether to show the prices column in the fit editor.
         */
        val showInFitEditorState: MutableState<Boolean> = mutableStateOfStoredSetting(storedSettings::showInFitEditor)


        /**
         * Whether to show the prices column in the fit editor.
         */
        var showInFitEditor: Boolean by showInFitEditorState


        /**
         * The state for whether to show the prices column in the fit editor autosuggest.
         */
        val showInFitEditorSuggestedItemsState: MutableState<Boolean> =
            mutableStateOfStoredSetting(storedSettings::showInFitEditorSuggestedItems)


        /**
         * Whether to show the prices column in the fit editor autosuggest.
         */
        var showInFitEditorSuggestedItems: Boolean by showInFitEditorSuggestedItemsState


        /**
         * The state for whether to colorize the prices in the fit editor.
         */
        val colorizeState: MutableState<Boolean> = mutableStateOfStoredSetting(storedSettings::colorize)


        /**
         * Whether to colorize prices in the fit editor.
         */
        var colorize: Boolean by colorizeState


    }


    /**
     * Groups the analytics related settings.
     */
    inner class AnalyticsSettings(
        private val storedSettings: StoredAnalyticsSettings
    ) {


        /**
         * The client id for analytics.
         */
        val clientId: String
            get() = storedSettings.clientId


        /**
         * The user's consent to analytics tracking.
         */
        @OptIn(DelicateCoroutinesApi::class)
        var consent: AnalyticsConsent by mutableStateOfStoredSetting(storedSettings::consent).onSet {
            GlobalScope.launch(Dispatchers.IO) {
                reportTrackingConsent(it)
            }
        }


    }


}


/**
 * Returns a property backed by a compose state that also updates the given setting when written to.
 *
 * This allows presenting a [TheorycrafterSettings.storedSetting] as Compose state.
 */
fun <T> mutableStateOfSetting(
    setting: KMutableProperty0<T>,
    initialValue: T = setting.get()
) = object: ReadWriteProperty<Any?, T> {


    /**
     * The backing [MutableState].
     */
    private val mutableState: MutableState<T> = mutableStateOf(initialValue)


    override fun getValue(thisRef: Any?, property: KProperty<*>): T {
        return mutableState.value
    }


    override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
        mutableState.value = value
        setting.set(value)
    }


}


/**
 * The serializable app settings.
 */
@Serializable
private data class StoredSettings(


    /**
     * The settings of the main window.
     */
    val mainWindow: StoredFitWindowSettings = StoredFitWindowSettings(),


    /**
     * The settings of secondary fit windows.
     * When the [StoredFitWindowSettings.fitId] is null, it means that this window isn't displayed, but we still store its
     * settings, so that when a new window is displayed, it uses them.
     */
    @SerialName("secondaryWindows")
    val secondaryFitWindows: MutableList<StoredFitWindowSettings> = mutableListOf(),


    /**
     * The settings of the market tree window.
     */
    val marketTreeWindow: StoredWindowSettingsWithOpenState = StoredWindowSettingsWithOpenState(),


    /**
     * The settings of the tournament window.
     */
    val tournamentWindow: StoredTournamentWindowSettings = StoredTournamentWindowSettings(),


    /**
     * The settings of the doctrines window.
     */
    val doctrinesWindow: StoredWindowSettingsWithOpenState = StoredWindowSettingsWithOpenState(),


    /**
     * The id of the default skill set.
     */
    var defaultSkillSetId: Int = BuiltInSkillSets.Default.skillSetId,


    /**
     * The settings for the "Pack cargo for battle" dialog.
     */
    val packForBattleDialogSettings: StoredPackForBattleDialogSettings = StoredPackForBattleDialogSettings(),


    /**
     * The id of the active tournament; `null` if none.
     */
    var activeTournamentId: String? = null,


    /**
     * Whether to suggest adding the active tournament's [TournamentDescriptor.suggestedFitTag] as a tag to newly
     * created fits.
     */
    var suggestTournamentFitTagForNewFits: Boolean = true,


    /**
    * Whether the user has checked to not be asked about clearing a non-current tournament from being active.
     */
    var doNotAskAboutNonCurrentButActiveTournament: Boolean = false,


    /**
     * The color theme.
     */
    var colorThemeStyle: ColorThemeStyle = ColorThemeStyle.System,


    /**
     * The UI scale factor.
     */
    var uiScalePct: Int = (DefaultApplicationUiScale*100).roundToInt(),


    /**
     * The last-used options in the fit export options dialog.
     */
    @SerialName("fitExportOptionsDialog")
    var fitExportOptionsDialogValues: FitExportOptionSettings = FitExportOptionSettings(
        eftExportOptions = EftExportOptions.IncludeAll
    ),


    /**
     * The fit export options for the regular "Copy Fit" action.
     */
    @SerialName("fitExportOptions")
    var regularFitExportOptions: FitExportOptionSettings = FitExportOptionSettings(
        eftExportOptions = EftExportOptions.IncludeAll
    ),


    /**
     * The graph panes settings.
     */
    val graphs: StoredGraphSettings = StoredGraphSettings(),


    /**
     * The last-used options in the composition export options dialog.
     */
    @SerialName("compositionExportOptions")
    var compositionExportOptions: CompositionExportOptionSettings = CompositionExportOptionSettings(
        includeInactiveShips = true,
        includeReplacementShips = true,
        includeUtilities = true,
        includeNote = true,
        useTemplates = false,
    ),


    /**
     * The templates for exporting compositions.
     */
    @SerialName("compositionExportTemplates")
    var compositionExportTemplates: CompositionExportTemplatesSettings = CompositionExportTemplatesSettings(
        titleTemplate = "",
        shipTemplate = "",
        utilityTemplate = "",
        noteTemplate = ""
    ),


    /**
     * The pinned fit searches.
     */
    @SerialName("pinnedFitSearches")
    var pinnedFitSearches: List<String> = emptyList(),


    /**
     * The simulated annealing configuration for the fit optimizer.
     */
    @SerialName("fitOptimizer")
    val fitOptimizer: StoredFitOptimizerSettings = StoredFitOptimizerSettings(),


    /**
     * The price settings.
     */
    @SerialName("prices")
    val prices: StoredEveItemPricesSettings = StoredEveItemPricesSettings(),


    /**
     * The new release settings.
     */
    @SerialName("theorycrafterReleases")
    val theorycrafterReleases: StoredTheorycrafterReleasesSettings = StoredTheorycrafterReleasesSettings(),


    /**
     * The analytics settings.
     */
    @SerialName("analytics")
    val analytics: StoredAnalyticsSettings = StoredAnalyticsSettings(),


    )


/**
 * The interface for all serializable window settings.
 */
interface StoredWindowSettings {
    var bounds: StoredWindowBounds?
}


/**
 * Serializable window settings for windows that only store the window bounds.
 */
@Suppress("unused")
@Serializable
data class SimpleStoredWindowSettings(
    override var bounds: StoredWindowBounds? = null
): StoredWindowSettings


/**
 * Serializable window settings for windows that only store the window bounds and whether it's open.
 */
@Serializable
data class StoredWindowSettingsWithOpenState(
    var open: Boolean = false,
    override var bounds: StoredWindowBounds? = null
): StoredWindowSettings


/**
 * The serializable fit window settings.
 */
@Serializable
data class StoredFitWindowSettings(
    var fitId: Int? = null,
    override var bounds: StoredWindowBounds? = null
): StoredWindowSettings


/**
 * The serializable tournament window settings.
 */
@Serializable
data class StoredTournamentWindowSettings(
    var tournamentId: String? = null,
    override var bounds: StoredWindowBounds? = null
): StoredWindowSettings


/**
 * The serializable window bounds.
 */
@Serializable(with = StoredWindowBounds.Serializer::class)
data class StoredWindowBounds(
    val x: Int,
    val y: Int,
    val width: Int,
    val height: Int
) {


    /**
     * The position of the window.
     */
    val position: WindowPosition
        get() = WindowPosition(x = x.dp, y = y.dp)


    /**
     * The size of the window.
     */
    val size: DpSize
        get() = DpSize(width = width.dp, height = height.dp)


    /**
     * The serializer of [StoredWindowBounds] objects.
     */
    companion object Serializer : KSerializer<StoredWindowBounds> {


        /**
         * The delegate serializer into an array of ints.
         */
        private val delegateSerializer = IntArraySerializer()


        @OptIn(ExperimentalSerializationApi::class)
        override val descriptor = SerialDescriptor("WindowBounds", delegateSerializer.descriptor)


        override fun serialize(encoder: Encoder, value: StoredWindowBounds) {
            encoder.encodeSerializableValue(
                serializer = delegateSerializer,
                value = with(value) { intArrayOf(x, y, width, height) }
            )
        }


        override fun deserialize(decoder: Decoder): StoredWindowBounds {
            val array = decoder.decodeSerializableValue(delegateSerializer)
            return StoredWindowBounds(array[0], array[1], array[2], array[3])
        }


    }


}


/**
 * Light/dark theme setting.
 */
enum class ColorThemeStyle {
    @SerialName("system") System,
    @SerialName("light") Light,
    @SerialName("dark") Dark
}


@Serializable
data class StoredPackForBattleDialogSettings(
    @SerialName("battleDuration") var battleDuration: Int = 10.minutes.inWholeSeconds.toInt(),
    @SerialName("selectTech1Ammo") var selectTech1Ammo: Boolean = true,
    @SerialName("selectFactionAmmo") var selectFactionAmmo: Boolean = true,
    @SerialName("selectPirateAmmo") var selectPirateAmmo: Boolean = true,
    @SerialName("selectTech2Ammo") var selectTech2Ammo: Boolean = true,
    @SerialName("selectScripts") var selectScripts: Boolean = true,
    @SerialName("selectCommandBurstCharges") var selectCommandBurstCharges: Boolean = true,
    @SerialName("selectCapBoosters") var selectCapBoosters: Boolean = true,
    @SerialName("selectInterdictionProbes") var selectInterdictionProbes: Boolean = true,
    @SerialName("selectBoosters") var selectBoosters: Boolean = true,
    @SerialName("selectImplants") var selectImplants: Boolean = true,
)


/**
 * The options when exporting a fit.
 */
@Serializable
data class FitExportOptionSettings(
    @SerialName("eft") val eftExportOptions: EftExportOptions
)


/**
 * The options when exporting a composition.
 */
@Serializable
data class CompositionExportOptionSettings(
    @SerialName("includeInactiveShips") val includeInactiveShips: Boolean = true,
    @SerialName("includeReplacementShips") val includeReplacementShips: Boolean = true,
    @SerialName("includeUtilities") val includeUtilities: Boolean = true,
    @SerialName("includeNote") val includeNote: Boolean = true,
    @SerialName("useTemplate") val useTemplates: Boolean = true,
)


/**
 * The templates when exporting a composition with templates.
 */
@Serializable
data class CompositionExportTemplatesSettings(
    @SerialName("title") val titleTemplate: String = "",
    @SerialName("ship") val shipTemplate: String = "",
    @SerialName("utility") val utilityTemplate: String = "",
    @SerialName("note") val noteTemplate: String = "",
)


/**
 * The user's consent for analytics tracking.
 */
@Serializable
enum class AnalyticsConsent {
    @SerialName("yes") Yes,
    @SerialName("no") No,
    @SerialName("undecided") Undecided
}


/**
 * The settings of the graph panes.
 */
@Serializable
data class StoredGraphSettings(
    @SerialName("window") override var bounds: StoredWindowBounds? = null,
    val damage: StoredDamageGraphSettings = StoredDamageGraphSettings(),
    val remoteRepairs: StoredOptionalDronesGraphSettings = StoredOptionalDronesGraphSettings(),
    val neutralization: StoredNeutralizationGraphSettings = StoredNeutralizationGraphSettings(),
    val ecm: StoredEcmGraphSettings = StoredEcmGraphSettings(),
    val webification: StoredOptionalDronesGraphSettings = StoredOptionalDronesGraphSettings(),
    val targetRangeDampening: StoredOptionalDronesGraphSettings = StoredOptionalDronesGraphSettings(),
    val scanResDampening: StoredOptionalDronesGraphSettings = StoredOptionalDronesGraphSettings(),
    val turretRangeDisruption: StoredOptionalDronesGraphSettings = StoredOptionalDronesGraphSettings(),
    val targetPainting: StoredOptionalDronesGraphSettings = StoredOptionalDronesGraphSettings(),
    val lockTime: StoredLockTimeGraphSettings = StoredLockTimeGraphSettings(),
    val genericEffect: StoredGenericEffectGraphSettings = StoredGenericEffectGraphSettings(),
): StoredWindowSettings


/**
 * The settings of the damage graph pane.
 */
@Serializable
data class StoredDamageGraphSettings(
    var showVolley: Boolean = false,
    var includeDrones: Boolean = true,
    var includeWreckingShots: Boolean = false,
    var ammoSelection: AmmoSelection = CurrentlyLoaded,
    var vtSigRadius: Double = 200.0,
    var vtMaxVelocity: Double = 500.0
)


/**
 * The settings of а graph pane with only an `includeDrones` setting.
 */
@Serializable
data class StoredOptionalDronesGraphSettings(
    var includeDrones: Boolean = true,
)


/**
 * The settings of the energy neutralization graph pane.
 */
@Serializable
data class StoredNeutralizationGraphSettings(
    var includeDrones: Boolean = true,
    var includeNosferatus: Boolean = true
)


/**
 * The serializer for [SensorType].
 */
object SensorTypeSerializer: KSerializer<SensorType> {

    override val descriptor: SerialDescriptor =
        PrimitiveSerialDescriptor("theorycrafter.SensorTypeSerializer", PrimitiveKind.STRING)

    override fun serialize(encoder: Encoder, value: SensorType) {
        encoder.encodeString(
            when(value) {
                RADAR -> "radar"
                LADAR -> "ladar"
                GRAVIMETRIC -> "gravimetric"
                MAGNETOMETRIC -> "magnetometric"
            }
        )
    }

    override fun deserialize(decoder: Decoder): SensorType {
        return when (val value = decoder.decodeString()) {
            "radar" -> RADAR
            "ladar" -> LADAR
            "magnetometric" -> MAGNETOMETRIC
            "gravimetric" -> GRAVIMETRIC
            else -> throw IllegalArgumentException("Unexpected sensor type: $value")
        }
    }

}


/**
 * The settings of the ECM effectiness graph pane.
 */
@Serializable
data class StoredEcmGraphSettings(
    var includeDrones: Boolean = true,
    var showChanceToJam: Boolean = true,
    @Serializable(with = SensorTypeSerializer::class)
    var vtSensorType: SensorType = SensorType.entries.first(),
    var vtSensorStrength: Double = 25.0
)


/**
 * The settings for the locking time graph pane.
 */
@Serializable
data class StoredLockTimeGraphSettings(
    var virtualFitScanResolution: Double = 500.0
)


/**
 * The settings for the generic effect graph pane.
 */
@Serializable
data class StoredGenericEffectGraphSettings(
    var effect: Double = 100.0,
    var optimal: Double = 10.0,
    var falloff: Double = 10.0
)


/**
 * The settings related to the fit optimizer.
 */
@Serializable
data class StoredFitOptimizerSettings(

    @SerialName("concurrentExecutions")
    var concurrentExecutions: Int = (Runtime.getRuntime().availableProcessors()/4).coerceAtLeast(1),

    @SerialName("coolingRate")
    var coolingRate: Double = 0.9999,

    @SerialName("iterationsPerTemp")
    var iterationsPerTemperature: Int = 10,

    @SerialName("priceLimit")
    var priceLimit: Double = 100_000_000.0,

    @SerialName("priceLimitEnabled")
    var priceLimitEnabled: Boolean = false,

)


/**
 * The settings related to EVE item prices.
 */
@Serializable
data class StoredEveItemPricesSettings(

    @SerialName("showInFitEditor")
    var showInFitEditor: Boolean = false,

    @SerialName("showInFitEditorSuggestedItems")
    var showInFitEditorSuggestedItems: Boolean = true,

    @SerialName("colorize")
    var colorize: Boolean = true,

)


/**
 * The settings related to new Theorycrafter releases.
 */
@Serializable
data class StoredTheorycrafterReleasesSettings(

    @SerialName("notifyOnNewRelease")
    var notifyOnNewRelease: Boolean = true,

    @SerialName("upgradeLane")
    var upgradeLane: ReleaseType = Theorycrafter.AppReleaseType,

    @SerialName("latestReleaseVersionCode")
    var latestReleaseVersionCode: Map<String, Int> = emptyMap(),

    @SerialName("latestSkippedVersionCode")
    var latestSkippedVersionCode: Int = 0

)


/**
 * The settings related to analytics.
 */
@Serializable
data class StoredAnalyticsSettings(

    /**
     * The client id for analytics.
     */
    @OptIn(ExperimentalUuidApi::class)
    @SerialName("clientId")
    val clientId: String = Uuid.random().toString(),

    /**
     * The user's consent to analytics tracking.
     */
    @SerialName("consent")
    var consent: AnalyticsConsent = AnalyticsConsent.Undecided,

)
