/*
 * MIT License
 *
 * Copyright (c) 2017 Yuriy Budiyev [[email protected]]
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package com.budiyev.android.codescanner;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;

import android.Manifest;
import android.app.Activity;
import android.content.Context;
import android.graphics.ImageFormat;
import android.hardware.Camera;
import android.hardware.Camera.CameraInfo;
import android.hardware.Camera.Parameters;
import android.os.Handler;
import android.os.Process;
import android.view.SurfaceHolder;

import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.zxing.BarcodeFormat;

/**
 * Code scanner.
 * Supports portrait and landscape screen orientations, back and front facing cameras,
 * auto focus and flash light control, touch focus, viewfinder customization.
 *
 * @see CodeScannerView
 * @see BarcodeFormat
 */
public final class CodeScanner {

    /**
     * All supported barcode formats
     */
    public static final List<BarcodeFormat> ALL_FORMATS =
            Collections.unmodifiableList(Arrays.asList(BarcodeFormat.values()));

    /**
     * One dimensional barcode formats
     */
    public static final List<BarcodeFormat> ONE_DIMENSIONAL_FORMATS = Collections.unmodifiableList(
            Arrays.asList(BarcodeFormat.CODABAR, BarcodeFormat.CODE_39, BarcodeFormat.CODE_93,
                    BarcodeFormat.CODE_128, BarcodeFormat.EAN_8, BarcodeFormat.EAN_13,
                    BarcodeFormat.ITF, BarcodeFormat.RSS_14, BarcodeFormat.RSS_EXPANDED,
                    BarcodeFormat.UPC_A, BarcodeFormat.UPC_E, BarcodeFormat.UPC_EAN_EXTENSION));

    /**
     * Two dimensional barcode formats
     */
    public static final List<BarcodeFormat> TWO_DIMENSIONAL_FORMATS = Collections.unmodifiableList(
            Arrays.asList(BarcodeFormat.AZTEC, BarcodeFormat.DATA_MATRIX, BarcodeFormat.MAXICODE,
                    BarcodeFormat.PDF_417, BarcodeFormat.QR_CODE));

    /**
     * First back-facing camera
     */
    public static final int CAMERA_BACK = -1;

    /**
     * First front-facing camera
     */
    public static final int CAMERA_FRONT = -2;

    private static final List<BarcodeFormat> DEFAULT_FORMATS = ALL_FORMATS;
    private static final ScanMode DEFAULT_SCAN_MODE = ScanMode.SINGLE;
    private static final AutoFocusMode DEFAULT_AUTO_FOCUS_MODE = AutoFocusMode.SAFE;
    private static final boolean DEFAULT_AUTO_FOCUS_ENABLED = true;
    private static final boolean DEFAULT_TOUCH_FOCUS_ENABLED = true;
    private static final boolean DEFAULT_FLASH_ENABLED = false;
    private static final long DEFAULT_SAFE_AUTO_FOCUS_INTERVAL = 2000L;
    private static final int SAFE_AUTO_FOCUS_ATTEMPTS_THRESHOLD = 2;
    private final Object mInitializeLock = new Object();
    private final Context mContext;
    private final Handler mMainThreadHandler;
    private final CodeScannerView mScannerView;
    private final SurfaceHolder mSurfaceHolder;
    private final SurfaceHolder.Callback mSurfaceCallback;
    private final Camera.PreviewCallback mPreviewCallback;
    private final Camera.AutoFocusCallback mTouchFocusCallback;
    private final Camera.AutoFocusCallback mSafeAutoFocusCallback;
    private final Runnable mSafeAutoFocusTask;
    private final Runnable mStopPreviewTask;
    private final DecoderStateListener mDecoderStateListener;
    private volatile List<BarcodeFormat> mFormats = DEFAULT_FORMATS;
    private volatile ScanMode mScanMode = DEFAULT_SCAN_MODE;
    private volatile AutoFocusMode mAutoFocusMode = DEFAULT_AUTO_FOCUS_MODE;
    private volatile DecodeCallback mDecodeCallback = null;
    private volatile ErrorCallback mErrorCallback = null;
    private volatile DecoderWrapper mDecoderWrapper = null;
    private volatile boolean mInitialization = false;
    private volatile boolean mInitialized = false;
    private volatile boolean mStoppingPreview = false;
    private volatile boolean mAutoFocusEnabled = DEFAULT_AUTO_FOCUS_ENABLED;
    private volatile boolean mFlashEnabled = DEFAULT_FLASH_ENABLED;
    private volatile long mSafeAutoFocusInterval = DEFAULT_SAFE_AUTO_FOCUS_INTERVAL;
    private volatile int mCameraId = CAMERA_BACK;
    private volatile int mZoom = 0;
    private boolean mTouchFocusEnabled = DEFAULT_TOUCH_FOCUS_ENABLED;
    private boolean mTouchFocusing = false;
    private boolean mPreviewActive = false;
    private boolean mSafeAutoFocusing = false;
    private boolean mSafeAutoFocusTaskScheduled = false;
    private boolean mInitializationRequested = false;
    private int mSafeAutoFocusAttemptsCount = 0;
    private int mViewWidth = 0;
    private int mViewHeight = 0;

    /**
     * CodeScanner, associated with the first back-facing camera on the device
     *
     * @param context Context
     * @param view    A view to display the preview
     * @see CodeScannerView
     */
    @MainThread
    public CodeScanner(@NonNull final Context context, @NonNull final CodeScannerView view) {
        mContext = context;
        mScannerView = view;
        mSurfaceHolder = view.getPreviewView().getHolder();
        mMainThreadHandler = new Handler();
        mSurfaceCallback = new SurfaceCallback();
        mPreviewCallback = new PreviewCallback();
        mTouchFocusCallback = new TouchFocusCallback();
        mSafeAutoFocusCallback = new SafeAutoFocusCallback();
        mSafeAutoFocusTask = new SafeAutoFocusTask();
        mStopPreviewTask = new StopPreviewTask();
        mDecoderStateListener = new DecoderStateListener();
        mScannerView.setCodeScanner(this);
        mScannerView.setSizeListener(new ScannerSizeListener());
    }

    /**
     * CodeScanner, associated with particular hardware camera
     *
     * @param context  Context
     * @param view     A view to display the preview
     * @param cameraId Camera id (between {@code 0} and
     *                 {@link Camera#getNumberOfCameras()} - {@code 1})
     *                 or {@link #CAMERA_BACK} or {@link #CAMERA_FRONT}
     * @see CodeScannerView
     */
    @MainThread
    public CodeScanner(@NonNull final Context context, @NonNull final CodeScannerView view,
            final int cameraId) {
        this(context, view);
        mCameraId = cameraId;
    }

    /**
     * Get current camera id, or {@link #CAMERA_BACK} or {@link #CAMERA_FRONT}
     *
     * @see #setCamera
     */
    public int getCamera() {
        return mCameraId;
    }

    /**
     * Camera to use
     *
     * @param cameraId Camera id (between {@code 0} and
     *                 {@link Camera#getNumberOfCameras()} - {@code 1})
     *                 or {@link #CAMERA_BACK} or {@link #CAMERA_FRONT}
     */
    @MainThread
    public void setCamera(final int cameraId) {
        synchronized (mInitializeLock) {
            if (mCameraId != cameraId) {
                mCameraId = cameraId;
                if (mInitialized) {
                    final boolean previewActive = mPreviewActive;
                    releaseResources();
                    if (previewActive) {
                        initialize();
                    }
                }
            }
        }
    }

    /**
     * Get current list of formats to decode
     *
     * @see #setFormats(List)
     */
    @NonNull
    public List<BarcodeFormat> getFormats() {
        return mFormats;
    }

    /**
     * Formats, decoder to react to ({@link #ALL_FORMATS} by default)
     *
     * @param formats Formats
     * @see BarcodeFormat
     * @see #ALL_FORMATS
     * @see #ONE_DIMENSIONAL_FORMATS
     * @see #TWO_DIMENSIONAL_FORMATS
     */
    @MainThread
    public void setFormats(@NonNull final List<BarcodeFormat> formats) {
        synchronized (mInitializeLock) {
            mFormats = Objects.requireNonNull(formats);
            if (mInitialized) {
                final DecoderWrapper decoderWrapper = mDecoderWrapper;
                if (decoderWrapper != null) {
                    decoderWrapper.getDecoder().setFormats(formats);
                }
            }
        }
    }

    /**
     * Get current decode callback
     *
     * @see #setDecodeCallback
     */
    @Nullable
    public DecodeCallback getDecodeCallback() {
        return mDecodeCallback;
    }

    /**
     * Callback of decoding process
     *
     * @param decodeCallback Callback
     * @see DecodeCallback
     */
    public void setDecodeCallback(@Nullable final DecodeCallback decodeCallback) {
        synchronized (mInitializeLock) {
            mDecodeCallback = decodeCallback;
            if (mInitialized) {
                final DecoderWrapper decoderWrapper = mDecoderWrapper;
                if (decoderWrapper != null) {
                    decoderWrapper.getDecoder().setCallback(decodeCallback);
                }
            }
        }
    }

    /**
     * Get current error callback
     *
     * @see #setErrorCallback
     */
    @Nullable
    public ErrorCallback getErrorCallback() {
        return mErrorCallback;
    }

    /**
     * Camera initialization error callback.
     * If not set, an exception will be thrown when error will occur.
     *
     * @param errorCallback Callback
     * @see ErrorCallback#SUPPRESS
     * @see ErrorCallback
     */
    public void setErrorCallback(@Nullable final ErrorCallback errorCallback) {
        mErrorCallback = errorCallback;
    }

    /**
     * Get current scan mode
     *
     * @see #setScanMode
     */
    @NonNull
    public ScanMode getScanMode() {
        return mScanMode;
    }

    /**
     * Scan mode, {@link ScanMode#SINGLE} by default
     *
     * @see ScanMode
     */
    public void setScanMode(@NonNull final ScanMode scanMode) {
        mScanMode = Objects.requireNonNull(scanMode);
    }

    /**
     * Get current zoom value
     */
    public int getZoom() {
        return mZoom;
    }

    /**
     * Set current zoom value (between {@code 0} and {@link Parameters#getMaxZoom()}, if larger,
     * max zoom value will be set
     */
    public void setZoom(final int zoom) {
        if (zoom < 0) {
            throw new IllegalArgumentException("Zoom value must be greater than or equal to zero");
        }
        synchronized (mInitializeLock) {
            if (zoom != mZoom) {
                mZoom = zoom;
                if (mInitialized) {
                    final DecoderWrapper decoderWrapper = mDecoderWrapper;
                    if (decoderWrapper != null) {
                        final Camera camera = decoderWrapper.getCamera();
                        final Parameters parameters = camera.getParameters();
                        Utils.setZoom(parameters, zoom);
                        camera.setParameters(parameters);
                    }
                }
            }
        }
        mZoom = zoom;
    }

    /**
     * Touch focus is currently enabled or not
     */
    public boolean isTouchFocusEnabled() {
        return mTouchFocusEnabled;
    }

    /**
     * Enable or disable touch focus. If enabled, touches inside viewfinder frame will cause focusing into
     * specified area, auto focus will be switched off.
     */
    public void setTouchFocusEnabled(final boolean touchFocusEnabled) {
        mTouchFocusEnabled = touchFocusEnabled;
    }

    /**
     * Auto focus is currently enabled or not
     *
     * @see #setAutoFocusEnabled
     */
    public boolean isAutoFocusEnabled() {
        return mAutoFocusEnabled;
    }

    /**
     * Enable or disable auto focus if it's supported, {@code true} by default
     */
    @MainThread
    public void setAutoFocusEnabled(final boolean autoFocusEnabled) {
        synchronized (mInitializeLock) {
            final boolean changed = mAutoFocusEnabled != autoFocusEnabled;
            mAutoFocusEnabled = autoFocusEnabled;
            mScannerView.setAutoFocusEnabled(autoFocusEnabled);
            final DecoderWrapper decoderWrapper = mDecoderWrapper;
            if (mInitialized && mPreviewActive && changed && decoderWrapper != null &&
                    decoderWrapper.isAutoFocusSupported()) {
                setAutoFocusEnabledInternal(autoFocusEnabled);
            }
        }
    }

    /**
     * Get current auto focus mode
     *
     * @see #setAutoFocusMode
     */
    @NonNull
    public AutoFocusMode getAutoFocusMode() {
        return mAutoFocusMode;
    }

    /**
     * Auto focus mode, {@link AutoFocusMode#SAFE} by default
     *
     * @see AutoFocusMode
     */
    @MainThread
    public void setAutoFocusMode(@NonNull final AutoFocusMode autoFocusMode) {
        synchronized (mInitializeLock) {
            mAutoFocusMode = Objects.requireNonNull(autoFocusMode);
            if (mInitialized && mAutoFocusEnabled) {
                setAutoFocusEnabledInternal(true);
            }
        }
    }

    /**
     * Auto focus interval in milliseconds for {@link AutoFocusMode#SAFE} mode, 2000 by default
     *
     * @see #setAutoFocusMode
     */
    public void setAutoFocusInterval(final long autoFocusInterval) {
        mSafeAutoFocusInterval = autoFocusInterval;
    }

    /**
     * Flash light is currently enabled or not
     */
    public boolean isFlashEnabled() {
        return mFlashEnabled;
    }

    /**
     * Enable or disable flash light if it's supported, {@code false} by default
     */
    @MainThread
    public void setFlashEnabled(final boolean flashEnabled) {
        synchronized (mInitializeLock) {
            final boolean changed = mFlashEnabled != flashEnabled;
            mFlashEnabled = flashEnabled;
            mScannerView.setFlashEnabled(flashEnabled);
            final DecoderWrapper decoderWrapper = mDecoderWrapper;
            if (mInitialized && mPreviewActive && changed && decoderWrapper != null &&
                    decoderWrapper.isFlashSupported()) {
                setFlashEnabledInternal(flashEnabled);
            }
        }
    }

    /**
     * Preview is active or not
     */
    public boolean isPreviewActive() {
        return mPreviewActive;
    }

    /**
     * Start camera preview
     * <br>
     * Requires {@link Manifest.permission#CAMERA} permission
     */
    @MainThread
    public void startPreview() {
        synchronized (mInitializeLock) {
            if (!mInitialized && !mInitialization) {
                initialize();
                return;
            }
        }
        if (!mPreviewActive) {
            mSurfaceHolder.addCallback(mSurfaceCallback);
            startPreviewInternal(false);
        }
    }

    /**
     * Stop camera preview
     */
    @MainThread
    public void stopPreview() {
        if (mInitialized && mPreviewActive) {
            mSurfaceHolder.removeCallback(mSurfaceCallback);
            stopPreviewInternal(false);
        }
    }

    /**
     * Release resources, and stop preview if needed; call this method in {@link Activity#onPause()}
     */
    @MainThread
    public void releaseResources() {
        if (mInitialized) {
            if (mPreviewActive) {
                stopPreview();
            }
            releaseResourcesInternal();
        }
    }

    @SuppressWarnings("SuspiciousNameCombination")
    void performTouchFocus(final Rect viewFocusArea) {
        synchronized (mInitializeLock) {
            if (mInitialized && mPreviewActive && !mTouchFocusing) {
                try {
                    setAutoFocusEnabled(false);
                    final DecoderWrapper decoderWrapper = mDecoderWrapper;
                    if (mPreviewActive && decoderWrapper != null &&
                            decoderWrapper.isAutoFocusSupported()) {
                        final Point imageSize = decoderWrapper.getImageSize();
                        int imageWidth = imageSize.getX();
                        int imageHeight = imageSize.getY();
                        final int orientation = decoderWrapper.getDisplayOrientation();
                        if (orientation == 90 || orientation == 270) {
                            final int width = imageWidth;
                            imageWidth = imageHeight;
                            imageHeight = width;
                        }
                        final Rect imageArea =
                                Utils.getImageFrameRect(imageWidth, imageHeight, viewFocusArea,
                                        decoderWrapper.getPreviewSize(),
                                        decoderWrapper.getViewSize());
                        final Camera camera = decoderWrapper.getCamera();
                        camera.cancelAutoFocus();
                        final Parameters parameters = camera.getParameters();
                        Utils.configureFocusArea(parameters, imageArea, imageWidth, imageHeight,
                                orientation);
                        Utils.configureFocusModeForTouch(parameters);
                        camera.setParameters(parameters);
                        camera.autoFocus(mTouchFocusCallback);
                        mTouchFocusing = true;
                    }
                } catch (final Exception ignored) {
                }
            }
        }
    }

    boolean isAutoFocusSupportedOrUnknown() {
        final DecoderWrapper wrapper = mDecoderWrapper;
        return wrapper == null || wrapper.isAutoFocusSupported();
    }

    boolean isFlashSupportedOrUnknown() {
        final DecoderWrapper wrapper = mDecoderWrapper;
        return wrapper == null || wrapper.isFlashSupported();
    }

    private void initialize() {
        initialize(mScannerView.getWidth(), mScannerView.getHeight());
    }

    private void initialize(final int width, final int height) {
        mViewWidth = width;
        mViewHeight = height;
        if (width > 0 && height > 0) {
            mInitialization = true;
            mInitializationRequested = false;
            new InitializationThread(width, height).start();
        } else {
            mInitializationRequested = true;
        }
    }

    private void startPreviewInternal(final boolean internal) {
        try {
            final DecoderWrapper decoderWrapper = mDecoderWrapper;
            if (decoderWrapper != null) {
                final Camera camera = decoderWrapper.getCamera();
                camera.setPreviewCallback(mPreviewCallback);
                camera.setPreviewDisplay(mSurfaceHolder);
                if (!internal && decoderWrapper.isFlashSupported() && mFlashEnabled) {
                    setFlashEnabledInternal(true);
                }
                camera.startPreview();
                mStoppingPreview = false;
                mPreviewActive = true;
                mSafeAutoFocusing = false;
                mSafeAutoFocusAttemptsCount = 0;
                if (decoderWrapper.isAutoFocusSupported() && mAutoFocusEnabled) {
                    final Rect frameRect = mScannerView.getFrameRect();
                    if (frameRect != null) {
                        final Parameters parameters = camera.getParameters();
                        Utils.configureDefaultFocusArea(parameters, decoderWrapper, frameRect);
                        camera.setParameters(parameters);
                    }
                    if (mAutoFocusMode == AutoFocusMode.SAFE) {
                        scheduleSafeAutoFocusTask();
                    }
                }
            }
        } catch (final Exception ignored) {
        }
    }

    private void startPreviewInternalSafe() {
        if (mInitialized && !mPreviewActive) {
            startPreviewInternal(true);
        }
    }

    private void stopPreviewInternal(final boolean internal) {
        try {
            final DecoderWrapper decoderWrapper = mDecoderWrapper;
            if (decoderWrapper != null) {
                final Camera camera = decoderWrapper.getCamera();
                camera.cancelAutoFocus();
                final Parameters parameters = camera.getParameters();
                if (!internal && decoderWrapper.isFlashSupported() && mFlashEnabled) {
                    Utils.setFlashMode(parameters, Parameters.FLASH_MODE_OFF);
                }
                camera.setParameters(parameters);
                camera.setPreviewCallback(null);
                camera.stopPreview();
            }
        } catch (final Exception ignored) {
        }
        mStoppingPreview = false;
        mPreviewActive = false;
        mSafeAutoFocusing = false;
        mSafeAutoFocusAttemptsCount = 0;
    }

    private void stopPreviewInternalSafe() {
        if (mInitialized && mPreviewActive) {
            stopPreviewInternal(true);
        }
    }

    private void releaseResourcesInternal() {
        mInitialized = false;
        mInitialization = false;
        mStoppingPreview = false;
        mPreviewActive = false;
        mSafeAutoFocusing = false;
        final DecoderWrapper decoderWrapper = mDecoderWrapper;
        if (decoderWrapper != null) {
            mDecoderWrapper = null;
            decoderWrapper.release();
        }
    }

    private void setFlashEnabledInternal(final boolean flashEnabled) {
        try {
            final DecoderWrapper decoderWrapper = mDecoderWrapper;
            if (decoderWrapper != null) {
                final Camera camera = decoderWrapper.getCamera();
                final Parameters parameters = camera.getParameters();
                if (parameters == null) {
                    return;
                }
                if (flashEnabled) {
                    Utils.setFlashMode(parameters, Parameters.FLASH_MODE_TORCH);
                } else {
                    Utils.setFlashMode(parameters, Parameters.FLASH_MODE_OFF);
                }
                camera.setParameters(parameters);
            }
        } catch (final Exception ignored) {
        }
    }

    private void setAutoFocusEnabledInternal(final boolean autoFocusEnabled) {
        try {
            final DecoderWrapper decoderWrapper = mDecoderWrapper;
            if (decoderWrapper != null) {
                final Camera camera = decoderWrapper.getCamera();
                camera.cancelAutoFocus();
                mTouchFocusing = false;
                final Parameters parameters = camera.getParameters();
                final AutoFocusMode autoFocusMode = mAutoFocusMode;
                if (autoFocusEnabled) {
                    Utils.setAutoFocusMode(parameters, autoFocusMode);
                } else {
                    Utils.disableAutoFocus(parameters);
                }
                if (autoFocusEnabled) {
                    final Rect frameRect = mScannerView.getFrameRect();
                    if (frameRect != null) {
                        Utils.configureDefaultFocusArea(parameters, decoderWrapper, frameRect);
                    }
                }
                camera.setParameters(parameters);
                if (autoFocusEnabled) {
                    mSafeAutoFocusAttemptsCount = 0;
                    mSafeAutoFocusing = false;
                    if (autoFocusMode == AutoFocusMode.SAFE) {
                        scheduleSafeAutoFocusTask();
                    }
                }
            }
        } catch (final Exception ignored) {
        }
    }

    private void safeAutoFocusCamera() {
        if (!mInitialized || !mPreviewActive) {
            return;
        }
        final DecoderWrapper decoderWrapper = mDecoderWrapper;
        if (decoderWrapper == null || !decoderWrapper.isAutoFocusSupported() ||
                !mAutoFocusEnabled) {
            return;
        }
        if (mSafeAutoFocusing && mSafeAutoFocusAttemptsCount < SAFE_AUTO_FOCUS_ATTEMPTS_THRESHOLD) {
            mSafeAutoFocusAttemptsCount++;
        } else {
            try {
                final Camera camera = decoderWrapper.getCamera();
                camera.cancelAutoFocus();
                camera.autoFocus(mSafeAutoFocusCallback);
                mSafeAutoFocusAttemptsCount = 0;
                mSafeAutoFocusing = true;
            } catch (final Exception e) {
                mSafeAutoFocusing = false;
            }
        }
        scheduleSafeAutoFocusTask();
    }

    private void scheduleSafeAutoFocusTask() {
        if (mSafeAutoFocusTaskScheduled) {
            return;
        }
        mSafeAutoFocusTaskScheduled = true;
        mMainThreadHandler.postDelayed(mSafeAutoFocusTask, mSafeAutoFocusInterval);
    }

    private final class ScannerSizeListener implements CodeScannerView.SizeListener {
        @Override
        public void onSizeChanged(final int width, final int height) {
            synchronized (mInitializeLock) {
                if (width != mViewWidth || height != mViewHeight) {
                    final boolean previewActive = mPreviewActive;
                    if (mInitialized) {
                        releaseResources();
                    }
                    if (previewActive || mInitializationRequested) {
                        initialize(width, height);
                    }
                }
            }
        }
    }

    private final class PreviewCallback implements Camera.PreviewCallback {
        @Override
        public void onPreviewFrame(final byte[] data, final Camera camera) {
            if (!mInitialized || mStoppingPreview || mScanMode == ScanMode.PREVIEW ||
                    data == null) {
                return;
            }
            final DecoderWrapper decoderWrapper = mDecoderWrapper;
            if (decoderWrapper == null) {
                return;
            }
            final Decoder decoder = decoderWrapper.getDecoder();
            if (decoder.getState() != Decoder.State.IDLE) {
                return;
            }
            final Rect frameRect = mScannerView.getFrameRect();
            if (frameRect == null || frameRect.getWidth() < 1 || frameRect.getHeight() < 1) {
                return;
            }
            decoder.decode(new DecodeTask(data, decoderWrapper.getImageSize(),
                    decoderWrapper.getPreviewSize(), decoderWrapper.getViewSize(), frameRect,
                    decoderWrapper.getDisplayOrientation(),
                    decoderWrapper.shouldReverseHorizontal()));
        }
    }

    private final class SurfaceCallback implements SurfaceHolder.Callback {
        @Override
        public void surfaceCreated(final SurfaceHolder holder) {
            startPreviewInternalSafe();
        }

        @Override
        public void surfaceChanged(final SurfaceHolder holder, final int format, final int width,
                final int height) {
            if (holder.getSurface() == null) {
                mPreviewActive = false;
                return;
            }
            stopPreviewInternalSafe();
            startPreviewInternalSafe();
        }

        @Override
        public void surfaceDestroyed(final SurfaceHolder holder) {
            stopPreviewInternalSafe();
        }
    }

    private final class DecoderStateListener implements Decoder.StateListener {
        @Override
        public boolean onStateChanged(@NonNull final Decoder.State state) {
            if (state == Decoder.State.DECODED) {
                final ScanMode scanMode = mScanMode;
                if (scanMode == ScanMode.PREVIEW) {
                    return false;
                } else if (scanMode == ScanMode.SINGLE) {
                    mStoppingPreview = true;
                    mMainThreadHandler.post(mStopPreviewTask);
                }
            }
            return true;
        }
    }

    private final class InitializationThread extends Thread {
        private final int mWidth;
        private final int mHeight;

        public InitializationThread(final int width, final int height) {
            super("cs-init");
            mWidth = width;
            mHeight = height;
        }

        @Override
        public void run() {
            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
            try {
                initialize();
            } catch (final Exception e) {
                releaseResourcesInternal();
                final ErrorCallback errorCallback = mErrorCallback;
                if (errorCallback != null) {
                    errorCallback.onError(e);
                } else {
                    throw e;
                }
            }
        }

        private void initialize() {
            Camera camera = null;
            final CameraInfo cameraInfo = new CameraInfo();
            final int cameraId = mCameraId;
            if (cameraId == CAMERA_BACK || cameraId == CAMERA_FRONT) {
                final int numberOfCameras = Camera.getNumberOfCameras();
                final int facing = cameraId == CAMERA_BACK ? CameraInfo.CAMERA_FACING_BACK :
                        CameraInfo.CAMERA_FACING_FRONT;
                for (int i = 0; i < numberOfCameras; i++) {
                    Camera.getCameraInfo(i, cameraInfo);
                    if (cameraInfo.facing == facing) {
                        camera = Camera.open(i);
                        mCameraId = i;
                        break;
                    }
                }
            } else {
                camera = Camera.open(cameraId);
                Camera.getCameraInfo(cameraId, cameraInfo);
            }
            if (camera == null) {
                throw new CodeScannerException("Unable to access camera");
            }
            final Parameters parameters = camera.getParameters();
            if (parameters == null) {
                throw new CodeScannerException("Unable to configure camera");
            }
            final int orientation = Utils.getDisplayOrientation(mContext, cameraInfo);
            final boolean portrait = Utils.isPortrait(orientation);
            final Point imageSize =
                    Utils.findSuitableImageSize(parameters, portrait ? mHeight : mWidth,
                            portrait ? mWidth : mHeight);
            final int imageWidth = imageSize.getX();
            final int imageHeight = imageSize.getY();
            parameters.setPreviewSize(imageWidth, imageHeight);
            parameters.setPreviewFormat(ImageFormat.NV21);
            final Point previewSize = Utils.getPreviewSize(portrait ? imageHeight : imageWidth,
                    portrait ? imageWidth : imageHeight, mWidth, mHeight);
            final List<String> focusModes = parameters.getSupportedFocusModes();
            final boolean autoFocusSupported = focusModes != null &&
                    (focusModes.contains(Parameters.FOCUS_MODE_AUTO) ||
                            focusModes.contains(Parameters.FOCUS_MODE_CONTINUOUS_PICTURE));
            if (!autoFocusSupported) {
                mAutoFocusEnabled = false;
            }
            final Point viewSize = new Point(mWidth, mHeight);
            if (autoFocusSupported && mAutoFocusEnabled) {
                Utils.setAutoFocusMode(parameters, mAutoFocusMode);
                final Rect frameRect = mScannerView.getFrameRect();
                if (frameRect != null) {
                    Utils.configureDefaultFocusArea(parameters, frameRect, previewSize, viewSize,
                            imageWidth, imageHeight, orientation);
                }
            }
            final List<String> flashModes = parameters.getSupportedFlashModes();
            final boolean flashSupported =
                    flashModes != null && flashModes.contains(Parameters.FLASH_MODE_TORCH);
            if (!flashSupported) {
                mFlashEnabled = false;
            }
            final int zoom = mZoom;
            if (zoom != 0) {
                Utils.setZoom(parameters, zoom);
            }
            Utils.configureFpsRange(parameters);
            Utils.configureSceneMode(parameters);
            Utils.configureVideoStabilization(parameters);
            camera.setParameters(parameters);
            camera.setDisplayOrientation(orientation);
            synchronized (mInitializeLock) {
                final Decoder decoder =
                        new Decoder(mDecoderStateListener, mFormats, mDecodeCallback);
                mDecoderWrapper =
                        new DecoderWrapper(camera, cameraInfo, decoder, imageSize, previewSize,
                                viewSize, orientation, autoFocusSupported, flashSupported);
                decoder.start();
                mInitialization = false;
                mInitialized = true;
            }
            mMainThreadHandler.post(new FinishInitializationTask(previewSize));
        }
    }

    private final class TouchFocusCallback implements Camera.AutoFocusCallback {
        @Override
        public void onAutoFocus(final boolean success, @NonNull final Camera camera) {
            mTouchFocusing = false;
        }
    }

    private final class SafeAutoFocusCallback implements Camera.AutoFocusCallback {
        @Override
        public void onAutoFocus(final boolean success, @NonNull final Camera camera) {
            mSafeAutoFocusing = false;
        }
    }

    private final class SafeAutoFocusTask implements Runnable {
        @Override
        public void run() {
            mSafeAutoFocusTaskScheduled = false;
            if (mAutoFocusMode == AutoFocusMode.SAFE) {
                safeAutoFocusCamera();
            }
        }
    }

    private final class StopPreviewTask implements Runnable {
        @Override
        public void run() {
            stopPreview();
        }
    }

    private final class FinishInitializationTask implements Runnable {
        private final Point mPreviewSize;

        private FinishInitializationTask(@NonNull final Point previewSize) {
            mPreviewSize = previewSize;
        }

        @Override
        public void run() {
            if (!mInitialized) {
                return;
            }
            mScannerView.setPreviewSize(mPreviewSize);
            mScannerView.setAutoFocusEnabled(isAutoFocusEnabled());
            mScannerView.setFlashEnabled(isFlashEnabled());
            startPreview();
        }
    }
}