package com.ekreutz.barcodescanner.ui;

import android.Manifest;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.hardware.Camera;
import android.support.v4.app.ActivityCompat;
import android.support.v7.app.AlertDialog;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.ViewGroup;

import com.ekreutz.barcodescanner.camera.CameraSource;
import com.ekreutz.barcodescanner.camera.CameraSourcePreview;
import com.ekreutz.barcodescanner.util.BarcodeFormat;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.uimanager.events.RCTEventEmitter;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GoogleApiAvailability;
import com.google.android.gms.vision.MultiProcessor;
import com.google.android.gms.vision.Tracker;
import com.google.android.gms.vision.barcode.Barcode;
import com.google.android.gms.vision.barcode.BarcodeDetector;

import java.io.IOException;

public class BarcodeScannerView extends ViewGroup implements CameraSource.AutoFocusCallback, MultiProcessor.Factory<Barcode> {

    private final static String TAG = "BARCODE_CAPTURE_VIEW";
    private final Context mContext;
    private boolean hasAllCapabilities = false; // barcode scanner library and newest play services

    private static final String BARCODE_FOUND_KEY = "barcode_found";
    private static final String LOW_STORAGE_KEY = "low_storage";
    private static final String NOT_YET_OPERATIONAL = "not_yet_operational";
    private static final String NO_PLAY_SERVICES_KEY = "no_play_services";

    // intent request code to handle updating play services if needed.
    private static final int RC_HANDLE_GMS = 9001;

    // For focusing we prefer two continuous methods first, and then finally the "auto" mode which is fired on tap.
    // A device should support at least one of these for scanning to be possible at all.
    private static final String[] PREFERRED_FOCUS_MODES = {Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE, Camera.Parameters.FOCUS_MODE_AUTO, Camera.Parameters.FOCUS_MODE_FIXED};

    private CameraSource mCameraSource;
    private CameraSourcePreview mPreview;
    private BarcodeDetector mBarcodeDetector;
    private boolean mIsPaused = true;

    private int mBarcodeTypes = 0; // 0 for all supported types

    public BarcodeScannerView(Context context) {
        super(context);
        mContext = context;
        init();
    }

    public BarcodeScannerView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mContext = context;
        init();
    }

    public BarcodeScannerView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mContext = context;
        init();
    }

    private boolean hasCameraPermission() {
        int rc = ActivityCompat.checkSelfPermission(mContext, Manifest.permission.CAMERA);
        return rc == PackageManager.PERMISSION_GRANTED;
    }

    private boolean hasNecessaryCapabilities() {
        return hasCameraPermission() && hasAllCapabilities;
    }

    public void init() {
        mPreview = new CameraSourcePreview(mContext, null);
        addView(mPreview);

        start();
    }

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

        if (!hasCameraPermission()) {
            // No camera permission. Alert user.
            AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
            builder.setTitle("No Camera permission")
                .setMessage("Enable camera permission in settings to use the scanner.")
                .setPositiveButton("Ok", null)
                .show();

            return;
        }

        /**
         * Check for a few other things that the device needs for the scanner to work.
         * And send a JS event if something goes wrongs.
         *
         * Checklist: (things are checked in this order)
         * 1. The device has the latest play services
         * 2. The device has sufficient storage
         * 3. The scanner dependencies are downloaded
         */

        // check that the device has (the latest) play services available.
        int code = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(mContext.getApplicationContext());
        if (code != ConnectionResult.SUCCESS) {
            sendNativeEvent(NO_PLAY_SERVICES_KEY, Arguments.createMap());
        } else if (mBarcodeDetector != null && !mBarcodeDetector.isOperational()) {
            // Note: The first time that an app using the barcode or face API is installed on a
            // device, GMS will download a native libraries to the device in order to do detection.
            // Usually this completes before the app is run for the first time.  But if that
            // download has not yet completed, then the above call will not detect any barcodes
            // and/or faces.
            //
            // isOperational() can be used to check if the required native libraries are currently
            // available.  The detectors will automatically become operational once the library
            // downloads complete on device.
            Log.w(TAG, "Detector dependencies are not yet available.");

            // Check for low storage.  If there is low storage, the native library will not be
            // downloaded, so detection will not become operational.
            IntentFilter lowstorageFilter = new IntentFilter(Intent.ACTION_DEVICE_STORAGE_LOW);
            boolean hasLowStorage = mContext.registerReceiver(null, lowstorageFilter) != null;

            if (hasLowStorage) {
                // Detector dependencies can't be downloaded due to low storage
                sendNativeEvent(LOW_STORAGE_KEY, Arguments.createMap());
            } else {
                // Storage isn't low, but dependencies haven't been downloaded yet
                sendNativeEvent(NOT_YET_OPERATIONAL, Arguments.createMap());
            }
        } else {
            hasAllCapabilities = true;
            start();
        }
    }

    /**
     * Start the camera for the first time.
     */
    public void start() {
        if (!hasNecessaryCapabilities())
            return;

        createCameraSource();
        startCameraSource();
    }

    /**
     * Restarts the camera.
     */
    public void resume() {
        // start the camera only if it isn't already running
        if (mIsPaused && hasNecessaryCapabilities()) {
            startCameraSource();
        }
    }

    /**
     * Stops the camera.
     */
    public void pause() {
        if (mPreview != null && !mIsPaused && hasNecessaryCapabilities()) {
            mPreview.stop();
            mIsPaused = true;
        }
    }

    /**
     * Releases the resources associated with the camera source, the associated detectors, and the
     * rest of the processing pipeline.
     */
    public void release() {
        if (mPreview != null && hasNecessaryCapabilities()) {
            mPreview.release();
            mIsPaused = true;
        }
    }

    /**
     * Note: restarts the camera, so can be slow.
     * @param barcodeTypes: desired types bitmask
     */
    public void setBarcodeTypes(int barcodeTypes) {
        if (mBarcodeTypes == barcodeTypes) {
            return;
        }

        mBarcodeTypes = barcodeTypes;

        if (mPreview != null && ActivityCompat.checkSelfPermission(mContext, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
            try {
                mPreview.replaceBarcodeDetector(createBarcodeDetector(), !mIsPaused);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * Set focus mode.
     * Possible values: 0 = continuous focus (if supported), 1 = tap-to-focus (if supported), 2 = fixed focus
     * @param focusMode
     */
    public boolean setFocusMode(int focusMode) {
        if (focusMode < 0 || focusMode > 2) {
            focusMode = 0;
        }

        return mCameraSource != null && mCameraSource.setFocusMode(PREFERRED_FOCUS_MODES[focusMode]);
    }

    /**
     * Set camera fill mode.
     * Possible values:
     *   0 = camera stream will fill the entire view (possibly being cropped)
     *   1 = camera stream will fit snugly within the view (possibly showing fat borders around)
     */
    public void setCameraFillMode(int fillMode) {
        if (mPreview != null) {
            mPreview.setFillMode(fillMode);
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        for (int i = 0, len = getChildCount(); i < len; i++) {
            // tell the child to fill the whole view when layouting
            getChildAt(i).layout(0, 0, r - l, b - t);
        }
    }

    /**
     * Creates and starts the camera.  Note that this uses a higher resolution in comparison
     * to other detection examples to enable the barcode detector to detect small barcodes
     * at long distances.
     *
     * Suppressing InlinedApi since there is a check that the minimum version is met before using
     * the constant.
     */
    @SuppressLint("InlinedApi")
    private void createCameraSource() {
        // set preferred mBarcodeTypes before this :)
        BarcodeDetector barcodeDetector = createBarcodeDetector();

        if (!hasNecessaryCapabilities()) {
            return;
        }

        // Creates and starts the camera.  Note that this uses a higher resolution in comparison
        // to other detection examples to enable the barcode detector to detect small barcodes
        // at long distances.
        mCameraSource = new CameraSource.Builder(mContext.getApplicationContext(), barcodeDetector)
                .setFacing(CameraSource.CAMERA_FACING_BACK)
                .setRequestedPreviewSize(1600, 900)
                .setRequestedFps(15.0f)
                .setPreferredFocusModes(PREFERRED_FOCUS_MODES)
                .build();
    }

    private BarcodeDetector createBarcodeDetector() {
        // A barcode detector is created to track barcodes.  An associated multi-processor instance
        // is set to receive the barcode detection results, and track the barcodes.
        // The factory is used by the multi-processor to
        // create a separate tracker instance for each barcode.
        BarcodeDetector barcodeDetector = new BarcodeDetector.Builder(mContext)
            .setBarcodeFormats(mBarcodeTypes)
            .build();

        barcodeDetector.setProcessor(new MultiProcessor.Builder<>(this).build());

        return mBarcodeDetector = barcodeDetector;
    }

    /**
     * Starts or restarts the camera source, if it exists.  If the camera source doesn't exist yet
     * (e.g., because onResume was called before the camera source was created), this will be called
     * again when the camera source is created.
     */
    private void startCameraSource() throws SecurityException {
        if (mCameraSource != null) {
            try {
                mPreview.start(mCameraSource);
                mIsPaused = false;
            } catch (IOException e) {
                mCameraSource.release();
                mCameraSource = null;
            }
        } else {
            Log.d(TAG, "Camera source is null!");
        }
    }

    private void tryAutoFocus() {
        if (mCameraSource != null) {
            mCameraSource.autoFocus(this);
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (mCameraSource != null && mCameraSource.getFocusMode() != null && mCameraSource.getFocusMode().equals(Camera.Parameters.FOCUS_MODE_AUTO)) {
            tryAutoFocus();
            return true;
        }

        return super.onTouchEvent(event);
    }

    @Override
    public void onAutoFocus(boolean success) {
        // No actions needed for the focus callback.
        Log.d(TAG, "Did autofocus.");
    }

    @Override
    public Tracker<Barcode> create(Barcode barcode) {
        return new Tracker<Barcode>() {
            /**
             * Start tracking the detected item instance within the item overlay.
             */
            @Override
            public void onNewItem(int id, Barcode item) {
                // Act on new barcode found
                WritableMap event = Arguments.createMap();
                event.putString("data", item.displayValue);
                event.putString("type", BarcodeFormat.get(item.format));

                sendNativeEvent(BARCODE_FOUND_KEY, event);
            }
        };
    }

    private void sendNativeEvent(String key, WritableMap event) {
        if (getId() < 0) {
            Log.w(TAG, "Tried to send native event with negative id!");
            return;
        }

        event.putString("key", key);

        // Send the newly found data to the JS side
        ReactContext reactContext = (ReactContext) mContext;
        reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(
            getId(),
            "topChange",
            event);
    }
}
