package net.sergeych.toread

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

private const val DefaultToastDurationMillis = 1_600L
private const val DeleteUndoDurationMillis = 5_000L
private const val ScanResultToastDurationMillis = 5_000L
private const val BackgroundLibraryPageSize = 50

private data class AppToastData(
    val id: Long,
    val message: String,
    val actionLabel: String? = null,
    val onAction: (() -> Unit)? = null,
    val durationMillis: Long = DefaultToastDurationMillis,
)

private data class PendingLibraryDelete(
    val id: Long,
    val request: LibraryDeleteRequest,
    val restore: () -> Unit,
)

internal data class LibraryItemRefreshRequest(
    val id: Long,
    val fileId: String,
)

@Composable
@Preview
fun App() {
    var themeMode by remember { mutableStateOf(ThemeMode.SYSTEM) }
    var systemDark by remember { mutableStateOf(isPlatformDarkTheme()) }
    var localePreferenceLoaded by remember { mutableStateOf(false) }
    var toast by remember { mutableStateOf<AppToastData?>(null) }
    var nextToastId by remember { mutableStateOf(0L) }
    val scope = rememberCoroutineScope()
    fun showToast(
        message: String,
        actionLabel: String? = null,
        onAction: (() -> Unit)? = null,
        durationMillis: Long = DefaultToastDurationMillis,
    ) {
        nextToastId += 1
        toast = AppToastData(nextToastId, message, actionLabel, onAction, durationMillis)
    }
    val useDark = when (themeMode) {
        ThemeMode.LIGHT -> false
        ThemeMode.DARK -> true
        ThemeMode.SYSTEM -> systemDark
    }

    DisposableEffect(Unit) {
        val dispose = watchPlatformDarkTheme { systemDark = it }
        onDispose(dispose)
    }

    LaunchedEffect(Unit) {
        themeMode = loadThemeMode()
    }

    LaunchedEffect(Unit) {
        selectedAppLocaleTag = loadAppLocaleTag()?.takeIf { saved ->
            availableAppLanguages.any { it.localeTag == saved }
        }
        localePreferenceLoaded = true
    }

    LaunchedEffect(toast?.id) {
        val current = toast ?: return@LaunchedEffect
        delay(current.durationMillis)
        if (toast?.id == current.id) {
            toast = null
        }
    }

    MaterialTheme(colorScheme = if (useDark) darkReaderColorScheme() else lightReaderColorScheme()) {
        CompositionLocalProvider(LocalReaderThemeIsDark provides useDark) {
            Box(Modifier.fillMaxSize()) {
                Surface(Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
                    if (localePreferenceLoaded) {
                        BookReaderApp(
                            onThemeToggle = {
                                val next = themeMode.next()
                                themeMode = next
                                showToast(strings.themeChanged(next))
                                scope.launch {
                                    saveThemeMode(next)
                                }
                            },
                            onShowToast = ::showToast,
                        )
                    } else {
                        LoadingScreen(strings.loadingOpeningBook)
                    }
                }
                AppToast(toast, modifier = Modifier.align(Alignment.BottomCenter))
            }
        }
    }
}

@Composable
private fun AppToast(toast: AppToastData?, modifier: Modifier = Modifier) {
    if (toast != null) {
        Surface(
            modifier = modifier.padding(horizontal = 16.dp, vertical = 24.dp),
            shape = RoundedCornerShape(8.dp),
            color = MaterialTheme.colorScheme.inverseSurface,
            tonalElevation = 6.dp,
        ) {
            Row(
                modifier = Modifier
                    .widthIn(max = 520.dp)
                    .padding(start = 16.dp, end = if (toast.actionLabel == null) 16.dp else 8.dp, top = 8.dp, bottom = 8.dp),
                verticalAlignment = Alignment.CenterVertically,
            ) {
                Text(
                    toast.message,
                    style = MaterialTheme.typography.bodyMedium,
                    color = MaterialTheme.colorScheme.inverseOnSurface,
                    modifier = Modifier.weight(1f),
                )
                if (toast.actionLabel != null && toast.onAction != null) {
                    TextButton(
                        onClick = toast.onAction,
                        colors = ButtonDefaults.textButtonColors(
                            contentColor = MaterialTheme.colorScheme.inversePrimary,
                        ),
                    ) {
                        Text(toast.actionLabel)
                    }
                }
            }
        }
    }
}

@Composable
private fun BookReaderApp(
    onThemeToggle: () -> Unit,
    onShowToast: (
        message: String,
        actionLabel: String?,
        onAction: (() -> Unit)?,
        durationMillis: Long,
    ) -> Unit,
) {
    var state by remember { mutableStateOf<AppState>(AppState.LoadingStartup) }
    var libraryBackState by remember { mutableStateOf<AppState.Library?>(null) }
    var activeScan by remember { mutableStateOf<LibraryScanProgress?>(null) }
    var scanJob by remember { mutableStateOf<Job?>(null) }
    var pendingDelete by remember { mutableStateOf<PendingLibraryDelete?>(null) }
    var pendingDeleteJob by remember { mutableStateOf<Job?>(null) }
    var hiddenDeletedFileIds by remember { mutableStateOf<Set<String>>(emptySet()) }
    var nextDeleteId by remember { mutableStateOf(0L) }
    var nextLibraryItemRefreshId by remember { mutableStateOf(0L) }
    var libraryItemRefreshRequest by remember { mutableStateOf<LibraryItemRefreshRequest?>(null) }
    var imageViewer by remember { mutableStateOf<ViewedBookImage?>(null) }
    var resumeGeneration by remember { mutableStateOf(0L) }
    var downloadsRescanRequestGeneration by remember { mutableStateOf(0L) }
    val lifecycleOwner = LocalLifecycleOwner.current
    val scope = rememberCoroutineScope()

    fun refreshLibraryItem(fileId: String) {
        nextLibraryItemRefreshId += 1
        libraryItemRefreshRequest = LibraryItemRefreshRequest(nextLibraryItemRefreshId, fileId)
    }

    LaunchedEffect(Unit) {
        state = loadStartupState()
    }

    DisposableEffect(lifecycleOwner) {
        val observer = LifecycleEventObserver { _, event ->
            if (event == Lifecycle.Event.ON_RESUME) {
                resumeGeneration += 1
            }
        }
        lifecycleOwner.lifecycle.addObserver(observer)
        onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
    }

    LaunchedEffect(Unit) {
        while (true) {
            delay(500)
            val requestResult = runCatching { loadPlatformOpenBookRequest() }
            if (requestResult.isFailure) {
                val scanPath = when (val current = state) {
                    is AppState.Library -> current.scanPath
                    is AppState.Scan -> current.scanPath
                    is AppState.Reader -> current.scanPath
                    is AppState.BookInfo -> current.scanPath
                    is AppState.Error, AppState.LoadingStartup -> defaultLibraryScanPath().orEmpty()
                }
                state = AppState.Library(
                    emptyList(),
                    scanPath,
                    requestResult.exceptionOrNull()?.message ?: strings.couldNotOpenLibrary,
                    loadLibraryFilter(),
                )
                continue
            }
            val request = requestResult.getOrNull() ?: continue
            val scanPath = when (val current = state) {
                is AppState.Library -> current.scanPath
                is AppState.Scan -> current.scanPath
                is AppState.Reader -> current.scanPath
                is AppState.BookInfo -> current.scanPath
                is AppState.Error, AppState.LoadingStartup -> defaultLibraryScanPath().orEmpty()
            }
            val nextState = runCatching {
                val book = parseBookInBackground(request.bytes, request.displayName)
                saveActiveReadingFileId(request.id)
                AppState.Reader(
                    fileId = request.id,
                    book = book,
                    libraryItems = emptyList(),
                    scanPath = scanPath,
                    selectedFilter = when (val current = state) {
                        is AppState.Library -> current.selectedFilter
                        is AppState.Scan -> current.selectedFilter
                        is AppState.Reader -> current.selectedFilter
                        is AppState.BookInfo -> current.selectedFilter
                        is AppState.Error, AppState.LoadingStartup -> loadLibraryFilter()
                    },
                )
            }.getOrElse {
                AppState.Library(emptyList(), scanPath, it.message ?: strings.couldNotOpen(request.displayName), loadLibraryFilter())
            }
            libraryBackState = when (val current = state) {
                is AppState.Library -> current
                is AppState.Scan -> AppState.Library(current.items, current.scanPath, current.message, current.selectedFilter)
                is AppState.Reader, is AppState.BookInfo -> libraryBackState
                is AppState.Error, AppState.LoadingStartup -> null
            }
            state = nextState
            refreshLibraryItem(request.id)
        }
    }

    val activeBookState = when (val current = state) {
        is AppState.Reader -> current.fileId to current.libraryItems.isEmpty()
        is AppState.BookInfo -> current.fileId to current.libraryItems.isEmpty()
        else -> null
    }
    LaunchedEffect(activeBookState?.first, activeBookState?.second) {
        val (fileId, needsLibraryItems) = activeBookState ?: return@LaunchedEffect
        if (!needsLibraryItems) return@LaunchedEffect
        runCatching { loadLibraryItemsPage(BackgroundLibraryPageSize, 0) }
            .onSuccess { items ->
                state = when (val current = state) {
                    is AppState.Reader ->
                        if (current.fileId == fileId && current.libraryItems.isEmpty()) {
                            current.copy(libraryItems = items)
                        } else {
                            current
                        }
                    is AppState.BookInfo ->
                        if (current.fileId == fileId && current.libraryItems.isEmpty()) {
                            current.copy(libraryItems = items)
                        } else {
                            current
                        }
                    else -> current
                }
            }
    }

    fun commitPendingDelete(delete: PendingLibraryDelete) {
        scope.launch {
            val result = deleteLibraryBook(delete.request.fileId, delete.request.title)
            if (!result.deleted) {
                delete.restore()
                onShowToast(result.message, null, null, DefaultToastDurationMillis)
            } else {
                hiddenDeletedFileIds = hiddenDeletedFileIds + delete.request.fileId
            }
        }
    }

    fun requestDelete(
        request: LibraryDeleteRequest,
        remove: () -> Unit,
        restore: () -> Unit,
    ) {
        pendingDeleteJob?.cancel()
        pendingDelete?.let(::commitPendingDelete)

        nextDeleteId += 1
        val deleteId = nextDeleteId
        val nextDelete = PendingLibraryDelete(deleteId, request, restore)
        pendingDelete = nextDelete
        hiddenDeletedFileIds = hiddenDeletedFileIds - request.fileId
        remove()

        pendingDeleteJob = scope.launch {
            delay(DeleteUndoDurationMillis)
            val result = deleteLibraryBook(request.fileId, request.title)
            if (pendingDelete?.id == deleteId) {
                pendingDelete = null
                pendingDeleteJob = null
                if (!result.deleted) {
                    restore()
                    onShowToast(result.message, null, null, DefaultToastDurationMillis)
                } else {
                    hiddenDeletedFileIds = hiddenDeletedFileIds + request.fileId
                }
            }
        }

        onShowToast(
            strings.removed(request.title),
            strings.undo,
            {
                if (pendingDelete?.id == deleteId) {
                    pendingDeleteJob?.cancel()
                    pendingDeleteJob = null
                    pendingDelete = null
                    restore()
                    onShowToast(strings.restored(request.title), null, null, DefaultToastDurationMillis)
                }
            },
            DeleteUndoDurationMillis,
        )
    }

    fun navigateBack() {
        imageViewer?.let {
            imageViewer = null
            return
        }
        state = when (val current = state) {
            is AppState.BookInfo -> AppState.Reader(
                fileId = current.fileId,
                book = current.book,
                libraryItems = current.libraryItems,
                scanPath = current.scanPath,
                message = current.message,
                selectedFilter = current.selectedFilter,
            )
            is AppState.Reader -> {
                scope.launch { saveActiveReadingFileId(null) }
                refreshLibraryItem(current.fileId)
                val backState = libraryBackState?.copy(message = current.message)
                    ?: AppState.Library(
                        current.libraryItems,
                        current.scanPath,
                        current.message,
                        current.selectedFilter,
                    )
                libraryBackState = null
                downloadsRescanRequestGeneration += 1
                backState
            }
            is AppState.Scan -> AppState.Library(current.items, current.scanPath, current.message, current.selectedFilter)
            is AppState.Error -> AppState.LoadingStartup
            is AppState.Library, AppState.LoadingStartup -> current
        }
    }

    val navigationDepth = when (state) {
        is AppState.BookInfo -> 2
        is AppState.Error, is AppState.Reader, is AppState.Scan -> 1
        is AppState.Library, AppState.LoadingStartup -> 0
    } + if (imageViewer != null) 1 else 0

    PlatformBackHandler(
        enabled = navigationDepth > 0,
        navigationDepth = navigationDepth,
        onBack = ::navigateBack,
    )

    fun setLibraryFilter(filter: LibraryFilter) {
        state = when (val current = state) {
            is AppState.Library -> current.copy(selectedFilter = filter)
            is AppState.Scan -> current.copy(selectedFilter = filter)
            else -> current
        }
        libraryBackState = libraryBackState?.copy(selectedFilter = filter)
    }

    fun saveAndSetLibraryFilter(filter: LibraryFilter) {
        setLibraryFilter(filter)
        scope.launch {
            saveLibraryFilter(filter)
        }
    }

    fun startScan(path: String, userInitiated: Boolean) {
        if (scanJob?.isActive == true) return
        activeScan = LibraryScanProgress(0, 0, 0, 0)
        scanJob = scope.launch {
            val report = runCatching {
                scanLibrarySubtree(path) { progress ->
                    scope.launch {
                        if (scanJob?.isActive == true) activeScan = progress
                    }
                }
            }
            val message = report.fold(
                onSuccess = {
                    if (isDownloadsScanPath(path)) {
                        saveDownloadsWasScanned(true)
                    }
                    strings.scanReport(it.scannedFiles, it.importedFiles, it.skippedFiles, it.failedFiles)
                },
                onFailure = { it.message ?: strings.scanFailed },
            )
            val importedFiles = report.getOrNull()?.importedFiles
            when {
                importedFiles == null -> Unit
                importedFiles > 0 -> {
                    onShowToast(
                        strings.newBooksAdded(importedFiles),
                        strings.show,
                        { saveAndSetLibraryFilter(LibraryFilter.RecentlyAdded) },
                        ScanResultToastDurationMillis,
                    )
                }
                userInitiated -> {
                    onShowToast(strings.noNewBooksFound, null, null, DefaultToastDurationMillis)
                }
            }
            scanJob = null
            activeScan = null
            state = when (val current = state) {
                is AppState.Library -> current.copy(
                    scanPath = path,
                    message = message,
                )
                is AppState.Scan -> AppState.Library(
                    current.items,
                    path,
                    message,
                    selectedFilter = current.selectedFilter,
                )
                AppState.LoadingStartup -> loadLibraryState(message, path)
                is AppState.Reader -> current.copy(message = message)
                is AppState.BookInfo -> current.copy(message = message)
                is AppState.Error -> current
            }
        }
    }

    LaunchedEffect(resumeGeneration, downloadsRescanRequestGeneration, state !is AppState.LoadingStartup) {
        if (state is AppState.LoadingStartup || activeScan != null || scanJob?.isActive == true) return@LaunchedEffect
        if (!loadScanDownloadsAutomatically() || !loadDownloadsWasScanned()) return@LaunchedEffect
        val path = downloadsScanPath() ?: return@LaunchedEffect
        state = state.withMessage(strings.scanningDownloads)
        startScan(path, userInitiated = false)
    }

    Box(Modifier.fillMaxSize()) {
        val currentLibraryState = when (val current = state) {
            is AppState.Library -> current
            is AppState.Reader, is AppState.BookInfo -> libraryBackState
            else -> null
        }
        currentLibraryState?.let { libraryState ->
            LibraryScreen(
                state = libraryState,
                activeScan = activeScan,
                itemRefreshRequest = libraryItemRefreshRequest,
                hiddenFileIds = hiddenDeletedFileIds + pendingDelete?.request?.fileId?.let(::setOf).orEmpty(),
                onStateChange = { next ->
                    val currentState = state
                    if (next is AppState.Reader && currentState is AppState.Library) {
                        libraryBackState = currentState
                    }
                    state = next
                },
                onLibraryFilterChange = ::setLibraryFilter,
                onNavigateToScan = {
                    libraryBackState = null
                    state = AppState.Scan(
                        libraryState.items,
                        libraryState.scanPath,
                        libraryState.message,
                        libraryState.selectedFilter,
                    )
                },
                onDeleteRequested = ::requestDelete,
            )
        }

        when (val current = state) {
            AppState.LoadingStartup -> LoadingScreen(strings.loadingOpeningBook)
            is AppState.Library -> Unit
            is AppState.Scan -> ScanScreen(
                state = current,
                activeScan = activeScan,
                onStateChange = { state = it },
                onStartScan = { path ->
                    startScan(path, userInitiated = true)
                    state = AppState.Library(current.items, path, strings.scanning, current.selectedFilter)
                },
            )
            is AppState.Reader -> BookView(
                fileId = current.fileId,
                book = current.book,
                onImageOpen = { imageViewer = it },
                onThemeToggle = onThemeToggle,
                onBookChanged = ::refreshLibraryItem,
                onBookInfo = {
                    state = AppState.BookInfo(
                        fileId = current.fileId,
                        book = current.book,
                        libraryItems = current.libraryItems,
                        scanPath = current.scanPath,
                        message = current.message,
                        selectedFilter = current.selectedFilter,
                    )
                },
                onDeleted = { message ->
                    state = libraryBackState?.copy(message = message)
                        ?: AppState.Library(emptyList(), current.scanPath, message, current.selectedFilter)
                    libraryBackState = null
                },
                onDeleteRequested = ::requestDelete,
                onBack = ::navigateBack,
            )
            is AppState.BookInfo -> BookInfoScreen(
                fileId = current.fileId,
                book = current.book,
                onImageOpen = { imageViewer = it },
                onBack = ::navigateBack,
            )
            is AppState.Error -> ErrorScreen(current.message, onBack = ::navigateBack)
        }
    }

    imageViewer?.let { image ->
        ImageViewer(
            image = image,
            onBack = ::navigateBack,
        )
    }
}

internal data class ViewedBookImage(
    val bitmap: ImageBitmap,
    val bytes: ByteArray,
    val mimeType: String,
    val title: String,
)

private fun AppState.withMessage(message: String): AppState =
    when (this) {
        is AppState.Library -> copy(message = message)
        is AppState.Scan -> copy(message = message)
        is AppState.Reader -> copy(message = message)
        is AppState.BookInfo -> copy(message = message)
        is AppState.Error, AppState.LoadingStartup -> this
    }
