package jp.juggler.subwaytooter

import android.os.SystemClock
import jp.juggler.subwaytooter.api.*
import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.util.ScrollPosition
import jp.juggler.util.*

class ColumnTask_Refresh(
	columnArg: Column,
	private val bSilent: Boolean,
	val bBottom: Boolean,
	internal val posted_status_id: EntityId? = null,
	internal val refresh_after_toot: Int = -1
) : ColumnTask(
	columnArg,
	if (bBottom) ColumnTaskType.REFRESH_BOTTOM else ColumnTaskType.REFRESH_TOP
) {

    companion object {
        private val log = LogCategory("CT_Refresh")
    }

    private var filterUpdated = false

    override suspend fun background(): TootApiResult? {
        ctStarted.set(true)

        val client = TootApiClient(context, callback = object : TootApiCallback {
			override val isApiCancelled: Boolean
				get() = isCancelled || column.is_dispose.get()

			override suspend fun publishApiProgress(s: String) {
				runOnMainLooper {
					if (isCancelled) return@runOnMainLooper
					column.task_progress = s
					column.fireShowContent(reason = "refresh progress", changeList = ArrayList())
				}
			}
		})
        client.account = access_info
        try {

            if (!bBottom) {
                val filterList = column.loadFilter2(client)
                if (filterList != null) {
                    column.keywordFilterTrees = column.encodeFilterTree(filterList)
                    filterUpdated = true
                }
            }

            return column.type.refresh(this, client)
        } catch (ex: Throwable) {
            return TootApiResult(ex.withCaption("refresh failed."))
        } finally {
            try {
                column.updateRelation(client, list_tmp, column.who_account, parser)
            } catch (ex: Throwable) {
                log.trace(ex)
            }
            ctClosed.set(true)
            runOnMainLooperDelayed(333L) {
                if (!isCancelled) column.fireShowColumnStatus()
            }
        }
    }

    override suspend fun handleResult(result: TootApiResult?) {
        if (column.is_dispose.get()) return

        if (isCancelled || result == null) {
            return
        }
        try {
            column.lastTask = null
            column.bRefreshLoading = false

            if (filterUpdated) {
                column.checkFiltersForListData(column.keywordFilterTrees)
            }

            val error = result.error
            if (error != null) {
                column.mRefreshLoadingError = error
                column.mRefreshLoadingErrorTime = SystemClock.elapsedRealtime()
                column.fireShowContent(reason = "refresh error", changeList = ArrayList())
                return
            }

            val list_new = column.duplicate_map.filterDuplicate(list_tmp)
            if (list_new.isEmpty()) {
                column.fireShowContent(
					reason = "refresh list_new is empty",
					changeList = ArrayList()
				)
                return
            }

            // 事前にスクロール位置を覚えておく
            var sp: ScrollPosition? = null
            val holder = column.viewHolder
            if (holder != null) {
                sp = holder.scrollPosition
            }



            if (bBottom) {
                val changeList = listOf(
					AdapterChange(
						AdapterChangeType.RangeInsert,
						column.list_data.size,
						list_new.size
					)
				)
                column.list_data.addAll(list_new)
                column.fireShowContent(reason = "refresh updated bottom", changeList = changeList)

                // 新着が少しだけ見えるようにスクロール位置を移動する
                if (sp != null) {
                    holder?.setScrollPosition(sp, 20f)
                }
            } else {

                val changeList = ArrayList<AdapterChange>()

                if (column.list_data.isNotEmpty() && column.list_data[0] is TootGap) {
                    changeList.add(AdapterChange(AdapterChangeType.RangeRemove, 0, 1))
                    column.list_data.removeAt(0)
                }

                var doneSound = false
                for (o in list_new) {
                    if (o is TootStatus) {
                        o.highlightSound?.let {
                            if (!doneSound) {
                                doneSound = true
                                App1.sound(it)
                            }
                        }
                        o.highlightSpeech?.let {
                            column.app_state.addSpeech(it.name, dedupMode = DedupMode.RecentExpire)
                        }
                    }
                }

                column.replaceConversationSummary(changeList, list_new, column.list_data)

                val added = list_new.size // may 0

                // 投稿後のリフレッシュなら当該投稿の位置を探す
                var status_index = -1
                for (i in 0 until added) {
                    val o = list_new[i]
                    if (o is TootStatus && o.id == posted_status_id) {
                        status_index = i
                        break
                    }
                }

                changeList.add(AdapterChange(AdapterChangeType.RangeInsert, 0, added))
                column.list_data.addAll(0, list_new)
                column.fireShowContent(reason = "refresh updated head", changeList = changeList)

                if (status_index >= 0 && refresh_after_toot == Pref.RAT_REFRESH_SCROLL) {
                    // 投稿後にその投稿にスクロールする
                    if (holder != null) {
                        holder.setScrollPosition(
							ScrollPosition(column.toAdapterIndex(status_index)),
							0f
						)
                    } else {
                        column.scroll_save = ScrollPosition(column.toAdapterIndex(status_index))
                    }
                } else {
                    //
                    val scroll_save = column.scroll_save
                    when {
                        // ViewHolderがある場合は増加件数分+deltaの位置にスクロールする
                        sp != null -> {
                            sp.adapterIndex += added
                            val delta = if (bSilent) 0f else -20f
                            holder?.setScrollPosition(sp, delta)
                        }
                        // ViewHolderがなくて保存中の位置がある場合、増加件数分ずらす。deltaは難しいので反映しない
                        scroll_save != null -> scroll_save.adapterIndex += added
                        // 保存中の位置がない場合、保存中の位置を新しく作る
                        else -> column.scroll_save =
                            ScrollPosition(column.toAdapterIndex(added))
                    }
                }
            }

            column.updateMisskeyCapture()

        } finally {
            column.fireShowColumnStatus()

            if (!bBottom) {
                column.bRefreshingTop = false

                // TODO ロード状態の度にストリーミングを止めるのはやめる
                // TODO ストリーミングから受信したデータの反映をロード中は行わないようにしたい
                // column.resumeStreaming(false)
            }
        }
    }

    private suspend fun <T : TimelineItem> refreshTopMisskey(
		logCaption: String,
		requester: suspend (first: Boolean) -> TootApiResult?,
		arrayFinder: (JsonObject) -> JsonArray?,
		listParser: (parser: TootParser, jsonArray: JsonArray) -> List<T>,
		adder: (List<T>, Boolean) -> Unit
	): TootApiResult? {

        // ColumnPagingType.Defaultだけが始端を更新できる
        if (column.pagingType != ColumnPagingType.Default)
            return TootApiResult("can't refresh top.")

        val addToHead = true

        val time_start = SystemClock.elapsedRealtime()

        var result = requester(true)

        val firstResult = result

        var jsonObject = result?.jsonObject
        if (jsonObject != null)
            result?.data = arrayFinder(jsonObject)

        var array = result?.jsonArray
        if (array != null) {
            list_tmp = ArrayList()

            var src = listParser(parser, array)
            adder(src, addToHead)
            column.saveRangeTop(result, src)

            // misskeyの場合、sinceIdを指定したら未読範囲の古い方から読んでしまう
            // 最新まで読めるとは限らない
            // 先頭にギャップを置くかもしれない
            var willAddGap = false
            while (true) {
                // 頭の方を読む時は隙間を減らすため、フィルタの有無に関係なく繰り返しを行う

                if (isCancelled) {
                    log.d("$logCaption: cancelled.")
                    break
                }

                if (src.isEmpty()) {
                    // 直前のデータが0個なら終了とみなす
                    log.d("$logCaption: previous size == 0.")
                    break
                }

                if ((list_tmp?.size ?: 0) >= Column.LOOP_READ_ENOUGH) {
                    // 既に十分読んだなら止める
                    log.d("$logCaption: read enough. make gap.")
                    willAddGap = true
                    break
                }

                if (SystemClock.elapsedRealtime() - time_start > Column.LOOP_TIMEOUT) {
                    log.d("$logCaption: timeout.")
                    willAddGap = true
                    break
                }

                result = requester(false)

                jsonObject = result?.jsonObject
                if (jsonObject != null)
                    result?.data = arrayFinder(jsonObject)

                array = result?.jsonArray
                if (array == null) {
                    log.d("$logCaption: error or cancelled.")
                    willAddGap = true
                    break
                }

                src = listParser(parser, array)
                adder(src, addToHead)
                column.saveRangeTop(result = result, list = src)
            }

            // MisskeyはsinceIdを指定するとID昇順のデータが得られるので、ID降順に並べ直す
            list_tmp?.sortByDescending { it.getOrderId() }

            if (!isCancelled
                && list_tmp?.isNotEmpty() == true
                && (willAddGap || Pref.bpForceGap(context))
            ) {
                addOne(list_tmp, TootGap.mayNull(null, column.idRecent), head = addToHead)
            }
        }
        return firstResult
    }

    private suspend fun <T : TimelineItem> refreshBottomMisskey(
		logCaption: String,
		requester: suspend (first: Boolean) -> TootApiResult?,
		arrayFinder: (JsonObject) -> JsonArray?,
		listParser: (parser: TootParser, jsonArray: JsonArray) -> List<T>,
		adder: (List<T>, Boolean) -> Unit,
		repeatReading: Boolean = false,
	): TootApiResult? {

        when (column.pagingType) {
			ColumnPagingType.None ->
				return TootApiResult(context.getString(R.string.end_of_list))

			ColumnPagingType.Cursor ->
				if (column.idOld == null)
					return TootApiResult(context.getString(R.string.end_of_list))

            else -> {
            }
        }

        val time_start = SystemClock.elapsedRealtime()
        val addToHead = false

        var result = requester(true)
        val firstResult = result

        fun saveRange(src: List<T>): Boolean = when (column.pagingType) {
			ColumnPagingType.Offset -> {
				column.offsetNext += src.size
				true
			}

            else -> // ColumnPagingType.Default
                column.saveRangeBottom(result, src)
                    .also {
                        if (!it) log.d("$logCaption: saveRangeBottom returns false. no more unread contents.")
                    }
        }

        var jsonObject = result?.jsonObject
        if (jsonObject != null) {
            if (column.pagingType == ColumnPagingType.Cursor) {
                column.idOld = EntityId.mayNull(jsonObject.string("next"))
            }
            result?.data = arrayFinder(jsonObject)
        }

        var array = result?.jsonArray
        if (array != null) {
            list_tmp = ArrayList()

            var src = listParser(parser, array)
            adder(src, addToHead)
            var hasMoreRange = saveRange(src)

            while (hasMoreRange && repeatReading) {
                if (isCancelled) {
                    log.d("$logCaption: cancelled.")
                    break
                }

                // bottomの場合、フィルタなしなら繰り返さない
                if (!column.isFilterEnabled) {
                    log.d("$logCaption: isFilterEnabled is false.")
                    break
                }

                // max_id だけを指定した場合、必ずlimit個のデータが帰ってくるとは限らない
                // 直前のデータが0個なら終了とみなすしかなさそう
                if (src.isEmpty()) {
                    log.d("$logCaption: previous size == 0.")
                    break
                }

                if (column.idOld == null) {
                    log.d("$logCaption: idOld is null.")
                    break
                }

                // 十分読んだらそれで終了
                if ((list_tmp?.size ?: 0) >= Column.LOOP_READ_ENOUGH) {
                    log.d("$logCaption: read enough data.")
                    break
                }

                if (SystemClock.elapsedRealtime() - time_start > Column.LOOP_TIMEOUT) {
                    // タイムアウト
                    log.d("$logCaption: loop timeout.")
                    break
                }

                result = requester(false)

                jsonObject = result?.jsonObject
                if (jsonObject != null) {
                    if (column.pagingType == ColumnPagingType.Cursor) {
                        column.idOld = EntityId.mayNull(jsonObject.string("next"))
                    }
                    result?.data = arrayFinder(jsonObject)
                }

                array = result?.jsonArray
                if (array == null) {
                    log.d("$logCaption:  error or cancelled.")
                    break
                }

                src = listParser(parser, array)
                adder(src, addToHead)
                hasMoreRange = saveRange(src)
            }
        }
        return firstResult
    }

    private suspend fun <T : TimelineItem> refreshTopMastodon(
		logCaption: String,
		requester: suspend (first: Boolean, gapIdNewer: EntityId?, gapIdOlder: EntityId?) -> TootApiResult?,
		arrayFinder: (JsonObject) -> JsonArray?,
		listParser: (parser: TootParser, jsonArray: JsonArray) -> List<T>,
		adder: (List<T>, Boolean) -> Unit
	): TootApiResult? {

        // 上端の差分更新に対応できるのは ColumnPagingType.Default だけ
        if (column.pagingType != ColumnPagingType.Default)
            return TootApiResult("can't refresh top.")

        val time_start = SystemClock.elapsedRealtime()
        val addToHead = false

        var result = requester(true, null, null)
        val firstResult = result

        var jsonObject = result?.jsonObject
        if (jsonObject != null) result?.data = arrayFinder(jsonObject)

        var array = result?.jsonArray
        if (array != null) {
            val last_since_id = column.idRecent
            list_tmp = ArrayList()

            var src = listParser(parser, array)
            adder(src, addToHead)
            column.saveRangeTop(result, src)
            // TLは (新しいデータ)(ギャップ)(古いデータ) となるので、レンジを保存するのはここだけで良い
            // 続く読み込みはギャップを埋めるものなのでレンジを保存してはいけない

            // 頭の方を読む時は隙間を減らすため、フィルタの有無に関係なく繰り返しを行う
            var willAddGap = false
            var max_id: EntityId? = null
            while (true) {
                if (isCancelled) {
                    log.d("$logCaption: cancelled.")
                    break
                }

                if (src.isEmpty()) {
                    // max_id だけを指定した場合、必ずlimit個のデータが帰ってくるとは限らない
                    // 直前のデータが0個なら終了とみなすしかなさそう
                    log.d("$logCaption: previous size == 0.")
                    break
                }

                // 直前に読んだ範囲のmaxIdを調べる
                max_id = column.parseRange(result, src).first
                if (max_id == null) {
                    log.d("$logCaption: max_id is null.")
                    break
                }

                if ((list_tmp?.size ?: 0) >= Column.LOOP_READ_ENOUGH) {
                    log.d("$logCaption: read enough. make gap.")
                    willAddGap = true
                    break
                }

                if (SystemClock.elapsedRealtime() - time_start > Column.LOOP_TIMEOUT) {
                    // タイムアウト
                    log.d("$logCaption: timeout. make gap.")
                    willAddGap = true
                    break
                }

                result = requester(false, max_id, last_since_id)

                jsonObject = result?.jsonObject
                if (jsonObject != null) result?.data = arrayFinder(jsonObject)

                array = result?.jsonArray

                if (array == null) {
                    // エラー
                    log.d("$logCaption: error or cancelled. make gap.")
                    willAddGap = true
                    break
                }

                src = listParser(parser, array)
                adder(src, addToHead)

                // 今読んだのはギャップなので範囲を保存してはいけない
            }

            if (!isCancelled
                && list_tmp?.isNotEmpty() == true
                && (willAddGap || Pref.bpForceGap(context))
            ) {
                addOne(list_tmp, TootGap.mayNull(max_id, last_since_id), head = addToHead)
            }
        }
        return firstResult
    }

    private suspend fun <T : TimelineItem> refreshTopMastodonMinId(
		logCaption: String,
		requester: suspend (first: Boolean, gapIdNewer: EntityId?, gapIdOlder: EntityId?) -> TootApiResult?,
		arrayFinder: (JsonObject) -> JsonArray?,
		listParser: (parser: TootParser, jsonArray: JsonArray) -> List<T>,
		adder: (List<T>, Boolean) -> Unit
	): TootApiResult? {
        list_tmp = ArrayList()

        // 上端の差分更新に対応できるのは ColumnPagingType.Default だけ
        if (column.pagingType != ColumnPagingType.Default)
            return TootApiResult("can't refresh top.")

        val last_since_id = column.idRecent
        val addToHead = true

        val time_start = SystemClock.elapsedRealtime()

        var result = requester(true, null, null)

        val firstResult = result

        var jsonObject = result?.jsonObject
        if (jsonObject != null) {
            result?.data = arrayFinder(jsonObject)
        }

        var array = result?.jsonArray
        if (array != null) {

            var src = listParser(parser, array)
            adder(src, addToHead)

            var willAddGap = false
            var max_id: EntityId? = null
            while (true) {
                column.saveRangeTop(result, src)

                if (isCancelled) {
                    log.d("$logCaption: cancelled.")
                    break
                }

                // max_id だけを指定した場合、必ずlimit個のデータが帰ってくるとは限らない
                // 直前のデータが0個なら終了とみなすしかなさそう
                if (src.isEmpty()) {
                    log.d("$logCaption: previous size == 0.")
                    break
                }

                // 直前に読んだ範囲のmaxIdを調べる
                max_id = column.parseRange(result, src).first
                if (max_id == null) {
                    log.d("$logCaption: max_id is null.")
                    break
                }

                if (SystemClock.elapsedRealtime() - time_start > Column.LOOP_TIMEOUT) {
                    log.d("$logCaption: timeout. make gap.")
                    // タイムアウト
                    // 隙間ができるかもしれない。後ほど手動で試してもらうしかない
                    willAddGap = true
                    break
                }

                result = requester(false, null, null)

                jsonObject = result?.jsonObject
                if (jsonObject != null)
                    result?.data = arrayFinder(jsonObject)

                array = result?.jsonArray

                if (array == null) {
                    log.d("$logCaption: error or cancelled. make gap.")
                    // エラー
                    // 隙間ができるかもしれない。後ほど手動で試してもらうしかない
                    willAddGap = true
                    break
                }

                src = listParser(parser, array)
                adder(src, addToHead)

            }

            if (!isCancelled
                && list_tmp?.isNotEmpty() == true
                && (willAddGap || Pref.bpForceGap(context))
            ) {
                addOne(list_tmp, TootGap.mayNull(max_id, last_since_id), head = addToHead)
            }
        }
        return firstResult
    }

    private suspend fun <T : TimelineItem> refreshBottomMastodon(
		logCaption: String,
		requester: suspend (first: Boolean, gapIdNewer: EntityId?, gapIdOlder: EntityId?) -> TootApiResult?,
		arrayFinder: (JsonObject) -> JsonArray?,
		listParser: (parser: TootParser, jsonArray: JsonArray) -> List<T>,
		adder: (List<T>, Boolean) -> Unit,
		repeatReading: Boolean = false
	): TootApiResult? {

        @Suppress("NON_EXHAUSTIVE_WHEN")
        when (column.pagingType) {
			ColumnPagingType.None ->
				return TootApiResult(context.getString(R.string.end_of_list))

			ColumnPagingType.Cursor ->
				if (column.idOld == null)
					return TootApiResult(context.getString(R.string.end_of_list))
        }

        val time_start = SystemClock.elapsedRealtime()
        val addToHead = false

        var result = requester(true, null, null)
        val firstResult = result

        var jsonObject = result?.jsonObject
        if (jsonObject != null) {
            if (column.pagingType == ColumnPagingType.Cursor) {
                column.idOld = EntityId.mayNull(jsonObject.string("next"))
            }
            result?.data = arrayFinder(jsonObject)
        }

        var array = result?.jsonArray
        if (array != null) {
            list_tmp = ArrayList()
            var src = listParser(parser, array)
            adder(src, addToHead)

            while (repeatReading) {

                // returns false if no more result
                fun saveResultRange(src: List<T>) = when (column.pagingType) {
					ColumnPagingType.Offset -> {
						column.offsetNext += src.size
						true
					}

                    else -> // ColumnPagingType.Default
                        column.saveRangeBottom(result, src)
                }

                if (!saveResultRange(src)) {
                    log.d("$logCaption: saveRangeBottom returns false. no more items.")
                    break
                }

                if (isCancelled) {
                    log.d("$logCaption: cancelled.")
                    break
                }

                if (!column.isFilterEnabled) {
                    // bottomの場合、フィルタなしなら繰り返さない
                    log.d("$logCaption: isFiltered is false.")
                    break
                }

                if (src.isEmpty()) {
                    // max_id だけを指定した場合、必ずlimit個のデータが帰ってくるとは限らない
                    // 直前のデータが0個なら終了とみなすしかなさそう
                    log.d("$logCaption: previous size == 0.")
                    break
                }

                if (column.idOld == null) {
                    log.d("$logCaption: idOld is null.")
                    break
                }

                if ((list_tmp?.size ?: 0) >= Column.LOOP_READ_ENOUGH) {
                    // 十分読んだらそれで終了
                    log.d("$logCaption: read enough data.")
                    break
                }

                if (SystemClock.elapsedRealtime() - time_start > Column.LOOP_TIMEOUT) {
                    // タイムアウト
                    log.d("$logCaption: loop timeout.")
                    break
                }

                result = requester(false, null, null)

                jsonObject = result?.jsonObject
                if (jsonObject != null)
                    result?.data = arrayFinder(jsonObject)

                array = result?.jsonArray
                if (array == null) {
                    log.d("$logCaption: error or cancelled.")
                    break
                }

                src = listParser(parser, array)
                adder(src, addToHead)
            }
        }
        return firstResult
    }

    private suspend fun defaultRequesterMisskey(
		client: TootApiClient,
		path_base: String,
		params: JsonObject,
		first: Boolean
	) =
        client.request(
			path_base,
			params.apply {
				if (!bBottom) {
					if (first) {

						addRangeMisskey(bBottom)
					} else {
						putMisskeySince(column.idRecent)
					}
				} else {
					if (first) {

						when (column.pagingType) {
							ColumnPagingType.Default -> addRangeMisskey(bBottom)
							ColumnPagingType.Offset -> put("offset", column.offsetNext)
							ColumnPagingType.Cursor -> put("cursor", column.idOld)

							ColumnPagingType.None -> {
							}
						}
					} else {
						when (column.pagingType) {
							ColumnPagingType.Default -> putMisskeyUntil(column.idOld)
							ColumnPagingType.Offset -> put("offset", column.offsetNext)
							ColumnPagingType.Cursor -> put("cursor", column.idOld)

							ColumnPagingType.None -> {
							}
						}
					}
				}
			}.toPostRequestBuilder()
		)

    private suspend fun defaultRequesterMastodon(
		client: TootApiClient,
		path_base: String,
		delimiter: Char,
		@Suppress("UNUSED_PARAMETER") first: Boolean,
		useMinId: Boolean,
		gapIdNewer: EntityId?,
		gapIdOlder: EntityId?
	) =
        client.request(
			when {
				// profile directory 用
				column.pagingType == ColumnPagingType.Offset ->
					"$path_base${delimiter}offset=${column.offsetNext}"

				useMinId ->
					column.addRangeMin(path_base)

				gapIdNewer != null ->
					"$path_base${delimiter}max_id=$gapIdNewer&since_id=$gapIdOlder"

				else ->
					column.addRange(bBottom = bBottom, path_base, delimiter = delimiter)
			}
		)

    suspend fun getStatusList(
		client: TootApiClient,
		path_base: String?,
		useMinId: Boolean = false,
		misskeyParams: JsonObject? = null,
		listParser: (parser: TootParser, jsonArray: JsonArray) -> List<TootStatus> =
			defaultStatusListParser
	): TootApiResult? {

        path_base ?: return null // cancelled.

        val logCaption = "getStatusList"
        val adder: (List<TootStatus>, Boolean) -> Unit =
            { src, head -> addWithFilterStatus(list_tmp, src, head = head) }

        return if (isMisskey) {

            val params = misskeyParams ?: column.makeMisskeyTimelineParameter(parser)

            val requester: suspend (Boolean) -> TootApiResult? =
                { defaultRequesterMisskey(client, path_base, params, it) }

            when {
                bBottom -> refreshBottomMisskey(
					logCaption,
					requester = requester,
					arrayFinder = nullArrayFinder,
					listParser = listParser,
					adder = adder,
					repeatReading = true
				)
                else -> refreshTopMisskey(
					logCaption,
					requester = requester,
					arrayFinder = nullArrayFinder,
					listParser = listParser,
					adder = adder
				)
            }

        } else {

            val delimiter = if (-1 != path_base.indexOf('?')) '&' else '?'

            val requester: suspend (first: Boolean, gapIdNewer: EntityId?, gapIdOlder: EntityId?) -> TootApiResult? =
                { first, gapIdNewer, gapIdOlder ->
                    defaultRequesterMastodon(
						client,
						path_base,
						delimiter,
						first,
						useMinId = useMinId,
						gapIdNewer,
						gapIdOlder,
					)
                }

            when {
                bBottom -> refreshBottomMastodon(
					logCaption,
					requester = requester,
					arrayFinder = nullArrayFinder,
					listParser = listParser,
					adder = adder,
					repeatReading = true
				)
                useMinId -> refreshTopMastodonMinId(
					logCaption,
					requester = requester,
					arrayFinder = nullArrayFinder,
					listParser = listParser,
					adder
				)
                else -> refreshTopMastodon(
					logCaption,
					requester = requester,
					arrayFinder = nullArrayFinder,
					listParser = listParser,
					adder
				)
            }
        }
    }

    suspend fun getNotificationList(
		client: TootApiClient,
		fromAcct: String? = null,
		useMinId: Boolean = false
	): TootApiResult? {

        val logCaption = "getNotificationList"

        val listParser: (parser: TootParser, jsonArray: JsonArray) -> List<TootNotification> =
            defaultNotificationListParser

        val adder: (List<TootNotification>, Boolean) -> Unit =
            { src, head -> addWithFilterNotification(list_tmp, src, head = head) }

        // Misskeyの通知TLはfromAcctに対応していない
        val path_base = column.makeNotificationUrl(client, fromAcct)

        return if (isMisskey) {

            val params =
                column.makeMisskeyBaseParameter(parser).addMisskeyNotificationFilter(column)

            val requester: suspend (Boolean) -> TootApiResult? =
                { defaultRequesterMisskey(client, path_base, params, it) }

            when {
                bBottom -> refreshBottomMisskey(
					logCaption,
					requester = requester,
					arrayFinder = nullArrayFinder,
					listParser = listParser,
					adder = adder,
					repeatReading = true
				)
                else -> refreshTopMisskey(
					logCaption,
					requester = requester,
					arrayFinder = nullArrayFinder,
					listParser = listParser,
					adder = adder
				)
            }
        } else {

            val delimiter = if (-1 != path_base.indexOf('?')) '&' else '?'

            val requester: suspend (first: Boolean, gapIdNewer: EntityId?, gapIdOlder: EntityId?) -> TootApiResult? =
                { first, gapIdNewer, gapIdOlder ->
                    defaultRequesterMastodon(
						client,
						path_base,
						delimiter,
						first,
						useMinId = useMinId,
						gapIdNewer,
						gapIdOlder,
					)
                }

            when {
                bBottom -> refreshBottomMastodon(
					logCaption,
					requester = requester,
					arrayFinder = nullArrayFinder,
					listParser = listParser,
					adder = adder,
					repeatReading = true
				)
                useMinId -> refreshTopMastodonMinId(
					logCaption,
					requester = requester,
					arrayFinder = nullArrayFinder,
					listParser = listParser,
					adder = adder
				)
                else -> refreshTopMastodon(
					logCaption,
					requester = requester,
					arrayFinder = nullArrayFinder,
					listParser = listParser,
					adder = adder
				)
            }
        }
    }

    suspend fun getAccountList(
		client: TootApiClient,
		path_base: String,
		misskeyParams: JsonObject? = null,
		arrayFinder: (JsonObject) -> JsonArray? =
			nullArrayFinder,
		listParser: (parser: TootParser, jsonArray: JsonArray) -> List<TootAccountRef> =
			defaultAccountListParser
	): TootApiResult? {

        val logCaption = "getAccountList"
        val adder: (List<TootAccountRef>, Boolean) -> Unit =
            { src, head -> addAll(list_tmp, src, head = head) }

        return if (isMisskey) {
            val params = misskeyParams ?: column.makeMisskeyBaseParameter(parser)
            val requester: suspend (Boolean) -> TootApiResult? =
                { defaultRequesterMisskey(client, path_base, params, it) }
            when {
                bBottom -> refreshBottomMisskey(
					logCaption,
					requester = requester,
					arrayFinder = arrayFinder,
					listParser = listParser,
					adder = adder
				)
                else -> refreshTopMisskey(
					logCaption,
					requester = requester,
					arrayFinder = arrayFinder,
					listParser = listParser,
					adder = adder
				)
            }
        } else {
            val delimiter = if (-1 != path_base.indexOf('?')) '&' else '?'
            val requester: suspend (first: Boolean, gapIdNewer: EntityId?, gapIdOlder: EntityId?) -> TootApiResult? =
                { first, gapIdNewer, gapIdOlder ->
                    defaultRequesterMastodon(
						client,
						path_base,
						delimiter,
						first,
						useMinId = false,
						gapIdNewer,
						gapIdOlder,
					)
                }

            when {
                bBottom -> refreshBottomMastodon(
					logCaption,
					requester = requester,
					arrayFinder = arrayFinder,
					listParser = listParser,
					adder = adder
				)
                else -> refreshTopMastodon(
					logCaption,
					requester = requester,
					arrayFinder = arrayFinder,
					listParser = listParser,
					adder = adder
				)
            }
        }
    }

    suspend fun getDomainList(
		client: TootApiClient,
		path_base: String
	): TootApiResult? {

        val logCaption = "getDomainList"

        val adder: (List<TimelineItem>, Boolean) -> Unit =
            { src, head -> addAll(list_tmp, src, head = head) }

        val listParser = defaultDomainBlockListParser

        return if (isMisskey) {
            TootApiResult("misskey support is not yet implemented.")
        } else {
            // ページングIDはサーバ側の内部IDで、Linkヘッダ以外には露出しない。
            val delimiter = if (-1 != path_base.indexOf('?')) '&' else '?'
            val requester: suspend (first: Boolean, gapIdNewer: EntityId?, gapIdOlder: EntityId?) -> TootApiResult? =
                { first, gapIdNewer, gapIdOlder ->
                    defaultRequesterMastodon(
						client,
						path_base,
						delimiter,
						first,
						useMinId = false,
						gapIdNewer,
						gapIdOlder,
					)
                }

            when {
                bBottom -> refreshBottomMastodon(
					logCaption,
					requester = requester,
					arrayFinder = nullArrayFinder,
					listParser = listParser,
					adder = adder
				)
                else -> refreshTopMastodon(
					logCaption,
					requester = requester,
					arrayFinder = nullArrayFinder,
					listParser = listParser,
					adder = adder
				)
            }
        }
    }

    // リスト一覧にはページネーションがない
    //			fun getListList(client : TootApiClient, path_base : String) : TootApiResult? {
    //
    //				if(isMisskey) return TootApiResult("misskey support is not yet implemented.")
    //
    //				return TootApiResult("Mastodon's /api/v1/lists has no pagination.")
    //			}

    suspend fun getReportList(
		client: TootApiClient,
		path_base: String
	): TootApiResult? {

        val logCaption = "getReportList"
        val adder: (List<TootReport>, Boolean) -> Unit =
            { src, head -> addAll(list_tmp, src, head = head) }

        val listParser: (parser: TootParser, jsonArray: JsonArray) -> List<TootReport> =
            { _, jsonArray -> parseList(::TootReport, jsonArray) }

        return if (isMisskey) {
            TootApiResult("Misskey has no API to list reports from you.")
        } else {
            // ページングIDはサーバ側の内部IDで、Linkヘッダ以外には露出しない。
            val delimiter = if (-1 != path_base.indexOf('?')) '&' else '?'
            val requester: suspend (first: Boolean, gapIdNewer: EntityId?, gapIdOlder: EntityId?) -> TootApiResult? =
                { first, gapIdNewer, gapIdOlder ->
                    defaultRequesterMastodon(
						client,
						path_base,
						delimiter,
						first,
						useMinId = false,
						gapIdNewer,
						gapIdOlder,
					)
                }

            when {
                bBottom -> refreshBottomMastodon(
					logCaption,
					requester = requester,
					arrayFinder = nullArrayFinder,
					listParser = listParser,
					adder = adder
				)
                else -> refreshTopMastodon(
					logCaption,
					requester = requester,
					arrayFinder = nullArrayFinder,
					listParser = listParser,
					adder = adder
				)
            }
        }
    }

    suspend fun getConversationSummaryList(
		client: TootApiClient,
		path_base: String,
		//	aroundMin : Boolean = false,
		misskeyParams: JsonObject? = null,
	): TootApiResult? {
        val logCaption = "getConversationSummaryList"

        val listParser: (parser: TootParser, jsonArray: JsonArray) -> List<TootConversationSummary> =
            defaultConversationSummaryListParser

        val adder: (List<TootConversationSummary>, Boolean) -> Unit =
            { src, head -> addWithFilterConversationSummary(list_tmp, src, head = head) }

        return if (isMisskey) {

            val params = misskeyParams ?: column.makeMisskeyTimelineParameter(parser)

            val requester: suspend (Boolean) -> TootApiResult? =
                { defaultRequesterMisskey(client, path_base, params, it) }

            when {
                bBottom -> refreshBottomMisskey(
					logCaption,
					requester = requester,
					arrayFinder = nullArrayFinder,
					listParser = listParser,
					adder = adder,
					repeatReading = true
				)
                else -> refreshTopMisskey(
					logCaption,
					requester = requester,
					arrayFinder = nullArrayFinder,
					listParser = listParser,
					adder = adder
				)
            }
        } else {
            val delimiter = if (-1 != path_base.indexOf('?')) '&' else '?'
            val requester: suspend (first: Boolean, gapIdNewer: EntityId?, gapIdOlder: EntityId?) -> TootApiResult? =
                { first, gapIdNewer, gapIdOlder ->
                    defaultRequesterMastodon(
						client,
						path_base,
						delimiter,
						first,
						useMinId = false,
						gapIdNewer,
						gapIdOlder,
					)
                }

            when {
                bBottom -> refreshBottomMastodon(
					logCaption,
					requester = requester,
					arrayFinder = nullArrayFinder,
					listParser = listParser,
					adder = adder,
					repeatReading = true
				)
                else -> refreshTopMastodon(
					logCaption,
					requester = requester,
					arrayFinder = nullArrayFinder,
					listParser = listParser,
					adder
				)
            }
        }
    }

    suspend fun getScheduledStatuses(client: TootApiClient): TootApiResult? {
        val result = client.request(column.addRange(bBottom, Column.PATH_SCHEDULED_STATUSES))
        val src = parseList(::TootScheduled, parser, result?.jsonArray)
        list_tmp = addAll(list_tmp, src)
        column.saveRange(bBottom, !bBottom, result, src)
        return result
    }
}
