#include "SMXDeviceConnection.h"
#include "Helpers.h"

#include <string>
#include <memory>
using namespace std;
using namespace SMX;

extern "C"
{
#include <hidsdi.h>
}
#include <setupapi.h>

SMX::SMXDeviceConnection::PendingCommandPacket::PendingCommandPacket()
{
}

SMXDeviceConnection::PendingCommand::PendingCommand()
{
    memset(&m_Overlapped, 0, sizeof(m_Overlapped));
}

shared_ptr<SMX::SMXDeviceConnection> SMXDeviceConnection::Create()
{
    return CreateObj<SMXDeviceConnection>();
}

SMX::SMXDeviceConnection::SMXDeviceConnection(shared_ptr<SMXDeviceConnection> &pSelf):
    m_pSelf(GetPointers(pSelf, this))
{
    memset(&overlapped_read, 0, sizeof(overlapped_read));
}

SMX::SMXDeviceConnection::~SMXDeviceConnection()
{
    Close();
}

bool SMX::SMXDeviceConnection::Open(shared_ptr<AutoCloseHandle> DeviceHandle, wstring &sError)
{
    m_hDevice = DeviceHandle;

    if(!HidD_SetNumInputBuffers(DeviceHandle->value(), 512))
        Log(ssprintf("Error: HidD_SetNumInputBuffers: %ls", GetErrorString(GetLastError()).c_str()));

    // Begin the first async read.
    BeginAsyncRead(sError);

    // Request device info.
    RequestDeviceInfo([&](string response) {
        Log(ssprintf("Received device info.  Master version: %i, P%i", m_DeviceInfo.m_iFirmwareVersion, m_DeviceInfo.m_bP2+1));
        m_bGotInfo = true;
    });

    return true;
}

void SMX::SMXDeviceConnection::Close()
{
    Log("Closing device");

    if(m_hDevice)
        CancelIo(m_hDevice->value());

    // If we're being closed while a command was in progress, call its completion
    // callback, so it's guaranteed to always be called.
    if(m_pCurrentCommand && m_pCurrentCommand->m_pComplete)
        m_pCurrentCommand->m_pComplete("");

    // If any commands were queued with completion callbacks, call their completion
    // callbacks.
    for(auto &pendingCommand: m_aPendingCommands)
    {
        if(pendingCommand->m_pComplete)
            pendingCommand->m_pComplete("");
    }

    m_hDevice.reset();
    m_sReadBuffers.clear();
    m_aPendingCommands.clear();
    memset(&overlapped_read, 0, sizeof(overlapped_read));
    m_bActive = false;
    m_bGotInfo = false;
    m_pCurrentCommand = nullptr;
    m_iInputState = 0;
}

void SMX::SMXDeviceConnection::SetActive(bool bActive)
{
    if(m_bActive == bActive)
        return;

    m_bActive = bActive;
}

void SMX::SMXDeviceConnection::Update(wstring &sError)
{
    if(!sError.empty())
        return;

    if(m_hDevice == nullptr)
    {
        sError = L"Device not open";
        return;
    }

    // A read packet can allow us to initiate a write, so check reads before writes.
    CheckReads(sError);
    CheckWrites(sError);
}

bool SMX::SMXDeviceConnection::ReadPacket(string &out)
{
    if(m_sReadBuffers.empty())
        return false;
    out = m_sReadBuffers.front();
    m_sReadBuffers.pop_front();
    return true;
}

void SMX::SMXDeviceConnection::CheckReads(wstring &error)
{
    if(m_pCurrentCommand)
    {
        // See if this command timed out.  This doesn't happen often, so this is
        // mostly just a failsafe.  The controller takes a moment to initialize on
        // startup, so we use a large enough timeout that this doesn't trigger on
        // every connection.
        double fSecondsAgo = SMX::GetMonotonicTime() - m_pCurrentCommand->m_fSentAt;
        if(fSecondsAgo > 2.0f)
        {
            // If we didn't get a response in this long, we're not going to.  Retry the
            // command by cancelling its I/O and moving it back to the command queue.
            //
            // if we were delayed and the response is in the queue, we'll get out of sync
            Log("Command timed out.  Retrying...");
            CancelIoEx(m_hDevice->value(), &m_pCurrentCommand->m_Overlapped);

            // Block until the cancellation completes.  This should happen quickly.
            DWORD unused;
            GetOverlappedResult(m_hDevice->value(), &m_pCurrentCommand->m_Overlapped, &unused, true);

            m_aPendingCommands.push_front(m_pCurrentCommand);
            m_pCurrentCommand = nullptr;
            Log("Command requeued");
        }
    }

    DWORD bytes;
    int result = GetOverlappedResult(m_hDevice->value(), &overlapped_read, &bytes, FALSE);
    if(result == 0)
    {
        int windows_error = GetLastError();
        if(windows_error != ERROR_IO_PENDING && windows_error != ERROR_IO_INCOMPLETE)
            error = wstring(L"Error reading device: ") + GetErrorString(windows_error).c_str();
        return;
    }

    HandleUsbPacket(string(overlapped_read_buffer, bytes));

    // Start the next read.
    BeginAsyncRead(error);
}

void SMX::SMXDeviceConnection::HandleUsbPacket(const string &buf)
{
    if(buf.empty())
        return;
    // Log(ssprintf("Read: %s", BinaryToHex(buf).c_str()));

    int iReportId = buf[0];
    switch(iReportId)
    {
    case 3:
        // Input state.  We could also read this as a normal HID button change.
        m_iInputState = ((buf[2] & 0xFF) << 8) |
                ((buf[1] & 0xFF) << 0);

        // Log(ssprintf("Input state: %x (%x %x)\n", m_iInputState, buf[2], buf[1]));
        break;

    case 6:
        // A HID serial packet.
        if(buf.size() < 3)
            return;

        int cmd = buf[1];

#define PACKET_FLAG_START_OF_COMMAND      0x04
#define PACKET_FLAG_END_OF_COMMAND        0x01
#define PACKET_FLAG_HOST_CMD_FINISHED     0x02
#define PACKET_FLAG_DEVICE_INFO           0x80

        size_t bytes = buf[2];
        if(3 + bytes > buf.size())
        {
            Log("Communication error: oversized packet (ignored)");
            return;
        }

        string sPacket( buf.begin()+3, buf.begin()+3+bytes );

        if(cmd & PACKET_FLAG_DEVICE_INFO)
        {
            // This is a response to RequestDeviceInfo.  Since any application can send this,
            // we ignore the packet if we didn't request it, since it might be requested for
            // a different program.
            if(m_pCurrentCommand == nullptr || !m_pCurrentCommand->m_bIsDeviceInfoCommand)
                break;

            // We're little endian and the device is too, so we can just match the struct.
            // We're depending on correct padding.
            struct data_info_packet
            {
                char cmd; // always 'I'
                uint8_t packet_size; // not used
                char player; // '0' for P1, '1' for P2:
                char unused2;
                uint8_t serial[16];
                uint16_t firmware_version;
                char unused3; // always '\n'
            };

            // The packet contains data_info_packet.  The packet is actually one byte smaller
            // due to a padding byte added (it contains 23 bytes of data but the struct is
            // 24 bytes).  Resize to be sure.
            sPacket.resize(sizeof(data_info_packet));

            // Convert the info packet from the wire protocol to our friendlier API.
            const data_info_packet *packet = (data_info_packet *) sPacket.data();
            m_DeviceInfo.m_bP2 = packet->player == '1';
            m_DeviceInfo.m_iFirmwareVersion = packet->firmware_version;

            // The serial is binary in this packet.  Hex format it, which is the same thing
            // we'll get if we read the USB serial number (eg. HidD_GetSerialNumberString).
            string sHexSerial = BinaryToHex(packet->serial, 16);
            memcpy(m_DeviceInfo.m_Serial, sHexSerial.c_str(), 33);

            if(m_pCurrentCommand->m_pComplete)
                m_pCurrentCommand->m_pComplete(sPacket);
            m_pCurrentCommand = nullptr;

            break;
        }

        // If we're not active, ignore all packets other than device info.  This is always false
        // while we're in Open() waiting for the device info response.
        if(!m_bActive)
            break;

        if(cmd & PACKET_FLAG_START_OF_COMMAND && !m_sCurrentReadBuffer.empty())
        {
            // When we get a start packet, the read buffer should already be empty.  If
            // it isn't, we got a command that didn't end with an END_OF_COMMAND packet,
            // and something is wrong.  This shouldn't happen, so warn about it and recover
            // by clearing the junk in the buffer.
            Log(ssprintf("Got PACKET_FLAG_START_OF_COMMAND, but we had %i bytes in the read buffer",
                m_sCurrentReadBuffer.size()));

            m_sCurrentReadBuffer.clear();
        }

        m_sCurrentReadBuffer.append(sPacket);

        // Note that if PACKET_FLAG_HOST_CMD_FINISHED is set, PACKET_FLAG_END_OF_COMMAND
        // will always also be set.
        if(cmd & PACKET_FLAG_HOST_CMD_FINISHED)
        {
            // This tells us that a command we wrote to the device has finished executing, and
            // it's safe to start writing another.
            if(m_pCurrentCommand && m_pCurrentCommand->m_pComplete)
                m_pCurrentCommand->m_pComplete(m_sCurrentReadBuffer);
            m_pCurrentCommand = nullptr;
        }

        if(cmd & PACKET_FLAG_END_OF_COMMAND)
        {
            if(!m_sCurrentReadBuffer.empty())
                m_sReadBuffers.push_back(m_sCurrentReadBuffer);
            m_sCurrentReadBuffer.clear();
        }

        break;
    }

}

void SMX::SMXDeviceConnection::BeginAsyncRead(wstring &error)
{
    while(1)
    {
        // Our read buffer is 64 bytes.  The HID input packet is much smaller than that,
        // but Windows pads packets to the maximum size of any HID report, and the HID
        // serial packet is 64 bytes, so we'll get 64 bytes even for 3-byte input packets.
        // If this didn't happen, we'd have to be smarter about pulling data out of the
        // read buffer.
        DWORD bytes;
        memset(overlapped_read_buffer, 0, sizeof(overlapped_read_buffer));
        if(!ReadFile(m_hDevice->value(), overlapped_read_buffer, sizeof(overlapped_read_buffer), &bytes, &overlapped_read))
        {
            int windows_error = GetLastError();
            if(windows_error != ERROR_IO_PENDING && windows_error != ERROR_IO_INCOMPLETE)
                error = wstring(L"Error reading device: ") + GetErrorString(windows_error).c_str();
            return;
        }

        // The async read finished synchronously.  This just means that there was already data waiting.
        // Handle the result, and loop to try to start the next async read again.
        HandleUsbPacket(string(overlapped_read_buffer, bytes));
    }
}

void SMX::SMXDeviceConnection::CheckWrites(wstring &error)
{
    if(m_pCurrentCommand)
    {
        // A command is in progress.  See if its writes have completed.
        if(m_pCurrentCommand->m_bWriting)
        {
            DWORD bytes;
            int iResult = GetOverlappedResult(m_hDevice->value(), &m_pCurrentCommand->m_Overlapped, &bytes, FALSE);
            if(iResult == 0)
            {
                int windows_error = GetLastError();
                if(windows_error != ERROR_IO_PENDING && windows_error != ERROR_IO_INCOMPLETE)
                    error = wstring(L"Error writing to device: ") + GetErrorString(windows_error).c_str();
                return;
            }

            m_pCurrentCommand->m_bWriting = false;
        }

        // Don't clear m_pCurrentCommand here.  It'll stay set until we get a PACKET_FLAG_HOST_CMD_FINISHED
        // packet from the device, which tells us it's ready to receive another command.
        //
        // Don't send packets if there's a command in progress.
        return;
    }

    // Stop if we have nothing to do.
    if(m_aPendingCommands.empty())
        return;

    // Send the next command.
    shared_ptr<PendingCommand> pPendingCommand = m_aPendingCommands.front();

    // Record the time.  We can use this for timeouts.
    pPendingCommand->m_fSentAt = SMX::GetMonotonicTime();

    for(shared_ptr<PendingCommandPacket> &pPacket: pPendingCommand->m_Packets)
    {
        // In theory the API allows this to return success if the write completed successfully without needing to
        // be async, like reads can.  However, this can't really happen (the write always needs to go to the device
        // first, unlike reads which might already be buffered), and there's no way to test it if we implement that,
        // so this assumes all writes are async.
        DWORD unused;
        // Log(ssprintf("Write: %s", BinaryToHex(pPacket->sData).c_str()));
        if(!WriteFile(m_hDevice->value(), pPacket->sData.data(), pPacket->sData.size(), &unused, &pPendingCommand->m_Overlapped))
        {
            int windows_error = GetLastError();
            if(windows_error != ERROR_IO_PENDING && windows_error != ERROR_IO_INCOMPLETE)
            {
                error = wstring(L"Error writing to device: ") + GetErrorString(windows_error).c_str();
                return;
            }
        }
    }

    pPendingCommand->m_bWriting = true;

    // Remove this command and store it in m_pCurrentCommand, and we'll stop sending data until the command finishes.
    m_pCurrentCommand = pPendingCommand;
    m_aPendingCommands.pop_front();
}

// Request device info.  This is the same as sending an 'i' command, but we can send it safely
// at any time, even if another application is talking to the device, so we can do this during
// enumeration.
void SMX::SMXDeviceConnection::RequestDeviceInfo(function<void(string response)> pComplete)
{
    shared_ptr<PendingCommand> pPendingCommand = make_shared<PendingCommand>();
    pPendingCommand->m_pComplete = pComplete;
    pPendingCommand->m_bIsDeviceInfoCommand = true;

    shared_ptr<PendingCommandPacket> pCommandPacket = make_shared<PendingCommandPacket>();

    string sPacket({
        5, // report ID
        (char) (uint8_t) PACKET_FLAG_DEVICE_INFO, // flags
        (char) 0, // bytes in packet
    });
    sPacket.resize(64, 0);
    pCommandPacket->sData = sPacket;

    pPendingCommand->m_Packets.push_back(pCommandPacket);

    m_aPendingCommands.push_back(pPendingCommand);
}

void SMX::SMXDeviceConnection::SendCommand(const string &cmd, function<void(string response)> pComplete)
{
    shared_ptr<PendingCommand> pPendingCommand = make_shared<PendingCommand>();
    pPendingCommand->m_pComplete = pComplete;

    // Send the command in packets.  We allow sending zero-length packets here
    // for testing purposes.
    size_t i = 0;
    do {
        shared_ptr<PendingCommandPacket> pCommandPacket = make_shared<PendingCommandPacket>();

        int iFlags = 0;
        size_t iPacketSize = min(cmd.size() - i, static_cast<size_t>(61));

        bool bFirstPacket = (i == 0);
        if(bFirstPacket)
            iFlags |= PACKET_FLAG_START_OF_COMMAND;

        bool bLastPacket = (i + iPacketSize == cmd.size());
        if(bLastPacket)
            iFlags |= PACKET_FLAG_END_OF_COMMAND;

        string sPacket({
            5, // report ID
            (char) iFlags,
            (char) iPacketSize, // bytes in packet
        });

        sPacket.append(cmd.begin() + i, cmd.begin() + i + iPacketSize);
        sPacket.resize(64, 0);
        pCommandPacket->sData = sPacket;

        pPendingCommand->m_Packets.push_back(pCommandPacket);

        i += iPacketSize;
    }
    while(i < cmd.size());

    m_aPendingCommands.push_back(pPendingCommand);
}
