#include "SMXManager.h"
#include "SMXDevice.h"
#include "SMXDeviceConnection.h"
#include "SMXDeviceSearchThreaded.h"
#include "Helpers.h"

#include <windows.h>
#include <memory>
#include <stdexcept>
using namespace std;
using namespace SMX;

namespace {
    Mutex g_Lock;
}

shared_ptr<SMXManager> SMXManager::g_pSMX;

SMX::SMXManager::SMXManager(function<void(int PadNumber, SMXUpdateCallbackReason reason)> pCallback):
    m_UserCallbackThread("SMXUserCallbackThread")
{
    // Raise the priority of the user callback thread, since we don't want input
    // events to be preempted by other things and reduce timing accuracy.
    m_UserCallbackThread.SetHighPriority(true);
    m_hEvent = make_shared<AutoCloseHandle>(CreateEvent(NULL, false, false, NULL));
    m_pSMXDeviceSearchThreaded = make_shared<SMXDeviceSearchThreaded>();

    // Create the SMXDevices.  We don't create these as we connect, we just reuse the same
    // ones.
    for(int i = 0; i < 2; ++i)
    {
        shared_ptr<SMXDevice> pDevice = SMXDevice::Create(m_hEvent, g_Lock);
        m_pDevices.push_back(pDevice);
    }

    // The callback we send to SMXDeviceConnection will be called from our thread.  Wrap
    // it so it's called from UserCallbackThread instead.
    auto pCallbackInThread = [this, pCallback](int PadNumber, SMXUpdateCallbackReason reason) {
        m_UserCallbackThread.RunInThread([pCallback, PadNumber, reason]() {
            pCallback(PadNumber, reason);
        });
    };

    // Set the update callbacks.  Do this before starting the thread, to avoid race conditions.
    for(int pad = 0; pad < 2; ++pad)
        m_pDevices[pad]->SetUpdateCallback(pCallbackInThread);

    // Start the thread.
    DWORD id;
    m_hThread = CreateThread(NULL, 0, ThreadMainStart, this, 0, &id);
    SMX::SetThreadName(id, "SMXManager");

    // Raise the priority of the I/O thread, since we don't want input
    // events to be preempted by other things and reduce timing accuracy.
    SetThreadPriority( m_hThread, THREAD_PRIORITY_HIGHEST );
}

SMX::SMXManager::~SMXManager()
{
    // Shut down the thread, if it's still running.
    Shutdown();
}

shared_ptr<SMXDevice> SMX::SMXManager::GetDevice(int pad)
{
    return m_pDevices[pad];
}

void SMX::SMXManager::Shutdown()
{
    g_Lock.AssertNotLockedByCurrentThread();

    // Make sure we're not being called from within m_UserCallbackThread, since that'll
    // deadlock when we shut down m_UserCallbackThread.
    if(m_UserCallbackThread.IsCurrentThread())
        throw runtime_error("SMX::SMXManager::Shutdown must not be called from an SMX callback");

    // Shut down the thread we make user callbacks from.
    m_UserCallbackThread.Shutdown();

    // Shut down the device search thread.
    m_pSMXDeviceSearchThreaded->Shutdown();

    if(m_hThread == INVALID_HANDLE_VALUE)
        return;

    // Tell the thread to shut down, and wait for it before returning.
    m_bShutdown = true;
    SetEvent(m_hEvent->value());

    WaitForSingleObject(m_hThread, INFINITE);
    m_hThread = INVALID_HANDLE_VALUE;
}

DWORD WINAPI SMX::SMXManager::ThreadMainStart(void *self_)
{
    SMXManager *self = (SMXManager *) self_;
    self->ThreadMain();
    return 0;
}

// When we connect to a device, we don't know whether it's P1 or P2, since we get that
// info from the device after we connect to it.  If we have a P2 device in SMX_PadNumber_1
// or a P1 device in SMX_PadNumber_2, swap the two.  
void SMX::SMXManager::CorrectDeviceOrder()
{
    // We're still holding the lock from when we updated the devices, so the application
    // won't see the devices out of order before we do this.
    g_Lock.AssertLockedByCurrentThread();

    SMXInfo info[2];
    m_pDevices[0]->GetInfoLocked(info[0]);
    m_pDevices[1]->GetInfoLocked(info[1]);

    // If we have two P1s or two P2s, the pads are misconfigured and we'll just leave the order alone.
    bool Player2[2] = {
        m_pDevices[0]->IsPlayer2Locked(),
        m_pDevices[1]->IsPlayer2Locked(),
    };
    if(info[0].m_bConnected && info[1].m_bConnected && Player2[0] == Player2[1])
        return;

    bool bP1NeedsSwap = info[0].m_bConnected && Player2[0];
    bool bP2NeedsSwap = info[1].m_bConnected && !Player2[1];
    if(bP1NeedsSwap || bP2NeedsSwap)
        swap(m_pDevices[0], m_pDevices[1]);
}

void SMX::SMXManager::ThreadMain()
{
    g_Lock.Lock();

    while(!m_bShutdown)
    {
        // If there are any lights commands to be sent, send them now.  Do this before callig Update(),
        // since this actually just queues commands, which are actually handled in Update.
        SendLightUpdates();

        // Send panel test mode commands if needed.
        UpdatePanelTestMode();

        // See if there are any new devices.
        AttemptConnections();

        // Update all connected devices.
        for(shared_ptr<SMXDevice> pDevice: m_pDevices)
        {
            wstring sError;
            pDevice->Update(sError);

            if(!sError.empty())
            {
                Log(ssprintf("Device error: %ls", sError.c_str()));

                // Tell m_pDeviceList that the device was closed, so it'll discard the device
                // and notice if a new device shows up on the same path.
                m_pSMXDeviceSearchThreaded->DeviceWasClosed(pDevice->GetDeviceHandle());
                pDevice->CloseDevice();
            }
        }

        // Devices may have finished initializing, so see if we need to update the ordering.
        CorrectDeviceOrder();

        // Make a list of handles for WaitForMultipleObjectsEx.
        vector<HANDLE> aHandles = { m_hEvent->value() };
        for(shared_ptr<SMXDevice> pDevice: m_pDevices)
        {
            shared_ptr<AutoCloseHandle> pHandle = pDevice->GetDeviceHandle();
            if(pHandle)
                aHandles.push_back(pHandle->value());
        }

        // See how long we should block waiting for I/O.  If we have any scheduled lights commands,
        // wait until the next command should be sent, otherwise wait for a second.
        int iDelayMS = 1000;
        if(!m_aPendingLightsCommands.empty())
        {
            double fSendIn = m_aPendingLightsCommands[0].fTimeToSend - GetMonotonicTime();

            // Add 1ms to the delay time.  We're using a high resolution timer, but
            // WaitForMultipleObjectsEx only has 1ms resolution, so this keeps us from
            // repeatedly waking up slightly too early.
            iDelayMS = int(fSendIn * 1000) + 1;
            iDelayMS = max(0, iDelayMS);
        }

        // Wait until there's something to do for a connected device, or delay briefly if we're
        // not connected to anything.  Unlock while we block.  Devices are only ever opened or
        // closed from within this thread, so the handles won't go away while we're waiting on
        // them.
        g_Lock.Unlock();
        WaitForMultipleObjectsEx(aHandles.size(), aHandles.data(), false, iDelayMS, true);
        g_Lock.Lock();
    }
    g_Lock.Unlock();
}

// Lights are updated with two commands.  The top two rows of LEDs in each panel are
// updated by the first command, and the bottom two rows are updated by the second
// command.  We need to send the two commands in order.  The panel won't update lights
// until both commands have been received, so we don't flicker the partial top update
// before the bottom update is received.
//
// A complete update can be performed at up to 30 FPS, but we actually update at 60
// FPS, alternating between updating the top and bottom half.
//
// This interlacing is performed to reduce the amount of work the panels and master
// controller need to do on each update.  This improves timing accuracy, since less
// time is taken by each update.
//
// The order of lights is:
//
// 0123 0123 0123
// 4567 4567 4567
// 89AB 89AB 89AB
// CDEF CDEF CDEF
//
// 0123 0123 0123
// 4567 4567 4567
// 89AB 89AB 89AB
// CDEF CDEF CDEF
//
// 0123 0123 0123
// 4567 4567 4567
// 89AB 89AB 89AB
// CDEF CDEF CDEF
//
// with panels left-to-right, top-to-bottom.  The first packet sends all 0123 and 4567
// lights, and the second packet sends 78AB and CDEF.
//
// We hide these details from the API to simplify things for the user:
//
// - The user sends us a complete lights set.  This should be sent at (up to) 30Hz.
// If we get lights data too quickly, we'll always complete the one we started before
// sending the next.
// - We don't limit to exactly 30Hz to prevent phase issues where a 60 FPS game is
// coming in and out of phase with our timer.  To avoid this, we limit to 40Hz.
// - When we have new lights data to send, we send the first half right away, wait
// 16ms (60Hz), then send the second half, which is the pacing the device expects.
// - If we get a new lights update in between the two lights commands, we won't split
// the lights.  The two lights commands will always come from the same update, so
// we don't get weird interlacing effects.
// - If SMX_ReenableAutoLights is called between the two commands, we need to guarantee
// that we don't send the second lights commands, since that may re-disable auto lights.
// - If we have two pads, the lights update is for both pads and we'll send both commands
// for both pads at the same time, so both pads update lights simultaneously.
void SMX::SMXManager::SetLights(const string sPanelLights[2])
{
    g_Lock.AssertNotLockedByCurrentThread();
    LockMutex L(g_Lock);

    // Don't send lights when a panel test mode is active.
    if(m_PanelTestMode != PanelTestMode_Off)
        return;

    // If m_bOnlySendLightsOnChange is true, only send lights commands if the lights have
    // actually changed.  This is only used for internal testing, and the controllers normally
    // expect to receive regular lights updates, even if the lights aren't actually changing.
    if(m_bOnlySendLightsOnChange)
    {
        static string sLastPanelLights[2];
        if(sPanelLights[0] == sLastPanelLights[0] && sPanelLights[1] == sLastPanelLights[1])
        {
            Log("no change");
            return;
        }

        sLastPanelLights[0] = sPanelLights[0];
        sLastPanelLights[1] = sPanelLights[1];
    }

    // Separate top and bottom lights commands.
    //
    // sPanelLights[iPad] is
    //
    // 0123 0123 0123
    // 4567 4567 4567
    // 89AB 89AB 89AB
    // CDEF CDEF CDEF
    //
    // 0123 0123 0123
    // 4567 4567 4567
    // 89AB 89AB 89AB
    // CDEF CDEF CDEF
    //
    // 0123 0123 0123
    // 4567 4567 4567
    // 89AB 89AB 89AB
    // CDEF CDEF CDEF
    //
    // If we're on a 25-light device, we have an additional grid of 3x3 LEDs:
    //
    //
    // x x x x
    //  0 1 2
    // x x x x
    //  3 4 5
    // x x x x
    //  6 7 8
    // x x x x
    //
    // Set sLightsCommand[iPad][0] to include 0123 4567, [1] to 89AB CDEF,
    // and [2] to the 3x3 grid.
    string sLightCommands[3][2]; // sLightCommands[command][pad]

    // Read the linearly arranged color data we've been given and split it into top and
    // bottom commands for each pad.
    for(int iPad = 0; iPad < 2; ++iPad)
    {
        // If there's no data for this pad, leave the command empty.
        string sLightsDataForPad = sPanelLights[iPad];
        if(sLightsDataForPad.empty())
            continue;

        // Sanity check the lights data.  For 4x4 lights, it should have 9*4*4*3 bytes of
        // data: RGB for each of 4x4 LEDs on 9 panels.  For 25-light panels there should
        // be 4x4+3x3 (25) lights of data.
        size_t LightSize4x4 = 9*4*4*3;
        size_t LightSize25 = 9*5*5*3;
        if(sLightsDataForPad.size() != LightSize4x4 && sLightsDataForPad.size() != LightSize25)
        {
            Log(ssprintf("SetLights: Lights data should be %i or %i bytes, received %i",
                LightSize4x4, LightSize25, sLightsDataForPad.size()));
            continue;
        }

        // If we've been given 16 lights, pad to 25.
        if(sLightsDataForPad.size() == LightSize4x4)
            sLightsDataForPad.append(LightSize25 - LightSize4x4, '\0');

        // Lights are sent in three commands:
        // 
        // 4: the 3x3 inner grid
        // 2: the top 4x2 lights
        // 3: the bottom 4x2 lights
        //
        // Command 4 is only used by firmware version 4+.
        //
        // Always send all three commands if the firmware expects it, even if we've
        // been given 4x4 data.
        sLightCommands[0][iPad] = "4";
        sLightCommands[1][iPad] = "2";
        sLightCommands[2][iPad] = "3";
        int iNextInputByte = 0;
        auto scaleLight = [](uint8_t iColor) {
            // Apply color scaling.  Values over about 170 don't make the LEDs any brighter, so this
            // gives better contrast and draws less power.
            return uint8_t(iColor * 0.6666f);
        };
        for(int iPanel = 0; iPanel < 9; ++iPanel)
        {
            // Create the 2 and 3 commands.
            for(int iByte = 0; iByte < 4*4*3; ++iByte)
            {
                uint8_t iColor = sLightsDataForPad[iNextInputByte++];
                iColor = scaleLight(iColor);
                
                int iCommandIndex = iByte < 4*2*3? 1:2;
                sLightCommands[iCommandIndex][iPad].append(1, iColor);
            }

            // Create the 4 command.
            for(int iByte = 0; iByte < 3*3*3; ++iByte)
            {
                uint8_t iColor = sLightsDataForPad[iNextInputByte++];
                iColor = scaleLight(iColor);
                sLightCommands[0][iPad].append(1, iColor);
            }
        }

        sLightCommands[0][iPad].push_back('\n');
        sLightCommands[1][iPad].push_back('\n');
        sLightCommands[2][iPad].push_back('\n');
    }

    // Each update adds one entry to m_aPendingLightsCommands for each lights command.
    //
    // If there are at least as many entries in m_aPendingLightsCommands as there are
    // commands to send, then lights updates are happening faster than they can be sent
    // to the pad.  If that happens, replace the existing commands rather than adding
    // new ones.
    //
    // Make sure we always finish a lights update once we start it, so if we receive lights
    // updates very quickly we won't just keep sending the first half and never finish one.
    // Otherwise, we'll update with the newest data we have available.
    //
    // Note that m_aPendingLightsCommands contains the update for both pads, to guarantee
    // we always send light updates for both pads together and they never end up out of
    // phase.
    if(m_aPendingLightsCommands.size() < 3)
    {
        // There's a subtle but important difference between command timing in
        // firmware version 4 compared to earlier versions:
        //
        // Earlier firmwares would process host commands as soon as they're received.
        // Because of this, we have to wait before sending the '3' command to give
        // the master controller time to finish sending the '2' command to panels.
        // If we don't do this everything will still work, but the master will block
        // while processing the second command waiting for panel data to finish sending
        // since the TX queue will be full.  If this happens it isn't processing HID
        // data, which reduces input timing accuracy.
        //
        // Firmware version 4 won't process a host command if there's data still being
        // sent to the panels.  It'll wait until the data is flushed.  This means that
        // we can queue all three lights commands at once, and just send them as fast
        // as the host acknowledges them.  The second command will sit around on the
        // master controller's buffer until it finishes sending the first command to
        // the panels, then the third command will do the same.
        //
        // This change is only needed due to the larger amount of data sent in 25-light
        // mode.  Since we're spending more time sending data from the master to the
        // panels, the timing requirements are tighter.  Doing it in the same manual-delay
        // fashion causes too much latency and makes it harder to maintain 30 FPS.
        //
        // If two controllers are connected, they should either both be 4+ or not.  We
        // don't handle the case where they're different and both timings are needed.
        double fNow = GetMonotonicTime();
        double fSendCommandAt = max(fNow, m_fDelayLightCommandsUntil);
        double fCommandTimes[3] = { fNow, fNow, fNow };

        bool masterIsV4 = false;
        bool anyMasterConnected = false;
        for(int iPad = 0; iPad < 2; ++iPad)
        {
            SMXConfig config;
            if(!m_pDevices[iPad]->GetConfigLocked(config))
                continue;

            anyMasterConnected = true;
            if(config.masterVersion >= 4)
                masterIsV4 = true;
        }

        // If we don't have the config yet, the master is in the process of connecting, so don't
        // queue lights.
        if(!anyMasterConnected)
            return;

        // If we're on master firmware < 4, set delay times.  For 4+, just queue commands.
        // We don't need to set fCommandTimes[0] since the '4' packet won't be sent.
        if(!masterIsV4)
        {
            const double fDelayBetweenLightsCommands = 1/60.0;
            fCommandTimes[1] = fSendCommandAt;
            fCommandTimes[2] = fCommandTimes[1] + fDelayBetweenLightsCommands;
        }

        // Update m_fDelayLightCommandsUntil, so we know when the next
        // lights command can be sent.
        m_fDelayLightCommandsUntil = fSendCommandAt + 1/30.0f;

        // Add three commands to the list, scheduled at fFirstCommandTime and fSecondCommandTime.
        m_aPendingLightsCommands.push_back(PendingCommand(fCommandTimes[0]));
        m_aPendingLightsCommands.push_back(PendingCommand(fCommandTimes[1]));
        m_aPendingLightsCommands.push_back(PendingCommand(fCommandTimes[2]));
    }

    // Set the pad commands.
    for(int iPad = 0; iPad < 2; ++iPad)
    {
        // If the command for this pad is empty, leave any existing pad command alone.
        if(sLightCommands[0][iPad].empty())
            continue;

        SMXConfig config;
        if(!m_pDevices[iPad]->GetConfigLocked(config))
            continue;

        // If this pad is firmware version 4, send the 4 command.  Otherwise, leave the 4 command
        // empty and no command will be sent.
        PendingCommand *pPending4Commands = &m_aPendingLightsCommands[m_aPendingLightsCommands.size()-3]; // 3
        if(config.masterVersion >= 4)
            pPending4Commands->sPadCommand[iPad] = sLightCommands[0][iPad];
        else
            pPending4Commands->sPadCommand[iPad] = "";

        PendingCommand *pPending2Commands = &m_aPendingLightsCommands[m_aPendingLightsCommands.size()-2]; // 2
        pPending2Commands->sPadCommand[iPad] = sLightCommands[1][iPad];

        PendingCommand *pPending3Commands = &m_aPendingLightsCommands[m_aPendingLightsCommands.size()-1]; // 3
        pPending3Commands->sPadCommand[iPad] = sLightCommands[2][iPad];
    }

    // Wake up the I/O thread if it's blocking on WaitForMultipleObjectsEx.
    SetEvent(m_hEvent->value());
}

void SMX::SMXManager::SetPlatformLights(const string sPanelLights[2])
{
    g_Lock.AssertNotLockedByCurrentThread();
    LockMutex L(g_Lock);

    // Read the linearly arranged color data we've been given and split it into top and
    // bottom commands for each pad.
    for(int iPad = 0; iPad < 2; ++iPad)
    {
        // If there's no data for this pad, skip it.
        string sLightsDataForPad = sPanelLights[iPad];
        if(sLightsDataForPad.empty())
            continue;

        if(sLightsDataForPad.size() != 44*3)
        {
            Log(ssprintf("SetPlatformLights: Platform lights data should be %i bytes, received %i",
                44*3, sLightsDataForPad.size()));
            continue;
        }

        // If this master doesn't support this, skip it.
        SMXConfig config;
        if(!m_pDevices[iPad]->GetConfigLocked(config))
            continue;
        if(config.masterVersion < 4)
            continue;

        string sLightCommand;
        sLightCommand.push_back('L');
        sLightCommand.push_back(0); // LED strip index (always 0)
        sLightCommand.push_back(44); // number of LEDs to set
        sLightCommand += sLightsDataForPad;

        m_pDevices[iPad]->SendCommandLocked(sLightCommand);
    }

    // Wake up the I/O thread if it's blocking on WaitForMultipleObjectsEx.
    SetEvent(m_hEvent->value());
}

void SMX::SMXManager::ReenableAutoLights()
{
    g_Lock.AssertNotLockedByCurrentThread();
    LockMutex L(g_Lock);

    // Clear any pending lights commands, so we don't re-disable auto-lighting by sending a
    // lights command after we enable it.  If we've sent the first half of a lights update
    // and this causes us to not send the second half, the controller will just discard it.
    m_aPendingLightsCommands.clear();
    for(int iPad = 0; iPad < 2; ++iPad)
        m_pDevices[iPad]->SendCommandLocked(string("S 1\n", 4));
}

// Check to see if we should send any commands in m_aPendingLightsCommands.
void SMX::SMXManager::SendLightUpdates()
{
    g_Lock.AssertLockedByCurrentThread();

    // If previous lights commands are being sent, wait for them to complete before
    // queueing more.
    if(m_iLightsCommandsInProgress > 0)
        return;

    // If we have more than one command queued, we can queue several of them if we're
    // before fTimeToSend.  For the V4 pads that require more commands, this lets us queue
    // the whole lights update at once.  V3 pads require us to time commands, so we can't
    // spam both lights commands at once, which is handled by fTimeToSend.
    while( !m_aPendingLightsCommands.empty() )
    {
        // Send the lights command for each pad.  If either pad isn't connected, this won't do
        // anything.
        const PendingCommand &command = m_aPendingLightsCommands[0];

        // See if it's time to send this command.
        if(command.fTimeToSend > GetMonotonicTime())
            break;

        for(int iPad = 0; iPad < 2; ++iPad)
        {
            if(!command.sPadCommand[iPad].empty())
            {
                // Count the number of commands we've queued.  We won't send any more until
                // this reaches 0 and all queued commands were sent.
                m_iLightsCommandsInProgress++;

                // The completion callback is guaranteed to always be called, even if the controller
                // disconnects and the command wasn't sent.
                m_pDevices[iPad]->SendCommandLocked(command.sPadCommand[iPad], [this, iPad](string response) {
                    g_Lock.AssertLockedByCurrentThread();
                    m_iLightsCommandsInProgress--;
                });
            }
        }

        // Remove the command we've sent.
        m_aPendingLightsCommands.erase(m_aPendingLightsCommands.begin(), m_aPendingLightsCommands.begin()+1);
    }
}

void SMX::SMXManager::SetPanelTestMode(PanelTestMode mode)
{
    g_Lock.AssertNotLockedByCurrentThread();
    LockMutex Lock(g_Lock);
    m_PanelTestMode = mode;
}

void SMX::SMXManager::UpdatePanelTestMode()
{
    // If the test mode has changed, send the new test mode.
    //
    // When the test mode is enabled, send the test mode again periodically, or it'll time
    // out on the master and be turned off.  Don't repeat the PanelTestMode_Off command.
    g_Lock.AssertLockedByCurrentThread();
    uint32_t now = GetTickCount();
    if(m_PanelTestMode == m_LastSentPanelTestMode && 
        (m_PanelTestMode == PanelTestMode_Off || now - m_SentPanelTestModeAtTicks < 1000))
        return;

    // When we first send the test mode command (not for repeats), turn off lights.
    if(m_LastSentPanelTestMode == PanelTestMode_Off)
    {
        // The 'l' command used to set lights, but it's now only used to turn lights off
        // for cases like this.
        string sData = "l";
        sData.append(108, 0);
        sData += "\n";
        for(int iPad = 0; iPad < 2; ++iPad)
            m_pDevices[iPad]->SendCommandLocked(sData);
    }

    m_SentPanelTestModeAtTicks = now;
    m_LastSentPanelTestMode = m_PanelTestMode;
    for(int iPad = 0; iPad < 2; ++iPad)
        m_pDevices[iPad]->SendCommandLocked(ssprintf("t %c\n", m_PanelTestMode));
}

// Assign a serial number to master controllers if one isn't already assigned.  This
// will have no effect if a serial is already set.
//
// We just assign a random number.  The serial number will be used as the USB serial
// number, and can be queried in SMXInfo.
void SMX::SMXManager::SetSerialNumbers()
{
    g_Lock.AssertNotLockedByCurrentThread();
    LockMutex L(g_Lock);

    m_aPendingLightsCommands.clear();
    for(int iPad = 0; iPad < 2; ++iPad)
    {
        string sData = "s";
        uint8_t serial[16];
        SMX::GenerateRandom(serial, sizeof(serial));
        sData.append((char *) serial, sizeof(serial));
        sData.append(1, '\n');

        m_pDevices[iPad]->SendCommandLocked(sData);
    }
}

void SMX::SMXManager::RunInHelperThread(function<void()> func)
{
    m_UserCallbackThread.RunInThread(func);
}

// See if there are any new devices to connect to.
void SMX::SMXManager::AttemptConnections()
{
    g_Lock.AssertLockedByCurrentThread();

    vector<shared_ptr<AutoCloseHandle>> apDevices = m_pSMXDeviceSearchThreaded->GetDevices();

    // Check each device that we've found.  This will include ones we already have open.
    for(shared_ptr<AutoCloseHandle> pHandle: apDevices)
    {
        // See if this device is already open.  If it is, we don't need to do anything with it.
        bool bAlreadyOpen = false;
        for(shared_ptr<SMXDevice> pDevice: m_pDevices)
        {
            if(pDevice->GetDeviceHandle() == pHandle)
                bAlreadyOpen = true;
        }
        if(bAlreadyOpen)
            continue;

        // Find an open device slot.
        shared_ptr<SMXDevice> pDeviceToOpen;
        for(shared_ptr<SMXDevice> pDevice: m_pDevices)
        {
            // Note that we check whether the device has a handle rather than calling IsConnected, since
            // devices aren't actually considered connected until they've read the configuration.
            if(pDevice->GetDeviceHandle() == NULL)
            {
                pDeviceToOpen = pDevice;
                break;
            }
        }

        if(pDeviceToOpen == nullptr)
        {
            // All device slots are used.  Are there more than two devices plugged in?
            Log("Error: No available slots for device.  Are more than two devices connected?");
            break;
        }

        // Open the device in this slot.
        Log("Opening SMX device");
        wstring sError;
        pDeviceToOpen->OpenDeviceHandle(pHandle, sError);
        if(!sError.empty())
            Log(ssprintf("Error opening device: %ls", sError.c_str()));
    }
}



