package fr.drangies.cordova.serial;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import cn.wch.ch34xuartdriver.CH34xUARTDriver;

import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.PluginResult;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbDeviceConnection;
import android.hardware.usb.UsbManager;
import android.util.Base64;
import android.util.Log;
import android.os.Build;

import android.widget.Toast;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Handler;

/**
 * Cordova plugin to communicate with the android serial port
 *
 * @author Xavier Seignard <xavier.seignard@gmail.com>
 */
public class Serial extends CordovaPlugin {
    // logging tag
    private final String TAG = Serial.class.getSimpleName();
    // actions definitions
    private static final String ACTION_REQUEST_PERMISSION = "requestPermission";
    private static final String ACTION_OPEN = "openSerial";
    private static final String ACTION_READ = "readSerial";
    private static final String ACTION_WRITE = "writeSerial";
    private static final String ACTION_WRITE_HEX = "writeSerialHex";
    private static final String ACTION_CLOSE = "closeSerial";
    private static final String ACTION_USB_DETACHED = "usbDetached";
    private static final String ACTION_READ_CALLBACK = "registerReadCallback";
    private static final String USB_PERMISSION = "fr.drangies.cordova.serial.USB_PERMISSION";

    private static final String ACTION_USB_ATTACHED = "usbAttached";

    public readThread handlerThread;

    // UsbManager instance to deal with permission and opening
    private UsbManager manager;
    // The current driver that handle the serial port
    public CH34xUARTDriver driver;
    // Read buffer, and read params
    private static final int READ_WAIT_MILLIS = 200;
    private static final int BUFSIZ = 4096;
    private final ByteBuffer mReadBuffer = ByteBuffer.allocate(BUFSIZ);
    // Connection info
    private int baudRate;
    private byte stopBits;
    private byte dataBits;
    private byte parity;
    private byte flowControl = 1;
    // 使用线程安全的连接状态标记
    private volatile boolean isOpen = false;

    // callback that will be used to send back data to the cordova app
    private CallbackContext readCallback;

    /**
     * Overridden execute method
     *
     * @param action          the string representation of the action to execute
     * @param args
     * @param callbackContext the cordova {@link CallbackContext}
     * @return true if the action exists, false otherwise
     * @throws JSONException if the args parsing fails
     */
    @Override
    public boolean execute(String action, JSONArray args, final CallbackContext callbackContext) throws JSONException {
        Log.d(TAG, "Action: " + action);
        JSONObject arg_object = args.optJSONObject(0);
        // request permission
        if (ACTION_REQUEST_PERMISSION.equals(action)) {
            JSONObject opts = arg_object.has("opts") ? arg_object.getJSONObject("opts") : new JSONObject();
            requestPermission(opts, callbackContext);
            return true;
        }
        // open serial port
        else if (ACTION_OPEN.equals(action)) {
            JSONObject opts = arg_object.has("opts") ? arg_object.getJSONObject("opts") : new JSONObject();
            openSerial(opts, callbackContext);
            return true;
        }
        // write hex to the serial port
        else if (ACTION_WRITE_HEX.equals(action)) {
            String data = arg_object.getString("data");
            writeSerialHex(data, callbackContext);
            return true;
        }
        // read on the serial port
        else if (ACTION_READ.equals(action)) {
            readSerial(callbackContext);
            return true;
        }
        // close the serial port
        else if (ACTION_CLOSE.equals(action)) {
            closeSerial(callbackContext);
            return true;
        }
        // Register read callback
        else if (ACTION_READ_CALLBACK.equals(action)) {
            registerReadCallback(callbackContext);
            return true;
        }
        // Receive USB device detach
        else if (ACTION_USB_DETACHED.equals(action)) {
            usbDetached(callbackContext);
            return true;
        }
        // Receive USB device attach
        else if (ACTION_USB_ATTACHED.equals(action)) {
            usbAttached(callbackContext);
            return true;
        }
        // the action doesn't exist
        return false;
    }

    @Override
    protected void pluginInitialize() {
        Log.d(TAG, "Initializing Serial");
        Context context = cordova.getActivity().getApplicationContext();
        manager = (UsbManager) cordova.getActivity().getSystemService(Context.USB_SERVICE);
        driver = new CH34xUARTDriver(manager, context, USB_PERMISSION);
        if (!driver.UsbFeatureSupported()) {
            Dialog dialog = new AlertDialog.Builder(context)
                .setTitle("提示")
                .setMessage("您的设备不支持USB HOST，请更换其他设备再试！")
                .setPositiveButton("确认",
                    new DialogInterface.OnClickListener() {

                        @Override
                        public void onClick(DialogInterface arg0,
                                            int arg1) {
                            System.exit(0);
                        }
                    }).create();
            dialog.setCanceledOnTouchOutside(false);
            dialog.show();
        }
        isOpen = false;
    }

    /**
     * Request permission the the user for the app to use the USB/serial port
     *
     * @param callbackContext the cordova {@link CallbackContext}
     */
    private void requestPermission(final JSONObject opts, final CallbackContext callbackContext) {
        cordova.getThreadPool().execute(new Runnable() {
            public void run() {
                if (driver == null) {
                    Context context = cordova.getActivity().getApplicationContext();
                    manager = (UsbManager) cordova.getActivity().getSystemService(Context.USB_SERVICE);
                    driver = new CH34xUARTDriver(manager, context, USB_PERMISSION);
                }

                int retval = driver.ResumeUsbPermission();
                if (retval == -1) {
                    callbackContext.error("No device found!");
                    driver.CloseDevice();
                } else if (retval == 0) {
                    if (driver.mDeviceConnection != null) {
                        if (!driver.UartInit()) {
                            callbackContext.error("Initialization failed!");
                            return;
                        }
                        isOpen = true;
                        // and a filter on the permission we ask
                        IntentFilter filter = new IntentFilter();
                        filter.addAction(USB_PERMISSION);
                        // this broadcast receiver will handle the permission results
                        UsbBroadcastReceiver usbReceiver = new UsbBroadcastReceiver(callbackContext, cordova.getActivity());
                        cordova.getActivity().registerReceiver(usbReceiver, filter);
                    } else {
                        callbackContext.error("Open failed!");
                    }
                }
            }
        });
    }

    /**
     * Open the serial port from Cordova
     *
     * @param opts            a {@link JSONObject} containing the connection paramters
     * @param callbackContext the cordova {@link CallbackContext}
     */
    private void openSerial(final JSONObject opts, final CallbackContext callbackContext) {
        cordova.getThreadPool().execute(new Runnable() {
            public void run() {
                try {
                    if (driver.mDeviceConnection != null) {
                        baudRate = opts.has("baudRate") ? opts.getInt("baudRate") : 9600;
                        dataBits = (byte) Integer.parseInt(opts.has("dataBits") ? opts.getInt("dataBits") : 8);
                        stopBits = (byte) Integer.parseInt(opts.has("stopBits") ? opts.getInt("stopBits") : 1);
                        parity = (byte) Integer.parseInt(opts.has("parity") ? opts.getInt("parity") : 0);
                        if (driver.SetConfig(baudRate, dataBits, stopBits, parity, flowControl)) {
                            new readThread().start();
                            callbackContext.success("Config successfully");
                        } else {
                            callbackContext.error("Config failed!");
                        }
                    } else {
                        callbackContext.error("Config failed!");
                    }
                } catch (RuntimeException e) {
                    Log.d(TAG, Objects.requireNonNull(e.getMessage()));
                    callbackContext.error(e.getMessage());
                }
            }
        });
    }

    /**
     * Write hex on the serial port
     *
     * @param data            the {@link String} representation of the data to be written on the port as hexadecimal string
     *                        e.g. "ff55aaeeef000233"
     * @param callbackContext the cordova {@link CallbackContext}
     */
    private void writeSerialHex(final String data, final CallbackContext callbackContext) {
        cordova.getThreadPool().execute(new Runnable() {
            public void run() {
                if (driver.mDeviceConnection == null) {
                    callbackContext.error("Writing a closed port.");
                } else {
                    try {
                        byte[] to_send = toByteArray(data);
                        int retval = driver.WriteData(to_send, to_send.length);
                        if (retval < 0) {
                            callbackContext.error("Write failed!");
                        } else {
                            callbackContext.success(retval + " written.");
                        }
                    } catch (IOException e) {
                        Log.d(TAG, e.getMessage());
                        callbackContext.error(e.getMessage());
                    }
                }
            }
        });
    }
    private class readThread extends Thread {

        public void run() {

            byte[] buffer = new byte[4096];

            while (true) {

                if (driver.mDeviceConnection == null) {
                    break;
                }
                int length = driver.ReadData(buffer, 4096);
                if (length > 0) {
                    final byte[] data = new byte[length];
                    updateReceivedData(data);
                }
            }
        }
    }


    /**
     * Convert a given string of hexadecimal numbers
     * into a byte[] array where every 2 hex chars get packed into
     * a single byte.
     * <p>
     * E.g. "ffaa55" results in a 3 byte long byte array
     *
     * @param s
     * @return
     */
    private byte[] hexStringToByteArray(String s) {
        int len = s.length();
        byte[] data = new byte[len / 2];
        for (int i = 0; i < len; i += 2) {
            data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
                + Character.digit(s.charAt(i + 1), 16));
        }
        return data;
    }

    private byte[] toByteArray(String arg) {
        if (arg != null) {
            char[] NewArray = new char[1000];
            char[] array = arg.toCharArray();
            int length = 0;
            for (int i = 0; i < array.length; i++) {
                if (array[i] != ' ') {
                    NewArray[length] = array[i];
                    length++;
                }
            }
            int EvenLength = (length % 2 == 0) ? length : length + 1;
            if (EvenLength != 0) {
                int[] data = new int[EvenLength];
                data[EvenLength - 1] = 0;
                for (int i = 0; i < length; i++) {
                    if (NewArray[i] >= '0' && NewArray[i] <= '9') {
                        data[i] = NewArray[i] - '0';
                    } else if (NewArray[i] >= 'a' && NewArray[i] <= 'f') {
                        data[i] = NewArray[i] - 'a' + 10;
                    } else if (NewArray[i] >= 'A' && NewArray[i] <= 'F') {
                        data[i] = NewArray[i] - 'A' + 10;
                    }
                }
                byte[] byteArray = new byte[EvenLength / 2];
                for (int i = 0; i < EvenLength / 2; i++) {
                    byteArray[i] = (byte) (data[i * 2] * 16 + data[i * 2 + 1]);
                }
                return byteArray;
            }
        }
        return new byte[] {};
    }

    private String toHexString(byte[] arg, int length) {
        String result = new String();
        if (arg != null) {
            for (int i = 0; i < length; i++) {
                result = result
                    + (Integer.toHexString(
                    arg[i] < 0 ? arg[i] + 256 : arg[i]).length() == 1 ? "0"
                    + Integer.toHexString(arg[i] < 0 ? arg[i] + 256
                    : arg[i])
                    : Integer.toHexString(arg[i] < 0 ? arg[i] + 256
                    : arg[i])) + " ";
            }
            return result;
        }
        return "";
    }

    /**
     * Read on the serial port
     *
     * @param callbackContext the {@link CallbackContext}
     */
    private void readSerial(final CallbackContext callbackContext) {
        cordova.getThreadPool().execute(new Runnable() {
            public void run() {
                if (driver.mDeviceConnection == null) {
                    callbackContext.error("Reading a closed port.");
                } else {
                    try {
                        PluginResult.Status status = PluginResult.Status.OK;
                        byte[] buffer = new byte[4096];
                        int length = driver.ReadData(buffer, 4096);
                        if (length > 0) {
                            final byte[] data = new byte[length];
                            callbackContext.sendPluginResult(new PluginResult(status, data));
                        } else {
                            final byte[] data = new byte[0];
                            callbackContext.sendPluginResult(new PluginResult(status, data));
                        }
                    } catch (IOException e) {
                        // deal with error
                        Log.d(TAG, e.getMessage());
                        callbackContext.error(e.getMessage());
                    }
                }
            }
        });
    }

    /**
     * Close the serial port
     *
     * @param callbackContext the cordova {@link CallbackContext}
     */
    private void closeSerial(final CallbackContext callbackContext) {
        cordova.getThreadPool().execute(new Runnable() {
            public void run() {
                try {
                    // Make sure we don't die if we try to close an non-existing port!
                    if (driver.mDeviceConnection != null) {
                        driver.CloseDevice();
                    }
                    callbackContext.success();
                } catch (IOException e) {
                    // deal with error
                    Log.d(TAG, e.getMessage());
                    callbackContext.error(e.getMessage());
                }
            }
        });
    }

    /**
     * Dispatch read data to javascript
     *
     * @param data the array of bytes to dispatch
     */
    private void updateReceivedData(byte[] data) {
        if (readCallback != null) {
            PluginResult result = new PluginResult(PluginResult.Status.OK, data);
            result.setKeepCallback(true);
            readCallback.sendPluginResult(result);
        }
    }

    /**
     * Register callback for read data
     *
     * @param callbackContext the cordova {@link CallbackContext}
     */
    private void registerReadCallback(final CallbackContext callbackContext) {
        Log.d(TAG, "Registering callback");
        cordova.getThreadPool().execute(new Runnable() {
            @Override
            public void run() {
                Log.d(TAG, "Registering Read Callback");
                readCallback = callbackContext;
                PluginResult pluginResult = new PluginResult(PluginResult.Status.OK);
                pluginResult.setKeepCallback(true);
                callbackContext.sendPluginResult(pluginResult);
            }
        });
    }

    /**
     * BroadcastReceiver for USB detached
     *
     * @param callbackContext the cordova {@link CallbackContext}
     */
    private void usbDetached(final CallbackContext callbackContext) {
        Log.d(TAG, "Registering callback");
        cordova.getThreadPool().execute(new Runnable() {
            @Override
            public void run() {
                IntentFilter filterAttachDetach = new IntentFilter();
                filterAttachDetach.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED);
                // this broadcast receiver will handle the results
                UsbBroadcastReceiver usbReceiver = new UsbBroadcastReceiver(callbackContext, cordova.getActivity());
                cordova.getActivity().registerReceiver(usbReceiver, filterAttachDetach);
            }
        });
    }

    /**
     * BroadcastReceiver for USB detached
     *
     * @param callbackContext the cordova {@link CallbackContext}
     */
    private void usbAttached(final CallbackContext callbackContext) {
        Log.d(TAG, "Registering callback");
        cordova.getThreadPool().execute(new Runnable() {
            @Override
            public void run() {
                IntentFilter filterAttachDetach = new IntentFilter();
                filterAttachDetach.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
                // this broadcast receiver will handle the results
                UsbBroadcastReceiver usbReceiver = new UsbBroadcastReceiver(callbackContext, cordova.getActivity());
                cordova.getActivity().registerReceiver(usbReceiver, filterAttachDetach);
            }
        });
    }

    /**
     * Destroy activity handler
     *
     * @see org.apache.cordova.CordovaPlugin#onDestroy()
     */
    @Override
    public void onDestroy() {
        if (driver.mDeviceConnection != null) {
            try {
                driver.CloseDevice();
            } catch (IOException e) {
                Log.d(TAG, e.getMessage());
            }
        }
    }
}
