package net.sergeych.toread

import net.sergeych.toread.fb2.Fb2Binary
import net.sergeych.toread.fb2.Fb2Book
import net.sergeych.toread.storage.BookReadingStatus
import kotlin.math.abs

data class LibraryItem(
    val fileId: String,
    val bookId: String?,
    val title: String,
    val authors: List<String> = emptyList(),
    val language: String? = null,
    val date: String? = null,
    val format: String?,
    val sizeBytes: Long?,
    val storageUri: String?,
    val lastSeenAt: Long?,
    val readingStatus: BookReadingStatus = BookReadingStatus.NEW,
    val favorite: Boolean = false,
    val lastReadAt: Long? = null,
    val importedAt: Long? = null,
    val coverImage: ByteArray? = null,
    val coverImageMimeType: String? = null,
)

data class LibraryCover(
    val image: ByteArray,
    val mimeType: String?,
)

data class LibraryScanReport(
    val scannedFiles: Int,
    val importedFiles: Int,
    val skippedFiles: Int,
    val failedFiles: Int,
    val totalFiles: Int? = null,
)

data class LibraryScanProgress(
    val scannedFiles: Int,
    val importedFiles: Int,
    val skippedFiles: Int,
    val failedFiles: Int,
    val currentFile: String? = null,
    val totalFiles: Int? = null,
)

data class LibraryRescanReport(
    val scannedFiles: Int,
    val updatedFiles: Int,
    val failedFiles: Int,
)

data class PlatformOpenBookRequest(
    val id: String,
    val displayName: String,
    val bytes: ByteArray,
)

data class ReadingPosition(
    val itemIndex: Int,
    val scrollOffset: Int,
    val readAloudSentenceIndex: Int? = null,
    val backStack: List<ReadingPosition> = emptyList(),
)

data class ReaderFontSettings(
    val fontSizeSp: Float = defaultReaderFontSizeSp(),
    val lineHeightSp: Float = DefaultReaderLineHeightSp,
    val fontWeight: Int = defaultReaderFontWeight(),
    val fontName: String? = null,
    val lightBackgroundArgb: Long? = null,
    val lightTextArgb: Long? = null,
    val darkBackgroundArgb: Long? = null,
    val darkTextArgb: Long? = null,
    val lightCustomBackgroundArgb: Long? = null,
    val lightCustomTextArgb: Long? = null,
    val darkCustomBackgroundArgb: Long? = null,
    val darkCustomTextArgb: Long? = null,
)

data class BookInfoExtras(
    val sourceFileName: String? = null,
    val sourceFilePath: String? = null,
    val bookmarks: List<BookmarkInfo> = emptyList(),
    val notes: List<NoteInfo> = emptyList(),
    val lastReadingPosition: ReadingPosition? = null,
)

data class BookmarkInfo(
    val title: String?,
    val selectedText: String?,
    val progress: Double?,
    val updatedAt: Long,
)

data class NoteInfo(
    val text: String,
    val progress: Double?,
    val updatedAt: Long,
)

enum class ThemeMode {
    LIGHT,
    DARK,
    SYSTEM,
}

expect fun defaultLibraryScanPath(): String?

expect suspend fun loadPlatformOpenBookRequest(): PlatformOpenBookRequest?

expect suspend fun chooseLibraryScanDirectory(): String?

expect suspend fun loadLibraryItems(): List<LibraryItem>

expect suspend fun loadLibraryItemsPage(limit: Int, offset: Int): List<LibraryItem>

expect suspend fun loadRecentlyAddedLibraryItems(sinceImportedAt: Long, limit: Int = 50): List<LibraryItem>

expect suspend fun searchLibraryItems(query: String, limit: Int = 100): List<LibraryItem>

expect fun currentLibraryTimeMillis(): Long

expect suspend fun loadLibraryItem(fileId: String): LibraryItem?

expect suspend fun loadLibraryItemCover(fileId: String): LibraryCover?

expect suspend fun scanLibrarySubtree(path: String, onProgress: (LibraryScanProgress) -> Unit): LibraryScanReport

expect suspend fun openLibraryBook(fileId: String): ByteArray?

expect suspend fun refreshLibraryItemFromParsedBook(fileId: String, book: Fb2Book): LibraryItem?

expect suspend fun rescanAllLibraryBooks(): LibraryRescanReport

expect suspend fun deleteLibraryItem(fileId: String): Boolean

expect suspend fun loadLibraryReadingPosition(fileId: String): ReadingPosition?

expect suspend fun saveLibraryReadingPosition(fileId: String, position: ReadingPosition)

expect suspend fun clearLibraryReadingPosition(fileId: String)

expect suspend fun markLibraryReadingStatus(fileId: String, status: BookReadingStatus): Boolean

expect suspend fun markLibraryFavorite(fileId: String, favorite: Boolean): Boolean

expect suspend fun shareLibraryBookFile(fileId: String): Boolean

expect suspend fun viewLibraryBookFile(folder: String, fileName: String): Boolean

expect fun openPlatformUrl(url: String): Boolean

expect suspend fun loadBookInfoExtras(fileId: String): BookInfoExtras

expect suspend fun loadActiveReadingFileId(): String?

expect suspend fun saveActiveReadingFileId(fileId: String?)

expect suspend fun loadThemeMode(): ThemeMode

expect suspend fun saveThemeMode(mode: ThemeMode)

expect suspend fun loadReaderFontSettings(): ReaderFontSettings

expect suspend fun saveReaderFontSettings(settings: ReaderFontSettings)

expect suspend fun loadReaderFullScreen(): Boolean

expect suspend fun saveReaderFullScreen(fullScreen: Boolean)

expect suspend fun loadScanDownloadsAutomatically(): Boolean

expect suspend fun saveScanDownloadsAutomatically(enabled: Boolean)

internal expect suspend fun loadLibraryFilter(): LibraryFilter

internal expect suspend fun saveLibraryFilter(filter: LibraryFilter)

expect suspend fun loadAppLocaleTag(): String?

expect suspend fun saveAppLocaleTag(localeTag: String?)

expect suspend fun loadDownloadsWasScanned(): Boolean

expect suspend fun saveDownloadsWasScanned(scanned: Boolean)

expect fun downloadsScanPath(): String?

expect fun isDownloadsScanPath(path: String): Boolean

expect fun isPlatformDarkTheme(): Boolean

expect fun watchPlatformDarkTheme(onChange: (Boolean) -> Unit): () -> Unit

expect fun libraryLogPath(): String?

expect fun formatLibraryLastReadTime(millis: Long): String

internal fun Fb2Book.libraryCoverBinary(): Fb2Binary? {
    val image = coverImages.firstOrNull() ?: bodyImages.firstOrNull()
    return image?.let(::binaryFor)
}

internal fun canShareLibraryBookFile(): Boolean =
    getPlatform().name.startsWith("Android")

internal suspend fun shareLibraryBook(fileId: String): String =
    if (shareLibraryBookFile(fileId)) strings.shareOpened() else strings.couldNotShareBook()

internal fun canViewLibraryBookFile(): Boolean =
    getPlatform().name.startsWith("Java")

internal suspend fun viewLibraryBook(fileId: String): String {
    val location = loadLibraryItem(fileId)?.storageUri?.toLibraryBookFileLocation()
        ?: return strings.couldNotOpenFileLocation()
    return if (viewLibraryBookFile(location.folder, location.fileName)) {
        strings.openedFileLocation()
    } else {
        strings.couldNotOpenFileLocation()
    }
}

private data class LibraryBookFileLocation(
    val folder: String,
    val fileName: String,
)

private fun String.toLibraryBookFileLocation(): LibraryBookFileLocation? {
    val separatorIndex = maxOf(lastIndexOf('/'), lastIndexOf('\\'))
    if (separatorIndex < 0 || separatorIndex >= lastIndex) return null
    return LibraryBookFileLocation(
        folder = if (separatorIndex == 0) substring(0, 1) else substring(0, separatorIndex),
        fileName = substring(separatorIndex + 1),
    )
}

internal const val DefaultReaderLineHeightSp = 26f
internal const val ReaderFontSizeStepSp = 1f
internal const val ReaderLineHeightStepSp = 1f

private const val MinReaderFontSizeSp = 14f
private const val MaxReaderFontSizeSp = 34f
private const val MinReaderLineHeightSp = 18f
private const val MaxReaderLineHeightSp = 48f
private const val MinReaderFontWeight = 100
private const val MaxReaderFontWeight = 900
private val ReaderFontWeightSteps = listOf(100, 200, 300, 400, 500, 600, 700, 800, 900)

internal fun defaultReaderFontSettings(): ReaderFontSettings = ReaderFontSettings()

internal fun defaultReaderFontSizeSp(): Float =
    if (getPlatform().name.startsWith("Android")) 20f else 18f

internal fun defaultReaderFontWeight(): Int =
    if (getPlatform().name.startsWith("Android")) 350 else 400

internal fun ReaderFontSettings.coerced(): ReaderFontSettings =
    copy(
        fontSizeSp = fontSizeSp.coerceIn(MinReaderFontSizeSp, MaxReaderFontSizeSp),
        lineHeightSp = lineHeightSp.coerceIn(MinReaderLineHeightSp, MaxReaderLineHeightSp),
        fontWeight = fontWeight.coerceIn(MinReaderFontWeight, MaxReaderFontWeight),
        fontName = fontName?.takeIf { it.isNotBlank() },
        lightBackgroundArgb = lightBackgroundArgb?.coerceArgb(),
        lightTextArgb = lightTextArgb?.coerceArgb(),
        darkBackgroundArgb = darkBackgroundArgb?.coerceArgb(),
        darkTextArgb = darkTextArgb?.coerceArgb(),
        lightCustomBackgroundArgb = lightCustomBackgroundArgb?.coerceArgb(),
        lightCustomTextArgb = lightCustomTextArgb?.coerceArgb(),
        darkCustomBackgroundArgb = darkCustomBackgroundArgb?.coerceArgb(),
        darkCustomTextArgb = darkCustomTextArgb?.coerceArgb(),
    )

internal fun ReaderFontSettings.adjustFontSize(steps: Int): ReaderFontSettings {
    val current = coerced()
    val nextFontSize = (current.fontSizeSp + steps * ReaderFontSizeStepSp)
        .coerceIn(MinReaderFontSizeSp, MaxReaderFontSizeSp)
    val scale = if (current.fontSizeSp > 0f) nextFontSize / current.fontSizeSp else 1f
    return current.copy(
        fontSizeSp = nextFontSize,
        lineHeightSp = (current.lineHeightSp * scale).coerceIn(MinReaderLineHeightSp, MaxReaderLineHeightSp),
    )
}

internal fun ReaderFontSettings.adjustLineHeight(steps: Int): ReaderFontSettings =
    coerced().let { current ->
        current.copy(
            lineHeightSp = (current.lineHeightSp + steps * ReaderLineHeightStepSp)
                .coerceIn(MinReaderLineHeightSp, MaxReaderLineHeightSp),
        )
    }

internal fun ReaderFontSettings.adjustFontWeight(steps: Int): ReaderFontSettings {
    val current = coerced()
    if (steps == 0) return current

    var nextWeight = current.fontWeight
    repeat(abs(steps)) {
        nextWeight = current.fontWeight + if (steps > 0) 50 else -50
        if( nextWeight == 100 || nextWeight == 900 ) return@repeat
    }
    return current.copy(fontWeight = nextWeight)
}

internal fun ReaderFontSettings.toStorageValue(): String =
    coerced().let { current ->
        listOf(
            current.fontSizeSp.toString(),
            current.lineHeightSp.toString(),
            current.fontWeight.toString(),
            current.fontName.orEmpty(),
            current.lightBackgroundArgb?.toStorageArgb().orEmpty(),
            current.lightTextArgb?.toStorageArgb().orEmpty(),
            current.darkBackgroundArgb?.toStorageArgb().orEmpty(),
            current.darkTextArgb?.toStorageArgb().orEmpty(),
            current.lightCustomBackgroundArgb?.toStorageArgb().orEmpty(),
            current.lightCustomTextArgb?.toStorageArgb().orEmpty(),
            current.darkCustomBackgroundArgb?.toStorageArgb().orEmpty(),
            current.darkCustomTextArgb?.toStorageArgb().orEmpty(),
        ).joinToString("|")
    }

internal fun readerFontSettingsFromStorageValue(value: String?): ReaderFontSettings {
    val defaults = defaultReaderFontSettings()
    val parts = value?.split("|") ?: return defaults
    return ReaderFontSettings(
        fontSizeSp = parts.getOrNull(0)?.toFloatOrNull() ?: defaults.fontSizeSp,
        lineHeightSp = parts.getOrNull(1)?.toFloatOrNull() ?: defaults.lineHeightSp,
        fontWeight = parts.getOrNull(2)?.toIntOrNull() ?: defaults.fontWeight,
        fontName = parts.getOrNull(3)?.takeIf { it.isNotBlank() } ?: defaults.fontName,
        lightBackgroundArgb = parts.getOrNull(4)?.storageArgbOrNull(),
        lightTextArgb = parts.getOrNull(5)?.storageArgbOrNull(),
        darkBackgroundArgb = parts.getOrNull(6)?.storageArgbOrNull(),
        darkTextArgb = parts.getOrNull(7)?.storageArgbOrNull(),
        lightCustomBackgroundArgb = parts.getOrNull(8)?.storageArgbOrNull(),
        lightCustomTextArgb = parts.getOrNull(9)?.storageArgbOrNull(),
        darkCustomBackgroundArgb = parts.getOrNull(10)?.storageArgbOrNull(),
        darkCustomTextArgb = parts.getOrNull(11)?.storageArgbOrNull(),
    ).coerced()
}

internal fun ReaderFontSettings.withThemeBackgroundArgb(useDarkTheme: Boolean, argb: Long?): ReaderFontSettings =
    if (useDarkTheme) copy(darkBackgroundArgb = argb?.coerceArgb()) else copy(lightBackgroundArgb = argb?.coerceArgb())

internal fun ReaderFontSettings.withThemeTextArgb(useDarkTheme: Boolean, argb: Long?): ReaderFontSettings =
    if (useDarkTheme) copy(darkTextArgb = argb?.coerceArgb()) else copy(lightTextArgb = argb?.coerceArgb())

internal fun ReaderFontSettings.withThemeCustomBackgroundArgb(useDarkTheme: Boolean, argb: Long?): ReaderFontSettings =
    if (useDarkTheme) {
        copy(darkCustomBackgroundArgb = argb?.coerceArgb(), darkBackgroundArgb = argb?.coerceArgb())
    } else {
        copy(lightCustomBackgroundArgb = argb?.coerceArgb(), lightBackgroundArgb = argb?.coerceArgb())
    }

internal fun ReaderFontSettings.withThemeCustomTextArgb(useDarkTheme: Boolean, argb: Long?): ReaderFontSettings =
    if (useDarkTheme) {
        copy(darkCustomTextArgb = argb?.coerceArgb(), darkTextArgb = argb?.coerceArgb())
    } else {
        copy(lightCustomTextArgb = argb?.coerceArgb(), lightTextArgb = argb?.coerceArgb())
    }

internal fun ReaderFontSettings.themeBackgroundArgb(useDarkTheme: Boolean): Long? =
    if (useDarkTheme) darkBackgroundArgb else lightBackgroundArgb

internal fun ReaderFontSettings.themeTextArgb(useDarkTheme: Boolean): Long? =
    if (useDarkTheme) darkTextArgb else lightTextArgb

internal fun ReaderFontSettings.themeCustomBackgroundArgb(useDarkTheme: Boolean): Long? =
    if (useDarkTheme) darkCustomBackgroundArgb else lightCustomBackgroundArgb

internal fun ReaderFontSettings.themeCustomTextArgb(useDarkTheme: Boolean): Long? =
    if (useDarkTheme) darkCustomTextArgb else lightCustomTextArgb

internal fun ReaderFontSettings.resetThemeColorScheme(useDarkTheme: Boolean): ReaderFontSettings =
    if (useDarkTheme) {
        copy(
            darkBackgroundArgb = null,
            darkTextArgb = null,
        )
    } else {
        copy(
            lightBackgroundArgb = null,
            lightTextArgb = null,
        )
    }

private fun Long.coerceArgb(): Long =
    coerceIn(0x00000000L, 0xFFFFFFFFL)

private fun Long.toStorageArgb(): String =
    coerceArgb().toString(16).padStart(8, '0').uppercase()

private fun String.storageArgbOrNull(): Long? =
    trim()
        .removePrefix("#")
        .takeIf { it.length == 8 }
        ?.toLongOrNull(16)
        ?.coerceArgb()
