Pith - varnish
package com.vgmlr.varnish

import android.app.Application
import android.graphics.Rect
import android.net.Uri
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import java.io.File
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch

sealed interface Screen {
    val id: String

    data object Home : Screen {
        override val id = "home"
    }

    data object Settings : Screen {
        override val id = "settings"
    }

    data class Folder(val path: String) : Screen {
        override val id: String get() = "folder:$path"
    }

    data class Image(val path: String, val folder: String?) : Screen {
        override val id: String get() = "image:${folder ?: "search"}"
    }

    data class Crop(val path: String) : Screen {
        override val id: String get() = "crop:$path"
    }
}

@OptIn(FlowPreview::class)
class VarnishViewModel(app: Application) : AndroidViewModel(app) {
    val repo = VarnishRepo(app)
    private val prefs = VarnishPrefs(app)

    val settings: StateFlow<VarnishSettings> =
        prefs.settings.stateIn(viewModelScope, SharingStarted.Eagerly, VarnishSettings())

    private val _access = MutableStateFlow(repo.hasAccess())
    val access: StateFlow<Boolean> = _access.asStateFlow()

    private val _scan = MutableStateFlow(ScanResult())
    val scan: StateFlow<ScanResult> = _scan.asStateFlow()

    private val _scanning = MutableStateFlow(false)
    val scanning: StateFlow<Boolean> = _scanning.asStateFlow()

    private val _query = MutableStateFlow("")
    val query: StateFlow<String> = _query.asStateFlow()

    private val _stack = MutableStateFlow<List<Screen>>(listOf(Screen.Home))
    val stack: StateFlow<List<Screen>> = _stack.asStateFlow()

    private val _folderFiles = MutableStateFlow<List<MediaFile>>(emptyList())
    val folderFiles: StateFlow<List<MediaFile>> = _folderFiles.asStateFlow()

    val folderBlur: StateFlow<Set<String>> = marks(MarkKind.FOLDER_BLUR)
    val fileBlur: StateFlow<Set<String>> = marks(MarkKind.FILE_BLUR)
    val favorites: StateFlow<Set<String>> = marks(MarkKind.FAVORITE)
    val listFolders: StateFlow<Set<String>> = marks(MarkKind.LIST_VIEW)

    private val favFiles: StateFlow<List<MediaFile>> =
        combine(favorites, settings, _scan) { f, s, _ -> f to s }
            .map { (f, s) -> repo.mediaAt(f, s) }
            .stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())

    val folders: StateFlow<List<MediaFolder>> = combine(_scan, favFiles) { r, f ->
        if (f.isEmpty()) r.folders
        else listOf(
            repo.virtualFolder(VarnishConfig.FAV_PATH, VarnishConfig.FAV_NAME, f)
        ) + r.folders
    }.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())

    val searchResults: StateFlow<List<MediaFile>> = combine(
        _scan,
        _query.debounce(150),
        settings.map { it.sort }.distinctUntilChanged()
    ) { r, q, sort -> repo.search(r, q, sort) }
        .flowOn(Dispatchers.Default)
        .stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())

    private var current: VarnishSettings? = null
    private var folderPath: String? = null
    private var scanJob: Job? = null
    var topFor: List<MediaFile>? = null

    init {
        viewModelScope.launch {
            var key: Any? = null
            var sort: SortMode? = null
            prefs.settings.collect { s ->
                current = s
                val k = Triple(s.scan, s.types, s.showHidden)
                if (k != key) {
                    key = k
                    rescan(s)
                } else if (s.sort != sort) {
                    val sorted = repo.sortFiles(_folderFiles.value, s.sort)
                    topFor = sorted
                    _folderFiles.value = sorted
                }
                sort = s.sort
            }
        }
        viewModelScope.launch {
            favFiles.collect { if (folderPath == VarnishConfig.FAV_PATH) _folderFiles.value = it }
        }
    }

    // scan

    fun refresh() {
        val s = current
        if (s == null) _access.value = repo.hasAccess() else rescan(s)
    }

    private fun rescan(s: VarnishSettings) {
        _access.value = repo.hasAccess()
        if (!_access.value) return
        scanJob?.cancel()
        scanJob = viewModelScope.launch {
            _scanning.value = true
            try {
                reloadFolderNow()
                _scan.value = repo.scan(s)
            } finally {
                if (scanJob == coroutineContext[Job]) _scanning.value = false
            }
        }
    }

    private suspend fun reloadFolderNow() {
        val p = folderPath ?: return
        if (p == VarnishConfig.FAV_PATH) {
            _folderFiles.value = favFiles.value
            return
        }
        val list = repo.folderFiles(p, settingsNow())
        if (folderPath == p) _folderFiles.value = list
    }

    private suspend fun changed() {
        reloadFolderNow()
        refresh()
    }

    private fun settingsNow() = current ?: settings.value

    private fun marks(kind: Int): StateFlow<Set<String>> =
        repo.marks(kind).stateIn(viewModelScope, SharingStarted.Eagerly, emptySet())

    // navigation

    fun open(s: Screen) {
        _stack.update { it + s }
        when (s) {
            is Screen.Folder -> focusFolder(s.path, force = true)
            is Screen.Image -> s.folder?.let { focusFolder(it, force = false) }
            else -> Unit
        }
    }

    fun back(): Boolean {
        val st = _stack.value
        if (st.size <= 1) return false
        _stack.value = st.dropLast(1)
        when (val t = _stack.value.last()) {
            is Screen.Folder -> focusFolder(t.path, force = false)
            is Screen.Image -> t.folder?.let { focusFolder(it, force = false) }
            else -> Unit
        }
        return true
    }

    fun openExternal(uri: Uri) = op {
        val path = repo.resolve(uri) ?: return@op
        val folder = File(path).parent ?: return@op
        _stack.value = listOf(Screen.Image(path, folder))
        focusFolder(folder, force = true)
    }

    private fun focusFolder(path: String, force: Boolean) {
        if (!force && path == folderPath) return
        if (path != folderPath) _folderFiles.value = emptyList()
        folderPath = path
        viewModelScope.launch { reloadFolderNow() }
    }

    // state

    fun setQuery(q: String) {
        _query.value = q
    }

    fun updateSettings(block: (VarnishSettings) -> VarnishSettings) {
        viewModelScope.launch { prefs.update(block) }
    }

    fun sortAlpha() = updateSettings { it.copy(sort = it.sort.byAlpha()) }
    fun sortDate() = updateSettings { it.copy(sort = it.sort.byDate()) }

    fun setFolderBlur(path: String, on: Boolean) = op {
        repo.setMarks(MarkKind.FOLDER_BLUR, listOf(path), on)
    }

    fun setFileBlur(paths: Collection<String>, on: Boolean) = op {
        repo.setMarks(MarkKind.FILE_BLUR, paths, on)
    }

    fun setFavorite(path: String, on: Boolean) = op {
        repo.setMarks(MarkKind.FAVORITE, listOf(path), on)
    }

    fun setListView(path: String, on: Boolean) = op {
        repo.setMarks(MarkKind.LIST_VIEW, listOf(path), on)
    }

    // file ops

    fun deleteFiles(paths: List<String>, done: (List<String>) -> Unit = {}) = op {
        val gone = repo.deleteFiles(paths)
        changed()
        done(gone)
    }

    fun deleteFolder(path: String, done: (Boolean) -> Unit = {}) = op {
        val ok = repo.deleteFolder(path)
        changed()
        done(ok)
    }

    fun renameFile(path: String, base: String, done: (String?) -> Unit = {}) = op {
        val to = repo.renameFile(path, base)
        if (to != null) changed()
        done(to)
    }

    fun renameFolder(path: String, name: String, done: (String?) -> Unit = {}) = op {
        val to = repo.renameFolder(path, name)
        if (to != null) {
            _stack.update { st ->
                st.map { if (it is Screen.Folder && it.path == path) Screen.Folder(to) else it }
            }
            if (folderPath == path) folderPath = to
            changed()
        }
        done(to)
    }

    fun createFolder(parent: String, name: String, done: (String?) -> Unit = {}) = op {
        val dir = repo.createFolder(parent, name)
        if (dir != null) refresh()
        done(dir)
    }

    fun moveFile(path: String, destDir: String, done: (String?) -> Unit = {}) = op {
        val to = repo.moveFile(path, destDir)
        if (to != null) changed()
        done(to)
    }

    fun crop(path: String, rect: Rect, done: (Boolean) -> Unit = {}) = op {
        val out = repo.cropSave(path, rect, settingsNow())
        if (out != null) {
            val folder = File(out).parent.orEmpty()
            folderPath = folder
            reloadFolderNow()
            _stack.update { st ->
                st.dropLastWhile { it is Screen.Crop || it is Screen.Image } +
                    Screen.Image(out, folder)
            }
            refresh()
        }
        done(out != null)
    }

    fun info(path: String, done: (MediaInfo) -> Unit) = op { done(repo.info(path)) }

    private fun op(block: suspend () -> Unit) {
        viewModelScope.launch { block() }
    }
}