class ByteString(private val bytes: ByteArray) : Comparable<ByteString> {
    val length get() = bytes.size

    fun isEmpty() = bytes.isEmpty()

    operator fun plus(other: ByteString): ByteString = ByteString(bytes + other.bytes)

    operator fun plus(byte: Byte) = ByteString(bytes + byte)

    operator fun get(index: Int): Byte {
        require (index in 0 until length)
        return bytes[index]
    }

    fun toByteArray() = bytes

    fun copy() = ByteString(bytes.copyOf())

    override fun compareTo(other: ByteString) = this.toString().compareTo(other.toString())

    override fun equals(other: Any?): Boolean {
        if (other == null || other !is ByteString) return false
        return compareTo(other) == 0
    }

    override fun hashCode() = this.toString().hashCode()

    fun substring(startIndex: Int) = ByteString(bytes.sliceArray(startIndex until length))

    fun substring(startIndex: Int, endIndex: Int) =
        ByteString(bytes.sliceArray(startIndex until endIndex))

    fun replace(oldByte: Byte, newByte: Byte): ByteString {
        val ba = ByteArray(length) { if (bytes[it] == oldByte) newByte else bytes[it] }
        return ByteString(ba)
    }

    fun replace(oldValue: ByteString, newValue: ByteString) =
        this.toString().replace(oldValue.toString(), newValue.toString()).toByteString()

    override fun toString(): String {
        val chars = CharArray(length)
        for (i in 0 until length) {
            chars[i] = when (bytes[i]) {
                in 0..127  -> bytes[i].toChar()
                else       -> (256 + bytes[i]).toChar()
            }
        }
        return chars.joinToString("")
    }
}

fun String.toByteString(): ByteString {
    val bytes = ByteArray(this.length)
    for (i in 0 until this.length) {
        bytes[i] = when (this[i].toInt()) {
            in 0..127   -> this[i].toByte()
            in 128..255 -> (this[i] - 256).toByte()
            else        -> '?'.toByte()