package com.foxx.androidcast.sender.camera;

/*********************************************************************
 * CameraCastCapture.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 CameraCastCapture
 * Contributors:
 *    - Anton Afanasyeu <a.afanasieff@gmail.com> (3 commits, 167 lines)
 *    - Cursor Agent (project assistant)
 * Digest: SHA256	0cea27cf579edb987b0ea25fd6198cc86daf4b86adf48d8080cfb3f141c9613d
**********************************************************************/
import android.content.Context;
import android.graphics.SurfaceTexture;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCaptureSession;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CameraManager;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.params.StreamConfigurationMap;
import android.os.Handler;
import android.os.HandlerThread;
import android.util.Log;
import android.util.Size;
import android.view.Surface;

import java.util.Collections;

/** Camera2 preview into the video encoder input {@link Surface}. */
public final class CameraCastCapture {
    private static final String TAG = "CameraCastCapture";

    private final Context appContext;
    private HandlerThread thread;
    private Handler handler;
    private CameraDevice camera;
    private CameraCaptureSession session;
    private String cameraId;

    public CameraCastCapture(Context context) {
        appContext = context.getApplicationContext();
    }

    public void start(Surface encoderSurface, int targetWidth, int targetHeight, boolean useFrontCamera)
            throws CameraAccessException {
        stop();
        thread = new HandlerThread("CameraCast");
        thread.start();
        handler = new Handler(thread.getLooper());
        CameraManager mgr = appContext.getSystemService(CameraManager.class);
        cameraId = chooseCameraId(mgr, useFrontCamera);
        if (cameraId == null) {
            throw new CameraAccessException(CameraAccessException.CAMERA_ERROR,
                    useFrontCamera ? "No front camera available" : "No back camera available");
        }
        Size preview = choosePreviewSize(mgr, cameraId, targetWidth, targetHeight);
        Log.i(TAG, "Opening camera " + cameraId + " preview " + preview.getWidth() + "x" + preview.getHeight());
        mgr.openCamera(cameraId, new CameraDevice.StateCallback() {
            @Override
            public void onOpened(CameraDevice device) {
                camera = device;
                try {
                    createSession(device, encoderSurface, preview);
                } catch (CameraAccessException e) {
                    Log.e(TAG, "Session failed", e);
                    stop();
                }
            }

            @Override
            public void onDisconnected(CameraDevice device) {
                device.close();
                camera = null;
            }

            @Override
            public void onError(CameraDevice device, int error) {
                Log.e(TAG, "Camera error " + error);
                device.close();
                camera = null;
            }
        }, handler);
    }

    private void createSession(CameraDevice device, Surface encoderSurface, Size preview)
            throws CameraAccessException {
        CaptureRequest.Builder builder = device.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);
        builder.addTarget(encoderSurface);
        device.createCaptureSession(Collections.singletonList(encoderSurface),
                new CameraCaptureSession.StateCallback() {
                    @Override
                    public void onConfigured(CameraCaptureSession captureSession) {
                        session = captureSession;
                        try {
                            builder.set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_AUTO);
                            captureSession.setRepeatingRequest(builder.build(), null, handler);
                            Log.i(TAG, "Camera preview running");
                        } catch (CameraAccessException e) {
                            Log.e(TAG, "Repeating request failed", e);
                        }
                    }

                    @Override
                    public void onConfigureFailed(CameraCaptureSession captureSession) {
                        Log.e(TAG, "Camera session configure failed");
                    }
                }, handler);
    }

    private static String chooseCameraId(CameraManager mgr, boolean useFrontCamera) throws CameraAccessException {
        int want = useFrontCamera
                ? CameraCharacteristics.LENS_FACING_FRONT
                : CameraCharacteristics.LENS_FACING_BACK;
        for (String id : mgr.getCameraIdList()) {
            CameraCharacteristics chars = mgr.getCameraCharacteristics(id);
            Integer facing = chars.get(CameraCharacteristics.LENS_FACING);
            if (facing != null && facing == want) {
                return id;
            }
        }
        String[] ids = mgr.getCameraIdList();
        return ids.length > 0 ? ids[0] : null;
    }

    private static Size choosePreviewSize(CameraManager mgr, String id, int targetW, int targetH)
            throws CameraAccessException {
        CameraCharacteristics chars = mgr.getCameraCharacteristics(id);
        StreamConfigurationMap map = chars.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
        if (map == null) {
            return new Size(targetW, targetH);
        }
        Size[] sizes = map.getOutputSizes(SurfaceTexture.class);
        if (sizes == null || sizes.length == 0) {
            return new Size(targetW, targetH);
        }
        Size best = sizes[0];
        long bestScore = Long.MAX_VALUE;
        for (Size s : sizes) {
            long score = Math.abs((long) s.getWidth() * s.getHeight() - (long) targetW * targetH);
            if (score < bestScore) {
                bestScore = score;
                best = s;
            }
        }
        return best;
    }

    public void stop() {
        if (session != null) {
            try {
                session.close();
            } catch (Exception ignored) {
            }
            session = null;
        }
        if (camera != null) {
            try {
                camera.close();
            } catch (Exception ignored) {
            }
            camera = null;
        }
        if (thread != null) {
            thread.quitSafely();
            thread = null;
        }
        handler = null;
    }
}
