/*
 * CachedDataSource.kt
 * Copyright (C) 2009-2023 Ultrasonic developers
 *
 * Distributed under terms of the GNU GPLv3 license.
 */

package org.moire.ultrasonic.data

import android.net.Uri
import androidx.core.net.toUri
import androidx.media3.common.C
import androidx.media3.common.PlaybackException
import androidx.media3.common.util.Util
import androidx.media3.datasource.BaseDataSource
import androidx.media3.datasource.DataSource
import androidx.media3.datasource.DataSpec
import androidx.media3.datasource.HttpDataSource.HttpDataSourceException
import java.io.IOException
import java.io.InputStream
import java.io.InterruptedIOException
import org.moire.ultrasonic.util.AbstractFile
import org.moire.ultrasonic.util.FileUtil
import org.moire.ultrasonic.util.Storage
import timber.log.Timber

@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
class CachedDataSource(
    private var upstreamDataSource: DataSource
) : BaseDataSource(true) {

    class Factory(
        private var upstreamDataSourceFactory: DataSource.Factory
    ) : DataSource.Factory {

        override fun createDataSource(): CachedDataSource {
            return createDataSourceInternal(
                upstreamDataSourceFactory.createDataSource()
            )
        }

        private fun createDataSourceInternal(upstreamDataSource: DataSource): CachedDataSource {
            return CachedDataSource(
                upstreamDataSource
            )
        }
    }

    private var bytesToRead: Long = 0
    private var bytesRead: Long = 0
    private var dataSpec: DataSpec? = null
    private var responseByteStream: InputStream? = null
    private var openedFile = false
    private var cachePath: String? = null
    private var cacheFile: AbstractFile? = null

    override fun open(dataSpec: DataSpec): Long {
        Timber.i(
            "CachedDatasource: Open: %s",
            dataSpec.toString()
        )

        this.dataSpec = dataSpec
        bytesRead = 0
        bytesToRead = 0

        val components = dataSpec.uri.toString().split('|')
        val path = components[2]
        val cacheLength = checkCache(path)

        // We have found an item in the cache, return early
        if (cacheLength > 0) {
            transferInitializing(dataSpec)
            bytesToRead = cacheLength
            transferStarted(dataSpec)
            skipFully(dataSpec.position, dataSpec)
            return bytesToRead
        }

        // else forward the call to upstream
        Timber.d("No cache hit, forwarding call")
        return upstreamDataSource.open(dataSpec)
    }

    @Suppress("MagicNumber")
    override fun read(buffer: ByteArray, offset: Int, length: Int): Int {
        // if (offset > 0 || length > 4)
        //    Timber.d("CachedDatasource: Read: %s %s", offset, length)
        return if (cachePath != null) {
            try {
                readInternal(buffer, offset, length)
            } catch (e: IOException) {
                throw HttpDataSourceException.createForIOException(
                    e,
                    Util.castNonNull(dataSpec),
                    HttpDataSourceException.TYPE_READ
                )
            }
        } else {
            upstreamDataSource.read(buffer, offset, length)
        }
    }

    private fun readInternal(buffer: ByteArray, offset: Int, readLength: Int): Int {
        var readLengthCpy = readLength
        if (readLengthCpy == 0) {
            return 0
        }
        if (bytesToRead != C.LENGTH_UNSET.toLong()) {
            val bytesRemaining = bytesToRead - bytesRead
            if (bytesRemaining == 0L) {
                return C.RESULT_END_OF_INPUT
            }
            readLengthCpy = readLengthCpy.toLong().coerceAtMost(bytesRemaining).toInt()
        }
        val read = Util.castNonNull(responseByteStream).read(buffer, offset, readLengthCpy)
        if (read == -1) {
            Timber.i("CachedDatasource: EndOfInput")
            return C.RESULT_END_OF_INPUT
        }
        bytesRead += read.toLong()
        bytesTransferred(read)
        return read
    }

    /**
     * Attempts to skip the specified number of bytes in full.
     *
     * @param bytesToSkip The number of bytes to skip.
     * @param dataSpec The [DataSpec].
     * @throws HttpDataSourceException If the thread is interrupted during the operation, or an error
     * occurs while reading from the source, or if the data ended before skipping the specified
     * number of bytes.
     */
    @Suppress("ThrowsCount")
    @Throws(HttpDataSourceException::class)
    private fun skipFully(bytesToSkip: Long, dataSpec: DataSpec) {
        var bytesToSkipCpy = bytesToSkip
        if (bytesToSkipCpy == 0L) {
            return
        }
        val skipBuffer = ByteArray(4096)
        try {
            while (bytesToSkipCpy > 0) {
                val readLength =
                    bytesToSkipCpy.coerceAtMost(skipBuffer.size.toLong()).toInt()
                val read = Util.castNonNull(responseByteStream).read(skipBuffer, 0, readLength)
                if (Thread.currentThread().isInterrupted) {
                    throw InterruptedIOException()
                }
                if (read == -1) {
                    throw HttpDataSourceException(
                        dataSpec,
                        PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE,
                        HttpDataSourceException.TYPE_OPEN
                    )
                }
                bytesToSkipCpy -= read.toLong()
                bytesTransferred(read)
            }
            return
        } catch (e: HttpDataSourceException) {
            throw e
        } catch (ignored: IOException) {
            throw HttpDataSourceException(
                dataSpec,
                PlaybackException.ERROR_CODE_IO_UNSPECIFIED,
                HttpDataSourceException.TYPE_OPEN
            )
        }
    }

    /*
     * This method is called by StatsDataSource to verify that the loading succeeded,
     * so its important that we return the correct value here..
     */
    override fun getUri(): Uri? {
        return cachePath?.toUri() ?: upstreamDataSource.uri
    }

    override fun close() {
        Timber.i("CachedDatasource: close %s", openedFile)
        if (openedFile) {
            openedFile = false
            transferEnded()
            responseByteStream?.close()
            responseByteStream = null
        } else {
            upstreamDataSource.close()
        }
    }

    /**
     * Checks our cache for a matching media file
     */
    private fun checkCache(path: String): Long {
        var filePath: String = path
        var found = Storage.isPathExists(path)

        if (!found) {
            filePath = FileUtil.getCompleteFile(path)
            found = Storage.isPathExists(filePath)
        }

        if (!found) return -1

        cachePath = filePath
        openedFile = true

        cacheFile = Storage.getFromPath(filePath)!!
        responseByteStream = cacheFile!!.getFileInputStream()

        val descriptor = cacheFile!!.getDocumentFileDescriptor("r")
        val length = descriptor!!.length
        descriptor.close()

        return length
    }
}
