package jp.juggler.subwaytooter.api.entity

import android.content.Context
import android.text.Spannable
import android.text.SpannableString
import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.api.TootParser
import jp.juggler.subwaytooter.util.DecodeOptions
import jp.juggler.util.*

enum class TootPollsType {
    Mastodon, // Mastodon 2.8
    Misskey, // Misskey
    FriendsNico, // friends.nico API
    Notestock, // notestock
}

class TootPollsChoice(
    val text: String,
    val decoded_text: Spannable,
    var isVoted: Boolean = false, // misskey
    var votes: Int? = 0, // misskey
    var checked: Boolean = false, // Mastodon
)

class TootPolls(
    parser: TootParser,
    val pollType: TootPollsType,
    status: TootStatus,
    list_attachment: ArrayList<TootAttachmentLike>?,
    src: JsonObject,
    srcArray: JsonArray? = null,
) {

    // one of enquete,enquete_result
    val type: String?

    val question: String? // HTML text

    val decoded_question: Spannable // 表示用にデコードしてしまうのでNonNullになる

    // array of text with emoji
    val items: ArrayList<TootPollsChoice>?

    // 結果の数値 // null or array of number
    var ratios: MutableList<Float>? = null

    // 結果の数値のテキスト // null or array of string
    private var ratios_text: MutableList<String>? = null

    // 以下はJSONには存在しないが内部で使う
    val time_start: Long
    val status_id: EntityId

    // Mastodon poll API
    var expired_at = Long.MAX_VALUE
    var expired = false
    var multiple = false
    var votes_count: Int? = null
    var maxVotesCount: Int? = null
    var pollId: EntityId? = null

    var ownVoted: Boolean

    init {

        this.time_start = status.time_created_at
        this.status_id = status.id

        when (pollType) {
            TootPollsType.Misskey -> {

                this.items = parseChoiceListMisskey(

                    src.jsonArray("choices")
                )

                val votesList = ArrayList<Int>()
                var votesMax = 1
                var ownVoted = false
                items?.forEach { choice ->
                    if (choice.isVoted) ownVoted = true
                    val votes = choice.votes ?: 0
                    votesList.add(votes)
                    if (votes > votesMax) votesMax = votes
                }
                this.ownVoted = ownVoted

                if (votesList.isNotEmpty()) {
                    this.ratios =
                        votesList.map { (it.toFloat() / votesMax.toFloat()) }.toMutableList()
                    this.ratios_text =
                        votesList.map { parser.context.getString(R.string.vote_count_text, it) }
                            .toMutableList()
                } else {
                    this.ratios = null
                    this.ratios_text = null
                }

                this.type = TYPE_ENQUETE

                this.question = status.content
                this.decoded_question = DecodeOptions(
                    parser.context,
                    parser.linkHelper,
                    short = true,
                    decodeEmoji = true,
                    attachmentList = list_attachment,
                    linkTag = status,
                    emojiMapCustom = status.custom_emojis,
                    emojiMapProfile = status.profile_emojis,
                    mentions = status.mentions,
                    mentionDefaultHostDomain = status.account
                ).decodeHTML(this.question ?: "?")
            }

            TootPollsType.Mastodon -> {
                this.type = "enquete"

                this.question = status.content
                this.decoded_question = DecodeOptions(
                    parser.context,
                    parser.linkHelper,
                    short = true,
                    decodeEmoji = true,
                    attachmentList = list_attachment,
                    linkTag = status,
                    emojiMapCustom = status.custom_emojis,
                    emojiMapProfile = status.profile_emojis,
                    mentions = status.mentions,
                    mentionDefaultHostDomain = status.account
                ).decodeHTML(this.question ?: "?")

                this.items = parseChoiceListMastodon(
                    parser.context,
                    status,
                    src.jsonArray("options")?.objectList()
                )

                this.pollId = EntityId.mayNull(src.string("id"))
                this.expired_at =
                    TootStatus.parseTime(src.string("expires_at")).notZero() ?: Long.MAX_VALUE
                this.expired = src.optBoolean("expired", false)
                this.multiple = src.optBoolean("multiple", false)
                this.votes_count = src.int("votes_count")

                var ownVoted = src.optBoolean("voted", false)

                src.jsonArray("own_votes")?.forEach {
                    if (it is Number) {
                        val i = it.toInt()
                        items?.get(i)?.isVoted = true
                        ownVoted = true
                    }
                }

                this.ownVoted = ownVoted

                when {
                    this.items == null -> maxVotesCount = null

                    this.multiple -> {
                        var max: Int? = null
                        for (item in items) {
                            val v = item.votes
                            if (v != null && (max == null || v > max)) max = v
                        }
                        maxVotesCount = max
                    }

                    else -> {
                        var sum: Int? = null
                        for (item in items) {
                            val v = item.votes
                            if (v != null) sum = (sum ?: 0) + v
                        }
                        maxVotesCount = sum
                    }
                }
            }

            TootPollsType.FriendsNico -> {
                this.type = src.string("type")

                this.question = src.string("question")
                this.decoded_question = DecodeOptions(
                    parser.context,
                    parser.linkHelper,
                    short = true,
                    decodeEmoji = true,
                    attachmentList = list_attachment,
                    linkTag = status,
                    emojiMapCustom = status.custom_emojis,
                    emojiMapProfile = status.profile_emojis,
                    mentions = status.mentions,
                    mentionDefaultHostDomain = status.account
                ).decodeHTML(this.question ?: "?")

                this.items = parseChoiceListFriendsNico(
                    parser.context,
                    status,
                    src.stringArrayList("items")
                )

                this.ratios = src.floatArrayList("ratios")
                this.ratios_text = src.stringArrayList("ratios_text")

                this.ownVoted = false
            }

            TootPollsType.Notestock -> {
                this.type = "enquete"

                this.question = status.content
                this.decoded_question = DecodeOptions(
                    parser.context,
                    parser.linkHelper,
                    short = true,
                    decodeEmoji = true,
                    attachmentList = list_attachment,
                    linkTag = status,
                    emojiMapCustom = status.custom_emojis,
                    emojiMapProfile = status.profile_emojis,
                    mentions = status.mentions,
                    mentionDefaultHostDomain = status.account,
                    unwrapEmojiImageTag = true, // notestockはカスタム絵文字がimageタグになってる
                ).decodeHTML(this.question ?: "?")

                this.items = parseChoiceListNotestock(
                    parser.context,
                    status,
                    srcArray?.objectList()
                )

                this.pollId = EntityId.DEFAULT
                this.expired_at =
                    TootStatus.parseTime(src.string("endTime")).notZero() ?: Long.MAX_VALUE
                this.expired = expired_at >= System.currentTimeMillis()
                this.multiple = src.containsKey("anyOf")
                this.votes_count = items?.sumOf { it.votes ?: 0 }?.notZero()

                this.ownVoted = false

                when {
                    this.items == null -> maxVotesCount = null

                    this.multiple -> {
                        var max: Int? = null
                        for (item in items) {
                            val v = item.votes
                            if (v != null && (max == null || v > max)) max = v
                        }
                        maxVotesCount = max
                    }

                    else -> {
                        var sum: Int? = null
                        for (item in items) {
                            val v = item.votes
                            if (v != null) sum = (sum ?: 0) + v
                        }
                        maxVotesCount = sum
                    }
                }
            }
        }
    }

    companion object {

        internal val log = LogCategory("TootPolls")

        const val ENQUETE_EXPIRE = 30000L

        const val TYPE_ENQUETE = "enquete"

        @Suppress("unused")
        const val TYPE_ENQUETE_RESULT = "enquete_result"

        @Suppress("HasPlatformType")
        private val reWhitespace = """[\s\t\x0d\x0a]+""".asciiPattern()

        fun parse(
            parser: TootParser,
            pollType: TootPollsType,
            status: TootStatus,
            listAttachment: ArrayList<TootAttachmentLike>?,
            src: JsonObject?,
        ): TootPolls? {
            src ?: return null
            return try {
                TootPolls(
                    parser,
                    pollType,
                    status,
                    listAttachment,
                    src
                )
            } catch (ex: Throwable) {
                log.trace(ex)
                null
            }
        }

        private fun parseChoiceListMastodon(
            context: Context,
            status: TootStatus,
            objectArray: List<JsonObject>?,
        ): ArrayList<TootPollsChoice>? {
            if (objectArray != null) {
                val size = objectArray.size
                val items = ArrayList<TootPollsChoice>(size)
                val options = DecodeOptions(
                    context,
                    emojiMapCustom = status.custom_emojis,
                    emojiMapProfile = status.profile_emojis,
                    decodeEmoji = true,
                    mentionDefaultHostDomain = status.account
                )
                for (o in objectArray) {
                    val text = reWhitespace
                        .matcher((o.string("title") ?: "?").sanitizeBDI())
                        .replaceAll(" ")
                    val decoded_text = options.decodeEmoji(text)

                    items.add(
                        TootPollsChoice(
                            text,
                            decoded_text,
                            votes = o.int("votes_count") // may null
                        )
                    )
                }
                if (items.isNotEmpty()) return items
            }
            return null
        }

        private fun parseChoiceListNotestock(
            context: Context,
            status: TootStatus,
            objectArray: List<JsonObject>?,
        ): ArrayList<TootPollsChoice>? {
            if (objectArray != null) {
                val size = objectArray.size
                val items = ArrayList<TootPollsChoice>(size)
                val options = DecodeOptions(
                    context,
                    emojiMapCustom = status.custom_emojis,
                    emojiMapProfile = status.profile_emojis,
                    decodeEmoji = true,
                    mentionDefaultHostDomain = status.account
                )
                for (o in objectArray) {
                    val text = reWhitespace
                        .matcher((o.string("name") ?: "?").sanitizeBDI())
                        .replaceAll(" ")
                    val decoded_text = options.decodeEmoji(text)

                    items.add(
                        TootPollsChoice(
                            text,
                            decoded_text,
                            votes = o.jsonObject("replies")?.int("totalItems") // may null
                        )
                    )
                }
                if (items.isNotEmpty()) return items
            }
            return null
        }

        private fun parseChoiceListFriendsNico(
            context: Context,
            status: TootStatus,
            stringArray: ArrayList<String>?,
        ): ArrayList<TootPollsChoice>? {
            if (stringArray != null) {
                val size = stringArray.size
                val items = ArrayList<TootPollsChoice>(size)
                val options = DecodeOptions(
                    context,
                    emojiMapCustom = status.custom_emojis,
                    emojiMapProfile = status.profile_emojis,
                    decodeEmoji = true,
                    mentionDefaultHostDomain = status.account
                )
                for (i in 0 until size) {
                    val text = reWhitespace
                        .matcher(stringArray[i].sanitizeBDI())
                        .replaceAll(" ")
                    val decoded_text = options.decodeHTML(text)

                    items.add(
                        TootPollsChoice(
                            text,
                            decoded_text
                        )
                    )
                }
                if (items.isNotEmpty()) return items
            }
            return null
        }

        private fun parseChoiceListMisskey(
            choices: JsonArray?,
        ): ArrayList<TootPollsChoice>? {
            if (choices != null) {
                val items = ArrayList<TootPollsChoice>()
                choices.forEach {
                    it.cast<JsonObject>()?.let { src ->
                        val text = reWhitespace
                            .matcher(src.string("text")?.sanitizeBDI() ?: "")
                            .replaceAll(" ")
                        val decoded_text = SpannableString(text) // misskey ではマークダウン不可で絵文字もない

                        val dst = TootPollsChoice(
                            text = text,
                            decoded_text = decoded_text,
                            // 配列インデクスと同じだった id = EntityId.mayNull(src.long("id")),
                            votes = src.int("votes") ?: 0,
                            isVoted = src.optBoolean("isVoted")
                        )
                        items.add(dst)
                    }
                }

                if (items.isNotEmpty()) return items
            }
            return null
        }
    }

    // misskey用
    fun increaseVote(context: Context, argChoice: Int?, isMyVoted: Boolean): Boolean {
        argChoice ?: return false

        synchronized(this) {
            try {
                // 既に投票済み状態なら何もしない
                if (ownVoted) return false

                val item = this.items?.get(argChoice) ?: return false
                item.votes = (item.votes ?: 0) + 1
                if (isMyVoted) item.isVoted = true

                // update ratios
                val votesList = ArrayList<Int>()
                var votesMax = 1
                items.forEach { choice ->
                    if (choice.isVoted) ownVoted = true
                    val votes = choice.votes ?: 0
                    votesList.add(votes)
                    if (votes > votesMax) votesMax = votes
                }

                if (votesList.isNotEmpty()) {

                    this.ratios = votesList.asSequence()
                        .map { (it.toFloat() / votesMax.toFloat()) }
                        .toMutableList()

                    this.ratios_text = votesList.asSequence()
                        .map { context.getString(R.string.vote_count_text, it) }
                        .toMutableList()
                } else {
                    this.ratios = null
                    this.ratios_text = null
                }

                return true
            } catch (ex: Throwable) {
                log.e(ex, "increaseVote failed")
                return false
            }
        }
    }
}
