package jp.juggler.subwaytooter.util

import android.content.ContentResolver
import android.content.Context
import android.media.MediaMetadataRetriever
import android.net.Uri
import jp.juggler.subwaytooter.api.entity.InstanceType
import jp.juggler.subwaytooter.api.entity.TootInstance
import jp.juggler.util.data.notEmpty
import jp.juggler.util.data.toByteArray
import jp.juggler.util.data.toLowerByteArray
import jp.juggler.util.log.LogCategory
import jp.juggler.util.media.bitmapMimeType

private val log = LogCategory("MimeTypeUtils")

const val MIME_TYPE_JPEG = "image/jpeg"
const val MIME_TYPE_PNG = "image/png"
const val MIME_TYPE_GIF = "image/gif"
const val MIME_TYPE_WEBP = "image/webp"

private val acceptableMimeTypes = HashSet<String>().apply {
    //
    add("image/jpeg")
    add("image/png")
    add("image/gif")
    //
    add("video/webm")
    add("video/mp4")
    add("video/quicktime")
    //
    add("audio/webm")
    add("audio/ogg")
    add("audio/mpeg")
    add("audio/mp3")
    add("audio/wav")
    add("audio/wave")
    add("audio/x-wav")
    add("audio/x-pn-wav")
    add("audio/flac")
    add("audio/x-flac")

    // https://github.com/tootsuite/mastodon/pull/11342
    add("audio/aac")
    add("audio/m4a")
    add("audio/3gpp")
}

private val acceptableMimeTypesPixelfed = HashSet<String>().apply {
    //
    add("image/jpeg")
    add("image/png")
    add("image/gif")
    //
    add("video/mp4")
    add("video/m4v")
}

private val imageHeaderList = listOf(
    Pair(
        "image/jpeg",
        intArrayOf(0xff, 0xd8, 0xff).toByteArray()
    ),
    Pair(
        "image/png",
        intArrayOf(0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A).toByteArray()
    ),
    Pair(
        "image/gif",
        "GIF".toByteArray(Charsets.UTF_8)
    ),
    Pair(
        "audio/wav",
        "RIFF".toByteArray(Charsets.UTF_8),
    ),
    Pair(
        "audio/ogg",
        "OggS".toByteArray(Charsets.UTF_8),
    ),
    Pair(
        "audio/flac",
        "fLaC".toByteArray(Charsets.UTF_8),
    ),
    Pair(
        "image/bmp",
        "BM".toByteArray(Charsets.UTF_8),
    ),
    Pair(
        "image/webp",
        "RIFF****WEBP".toByteArray(Charsets.UTF_8),
    ),
).sortedByDescending { it.second.size }

private val sig3gp = arrayOf(
    "3ge6",
    "3ge7",
    "3gg6",
    "3gp1",
    "3gp2",
    "3gp3",
    "3gp4",
    "3gp5",
    "3gp6",
    "3gp7",
    "3gr6",
    "3gr7",
    "3gs6",
    "3gs7",
    "kddi"
).map { it.toCharArray().toLowerByteArray() }

private val sigM4a = arrayOf(
    "M4A ",
    "M4B ",
    "M4P "
).map { it.toCharArray().toLowerByteArray() }

private const val BYTE_QUESTION = '?'.code.toByte()

private val sigFtyp = "ftyp".toCharArray().toLowerByteArray()

private fun matchSig(
    data: ByteArray,
    dataOffset: Int,
    sig: ByteArray,
    sigSize: Int = sig.size,
): Boolean {
    for (i in 0 until sigSize) {
        if (data[dataOffset + i] != sig[i]) return false
    }
    return true
}

private fun ByteArray.startWithWildcard(
    key: ByteArray,
    thisOffset: Int = 0,
    keyOffset: Int = 0,
    length: Int = key.size - keyOffset,
): Boolean {
    if (thisOffset + length > this.size || keyOffset + length > key.size) {
        return false
    }
    for (i in 0 until length) {
        val cThis = this[i + thisOffset]
        val cKey = key[i + keyOffset]
        if (cKey != BYTE_QUESTION && cKey != cThis) return false
    }
    return true
}

private fun findMimeTypeByFileHeader(
    contentResolver: ContentResolver,
    uri: Uri,
): String? {
    try {
        contentResolver.openInputStream(uri)?.use { inStream ->
            val data = ByteArray(65536)
            val nRead = inStream.read(data, 0, data.size)
            for (pair in imageHeaderList) {
                val type = pair.first
                val header = pair.second
                if (nRead >= header.size && data.startWithWildcard(header)) return type
            }

            // scan frame header
            for (i in 0 until nRead - 8) {

                if (!matchSig(data, i, sigFtyp)) continue

                // 3gpp check
                for (s in sig3gp) {
                    if (matchSig(data, i + 4, s)) return "audio/3gpp"
                }

                // m4a check
                for (s in sigM4a) {
                    if (matchSig(data, i + 4, s)) return "audio/m4a"
                }
            }

            // scan frame header
            loop@ for (i in 0 until nRead - 2) {

                // mpeg frame header
                val b0 = data[i].toInt() and 255
                if (b0 != 255) continue
                val b1 = data[i + 1].toInt() and 255
                if ((b1 and 0b11100000) != 0b11100000) continue

                val mpegVersionId = ((b1 shr 3) and 3)
                // 00 mpeg 2.5
                // 01 not used
                // 10 (mp3) mpeg 2 / (AAC) mpeg-4
                // 11 (mp3) mpeg 1 / (AAC) mpeg-2

                @Suppress("MoveVariableDeclarationIntoWhen")
                val mpegLayerId = ((b1 shr 1) and 3)
                // 00 (mp3)not used / (AAC) always 0
                // 01 (mp3)layer III
                // 10 (mp3)layer II
                // 11 (mp3)layer I

                when (mpegLayerId) {
                    0 -> when (mpegVersionId) {
                        2, 3 -> return "audio/aac"

                        else -> {
                        }
                    }

                    1 -> when (mpegVersionId) {
                        0, 2, 3 -> return "audio/mp3"

                        else -> {
                        }
                    }
                }
            }
        }
    } catch (ex: Throwable) {
        log.e(ex, "findMimeTypeByFileHeader failed.")
    }
    return null
}

/**
 * issueを抱えたmimeTypeなら真
 * - サーバ情報にある利用可能mimeTypeリストがアテにならん…
 */
private fun String.isProblematicImageType(instance: TootInstance) =
    when (instance.instanceType) {
        InstanceType.Mastodon -> when (this) {
            // https://github.com/mastodon/mastodon/issues/23588
            "image/heic", "image/heif" -> true
            // https://github.com/mastodon/mastodon/issues/20834
            "image/avif" -> true

            else -> false
        }

        InstanceType.Pixelfed -> when (this) {
            // Pixelfed は PC Web UI で画像を開くダイアログの時点でHEIC,HEIF,AVIF を選択できない
            "image/heic", "image/heif", "image/avif" -> true
            else -> false
        }
        // PleromaやMisskeyでの問題は調べてない
        else -> false
    }

/**
 * サーバに送信できるmimeTypeなら真
 */
fun String.mimeTypeIsSupported(instance: TootInstance) = when {
    isProblematicImageType(instance) -> false
    else -> instance.configuration
        ?.jsonObject("media_attachments")
        ?.jsonArray("supported_mime_types")
        ?.contains(this)
        ?: when (instance.instanceType) {
            InstanceType.Pixelfed -> acceptableMimeTypesPixelfed
            else -> acceptableMimeTypes
        }.contains(this)
}

fun Uri.resolveMimeType(mimeTypeArg: String?, context: Context): String? {

    // GIFはそのまま通す
    if (mimeTypeArg == MIME_TYPE_GIF) return MIME_TYPE_GIF

    // 動画ではないなら
    if (mimeTypeArg?.startsWith("video") != true) {
        // BitmapFactory で静止画の mimeType を調べる
        // image/j()pg だの image/j(e)pg だの、mime type を誤記するアプリがあまりに多い
        // application/octet-stream などが誤設定されてることもある
        bitmapMimeType(context.contentResolver)?.notEmpty()?.let { return it }
    }

    // MediaMetadataRetriever で動画/音声の  mimeType を調べる
    try {
        MediaMetadataRetriever().use { mmr ->
            mmr.setDataSource(context, this)
            mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_MIMETYPE)
        }?.notEmpty()?.let { return it }
    } catch (ex: Throwable) {
        log.w(ex, "not video or audio.")
    }

    // ContentResolverに尋ねる
    try {
        context.contentResolver.getType(this)
            ?.notEmpty()?.let { return it }
    } catch (ex: Throwable) {
        log.w(ex, "contentResolver.getType failed.")
    }

    // gboardのステッカーではUriのクエリパラメータにmimeType引数がある
    try {
        getQueryParameter("mimeType")
            ?.notEmpty()?.let { return it }
    } catch (ex: Throwable) {
        log.w(ex, "getQueryParameter(mimeType) failed.")
    }

    // 引数のmimeType
    try {
        mimeTypeArg?.notEmpty()?.let { return it }
    } catch (ex: Throwable) {
        log.w(ex, "mimeTypeArg1 check failed.")
    }

    try {
        // ファイルヘッダを読んで判定する
        findMimeTypeByFileHeader(context.contentResolver, this)
            ?.notEmpty()?.let { return it }
    } catch (ex: Throwable) {
        log.w(ex, "findMimeTypeByFileHeader check failed.")
    }

    return null
}
