package com.foxx.androidcast.receiver;

/*********************************************************************
 * VideoDecoder.java
 * Created at: Fri 15 May 2026 16:49:02 +0200
 * Updated at: Wed 20 May 2026 15:17:13 +0200 by Anton Afanasyeu <a.afanasieff@gmail.com>
 * Commit: 5d8e82d2e60a21fff3138d2a394ee4e8b4c6dcb8
 * class VideoDecoder
 * Contributors:
 *    - Anton Afanasyeu <a.afanasieff@gmail.com> (7 commits, 213 lines)
 *    - Cursor Agent (project assistant)
 * Digest: SHA256	198572dccc10d66707f67d249a8ba20391fc8967d8d07737bf162b6b96a85ac4
**********************************************************************/
import android.media.MediaCodec;
import android.media.MediaFormat;
import android.util.Log;
import android.view.Surface;

import com.foxx.androidcast.media.CodecNegotiator;
import com.foxx.androidcast.media.H264Bitstream;
import com.foxx.androidcast.media.codec.VideoDecoderSink;
import com.foxx.androidcast.receiver.av.ReceiverAvRuntime;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayDeque;
import java.util.Queue;

/** Thread-safe hardware video decoder feeding a Surface. */
public class VideoDecoder implements VideoDecoderSink {
    private static final String TAG = "VideoDecoder";

    private final Object lock = new Object();
    private MediaCodec codec;
    private final Queue<PendingFrame> pending = new ArrayDeque<>();
    private boolean configured;
    private boolean released;
    private VideoDecoderSink.Listener listener;
    private boolean firstFrameNotified;

    private String mime = MediaFormat.MIMETYPE_VIDEO_AVC;

    public void configure(String mime, int width, int height, byte[] csd0, byte[] csd1, Surface surface,
            VideoDecoderSink.Listener listener) throws IOException {
        synchronized (lock) {
            releaseLocked();
            released = false;
            this.listener = listener;
            this.mime = mime != null ? mime : MediaFormat.MIMETYPE_VIDEO_AVC;
            firstFrameNotified = false;
            MediaFormat format = MediaFormat.createVideoFormat(this.mime, width, height);
            format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, width * height * 2);
            if (csd0 != null && csd0.length > 0) {
                byte[] c0 = CodecNegotiator.isH264(this.mime) ? H264Bitstream.toAnnexB(csd0) : csd0;
                format.setByteBuffer("csd-0", ByteBuffer.wrap(c0));
            }
            if (csd1 != null && csd1.length > 0) {
                byte[] c1 = CodecNegotiator.isH264(this.mime) ? H264Bitstream.toAnnexB(csd1) : csd1;
                format.setByteBuffer("csd-1", ByteBuffer.wrap(c1));
            }
            codec = MediaCodec.createDecoderByType(this.mime);
            codec.configure(format, surface, null, 0);
            codec.start();
            configured = true;
            Log.i(TAG, "Configured " + CodecNegotiator.displayName(this.mime) + " " + width + "x" + height);
            drainPendingLocked();
        }
    }

    public void queueFrame(long ptsUs, boolean keyFrame, byte[] data) {
        synchronized (lock) {
            if (released || data == null || data.length == 0) {
                return;
            }
            byte[] annexB = needsAnnexB(mime) ? H264Bitstream.toAnnexB(data) : data;
            if (!configured || codec == null) {
                pending.add(new PendingFrame(ptsUs, keyFrame, annexB));
                return;
            }
            decodeFrameLocked(ptsUs, keyFrame, annexB);
        }
    }

    public void release() {
        synchronized (lock) {
            releaseLocked();
        }
    }

    private void releaseLocked() {
        configured = false;
        released = true;
        listener = null;
        firstFrameNotified = false;
        pending.clear();
        MediaCodec c = codec;
        codec = null;
        if (c != null) {
            try {
                c.stop();
            } catch (Exception ignored) {
            }
            try {
                c.release();
            } catch (Exception ignored) {
            }
        }
    }

    private void drainPendingLocked() {
        PendingFrame frame;
        while ((frame = pending.poll()) != null) {
            decodeFrameLocked(frame.ptsUs, frame.keyFrame, frame.data);
        }
    }

    private void decodeFrameLocked(long ptsUs, boolean keyFrame, byte[] data) {
        MediaCodec c = codec;
        if (c == null || !configured) {
            return;
        }
        try {
            int inIndex = c.dequeueInputBuffer(10_000);
            if (inIndex < 0) {
                return;
            }
            ByteBuffer buffer = c.getInputBuffer(inIndex);
            if (buffer == null) {
                return;
            }
            buffer.clear();
            buffer.put(data);
            int flags = keyFrame ? MediaCodec.BUFFER_FLAG_KEY_FRAME : 0;
            c.queueInputBuffer(inIndex, 0, data.length, ptsUs, flags);
            drainOutputLocked(c);
        } catch (IllegalStateException e) {
            Log.w(TAG, "Decode skipped (codec state): " + e.getMessage());
        } catch (Exception e) {
            Log.e(TAG, "Decode error", e);
        }
    }

    private void drainOutputLocked(MediaCodec c) {
        try {
            MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
            for (;;) {
                int outIndex = c.dequeueOutputBuffer(info, 10_000);
                if (outIndex == MediaCodec.INFO_TRY_AGAIN_LATER) {
                    break;
                }
                if (outIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                    MediaFormat fmt = c.getOutputFormat();
                    Log.i(TAG, "Output format: " + fmt);
                    notifyOutputSizeLocked(fmt);
                    continue;
                }
                if (outIndex < 0) {
                    continue;
                }
                boolean render = info.size > 0
                        && (info.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) == 0;
                c.releaseOutputBuffer(outIndex, render);
                if (render) {
                    ReceiverAvRuntime.video().onFrameDecoded(info.presentationTimeUs);
                    notifyFirstFrameLocked();
                }
            }
        } catch (IllegalStateException e) {
            Log.w(TAG, "Drain skipped (codec state): " + e.getMessage());
        }
    }

    private void notifyOutputSizeLocked(MediaFormat format) {
        if (listener == null || format == null) {
            return;
        }
        int w = format.getInteger(MediaFormat.KEY_WIDTH);
        int h = format.getInteger(MediaFormat.KEY_HEIGHT);
        if (format.containsKey("crop-right") && format.containsKey("crop-left")) {
            w = format.getInteger("crop-right") + 1 - format.getInteger("crop-left");
        }
        if (format.containsKey("crop-bottom") && format.containsKey("crop-top")) {
            h = format.getInteger("crop-bottom") + 1 - format.getInteger("crop-top");
        }
        if (w > 0 && h > 0) {
            listener.onOutputSizeChanged(w, h);
        }
    }

    private void notifyFirstFrameLocked() {
        if (listener == null) {
            return;
        }
        Listener cb = listener;
        if (!firstFrameNotified) {
            firstFrameNotified = true;
            Log.i(TAG, "First frame rendered");
            cb.onFirstFrameRendered();
        } else {
            cb.onFrameRendered();
        }
    }

    private static boolean needsAnnexB(String mime) {
        return CodecNegotiator.isH264(mime) || CodecNegotiator.isHevc(mime);
    }

    private static final class PendingFrame {
        final long ptsUs;
        final boolean keyFrame;
        final byte[] data;

        PendingFrame(long ptsUs, boolean keyFrame, byte[] data) {
            this.ptsUs = ptsUs;
            this.keyFrame = keyFrame;
            this.data = data;
        }
    }
}
