package com.example.myapplication.util;

import static android.app.PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT;
import static android.app.PendingIntent.FLAG_MUTABLE;
import static android.content.Context.RECEIVER_EXPORTED;
import static android.content.Context.RECEIVER_NOT_EXPORTED;

import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.AssetManager;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbDeviceConnection;
import android.hardware.usb.UsbInterface;
import android.hardware.usb.UsbManager;
import android.os.Build;
import android.os.Handler;
import android.util.Log;

import androidx.annotation.NonNull;

import com.felhr.usbserial.UsbSerialDevice;
import com.felhr.usbserial.UsbSerialInterface;
import com.example.myapplication.R;

import java.io.ByteArrayOutputStream;
import java.util.List;


public class SenxorDevicesHelper
{
    private boolean mIsSerialOpen;
    private String LOG_TAG = this.getClass().getSimpleName();
    private Context mContext;

    private Handler mHandler;

    private PendingIntent mPendingIntent;

    private UsbDevice mSenxorDev;
    private UsbDevice mSenxorDFUDev;

    private UsbDeviceConnection mSenxorConnection;
    private UsbSerialInterface mSenxorSerialPort;
    private UsbInterface mSenxorIfcCom;
    private UsbInterface mSenxorIfcData;
    private UsbManager mUsbManger;

    private int [][] mDevIdFilter;

    private int [][] mFwVer;
    private int mFwMajorVer;
    private int mFwMinorVer;
    private int mFwBldVer;

    public SenxorDevicesHelper(@NonNull Context context, @NonNull Handler handler)
    {
        this.mIsSerialOpen = false;
        this.mContext = context;
        this.mHandler = handler;
        this.mFwVer = new int[1][3];

        int flags = 0;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
        {
            // Up until Build.VERSION_CODES.R, PendingIntents are assumed to be mutable by default, unless FLAG_IMMUTABLE is set.
            // Starting with Build.VERSION_CODES.S, it will be required to explicitly specify the mutability of PendingIntents on creation with either (@link #FLAG_IMMUTABLE} or FLAG_MUTABLE.
            flags = PendingIntent.FLAG_MUTABLE;
        }
        if(Build.VERSION.SDK_INT == Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
        {
            flags = PendingIntent.FLAG_MUTABLE | FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT;                     //For Android 14
        }

        this.mPendingIntent = PendingIntent.getBroadcast(this.mContext, 0, new Intent("com.example.myapplication.USB_PERMISSION"), flags);


        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
        {
            this.mContext.registerReceiver(new SenxorDevUsbReceiver(), new IntentFilter("com.example.myapplication.USB_PERMISSION"),RECEIVER_EXPORTED);
        }
        else
        {
            this.mContext.registerReceiver(new SenxorDevUsbReceiver(), new IntentFilter("com.example.myapplication.USB_PERMISSION"));
        }//End if-else

        this.mUsbManger = (UsbManager) this.mContext.getSystemService(Context.USB_SERVICE);
        /*
            Load vid and pid
         */
        String [] vid = this.mContext.getResources().getStringArray(R.array.DEV_VID);
        String [] pid = this.mContext.getResources().getStringArray(R.array.DEV_PID);
        this.mDevIdFilter = new int[vid.length][2];
        assert vid.length == pid.length;

        for(int i = 0 ; i < vid.length ; i++)
        {
            final int v = Integer.parseInt(vid[i].substring(2), 16);
            final int p = Integer.parseInt(pid[i].substring(2), 16);
            mDevIdFilter[i][0] = v;
            mDevIdFilter[i][1] = p;
        }//End for

    }

    /**
     * attach
     * Identify a SenXor device and request permission to interface with the device
     * @param       usbDevice - A list contains UsbDevice
     * @exception   None
     * @return      None
     */
    public void attach(List<UsbDevice>usbDevice)
    {
        //Iterate through the list
        for (UsbDevice dev: usbDevice)
        {
            //If a valid device is discovered, request permission to access this device
            if(isSenxor(dev))
            {
                this.mUsbManger.requestPermission(dev, this.mPendingIntent);                             //Request permission
                break;
            }//End if
        }//End for
    }

    /**
     *              flashFirmware
     *              Load firmware binary from app resources and flash into MCU
     * @param       resId - Resources ID
     * @exception   None
     * @return      None
     */
    public void flashFirmware(int resId)
    {
        if(this.mSenxorDFUDev == null)
        {
            return;
        }//End if

        AssetManager.AssetInputStream istream = (AssetManager.AssetInputStream)(this.mContext.getResources().openRawResource(resId));
        ByteArrayOutputStream ostream = new ByteArrayOutputStream();

        try
        {
            int dataRead = istream.read();
            while (dataRead != -1)
            {
                ostream.write(dataRead);
                dataRead = istream.read();
            }//End while
            istream.close();
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }//End try-catch

        final Intent intent = new Intent("com.example.myapplication.fwReadDone");
        intent.putExtra("fwSize",ostream.size());
        intent.putExtra("fwName",this.mContext.getResources().getResourceName(resId));
        mContext.sendBroadcast(intent);
        DfuProgramRunnable runProgram = new DfuProgramRunnable(this.mContext, this.mHandler, this.mSenxorDFUDev, ostream.toByteArray());
        new Thread(runProgram).start();
    }

    /**
     *              release
     *              Release attached devices and free its resources
     * @param       resId - Resources ID
     * @exception   None
     * @return      None
     */
    public void release()
    {
        this.mSenxorDFUDev = null;
        this.mSenxorDev = null;
        if(this.mIsSerialOpen)
        {
            this.disconenctSerial();
        }
    }

    /**
     *              connectSerial
     *              Establish serial connection to a SenXor device
     * @param       None
     * @exception   None
     * @return      None
     */
    public boolean connectSerial()
    {
        if(this.mSenxorDev == null)
        {
            return false;
        }

        this.mSenxorConnection = this.mUsbManger.openDevice(this.mSenxorDev);
        if(this.mSenxorConnection == null)
        {
            return false;
        }
        this.mSenxorSerialPort = UsbSerialDevice.createUsbSerialDevice(this.mSenxorDev, this.mSenxorConnection);
        if(this.mSenxorSerialPort.open())
        {
            this.mSenxorSerialPort.setBaudRate(115200);
            this.mSenxorSerialPort.setDataBits(UsbSerialInterface.DATA_BITS_8);
            this.mSenxorSerialPort.setStopBits(UsbSerialInterface.STOP_BITS_1);
            this.mSenxorSerialPort.setParity(UsbSerialInterface.PARITY_NONE);
            this.mSenxorSerialPort.setFlowControl(UsbSerialInterface.FLOW_CONTROL_OFF);
            this.mSenxorSerialPort.read(new SenxorSerialCallback());
            this.mIsSerialOpen = true;
            return true;
        }
        else
        {
            this.mSenxorSerialPort.close();
            this.mIsSerialOpen = false;
            return false;
        }//End if-else
    }

    /**
     *              connectSerial
     *              Establish serial connection to a SenXor device
     * @param       usbDevice - A USB device
     *              usbReadCallback - Callback function
     * @exception   None
     * @return      None
     */
    public void connectSerial(UsbDevice usbDevice, UsbSerialInterface.UsbReadCallback usbReadCallback)
    {
        this.mSenxorSerialPort = UsbSerialDevice.createUsbSerialDevice(usbDevice, this.mSenxorConnection);
        this.mSenxorSerialPort.setBaudRate(115200);
        this.mSenxorSerialPort.setDataBits(UsbSerialInterface.DATA_BITS_8);
        this.mSenxorSerialPort.setStopBits(UsbSerialInterface.STOP_BITS_1);
        this.mSenxorSerialPort.setParity(UsbSerialInterface.PARITY_NONE);
        this.mSenxorSerialPort.setFlowControl(UsbSerialInterface.FLOW_CONTROL_OFF);
        this.mSenxorSerialPort.read(usbReadCallback);
    }

    /**
     *              disconenctSerial
     *              Disconnect serial connection to a SenXor device
     * @param       None
     * @exception   None
     * @return      None
     */
    public void disconenctSerial()
    {
        if(this.mSenxorSerialPort != null)
        {
            this.mSenxorSerialPort.close();
        }
        this.mIsSerialOpen = false;
    }

    /**
     *              getIsSerialOpen
     *              Check if the serial port is opened
     * @param       None
     * @exception   None
     * @return      boolean: True if open. False otherwise
     */
    public boolean getIsSerialOpen()
    {
        return this.mIsSerialOpen;
    }

    /**
     * readDevFwVer
     * Query Senxor firmware by writing command. Obtain result via callback
     * @param       None
     * @exception   None
     * @return      None
     */
    public void readDevFwVer()
    {
        if(!this.connectSerial())
        {
            return;
        }//End if
        final String cmd = SenXorEvkCommand.RRSE_FW_VERSION_SUBSTR + this.getCRC(SenXorEvkCommand.RRSE_FW_VERSION_SUBSTR);
        this.mSenxorSerialPort.write(cmd.getBytes());
    }

    /**
     * isSenxor
     * Identify whether the USB device a SenXor device
     * @param       device - UsbDevice object
     * @exception   None
     * @return      True if the USB device a SenXor device. False otherwise
     */
    private boolean isSenxor(UsbDevice device)
    {
        int vid = device.getVendorId();
        int pid = device.getProductId();

        for(int i = 0 ; i<this.mDevIdFilter.length;++i)
        {
            if(vid == this.mDevIdFilter[i][0] && pid == this.mDevIdFilter[i][1])
            {
                return true;
            }
        }
        return false;
    }//End isSenXor

    /**
     * getSenxorDev
     * Get SenXor Device
     * @param       None
     * @exception   None
     * @return      mSenxorDev - UsbDevice object
     */
    public UsbDevice getSenxorDev()
    {
        return this.mSenxorDev;
    }

    /**
     * getSenxorDev
     * Get SenXor Device in DFU mode
     * @param       None
     * @exception   None
     * @return      mSenxorDFUDev - UsbDevice object
     */
    public UsbDevice getSenxorDFUDev()
    {
        return this.mSenxorDFUDev;
    }

    /**
     *              getCRC
     *              calculate CRC value
     * @param       input - String contains Evk command
     * @exception   None
     * @return      String: CRC value
     */
    private String getCRC(String input)
    {
        if(input.startsWith("#"))
        {
            input = input.substring(1);
        }

        int crcValue = 0;

        for(int i=0; i<input.length(); i++)
        {
            crcValue += input.charAt(i);
        }

        return String.format("%04X", crcValue);
    }

    public int [][] getFwVersion()
    {
        return this.mFwVer;
    }
    /**
     * SenxorDevUsbReceiver
     * Broadcast receiver for handling USB permission granted
     */
    private class SenxorDevUsbReceiver extends BroadcastReceiver
    {
        @Override
        public void onReceive(final Context context, final Intent intent)
        {
            final String action = intent.getAction();

            //If permission is granted
            if (("com.example.myapplication.USB_PERMISSION").equals(action))
            {
                synchronized (this)
                {
                    final UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);

                    if(usbDevice == null)
                    {
                        Log.e(LOG_TAG,"usb device is null");
                        return;
                    }//End if

                    if(UsbDfuConstants.findDfuRuntimeModeInterface(usbDevice) > -1)
                    {
                        //If the found device has a DFU interface
                        mSenxorDev = usbDevice;
                        intent.setAction("com.example.myapplication.findDev");
                        mContext.sendBroadcast(intent);
                        Log.d(LOG_TAG,"Find DFU interface");
                    }
                    else if (UsbDfuConstants.findDfuModeInterface(usbDevice) > -1)
                    {
                        mSenxorDFUDev = usbDevice;
                        intent.setAction("com.example.myapplication.findDfu");
                        mContext.sendBroadcast(intent);
                        Log.d(LOG_TAG,"Find DFU device");
                    }
                    else
                    {
                        intent.setAction("com.example.myapplication.findDevNotRecognise");
                        mContext.sendBroadcast(intent);
                    }//End if-else
                }
            }//End if
        }//End onReceive
    };

    /**
     * SenxorSerialCallback
     * Callback when data is received
     */
    private class SenxorSerialCallback implements UsbSerialInterface.UsbReadCallback
    {

        @Override
        public void onReceivedData(final byte[] arg0)
        {

            if(arg0.length != 24)
            {
                return;
            }

            final String msg = new String(arg0);
            Log.d(LOG_TAG,msg);
            final String cmdStr = msg.substring(8 ,12);

            if (cmdStr.compareTo("RRSE") == 0)
            {
                if(msg.substring(12 ,14).compareTo("B2") == 0)
                {
                    Log.d(LOG_TAG,"version = "+Character.getNumericValue(arg0[14]) + "."+Character.getNumericValue(arg0[15]));
                    mFwVer[0][0] = getHexValue((char)arg0[14]);
                    mFwVer[0][1]  = getHexValue((char)arg0[15]);
                }
                if(msg.substring(16 ,18).compareTo("B3") == 0)
                {
                    char[] hexDigits = new char[2];
                    hexDigits[0] = (char) arg0[18];
                    hexDigits[1] = (char) arg0[19];
                    mFwVer[0][2] = Integer.parseInt(String.valueOf(hexDigits), 16);
//                    mFwVer[0][2] = getHexValue((char)arg0[18])*10 + getHexValue((char)arg0[19]);
                }
                Log.d(LOG_TAG,"version = "+mFwVer[0][0] + "."+mFwVer[0][1]+"."+mFwVer[0][2]);
                mContext.sendBroadcast(new Intent("com.example.myapplication.fwVerRdDone"));
            }

        }//End onReceivedData
    }

    /******************************************************************************
     * @brief       getHexValue
     * @param       b - Input
     * @return      Hexadecimal value of the input b
     * @details     Convert a value to hexadecimal
     *****************************************************************************/
    private int getHexValue(char b)
    {
    if (b >= '0' && b <= '9')
    {
        return (b - '0');
    }
    else if (b >= 'A' && b <= 'F')
    {
        return (b -  'A' + 10);
    }
    else if (b >= 'a' && b <= 'f')
    {
        return (b - 'a' + 10);
    }
    else
    {
        return -1;
    }
}
}
