package ac.mdiq.podcini.storage.utils

import ac.mdiq.podcini.shared.PodciniHttpClient.getKtorClient
import ac.mdiq.podcini.storage.database.canCheckMediaSize
import ac.mdiq.podcini.storage.database.upsert
import ac.mdiq.podcini.storage.model.Chapter
import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.parser.FrameHeader
import ac.mdiq.podcini.storage.parser.ID3Reader
import ac.mdiq.podcini.storage.parser.VorbisCommentReader
import ac.mdiq.podcini.storage.parser.VorbisCommentReaderException
import ac.mdiq.podcini.utils.Logd
import ac.mdiq.podcini.utils.Loge
import ac.mdiq.podcini.utils.Logs
import android.content.ContentResolver
import io.ktor.client.call.body
import io.ktor.client.plugins.HttpTimeoutConfig
import io.ktor.client.plugins.timeout
import io.ktor.client.request.get
import io.ktor.client.request.head
import io.ktor.client.request.header
import io.ktor.client.request.prepareGet
import io.ktor.client.statement.bodyAsChannel
import io.ktor.client.statement.bodyAsText
import io.ktor.http.HttpHeaders
import io.ktor.http.decodeURLQueryComponent
import io.ktor.http.isSuccess
import io.ktor.utils.io.ByteReadChannel
import io.ktor.utils.io.readAvailable
import okio.Buffer
import okio.BufferedSource
import okio.EOFException
import okio.FileSystem
import okio.buffer
import org.json.JSONException
import org.json.JSONObject
import kotlin.math.abs
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds

private const val TAG: String = "ChapterUtils"

suspend fun fetchChapters(episode: Episode): List<Chapter> {
    Logd(TAG, "fetchChapters for ${episode.title}")
    var chaptersFromDatabase: List<Chapter>? = null
    var chaptersFromPodcastIndex: List<Chapter>? = null
    if (episode.chapters.isNotEmpty()) chaptersFromDatabase = episode.chapters
    if (!episode.podcastIndexChapterUrl.isNullOrEmpty()) {
        fun parse(jsonStr: String): List<Chapter> {
            try {
                val chapters: MutableList<Chapter> = mutableListOf()
                val obj = JSONObject(jsonStr)
                val objChapters = obj.getJSONArray("chapters")
                for (i in 0 until objChapters.length()) {
                    val jsonObject = objChapters.getJSONObject(i)
                    val startTime = jsonObject.optInt("startTime", 0)
                    val title = jsonObject.optString("title").takeIf { it.isNotEmpty() }
                    val link = jsonObject.optString("url").takeIf { it.isNotEmpty() }
                    val img = jsonObject.optString("img").takeIf { it.isNotEmpty() }
                    chapters.add(Chapter(startTime * 1000L, title, link, img))
                }
                return chapters
            } catch (e: JSONException) { Logs(TAG, e, "Error parse json string for chapters: $jsonStr ${episode.title}") }
            return listOf()
        }
        chaptersFromPodcastIndex = try {
            val url = episode.podcastIndexChapterUrl!!
            Logd(TAG, "fetchChapters fetching from url: $url")
            val response = getKtorClient().get(url) { header(HttpHeaders.CacheControl, null) }
            if (response.status.isSuccess()) parse(response.bodyAsText()) else listOf()
        } catch (e: Exception) {
            Logs(TAG, e, "Error getting chapters from url for ${episode.title}")
            listOf()
        }
    }
    return if (chaptersFromPodcastIndex.isNullOrEmpty()) mergeChapters(chaptersFromDatabase, loadChaptersFromMedia(episode)) ?: listOf()
    else mergeChapters(chaptersFromDatabase, chaptersFromPodcastIndex) ?: listOf()
}

suspend fun loadChapters(episode: Episode, forceReload: Boolean) {
    if (!canCheckMediaSize(episode)) return
    Logd(TAG, "loadChapters chaptersLoaded: ${episode.chaptersLoaded} forceReload: $forceReload")
    if (episode.chaptersLoaded && !forceReload) return
    val chapters = fetchChapters(episode)
    Logd(TAG, "loadChapters chapters size: ${chapters.size} ${episode.getEpisodeTitle()}")
    upsert(episode) { it.setChapters(chapters) }
}

private fun mergeChapters(chapters1: List<Chapter>?, chapters2: List<Chapter>?): List<Chapter>? {
    Logd(TAG, "Merging chapters")
    fun score(chapters: List<Chapter>): Int {
        var score = 0
        for (chapter in chapters) {
            score = (score + (if (chapter.title.isNullOrEmpty()) 0 else 1) + (if (chapter.link.isNullOrEmpty()) 0 else 1) + (if (chapter.imageUrl.isNullOrEmpty()) 0 else 1))
        }
        return score
    }
    when {
        chapters1 == null -> return chapters2
        chapters2 == null -> return chapters1
        chapters2.size > chapters1.size -> return chapters2
        chapters2.size < chapters1.size -> return chapters1
        else -> {
            for (i in chapters2.indices) {
                val chapterTarget = chapters2[i]
                val chapterOther = chapters1[i]

                if (abs((chapterTarget.start - chapterOther.start).toDouble()) > 1000) {
                    Loge(TAG, "Chapter lists are too different. Cancelling merge.")
                    return if (score(chapters1) > score(chapters2)) chapters1 else chapters2
                }

                if (chapterTarget.imageUrl.isNullOrEmpty()) chapterTarget.imageUrl = chapterOther.imageUrl
                if (chapterTarget.link.isNullOrEmpty()) chapterTarget.link = chapterOther.link
                if (chapterTarget.title.isNullOrEmpty()) chapterTarget.title = chapterOther.title
            }
            return chapters2
        }
    }
}

/**
 * Reads ID3 chapters.
 * See https://id3.org/id3v2-chapters-1.0
 */
class ChapterReader(input: CountingSource) : ID3Reader(input) {
    val chapters: MutableList<Chapter> = mutableListOf()

    override fun readFrame(frameHeader: FrameHeader) {
        var chapBuffer = Buffer()
        var chapBufferSize = 0L

        fun readChapter(): Chapter {
            val elementId = readNullTerminatedString(chapBuffer)
            val startTime = chapBuffer.readInt().toLong()
            chapBuffer.skip(12) // don't need endTime, startOffset, and endOffset
            val chapter = Chapter().apply {
                this.start = startTime
                this.chapterId = elementId
            }
            while (!chapBuffer.exhausted()) {
                val subFrameHeader = readSubFrameHeader(chapBuffer) ?: break
                Logd(TAG, "readChapter Handling subframe: $subFrameHeader ${chapBuffer.size}")
                val payload = chapBuffer.readByteArray(subFrameHeader.size.toLong())
                when (subFrameHeader.id) {
                    FRAME_ID_TITLE -> {
                        chapter.title = readTextFrame(payload)
                        Logd(TAG, "readChapter Found title: ${chapter.title} ${chapter.start}")
                    }
                    FRAME_ID_LINK -> {
                        val (_, url) = readWXXXFrame(payload)
                        try {
                            val decodedLink = url.decodeURLQueryComponent(charset = Charsets.ISO_8859_1)
                            chapter.link = decodedLink
                            Logd(TAG, "readChapter Found link: ${chapter.link}")
                        } catch (e: IllegalArgumentException) { Logs(TAG, e, "readChapter Bad URL found in ID3 data") }
                    }
                    FRAME_ID_PICTURE -> {
//                        val encoding = buffer.readByte()
//                        val mime = readIsoStringNullTerminated(subFrameHeader.size)
//                        val type = buffer.readByte()
//                        val description = readEncodedString(encoding.toInt(), subFrameHeader.size)
//                        Logd(TAG, "readChapter Found apic: $mime,$description")
//                        if (MIME_IMAGE_URL == mime) {
//                            val link = readIsoStringNullTerminated(subFrameHeader.size)
//                            Logd(TAG, "readChapter Link: $link")
//                            if (chapter.imageUrl.isNullOrEmpty() || type.toInt() == IMAGE_TYPE_COVER) chapter.imageUrl = link
//                        } else {
//                            val alreadyConsumed = position - frameStartPosition
//                            val rawImageDataLength = subFrameHeader.size - alreadyConsumed
//                            if (chapter.imageUrl.isNullOrEmpty() || type.toInt() == IMAGE_TYPE_COVER) chapter.imageUrl = "embedded-image://$position/$rawImageDataLength"
//                        }
                    }
                    else -> Logd(TAG, "readChapter Unknown chapter sub-frame ${subFrameHeader.id}")
                }
                Logd(TAG, "readChapter subFrameHeader.size: ${subFrameHeader.size} ")
                Logd(TAG, "chapBufferSize: $chapBufferSize chapBufferEndSize: ${chapBuffer.size}")
            }
            return chapter
        }

        if (FRAME_ID_CHAPTER == frameHeader.id) {
            val size = frameHeader.size.toLong()
            if (size > remainingTagBytes) {
                Loge(TAG, "Frame exceeds tag boundary: size=$size remaining=$remainingTagBytes")
                return
            }
            Logd(TAG, "readFrame size: $size remainingTagBytes: $remainingTagBytes")
            val chapBody = buffer.readByteArray(size)
            remainingTagBytes -= size
            chapBuffer = Buffer().write(chapBody)
            chapBufferSize = chapBuffer.size
            Logd(TAG, "chapBufferSize: $chapBufferSize size: $size")

            Logd(TAG, "readFrame Handling frame: $frameHeader")
            val chapter = readChapter()
            Logd(TAG, "readFrame Chapter done: $chapter")
            chapters.add(chapter)
        }
//        else super.readFrame(frameHeader)
    }

    companion object {
        private val TAG: String = ChapterReader::class.simpleName ?: "Anonymous"

        const val FRAME_ID_CHAPTER: String = "CHAP"
        const val FRAME_ID_TITLE: String = "TIT2"
        const val FRAME_ID_LINK: String = "WXXX"
        const val FRAME_ID_PICTURE: String = "APIC"
        const val MIME_IMAGE_URL: String = "-->"
        const val IMAGE_TYPE_COVER: Int = 3
    }
}

class MP4ChapterReader(val source: BufferedSource) {
    val chapters: MutableList<Chapter> = mutableListOf()

    fun parseM4A() {
        while (!source.exhausted()) {
            val size = source.readInt().toLong() and 0xFFFFFFFFL
            val type = source.readUtf8(4)
            when (type) {
                "moov", "udta" -> continue
                "chpl" -> {
                    extractM4AChapters(size - 8)
                    return
                }
                else -> if (size >= 8) source.skip(size - 8)
            }
        }
    }

    private fun extractM4AChapters(atomDataSize: Long) {
        var bytesRead = 0L
        source.skip(4)
        bytesRead += 4
        val chapterCount = source.readByte().toInt() and 0xFF
        bytesRead += 1

        for (i in 0 until chapterCount) {
            if (bytesRead + 9 > atomDataSize) break

            val startTimeMs = source.readLong() / 10_000
            val titleLength = source.readByte().toInt() and 0xFF
            bytesRead += 9

            val remainingInAtom = atomDataSize - bytesRead
            val actualTitleLength = minOf(titleLength.toLong(), remainingInAtom)

            val title = source.readUtf8(actualTitleLength)
            bytesRead += actualTitleLength

            val chapter = Chapter()
            chapter.title = title
            chapter.chapterId = "ch_${i}_${startTimeMs}"
            chapter.start = startTimeMs
            chapters.add(chapter)
        }

        if (bytesRead < atomDataSize) source.skip(atomDataSize - bytesRead)
    }
}

class VorbisCommentChapterReader(source: CountingSource) : VorbisCommentReader(source) {
    val chapters: MutableList<Chapter> = mutableListOf()

    public override fun handles(key: String?): Boolean {
        return key?.matches(CHAPTER_KEY.toRegex()) ?: false
    }

    @Throws(VorbisCommentReaderException::class)
    public override fun onContentVectorValue(key: String?, value: String) {
//        Logd(TAG, "Key: $key, value: $value")
        fun getChapterById(id: Long): Chapter? {
            for (c in chapters) if (("" + id) == c.chapterId) return c
            return null
        }
        @Throws(VorbisCommentReaderException::class)
        fun getStartTimeFromValue(value: String?): Long {
            val parts = value!!.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
            if (parts.size >= 3) {
                try {
                    val hours = parts[0].toLong().hours.inWholeMilliseconds
                    val minutes = parts[1].toLong().minutes.inWholeMilliseconds
                    if (parts[2].contains("-->")) parts[2] = parts[2].substring(0, parts[2].indexOf("-->"))
                    val seconds = parts[2].toFloat().toLong().seconds.inWholeMilliseconds
                    return hours + minutes + seconds
                } catch (e: NumberFormatException) { throw VorbisCommentReaderException(e) }
            } else throw VorbisCommentReaderException("Invalid time string")
        }
        /**
         * Return the id of a vorbiscomment chapter from a string like CHAPTERxxx*
         * @return the id of the chapter key or -1 if the id couldn't be read.
         * @throws VorbisCommentReaderException
         */
        @Throws(VorbisCommentReaderException::class)
        fun getIdFromKey(key: String?): Int {
            if (key!!.length >= CHAPTERXXX_LENGTH) { // >= CHAPTERxxx
                try {
                    val strId = key.substring(8, 10)
                    return strId.toInt()
                } catch (e: NumberFormatException) { throw VorbisCommentReaderException(e) }
            }
            throw VorbisCommentReaderException("key is too short ($key)")
        }
        /**
         * Get the string that comes after 'CHAPTERxxx', for example 'name' or 'url'.
         */
        fun getAttributeTypeFromKey(key: String?): String? {
            if (key != null && key.length > CHAPTERXXX_LENGTH) return key.substring(CHAPTERXXX_LENGTH)
            return null
        }

        val attribute = getAttributeTypeFromKey(key)
        val id = getIdFromKey(key)
        var chapter = getChapterById(id.toLong())
        when (attribute) {
            null -> {
                if (getChapterById(id.toLong()) == null) {
                    // new chapter
                    val start = getStartTimeFromValue(value)
                    chapter = Chapter()
                    chapter.chapterId = "" + id
                    chapter.start = start
                    chapters.add(chapter)
                } else throw VorbisCommentReaderException("Found chapter with duplicate ID ($key, $value)")
            }
            CHAPTER_ATTRIBUTE_TITLE -> if (chapter != null) chapter.title = value
            CHAPTER_ATTRIBUTE_LINK -> if (chapter != null) chapter.link = value
        }
    }

    companion object {
        private val TAG: String = VorbisCommentChapterReader::class.simpleName ?: "Anonymous"

        private const val CHAPTER_KEY = "chapter\\d\\d\\d.*"
        private const val CHAPTER_ATTRIBUTE_TITLE = "name"
        private const val CHAPTER_ATTRIBUTE_LINK = "url"
        private const val CHAPTERXXX_LENGTH = "chapterxxx".length
    }
}

suspend fun loadChaptersFromMedia(episode: Episode): List<Chapter> {
    val tempPath = FileSystem.SYSTEM_TEMPORARY_DIRECTORY / "stream_buffer.tmp"
    val fileUrl = episode.fileUrl
    val streamurl = episode.downloadUrl

    suspend fun openSource(cb: suspend (BufferedSource, Long?)->Unit) {
        if (!fileUrl.isNullOrBlank()) {
//            Logd(TAG, "openSource fileUrl: $fileUrl")
            val file = fileUrl.toUF()
            if (!file.exists()) {
                Loge(TAG, "Failed loading chapters for ${episode.title}: file doesn't exist: $fileUrl")
                return
            }
            val size = file.size()
//            Logd(TAG, "openSource size: $size")
            if (size == null || size > 0) cb(file.source().buffer(), size)
            else Loge(TAG, "Failed loading chapters for ${episode.title}: file is empty: $fileUrl")
        } else {
            if (streamurl != null && (streamurl.startsWith(ContentResolver.SCHEME_CONTENT) || streamurl.startsWith(ContentResolver.SCHEME_FILE))) {
                val file = streamurl.toUF()
                if (!file.exists()) {
                    Loge(TAG, "Failed loading chapters for ${episode.title}: streamurl file doesn't exist: $streamurl")
                    return
                }
                val size = file.size()
                cb(file.source().buffer(), size)
            } else {
                if (streamurl.isNullOrEmpty()) throw Exception("Failed loading chapters for ${episode.title}: stream url is null of empty")
//                Logd(TAG, "openSource open streaming source")
                getKtorClient().prepareGet(streamurl) {
                    header("Range", "bytes=0-131072")
                    timeout { requestTimeoutMillis = HttpTimeoutConfig.INFINITE_TIMEOUT_MS }
                }.execute { response ->
                    val channel: ByteReadChannel = response.body()
                    val buffer = ByteArray(8192)
                    val limit = 128 * 1024L
                    var totalRead = 0L
                    fs.sink(tempPath).buffer().use { sink ->
                        while (!channel.isClosedForRead && totalRead < limit) {
                            val read = channel.readAvailable(buffer, 0, minOf(buffer.size.toLong(), limit - totalRead).toInt())
                            if (read <= 0) break
                            sink.write(buffer, 0, read)
                            totalRead += read
                        }
                    }
                    fs.source(tempPath).buffer().use { fileSource -> cb(fileSource, null) }
                }
            }
        }
    }

    suspend fun openNetSourceTail(cb: (BufferedSource)->Unit) {
        val client = getKtorClient()
        val contentLength = client.head(streamurl!!).headers[HttpHeaders.ContentLength]?.toLong() ?: 0L
        if (contentLength > 1_000_000L) {
            val tailSize = 128_000L // 128KB is usually enough for the 'moov' atom
            val startRange = contentLength - tailSize
            try {
                client.prepareGet(streamurl) { header(HttpHeaders.Range, "bytes=$startRange-$contentLength") }.execute { response ->
                    val channel = response.bodyAsChannel()
                    fs.appendingSink(tempPath).buffer().use { sink ->
                        val buffer = ByteArray(8192)
                        while (!channel.isClosedForRead) {
                            val read = channel.readAvailable(buffer, 0, buffer.size)
                            if (read == -1) break
                            sink.write(buffer, 0, read)
                        }
                        sink.flush()
                    }
                    fs.source(tempPath).buffer().use { fileSource -> cb(fileSource) }
                }
            } catch (e: Exception) { Logs(TAG, e, "Tail download failed") }
        }
    }

    var chapters: List<Chapter> = listOf()
    openSource { fileSource, size ->
        val format = peekFileFormat(fileSource)
        Logd(TAG, "loadChaptersFromMediaFile1 format: $format")
        val countingSource = CountingSource(fileSource)
        fun enumerateEmptyChapterTitles(chapters: List<Chapter>) {
            for (i in chapters.indices) {
                val c = chapters[i]
                if (c.title == null) c.title = i.toString()
            }
        }
        fun chaptersValid(chapters: List<Chapter>): Boolean {
            if (chapters.isEmpty()) return false
            for (c in chapters) if (c.start < 0) return false
            return true
        }
        var chapters_ = when(format) {
            MediaFormat.MP3 -> {
                val reader = ChapterReader(countingSource)
                reader.readSource()
                reader.chapters.toList()
            }
            MediaFormat.M4A -> {
                val reader = MP4ChapterReader(fileSource)
                try {
                    reader.parseM4A()
                    reader.chapters.toList()
                } catch (e: EOFException) {
                    Logd(TAG, "failed getting chapters in MP4 media header, try tail")
                    var cList = listOf<Chapter>()
                    if (size != null) {
                        val skipAmount = maxOf(0L, size - 128_000L)
                        fileSource.use { buffered ->
                            buffered.skip(skipAmount)
                            reader.parseM4A()
                            cList = reader.chapters.toList()
                        }
                    } else if (!streamurl.isNullOrBlank()) {
                        openNetSourceTail { fileSource ->
                            val reader = MP4ChapterReader(fileSource)
                            try {
                                reader.parseM4A()
                                cList = reader.chapters.toList()
                            } catch (e: Exception) {
                                Logs(TAG, e, "failed to get chapters for MP4 media ${episode.title}")
                                cList = emptyList()
                            }
                        }
                    }
                    cList
                }
            }
            MediaFormat.OGG, MediaFormat.FLAC -> {
                val reader = VorbisCommentChapterReader(countingSource)
                reader.readSource()
                reader.chapters.toList()
            }
            else -> {
//                LogtFor(TAG, episode.id, "failed to get chapters: file format $format currently not handled")
                Logd(TAG, "failed to get chapters: file format $format currently not handled")
                emptyList()
            }
        }
        chapters_ = chapters_.sortedWith { c1, c2 -> (c1.start - c2.start).toInt() }
        enumerateEmptyChapterTitles(chapters_)
        if (!chaptersValid(chapters_)) chapters_ = emptyList()
        if (chapters_.isNotEmpty()) chapters = chapters_
    }
    return chapters
}

