/*
 * Copyright 2019 Jeremy Jamet / Kunzisoft.
 *
 * This file is part of KeePassDX.
 *
 *  KeePassDX is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  KeePassDX is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with KeePassDX.  If not, see <http://www.gnu.org/licenses/>.
 *
 */
package com.kunzisoft.keepass.utils

import com.kunzisoft.keepass.database.element.DateInstant
import org.joda.time.DateTime
import org.joda.time.Instant
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.nio.charset.Charset
import java.util.*

/**
 * Read all data of stream and invoke [readBytes] each time the buffer is full or no more data to read.
 */
@Throws(IOException::class)
fun InputStream.readAllBytes(bufferSize: Int = DEFAULT_BUFFER_SIZE,
                             cancelCondition: ()-> Boolean = { false },
                             readBytes: (bytesRead: ByteArray) -> Unit) {
    val buffer = ByteArray(bufferSize)
    var read = 0
    while (read != -1 && !cancelCondition()) {
        read = this.read(buffer, 0, buffer.size)
        if (read != -1) {
            val optimizedBuffer: ByteArray = if (buffer.size == read) {
                buffer
            } else {
                buffer.copyOf(read)
            }
            readBytes.invoke(optimizedBuffer)
        }
    }
}

/**
 * Read number of bytes defined by [length] and invoke [readBytes] each time the buffer is full or no more data to read.
 */
@Throws(IOException::class)
fun InputStream.readBytes(length: Int, bufferSize: Int = DEFAULT_BUFFER_SIZE,
                          readBytes: (bytesRead: ByteArray) -> Unit) {
    var bufferLength = bufferSize
    var buffer = ByteArray(bufferLength)

    var offset = 0
    var read = 0
    while (offset < length && read != -1) {

        // To reduce the buffer for the last bytes reads
        if (length - offset < bufferLength) {
            bufferLength = length - offset
            buffer = ByteArray(bufferLength)
        }
        read = this.read(buffer, 0, bufferLength)

        // To get only the bytes read
        val optimizedBuffer: ByteArray = if (read >= 0 && buffer.size > read) {
            buffer.copyOf(read)
        } else {
            buffer
        }
        readBytes.invoke(optimizedBuffer)
        offset += read
    }
}

/**
 *  Read a 32-bit value and return it as a long, so that it can
 *  be interpreted as an unsigned integer.
 */
@Throws(IOException::class)
fun InputStream.readBytes4ToUInt(): UnsignedInt {
    return bytes4ToUInt(readBytesLength(4))
}

@Throws(IOException::class)
fun InputStream.readBytes2ToUShort(): Int {
    return bytes2ToUShort(readBytesLength(2))
}

@Throws(IOException::class)
fun InputStream.readBytes5ToDate(): DateInstant {
    return bytes5ToDate(readBytesLength(5))
}

@Throws(IOException::class)
fun InputStream.readBytes16ToUuid(): UUID {
    return bytes16ToUuid(readBytesLength(16))
}

@Throws(IOException::class)
fun InputStream.readBytesToString(length: Int, replaceCRLF: Boolean = true): String {
    return bytesToString(this.readBytesLength(length), replaceCRLF)
}

@Throws(IOException::class)
fun InputStream.readBytesLength(length: Int): ByteArray {
    val buf = ByteArray(length)
    // WARNING this.read(buf, 0, length) Doesn't work
    for (i in 0 until length) {
        buf[i] = this.read().toByte()
    }
    return buf
}

@Throws(IOException::class)
fun OutputStream.write4BytesUInt(value: UnsignedInt) {
    this.write(uIntTo4Bytes(value))
}

@Throws(IOException::class)
fun OutputStream.writeBooleanByte(value: Boolean) {
    this.writeByte(if (value) 1.toByte() else 0.toByte())
}

@Throws(IOException::class)
fun OutputStream.writeByte(byte: Byte) {
    this.write(byte.toInt())
}

@Throws(IOException::class)
fun OutputStream.write8BytesLong(value: Long) {
    this.write(longTo8Bytes(value))
}

@Throws(IOException::class)
fun OutputStream.write8BytesLong(value: UnsignedLong) {
    this.write(uLongTo8Bytes(value))
}

@Throws(IOException::class)
fun OutputStream.write2BytesUShort(value: Int) {
    this.write(uShortTo2Bytes(value))
}

/**
 * Read an unsigned 16-bit value.
 */
fun bytes2ToUShort(buf: ByteArray): Int {
    return ((buf[0].toInt() and 0xFF)
            + (buf[1].toInt() and 0xFF shl 8))
}

/**
 * Read a 64 bit to unsigned long
 */
fun bytes64ToULong(buf: ByteArray): UnsignedLong {
    return UnsignedLong((buf[0].toLong() and 0xFF)
            + (buf[1].toLong() and 0xFF shl 8)
            + (buf[2].toLong() and 0xFF shl 16)
            + (buf[3].toLong() and 0xFF shl 24)
            + (buf[4].toLong() and 0xFF shl 32)
            + (buf[5].toLong() and 0xFF shl 40)
            + (buf[6].toLong() and 0xFF shl 48)
            + (buf[7].toLong() and 0xFF shl 56))
}

/**
 * Read a 64 bit long
 */
fun bytes64ToLong(buf: ByteArray): Long {
    return ((buf[0].toLong() and 0xFF)
            + (buf[1].toLong() and 0xFF shl 8)
            + (buf[2].toLong() and 0xFF shl 16)
            + (buf[3].toLong() and 0xFF shl 24)
            + (buf[4].toLong() and 0xFF shl 32)
            + (buf[5].toLong() and 0xFF shl 40)
            + (buf[6].toLong() and 0xFF shl 48)
            + (buf[7].toLong() and 0xFF shl 56))
}

/**
 * Read a 32-bit value.
 */
fun bytes4ToUInt(buf: ByteArray): UnsignedInt {
    return UnsignedInt((buf[0].toInt() and 0xFF)
            + (buf[1].toInt() and 0xFF shl 8)
            + (buf[2].toInt() and 0xFF shl 16)
            + (buf[3].toInt() and 0xFF shl 24))
}

fun bytes16ToUuid(buf: ByteArray): UUID {
    var lsb: Long = 0
    for (i in 15 downTo 8) {
        lsb = lsb shl 8 or (buf[i].toLong() and 0xff)
    }

    var msb: Long = 0
    for (i in 7 downTo 0) {
        msb = msb shl 8 or (buf[i].toLong() and 0xff)
    }

    return UUID(msb, lsb)
}

/**
 * Unpack date from 5 byte format. The five bytes at 'offset' are unpacked
 * to a java.util.Date instance.
 */
fun bytes5ToDate(buf: ByteArray): DateInstant {
    val dateSize = 5
    val cDate = ByteArray(dateSize)
    System.arraycopy(buf, 0, cDate, 0, dateSize)

    val readOffset = 0
    val dw1 = cDate[readOffset].toInt() and 0xFF
    val dw2 = cDate[readOffset + 1].toInt() and 0xFF
    val dw3 = cDate[readOffset + 2].toInt() and 0xFF
    val dw4 = cDate[readOffset + 3].toInt() and 0xFF
    val dw5 = cDate[readOffset + 4].toInt() and 0xFF

    // Unpack 5 byte structure to date and time
    val year = dw1 shl 6 or (dw2 shr 2)
    val month = dw2 and 0x00000003 shl 2 or (dw3 shr 6)

    val day = dw3 shr 1 and 0x0000001F
    val hour = dw3 and 0x00000001 shl 4 or (dw4 shr 4)
    val minute = dw4 and 0x0000000F shl 2 or (dw5 shr 6)
    val second = dw5 and 0x0000003F

    return DateInstant(Instant.ofEpochMilli(DateTime(
        year,
        month,
        day,
        hour,
        minute,
        second
    ).millis))
}


/**
 * Write an unsigned 16-bit value
 */
fun uShortTo2Bytes(value: Int): ByteArray {
    val buf = ByteArray(2)
    buf[0] = (value and 0x00FF).toByte()
    buf[1] = (value and 0xFF00 shr 8).toByte()
    return buf
}

/**
 * Write a 32-bit Int value.
 */
fun uIntTo4Bytes(value: UnsignedInt): ByteArray {
    val buf = ByteArray(4)
    for (i in 0 until 4) {
        buf[i] = (value.toKotlinInt().ushr(8 * i) and 0xFF).toByte()
    }
    return buf
}

fun uLongTo8Bytes(value: UnsignedLong): ByteArray {
    return longTo8Bytes(value.toKotlinLong())
}

fun longTo8Bytes(value: Long): ByteArray {
    val buf = ByteArray(8)
    for (i in 0 until 8) {
        buf[i] = (value.ushr(8 * i) and 0xFF).toByte()
    }
    return buf
}

fun uuidTo16Bytes(uuid: UUID): ByteArray {
    val buf = ByteArray(16)
    for (i in 0 until 8) {
        buf[i] = (uuid.mostSignificantBits.ushr(8 * i) and 0xFF).toByte()
    }
    for (i in 8 until 16) {
        buf[i] = (uuid.leastSignificantBits.ushr(8 * i) and 0xFF).toByte()
    }
    return buf
}

fun dateTo5Bytes(dateInstant: DateInstant): ByteArray {
    val year = dateInstant.getYear()
    val month = dateInstant.getMonth()
    val day = dateInstant.getDay()
    val hour = dateInstant.getHour()
    val minute = dateInstant.getMinute()
    val second = dateInstant.getSecond()

    val buf = ByteArray(5)
    buf[0] = UnsignedInt(year shr 6 and 0x0000003F).toKotlinByte()
    buf[1] = UnsignedInt(year and 0x0000003F shl 2 or (month shr 2 and 0x00000003)).toKotlinByte()
    buf[2] = (month and 0x00000003 shl 6
            or (day and 0x0000001F shl 1) or (hour shr 4 and 0x00000001)).toByte()
    buf[3] = (hour and 0x0000000F shl 4 or (minute shr 2 and 0x0000000F)).toByte()
    buf[4] = (minute and 0x00000003 shl 6 or (second and 0x0000003F)).toByte()

    return buf
}

private val defaultCharset = Charset.forName("UTF-8")

private val CRLFbuf = byteArrayOf(0x0D, 0x0A)
private val CRLF = String(CRLFbuf)
private val SEP = System.getProperty("line.separator")
private val REPLACE = SEP != CRLF

fun bytesToString(buf: ByteArray, replaceCRLF: Boolean = true): String {
    // length of null-terminated string (i.e. distance to null) within a byte buffer.
    var len = 0
    while (buf[len].toInt() != 0) {
        len++
    }
    // Get string
    var jstring = String(buf, 0, len, defaultCharset)
    if (replaceCRLF && REPLACE) {
        jstring = jstring.replace(CRLF, SEP!!)
    }
    return jstring
}

@Throws(IOException::class)
fun writeStringToStream(outputStream: OutputStream, string: String?): Int {
    var str = string
    if (str == null) {
        // Write out a null character
        outputStream.write(uIntTo4Bytes(UnsignedInt(1)))
        outputStream.write(0x00)
        return 0
    }

    if (REPLACE) {
        str = str.replace(SEP!!, CRLF)
    }

    val initial = str.toByteArray(defaultCharset)

    val length = initial.size + 1
    outputStream.write(uIntTo4Bytes(UnsignedInt(length)))
    outputStream.write(initial)
    outputStream.write(0x00)

    return length
}
