package dev.bessems.usbserial;

import android.app.PendingIntent;
import android.content.BroadcastReceiver;
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.Log;

import com.felhr.usbserial.UsbSerialDevice;
import com.felhr.usbserial.UsbSerialInterface;

import java.util.HashMap;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;

import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result;
import io.flutter.plugin.common.EventChannel;
import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.plugin.common.BinaryMessenger;

import androidx.annotation.NonNull;


/** UsbSerialPlugin */
public class UsbSerialPlugin implements FlutterPlugin, MethodCallHandler, EventChannel.StreamHandler {

    private final String TAG = UsbSerialPortAdapter.class.getSimpleName();

    private android.content.Context m_Context;
    private UsbManager m_Manager;
    private int m_InterfaceId;
    private BinaryMessenger m_Messenger;
    private EventChannel.EventSink m_EventSink;

    private static final String ACTION_USB_PERMISSION = "com.android.example.USB_PERMISSION";
    public static final String ACTION_USB_ATTACHED = "android.hardware.usb.action.USB_DEVICE_ATTACHED";
    public static final String ACTION_USB_DETACHED = "android.hardware.usb.action.USB_DEVICE_DETACHED";


    private final BroadcastReceiver usbReceiver = new BroadcastReceiver() {

        @Override
        public void onReceive(Context context, Intent intent) {
            if (intent.getAction().equals(ACTION_USB_ATTACHED)) {
                Log.d(TAG, "ACTION_USB_ATTACHED");
                if ( m_EventSink != null ) {
                    UsbDevice device = (UsbDevice)intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
                    HashMap<String, Object> msg = serializeDevice(device);
                    msg.put("event", ACTION_USB_ATTACHED);
                    m_EventSink.success(msg);
                }
            } else if (intent.getAction().equals(ACTION_USB_DETACHED)) {
                Log.d(TAG, "ACTION_USB_DETACHED");
                if ( m_EventSink != null ) {
                    UsbDevice device = (UsbDevice)intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
                    HashMap<String, Object> msg = serializeDevice(device);
                    msg.put("event", ACTION_USB_DETACHED);
                    m_EventSink.success(msg);
                }
            }
        }
    };

    public UsbSerialPlugin() {
        m_Messenger = null;
        m_Context = null;
        m_Manager = null;
        m_InterfaceId = 0;
    }


    private interface AcquirePermissionCallback {
        void onSuccess(UsbDevice device);
        void onFailed(UsbDevice device);
    }
    private void acquirePermissions(UsbDevice device, AcquirePermissionCallback cb) {

        class BRC2 extends  BroadcastReceiver {

            private UsbDevice m_Device;
            private AcquirePermissionCallback m_CB;

            BRC2(UsbDevice device, AcquirePermissionCallback cb ) {
                m_Device = device;
                m_CB = cb;
            }

            @Override
            public void onReceive(Context context, Intent intent) {
                String action = intent.getAction();
                if (ACTION_USB_PERMISSION.equals(action)) {
                    Log.e(TAG, "BroadcastReceiver intent arrived, entering sync...");
                    m_Context.unregisterReceiver(this);
                    synchronized (this) {
                        Log.e(TAG, "BroadcastReceiver in sync");
                        /* UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); */
                        if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
                            // createPort(m_DriverIndex, m_PortIndex, m_Result, false);
                            m_CB.onSuccess(m_Device);
                        } else {
                            Log.d(TAG, "permission denied for device ");
                            m_CB.onFailed(m_Device);
                        }
                    }
                }
            }
        }

        Context cw = m_Context; //m_Registrar.context();

        BRC2 usbReceiver = new BRC2(device, cb);

        int flags = 0;

        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
            flags = PendingIntent.FLAG_MUTABLE;
        }

        PendingIntent permissionIntent = PendingIntent.getBroadcast(cw, 0, new Intent(ACTION_USB_PERMISSION), flags);

        IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION);
        cw.registerReceiver(usbReceiver, filter);

        m_Manager.requestPermission(device, permissionIntent);
    }

    private void openDevice(String type, UsbDevice device, int iface, Result result, boolean allowAcquirePermission) {

        final AcquirePermissionCallback cb = new AcquirePermissionCallback() {

            @Override
            public void onSuccess(UsbDevice device) {
                openDevice(type, device, iface, result, false);
            }

            @Override
            public void onFailed(UsbDevice device) {
                result.error(TAG, "Failed to acquire permissions.", null);
            }
        };

        try {
            UsbDeviceConnection connection = m_Manager.openDevice(device);

            if ( connection == null && allowAcquirePermission ) {
                acquirePermissions(device, cb);
                return;
            }

            UsbSerialDevice serialDeviceDevice;
            if ( type.equals("") ) {
                serialDeviceDevice = UsbSerialDevice.createUsbSerialDevice(device, connection, iface);
            } else {
                serialDeviceDevice = UsbSerialDevice.createUsbSerialDevice(type, device, connection, iface);
            }

            if (serialDeviceDevice != null) {
                int interfaceId = m_InterfaceId++;
                UsbSerialPortAdapter adapter = new UsbSerialPortAdapter(m_Messenger, interfaceId, connection, serialDeviceDevice);
                result.success(adapter.getMethodChannelName());
                Log.d(TAG, "success.");
                return;
            }
            result.error(TAG, "Not an Serial device.", null);

        } catch ( java.lang.SecurityException e ) {

            if ( allowAcquirePermission ) {
                acquirePermissions(device, cb);
                return;
            } else {
                result.error(TAG, "Failed to acquire USB permission.", null);
            }
        } catch ( java.lang.Exception e ) {
            result.error(TAG, "Failed to acquire USB device.", null);
        }
    }

    private void createTyped(String type, int vid, int pid, int deviceId, int iface, Result result) {
        Map<String, UsbDevice> devices = m_Manager.getDeviceList();
        for (UsbDevice device : devices.values()) {

            if ( deviceId == device.getDeviceId() || (device.getVendorId() == vid && device.getProductId() == pid) ) {
                openDevice(type, device, iface, result, true);
                return;
            }
        }

        result.error(TAG, "No such device", null);
    }

    private HashMap<String, Object> serializeDevice(UsbDevice device) {
        HashMap<String, Object> dev = new HashMap<>();
        dev.put("deviceName", device.getDeviceName());
        dev.put("vid", device.getVendorId());
        dev.put("pid", device.getProductId());
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
            dev.put("manufacturerName", device.getManufacturerName());
            dev.put("productName", device.getProductName());
            dev.put("interfaceCount", device.getInterfaceCount());
            /* if the app targets SDK >= android.os.Build.VERSION_CODES.Q and the app does not have permission to read from the device. */
            try {
                dev.put("serialNumber", device.getSerialNumber());
            } catch  ( java.lang.SecurityException e ) {
            }
        }
        dev.put("deviceId", device.getDeviceId());
        return dev;
    }

    private void listDevices(Result result) {
        Map<String, UsbDevice> devices = m_Manager.getDeviceList();
        if ( devices == null ) {
            result.error(TAG, "Could not get USB device list.", null);
            return;
        }
        List<HashMap<String, Object>> transferDevices = new ArrayList<>();

        for (UsbDevice device : devices.values()) {
            transferDevices.add(serializeDevice(device));
        }
        result.success(transferDevices);
    }


    @Override
    public void onListen(Object o, EventChannel.EventSink eventSink) {
        m_EventSink = eventSink;

    }

    @Override
    public void onCancel(Object o) {
        m_EventSink = null;
    }


    private EventChannel m_EventChannel;
    private void register(BinaryMessenger messenger, android.content.Context context) {
        m_Messenger = messenger;
        m_Context = context;
        m_Manager = (UsbManager) m_Context.getSystemService(android.content.Context.USB_SERVICE);
        m_InterfaceId = 100;
        m_EventChannel = new EventChannel(messenger, "usb_serial/usb_events");
        m_EventChannel.setStreamHandler(this);

        IntentFilter filter = new IntentFilter();
        filter.addAction(ACTION_USB_DETACHED);
        filter.addAction(ACTION_USB_ATTACHED);
        m_Context.registerReceiver(usbReceiver, filter);
    }

    private void unregister() {
        m_Context.unregisterReceiver(usbReceiver);
        m_EventChannel.setStreamHandler(null);
        m_Manager = null;
        m_Context = null;
        m_Messenger = null;
    }


    /// This local reference serves to register the plugin with the Flutter Engine and unregister it
    /// when the Flutter Engine is detached from the Activity
    private MethodChannel m_Channel;

    @Override
    public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
        register(flutterPluginBinding.getBinaryMessenger(), flutterPluginBinding.getApplicationContext());
        m_Channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), "usb_serial");
        m_Channel.setMethodCallHandler(this);

    }

    @Override
    public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
        m_Channel.setMethodCallHandler(null);
        unregister();
    }

    @Override
    public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {

        switch (call.method) {

            case "create": {
                createTyped((String)call.argument("type"), (int)call.argument("vid"),
                        (int)call.argument("pid"), (int)call.argument("deviceId"),
                        (int)call.argument("interface"), result);
                break;
            }
            case "listDevices":
                listDevices(result);
                break;

            default:
                result.notImplemented();
                break;
            }

    }

}

