package invalid.lena.rsend

import android.content.Context
import java.io.File
import java.io.FileOutputStream
import java.io.RandomAccessFile
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale

// SyncLog is a plain-text, append-only log of sync runs, capped in size. It is
// what the user reads to see what rsend did and to debug failures.
class SyncLog(ctx: Context) {

    val file: File = File(ctx.filesDir, "sync.log")
    private val previous: File = File(ctx.filesDir, "sync.log.1")
    private val fmt = SimpleDateFormat("MM-dd HH:mm:ss", Locale.US)

    fun line(s: String) {
        synchronized(lock) {
            val clean = cleanLine(s)
            val entry = "${fmt.format(Date())} $clean\n".toByteArray(Charsets.UTF_8)
            if (file.exists() && file.length() + entry.size > MAX_BYTES) rotate()
            FileOutputStream(file, true).use { it.write(entry) }
        }
    }

    // Called once at the start of a run; line() also enforces the cap as the
    // run goes, so a single long run cannot grow without bound.
    fun rotateIfBig() {
        synchronized(lock) {
            if (file.exists() && file.length() >= MAX_BYTES) rotate()
        }
    }

    // Roll over rather than truncate. Truncating destroyed every earlier run,
    // and a run that outgrew the cap destroyed its own output, including the
    // record of what it had just deleted on the server.
    private fun rotate() {
        if (previous.exists() && !previous.delete()) {
            throw IllegalStateException("could not remove ${previous.name}")
        }
        if (file.exists() && !file.renameTo(previous)) {
            throw IllegalStateException("could not rotate ${file.name}")
        }
    }

    // Both halves, oldest first. Showing only the current file would hide
    // everything a rotation moved aside, which is where a long run's earliest
    // and most interesting lines end up.
    fun text(): String {
        synchronized(lock) {
            val current = tail(file, DISPLAY_BYTES)
            val left = DISPLAY_BYTES - current.bytes.size
            val old = if (left > 0) tail(previous, left) else Tail(ByteArray(0), previous.exists())
            val omitted = current.omitted || old.omitted
            val bytes = old.bytes + current.bytes
            val text = String(skipUtf8Continuation(bytes), Charsets.UTF_8)
            return if (omitted) "[older log omitted]\n$text" else text
        }
    }

    // Clear both halves, or Clear would leave the rotated one on screen.
    fun clear() {
        synchronized(lock) {
            if (previous.exists() && !previous.delete()) {
                throw IllegalStateException("could not remove ${previous.name}")
            }
            FileOutputStream(file, false).use { }
        }
    }

    companion object {
        private const val MAX_BYTES = 512L * 1024
        private const val DISPLAY_BYTES = 64 * 1024
        private const val MAX_LINE_CHARS = 4_000
        private val lock = Any()

        internal fun cleanLine(value: String): String = buildString(minOf(value.length, MAX_LINE_CHARS + 20)) {
            for (c in value) {
                when (c) {
                    '\n' -> append("\\n")
                    '\r' -> append("\\r")
                    else -> if (unsafeDisplayCharacter(c)) {
                        append("\\u")
                        append(c.code.toString(16).padStart(4, '0'))
                    } else {
                        append(c)
                    }
                }
                if (length >= MAX_LINE_CHARS) {
                    append(" [truncated]")
                    break
                }
            }
        }

        private fun unsafeDisplayCharacter(c: Char): Boolean {
            val type = Character.getType(c)
            return c.isISOControl() || type == Character.FORMAT.toInt() ||
                type == Character.LINE_SEPARATOR.toInt() || type == Character.PARAGRAPH_SEPARATOR.toInt()
        }
    }

    private data class Tail(val bytes: ByteArray, val omitted: Boolean)

    private fun tail(source: File, limit: Int): Tail {
        if (!source.exists() || limit <= 0) return Tail(ByteArray(0), source.exists() && source.length() > 0)
        val length = source.length()
        val count = minOf(length, limit.toLong()).toInt()
        val bytes = ByteArray(count)
        RandomAccessFile(source, "r").use {
            it.seek(length - count)
            it.readFully(bytes)
        }
        return Tail(bytes, length > count)
    }

    private fun skipUtf8Continuation(bytes: ByteArray): ByteArray {
        var first = 0
        while (first < bytes.size && bytes[first].toInt() and 0xc0 == 0x80) first++
        return if (first == 0) bytes else bytes.copyOfRange(first, bytes.size)
    }
}
