package com.journeyapps.barcodescanner;

import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.SurfaceTexture;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.TextureView;
import android.view.ViewGroup;
import android.view.WindowManager;

import com.google.zxing.client.android.R;
import com.journeyapps.barcodescanner.camera.CameraInstance;
import com.journeyapps.barcodescanner.camera.CameraSettings;
import com.journeyapps.barcodescanner.camera.CameraSurface;
import com.journeyapps.barcodescanner.camera.CenterCropStrategy;
import com.journeyapps.barcodescanner.camera.FitCenterStrategy;
import com.journeyapps.barcodescanner.camera.DisplayConfiguration;
import com.journeyapps.barcodescanner.camera.FitXYStrategy;
import com.journeyapps.barcodescanner.camera.PreviewScalingStrategy;

import java.util.ArrayList;
import java.util.List;

/**
 * CameraPreview is a view that handles displaying of a camera preview on a SurfaceView. It is
 * intended to be used as a base for realtime processing of camera images, e.g. barcode decoding
 * or OCR, although none of this happens in CameraPreview itself.
 *
 * The camera is managed on a separate thread, using CameraInstance.
 *
 * Two methods MUST be called on CameraPreview to manage its state:
 * 1. resume() - initialize the camera and start the preview. Call from the Activity's onResume().
 * 2. pause() - stop the preview and release any resources. Call from the Activity's onPause().
 *
 * Startup sequence:
 *
 * 1. Create SurfaceView.
 * 2. open camera.
 * 2. layout this container, to get size
 * 3. set display config, according to the container size
 * 4. configure()
 * 5. wait for preview size to be ready
 * 6. set surface size according to preview size
 * 7. set surface and start preview
 */
public class CameraPreview extends ViewGroup {
    public interface StateListener {
        /**
         * Preview and frame sizes are determined.
         */
        void previewSized();

        /**
         * Preview has started.
         */
        void previewStarted();

        /**
         * Preview has stopped.
         */
        void previewStopped();

        /**
         * The camera has errored, and cannot display a preview.
         *
         * @param error the error
         */
        void cameraError(Exception error);

        /**
         * The camera has been closed.
         */
        void cameraClosed();
    }

    private static final String TAG = CameraPreview.class.getSimpleName();

    private CameraInstance cameraInstance;

    private WindowManager windowManager;

    private Handler stateHandler;

    private boolean useTextureView = false;

    private SurfaceView surfaceView;
    private TextureView textureView;

    private boolean previewActive = false;

    private RotationListener rotationListener;
    private int openedOrientation = -1;

    // Delay after rotation change is detected before we reorientate ourselves.
    // This is to avoid double-reinitialization when the Activity is destroyed and recreated.
    private static final int ROTATION_LISTENER_DELAY_MS = 250;

    private List<StateListener> stateListeners = new ArrayList<>();

    private DisplayConfiguration displayConfiguration;
    private CameraSettings cameraSettings = new CameraSettings();

    // Size of this container, non-null after layout is performed
    private Size containerSize;

    // Size of the preview resolution
    private Size previewSize;

    // Rect placing the preview surface
    private Rect surfaceRect;

    // Size of the current surface. non-null if the surface is ready
    private Size currentSurfaceSize;

    // Framing rectangle relative to this view
    private Rect framingRect = null;

    // Framing rectangle relative to the preview resolution
    private Rect previewFramingRect = null;

    // Size of the framing rectangle. If null, defaults to using a margin percentage.
    private Size framingRectSize = null;

    // Fraction of the width / heigth to use as a margin. This fraction is used on each size, so
    // must be smaller than 0.5;
    private double marginFraction = 0.1d;

    private PreviewScalingStrategy previewScalingStrategy = null;

    private boolean torchOn = false;

    @TargetApi(14)
    private TextureView.SurfaceTextureListener surfaceTextureListener() {
        // Cannot initialize automatically, since we may be API < 14
        return new TextureView.SurfaceTextureListener() {
            @Override
            public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
                onSurfaceTextureSizeChanged(surface, width, height);
            }

            @Override
            public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
                currentSurfaceSize = new Size(width, height);
                startPreviewIfReady();
            }

            @Override
            public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
                return false;
            }

            @Override
            public void onSurfaceTextureUpdated(SurfaceTexture surface) {

            }
        };
    }

    private final SurfaceHolder.Callback surfaceCallback = new SurfaceHolder.Callback() {

        @Override
        public void surfaceCreated(SurfaceHolder holder) {

        }

        @Override
        public void surfaceDestroyed(SurfaceHolder holder) {
            currentSurfaceSize = null;
        }

        @Override
        public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
            if (holder == null) {
                Log.e(TAG, "*** WARNING *** surfaceChanged() gave us a null surface!");
                return;
            }
            currentSurfaceSize = new Size(width, height);
            startPreviewIfReady();
        }
    };

    private final Handler.Callback stateCallback = new Handler.Callback() {
        @Override
        public boolean handleMessage(Message message) {
            if (message.what == R.id.zxing_prewiew_size_ready) {
                previewSized((Size) message.obj);
                return true;
            } else if (message.what == R.id.zxing_camera_error) {
                Exception error = (Exception) message.obj;

                if (isActive()) {
                    // This check prevents multiple errors from begin passed through.
                    pause();
                    fireState.cameraError(error);
                }
            } else if(message.what == R.id.zxing_camera_closed) {
                fireState.cameraClosed();
            }
            return false;
        }
    };

    private RotationCallback rotationCallback = new RotationCallback() {
        @Override
        public void onRotationChanged(int rotation) {
            // Make sure this is run on the main thread.
            stateHandler.postDelayed(new Runnable() {
                @Override
                public void run() {
                    rotationChanged();
                }
            }, ROTATION_LISTENER_DELAY_MS);
        }
    };

    public CameraPreview(Context context) {
        super(context);
        initialize(context, null, 0, 0);
    }

    public CameraPreview(Context context, AttributeSet attrs) {
        super(context, attrs);
        initialize(context, attrs, 0, 0);
    }

    public CameraPreview(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initialize(context, attrs, defStyleAttr, 0);
    }

    private void initialize(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        if (getBackground() == null) {
            // Default to SurfaceView colour, so that there are less changes.
            setBackgroundColor(Color.BLACK);
        }

        initializeAttributes(attrs);

        windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);

        stateHandler = new Handler(stateCallback);

        rotationListener = new RotationListener();
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();

        setupSurfaceView();
    }

    /**
     * Initialize from XML attributes.
     *
     * @param attrs the attributes
     */
    protected void initializeAttributes(AttributeSet attrs) {
        TypedArray styledAttributes = getContext().obtainStyledAttributes(attrs, R.styleable.zxing_camera_preview);

        int framingRectWidth = (int) styledAttributes.getDimension(R.styleable.zxing_camera_preview_zxing_framing_rect_width, -1);
        int framingRectHeight = (int) styledAttributes.getDimension(R.styleable.zxing_camera_preview_zxing_framing_rect_height, -1);

        if (framingRectWidth > 0 && framingRectHeight > 0) {
            this.framingRectSize = new Size(framingRectWidth, framingRectHeight);
        }

        this.useTextureView = styledAttributes.getBoolean(R.styleable.zxing_camera_preview_zxing_use_texture_view, true);

        // See zxing_attrs.xml for the enum values
        int scalingStrategyNumber = styledAttributes.getInteger(R.styleable.zxing_camera_preview_zxing_preview_scaling_strategy, -1);
        if(scalingStrategyNumber == 1) {
            previewScalingStrategy = new CenterCropStrategy();
        } else if(scalingStrategyNumber == 2) {
            previewScalingStrategy = new FitCenterStrategy();
        } else if(scalingStrategyNumber == 3) {
            previewScalingStrategy = new FitXYStrategy();
        }

        styledAttributes.recycle();
    }

    private void rotationChanged() {
        // Confirm that it did actually change
        if(isActive() && getDisplayRotation() != openedOrientation) {
            pause();
            resume();
        }
    }

    @SuppressWarnings("deprecation")
    @SuppressLint("NewAPI")
    private void setupSurfaceView() {
        if(useTextureView && Build.VERSION.SDK_INT >= 14) {
            textureView = new TextureView(getContext());
            textureView.setSurfaceTextureListener(surfaceTextureListener());
            addView(textureView);
        } else {
            surfaceView = new SurfaceView(getContext());
            if (Build.VERSION.SDK_INT < 11) {
                surfaceView.getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
            }
            surfaceView.getHolder().addCallback(surfaceCallback);
            addView(surfaceView);
        }
    }

    /**
     * Add a listener to be notified of changes to the preview state, as well as camera errors.
     *
     * @param listener the listener
     */
    public void addStateListener(StateListener listener) {
        stateListeners.add(listener);
    }

    private final StateListener fireState = new StateListener() {
        @Override
        public void previewSized() {
            for (StateListener listener : stateListeners) {
                listener.previewSized();
            }
        }

        @Override
        public void previewStarted() {
            for (StateListener listener : stateListeners) {
                listener.previewStarted();
            }

        }

        @Override
        public void previewStopped() {
            for (StateListener listener : stateListeners) {
                listener.previewStopped();
            }
        }

        @Override
        public void cameraError(Exception error) {
            for (StateListener listener : stateListeners) {
                listener.cameraError(error);
            }
        }

        @Override
        public void cameraClosed() {
            for (StateListener listener : stateListeners) {
                listener.cameraClosed();
            }
        }
    };

    private void calculateFrames() {
        if (containerSize == null || previewSize == null || displayConfiguration == null) {
            previewFramingRect = null;
            framingRect = null;
            surfaceRect = null;
            throw new IllegalStateException("containerSize or previewSize is not set yet");
        }

        int previewWidth = previewSize.width;
        int previewHeight = previewSize.height;

        int width = containerSize.width;
        int height = containerSize.height;

        surfaceRect = displayConfiguration.scalePreview(previewSize);

        Rect container = new Rect(0, 0, width, height);
        framingRect = calculateFramingRect(container, surfaceRect);
        Rect frameInPreview = new Rect(framingRect);
        frameInPreview.offset(-surfaceRect.left, -surfaceRect.top);

        previewFramingRect = new Rect(frameInPreview.left * previewWidth / surfaceRect.width(),
                frameInPreview.top * previewHeight / surfaceRect.height(),
                frameInPreview.right * previewWidth / surfaceRect.width(),
                frameInPreview.bottom * previewHeight / surfaceRect.height());

        if (previewFramingRect.width() <= 0 || previewFramingRect.height() <= 0) {
            previewFramingRect = null;
            framingRect = null;
            Log.w(TAG, "Preview frame is too small");
        } else {
            fireState.previewSized();
        }
    }

    /**
     * Call this on the main thread, while the preview is running.
     *
     * @param on true to turn on the torch
     */
    public void setTorch(boolean on) {
        torchOn = on;
        if (cameraInstance != null) {
            cameraInstance.setTorch(on);
        }
    }

    private void containerSized(Size containerSize) {
        this.containerSize = containerSize;
        if (cameraInstance != null) {
            if (cameraInstance.getDisplayConfiguration() == null) {
                displayConfiguration = new DisplayConfiguration(getDisplayRotation(), containerSize);
                displayConfiguration.setPreviewScalingStrategy(getPreviewScalingStrategy());
                cameraInstance.setDisplayConfiguration(displayConfiguration);
                cameraInstance.configureCamera();
                if(torchOn) {
                    cameraInstance.setTorch(torchOn);
                }
            }
        }
    }

    /**
     * Override the preview scaling strategy.
     *
     * @param previewScalingStrategy null for the default
     */
    public void setPreviewScalingStrategy(PreviewScalingStrategy previewScalingStrategy) {
        this.previewScalingStrategy = previewScalingStrategy;
    }

    /**
     * Override this to specify a different preview scaling strategy.
     */
    public PreviewScalingStrategy getPreviewScalingStrategy() {
        if(previewScalingStrategy != null) {
            return previewScalingStrategy;
        }

        // If we are using SurfaceTexture, it is safe to use centerCrop.
        // For SurfaceView, it's better to use fitCenter, otherwise the preview may overlap to
        // other views.
        if(textureView != null) {
            return new CenterCropStrategy();
        } else {
            return new FitCenterStrategy();
        }

    }

    private void previewSized(Size size) {
        this.previewSize = size;
        if (containerSize != null) {
            calculateFrames();
            requestLayout();
            startPreviewIfReady();
        }
    }

    /**
     * Calculate transformation for the TextureView.
     *
     * An identity matrix would cause the preview to be scaled up/down to fill the TextureView.
     *
     * @param textureSize the size of the textureView
     * @param previewSize the camera preview resolution
     * @return the transform matrix for the TextureView
     */
    protected Matrix calculateTextureTransform(Size textureSize, Size previewSize) {
        float ratioTexture = (float) textureSize.width / (float) textureSize.height;
        float ratioPreview = (float) previewSize.width / (float) previewSize.height;

        float scaleX;
        float scaleY;

        // We scale so that either width or height fits exactly in the TextureView, and the other
        // is bigger (cropped).
        if (ratioTexture < ratioPreview) {
            scaleX = ratioPreview / ratioTexture;
            scaleY = 1;
        } else {
            scaleX = 1;
            scaleY = ratioTexture / ratioPreview;
        }

        Matrix matrix = new Matrix();

        matrix.setScale(scaleX, scaleY);

        // Center the preview
        float scaledWidth = textureSize.width * scaleX;
        float scaledHeight = textureSize.height * scaleY;
        float dx = (textureSize.width - scaledWidth) / 2;
        float dy = (textureSize.height - scaledHeight) / 2;

        // Perform the translation on the scaled preview
        matrix.postTranslate(dx, dy);

        return matrix;
    }

    private void startPreviewIfReady() {
        if (currentSurfaceSize != null && previewSize != null && surfaceRect != null) {
            if (surfaceView != null && currentSurfaceSize.equals(new Size(surfaceRect.width(), surfaceRect.height()))) {
                startCameraPreview(new CameraSurface(surfaceView.getHolder()));
            } else if(textureView != null && Build.VERSION.SDK_INT >= 14 && textureView.getSurfaceTexture() != null) {
                if(previewSize != null) {
                    Matrix transform = calculateTextureTransform(new Size(textureView.getWidth(), textureView.getHeight()), previewSize);
                    textureView.setTransform(transform);
                }

                startCameraPreview(new CameraSurface(textureView.getSurfaceTexture()));
            } else {
                // Surface is not the correct size yet
            }
        }
    }

    @SuppressLint("DrawAllocation")
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        containerSized(new Size(r - l, b - t));

        if(surfaceView != null) {
            if (surfaceRect == null) {
                // Match the container, to reduce the risk of issues. The preview should never be drawn
                // while the surface has this size.
                surfaceView.layout(0, 0, getWidth(), getHeight());
            } else {
                surfaceView.layout(surfaceRect.left, surfaceRect.top, surfaceRect.right, surfaceRect.bottom);
            }
        } else if(textureView != null && Build.VERSION.SDK_INT >= 14) {
            textureView.layout(0, 0, getWidth(), getHeight());
        }
    }

    /**
     * The framing rectangle, relative to this view. Use to draw the rectangle.
     *
     * Will never be null while the preview is active.
     *
     * @return the framing rect, or null
     * @see #isPreviewActive()
     */
    public Rect getFramingRect() {
        return framingRect;
    }

    /**
     * The framing rect, relative to the camera preview resolution.
     *
     * Will never be null while the preview is active.
     *
     * @return the preview rect, or null
     * @see #isPreviewActive()
     */
    public Rect getPreviewFramingRect() {
        return previewFramingRect;
    }

    /**
     * @return the CameraSettings currently in use
     */
    public CameraSettings getCameraSettings() {
        return cameraSettings;
    }

    /**
     * Set the CameraSettings. Use this to select a different camera, change exposure and torch
     * settings, and some other options.
     *
     * This has no effect if the camera is already open.
     *
     * @param cameraSettings the new settings
     */
    public void setCameraSettings(CameraSettings cameraSettings) {
        this.cameraSettings = cameraSettings;
    }

    /**
     * Start the camera preview and decoding. Typically this should be called from the Activity's
     * onResume() method.
     *
     * Call from UI thread only.
     */
    public void resume() {
        // This must be safe to call multiple times
        Util.validateMainThread();
        Log.d(TAG, "resume()");

        // initCamera() does nothing if called twice, but does log a warning
        initCamera();

        if (currentSurfaceSize != null) {
            // The activity was paused but not stopped, so the surface still exists. Therefore
            // surfaceCreated() won't be called, so init the camera here.
            startPreviewIfReady();
        } else if(surfaceView != null) {
            // Install the callback and wait for surfaceCreated() to init the camera.
            surfaceView.getHolder().addCallback(surfaceCallback);
        } else if(textureView != null && Build.VERSION.SDK_INT >= 14) {
            if(textureView.isAvailable()) {
                surfaceTextureListener().onSurfaceTextureAvailable(textureView.getSurfaceTexture(), textureView.getWidth(), textureView.getHeight());
            } else {
                textureView.setSurfaceTextureListener(surfaceTextureListener());
            }
        }

        // To trigger surfaceSized again
        requestLayout();
        rotationListener.listen(getContext(), rotationCallback);
    }

    /**
     * Pause scanning and the camera preview. Typically this should be called from the Activity's
     * onPause() method.
     *
     * Call from UI thread only.
     */
    public void pause() {
        // This must be safe to call multiple times.
        Util.validateMainThread();
        Log.d(TAG, "pause()");

        openedOrientation = -1;
        if (cameraInstance != null) {
            cameraInstance.close();
            cameraInstance = null;
            previewActive = false;
        } else {
            stateHandler.sendEmptyMessage(R.id.zxing_camera_closed);
        }
        if (currentSurfaceSize == null && surfaceView != null) {
            SurfaceHolder surfaceHolder = surfaceView.getHolder();
            surfaceHolder.removeCallback(surfaceCallback);
        }
        if(currentSurfaceSize == null && textureView != null && Build.VERSION.SDK_INT >= 14) {
            textureView.setSurfaceTextureListener(null);
        }

        this.containerSize = null;
        this.previewSize = null;
        this.previewFramingRect = null;
        rotationListener.stop();

        fireState.previewStopped();
    }

    /**
     * Pause scanning and preview; waiting for the Camera to be closed.
     *
     * This blocks the main thread.
     */
    public void pauseAndWait() {
        CameraInstance instance = getCameraInstance();
        pause();
        long startTime = System.nanoTime();
        while(instance != null && !instance.isCameraClosed()) {
            if(System.nanoTime() - startTime > 2000000000) {
                // Don't wait for longer than 2 seconds
                break;
            }
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                break;
            }
        }
    }

    public Size getFramingRectSize() {
        return framingRectSize;
    }

    /**
     * Set an exact size for the framing rectangle. It will be centered in the view.
     *
     * @param framingRectSize the size
     */
    public void setFramingRectSize(Size framingRectSize) {
        this.framingRectSize = framingRectSize;
    }

    public double getMarginFraction() {
        return marginFraction;
    }

    /**
     * The the fraction of the width/height of view to be used as a margin for the framing rect.
     * This is ignored if framingRectSize is specified.
     *
     * @param marginFraction the fraction
     */
    public void setMarginFraction(double marginFraction) {
        if(marginFraction >= 0.5d) {
            throw new IllegalArgumentException("The margin fraction must be less than 0.5");
        }
        this.marginFraction = marginFraction;
    }

    public boolean isUseTextureView() {
        return useTextureView;
    }

    /**
     * Set to true to use TextureView instead of SurfaceView.
     *
     * Will only have an effect on API >= 14.
     *
     * @param useTextureView true to use TextureView.
     */
    public void setUseTextureView(boolean useTextureView) {
        this.useTextureView = useTextureView;
    }

    /**
     * Considered active if between resume() and pause().
     *
     * @return true if active
     */
    protected boolean isActive() {
        return cameraInstance != null;
    }

    private int getDisplayRotation() {
        return windowManager.getDefaultDisplay().getRotation();
    }

    private void initCamera() {
        if (cameraInstance != null) {
            Log.w(TAG, "initCamera called twice");
            return;
        }

        cameraInstance = createCameraInstance();

        cameraInstance.setReadyHandler(stateHandler);
        cameraInstance.open();

        // Keep track of the orientation we opened at, so that we don't reopen the camera if we
        // don't need to.
        openedOrientation = getDisplayRotation();
    }

    /**
     * Create a new CameraInstance.
     *
     * Override to use a custom CameraInstance.
     *
     * @return a new CameraInstance
     */
    protected CameraInstance createCameraInstance() {
        CameraInstance cameraInstance = new CameraInstance(getContext());
        cameraInstance.setCameraSettings(cameraSettings);
        return cameraInstance;
    }

    private void startCameraPreview(CameraSurface surface) {
        if (!previewActive && cameraInstance != null) {
            Log.i(TAG, "Starting preview");
            cameraInstance.setSurface(surface);
            cameraInstance.startPreview();
            previewActive = true;

            previewStarted();
            fireState.previewStarted();
        }
    }

    /**
     * Called when the preview is started. Override this to start decoding work.
     */
    protected void previewStarted() {

    }

    /**
     * Get the current CameraInstance. This may be null, and may change when
     * pausing / resuming the preview.
     *
     * While the preview is active, getCameraInstance() will never be null.
     *
     * @return the current CameraInstance
     * @see #isPreviewActive()
     */
    public CameraInstance getCameraInstance() {
        return cameraInstance;
    }

    /**
     * The preview typically starts being active a while after calling resume(), and stops
     * when calling pause().
     *
     * @return true if the preview is active
     */
    public boolean isPreviewActive() {
        return previewActive;
    }

    /**
     * Calculate framing rectangle, relative to the preview frame.
     *
     * Note that the SurfaceView may be larger than the container.
     *
     * Override this for more control over the framing rect calculations.
     *
     * @param container this container, with left = top = 0
     * @param surface   the SurfaceView, relative to this container
     * @return the framing rect, relative to this container
     */
    protected Rect calculateFramingRect(Rect container, Rect surface) {
        // intersection is the part of the container that is used for the preview
        Rect intersection = new Rect(container);
        boolean intersects = intersection.intersect(surface);

        if(framingRectSize != null) {
            // Specific size is specified. Make sure it's not larger than the container or surface.
            int horizontalMargin = Math.max(0, (intersection.width() - framingRectSize.width) / 2);
            int verticalMargin = Math.max(0, (intersection.height() - framingRectSize.height) / 2);
            intersection.inset(horizontalMargin, verticalMargin);
            return intersection;
        }
        // margin as 10% (default) of the smaller of width, height
        int margin = (int)Math.min(intersection.width() * marginFraction, intersection.height() * marginFraction);
        intersection.inset(margin, margin);
        if (intersection.height() > intersection.width()) {
            // We don't want a frame that is taller than wide.
            intersection.inset(0, (intersection.height() - intersection.width()) / 2);
        }
        return intersection;
    }

    @Override
    protected Parcelable onSaveInstanceState() {
        Parcelable superState = super.onSaveInstanceState();

        Bundle myState = new Bundle();
        myState.putParcelable("super", superState);
        myState.putBoolean("torch", torchOn);
        return myState;
    }

    @Override
    protected void onRestoreInstanceState(Parcelable state) {
        if(!(state instanceof Bundle)) {
            super.onRestoreInstanceState(state);
            return;
        }
        Bundle myState = (Bundle)state;
        Parcelable superState = myState.getParcelable("super");
        super.onRestoreInstanceState(superState);
        boolean torch = myState.getBoolean("torch");
        setTorch(torch);
    }

    /**
     *
     * @return true if the camera has been closed in a background thread.
     */
    public boolean isCameraClosed() {
        return cameraInstance == null || cameraInstance.isCameraClosed();
    }
}
