package jp.juggler.subwaytooter.column

import android.content.Context
import android.os.Environment
import androidx.annotation.RawRes
import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.api.TootApiClient
import jp.juggler.subwaytooter.api.TootApiResult
import jp.juggler.subwaytooter.api.TootParser
import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.api.entity.TootAccountRef.Companion.tootAccountRefOrNull
import jp.juggler.subwaytooter.columnviewholder.saveScrollPosition
import jp.juggler.util.data.*
import jp.juggler.util.log.LogCategory
import jp.juggler.util.network.toPostRequestBuilder
import java.io.File

private val log = LogCategory("ColumnExtra2")

val Column.isMastodon: Boolean
    get() = accessInfo.isMastodon

val Column.isMisskey: Boolean
    get() = accessInfo.isMisskey

val Column.misskeyVersion: Int
    get() = accessInfo.misskeyVersion

val Column.isSearchColumn: Boolean
    get() {
        return when (type) {
            ColumnType.SEARCH,
            ColumnType.SEARCH_MSP,
            ColumnType.SEARCH_TS,
            ColumnType.SEARCH_NOTESTOCK,
            -> true
            else -> false
        }
    }

val Column.isNotificationColumn: Boolean
    get() = when (type) {
        ColumnType.NOTIFICATIONS, ColumnType.NOTIFICATION_FROM_ACCT -> true
        else -> false
    }

// 公開ストリームなら真
val Column.isPublicStream: Boolean
    get() {
        return when (type) {
            ColumnType.LOCAL,
            ColumnType.FEDERATE,
            ColumnType.HASHTAG,
            ColumnType.LOCAL_AROUND,
            ColumnType.FEDERATED_AROUND,
            ColumnType.DOMAIN_TIMELINE,
            -> true

            else -> false
        }
    }

fun Column.canAutoRefresh() =
    !accessInfo.isNA && type.canAutoRefresh

val Column.isConversation
    get() = when (type) {
        ColumnType.CONVERSATION,
        ColumnType.CONVERSATION_WITH_REFERENCE,
        -> true
        else -> false
    }

/////////////////////////////////////////////////////////////////////////////
// 読み込み処理の内部で使うメソッド

fun Column.getNotificationTypeString(): String {
    val list = buildList {
        when (quickFilter) {
            Column.QUICK_FILTER_MENTION -> add(R.string.notification_type_mention)
            Column.QUICK_FILTER_FAVOURITE -> add(R.string.notification_type_favourite)
            Column.QUICK_FILTER_BOOST -> add(R.string.notification_type_boost)
            Column.QUICK_FILTER_FOLLOW -> add(R.string.notification_type_follow)
            Column.QUICK_FILTER_REACTION -> add(R.string.notification_type_reaction)
            Column.QUICK_FILTER_VOTE -> add(R.string.notification_type_vote)
            Column.QUICK_FILTER_POST -> add(R.string.notification_type_post)
            Column.QUICK_FILTER_ALL -> {
                if (!dontShowReply) add(R.string.notification_type_mention)
                if (!dontShowFollow) add(R.string.notification_type_follow)
                if (!dontShowBoost) add(R.string.notification_type_boost)
                if (!dontShowFavourite) add(R.string.notification_type_favourite)
                if (isMisskey && !dontShowReaction) add(R.string.notification_type_reaction)
                if (!dontShowVote) add(R.string.notification_type_vote)
            }
        }
    }
    val nMax = when {
        isMisskey -> 6
        else -> 5
    }
    return when {
        // 全部か皆無なら部分表記は要らない
        list.isEmpty() || list.size == nMax -> ""
        else -> "(${list.joinToString(", ") { context.getString(it) }})"
    }
}

suspend fun Column.loadProfileAccount(
    client: TootApiClient,
    parser: TootParser,
    bForceReload: Boolean,
): TootApiResult? =
    when {
        // リロード不要なら何もしない
        this.whoAccount != null && !bForceReload -> null

        isMisskey -> client.request(
            "/api/users/show",
            accessInfo.putMisskeyApiToken().apply {
                put("userId", profileId)
            }.toPostRequestBuilder()
        )?.also { result1 ->
            // ユーザリレーションの取り扱いのため、別のparserを作ってはいけない
            parser.misskeyDecodeProfilePin = true
            try {
                tootAccountRefOrNull(parser, parser.account(result1.jsonObject))?.also { a ->
                    this.whoAccount = a
                    client.publishApiProgress("") // カラムヘッダの再表示
                }
            } finally {
                parser.misskeyDecodeProfilePin = false
            }
        }

        else -> client.request(
            "/api/v1/accounts/$profileId"
        )?.also { result1 ->
            tootAccountRefOrNull(parser, parser.account(result1.jsonObject))?.also { a ->
                this.whoAccount = a

                this.whoFeaturedTags = null
                client.request("/api/v1/accounts/$profileId/featured_tags")
                    ?.also { result2 ->

                        this.whoFeaturedTags =
                            TootTag.parseListOrNull(parser, result2.jsonArray)
                    }

                client.publishApiProgress("") // カラムヘッダの再表示
            }
        }
    }

fun Column.loadSearchDesc(@RawRes rawEn: Int, @RawRes rawJa: Int): String {
    @RawRes val rawId = if ("ja" == context.getString(R.string.language_code)) rawJa else rawEn
    return context.loadRawResource(rawId).decodeUTF8()
}

suspend fun Column.updateRelation(
    client: TootApiClient,
    list: ArrayList<TimelineItem>?,
    whoRef: TootAccountRef?,
    parser: TootParser,
) {
    if (accessInfo.isPseudo) return

    val env = UserRelationLoader(this)

    env.add(whoRef)

    list?.forEach {
        when (it) {
            is TootAccountRef -> env.add(it)
            is TootStatus -> env.add(it)
            is TootNotification -> env.add(it)
            is TootConversationSummary -> env.add(it.last_status)
        }
    }
    env.update(client, parser)
}

fun Column.parseRange(
    result: TootApiResult?,
    list: List<TimelineItem>?,
): Pair<EntityId?, EntityId?> {
    var idMin: EntityId? = null
    var idMax: EntityId? = null

    if (isMisskey && list != null) {
        // MisskeyはLinkヘッダがないので、常にデータからIDを読む

        for (item in list) {
            // injectされたデータをデータ範囲に追加しない
            if (item.isInjected()) continue

            val id = item.getOrderId()
            if (id.notDefaultOrConfirming) {
                if (idMin == null || id < idMin) idMin = id
                if (idMax == null || id > idMax) idMax = id
            }
        }
    } else {
        // Linkヘッダを読む
        idMin = Column.reMaxId.matcher(result?.linkOlder ?: "").findOrNull()
            ?.let {
                EntityId(it.groupEx(1)!!)
            }

        idMax = Column.reMinId.matcher(result?.linkNewer ?: "").findOrNull()
            ?.let {
                // min_idとsince_idの読み分けは現在利用してない it.groupEx(1)=="min_id"
                EntityId(it.groupEx(2)!!)
            }
    }

    return Pair(idMin, idMax)
}
// int scroll_hack;

// return true if list bottom may have unread remain
// カラムが既に範囲を持ってる場合、その範囲を拡張する。
fun Column.saveRange(
    bBottom: Boolean,
    bTop: Boolean,
    result: TootApiResult?,
    list: List<TimelineItem>?,
): Boolean {
    val (idMin, idMax) = parseRange(result, list)

    var hasBottomRemain = false

    if (bBottom) when (idMin) {
        null -> idOld = null // リストの終端
        else -> {
            val i = idOld?.compareTo(idMin)
            if (i == null || i > 0) {
                idOld = idMin
                hasBottomRemain = true
            }
        }
    }

    if (bTop) when (idMax) {
        null -> {
            // リロードを許容するため、取得内容がカラでもidRecentを変更しない
        }

        else -> {
            val i = idRecent?.compareTo(idMax)
            if (i == null || i < 0) {
                idRecent = idMax
            }
        }
    }

    return hasBottomRemain
}

// return true if list bottom may have unread remain
fun Column.saveRangeBottom(result: TootApiResult?, list: List<TimelineItem>?) =
    saveRange(true, bTop = false, result = result, list = list)

// no return value: can't find there may be more items.
fun Column.saveRangeTop(result: TootApiResult?, list: List<TimelineItem>?) {
    saveRange(false, bTop = true, result = result, list = list)
}

fun Column.addRange(
    bBottom: Boolean,
    path: String,
    delimiter: Char = if (-1 == path.indexOf('?')) '?' else '&',
) = if (bBottom) {
    if (idOld != null) "$path${delimiter}max_id=$idOld" else path
} else {
    if (idRecent != null) "$path${delimiter}since_id=$idRecent" else path
}

fun Column.addRangeMin(
    path: String,
    delimiter: Char = if (-1 != path.indexOf('?')) '&' else '?',
) = if (idRecent == null) path else "$path${delimiter}min_id=$idRecent"

fun Column.toAdapterIndex(listIndex: Int): Int {
    return if (type.headerType != null) listIndex + 1 else listIndex
}

fun Column.toListIndex(adapterIndex: Int): Int {
    return if (type.headerType != null) adapterIndex - 1 else adapterIndex
}

fun Column.saveScrollPosition() {
    try {
        if (viewHolder?.saveScrollPosition() == true) {
            val ss = this.scrollSave
            if (ss != null) {
                val idx = toListIndex(ss.adapterIndex)
                if (0 <= idx && idx < listData.size) {
                    val item = listData[idx]
                    this.lastViewingItemId = item.getOrderId()
                    // とりあえず保存はするが
                    // TLデータそのものを永続化しないかぎり出番はないっぽい
                }
            }
        }
    } catch (ex: Throwable) {
        log.e(ex, "can't get last_viewing_item_id.")
    }
}

inline fun <reified T : TimelineItem> addAll(
    dstArg: ArrayList<TimelineItem>?,
    src: List<T>,
    head: Boolean = false,
): ArrayList<TimelineItem> =
    (dstArg ?: ArrayList(src.size)).apply {
        if (head) {
            addAll(0, src)
        } else {
            addAll(src)
        }
    }

fun addOne(
    dstArg: ArrayList<TimelineItem>?,
    item: TimelineItem?,
    head: Boolean = false,
): ArrayList<TimelineItem> =
    (dstArg ?: ArrayList()).apply {
        if (item != null) {
            if (head) {
                add(0, item)
            } else {
                add(item)
            }
        }
    }

fun ColumnTask.addWithFilterStatus(
    dstArg: ArrayList<TimelineItem>?,
    srcArg: List<TootStatus>,
    head: Boolean = false,
): ArrayList<TimelineItem> =
    (dstArg ?: ArrayList(srcArg.size)).apply {
        val src = srcArg.filter { !column.isFiltered(it) }
        if (head) {
            addAll(0, src)
        } else {
            addAll(src)
        }
    }

fun ColumnTask.addWithFilterConversationSummary(
    dstArg: ArrayList<TimelineItem>?,
    srcArg: List<TootConversationSummary>,
    head: Boolean = false,
): ArrayList<TimelineItem> =
    (dstArg ?: ArrayList(srcArg.size)).apply {
        val src = srcArg.filter { !column.isFiltered(it.last_status) }
        if (head) {
            addAll(0, src)
        } else {
            addAll(src)
        }
    }

fun ColumnTask.addWithFilterNotification(
    dstArg: ArrayList<TimelineItem>?,
    srcArg: List<TootNotification>,
    head: Boolean = false,
): ArrayList<TimelineItem> =
    (dstArg ?: ArrayList(srcArg.size)).apply {
        val src = srcArg.filter { !column.isFiltered(it) }
        if (head) {
            addAll(0, src)
        } else {
            addAll(src)
        }
    }

fun Column.dispatchProfileTabStatus() =
    when {
        isMisskey -> ColumnType.ProfileStatusMisskey
        else -> ColumnType.ProfileStatusMastodon
    }

fun Column.dispatchProfileTabFollowing() =
    when {
        misskeyVersion >= 11 -> ColumnType.FollowingMisskey11
        isMisskey -> ColumnType.FollowingMisskey10
        accessInfo.isPseudo -> ColumnType.FollowingMastodonPseudo
        else -> ColumnType.FollowingMastodon
    }

fun Column.dispatchProfileTabFollowers() =
    when {
        misskeyVersion >= 11 -> ColumnType.FollowersMisskey11
        isMisskey -> ColumnType.FollowersMisskey10
        accessInfo.isPseudo -> ColumnType.FollowersMastodonPseudo
        else -> ColumnType.FollowersMastodon
    }

fun ColumnTask.dispatchProfileTabStatus() =
    column.dispatchProfileTabStatus()

fun ColumnTask.dispatchProfileTabFollowing() =
    column.dispatchProfileTabFollowing()

fun ColumnTask.dispatchProfileTabFollowers() =
    column.dispatchProfileTabFollowers()

suspend fun Column.loadListInfo(client: TootApiClient, bForceReload: Boolean) {
    val parser = TootParser(context, accessInfo)
    if (bForceReload || this.listInfo == null) {
        val result = if (isMisskey) {
            client.request(
                "/api/users/lists/show",
                makeMisskeyBaseParameter(parser).apply {
                    put("listId", profileId)
                }.toPostRequestBuilder()
            )
        } else {
            client.request("/api/v1/lists/$profileId")
        }

        val jsonObject = result?.jsonObject
        if (jsonObject != null) {
            val data =
                parseItem(jsonObject) { TootList(parser, it) }
            if (data != null) {
                this.listInfo = data
                client.publishApiProgress("") // カラムヘッダの再表示
            }
        }
    }
}

suspend fun Column.loadAntennaInfo(client: TootApiClient, bForceReload: Boolean) {
    val parser = TootParser(context, accessInfo)
    if (bForceReload || this.antennaInfo == null) {

        val result = if (isMisskey) {
            client.request(
                "/api/antennas/show",
                makeMisskeyBaseParameter(parser).apply {
                    put("antennaId", profileId)
                }.toPostRequestBuilder()
            )
        } else {
            TootApiResult("antenna feature is not supported on Mastodon")
        }

        val jsonObject = result?.jsonObject
        if (jsonObject != null) {
            val data = parseItem(jsonObject) { MisskeyAntenna(it) }
            if (data != null) {
                this.antennaInfo = data
                client.publishApiProgress("") // カラムヘッダの再表示
            }
        }
    }
}

fun JsonObject.putMisskeyUntil(id: EntityId?): JsonObject {
    if (id != null) put("untilId", id.toString())
    return this
}

fun JsonObject.putMisskeySince(id: EntityId?): JsonObject {
    if (id != null) put("sinceId", id.toString())
    return this
}

fun JsonObject.addRangeMisskey(column: Column, bBottom: Boolean): JsonObject {
    if (bBottom) {
        putMisskeyUntil(column.idOld)
    } else {
        putMisskeySince(column.idRecent)
    }
    return this
}

///////////////////////////////////////////////////////////////////////

private const val DIR_BACKGROUND_IMAGE = "columnBackground"

fun getBackgroundImageDir(context: Context): File {
    val externalDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
    if (externalDir == null) {
        log.e("getExternalFilesDir is null.")
    } else {
        val state = Environment.getExternalStorageState()
        if (state != Environment.MEDIA_MOUNTED) {
            log.e("getExternalStorageState: $state")
        } else {
            log.i("externalDir: $externalDir")
            externalDir.mkdir()
            val backgroundDir = File(externalDir, DIR_BACKGROUND_IMAGE)
            backgroundDir.mkdir()
            log.i("backgroundDir: $backgroundDir exists=${backgroundDir.exists()}")
            return backgroundDir
        }
    }
    val backgroundDir = context.getDir(DIR_BACKGROUND_IMAGE, Context.MODE_PRIVATE)
    log.i("backgroundDir: $backgroundDir exists=${backgroundDir.exists()}")
    return backgroundDir
}
