package fr.username404.snowygui.config import com.typesafe.config.Config import com.typesafe.config.ConfigException import com.typesafe.config.ConfigFactory.empty import com.typesafe.config.ConfigFactory.load import com.typesafe.config.ConfigFactory.parseFile import com.typesafe.config.ConfigFactory.parseString import com.typesafe.config.ConfigRenderOptions import com.typesafe.config.ConfigValueFactory import dev.architectury.injectables.targets.ArchitecturyTarget import fr.username404.snowygui.ClickGui import fr.username404.snowygui.Snowy import fr.username404.snowygui.Snowy.Companion.MissingComponent import fr.username404.snowygui.gui.feature.Category import fr.username404.snowygui.gui.feature.Macro import fr.username404.snowygui.gui.feature.shouldSave import io.github.config4k.extract import io.github.config4k.getValue import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import net.minecraft.network.chat.contents.TranslatableContents import org.jetbrains.annotations.ApiStatus import java.io.File import java.nio.file.Path import kotlin.io.path.absolutePathString import kotlin.reflect.KClass import kotlin.reflect.KProperty import kotlin.reflect.full.isSuperclassOf object Configuration { @Deprecated("Use the getValue or setValue methods instead", level = DeprecationLevel.ERROR) @JvmStatic val ModifiableValues: MutableMap by lazy { mutableMapOf( "enabledFeatures" to enabledFeatures, "macros" to lazy { Category.MACROS.box.buttons.map { Json.encodeToString(it as Macro) } }, "box_colors" to mapOf(*ClickGui.clickBoxes.filter { box -> ((box.name.contents as TranslatableContents).key != (MissingComponent.contents as TranslatableContents).key) && Category.fromBox(box)?.categoryColor != box.color }.map { (it.name as TranslatableContents).key to it.color }.toTypedArray()), ) } private val configDirectory: String = (if (ArchitecturyTarget.getCurrentTarget() == "fabric") { Class.forName("net.fabricmc.loader.api.FabricLoader").run { getMethod("getConfigDir").invoke(getMethod("getInstance").invoke(null)) } } else { Class.forName("net.neoforged.fml.loading.FMLPaths") .getField("CONFIGDIR") .get(null) .let { enum -> enum.javaClass.getMethod("get").invoke(enum) } } as Path).absolutePathString() private val file: File = File(configDirectory + File.separator + "snowy.conf") private val obtained: Config = run { var result: Config = empty() with(file) { if (!exists()) { createNewFile() setWritable(true) setReadable(true) } else try { result = parseFile(file) } catch (_: ConfigException) { Snowy.logs.warn("Could not parse the snowy configuration file, the default configuration will be used instead.") } } load(result).withFallback( parseString( """ |Snowy { | macros = [] | sortAlphabetically = true |} """.trimMargin() ) ).extract("Snowy") }.also { runBlocking { writeConfig(it) } } val enabledFeatures = mutableMapOf().apply { "enabledFeatures".let { obtained.run { if (hasPath(it)) { putAll(extract>(it)) } } } } internal val foundMacros: Set = run { obtained.getStringList("macros").map { Json.decodeFromString(it) }.toSet() } init { Runtime.getRuntime().addShutdownHook( Thread { runBlocking { ClickGui.boxContext { enabledFeatures.putAll(buttons.filter { it.info.shouldSave() }.map { it.title to it.toggled }) } writeConfig(obtained.withFullModifiableValues()).join() } } ) } private fun Config.withFullModifiableValues() = @Suppress("DEPRECATION_ERROR") ModifiableValues.entries.fold(this) { previous, entry -> previous.withValue(entry.key, entry.value.let { ConfigValueFactory.fromAnyRef( if (it !is Lazy<*>) it else it.value ) }) } private suspend fun writeConfig(c: Config) = coroutineScope { launch(start = CoroutineStart.UNDISPATCHED) { file.writeText( """ |Snowy { |${ c.root().render( ConfigRenderOptions.defaults() .setFormatted(true) .setJson(false) .setOriginComments(false) ).prependIndent(" ").trimEnd() } |} """.trimMargin() ) } } operator fun invoke() = obtained @JvmSynthetic @ApiStatus.Internal @Suppress("DeprecatedCallableAddReplaceWith") @Deprecated("This method should only be used in the getValue operator of the Configuration object", level = DeprecationLevel.ERROR) fun convertValue(type: KClass): T? = @Suppress("UNCHECKED_CAST") when { Number::class.isSuperclassOf(type) -> when (type) { Double::class -> 2.0 Int::class, UInt::class, Short::class, UShort::class, Long::class, ULong::class -> 2 Float::class -> 2F else -> null } as T type == Boolean::class -> false as T else -> null } inline operator fun getValue(ref: Any?, property: KProperty<*>): T = @Suppress("DEPRECATION_ERROR") if (ModifiableValues.containsKey(property.name)) ModifiableValues[property.name] as T else try { invoke().getValue(ref, property) } catch (e: ConfigException) { convertValue(T::class) ?: throw e } operator fun setValue(ref: Any?, property: KProperty<*>, value: T) = @Suppress("DEPRECATION_ERROR") ModifiableValues.setValue(ref, property, value) }