package fr.drangies.cordova.serial;

package cn.wch.ch34xuartdriver;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

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;

/**
 * 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 ACTION_USB_ATTACHED = "usbAttached";
    private static final String ACTION_USB_PERMISSION = "cn.wch.wchusbdriver.USB_PERMISSION";

    public CH34xUARTDriver driver;
    // UsbManager instance to deal with permission and opening
    private UsbManager usbManager;
    private Context applicationContext;
    // 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 int dataBits;
    private int stopBits;
    private int parity;
    private boolean setDTR;
    private boolean setRTS;
    private boolean sleepOnPause;
    // 使用线程安全的连接状态标记
    private volatile boolean isOpen = false;

    // callback that will be used to send back data to the cordova app
    private CallbackContext readCallback;

    @Override
    protected void pluginInitialize() {
        Log.d(TAG, "Initializing Serial");
        applicationContext = getApplicationContext();
        usbManager = ((UsbManager) applicationContext.getSystemService("usb"));
        driver = new CH34xUARTDriver(usbManager, this, ACTION_USB_PERMISSION);
        if (!driver.UsbFeatureSupported()) {
            Dialog dialog = new AlertDialog.Builder(this)
                    .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;
    }

    private Context getApplicationContext() {
        return cordova.getActivity().getApplicationContext();
    }

    @Override
    public void onDestroy() {
        synchronized (this) {
            try {
                this.isOpen = false;
                this.driver.CloseDevice();
                this.applicationContext.unregisterReceiver(this.detachReceiver);
            } catch (Exception exp) {
                Log.e(TAG, "Issue while unregistering USB detach listener",exp);
            }
            try {
                this.applicationContext.unregisterReceiver(this.mPermissionReceiver);
            } catch (Exception exp) {
                Log.e(TAG, "Issue while unregistering USB permission listener", exp);
            }
        }
    }

    private void requestPermission(final JSONObject opts, final CallbackContext callbackContext) {
        cordova.getThreadPool().execute(new Runnable() {
            public void run() {
                if (!isOpen) {
                    if (driver.UsbFeatureSupported()) {
                        callbackContext.error("Not Support!");
                    } else {
                        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;
                                PendingIntent pendingIntent = PendingIntent.getBroadcast(cordova.getActivity(), 0, new Intent(UsbBroadcastReceiver.USB_PERMISSION), 0);
                                // and a filter on the permission we ask
                                IntentFilter filter = new IntentFilter();
                                filter.addAction(UsbBroadcastReceiver.USB_PERMISSION);
                                // this broadcast receiver will handle the permission results
                                UsbBroadcastReceiver usbReceiver = new UsbBroadcastReceiver(callbackContext, cordova.getActivity());
                                cordova.getActivity().registerReceiver(usbReceiver, filter);
                                new readThread().start();
                            } else {
                                callbackContext.error("Open failed!");
                            }
                        }
                    }
                }

            }
        });

    }



    /**
     * 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;
    }

    /**
     * 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() {
                if (driver.mDeviceConnection != null) {
                   baudRate = opts.has("baudRate") ? opts.getInt("baudRate") : 9600;
                   dataBits = opts.has("dataBits") ? opts.getInt("dataBits") : 8;
                   stopBits = opts.has("stopBits") ? opts.getInt("stopBits") : 1;
                   parity = opts.has("parity") ? opts.getInt("parity") : 0;
                    if (driver.SetConfig(baudRate, dataBits, stopBits, parity, true)) {
                        callbackContext.success("Config successfully");
                    } else {
                       callbackContext.error("Config failed!");
                    }
                    onDeviceStateChange();
               } else {
                    callbackContext.error("Config failed!");
               }
            }
        });

    }

    /**
     * Write on the serial port
     *
     * @param data            the {@link String} representation of the data to be written on the port
     * @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(writeText.getText().toString());
                        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 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) {
                            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 (port != null) {
                        port.close();
                    }
                    port = null;
                    callbackContext.success();
                } catch (IOException e) {
                    // deal with error
                    Log.d(TAG, e.getMessage());
                    callbackContext.error(e.getMessage());
                }
                onDeviceStateChange();
            }
        });
    }


    /**
     * Stop observing serial connection
     */
    private void stopIoManager() {
        if (mSerialIoManager != null) {
            Log.i(TAG, "Stopping io manager.");
            mSerialIoManager.stop();
            mSerialIoManager = null;
        }
    }

    /**
     * Observe serial connection
     */
    private void startIoManager() {
        if (driver != null) {
            Log.i(TAG, "Starting io manager.");
            mSerialIoManager = new SerialInputOutputManager(port, mListener);
            mExecutor.submit(mSerialIoManager);
        }
    }

    /**
     * Restart the observation of the serial connection
     */
    private void onDeviceStateChange() {
        stopIoManager();
        startIoManager();
    }

    /**
     * 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);
            }
        });
    }


    @Override
    public void onDestroy() {
        try {
            driver.CloseDevice();
            isOpen = false;
        } catch (IOException e) {
            Log.d(TAG, e.getMessage());
        }
        onDeviceStateChange();
    }
}
