package com.foxx.androidcast.sender.calibration;

/*********************************************************************
 * CalibrationCastDriver.java
 * Created at: Sat 16 May 2026 18:50:36 +0200
 * Updated at: Wed 20 May 2026 15:17:13 +0200 by Anton Afanasyeu <a.afanasieff@gmail.com>
 * Commit: 5d8e82d2e60a21fff3138d2a394ee4e8b4c6dcb8
 * class CalibrationCastDriver
 * Contributors:
 *    - Anton Afanasyeu <a.afanasieff@gmail.com> (3 commits, 206 lines)
 *    - Cursor Agent (project assistant)
 * Digest: SHA256	54293eb993f043396f4f41e4ff26c86120572b482cc5e0d2b02489a8ede8c904
**********************************************************************/
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.os.Handler;
import android.os.HandlerThread;
import android.util.Log;
import android.view.Surface;

import com.foxx.androidcast.media.PcmLevelAnalyzer;
import com.foxx.androidcast.media.codec.VideoEncoderSink;

/**
 * Renders calibration chart + VU border into the video encoder (GLES surface or YUV buffer fallback).
 */
public final class CalibrationCastDriver {
    private static final String TAG = "CalibrationCastDriver";
    private static final int EGL_FAIL_THRESHOLD = 5;

    public interface FallbackHost {
        /** Recreate encoder with {@link VideoEncoderSink#prepareBufferInput} after GLES failure. */
        void onCalibrationNeedBufferEncoder();
    }

    private enum FeedMode {
        GLES_SURFACE,
        YUV_BUFFER
    }

    private final Context appContext;
    private final HandlerThread thread;
    private final Handler handler;
    private final FallbackHost fallbackHost;
    private CalibrationEglRenderer eglRenderer;
    private VideoEncoderSink videoEncoder;
    private Bitmap chartBitmap;
    private Bitmap frameBitmap;
    private Canvas frameCanvas;
    private int width;
    private int height;
    private float density;
    private int scrollPx;
    private volatile float vuLevel;
    private volatile boolean running;
    private Runnable tick;
    private long encodeStartNs;
    private FeedMode feedMode = FeedMode.YUV_BUFFER;
    private int eglFailures;
    private boolean fallbackRequested;

    public CalibrationCastDriver(Context context) {
        this(context, null);
    }

    public CalibrationCastDriver(Context context, FallbackHost fallbackHost) {
        appContext = context.getApplicationContext();
        this.fallbackHost = fallbackHost;
        thread = new HandlerThread("CalibrationCast");
        thread.start();
        handler = new Handler(thread.getLooper());
    }

    /** Preferred path: GLES into encoder {@link Surface}. */
    public void attachSurface(VideoEncoderSink encoder, Surface encoderSurface, int width, int height, float density) {
        this.videoEncoder = encoder;
        initBitmaps(width, height, density);
        eglFailures = 0;
        fallbackRequested = false;
        try {
            eglRenderer = new CalibrationEglRenderer();
            eglRenderer.start(encoderSurface, width, height);
            feedMode = FeedMode.GLES_SURFACE;
            Log.i(TAG, "Calibration GLES " + width + "x" + height);
        } catch (Exception e) {
            Log.w(TAG, "GLES init failed: " + e.getMessage());
            releaseEgl();
            feedMode = FeedMode.YUV_BUFFER;
            requestBufferEncoderFallback();
        }
        running = true;
        encodeStartNs = System.nanoTime();
        startTick();
    }

    /** Reliable path when GLES cannot use the encoder surface (e.g. EGL_BAD_ACCESS on MTK). */
    public void attachBuffer(VideoEncoderSink encoder, int width, int height, float density) {
        this.videoEncoder = encoder;
        releaseEgl();
        feedMode = FeedMode.YUV_BUFFER;
        initBitmaps(width, height, density);
        running = true;
        encodeStartNs = System.nanoTime();
        startTick();
        Log.i(TAG, "Calibration buffer feed " + width + "x" + height);
    }

    private void initBitmaps(int w, int h, float density) {
        this.width = w;
        this.height = h;
        this.density = density;
        scrollPx = 0;
        chartBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
        Canvas chartCanvas = new Canvas(chartBitmap);
        TvCalibrationGenerator.drawChart(chartCanvas, w, h, appContext);
        frameBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
        frameCanvas = new Canvas(frameBitmap);
    }

    public void setVuFromPcm(byte[] pcm, int length) {
        PcmLevelAnalyzer.Levels levels = PcmLevelAnalyzer.analyze(pcm, length);
        float instant = Math.max(levels.peak, levels.rms);
        if (levels.silent) {
            vuLevel *= 0.88f;
        } else {
            vuLevel = instant * 0.7f + vuLevel * 0.3f;
        }
    }

    private void startTick() {
        tick = new Runnable() {
            @Override
            public void run() {
                if (!running || frameCanvas == null) {
                    return;
                }
                long frameStart = System.nanoTime();
                drawFrame();
                scrollPx += Math.max(1, Math.round(1f * density));
                long elapsedMs = (System.nanoTime() - frameStart) / 1_000_000L;
                long delay = feedMode == FeedMode.GLES_SURFACE
                        ? Math.max(1L, 33L - elapsedMs)
                        : Math.max(1L, 50L - elapsedMs);
                handler.postDelayed(this, delay);
            }
        };
        handler.post(tick);
    }

    private void drawFrame() {
        frameCanvas.drawBitmap(chartBitmap, 0f, 0f, null);
        TvCalibrationGenerator.drawBorderOverlay(
                frameCanvas, width, height, density, scrollPx, vuLevel);
        long ptsUs = (System.nanoTime() - encodeStartNs) / 1000L;
        if (feedMode == FeedMode.GLES_SURFACE && eglRenderer != null && eglRenderer.isReady()) {
            if (eglRenderer.draw(frameBitmap, System.nanoTime())) {
                eglFailures = 0;
                return;
            }
            eglFailures++;
            if (eglFailures >= EGL_FAIL_THRESHOLD) {
                Log.w(TAG, "GLES feed failed " + eglFailures + " times — switching to buffer");
                releaseEgl();
                feedMode = FeedMode.YUV_BUFFER;
                requestBufferEncoderFallback();
            }
            return;
        }
        if (videoEncoder != null && feedMode == FeedMode.YUV_BUFFER) {
            videoEncoder.queueBitmapFrame(frameBitmap, ptsUs);
        }
    }

    private void requestBufferEncoderFallback() {
        if (fallbackRequested || fallbackHost == null) {
            return;
        }
        fallbackRequested = true;
        handler.post(() -> fallbackHost.onCalibrationNeedBufferEncoder());
    }

    private void releaseEgl() {
        if (eglRenderer != null) {
            eglRenderer.release();
            eglRenderer = null;
        }
    }

    public void stop() {
        running = false;
        handler.removeCallbacksAndMessages(null);
        releaseEgl();
        if (chartBitmap != null) {
            chartBitmap.recycle();
            chartBitmap = null;
        }
        if (frameBitmap != null) {
            frameBitmap.recycle();
            frameBitmap = null;
        }
        frameCanvas = null;
        videoEncoder = null;
    }

    public void release() {
        stop();
        thread.quitSafely();
    }
}
