package ac.mdiq.podcini.storage.utils

import ac.mdiq.podcini.utils.Logs
import java.io.BufferedInputStream
import java.io.BufferedOutputStream
import java.io.DataInputStream
import java.io.DataOutputStream
import java.io.EOFException
import java.io.File
import java.io.FileInputStream
import java.io.FileNotFoundException
import java.io.FileOutputStream
import java.io.IOException
import java.io.RandomAccessFile
import java.nio.ByteBuffer
import java.nio.ByteOrder

// converted to Kotlin from the java file: https://gist.github.com/DrustZ/d3d3fc8fcc1067433db4dd3079f8d187
private const val TAG = "AudioMediaTools"

// TODO: need to accept uri rather than path
fun mergeAudios(selection: Array<String>, outpath: String?, callback: OperationCallbacks?) {
    var RECORDER_SAMPLERATE = 0
    try {
        val amplifyOutputStream = DataOutputStream(BufferedOutputStream(FileOutputStream(outpath)))
        val mergeFilesStream = arrayOfNulls<DataInputStream>(selection.size)
        val sizes = LongArray(selection.size)
        for (i in selection.indices) {
            val file = File(selection[i])
            sizes[i] = (file.length() - 44) / 2
        }
        for (i in selection.indices) {
            mergeFilesStream[i] = DataInputStream(BufferedInputStream(FileInputStream(selection[i])))
            if (i == selection.size - 1) {
                mergeFilesStream[i]!!.skip(24)
                val sampleRt = ByteArray(4)
                mergeFilesStream[i]!!.read(sampleRt)
                val bbInt = ByteBuffer.wrap(sampleRt).order(ByteOrder.LITTLE_ENDIAN)
                RECORDER_SAMPLERATE = bbInt.getInt()
                mergeFilesStream[i]!!.skip(16)
            } else mergeFilesStream[i]!!.skip(44)
        }

        for (b in selection.indices) {
            for (i in 0 until sizes[b].toInt()) {
                val dataBytes = ByteArray(2)
                try {
                    dataBytes[0] = mergeFilesStream[b]!!.readByte()
                    dataBytes[1] = mergeFilesStream[b]!!.readByte()
                } catch (e: EOFException) { amplifyOutputStream.close() }

                val dataInShort = ByteBuffer.wrap(dataBytes).order(ByteOrder.LITTLE_ENDIAN).getShort()
                val dataInFloat = dataInShort.toFloat() / 37268.0f

                val outputSample = (dataInFloat * 37268.0f).toInt().toShort()
                val dataFin = ByteArray(2)
                dataFin[0] = (outputSample.toInt() and 0xff).toByte()
                dataFin[1] = ((outputSample.toInt() shr 8) and 0xff).toByte()
                amplifyOutputStream.write(dataFin, 0, 2)
            }
        }
        amplifyOutputStream.close()
        for (i in selection.indices) mergeFilesStream[i]!!.close()
    } catch (e: FileNotFoundException) {
        callback?.onAudioOperationError(e)
        Logs(TAG, e)
    } catch (e: IOException) {
        callback?.onAudioOperationError(e)
        Logs(TAG, e)
    }
    var size: Long = 0
    try {
        val fileSize = FileInputStream(outpath)
        size = fileSize.channel.size()
        fileSize.close()
    } catch (e1: FileNotFoundException) { Logs(TAG, e1)
    } catch (e: IOException) { Logs(TAG, e) }

    val RECORDER_BPP = 16

    val datasize = size + 36
    val byteRate = ((RECORDER_BPP * RECORDER_SAMPLERATE) / 8).toLong()
    val longSampleRate = RECORDER_SAMPLERATE.toLong()
    val header = ByteArray(44)

    header[0] = 'R'.code.toByte() // RIFF/WAVE header
    header[1] = 'I'.code.toByte()
    header[2] = 'F'.code.toByte()
    header[3] = 'F'.code.toByte()
    header[4] = (datasize and 0xffL).toByte()
    header[5] = ((datasize shr 8) and 0xffL).toByte()
    header[6] = ((datasize shr 16) and 0xffL).toByte()
    header[7] = ((datasize shr 24) and 0xffL).toByte()
    header[8] = 'W'.code.toByte()
    header[9] = 'A'.code.toByte()
    header[10] = 'V'.code.toByte()
    header[11] = 'E'.code.toByte()
    header[12] = 'f'.code.toByte() // 'fmt ' chunk
    header[13] = 'm'.code.toByte()
    header[14] = 't'.code.toByte()
    header[15] = ' '.code.toByte()
    header[16] = 16 // 4 bytes: size of 'fmt ' chunk
    header[17] = 0
    header[18] = 0
    header[19] = 0
    header[20] = 1 // format = 1
    header[21] = 0
    header[22] = 1.toByte()
    header[23] = 0
    header[24] = (longSampleRate and 0xffL).toByte()
    header[25] = ((longSampleRate shr 8) and 0xffL).toByte()
    header[26] = ((longSampleRate shr 16) and 0xffL).toByte()
    header[27] = ((longSampleRate shr 24) and 0xffL).toByte()
    header[28] = (byteRate and 0xffL).toByte()
    header[29] = ((byteRate shr 8) and 0xffL).toByte()
    header[30] = ((byteRate shr 16) and 0xffL).toByte()
    header[31] = ((byteRate shr 24) and 0xffL).toByte()
    header[32] = ((RECORDER_BPP) / 8).toByte() // block align
    header[33] = 0
    header[34] = RECORDER_BPP.toByte() // bits per sample
    header[35] = 0
    header[36] = 'd'.code.toByte()
    header[37] = 'a'.code.toByte()
    header[38] = 't'.code.toByte()
    header[39] = 'a'.code.toByte()
    header[40] = (size and 0xffL).toByte()
    header[41] = ((size shr 8) and 0xffL).toByte()
    header[42] = ((size shr 16) and 0xffL).toByte()
    header[43] = ((size shr 24) and 0xffL).toByte()

    // out.write(header, 0, 44);
    try {
        val rFile = RandomAccessFile(outpath, "rw")
        rFile.seek(0)
        rFile.write(header)
        rFile.close()
        callback?.onAudioOperationFinished()
    } catch (e: FileNotFoundException) {
        // TODO Auto-generated catch block
        callback?.onAudioOperationError(e)
        Logs(TAG, e)
    } catch (e: IOException) {
        // TODO Auto-generated catch block
        callback?.onAudioOperationError(e)
        Logs(TAG, e)
    }
}

@Throws(IOException::class)
fun rawToWave(rawfn: String, wavefn: String) {
    val rawFile = File(rawfn)
    val waveFile = File(wavefn)
    val rawData = ByteArray(rawFile.length().toInt())
    var input: DataInputStream? = null
    try {
        input = DataInputStream(FileInputStream(rawFile))
        input.read(rawData)
    } finally {
        input?.close()
    }

    val SAMPLE_RATE = 24000

    var output: DataOutputStream? = null
    try {
        output = DataOutputStream(FileOutputStream(waveFile))
        // WAVE header
        // see http://ccrma.stanford.edu/courses/422/projects/WaveFormat/
        writeString(output, "RIFF") // chunk id
        writeInt(output, 36 + rawData.size) // chunk size
        writeString(output, "WAVE") // format
        writeString(output, "fmt ") // subchunk 1 id
        writeInt(output, 16) // subchunk 1 size
        writeShort(output, 1.toShort()) // audio format (1 = PCM)
        writeShort(output, 1.toShort()) // number of channels
        writeInt(output, SAMPLE_RATE) // sample rate
        writeInt(output, SAMPLE_RATE * 2) // byte rate
        writeShort(output, 2.toShort()) // block align
        writeShort(output, 16.toShort()) // bits per sample
        writeString(output, "data") // subchunk 2 id
        writeInt(output, rawData.size) // subchunk 2 size
        // Audio data (conversion big endian -> little endian)
        val shorts = ShortArray(rawData.size / 2)
        ByteBuffer.wrap(rawData).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer()[shorts]
        val bytes = ByteBuffer.allocate(shorts.size * 2)
        for (s in shorts) {
            bytes.putShort(s)
        }

        output.write(fullyReadFileToBytes(rawFile))
    } finally {
        output?.close()
    }
}

@Throws(IOException::class)
private fun fullyReadFileToBytes(f: File): ByteArray {
    val size = f.length().toInt()
    val bytes = ByteArray(size)
    val tmpBuff = ByteArray(size)
    val fis = FileInputStream(f)
    try {
        var read = fis.read(bytes, 0, size)
        if (read < size) {
            var remain = size - read
            while (remain > 0) {
                read = fis.read(tmpBuff, 0, remain)
                System.arraycopy(tmpBuff, 0, bytes, size - remain, read)
                remain -= read
            }
        }
    } catch (e: IOException) { throw e
    } finally { fis.close() }
    return bytes
}

@Throws(IOException::class)
private fun writeInt(output: DataOutputStream, value: Int) {
    output.write(value shr 0)
    output.write(value shr 8)
    output.write(value shr 16)
    output.write(value shr 24)
}

@Throws(IOException::class)
private fun writeShort(output: DataOutputStream, value: Short) {
    output.write(value.toInt() shr 0)
    output.write(value.toInt() shr 8)
}

@Throws(IOException::class)
private fun writeString(output: DataOutputStream, value: String) {
    for (element in value) output.write(element.code)
}

interface OperationCallbacks {
    fun onAudioOperationFinished()
    fun onAudioOperationError(e: Exception?)
}
