package ani.dantotsu.settings.saving

import android.content.Context
import android.content.SharedPreferences
import android.util.Base64
import ani.dantotsu.settings.saving.internal.Compat
import ani.dantotsu.settings.saving.internal.Location
import ani.dantotsu.settings.saving.internal.PreferencePackager
import ani.dantotsu.snackString
import ani.dantotsu.util.Logger
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.ObjectInputStream
import java.io.ObjectOutputStream

object PrefManager {

    private var generalPreferences: SharedPreferences? = null
    private var uiPreferences: SharedPreferences? = null
    private var playerPreferences: SharedPreferences? = null
    private var readerPreferences: SharedPreferences? = null
    private var irrelevantPreferences: SharedPreferences? = null
    private var animeDownloadsPreferences: SharedPreferences? = null
    private var protectedPreferences: SharedPreferences? = null

    fun init(context: Context) {  //must be called in Application class or will crash
        if (generalPreferences != null) return
        generalPreferences =
            context.getSharedPreferences(Location.General.location, Context.MODE_PRIVATE)
        uiPreferences =
            context.getSharedPreferences(Location.UI.location, Context.MODE_PRIVATE)
        playerPreferences =
            context.getSharedPreferences(Location.Player.location, Context.MODE_PRIVATE)
        readerPreferences =
            context.getSharedPreferences(Location.Reader.location, Context.MODE_PRIVATE)
        irrelevantPreferences =
            context.getSharedPreferences(Location.Irrelevant.location, Context.MODE_PRIVATE)
        animeDownloadsPreferences =
            context.getSharedPreferences(Location.AnimeDownloads.location, Context.MODE_PRIVATE)
        protectedPreferences =
            context.getSharedPreferences(Location.Protected.location, Context.MODE_PRIVATE)
        Compat.importOldPrefs(context)
    }

    @Suppress("UNCHECKED_CAST")
    fun <T> setVal(prefName: PrefName, value: T?) {
        val pref = getPrefLocation(prefName.data.prefLocation)
        with(pref.edit()) {
            when (value) {
                is Boolean -> putBoolean(prefName.name, value)
                is Int -> putInt(prefName.name, value)
                is Float -> putFloat(prefName.name, value)
                is Long -> putLong(prefName.name, value)
                is String -> putString(prefName.name, value)
                is Set<*> -> putStringSet(prefName.name, value as Set<String>)
                null -> remove(prefName.name)
                else -> serializeClass(prefName.name, value, prefName.data.prefLocation)
            }
            apply()
        }
    }

    @Suppress("UNCHECKED_CAST")
    fun <T> getVal(prefName: PrefName, default: T): T {
        return try {
            val pref = getPrefLocation(prefName.data.prefLocation)
            when (prefName.data.type) {
                Boolean::class -> pref.getBoolean(prefName.name, default as Boolean) as T
                Int::class -> pref.getInt(prefName.name, default as Int) as T
                Float::class -> pref.getFloat(prefName.name, default as Float) as T
                Long::class -> pref.getLong(prefName.name, default as Long) as T
                String::class -> pref.getString(prefName.name, default as String?) as T
                Set::class -> pref.getStringSet(prefName.name, default as Set<String>) as T

                List::class -> deserializeClass(
                    prefName.name,
                    default,
                    prefName.data.prefLocation
                ) as T

                else -> throw IllegalArgumentException("Type not supported")
            }
        } catch (e: Exception) {
            default
        }
    }

    @Suppress("UNCHECKED_CAST")
    fun <T> getVal(prefName: PrefName): T {
        return try {
            val pref = getPrefLocation(prefName.data.prefLocation)
            when (prefName.data.type) {
                Boolean::class -> pref.getBoolean(
                    prefName.name,
                    prefName.data.default as Boolean
                ) as T

                Int::class -> pref.getInt(prefName.name, prefName.data.default as Int) as T
                Float::class -> pref.getFloat(prefName.name, prefName.data.default as Float) as T
                Long::class -> pref.getLong(prefName.name, prefName.data.default as Long) as T
                String::class -> pref.getString(
                    prefName.name,
                    prefName.data.default as String?
                ) as T

                Set::class -> pref.getStringSet(
                    prefName.name,
                    prefName.data.default as Set<String>
                ) as T

                List::class -> deserializeClass(
                    prefName.name,
                    prefName.data.default,
                    prefName.data.prefLocation
                ) as T

                else -> throw IllegalArgumentException("Type not supported")
            }
        } catch (e: Exception) {
            prefName.data.default as T
        }
    }

    @Suppress("UNCHECKED_CAST")
    fun <T> getNullableVal(
        prefName: PrefName,
        default: T?
    ): T? {  //Strings don't necessarily need to use this one
        return try {
            val pref = getPrefLocation(prefName.data.prefLocation)
            when (prefName.data.type) {
                Boolean::class -> pref.getBoolean(prefName.name, default as Boolean) as T?
                Int::class -> pref.getInt(prefName.name, default as Int) as T?
                Float::class -> pref.getFloat(prefName.name, default as Float) as T?
                Long::class -> pref.getLong(prefName.name, default as Long) as T?
                String::class -> pref.getString(prefName.name, default as String?) as T?
                Set::class -> pref.getStringSet(prefName.name, default as Set<String>) as T?

                else -> deserializeClass(prefName.name, default, prefName.data.prefLocation)
            }
        } catch (e: Exception) {
            default
        }
    }

    @Suppress("UNCHECKED_CAST")
    fun <T> getCustomVal(key: String, default: T): T {
        return try {
            when (default) {
                is Boolean -> irrelevantPreferences!!.getBoolean(key, default) as T
                is Int -> irrelevantPreferences!!.getInt(key, default) as T
                is Float -> irrelevantPreferences!!.getFloat(key, default) as T
                is Long -> irrelevantPreferences!!.getLong(key, default) as T
                is String -> irrelevantPreferences!!.getString(key, default) as T
                is Set<*> -> irrelevantPreferences!!.getStringSet(key, default as Set<String>) as T
                else -> throw IllegalArgumentException("Type not supported")
            }
        } catch (e: Exception) {
            default
        }
    }

    @Suppress("UNCHECKED_CAST")
    fun <T> getNullableCustomVal(key: String, default: T?, clazz: Class<T>): T? {
        return try {
            when {
                clazz.isAssignableFrom(Boolean::class.java) -> irrelevantPreferences!!.getBoolean(
                    key,
                    default as? Boolean ?: false
                ) as T?

                clazz.isAssignableFrom(Int::class.java) -> irrelevantPreferences!!.getInt(
                    key,
                    default as? Int ?: 0
                ) as T?

                clazz.isAssignableFrom(Float::class.java) -> irrelevantPreferences!!.getFloat(
                    key,
                    default as? Float ?: 0f
                ) as T?

                clazz.isAssignableFrom(Long::class.java) -> irrelevantPreferences!!.getLong(
                    key,
                    default as? Long ?: 0L
                ) as T?

                clazz.isAssignableFrom(String::class.java) -> irrelevantPreferences!!.getString(
                    key,
                    default as? String
                ) as T?

                clazz.isAssignableFrom(Set::class.java) -> irrelevantPreferences!!.getStringSet(
                    key,
                    default as? Set<String> ?: setOf()
                ) as T?

                else -> deserializeClass(key, default, Location.Irrelevant)
            }
        } catch (e: Exception) {
            default
        }
    }


    fun removeVal(prefName: PrefName) {
        val pref = getPrefLocation(prefName.data.prefLocation)
        with(pref.edit()) {
            remove(prefName.name)
            apply()
        }
    }

    @Suppress("UNCHECKED_CAST")
    fun <T> setCustomVal(key: String, value: T?) {
        //for custom force irrelevant
        with(irrelevantPreferences!!.edit()) {
            when (value) {
                is Boolean -> putBoolean(key, value as Boolean)
                is Int -> putInt(key, value as Int)
                is Float -> putFloat(key, value as Float)
                is Long -> putLong(key, value as Long)
                is String -> putString(key, value as String)
                is Set<*> -> putStringSet(key, value as Set<String>)
                null -> remove(key)
                else -> serializeClass(key, value, Location.Irrelevant)
            }
            apply()
        }
    }

    fun removeCustomVal(key: String) {
        //for custom force irrelevant
        with(irrelevantPreferences!!.edit()) {
            remove(key)
            apply()
        }
    }

    @Suppress("UNCHECKED_CAST")
    fun <T> getLiveVal(prefName: PrefName, default: T): SharedPreferenceLiveData<T> {
        val pref = getPrefLocation(prefName.data.prefLocation)
        return when (prefName.data.type) {
            Boolean::class -> SharedPreferenceBooleanLiveData(
                pref,
                prefName.name,
                default as Boolean
            ) as SharedPreferenceLiveData<T>

            Int::class -> SharedPreferenceIntLiveData(
                pref,
                prefName.name,
                default as Int
            ) as SharedPreferenceLiveData<T>

            Float::class -> SharedPreferenceFloatLiveData(
                pref,
                prefName.name,
                default as Float
            ) as SharedPreferenceLiveData<T>

            Long::class -> SharedPreferenceLongLiveData(
                pref,
                prefName.name,
                default as Long
            ) as SharedPreferenceLiveData<T>

            String::class -> SharedPreferenceStringLiveData(
                pref,
                prefName.name,
                default as String
            ) as SharedPreferenceLiveData<T>

            Set::class -> SharedPreferenceStringSetLiveData(
                pref,
                prefName.name,
                default as Set<String>
            ) as SharedPreferenceLiveData<T>

            else -> throw IllegalArgumentException("Type not supported")
        }
    }

    fun SharedPreferenceLiveData<*>.asLiveBool(): SharedPreferenceBooleanLiveData =
        this as? SharedPreferenceBooleanLiveData
            ?: throw ClassCastException("Cannot cast to SharedPreferenceLiveData<Boolean>")

    fun SharedPreferenceLiveData<*>.asLiveInt(): SharedPreferenceIntLiveData =
        this as? SharedPreferenceIntLiveData
            ?: throw ClassCastException("Cannot cast to SharedPreferenceLiveData<Int>")

    fun SharedPreferenceLiveData<*>.asLiveFloat(): SharedPreferenceFloatLiveData =
        this as? SharedPreferenceFloatLiveData
            ?: throw ClassCastException("Cannot cast to SharedPreferenceLiveData<Float>")

    fun SharedPreferenceLiveData<*>.asLiveLong(): SharedPreferenceLongLiveData =
        this as? SharedPreferenceLongLiveData
            ?: throw ClassCastException("Cannot cast to SharedPreferenceLiveData<Long>")

    fun SharedPreferenceLiveData<*>.asLiveString(): SharedPreferenceStringLiveData =
        this as? SharedPreferenceStringLiveData
            ?: throw ClassCastException("Cannot cast to SharedPreferenceLiveData<String>")

    fun SharedPreferenceLiveData<*>.asLiveStringSet(): SharedPreferenceStringSetLiveData =
        this as? SharedPreferenceStringSetLiveData
            ?: throw ClassCastException("Cannot cast to SharedPreferenceLiveData<Set<String>>")

    fun getAnimeDownloadPreferences(): SharedPreferences =
        animeDownloadsPreferences!!  //needs to be used externally

    fun exportAllPrefs(prefLocation: List<Location>): String {
        return PreferencePackager.pack(
            prefLocation.associateWith { getPrefLocation(it) }
        )
    }


    /**
     * @param prefs Map of preferences to import
     * @param prefLocation Location to import to
     * @return true if successful, false if error
     */

    @Suppress("UNCHECKED_CAST")
    fun importAllPrefs(prefs: Map<String, *>, prefLocation: Location): Boolean {
        val pref = getPrefLocation(prefLocation)
        var hadError = false
        pref.edit().clear().apply()
        with(pref.edit()) {
            prefs.forEach { (key, value) ->
                when (value) {
                    is Boolean -> putBoolean(key, value)
                    is Int -> putInt(key, value)
                    is Float -> putFloat(key, value)
                    is Long -> putLong(key, value)
                    is String -> putString(key, value)
                    is HashSet<*> -> putStringSet(key, value as Set<String>)
                    is ArrayList<*> -> putStringSet(key, arrayListToSet(value))
                    is Set<*> -> putStringSet(key, value as Set<String>)
                    else -> hadError = true
                }
            }
            apply()
            return if (hadError) {
                snackString("Error importing preferences")
                false
            } else {
                snackString("Preferences imported")
                true
            }
        }
    }

    private fun arrayListToSet(arrayList: ArrayList<*>): Set<String> {
        return arrayList.map { it.toString() }.toSet()
    }

    private fun getPrefLocation(prefLoc: Location): SharedPreferences {
        return when (prefLoc) {
            Location.General -> generalPreferences
            Location.UI -> uiPreferences
            Location.Player -> playerPreferences
            Location.Reader -> readerPreferences
            Location.NovelReader -> readerPreferences
            Location.Irrelevant -> irrelevantPreferences
            Location.AnimeDownloads -> animeDownloadsPreferences
            Location.Protected -> protectedPreferences
        }!!
    }

    private fun <T> serializeClass(key: String, value: T, location: Location) {
        val pref = getPrefLocation(location)
        try {
            val bos = ByteArrayOutputStream()
            ObjectOutputStream(bos).use { oos ->
                oos.writeObject(value)
            }

            val serialized = Base64.encodeToString(bos.toByteArray(), Base64.DEFAULT)
            pref.edit().putString(key, serialized).apply()
        } catch (e: Exception) {
            snackString("Error serializing preference: ${e.message}")
            Logger.log(e)
        }
    }

    @Suppress("UNCHECKED_CAST")
    private fun <T> deserializeClass(key: String, default: T?, location: Location): T? {
        return try {
            val pref = getPrefLocation(location)
            val serialized = pref.getString(key, null)
            if (serialized != null) {
                val data = Base64.decode(serialized, Base64.DEFAULT)
                val bis = ByteArrayInputStream(data)
                val ois = ObjectInputStream(bis)
                val obj = ois.readObject() as T?
                obj
            } else {
                default
            }
        } catch (e: Exception) {
            Logger.log(e)
            default
        }
    }
}