package net.sergeych.toread

import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.Card
import androidx.compose.material3.Checkbox
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateMapOf
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.draw.clip
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.key.type
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import net.sergeych.toread.fb2.Fb2Format
import net.sergeych.toread.storage.BookReadingStatus
import kotlin.math.roundToInt
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

@Composable
@OptIn(ExperimentalMaterial3Api::class)
internal fun LibraryScreen(
    state: AppState.Library,
    activeScan: LibraryScanProgress?,
    onStateChange: (AppState) -> Unit,
    onNavigateToScan: () -> Unit,
    onStartScan: (String) -> Unit,
) {
    val scope = rememberCoroutineScope()
    var busy by remember { mutableStateOf(false) }
    var message by remember(state.message) { mutableStateOf(state.message) }
    var items by remember(state.items) { mutableStateOf(state.items) }
    var nextOffset by remember(state.items) { mutableStateOf(state.items.size) }
    var loadingPage by remember(state.items) { mutableStateOf(false) }
    var endReached by remember(state.items) { mutableStateOf(false) }
    var wasScanning by remember { mutableStateOf(false) }
    var settingsMenuOpen by remember { mutableStateOf(false) }
    var autoScanDownloads by remember { mutableStateOf(true) }
    var autoScanSettingLoaded by remember { mutableStateOf(false) }
    var searchText by remember { mutableStateOf("") }
    var searchResults by remember { mutableStateOf<List<LibraryItem>>(emptyList()) }
    var searching by remember { mutableStateOf(false) }
    val coverCache = remember { mutableStateMapOf<String, LibraryCover?>() }
    val searchActive = searchText.isNotBlank()
    val visibleItems = if (searchActive) searchResults else items

    suspend fun loadPage(reset: Boolean = false) {
        if (loadingPage) return
        loadingPage = true
        if (reset) {
            items = emptyList()
            nextOffset = 0
            endReached = false
            coverCache.clear()
        }
        val offset = if (reset) 0 else nextOffset
        try {
            val page = loadLibraryItemsPage(LibraryPageSize, offset)
            items = if (reset) page else items + page
            nextOffset = offset + page.size
            endReached = page.size < LibraryPageSize
        } catch (t: Throwable) {
            message = t.message ?: "Could not load library."
            endReached = true
        } finally {
            loadingPage = false
        }
    }

    fun refresh(nextMessage: String? = message) {
        message = nextMessage
        scope.launch {
            if (searchActive) {
                searching = true
                searchResults = searchLibraryItems(searchText, SearchResultLimit)
                searching = false
            } else {
                loadPage(reset = true)
            }
        }
    }

    fun clearSearch() {
        searchText = ""
        searchResults = emptyList()
        searching = false
    }

    LaunchedEffect(state.scanPath, state.message) {
        if (items.isEmpty() && !endReached) loadPage(reset = true)
    }

    LaunchedEffect(searchText) {
        val query = searchText
        if (query.isBlank()) {
            searchResults = emptyList()
            searching = false
            return@LaunchedEffect
        }
        searching = true
        delay(1_000)
        runCatching { searchLibraryItems(query, SearchResultLimit) }
            .onSuccess {
                if (searchText == query) {
                    searchResults = it
                    message = null
                }
            }
            .onFailure {
                if (searchText == query) {
                    searchResults = emptyList()
                    message = it.message ?: "Search failed."
                }
            }
        if (searchText == query) searching = false
    }

    LaunchedEffect(Unit) {
        autoScanDownloads = loadScanDownloadsAutomatically()
        autoScanSettingLoaded = true
    }

    LaunchedEffect(autoScanSettingLoaded, autoScanDownloads) {
        if (!autoScanSettingLoaded || !autoScanDownloads || activeScan != null) return@LaunchedEffect
        if (loadDownloadsWasScanned()) {
            downloadsScanPath()?.let { path ->
                message = "Scanning Downloads..."
                onStartScan(path)
            }
        }
    }

    LaunchedEffect(activeScan != null) {
        if (activeScan != null) {
            wasScanning = true
            while (true) {
                delay(5_000)
                loadPage(reset = true)
            }
        } else if (wasScanning) {
            wasScanning = false
            loadPage(reset = true)
        }
    }

    Scaffold(
        topBar = {
            TopAppBar(
                title = {
                    LibrarySearchField(
                        value = searchText,
                        onValueChange = { searchText = it },
                        onClear = ::clearSearch,
                        modifier = Modifier.fillMaxWidth(),
                    )
                },
                colors = themedTopAppBarColors(),
                actions = {
                    IconButton(onClick = { refresh() }, enabled = !busy && !loadingPage) {
                        Icon(Icons.Filled.Refresh, contentDescription = "Refresh library")
                    }
                    Box {
                        IconButton(onClick = { settingsMenuOpen = true }) {
                            Icon(Icons.Filled.MoreVert, contentDescription = "Library options")
                        }
                        DropdownMenu(expanded = settingsMenuOpen, onDismissRequest = { settingsMenuOpen = false }) {
                            DropdownMenuItem(
                                leadingIcon = {
                                    Checkbox(checked = autoScanDownloads, onCheckedChange = null)
                                },
                                text = { Text("Scan Downloads automatically") },
                                onClick = {
                                    val next = !autoScanDownloads
                                    autoScanDownloads = next
                                    settingsMenuOpen = false
                                    scope.launch { saveScanDownloadsAutomatically(next) }
                                },
                            )
                        }
                    }
                },
            )
        },
        floatingActionButton = {
            FloatingActionButton(onClick = onNavigateToScan) {
                Icon(Icons.Filled.Add, contentDescription = "Scan folder")
            }
        },
    ) {
        BoxWithConstraints(
            modifier = Modifier
                .fillMaxSize()
                .padding(it)
                .onPreviewKeyEvent { event ->
                    if (event.type == KeyEventType.KeyDown && event.key == Key.Escape && searchText.isNotBlank()) {
                        clearSearch()
                        true
                    } else {
                        false
                    }
                }
                .background(readerBackground()),
        ) {
            val wide = maxWidth >= 800.dp
            if (visibleItems.isEmpty() && (loadingPage || searching)) {
                Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
                    CircularProgressIndicator()
                }
            } else if (visibleItems.isEmpty()) {
                if (searchActive) {
                    EmptySearchPane(modifier = Modifier.fillMaxSize().padding(if (wide) 24.dp else 14.dp))
                } else {
                    EmptyLibraryPane(modifier = Modifier.fillMaxSize().padding(if (wide) 24.dp else 14.dp))
                }
            } else {
                LazyColumn(
                    modifier = Modifier.fillMaxSize(),
                    contentPadding = PaddingValues(bottom = if (activeScan != null) 88.dp else 0.dp),
                    verticalArrangement = Arrangement.spacedBy(4.dp),
                ) {
                    val hasReadingNow = !searchActive && visibleItems.firstOrNull()?.readingStatus == BookReadingStatus.READING
                    if (hasReadingNow) {
                        item(key = "section-reading") {
                            LibrarySectionHeader("reading now")
                        }
                    }
                    itemsIndexed(visibleItems, key = { _, item -> item.fileId }) { index, item ->
                        if (
                            hasReadingNow &&
                            item.readingStatus != BookReadingStatus.READING &&
                            (index == 0 || visibleItems[index - 1].readingStatus == BookReadingStatus.READING)
                        ) {
                            LibrarySectionHeader("my library")
                        }
                        LibraryRow(
                            item = item,
                            coverCache = coverCache,
                            enabled = !busy,
                            onOpen = {
                                scope.launch {
                                    busy = true
                                    try {
                                        val next = runCatching {
                                            val bytes = openLibraryBook(item.fileId) ?: error("Book file is not available.")
                                            val book = Fb2Format.parse(bytes, item.storageUri ?: item.title)
                                            markLibraryReadingStatus(item.fileId, BookReadingStatus.READING)
                                            saveActiveReadingFileId(item.fileId)
                                            AppState.Reader(
                                                fileId = item.fileId,
                                                book = book,
                                                libraryItems = visibleItems,
                                                scanPath = state.scanPath,
                                                message = message,
                                            )
                                        }.getOrElse {
                                            AppState.Library(visibleItems, state.scanPath, it.message ?: "Could not open book.")
                                        }
                                        onStateChange(next)
                                    } finally {
                                        busy = false
                                    }
                                }
                            },
                            onMarkAsRead = {
                                scope.launch {
                                    busy = true
                                    try {
                                        if (markLibraryReadingStatus(item.fileId, BookReadingStatus.READ)) {
                                            message = "Marked ${item.title} as read."
                                            refresh()
                                        } else {
                                            message = "Could not update ${item.title}."
                                        }
                                    } finally {
                                        busy = false
                                    }
                                }
                            },
                            onMarkAsUnread = {
                                scope.launch {
                                    busy = true
                                    try {
                                        if (markLibraryReadingStatus(item.fileId, BookReadingStatus.NEW)) {
                                            message = "Marked ${item.title} as unread."
                                            refresh()
                                        } else {
                                            message = "Could not update ${item.title}."
                                        }
                                    } finally {
                                        busy = false
                                    }
                                }
                            },
                            onNotInterested = {
                                scope.launch {
                                    busy = true
                                    try {
                                        if (markLibraryReadingStatus(item.fileId, BookReadingStatus.NOT_INTERESTED)) {
                                            message = "Marked ${item.title} as not interesting."
                                            refresh()
                                        } else {
                                            message = "Could not update ${item.title}."
                                        }
                                    } finally {
                                        busy = false
                                    }
                                }
                            },
                            onDelete = {
                                scope.launch {
                                    busy = true
                                    try {
                                        val deleted = runCatching { deleteLibraryItem(item.fileId) }.getOrDefault(false)
                                        message = if (deleted) "Removed ${item.title}." else "Could not remove ${item.title}."
                                        if (deleted) {
                                            items = items.filterNot { it.fileId == item.fileId }
                                            searchResults = searchResults.filterNot { it.fileId == item.fileId }
                                            coverCache.remove(item.fileId)
                                            nextOffset = (nextOffset - 1).coerceAtLeast(items.size)
                                        }
                                    } finally {
                                        busy = false
                                    }
                                }
                            },
                        )
                    }
                    if (!searchActive && !endReached) {
                        item(key = "load-more") {
                            LaunchedEffect(nextOffset, items.size) {
                                if (!loadingPage) loadPage()
                            }
                            Box(
                                modifier = Modifier.fillMaxWidth().padding(18.dp),
                                contentAlignment = Alignment.Center,
                            ) {
                                CircularProgressIndicator(modifier = Modifier.width(24.dp).height(24.dp), strokeWidth = 2.dp)
                            }
                        }
                    }
                }
            }
            activeScan?.let { progress ->
                LibraryScanStatusPanel(
                    progress = progress,
                    modifier = Modifier
                        .align(Alignment.BottomCenter)
                        .padding(horizontal = if (wide) 24.dp else 14.dp, vertical = 14.dp),
                )
            }
        }
    }
}

@Composable
private fun LibrarySearchField(
    value: String,
    onValueChange: (String) -> Unit,
    onClear: () -> Unit,
    modifier: Modifier = Modifier,
) {
    val shape = RoundedCornerShape(16.dp)
    Row(
        modifier = modifier
            .height(42.dp)
            .clip(shape)
            .background(MaterialTheme.colorScheme.surface)
            .padding(horizontal = 12.dp)
            .onPreviewKeyEvent { event ->
                if (event.type == KeyEventType.KeyDown && event.key == Key.Escape && value.isNotBlank()) {
                    onClear()
                    true
                } else {
                    false
                }
            },
        verticalAlignment = Alignment.CenterVertically,
    ) {
        Icon(
            Icons.Filled.Search,
            contentDescription = "Search library",
            modifier = Modifier.size(18.dp),
            tint = MaterialTheme.colorScheme.outline,
        )
        Box(
            modifier = Modifier
                .weight(1f)
                .padding(start = 8.dp, end = 6.dp),
            contentAlignment = Alignment.CenterStart,
        ) {
            BasicTextField(
                value = value,
                onValueChange = onValueChange,
                singleLine = true,
                textStyle = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onSurface),
                cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
                modifier = Modifier.fillMaxWidth(),
            )
            if (value.isBlank()) {
                Text(
                    "Search library",
                    style = MaterialTheme.typography.bodyMedium,
                    color = MaterialTheme.colorScheme.outline.copy(alpha = 0.72f),
                    maxLines = 1,
                )
            }
        }
        if (value.isNotBlank()) {
            IconButton(onClick = onClear, modifier = Modifier.size(30.dp)) {
                Icon(Icons.Filled.Close, contentDescription = "Clear search", modifier = Modifier.size(17.dp))
            }
        }
    }
}

@Composable
private fun EmptyLibraryPane(modifier: Modifier = Modifier) {
    Box(modifier, contentAlignment = Alignment.Center) {
        Card(shape = RoundedCornerShape(8.dp), colors = quietCardColors()) {
            Column(Modifier.padding(20.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
                Text("No books indexed", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
                Text("Scan a folder containing FB2 or FB2.ZIP files.", style = MaterialTheme.typography.bodyMedium)
            }
        }
    }
}

@Composable
private fun EmptySearchPane(modifier: Modifier = Modifier) {
    Box(modifier, contentAlignment = Alignment.Center) {
        Text("No matches", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.outline)
    }
}

@Composable
private fun LibrarySectionHeader(text: String) {
    Text(
        text,
        style = MaterialTheme.typography.labelSmall,
        color = MaterialTheme.colorScheme.outline,
        textAlign = TextAlign.Center,
        modifier = Modifier.fillMaxWidth().padding(top = 10.dp, bottom = 4.dp),
    )
}

@Composable
private fun LibraryScanStatusPanel(progress: LibraryScanProgress, modifier: Modifier = Modifier) {
    Card(
        shape = RoundedCornerShape(8.dp),
        colors = quietCardColors(),
        modifier = modifier.fillMaxWidth(),
    ) {
        Row(
            modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 12.dp),
            horizontalArrangement = Arrangement.spacedBy(12.dp),
            verticalAlignment = Alignment.CenterVertically,
        ) {
            CircularProgressIndicator(modifier = Modifier.width(22.dp).height(22.dp), strokeWidth = 2.dp)
            Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
                Text(
                    progress.toCatalogScanMessage(),
                    style = MaterialTheme.typography.bodyMedium,
                    color = MaterialTheme.colorScheme.primary,
                    fontWeight = FontWeight.SemiBold,
                )
                Text(
                    "Imported ${progress.importedFiles}, skipped ${progress.skippedFiles}, failed ${progress.failedFiles}",
                    style = MaterialTheme.typography.bodySmall,
                    color = MaterialTheme.colorScheme.outline,
                )
            }
        }
    }
}

@Composable
private fun LibraryRow(
    item: LibraryItem,
    coverCache: MutableMap<String, LibraryCover?>,
    enabled: Boolean,
    onOpen: () -> Unit,
    onMarkAsRead: () -> Unit,
    onMarkAsUnread: () -> Unit,
    onNotInterested: () -> Unit,
    onDelete: () -> Unit,
) {
    var menuOpen by remember { mutableStateOf(false) }

    Card(shape = RoundedCornerShape(6.dp), colors = quietCardColors(), modifier = Modifier.fillMaxWidth()) {
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .clickable(enabled = enabled, onClick = onOpen)
                .padding(horizontal = 8.dp, vertical = 6.dp),
            horizontalArrangement = Arrangement.spacedBy(10.dp),
            verticalAlignment = Alignment.CenterVertically,
        ) {
            LibraryCover(item, coverCache, modifier = Modifier.width(46.dp).aspectRatio(0.68f))
            Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
                Text(
                    item.title,
                    style = MaterialTheme.typography.titleMedium,
                    fontWeight = FontWeight.SemiBold,
                    maxLines = 1,
                )
                Text(
                    item.authors.joinToString().ifBlank { "Unknown author" },
                    style = MaterialTheme.typography.bodyMedium,
                    color = MaterialTheme.colorScheme.secondary,
                    maxLines = 1,
                )
                Text(
                    item.libraryMetadataLine(),
                    style = MaterialTheme.typography.bodySmall,
                    color = MaterialTheme.colorScheme.outline,
                    maxLines = 1,
                )
            }
            Box {
                IconButton(onClick = { menuOpen = true }, enabled = enabled) {
                    Icon(Icons.Filled.MoreVert, contentDescription = "Book menu for ${item.title}")
                }
                DropdownMenu(expanded = menuOpen, onDismissRequest = { menuOpen = false }) {
                    DropdownMenuItem(
                        text = { Text("Open") },
                        onClick = {
                            menuOpen = false
                            onOpen()
                        },
                    )
                    HorizontalDivider()
                    if (item.readingStatus != BookReadingStatus.READ) {
                        DropdownMenuItem(
                            text = { Text("Mark as read") },
                            onClick = {
                                menuOpen = false
                                onMarkAsRead()
                            },
                        )
                    }
                    if (item.readingStatus == BookReadingStatus.READ) {
                        DropdownMenuItem(
                            text = { Text("Mark as unread") },
                            onClick = {
                                menuOpen = false
                                onMarkAsUnread()
                            },
                        )
                    }
                    DropdownMenuItem(
                        text = { Text("Not interesting") },
                        onClick = {
                            menuOpen = false
                            onNotInterested()
                        },
                    )
                    HorizontalDivider()
                    DropdownMenuItem(
                        text = { Text("Delete") },
                        onClick = {
                            menuOpen = false
                            onDelete()
                        },
                    )
                }
            }
        }
    }
}

@Composable
private fun LibraryCover(
    item: LibraryItem,
    coverCache: MutableMap<String, LibraryCover?>,
    modifier: Modifier = Modifier.width(54.dp).aspectRatio(0.68f),
) {
    LaunchedEffect(item.fileId) {
        if (!coverCache.containsKey(item.fileId)) {
            coverCache[item.fileId] = loadLibraryItemCover(item.fileId)
        }
    }
    val cover = coverCache[item.fileId]
    val bitmap = remember(item.fileId, cover?.image) {
        cover?.image?.let(::decodeImageBytes)
    }
    Box(
        modifier = modifier
            .clip(RoundedCornerShape(6.dp))
            .background(MaterialTheme.colorScheme.surfaceVariant),
        contentAlignment = Alignment.Center,
    ) {
        if (bitmap != null) {
            Image(
                bitmap = bitmap,
                contentDescription = item.title,
                modifier = Modifier.fillMaxSize(),
                contentScale = ContentScale.Crop,
            )
        } else {
            Text(
                item.title.firstOrNull()?.uppercase() ?: "",
                style = MaterialTheme.typography.titleMedium,
                color = MaterialTheme.colorScheme.outline,
            )
        }
    }
}

private fun LibraryItem.libraryMetadataLine(): String =
    listOfNotNull(
        readingStatus.displayLabel,
        lastReadAt?.formatLastRead(),
        date?.yearOrRaw(),
        language?.uppercase(),
        format?.uppercase(),
        sizeBytes?.formatBytes(),
    ).joinToString("  |  ").ifBlank { "No metadata" }

private val BookReadingStatus.displayLabel: String
    get() = when (this) {
        BookReadingStatus.NEW -> "New"
        BookReadingStatus.READING -> "Reading"
        BookReadingStatus.READ -> "Read"
        BookReadingStatus.NOT_INTERESTED -> "Not interested"
    }

private fun Long.formatLastRead(): String = "Last read ${formatLibraryLastReadTime(this)}"

private fun String.yearOrRaw(): String =
    Regex("""\d{4}""").find(this)?.value ?: this

private fun Long.formatBytes(): String =
    when {
        this >= 1024L * 1024L -> "${(this / (1024.0 * 1024.0)).roundToInt()} MB"
        this >= 1024L -> "${(this / 1024.0).roundToInt()} KB"
        else -> "$this B"
    }

private fun LibraryScanProgress.toCatalogScanMessage(): String {
    val total = totalFiles ?: return "Scanned $scannedFiles books"
    val percent = if (total <= 0) {
        100
    } else {
        ((scannedFiles.toDouble() / total.toDouble()) * 100.0).roundToInt().coerceIn(0, 100)
    }
    return "Scanned $scannedFiles of $total, $percent% done"
}

private const val LibraryPageSize: Int = 50
private const val SearchResultLimit: Int = 100
