package li.cil.oc

import java.io._
import java.net.{Inet4Address, InetAddress}
import java.util.logging.Level

import com.google.common.net.InetAddresses
import com.typesafe.config._
import cpw.mods.fml.common.Loader
import cpw.mods.fml.common.versioning.{DefaultArtifactVersion, VersionRange}
import li.cil.oc.api.component.TextBuffer.ColorDepth
import li.cil.oc.util.mods.Mods
import org.apache.commons.lang3.StringEscapeUtils

import scala.collection.convert.WrapAsScala._
import scala.io.{Codec, Source}

class Settings(config: Config) {
  val itemId = config.getInt("ids.item")
  val (blockId1, blockId2, blockId3, blockId4) = Array(config.getIntList("ids.block"): _*) match {
    case Array(id1, id2, id3, id4) =>
      (id1: Int, id2: Int, id3: Int, id4: Int)
    case _ =>
      OpenComputers.log.warning("Bad number of block ids, ignoring.")
      (3650, 3651, 3652, 3653)
  }

  // ----------------------------------------------------------------------- //
  // client

  val screenTextFadeStartDistance = config.getDouble("client.screenTextFadeStartDistance")
  val maxScreenTextRenderDistance = config.getDouble("client.maxScreenTextRenderDistance")
  val textLinearFiltering = config.getBoolean("client.textLinearFiltering")
  val textAntiAlias = config.getBoolean("client.textAntiAlias")
  val robotLabels = config.getBoolean("client.robotLabels")
  val soundVolume = config.getDouble("client.soundVolume").toFloat max 0 min 2
  val fontCharScale = config.getDouble("client.fontCharScale") max 0.5 min 2
  val hologramFadeStartDistance = config.getDouble("client.hologramFadeStartDistance") max 0
  val hologramRenderDistance = config.getDouble("client.hologramRenderDistance") max 0
  val hologramFlickerFrequency = config.getDouble("client.hologramFlickerFrequency") max 0
  val logOpenGLErrors = config.getBoolean("client.logOpenGLErrors")
  val useOldTextureFontRenderer = config.getBoolean("client.useOldTextureFontRenderer")

  // ----------------------------------------------------------------------- //
  // computer
  val threads = config.getInt("computer.threads") max 1
  val timeout = config.getDouble("computer.timeout") max 0
  val startupDelay = config.getDouble("computer.startupDelay") max 0.05
  val ramSizes = Array(config.getIntList("computer.ramSizes"): _*) match {
    case Array(tier1, tier2, tier3, tier4, tier5, tier6) =>
      Array(tier1: Int, tier2: Int, tier3: Int, tier4: Int, tier5: Int, tier6: Int)
    case _ =>
      OpenComputers.log.warning("Bad number of RAM sizes, ignoring.")
      Array(192, 256, 384, 512, 768, 1024)
  }
  val ramScaleFor64Bit = config.getDouble("computer.ramScaleFor64Bit") max 1
  val cpuComponentSupport = Array(config.getIntList("computer.cpuComponentCount"): _*) match {
    case Array(tier1, tier2, tier3) =>
      Array(tier1: Int, tier2: Int, tier3: Int)
    case _ =>
      OpenComputers.log.warning("Bad number of CPU component counts, ignoring.")
      Array(8, 12, 16)
  }
  val canComputersBeOwned = config.getBoolean("computer.canComputersBeOwned")
  val maxUsers = config.getInt("computer.maxUsers") max 0
  val maxUsernameLength = config.getInt("computer.maxUsernameLength") max 0
  val allowBytecode = config.getBoolean("computer.allowBytecode")
  val eraseTmpOnReboot = config.getBoolean("computer.eraseTmpOnReboot")
  val executionDelay = config.getInt("computer.executionDelay") max 0

  // ----------------------------------------------------------------------- //
  // computer.debug

  val logLuaCallbackErrors = config.getBoolean("computer.debug.logCallbackErrors")
  val forceLuaJ = config.getBoolean("computer.debug.forceLuaJ")
  val allowUserdata = !config.getBoolean("computer.debug.disableUserdata")
  val allowPersistence = !config.getBoolean("computer.debug.disablePersistence")
  val limitMemory = !config.getBoolean("computer.debug.disableMemoryLimit")
  val forceCaseInsensitive = config.getBoolean("computer.debug.forceCaseInsensitiveFS")

  // ----------------------------------------------------------------------- //
  // robot

  val allowActivateBlocks = config.getBoolean("robot.allowActivateBlocks")
  val allowUseItemsWithDuration = config.getBoolean("robot.allowUseItemsWithDuration")
  val canAttackPlayers = config.getBoolean("robot.canAttackPlayers")
  val screwCobwebs = config.getBoolean("robot.notAfraidOfSpiders")
  val swingRange = config.getDouble("robot.swingRange")
  val useAndPlaceRange = config.getDouble("robot.useAndPlaceRange")
  val itemDamageRate = config.getDouble("robot.itemDamageRate") max 0 min 1
  val nameFormat = config.getString("robot.nameFormat")

  // ----------------------------------------------------------------------- //
  // robot.xp

  val baseXpToLevel = config.getDouble("robot.xp.baseValue") max 0
  val constantXpGrowth = config.getDouble("robot.xp.constantGrowth") max 1
  val exponentialXpGrowth = config.getDouble("robot.xp.exponentialGrowth") max 1
  val robotActionXp = config.getDouble("robot.xp.actionXp") max 0
  val robotExhaustionXpRate = config.getDouble("robot.xp.exhaustionXpRate") max 0
  val robotOreXpRate = config.getDouble("robot.xp.oreXpRate") max 0
  val bufferPerLevel = config.getDouble("robot.xp.bufferPerLevel") max 0
  val toolEfficiencyPerLevel = config.getDouble("robot.xp.toolEfficiencyPerLevel") max 0
  val harvestSpeedBoostPerLevel = config.getDouble("robot.xp.harvestSpeedBoostPerLevel") max 0

  // ----------------------------------------------------------------------- //
  // robot.delays

  // Note: all delays are reduced by one tick to account for the tick they are
  // performed in (since all actions are delegated to the server thread).
  val turnDelay = (config.getDouble("robot.delays.turn") - 0.06) max 0.05
  val moveDelay = (config.getDouble("robot.delays.move") - 0.06) max 0.05
  val swingDelay = (config.getDouble("robot.delays.swing") - 0.06) max 0
  val useDelay = (config.getDouble("robot.delays.use") - 0.06) max 0
  val placeDelay = (config.getDouble("robot.delays.place") - 0.06) max 0
  val dropDelay = (config.getDouble("robot.delays.drop") - 0.06) max 0
  val suckDelay = (config.getDouble("robot.delays.suck") - 0.06) max 0
  val harvestRatio = config.getDouble("robot.delays.harvestRatio") max 0

  // ----------------------------------------------------------------------- //
  // power

  val pureIgnorePower = config.getBoolean("power.ignorePower")
  val ignorePower = pureIgnorePower ||
    (!Mods.BuildCraftPower.isAvailable &&
      !Mods.IndustrialCraft2.isAvailable &&
      !Mods.ThermalExpansion.isAvailable &&
      !Mods.UniversalElectricity.isAvailable)
  val tickFrequency = config.getDouble("power.tickFrequency") max 1
  val chargeRate = config.getDouble("power.chargerChargeRate")
  val generatorEfficiency = config.getDouble("power.generatorEfficiency")
  val solarGeneratorEfficiency = config.getDouble("power.solarGeneratorEfficiency")
  val assemblerTickAmount = config.getDouble("power.assemblerTickAmount") max 1
  val disassemblerTickAmount = config.getDouble("power.disassemblerTickAmount") max 1

  // power.buffer
  val bufferCapacitor = config.getDouble("power.buffer.capacitor") max 0
  val bufferCapacitorAdjacencyBonus = config.getDouble("power.buffer.capacitorAdjacencyBonus") max 0
  val bufferComputer = config.getDouble("power.buffer.computer") max 0
  val bufferRobot = config.getDouble("power.buffer.robot") max 0
  val bufferConverter = config.getDouble("power.buffer.converter") max 0
  val bufferDistributor = config.getDouble("power.buffer.distributor") max 0
  val bufferCapacitorUpgrades = Array(config.getDoubleList("power.buffer.batteryUpgrades"): _*) match {
    case Array(tier1, tier2, tier3) =>
      Array(tier1: Double, tier2: Double, tier3: Double)
    case _ =>
      OpenComputers.log.warning("Bad number of battery upgrade buffer sizes, ignoring.")
      Array(10000.0, 15000.0, 20000.0)
  }

  // power.cost
  val computerCost = config.getDouble("power.cost.computer") max 0
  val robotCost = config.getDouble("power.cost.robot") max 0
  val sleepCostFactor = config.getDouble("power.cost.sleepFactor") max 0
  val screenCost = config.getDouble("power.cost.screen") max 0
  val hologramCost = config.getDouble("power.cost.hologram") max 0
  val hddReadCost = (config.getDouble("power.cost.hddRead") max 0) / 1024
  val hddWriteCost = (config.getDouble("power.cost.hddWrite") max 0) / 1024
  val gpuSetCost = (config.getDouble("power.cost.gpuSet") max 0) / Settings.basicScreenPixels
  val gpuFillCost = (config.getDouble("power.cost.gpuFill") max 0) / Settings.basicScreenPixels
  val gpuClearCost = (config.getDouble("power.cost.gpuClear") max 0) / Settings.basicScreenPixels
  val gpuCopyCost = (config.getDouble("power.cost.gpuCopy") max 0) / Settings.basicScreenPixels
  val robotTurnCost = config.getDouble("power.cost.robotTurn") max 0
  val robotMoveCost = config.getDouble("power.cost.robotMove") max 0
  val robotExhaustionCost = config.getDouble("power.cost.robotExhaustion") max 0
  val wirelessCostPerRange = config.getDouble("power.cost.wirelessCostPerRange") max 0
  val abstractBusPacketCost = config.getDouble("power.cost.abstractBusPacket") max 0
  val geolyzerScanCost = config.getDouble("power.cost.geolyzerScan") max 0
  val robotBaseCost = config.getDouble("power.cost.robotAssemblyBase") max 0
  val robotComplexityCost = config.getDouble("power.cost.robotAssemblyComplexity") max 0
  val disassemblerItemCost = config.getDouble("power.cost.disassemblerPerItem") max 0
  val chunkloaderCost = config.getDouble("power.cost.chunkloaderCost") max 0

  // ----------------------------------------------------------------------- //
  // filesystem
  val fileCost = config.getInt("filesystem.fileCost") max 0
  val bufferChanges = config.getBoolean("filesystem.bufferChanges")
  val hddSizes = Array(config.getIntList("filesystem.hddSizes"): _*) match {
    case Array(tier1, tier2, tier3) =>
      Array(tier1: Int, tier2: Int, tier3: Int)
    case _ =>
      OpenComputers.log.warning("Bad number of HDD sizes, ignoring.")
      Array(1024, 2048, 4096)
  }
  val floppySize = config.getInt("filesystem.floppySize") max 0
  val tmpSize = config.getInt("filesystem.tmpSize") max 0
  val maxHandles = config.getInt("filesystem.maxHandles") max 0
  val maxReadBuffer = config.getInt("filesystem.maxReadBuffer") max 0

  // ----------------------------------------------------------------------- //
  // internet
  val httpEnabled = config.getBoolean("internet.enableHttp")
  val tcpEnabled = config.getBoolean("internet.enableTcp")
  val httpHostBlacklist = Array(config.getStringList("internet.blacklist").map(new Settings.AddressValidator(_)): _*)
  val httpHostWhitelist = Array(config.getStringList("internet.whitelist").map(new Settings.AddressValidator(_)): _*)
  val httpTimeout = (config.getInt("internet.requestTimeout") max 0) * 1000
  val httpMaxDownloadSize = config.getInt("internet.requestMaxDownloadSize") max 0
  val maxConnections = config.getInt("internet.maxTcpConnections") max 0
  val internetThreads = config.getInt("internet.threads") max 1

  // ----------------------------------------------------------------------- //
  // misc
  val maxScreenWidth = config.getInt("misc.maxScreenWidth") max 1
  val maxScreenHeight = config.getInt("misc.maxScreenHeight") max 1
  val inputUsername = config.getBoolean("misc.inputUsername")
  val maxClipboard = config.getInt("misc.maxClipboard") max 0
  val maxNetworkPacketSize = config.getInt("misc.maxNetworkPacketSize") max 0
  val maxOpenPorts = config.getInt("misc.maxOpenPorts") max 0
  val maxWirelessRange = config.getDouble("misc.maxWirelessRange") max 0
  val rTreeMaxEntries = 10
  val terminalsPerTier = Array(config.getIntList("misc.terminalsPerTier"): _*) match {
    case Array(tier1, tier2, tier3) =>
      Array(math.max(tier1, 1), math.max(tier2, 1), math.max(tier3, 1))
    case _ =>
      OpenComputers.log.warning("Bad number of Remote Terminal counts, ignoring.")
      Array(2, 4, 8)
  }
  val updateCheck = config.getBoolean("misc.updateCheck")
  val alwaysTryNative = config.getBoolean("misc.alwaysTryNative")
  val lootProbability = config.getInt("misc.lootProbability")
  val debugPersistence = config.getBoolean("misc.verbosePersistenceErrors")
  val geolyzerRange = config.getInt("misc.geolyzerRange")
  val geolyzerNoise = config.getDouble("misc.geolyzerNoise").toFloat max 0
  val disassembleAllTheThings = config.getBoolean("misc.disassembleAllTheThings")
  val disassemblerBreakChance = config.getDouble("misc.disassemblerBreakChance") max 0 min 1
  val hideOwnPet = config.getBoolean("misc.hideOwnSpecial")
}

object Settings {
  val resourceDomain = "opencomputers"
  val namespace = "oc:"
  val savePath = "opencomputers/"
  val scriptPath = "/assets/" + resourceDomain + "/lua/"
  val screenResolutionsByTier = Array((50, 16), (80, 25), (160, 50))
  val screenDepthsByTier = Array(ColorDepth.OneBit, ColorDepth.FourBit, ColorDepth.EightBit)
  val hologramMaxScaleByTier = Array(3, 4)
  val robotComplexityByTier = Array(12, 24, 32, 9001)
  var rTreeDebugRenderer = false

  // Power conversion values. These are the same values used by Universal
  // Electricity to provide global power support.
  val valueBC = 56280.0
  val valueIC2 = 22512.0
  val valueTE = 5628.0
  val valueUE = 1.0

  val valueOC = valueBC

  val ratioBC = valueBC / valueOC
  val ratioIC2 = valueIC2 / valueOC
  val ratioTE = valueTE / valueOC
  val ratioUE = valueUE / valueOC

  def basicScreenPixels = screenResolutionsByTier(0)._1 * screenResolutionsByTier(0)._2

  private var settings: Settings = _

  def get = settings

  def load(file: File) = {
    import scala.compat.Platform.EOL
    // typesafe config's internal method for loading the reference.conf file
    // seems to fail on some systems (as does their parseResource method), so
    // we'll have to load the default config manually. This was reported on the
    // Minecraft Forums, I could not reproduce the issue, but this version has
    // reportedly fixed the problem.
    val defaults = {
      val in = classOf[Settings].getResourceAsStream("/application.conf")
      val config = Source.fromInputStream(in)(Codec.UTF8).getLines().mkString("", EOL, EOL)
      in.close()
      ConfigFactory.parseString(config)
    }
    val config =
      try {
        val plain = Source.fromFile(file)(Codec.UTF8).getLines().mkString("", EOL, EOL)
        val config = patchConfig(ConfigFactory.parseString(plain), defaults).withFallback(defaults)
        settings = new Settings(config.getConfig("opencomputers"))
        config
      }
      catch {
        case e: Throwable =>
          if (file.exists()) {
            OpenComputers.log.log(Level.WARNING, "Failed loading config, using defaults.", e)
          }
          settings = new Settings(defaults.getConfig("opencomputers"))
          defaults
      }
    try {
      val renderSettings = ConfigRenderOptions.defaults.setJson(false).setOriginComments(false)
      val nl = sys.props("line.separator")
      val nle = StringEscapeUtils.escapeJava(nl)
      val out = new PrintWriter(file)
      out.write(config.root.render(renderSettings).lines.
        // Indent two spaces instead of four.
        map(line => """^(\s*)""".r.replaceAllIn(line, m => m.group(1).replace("  ", " "))).
        // Finalize the string.
        filter(_ != "").mkString(nl).
        // Newline after values.
        replaceAll(s"((?:\\s*#.*$nle)(?:\\s*[^#\\s].*$nle)+)", "$1" + nl))
      out.close()
    }
    catch {
      case e: Throwable =>
        OpenComputers.log.log(Level.WARNING, "Failed saving config.", e)
    }
  }

  private val configPatches = Array(
    // Upgrading to version 1.3, increased lower bounds for default RAM sizes
    // and reworked the way black- and whitelisting works (IP based).
    VersionRange.createFromVersionSpec("[0.0,1.3-alpha)") -> Array(
      "computer.ramSizes",
      "internet.blacklist",
      "internet.whitelist"
    )
  )

  // Checks the config version (i.e. the version of the mod the config was
  // created by) against the current version to see if some hard changes
  // were made. If so, the new default values are copied over.
  private def patchConfig(config: Config, defaults: Config) = {
    val mod = Loader.instance.activeModContainer
    val prefix = "opencomputers."
    val configVersion = new DefaultArtifactVersion(if (config.hasPath(prefix + "version")) config.getString(prefix + "version") else "0.0.0")
    var patched = config
    if (configVersion.compareTo(mod.getProcessedVersion) != 0) {
      OpenComputers.log.info(s"Updating config from version '${configVersion.getVersionString}' to '${defaults.getString(prefix + "version")}'.")
      patched = patched.withValue(prefix + "version", defaults.getValue(prefix + "version"))
      for ((version, paths) <- configPatches if version.containsVersion(configVersion)) {
        for (path <- paths) {
          val fullPath = prefix + path
          OpenComputers.log.info(s"Updating setting '$fullPath'. ")
          patched = patched.withValue(fullPath, defaults.getValue(fullPath))
        }
      }
    }
    patched
  }

  val cidrPattern = """(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(?:/(\d{1,2}))""".r

  class AddressValidator(val value: String) {
    val validator = try cidrPattern.findFirstIn(value) match {
      case Some(cidrPattern(address, prefix)) =>
        val addr = InetAddresses.coerceToInteger(InetAddresses.forString(address))
        val mask = 0xFFFFFFFF << (32 - prefix.toInt)
        val min = addr & mask
        val max = min | ~mask
        (inetAddress: InetAddress, host: String) => inetAddress match {
          case v4: Inet4Address =>
            val numeric = InetAddresses.coerceToInteger(v4)
            min <= numeric && numeric <= max
          case _ => true // Can't check IPv6 addresses so we pass them.
        }
      case _ =>
        val address = InetAddress.getByName(value)
        (inetAddress: InetAddress, host: String) => host == value || inetAddress == address
    } catch {
      case t: Throwable =>
        OpenComputers.log.log(Level.WARNING, "Invalid entry in internet blacklist / whitelist: " + value, t)
        (inetAddress: InetAddress, host: String) => true
    }

    def apply(inetAddress: InetAddress, host: String) = validator(inetAddress, host)
  }

}