/**
 * DesfireProtocol.kt
 *
 * Copyright (C) 2011 Eric Butler
 * Copyright (C) 2019  <seil0@mosad.xyz>
 *
 * Authors:
 * Eric Butler <eric@codebutler.com>
 *
 * This program 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.
 *
 * This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.codebutler.farebot.card.desfire

import android.nfc.tech.IsoDep
import com.codebutler.farebot.Utils

import java.io.ByteArrayOutputStream
import java.io.IOException

import org.apache.commons.lang3.ArrayUtils
import kotlin.experimental.and

class DesfireProtocol(private val mTagTech: IsoDep) {

    @Suppress("unused")
    val manufacturingData: DesfireManufacturingData
        @Throws(DesfireException::class)
        get() {
            val respBuffer = sendRequest(GET_MANUFACTURING_DATA)

            if (respBuffer.size != 28)
                throw DesfireException("Invalid response")

            return DesfireManufacturingData(respBuffer)
        }

    @Suppress("unused")
    val appList: IntArray
        @Throws(DesfireException::class)
        get() {
            val appDirBuf = sendRequest(GET_APPLICATION_DIRECTORY)

            val appIds = IntArray(appDirBuf.size / 3)

            var app = 0
            while (app < appDirBuf.size) {
                val appId = ByteArray(3)
                System.arraycopy(appDirBuf, app, appId, 0, 3)

                appIds[app / 3] = Utils.byteArrayToInt(appId)
                app += 3
            }

            return appIds
        }

    val fileList: IntArray
        @Throws(DesfireException::class)
        get() {
            val buf = sendRequest(GET_FILES)
            val fileIds = IntArray(buf.size)
            for (x in buf.indices) {
                fileIds[x] = buf[x].toInt()
            }
            return fileIds
        }

    @Throws(DesfireException::class)
    fun selectApp(appId: Int) {
        val appIdBuff = ByteArray(3)
        appIdBuff[0] = (appId and 0xFF0000 shr 16).toByte()
        appIdBuff[1] = (appId and 0xFF00 shr 8).toByte()
        appIdBuff[2] = (appId and 0xFF).toByte()

        sendRequest(SELECT_APPLICATION, appIdBuff)
    }

    @Throws(DesfireException::class)
    fun getFileSettings(fileNo: Int): DesfireFileSettings {
        val data = sendRequest(GET_FILE_SETTINGS, byteArrayOf(fileNo.toByte()))
        return DesfireFileSettings.create(data)
    }

    @Suppress("unused")
    @Throws(DesfireException::class)
    fun readFile(fileNo: Int): ByteArray {
        return sendRequest(
            READ_DATA,
            byteArrayOf(
                fileNo.toByte(),
                0x0.toByte(),
                0x0.toByte(),
                0x0.toByte(),
                0x0.toByte(),
                0x0.toByte(),
                0x0.toByte()
            )
        )
    }

    @Suppress("unused")
    @Throws(DesfireException::class)
    fun readRecord(fileNum: Int): ByteArray {
        return sendRequest(
            READ_RECORD,
            byteArrayOf(
                fileNum.toByte(),
                0x0.toByte(),
                0x0.toByte(),
                0x0.toByte(),
                0x0.toByte(),
                0x0.toByte(),
                0x0.toByte()
            )
        )
    }

    @Throws(DesfireException::class)
    fun readValue(fileNum: Int): Int {
        val buf = sendRequest(READ_VALUE, byteArrayOf(fileNum.toByte()))
        ArrayUtils.reverse(buf)
        return Utils.byteArrayToInt(buf)
    }

    @Throws(DesfireException::class)
    private fun sendRequest(command: Byte, parameters: ByteArray? = null): ByteArray {
        val output = ByteArrayOutputStream()

        var recvBuffer: ByteArray
        try {
            recvBuffer = mTagTech.transceive(wrapMessage(command, parameters))
        } catch (e: IOException) {
            throw DesfireException(e)
        }

        while (true) {
            if (recvBuffer[recvBuffer.size - 2] != 0x91.toByte())
                throw DesfireException("Invalid response")

            output.write(recvBuffer, 0, recvBuffer.size - 2)

            val status = recvBuffer[recvBuffer.size - 1]
            if (status == OPERATION_OK) {
                break
            } else if (status == ADDITIONAL_FRAME) {
                try {
                    recvBuffer = mTagTech.transceive(wrapMessage(GET_ADDITIONAL_FRAME, null))
                } catch (e: IOException) {
                    throw DesfireException(e)
                }

            } else if (status == PERMISSION_DENIED) {
                throw DesfireException("Permission denied")
            } else {
                throw DesfireException("Unknown status code: " + Integer.toHexString((status and 0xFF.toByte()).toInt()))
            }
        }

        return output.toByteArray()
    }

    @Throws(DesfireException::class)
    private fun wrapMessage(command: Byte, parameters: ByteArray?): ByteArray {
        val stream = ByteArrayOutputStream()

        stream.write(0x90.toByte().toInt())
        stream.write(command.toInt())
        stream.write(0x00.toByte().toInt())
        stream.write(0x00.toByte().toInt())
        if (parameters != null) {
            stream.write(parameters.size.toByte().toInt())
            try {
                stream.write(parameters)
            } catch (e: IOException) {
                throw DesfireException(e)
            }

        }
        stream.write(0x00.toByte().toInt())

        return stream.toByteArray()
    }

    companion object {
        /* Commands */
        internal const val GET_MANUFACTURING_DATA = 0x60.toByte()
        internal const val GET_APPLICATION_DIRECTORY = 0x6A.toByte()
        internal const val GET_ADDITIONAL_FRAME = 0xAF.toByte()
        internal const val SELECT_APPLICATION = 0x5A.toByte()
        internal const val READ_DATA = 0xBD.toByte()
        internal const val READ_RECORD = 0xBB.toByte()
        internal const val READ_VALUE = 0x6C.toByte()
        internal const val GET_FILES = 0x6F.toByte()
        internal const val GET_FILE_SETTINGS = 0xF5.toByte()

        /* Status codes */
        internal const val OPERATION_OK = 0x00.toByte()
        internal const val PERMISSION_DENIED = 0x9D.toByte()
        internal const val ADDITIONAL_FRAME = 0xAF.toByte()
    }

}