package xyz.cottongin.radio247.audio

import android.media.AudioAttributes
import android.media.AudioFormat
import android.media.AudioTrack
import android.media.MediaCodec
import android.media.MediaFormat
import android.util.Log
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import java.io.InputStream
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicLong

class AudioEngine(
    private val url: String,
    private val bufferMs: Int = 0
) {
    private val _events = MutableSharedFlow<AudioEngineEvent>(extraBufferCapacity = 64)
    val events: SharedFlow<AudioEngineEvent> = _events

    private var thread: Thread? = null
    @Volatile
    private var running = false
    private val pendingSkips = AtomicInteger(0)

    private val _estimatedLatencyMs = AtomicLong(0)
    val estimatedLatencyMs: Long get() = _estimatedLatencyMs.get()

    @Volatile
    private var currentRingBuffer: RingBuffer? = null
    @Volatile
    private var currentAudioTrack: AudioTrack? = null
    @Volatile
    private var currentCodec: MediaCodec? = null
    @Volatile
    private var catchingUp = true
    private var catchupFramesSkipped = 0
    @Volatile
    private var timedStream: TimedInputStream? = null
    private var presentationTimeUs = 0L
    private var frameDurationUs = 0L
    private var framesPerSecond = 0
    private var configuredSampleRate = 0
    private var configuredChannelCount = 0

    fun start() {
        Log.i(TAG, "start() url=$url")
        running = true
        catchingUp = true
        catchupFramesSkipped = 0
        presentationTimeUs = 0L
        thread = Thread({
            try {
                runPipeline()
            } catch (e: Exception) {
                if (running) {
                    Log.e(TAG, "Pipeline error (active): ${e.message}", e)
                    val error = when (e) {
                        is ConnectionFailed ->
                            EngineError.ConnectionFailed(e)
                        else -> EngineError.DecoderError(e)
                    }
                    _events.tryEmit(AudioEngineEvent.Error(error))
                } else {
                    Log.i(TAG, "Pipeline stopped: ${e.message}")
                }
            } finally {
                Log.i(TAG, "Pipeline finally — emitting Stopped")
                _events.tryEmit(AudioEngineEvent.Stopped)
            }
        }, "AudioEngine").apply { start() }
    }

    fun stop() {
        Log.i(TAG, "stop() called, running=$running, thread=${thread?.name}")
        running = false
        try { timedStream?.close() } catch (_: Exception) {}
        thread?.interrupt()
        thread = null
    }

    fun skipAhead() {
        pendingSkips.incrementAndGet()
    }

    fun setVolume(gain: Float) {
        currentAudioTrack?.setVolume(gain.coerceIn(0f, 1f))
    }

    private fun runPipeline() {
        Log.i(TAG, "runPipeline() connecting to $url")
        val connection = StreamConnection(url)
        connection.open()
        Log.i(TAG, "Connected: metaint=${connection.metaint}, info=${connection.streamInfo}")

        val tStream = TimedInputStream(connection.inputStream!!)
        timedStream = tStream

        var audioTrack: AudioTrack? = null
        var codec: MediaCodec? = null
        var ringBuffer: RingBuffer? = null

        _events.tryEmit(AudioEngineEvent.Started)
        connection.streamInfo?.let { _events.tryEmit(AudioEngineEvent.StreamInfoReceived(it)) }

        fun initAudioOutput(format: Mp3FrameInfo) {
            frameDurationUs = format.samplesPerFrame.toLong() * 1_000_000 / format.sampleRate
            framesPerSecond = (1_000_000L / frameDurationUs).toInt()
            configuredSampleRate = format.sampleRate
            configuredChannelCount = format.channelCount

            Log.i(TAG, "Detected format: ${format.sampleRate} Hz, ${format.channelCount}ch, " +
                "${format.samplesPerFrame} samples/frame, frameDuration=${frameDurationUs}us")

            val channelConfig = if (format.channelCount == 1)
                AudioFormat.CHANNEL_OUT_MONO else AudioFormat.CHANNEL_OUT_STEREO
            val encoding = AudioFormat.ENCODING_PCM_16BIT
            val minBuf = AudioTrack.getMinBufferSize(format.sampleRate, channelConfig, encoding)

            audioTrack = AudioTrack.Builder()
                .setAudioAttributes(
                    AudioAttributes.Builder()
                        .setUsage(AudioAttributes.USAGE_MEDIA)
                        .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
                        .build()
                )
                .setAudioFormat(
                    AudioFormat.Builder()
                        .setSampleRate(format.sampleRate)
                        .setChannelMask(channelConfig)
                        .setEncoding(encoding)
                        .build()
                )
                .setBufferSizeInBytes(minBuf)
                .setPerformanceMode(AudioTrack.PERFORMANCE_MODE_LOW_LATENCY)
                .setTransferMode(AudioTrack.MODE_STREAM)
                .build()

            codec = MediaCodec.createDecoderByType("audio/mpeg")
            val mediaFormat = MediaFormat.createAudioFormat(
                "audio/mpeg", format.sampleRate, format.channelCount
            )
            codec!!.configure(mediaFormat, null, null, 0)
            codec!!.start()
            audioTrack!!.play()

            currentAudioTrack = audioTrack
            currentCodec = codec

            val frameDurationMs = (frameDurationUs / 1000).toInt().coerceAtLeast(1)
            val bufferFrames = if (bufferMs > 0) (bufferMs / frameDurationMs).coerceAtLeast(1) else 0

            ringBuffer = RingBuffer(bufferFrames) { mp3Frame ->
                decodeToPcm(codec!!, mp3Frame, audioTrack!!)
            }
            currentRingBuffer = ringBuffer

            _events.tryEmit(AudioEngineEvent.AudioFormatDetected(
                format.sampleRate, format.channelCount
            ))
        }

        try {
            val frameSync = Mp3FrameSync { mp3Frame ->
                if (audioTrack == null) {
                    val format = Mp3FrameSync.parseFrameInfo(mp3Frame)
                        ?: Mp3FrameInfo(44100, 2, 1152)
                    initAudioOutput(format)
                }
                ringBuffer!!.write(mp3Frame)
            }

            val icyParser = IcyParser(
                input = tStream,
                metaint = connection.metaint,
                onAudioData = { buf, off, len -> frameSync.feed(buf, off, len) },
                onMetadata = { _events.tryEmit(AudioEngineEvent.MetadataChanged(it)) }
            )

            icyParser.readAll()

            ringBuffer?.flush()
            frameSync.flush()
            _events.tryEmit(AudioEngineEvent.Error(EngineError.StreamEnded))
        } finally {
            timedStream = null
            currentRingBuffer = null
            currentCodec = null
            currentAudioTrack = null
            codec?.stop()
            codec?.release()
            audioTrack?.stop()
            audioTrack?.release()
            connection.close()
        }
    }

    private fun decodeToPcm(codec: MediaCodec, mp3Frame: ByteArray, audioTrack: AudioTrack) {
        if (catchingUp) {
            catchupFramesSkipped++
            val lastReadMs = timedStream?.lastReadDurationMs ?: 0L
            val maxCatchupFrames = framesPerSecond * 5
            if (lastReadMs >= CATCHUP_THRESHOLD_MS || catchupFramesSkipped >= maxCatchupFrames) {
                if (catchupFramesSkipped >= maxCatchupFrames) {
                    Log.w(TAG, "Catchup cap reached after $catchupFramesSkipped frames, starting playback")
                }
                catchingUp = false
            } else {
                return
            }
        }

        val skips = pendingSkips.getAndSet(0)
        if (skips > 0) {
            val framesToDrop = skips * framesPerSecond
            currentRingBuffer?.drop(framesToDrop)
            audioTrack.pause()
            audioTrack.flush()
            audioTrack.play()
            drainCodecOutput(codec)
            presentationTimeUs = 0L
            return
        }

        val inIdx = codec.dequeueInputBuffer(1000)
        if (inIdx >= 0) {
            val inBuf = codec.getInputBuffer(inIdx)!!
            inBuf.clear()
            inBuf.put(mp3Frame)
            codec.queueInputBuffer(inIdx, 0, mp3Frame.size, presentationTimeUs, 0)
            presentationTimeUs += frameDurationUs
        }

        val bufferInfo = MediaCodec.BufferInfo()
        var outIdx = codec.dequeueOutputBuffer(bufferInfo, 1000)
        while (outIdx >= 0) {
            if (outIdx == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                val outFormat = codec.outputFormat
                val outRate = outFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE)
                val outChannels = outFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT)
                Log.i(TAG, "Decoder output format: ${outRate} Hz, ${outChannels}ch")
                if (outRate != configuredSampleRate || outChannels != configuredChannelCount) {
                    Log.w(TAG, "Decoder output ($outRate Hz, ${outChannels}ch) differs from " +
                        "AudioTrack ($configuredSampleRate Hz, ${configuredChannelCount}ch)")
                }
                outIdx = codec.dequeueOutputBuffer(bufferInfo, 0)
                continue
            }
            val outBuf = codec.getOutputBuffer(outIdx)!!
            outBuf.position(bufferInfo.offset)
            outBuf.limit(bufferInfo.offset + bufferInfo.size)
            val pcmData = ByteArray(bufferInfo.size)
            outBuf.get(pcmData)
            codec.releaseOutputBuffer(outIdx, false)
            audioTrack.write(pcmData, 0, pcmData.size)
            outIdx = codec.dequeueOutputBuffer(bufferInfo, 0)
        }
    }

    private fun drainCodecOutput(codec: MediaCodec) {
        val bufferInfo = MediaCodec.BufferInfo()
        var outIdx = codec.dequeueOutputBuffer(bufferInfo, 0)
        while (outIdx >= 0) {
            codec.releaseOutputBuffer(outIdx, false)
            outIdx = codec.dequeueOutputBuffer(bufferInfo, 0)
        }
    }

    companion object {
        private const val TAG = "AudioEngine"
        private const val CATCHUP_THRESHOLD_MS = 30L
    }
}

private class TimedInputStream(private val delegate: InputStream) : InputStream() {
    @Volatile
    var lastReadDurationMs: Long = 0L
        private set

    override fun read(): Int {
        val start = System.nanoTime()
        val result = delegate.read()
        lastReadDurationMs = (System.nanoTime() - start) / 1_000_000
        return result
    }

    override fun read(b: ByteArray, off: Int, len: Int): Int {
        val start = System.nanoTime()
        val result = delegate.read(b, off, len)
        lastReadDurationMs = (System.nanoTime() - start) / 1_000_000
        return result
    }

    override fun available(): Int = delegate.available()
    override fun close() = delegate.close()
}
