package com.foxx.androidcast.sender;

/*********************************************************************
 * AudioEncoder.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 AudioEncoder
 * Contributors:
 *    - Anton Afanasyeu <a.afanasieff@gmail.com> (7 commits, 184 lines)
 *    - Cursor Agent (project assistant)
 * Digest: SHA256	36ce52402928a59d3f331eca3c59dfc761e12eda518e258d5b96be48413b1f3f
**********************************************************************/
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaFormat;
import android.util.Log;

import com.foxx.androidcast.CastConfig;
import com.foxx.androidcast.media.codec.AudioEncoderSink;

import java.io.IOException;
import java.nio.ByteBuffer;

/** AAC encoder for captured PCM. */
public class AudioEncoder implements AudioEncoderSink {
    private static final String TAG = "AudioEncoder";
    private static final String MIME = MediaFormat.MIMETYPE_AUDIO_AAC;

    private final Object inputLock = new Object();
    private MediaCodec codec;
    private Callback callback;
    private volatile boolean running;
    private Thread drainThread;
    private long droppedPcmFrames;

    public void prepare(Callback callback) throws IOException {
        this.callback = callback;
        MediaFormat format = MediaFormat.createAudioFormat(MIME, CastConfig.AUDIO_SAMPLE_RATE, CastConfig.AUDIO_CHANNEL_COUNT);
        format.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
        format.setInteger(MediaFormat.KEY_BIT_RATE, CastConfig.AUDIO_BITRATE);
        format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 16384);

        codec = MediaCodec.createEncoderByType(MIME);
        codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
        codec.start();
        running = true;
        primeSilenceFrames();
        drainThread = new Thread(this::drainLoop, "AudioEncoderDrain");
        drainThread.start();
    }

    /** Minimal silence so AAC emits config before mic PCM (no extra thread). */
    private void primeSilenceFrames() {
        int frameBytes = CastConfig.AUDIO_SAMPLE_RATE * CastConfig.AUDIO_CHANNEL_COUNT * 2 / 50;
        byte[] silence = new byte[frameBytes];
        for (int i = 0; i < 3; i++) {
            queuePcm(silence, frameBytes, i * 20_000L);
        }
    }

    public long getDroppedPcmFrames() {
        return droppedPcmFrames;
    }

    public void queuePcm(byte[] pcm, int length, long ptsUs) {
        if (!running) {
            return;
        }
        synchronized (inputLock) {
            MediaCodec c = codec;
            if (c == null) {
                return;
            }
            try {
                int index = -1;
                for (int attempt = 0; attempt < 4 && index < 0; attempt++) {
                    index = c.dequeueInputBuffer(attempt == 0 ? 0 : 3_000);
                }
                if (index < 0) {
                    droppedPcmFrames++;
                    if (droppedPcmFrames == 1 || droppedPcmFrames % 50 == 0) {
                        Log.w(TAG, "Dropped PCM frame (encoder input busy), total=" + droppedPcmFrames);
                    }
                    return;
                }
                ByteBuffer buffer = c.getInputBuffer(index);
                if (buffer == null) {
                    return;
                }
                buffer.clear();
                buffer.put(pcm, 0, length);
                c.queueInputBuffer(index, 0, length, ptsUs, 0);
            } catch (Exception e) {
                Log.w(TAG, "queuePcm failed", e);
            }
        }
    }

    public void stop() {
        running = false;
        Thread drain = drainThread;
        drainThread = null;
        if (drain != null) {
            try {
                drain.join(1000);
            } catch (InterruptedException ignored) {
                Thread.currentThread().interrupt();
            }
        }
        synchronized (inputLock) {
            MediaCodec c = codec;
            codec = null;
            if (c != null) {
                try {
                    c.stop();
                } catch (Exception ignored) {
                }
                try {
                    c.release();
                } catch (Exception ignored) {
                }
            }
        }
    }

    private void drainLoop() {
        MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
        boolean configSent = false;
        while (true) {
            MediaCodec c;
            synchronized (inputLock) {
                if (!running) {
                    break;
                }
                c = codec;
                if (c == null) {
                    break;
                }
            }
            int index;
            try {
                index = c.dequeueOutputBuffer(info, 10_000);
            } catch (IllegalStateException e) {
                break;
            }
            if (index == MediaCodec.INFO_TRY_AGAIN_LATER) {
                continue;
            }
            if (index == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                MediaFormat fmt = c.getOutputFormat();
                if (!configSent && callback != null) {
                    ByteBuffer csd = fmt.getByteBuffer("csd-0");
                    callback.onConfig(
                            CastConfig.AUDIO_SAMPLE_RATE,
                            CastConfig.AUDIO_CHANNEL_COUNT,
                            csd != null ? toArray(csd) : new byte[0]);
                    configSent = true;
                }
                continue;
            }
            if (index < 0) {
                continue;
            }
            ByteBuffer buffer = c.getOutputBuffer(index);
            if (buffer != null && info.size > 0 && (info.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) == 0) {
                byte[] data = new byte[info.size];
                buffer.position(info.offset);
                buffer.limit(info.offset + info.size);
                buffer.get(data);
                if (callback != null) {
                    callback.onEncodedFrame(info.presentationTimeUs, data);
                }
            }
            try {
                c.releaseOutputBuffer(index, false);
            } catch (IllegalStateException ignored) {
                break;
            }
        }
    }

    private static byte[] toArray(ByteBuffer buffer) {
        byte[] arr = new byte[buffer.remaining()];
        buffer.duplicate().get(arr);
        return arr;
    }
}
