/* Copyright 2011-2013 Google Inc.
 * Copyright 2013 mike wakerly <opensource@hoho.com>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
 * USA.
 *
 * Project home page: https://github.com/mik3y/usb-serial-for-android
 */

package com.hoho.android.usbserial.driver;

import android.hardware.usb.UsbConstants;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbDeviceConnection;
import android.hardware.usb.UsbEndpoint;
import android.hardware.usb.UsbInterface;
import android.hardware.usb.UsbRequest;
import android.os.Build;
import android.util.Log;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * USB CDC/ACM serial driver implementation.
 *
 * @author mike wakerly (opensource@hoho.com)
 * @see <a
 *      href="http://www.usb.org/developers/devclass_docs/usbcdc11.pdf">Universal
 *      Serial Bus Class Definitions for Communication Devices, v1.1</a>
 */
public class CdcAcmSerialDriver implements UsbSerialDriver {

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

    private final UsbDevice mDevice;
    private final UsbSerialPort mPort;

    public CdcAcmSerialDriver(UsbDevice device) {
        mDevice = device;
        mPort = new CdcAcmSerialPort(device, 0);
    }

    @Override
    public UsbDevice getDevice() {
        return mDevice;
    }

    @Override
    public List<UsbSerialPort> getPorts() {
        return Collections.singletonList(mPort);
    }

    class CdcAcmSerialPort extends CommonUsbSerialPort {

        private final boolean mEnableAsyncReads;
        private UsbInterface mControlInterface;
        private UsbInterface mDataInterface;

        private UsbEndpoint mControlEndpoint;
        private UsbEndpoint mReadEndpoint;
        private UsbEndpoint mWriteEndpoint;

        private boolean mRts = false;
        private boolean mDtr = false;

        private static final int USB_RECIP_INTERFACE = 0x01;
        private static final int USB_RT_ACM = UsbConstants.USB_TYPE_CLASS | USB_RECIP_INTERFACE;

        private static final int SET_LINE_CODING = 0x20;  // USB CDC 1.1 section 6.2
        private static final int GET_LINE_CODING = 0x21;
        private static final int SET_CONTROL_LINE_STATE = 0x22;
        private static final int SEND_BREAK = 0x23;

        public CdcAcmSerialPort(UsbDevice device, int portNumber) {
            super(device, portNumber);
            mEnableAsyncReads = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1);
        }

        @Override
        public UsbSerialDriver getDriver() {
            return CdcAcmSerialDriver.this;
        }

        @Override
        public void open(UsbDeviceConnection connection) throws IOException {
            if (mConnection != null) {
                throw new IOException("Already open");
            }

            mConnection = connection;
            boolean opened = false;
            try {

                if (1 == mDevice.getInterfaceCount()) {
                    Log.d(TAG,"device might be castrated ACM device, trying single interface logic");
                    openSingleInterface();
                } else {
                    Log.d(TAG,"trying default interface logic");
                    openInterface();
                }

                if (mEnableAsyncReads) {
                    Log.d(TAG, "Async reads enabled");
                } else {
                    Log.d(TAG, "Async reads disabled.");
                }


                opened = true;
            } finally {
                if (!opened) {
                    mConnection = null;
                    // just to be on the save side
                    mControlEndpoint = null;
                    mReadEndpoint = null;
                    mWriteEndpoint = null;
                }
            }
        }

        private void openSingleInterface() throws IOException {
            // the following code is inspired by the cdc-acm driver
            // in the linux kernel

            mControlInterface = mDevice.getInterface(0);
            Log.d(TAG, "Control iface=" + mControlInterface);

            mDataInterface = mDevice.getInterface(0);
            Log.d(TAG, "data iface=" + mDataInterface);

            if (!mConnection.claimInterface(mControlInterface, true)) {
                throw new IOException("Could not claim shared control/data interface.");
            }

            int endCount = mControlInterface.getEndpointCount();

            if (endCount < 3) {
                Log.d(TAG,"not enough endpoints - need 3. count=" + mControlInterface.getEndpointCount());
                throw new IOException("Insufficient number of endpoints(" + mControlInterface.getEndpointCount() + ")");
            }

            // Analyse endpoints for their properties
            mControlEndpoint = null;
            mReadEndpoint = null;
            mWriteEndpoint = null;
            for (int i = 0; i < endCount; ++i) {
                UsbEndpoint ep = mControlInterface.getEndpoint(i);
                if ((ep.getDirection() == UsbConstants.USB_DIR_IN) &&
                    (ep.getType() == UsbConstants.USB_ENDPOINT_XFER_INT)) {
                    Log.d(TAG,"Found controlling endpoint");
                    mControlEndpoint = ep;
                } else if ((ep.getDirection() == UsbConstants.USB_DIR_IN) &&
                           (ep.getType() == UsbConstants.USB_ENDPOINT_XFER_BULK)) {
                    Log.d(TAG,"Found reading endpoint");
                    mReadEndpoint = ep;
                } else if ((ep.getDirection() == UsbConstants.USB_DIR_OUT) &&
                        (ep.getType() == UsbConstants.USB_ENDPOINT_XFER_BULK)) {
                    Log.d(TAG,"Found writing endpoint");
                    mWriteEndpoint = ep;
                }


                if ((mControlEndpoint != null) &&
                    (mReadEndpoint != null) &&
                    (mWriteEndpoint != null)) {
                    Log.d(TAG,"Found all required endpoints");
                    break;
                }
            }

            if ((mControlEndpoint == null) ||
                    (mReadEndpoint == null) ||
                    (mWriteEndpoint == null)) {
                Log.d(TAG,"Could not establish all endpoints");
                throw new IOException("Could not establish all endpoints");
            }
        }

        private void openInterface() throws IOException {
            Log.d(TAG, "claiming interfaces, count=" + mDevice.getInterfaceCount());

            mControlInterface = mDevice.getInterface(0);
            Log.d(TAG, "Control iface=" + mControlInterface);
            // class should be USB_CLASS_COMM

            if (!mConnection.claimInterface(mControlInterface, true)) {
                throw new IOException("Could not claim control interface.");
            }

            mControlEndpoint = mControlInterface.getEndpoint(0);
            Log.d(TAG, "Control endpoint direction: " + mControlEndpoint.getDirection());

            Log.d(TAG, "Claiming data interface.");
            mDataInterface = mDevice.getInterface(1);
            Log.d(TAG, "data iface=" + mDataInterface);
            // class should be USB_CLASS_CDC_DATA

            if (!mConnection.claimInterface(mDataInterface, true)) {
                throw new IOException("Could not claim data interface.");
            }
            mReadEndpoint = mDataInterface.getEndpoint(1);
            Log.d(TAG, "Read endpoint direction: " + mReadEndpoint.getDirection());
            mWriteEndpoint = mDataInterface.getEndpoint(0);
            Log.d(TAG, "Write endpoint direction: " + mWriteEndpoint.getDirection());
        }

        private int sendAcmControlMessage(int request, int value, byte[] buf) {
            return mConnection.controlTransfer(
                    USB_RT_ACM, request, value, 0, buf, buf != null ? buf.length : 0, 5000);
        }

        @Override
        public void close() throws IOException {
            if (mConnection == null) {
                throw new IOException("Already closed");
            }
            mConnection.close();
            mConnection = null;
        }

        @Override
        public int read(byte[] dest, int timeoutMillis) throws IOException {
            if (mEnableAsyncReads) {
              final UsbRequest request = new UsbRequest();
              try {
                request.initialize(mConnection, mReadEndpoint);
                final ByteBuffer buf = ByteBuffer.wrap(dest);
                if (!request.queue(buf, dest.length)) {
                  throw new IOException("Error queueing request.");
                }

                final UsbRequest response = mConnection.requestWait();
                if (response == null) {
                  throw new IOException("Null response");
                }

                final int nread = buf.position();
                if (nread > 0) {
                  //Log.d(TAG, HexDump.dumpHexString(dest, 0, Math.min(32, dest.length)));
                  return nread;
                } else {
                  return 0;
                }
              } finally {
                request.close();
              }
            }

            final int numBytesRead;
            synchronized (mReadBufferLock) {
                int readAmt = Math.min(dest.length, mReadBuffer.length);
                numBytesRead = mConnection.bulkTransfer(mReadEndpoint, mReadBuffer, readAmt,
                        timeoutMillis);
                if (numBytesRead < 0) {
                    // This sucks: we get -1 on timeout, not 0 as preferred.
                    // We *should* use UsbRequest, except it has a bug/api oversight
                    // where there is no way to determine the number of bytes read
                    // in response :\ -- http://b.android.com/28023
                    if (timeoutMillis == Integer.MAX_VALUE) {
                        // Hack: Special case "~infinite timeout" as an error.
                        return -1;
                    }
                    return 0;
                }
                System.arraycopy(mReadBuffer, 0, dest, 0, numBytesRead);
            }
            return numBytesRead;
        }

        @Override
        public int write(byte[] src, int timeoutMillis) throws IOException {
            // TODO(mikey): Nearly identical to FtdiSerial write. Refactor.
            int offset = 0;

            while (offset < src.length) {
                final int writeLength;
                final int amtWritten;

                synchronized (mWriteBufferLock) {
                    final byte[] writeBuffer;

                    writeLength = Math.min(src.length - offset, mWriteBuffer.length);
                    if (offset == 0) {
                        writeBuffer = src;
                    } else {
                        // bulkTransfer does not support offsets, make a copy.
                        System.arraycopy(src, offset, mWriteBuffer, 0, writeLength);
                        writeBuffer = mWriteBuffer;
                    }

                    amtWritten = mConnection.bulkTransfer(mWriteEndpoint, writeBuffer, writeLength,
                            timeoutMillis);
                }
                if (amtWritten <= 0) {
                    throw new IOException("Error writing " + writeLength
                            + " bytes at offset " + offset + " length=" + src.length);
                }

                Log.d(TAG, "Wrote amt=" + amtWritten + " attempted=" + writeLength);
                offset += amtWritten;
            }
            return offset;
        }

        @Override
        public void setParameters(int baudRate, int dataBits, int stopBits, int parity) {
            byte stopBitsByte;
            switch (stopBits) {
                case STOPBITS_1: stopBitsByte = 0; break;
                case STOPBITS_1_5: stopBitsByte = 1; break;
                case STOPBITS_2: stopBitsByte = 2; break;
                default: throw new IllegalArgumentException("Bad value for stopBits: " + stopBits);
            }

            byte parityBitesByte;
            switch (parity) {
                case PARITY_NONE: parityBitesByte = 0; break;
                case PARITY_ODD: parityBitesByte = 1; break;
                case PARITY_EVEN: parityBitesByte = 2; break;
                case PARITY_MARK: parityBitesByte = 3; break;
                case PARITY_SPACE: parityBitesByte = 4; break;
                default: throw new IllegalArgumentException("Bad value for parity: " + parity);
            }

            byte[] msg = {
                    (byte) ( baudRate & 0xff),
                    (byte) ((baudRate >> 8 ) & 0xff),
                    (byte) ((baudRate >> 16) & 0xff),
                    (byte) ((baudRate >> 24) & 0xff),
                    stopBitsByte,
                    parityBitesByte,
                    (byte) dataBits};
            sendAcmControlMessage(SET_LINE_CODING, 0, msg);
        }

        @Override
        public boolean getCD() throws IOException {
            return false;  // TODO
        }

        @Override
        public boolean getCTS() throws IOException {
            return false;  // TODO
        }

        @Override
        public boolean getDSR() throws IOException {
            return false;  // TODO
        }

        @Override
        public boolean getDTR() throws IOException {
            return mDtr;
        }

        @Override
        public void setDTR(boolean value) throws IOException {
            mDtr = value;
            setDtrRts();
        }

        @Override
        public boolean getRI() throws IOException {
            return false;  // TODO
        }

        @Override
        public boolean getRTS() throws IOException {
            return mRts;
        }

        @Override
        public void setRTS(boolean value) throws IOException {
            mRts = value;
            setDtrRts();
        }

        private void setDtrRts() {
            int value = (mRts ? 0x2 : 0) | (mDtr ? 0x1 : 0);
            sendAcmControlMessage(SET_CONTROL_LINE_STATE, value, null);
        }

    }

    public static Map<Integer, int[]> getSupportedDevices() {
        final Map<Integer, int[]> supportedDevices = new LinkedHashMap<Integer, int[]>();
        supportedDevices.put(Integer.valueOf(UsbId.VENDOR_ARDUINO),
                new int[] {
                        UsbId.ARDUINO_UNO,
                        UsbId.ARDUINO_UNO_R3,
                        UsbId.ARDUINO_MEGA_2560,
                        UsbId.ARDUINO_MEGA_2560_R3,
                        UsbId.ARDUINO_SERIAL_ADAPTER,
                        UsbId.ARDUINO_SERIAL_ADAPTER_R3,
                        UsbId.ARDUINO_MEGA_ADK,
                        UsbId.ARDUINO_MEGA_ADK_R3,
                        UsbId.ARDUINO_LEONARDO,
                        UsbId.ARDUINO_MICRO,
                });
        supportedDevices.put(Integer.valueOf(UsbId.VENDOR_VAN_OOIJEN_TECH),
                new int[] {
                    UsbId.VAN_OOIJEN_TECH_TEENSYDUINO_SERIAL,
                });
        supportedDevices.put(Integer.valueOf(UsbId.VENDOR_ATMEL),
                new int[] {
                    UsbId.ATMEL_LUFA_CDC_DEMO_APP,
                });
        supportedDevices.put(Integer.valueOf(UsbId.VENDOR_LEAFLABS),
                new int[] {
                    UsbId.LEAFLABS_MAPLE,
                });
        return supportedDevices;
    }

}
