package net.sergeych.toread

import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.animateScrollBy
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.waitForUpOrCancellation
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
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.heightIn
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
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.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.CompositingStrategy
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.PointerType
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.text.intl.LocaleList
import androidx.compose.ui.text.style.BaselineShift
import androidx.compose.ui.text.style.Hyphens
import androidx.compose.ui.text.style.LineBreak
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.withAnnotation
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.isSpecified
import androidx.compose.ui.unit.sp
import net.sergeych.toread.fb2.Fb2Block
import net.sergeych.toread.fb2.Fb2Book
import net.sergeych.toread.fb2.Fb2Cite
import net.sergeych.toread.fb2.Fb2Epigraph
import net.sergeych.toread.fb2.Fb2EpigraphBlock
import net.sergeych.toread.fb2.Fb2ImageRef
import net.sergeych.toread.fb2.Fb2Poem
import net.sergeych.toread.fb2.Fb2PoemBlock
import net.sergeych.toread.fb2.Fb2Section
import net.sergeych.toread.fb2.Fb2Text
import net.sergeych.toread.fb2.Fb2TextSpan
import net.sergeych.toread.fb2.Fb2TextStyle
import net.sergeych.toread.text.HyphenationRegistry
import net.sergeych.toread.text.SoftHyphen
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
import kotlin.math.sqrt

private data class DecodedBookImage(
    val bitmap: ImageBitmap,
    val bytes: ByteArray,
)

@Composable
internal fun ContinuousBookReader(
    book: Fb2Book,
    stats: BookStats,
    listState: LazyListState,
    modifier: Modifier = Modifier,
    contentPlan: ReaderContentPlan = remember(book) { buildReaderContentPlan(book) },
    highlightedSentence: ReadAloudSentence? = null,
    readerFontSettings: ReaderFontSettings = defaultReaderFontSettings(),
    readerBackgroundColor: Color = MaterialTheme.colorScheme.surface,
    readerTextColor: Color = MaterialTheme.colorScheme.onSurface,
    onReaderFontZoom: (Int) -> Unit = {},
    onUserScroll: () -> Unit = {},
    onImageOpen: (ViewedBookImage) -> Unit = {},
    onNoteOpen: (String) -> Unit = {},
) {
    val hyphenation = remember { HyphenationRegistry() }
    val scope = rememberCoroutineScope()
    val textLineMetricsByItem = remember(contentPlan, readerFontSettings) { mutableStateMapOf<Int, TextLineMetrics>() }
    val pageHistory = remember(contentPlan, readerFontSettings) { ReaderPageHistory() }
    val contentPadding = PaddingValues(top=6.dp, bottom = 6.dp, start = 4.dp, end = 6.dp)
    val userScrollConnection = remember(onUserScroll) {
        object : NestedScrollConnection {
            override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
                if (source == NestedScrollSource.UserInput && available.y != 0f) {
                    onUserScroll()
                }
                return Offset.Zero
            }
        }
    }

    LazyColumn(
        state = listState,
        modifier = modifier
            .background(readerBackgroundColor)
            .nestedScroll(userScrollConnection)
            .readerFontZoomGesture(onReaderFontZoom)
            .pageTurnOnTouchSwipe(
                onPageDown = {
                    onUserScroll()
                    scope.launch {
                        listState.pageScrollByPage(1, textLineMetricsByItem, pageHistory)
                    }
                },
                onPageUp = {
                    onUserScroll()
                    scope.launch {
                        listState.pageScrollByPage(-1, textLineMetricsByItem, pageHistory)
                    }
                },
            )
            .pageTurnOnTouchTap(
                onPageDown = {
                    onUserScroll()
                    scope.launch {
                        listState.pageScrollByPage(1, textLineMetricsByItem, pageHistory)
                    }
                },
                onPageUp = {
                    onUserScroll()
                    scope.launch {
                        listState.pageScrollByPage(-1, textLineMetricsByItem, pageHistory)
                    }
                },
            ),
        contentPadding = contentPadding,
        verticalArrangement = Arrangement.spacedBy(10.dp),
    ) {
        itemsIndexed(contentPlan.elements) { itemIndex, element ->
            val highlightedRange = highlightedSentence
                ?.takeIf { it.itemIndex == itemIndex }
                ?.let { ReaderSentenceRange(it.start, it.endExclusive) }
            when (element) {
                ReaderElement.Cover -> Column(verticalArrangement = Arrangement.spacedBy(14.dp)) {
                    CoverAndTitle(book, onImageOpen = onImageOpen)
                    MetadataCard(book)
                    StatsCard(stats)
                }
                is ReaderElement.FixedSpacer -> Spacer(Modifier.height(element.heightDp.dp))
                ReaderElement.SectionSeparator -> Spacer(
                    Modifier.height(2.dp)
                        .background(MaterialTheme.colorScheme.secondaryFixedDim)
                        .fillMaxWidth().padding(vertical = 5.dp, horizontal = 4.dp)
                )
                is ReaderElement.SectionTitle -> {
                    val titleModifier = Modifier
                        .fillMaxWidth()
                        .then(
                            if (highlightedRange != null) {
                                Modifier.background(MaterialTheme.colorScheme.secondaryContainer)
                            } else {
                                Modifier
                            },
                        )
                        .padding(
                            top = if (element.depth == 0) 22.dp else 14.dp,
                            start = (element.depth * 12).dp,
                            bottom = 4.dp,
                        )
                    Text(
                        element.title,
                        style = when (element.depth) {
                            0 -> MaterialTheme.typography.headlineMedium
                            1 -> MaterialTheme.typography.titleLarge
                            else -> MaterialTheme.typography.titleMedium
                        }.copy(color = readerTextColor),
                        fontWeight = FontWeight.Bold,
                        lineHeight = if (element.depth == 0) 36.sp else 28.sp,
                        modifier = titleModifier,
                    )
                }
                is ReaderElement.BookImage -> BookImage(
                    book = book,
                    image = element.image,
                    modifier = Modifier.fillMaxWidth().padding(vertical = 10.dp),
                    contentScale = ContentScale.Fit,
                    onOpen = onImageOpen,
                )
                is ReaderElement.Paragraph -> ReaderText(
                    text = element.text,
                    language = book.language,
                    hyphenation = hyphenation,
                    style = readerParagraphTextStyle(book.language, readerFontSettings).copy(color = readerTextColor),
                    highlightedRange = highlightedRange,
                    textAlign = TextAlign.Justify,
                    modifier = Modifier.padding(start = (element.depth * 8).dp, end = 0.dp),
                    onTextLayout = { textLineMetricsByItem[itemIndex] = it.toTextLineMetrics() },
                    onLinkOpen = onNoteOpen,
                )
                is ReaderElement.Subtitle -> ReaderText(
                    text = element.text,
                    language = book.language,
                    hyphenation = hyphenation,
                    style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold, color = readerTextColor),
                    highlightedRange = highlightedRange,
                    textAlign = TextAlign.Center,
                    modifier = Modifier.fillMaxWidth().padding(top = 18.dp, bottom = 8.dp),
                    onTextLayout = { textLineMetricsByItem[itemIndex] = it.toTextLineMetrics() },
                    onLinkOpen = onNoteOpen,
                )
                is ReaderElement.Epigraph -> ReaderEpigraph(
                    epigraph = element.epigraph,
                    language = book.language,
                    hyphenation = hyphenation,
                    highlightedRange = highlightedRange,
                    depth = element.depth,
                    textColor = readerTextColor,
                    onLinkOpen = onNoteOpen,
                )
                is ReaderElement.Cite -> ReaderCite(
                    cite = element.cite,
                    language = book.language,
                    hyphenation = hyphenation,
                    highlightedRange = highlightedRange,
                    depth = element.depth,
                    textColor = readerTextColor,
                    onLinkOpen = onNoteOpen,
                )
                is ReaderElement.Poem -> ReaderPoem(
                    poem = element.poem,
                    language = book.language,
                    hyphenation = hyphenation,
                    highlightedRange = highlightedRange,
                    depth = element.depth,
                    textColor = readerTextColor,
                    onLinkOpen = onNoteOpen,
                )
            }
        }
    }
}

private fun Modifier.readerFontZoomGesture(
    onZoom: (Int) -> Unit,
): Modifier = pointerInput(onZoom) {
    awaitEachGesture {
        var baselineDistance: Float? = null
        while (true) {
            val event = awaitPointerEvent(pass = PointerEventPass.Main)
            val touches = event.changes.filter { it.pressed && it.type == PointerType.Touch }
            if (touches.isEmpty()) break
            if (touches.size < 2) {
                baselineDistance = null
                continue
            }

            val distance = touches.firstTwoDistance()
            val baseline = baselineDistance
            if (baseline == null || baseline <= 0f) {
                baselineDistance = distance
            } else {
                val zoom = distance / baseline
                when {
                    zoom >= ReaderZoomGestureStep -> {
                        onZoom(1)
                        baselineDistance = distance
                    }
                    zoom <= 1f / ReaderZoomGestureStep -> {
                        onZoom(-1)
                        baselineDistance = distance
                    }
                }
            }
            touches.forEach { it.consume() }
        }
    }
}

private fun List<androidx.compose.ui.input.pointer.PointerInputChange>.firstTwoDistance(): Float {
    val first = this[0].position
    val second = this[1].position
    val dx = second.x - first.x
    val dy = second.y - first.y
    return sqrt(dx * dx + dy * dy)
}

private fun Modifier.pageTurnOnTouchSwipe(
    onPageDown: () -> Unit,
    onPageUp: () -> Unit,
): Modifier = pointerInput(onPageDown, onPageUp) {
    awaitEachGesture {
        val down = awaitFirstDown(requireUnconsumed = false, pass = PointerEventPass.Initial)
        if (down.isConsumed) return@awaitEachGesture
        if (down.type != PointerType.Touch) {
            waitForUpOrCancellation(pass = PointerEventPass.Final)
            return@awaitEachGesture
        }

        val touchSlop = viewConfiguration.touchSlop
        val swipeSlop = touchSlop * ReaderSwipeTouchSlopMultiplier
        val touchSlopSquared = touchSlop * touchSlop
        var cancelled = false
        var movedBeyondTapSlop = false
        var pageScrollDirection = 0

        while (!cancelled && pageScrollDirection == 0) {
            val event = awaitPointerEvent(pass = PointerEventPass.Initial)
            if (event.changes.count { it.pressed && it.type == PointerType.Touch } > 1) {
                cancelled = true
                continue
            }

            val change = event.changes.firstOrNull { it.id == down.id }
            if (change == null) {
                cancelled = true
            } else if (change.isConsumed) {
                cancelled = true
            } else if (change.pressed) {
                val dx = change.position.x - down.position.x
                val dy = change.position.y - down.position.y
                val absX = abs(dx)
                val absY = abs(dy)
                val isHorizontalSwipeCandidate = absX >= absY * ReaderHorizontalSwipeDominance
                if (dx * dx + dy * dy > touchSlopSquared) {
                    movedBeyondTapSlop = true
                }
                if (absX >= swipeSlop && isHorizontalSwipeCandidate) {
                    pageScrollDirection = if (dx < 0f) 1 else -1
                    change.consume()
                } else if (movedBeyondTapSlop && !isHorizontalSwipeCandidate) {
                    cancelled = true
                } else if (movedBeyondTapSlop && change.position != change.previousPosition) {
                    change.consume()
                }
            } else {
                cancelled = true
            }
        }

        if (cancelled) return@awaitEachGesture

        if (pageScrollDirection != 0) {
            var released = false
            while (!released) {
                val event = awaitPointerEvent(pass = PointerEventPass.Initial)
                event.changes.forEach { change ->
                    if (change.type == PointerType.Touch) {
                        change.consume()
                    }
                }
                released = event.changes
                    .firstOrNull { it.id == down.id }
                    ?.pressed != true
            }
            if (pageScrollDirection > 0) {
                onPageDown()
            } else {
                onPageUp()
            }
            return@awaitEachGesture
        }
    }
}

private fun Modifier.pageTurnOnTouchTap(
    onPageDown: () -> Unit,
    onPageUp: () -> Unit,
): Modifier = pointerInput(onPageDown, onPageUp) {
    awaitEachGesture {
        val down = awaitFirstDown(requireUnconsumed = false, pass = PointerEventPass.Final)
        if (down.isConsumed) return@awaitEachGesture
        if (down.type != PointerType.Touch) {
            waitForUpOrCancellation(pass = PointerEventPass.Final)
            return@awaitEachGesture
        }

        val touchSlop = viewConfiguration.touchSlop
        val touchSlopSquared = touchSlop * touchSlop
        var upPosition: Offset? = null
        var cancelled = false

        while (upPosition == null && !cancelled) {
            val event = awaitPointerEvent(pass = PointerEventPass.Final)
            val change = event.changes.firstOrNull { it.id == down.id }
            if (change == null) {
                cancelled = true
            } else if (change.isConsumed) {
                cancelled = true
            } else if (change.pressed) {
                val dx = change.position.x - down.position.x
                val dy = change.position.y - down.position.y
                if (dx * dx + dy * dy > touchSlopSquared) {
                    cancelled = true
                }
            } else {
                upPosition = change.position
            }
        }

        if (cancelled) return@awaitEachGesture

        if (down.position.x < size.width / 2f) {
            onPageDown()
        } else {
            onPageUp()
        }
    }
}

private suspend fun LazyListState.pageScrollByPage(
    direction: Int,
    textLineMetricsByItem: Map<Int, TextLineMetrics>,
    pageHistory: ReaderPageHistory,
) {
    val current = currentPageAnchor(textLineMetricsByItem)
    val target = if (current != null) {
        pageHistory.targetFor(current, direction) {
            pageScrollTarget(direction, textLineMetricsByItem)
        }
    } else {
        pageScrollTarget(direction, textLineMetricsByItem)
    }
    if (target != null) {
        animateScrollToItem(target.itemIndex, target.scrollOffset)
    } else {
        animateScrollBy(pageScrollDistance() * direction)
    }
}

private fun LazyListState.pageScrollDistance(): Float {
    val layoutInfo = layoutInfo
    val viewportHeight = layoutInfo.viewportEndOffset - layoutInfo.viewportStartOffset
    return viewportHeight.toFloat().coerceAtLeast(0f)
}

private fun LazyListState.pageScrollTarget(
    direction: Int,
    textLineMetricsByItem: Map<Int, TextLineMetrics>,
): PageScrollTarget? {
    val layoutInfo = layoutInfo
    val viewportHeight = layoutInfo.viewportEndOffset - layoutInfo.viewportStartOffset
    val targetViewportOffset = if (direction > 0) {
        layoutInfo.viewportEndOffset
    } else {
        layoutInfo.viewportStartOffset - viewportHeight
    }
    val targetItem = layoutInfo.visibleItemsInfo.firstOrNull { item ->
        targetViewportOffset >= item.offset && targetViewportOffset < item.offset + item.size
    } ?: return null
    val metrics = textLineMetricsByItem[targetItem.index] ?: return null
    val targetItemOffset = targetViewportOffset - targetItem.offset
    val lineTop = metrics.lineTopForViewportStart(targetItemOffset) ?: return null

    return PageScrollTarget(targetItem.index, lineTop)
}

private fun LazyListState.currentPageAnchor(
    textLineMetricsByItem: Map<Int, TextLineMetrics>,
): PageScrollTarget? {
    val itemIndex = firstVisibleItemIndex
    val scrollOffset = firstVisibleItemScrollOffset
    val normalizedScrollOffset = textLineMetricsByItem[itemIndex]
        ?.lineTopForScrollOffset(scrollOffset)
        ?: scrollOffset
    return PageScrollTarget(itemIndex, normalizedScrollOffset)
}

private class ReaderPageHistory {
    private val anchors = mutableListOf<PageScrollTarget>()
    private var currentIndex = -1

    fun targetFor(
        current: PageScrollTarget,
        direction: Int,
        calculateTarget: () -> PageScrollTarget?,
    ): PageScrollTarget? {
        moveToCurrent(current)
        val adjacentIndex = currentIndex + direction
        if (adjacentIndex in anchors.indices) {
            currentIndex = adjacentIndex
            return anchors[currentIndex]
        }

        val target = calculateTarget() ?: return null
        if (target == current) return target

        if (direction > 0) {
            if (currentIndex < anchors.lastIndex) {
                anchors.subList(currentIndex + 1, anchors.size).clear()
            }
            anchors += target
            currentIndex = anchors.lastIndex
        } else {
            anchors.add(0, target)
            currentIndex = 0
        }
        return target
    }

    private fun moveToCurrent(current: PageScrollTarget) {
        if (currentIndex in anchors.indices && anchors[currentIndex] == current) return
        val existingIndex = anchors.indexOf(current)
        if (existingIndex >= 0) {
            currentIndex = existingIndex
        } else {
            anchors.clear()
            anchors += current
            currentIndex = 0
        }
    }
}

private data class PageScrollTarget(
    val itemIndex: Int,
    val scrollOffset: Int,
)

private data class TextLineMetrics(
    val lineTops: List<Int>,
    val lineBottoms: List<Int>,
) {
    fun lineTopForViewportStart(offsetPx: Int): Int? =
        lineTopContaining(offsetPx)
            ?: lineTops.firstOrNull { it >= offsetPx }
            ?: lineTops.lastOrNull()

    fun lineTopForScrollOffset(offsetPx: Int): Int? =
        lineTops.firstOrNull { it == offsetPx } ?: lineTopForViewportStart(offsetPx)

    private fun lineTopContaining(offsetPx: Int): Int? =
        lineTops.indices.firstOrNull { index ->
            offsetPx >= lineTops[index] && offsetPx < lineBottoms[index]
        }?.let { lineTops[it] }
}

private fun TextLayoutResult.toTextLineMetrics(): TextLineMetrics =
    TextLineMetrics(
        lineTops = List(lineCount) { line -> getLineTop(line).roundToInt() },
        lineBottoms = List(lineCount) { line -> getLineBottom(line).roundToInt() },
    )

@Composable
internal fun Fb2NoteReferenceDialog(
    book: Fb2Book,
    noteId: String,
    onDismiss: () -> Unit,
    onImageOpen: (ViewedBookImage) -> Unit,
    onNoteOpen: (String) -> Unit,
) {
    val note = book.notes[noteId]
    val hyphenation = remember { HyphenationRegistry() }

    AlertDialog(
        onDismissRequest = onDismiss,
        title = { Text(note?.title?.ifBlank { null } ?: strings.notes) },
        text = {
            if (note == null) {
                Text(strings.noNotes, style = MaterialTheme.typography.bodyMedium)
            } else {
                LazyColumn(
                    modifier = Modifier.fillMaxWidth().heightIn(max = 520.dp),
                    verticalArrangement = Arrangement.spacedBy(10.dp),
                ) {
                    items(note.readableBlocks()) { block ->
                        when (block) {
                            Fb2Block.EmptyLine -> Spacer(Modifier.height(12.dp))
                            is Fb2Block.Image -> BookImage(
                                book = book,
                                image = block.image,
                                modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp),
                                contentScale = ContentScale.Fit,
                                onOpen = onImageOpen,
                            )
                            is Fb2Block.Paragraph -> ReaderText(
                                text = block.content,
                                language = book.language,
                                hyphenation = hyphenation,
                                style = MaterialTheme.typography.bodyMedium.copy(lineHeight = 22.sp),
                                textAlign = TextAlign.Start,
                                onLinkOpen = onNoteOpen,
                            )
                            is Fb2Block.Subtitle -> ReaderText(
                                text = block.content,
                                language = book.language,
                                hyphenation = hyphenation,
                                style = MaterialTheme.typography.titleSmall.copy(fontWeight = FontWeight.SemiBold),
                                textAlign = TextAlign.Start,
                                onLinkOpen = onNoteOpen,
                            )
                            is Fb2Block.Epigraph -> ReaderEpigraph(
                                epigraph = block.epigraph,
                                language = book.language,
                                hyphenation = hyphenation,
                                highlightedRange = null,
                                depth = 0,
                                onLinkOpen = onNoteOpen,
                            )
                            is Fb2Block.Cite -> ReaderCite(
                                cite = block.cite,
                                language = book.language,
                                hyphenation = hyphenation,
                                highlightedRange = null,
                                depth = 0,
                                onLinkOpen = onNoteOpen,
                            )
                            is Fb2Block.Poem -> ReaderPoem(
                                poem = block.poem,
                                language = book.language,
                                hyphenation = hyphenation,
                                highlightedRange = null,
                                depth = 0,
                                onLinkOpen = onNoteOpen,
                            )
                        }
                    }
                }
            }
        },
        confirmButton = {
            TextButton(onClick = onDismiss) {
                Text(strings.done)
            }
        },
    )
}

@Composable
private fun DetailsPane(
    book: Fb2Book,
    stats: BookStats,
    chapters: List<ChapterEntry>,
    selectedChapter: Int,
    onChapterSelected: (Int) -> Unit,
    modifier: Modifier = Modifier,
) {
    LazyColumn(
        modifier = modifier,
        verticalArrangement = Arrangement.spacedBy(14.dp),
    ) {
        item {
            Column(verticalArrangement = Arrangement.spacedBy(14.dp)) {
                CoverAndTitle(book)
                MetadataCard(book)
                StatsCard(stats)
            }
        }
        item {
            Text(
                strings.sections,
                style = MaterialTheme.typography.titleMedium,
                fontWeight = FontWeight.SemiBold,
                modifier = Modifier.padding(top = 6.dp, start = 2.dp),
            )
        }
        itemsIndexed(chapters) { index, chapter ->
            Button(
                onClick = { onChapterSelected(index) },
                shape = RoundedCornerShape(8.dp),
                modifier = Modifier.fillMaxWidth(),
            ) {
                Text(
                    chapter.title,
                    maxLines = 2,
                    textAlign = TextAlign.Start,
                    modifier = Modifier.fillMaxWidth().padding(start = (chapter.depth * 12).dp),
                    fontWeight = if (index == selectedChapter) FontWeight.Bold else FontWeight.Normal,
                )
            }
        }
    }
}

@Composable
internal fun CoverAndTitle(book: Fb2Book, onImageOpen: (ViewedBookImage) -> Unit = {}) {
    Row(
        horizontalArrangement = Arrangement.spacedBy(16.dp),
        verticalAlignment = Alignment.CenterVertically,
        modifier = Modifier.fillMaxWidth(),
    ) {
        BookImage(
            book = book,
            image = book.coverImages.firstOrNull() ?: book.bodyImages.firstOrNull(),
            modifier = Modifier.width(112.dp).aspectRatio(0.68f),
            contentScale = ContentScale.Crop,
            fitBackgroundToBitmapBounds = false,
            onOpen = onImageOpen,
        )
        Column(verticalArrangement = Arrangement.spacedBy(6.dp), modifier = Modifier.weight(1f)) {
            Text(book.title, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
            Text(
                book.authors.joinToString { it.displayName }.ifBlank { strings.unknownAuthor },
                style = MaterialTheme.typography.titleMedium,
                color = MaterialTheme.colorScheme.secondary,
            )
            Text(
                listOfNotNull(book.date, book.language?.uppercase()).joinToString("  |  "),
                style = MaterialTheme.typography.labelLarge,
                color = MaterialTheme.colorScheme.outline,
            )
        }
    }
}

@Composable
@OptIn(ExperimentalLayoutApi::class)
internal fun MetadataCard(book: Fb2Book) {
    Card(shape = RoundedCornerShape(8.dp), colors = quietCardColors(), modifier = Modifier.fillMaxWidth()) {
        Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
            DetailLine(strings.genres, book.genres.joinToString().ifBlank { strings.notSpecified })
            DetailLine(strings.translator, book.translators.joinToString { it.displayName }.ifBlank { strings.notSpecified })
            DetailLine(strings.source, book.sourceLanguage?.uppercase() ?: strings.notSpecified)
            if (book.annotation.isNullOrBlank().not()) {
                Text(book.annotation.orEmpty(), style = MaterialTheme.typography.bodyMedium, lineHeight = 20.sp)
            }
            if (book.sequences.isNotEmpty()) {
                FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
                    book.sequences.forEach { sequence ->
                        Chip("${sequence.name}${sequence.number?.let { " #$it" }.orEmpty()}")
                    }
                }
            }
        }
    }
}

@Composable
internal fun StatsCard(stats: BookStats) {
    Card(shape = RoundedCornerShape(8.dp), colors = quietCardColors(), modifier = Modifier.fillMaxWidth()) {
        Row(Modifier.fillMaxWidth().padding(16.dp), horizontalArrangement = Arrangement.SpaceBetween) {
            StatBlock(strings.words, stats.words.formatCompact())
            StatBlock(strings.sections, stats.sections.toString())
            StatBlock(strings.images, stats.images.toString())
        }
    }
}

@Composable
private fun ReaderPane(book: Fb2Book, section: Fb2Section?, modifier: Modifier = Modifier) {
    val hyphenation = remember { HyphenationRegistry() }
    val blocks = remember(section) { section?.readableBlocks().orEmpty() }

    LazyColumn(
        modifier = modifier
            .clip(RoundedCornerShape(8.dp))
            .background(MaterialTheme.colorScheme.surface)
            .padding(horizontal = 18.dp),
        verticalArrangement = Arrangement.spacedBy(10.dp),
    ) {
        item { Spacer(Modifier.height(14.dp)) }
        item {
            Text(
                section?.title ?: book.title,
                style = MaterialTheme.typography.headlineMedium,
                fontWeight = FontWeight.Bold,
                lineHeight = 36.sp,
                modifier = Modifier.padding(bottom = 8.dp),
            )
        }
        items(blocks) { block ->
            when (block) {
                Fb2Block.EmptyLine -> Spacer(Modifier.height(16.dp))
                is Fb2Block.Image -> BookImage(
                    book = book,
                    image = block.image,
                    modifier = Modifier.fillMaxWidth().padding(vertical = 10.dp),
                    contentScale = ContentScale.Fit,
                )
                is Fb2Block.Paragraph -> ReaderText(
                    text = block.content,
                    language = book.language,
                    hyphenation = hyphenation,
                    style = readerParagraphTextStyle(book.language),
                    textAlign = TextAlign.Unspecified,
                )
                is Fb2Block.Poem -> ReaderPoem(
                    poem = block.poem,
                    language = book.language,
                    hyphenation = hyphenation,
                    highlightedRange = null,
                    depth = 0,
                    onLinkOpen = {},
                )
                is Fb2Block.Subtitle -> ReaderText(
                    text = block.content,
                    language = book.language,
                    hyphenation = hyphenation,
                    style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold),
                    textAlign = TextAlign.Center,
                    modifier = Modifier.fillMaxWidth().padding(top = 18.dp, bottom = 8.dp),
                )
                is Fb2Block.Epigraph -> ReaderEpigraph(
                    epigraph = block.epigraph,
                    language = book.language,
                    hyphenation = hyphenation,
                    highlightedRange = null,
                    depth = 0,
                    onLinkOpen = {},
                )
                is Fb2Block.Cite -> ReaderCite(
                    cite = block.cite,
                    language = book.language,
                    hyphenation = hyphenation,
                    highlightedRange = null,
                    depth = 0,
                    onLinkOpen = {},
                )
            }
        }
        item { Spacer(Modifier.height(22.dp)) }
    }
}

@Composable
private fun ReaderText(
    text: Fb2Text,
    language: String?,
    hyphenation: HyphenationRegistry,
    style: TextStyle,
    textAlign: TextAlign,
    modifier: Modifier = Modifier,
    highlightedRange: ReaderSentenceRange? = null,
    onTextLayout: (TextLayoutResult) -> Unit = {},
    onLinkOpen: (String) -> Unit = {},
) {
    val highlightColor = MaterialTheme.colorScheme.secondaryContainer
    val annotatedText = text.toAnnotatedString(
        language = language,
        hyphenation = hyphenation,
        highlightedRange = highlightedRange,
        highlightColor = highlightColor,
    )
    val hasLinks = annotatedText.hasReaderLinks()
    val needsSoftHyphenPaintWorkaround = isDesktopPlatform
    var textLayout by remember(annotatedText) { mutableStateOf<TextLayoutResult?>(null) }
    val desktopHyphenColor = if (style.color == Color.Unspecified) MaterialTheme.colorScheme.onSurface else style.color
    val desktopHyphenGutter = 8.dp
    val textModifier = modifier
        .fillMaxWidth()
        .then(
            if (needsSoftHyphenPaintWorkaround) {
                Modifier.drawWithContent {
                    drawContent()

                    val layout = textLayout ?: return@drawWithContent
                    val layoutText = layout.layoutInput.text.text
                    val fontSizePx = if (style.fontSize.isSpecified) style.fontSize.toPx() else 18.sp.toPx()
                    val hyphenLength = fontSizePx * 0.36f
                    val strokeWidth = max(1f, fontSizePx * 0.055f)

                    for (line in 0 until layout.lineCount) {
                        if (!layout.endsAtSoftHyphen(layoutText, line)) continue

                        val lineRight = layout.getLineRight(line)
                        val x = min(lineRight + hyphenLength * 0.12f, size.width - hyphenLength)
                        val y = layout.getLineBaseline(line) - fontSizePx * 0.32f
                        drawLine(
                            color = desktopHyphenColor,
                            start = Offset(x, y),
                            end = Offset(x + hyphenLength, y),
                            strokeWidth = strokeWidth,
                            cap = StrokeCap.Square,
                        )
                    }
                }.padding(end = desktopHyphenGutter)
            } else {
                Modifier
            },
        )

    Box(modifier = textModifier) {
        Text(
            text = annotatedText,
            style = style,
            textAlign = textAlign,
            modifier = Modifier.fillMaxWidth(),
            onTextLayout = {
                textLayout = it
                onTextLayout(it)
            },
        )
        val layout = textLayout
        if (hasLinks && layout != null) {
            ReaderLinkTouchOverlays(
                annotatedText = annotatedText,
                textLayout = layout,
                onLinkOpen = onLinkOpen,
            )
        }
    }
}

@Composable
private fun ReaderPoem(
    poem: Fb2Poem,
    language: String?,
    hyphenation: HyphenationRegistry,
    highlightedRange: ReaderSentenceRange?,
    depth: Int,
    textColor: Color = MaterialTheme.colorScheme.onSurface,
    modifier: Modifier = Modifier,
    onLinkOpen: (String) -> Unit = {},
) {
    val segments = remember(poem) { poem.readerSegments() }
    Column(
        modifier = modifier
            .fillMaxWidth()
            .padding(start = (depth * 8).dp, top = 8.dp, bottom = 8.dp),
    ) {
        poem.epigraphs.forEachIndexed { index, epigraph ->
            ReaderEpigraph(
                epigraph = epigraph,
                language = language,
                hyphenation = hyphenation,
                highlightedRange = highlightedRange,
                depth = 0,
                textColor = textColor,
                modifier = Modifier.padding(top = if (index == 0) 0.dp else 8.dp, bottom = 8.dp),
                onLinkOpen = onLinkOpen,
            )
        }
        segments.forEach { segment ->
            if (segment.gapBeforeDp > 0) {
                Spacer(Modifier.height(segment.gapBeforeDp.dp))
            }
            ReaderText(
                text = segment.text,
                language = language,
                hyphenation = hyphenation,
                style = poemTextStyle(segment.kind, language).copy(color = textColor),
                highlightedRange = highlightedRange?.forSegment(segment),
                textAlign = when (segment.kind) {
                    ReaderPoemSegmentKind.Title,
                    ReaderPoemSegmentKind.Subtitle -> TextAlign.Center
                    ReaderPoemSegmentKind.TextAuthor,
                    ReaderPoemSegmentKind.Date -> TextAlign.End
                    ReaderPoemSegmentKind.Verse -> TextAlign.Start
                },
                modifier = when (segment.kind) {
                    ReaderPoemSegmentKind.Verse -> Modifier.padding(start = 22.dp)
                    else -> Modifier
                },
                onLinkOpen = onLinkOpen,
            )
        }
    }
}

@Composable
private fun ReaderEpigraph(
    epigraph: Fb2Epigraph,
    language: String?,
    hyphenation: HyphenationRegistry,
    highlightedRange: ReaderSentenceRange?,
    depth: Int,
    textColor: Color = MaterialTheme.colorScheme.onSurface,
    modifier: Modifier = Modifier,
    onLinkOpen: (String) -> Unit = {},
) {
    Column(
        modifier = modifier
            .fillMaxWidth()
            .padding(start = (depth * 8 + 26).dp, end = 18.dp, top = 8.dp, bottom = 8.dp),
        verticalArrangement = Arrangement.spacedBy(6.dp),
    ) {
        epigraph.blocks.forEach { block ->
            ReaderEpigraphBlock(
                block = block,
                language = language,
                hyphenation = hyphenation,
                highlightedRange = null,
                depth = 0,
                textColor = textColor,
                onLinkOpen = onLinkOpen,
            )
        }
        epigraph.textAuthors.forEach { author ->
            ReaderText(
                text = author,
                language = language,
                hyphenation = hyphenation,
                style = epigraphAuthorTextStyle(language).copy(color = textColor),
                highlightedRange = null,
                textAlign = TextAlign.End,
                modifier = Modifier.fillMaxWidth().padding(top = 2.dp),
                onLinkOpen = onLinkOpen,
            )
        }
    }
}

@Composable
private fun ReaderCite(
    cite: Fb2Cite,
    language: String?,
    hyphenation: HyphenationRegistry,
    highlightedRange: ReaderSentenceRange?,
    depth: Int,
    textColor: Color = MaterialTheme.colorScheme.onSurface,
    modifier: Modifier = Modifier,
    onLinkOpen: (String) -> Unit = {},
) {
    Column(
        modifier = modifier
            .fillMaxWidth()
            .padding(start = (depth * 8 + 22).dp, end = 14.dp, top = 8.dp, bottom = 8.dp),
        verticalArrangement = Arrangement.spacedBy(6.dp),
    ) {
        cite.blocks.forEach { block ->
            ReaderEpigraphBlock(
                block = block,
                language = language,
                hyphenation = hyphenation,
                highlightedRange = null,
                depth = 0,
                textColor = textColor,
                onLinkOpen = onLinkOpen,
            )
        }
        cite.textAuthors.forEach { author ->
            ReaderText(
                text = author,
                language = language,
                hyphenation = hyphenation,
                style = epigraphAuthorTextStyle(language).copy(color = textColor),
                highlightedRange = null,
                textAlign = TextAlign.End,
                modifier = Modifier.fillMaxWidth().padding(top = 2.dp),
                onLinkOpen = onLinkOpen,
            )
        }
    }
}

@Composable
private fun ReaderEpigraphBlock(
    block: Fb2EpigraphBlock,
    language: String?,
    hyphenation: HyphenationRegistry,
    highlightedRange: ReaderSentenceRange?,
    depth: Int,
    textColor: Color = MaterialTheme.colorScheme.onSurface,
    onLinkOpen: (String) -> Unit,
) {
    when (block) {
        Fb2EpigraphBlock.EmptyLine -> Spacer(Modifier.height(12.dp))
        is Fb2EpigraphBlock.Paragraph -> ReaderText(
            text = block.content,
            language = language,
            hyphenation = hyphenation,
            style = epigraphTextStyle(language).copy(color = textColor),
            highlightedRange = highlightedRange,
            textAlign = TextAlign.Start,
            onLinkOpen = onLinkOpen,
        )
        is Fb2EpigraphBlock.Subtitle -> ReaderText(
            text = block.content,
            language = language,
            hyphenation = hyphenation,
            style = epigraphTextStyle(language).copy(fontWeight = FontWeight.SemiBold, color = textColor),
            highlightedRange = highlightedRange,
            textAlign = TextAlign.Center,
            onLinkOpen = onLinkOpen,
        )
        is Fb2EpigraphBlock.Poem -> ReaderPoem(
            poem = block.poem,
            language = language,
            hyphenation = hyphenation,
            highlightedRange = highlightedRange,
            depth = depth,
            textColor = textColor,
            onLinkOpen = onLinkOpen,
        )
        is Fb2EpigraphBlock.Cite -> ReaderCite(
            cite = block.cite,
            language = language,
            hyphenation = hyphenation,
            highlightedRange = highlightedRange,
            depth = depth,
            textColor = textColor,
            onLinkOpen = onLinkOpen,
        )
    }
}

@Composable
private fun poemTextStyle(kind: ReaderPoemSegmentKind, language: String?): TextStyle =
    when (kind) {
        ReaderPoemSegmentKind.Title -> MaterialTheme.typography.titleMedium.copy(
            fontWeight = FontWeight.SemiBold,
            lineHeight = 26.sp,
            localeList = language?.takeIf(String::isNotBlank)?.let { LocaleList(Locale(it)) },
        )
        ReaderPoemSegmentKind.Subtitle -> MaterialTheme.typography.bodyLarge.copy(
            fontStyle = FontStyle.Italic,
            lineHeight = 24.sp,
            localeList = language?.takeIf(String::isNotBlank)?.let { LocaleList(Locale(it)) },
        )
        ReaderPoemSegmentKind.Verse -> readerParagraphTextStyle(language).copy(
            lineHeight = 24.sp,
        )
        ReaderPoemSegmentKind.TextAuthor,
        ReaderPoemSegmentKind.Date -> MaterialTheme.typography.bodyMedium.copy(
            fontStyle = FontStyle.Italic,
            lineHeight = 22.sp,
            localeList = language?.takeIf(String::isNotBlank)?.let { LocaleList(Locale(it)) },
        )
    }

@Composable
private fun epigraphTextStyle(language: String?): TextStyle =
    MaterialTheme.typography.bodyMedium.copy(
        fontStyle = FontStyle.Italic,
        lineHeight = 22.sp,
        localeList = language?.takeIf(String::isNotBlank)?.let { LocaleList(Locale(it)) },
    )

@Composable
private fun epigraphAuthorTextStyle(language: String?): TextStyle =
    epigraphTextStyle(language).copy(fontWeight = FontWeight.SemiBold)

@Composable
private fun readerParagraphTextStyle(
    language: String?,
    settings: ReaderFontSettings = defaultReaderFontSettings(),
): TextStyle {
    val coerced = settings.coerced()
    return MaterialTheme.typography.bodyLarge.copy(
        fontWeight = FontWeight(coerced.fontWeight),
        fontSize = coerced.fontSizeSp.sp,
        lineHeight = coerced.lineHeightSp.sp,
        fontFamily = coerced.readerFontFamily(),
        letterSpacing = if (isAndroidPlatform) 0.sp else MaterialTheme.typography.bodyLarge.letterSpacing,
        hyphens = if (isAndroidPlatform) Hyphens.Auto else Hyphens.Unspecified,
        lineBreak = if (isAndroidPlatform) LineBreak.Paragraph else LineBreak.Unspecified,
        localeList = language?.takeIf(String::isNotBlank)?.let { LocaleList(Locale(it)) },
    )
}

private fun ReaderFontSettings.readerFontFamily(): FontFamily? =
    when (fontName?.lowercase()) {
        "serif" -> FontFamily.Serif
        "sans", "sansserif", "sans-serif" -> FontFamily.SansSerif
        "monospace", "mono" -> FontFamily.Monospace
        "cursive" -> FontFamily.Cursive
        else -> null
    }

private val isAndroidPlatform: Boolean by lazy {
    getPlatform().name.startsWith("Android")
}

private val isDesktopPlatform: Boolean by lazy {
    getPlatform().name.startsWith("Java")
}

private const val ReaderZoomGestureStep = 1.12f
private const val ReaderSwipeTouchSlopMultiplier = 1.5f
private const val ReaderHorizontalSwipeDominance = 2f

private val MinimumBookImageMaxDimension = 800.dp

private fun minimumBookImageMaxDimension(availableWidth: Dp): Dp {
    val relativeMinimum = availableWidth * 0.55f
    return if (MinimumBookImageMaxDimension < relativeMinimum) MinimumBookImageMaxDimension else relativeMinimum
}

private fun TextLayoutResult.endsAtSoftHyphen(text: String, line: Int): Boolean {
    val end = getLineEnd(line, visibleEnd = false)
    return text.getOrNull(end - 1) == SoftHyphen || text.getOrNull(end) == SoftHyphen
}

@Composable
private fun BookImage(
    book: Fb2Book,
    image: Fb2ImageRef?,
    modifier: Modifier = Modifier,
    contentScale: ContentScale = ContentScale.Fit,
    fitBackgroundToBitmapBounds: Boolean = true,
    onOpen: (ViewedBookImage) -> Unit = {},
) {
    val binary = remember(book, image) {
        image?.let(book::binaryFor)
    }
    var decodedImage by remember(binary) { mutableStateOf<DecodedBookImage?>(null) }
    LaunchedEffect(binary) {
        decodedImage = withContext(Dispatchers.Default) {
            binary?.let {
                val bytes = it.imageBytes()
                val bitmap = decodeImageBytes(bytes) ?: return@let null
                DecodedBookImage(bitmap, bytes)
            }
        }
    }
    val currentDecodedImage = decodedImage
    val bitmap = currentDecodedImage?.bitmap
    val imageTitle = image?.alt?.ifBlank { null } ?: book.title
    val imageBackgroundColor = readerImageBackgroundColor()
    val imageShape = RoundedCornerShape(8.dp)
    val smoothImageEdges = imageBackgroundColor != MaterialTheme.colorScheme.surface
    val density = LocalDensity.current
    BoxWithConstraints(
        modifier = modifier
            .clip(RoundedCornerShape(8.dp))
            .background(MaterialTheme.colorScheme.surface)
            .then(
                if (currentDecodedImage != null && binary != null) {
                    Modifier.clickable {
                        onOpen(
                            ViewedBookImage(
                                bitmap = currentDecodedImage.bitmap,
                                bytes = currentDecodedImage.bytes,
                                mimeType = binary.contentType,
                                title = imageTitle,
                            ),
                        )
                    }
                } else {
                    Modifier
                },
            ),
        contentAlignment = Alignment.Center,
    ) {
        if (bitmap != null) {
            val imageAspectRatio = bitmap.width.toFloat() / bitmap.height.coerceAtLeast(1).toFloat()
            val imageModifier = if (fitBackgroundToBitmapBounds) {
                val bitmapWidth = with(density) { bitmap.width.toDp() }
                val bitmapHeight = with(density) { bitmap.height.toDp() }
                val bitmapMaxDimension = if (bitmapWidth > bitmapHeight) bitmapWidth else bitmapHeight
                val minimumMaxDimension = minimumBookImageMaxDimension(maxWidth)
                val displayWidth = when {
                    bitmapMaxDimension < minimumMaxDimension ->
                        bitmapWidth * (minimumMaxDimension.value / bitmapMaxDimension.value)
                    bitmapWidth < maxWidth -> bitmapWidth
                    else -> maxWidth
                }
                Modifier
                    .width(displayWidth)
                    .aspectRatio(imageAspectRatio)
            } else {
                Modifier.fillMaxSize()
            }
            Box(
                imageModifier
                    .then(if (smoothImageEdges) Modifier.softImageEdgeFade() else Modifier)
                    .clip(imageShape)
                    .background(imageBackgroundColor),
            ) {
                Image(
                    bitmap = bitmap,
                    contentDescription = imageTitle,
                    modifier = Modifier.fillMaxSize(),
                    contentScale = contentScale,
                )
            }
        } else {
            Text(strings.noImage, style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.outline)
        }
    }
}

private fun Modifier.softImageEdgeFade(): Modifier =
    graphicsLayer {
        compositingStrategy = CompositingStrategy.Offscreen
    }.drawWithContent {
        drawContent()

        val fade = min(8.dp.toPx(), min(size.width, size.height) / 4f)
        if (fade <= 0f) return@drawWithContent

        drawRect(
            brush = Brush.verticalGradient(
                0f to Color.Transparent,
                1f to Color.Black,
                startY = 0f,
                endY = fade,
            ),
            size = Size(size.width, fade),
            blendMode = BlendMode.DstIn,
        )
        drawRect(
            brush = Brush.verticalGradient(
                0f to Color.Black,
                1f to Color.Transparent,
                startY = size.height - fade,
                endY = size.height,
            ),
            topLeft = Offset(0f, size.height - fade),
            size = Size(size.width, fade),
            blendMode = BlendMode.DstIn,
        )
        drawRect(
            brush = Brush.horizontalGradient(
                0f to Color.Transparent,
                1f to Color.Black,
                startX = 0f,
                endX = fade,
            ),
            size = Size(fade, size.height),
            blendMode = BlendMode.DstIn,
        )
        drawRect(
            brush = Brush.horizontalGradient(
                0f to Color.Black,
                1f to Color.Transparent,
                startX = size.width - fade,
                endX = size.width,
            ),
            topLeft = Offset(size.width - fade, 0f),
            size = Size(fade, size.height),
            blendMode = BlendMode.DstIn,
        )
    }

@Composable
private fun BoxScope.ReaderLinkTouchOverlays(
    annotatedText: AnnotatedString,
    textLayout: TextLayoutResult,
    onLinkOpen: (String) -> Unit,
) {
    val density = LocalDensity.current
    val touchAreas = remember(annotatedText, textLayout, density) {
        with(density) {
            textLayout.readerLinkTouchAreas(
                text = annotatedText,
                trimLeftPx = ReaderLinkTouchTrimLeft.toPx(),
                extendRightPx = ReaderLinkTouchExtendRight.toPx(),
                minimumHeightPx = ReaderLinkTouchMinimumHeight.toPx(),
                verticalPaddingPx = ReaderLinkTouchVerticalPadding.toPx(),
            )
        }
    }

    Box(Modifier.matchParentSize()) {
        touchAreas.forEach { area ->
            Box(
                Modifier
                    .offset {
                        IntOffset(
                            x = area.rect.left.roundToInt(),
                            y = area.rect.top.roundToInt(),
                        )
                    }
                    .width(with(density) { area.rect.width.toDp() })
                    .height(with(density) { area.rect.height.toDp() })
                    // Link hit-area debugging: uncomment this background to visualize the
                    // actual touch overlay, including the right-side extension.
                    // .background(ReaderLinkTouchDebugBackground)
                    .clickable { onLinkOpen(area.target) },
            )
        }
    }
}

private data class ReaderLinkTouchArea(
    val target: String,
    val rect: Rect,
)

private data class ReaderLinkLineBounds(
    val annotationIndex: Int,
    val target: String,
    val line: Int,
    val rect: Rect,
)

private fun TextLayoutResult.readerLinkTouchAreas(
    text: AnnotatedString,
    trimLeftPx: Float,
    extendRightPx: Float,
    minimumHeightPx: Float,
    verticalPaddingPx: Float,
): List<ReaderLinkTouchArea> {
    val linkBounds = text.getStringAnnotations(ReaderLinkAnnotationTag, 0, text.length)
        .sortedBy { it.start }
        .flatMapIndexed { annotationIndex, annotation ->
            val safeStart = min(annotation.start, layoutInput.text.length)
            val safeEnd = min(annotation.end, layoutInput.text.length)
            if (safeStart >= safeEnd) return@flatMapIndexed emptyList()

            val startLine = getLineForOffset(safeStart)
            val endLine = getLineForOffset(safeEnd - 1)

            (startLine..endLine).mapNotNull { line ->
                val lineStart = max(safeStart, getLineStart(line))
                val lineEnd = min(safeEnd, getLineEnd(line))
                if (lineStart >= lineEnd) return@mapNotNull null

                val rangeBounds = getPathForRange(lineStart, lineEnd).getBounds()
                val horizontalBounds = Rect(
                    left = rangeBounds.left,
                    top = rangeBounds.top,
                    right = rangeBounds.right,
                    bottom = rangeBounds.bottom,
                )
                ReaderLinkLineBounds(
                    annotationIndex = annotationIndex,
                    target = annotation.item,
                    line = line,
                    rect = horizontalBounds,
                )
            }
        }

    return linkBounds.map { bounds ->
        val nextLinkLeft = linkBounds
            .asSequence()
            .filter {
                it.line == bounds.line &&
                    it.annotationIndex != bounds.annotationIndex &&
                    it.rect.left >= bounds.rect.right
            }
            .map { it.rect.left }
            .minOrNull()
        val rightLimit = nextLinkLeft ?: size.width.toFloat()
        val right = minOf(bounds.rect.right + extendRightPx, rightLimit, size.width.toFloat())
            .coerceAtLeast(bounds.rect.right)
        val touchHeight = max(bounds.rect.height + verticalPaddingPx * 2f, minimumHeightPx)
        val centerY = bounds.rect.center.y

        ReaderLinkTouchArea(
            target = bounds.target,
            rect = Rect(
                left = min(bounds.rect.left + trimLeftPx, bounds.rect.right),
                top = max(0f, centerY - touchHeight / 2f),
                right = right,
                bottom = centerY + touchHeight / 2f,
            ),
        )
    }
}

private fun Rect.union(other: Rect): Rect =
    Rect(
        left = min(left, other.left),
        top = min(top, other.top),
        right = max(right, other.right),
        bottom = max(bottom, other.bottom),
    )

private fun AnnotatedString.hasReaderLinks(): Boolean =
    getStringAnnotations(ReaderLinkAnnotationTag, 0, length).isNotEmpty()

private fun Fb2Text.toAnnotatedString(
    language: String?,
    hyphenation: HyphenationRegistry,
    highlightedRange: ReaderSentenceRange?,
    highlightColor: Color,
): AnnotatedString =
    buildAnnotatedString {
        var plainOffset = 0
        spans.forEach { span ->
            val isLink = span.href.isNullOrBlank().not()
            val spanStyle = SpanStyle(
                fontStyle = if (Fb2TextStyle.Emphasis in span.styles) FontStyle.Italic else null,
                fontWeight = if (Fb2TextStyle.Strong in span.styles) FontWeight.Bold else null,
                fontFamily = if (Fb2TextStyle.Code in span.styles) FontFamily.Monospace else null,
                color = if (isLink) LinkTextColor else Color.Unspecified,
                // Link hit-area debugging: set this to ReaderLinkDebugBackground for links
                // to compare the annotated text range with the touch overlay.
                background = Color.Unspecified,
                textDecoration = when {
                    isLink && Fb2TextStyle.Strikethrough in span.styles ->
                        TextDecoration.combine(listOf(TextDecoration.Underline, TextDecoration.LineThrough))
                    isLink -> TextDecoration.Underline
                    Fb2TextStyle.Strikethrough in span.styles -> TextDecoration.LineThrough
                    else -> null
                },
                baselineShift = when {
                    Fb2TextStyle.Superscript in span.styles -> BaselineShift.Superscript
                    Fb2TextStyle.Subscript in span.styles -> BaselineShift.Subscript
                    else -> null
                },
            )
            if (isLink) {
                withAnnotation(ReaderLinkAnnotationTag, span.href!!) {
                    withStyle(spanStyle) {
                        appendWithHighlight(span.text, plainOffset, highlightedRange, highlightColor, language, hyphenation)
                    }
                }
            } else {
                appendWithDetectedWebLinks(
                    text = span.text,
                    spanStyle = spanStyle,
                    plainOffset = plainOffset,
                    highlightedRange = highlightedRange,
                    highlightColor = highlightColor,
                    language = language,
                    hyphenation = hyphenation,
                )
            }
            plainOffset += span.text.length
        }
    }

private fun AnnotatedString.Builder.appendWithDetectedWebLinks(
    text: String,
    spanStyle: SpanStyle,
    plainOffset: Int,
    highlightedRange: ReaderSentenceRange?,
    highlightColor: Color,
    language: String?,
    hyphenation: HyphenationRegistry,
) {
    var cursor = 0
    text.webUrlRanges().forEach { range ->
        if (cursor < range.start) {
            withStyle(spanStyle) {
                appendWithHighlight(
                    text = text.substring(cursor, range.start),
                    plainOffset = plainOffset + cursor,
                    highlightedRange = highlightedRange,
                    highlightColor = highlightColor,
                    language = language,
                    hyphenation = hyphenation,
                )
            }
        }

        val url = text.substring(range)
        val linkStyle = spanStyle.readerLinkStyle()
        withAnnotation(ReaderLinkAnnotationTag, url) {
            withStyle(linkStyle) {
                appendWithHighlight(
                    text = url,
                    plainOffset = plainOffset + range.start,
                    highlightedRange = highlightedRange,
                    highlightColor = highlightColor,
                    language = null,
                    hyphenation = hyphenation,
                )
            }
        }
        cursor = range.endInclusive + 1
    }

    if (cursor < text.length) {
        withStyle(spanStyle) {
            appendWithHighlight(
                text = text.substring(cursor),
                plainOffset = plainOffset + cursor,
                highlightedRange = highlightedRange,
                highlightColor = highlightColor,
                language = language,
                hyphenation = hyphenation,
            )
        }
    }
}

private fun SpanStyle.readerLinkStyle(): SpanStyle =
    copy(
        color = LinkTextColor,
        // Link hit-area debugging: set this to ReaderLinkDebugBackground to compare
        // detected URL text ranges with the touch overlay.
        background = Color.Unspecified,
        textDecoration = when (textDecoration) {
            TextDecoration.LineThrough ->
                TextDecoration.combine(listOf(TextDecoration.Underline, TextDecoration.LineThrough))
            else -> TextDecoration.Underline
        },
    )

internal fun String.webUrlRanges(): List<IntRange> =
    WebUrlRegex.findAll(this).mapNotNull { match ->
        val trimmed = match.value.trimEnd(*WebUrlTrailingPunctuation)
        if (trimmed.substringAfter("://").isEmpty()) return@mapNotNull null
        match.range.first..<(match.range.first + trimmed.length)
    }.toList()

internal fun String.isWebUrl(): Boolean =
    startsWith("https://", ignoreCase = true) || startsWith("http://", ignoreCase = true)

private fun AnnotatedString.Builder.appendWithHighlight(
    text: String,
    plainOffset: Int,
    highlightedRange: ReaderSentenceRange?,
    highlightColor: Color,
    language: String?,
    hyphenation: HyphenationRegistry,
) {
    if (highlightedRange == null) {
        append(hyphenation.hyphenate(text, language))
        return
    }

    var cursor = 0
    while (cursor < text.length) {
        val absolute = plainOffset + cursor
        val inHighlight = absolute >= highlightedRange.start && absolute < highlightedRange.endExclusive
        val nextBoundary = if (inHighlight) {
            min(text.length, highlightedRange.endExclusive - plainOffset)
        } else {
            min(text.length, max(0, highlightedRange.start - plainOffset))
                .takeIf { it > cursor } ?: text.length
        }
        val part = text.substring(cursor, nextBoundary)
        if (inHighlight) {
            withStyle(SpanStyle(background = highlightColor)) {
                append(hyphenation.hyphenate(part, language))
            }
        } else {
            append(hyphenation.hyphenate(part, language))
        }
        cursor = nextBoundary
    }
}

internal fun buildReaderContentPlan(book: Fb2Book): ReaderContentPlan {
    val elements = mutableListOf<ReaderElement>()
    val sentences = mutableListOf<ReadAloudSentence>()
    var pendingPauseBeforeMillis = 0L

    fun addTextSentences(
        itemIndex: Int,
        text: Fb2Text,
        offset: Int = 0,
        pauseBeforeMillis: Long = 0,
        pauseAfterMillis: Long = 0,
    ) {
        val plainText = text.plainText()
        if (plainText.isReadAloudPauseBreak()) {
            pendingPauseBeforeMillis = max(pendingPauseBeforeMillis, StarBreakPauseMillis)
            return
        }

        plainText.sentenceRanges().forEach { range ->
            val sentenceText = plainText.substring(range.start, range.endExclusive).trim()
            val spokenText = sentenceText.toReadAloudSpokenText()
            if (spokenText.isNotEmpty()) {
                val effectivePauseBefore = pauseBeforeMillis + pendingPauseBeforeMillis
                pendingPauseBeforeMillis = 0L
                sentences += ReadAloudSentence(
                    index = sentences.size,
                    itemIndex = itemIndex,
                    start = offset + range.start,
                    endExclusive = offset + range.endExclusive,
                    text = sentenceText,
                    spokenText = spokenText,
                    pauseBeforeMillis = effectivePauseBefore,
                    pauseAfterMillis = pauseAfterMillis + range.pauseAfterMillis,
                )
            }
        }
    }

    lateinit var addPoemSentences: (Int, Fb2Poem) -> Unit
    lateinit var addCiteSentences: (Int, Fb2Cite) -> Unit
    lateinit var addEpigraphSentences: (Int, Fb2Epigraph) -> Unit
    lateinit var addSections: (List<Fb2Section>, Int) -> Unit
    val tocEntries = mutableListOf<ReaderTocEntry>()

    addPoemSentences = { itemIndex, poem ->
        poem.epigraphs.forEach { epigraph -> addEpigraphSentences(itemIndex, epigraph) }
        poem.readerSegments().forEach { segment ->
            addTextSentences(itemIndex, segment.text, offset = segment.startOffset)
        }
    }

    addCiteSentences = { itemIndex, cite ->
        cite.blocks.forEach { block ->
            when (block) {
                Fb2EpigraphBlock.EmptyLine -> Unit
                is Fb2EpigraphBlock.Paragraph -> addTextSentences(itemIndex, block.content)
                is Fb2EpigraphBlock.Subtitle -> addTextSentences(itemIndex, block.content)
                is Fb2EpigraphBlock.Poem -> addPoemSentences(itemIndex, block.poem)
                is Fb2EpigraphBlock.Cite -> addCiteSentences(itemIndex, block.cite)
            }
        }
        cite.textAuthors.forEach { addTextSentences(itemIndex, it) }
    }

    addEpigraphSentences = { itemIndex, epigraph ->
        epigraph.blocks.forEach { block ->
            when (block) {
                Fb2EpigraphBlock.EmptyLine -> Unit
                is Fb2EpigraphBlock.Paragraph -> addTextSentences(itemIndex, block.content)
                is Fb2EpigraphBlock.Subtitle -> addTextSentences(itemIndex, block.content)
                is Fb2EpigraphBlock.Poem -> addPoemSentences(itemIndex, block.poem)
                is Fb2EpigraphBlock.Cite -> addCiteSentences(itemIndex, block.cite)
            }
        }
        epigraph.textAuthors.forEach { addTextSentences(itemIndex, it) }
    }

    fun addSection(section: Fb2Section, readerDepth: Int, fallbackTitle: String) {
        val title = section.title?.ifBlank { null } ?: fallbackTitle
        val itemIndex = elements.size
        tocEntries += ReaderTocEntry(title, itemIndex, readerDepth)
        if (section.title.isNullOrBlank()) {
            elements += ReaderElement.SectionSeparator
        } else {
            elements += ReaderElement.SectionTitle(title, readerDepth)
            sentences += ReadAloudSentence(
                index = sentences.size,
                itemIndex = itemIndex,
                start = 0,
                endExclusive = title.length,
                text = title,
                pauseBeforeMillis = HeadingPauseBeforeMillis + pendingPauseBeforeMillis,
                pauseAfterMillis = HeadingPauseAfterMillis,
            )
            pendingPauseBeforeMillis = 0L
        }
        val blocks = section.readableBlocks()
        blocks.forEach { block ->
            val itemIndex = elements.size
            when (block) {
                Fb2Block.EmptyLine -> elements += ReaderElement.FixedSpacer(16)
                is Fb2Block.Image -> elements += ReaderElement.BookImage(block.image)
                is Fb2Block.Paragraph -> {
                    addTextSentences(itemIndex, block.content)
                    elements += ReaderElement.Paragraph(block.content, readerDepth)
                }
                is Fb2Block.Poem -> {
                    addPoemSentences(itemIndex, block.poem)
                    elements += ReaderElement.Poem(block.poem, readerDepth)
                }
                is Fb2Block.Subtitle -> {
                    addTextSentences(
                        itemIndex = itemIndex,
                        text = block.content,
                        pauseBeforeMillis = HeadingPauseBeforeMillis,
                        pauseAfterMillis = HeadingPauseAfterMillis,
                    )
                    elements += ReaderElement.Subtitle(block.content)
                }
                is Fb2Block.Epigraph -> {
                    addEpigraphSentences(itemIndex, block.epigraph)
                    elements += ReaderElement.Epigraph(block.epigraph, readerDepth)
                }
                is Fb2Block.Cite -> {
                    addCiteSentences(itemIndex, block.cite)
                    elements += ReaderElement.Cite(block.cite, readerDepth)
                }
            }
        }
        val childDepth = if (blocks.hasIndentBearingContent()) readerDepth + 1 else readerDepth
        addSections(section.sections, childDepth)
    }

    addSections = { sections, readerDepth ->
        sections.forEachIndexed { index, section ->
            addSection(section, readerDepth, strings.sectionFallback(index))
        }
    }

    elements += ReaderElement.Cover
    elements += ReaderElement.FixedSpacer(6)
    book.bodyEpigraphs.forEach { epigraph ->
        val itemIndex = elements.size
        addEpigraphSentences(itemIndex, epigraph)
        elements += ReaderElement.Epigraph(epigraph, 0)
    }
    addSections(book.sections, 0)
    elements += ReaderElement.FixedSpacer(22)

    return ReaderContentPlan(elements, sentences, tocEntries)
}

internal data class ReaderContentPlan(
    val elements: List<ReaderElement>,
    val sentences: List<ReadAloudSentence>,
    val tocEntries: List<ReaderTocEntry>,
) {
    fun sentenceIndexAtOrAfterItem(itemIndex: Int): Int =
        sentences.firstOrNull { it.itemIndex >= itemIndex }?.index
            ?: sentences.lastOrNull()?.index
            ?: 0

    fun resumeSentenceIndex(position: ReadingPosition): Int =
        position.readAloudSentenceIndex
            ?.takeIf { index -> sentences.getOrNull(index)?.itemIndex == position.itemIndex }
            ?: sentenceIndexAtOrAfterItem(position.itemIndex)
}

internal data class ReaderTocEntry(
    val title: String,
    val itemIndex: Int,
    val depth: Int,
)

internal sealed interface ReaderElement {
    data object Cover : ReaderElement
    data class FixedSpacer(val heightDp: Int) : ReaderElement
    data object SectionSeparator : ReaderElement
    data class SectionTitle(val title: String, val depth: Int) : ReaderElement
    data class BookImage(val image: Fb2ImageRef) : ReaderElement
    data class Paragraph(val text: Fb2Text, val depth: Int) : ReaderElement
    data class Subtitle(val text: Fb2Text) : ReaderElement
    data class Epigraph(val epigraph: Fb2Epigraph, val depth: Int) : ReaderElement
    data class Cite(val cite: Fb2Cite, val depth: Int) : ReaderElement
    data class Poem(val poem: Fb2Poem, val depth: Int) : ReaderElement
}

internal data class ReaderPoemSegment(
    val text: Fb2Text,
    val kind: ReaderPoemSegmentKind,
    val startOffset: Int,
    val gapBeforeDp: Int,
)

internal enum class ReaderPoemSegmentKind {
    Title,
    Subtitle,
    Verse,
    TextAuthor,
    Date,
}

private fun Fb2Poem.readerSegments(): List<ReaderPoemSegment> {
    val segments = mutableListOf<ReaderPoemSegment>()
    var offset = 0

    fun add(text: Fb2Text, kind: ReaderPoemSegmentKind, gapBeforeDp: Int) {
        segments += ReaderPoemSegment(text, kind, offset, gapBeforeDp)
        offset += text.plainText().length + 1
    }

    title.forEachIndexed { index, text ->
        add(text, ReaderPoemSegmentKind.Title, if (index == 0) 0 else 2)
    }
    blocks.forEach { block ->
        when (block) {
            is Fb2PoemBlock.Subtitle -> add(block.content, ReaderPoemSegmentKind.Subtitle, 8)
            is Fb2PoemBlock.Stanza -> {
                val stanza = block.stanza
                val startsNewStanza = segments.isNotEmpty()
                stanza.title.forEachIndexed { index, text ->
                    add(text, ReaderPoemSegmentKind.Subtitle, if (startsNewStanza && index == 0) 12 else 4)
                }
                stanza.subtitle?.let { add(it, ReaderPoemSegmentKind.Subtitle, 4) }
                stanza.verses.forEachIndexed { index, verse ->
                    val gap = when {
                        index == 0 && startsNewStanza && stanza.title.isEmpty() && stanza.subtitle == null -> 12
                        index == 0 -> 4
                        else -> 0
                    }
                    add(verse, ReaderPoemSegmentKind.Verse, gap)
                }
            }
        }
    }
    textAuthors.forEachIndexed { index, text ->
        add(text, ReaderPoemSegmentKind.TextAuthor, if (index == 0) 8 else 2)
    }
    date?.takeIf { it.isNotBlank() }?.let { date ->
        add(Fb2Text(listOf(Fb2TextSpan(date))), ReaderPoemSegmentKind.Date, 2)
    }

    return segments
}

private fun ReaderSentenceRange.forSegment(segment: ReaderPoemSegment): ReaderSentenceRange? {
    val segmentStart = segment.startOffset
    val segmentEnd = segmentStart + segment.text.plainText().length
    val overlapStart = max(start, segmentStart)
    val overlapEnd = min(endExclusive, segmentEnd)
    if (overlapStart >= overlapEnd) return null
    return ReaderSentenceRange(
        start = overlapStart - segmentStart,
        endExclusive = overlapEnd - segmentStart,
        pauseAfterMillis = pauseAfterMillis,
    )
}

private data class ReaderSentenceRange(
    val start: Int,
    val endExclusive: Int,
    val pauseAfterMillis: Long = 0L,
)

private fun Fb2Text.plainText(): String =
    spans.joinToString(separator = "") { it.text }

private fun String.isReadAloudPauseBreak(): Boolean {
    val compact = trim()
    return compact.length >= 3 && compact.all { it == '*' || it.isWhitespace() }
}

private fun String.toReadAloudSpokenText(): String =
    applyReadAloudTextReplacements(ReadAloudHardcodedTextReplacements)
        .withReadAloudStressMarkers()
        .replace(Regex("\\.{2,}"), ".")
        .replace(Regex("([!?])\\.+"), "$1")
        .replace('…', '.')
        .trim()

private fun String.applyReadAloudTextReplacements(replacements: List<ReadAloudTextReplacement>): String =
    replacements.fold(this) { text, replacement ->
        text.replace(replacement.from, replacement.to, ignoreCase = !replacement.caseSensitive)
    }

private fun String.withReadAloudStressMarkers(): String = buildString(length) {
    var index = 0
    while (index < this@withReadAloudStressMarkers.length) {
        val current = this@withReadAloudStressMarkers[index]
        val next = this@withReadAloudStressMarkers.getOrNull(index + 1)
        if ((current == '/' || current == '\'') && next != null && next.isReadAloudStressableLetter()) {
            append(next)
            append(CombiningAcuteAccent)
            index += 2
        } else {
            append(current)
            index += 1
        }
    }
}

private fun Char.isReadAloudStressableLetter(): Boolean =
    this in ReadAloudStressableLetters

private fun String.sentenceRanges(): List<ReaderSentenceRange> {
    val ranges = mutableListOf<ReaderSentenceRange>()
    var start = 0

    fun skipLeadingWhitespace() {
        while (start < length && this[start].isWhitespace()) start += 1
    }

    skipLeadingWhitespace()
    var index = start
    while (index < length) {
        if (this[index].isSentenceTerminator()) {
            val terminatorEnd = sentenceTerminatorEnd(index)
            val pauseAfterMillis = if (hasReadAloudEllipsisAt(index, terminatorEnd)) {
                EllipsisPauseAfterMillis
            } else {
                0L
            }
            var end = terminatorEnd
            while (end < length && this[end] in "\"'»”’)]}") end += 1
            ranges += ReaderSentenceRange(start, end, pauseAfterMillis)
            start = end
            skipLeadingWhitespace()
            index = start
        } else {
            index += 1
        }
    }
    if (start < length) {
        ranges += ReaderSentenceRange(start, length)
    }
    return ranges
}

private fun String.sentenceTerminatorEnd(index: Int): Int {
    var end = index + 1
    if (this[index] == '.' || this[index] == '!' || this[index] == '?') {
        while (end < length && this[end] == '.') end += 1
    }
    return end
}

private fun String.hasReadAloudEllipsisAt(index: Int, terminatorEnd: Int): Boolean =
    this[index] == '…' || (this[index] == '.' && terminatorEnd - index >= 2)

private fun Char.isSentenceTerminator(): Boolean =
    this == '.' || this == '!' || this == '?' || this == '…'

private const val HeadingPauseBeforeMillis = 1_000L
private const val HeadingPauseAfterMillis = 600L
private const val StarBreakPauseMillis = 1_200L
private const val EllipsisPauseAfterMillis = 350L
private const val CombiningAcuteAccent = '\u0301'
private const val ReadAloudStressableLetters = "аеёиоуыэюяАЕЁИОУЫЭЮЯaeiouyAEIOUY"
private val WebUrlRegex = Regex("""https?://\S+""", RegexOption.IGNORE_CASE)
private val WebUrlTrailingPunctuation = charArrayOf('.', ',', ';', ':', '!', '?', ')', ']', '}', '>', '"', '\'', '»', '”', '’')
private val LinkTextColor = Color(0xFF0B57D0)
// Link hit-area debugging colors. Kept here so the temporary backgrounds above can be
// re-enabled quickly when continuing touch overlay investigation.
// private val ReaderLinkDebugBackground = Color(0x33FFB300)
// private val ReaderLinkTouchDebugBackground = Color(0x3300BCD4)
private const val ReaderLinkAnnotationTag = "fb2-link"
private val ReaderLinkTouchTrimLeft = 8.dp
private val ReaderLinkTouchExtendRight = 48.dp
private val ReaderLinkTouchMinimumHeight = 34.dp
private val ReaderLinkTouchVerticalPadding = 4.dp

private val ReadAloudHardcodedTextReplacements = listOf(
    ReadAloudTextReplacement(from = "Господа,", to = "Господ/а,", caseSensitive = true),
    ReadAloudTextReplacement(from = "господа", to = "господ/а", caseSensitive = true),
    ReadAloudTextReplacement(from = "прошуршала", to = "прошурш/ала", caseSensitive = false),
    ReadAloudTextReplacement(from = "железной дорогой", to = "железной дор/огой"),
    ReadAloudTextReplacement(from = "пустовавшими", to = "пустов/авшими"),
    ReadAloudTextReplacement(from = "непарадной", to = "непар/адной"),
    ReadAloudTextReplacement(from = "свертывали", to = "свёртывали"),
)

private fun List<Fb2Section>.flattenSections(depth: Int = 0): List<ChapterEntry> =
    flatMapIndexed { index, section ->
        val fallback = strings.sectionFallback(index)
        listOf(ChapterEntry(section.title?.ifBlank { null } ?: fallback, section, depth)) +
            section.sections.flattenSections(depth + 1)
    }

private fun Fb2Section.readableBlocks(): List<Fb2Block> =
    blocks.ifEmpty {
        images.map(Fb2Block::Image) + paragraphs.map { Fb2Block.Paragraph(Fb2Text(listOf(Fb2TextSpan(it)))) }
    }

private fun List<Fb2Block>.hasIndentBearingContent(): Boolean =
    any { block ->
        when (block) {
            Fb2Block.EmptyLine,
            is Fb2Block.Image,
            is Fb2Block.Subtitle -> false
            is Fb2Block.Cite,
            is Fb2Block.Epigraph,
            is Fb2Block.Paragraph,
            is Fb2Block.Poem -> true
        }
    }

private data class ChapterEntry(
    val title: String,
    val section: Fb2Section,
    val depth: Int,
)

internal data class BookStats(
    val words: Int,
    val sections: Int,
    val images: Int,
) {
    companion object {
        fun from(book: Fb2Book): BookStats {
            val sections = book.sections.flattenSections()
            val words = sections.sumOf { entry ->
                entry.section.readableBlocks().sumOf { block -> block.wordCount() }
            }
            val bodyImages = sections.sumOf { it.section.images.size } + book.bodyImages.size
            return BookStats(words = words, sections = sections.size, images = bodyImages + book.coverImages.size)
        }
    }
}

private fun Fb2Block.wordCount(): Int =
    when (this) {
        Fb2Block.EmptyLine,
        is Fb2Block.Image -> 0
        is Fb2Block.Cite -> cite.wordCount()
        is Fb2Block.Epigraph -> epigraph.wordCount()
        is Fb2Block.Paragraph -> content.plainText().wordCount()
        is Fb2Block.Poem -> poem.wordCount()
        is Fb2Block.Subtitle -> content.plainText().wordCount()
    }

private fun Fb2Poem.wordCount(): Int =
    epigraphs.sumOf { it.wordCount() } + readerSegments().sumOf { it.text.plainText().wordCount() }

private fun Fb2Epigraph.wordCount(): Int =
    blocks.sumOf { it.wordCount() } + textAuthors.sumOf { it.plainText().wordCount() }

private fun Fb2Cite.wordCount(): Int =
    blocks.sumOf { it.wordCount() } + textAuthors.sumOf { it.plainText().wordCount() }

private fun Fb2EpigraphBlock.wordCount(): Int =
    when (this) {
        Fb2EpigraphBlock.EmptyLine -> 0
        is Fb2EpigraphBlock.Cite -> cite.wordCount()
        is Fb2EpigraphBlock.Paragraph -> content.plainText().wordCount()
        is Fb2EpigraphBlock.Poem -> poem.wordCount()
        is Fb2EpigraphBlock.Subtitle -> content.plainText().wordCount()
    }

private fun String.wordCount(): Int =
    split(Regex("\\s+")).count { it.isNotBlank() }
