package ocelot.desktop

import com.typesafe.config.{Config, ConfigFactory, ConfigRenderOptions, ConfigValueFactory}
import ocelot.desktop.Settings.ExtendedConfig
import ocelot.desktop.util.{Logging, SettingsData}
import org.apache.commons.lang3.SystemUtils

import java.nio.charset.StandardCharsets
import java.nio.file.{Files, Path}
import java.util

class Settings(val config: Config) extends SettingsData {
  // TODO: refactor this mess (having to declare every field 3 times is extremely error-prone)

  brainCustomConfigPath = config.getOptionalString("ocelot.brain.customConfigPath")

  volumeMaster = (config.getDoubleOrElse("ocelot.sound.volumeMaster", 1) max 0 min 1).toFloat
  volumeRecords = (config.getDoubleOrElse("ocelot.sound.volumeRecords", 1) max 0 min 1).toFloat
  volumeBeep = (config.getDoubleOrElse("ocelot.sound.volumeBeep", 1) max 0 min 1).toFloat
  volumeEnvironment = (config.getDoubleOrElse("ocelot.sound.volumeEnvironment", 1) max 0 min 1).toFloat
  volumeInterface = (config.getDoubleOrElse("ocelot.sound.volumeInterface", 0.5) max 0 min 1).toFloat

  audioDisable = config.getBooleanOrElse("ocelot.sound.audioDisable", default = false)
  soundPositional = config.getBooleanOrElse("ocelot.sound.positional", default = true)
  logAudioErrorStacktrace = config.getBooleanOrElse("ocelot.sound.logAudioErrorStacktrace", default = false)

  scaleFactor = (config.getDoubleOrElse("ocelot.window.scaleFactor", default = 1) max 1 min 3).toFloat
  windowSize = config.getInt2D("ocelot.window.size")
  windowPosition = config.getInt2D("ocelot.window.position")
  windowValidatePosition = config.getBooleanOrElse("ocelot.window.validatePosition", default = true)
  windowFullscreen = config.getBooleanOrElse("ocelot.window.fullscreen", default = false)
  disableVsync = config.getBooleanOrElse("ocelot.window.disableVsync", default = false)
  debugLwjgl = config.getBooleanOrElse("ocelot.window.debugLwjgl", default = false)

  // Windows uses life-hack when it sets position to (-8,-8) for maximized windows to hide the frame.
  // (https://devblogs.microsoft.com/oldnewthing/20150304-00/?p=44543)
  // In LWJGL 2 we cannot read the "maximize" flag from the window, so we will just adjust the position.
  if (SystemUtils.IS_OS_WINDOWS && windowPosition.x == -8 && windowPosition.y == -8) {
    windowPosition.x = 0
    windowPosition.y = 0
    windowSize.x -= 16
    windowSize.y -= 16
  }

  if (config.hasPath("ocelot.keymap")) {
    keymap.load(config.getConfig("ocelot.keymap"))
  }

  recentWorkspace = config.getOptionalString("ocelot.workspace.recent")
  pinNewWindows = config.getBooleanOrElse("ocelot.workspace.pinNewWindows", default = true)
  unfocusedWindowTransparency = config.getDoubleOrElse("ocelot.workspace.unfocusedWindowTransparency", 0.5).toFloat
  unfocusedWindowHide = config.getBooleanOrElse("ocelot.workspace.unfocusedWindowHide", default = false)
  saveOnExit = config.getBooleanOrElse("ocelot.workspace.saveOnExit", default = true)
  autosave = config.getBooleanOrElse("ocelot.workspace.autosave", default = true)
  autosavePeriod = config.getIntOrElse("ocelot.workspace.autosavePeriod", default = 300)
  openLastWorkspace = config.getBooleanOrElse("ocelot.workspace.openLastWorkspace", default = true)
  renderScreenDataOnNodes = config.getBooleanOrElse("ocelot.workspace.renderScreenDataOnNodes", default = true)
  tooltipDelayItem = (config.getDoubleOrElse("ocelot.workspace.tooltipDelayItem", 0.001) max 0.001 min 2.0).toFloat
  tooltipDelayUI = (config.getDoubleOrElse("ocelot.workspace.tooltipDelayUI", 0.3) max 0.001 min 2.0).toFloat
  enableFestiveDecorations = config.getBooleanOrElse("ocelot.workspace.enableFestiveDecorations", default = true)

  screenWindowMipmap = config.getBooleanOrElse("ocelot.render.screenWindowMipmap", default = true)
}

object Settings extends Logging {
  implicit class ExtendedConfig(val config: Config) {
    def getInt2D(path: String): Int2D =
      if (config.hasPath(path)) new Int2D(config.getIntList(path))
      else new Int2D()

    def getOptionalString(path: String): Option[String] =
      if (config.hasPath(path)) Option(config.getString(path))
      else None

    def getBooleanOrElse(path: String, default: Boolean): Boolean =
      if (config.hasPath(path)) config.getBoolean(path)
      else default

    def getIntOrElse(path: String, default: Int): Int =
      if (config.hasPath(path)) config.getInt(path)
      else default

    def getDoubleOrElse(path: String, default: Double): Double =
      if (config.hasPath(path)) config.getDouble(path)
      else default

    def withValuePreserveOrigin(path: String, value: Any): Config = {
      config.withValue(
        path,
        if (config.hasPath(path))
          ConfigValueFactory.fromAnyRef(value).withOrigin(config.getValue(path).origin())
        else ConfigValueFactory.fromAnyRef(value),
      )
    }

    def withValuePreserveOrigin(path: String, value: Option[Any]): Config = {
      config.withValue(
        path,
        if (config.hasPath(path))
          ConfigValueFactory.fromAnyRef(value.orNull).withOrigin(config.getValue(path).origin())
        else ConfigValueFactory.fromAnyRef(value.orNull),
      )
    }

    def withValue(path: String, value: Int2D): Config = {
      config.withValue(path, ConfigValueFactory.fromIterable(util.Arrays.asList(value.x, value.y)))
    }

    def withValue(path: String, value: Option[Any]): Config =
      config.withValue(path, ConfigValueFactory.fromAnyRef(value.orNull))

    def withValue(path: String, value: Int): Config =
      config.withValue(path, ConfigValueFactory.fromAnyRef(value))
  }

  class Int2D(var x: Int, var y: Int) {
    var isSet: Boolean = false

    def this() = this(0, 0)

    def this(list: util.List[Integer]) = {
      this()
      if (list.size() == 2) {
        set(list.get(0), list.get(1))
        isSet = true
      }
    }

    def set(x: Int, y: Int): Unit = {
      this.x = x
      this.y = y
      isSet = true
    }
  }

  private val renderOptions = ConfigRenderOptions.defaults().setJson(false).setOriginComments(false)
  private var settings: Settings = _
  def get: Settings = settings

  def load(path: Path): Unit = {
    if (Files.exists(path)) {
      try {
        settings = new Settings(ConfigFactory.parseFile(path.toFile))
        logger.info(s"Loaded Ocelot Desktop configuration from: $path")
        return
      } catch {
        case t: Throwable => logger.error(s"Failed to parse $path!", t)
      }
    }

    logger.info(s"Using default Ocelot Desktop configuration...")
    settings = new Settings(ConfigFactory.parseResources(OcelotDesktop.getClass, "/ocelot/desktop/ocelot.conf"))
  }

  def save(path: Path): Unit = {
    if (settings != null) {
      val updatedConfig = settings.config
        .withValuePreserveOrigin("ocelot.brain.customConfigPath", settings.brainCustomConfigPath)
        .withValuePreserveOrigin("ocelot.sound.volumeMaster", settings.volumeMaster)
        .withValuePreserveOrigin("ocelot.sound.volumeRecords", settings.volumeRecords)
        .withValuePreserveOrigin("ocelot.sound.volumeBeep", settings.volumeBeep)
        .withValuePreserveOrigin("ocelot.sound.volumeEnvironment", settings.volumeEnvironment)
        .withValuePreserveOrigin("ocelot.sound.volumeInterface", settings.volumeInterface)
        .withValuePreserveOrigin("ocelot.sound.audioDisable", settings.audioDisable)
        .withValuePreserveOrigin("ocelot.sound.positional", settings.soundPositional)
        .withValuePreserveOrigin("ocelot.sound.logAudioErrorStacktrace", settings.logAudioErrorStacktrace)
        .withValuePreserveOrigin("ocelot.window.scaleFactor", settings.scaleFactor.doubleValue)
        .withValue("ocelot.window.position", settings.windowPosition)
        .withValuePreserveOrigin("ocelot.window.validatePosition", settings.windowValidatePosition)
        .withValue("ocelot.window.size", settings.windowSize)
        .withValuePreserveOrigin("ocelot.window.fullscreen", settings.windowFullscreen)
        .withValuePreserveOrigin("ocelot.window.disableVsync", settings.disableVsync)
        .withValuePreserveOrigin("ocelot.window.debugLwjgl", settings.debugLwjgl)
        .withValue("ocelot.keymap", settings.keymap.save())
        .withValue("ocelot.workspace.recent", settings.recentWorkspace)
        .withValuePreserveOrigin("ocelot.workspace.pinNewWindows", settings.pinNewWindows)
        .withValuePreserveOrigin("ocelot.workspace.unfocusedWindowTransparency", settings.unfocusedWindowTransparency)
        .withValuePreserveOrigin("ocelot.workspace.unfocusedWindowHide", settings.unfocusedWindowHide)
        .withValuePreserveOrigin("ocelot.workspace.saveOnExit", settings.saveOnExit)
        .withValuePreserveOrigin("ocelot.workspace.autosave", settings.autosave)
        .withValuePreserveOrigin("ocelot.workspace.autosavePeriod", settings.autosavePeriod)
        .withValuePreserveOrigin("ocelot.workspace.openLastWorkspace", settings.openLastWorkspace)
        .withValuePreserveOrigin("ocelot.workspace.renderScreenDataOnNodes", settings.renderScreenDataOnNodes)
        .withValuePreserveOrigin("ocelot.workspace.tooltipDelayItem", settings.tooltipDelayItem)
        .withValuePreserveOrigin("ocelot.workspace.tooltipDelayUI", settings.tooltipDelayUI)
        .withValuePreserveOrigin("ocelot.workspace.enableFestiveDecorations", settings.enableFestiveDecorations)

      if (!Files.exists(path.getParent))
        Files.createDirectory(path.getParent)

      Files.write(path, updatedConfig.root().render(renderOptions).getBytes(StandardCharsets.UTF_8))
      logger.info(s"Saved Ocelot Desktop configuration to: $path")
    }
  }
}
