/*
 * Copyright 2015 Umbrela Smart, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package co.umbrela.tools.stm32dfuprogrammer;

import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbDeviceConnection;
import android.hardware.usb.UsbEndpoint;
import android.hardware.usb.UsbInterface;
import android.hardware.usb.UsbManager;
import android.util.Log;

import java.util.HashMap;
import java.util.Iterator;

public class Usb {

    final static String TAG = "Umbrela Client: USB";

    private Context mContext;

    private UsbManager mUsbManager;
    private UsbDevice mDevice;
    private UsbDeviceConnection mConnection;
    private UsbInterface mInterface;
    private int mDeviceVersion;

    /* USB DFU ID's (may differ by device) */
    public final static int USB_VENDOR_ID = 1155;   // VID while in DFU mode 0x0483
    public final static int USB_PRODUCT_ID = 57105; // PID while in DFU mode 0xDF11

    public static final String ACTION_USB_PERMISSION = "com.android.example.USB_PERMISSION";

    /* Callback Interface */
    public interface OnUsbChangeListener {
        void onUsbConnected();
    }

    public void setOnUsbChangeListener(OnUsbChangeListener l) {
        mOnUsbChangeListener = l;
    }

    private OnUsbChangeListener mOnUsbChangeListener;

    public UsbDevice getUsbDevice() {
        return mDevice;
    }

    /* Broadcast Receiver*/
    private final BroadcastReceiver mUsbReceiver = new BroadcastReceiver() {

        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();

            if (ACTION_USB_PERMISSION.equals(action)) {
                synchronized (this) {
                    UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);

                    if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
                        if (device != null) {
                            //call method to set up device communication
                            setDevice(device);


                            if (mOnUsbChangeListener != null) {
                                mOnUsbChangeListener.onUsbConnected();
                            }
                        }
                    } else {
                        Log.d(TAG, "permission denied for device " + device);
                    }
                }
            } else if (UsbManager.ACTION_USB_DEVICE_ATTACHED.equals(action)) {
                synchronized (this) {
                    //request permission for just attached USB Device if it matches the VID/PID
                    requestPermission(mContext, USB_VENDOR_ID, USB_PRODUCT_ID);
                }
            } else if (UsbManager.ACTION_USB_DEVICE_DETACHED.equals(action)) {
                synchronized (this) {
                    UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
                    if (mDevice != null && mDevice.equals(device)) {
                        release();
                    }
                }
            }
        }
    };

    public BroadcastReceiver getmUsbReceiver() {
        return mUsbReceiver;
    }

    public Usb(Context context) {
        mContext = context;
    }

    public void setUsbManager(UsbManager usbManager) {
        this.mUsbManager = usbManager;
    }

    public void requestPermission(Context context, int vendorId, int productId) {
        // Setup Pending Intent
        PendingIntent permissionIntent = PendingIntent.getBroadcast(context, 0, new Intent(Usb.ACTION_USB_PERMISSION), 0);
        UsbDevice device = getUsbDevice(vendorId, productId);

        if (device != null) {
            mUsbManager.requestPermission(device, permissionIntent);
        }
    }

    private UsbDevice getUsbDevice(int vendorId, int productId) {
        HashMap<String, UsbDevice> deviceList = mUsbManager.getDeviceList();
        Iterator<UsbDevice> deviceIterator = deviceList.values().iterator();

        UsbDevice device;
        while (deviceIterator.hasNext()) {
            device = deviceIterator.next();
            if (device.getVendorId() == vendorId && device.getProductId() == productId) {
                return device;
            }
        }
        return null;
    }

    public boolean release() {
        boolean isReleased = false;

        if (mConnection != null) {
            isReleased = mConnection.releaseInterface(mInterface);
            mConnection.close();
            mConnection = null;
        }
        return isReleased;
    }

    public void setDevice(UsbDevice device) {
        mDevice = device;

        // The first interface is the one we want
        mInterface = device.getInterface(0);    // todo check when changing if alternative interface is changing

        if (device != null) {
            UsbDeviceConnection connection = mUsbManager.openDevice(device);
            if (connection != null && connection.claimInterface(mInterface, true)) {
                Log.i(TAG, "open SUCCESS");
                mConnection = connection;

                // get the bcdDevice version
                byte[] rawDescriptor = mConnection.getRawDescriptors();
                mDeviceVersion = rawDescriptor[13] << 8;
                mDeviceVersion |= rawDescriptor[12];

                Log.i("USB", getDeviceInfo(device));
            } else {
                Log.e(TAG, "open FAIL");
                mConnection = null;
            }
        }
    }

    public boolean isConnected() {
        return (mConnection != null);
    }

    public String getDeviceInfo(UsbDevice device) {
        if (device == null)
            return "No device found.";

        StringBuilder sb = new StringBuilder();
        sb.append("Model: " + device.getDeviceName() + "\n");
        sb.append("ID: " + device.getDeviceId() + " (0x" + Integer.toHexString(device.getDeviceId()) + ")" + "\n");
        sb.append("Class: " + device.getDeviceClass() + "\n");
        sb.append("Subclass: " + device.getDeviceSubclass() + "\n");
        sb.append("Protocol: " + device.getDeviceProtocol() + "\n");
        sb.append("Vendor ID " + device.getVendorId() + " (0x" + Integer.toHexString(device.getVendorId()) + ")" + "\n");
        sb.append("Product ID: " + device.getProductId() + " (0x" + Integer.toHexString(device.getProductId()) + ")" + "\n");
        sb.append("Device Ver: 0x" + Integer.toHexString(mDeviceVersion) + "\n");
        sb.append("Interface count: " + device.getInterfaceCount() + "\n");

        for (int i = 0; i < device.getInterfaceCount(); i++) {

            UsbInterface usbInterface = device.getInterface(i);

            sb.append("Interface: " + usbInterface.toString() + "\n");
            sb.append("Endpoint Count: " + usbInterface.getEndpointCount() + "\n");

            for (int j = 0; j < usbInterface.getEndpointCount(); j++) {

                UsbEndpoint ep = usbInterface.getEndpoint(j);

                sb.append("Endpoint: " + ep.toString() + "\n");
            }
        }

        return sb.toString();
    }

    public int getDeviceVersion() {
        return mDeviceVersion;
    }


    /**
     * Performs a control transaction on endpoint zero for this device.
     * The direction of the transfer is determined by the request type.
     * If requestType & {@link android.hardware.usb.UsbConstants#USB_ENDPOINT_DIR_MASK} is
     * {@link android.hardware.usb.UsbConstants#USB_DIR_OUT}, then the transfer is a write,
     * and if it is {@link android.hardware.usb.UsbConstants#USB_DIR_IN}, then the transfer
     * is a read.
     *
     * @param requestType MSB selects direction, rest defines to whom request is addressed
     * @param request     DFU command ID
     * @param value       0 for commands, >0 for firmware blocks
     * @param index       often 0
     * @param buffer      buffer for data portion of transaction,
     *                    or null if no data needs to be sent or received
     * @param length      the length of the data to send or receive
     * @param timeout     50ms f
     * @return length of data transferred (or zero) for success,
     * or negative value for failure
     */
    public int controlTransfer(int requestType, int request, int value, int index, byte[] buffer, int length, int timeout) {
        synchronized (this) {
            return mConnection.controlTransfer(requestType, request, value, index, buffer, length, timeout);
        }
    }
}