package jp.juggler.subwaytooter

import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.ResolveInfo
import android.graphics.Color
import android.graphics.Typeface
import android.net.Uri
import android.os.Bundle
import android.os.Handler
import android.text.Editable
import android.text.TextWatcher
import android.util.JsonWriter
import android.view.View
import android.view.ViewGroup
import android.view.Window
import android.widget.*
import androidx.annotation.ColorInt
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SwitchCompat
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import com.jrummyapps.android.colorpicker.ColorPickerDialog
import com.jrummyapps.android.colorpicker.ColorPickerDialogListener
import jp.juggler.subwaytooter.dialog.DlgAppPicker
import jp.juggler.subwaytooter.notification.PollingWorker
import jp.juggler.subwaytooter.table.AcctColor
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.util.CustomShare
import jp.juggler.subwaytooter.util.CustomShareTarget
import jp.juggler.subwaytooter.util.cn
import jp.juggler.util.*
import org.apache.commons.io.IOUtils
import java.io.File
import java.io.FileOutputStream
import java.io.InputStream
import java.io.OutputStreamWriter
import java.text.NumberFormat
import java.util.*
import java.util.concurrent.TimeUnit
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
import kotlin.collections.ArrayList
import kotlin.math.abs

class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnClickListener {

    companion object {

        internal val log = LogCategory("ActAppSetting")

        fun createIntent(activity: ActMain) =
            Intent(activity, ActAppSetting::class.java)

        private const val COLOR_DIALOG_ID = 1

        private const val STATE_CHOOSE_INTENT_TARGET = "customShareTarget"

        // 他の設定子画面と重複しない値にすること
//		const val REQUEST_CODE_OTHER = 0
//		const val REQUEST_CODE_APP_DATA_IMPORT = 1
//		const val REQUEST_CODE_TIMELINE_FONT = 2

        val reLinefeed = Regex("[\\x0d\\x0a]+")
    }

    private var customShareTarget: CustomShareTarget? = null

    lateinit var pref: SharedPreferences
    lateinit var handler: Handler
    private lateinit var lvList: ListView
    private lateinit var adapter: MyAdapter
    private lateinit var etSearch: EditText

    private val arNoop = activityResultHandler { }

    private val arImportAppData = activityResultHandler { ar ->
        if (ar?.resultCode == RESULT_OK) {
            ar.data?.handleGetContentResult(contentResolver)
                ?.firstOrNull()
                ?.uri?.let { importAppData2(false, it) }
        }
    }

    val arTimelineFont = activityResultHandler { ar ->
        if (ar?.resultCode == RESULT_OK) {
            ar.data?.let { handleFontResult(AppSettingItem.TIMELINE_FONT, it, "TimelineFont") }
        }
    }

    val arTimelineFontBold = activityResultHandler { ar ->
        if (ar?.resultCode == RESULT_OK) {
            ar.data?.let { handleFontResult(AppSettingItem.TIMELINE_FONT_BOLD, it, "TimelineFontBold") }
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        arNoop.register(this, log)
        arImportAppData.register(this, log)
        arTimelineFont.register(this, log)
        arTimelineFontBold.register(this, log)

        requestWindowFeature(Window.FEATURE_NO_TITLE)
        App1.setActivityTheme(this, noActionBar = true)

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

        //		val intent = this.intent
        //		val layoutId = intent.getIntExtra(EXTRA_LAYOUT_ID, 0)
        //		val titleId = intent.getIntExtra(EXTRA_TITLE_ID, 0)
        //		this.title = getString(titleId)

        if (savedInstanceState != null) {
            try {
                val sv = savedInstanceState.getString(STATE_CHOOSE_INTENT_TARGET)
                customShareTarget = CustomShareTarget.values().firstOrNull { it.name == sv }
            } catch (ex: Throwable) {
                log.e(ex, "can't restore customShareTarget.")
            }
        }

        initUi()

        removeDefaultPref()

        load(null, null)
    }

    private fun initUi() {
        setContentView(R.layout.act_app_setting)
        App1.initEdgeToEdge(this)

        Styler.fixHorizontalPadding0(findViewById(R.id.llContent))
        lvList = findViewById(R.id.lvList)

        adapter = MyAdapter()
        lvList.adapter = adapter

        etSearch = findViewById<EditText>(R.id.etSearch).apply {
            addTextChangedListener(object : TextWatcher {
                override fun afterTextChanged(p0: Editable?) {
                    pendingQuery = p0?.toString()
                    this@ActAppSetting.handler.removeCallbacks(procQuery)
                    this@ActAppSetting.handler.postDelayed(procQuery, 166L)
                }

                override fun beforeTextChanged(
                    p0: CharSequence?,
                    p1: Int,
                    p2: Int,
                    p3: Int,
                ) {
                }

                override fun onTextChanged(
                    p0: CharSequence?,
                    p1: Int,
                    p2: Int,
                    p3: Int,
                ) {
                }
            })
        }

        findViewById<View>(R.id.btnSearchReset).apply {
            setOnClickListener(this@ActAppSetting)
        }
    }

    private fun removeDefaultPref() {
        val e = pref.edit()
        var changed = false
        appSettingRoot.scan {
            if (it.pref?.removeDefault(pref, e) == true) changed = true
        }
        if (changed) e.apply()
    }

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        val sv = customShareTarget?.name
        if (sv != null) outState.putString(STATE_CHOOSE_INTENT_TARGET, sv)
    }

    override fun onStop() {
        super.onStop()

        // Pull通知チェック間隔を変更したかもしれないのでジョブを再設定する
        PollingWorker.onAppSettingStop(this)
    }

    override fun onBackPressed() {
        when {
            lastQuery != null -> load(lastSection, null)
            lastSection != null -> load(null, null)
            else -> super.onBackPressed()
        }
    }

    override fun onClick(v: View) {
        when (v.id) {
            R.id.btnSearchReset -> {
                handler.removeCallbacks(procQuery)
                etSearch.setText("")
                etSearch.hideKeyboard()
                load(lastSection, null)
            }
        }
    }

    ///////////////////////////////////////////////////////////////
    private var pendingQuery: String? = null

    private val procQuery: Runnable = Runnable {
        if (pendingQuery != null) load(null, pendingQuery)
    }

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

    private val divider = Any()
    private val list = ArrayList<Any>()

    private var lastSection: AppSettingItem? = null
    private var lastQuery: String? = null

    private fun load(section: AppSettingItem?, query: String?) {
        list.clear()

        var lastPath: String? = null
        fun addParentPath(item: AppSettingItem) {
            list.add(divider)

            val pathList = ArrayList<String>()
            var parent = item.parent
            while (parent != null) {
                if (parent.caption != 0) pathList.add(0, getString(parent.caption))
                parent = parent.parent
            }
            val path = pathList.joinToString("/")
            if (path != lastPath) {
                lastPath = path
                list.add(path)
                list.add(divider)
            }
        }

        if (query?.isNotEmpty() == true) {
            lastQuery = query
            fun scanGroup(level: Int, item: AppSettingItem) {
                if (item.caption == 0) return
                if (item.type != SettingType.Section) {
                    var match = getString(item.caption).contains(query, ignoreCase = true)
                    if (item.type == SettingType.Group) {
                        for (child in item.items) {
                            if (child.caption == 0) continue
                            if (getString(item.caption).contains(query, ignoreCase = true)) {
                                match = true
                                break
                            }
                        }
                        if (match) {
                            // put entire group
                            addParentPath(item)
                            list.add(item)
                            for (child in item.items) {
                                list.add(child)
                            }
                        }
                        return
                    }

                    if (match) {
                        addParentPath(item)
                        list.add(item)
                    }
                }
                for (child in item.items) {
                    scanGroup(level + 1, child)
                }
            }
            scanGroup(0, appSettingRoot)
            if (list.isNotEmpty()) list.add(divider)
        } else if (section == null) {
            // show root page
            val root = appSettingRoot
            lastQuery = null
            lastSection = null
            for (child in root.items) {
                list.add(divider)
                list.add(child)
            }
            list.add(divider)
        } else {
            // show section page
            lastSection = section
            lastQuery = null
            fun scanGroup(level: Int, parent: AppSettingItem?) {
                parent ?: return
                for (item in parent.items) {
                    list.add(divider)
                    list.add(item)
                    if (item.items.isNotEmpty()) {
                        if (item.type == SettingType.Group) {
                            for (child in item.items) {
                                list.add(child)
                            }
                        } else {
                            scanGroup(level + 1, item)
                        }
                    }
                }
            }
            scanGroup(0, section.cast())
            if (list.isNotEmpty()) list.add(divider)
        }
        adapter.notifyDataSetChanged()
        lvList.setSelectionFromTop(0, 0)
    }

    inner class MyAdapter : BaseAdapter() {

        override fun getCount(): Int = list.size
        override fun getItemId(position: Int): Long = 0
        override fun getItem(position: Int): Any = list[position]
        override fun getViewTypeCount(): Int = SettingType.values().maxByOrNull { it.id }!!.id + 1

        override fun getItemViewType(position: Int): Int =
            when (val item = list[position]) {
                is AppSettingItem -> item.type.id
                is String -> SettingType.Path.id
                divider -> SettingType.Divider.id
                else -> error("can't generate view for type $item")
            }

        // true if the item at the specified position is not a separator.
        // (A separator is a non-selectable, non-clickable item).
        override fun areAllItemsEnabled(): Boolean = false

        override fun isEnabled(position: Int): Boolean = list[position] is AppSettingItem

        override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View =
            when (val item = list[position]) {
                is AppSettingItem ->
                    getViewSettingItem(item, convertView, parent)
                is String -> getViewPath(item, convertView)
                divider -> getViewDivider(convertView)
                else -> error("can't generate view for type $item")
            }
    }

    private fun dip(dp: Float): Int =
        (resources.displayMetrics.density * dp + 0.5f).toInt()

    private fun dip(dp: Int): Int = dip(dp.toFloat())

    private fun getViewDivider(convertView: View?): View =
        convertView ?: FrameLayout(this@ActAppSetting).apply {
            layoutParams = AbsListView.LayoutParams(
                AbsListView.LayoutParams.MATCH_PARENT,
                AbsListView.LayoutParams.WRAP_CONTENT
            )
            addView(View(this@ActAppSetting).apply {
                layoutParams = FrameLayout.LayoutParams(
                    AbsListView.LayoutParams.MATCH_PARENT,
                    dip(1)
                ).apply {
                    val marginX = 0
                    val marginY = dip(6)
                    setMargins(marginX, marginY, marginX, marginY)
                }
                setBackgroundColor(context.attrColor(R.attr.colorSettingDivider))
            })
        }

    private fun getViewPath(path: String, convertView: View?): View {
        val tv: TextView = convertView.cast() ?: TextView(this@ActAppSetting).apply {
            layoutParams = AbsListView.LayoutParams(
                AbsListView.LayoutParams.MATCH_PARENT,
                AbsListView.LayoutParams.WRAP_CONTENT
            )
            val padX = 0
            val padY = dip(3)
            setTypeface(typeface, Typeface.BOLD)
            setPaddingRelative(padX, padY, padX, padY)
        }
        tv.text = path
        return tv
    }

    private fun getViewSettingItem(
        item: AppSettingItem,
        convertView: View?,
        parent: ViewGroup?,
    ): View {
        val view: View
        val holder: ViewHolderSettingItem
        if (convertView != null) {
            view = convertView
            holder = convertView.tag.cast()!!
        } else {
            view = layoutInflater.inflate(R.layout.lv_setting_item, parent, false)
            holder = ViewHolderSettingItem(view)
            view.tag = holder
        }
        holder.bind(item)
        return view
    }

    private var colorTarget: AppSettingItem? = null

    override fun onDialogDismissed(dialogId: Int) {
    }

    override fun onColorSelected(dialogId: Int, @ColorInt colorSelected: Int) {
        val colorTarget = this.colorTarget ?: return
        val ip: IntPref = colorTarget.pref.cast() ?: error("$colorTarget has no in pref")
        val c = when (colorTarget.type) {
            SettingType.ColorAlpha -> colorSelected.notZero() ?: 0x01000000
            else -> colorSelected or Color.BLACK
        }
        pref.edit().put(ip, c).apply()
        findItemViewHolder(colorTarget)?.showColor()
        colorTarget.changed(this)
    }

    inner class ViewHolderSettingItem(viewRoot: View) :
        TextWatcher,
        AdapterView.OnItemSelectedListener,
        CompoundButton.OnCheckedChangeListener {

        private val tvCaption: TextView = viewRoot.findViewById(R.id.tvCaption)
        private val btnAction: Button = viewRoot.findViewById(R.id.btnAction)

        private val checkBox: CheckBox = viewRoot.findViewById<CheckBox>(R.id.checkBox)
            .also { it.setOnCheckedChangeListener(this) }

        private val swSwitch: SwitchCompat = viewRoot.findViewById<SwitchCompat>(R.id.swSwitch)
            .also { it.setOnCheckedChangeListener(this) }

        val llExtra: LinearLayout = viewRoot.findViewById(R.id.llExtra)

        val textView1: TextView = viewRoot.findViewById(R.id.textView1)

        private val llButtonBar: LinearLayout = viewRoot.findViewById(R.id.llButtonBar)
        private val vColor: View = viewRoot.findViewById(R.id.vColor)
        private val btnEdit: Button = viewRoot.findViewById(R.id.btnEdit)
        private val btnReset: Button = viewRoot.findViewById(R.id.btnReset)

        private val spSpinner: Spinner = viewRoot.findViewById<Spinner>(R.id.spSpinner)
            .also { it.onItemSelectedListener = this }

        private val etEditText: EditText = viewRoot.findViewById<EditText>(R.id.etEditText)
            .also { it.addTextChangedListener(this) }

        private val tvDesc: TextView = viewRoot.findViewById(R.id.tvDesc)
        private val tvError: TextView = viewRoot.findViewById(R.id.tvError)

        val activity: ActAppSetting
            get() = this@ActAppSetting

        var item: AppSettingItem? = null

        private var bindingBusy = false

        fun bind(item: AppSettingItem) {
            bindingBusy = true
            try {
                this.item = item

                tvCaption.vg(false)
                btnAction.vg(false)
                checkBox.vg(false)
                swSwitch.vg(false)
                llExtra.vg(false)
                textView1.vg(false)
                llButtonBar.vg(false)
                vColor.vg(false)
                spSpinner.vg(false)
                etEditText.vg(false)
                tvDesc.vg(false)
                tvError.vg(false)

                val name = if (item.caption == 0) "" else getString(item.caption)

                if (item.desc != 0) {
                    tvDesc.vg(true)
                    tvDesc.text = getString(item.desc)
                    if (item.descClickSet) {
                        tvDesc.background = ContextCompat.getDrawable(
                            activity,
                            R.drawable.btn_bg_transparent_round6dp
                        )
                        tvDesc.setOnClickListener { item.descClick.invoke(activity) }
                    } else {
                        tvDesc.background = null
                        tvDesc.setOnClickListener(null)
                        tvDesc.isClickable = false
                    }
                }

                when (item.type) {

                    SettingType.Section -> {
                        btnAction.vg(true)
                        btnAction.text = name
                        btnAction.isEnabledAlpha = item.enabled
                        btnAction.setOnClickListener {
                            load(item.cast()!!, null)
                        }
                    }

                    SettingType.Action -> {
                        btnAction.vg(true)
                        btnAction.text = name
                        btnAction.isEnabledAlpha = item.enabled
                        btnAction.setOnClickListener {
                            item.action(activity)
                        }
                    }

                    SettingType.CheckBox -> {
                        val bp: BooleanPref =
                            item.pref.cast() ?: error("$name has no boolean pref")
                        checkBox.vg(false) // skip animation
                        checkBox.text = name
                        checkBox.isEnabledAlpha = item.enabled
                        checkBox.isChecked = bp(pref)
                        checkBox.vg(true)
                    }

                    SettingType.Switch -> {
                        val bp: BooleanPref =
                            item.pref.cast() ?: error("$name has no boolean pref")
                        showCaption(name)
                        swSwitch.vg(false) // skip animation
                        setSwitchColor(pref, swSwitch)
                        swSwitch.isEnabledAlpha = item.enabled
                        swSwitch.isChecked = bp(pref)
                        swSwitch.vg(true)
                    }

                    SettingType.Group -> {
                        showCaption(name)
                    }

                    SettingType.Sample -> {
                        llExtra.vg(true)
                        llExtra.removeAllViews()
                        layoutInflater.inflate(item.sampleLayoutId, llExtra, true)
                        item.sampleUpdate(activity, llExtra)
                    }

                    SettingType.ColorAlpha, SettingType.ColorOpaque -> {
                        val ip = item.pref.cast<IntPref>() ?: error("$name has no int pref")
                        showCaption(name)
                        llButtonBar.vg(true)
                        vColor.vg(true)
                        vColor.setBackgroundColor(ip(pref))
                        btnEdit.isEnabledAlpha = item.enabled
                        btnReset.isEnabledAlpha = item.enabled
                        btnEdit.setOnClickListener {
                            colorTarget = item
                            val color = ip(pref)
                            val builder = ColorPickerDialog.newBuilder()
                                .setDialogType(ColorPickerDialog.TYPE_CUSTOM)
                                .setAllowPresets(true)
                                .setShowAlphaSlider(item.type == SettingType.ColorAlpha)
                                .setDialogId(COLOR_DIALOG_ID)
                            if (color != 0) builder.setColor(color)
                            builder.show(activity)
                        }
                        btnReset.setOnClickListener {
                            pref.edit().remove(ip).apply()
                            showColor()
                            item.changed.invoke(activity)
                        }
                    }

                    SettingType.Spinner -> {
                        showCaption(name)

                        spSpinner.vg(true)
                        spSpinner.isEnabledAlpha = item.enabled

                        val pi = item.pref
                        if (pi is IntPref) {
                            // 整数型の設定のSpinnerは全て選択肢を単純に覚える
                            val argsInt = item.spinnerArgs
                            if (argsInt != null) {
                                initSpinner(spSpinner, argsInt.map { getString(it) })
                            } else {
                                initSpinner(spSpinner, item.spinnerArgsProc(activity))
                            }
                            spSpinner.setSelection(pi.invoke(pref))
                        } else {
                            item.spinnerInitializer.invoke(activity, spSpinner)
                        }
                    }

                    SettingType.EditText -> {
                        showCaption(name)
                        etEditText.vg(true)

                            ?: error("EditText must have preference.")
                        etEditText.inputType = item.inputType
                        val text = when (val pi = item.pref) {
                            is FloatPref -> {
                                item.fromFloat.invoke(activity, pi(pref))
                            }

                            is StringPref -> {
                                pi(pref)
                            }

                            else -> error("EditText han incorrect pref $pi")
                        }
                        etEditText.setText(text)
                        etEditText.setSelection(0, text.length)

                        item.hint?.let { etEditText.hint = it }

                        updateErrorView()
                    }

                    SettingType.TextWithSelector -> {
                        showCaption(name)
                        llButtonBar.vg(true)
                        vColor.vg(false)
                        textView1.vg(true)

                        item.showTextView.invoke(activity, textView1)

                        btnEdit.setOnClickListener {
                            item.onClickEdit.invoke(activity)
                        }
                        btnReset.setOnClickListener {
                            item.onClickReset.invoke(activity)
                        }
                    }

                    else -> error("unknown type ${item.type}")
                }
            } finally {
                bindingBusy = false
            }
        }

        private fun showCaption(caption: String) {
            if (caption.isNotEmpty()) {
                tvCaption.vg(true)
                tvCaption.text = caption
                updateCaption()
            }
        }

        fun updateCaption() {
            val item = item ?: return
            val key = item.pref?.key ?: return

            val sample: TextView = tvCaption
            var defaultExtra = defaultLineSpacingExtra[key]
            if (defaultExtra == null) {
                defaultExtra = sample.lineSpacingExtra
                defaultLineSpacingExtra[key] = defaultExtra
            }
            var defaultMultiplier = defaultLineSpacingMultiplier[key]
            if (defaultMultiplier == null) {
                defaultMultiplier = sample.lineSpacingMultiplier
                defaultLineSpacingMultiplier[key] = defaultMultiplier
            }

            val size = item.captionFontSize.invoke(activity)
            if (size != null) sample.textSize = size

            val spacing = item.captionSpacing.invoke(activity)
            if (spacing == null || !spacing.isFinite()) {
                sample.setLineSpacing(defaultExtra, defaultMultiplier)
            } else {
                sample.setLineSpacing(0f, spacing)
            }
        }

        private fun updateErrorView() {
            val item = item ?: return
            val sv = etEditText.text.toString()
            val error = item.getError.invoke(activity, sv)
            tvError.vg(error != null)?.text = error
        }

        fun showColor() {
            val item = item ?: return
            val ip = item.pref.cast<IntPref>() ?: return
            val c = ip(pref)
            vColor.setBackgroundColor(c)
        }

        override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
        }

        override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
        }

        override fun afterTextChanged(p0: Editable?) {
            if (bindingBusy) return
            val item = item ?: return

            val sv = item.filter.invoke(p0?.toString() ?: "")

            when (val pi = item.pref) {

                is StringPref -> {
                    pref.edit().put(pi, sv).apply()
                }

                is FloatPref -> {
                    val fv = item.toFloat.invoke(activity, sv)
                    if (fv.isFinite()) {
                        pref.edit().put(pi, fv).apply()
                    } else {
                        pref.edit().remove(pi.key).apply()
                    }
                }

                else -> {
                    error("not FloatPref or StringPref")
                }
            }

            item.changed.invoke(activity)
            updateErrorView()
        }

        override fun onNothingSelected(v: AdapterView<*>?) = Unit

        override fun onItemSelected(
            parent: AdapterView<*>?,
            view: View?,
            position: Int,
            id: Long,
        ) {
            if (bindingBusy) return
            val item = item ?: return
            when (val pi = item.pref) {
                is IntPref -> pref.edit().put(pi, spSpinner.selectedItemPosition).apply()
                else -> item.spinnerOnSelected.invoke(activity, spSpinner, position)
            }
            item.changed.invoke(activity)
        }

        override fun onCheckedChanged(v: CompoundButton?, isChecked: Boolean) {
            if (bindingBusy) return
            val item = item ?: return
            when (val pi = item.pref) {
                is BooleanPref -> pref.edit().put(pi, isChecked).apply()
                else -> error("CompoundButton has no booleanPref $pi")
            }
            item.changed.invoke(activity)
        }
    }

    private fun initSpinner(spinner: Spinner, captions: List<String>) {
        spinner.adapter = ArrayAdapter(
            this,
            android.R.layout.simple_spinner_item,
            captions.toTypedArray()
        ).apply {
            setDropDownViewResource(R.layout.lv_spinner_dropdown)
        }
    }

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

    @Suppress("BlockingMethodInNonBlockingContext")
    fun exportAppData() {
        val activity = this
        launchMain {
            runWithProgress(
                "export app data",
                doInBackground = {
                    val cacheDir = activity.cacheDir

                    cacheDir.mkdir()

                    val file = File(
                        cacheDir,
                        "SubwayTooter.${android.os.Process.myPid()}.${android.os.Process.myTid()}.zip"
                    )

                    // ZipOutputStreamオブジェクトの作成
                    ZipOutputStream(FileOutputStream(file)).use { zipStream ->

                        // アプリデータjson
                        zipStream.putNextEntry(ZipEntry("AppData.json"))
                        try {
                            val jw = JsonWriter(OutputStreamWriter(zipStream, "UTF-8"))
                            AppDataExporter.encodeAppData(activity, jw)
                            jw.flush()
                        } finally {
                            zipStream.closeEntry()
                        }

                        // カラム背景画像
                        val appState = App1.getAppState(activity)
                        for (column in appState.columnList) {
                            AppDataExporter.saveBackgroundImage(activity, zipStream, column)
                        }
                    }

                    file
                },
                afterProc = {
                    val uri = FileProvider.getUriForFile(activity, App1.FILE_PROVIDER_AUTHORITY, it)
                    val intent = Intent(Intent.ACTION_SEND)
                    intent.type = contentResolver.getType(uri)
                    intent.putExtra(Intent.EXTRA_SUBJECT, "SubwayTooter app data")
                    intent.putExtra(Intent.EXTRA_STREAM, uri)
                    intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION)
                    arNoop.launch(intent)
                }
            )
        }
    }

    // open data picker
    fun importAppData1() {
        try {
            val intent = intentOpenDocument("*/*")
            arImportAppData.launch(intent)
        } catch (ex: Throwable) {
            showToast(ex, "importAppData(1) failed.")
        }
    }

    // after data picked
    private fun importAppData2(bConfirm: Boolean, uri: Uri) {

        val type = contentResolver.getType(uri)
        log.d("importAppData type=$type")

        if (!bConfirm) {
            AlertDialog.Builder(this)
                .setMessage(getString(R.string.app_data_import_confirm))
                .setNegativeButton(R.string.cancel, null)
                .setPositiveButton(R.string.ok) { _, _ -> importAppData2(true, uri) }
                .show()
            return
        }

        val data = Intent()
        data.data = uri
        setResult(ActMain.RESULT_APP_DATA_IMPORT, data)
        finish()
    }

    fun findItemViewHolder(item: AppSettingItem?): ViewHolderSettingItem? {
        if (item != null) {
            for (i in 0 until lvList.childCount) {
                val view = lvList.getChildAt(i)
                val holder: ViewHolderSettingItem? = view?.tag?.cast()
                if (holder?.item == item) return holder
            }
        }
        return null
    }

    fun showSample(item: AppSettingItem?) {
        item ?: error("showSample: missing item…")
        findItemViewHolder(item)?.let {
            item.sampleUpdate.invoke(this, it.llExtra)
        }
    }

    fun setSwitchColor() =
        setSwitchColor(pref, lvList)

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

    fun formatFontSize(fv: Float): String =
        when {
            fv.isFinite() -> String.format(defaultLocale(this), "%.1f", fv)
            else -> ""
        }

    fun parseFontSize(src: String): Float {
        try {
            if (src.isNotEmpty()) {
                val f = NumberFormat.getInstance(defaultLocale(this)).parse(src)?.toFloat()
                return when {
                    f == null -> Float.NaN
                    f.isNaN() -> Float.NaN
                    f < 0f -> 0f
                    f > 999f -> 999f
                    else -> f
                }
            }
        } catch (ex: Throwable) {
            log.trace(ex)
        }

        return Float.NaN
    }

    private val defaultLineSpacingExtra = HashMap<String, Float>()
    private val defaultLineSpacingMultiplier = HashMap<String, Float>()

    private fun handleFontResult(item: AppSettingItem?, data: Intent, fileName: String) {
        item ?: error("handleFontResult : setting item is null")
        data.handleGetContentResult(contentResolver).firstOrNull()?.uri?.let {
            val file = saveTimelineFont(it, fileName)
            if (file != null) {
                pref.edit().put(item.pref.cast()!!, file.absolutePath).apply()
                showTimelineFont(item)
            }
        }
    }

    fun showTimelineFont(item: AppSettingItem?) {
        item ?: return
        val holder = findItemViewHolder(item) ?: return
        item.showTextView.invoke(this, holder.textView1)
    }

    fun showTimelineFont(item: AppSettingItem, tv: TextView) {
        val fontUrl = item.pref.cast<StringPref>()!!.invoke(this)
        try {
            if (fontUrl.isNotEmpty()) {
                tv.typeface = Typeface.DEFAULT
                val face = Typeface.createFromFile(fontUrl)
                tv.typeface = face
                tv.text = fontUrl
                return
            }
        } catch (ex: Throwable) {
            log.trace(ex)
        }

        // fallback
        tv.text = getString(R.string.not_selected)
        tv.typeface = Typeface.DEFAULT
    }

    private fun saveTimelineFont(uri: Uri?, fileName: String): File? {
        try {
            if (uri == null) {
                showToast(false, "missing uri.")
                return null
            }

            contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)

            val dir = filesDir

            dir.mkdir()

            val tmpFile = File(dir, "$fileName.tmp")

            val source: InputStream? = contentResolver.openInputStream(uri)
            if (source == null) {
                showToast(false, "openInputStream returns null. uri=$uri")
                return null
            } else {
                source.use { inStream ->
                    FileOutputStream(tmpFile).use { outStream ->
                        IOUtils.copy(inStream, outStream)
                    }
                }
            }

            val face = Typeface.createFromFile(tmpFile)
            if (face == null) {
                showToast(false, "Typeface.createFromFile() failed.")
                return null
            }

            val file = File(dir, fileName)
            if (!tmpFile.renameTo(file)) {
                showToast(false, "File operation failed.")
                return null
            }

            return file
        } catch (ex: Throwable) {
            log.trace(ex)
            showToast(ex, "saveTimelineFont failed.")
            return null
        }
    }

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

    inner class AccountAdapter internal constructor() : BaseAdapter() {

        internal val list = java.util.ArrayList<SavedAccount>()

        init {
            for (a in SavedAccount.loadAccountList(this@ActAppSetting)) {
                if (a.isPseudo) continue
                list.add(a)
            }
            SavedAccount.sort(list)
        }

        override fun getCount(): Int {
            return 1 + list.size
        }

        override fun getItem(position: Int): Any? {
            return if (position == 0) null else list[position - 1]
        }

        override fun getItemId(position: Int): Long {
            return 0
        }

        override fun getView(position: Int, viewOld: View?, parent: ViewGroup): View {
            val view = viewOld ?: layoutInflater.inflate(
                android.R.layout.simple_spinner_item,
                parent,
                false
            )
            view.findViewById<TextView>(android.R.id.text1).text = when (position) {
                0 -> getString(R.string.ask_always)
                else -> AcctColor.getNickname(list[position - 1])
            }
            return view
        }

        override fun getDropDownView(position: Int, viewOld: View?, parent: ViewGroup): View {
            val view = viewOld ?: layoutInflater.inflate(R.layout.lv_spinner_dropdown, parent, false)
            view.findViewById<TextView>(android.R.id.text1).text = when (position) {
                0 -> getString(R.string.ask_always)
                else -> AcctColor.getNickname(list[position - 1])
            }
            return view
        }

        // 見つからなければ0,見つかったら1以上
        internal fun getIndexFromId(dbId: Long): Int =
            1 + list.indexOfFirst { it.db_id == dbId }

        internal fun getIdFromIndex(position: Int): Long =
            if (position > 0) list[position - 1].db_id else -1L
    }

    private class Item(
        val id: String,
        val caption: String,
        val offset: Int,
    )

    inner class TimeZoneAdapter internal constructor() : BaseAdapter() {

        private val list = ArrayList<Item>()

        init {

            for (id in TimeZone.getAvailableIDs()) {
                val tz = TimeZone.getTimeZone(id)

                // GMT数字を指定するタイプのタイムゾーンは無視する。ただしGMT-12:00の１項目だけは残す
                // 3文字のIDは曖昧な場合があるので非推奨
                // '/' を含まないIDは列挙しない
                if (!when {
                        !tz.id.contains('/') -> false
                        tz.id == "Etc/GMT+12" -> true
                        tz.id.startsWith("Etc/") -> false
                        else -> true
                    }
                ) continue

                var offset = tz.rawOffset.toLong()
                val caption = when (offset) {
                    0L -> "(UTC\u00B100:00) ${tz.id} ${tz.displayName}"

                    else -> {

                        val format = when {
                            offset > 0 -> "(UTC+%02d:%02d) %s %s"
                            else -> "(UTC-%02d:%02d) %s %s"
                        }

                        offset = abs(offset)

                        val hours = TimeUnit.MILLISECONDS.toHours(offset)
                        val minutes =
                            TimeUnit.MILLISECONDS.toMinutes(offset) - TimeUnit.HOURS.toMinutes(hours)

                        String.format(format, hours, minutes, tz.id, tz.displayName)
                    }
                }
                if (null == list.find { it.caption == caption }) {
                    list.add(Item(id, caption, tz.rawOffset))
                }
            }

            list.sortWith { a, b ->
                (a.offset - b.offset).notZero() ?: a.caption.compareTo(b.caption)
            }

            list.add(0, Item("", getString(R.string.device_timezone), 0))
        }

        override fun getCount(): Int {
            return list.size
        }

        override fun getItem(position: Int): Any {
            return list[position]
        }

        override fun getItemId(position: Int): Long {
            return 0
        }

        override fun getView(position: Int, viewOld: View?, parent: ViewGroup): View {
            val view = viewOld ?: layoutInflater.inflate(
                android.R.layout.simple_spinner_item,
                parent,
                false
            )
            val item = list[position]
            view.findViewById<TextView>(android.R.id.text1).text = item.caption
            return view
        }

        override fun getDropDownView(position: Int, viewOld: View?, parent: ViewGroup): View {
            val view =
                viewOld ?: layoutInflater.inflate(R.layout.lv_spinner_dropdown, parent, false)
            val item = list[position]
            view.findViewById<TextView>(android.R.id.text1).text = item.caption
            return view
        }

        internal fun getIndexFromId(tzId: String): Int {
            val index = list.indexOfFirst { it.id == tzId }
            return if (index == -1) 0 else index
        }

        internal fun getIdFromIndex(position: Int): String {
            return list[position].id
        }
    }

    fun openCustomShareChooser(appSettingItem: AppSettingItem, target: CustomShareTarget) {
        try {
            val rv = DlgAppPicker(
                this,
                intent = Intent().apply {
                    action = Intent.ACTION_SEND
                    type = "text/plain"
                    putExtra(Intent.EXTRA_TEXT, getString(R.string.content_sample))
                },
                addCopyAction = true
            ) { setCustomShare(appSettingItem, target, it) }
                .show()
            if (!rv) showToast(true, "share target app is not installed.")
        } catch (ex: Throwable) {
            log.trace(ex)
            showToast(ex, "openCustomShareChooser failed.")
        }
    }

    fun setCustomShare(appSettingItem: AppSettingItem, target: CustomShareTarget, value: String) {
        val sp: StringPref = appSettingItem.pref.cast() ?: error("$target: not StringPref")
        pref.edit().put(sp, value).apply()

        showCustomShareIcon(findItemViewHolder(appSettingItem)?.textView1, target)
    }

    fun showCustomShareIcon(tv: TextView?, target: CustomShareTarget) {
        tv ?: return
        val cn = CustomShare.getCustomShareComponentName(pref, target)
        val (label, icon) = CustomShare.getInfo(this, cn)
        tv.text = label ?: getString(R.string.not_selected)
        tv.setCompoundDrawablesRelativeWithIntrinsicBounds(icon, null, null, null)
        tv.compoundDrawablePadding = (resources.displayMetrics.density * 4f + 0.5f).toInt()
    }

    fun openWebBrowserChooser(appSettingItem: AppSettingItem, intent: Intent, filter: (ResolveInfo) -> Boolean) {
        try {
            val rv = DlgAppPicker(
                this,
                intent = intent,
                filter = filter,
                addCopyAction = false
            ) { setWebBrowser(appSettingItem, it) }
                .show()
            if (!rv) showToast(true, "share target app is not installed.")
        } catch (ex: Throwable) {
            log.trace(ex)
            showToast(ex, "openCustomShareChooser failed.")
        }
    }

    fun setWebBrowser(appSettingItem: AppSettingItem, value: String) {
        val sp: StringPref = appSettingItem.pref.cast() ?: error("${getString(appSettingItem.caption)}: not StringPref")
        pref.edit().put(sp, value).apply()

        showWebBrowser(findItemViewHolder(appSettingItem)?.textView1, value)
    }

    fun showWebBrowser(tv: TextView?, prefValue: String) {
        tv ?: return
        val cn = prefValue.cn()
        val (label, icon) = CustomShare.getInfo(this, cn)
        tv.text = label ?: getString(R.string.not_selected)
        tv.setCompoundDrawablesRelativeWithIntrinsicBounds(icon, null, null, null)
        tv.compoundDrawablePadding = (resources.displayMetrics.density * 4f + 0.5f).toInt()
    }
}
