package org.openrs2.cache

import io.netty.buffer.ByteBuf
import io.netty.buffer.ByteBufUtil
import org.openrs2.buffer.readUnsignedIntSmart
import org.openrs2.buffer.writeUnsignedIntSmart
import org.openrs2.crypto.Whirlpool

public class Js5Index(
    public var protocol: Js5Protocol,
    public var version: Int = 0,
    public var hasNames: Boolean = false,
    public var hasDigests: Boolean = false,
    public var hasLengths: Boolean = false,
    public var hasUncompressedChecksums: Boolean = false
) : MutableNamedEntryCollection<Js5Index.MutableGroup>(::MutableGroup) {
    public interface Group<out T : File> : NamedEntryCollection<T>, NamedEntry {
        public val version: Int
        public val checksum: Int
        public val uncompressedChecksum: Int
        public val length: Int
        public val uncompressedLength: Int
    }

    public class MutableGroup internal constructor(
        parent: MutableNamedEntryCollection<MutableGroup>,
        override val id: Int
    ) : MutableNamedEntryCollection<MutableFile>(::MutableFile), MutableNamedEntry, Group<MutableFile> {
        private var parent: MutableNamedEntryCollection<MutableGroup>? = parent
        public override var version: Int = 0
        public override var checksum: Int = 0
        public override var uncompressedChecksum: Int = 0
        public override var length: Int = 0
        public override var uncompressedLength: Int = 0

        public override var nameHash: Int = -1
            set(value) {
                parent?.rename(id, field, value)
                field = value
            }

        public var digest: ByteArray? = null
            set(value) {
                require(value == null || value.size == Whirlpool.DIGESTBYTES)
                field = value
            }

        override fun remove() {
            parent?.remove(this)
            parent = null
        }

        override fun equals(other: Any?): Boolean {
            if (this === other) return true
            if (javaClass != other?.javaClass) return false
            if (!super.equals(other)) return false

            other as MutableGroup

            if (id != other.id) return false
            if (version != other.version) return false
            if (checksum != other.checksum) return false
            if (uncompressedChecksum != other.uncompressedChecksum) return false
            if (length != other.length) return false
            if (uncompressedLength != other.uncompressedLength) return false
            if (nameHash != other.nameHash) return false
            if (digest != null) {
                if (other.digest == null) return false
                if (!digest.contentEquals(other.digest)) return false
            } else if (other.digest != null) return false

            return true
        }

        override fun hashCode(): Int {
            var result = super.hashCode()
            result = 31 * result + id
            result = 31 * result + version
            result = 31 * result + checksum
            result = 31 * result + uncompressedChecksum
            result = 31 * result + length
            result = 31 * result + uncompressedLength
            result = 31 * result + nameHash
            result = 31 * result + (digest?.contentHashCode() ?: 0)
            return result
        }

        override fun toString(): String {
            val digest = digest
            val hex = if (digest != null) {
                ByteBufUtil.hexDump(digest)
            } else {
                "null"
            }
            return "Group{id=$id, nameHash=$nameHash, version=$version, checksum=$checksum, " +
                "uncompressedChecksum=$uncompressedChecksum, length=$length, uncompressedLength=$uncompressedLength, " +
                "digest=$hex, size=$size, capacity=$capacity}"
        }
    }

    public interface File : NamedEntry

    public class MutableFile internal constructor(
        parent: MutableNamedEntryCollection<MutableFile>,
        override val id: Int
    ) : MutableNamedEntry, File {
        private var parent: MutableNamedEntryCollection<MutableFile>? = parent

        public override var nameHash: Int = -1
            set(value) {
                parent?.rename(id, field, value)
                field = value
            }

        override fun remove() {
            parent?.remove(this)
            parent = null
        }

        override fun equals(other: Any?): Boolean {
            if (this === other) return true
            if (javaClass != other?.javaClass) return false

            other as MutableFile

            if (id != other.id) return false
            if (nameHash != other.nameHash) return false

            return true
        }

        override fun hashCode(): Int {
            var result = id
            result = 31 * result + nameHash
            return result
        }

        override fun toString(): String {
            return "File{id=$id, nameHash=$nameHash}"
        }
    }

    public fun write(buf: ByteBuf) {
        val writeFunc = if (protocol >= Js5Protocol.SMART) {
            buf::writeUnsignedIntSmart
        } else {
            { value ->
                check(value in 0..65535) {
                    "$value outside of valid non-SMART range"
                }
                buf.writeShort(value)
            }
        }

        buf.writeByte(protocol.id)

        if (protocol >= Js5Protocol.VERSIONED) {
            buf.writeInt(version)
        }

        var flags = 0
        if (hasNames) {
            flags = flags or FLAG_NAMES
        }
        if (hasDigests) {
            flags = flags or FLAG_DIGESTS
        }
        if (hasLengths) {
            flags = flags or FLAG_LENGTHS
        }
        if (hasUncompressedChecksums) {
            flags = flags or FLAG_UNCOMPRESSED_CHECKSUMS
        }
        buf.writeByte(flags)

        writeFunc(size)

        var prevGroupId = 0
        for (group in this) {
            writeFunc(group.id - prevGroupId)
            prevGroupId = group.id
        }

        if (hasNames) {
            for (group in this) {
                buf.writeInt(group.nameHash)
            }
        }

        for (group in this) {
            buf.writeInt(group.checksum)
        }

        if (hasUncompressedChecksums) {
            for (group in this) {
                buf.writeInt(group.uncompressedChecksum)
            }
        }

        if (hasDigests) {
            for (group in this) {
                val digest = group.digest
                if (digest != null) {
                    buf.writeBytes(digest)
                } else {
                    buf.writeZero(Whirlpool.DIGESTBYTES)
                }
            }
        }

        if (hasLengths) {
            for (group in this) {
                buf.writeInt(group.length)
                buf.writeInt(group.uncompressedLength)
            }
        }

        for (group in this) {
            buf.writeInt(group.version)
        }

        for (group in this) {
            writeFunc(group.size)
        }

        for (group in this) {
            var prevFileId = 0
            for (file in group) {
                writeFunc(file.id - prevFileId)
                prevFileId = file.id
            }
        }

        if (hasNames) {
            for (group in this) {
                for (file in group) {
                    buf.writeInt(file.nameHash)
                }
            }
        }
    }

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false
        if (!super.equals(other)) return false

        other as Js5Index

        if (protocol != other.protocol) return false
        if (version != other.version) return false
        if (hasNames != other.hasNames) return false
        if (hasDigests != other.hasDigests) return false
        if (hasLengths != other.hasLengths) return false
        if (hasUncompressedChecksums != other.hasUncompressedChecksums) return false

        return true
    }

    override fun hashCode(): Int {
        var result = super.hashCode()
        result = 31 * result + protocol.hashCode()
        result = 31 * result + version
        result = 31 * result + hasNames.hashCode()
        result = 31 * result + hasDigests.hashCode()
        result = 31 * result + hasLengths.hashCode()
        result = 31 * result + hasUncompressedChecksums.hashCode()
        return result
    }

    override fun toString(): String {
        return "Js5Index{protocol=$protocol, version=$version, hasNames=$hasNames, hasDigests=$hasDigests, " +
            "hasLengths=$hasLengths, hasUncompressedChecksums=$hasUncompressedChecksums, size=$size, " +
            "capacity=$capacity}"
    }

    public companion object {
        private const val FLAG_NAMES = 0x01
        private const val FLAG_DIGESTS = 0x02
        private const val FLAG_LENGTHS = 0x04
        private const val FLAG_UNCOMPRESSED_CHECKSUMS = 0x08

        @JvmStatic
        public fun read(buf: ByteBuf): Js5Index {
            val number = buf.readUnsignedByte().toInt()
            val protocol = Js5Protocol.fromId(number)
            require(protocol != null) {
                "Unsupported JS5 protocol number: $number"
            }

            val readFunc = if (protocol >= Js5Protocol.SMART) {
                buf::readUnsignedIntSmart
            } else {
                buf::readUnsignedShort
            }

            val version = if (protocol >= Js5Protocol.VERSIONED) {
                buf.readInt()
            } else {
                0
            }
            val flags = buf.readUnsignedByte().toInt()
            val size = readFunc()

            val index = Js5Index(
                protocol,
                version,
                hasNames = flags and FLAG_NAMES != 0,
                hasDigests = flags and FLAG_DIGESTS != 0,
                hasLengths = flags and FLAG_LENGTHS != 0,
                hasUncompressedChecksums = flags and FLAG_UNCOMPRESSED_CHECKSUMS != 0
            )

            var prevGroupId = 0
            for (i in 0 until size) {
                prevGroupId += readFunc()
                index.createOrGet(prevGroupId)
            }

            if (index.hasNames) {
                for (group in index) {
                    group.nameHash = buf.readInt()
                }
            }

            for (group in index) {
                group.checksum = buf.readInt()
            }

            if (index.hasUncompressedChecksums) {
                for (group in index) {
                    group.uncompressedChecksum = buf.readInt()
                }
            }

            if (index.hasDigests) {
                for (group in index) {
                    val digest = ByteArray(Whirlpool.DIGESTBYTES)
                    buf.readBytes(digest)
                    group.digest = digest
                }
            }

            if (index.hasLengths) {
                for (group in index) {
                    group.length = buf.readInt()
                    group.uncompressedLength = buf.readInt()
                }
            }

            for (group in index) {
                group.version = buf.readInt()
            }

            val groupSizes = IntArray(size) {
                readFunc()
            }

            for ((i, group) in index.withIndex()) {
                val groupSize = groupSizes[i]

                var prevFileId = 0
                for (j in 0 until groupSize) {
                    prevFileId += readFunc()
                    group.createOrGet(prevFileId)
                }
            }

            if (index.hasNames) {
                for (group in index) {
                    for (file in group) {
                        file.nameHash = buf.readInt()
                    }
                }
            }

            return index
        }
    }
}
