package net.torvald.terrarum

import com.google.gson.JsonArray
import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive
import net.torvald.imagefont.GameFontImpl
import net.torvald.JsonFetcher
import net.torvald.JsonWriter
import net.torvald.imagefont.TinyAlphNum
import net.torvald.terrarum.gameworld.toUint
import org.lwjgl.input.Controllers
import org.lwjgl.opengl.*
import org.newdawn.slick.*
import org.newdawn.slick.opengl.Texture
import org.newdawn.slick.state.StateBasedGame
import java.io.File
import java.io.IOException
import java.lang.management.ManagementFactory
import java.nio.ByteOrder
import java.text.SimpleDateFormat
import java.util.*
import java.util.logging.FileHandler
import java.util.logging.Level
import java.util.logging.Logger
import java.util.logging.SimpleFormatter

const val GAME_NAME = "Terrarum"

typealias Millisec = Int

/**
 * Created by minjaesong on 15-12-30.
 */
object Terrarum : StateBasedGame(GAME_NAME) {


    val sysLang: String
        get() {
            val lan = System.getProperty("user.language")
            val country = System.getProperty("user.country")
            return lan + country
        }

    /**
     * To be used with physics simulator
     */
    val TARGET_FPS = 50

    /**
     * To be used with render, to achieve smooth frame drawing

     * TARGET_INTERNAL_FPS > TARGET_FPS for smooth frame drawing

     * Must choose a value so that (1000 / VAL) is still integer
     */
    val TARGET_INTERNAL_FPS = 100

    lateinit var appgc: AppGameContainer

    var WIDTH = 1072
    var HEIGHT = 742 // IMAX ratio
    var VSYNC = true
    val VSYNC_TRIGGER_THRESHOLD = 56
    val HALFW: Int
        get() = WIDTH.ushr(1)
    val HALFH: Int
        get() = HEIGHT.ushr(1)

    var gameStarted = false

    var ingame: StateInGame? = null
    private val gameConfig = GameConfig()

    val OSName = System.getProperty("os.name")
    val OSVersion = System.getProperty("os.version")
    lateinit var OperationSystem: String // all caps "WINDOWS, "OSX", "LINUX", "SOLARIS", "UNKNOWN"
        private set
    val isWin81: Boolean
        get() = OperationSystem == "WINDOWS" && OSVersion.toDouble() >= 8.1
    lateinit var defaultDir: String
        private set
    lateinit var defaultSaveDir: String
        private set

    val memInUse: Long
        get() = (Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()) shr 20
    val memTotal: Long
        get() = Runtime.getRuntime().totalMemory() shr 20
    val memXmx: Long
        get() = Runtime.getRuntime().maxMemory() shr 20

    lateinit var environment: RunningEnvironment

    private val localeSimple = arrayOf("de", "en", "es", "it")
    var gameLocale = "####" // lateinit placeholder
        set(value) {
            if (localeSimple.contains(value.substring(0..1)))
                field = value.substring(0..1)
            else
                field = value

            if (fontGame != null) (fontGame as GameFontImpl).reload()
        }

    var fontGame: Font? = null
        private set
    lateinit var fontSmallNumbers: Font
        private set

    var joypadLabelStart: Char = 0xE000.toChar() // lateinit
    var joypadLableSelect: Char = 0xE000.toChar() // lateinit
    var joypadLabelNinA: Char = 0xE000.toChar() // lateinit TODO
    var joypadLabelNinB: Char = 0xE000.toChar() // lateinit TODO
    var joypadLabelNinX: Char = 0xE000.toChar() // lateinit TODO
    var joypadLabelNinY: Char = 0xE000.toChar() // lateinit TODO
    var joypadLabelNinL: Char = 0xE000.toChar() // lateinit TODO
    var joypadLabelNinR: Char = 0xE000.toChar() // lateinit TODO
    var joypadLabelNinZL: Char = 0xE000.toChar() // lateinit TODO
    var joypadLabelNinZR: Char = 0xE000.toChar() // lateinit TODO
    val joypadLabelLEFT = 0xE068.toChar()
    val joypadLabelDOWN = 0xE069.toChar()
    val joypadLabelUP = 0xE06A.toChar()
    val joypadLabelRIGHT = 0xE06B.toChar()

    // 0x0 - 0xF: Game-related
    // 0x10 - 0x1F: Config
    // 0x100 and onward: unit tests for dev
    val STATE_ID_SPLASH = 0x0
    val STATE_ID_HOME = 0x1
    val STATE_ID_GAME = 0x3
    val STATE_ID_CONFIG_CALIBRATE = 0x11

    val STATE_ID_TEST_FONT = 0x100
    val STATE_ID_TEST_LIGHTNING_GFX = 0x101
    val STATE_ID_TEST_TTY = 0x102
    val STATE_ID_TEST_BLUR = 0x103
    val STATE_ID_TEST_SHADER = 0x104
    val STATE_ID_TEST_UI = 0x105

    val STATE_ID_TOOL_NOISEGEN = 0x200

    var controller: org.lwjgl.input.Controller? = null
        private set
    val CONTROLLER_DEADZONE = 0.1f

    /** Available CPU threads */
    val THREADS = Runtime.getRuntime().availableProcessors()

    /**
     * If the game is multithreading.
     * True if:
     *
     *     THREADS >= 2 and config "multithread" is true
     */
    val MULTITHREAD: Boolean
        get() = THREADS >= 2 && getConfigBoolean("multithread")

    private lateinit var configDir: String

    /**
     * 0xAA_BB_XXXX
     * AA: Major version
     * BB: Minor version
     * XXXX: Revision (Repository commits)
     *
     * e.g. 0x02010034 can be translated as 2.1.52
     */
    const val VERSION_RAW = 0x000200E1
    const val VERSION_STRING: String =
            "${VERSION_RAW.ushr(24)}.${VERSION_RAW.and(0xFF0000).ushr(16)}.${VERSION_RAW.and(0xFFFF)}"
    const val NAME = "Terrarum"

    var UPDATE_DELTA: Int = 0

    // these properties goes into the GameContainer

    var previousState: Int? = null // to be used with temporary states like StateMonitorCheck

    val systemArch = System.getProperty("os.arch")

    private val thirtyTwoBitArchs = arrayOf("i386", "i686", "ppc", "x86", "x86_32") // I know I should Write Once, Run Everywhere; but just in case :)
    val is32Bit = thirtyTwoBitArchs.contains(systemArch)

    init {

        // just in case
        println("[Terrarum] os.arch = $systemArch")

        if (is32Bit) {
            println("Java is running in 32 Bit")
        }

        joypadLabelStart = when (getConfigString("joypadlabelstyle")) {
            "nwii"     -> 0xE04B.toChar() // + mark
            "logitech" -> 0xE05A.toChar() // number 10
            else       -> 0xE042.toChar() // |> mark (sonyps, msxb360, generic)
        }
        joypadLableSelect = when (getConfigString("joypadlabelstyle")) {
            "nwii"     -> 0xE04D.toChar() // - mark
            "logitech" -> 0xE059.toChar() // number 9
            "sonyps"   -> 0xE043.toChar() // solid rectangle
            "msxb360"  -> 0xE041.toChar() // <| mark
            else       -> 0xE043.toChar() // solid rectangle
        }



        getDefaultDirectory()
        createDirs()

        val readFromDisk = readConfigJson()
        if (!readFromDisk) readConfigJson()

        try {
            Controllers.getController(0)
            environment = if (getConfigString("pcgamepadenv") == "console")
                RunningEnvironment.CONSOLE
            else
                RunningEnvironment.PC
        }
        catch (e: IndexOutOfBoundsException) {
            environment = RunningEnvironment.PC
        }
    }

    @Throws(SlickException::class)
    override fun initStatesList(gc: GameContainer) {
        gc.input.enableKeyRepeat()


        // get locale from config
        val gameLocaleFromConfig = gameConfig.getAsString("language") ?: sysLang

        // if bad game locale were set, use system locale
        if (gameLocaleFromConfig.length < 2)
            gameLocale = sysLang
        else
            gameLocale = gameLocaleFromConfig

        println("[Terrarum] Locale: " + gameLocale)


        fontGame = GameFontImpl()
        fontSmallNumbers = TinyAlphNum()


        // search for real controller
        // exclude controllers with name "Mouse", "keyboard"
        val notControllerRegex = Regex("mouse|keyboard")
        try {
            // gc.input.controllerCount is unreliable
            for (i in 0..255) {
                val controllerInQuo = Controllers.getController(i)

                println("Controller $i: ${controllerInQuo.name}")

                // check the name
                if (!controllerInQuo.name.toLowerCase().contains(notControllerRegex)) {
                    controller = controllerInQuo
                    println("Controller $i selected: ${controller!!.name}")
                    break
                }
            }
        }
        catch (e: IndexOutOfBoundsException) {
        }

        if (controller != null) {
            for (c in 0..controller!!.axisCount - 1) {
                controller!!.setDeadZone(c, CONTROLLER_DEADZONE)
            }
        }

        gc.graphics.clear() // clean up any 'dust' in the buffer

        //addState(StateVTTest())
        //addState(StateGraphicComputerTest())
        //addState(StateTestingLightning())
        //addState(StateSplash())
        //addState(StateMonitorCheck())
        //addState(StateFontTester())
        //addState(StateNoiseTexGen())
        //addState(StateBlurTest())
        //addState(StateShaderTest())
        //addState(StateNoiseTester())
        addState(StateUITest())

        //ingame = StateInGame()
        //addState(ingame)


        // foolproof
        if (stateCount < 1) {
            throw Error("Please add or un-comment addState statements")
        }
    }

    private fun getDefaultDirectory() {
        val OS = System.getProperty("os.name").toUpperCase()
        if (OS.contains("WIN")) {
            OperationSystem = "WINDOWS"
            defaultDir = System.getenv("APPDATA") + "/terrarum"
        }
        else if (OS.contains("OS X")) {
            OperationSystem = "OSX"
            defaultDir = System.getProperty("user.home") + "/Library/Application Support/terrarum"
        }
        else if (OS.contains("NUX") || OS.contains("NIX")) {
            OperationSystem = "LINUX"
            defaultDir = System.getProperty("user.home") + "/.terrarum"
        }
        else if (OS.contains("SUNOS")) {
            OperationSystem = "SOLARIS"
            defaultDir = System.getProperty("user.home") + "/.terrarum"
        }
        else {
            OperationSystem = "UNKNOWN"
            defaultDir = System.getProperty("user.home") + "/.terrarum"
        }

        defaultSaveDir = defaultDir + "/Saves"
        configDir = defaultDir + "/config.json"

        println("[Terrarum] os.name = $OSName")
        println("[Terrarum] os.version = $OSVersion")
    }

    private fun createDirs() {
        val dirs = arrayOf(File(defaultSaveDir))
        dirs.forEach { if (!it.exists()) it.mkdirs() }
    }

    @Throws(IOException::class)
    private fun createConfigJson() {
        val configFile = File(configDir)

        if (!configFile.exists() || configFile.length() == 0L) {
            JsonWriter.writeToFile(DefaultConfig.fetch(), configDir)
        }
    }

    private fun readConfigJson(): Boolean {
        try {
            // read from disk and build config from it
            val jsonObject = JsonFetcher(configDir)

            // make config
            jsonObject.entrySet().forEach { entry -> gameConfig[entry.key] = entry.value }

            return true
        }
        catch (e: IOException) {
            // write default config to game dir. Call this method again to read config from it.
            try {
                createConfigJson()
            }
            catch (e1: IOException) {
                e.printStackTrace()
            }

            return false
        }

    }

    /**
     * Return config from config set. If the config does not exist, default value will be returned.
     * @param key
     * *
     * @return Config from config set or default config if it does not exist.
     * *
     * @throws NullPointerException if the specified config simply does not exist.
     */
    fun getConfigInt(key: String): Int {
        val cfg = getConfigMaster(key)
        if (cfg is JsonPrimitive)
            return cfg.asInt
        else
            return cfg as Int
    }

    /**
     * Return config from config set. If the config does not exist, default value will be returned.
     * @param key
     * *
     * @return Config from config set or default config if it does not exist.
     * *
     * @throws NullPointerException if the specified config simply does not exist.
     */
    fun getConfigString(key: String): String {
        val cfg = getConfigMaster(key)
        if (cfg is JsonPrimitive)
            return cfg.asString
        else
            return cfg as String
    }

    /**
     * Return config from config set. If the config does not exist, default value will be returned.
     * @param key
     * *
     * @return Config from config set or default config if it does not exist.
     * *
     * @throws NullPointerException if the specified config simply does not exist.
     */
    fun getConfigBoolean(key: String): Boolean {
        val cfg = getConfigMaster(key)
        if (cfg is JsonPrimitive)
            return cfg.asBoolean
        else
            return cfg as Boolean
    }

    fun getConfigIntArray(key: String): IntArray {
        val cfg = getConfigMaster(key)
        if (cfg is JsonArray) {
            val jsonArray = cfg.asJsonArray
            return IntArray(jsonArray.size(), { i -> jsonArray[i].asInt })
        }
        else
            return cfg as IntArray
    }

    private fun getConfigMaster(key: String): Any {
        var cfg: Any? = null
        try {
            cfg = gameConfig[key.toLowerCase()]!!
        }
        catch (e: NullPointerException) {
            try {
                cfg = DefaultConfig.fetch()[key.toLowerCase()]
            }
            catch (e1: NullPointerException) {
                e.printStackTrace()
            }
        }
        return cfg!!
    }

    fun setConfig(key: String, value: Any) {
        gameConfig[key] = value
    }

    val currentSaveDir: File
        get() {
            val file = File(defaultSaveDir + "/test")

            // failsafe?
            if (!file.exists()) file.mkdir()

            return file // TODO TEST CODE
        }
}

fun main(args: Array<String>) {
    System.setProperty("java.library.path", "lib")
    System.setProperty("org.lwjgl.librarypath", File("lib").absolutePath)

    try {
        Terrarum.appgc = AppGameContainer(Terrarum)
        Terrarum.appgc.setDisplayMode(Terrarum.WIDTH, Terrarum.HEIGHT, false)

        Terrarum.appgc.setTargetFrameRate(Terrarum.TARGET_INTERNAL_FPS)
        Terrarum.appgc.setVSync(Terrarum.VSYNC)
        Terrarum.appgc.setMaximumLogicUpdateInterval(1000 / Terrarum.TARGET_INTERNAL_FPS) // 10 ms
        Terrarum.appgc.setMinimumLogicUpdateInterval(1000 / Terrarum.TARGET_INTERNAL_FPS - 1) // 9 ms

        Terrarum.appgc.setMultiSample(0)

        Terrarum.appgc.setShowFPS(false)

        // game will run normally even if it is not focused
        Terrarum.appgc.setUpdateOnlyWhenVisible(false)
        Terrarum.appgc.alwaysRender = true

        Terrarum.appgc.start()
    }
    catch (ex: Exception) {
        val logger = Logger.getLogger(Terrarum::class.java.name)
        val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH-mm-ss")
        val calendar = Calendar.getInstance()
        val filepath = "${Terrarum.defaultDir}/crashlog-${dateFormat.format(calendar.time)}.txt"
        val fileHandler = FileHandler(filepath)
        logger.addHandler(fileHandler)

        val formatter = SimpleFormatter()
        fileHandler.formatter = formatter

        //logger.info()
        println("The game has crashed!")
        println("Crash log were saved to $filepath.")
        println("================================================================================")
        logger.log(Level.SEVERE, null, ex)
    }
}

///////////////////////////////////
// customised blending functions //
///////////////////////////////////

fun blendMul() {
    // I must say: What the fuck is wrong with you, Slick2D? Your built-it blending is just fucking wrong.
    GL11.glEnable(GL11.GL_BLEND)
    GL11.glColorMask(true, true, true, true)
    GL11.glBlendFunc(GL11.GL_DST_COLOR, GL11.GL_ONE_MINUS_SRC_ALPHA)
}

fun blendNormal() {
    GL11.glEnable(GL11.GL_BLEND)
    GL11.glColorMask(true, true, true, true)
    //GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA)

    // semitransparent textures working as intended with this,
    // but needs further investigation in the case of:
    // TODO test blend in the situation of semitransparent over semitransparent
    GL14.glBlendFuncSeparate(
            GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA, // blend func for RGB channels
            GL11.GL_ONE, GL11.GL_ONE_MINUS_SRC_ALPHA // blend func for alpha channels
    )
}

fun blendLightenOnly() {
    GL11.glEnable(GL11.GL_BLEND)
    GL11.glColorMask(true, true, true, false)
    GL11.glBlendFunc(GL11.GL_ONE, GL11.GL_ONE)
    GL14.glBlendEquation(GL14.GL_MAX)
}

fun blendAlphaMap() {
    GL11.glDisable(GL11.GL_BLEND)
    GL11.glColorMask(false, false, false, true)
}

fun blendScreen() {
    GL11.glEnable(GL11.GL_BLEND)
    GL11.glColorMask(true, true, true, true)
    GL11.glBlendFunc(GL11.GL_ONE, GL11.GL_ONE_MINUS_SRC_COLOR)
}

fun blendDisable() {
    GL11.glDisable(GL11.GL_BLEND)
}

object BlendMode {
    const val SCREEN   = "GL_BLEND screen"
    const val MULTIPLY = "GL_BLEND multiply"
    const val NORMAL   = "GL_BLEND normal"
    const val MAX      = "GL_MAX"

    fun resolve(mode: String) {
        when (mode) {
            SCREEN   -> blendScreen()
            MULTIPLY -> blendMul()
            NORMAL   -> blendNormal()
            MAX      -> blendLightenOnly()
            else     -> throw Error("Unknown blend mode: $mode")
        }
    }
}

enum class RunningEnvironment {
    PC, CONSOLE, MOBILE
}

/** @return Intarray(R, G, B, A) */
fun Texture.getPixel(x: Int, y: Int): IntArray {
    val textureWidth = this.textureWidth
    val hasAlpha = this.hasAlpha()

    val offset = (if (hasAlpha) 4 else 3) * (textureWidth * y + x) // 4: # of channels (RGBA)

    if (ByteOrder.nativeOrder() == ByteOrder.LITTLE_ENDIAN) {
        return intArrayOf(
                this.textureData[offset].toUint(),
                this.textureData[offset + 1].toUint(),
                this.textureData[offset + 2].toUint(),
                if (hasAlpha)
                    this.textureData[offset + 3].toUint()
                else 255
        )
    }
    else {
        return intArrayOf(
                this.textureData[offset + 2].toUint(),
                this.textureData[offset + 1].toUint(),
                this.textureData[offset].toUint(),
                if (hasAlpha)
                    this.textureData[offset + 3].toUint()
                else 255
        )
    }
}

/** @return Intarray(R, G, B, A) */
fun Image.getPixel(x: Int, y: Int) = this.texture.getPixel(x, y)

fun Color.toInt() = redByte.shl(16) or greenByte.shl(8) or blueByte
fun Color.to10bit() = redByte.shl(20) or greenByte.shl(10) or blueByte

infix fun Color.screen(other: Color) = Color(
        1f - (1f - this.r) * (1f - other.r),
        1f - (1f - this.g) * (1f - other.g),
        1f - (1f - this.b) * (1f - other.b),
        1f - (1f - this.a) * (1f - other.a)
)

operator fun Color.times(other: Color) = Color(
        this.r * other.r,
        this.g * other.g,
        this.b * other.b,
        this.a * other.a
)
