package core

import core.api.PersistWorld
import core.api.getItemName
import core.api.log
import core.game.node.entity.npc.NPC
import core.game.node.entity.player.Player
import core.tools.Log
import org.json.simple.JSONArray
import org.json.simple.JSONObject
import org.json.simple.parser.JSONParser
import core.tools.SystemLogger
import core.tools.SystemLogger.logShutdown
import core.tools.SystemLogger.logStartup
import java.io.File
import java.io.FileReader
import java.io.FileWriter
import javax.script.ScriptEngineManager

class ServerStore : PersistWorld {
    override fun parse() {
        logStartup("Parsing server store...")
        val dir = File(ServerConstants.STORE_PATH!!)
        if(!dir.exists()){
            dir.mkdirs()
            return
        }

        var parser: JSONParser
        var reader: FileReader

        dir.listFiles()?.forEach { storeFile ->
            val key = storeFile.nameWithoutExtension

            reader = FileReader(storeFile)
            parser = JSONParser()

            try {
                val data = parser.parse(reader) as JSONObject
                fileMap[key] = data
                counter++
            } catch (e: Exception){
                log(this::class.java, Log.ERR,  "Failed parsing ${storeFile.name} - stack trace below.")
                e.printStackTrace()
                return@forEach
            }
        }

        logStartup("Initialized $counter store files.")
    }

    override fun save() {
        logShutdown("Saving server store...")
        val dir = File(ServerConstants.DATA_PATH + File.separator + "serverstore")
        if(!dir.exists()){
            dir.mkdirs()
            return
        }

        val manager = ScriptEngineManager()
        val scriptEngine = manager.getEngineByName("JavaScript")

        fileMap.forEach { (name, data) ->
            val path = dir.absolutePath + File.separator + name + ".json"

            scriptEngine.put("jsonString", data.toJSONString())
            scriptEngine.eval("result = JSON.stringify(JSON.parse(jsonString), null, 2)")
            val prettyPrintedJson = scriptEngine["result"] as String

            FileWriter(path).use { it.write(prettyPrintedJson); it.flush(); it.close() }
        }
    }

    companion object {
        val fileMap = HashMap<String,JSONObject>()
        var counter = 0

        @JvmStatic
        fun getArchive(name: String): JSONObject {
            if(fileMap[name] == null){
                fileMap[name] = JSONObject()
            }

            return fileMap[name]!!
        }

        fun setArchive(name: String, data: JSONObject){
            fileMap[name] = data
        }

        fun clearDailyEntries() {
            fileMap.keys.toTypedArray().forEach {
                if(it.toLowerCase().contains("daily")) fileMap[it]?.clear()
            }
        }

        fun clearWeeklyEntries() {
            fileMap.keys.toTypedArray().forEach {
                if(it.toLowerCase().contains("weekly")) fileMap[it]?.clear()
            }
        }

        @JvmStatic
        fun JSONObject.getInt(key: String, default: Int = 0): Int {
            return when(val value = this[key]){
                is Long -> value.toInt()
                is Double -> value.toInt()
                is Float -> value.toInt()
                is Int -> value
                is Nothing -> default
                else -> default
            }
        }

        @JvmStatic
        fun JSONObject.getString(key: String, default: String = "nothing"): String {
            return this[key] as? String ?: default
        }

        @JvmStatic
        fun JSONObject.getLong(key: String, default: Long = 0L): Long {
            return this[key] as? Long ?: default
        }

        @JvmStatic
        fun JSONObject.getBoolean(key: String, default: Boolean = false): Boolean {
            return this[key] as? Boolean ?: default
        }

        fun List<Int>.toJSONArray(): JSONArray{
            val jArray = JSONArray()
            for(i in this){
                jArray.add(i)
            }
            return jArray
        }

        inline fun <reified T> JSONObject.getList(key: String) : List<T> {
            val array = this[key] as? JSONArray ?: JSONArray()
            val list = ArrayList<T>()
            for(element in array) list.add(element as T)
            return list
        }

        fun JSONObject.addToList(key: String, value: Any) {
            val array = this.getOrPut(key) {JSONArray()} as JSONArray
            array.add(value)
        }

        /** NPCItemMemory
         * These next methods handle the NPCItemMemory database. these are server-stored JSON objects,
         * which are based on an npc and an item.
         * each NPC can have a JSON object for itself for any item.
         * the NPC Item object can store integer values for individual players, using their names as keys.
         * the first methods handle the serverstore filename and accessing the JSON object.
         * these methods are wrapped by more convenient ones that allow access for a particular player, see below.
         */
        fun NPCItemFilename(npc: Int, item: Int, period: String = "daily"): String {
            val itemName = getItemName(item).lowercase().replace(" ","-")
            val npcName = NPC(npc).name.lowercase()
            return "$period-$npcName-$itemName"
        }
        fun NPCItemMemory(npc: Int, item: Int, period: String = "daily"): JSONObject {
            return getArchive(NPCItemFilename(npc,item,period))
        }

        /** NPCItemMemory Player Access
         * These next functions handle individual player access to the NPC Item Memory database.
         * each of the functions has at least 3 mandatory arguments: npc, item, player.
         * These functions can be used to allow players to access a daily- or weekly-reset stock of a particular item overseen by a particular NPC.
         *
         * note that none of these functions perform tangible actions on the player.
         * whoever calls these functions is responsible for handling tangible actions like putting items in an inventory.
         * these functions simply return and update the NPCItemMemory database numbers in useful ways.
         */

        /** getNPCItemStock:
         * gets the available stock of a particular item at a particular NPC for a particular player.
         */
        fun getNPCItemStock(npc: Int, item: Int, limit: Int, player: Player, period: String = "daily"): Int {
            val itemMemory = NPCItemMemory(npc,item)
            val key = player.name
            var stock = limit-itemMemory.getInt(key)
            stock = maxOf(stock,0)
            return stock
        }
        /** getNPCItemAmount:
         * gets the usable amount of a particular item from a particular npc for a particular player.
         * this is essentially just a requested amount cut to conform to stock and nonnegative requirements.
         * what this function does is it takes a requested amount, cuts it down to be equal to available stock if
         * available stock is less than the requested amount, and then additionally sets a minimum of 0.
         */
        fun getNPCItemAmount(npc: Int, item: Int, limit: Int, player: Player, amount: Int, period: String = "daily"): Int {
            val stock = getNPCItemStock(npc,item,limit,player,period)
            var realamount = minOf(amount,stock)
            realamount = maxOf(realamount,0)
            return realamount
        }
        /** addNPCItemAmount:
         * this function updates the NPC Item Memory database entry for a particular player.
         * it is intended to be called after all the actions that use the number have been successfully completed.
         * this function will *add* the amount argument to the current npc item memory entry for that player. this means that
         * the amount argument is the amount to add, not the absolute final amount.
         * this function will handle correctly setting the limit and any other required semantics.
         */
        fun addNPCItemAmount(npc: Int, item: Int, limit: Int, player: Player, amount: Int, period: String =  "daily") {
            val itemMemory = NPCItemMemory(npc,item,period)
            val key = player.name
            var realamount = itemMemory.getInt(key) + amount
            realamount = minOf(realamount,limit)
            realamount = maxOf(realamount,0)
            itemMemory[key] = realamount
        }
    }
}

