package com.aletheia.app.ui.reader

import android.content.Context
import android.util.AtomicFile
import java.io.ByteArrayOutputStream
import java.io.File
import java.security.MessageDigest

class ReaderPaginationCache(context: Context) {
    private val directory = File(context.cacheDir, CACHE_DIRECTORY).apply { mkdirs() }

    fun key(file: File, preferences: ReaderPreferences, viewportWidth: Int, viewportHeight: Int): String {
        val canonical = file.canonicalFile
        val signature = listOf(
            ENGINE_VERSION,
            canonical.name,
            canonical.length(),
            canonical.lastModified(),
            viewportWidth,
            viewportHeight,
            preferences.fontName,
            preferences.fontSize,
            preferences.lineHeight,
            preferences.margin,
            preferences.textAlign,
            preferences.verticalScroll
        ).joinToString("|")
        return MessageDigest.getInstance("SHA-256")
            .digest(signature.toByteArray(Charsets.UTF_8))
            .joinToString("") { "%02x".format(it) }
    }

    @Synchronized
    fun read(key: String): String? = runCatching {
        val atomic = AtomicFile(cacheFile(key))
        atomic.openRead().use { input ->
            val output = ByteArrayOutputStream()
            val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
            var totalBytes = 0L
            while (true) {
                val read = input.read(buffer)
                if (read < 0) break
                totalBytes += read
                if (totalBytes > MAX_CACHE_BYTES) return@runCatching null
                output.write(buffer, 0, read)
            }
            output.toString(Charsets.UTF_8.name()).takeIf(String::isNotBlank)
        }
    }.getOrNull()

    @Synchronized
    fun write(key: String, locations: String) {
        val bytes = locations.toByteArray(Charsets.UTF_8)
        if (locations.isBlank() || bytes.size > MAX_CACHE_BYTES) return
        directory.mkdirs()
        val atomic = AtomicFile(cacheFile(key))
        val output = atomic.startWrite()
        try {
            output.write(bytes)
            output.flush()
            atomic.finishWrite(output)
        } catch (error: Exception) {
            atomic.failWrite(output)
            throw error
        }
        prune()
    }

    private fun prune() {
        directory.listFiles()
            ?.filter { file -> file.isFile && CACHE_FILE_NAME.matches(file.name) }
            ?.sortedByDescending(File::lastModified)
            ?.drop(MAX_CACHE_FILES)
            ?.forEach { file -> AtomicFile(file).delete() }
    }

    private fun cacheFile(key: String): File {
        require(key.matches(Regex("[a-f0-9]{64}")))
        return File(directory, "$key.locations")
    }

    companion object {
        private const val ENGINE_VERSION = "reader-v2-1"
        private const val CACHE_DIRECTORY = "reader-layout"
        private const val MAX_CACHE_FILES = 12
        private const val MAX_CACHE_BYTES = 5L * 1024L * 1024L
        private val CACHE_FILE_NAME = Regex("^[a-f0-9]{64}\\.locations$")
    }
}
