/*
 * Episteme Reader - A native Android document reader.
 * Copyright (C) 2026 Episteme
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 * mail: epistemereader@gmail.com
 */
@file:OptIn(ExperimentalSerializationApi::class)
package com.aryan.reader.paginatedreader

import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.isSpecified
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.ParagraphStyle
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.isSpecified
import com.aryan.reader.paginatedreader.serialization.AnnotatedStringSerializer
import com.aryan.reader.paginatedreader.serialization.ColorSerializer
import com.aryan.reader.paginatedreader.serialization.DpSerializer
import com.aryan.reader.paginatedreader.serialization.ParagraphStyleSerializer
import com.aryan.reader.paginatedreader.serialization.SpanStyleSerializer
import com.aryan.reader.paginatedreader.serialization.TextAlignSerializer
import com.aryan.reader.paginatedreader.serialization.TextUnitSerializer
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber

@Serializable
data class BlockStyle(
    @ProtoNumber(1) val margin: BoxBorders = BoxBorders(),
    @ProtoNumber(2) val padding: BoxBorders = BoxBorders(),
    @ProtoNumber(3) @Serializable(with = DpSerializer::class) val width: Dp = Dp.Unspecified,
    @ProtoNumber(4) @Serializable(with = DpSerializer::class) val maxWidth: Dp = Dp.Unspecified,
    @ProtoNumber(5) @Serializable(with = DpSerializer::class) val height: Dp = Dp.Unspecified,
    @ProtoNumber(6) @Serializable(with = ColorSerializer::class) val backgroundColor: Color = Color.Unspecified,
    @ProtoNumber(7) val borderTop: BorderStyle? = null,
    @ProtoNumber(8) val borderRight: BorderStyle? = null,
    @ProtoNumber(9) val borderBottom: BorderStyle? = null,
    @ProtoNumber(10) val borderLeft: BorderStyle? = null,
    @ProtoNumber(11) val listStyleType: String? = null,
    @ProtoNumber(12) val listStyleImage: String? = null,
    @ProtoNumber(13) val pageBreakInsideAvoid: Boolean = false,
    @ProtoNumber(14) val pageBreakAfterAvoid: Boolean = false,
    @ProtoNumber(15) val boxSizing: String? = null,
    @ProtoNumber(16) val float: String? = null,
    @ProtoNumber(17) val clear: String? = null,
    @ProtoNumber(18) val position: String? = null,
    @ProtoNumber(19) @Serializable(with = DpSerializer::class) val top: Dp = Dp.Unspecified,
    @ProtoNumber(20) @Serializable(with = DpSerializer::class) val right: Dp = Dp.Unspecified,
    @ProtoNumber(21) @Serializable(with = DpSerializer::class) val bottom: Dp = Dp.Unspecified,
    @ProtoNumber(22) @Serializable(with = DpSerializer::class) val left: Dp = Dp.Unspecified,
    @ProtoNumber(23) val display: String? = null,
    @ProtoNumber(24) val flexDirection: String? = null,
    @ProtoNumber(25) val justifyContent: String? = null,
    @ProtoNumber(26) val alignItems: String? = null,
    @ProtoNumber(27) val horizontalAlign: String? = null,
    @ProtoNumber(28) val filter: String? = null,
    @ProtoNumber(29) val borderCollapse: String? = null,
    @ProtoNumber(30) @Serializable(with = DpSerializer::class) val borderTopLeftRadius: Dp = 0.dp,
    @ProtoNumber(31) @Serializable(with = DpSerializer::class) val borderTopRightRadius: Dp = 0.dp,
    @ProtoNumber(32) @Serializable(with = DpSerializer::class) val borderBottomRightRadius: Dp = 0.dp,
    @ProtoNumber(33) @Serializable(with = DpSerializer::class) val borderBottomLeftRadius: Dp = 0.dp,
    @ProtoNumber(34) @Serializable(with = DpSerializer::class) val borderSpacing: Dp = 0.dp
) {
    fun merge(other: BlockStyle): BlockStyle {
        return BlockStyle(
            margin = BoxBorders(
                top = if (other.margin.top != 0.dp) other.margin.top else this.margin.top,
                bottom = if (other.margin.bottom != 0.dp) other.margin.bottom else this.margin.bottom,
                left = if (other.margin.left != 0.dp) other.margin.left else this.margin.left,
                right = if (other.margin.right != 0.dp) other.margin.right else this.margin.right
            ),
            padding = BoxBorders(
                top = if (other.padding.top != 0.dp) other.padding.top else this.padding.top,
                bottom = if (other.padding.bottom != 0.dp) other.padding.bottom else this.padding.bottom,
                left = if (other.padding.left != 0.dp) other.padding.left else this.padding.left,
                right = if (other.padding.right != 0.dp) other.padding.right else this.padding.right
            ),
            width = if (other.width != Dp.Unspecified) other.width else this.width,
            maxWidth = if (other.maxWidth != Dp.Unspecified) other.maxWidth else this.maxWidth,
            height = if (other.height != Dp.Unspecified) other.height else this.height,
            backgroundColor = if (other.backgroundColor.isSpecified) other.backgroundColor else this.backgroundColor,
            borderTop = other.borderTop ?: this.borderTop,
            borderRight = other.borderRight ?: this.borderRight,
            borderBottom = other.borderBottom ?: this.borderBottom,
            borderLeft = other.borderLeft ?: this.borderLeft,
            borderTopLeftRadius = if (other.borderTopLeftRadius != 0.dp) other.borderTopLeftRadius else this.borderTopLeftRadius,
            borderTopRightRadius = if (other.borderTopRightRadius != 0.dp) other.borderTopRightRadius else this.borderTopRightRadius,
            borderBottomRightRadius = if (other.borderBottomRightRadius != 0.dp) other.borderBottomRightRadius else this.borderBottomRightRadius,
            borderBottomLeftRadius = if (other.borderBottomLeftRadius != 0.dp) other.borderBottomLeftRadius else this.borderBottomLeftRadius,
            listStyleType = other.listStyleType ?: this.listStyleType,
            listStyleImage = other.listStyleImage ?: this.listStyleImage,
            pageBreakInsideAvoid = this.pageBreakInsideAvoid || other.pageBreakInsideAvoid,
            pageBreakAfterAvoid = this.pageBreakAfterAvoid || other.pageBreakAfterAvoid,
            boxSizing = other.boxSizing ?: this.boxSizing,
            float = other.float ?: this.float,
            clear = other.clear ?: this.clear,
            position = other.position ?: this.position,
            top = if (other.top.isSpecified) other.top else this.top,
            right = if (other.right.isSpecified) other.right else this.right,
            bottom = if (other.bottom.isSpecified) other.bottom else this.bottom,
            left = if (other.left.isSpecified) other.left else this.left,
            display = other.display ?: this.display,
            flexDirection = other.flexDirection ?: this.flexDirection,
            justifyContent = other.justifyContent ?: this.justifyContent,
            alignItems = other.alignItems ?: this.alignItems,
            horizontalAlign = other.horizontalAlign ?: this.horizontalAlign,
            filter = other.filter ?: this.filter,
            borderCollapse = other.borderCollapse ?: this.borderCollapse,
            borderSpacing = if (other.borderSpacing != 0.dp) other.borderSpacing else this.borderSpacing
        )
    }
}

@Serializable
data class BoxBorders(
    @ProtoNumber(1) @Serializable(with = DpSerializer::class) val top: Dp = 0.dp,
    @ProtoNumber(2) @Serializable(with = DpSerializer::class) val right: Dp = 0.dp,
    @ProtoNumber(3) @Serializable(with = DpSerializer::class) val bottom: Dp = 0.dp,
    @ProtoNumber(4) @Serializable(with = DpSerializer::class) val left: Dp = 0.dp
)

@Serializable
data class BorderStyle(
    @ProtoNumber(1) @Serializable(with = DpSerializer::class) val width: Dp = 0.dp,
    @ProtoNumber(2) @Serializable(with = ColorSerializer::class) val color: Color = Color.Transparent,
    @ProtoNumber(3) val style: String = "solid"
)

@Serializable
sealed interface ContentBlock {
    val style: BlockStyle
    val elementId: String?
    val cfi: String?
    val blockIndex: Int
    val expectedHeight: Int
}

sealed interface TextContentBlock : ContentBlock {
    val content: AnnotatedString
    val startCharOffsetInSource: Int
    val endCharOffsetInSource: Int
}

@Serializable
data class ParagraphBlock(
    @ProtoNumber(1) @Serializable(with = AnnotatedStringSerializer::class) override val content: AnnotatedString,
    @ProtoNumber(2) @Serializable(with = TextAlignSerializer::class) val textAlign: TextAlign? = null,
    @ProtoNumber(3) override val style: BlockStyle = BlockStyle(),
    @ProtoNumber(4) override val elementId: String? = null,
    @ProtoNumber(5) override val cfi: String? = null,
    @ProtoNumber(6) override val startCharOffsetInSource: Int = 0,
    @ProtoNumber(7) override val endCharOffsetInSource: Int = -1,
    @ProtoNumber(8) override val blockIndex: Int,
    @ProtoNumber(9) override val expectedHeight: Int = 0
) : TextContentBlock

@Serializable
data class ImageBlock(
    @ProtoNumber(1) val path: String,
    @ProtoNumber(2) val altText: String?,
    @ProtoNumber(3) val intrinsicWidth: Float? = null,
    @ProtoNumber(4) val intrinsicHeight: Float? = null,
    @ProtoNumber(5) override val style: BlockStyle = BlockStyle(),
    @ProtoNumber(6) override val elementId: String? = null,
    @ProtoNumber(7) override val cfi: String? = null,
    @ProtoNumber(8) val invertOnDarkTheme: Boolean = false,
    @ProtoNumber(9) override val blockIndex: Int,
    @ProtoNumber(10) override val expectedHeight: Int = 0
) : ContentBlock

@Serializable
data class HeaderBlock(
    @ProtoNumber(1) val level: Int,
    @ProtoNumber(2) @Serializable(with = AnnotatedStringSerializer::class) override val content: AnnotatedString,
    @ProtoNumber(3) @Serializable(with = TextAlignSerializer::class) val textAlign: TextAlign? = null,
    @ProtoNumber(4) override val style: BlockStyle = BlockStyle(),
    @ProtoNumber(5) override val elementId: String? = null,
    @ProtoNumber(6) override val cfi: String? = null,
    @ProtoNumber(7) override val startCharOffsetInSource: Int = 0,
    @ProtoNumber(8) override val endCharOffsetInSource: Int = -1,
    @ProtoNumber(9) override val blockIndex: Int,
    @ProtoNumber(10) override val expectedHeight: Int = 0
) : TextContentBlock

@Serializable
data class SpacerBlock(
    @ProtoNumber(1) @Serializable(with = DpSerializer::class) val height: Dp = 8.dp,
    @ProtoNumber(2) override val style: BlockStyle = BlockStyle(),
    @ProtoNumber(3) override val elementId: String? = null,
    @ProtoNumber(4) override val cfi: String? = null,
    @ProtoNumber(5) override val blockIndex: Int,
    @ProtoNumber(6) override val expectedHeight: Int = 0
) : ContentBlock

@Serializable
data class QuoteBlock(
    @ProtoNumber(1) @Serializable(with = AnnotatedStringSerializer::class) override val content: AnnotatedString,
    @ProtoNumber(2) @Serializable(with = TextAlignSerializer::class) val textAlign: TextAlign? = null,
    @ProtoNumber(3) override val style: BlockStyle = BlockStyle(),
    @ProtoNumber(4) override val elementId: String? = null,
    @ProtoNumber(5) override val cfi: String? = null,
    @ProtoNumber(6) override val startCharOffsetInSource: Int = 0,
    @ProtoNumber(7) override val endCharOffsetInSource: Int = -1,
    @ProtoNumber(8) override val blockIndex: Int,
    @ProtoNumber(9) override val expectedHeight: Int = 0
) : TextContentBlock

@Serializable
data class ListItemBlock(
    @ProtoNumber(1) @Serializable(with = AnnotatedStringSerializer::class) override val content: AnnotatedString,
    @ProtoNumber(2) val itemMarker: String?,
    @ProtoNumber(3) val itemMarkerImage: String? = null,
    @ProtoNumber(4) override val style: BlockStyle,
    @ProtoNumber(5) override val elementId: String? = null,
    @ProtoNumber(6) override val cfi: String? = null,
    @ProtoNumber(7) override val startCharOffsetInSource: Int = 0,
    @ProtoNumber(8) override val endCharOffsetInSource: Int = -1,
    @ProtoNumber(9) override val blockIndex: Int,
    @ProtoNumber(10) override val expectedHeight: Int = 0
) : TextContentBlock

@Serializable
data class TableCell(
    @ProtoNumber(1) val content: List<ContentBlock>,
    @ProtoNumber(2) val isHeader: Boolean = false,
    @ProtoNumber(3) val style: CssStyle = CssStyle(),
    @ProtoNumber(4) val colspan: Int = 1
)

@Serializable
data class TableBlock(
    @ProtoNumber(1) val rows: List<List<TableCell>>,
    @ProtoNumber(2) override val style: BlockStyle = BlockStyle(),
    @ProtoNumber(3) override val elementId: String? = null,
    @ProtoNumber(4) override val cfi: String? = null,
    @ProtoNumber(5) override val blockIndex: Int,
    @ProtoNumber(6) override val expectedHeight: Int = 0
) : ContentBlock

@Serializable
data class MathBlock(
    @ProtoNumber(1) val svgContent: String?,
    @ProtoNumber(2) val altText: String?,
    @ProtoNumber(3) override val style: BlockStyle,
    @ProtoNumber(4) override val elementId: String?,
    @ProtoNumber(5) override val cfi: String?,
    @ProtoNumber(6) val svgWidth: String? = null,
    @ProtoNumber(7) val svgHeight: String? = null,
    @ProtoNumber(8) val svgViewBox: String? = null,
    @ProtoNumber(9) val isFromMathJax: Boolean = false,
    @ProtoNumber(10) override val blockIndex: Int,
    @ProtoNumber(11) override val expectedHeight: Int = 0
) : ContentBlock

@Serializable
data class WrappingContentBlock(
    @ProtoNumber(1) val floatedImage: ImageBlock,
    @ProtoNumber(2) val paragraphsToWrap: List<ParagraphBlock>,
    @ProtoNumber(3) override val style: BlockStyle = BlockStyle(),
    @ProtoNumber(4) override val elementId: String? = null,
    @ProtoNumber(5) override val cfi: String? = null,
    @ProtoNumber(6) override val blockIndex: Int,
    @ProtoNumber(7) override val expectedHeight: Int = 0
) : ContentBlock

@Serializable
data class TextEmphasis(
    @ProtoNumber(1) val style: String? = null,
    @ProtoNumber(2) val fill: String? = null,
    @ProtoNumber(3) @Serializable(with = ColorSerializer::class) val color: Color = Color.Unspecified,
    @ProtoNumber(4) val position: String? = null
)

@Serializable
data class CssStyle(
    @ProtoNumber(1) @Serializable(with = SpanStyleSerializer::class) val spanStyle: SpanStyle = SpanStyle(),
    @ProtoNumber(2) @Serializable(with = ParagraphStyleSerializer::class) val paragraphStyle: ParagraphStyle = ParagraphStyle(),
    @ProtoNumber(3) val blockStyle: BlockStyle = BlockStyle(),
    @ProtoNumber(4) val fontFamilies: List<String> = emptyList(),
    @ProtoNumber(5) val display: String? = null,
    @ProtoNumber(6) @Serializable(with = TextUnitSerializer::class) val fontSize: TextUnit = TextUnit.Unspecified,
    @ProtoNumber(7) val textTransform: String? = null,
    @ProtoNumber(8) val boxSizing: String? = null,
    @ProtoNumber(9) val content: String? = null,
    @ProtoNumber(10) val hyphens: String? = null,
    @ProtoNumber(11) val fontVariantNumeric: String? = null,
    @ProtoNumber(12) val textEmphasis: TextEmphasis? = null,
    @ProtoNumber(13) @Serializable(with = TextUnitSerializer::class) val wordSpacing: TextUnit = TextUnit.Unspecified,
    @ProtoNumber(14) val textDecorationStyle: String? = null,
    @ProtoNumber(15) @Serializable(with = ColorSerializer::class) val textDecorationColor: Color = Color.Unspecified,
    @ProtoNumber(16) @Serializable(with = DpSerializer::class) val textUnderlineOffset: Dp = Dp.Unspecified
) {
    fun merge(other: CssStyle): CssStyle {
        return CssStyle(
            spanStyle = this.spanStyle.merge(other.spanStyle),
            paragraphStyle = this.paragraphStyle.merge(other.paragraphStyle),
            blockStyle = this.blockStyle.merge(other.blockStyle),
            fontFamilies = other.fontFamilies.takeIf { it.isNotEmpty() } ?: this.fontFamilies,
            display = other.display ?: this.display,
            fontSize = if (other.fontSize.isSpecified) other.fontSize else this.fontSize,
            textTransform = other.textTransform ?: this.textTransform,
            boxSizing = other.boxSizing ?: this.boxSizing,
            content = other.content ?: this.content,
            hyphens = other.hyphens ?: this.hyphens,
            fontVariantNumeric = other.fontVariantNumeric ?: this.fontVariantNumeric,
            textEmphasis = other.textEmphasis ?: this.textEmphasis,
            wordSpacing = if (other.wordSpacing.isSpecified) other.wordSpacing else this.wordSpacing,
            textDecorationStyle = other.textDecorationStyle ?: this.textDecorationStyle,
            textDecorationColor = if (other.textDecorationColor.isSpecified) other.textDecorationColor else this.textDecorationColor,
            textUnderlineOffset = if (other.textUnderlineOffset.isSpecified) other.textUnderlineOffset else this.textUnderlineOffset
        )
    }
}

@Serializable
data class CssSelector(
    @ProtoNumber(1) val selector: String,
    @ProtoNumber(2) val specificity: Int
)

@Serializable
data class CssRule(
    @ProtoNumber(1) val selector: CssSelector,
    @ProtoNumber(2) val style: CssStyle
)

@Serializable
data class FontFaceInfo(
    @ProtoNumber(1) val fontFamily: String,
    @ProtoNumber(2) val src: String,
    @ProtoNumber(3) @Serializable(with = com.aryan.reader.paginatedreader.serialization.FontWeightSerializer::class) val fontWeight: FontWeight?,
    @ProtoNumber(4) @Serializable(with = com.aryan.reader.paginatedreader.serialization.FontStyleSerializer::class) val fontStyle: FontStyle?
)

@Serializable
data class Page(
    @ProtoNumber(1) val content: List<ContentBlock>
)

@Serializable
data class FlexContainerBlock(
    @ProtoNumber(1) val children: List<ContentBlock>,
    @ProtoNumber(2) override val style: BlockStyle = BlockStyle(),
    @ProtoNumber(3) override val elementId: String? = null,
    @ProtoNumber(4) override val cfi: String? = null,
    @ProtoNumber(5) override val blockIndex: Int,
    @ProtoNumber(6) override val expectedHeight: Int = 0
) : ContentBlock

@Serializable
data class OptimizedCssRules(
    @ProtoNumber(1) val byTag: Map<String, List<CssRule>> = emptyMap(),
    @ProtoNumber(2) val byClass: Map<String, List<CssRule>> = emptyMap(),
    @ProtoNumber(3) val byId: Map<String, List<CssRule>> = emptyMap(),
    @ProtoNumber(4) val otherComplex: List<CssRule> = emptyList()
) {
    fun merge(other: OptimizedCssRules): OptimizedCssRules {
        fun mergeMap(
            m1: Map<String, List<CssRule>>,
            m2: Map<String, List<CssRule>>
        ): Map<String, List<CssRule>> {
            if (m1.isEmpty()) return m2
            if (m2.isEmpty()) return m1

            val result = LinkedHashMap(m1)
            for ((key, value) in m2) {
                val existing = result[key]
                if (existing != null) {
                    result[key] = existing + value
                } else {
                    result[key] = value
                }
            }
            return result
        }

        return OptimizedCssRules(
            byTag = mergeMap(this.byTag, other.byTag),
            byClass = mergeMap(this.byClass, other.byClass),
            byId = mergeMap(this.byId, other.byId),
            otherComplex = this.otherComplex + other.otherComplex
        )
    }

    fun toFlatList(): List<CssRule> {
        return byTag.values.flatten() + byClass.values.flatten() + byId.values.flatten() + otherComplex
    }
}

data class OptimizedCssParseResult(
    val rules: OptimizedCssRules,
    val fontFaces: List<FontFaceInfo>
)