package com.example.myapplication.util;

import android.content.Context;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbDeviceConnection;
import android.hardware.usb.UsbInterface;
import android.hardware.usb.UsbManager;
import android.os.Handler;
import android.os.Message;
import android.os.SystemClock;
import android.util.Log;

import java.util.Arrays;

public class DfuProgramRunnable implements Runnable {

    private final String TAG = "DfuProgramRunnable";
    private final int DFU_BLOCK_SIZE = 512; // TODO: determine from raw USB descriptors

    private int mIdxInterface;
    private UsbManager mManager = null;
    private UsbDevice mDevice = null;
    private UsbInterface mInterface = null;
    private UsbDeviceConnection mConn = null;

    private Handler mUiHandler;
    private byte[] mBinaryData;
    private int mBlockSize;   // in bytes. Read from DFU functional descriptor.
    private int mTotalBlocks; // number of DNLOAD requests needed (excluding block 0)

    private class DfuGetStatusResult
    {
        public int bStatus;
        public int bwPollTimeout;
        public int bState;
        public int iString;
        private byte[] mSrcData;

        DfuGetStatusResult(byte[] usbData)
        {
            bStatus = usbData[0];
            bwPollTimeout = (usbData[1] & 0xFF) + ((usbData[2] & 0xFF) << 8) + ((usbData[3] & 0xFF) << 16);
            bState = usbData[4];
            iString = usbData[5];
            mSrcData = usbData;
        }
    }

    public DfuProgramRunnable(Context context, Handler uiHandler, UsbDevice device, byte[] bytesToWrite)
    {
        /*
            TODO: parameter checking:
                context == null
                check bytesToWrite
         */
        mIdxInterface = UsbDfuConstants.findDfuModeInterface(device);
        if (mIdxInterface != -1)
        {
            mDevice = device;
            mInterface = device.getInterface(mIdxInterface);
            mUiHandler = uiHandler;
            mManager = (UsbManager)context.getSystemService(Context.USB_SERVICE);
            mBinaryData = bytesToWrite;
            mBlockSize = DFU_BLOCK_SIZE;
            mTotalBlocks = mBinaryData.length / DFU_BLOCK_SIZE + ((mBinaryData.length % DFU_BLOCK_SIZE) > 0? 1 : 0);
        }
        else
        {
            logMessageToUi("The device does not have a DFU mode Interface.");
        }
    }

    /**
     * postStatusToUi
     * Post status to UI
     * @param       status - Status code
     * @exception   None
     * @return      None
     */
    private void postStatusToUi(int state)
    {
        if(mUiHandler != null)
        {
            Message msg = mUiHandler.obtainMessage(2, state);
            msg.sendToTarget();
        }
    }

    /**
     * logProgressToUi
     * Post flash progress to UI
     * @param       progress - Progress
     * @exception   None
     * @return      None
     */
    private void logProgressToUi(float progress)
    {
        if (mUiHandler != null)
        {
            Message msg = mUiHandler.obtainMessage(1, progress);
            msg.sendToTarget();
        }
    }

    private void logMessageToUi(String textToLog)
    {
        if (mUiHandler != null)
        {
            Message msg = mUiHandler.obtainMessage(0, textToLog + System.lineSeparator());
            msg.sendToTarget();
        }
        // Debug
        Log.d(TAG, textToLog);
    }

    /*
     * A function that sends a DFU_GETSTATUS request to the device
     */
    private DfuGetStatusResult sendDfuGetStatus()
    {
        // TODO: check mConn not null

        int transferResult;
        byte[] byteBuffer = new byte[UsbDfuConstants.DFU_GETSTATUS_wLength];
        transferResult = mConn.controlTransfer(
                UsbDfuConstants.DFU_GETSTATUS_bmRequestType,
                UsbDfuConstants.DFU_GETSTATUS_bRequest,
                UsbDfuConstants.DFU_GETSTATUS_wValue,
                mIdxInterface,
                byteBuffer,
                UsbDfuConstants.DFU_GETSTATUS_wLength,
                UsbDfuConstants.USB_TRANSFER_TIMEOUT
        );

        // Debug
        switch (transferResult)
        {
            case -1:
                logMessageToUi("Error: Transfer result " + transferResult);
                return null;
            case UsbDfuConstants.DFU_GETSTATUS_wLength:
                return new DfuGetStatusResult(byteBuffer);
            default:
                logMessageToUi("Error: Expected " + UsbDfuConstants.DFU_GETSTATUS_wLength + " bytes but return" + transferResult+" bytes");
            return null;
        }//End switch

    }

    private int sendDfuGetState()
    {
        byte[] byteBuffer = new byte[UsbDfuConstants.DFU_GETSTATE_wLength];
        int trasnferResult = mConn.controlTransfer(
                UsbDfuConstants.DFU_GETSTATE_bmRequestType,
                UsbDfuConstants.DFU_GETSTATE_bRequest,
                UsbDfuConstants.DFU_GETSTATE_wValue,
                mIdxInterface,
                byteBuffer,
                UsbDfuConstants.DFU_GETSTATE_wLength,
                UsbDfuConstants.USB_TRANSFER_TIMEOUT
        );

        // TODO: handle transferResult == -1

        return byteBuffer[0] & 0xFF;
    }

    private int sendDfuDnloadBlock0()
    {
        byte msb = (byte)((mTotalBlocks & 0xFF00) >> 8);
        byte lsb = (byte)(mTotalBlocks & 0x00FF);
        byte dfuBlock0[] = { 'M', 'E', 'R', 'I', 'D', 'I', 'A', 'N', lsb, msb};
        return mConn.controlTransfer(
                UsbDfuConstants.DFU_DNLOAD_bmRequestType, // bmRequestType == 0010 0001
                UsbDfuConstants.DFU_DNLOAD_bRequest,       // bRequest == DFU_DNLOAD == 1
                0, // wValue == wBlockNum == 0 (block 0)
                mIdxInterface, // wIndex == interface index
                dfuBlock0, // buffer
                dfuBlock0.length,
                UsbDfuConstants.USB_TRANSFER_TIMEOUT
        );
    }

    private int sendDfuDnloadDataBlock(int blockNum, byte[] dataToSend) {
        return mConn.controlTransfer(
                UsbDfuConstants.DFU_DNLOAD_bmRequestType, // bmRequestType == 0010 0001
                UsbDfuConstants.DFU_DNLOAD_bRequest,      // bRequest == DFU_DNLOAD == 1
                blockNum, // wValue == blockNum
                mIdxInterface, // wIndex == interface index
                dataToSend, // buffer
                dataToSend.length,
                UsbDfuConstants.USB_TRANSFER_TIMEOUT
        );
    }

    private int sendDfuDnloadZeroLengthBlock(int blockNum) {
        return mConn.controlTransfer(
                UsbDfuConstants.DFU_DNLOAD_bmRequestType, // bmRequestType == 0010 0001
                UsbDfuConstants.DFU_DNLOAD_bRequest,      // bRequest == DFU_DNLOAD == 1
                blockNum, // wValue == blockNum
                mIdxInterface, // wIndex == interface index
                null,
                0,
                UsbDfuConstants.USB_TRANSFER_TIMEOUT
        );
    }

    @Override
    public void run()
    {

        // Check for a valid UsbDevice
        if (mDevice == null)
        {
            logMessageToUi( "run() called with mDevice == null!");
            return;
        }//End if

        if (mBinaryData == null)
        {
            logMessageToUi("run() called with mBinaryData == null!");
            return;
        }//End if

        // Open a UsbDeviceConnection
        mConn = mManager.openDevice(mDevice);
        if (mConn == null)
        {
            logMessageToUi("Could not open device connection!");
            return;
        }//End if

        // Claim interface (may be optional)
        if (!mConn.claimInterface(mInterface, false))
        {
            logMessageToUi("Failed to claim DFU mode interface!");
        }//End if

        // =========================================================================================
        // Begin DFU. Do some setup here.
        DfuGetStatusResult resultGetStatus;
        int resultTransfer;

        // Optional: Send DFU_GETSTATE here to confirm the device is at dfuIDLE state.

        // Optional: Send DFU_DETACH here to force device to dfuIDLE state

        // =========================================================================================
        // Send DFU_DNLOAD with Meridian DFU block 0.
        resultTransfer = sendDfuDnloadBlock0();
        // TODO: error checking. resultTransfer should equal length of Meridian DFU block 0 (== 10)
        // Device state should now be at dfuDNLOAD-SYNC, expecting a DFU_GETSTATUS request.
        logMessageToUi("Sent DFU download block 0 with status "+ resultTransfer+" ." );
        // =========================================================================================
        // Send DFU_GETSTATUS. Expecting bState == DFU_STATE_dfuDNBUSY (busy erasing)
        resultGetStatus = sendDfuGetStatus();
        if(resultGetStatus == null)
        {
            logMessageToUi("DFU_GETSTATUS return a null value");
            return;
        }//End if

        // TODO: more robust error handling
        if ( resultGetStatus.bStatus != UsbDfuConstants.DFU_STATUS_OK || resultGetStatus.bState != UsbDfuConstants.DFU_STATE_dfuDNBUSY)
        {
            logMessageToUi("DFU_GETSTATUS (block 0) did not return expected values. bStatus = " + resultGetStatus.bStatus + " & bState = " + resultGetStatus.bState);
            return;
        }//End if

        // ========================================================================================
        // wait for pollingTimeout as specified by DFU_GETSTATUS result.
        logMessageToUi("Erasing chip. Sleeping for" + resultGetStatus.bwPollTimeout + " ms");
        SystemClock.sleep(resultGetStatus.bwPollTimeout);
        // Device state should now be at dfuDNLOAD-SYNC, expecting a DFU_GETSTATUS request.

        // =========================================================================================
        // Send GETSTATUS again to enter dfuDNLOAD-IDLE
        resultGetStatus = sendDfuGetStatus(); // TODO: check return for error
        if(resultGetStatus == null)
        {
            logMessageToUi("DFU_GETSTATUS return a null value");
            return;
        }//End if

        if (resultGetStatus.bState != UsbDfuConstants.DFU_STATE_dfuDNLOAD_IDLE)
        {
            logMessageToUi("Error: Target is not in idle status. Returned: "+resultGetStatus.bState);
            return;
        }//End if

        // =========================================================================================
        // Repeatedly send DFU block until all blocks sent.
        // Note that the range of blockNum is 1 to mTotalBlocks
        int countBlocksSent = 1;
        int offset = 0;
        while (countBlocksSent <= mTotalBlocks)
        {
            // =====================================================================================
            // Current bState == DFU_STATE_dfuDNLOAD-IDLE. Send data block.
            byte[] dataToSend = Arrays.copyOfRange(mBinaryData, offset,
                    ((offset + mBlockSize) > mBinaryData.length)?
                            mBinaryData.length : offset + mBlockSize);

            int numBytesSent = sendDfuDnloadDataBlock(countBlocksSent, dataToSend);
            logMessageToUi("Sent " + countBlocksSent + " bytes... (Total " + mTotalBlocks + " bytes)");

            logProgressToUi(((float)(countBlocksSent))/mTotalBlocks);
            offset += mBlockSize;
            countBlocksSent += 1;

            // =====================================================================================
            // Current bState == DFU_STATE_dfuDNLOAD-SYNC. Send GETSTATUS to enter dfuDNBUSY
            resultGetStatus = sendDfuGetStatus(); // TODO: check return for error
            if(resultGetStatus == null)
            {
                logMessageToUi("DFU_GETSTATUS return a null value");
                return;
            }
            // =====================================================================================
            // wait for pollingTimeout as specified by DFU_GETSTATUS result.
            SystemClock.sleep(resultGetStatus.bwPollTimeout);
            // Device state should now be at dfuDNLOAD-SYNC, expecting a DFU_GETSTATUS request.

            // =====================================================================================
            // Current bState == DFU_STATE_dfuDNLOAD-SYNC. Send GETSTATUS to enter dfuDNLOAD_IDLE
            resultGetStatus = sendDfuGetStatus(); // TODO: check return for error
            if(resultGetStatus == null)
            {
                logMessageToUi("DFU_GETSTATUS return a null value");
                return;
            }

            if (resultGetStatus.bState != UsbDfuConstants.DFU_STATE_dfuDNLOAD_IDLE)
            {
                logMessageToUi("Not idle after waiting!");
                return;
            }
        }

        // =========================================================================================
        // Send zero length packet to signal the end of DFU_DNLOAD
        // TODO: check resultTransfer for errors
        resultTransfer = sendDfuDnloadZeroLengthBlock(countBlocksSent);
        if(resultTransfer < 0)
        {
            logMessageToUi("Error: Cannot send a block with length 0. ("+resultTransfer+")");
        }
        else
        {
            logMessageToUi("A packet with length 0 has been sent successfully. ("+resultTransfer+")");
        }

        // =========================================================================================
        // Current bState == DFU_STATE_dfuMANIFEST_SYNC. Send GETSTATUS to enter DFU_STATE_dfuMANIFEST
        // TODO: check resultGetStatus for errors
        resultGetStatus = sendDfuGetStatus();

        if(resultGetStatus == null)
        {
            logMessageToUi("DFU_GETSTATUS return a null value");
        }
        else
        {
            if (resultGetStatus.bState != UsbDfuConstants.DFU_STATE_dfuMANIFEST)
            {
                logMessageToUi("Error: Target status is not in DFU_STATE_dfuMANIFEST. Current status: " + resultGetStatus.bState);
            }
        }

        // Note: resultGetStatus is not checked here as the device may not be able to answer...

        mConn.releaseInterface(mInterface);
        mConn.close();

        logMessageToUi("DFU Download complete. Please wait while the target is resetting...");
        logProgressToUi(-1);
        // TODO: how to wait for new device?
    }
}
