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.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.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
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.Button
import androidx.compose.material3.Card
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap
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.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.withStyle
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.Fb2ImageRef
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.launch
import kotlin.math.max
import kotlin.math.min

@Composable
internal fun ContinuousBookReader(
    book: Fb2Book,
    stats: BookStats,
    listState: LazyListState,
    modifier: Modifier = Modifier,
    onImageOpen: (ViewedBookImage) -> Unit = {},
) {
    val hyphenation = remember { HyphenationRegistry() }
    val scope = rememberCoroutineScope()
    val contentPadding = if (isAndroidPlatform()) {
        PaddingValues(start = 6.dp, top = 6.dp, end = 0.dp, bottom = 6.dp)
    } else {
        PaddingValues(horizontal = 8.dp, vertical = 6.dp)
    }

    LazyColumn(
        state = listState,
        modifier = modifier
            .background(MaterialTheme.colorScheme.surface)
            .pageTurnOnTouchTap(
                onPageDown = {
                    scope.launch {
                        listState.animateScrollBy(listState.pageScrollDistance())
                    }
                },
                onPageUp = {
                    scope.launch {
                        listState.animateScrollBy(-listState.pageScrollDistance())
                    }
                },
            ),
        contentPadding = contentPadding,
        verticalArrangement = Arrangement.spacedBy(10.dp),
    ) {
        item {
            Column(verticalArrangement = Arrangement.spacedBy(14.dp)) {
                CoverAndTitle(book, onImageOpen = onImageOpen)
                MetadataCard(book)
                StatsCard(stats)
            }
        }
        item {
            Spacer(Modifier.height(6.dp))
        }
        book.sections.forEachIndexed { index, section ->
            sectionItems(
                book = book,
                section = section,
                depth = 0,
                keyPrefix = "section-$index",
                hyphenation = hyphenation,
                onImageOpen = onImageOpen,
            )
        }
        item { Spacer(Modifier.height(22.dp)) }
    }
}

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

        val up = waitForUpOrCancellation(pass = PointerEventPass.Final) ?: return@awaitEachGesture
        if (up.isConsumed) return@awaitEachGesture

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

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

private fun LazyListScope.sectionItems(
    book: Fb2Book,
    section: Fb2Section,
    depth: Int,
    keyPrefix: String,
    hyphenation: HyphenationRegistry,
    onImageOpen: (ViewedBookImage) -> Unit,
) {
    if( section.title.isNullOrBlank() ) {
        item {
            Spacer(Modifier.height(2.dp)
                .background(MaterialTheme.colorScheme.secondaryFixedDim)
                .fillMaxWidth().padding(vertical = 5.dp, horizontal = 4.dp)
            )
        }
    }
    else {
        item(key = "$keyPrefix-title") {
            Text(
                section.title!!,
                style = when (depth) {
                    0 -> MaterialTheme.typography.headlineMedium
                    1 -> MaterialTheme.typography.titleLarge
                    else -> MaterialTheme.typography.titleMedium
                },
                fontWeight = FontWeight.Bold,
                lineHeight = if (depth == 0) 36.sp else 28.sp,
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(top = if (depth == 0) 22.dp else 14.dp, start = (depth * 12).dp, bottom = 4.dp),
            )
        }
    }
    items(section.readableBlocks()) { 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,
                onOpen = onImageOpen,
            )
            is Fb2Block.Paragraph -> ReaderText(
                text = block.content,
                language = book.language,
                hyphenation = hyphenation,
                style = readerParagraphTextStyle(book.language),
                /* Justify adds extra padding to the end, which hardly can be removed */
                textAlign = TextAlign.Justify,
                // so we add 6.dp to make it look symmetric
                modifier = Modifier.padding(start = (depth * 8).dp + 6.dp, end = 0.dp),
            )
            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),
            )
        }
    }
    section.sections.forEachIndexed { index, child ->
        sectionItems(
            book = book,
            section = child,
            depth = depth + 1,
            keyPrefix = "$keyPrefix-$index",
            hyphenation = hyphenation,
            onImageOpen = onImageOpen,
        )
    }
}

@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(
                "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,
            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 { "Unknown author" },
                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("Genres", book.genres.joinToString().ifBlank { "Not specified" })
            DetailLine("Translator", book.translators.joinToString { it.displayName }.ifBlank { "Not specified" })
            DetailLine("Source", book.sourceLanguage?.uppercase() ?: "Not specified")
            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("Words", stats.words.formatCompact())
            StatBlock("Sections", stats.sections.toString())
            StatBlock("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.Justify,
                )
                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),
                )
            }
        }
        item { Spacer(Modifier.height(22.dp)) }
    }
}

@Composable
private fun ReaderText(
    text: Fb2Text,
    language: String?,
    hyphenation: HyphenationRegistry,
    style: TextStyle,
    textAlign: TextAlign,
    modifier: Modifier = Modifier,
) {
    val annotatedText = text.toAnnotatedString(language, hyphenation)
    val needsSoftHyphenPaintWorkaround = isDesktopPlatform()
    var textLayout by remember(annotatedText) { mutableStateOf<TextLayoutResult?>(null) }
    val desktopHyphenColor = MaterialTheme.colorScheme.onSurface
    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
            },
        )

    Text(
        text = annotatedText,
        style = style,
        textAlign = textAlign,
        modifier = textModifier,
        onTextLayout = { textLayout = it },
    )
}

@Composable
private fun readerParagraphTextStyle(language: String?): TextStyle =
    MaterialTheme.typography.bodyLarge.copy(
        fontWeight = FontWeight(350),
        fontSize = 21.sp,
        lineHeight = 28.sp,
        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 isAndroidPlatform(): Boolean =
    getPlatform().name.startsWith("Android")

private fun isDesktopPlatform(): Boolean =
    getPlatform().name.startsWith("Java")

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,
    onOpen: (ViewedBookImage) -> Unit = {},
) {
    val binary = remember(book, image) {
        image?.let(book::binaryFor)
    }
    val bitmap = remember(binary) {
        binary?.let { decodeBookImage(it) }
    }
    val imageTitle = image?.alt?.ifBlank { null } ?: book.title
    Box(
        modifier = modifier
            .clip(RoundedCornerShape(8.dp))
            .background(MaterialTheme.colorScheme.surfaceVariant)
            .then(
                if (bitmap != null && binary != null) {
                    Modifier.clickable {
                        onOpen(
                            ViewedBookImage(
                                bitmap = bitmap,
                                bytes = binary.imageBytes(),
                                mimeType = binary.contentType,
                                title = imageTitle,
                            ),
                        )
                    }
                } else {
                    Modifier
                },
            ),
        contentAlignment = Alignment.Center,
    ) {
        if (bitmap != null) {
            Image(
                bitmap = bitmap,
                contentDescription = imageTitle,
                modifier = Modifier.fillMaxWidth(),
                contentScale = contentScale,
            )
        } else {
            Text("No image", style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.outline)
        }
    }
}

private fun Fb2Text.toAnnotatedString(language: String?, hyphenation: HyphenationRegistry): AnnotatedString =
    buildAnnotatedString {
        spans.forEach { span ->
            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,
                textDecoration = if (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
                },
            )
            withStyle(spanStyle) {
                append(hyphenation.hyphenate(span.text, language))
            }
        }
    }

private fun List<Fb2Section>.flattenSections(depth: Int = 0): List<ChapterEntry> =
    flatMapIndexed { index, section ->
        val fallback = "Section ${index + 1}"
        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 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.paragraphs.sumOf { paragraph -> paragraph.split(Regex("\\s+")).count { it.isNotBlank() } }
            }
            val bodyImages = sections.sumOf { it.section.images.size } + book.bodyImages.size
            return BookStats(words = words, sections = sections.size, images = bodyImages + book.coverImages.size)
        }
    }
}
