package jp.juggler.subwaytooter

import android.app.Activity
import android.app.Dialog
import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.content.res.ColorStateList
import android.content.res.Configuration
import android.graphics.Typeface
import android.net.Uri
import android.os.*
import android.text.InputType
import android.text.Spannable
import android.text.SpannableStringBuilder
import android.util.JsonReader
import android.view.*
import android.view.inputmethod.EditorInfo
import android.widget.*
import androidx.appcompat.app.AlertDialog
import androidx.core.view.GravityCompat
import androidx.drawerlayout.widget.DrawerLayout
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager.widget.ViewPager
import jp.juggler.subwaytooter.action.*
import jp.juggler.subwaytooter.api.*
import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.api.entity.TootStatus.Companion.findStatusIdFromUrl
import jp.juggler.subwaytooter.api.entity.TootTag.Companion.findHashtagFromUrl
import jp.juggler.subwaytooter.dialog.AccountPicker
import jp.juggler.subwaytooter.dialog.ActionsDialog
import jp.juggler.subwaytooter.dialog.DlgQuickTootMenu
import jp.juggler.subwaytooter.dialog.DlgTextInput
import jp.juggler.subwaytooter.notification.PollingWorker
import jp.juggler.subwaytooter.span.MyClickableSpan
import jp.juggler.subwaytooter.table.AcctColor
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.util.*
import jp.juggler.subwaytooter.view.*
import jp.juggler.util.*
import kotlinx.coroutines.delay
import org.apache.commons.io.IOUtils
import org.jetbrains.anko.backgroundDrawable
import org.jetbrains.anko.imageResource
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.InputStreamReader
import java.lang.ref.WeakReference
import java.util.*
import java.util.concurrent.atomic.AtomicReference
import java.util.zip.ZipInputStream
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min

class ActMain : AsyncActivity(), View.OnClickListener,
    ViewPager.OnPageChangeListener, DrawerLayout.DrawerListener {

    class PhoneEnv {

        internal lateinit var pager: MyViewPager
        internal lateinit var pager_adapter: ColumnPagerAdapter
    }

    class TabletEnv {

        internal lateinit var tablet_pager: RecyclerView
        internal lateinit var tablet_pager_adapter: TabletColumnPagerAdapter
        internal lateinit var tablet_layout_manager: LinearLayoutManager
        internal lateinit var tablet_snap_helper: GravitySnapHelper
    }

    companion object {

        val log = LogCategory("ActMain")

        // リザルト
        const val RESULT_APP_DATA_IMPORT = Activity.RESULT_FIRST_USER

        // リクエスト
        const val REQUEST_CODE_COLUMN_LIST = 1
        const val REQUEST_CODE_ACCOUNT_SETTING = 2
        const val REQUEST_APP_ABOUT = 3
        const val REQUEST_CODE_NICKNAME = 4
        const val REQUEST_CODE_POST = 5
        const val REQUEST_CODE_COLUMN_COLOR = 6
        const val REQUEST_CODE_APP_SETTING = 7
        const val REQUEST_CODE_TEXT = 8
        const val REQUEST_CODE_LANGUAGE_FILTER = 9

        const val COLUMN_WIDTH_MIN_DP = 300

        const val STATE_CURRENT_PAGE = "current_page"

        // 外部からインテントを受信した後、アカウント選択中に画面回転したらアカウント選択からやり直す
        internal var sent_intent2: Intent? = null

        // アプリ設定のキャッシュ
        var boostButtonSize = 1
        var replyIconSize = 1
        var headerIconSize = 1
        var stripIconSize = 1
        var screenBottomPadding = 0
        var timeline_font: Typeface = Typeface.DEFAULT
        var timeline_font_bold: Typeface = Typeface.DEFAULT_BOLD

        private fun Float.clipFontSize(): Float =
            if (isNaN()) this else max(1f, this)
    }

    // アプリ設定のキャッシュ
    var density = 0f
    var acct_pad_lr = 0
    var timeline_font_size_sp = Float.NaN
    var acct_font_size_sp = Float.NaN
    var notification_tl_font_size_sp = Float.NaN
    var header_text_size_sp = Float.NaN
    var timeline_spacing: Float? = null
    var avatarIconSize: Int = 0
    var notificationTlIconSize: Int = 0

    // onResume() .. onPause() の間なら真
    private var isResumed = false

    // onStart() .. onStop() の間なら真
    private var isStart_ = false

    // onActivityResultで設定されてonResumeで消化される
    // 状態保存の必要なし
    private var posted_acct: Acct? = null // acctAscii
    private var posted_status_id: EntityId? = null
    private var posted_reply_id: EntityId? = null
    private var posted_redraft_id: EntityId? = null

    // 画面上のUI操作で生成されて
    // onPause,onPageDestroy 等のタイミングで閉じられる
    // 状態保存の必要なし
    internal var listItemPopup: StatusButtonsPopup? = null

    private var phoneEnv: PhoneEnv? = null
    private var tabletEnv: TabletEnv? = null

    private var nScreenColumn: Int = 0
    private var nColumnWidth: Int = 0 // dividerの幅を含む

    private var nAutoCwCellWidth = 0
    private var nAutoCwLines = 0

    private var dlgPrivacyPolicy: WeakReference<Dialog>? = null

    private var quickTootVisibility: TootVisibility = TootVisibility.AccountSetting

    //////////////////////////////////////////////////////////////////
    // 変更しない変数(lateinit)

    private lateinit var llQuickTootBar: LinearLayout
    private lateinit var etQuickToot: MyEditText
    private lateinit var btnQuickToot: ImageButton
    private lateinit var btnQuickTootMenu: ImageButton
    private lateinit var llEmpty: View
    private lateinit var llColumnStrip: ColumnStripLinearLayout
    private lateinit var svColumnStrip: HorizontalScrollView
    private lateinit var btnMenu: ImageButton
    private lateinit var btnToot: ImageButton
    private lateinit var vFooterDivider1: View
    private lateinit var vFooterDivider2: View

    lateinit var drawer: MyDrawerLayout

    lateinit var post_helper: PostHelper

    lateinit var pref: SharedPreferences
    lateinit var handler: Handler
    lateinit var app_state: AppState

    //////////////////////////////////////////////////////////////////
    // 読み取り専用のプロパティ

    val follow_complete_callback: () -> Unit = {
        showToast(false, R.string.follow_succeeded)
    }

    val unfollow_complete_callback: () -> Unit = {
        showToast(false, R.string.unfollow_succeeded)
    }
    val cancel_follow_request_complete_callback: () -> Unit = {
        showToast(false, R.string.follow_request_cancelled)
    }

    val favourite_complete_callback: () -> Unit = {
        showToast(false, R.string.favourite_succeeded)
    }
    val unfavourite_complete_callback: () -> Unit = {
        showToast(false, R.string.unfavourite_succeeded)
    }

    val bookmark_complete_callback: () -> Unit = {
        showToast(false, R.string.bookmark_succeeded)
    }
    val unbookmark_complete_callback: () -> Unit = {
        showToast(false, R.string.unbookmark_succeeded)
    }

    val boost_complete_callback: () -> Unit = {
        showToast(false, R.string.boost_succeeded)
    }

    val unboost_complete_callback: () -> Unit = {
        showToast(false, R.string.unboost_succeeded)
    }

    val reaction_complete_callback: () -> Unit = {
        showToast(false, R.string.reaction_succeeded)
    }

    // 相対時刻の表記を定期的に更新する
    private val proc_updateRelativeTime = object : Runnable {
        override fun run() {
            handler.removeCallbacks(this)
            if (!isStart_) return
            if (Pref.bpRelativeTimestamp(pref)) {
                app_state.columnList.forEach { it.fireRelativeTime() }
                handler.postDelayed(this, 10000L)
            }
        }
    }

    private val link_click_listener: (View, MyClickableSpan) -> Unit = { viewClicked, span ->

        // ビュー階層を下から辿って文脈を取得する
        var column: Column? = null
        var whoRef: TootAccountRef? = null
        var view = viewClicked
        loop@ while (true) {
            when (val tag = view.tag) {
                is ItemViewHolder -> {
                    column = tag.column
                    whoRef = tag.getAccount()
                    break@loop
                }
                is ViewHolderItem -> {
                    column = tag.ivh.column
                    whoRef = tag.ivh.getAccount()
                    break@loop
                }
                is ColumnViewHolder -> {
                    column = tag.column
                    whoRef = null
                    break@loop
                }
                is ViewHolderHeaderBase -> {
                    column = tag.column
                    whoRef = tag.getAccount()
                    break@loop
                }
                is TabletColumnViewHolder -> {
                    column = tag.columnViewHolder.column
                    break@loop
                }
                else -> when (val parent = view.parent) {
                    is View -> view = parent
                    else -> break@loop
                }
            }
        }

        val hashtagList = ArrayList<String>().apply {
            try {
                val cs = viewClicked.cast<TextView>()?.text
                if (cs is Spannable) {
                    for (s in cs.getSpans(0, cs.length, MyClickableSpan::class.java)) {
                        val li = s.linkInfo
                        val pair = li.url.findHashtagFromUrl()
                        if (pair != null) add(if (li.text.startsWith('#')) li.text else "#${pair.first}")
                    }
                }
            } catch (ex: Throwable) {
                log.trace(ex)
            }
        }

        val linkInfo = span.linkInfo

        openCustomTab(
            this,
            nextPosition(column),
            linkInfo.url,
            accessInfo = column?.access_info,
            tagList = hashtagList.notEmpty(),
            whoRef = whoRef,
            linkInfo = linkInfo
        )
    }


    private val dlgQuickTootMenu = DlgQuickTootMenu(this, object : DlgQuickTootMenu.Callback {

        override var visibility: TootVisibility
            get() = quickTootVisibility
            set(value) {
                if (value != quickTootVisibility) {
                    quickTootVisibility = value
                    pref.edit().put(Pref.spQuickTootVisibility, value.id.toString()).apply()
                    showQuickTootVisibility()
                }
            }

        override fun onMacro(text: String) {
            val editable = etQuickToot.text
            if (editable?.isNotEmpty() == true) {
                val start = etQuickToot.selectionStart
                val end = etQuickToot.selectionEnd
                editable.replace(start, end, text)
                etQuickToot.requestFocus()
                etQuickToot.setSelection(start + text.length)
            } else {
                etQuickToot.setText(text)
                etQuickToot.requestFocus()
                etQuickToot.setSelection(text.length)
            }
        }
    })

    val viewPool = RecyclerView.RecycledViewPool()

    // スマホモードなら現在のカラムを、タブレットモードなら-1Lを返す
    // (カラム一覧画面のデフォルト選択位置に使われる)
    val currentColumn: Int
        get() = phoneTab(
            { it.pager.currentItem },
            { -1 }
        )

    // 新しいカラムをどこに挿入するか
    // 現在のページの次の位置か、終端
    val defaultInsertPosition: Int
        get() = phoneTab(
            { it.pager.currentItem + 1 },
            { Integer.MAX_VALUE }
        )

    private val TabletEnv.visibleColumnsIndices: IntRange
        get() {
            var vs = tablet_layout_manager.findFirstVisibleItemPosition()
            var ve = tablet_layout_manager.findLastVisibleItemPosition()
            if (vs == RecyclerView.NO_POSITION || ve == RecyclerView.NO_POSITION) {
                return IntRange(-1, -2) // empty and less than zero
            }

            val child = tablet_layout_manager.findViewByPosition(vs)
            val slide_ratio =
                clipRange(0f, 1f, abs((child?.left ?: 0) / nColumnWidth.toFloat()))
            if (slide_ratio >= 0.95f) {
                ++vs
                ++ve
            }
            return IntRange(vs, min(ve, vs + nScreenColumn - 1))
        }

    private val TabletEnv.visibleColumns: List<Column>
        get() {
            val list = app_state.columnList
            return visibleColumnsIndices.mapNotNull { list.elementAtOrNull(it) }
        }

    // デフォルトの投稿先アカウントを探す。アカウント選択が必要な状況ならnull
    val currentPostTarget: SavedAccount?
        get() = phoneTab(
            { env ->
                val c = env.pager_adapter.getColumn(env.pager.currentItem)
                return when {
                    c == null || c.access_info.isPseudo -> null
                    else -> c.access_info
                }
            },
            { env ->

                val db_id = Pref.lpTabletTootDefaultAccount(App1.pref)
                if (db_id != -1L) {
                    val a = SavedAccount.loadAccount(this@ActMain, db_id)
                    if (a != null && !a.isPseudo) return a
                }

                val accounts = ArrayList<SavedAccount>()
                for (c in env.visibleColumns) {
                    try {
                        val a = c.access_info
                        // 画面内に疑似アカウントがあれば常にアカウント選択が必要
                        if (a.isPseudo) {
                            accounts.clear()
                            break
                        }
                        // 既出でなければ追加する
                        if (null == accounts.find { it == a }) accounts.add(a)
                    } catch (ex: Throwable) {

                    }
                }

                return when (accounts.size) {
                    // 候補が1つだけならアカウント選択は不要
                    1 -> accounts.first()
                    // 候補が2つ以上ならアカウント選択は必要
                    else -> null
                }
            })

    // 簡易投稿入力のテキスト
    val quickTootText: String
        get() = etQuickToot.text.toString()

    //////////////////////////////////////////////////////////////////
    // アクティビティイベント

    override fun onCreate(savedInstanceState: Bundle?) {
        log.d("onCreate")
        super.onCreate(savedInstanceState)
        requestWindowFeature(Window.FEATURE_NO_TITLE)
        App1.setActivityTheme(this, noActionBar = true)

        handler = App1.getAppState(this).handler
        app_state = App1.getAppState(this)
        pref = App1.pref

        EmojiDecoder.handleUnicodeEmoji = Pref.bpInAppUnicodeEmoji(pref)

        density = app_state.density
        acct_pad_lr = (0.5f + 4f * density).toInt()

        timeline_font_size_sp = Pref.fpTimelineFontSize(pref).clipFontSize()
        acct_font_size_sp = Pref.fpAcctFontSize(pref).clipFontSize()
        notification_tl_font_size_sp = Pref.fpNotificationTlFontSize(pref).clipFontSize()
        header_text_size_sp = Pref.fpHeaderTextSize(pref).clipFontSize()

        val fv = Pref.spTimelineSpacing(pref).toFloatOrNull()
        timeline_spacing = if (fv != null && fv.isFinite() && fv != 0f) fv else null

        initUI()

        updateColumnStrip()

        if (app_state.columnCount > 0) {

            val column_pos = Pref.ipLastColumnPos(pref)
            log.d("ipLastColumnPos load $column_pos")

            // 前回最後に表示していたカラムの位置にスクロールする
            if (column_pos in 0 until app_state.columnCount) {
                scrollToColumn(column_pos, false)
            }

            // 表示位置に合わせたイベントを発行
            phoneTab(
                { env -> onPageSelected(env.pager.currentItem) },
                { env -> resizeColumnWidth(env) }
            )
        }

        PollingWorker.queueUpdateNotification(this)

        if (savedInstanceState != null) {
            sent_intent2?.let { handleSentIntent(it) }
        }

        checkPrivacyPolicy()
    }

    override fun onDestroy() {
        log.d("onDestroy")
        super.onDestroy()
        post_helper.onDestroy()

        // このアクティビティに関連する ColumnViewHolder への参照を全カラムから除去する
        app_state.columnList.forEach {
            it.removeColumnViewHolderByActivity(this)
        }
    }

    override fun onNewIntent(intent: Intent?) {
        super.onNewIntent(intent)
        log.w("onNewIntent: isResumed=$isResumed")
    }

    override fun onConfigurationChanged(newConfig: Configuration) {
        log.w("onConfigurationChanged")
        super.onConfigurationChanged(newConfig)
        if (newConfig.screenHeightDp > 0 || newConfig.screenHeightDp > 0) {
            tabOnly { env -> resizeColumnWidth(env) }
        }
    }

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)

        log.d("onSaveInstanceState")

        phoneTab(
            { env -> outState.putInt(STATE_CURRENT_PAGE, env.pager.currentItem) },
            { env ->
                env.tablet_layout_manager.findLastVisibleItemPosition()
                    .takeIf { it != RecyclerView.NO_POSITION }
                    ?.let { outState.putInt(STATE_CURRENT_PAGE, it) }
            }
        )

        app_state.columnList.forEach { it.saveScrollPosition() }
    }

    override fun onRestoreInstanceState(savedInstanceState: Bundle) {
        log.d("onRestoreInstanceState")
        super.onRestoreInstanceState(savedInstanceState)
        val pos = savedInstanceState.getInt(STATE_CURRENT_PAGE)
        // 注意：開始は0じゃなく1
        if (pos in 1 until app_state.columnCount) {
            phoneTab(
                { env -> env.pager.currentItem = pos },
                { env ->
                    env.tablet_layout_manager
                        .smoothScrollToPosition(env.tablet_pager, null, pos)
                }
            )
        }
    }

    override fun onStart() {
        val tsTotal = SystemClock.elapsedRealtime()
        super.onStart()

        isStart_ = true
        log.d("onStart")

        var ts = SystemClock.elapsedRealtime()
        var te: Long

        // カラーカスタマイズを読み直す
        ListDivider.color = Pref.ipListDividerColor(pref)
        TabletColumnDivider.color = Pref.ipListDividerColor(pref)
        ItemViewHolder.toot_color_unlisted = Pref.ipTootColorUnlisted(pref)
        ItemViewHolder.toot_color_follower = Pref.ipTootColorFollower(pref)
        ItemViewHolder.toot_color_direct_user = Pref.ipTootColorDirectUser(pref)
        ItemViewHolder.toot_color_direct_me = Pref.ipTootColorDirectMe(pref)
        MyClickableSpan.showLinkUnderline = Pref.bpShowLinkUnderline(pref)
        MyClickableSpan.defaultLinkColor = Pref.ipLinkColor(pref).notZero()
            ?: attrColor(R.attr.colorLink)

        CustomShare.reloadCache(this, pref)

        te = SystemClock.elapsedRealtime()
        if (te - ts >= 100L) log.w("onStart: ${te - ts}ms : reload color")
        ts = SystemClock.elapsedRealtime()

        var tz = TimeZone.getDefault()
        try {
            val tz_id = Pref.spTimeZone(pref)
            if (tz_id.isNotEmpty()) {
                tz = TimeZone.getTimeZone(tz_id)
            }
        } catch (ex: Throwable) {
            log.e(ex, "getTimeZone failed.")
        }
        TootStatus.date_format.timeZone = tz

        te = SystemClock.elapsedRealtime()
        if (te - ts >= 100L) log.w("onStart: ${te - ts}ms : reload timezone")
        ts = SystemClock.elapsedRealtime()

        // バグいアカウントデータを消す
        try {
            SavedAccount.sweepBuggieData()
        } catch (ex: Throwable) {
            log.trace(ex)
        }

        te = SystemClock.elapsedRealtime()
        if (te - ts >= 100L) log.w("onStart: ${te - ts}ms : sweepBuggieData")
        ts = SystemClock.elapsedRealtime()

        // アカウント設定から戻ってきたら、カラムを消す必要があるかもしれない
        val new_order = app_state.columnList
            .mapIndexedNotNull { index, column ->
                if (!column.access_info.isNA && null == SavedAccount.loadAccount(
                        this@ActMain,
                        column.access_info.db_id
                    )
                ) {
                    null
                } else {
                    index
                }
            }

        if (new_order.size != app_state.columnCount) {
            setOrder(new_order)
        }

        te = SystemClock.elapsedRealtime()
        if (te - ts >= 100L) log.w("onStart: ${te - ts}ms : column order")
        ts = SystemClock.elapsedRealtime()

        // 背景画像を表示しない設定が変更された時にカラムの背景を設定しなおす
        app_state.columnList.forEach { column ->
            column.viewHolder?.lastAnnouncementShown = 0L
            column.fireColumnColor()
        }

        te = SystemClock.elapsedRealtime()
        if (te - ts >= 100L) log.w("onStart: ${te - ts}ms :fireColumnColor")
        ts = SystemClock.elapsedRealtime()

        // 各カラムのアカウント設定を読み直す
        reloadAccountSetting()

        te = SystemClock.elapsedRealtime()
        if (te - ts >= 100L) log.w("onStart: ${te - ts}ms :reloadAccountSetting")
        ts = SystemClock.elapsedRealtime()

        // 投稿直後ならカラムの再取得を行う
        refreshAfterPost()

        te = SystemClock.elapsedRealtime()
        if (te - ts >= 100L) log.w("onStart: ${te - ts}ms :refreshAfterPost")
        ts = SystemClock.elapsedRealtime()

        // 画面復帰時に再取得などを行う
        app_state.columnList.forEach { it.onStart() }

        te = SystemClock.elapsedRealtime()
        if (te - ts >= 100L) log.w("onStart: ${te - ts}ms :column.onStart")
        ts = SystemClock.elapsedRealtime()

        // 画面復帰時にストリーミング接続を開始する
        app_state.streamManager.onScreenStart()

        te = SystemClock.elapsedRealtime()
        if (te - ts >= 100L) log.w("onStart: ${te - ts}ms :multi_stream_reader.onScreenStart")
        ts = SystemClock.elapsedRealtime()

        // カラムの表示範囲インジケータを更新
        updateColumnStripSelection(-1, -1f)

        te = SystemClock.elapsedRealtime()
        if (te - ts >= 100L) log.w("onStart: ${te - ts}ms :updateColumnStripSelection")
        ts = SystemClock.elapsedRealtime()

        app_state.columnList.forEach {
            it.fireShowContent(
                reason = "ActMain onStart",
                reset = true
            )
        }

        te = SystemClock.elapsedRealtime()
        if (te - ts >= 100L) log.w("onStart: ${te - ts}ms :fireShowContent")

        // 相対時刻表示
        proc_updateRelativeTime.run()


        te = SystemClock.elapsedRealtime()
        if (te - tsTotal >= 100L) log.w("onStart: ${te - tsTotal}ms : total")

        app_state.enableSpeech()
    }

    override fun onStop() {

        log.d("onStop")

        isStart_ = false

        handler.removeCallbacks(proc_updateRelativeTime)

        post_helper.closeAcctPopup()

        closeListItemPopup()

        app_state.streamManager.onScreenStop()

        app_state.columnList.forEach { it.saveScrollPosition() }

        app_state.saveColumnList(bEnableSpeech = false)

        super.onStop()

    }

    override fun onResume() {
        log.d("onResume")
        isResumed = true

        super.onResume()
        /*
        super.onResume() から呼ばれる isTopOfTask() が android.os.RemoteException 例外をたまに出すが、放置することにした。

        java.lang.RuntimeException:
        at android.app.ActivityThread.performResumeActivity (ActivityThread.java:4430)
        at android.app.ActivityThread.handleResumeActivity (ActivityThread.java:4470)
        Caused by: java.lang.IllegalArgumentException:
        at android.os.Parcel.createException (Parcel.java:1957)
        at android.os.Parcel.readException (Parcel.java:1921)
        at android.os.Parcel.readException (Parcel.java:1871)
        at android.app.IActivityManager$Stub$Proxy.isTopOfTask (IActivityManager.java:7912)
        at android.app.Activity.isTopOfTask (Activity.java:6724)
        at android.app.Activity.onResume (Activity.java:1425)
        at androidx.fragment.app.FragmentActivity.onResume (FragmentActivity.java:456)
        at jp.juggler.subwaytooter.ActMain.onResume (ActMain.kt:685)
        at android.app.Instrumentation.callActivityOnResume (Instrumentation.java:1456)
        at android.app.Activity.performResume (Activity.java:7614)
        at android.app.ActivityThread.performResumeActivity (ActivityThread.java:4412)
        Caused by: android.os.RemoteException:
        at com.android.server.am.ActivityManagerService.isTopOfTask (ActivityManagerService.java:16128)
        at android.app.IActivityManager$Stub.onTransact (IActivityManager.java:2376)
        at com.android.server.am.ActivityManagerService.onTransact (ActivityManagerService.java:3648)
        at com.android.server.am.HwActivityManagerService.onTransact (HwActivityManagerService.java:609)
        at android.os.Binder.execTransact (Binder.java:739)
        */

        MyClickableSpan.link_callback = WeakReference(link_click_listener)

        if (Pref.bpDontScreenOff(pref)) {
            window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
        } else {
            window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
        }

        // 外部から受け取ったUriの処理
        val uri = ActCallback.last_uri.getAndSet(null)
        if (uri != null) {
            handleIntentUri(uri)
        }

        // 外部から受け取ったUriの処理
        val intent = ActCallback.sent_intent.getAndSet(null)
        if (intent != null) {
            handleSentIntent(intent)
        }

    }

    override fun onPause() {
        log.d("onPause")
        isResumed = false

        // 最後に表示していたカラムの位置
        val last_pos = phoneTab(
            { env -> env.pager.currentItem },
            { env -> env.visibleColumnsIndices.first })
        log.d("ipLastColumnPos save $last_pos")
        pref.edit().put(Pref.ipLastColumnPos, last_pos).apply()

        app_state.columnList.forEach { it.saveScrollPosition() }

        app_state.saveColumnList(bEnableSpeech = false)

        super.onPause()
    }

    //////////////////////////////////////////////////////////////////
    // UIイベント

    override fun onPageScrollStateChanged(state: Int) {
    }

    override fun onPageScrolled(
        position: Int,
        positionOffset: Float,
        positionOffsetPixels: Int
    ) {
        updateColumnStripSelection(position, positionOffset)
    }

    override fun onPageSelected(position: Int) {
        handler.post {
            app_state.column(position)?.let { column ->
                if (!column.bFirstInitialized) {
                    column.startLoading()
                }
                scrollColumnStrip(position)
                post_helper.setInstance(
                    when {
                        column.access_info.isNA -> null
                        else -> column.access_info
                    }
                )
            }
        }
    }

    override fun onClick(v: View) {
        when (v.id) {
            R.id.btnMenu -> if (!drawer.isDrawerOpen(GravityCompat.START)) {
                drawer.openDrawer(GravityCompat.START)
            }

            R.id.btnToot -> Action_Account.openPost(this)
            R.id.btnQuickToot -> performQuickPost(null)
            R.id.btnQuickTootMenu -> performQuickTootMenu()
        }
    }

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

    // スマホモードとタブレットモードでコードを切り替える
    private inline fun <R> phoneTab(
        codePhone: (PhoneEnv) -> R,
        codeTablet: (TabletEnv) -> R
    ): R {

        val pe = phoneEnv
        if (pe != null) return codePhone(pe)

        val te = tabletEnv
        if (te != null) return codeTablet(te)

        throw RuntimeException("missing phoneEnv or tabletEnv")
    }

    // スマホモードならラムダを実行する。タブレットモードならnullを返す
    private inline fun <R> phoneOnly(code: (PhoneEnv) -> R): R? {
        val pe = phoneEnv
        return if (pe != null) code(pe) else null
    }

    // タブレットモードならラムダを実行する。スマホモードならnullを返す
    @Suppress("unused")
    private inline fun <R> tabOnly(code: (TabletEnv) -> R): R? {
        val te = tabletEnv
        return if (te != null) code(te) else null
    }

    // 新しいカラムをどこに挿入するか
    // カラムの次の位置か、現在のページの次の位置か、終端
    fun nextPosition(column: Column?): Int =
        app_state.columnIndex(column)?.let { it + 1 } ?: defaultInsertPosition

    private fun showQuickTootVisibility() {
        btnQuickTootMenu.imageResource =
            when (val resId = Styler.getVisibilityIconId(false, quickTootVisibility)) {
                R.drawable.ic_question -> R.drawable.ic_description
                else -> resId
            }
    }

    private fun performQuickTootMenu() {
        dlgQuickTootMenu.toggle()
    }

    private fun refreshAfterPost() {
        val posted_acct = this.posted_acct
        val posted_status_id = this.posted_status_id

        if (posted_acct != null && posted_status_id == null) {
            // 予約投稿なら予約投稿リストをリロードする
            app_state.columnList.forEach { column ->
                if (column.type == ColumnType.SCHEDULED_STATUS
                    && column.access_info.acct == posted_acct
                ) {
                    column.startLoading()
                }

            }

        } else if (posted_acct != null && posted_status_id != null) {

            val posted_redraft_id = this.posted_redraft_id
            if (posted_redraft_id != null) {
                val host = posted_acct.host
                if (host != null) {
                    app_state.columnList.forEach {
                        it.onStatusRemoved(host, posted_redraft_id)
                    }
                }
                this.posted_redraft_id = null
            }

            val refresh_after_toot = Pref.ipRefreshAfterToot(pref)
            if (refresh_after_toot != Pref.RAT_DONT_REFRESH) {
                app_state.columnList
                    .filter { it.access_info.acct == posted_acct }
                    .forEach {
                        it.startRefreshForPost(
                            refresh_after_toot,
                            posted_status_id,
                            posted_reply_id
                        )
                    }

            }
        }
        this.posted_acct = null
        this.posted_status_id = null
    }

    private fun handleSentIntent(intent: Intent) {
        sent_intent2 = intent

        // Galaxy S8+ で STのSSを取った後に出るポップアップからそのまま共有でSTを選ぶと何も起きない問題への対策
        handler.post {
            AccountPicker.pick(
                this,
                bAllowPseudo = false,
                bAuto = true,
                message = getString(R.string.account_picker_toot),
                dismiss_callback = { sent_intent2 = null }
            ) { ai ->
                sent_intent2 = null
                ActPost.open(this@ActMain, REQUEST_CODE_POST, ai.db_id, sent_intent = intent)
            }
        }
    }

    fun closeListItemPopup() {
        try {
            listItemPopup?.dismiss()
        } catch (ignored: Throwable) {
        }
        listItemPopup = null
    }

    private fun performQuickPost(account: SavedAccount?) {
        if (account == null) {
            val a = if (tabletEnv != null && !Pref.bpQuickTootOmitAccountSelection(pref)) {
                // タブレットモードでオプションが無効なら
                // 簡易投稿は常にアカウント選択する
                null
            } else {
                currentPostTarget
            }

            if (a != null && !a.isPseudo) {
                performQuickPost(a)
            } else {
                // アカウントを選択してやり直し
                AccountPicker.pick(
                    this,
                    bAllowPseudo = false,
                    bAuto = true,
                    message = getString(R.string.account_picker_toot)
                ) { ai -> performQuickPost(ai) }
            }
            return
        }

        post_helper.content = etQuickToot.text.toString().trim { it <= ' ' }
        post_helper.spoiler_text = null

        post_helper.visibility = when (quickTootVisibility) {
            TootVisibility.AccountSetting -> account.visibility
            else -> quickTootVisibility
        }

        post_helper.bNSFW = false
        post_helper.in_reply_to_id = null
        post_helper.attachment_list = null
        post_helper.emojiMapCustom =
            App1.custom_emoji_lister.getMap(account)


        etQuickToot.hideKeyboard()

        post_helper.post(account, callback = object : PostHelper.PostCompleteCallback {
            override fun onPostComplete(
                target_account: SavedAccount,
                status: TootStatus
            ) {
                etQuickToot.setText("")
                posted_acct = target_account.acct
                posted_status_id = status.id
                posted_reply_id = status.in_reply_to_id
                posted_redraft_id = null
                refreshAfterPost()
            }

            override fun onScheduledPostComplete(target_account: SavedAccount) {
            }
        })
    }

    private fun isOrderChanged(new_order: List<Int>): Boolean {
        if (new_order.size != app_state.columnCount) return true
        for (i in new_order.indices) {
            if (new_order[i] != i) return true
        }
        return false
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        log.d("onActivityResult req=$requestCode res=$resultCode data=$data")

        if (resultCode == Activity.RESULT_OK) {
            when (requestCode) {
                REQUEST_CODE_COLUMN_LIST -> if (data != null) {
                    val order = data.getIntegerArrayListExtra(ActColumnList.EXTRA_ORDER)
                    if (order != null && isOrderChanged(order)) {
                        setOrder(order)
                    }

                    val select = data.getIntExtra(ActColumnList.EXTRA_SELECTION, -1)
                    if (select in 0 until app_state.columnCount) {
                        scrollToColumn(select)
                    }
                }

                REQUEST_APP_ABOUT -> if (data != null) {
                    val search = data.getStringExtra(ActAbout.EXTRA_SEARCH)
                    if (search?.isNotEmpty() == true) {
                        Action_Account.timeline(
                            this@ActMain,
                            defaultInsertPosition,
                            ColumnType.SEARCH,
                            args = arrayOf(search, true)
                        )
                    }
                    return
                }

                REQUEST_CODE_NICKNAME -> {
                    updateColumnStrip()
                    app_state.columnList.forEach { it.fireShowColumnHeader() }
                }

                REQUEST_CODE_POST -> if (data != null) {
                    etQuickToot.setText("")
                    posted_acct =
                        data.getStringExtra(ActPost.EXTRA_POSTED_ACCT)?.let { Acct.parse(it) }
                    if (data.extras?.containsKey(ActPost.EXTRA_POSTED_STATUS_ID) == true) {
                        posted_status_id = EntityId.from(data, ActPost.EXTRA_POSTED_STATUS_ID)
                        posted_reply_id = EntityId.from(data, ActPost.EXTRA_POSTED_REPLY_ID)
                        posted_redraft_id = EntityId.from(data, ActPost.EXTRA_POSTED_REDRAFT_ID)
                    } else {
                        posted_status_id = null
                    }
                }

                REQUEST_CODE_COLUMN_COLOR -> if (data != null) {
                    app_state.saveColumnList()
                    val idx = data.getIntExtra(ActColumnCustomize.EXTRA_COLUMN_INDEX, 0)
                    app_state.column(idx)?.let {
                        it.fireColumnColor()
                        it.fireShowContent(
                            reason = "ActMain column color changed",
                            reset = true
                        )
                    }
                    updateColumnStrip()
                }

                REQUEST_CODE_LANGUAGE_FILTER -> if (data != null) {
                    app_state.saveColumnList()
                    val idx = data.getIntExtra(ActLanguageFilter.EXTRA_COLUMN_INDEX, 0)
                    app_state.column(idx)?.onLanguageFilterChanged()
                }
            }
        }

        when (requestCode) {

            REQUEST_CODE_ACCOUNT_SETTING -> {
                updateColumnStrip()

                app_state.columnList.forEach { it.fireShowColumnHeader() }

                when (resultCode) {
                    RESULT_OK -> data?.data?.let { openBrowser(it) }

                    ActAccountSetting.RESULT_INPUT_ACCESS_TOKEN ->
                        data?.getLongExtra(ActAccountSetting.EXTRA_DB_ID, -1L)
                            ?.takeIf { it != -1L }?.let { checkAccessToken2(it) }
                }
            }

            REQUEST_CODE_APP_SETTING -> {
                Column.reloadDefaultColor(this, pref)
                showFooterColor()
                updateColumnStrip()

                if (resultCode == RESULT_APP_DATA_IMPORT) {
                    data?.data?.let { importAppData(it) }
                }
            }

            REQUEST_CODE_TEXT -> when (resultCode) {
                ActText.RESULT_SEARCH_MSP -> searchFromActivityResult(data, ColumnType.SEARCH_MSP)
                ActText.RESULT_SEARCH_TS -> searchFromActivityResult(data, ColumnType.SEARCH_TS)
                ActText.RESULT_SEARCH_NOTESTOCK -> searchFromActivityResult(
                    data,
                    ColumnType.SEARCH_NOTESTOCK
                )
            }
        }

        super.onActivityResult(requestCode, resultCode, data)
    }

    override fun onBackPressed() {

        // メニューが開いていたら閉じる
        if (drawer.isDrawerOpen(GravityCompat.START)) {
            drawer.closeDrawer(GravityCompat.START)
            return
        }

        // カラムが0個ならアプリを終了する
        if (app_state.columnCount == 0) {
            finish()
            return
        }

        // カラム設定が開いているならカラム設定を閉じる
        if (closeColumnSetting()) {
            return
        }

        fun getClosableColumnList(): List<Column> {
            val visibleColumnList = ArrayList<Column>()
            phoneTab({ env ->
                try {
                    app_state.column(env.pager.currentItem)?.addTo(visibleColumnList)
                } catch (ex: Throwable) {
                }
            }, { env ->
                visibleColumnList.addAll(env.visibleColumns)
            })

            return visibleColumnList.filter { !it.dont_close }

        }

        // カラムが1個以上ある場合は設定に合わせて挙動を変える
        when (Pref.ipBackButtonAction(pref)) {

            Pref.BACK_EXIT_APP -> this@ActMain.finish()

            Pref.BACK_OPEN_COLUMN_LIST -> Action_App.columnList(this@ActMain)

            Pref.BACK_CLOSE_COLUMN -> {

                val closeableColumnList = getClosableColumnList()
                when (closeableColumnList.size) {
                    0 -> {
                        if (Pref.bpExitAppWhenCloseProtectedColumn(pref)
                            && Pref.bpDontConfirmBeforeCloseColumn(pref)
                        ) {
                            this@ActMain.finish()
                        } else {
                            showToast(false, R.string.missing_closeable_column)
                        }
                    }

                    1 -> {
                        closeColumn(closeableColumnList.first())
                    }

                    else -> {
                        showToast(
                            false,
                            R.string.cant_close_column_by_back_button_when_multiple_column_shown
                        )
                    }
                }
            }

            // ActAppSetting.BACK_ASK_ALWAYS
            else -> {

                val closeableColumnList = getClosableColumnList()

                val dialog = ActionsDialog()


                if (closeableColumnList.size == 1) {
                    val column = closeableColumnList.first()
                    dialog.addAction(getString(R.string.close_column)) {
                        closeColumn(column, bConfirmed = true)
                    }
                }

                dialog.addAction(getString(R.string.open_column_list)) { Action_App.columnList(this@ActMain) }
                dialog.addAction(getString(R.string.app_exit)) { this@ActMain.finish() }
                dialog.show(this, null)
            }
        }
    }

    internal fun initUI() {
        setContentView(R.layout.act_main)
        App1.initEdgeToEdge(this)

        quickTootVisibility =
            TootVisibility.parseSavedVisibility(Pref.spQuickTootVisibility(pref))
                ?: quickTootVisibility


        Column.reloadDefaultColor(this, pref)

        var sv = Pref.spTimelineFont(pref)
        if (sv.isNotEmpty()) {
            try {
                timeline_font = Typeface.createFromFile(sv)
            } catch (ex: Throwable) {
                log.trace(ex)
            }
        }

        sv = Pref.spTimelineFontBold(pref)
        if (sv.isNotEmpty()) {
            try {
                timeline_font_bold = Typeface.createFromFile(sv)
            } catch (ex: Throwable) {
                log.trace(ex)
            }

        } else {
            try {
                timeline_font_bold = Typeface.create(timeline_font, Typeface.BOLD)
            } catch (ex: Throwable) {
                log.trace(ex)
            }
        }

        fun parseIconSize(stringPref: StringPref, minDp: Float = 1f): Int {
            var icon_size_dp = stringPref.defVal.toFloat()
            try {
                sv = stringPref(pref)
                val fv = if (sv.isEmpty()) Float.NaN else sv.toFloat()
                if (fv.isFinite() && fv >= minDp) {
                    icon_size_dp = fv
                }
            } catch (ex: Throwable) {
                log.trace(ex)
            }
            return (0.5f + icon_size_dp * density).toInt()
        }

        avatarIconSize = parseIconSize(Pref.spAvatarIconSize)
        notificationTlIconSize = parseIconSize(Pref.spNotificationTlIconSize)
        boostButtonSize = parseIconSize(Pref.spBoostButtonSize)
        replyIconSize = parseIconSize(Pref.spReplyIconSize)
        headerIconSize = parseIconSize(Pref.spHeaderIconSize)
        stripIconSize = parseIconSize(Pref.spStripIconSize)
        screenBottomPadding = parseIconSize(Pref.spScreenBottomPadding, minDp = 0f)

        run {
            var round_ratio = 33f
            try {
                if (Pref.bpDontRound(pref)) {
                    round_ratio = 0f
                } else {
                    sv = Pref.spRoundRatio(pref)
                    if (sv.isNotEmpty()) {
                        val fv = sv.toFloat()
                        if (fv.isFinite()) {
                            round_ratio = fv
                        }
                    }
                }
            } catch (ex: Throwable) {
                log.trace(ex)
            }
            Styler.round_ratio = clipRange(0f, 1f, round_ratio / 100f) * 0.5f
        }

        run {
            var boost_alpha = 0.8f
            try {
                val f = (Pref.spBoostAlpha.toInt(pref).toFloat() + 0.5f) / 100f
                boost_alpha = when {
                    f >= 1f -> 1f
                    f < 0f -> 0.66f
                    else -> f
                }
            } catch (ex: Throwable) {
                log.trace(ex)
            }
            Styler.boost_alpha = boost_alpha
        }

        llEmpty = findViewById(R.id.llEmpty)

        drawer = findViewById(R.id.drawer_layout)
        drawer.addDrawerListener(this)

        drawer.setExclusionSize(stripIconSize)





        SideMenuAdapter(this, handler, findViewById(R.id.nav_view), drawer)

        btnMenu = findViewById(R.id.btnMenu)
        btnToot = findViewById(R.id.btnToot)
        vFooterDivider1 = findViewById(R.id.vFooterDivider1)
        vFooterDivider2 = findViewById(R.id.vFooterDivider2)
        llColumnStrip = findViewById(R.id.llColumnStrip)
        svColumnStrip = findViewById(R.id.svColumnStrip)
        llQuickTootBar = findViewById(R.id.llQuickTootBar)
        etQuickToot = findViewById(R.id.etQuickToot)
        btnQuickToot = findViewById(R.id.btnQuickToot)
        btnQuickTootMenu = findViewById(R.id.btnQuickTootMenu)

        val llFormRoot: LinearLayout = findViewById(R.id.llFormRoot)

        llFormRoot.setPadding(0, 0, 0, screenBottomPadding)

        etQuickToot.typeface = timeline_font

        when (Pref.ipJustifyWindowContentPortrait(pref)) {
            Pref.JWCP_START -> {
                val iconW = (stripIconSize * 1.5f + 0.5f).toInt()
                val padding = resources.displayMetrics.widthPixels / 2 - iconW

                fun ViewGroup.addViewBeforeLast(v: View) = addView(v, childCount - 1)
                (svColumnStrip.parent as LinearLayout).addViewBeforeLast(
                    View(this).apply {
                        layoutParams = LinearLayout.LayoutParams(padding, 0)
                    }
                )
                llQuickTootBar.addViewBeforeLast(
                    View(this).apply {
                        layoutParams = LinearLayout.LayoutParams(padding, 0)
                    }
                )
            }

            Pref.JWCP_END -> {
                val iconW = (stripIconSize * 1.5f + 0.5f).toInt()
                val borderWidth = (1f * density + 0.5f).toInt()
                val padding = resources.displayMetrics.widthPixels / 2 - iconW - borderWidth

                fun ViewGroup.addViewAfterFirst(v: View) = addView(v, 1)
                (svColumnStrip.parent as LinearLayout).addViewAfterFirst(
                    View(this).apply {
                        layoutParams = LinearLayout.LayoutParams(padding, 0)
                    }
                )
                llQuickTootBar.addViewAfterFirst(
                    View(this).apply {
                        layoutParams = LinearLayout.LayoutParams(padding, 0)
                    }
                )
            }
        }

        if (!Pref.bpQuickTootBar(pref)) {
            llQuickTootBar.visibility = View.GONE
        }

        btnToot.setOnClickListener(this)
        btnMenu.setOnClickListener(this)
        btnQuickToot.setOnClickListener(this)
        btnQuickTootMenu.setOnClickListener(this)

        if (Pref.bpDontUseActionButtonWithQuickTootBar(pref)) {
            etQuickToot.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_MULTI_LINE
            etQuickToot.imeOptions = EditorInfo.IME_ACTION_NONE
            // 最後に指定する必要がある？
            etQuickToot.maxLines = 5
            etQuickToot.isVerticalScrollBarEnabled = true
            etQuickToot.isScrollbarFadingEnabled = false
        } else {
            etQuickToot.inputType = InputType.TYPE_CLASS_TEXT
            etQuickToot.imeOptions = EditorInfo.IME_ACTION_SEND
            etQuickToot.setOnEditorActionListener(TextView.OnEditorActionListener { _, actionId, _ ->
                if (actionId == EditorInfo.IME_ACTION_SEND) {
                    btnQuickToot.performClick()
                    return@OnEditorActionListener true
                }
                false
            })
            // 最後に指定する必要がある？
            etQuickToot.maxLines = 1
        }

        svColumnStrip.isHorizontalFadingEdgeEnabled = true

        post_helper = PostHelper(this, pref, app_state.handler)

        val dm = resources.displayMetrics

        val density = dm.density

        var media_thumb_height = 64
        sv = Pref.spMediaThumbHeight(pref)
        if (sv.isNotEmpty()) {
            try {
                val iv = Integer.parseInt(sv)
                if (iv >= 32) {
                    media_thumb_height = iv
                }
            } catch (ex: Throwable) {
                log.trace(ex)
            }

        }
        app_state.media_thumb_height = (0.5f + media_thumb_height * density).toInt()

        var column_w_min_dp = COLUMN_WIDTH_MIN_DP
        sv = Pref.spColumnWidth(pref)
        if (sv.isNotEmpty()) {
            try {
                val iv = Integer.parseInt(sv)
                if (iv >= 100) {
                    column_w_min_dp = iv
                }
            } catch (ex: Throwable) {
                log.trace(ex)
            }

        }
        val column_w_min = (0.5f + column_w_min_dp * density).toInt()

        val sw = dm.widthPixels


        if (Pref.bpDisableTabletMode(pref) || sw < column_w_min * 2) {
            // SmartPhone mode
            phoneEnv = PhoneEnv()
        } else {
            // Tablet mode
            tabletEnv = TabletEnv()
        }

        val tmpPhonePager: MyViewPager = findViewById(R.id.viewPager)
        val tmpTabletPager: RecyclerView = findViewById(R.id.rvPager)

        phoneTab({ env ->
            tmpTabletPager.visibility = View.GONE
            env.pager = tmpPhonePager
            env.pager_adapter = ColumnPagerAdapter(this)
            env.pager.adapter = env.pager_adapter
            env.pager.addOnPageChangeListener(this)

            resizeAutoCW(sw)

        }, { env ->
            tmpPhonePager.visibility = View.GONE
            env.tablet_pager = tmpTabletPager
            env.tablet_pager_adapter = TabletColumnPagerAdapter(this)
            env.tablet_layout_manager =
                LinearLayoutManager(
                    this,
                    LinearLayoutManager.HORIZONTAL,
                    false
                )

            if (env.tablet_pager.itemDecorationCount == 0) {
                env.tablet_pager.addItemDecoration(TabletColumnDivider(this@ActMain))
            }


            env.tablet_pager.adapter = env.tablet_pager_adapter
            env.tablet_pager.layoutManager = env.tablet_layout_manager
            env.tablet_pager.addOnScrollListener(object :
                RecyclerView.OnScrollListener() {

                override fun onScrollStateChanged(
                    recyclerView: RecyclerView,
                    newState: Int
                ) {
                    super.onScrollStateChanged(recyclerView, newState)

                    val vs = env.tablet_layout_manager.findFirstVisibleItemPosition()
                    val ve = env.tablet_layout_manager.findLastVisibleItemPosition()
                    // 端に近い方に合わせる
                    val distance_left = abs(vs)
                    val distance_right = abs(app_state.columnCount - 1 - ve)
                    if (distance_left < distance_right) {
                        scrollColumnStrip(vs)
                    } else {
                        scrollColumnStrip(ve)
                    }
                }

                override fun onScrolled(
                    recyclerView: RecyclerView,
                    dx: Int,
                    dy: Int
                ) {
                    super.onScrolled(recyclerView, dx, dy)
                    updateColumnStripSelection(-1, -1f)
                }
            })

            env.tablet_pager.itemAnimator = null
            //			val animator = env.tablet_pager.itemAnimator
            //			if( animator is DefaultItemAnimator){
            //				animator.supportsChangeAnimations = false
            //			}

            env.tablet_snap_helper = GravitySnapHelper(Gravity.START)
            env.tablet_snap_helper.attachToRecyclerView(env.tablet_pager)

        })

        showFooterColor()

        post_helper.attachEditText(
            llFormRoot,
            etQuickToot,
            true,
            object : PostHelper.Callback2 {
                override fun onTextUpdate() {}

                override fun canOpenPopup(): Boolean {
                    return !drawer.isDrawerOpen(GravityCompat.START)
                }
            })

        showQuickTootVisibility()
    }

    private fun isVisibleColumn(idx: Int) = phoneTab(
        { env ->
            val c = env.pager.currentItem
            c == idx
        }, { env ->
            idx >= 0 && idx in env.visibleColumnsIndices
        }
    )

    private fun updateColumnStrip() {
        llEmpty.vg(app_state.columnCount == 0)

        val iconSize = stripIconSize
        val rootW = (iconSize * 1.25f + 0.5f).toInt()
        val rootH = (iconSize * 1.5f + 0.5f).toInt()
        val iconTopMargin = (iconSize * 0.125f + 0.5f).toInt()
        val barHeight = (iconSize * 0.094f + 0.5f).toInt()
        val barTopMargin = (iconSize * 0.094f + 0.5f).toInt()

        // 両端のメニューと投稿ボタンの大きさ
        val pad = (rootH - iconSize) shr 1
        for (btn in arrayOf(btnToot, btnMenu, btnQuickTootMenu, btnQuickToot)) {
            btn.layoutParams.width = rootH // not W
            btn.layoutParams.height = rootH
            btn.setPaddingRelative(pad, pad, pad, pad)
        }

        llColumnStrip.removeAllViews()
        app_state.columnList.forEachIndexed { index, column ->


            val viewRoot = layoutInflater.inflate(R.layout.lv_column_strip, llColumnStrip, false)
            val ivIcon = viewRoot.findViewById<ImageView>(R.id.ivIcon)
            val vAcctColor = viewRoot.findViewById<View>(R.id.vAcctColor)

            // root: 48x48dp LinearLayout(vertical), gravity=center
            viewRoot.layoutParams.width = rootW
            viewRoot.layoutParams.height = rootH

            // ivIcon: 32x32dp marginTop="4dp" 図柄が32x32dp、パディングなし
            ivIcon.layoutParams.width = iconSize
            ivIcon.layoutParams.height = iconSize
            (ivIcon.layoutParams as? LinearLayout.LayoutParams)?.topMargin = iconTopMargin

            // vAcctColor: 32x3dp marginTop="3dp"
            vAcctColor.layoutParams.width = iconSize
            vAcctColor.layoutParams.height = barHeight
            (vAcctColor.layoutParams as? LinearLayout.LayoutParams)?.topMargin = barTopMargin

            viewRoot.tag = index
            viewRoot.setOnClickListener { v ->
                val idx = v.tag as Int
                if (Pref.bpScrollTopFromColumnStrip(pref) && isVisibleColumn(idx)) {
                    column.viewHolder?.scrollToTop2()
                    return@setOnClickListener
                }
                scrollToColumn(idx)
            }
            viewRoot.contentDescription = column.getColumnName(true)

            viewRoot.backgroundDrawable = getAdaptiveRippleDrawableRound(
                this,
                column.getHeaderBackgroundColor(),
                column.getHeaderNameColor()
            )

            ivIcon.setImageResource(column.getIconId())
            ivIcon.imageTintList = ColorStateList.valueOf(column.getHeaderNameColor())

            //
            val ac = AcctColor.load(column.access_info)
            if (AcctColor.hasColorForeground(ac)) {
                vAcctColor.setBackgroundColor(ac.color_fg)
            } else {
                vAcctColor.visibility = View.INVISIBLE
            }

            //
            llColumnStrip.addView(viewRoot)
        }
        svColumnStrip.requestLayout()
        updateColumnStripSelection(-1, -1f)

    }

    private fun updateColumnStripSelection(position: Int, positionOffset: Float) {
        handler.post(Runnable {
            if (isFinishing) return@Runnable

            if (app_state.columnCount == 0) {
                llColumnStrip.setVisibleRange(-1, -1, 0f)
            } else {
                phoneTab({ env ->
                    if (position >= 0) {
                        llColumnStrip.setVisibleRange(position, position, positionOffset)
                    } else {
                        val c = env.pager.currentItem
                        llColumnStrip.setVisibleRange(c, c, 0f)
                    }

                }, { env ->
                    val vs = env.tablet_layout_manager.findFirstVisibleItemPosition()
                    val ve = env.tablet_layout_manager.findLastVisibleItemPosition()
                    val vr = if (vs == RecyclerView.NO_POSITION || ve == RecyclerView.NO_POSITION) {
                        IntRange(-1, -2) // empty and less than zero
                    } else {
                        IntRange(vs, min(ve, vs + nScreenColumn - 1))
                    }
                    var slide_ratio = 0f
                    if (vr.first <= vr.last) {
                        val child = env.tablet_layout_manager.findViewByPosition(vr.first)
                        slide_ratio =
                            clipRange(0f, 1f, abs((child?.left ?: 0) / nColumnWidth.toFloat()))
                    }

                    llColumnStrip.setVisibleRange(vr.first, vr.last, slide_ratio)
                })
            }
        })
    }

    private fun scrollColumnStrip(select: Int) {
        val child_count = llColumnStrip.childCount
        if (select < 0 || select >= child_count) {
            return
        }

        val icon = llColumnStrip.getChildAt(select)

        val sv_width = (llColumnStrip.parent as View).width
        val ll_width = llColumnStrip.width
        val icon_width = icon.width
        val icon_left = icon.left

        if (sv_width == 0 || ll_width == 0 || icon_width == 0) {
            handler.postDelayed({ scrollColumnStrip(select) }, 20L)
        }

        val sx = icon_left + icon_width / 2 - sv_width / 2
        svColumnStrip.smoothScrollTo(sx, 0)

    }

    // ActOAuthCallbackで受け取ったUriを処理する
    private fun handleIntentUri(uri: Uri) {

        log.d("handleIntentUri ${uri}")

        when (uri.scheme) {
            "subwaytooter", "misskeyclientproto" -> return try {
                handleCustomSchemaUri(uri)
            } catch (ex: Throwable) {
                log.trace(ex)
                showToast(ex, "handleCustomSchemaUri failed.")
            }
        }

        val url = uri.toString()

        val statusInfo = url.findStatusIdFromUrl()
        if (statusInfo != null) {
            // ステータスをアプリ内で開く
            Action_Toot.conversationOtherInstance(
                this@ActMain,
                defaultInsertPosition,
                statusInfo.url,
                statusInfo.statusId,
                statusInfo.host,
                statusInfo.statusId
            )
            return
        }

        // ユーザページをアプリ内で開く
        var m = TootAccount.reAccountUrl.matcher(url)
        if (m.find()) {
            val host = m.groupEx(1)!!
            val user = m.groupEx(2)!!.decodePercent()
            val instance = m.groupEx(3)?.decodePercent()

            if (instance?.isNotEmpty() == true) {
                Action_User.profile(
                    this@ActMain,
                    defaultInsertPosition,
                    null,
                    "https://$instance/@$user",
                    Host.parse(instance),
                    user,
                    original_url = url
                )
            } else {
                Action_User.profile(
                    this@ActMain,
                    defaultInsertPosition,
                    null,
                    url,
                    Host.parse(host),
                    user
                )
            }
            return
        }

        // intentFilterの都合でこの形式のURLが飛んでくることはないのだが…。
        m = TootAccount.reAccountUrl2.matcher(url)
        if (m.find()) {
            val host = m.groupEx(1)!!
            val user = m.groupEx(2)!!.decodePercent()

            Action_User.profile(
                this@ActMain,
                defaultInsertPosition,
                null,
                url,
                Host.parse(host),
                user
            )
            return
        }

        // このアプリでは処理できないURLだった
        // 外部ブラウザを開きなおそうとすると無限ループの恐れがある
        // アプリケーションチューザーを表示する

        val error_message = getString(R.string.cant_handle_uri_of, url)

        try {
            val query_flag = if (Build.VERSION.SDK_INT >= 23) {
                // Android 6.0以降
                // MATCH_DEFAULT_ONLY だと標準の設定に指定されたアプリがあるとソレしか出てこない
                // MATCH_ALL を指定すると 以前と同じ挙動になる
                PackageManager.MATCH_ALL
            } else {
                // Android 5.xまでは MATCH_DEFAULT_ONLY でマッチするすべてのアプリを取得できる
                PackageManager.MATCH_DEFAULT_ONLY
            }

            // queryIntentActivities に渡すURLは実在しないホストのものにする
            val intent = Intent(Intent.ACTION_VIEW, "https://dummy.subwaytooter.club/".toUri())
            intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK

            val my_name = packageName
            val resolveInfoList = packageManager.queryIntentActivities(intent, query_flag)
                .filter { my_name != it.activityInfo.packageName }

            if (resolveInfoList.isEmpty()) {
                throw RuntimeException("resolveInfoList is empty.")
            }

            // このアプリ以外の選択肢を集める
            val choice_list = resolveInfoList
                .map {
                    Intent(Intent.ACTION_VIEW, uri).apply {
                        addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
                        `package` = it.activityInfo.packageName
                        setClassName(it.activityInfo.packageName, it.activityInfo.name)
                    }
                }.toMutableList()

            val chooser = Intent.createChooser(choice_list.removeAt(0), error_message)
            // 2つめ以降はEXTRAに渡す
            chooser.putExtra(Intent.EXTRA_INITIAL_INTENTS, choice_list.toTypedArray())

            // 指定した選択肢でチューザーを作成して開く
            startActivity(chooser)
            return
        } catch (ex: Throwable) {
            log.trace(ex)
        }

        AlertDialog.Builder(this)
            .setCancelable(true)
            .setMessage(error_message)
            .setPositiveButton(R.string.close, null)
            .show()

    }

    private fun handleNotificationClick(uri: Uri, dataIdString: String) {
        try {
            val account = dataIdString.toLongOrNull()?.let { SavedAccount.loadAccount(this, it) }
            if (account == null) {
                showToast(true, "handleNotificationClick: missing SavedAccount. id=$dataIdString")
                return
            }

            PollingWorker.queueNotificationClicked(this, uri)

            val columnList = app_state.columnList
            val column = columnList.firstOrNull {
                it.type == ColumnType.NOTIFICATIONS &&
                    it.access_info == account &&
                    !it.system_notification_not_related
            }?.also {
                scrollToColumn(columnList.indexOf(it))
            } ?: addColumn(
                true,
                defaultInsertPosition,
                account,
                ColumnType.NOTIFICATIONS
            )

            // 通知を読み直す
            if (!column.bInitialLoading) column.startLoading()
        } catch (ex: Throwable) {
            log.trace(ex)
        }
    }

    private fun handleOAuth2Callback(uri: Uri) {
        TootTaskRunner(this@ActMain).run(object : TootTask {

            var ta: TootAccount? = null
            var sa: SavedAccount? = null
            var apiHost: Host? = null
            var apDomain: Host? = null

            override suspend fun background(client: TootApiClient): TootApiResult? {

                val uriStr = uri.toString()
                if (uriStr.startsWith("subwaytooter://misskey/auth_callback")
                    || uriStr.startsWith("misskeyclientproto://misskeyclientproto/auth_callback")
                ) {

                    // Misskey 認証コールバック
                    val token = uri.getQueryParameter("token")
                    if (token.isNullOrBlank())
                        return TootApiResult("missing token in callback URL")

                    val prefDevice = PrefDevice.prefDevice(this@ActMain)

                    val instance = Host.parse(
                        prefDevice.getString(PrefDevice.LAST_AUTH_INSTANCE, null)
                            ?: return TootApiResult("missing instance name.")
                    )

                    when (val db_id = prefDevice.getLong(PrefDevice.LAST_AUTH_DB_ID, -1L)) {

                        // new registration
                        -1L -> client.apiHost = instance

                        // update access token
                        else -> try {
                            val sa = SavedAccount.loadAccount(this@ActMain, db_id)
                                ?: return TootApiResult("missing account db_id=$db_id")
                            this.sa = sa
                            client.account = sa
                        } catch (ex: Throwable) {
                            log.trace(ex)
                            return TootApiResult(ex.withCaption("invalid state"))
                        }
                    }

                    val (ti, r2) = TootInstance.get(client)
                    ti ?: return r2

                    this.apiHost = instance
                    this.apDomain = ti.uri?.let{ Host.parse(it)}

                    val parser = TootParser(
                        this@ActMain,
                        linkHelper = LinkHelper.create(
                            instance,
                            misskeyVersion = ti.misskeyVersion
                        )
                    )

                    return client.authentication2Misskey(
                        Pref.spClientName(this@ActMain),
                        token,
                        ti.misskeyVersion
                    )?.also { this.ta = parser.account(it.jsonObject) }

                } else {
                    // Mastodon 認証コールバック

                    // エラー時
                    // subwaytooter://oauth(\d*)/
                    // ?error=access_denied
                    // &error_description=%E3%83%AA%E3%82%BD%E3%83%BC%E3%82%B9%E3%81%AE%E6%89%80%E6%9C%89%E8%80%85%E3%81%BE%E3%81%9F%E3%81%AF%E8%AA%8D%E8%A8%BC%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC%E3%81%8C%E8%A6%81%E6%B1%82%E3%82%92%E6%8B%92%E5%90%A6%E3%81%97%E3%81%BE%E3%81%97%E3%81%9F%E3%80%82
                    // &state=db%3A3
                    val error = uri.getQueryParameter("error")
                    val error_description = uri.getQueryParameter("error_description")
                    if (error != null || error_description != null)
                        return TootApiResult(
                            error_description.notBlank() ?: error.notBlank()
                            ?: "?"
                        )

                    // subwaytooter://oauth(\d*)/
                    //    ?code=113cc036e078ac500d3d0d3ad345cd8181456ab087abc67270d40f40a4e9e3c2
                    //    &state=host%3Amastodon.juggler.jp

                    val code = uri.getQueryParameter("code")
                    val sv = uri.getQueryParameter("state")

                    if (code.isNullOrBlank())
                        return TootApiResult("missing code in callback url.")

                    if (sv.isNullOrBlank())
                        return TootApiResult("missing state in callback url.")

                    for (param in sv.split(",")) {
                        when {
                            param.startsWith("db:") -> try {
                                val dataId = param.substring(3).toLong(10)
                                val sa = SavedAccount.loadAccount(this@ActMain, dataId)
                                    ?: return TootApiResult("missing account db_id=$dataId")
                                this.sa = sa
                                client.account = sa
                            } catch (ex: Throwable) {
                                log.trace(ex)
                                return TootApiResult(ex.withCaption("invalid state"))
                            }

                            param.startsWith("host:") -> {
                                val host = Host.parse(param.substring(5))
                                client.apiHost = host
                            }

                            // ignore other parameter
                        }
                    }

                    val instance = client.apiHost
                        ?: return TootApiResult("missing instance in callback url.")
                    this.apiHost = instance



                    val parser = TootParser(
                        this@ActMain,
                        linkHelper = LinkHelper.create(instance)
                    )

                    val refToken = AtomicReference<String>(null)
                    return client.authentication2Mastodon(
                        Pref.spClientName(this@ActMain),
                        code,
                        outAccessToken = refToken
                    )?.also {
                        this.ta = parser.account(it.jsonObject)
                        if( ta != null){
                            val (ti, ri) = TootInstance.getEx(client, forceAccessToken = refToken.get())
                            ti ?: return ri
                            this.apDomain = ti.uri?.let{ it2->  Host.parse(it2)}
                        }
                    }
                }
            }

            override suspend fun handleResult(result: TootApiResult?) {
                val apiHost = this.apiHost
                val apDomain = this.apDomain
                val ta = this.ta
                var sa = this.sa

                if (ta != null && apiHost?.isValid == true && sa == null) {
                    val acct = Acct.parse(ta.username, apDomain ?: apiHost )
                    // アカウント追加時に、アプリ内に既にあるアカウントと同じものを登録していたかもしれない
                    sa = SavedAccount.loadAccountByAcct(this@ActMain, acct.ascii)
                }

                afterAccountVerify(result, ta, sa, apiHost, apDomain)
            }

        })
    }

    private fun handleCustomSchemaUri(uri: Uri) {
        val dataIdString = uri.getQueryParameter("db_id")
        if (dataIdString != null) {
            // subwaytooter://notification_click/?db_id=(db_id)
            handleNotificationClick(uri, dataIdString)
        } else {
            // OAuth2 認証コールバック
            // subwaytooter://oauth(\d*)/?...
            handleOAuth2Callback(uri)
        }
    }

    internal fun afterAccountVerify(
        result: TootApiResult?,
        ta: TootAccount?,
        sa: SavedAccount?,
        apiHost: Host?,
        apDomain: Host?
    ): Boolean {
        result ?: return false

        val jsonObject = result.jsonObject
        val token_info = result.tokenInfo
        val error = result.error

        when {
            error != null ->
                showToast(true, "${result.error} ${result.requestInfo}".trim())

            token_info == null ->
                showToast(true, "can't get access token.")

            jsonObject == null ->
                showToast(true, "can't parse json response.")

            // 自分のユーザネームを取れなかった
            // …普通はエラーメッセージが設定されてるはずだが
            ta == null -> showToast(true, "can't verify user credential.")

            // アクセストークン更新時
            // インスタンスは同じだと思うが、ユーザ名が異なる可能性がある
            sa != null -> if (sa.username != ta.username) {
                showToast(true, R.string.user_name_not_match)
            } else {
                showToast(false, R.string.access_token_updated_for, sa.acct.pretty)

                // DBの情報を更新する
                sa.updateTokenInfo(token_info)

                // 各カラムの持つアカウント情報をリロードする
                reloadAccountSetting()

                // 自動でリロードする
                app_state.columnList
                    .filter { it.access_info == sa }
                    .forEach { it.startLoading() }

                // 通知の更新が必要かもしれない
                PushSubscriptionHelper.clearLastCheck(sa)
                PollingWorker.queueUpdateNotification(this@ActMain)
                return true
            }

            apiHost != null -> {
                // アカウント追加時
                val user = Acct.parse(ta.username, apDomain ?: apiHost)

                val row_id = SavedAccount.insert(
                    acct = user.ascii,
                    host = apiHost.ascii,
                    domain = (apDomain ?: apiHost).ascii,
                    account = jsonObject,
                    token = token_info,
                    misskeyVersion = TootInstance.parseMisskeyVersion(token_info)
                )
                val account = SavedAccount.loadAccount(this@ActMain, row_id)
                if (account != null) {
                    var bModified = false

                    if (account.loginAccount?.locked == true) {
                        bModified = true
                        account.visibility = TootVisibility.PrivateFollowers
                    }
                    if (!account.isMisskey) {
                        val source = ta.source
                        if (source != null) {
                            val privacy = TootVisibility.parseMastodon(source.privacy)
                            if (privacy != null) {
                                bModified = true
                                account.visibility = privacy
                            }

                            // XXX ta.source.sensitive パラメータを読んで「添付画像をデフォルトでNSFWにする」を実現する
                            // 現在、アカウント設定にはこの項目はない( 「NSFWな添付メディアを隠さない」はあるが全く別の効果)
                        }

                        if (bModified) {
                            account.saveSetting()
                        }
                    }

                    showToast(false, R.string.account_confirmed)

                    // 通知の更新が必要かもしれない
                    PollingWorker.queueUpdateNotification(this@ActMain)

                    // 適当にカラムを追加する
                    val count = SavedAccount.count
                    if (count > 1) {
                        addColumn(false, defaultInsertPosition, account, ColumnType.HOME)
                    } else {
                        addColumn(false, defaultInsertPosition, account, ColumnType.HOME)
                        addColumn(false, defaultInsertPosition, account, ColumnType.NOTIFICATIONS)
                        addColumn(false, defaultInsertPosition, account, ColumnType.LOCAL)
                        addColumn(false, defaultInsertPosition, account, ColumnType.FEDERATE)
                    }

                    return true
                }
            }
        }
        return false
    }

    // アクセストークンを手動で入力した場合
    fun checkAccessToken(
        dialog_host: Dialog?,
        dialog_token: Dialog?,
        apiHost: Host,
        access_token: String,
        sa: SavedAccount?
    ) {

        TootTaskRunner(this@ActMain).run(apiHost, object : TootTask {

            var ta: TootAccount? = null
            var apDomain : Host? = null

            override suspend fun background(client: TootApiClient): TootApiResult? {

                val (ti,ri) = TootInstance.getEx(client,forceAccessToken = access_token)
                ti ?: return ri

                val apDomain = ti.uri?.let { Host.parse(it) }
                    ?: return TootApiResult("missing uri in Instance Information")

                this.apDomain = apDomain

                val misskeyVersion = ti.misskeyVersion

                val result = client.getUserCredential(access_token, misskeyVersion = misskeyVersion)

                this.ta = TootParser(
                    this@ActMain,
                    LinkHelper.create(
                        apiHostArg = apiHost,
                        apDomainArg = apDomain,
                        misskeyVersion = misskeyVersion
                    )
                ).account(result?.jsonObject)

                return result
            }

            override suspend fun handleResult(result: TootApiResult?) {
                if (afterAccountVerify(result, ta, sa, apiHost, apDomain)) {
                    dialog_host?.dismissSafe()
                    dialog_token?.dismissSafe()
                }
            }
        })
    }

    // アクセストークンの手動入力(更新)
    private fun checkAccessToken2(db_id: Long) {

        val sa = SavedAccount.loadAccount(this, db_id) ?: return

        DlgTextInput.show(
            this,
            getString(R.string.access_token_or_api_token),
            null,
            callback = object : DlgTextInput.Callback {
                override fun onOK(dialog: Dialog, text: String) {
                    checkAccessToken(null, dialog, sa.apiHost, text, sa)
                }

                override fun onEmptyError() {
                    showToast(true, R.string.token_not_specified)
                }
            })
    }

    private fun reloadAccountSetting() {
        val done_list = ArrayList<SavedAccount>()
        for (column in app_state.columnList) {
            val a = column.access_info
            if (done_list.contains(a)) continue
            done_list.add(a)
            if (!a.isNA) a.reloadSetting(this)
            column.fireShowColumnHeader()
        }
    }

    fun reloadAccountSetting(account: SavedAccount) {
        val done_list = ArrayList<SavedAccount>()
        for (column in app_state.columnList) {
            val a = column.access_info
            if (a != account) continue
            if (done_list.contains(a)) continue
            done_list.add(a)
            if (!a.isNA) a.reloadSetting(this@ActMain)
            column.fireShowColumnHeader()
        }
    }

    fun closeColumn(column: Column, bConfirmed: Boolean = false) {

        if (column.dont_close) {
            showToast(false, R.string.column_has_dont_close_option)
            return
        }

        if (!bConfirmed && !Pref.bpDontConfirmBeforeCloseColumn(pref)) {
            AlertDialog.Builder(this)
                .setMessage(R.string.confirm_close_column)
                .setNegativeButton(R.string.cancel, null)
                .setPositiveButton(R.string.ok) { _, _ -> closeColumn(column, bConfirmed = true) }
                .show()
            return
        }

        app_state.columnIndex(column)?.let { page_delete ->
            phoneTab({ env ->
                val page_showing = env.pager.currentItem

                removeColumn(column)

                if (page_showing == page_delete) {
                    scrollAndLoad(page_showing - 1)
                }

            }, {
                removeColumn(column)
                scrollAndLoad(page_delete - 1)
            })
        }
    }

    fun closeColumnAll(
        _lastColumnIndex: Int = -1,
        bConfirmed: Boolean = false
    ) {

        if (!bConfirmed) {
            AlertDialog.Builder(this)
                .setMessage(R.string.confirm_close_column_all)
                .setNegativeButton(R.string.cancel, null)
                .setPositiveButton(R.string.ok) { _, _ -> closeColumnAll(_lastColumnIndex, true) }
                .show()
            return
        }

        var lastColumnIndex = when (_lastColumnIndex) {
            -1 -> phoneTab(
                { it.pager.currentItem },
                { 0 }
            )
            else -> _lastColumnIndex
        }

        phoneOnly { env -> env.pager.adapter = null }

        app_state.editColumnList { list ->
            for (i in list.indices.reversed()) {
                val column = list[i]
                if (column.dont_close) continue
                list.removeAt(i).dispose()
                if (lastColumnIndex >= i) --lastColumnIndex
            }
        }

        phoneTab(
            { env -> env.pager.adapter = env.pager_adapter },
            { env -> resizeColumnWidth(env) }
        )

        updateColumnStrip()

        scrollAndLoad(lastColumnIndex)
    }

    private fun scrollAndLoad(idx: Int) {
        val c = app_state.column(idx) ?: return
        scrollToColumn(idx)
        if (!c.bFirstInitialized) c.startLoading()
    }

//////////////////////////////////////////////////////////////
// カラム追加系

    fun addColumn(
        indexArg: Int,
        ai: SavedAccount,
        type: ColumnType,
        vararg params: Any
    ): Column {
        return addColumn(
            Pref.bpAllowColumnDuplication(pref),
            indexArg,
            ai,
            type,
            *params
        )
    }

    fun addColumn(
        allowColumnDuplication: Boolean,
        indexArg: Int,
        ai: SavedAccount,
        type: ColumnType,
        vararg params: Any
    ): Column {
        if (!allowColumnDuplication) {
            // 既に同じカラムがあればそこに移動する
            app_state.columnList.forEachIndexed { i, column ->
                if (column.isSameSpec(ai, type, params)) {
                    scrollToColumn(i)
                    return column
                }
            }
        }

        //
        val col = Column(app_state, ai, type.id, *params)
        val index = addColumn(col, indexArg)
        scrollAndLoad(index)
        return col
    }

    fun showColumnMatchAccount(account: SavedAccount) {
        app_state.columnList.forEach { column ->
            if (account == column.access_info) {
                column.fireRebindAdapterItems()
            }
        }
    }

    private fun showFooterColor() {

        val footer_button_bg_color = Pref.ipFooterButtonBgColor(pref)
        val footer_button_fg_color = Pref.ipFooterButtonFgColor(pref)
        val footer_tab_bg_color = Pref.ipFooterTabBgColor(pref)
        val footer_tab_divider_color = Pref.ipFooterTabDividerColor(pref)
        val footer_tab_indicator_color = Pref.ipFooterTabIndicatorColor(pref)

        val colorColumnStripBackground = footer_tab_bg_color.notZero()
            ?: attrColor(R.attr.colorColumnStripBackground)

        svColumnStrip.setBackgroundColor(colorColumnStripBackground)
        llQuickTootBar.setBackgroundColor(colorColumnStripBackground)

        val colorButtonBg = footer_button_bg_color.notZero()
            ?: colorColumnStripBackground

        val colorButtonFg = footer_button_fg_color.notZero()
            ?: attrColor(R.attr.colorRippleEffect)

        btnMenu.backgroundDrawable =
            getAdaptiveRippleDrawableRound(this, colorButtonBg, colorButtonFg)
        btnToot.backgroundDrawable =
            getAdaptiveRippleDrawableRound(this, colorButtonBg, colorButtonFg)
        btnQuickToot.backgroundDrawable =
            getAdaptiveRippleDrawableRound(this, colorButtonBg, colorButtonFg)
        btnQuickTootMenu.backgroundDrawable =
            getAdaptiveRippleDrawableRound(this, colorButtonBg, colorButtonFg)

        val csl = ColorStateList.valueOf(
            footer_button_fg_color.notZero()
                ?: attrColor(R.attr.colorVectorDrawable)
        )
        btnToot.imageTintList = csl
        btnMenu.imageTintList = csl
        btnQuickToot.imageTintList = csl
        btnQuickTootMenu.imageTintList = csl

        val c = footer_tab_divider_color.notZero()
            ?: colorColumnStripBackground
        vFooterDivider1.setBackgroundColor(c)
        vFooterDivider2.setBackgroundColor(c)

        llColumnStrip.indicatorColor = footer_tab_indicator_color.notZero()
            ?: attrColor(R.attr.colorAccent)
    }

/////////////////////////////////////////////////////////////////////////
// タブレット対応で必要になった関数など

    private fun closeColumnSetting(): Boolean {
        phoneTab({ env ->
            val vh = env.pager_adapter.getColumnViewHolder(env.pager.currentItem)
            if (vh?.isColumnSettingShown == true) {
                vh.showColumnSetting(false)
                return@closeColumnSetting true
            }
        }, { env ->
            for (i in 0 until env.tablet_layout_manager.childCount) {

                val columnViewHolder = when (val v = env.tablet_layout_manager.getChildAt(i)) {
                    null -> null
                    else -> (env.tablet_pager.getChildViewHolder(v) as? TabletColumnViewHolder)?.columnViewHolder
                }

                if (columnViewHolder?.isColumnSettingShown == true) {
                    columnViewHolder.showColumnSetting(false)
                    return@closeColumnSetting true
                }
            }
        })
        return false
    }

    private fun addColumn(column: Column, indexArg: Int): Int {
        val index = indexArg.clip(0, app_state.columnCount)

        phoneOnly { env -> env.pager.adapter = null }

        app_state.editColumnList {
            it.add(index, column)
        }

        phoneTab(
            { env -> env.pager.adapter = env.pager_adapter },
            { env -> resizeColumnWidth(env) }
        )


        updateColumnStrip()

        return index
    }

    private fun removeColumn(column: Column) {
        val idx_column = app_state.columnIndex(column) ?: return

        phoneOnly { env -> env.pager.adapter = null }

        app_state.editColumnList {
            it.removeAt(idx_column).dispose()
        }

        phoneTab(
            { env -> env.pager.adapter = env.pager_adapter },
            { env -> resizeColumnWidth(env) }
        )

        updateColumnStrip()
    }

    private fun setOrder(new_order: List<Int>) {

        phoneOnly { env -> env.pager.adapter = null }

        app_state.editColumnList { list ->
            // columns with new order
            val tmp_list = new_order.mapNotNull { i -> list.elementAtOrNull(i) }
            val used_set = new_order.toSet()
            list.forEachIndexed { i, v ->
                if (!used_set.contains(i)) v.dispose()
            }
            list.clear()
            list.addAll(tmp_list)
        }

        phoneTab(
            { env -> env.pager.adapter = env.pager_adapter },
            { env -> resizeColumnWidth(env) }
        )

        app_state.saveColumnList()
        updateColumnStrip()
    }

    private fun resizeColumnWidth(env: TabletEnv) {

        var column_w_min_dp = COLUMN_WIDTH_MIN_DP
        val sv = Pref.spColumnWidth(pref)
        if (sv.isNotEmpty()) {
            try {
                val iv = Integer.parseInt(sv)
                if (iv >= 100) {
                    column_w_min_dp = iv
                }
            } catch (ex: Throwable) {
                log.trace(ex)
            }

        }

        val dm = resources.displayMetrics

        val screen_width = dm.widthPixels

        val density = dm.density
        var column_w_min = (0.5f + column_w_min_dp * density).toInt()
        if (column_w_min < 1) column_w_min = 1

        var column_w: Int

        if (screen_width < column_w_min * 2) {
            // 最小幅で2つ表示できないのなら1カラム表示
            nScreenColumn = 1
            column_w = screen_width
        } else {

            // カラム最小幅から計算した表示カラム数
            nScreenColumn = screen_width / column_w_min
            if (nScreenColumn < 1) nScreenColumn = 1

            // データのカラム数より大きくならないようにする
            // (でも最小は1)
            val column_count = app_state.columnCount
            if (column_count > 0 && column_count < nScreenColumn) {
                nScreenColumn = column_count
            }

            // 表示カラム数から計算したカラム幅
            column_w = screen_width / nScreenColumn

            // 最小カラム幅の1.5倍よりは大きくならないようにする
            val column_w_max = (0.5f + column_w_min * 1.5f).toInt()
            if (column_w > column_w_max) {
                column_w = column_w_max
            }
        }

        nColumnWidth = column_w // dividerの幅を含む

        val divider_width = (0.5f + 1f * density).toInt()
        column_w -= divider_width
        env.tablet_pager_adapter.columnWidth = column_w // dividerの幅を含まない
        // env.tablet_snap_helper.columnWidth = column_w //使われていない

        resizeAutoCW(column_w) // dividerの幅を含まない

        // 並べ直す
        env.tablet_pager_adapter.notifyDataSetChanged()
    }

    private fun scrollToColumn(index: Int, smoothScroll: Boolean = true) {
        scrollColumnStrip(index)
        phoneTab(

            // スマホはスムーススクロール基本ありだがたまにしない
            { env ->
                log.d("ipLastColumnPos beforeScroll=${env.pager.currentItem}")
                env.pager.setCurrentItem(index, smoothScroll)
            },

            // タブレットでスムーススクロールさせると頻繁にオーバーランするので絶対しない
            { env ->
                log.d("ipLastColumnPos beforeScroll=${env.visibleColumnsIndices.first}")
                env.tablet_pager.scrollToPosition(index)
            }
        )
    }

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

    @Suppress("BlockingMethodInNonBlockingContext")
    private fun importAppData(uri: Uri) {

        // remove all columns
        phoneOnly { env -> env.pager.adapter = null }

        app_state.editColumnList(save = false) { list ->
            list.forEach { it.dispose() }
            list.clear()
        }

        phoneTab(
            { env -> env.pager.adapter = env.pager_adapter },
            { env -> resizeColumnWidth(env) }
        )

        updateColumnStrip()


        runWithProgress(
            "importing app data",

            doInBackground = { progress ->
                fun setProgressMessage(sv: String) =
                    runOnMainLooper { progress.setMessageEx(sv) }

                var newColumnList: ArrayList<Column>? = null

                setProgressMessage("import data to local storage...")

                // アプリ内領域に一時ファイルを作ってコピーする
                val cacheDir = cacheDir
                cacheDir.mkdir()
                val file = File(
                    cacheDir,
                    "SubwayTooter.${Process.myPid()}.${Process.myTid()}.tmp"
                )
                val source = contentResolver.openInputStream(uri)
                if (source == null) {
                    showToast(true, "openInputStream failed.")
                    return@runWithProgress null
                }
                source.use { inStream ->
                    FileOutputStream(file).use { outStream ->
                        IOUtils.copy(inStream, outStream)
                    }
                }

                // 通知サービスを止める
                setProgressMessage("syncing notification poller…")
                PollingWorker.queueAppDataImportBefore(this@ActMain)
                while (PollingWorker.mBusyAppDataImportBefore.get()) {
                    delay(1000L)
                    log.d("syncing polling task...")
                }

                // データを読み込む
                setProgressMessage("reading app data...")
                var zipEntryCount = 0
                try {
                    ZipInputStream(FileInputStream(file)).use { zipStream ->
                        while (true) {
                            val entry = zipStream.nextEntry ?: break
                            ++zipEntryCount
                            try {
                                //
                                val entryName = entry.name
                                if (entryName.endsWith(".json")) {
                                    newColumnList = AppDataExporter.decodeAppData(
                                        this@ActMain,
                                        JsonReader(InputStreamReader(zipStream, "UTF-8"))
                                    )
                                    continue
                                }

                                if (AppDataExporter.restoreBackgroundImage(
                                        this@ActMain,
                                        newColumnList,
                                        zipStream,
                                        entryName
                                    )
                                ) {
                                    continue
                                }
                            } finally {
                                zipStream.closeEntry()
                            }
                        }
                    }
                } catch (ex: Throwable) {
                    log.trace(ex)
                    if (zipEntryCount != 0) {
                        showToast(ex, "importAppData failed.")
                    }
                }
                // zipではなかった場合、zipEntryがない状態になる。例外はPH-1では出なかったが、出ても問題ないようにする。
                if (zipEntryCount == 0) {
                    InputStreamReader(FileInputStream(file), "UTF-8").use { inStream ->
                        newColumnList = AppDataExporter.decodeAppData(
                            this@ActMain,
                            JsonReader(inStream)
                        )
                    }
                }

                newColumnList
            },
            afterProc = {
                // cancelled.
                if (it == null) return@runWithProgress

                try {
                    phoneOnly { env -> env.pager.adapter = null }

                    app_state.editColumnList { list ->
                        list.clear()
                        list.addAll(it)
                    }

                    phoneTab(
                        { env -> env.pager.adapter = env.pager_adapter },
                        { env -> resizeColumnWidth(env) }
                    )
                    updateColumnStrip()
                } finally {
                    // 通知サービスをリスタート
                    PollingWorker.queueAppDataImportAfter(this@ActMain)
                }

                showToast(true, R.string.import_completed_please_restart_app)
                finish()
            },
            preProc = {
                window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
            },
            postProc = {
                window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
            }
        )
    }

    override fun onDrawerSlide(drawerView: View, slideOffset: Float) {
        post_helper.closeAcctPopup()
    }

    override fun onDrawerOpened(drawerView: View) {
        post_helper.closeAcctPopup()
    }

    override fun onDrawerClosed(drawerView: View) {
        post_helper.closeAcctPopup()
    }

    override fun onDrawerStateChanged(newState: Int) {
        post_helper.closeAcctPopup()
    }

    private fun resizeAutoCW(column_w: Int) {
        val sv = Pref.spAutoCWLines(pref)
        nAutoCwLines = sv.optInt() ?: -1
        if (nAutoCwLines > 0) {
            val lv_pad = (0.5f + 12 * density).toInt()
            val icon_width = avatarIconSize
            val icon_end = (0.5f + 4 * density).toInt()
            nAutoCwCellWidth = column_w - lv_pad * 2 - icon_width - icon_end
        }
        // この後各カラムは再描画される
    }

    fun checkAutoCW(status: TootStatus, text: CharSequence) {
        if (nAutoCwCellWidth <= 0) {
            // 設定が無効
            status.auto_cw = null
            return
        }

        var auto_cw = status.auto_cw
        if (auto_cw != null &&
            auto_cw.refActivity?.get() === this@ActMain &&
            auto_cw.cell_width == nAutoCwCellWidth
        ) {
            // 以前に計算した値がまだ使える
            return
        }

        if (auto_cw == null) {
            auto_cw = TootStatus.AutoCW()
            status.auto_cw = auto_cw
        }

        // 計算時の条件(文字フォント、文字サイズ、カラム幅）を覚えておいて、再利用時に同じか確認する
        auto_cw.refActivity = WeakReference(this@ActMain)
        auto_cw.cell_width = nAutoCwCellWidth
        auto_cw.decoded_spoiler_text = null

        // テキストをレイアウトして行数を測定
        val tv = TextView(this).apply {
            layoutParams =
                LinearLayout.LayoutParams(nAutoCwCellWidth, LinearLayout.LayoutParams.WRAP_CONTENT)
            if (!timeline_font_size_sp.isNaN())
                textSize = timeline_font_size_sp

            val fv = timeline_spacing
            if (fv != null) setLineSpacing(0f, fv)

            typeface = timeline_font
            this.text = text
        }

        tv.measure(
            View.MeasureSpec.makeMeasureSpec(nAutoCwCellWidth, View.MeasureSpec.EXACTLY),
            View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
        )
        val l = tv.layout
        if (l != null) {
            auto_cw.originalLineCount = l.lineCount
            val line_count = auto_cw.originalLineCount

            if ((nAutoCwLines > 0 && line_count > nAutoCwLines)
                && status.spoiler_text.isEmpty()
                && (status.mentions?.size ?: 0) <= nAutoCwLines
            ) {
                val sb = SpannableStringBuilder()
                sb.append(getString(R.string.auto_cw_prefix))
                sb.append(text, 0, l.getLineEnd(nAutoCwLines - 1))
                var last = sb.length
                while (last > 0) {
                    val c = sb[last - 1]
                    if (c == '\n' || Character.isWhitespace(c)) {
                        --last
                        continue
                    }
                    break
                }
                if (last < sb.length) {
                    sb.delete(last, sb.length)
                }
                sb.append('…')
                auto_cw.decoded_spoiler_text = sb
            }
        }
    }

    private fun checkPrivacyPolicy() {

        // 既に表示中かもしれない
        if (dlgPrivacyPolicy?.get()?.isShowing == true) return

        val res_id = when (getString(R.string.language_code)) {
            "ja" -> R.raw.privacy_policy_ja
            "fr" -> R.raw.privacy_policy_fr
            else -> R.raw.privacy_policy_en
        }

        // プライバシーポリシーデータの読み込み
        val bytes = loadRawResource(res_id)
        if (bytes.isEmpty()) return

        // 同意ずみなら表示しない
        val digest = bytes.digestSHA256().encodeBase64Url()
        if (digest == Pref.spAgreedPrivacyPolicyDigest(pref)) return

        val dialog = AlertDialog.Builder(this)
            .setTitle(R.string.privacy_policy)
            .setMessage(bytes.decodeUTF8())
            .setNegativeButton(R.string.cancel) { _, _ ->
                finish()
            }
            .setOnCancelListener {
                finish()
            }
            .setPositiveButton(R.string.agree) { _, _ ->
                pref.edit().put(Pref.spAgreedPrivacyPolicyDigest, digest).apply()
            }
            .create()
        dlgPrivacyPolicy = WeakReference(dialog)
        dialog.show()
    }

    private fun searchFromActivityResult(data: Intent?, columnType: ColumnType) =
        data?.getStringExtra(Intent.EXTRA_TEXT)?.let {
            addColumn(
                false,
                defaultInsertPosition,
                SavedAccount.na,
                columnType,
                it
            )
        }

}
