package org.openrs2.archive.cache.finder

import com.github.michaelbull.logging.InlineLogger
import com.google.common.io.ByteStreams
import com.google.common.io.LittleEndianDataInputStream
import org.openrs2.util.charset.Cp1252Charset
import java.io.Closeable
import java.io.EOFException
import java.io.IOException
import java.io.InputStream
import java.io.PushbackInputStream
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.attribute.BasicFileAttributeView
import java.nio.file.attribute.FileTime
import java.time.Instant

public class CacheFinderExtractor(
    input: InputStream
) : Closeable {
    private val pushbackInput = PushbackInputStream(input)
    private val input = LittleEndianDataInputStream(pushbackInput)

    private fun readTimestamp(): FileTime {
        val lo = input.readInt().toLong() and 0xFFFFFFFF
        val hi = input.readInt().toLong() and 0xFFFFFFFF

        val seconds = (((hi shl 32) or lo) / 10_000_000) - FILETIME_TO_UNIX_EPOCH

        return FileTime.from(Instant.ofEpochSecond(seconds, lo))
    }

    private fun readName(): String {
        val bytes = ByteArray(MAX_PATH)
        input.readFully(bytes)

        var len = bytes.size
        for ((i, b) in bytes.withIndex()) {
            if (b.toInt() == 0) {
                len = i
                break
            }
        }

        return String(bytes, 0, len, Cp1252Charset)
    }

    private fun peekUnsignedByte(): Int {
        val n = pushbackInput.read()
        pushbackInput.unread(n)
        return n
    }

    public fun extract(destination: Path) {
        val newVersion = peekUnsignedByte() == 0xFE
        if (newVersion) {
            val signature = input.readInt()
            if (signature != 0x435352FE) {
                throw IOException("Invalid signature")
            }
        }

        var readDirectoryPath = true
        var number = 0
        var directorySuffix: String? = null

        while (true) {
            if (newVersion && readDirectoryPath) {
                val len = try {
                    input.readInt()
                } catch (ex: EOFException) {
                    break
                }

                val bytes = ByteArray(len)
                input.readFully(bytes)

                val path = String(bytes, Cp1252Charset)
                logger.info { "Extracting $path" }

                readDirectoryPath = false
                directorySuffix = path.substring(path.lastIndexOf('\\') + 1)
                    .replace(INVALID_CHARS, "_")

                continue
            }

            if (peekUnsignedByte() == 0xFF) {
                input.skipBytes(1)
                readDirectoryPath = true
                number++
                continue
            }

            val attributes = try {
                input.readInt()
            } catch (ex: EOFException) {
                break
            }

            val btime = readTimestamp()
            val atime = readTimestamp()
            val mtime = readTimestamp()

            val sizeHi = input.readInt().toLong() and 0xFFFFFFFF
            val sizeLo = input.readInt().toLong() and 0xFFFFFFFF
            val size = (sizeHi shl 32) or sizeLo

            input.skipBytes(8) // reserved

            val name = readName()

            input.skipBytes(14) // alternate name
            input.skipBytes(2) // padding

            val dir = if (directorySuffix != null) {
                destination.resolve("cache${number}_$directorySuffix")
            } else {
                destination.resolve("cache$number")
            }

            Files.createDirectories(dir)

            if ((attributes and FILE_ATTRIBUTE_DIRECTORY) == 0) {
                val file = dir.resolve(name)

                Files.newOutputStream(file).use { output ->
                    ByteStreams.copy(ByteStreams.limit(input, size), output)
                }

                val view = Files.getFileAttributeView(file, BasicFileAttributeView::class.java)
                view.setTimes(mtime, atime, btime)
            }
        }
    }

    override fun close() {
        input.close()
    }

    private companion object {
        private const val FILETIME_TO_UNIX_EPOCH: Long = 11644473600
        private const val MAX_PATH = 260
        private const val FILE_ATTRIBUTE_DIRECTORY = 0x10
        private val INVALID_CHARS = Regex("[^A-Za-z0-9-]")

        private val logger = InlineLogger()
    }
}
