/*
 * Copyright (C) 2015-2019 Zebra Technologies Corporation and/or its affiliates
 * All rights reserved.
 */

package com.symbol.barcodesample1;

import android.app.Activity;
import android.content.pm.ActivityInfo;
import android.os.Bundle;
import android.text.Html;
import android.text.method.ScrollingMovementMethod;
import android.util.DisplayMetrics;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.ArrayAdapter;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.ScrollView;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

// The following classes/enums are required for EMDK
import com.symbol.emdk.EMDKManager;
import com.symbol.emdk.EMDKManager.EMDKListener;
import com.symbol.emdk.EMDKManager.FEATURE_TYPE;
import com.symbol.emdk.EMDKResults;
import com.symbol.emdk.barcode.BarcodeManager;
import com.symbol.emdk.barcode.BarcodeManager.ConnectionState;
import com.symbol.emdk.barcode.BarcodeManager.ScannerConnectionListener;
import com.symbol.emdk.barcode.ScanDataCollection;
import com.symbol.emdk.barcode.ScanDataCollection.ScanData;
import com.symbol.emdk.barcode.Scanner;
import com.symbol.emdk.barcode.Scanner.DataListener;
import com.symbol.emdk.barcode.Scanner.StatusListener;
import com.symbol.emdk.barcode.Scanner.TriggerType;
import com.symbol.emdk.barcode.ScannerConfig;
import com.symbol.emdk.barcode.ScannerException;
import com.symbol.emdk.barcode.ScannerInfo;
import com.symbol.emdk.barcode.ScannerResults;
import com.symbol.emdk.barcode.StatusData;
import com.symbol.emdk.barcode.StatusData.ScannerStates;

/**
 * IMPORTANT:
 * Please note the following configuration changes:
 *  - EMDK permission and library reference in AndroidManifest.xml
 *  - EMDK SDK dependency in build.gradle(Module:app)
 *
 * All the time consuming tasks should execute in different threads without blocking the main thread.
 */

/**
 * Implements EMDK Listeners (ex: EMDKListener, DataListener, StatusListener and ScannerConnectionListener).
 * EMDK notifies when they are ready for use. You may need to implement additional listeners (ex: OnCheckedChangeListener)
 * as well depending on the features used in the application.
 */
public class MainActivity extends Activity implements EMDKListener, DataListener, StatusListener, ScannerConnectionListener, OnCheckedChangeListener {

    // Variables to hold EMDK related objects
    private EMDKManager emdkManager = null;
    private BarcodeManager barcodeManager = null;
    private Scanner scanner = null;
    private List<ScannerInfo> scannerInfoList = null;

    // Variables to hold handlers of UI controls
    private TextView textViewData = null;
    private TextView textViewStatus = null;
    private CheckBox checkBoxEAN8 = null;
    private CheckBox checkBoxEAN13 = null;
    private CheckBox checkBoxCode39 = null;
    private CheckBox checkBoxCode128 = null;
    private Spinner spinnerScannerDevices = null;

    // Variable to hold the index of the selected scanner
    private int scannerIndex = 0;

    // Variable to hold the index of the default scanner
    private int defaultIndex = 0;

    // Variable to hold scan data length
    private int dataLength = 0;

    // Variable to hold status messages
    private String statusString = "";

    // Flag to decide whether scanner configuration change is required or not (scanner.setConfig() will
    // be done after receiving IDLE status).
    private boolean applyScannerConfiguration = false;

    // Flag to decide whether the scan button on UI is pressed or not
    private boolean scanButtonPressed = false;

    // Flag to decide whether external scanner is disconnected or not
    private boolean extScannerDisconnected = false;

    // Variable to hold lock object which is used to synchronize scanner de-initialization and re-initialization
    // sequence upon external scanner disconnection and connection.
    private final Object lock = new Object();

    /**
     * onCreate() is the best place to request EMDKManager object. Similarly, onDestroy() is the best place to release
     * this object.
     */
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        scannerInfoList = new ArrayList<ScannerInfo>();

        // This sample does not support dynamic orientation change and is locked for default orientation
        // of the device.
        setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_NOSENSOR);
        setDefaultOrientation();

        textViewData = (TextView)findViewById(R.id.textViewData);
        textViewStatus = (TextView)findViewById(R.id.textViewStatus);
        checkBoxEAN8 = (CheckBox)findViewById(R.id.checkBoxEAN8);
        checkBoxEAN13 = (CheckBox)findViewById(R.id.checkBoxEAN13);
        checkBoxCode39 = (CheckBox)findViewById(R.id.checkBoxCode39);
        checkBoxCode128 = (CheckBox)findViewById(R.id.checkBoxCode128);
        spinnerScannerDevices = (Spinner)findViewById(R.id.spinnerScannerDevices);

        // Requests the EMDKManager object. This is an asynchronous call and should be called from the main thread.
        // The callback also will receive in the main thread without blocking it until the EMDK resources are ready.
        // However, all time consuming tasks related to the application should execute in different threads and free
        // the main thread to receive this asynchronous callback.
        EMDKResults results = EMDKManager.getEMDKManager(getApplicationContext(), this);

        // Check the return status of getEMDKManager()
        if (results.statusCode != EMDKResults.STATUS_CODE.SUCCESS) {
            updateStatus("EMDKManager object request failed!");
            return;
        } else {
            updateStatus("EMDKManager object initialization is in progress.......");
        }

        checkBoxEAN8.setOnCheckedChangeListener(this);
        checkBoxEAN13.setOnCheckedChangeListener(this);
        checkBoxCode39.setOnCheckedChangeListener(this);
        checkBoxCode128.setOnCheckedChangeListener(this);

        addSpinnerScannerDevicesListener();

        textViewData.setSelected(true);
        textViewData.setMovementMethod(new ScrollingMovementMethod());
    }

    /**
     * Implements EMDKListener.onOpened interface. EMDKManager will receive in to this callback.
     * NOTE: Main thread should be kept free to receive this callback.
     */
    @Override
    public void onOpened(EMDKManager emdkManager) {
        updateStatus("EMDK open success!");
        this.emdkManager = emdkManager;
        // Acquire the barcode manager resources
        initBarcodeManager();
        // Enumerate scanner devices
        enumerateScannerDevices();
        // Set default scanner
        spinnerScannerDevices.setSelection(defaultIndex);
    }

    /**
     * onResume() is the best place to acquire EMDK barcode manager resources if they have been released
     * during onPause().
     */
    @Override
    protected void onResume() {
        super.onResume();

        if (emdkManager != null) {
            // Acquire the barcode manager resources
            initBarcodeManager();
            // Enumerate scanner devices
            enumerateScannerDevices();
            // Set selected scanner
            spinnerScannerDevices.setSelection(scannerIndex);
            // Initialize scanner
            initScanner();
        }
    }

    /**
     * onPause() is the best place to release EMDK barcode manager resources. So that another
     * application would be able to acquire the scanner resources while this is background.
     */
    @Override
    protected void onPause() {
        super.onPause();
        // Release EMDK barcode manager resources
        deInitScanner();
        deInitBarcodeManager();
    }

    /**
     * Implements EMDKListener.onClosed interface. Called only if EMDK crashes or forcefully killed
     * by the OS.
     * NOTE: All time consuming tasks related to the application also should execute in different threads and
     * free the main thread to receive this asynchronous callback.
     */
    @Override
    public void onClosed() {
        // Release all the resources
        if (emdkManager != null) {
            emdkManager.release();
            emdkManager = null;
        }
        updateStatus("EMDK closed unexpectedly! Please close and restart the application.");
    }

    /**
     * onDestroy()is the best place to release EMDKManager object acquired in onCreate().
     * NOTE: Must release all the EMDK resources since the application is exiting.
     */
    @Override
    protected void onDestroy() {
        super.onDestroy();
        // Release all the EMDK resources
        if (emdkManager != null) {
            emdkManager.release();
            emdkManager = null;
        }
    }

    /**
     * Implements DataListener.onData interface. The scanned data will be notified with this callback.
     */
    @Override
    public void onData(ScanDataCollection scanDataCollection) {
        // Getting the scanner data on the callback
        if ((scanDataCollection != null) && (scanDataCollection.getResult() == ScannerResults.SUCCESS)) {
            ArrayList <ScanData> scanData = scanDataCollection.getScanData();
            for(ScanData data : scanData) {
                updateData("<font color='gray'>" + data.getLabelType() + "</font> : " + data.getData());
            }
        }
    }

    /**
     * Implements StatusListener.onStatus interface. The scanner status will be notified with this callback.
     */
    @Override
    public void onStatus(StatusData statusData) {
        // The status will be returned on multiple cases. Check the state and take the action.
        ScannerStates state = statusData.getState();
        switch(state) {
            case IDLE:
                // Scanner is idle and ready to change configuration and submit read.
                statusString = statusData.getFriendlyName()+" is enabled and idle...";
                updateStatus(statusString);
                // Select trigger type
                if(scanButtonPressed) {
                    // Scan button on the UI is pressed. So, use soft trigger
                    scanner.triggerType = TriggerType.SOFT_ONCE;
                    scanButtonPressed = false;
                } else {
                    // Use hard trigger
                    scanner.triggerType = TriggerType.HARD;
                }
                // Configure the scanner settings
                if(applyScannerConfiguration) {
                    setDecoders();
                    applyScannerConfiguration = false;
                }
                // Submit read
                if(!scanner.isReadPending() && !extScannerDisconnected) {
                    try {
                        scanner.read();
                    } catch (ScannerException e) {
                        updateStatus(e.getMessage());
                    }
                }
                break;
            case WAITING:
                // Scanner is waiting for trigger press to scan...
                statusString = "Scanner is waiting for trigger press...";
                updateStatus(statusString);
                break;
            case SCANNING:
                // Scanning is in progress...
                statusString = "Scanning...";
                updateStatus(statusString);
                break;
            case DISABLED:
                // Scanner is disabled
                statusString = statusData.getFriendlyName()+" is disabled.";
                updateStatus(statusString);
                break;
            case ERROR:
                // Error has occurred during scanning
                statusString = "An error has occurred.";
                updateStatus(statusString);
                break;
            default:
                break;
        }
    }

    /**
     * Implements BarcodeManager.onConnectionChange interface. The scanner connection status will be notified with this callback.
     * NOTE: It is recommended to synchronize initScanner() and deInitScanner() as connection and disconnection can happen rapidly.
     */
    @Override
    public void onConnectionChange(ScannerInfo scannerInfo, ConnectionState connectionState) {
        String status;
        String scannerName = "";
        String statusExtScanner = connectionState.toString();
        String scannerNameExtScanner = scannerInfo.getFriendlyName();
        if (scannerInfoList.size() != 0) {
            scannerName = scannerInfoList.get(scannerIndex).getFriendlyName();
        }
        if (scannerName.equalsIgnoreCase(scannerNameExtScanner)) {
            switch(connectionState) {
                case CONNECTED:
                    // Must acquire fresh scanner object once the external scanner is connected.
                    scanButtonPressed = false;
                    synchronized (lock) {
                        initScanner();
                        extScannerDisconnected = false;
                    }
                    break;
                case DISCONNECTED:
                    // Scanner object must be released once the external scanner is disconnected.
                    extScannerDisconnected = true;
                    synchronized (lock) {
                        deInitScanner();
                    }
                    break;
            }
            status = scannerNameExtScanner + ":" + statusExtScanner;
            updateStatus(status);
        }
        else {
            extScannerDisconnected = false;
            status =  statusString + " " + scannerNameExtScanner + ":" + statusExtScanner;
            updateStatus(status);
        }
    }

    /**
     * Implements scanner initialization tasks.
     * Option1:
     * scanner device can be acquired by specifying the corresponding ScannerInfo. List<ScannerInfo> is obtained by
     * calling barcodeManager.getSupportedDevicesInfo(). This approach would be helpful if there is any use case to switch the
     * scanner dynamically.
     * Example: scanner = barcodeManager.getDevice(scannerInfo);
     *
     * Option2:
     * scanner device can also be obtained by specifying the corresponding Device Identifier. Supported identifiers can
     * be found in the DeviceIdentifier enum. This approach would be helpful if the user need to use specific scanner or default
     * scanner.
     * Example: scanner = barcodeManager.getDevice(BarcodeManager.DeviceIdentifier.INTERNAL_IMAGER1);
     *
     * NOTE: DeviceIdentifier.DEFAULT can be used to obtain default scanner.
     */
    private void initScanner() {
        if (scanner == null) {
            if ((scannerInfoList != null) && (scannerInfoList.size() != 0)) {
                if (barcodeManager != null)

                    // Acquiring scanner using the scanner info (i.e. Option1) based on the index selected.
                    scanner = barcodeManager.getDevice(scannerInfoList.get(scannerIndex));
            }
            else {
                updateStatus("Failed to get the specified scanner device! Please close and restart the application.");
                return;
            }
            if (scanner != null) {
                // Implement the DataListener interface and pass the pointer of this object to get the data callbacks.
                scanner.addDataListener(this);

                // Implement the StatusListener interface and pass the pointer of this object to get the status callbacks.
                scanner.addStatusListener(this);
                try {
                    // Configuration should be done after scanner initialization. Hence, the flag is set to true.
                    applyScannerConfiguration = true;
                    // Enable the scanner
                    // NOTE: After calling enable(), user must wait for IDLE status before calling any other scanner APIs such as setConfig() or read().
                    scanner.enable();

                } catch (ScannerException e) {
                    // Configuration should be skipped as enable failed. Hence, the flag is set to false.
                    applyScannerConfiguration = false;
                    updateStatus(e.getMessage());
                    deInitScanner();
                }
            }else{
                updateStatus("Failed to initialize the scanner device.");
            }
        }
    }

    /**
     * Implements scanner release.
     */
    private void deInitScanner() {
        if (scanner != null) {
            try{
                // Release the scanner
                scanner.release();
            } catch (Exception e) {
                updateStatus(e.getMessage());
            }
            scanner = null;
        }
    }

    /**
     * Implements BarcodeManager initialization tasks.
     */
    private void initBarcodeManager(){

        // Get the feature object such as BarcodeManager object for accessing the feature.
        barcodeManager = (BarcodeManager) emdkManager.getInstance(FEATURE_TYPE.BARCODE);

        // Add external scanner connection listener
        if (barcodeManager != null) {
            barcodeManager.addConnectionListener(this);
        }
        else
        {
            Toast.makeText(this, "Barcode scanning is not supported.", Toast.LENGTH_LONG).show();
            finish();
        }
    }

    /**
     * Implements BarcodeManager release.
     */
    private void deInitBarcodeManager(){
        if (emdkManager != null) {
            // Release the barcodeManager which completely releases the all the associated resources
            emdkManager.release(FEATURE_TYPE.BARCODE);
        }
    }

    /**
     * Implements spinner listener interface.
     */
    private void addSpinnerScannerDevicesListener() {
        spinnerScannerDevices.setOnItemSelectedListener(new OnItemSelectedListener() {
            @Override
            public void onItemSelected(AdapterView<?> parent, View arg1, int position, long arg3) {
                if ((scannerIndex != position) || (scanner==null)) {
                    scannerIndex = position;
                    scanButtonPressed = false;
                    extScannerDisconnected = false;
                    deInitScanner();
                    initScanner();
                }
            }
            @Override
            public void onNothingSelected(AdapterView<?> arg0) {
            }
        });
    }

    /**
     * Implements scanner enumeration and spinner initialization.
     * NOTE: This would be helpful if there is any use case to populate supported scanners on the device and
     * switch the intended scanner dynamically based on the preference.
     */
    private void enumerateScannerDevices() {
        // Enumerate scanners
        if (barcodeManager != null) {
            List<String> friendlyNameList = new ArrayList<String>();
            int spinnerIndex = 0;
            scannerInfoList = barcodeManager.getSupportedDevicesInfo();
            if ((scannerInfoList != null) && (scannerInfoList.size() != 0)) {
                Iterator<ScannerInfo> it = scannerInfoList.iterator();
                while(it.hasNext()) {
                    ScannerInfo scnInfo = it.next();
                    friendlyNameList.add(scnInfo.getFriendlyName());
                    if(scnInfo.isDefaultScanner()) {
                        defaultIndex = spinnerIndex;
                    }
                    ++spinnerIndex;
                }
            }
            else {
                updateStatus("Failed to get the list of supported scanner devices! Please close and restart the application.");
            }
            // Populate spinner with scanner friendly names
            ArrayAdapter<String> spinnerAdapter = new ArrayAdapter<String>(MainActivity.this, android.R.layout.simple_spinner_item, friendlyNameList);
            spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
            spinnerScannerDevices.setAdapter(spinnerAdapter);
        }
    }

    /**
     * Implements the get-modify-set mechanism used to change scanner configuration .
     * NOTE: User must wait for IDLE status before calling getParams() and setParams() APIs.
     */
    private void setDecoders() {
        if (scanner != null) {
            try {
                // Get scanner config
                Bundle requiredParams = new Bundle();
                requiredParams.putString("decoder_ean8","");
                requiredParams.putString("decoder_ean13","");
                requiredParams.putString("decoder_code39","");
                requiredParams.putString("decoder_code128","");
                Bundle params = scanner.getParams(requiredParams);

                // Set EAN8
                if(params.containsKey("decoder_ean8")) {
                    params.putString("decoder_ean8",String.valueOf(checkBoxEAN8.isChecked()));
                }

                // Set EAN13
                if(params.containsKey("decoder_ean13")) {
                    params.putString("decoder_ean13",String.valueOf(checkBoxEAN13.isChecked()));
                }

                // Set Code39
                if(params.containsKey("decoder_code39")) {
                    params.putString("decoder_code39",String.valueOf(checkBoxCode39.isChecked()));
                }

                //Set Code128
                if(params.containsKey("decoder_code128")) {
                    params.putString("decoder_code128",String.valueOf(checkBoxCode128.isChecked()));
                }
                scanner.setParams(params);
            } catch (ScannerException e) {
                updateStatus(e.getMessage());
            }
        }
    }
    /**
     * Implements soft scan button action listener.
     */
    public void softScan(View view) {
        // Scan button on the UI is pressed and need to activate soft trigger for scanning. Hence, the flag is set to true.
        scanButtonPressed = true;
        // Cancel any pending read.
        cancelRead();
    }

    /**
     * Implements pending read cancellation functionality.
     */
    private void cancelRead(){
        if (scanner != null) {
            if (scanner.isReadPending()) {
                try {
                    // After calling cancelRead(), user must wait for IDLE status before calling any other scanner APIs
                    // such as setConfig() or read().
                    scanner.cancelRead();
                } catch (ScannerException e) {
                    updateStatus(e.getMessage());
                }
            }
        }
    }

    /**
     * Implements helper function to display the status string on UI from status callbacks and exceptions.
     */
    private void updateStatus(final String status){
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                textViewStatus.setText("" + status);
            }
        });
    }

    /**
     * Implements helper function to display the data string on UI from data callbacks.
     */
    private void updateData(final String result){
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                if (result != null) {
                    if(dataLength ++ > 100) {
                        // Clear the cache after 100 scans
                        textViewData.setText("");
                        dataLength = 0;
                    }
                    textViewData.append(Html.fromHtml(result));
                    textViewData.append("\n");
                    ((View) findViewById(R.id.scrollViewData)).post(new Runnable()
                    {
                        public void run()
                        {
                            ((ScrollView) findViewById(R.id.scrollViewData)).fullScroll(View.FOCUS_DOWN);
                        }
                    });
                }
            }
        });
    }

    /**
     * Implements the mechanism used to set default orientation.
     * NOTE: This sample does not support dynamic orientation change and is locked for default orientation
     * of the device. Better readability of the UI is achieved by dynamically inflating the dedicated landscape
     * or portrait layout based on the default orientation of the device.
     */
    private void setDefaultOrientation(){
        DisplayMetrics dm = new DisplayMetrics();
        getWindowManager().getDefaultDisplay().getMetrics(dm);
        int width = dm.widthPixels;
        int height = dm.heightPixels;
        if(width > height){
            setContentView(R.layout.activity_main_landscape);
        } else {
            setContentView(R.layout.activity_main);
        }
    }

    /**
     * Implements checkbox change listener.
     */
    @Override
    public void onCheckedChanged(CompoundButton arg0, boolean arg1) {
        // Need to apply new scanner configuration changes. Hence, cancel any pending read.
        applyScannerConfiguration = true;
        cancelRead();
    }

}
