package ani.dantotsu.parsers

import ani.dantotsu.FileUrl
import ani.dantotsu.R
import ani.dantotsu.currContext
import ani.dantotsu.logger
import ani.dantotsu.media.Media
import ani.dantotsu.settings.saving.PrefManager
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.source.model.SManga
import me.xdrop.fuzzywuzzy.FuzzySearch
import java.io.Serializable
import java.net.URLDecoder
import java.net.URLEncoder


abstract class BaseParser {

    /**
     * Name that will be shown in Source Selection
     * **/
    open val name: String = ""

    /**
     * Name used to save the ShowResponse selected by user or by autoSearch
     * **/
    open val saveName: String = ""

    /**
     * The main URL of the Site
     * **/
    open val hostUrl: String = ""

    /**
     * override as `true` if the site **only** has NSFW media
     * **/
    open val isNSFW = false

    /**
     * mostly redundant for official app, But override if you want to add different languages
     * **/
    open val language = "English"

    /**
     *  Search for Anime/Manga/Novel, returns a List of Responses
     *
     *  use `encode(query)` to encode the query for making requests
     * **/
    abstract suspend fun search(query: String): List<ShowResponse>

    /**
     * The function app uses to auto find the anime/manga using Media data provided by anilist
     *
     * Isn't necessary to override, but recommended, if you want to improve auto search results
     * **/
    open suspend fun autoSearch(mediaObj: Media): ShowResponse? {
        var response: ShowResponse? = loadSavedShowResponse(mediaObj.id)
        if (response != null && this !is OfflineMangaParser && this !is OfflineAnimeParser) {
            saveShowResponse(mediaObj.id, response, true)
        } else {
            setUserText("Searching : ${mediaObj.mainName()}")
            logger("Searching : ${mediaObj.mainName()}")
            val results = search(mediaObj.mainName())
            //log all results
            results.forEach {
                logger("Result: ${it.name}")
            }
            val sortedResults = if (results.isNotEmpty()) {
                results.sortedByDescending {
                    FuzzySearch.ratio(
                        it.name.lowercase(),
                        mediaObj.mainName().lowercase()
                    )
                }
            } else {
                emptyList()
            }
            response = sortedResults.firstOrNull()

            if (response == null || FuzzySearch.ratio(
                    response.name.lowercase(),
                    mediaObj.mainName().lowercase()
                ) < 100
            ) {
                setUserText("Searching : ${mediaObj.nameRomaji}")
                logger("Searching : ${mediaObj.nameRomaji}")
                val romajiResults = search(mediaObj.nameRomaji)
                val sortedRomajiResults = if (romajiResults.isNotEmpty()) {
                    romajiResults.sortedByDescending {
                        FuzzySearch.ratio(
                            it.name.lowercase(),
                            mediaObj.nameRomaji.lowercase()
                        )
                    }
                } else {
                    emptyList()
                }
                val closestRomaji = sortedRomajiResults.firstOrNull()
                logger("Closest match from RomajiResults: ${closestRomaji?.name ?: "None"}")

                response = if (response == null) {
                    logger("No exact match found in results. Using closest match from RomajiResults.")
                    closestRomaji
                } else {
                    val romajiRatio = FuzzySearch.ratio(
                        closestRomaji?.name?.lowercase() ?: "",
                        mediaObj.nameRomaji.lowercase()
                    )
                    val mainNameRatio = FuzzySearch.ratio(
                        response.name.lowercase(),
                        mediaObj.mainName().lowercase()
                    )
                    logger("Fuzzy ratio for closest match in results: $mainNameRatio for ${response.name.lowercase()}")
                    logger("Fuzzy ratio for closest match in RomajiResults: $romajiRatio for ${closestRomaji?.name?.lowercase() ?: "None"}")

                    if (romajiRatio > mainNameRatio) {
                        logger("RomajiResults has a closer match. Replacing response.")
                        closestRomaji
                    } else {
                        logger("Results has a closer or equal match. Keeping existing response.")
                        response
                    }
                }

            }
            saveShowResponse(mediaObj.id, response)
        }
        return response
    }


    /**
     * Used to get an existing Search Response which was selected by the user.
     * **/
    open suspend fun loadSavedShowResponse(mediaId: Int): ShowResponse? {
        checkIfVariablesAreEmpty()
        return PrefManager.getNullableCustomVal(
            "${saveName}_$mediaId",
            null,
            ShowResponse::class.java
        )
    }

    /**
     * Used to save Shows Response using `saveName`.
     * **/
    open fun saveShowResponse(mediaId: Int, response: ShowResponse?, selected: Boolean = false) {
        if (response != null) {
            checkIfVariablesAreEmpty()
            setUserText(
                "${
                    if (selected) currContext()!!.getString(R.string.selected) else currContext()!!.getString(
                        R.string.found
                    )
                } : ${response.name}"
            )
            PrefManager.setCustomVal("${saveName}_$mediaId", response)
        }
    }

    fun checkIfVariablesAreEmpty() {
        if (hostUrl.isEmpty()) throw UninitializedPropertyAccessException("Cannot find any installed extensions")
        if (name.isEmpty()) throw UninitializedPropertyAccessException("Cannot find any installed extensions")
        if (saveName.isEmpty()) throw UninitializedPropertyAccessException("Cannot find any installed extensions")
    }

    open var showUserText = ""
    open var showUserTextListener: ((String) -> Unit)? = null

    /**
     * Used to show messages & errors to the User, a useful way to convey what's currently happening or what was done.
     * **/
    fun setUserText(string: String) {
        showUserText = string
        showUserTextListener?.invoke(showUserText)
    }

    fun encode(input: String): String = URLEncoder.encode(input, "utf-8").replace("+", "%20")
    fun decode(input: String): String = URLDecoder.decode(input, "utf-8")

    val defaultImage = "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/default.jpg"
}


/**
 * A single show which contains some episodes/chapters which is sent by the site using their search function.
 *
 * You might wanna include `otherNames` & `total` too, to further improve user experience.
 *
 * You can also store a Map of Strings if you want to save some extra data.
 * **/
data class ShowResponse(
    val name: String,
    val link: String,
    val coverUrl: FileUrl,

    //would be Useful for custom search, ig
    val otherNames: List<String> = listOf(),

    //Total number of Episodes/Chapters in the show.
    val total: Int? = null,

    //In case you want to sent some extra data
    val extra: MutableMap<String, String>? = null,

    //SAnime object from Aniyomi
    val sAnime: SAnime? = null,

    //SManga object from Aniyomi
    val sManga: SManga? = null
) : Serializable {
    constructor(
        name: String,
        link: String,
        coverUrl: String,
        otherNames: List<String> = listOf(),
        total: Int? = null,
        extra: MutableMap<String, String>? = null
    )
            : this(name, link, FileUrl(coverUrl), otherNames, total, extra)

    constructor(
        name: String,
        link: String,
        coverUrl: String,
        otherNames: List<String> = listOf(),
        total: Int? = null
    )
            : this(name, link, FileUrl(coverUrl), otherNames, total)

    constructor(name: String, link: String, coverUrl: String, otherNames: List<String> = listOf())
            : this(name, link, FileUrl(coverUrl), otherNames)

    constructor(name: String, link: String, coverUrl: String)
            : this(name, link, FileUrl(coverUrl))

    constructor(name: String, link: String, coverUrl: String, sAnime: SAnime)
            : this(name, link, FileUrl(coverUrl), sAnime = sAnime)

    constructor(name: String, link: String, coverUrl: String, sManga: SManga)
            : this(name, link, FileUrl(coverUrl), sManga = sManga)
}


