/**
 * @file    GoDiscovery.c
 *
 * @internal
 * Copyright (C) 2016-2022 by LMI Technologies Inc.
 * Licensed under the MIT License.
 * Redistributed files must retain the above copyright notice.
 */
#include <GoSdk/Internal/GoDiscovery.h>
#include <kApi/Utils/kUtils.h>

kBeginValueEx(Go, GoDiscoveryInfo)
kEndValueEx()

kBeginValueEx(Go, GoDiscoveryInterface)
    kAddField(GoDiscoveryInterface, kIpAddress, address)
    kAddField(GoDiscoveryInterface, kUdpClient, client)
    kAddField(GoDiscoveryInterface, kSerializer, writer)
    kAddField(GoDiscoveryInterface, kBool, enabled)
kEndValueEx()

/*
 * GoDiscovery class
 */

kBeginClassEx(Go, GoDiscovery)
    kAddVMethod(GoDiscovery, kObject, VRelease)
kEndClassEx()

// For debugging discovery protocol.
#ifdef DEBUG_GO_DISCOVERY
#include <stdio.h>

#define DEBUG_PRINT printf

void PrintAddressPort(kIpAddress ipAddress, k32u port)
{
    kText16 ipAddrStr;

    kIpAddress_Format(ipAddress, ipAddrStr, kCountOf(ipAddrStr));
    DEBUG_PRINT("   @@@ socket %s:%u\n", ipAddrStr, port);
}

void PrintEndPoint(kIpEndPoint* endPoint)
{
    PrintAddressPort(endPoint->address, endPoint->port);
}

void PrintUdpClient(kUdpClient udpClient)
{
    kSocket listenerSocket;
    kIpEndPoint localEndPoint;

    listenerSocket = kUdpClient_Socket(udpClient);
    if (!kSuccess(kSocket_LocalEndPoint(listenerSocket, &localEndPoint)))
    {
        DEBUG_PRINT("   **** Error getting interface local endpoint for client %p\n", udpClient);
    }
    else
    {
        PrintEndPoint(&localEndPoint);
    }

}
#endif

// --------------- GoDiscoveryInfo Utility Functions --------------------
GoFx(void) GoDiscoveryInfo_Init(GoDiscoveryInfo* info)
{
    // Ignore error. Should never happen.
    (void) kMemSet(info, 0, sizeof(GoDiscoveryInfo));
}

GoFx(k32u) GoDiscoveryInfo_Id(const GoDiscoveryInfo* info)
{
    return info->id;
}

GoFx(GoAddressInfo) GoDiscoveryInfo_Address(const GoDiscoveryInfo* info)
{
    return info->address;
}

GoFx(GoPortInfo) GoDiscoveryInfo_Ports(const GoDiscoveryInfo* info)
{
    return info->ports;
}

GoFx(kVersion) GoDiscoveryInfo_Version(const GoDiscoveryInfo* info)
{
    return info->version;
}

GoFx(GoDiscoveryOpMode) GoDiscoveryInfo_OpMode(const GoDiscoveryInfo* info)
{
    return info->opMode;
}

GoFx(kIpAddress) GoDiscoveryInfo_AccelSensorIpAddress(const GoDiscoveryInfo* info)
{
    return info->accelSensorIpAddress;
}

GoFx(kIpAddress) GoDiscoveryInfo_DiscoveryServerAddress(const GoDiscoveryInfo* info)
{
    return info->discoveryServerAddress;
}

GoFx(kStatus) GoDiscoveryInfo_PartNumber(const GoDiscoveryInfo* info, kChar* partNumber, kSize capacity)
{
    kCheck(kStrCopy(partNumber, capacity, info->partNumber));

    return kOK;
}

GoFx(kStatus) GoDiscoveryInfo_ModelNumber(const GoDiscoveryInfo* info, kChar* modelNumber, kSize capacity)
{
    kCheck(kStrCopy(modelNumber, capacity, info->modelNumber));

    return kOK;
}

GoFx(kStatus) GoDiscoveryInfo_ModelDisplayName(const GoDiscoveryInfo* info, kChar* modelDisplayName, kSize capacity)
{
    kCheck(kStrCopy(modelDisplayName, capacity, info->modelDisplayName));

    return kOK;
}

GoFx(kStatus) GoDiscoveryInfo_Family(const GoDiscoveryInfo* info, kChar* family, kSize capacity)
{
    kCheck(kStrCopy(family, capacity, info->family));

    return kOK;
}

// ----------- End GoDiscoveryInfo Getter Functions -------------

GoFx(kStatus) GoDiscovery_Construct(GoDiscovery* discovery, kBool enableAutoDiscovery, kAlloc allocator)
{
    kAlloc alloc = kAlloc_Fallback(allocator);
    kStatus status;

    kCheck(kAlloc_GetObject(alloc, kTypeOf(GoDiscovery), discovery));

    if (!kSuccess(status = GoDiscovery_Init(*discovery, kTypeOf(GoDiscovery), enableAutoDiscovery, alloc)))
    {
        kAlloc_FreeRef(alloc, discovery);
    }

    return status;
}

GoFx(kStatus) GoDiscovery_Init(GoDiscovery discovery, kType type, kBool enableAutoDiscovery, kAlloc alloc)
{
    kObjR(GoDiscovery, discovery);
    kArrayList addresses = kNULL;
    kStatus status;
    kIpEndPoint localEndPoint;
    kIpEntry localIpEntry;
    kSize i;

    kCheck(kObject_Init(discovery, type, alloc));
    kAtomic32s_Init(&obj->compatEnabled, 0);
    obj->localPort = 0;
    obj->enumPeriod = 0;
    obj->onEnumerate.function = kNULL;
    obj->onEnumerate.receiver = kNULL;
    kZero(obj->infoList);
    kZero(obj->interfaces);
    kZero(obj->receiver);
    kZero(obj->readerBuffer);
    kZero(obj->reader);
    kZero(obj->eventTimer);
    kZero(obj->stopwatch);
    obj->enumPending = kFALSE;
    kZero(obj->enumLock);
    obj->runCount = 0;
    kZero(obj->sensorInfoMap);

    kTry
    {
        kAtomic32s_Exchange(&obj->compatEnabled, 0);

        kTest(kMap_Construct(&obj->sensorInfoMap, kTypeOf(k32u), kTypeOf(GoDiscoveryInfo), 0, alloc));
        kTest(kArrayList_Construct(&obj->infoList, kTypeOf(GoDiscoveryInfo), 0, alloc));
        kTest(kArrayList_Construct(&obj->interfaces, kTypeOf(GoDiscoveryInterface), 0, alloc));

        kTest(kUdpClient_Construct(&obj->receiver, kIP_VERSION_4, alloc));
        kTest(kUdpClient_EnableReuseAddress(obj->receiver, kTRUE));

        // Keep the QNX buffer size the same, in case the exact choice is important.
        // If the increase in discovery traffic from compat mode is a problem, user
        // can disable it with GoSystem_EnableDiscoveryCompatibility.
#if defined(K_QNX)
        kTest(kUdpClient_SetReadBuffers(obj->receiver, 65 * 1024, 2048));
#else
        kTest(kUdpClient_SetReadBuffers(obj->receiver, GO_DISCOVERY_RECV_BUFFER_SIZE, 2048));
#endif

        kTest(kUdpClient_Bind(obj->receiver, kIpAddress_AnyV4(), kIP_PORT_ANY));

        kTest(kUdpClient_LocalEndPoint(obj->receiver, &localEndPoint));
        obj->localPort = localEndPoint.port;

        kTest(kMemory_Construct(&obj->readerBuffer, alloc));
        kTest(kSerializer_Construct(&obj->reader, obj->readerBuffer, kNULL, alloc));

        kTest(kPeriodic_Construct(&obj->eventTimer, alloc));
        kTest(kTimer_Construct(&obj->stopwatch, alloc));

        kTest(kLock_Construct(&obj->enumLock, alloc));

        kTest(kArrayList_Construct(&addresses, kTypeOf(kIpEntry), 0, alloc));
        kTest(kNetwork_LocalIpInterfaces(addresses));

#if !defined(K_QNX) && !defined(K_POSIX)
        // Add localhost interfaces except for QNX & Linux systems.
        kTest(kStrCopy(localIpEntry.name, kCountOf(localIpEntry.name), "localhost"));
        localIpEntry.address = kIpAddress_LoopbackV4();
        kTest(kArrayList_AddT(addresses, &localIpEntry));
#endif

        for (i = 0; i < kArrayList_Count(addresses); ++i)
        {
            GoDiscoveryInterface iface;
            kIpEntry entry;

            entry = kArrayList_AsT(addresses, i, kIpEntry);
            iface.address = entry.address;
            iface.client = kNULL;
            iface.writer = kNULL;
            iface.enabled = enableAutoDiscovery;

            kTest(kUdpClient_Construct(&iface.client, kIP_VERSION_4, alloc));
            kTest(kUdpClient_EnableBroadcast(iface.client, kTRUE));
            kTest(kUdpClient_EnableReuseAddress(iface.client, kTRUE));

            if (kSuccess(kUdpClient_Bind(iface.client, iface.address, obj->localPort)))
            {
                kTest(kUdpClient_SetWriteBuffers(iface.client, -1, 2048));
                kTest(kSerializer_Construct(&iface.writer, iface.client, kNULL, alloc));
                kTest(kArrayList_AddT(obj->interfaces, &iface));

#if defined(K_QNX)
                kTest(kSocket_BindToDevice(kUdpClient_Socket(iface.client), entry.name));
#endif
            }
            else
            {
                kDestroyRef(&iface.client);
            }
        }
    }
    kCatchEx(&status)
    {
        GoDiscovery_VRelease(discovery);
        kEndCatchEx(status);
    }
    kFinallyEx
    {
        kDisposeRef(&addresses);
        kEndFinallyEx();
    }

    return kOK;
}

GoFx(kStatus) GoDiscovery_VRelease(GoDiscovery discovery)
{
    kObj(GoDiscovery, discovery);
    kSize i;

    if (!kIsNull(obj->eventTimer))
    {
        kCheck(GoDiscovery_StopEnum(discovery));
    }

    kCheck(kDisposeRef(&obj->infoList));
    kCheck(kDisposeRef(&obj->sensorInfoMap));

    if (!kIsNull(obj->interfaces))
    {
        for (i = 0; i < kArrayList_Count(obj->interfaces); ++i)
        {
            GoDiscoveryInterface* iface = kArrayList_AtT(obj->interfaces, i, GoDiscoveryInterface);

            kCheck(kDestroyRef(&iface->writer));
            kCheck(kDestroyRef(&iface->client));
        }

        kCheck(kDisposeRef(&obj->interfaces));
    }

    kCheck(kDestroyRef(&obj->enumLock));

    kCheck(kDestroyRef(&obj->reader));
    kCheck(kDestroyRef(&obj->readerBuffer));
    kCheck(kDestroyRef(&obj->receiver));

    kCheck(kDestroyRef(&obj->eventTimer));
    kCheck(kDestroyRef(&obj->stopwatch));

    return kObject_VRelease(discovery);
}

GoFx(kStatus) GoDiscovery_WriteIpAddress(kSerializer serializer, kIpAddress address)
{
    kByte bytes[8];

    bytes[0] = 0;
    bytes[1] = 0;
    bytes[2] = 0;
    bytes[3] = 0;
    bytes[4] = (address.address[0] & 0xFF);
    bytes[5] = (address.address[1] & 0xFF);
    bytes[6] = (address.address[2] & 0xFF);
    bytes[7] = (address.address[3] & 0xFF);

    kCheck(kSerializer_WriteByteArray(serializer, bytes, kCountOf(bytes)));

    return kOK;
}

GoFx(kStatus) GoDiscovery_ReadIpAddress(kSerializer serializer, kIpAddress* address)
{
    kByte bytes[8];
    kSize i;

    kCheck(kSerializer_ReadByteArray(serializer, bytes, kCountOf(bytes)));

    address->address[0] = bytes[4];
    address->address[1] = bytes[5];
    address->address[2] = bytes[6];
    address->address[3] = bytes[7];

    for (i = 4; i < 16; i++)
    {
        address->address[i] = 0;
    }

    address->version = kIP_VERSION_4;

    return kOK;
}

GoFx(kStatus) GoDiscovery_Enumerate(GoDiscovery discovery, kArrayList infoList)
{
    kObj(GoDiscovery, discovery);

    kCheck(kLock_Enter(obj->enumLock));

    kTry
    {
        kTest(GoDiscovery_BeginEnum(discovery));
        kTest(kThread_Sleep(GO_DISCOVERY_GET_ADDRESS_TIMEOUT));
        kTest(GoDiscovery_EndEnum(discovery, infoList));
    }
    kFinally
    {
        kCheck(kLock_Exit(obj->enumLock));
        kEndFinally();
    }

    return kOK;
}

GoFx(kStatus) GoDiscovery_GetAddress(GoDiscovery discovery, k32u deviceId, GoAddressInfo* address)
{
    kObj(GoDiscovery, discovery);
    GoDiscoveryInfo info;
    kIpEndPoint remoteEndPoint;
    kSize i;
    kStatus sendStatus = kERROR_NETWORK;
    kStatus recvStatus = kERROR_NOT_FOUND;

    kCheck(kLock_Enter(obj->enumLock));
    kTry
    {
        for (i = 0; i < kArrayList_Count(obj->interfaces); ++i)
        {
            GoDiscoveryInterface* iface = kArrayList_AtT(obj->interfaces, i, GoDiscoveryInterface);

            if (iface->enabled)
            {
                kTest(kSerializer_Write64s(iface->writer, GO_DISCOVERY_GET_ADDRESS_SIZE));
                kTest(kSerializer_Write64s(iface->writer, GO_DISCOVERY_GET_ADDRESS));
                kTest(kSerializer_Write64s(iface->writer, GO_DISOVERY_SIGNATURE));
                kTest(kSerializer_Write64s(iface->writer, deviceId));

                kTest(kSerializer_Flush(iface->writer));

                if (kSuccess(kUdpClient_Send(iface->client, kIpAddress_BroadcastV4(), GO_DISCOVERY_PORT, GO_DISCOVERY_GET_ADDRESS_TIMEOUT, kTRUE)))
                {
                    sendStatus = kOK;
                }
            }
        }

        kTest(sendStatus);
        kTest(kTimer_Start(obj->stopwatch, GO_DISCOVERY_GET_ADDRESS_TIMEOUT));

        do
        {
            if (kSuccess(GoDiscovery_ReceiveReply(discovery, &remoteEndPoint, kTimer_Remaining(obj->stopwatch)))
                && remoteEndPoint.port == GO_DISCOVERY_PORT
                && kSuccess(GoDiscovery_ParseGetAddressReply(discovery, &info, &remoteEndPoint))
                && info.id == deviceId)
            {
                *address = info.address;
                recvStatus = kOK;

#ifdef DEBUG_GO_DISCOVERY
                DEBUG_PRINT("%s: sensor %u remote end point\n", __FUNCTION__, deviceId);
                PrintEndPoint(&remoteEndPoint);
#endif

                break;
            }
        } while (!kTimer_IsExpired(obj->stopwatch));
    }
        kFinally
    {
        kCheck(kLock_Exit(obj->enumLock));
        kEndFinally();
    }

    return recvStatus;
}

GoFx(kStatus) GoDiscovery_SetAddress(GoDiscovery discovery, k32u deviceId, const GoAddressInfo* address)
{
    kObj(GoDiscovery, discovery);
    k32u responseDeviceId;
    kIpEndPoint remoteEndPoint;
    kIpAddress zero;
    kSize i;
    kStatus sendStatus = kERROR_NETWORK;

    for (i = 0; i < 16; i++)
    {
        zero.address[i] = 0;
    }

    for (i = 0; i < kArrayList_Count(obj->interfaces); ++i)
    {
        GoDiscoveryInterface* iface = kArrayList_AtT(obj->interfaces, i, GoDiscoveryInterface);

        if (iface->enabled)
        {
            kCheck(kSerializer_Write64s(iface->writer, GO_DISCOVERY_SET_ADDRESS_SIZE));
            kCheck(kSerializer_Write64s(iface->writer, GO_DISCOVERY_SET_ADDRESS));
            kCheck(kSerializer_Write64s(iface->writer, GO_DISOVERY_SIGNATURE));
            kCheck(kSerializer_Write64s(iface->writer, deviceId));
            kCheck(kSerializer_Write64s(iface->writer, address->useDhcp));

            kCheck(GoDiscovery_WriteIpAddress(iface->writer, (address->useDhcp) ? kIpAddress_AnyV4() : address->address));
            kCheck(GoDiscovery_WriteIpAddress(iface->writer, (address->useDhcp) ? kIpAddress_AnyV4() : address->mask));
            kCheck(GoDiscovery_WriteIpAddress(iface->writer, (address->useDhcp) ? kIpAddress_AnyV4() : address->gateway));
            kCheck(GoDiscovery_WriteIpAddress(iface->writer, (address->useDhcp) ? kIpAddress_AnyV4() : zero));

            kCheck(kSerializer_Flush(iface->writer));
            if (kSuccess(kUdpClient_Send(iface->client, kIpAddress_BroadcastV4(), GO_DISCOVERY_PORT, GO_DISCOVERY_SET_ADDRESS_TIMEOUT, kTRUE)))
            {
                sendStatus = kOK;
            }
        }
    }

    kCheck(sendStatus);
    kCheck(kTimer_Start(obj->stopwatch, GO_DISCOVERY_SET_ADDRESS_TIMEOUT));

    do
    {
        if (kSuccess(GoDiscovery_ReceiveReply(discovery, &remoteEndPoint, kTimer_Remaining(obj->stopwatch)))
            && remoteEndPoint.port == GO_DISCOVERY_PORT
            && kSuccess(GoDiscovery_ParseSetAddressReply(discovery, &responseDeviceId))
            && responseDeviceId == deviceId)
        {
            return kOK;
        }
    } while (!kTimer_IsExpired(obj->stopwatch));

    return kERROR_NOT_FOUND;
}

GoFx(kStatus) GoDiscovery_SetAddressTest(GoDiscovery discovery, k32u deviceId, const GoAddressInfo* address)
{
    kObj(GoDiscovery, discovery);
    k32u responseDeviceId;
    kIpEndPoint remoteEndPoint;
    kIpAddress zero;
    kSize i;

    for (i = 0; i < 16; i++)
    {
        zero.address[i] = 0;
    }

    for (i = 0; i < kArrayList_Count(obj->interfaces); ++i)
    {
        GoDiscoveryInterface* iface = kArrayList_AtT(obj->interfaces, i, GoDiscoveryInterface);

        if (iface->enabled)
        {
            kCheck(kSocket_Connect(kUdpClient_Socket(iface->client), kIpAddress_BroadcastV4(), GO_DISCOVERY_PORT, 1000000));

            kCheck(kSerializer_Write64s(iface->writer, GO_DISCOVERY_SET_ADDRESS_SIZE));
            kCheck(kSerializer_Write64s(iface->writer, GO_DISCOVERY_SET_ADDRESS_TEST));
            kCheck(kSerializer_Write64s(iface->writer, GO_DISOVERY_SIGNATURE));
            kCheck(kSerializer_Write64s(iface->writer, deviceId));
            kCheck(kSerializer_Write64s(iface->writer, address->useDhcp));

            kCheck(GoDiscovery_WriteIpAddress(iface->writer, (address->useDhcp) ? kIpAddress_AnyV4() : address->address));
            kCheck(GoDiscovery_WriteIpAddress(iface->writer, (address->useDhcp) ? kIpAddress_AnyV4() : address->mask));
            kCheck(GoDiscovery_WriteIpAddress(iface->writer, (address->useDhcp) ? kIpAddress_AnyV4() : address->gateway));
            kCheck(GoDiscovery_WriteIpAddress(iface->writer, (address->useDhcp) ? kIpAddress_AnyV4() : zero));

            kCheck(kSerializer_Flush(iface->writer));
            kCheck(kUdpClient_Send(iface->client, kIpAddress_BroadcastV4(), GO_DISCOVERY_PORT, GO_DISCOVERY_SET_ADDRESS_TIMEOUT, kTRUE));
        }
    }

    kCheck(kTimer_Start(obj->stopwatch, GO_DISCOVERY_SET_ADDRESS_TIMEOUT));

    do
    {
        if (kSuccess(GoDiscovery_ReceiveReply(discovery, &remoteEndPoint, kTimer_Remaining(obj->stopwatch)))
            && remoteEndPoint.port == GO_DISCOVERY_PORT
            && kSuccess(GoDiscovery_ParseSetTestReply(discovery, &responseDeviceId))
            && responseDeviceId == deviceId)
        {
            return kOK;
        }
    } while (!kTimer_IsExpired(obj->stopwatch));

    return kERROR_NOT_FOUND;
}

GoFx(kStatus) GoDiscovery_GetExtendedInfo(GoDiscovery discovery, k32u deviceId, GoDiscoveryExtInfo* info, kAlloc allocator)
{
    kObj(GoDiscovery, discovery);
    kIpEndPoint remoteEndPoint;
    kSize i;
    kStatus sendStatus = kERROR_NETWORK;
    kStatus recvStatus = kERROR_NOT_FOUND;

    allocator = kAlloc_Fallback(allocator);

    kCheck(kLock_Enter(obj->enumLock));

    kTry
    {

        for (i = 0; i < kArrayList_Count(obj->interfaces); ++i)
        {
            GoDiscoveryInterface* iface = kArrayList_AtT(obj->interfaces, i, GoDiscoveryInterface);

            if (iface->enabled)
            {
                kTest(kSerializer_Write32u(iface->writer, GO_DISCOVERY_GET_INFO_SIZE));
                kTest(kSerializer_Write32u(iface->writer, 0));
                kTest(kSerializer_Write32u(iface->writer, GO_DISCOVERY_GET_INFO));
                kTest(kSerializer_Write32u(iface->writer, 0));
                kTest(kSerializer_Write64u(iface->writer, GO_DISOVERY_SIGNATURE));
                kTest(kSerializer_Write32u(iface->writer, deviceId));
                kTest(kSerializer_Write32u(iface->writer, 0));

                kTest(kSerializer_Flush(iface->writer));

                if (kSuccess(kUdpClient_Send(iface->client, kIpAddress_BroadcastV4(), GO_DISCOVERY_PORT, GO_DISCOVERY_GET_INFO_TIMEOUT, kTRUE)))
                {
                    sendStatus = kOK;
                }
            }
        }

        kTest(sendStatus);
        kTest(kTimer_Start(obj->stopwatch, GO_DISCOVERY_GET_INFO_TIMEOUT));

        do
        {
            if (kSuccess(GoDiscovery_ReceiveReply(discovery, &remoteEndPoint, kTimer_Remaining(obj->stopwatch)))
                && remoteEndPoint.port == GO_DISCOVERY_PORT
                && kSuccess(GoDiscovery_ParseGetInfoReply(discovery, info, allocator))
                && GoDiscoveryExtInfo_Id(*info) == deviceId)
            {
#ifdef DEBUG_GO_DISCOVERY
                DEBUG_PRINT("%s: sensor %u remote end point\n", __FUNCTION__, deviceId);
                PrintEndPoint(&remoteEndPoint);
#endif

                recvStatus = kOK;
                break;
            }
            else
            {
                if (!kIsNull(*info))
                {
                    kDisposeRef(info);
                }
            }
        } while (!kTimer_IsExpired(obj->stopwatch));

    }
    kFinally
    {
        kCheck(kLock_Exit(obj->enumLock));
        kEndFinally();
    }

    return recvStatus;
}

GoFx(kStatus) GoDiscovery_ReceiveReply(GoDiscovery discovery, kIpEndPoint* endPoint, k64u timeout)
{
    kObj(GoDiscovery, discovery);
    kSize readSize;

    // Copy into a kStream whose pointer can be reset and the content read again.
    kCheck(kUdpClient_Receive(obj->receiver, endPoint, &readSize, timeout));

    kCheck(kStream_Seek(obj->readerBuffer, 0, kSEEK_ORIGIN_BEGIN));
    kCheck(kStream_Copy(obj->readerBuffer, obj->receiver, readSize));

    kCheck(kStream_Seek(obj->readerBuffer, 0, kSEEK_ORIGIN_BEGIN));

    return kOK;
}

GoFx(kStatus) GoDiscovery_ParseGetAddressReply(GoDiscovery discovery, GoDiscoveryInfo* info, kIpEndPoint* remoteEndPoint)
{
    kObj(GoDiscovery, discovery);
    k64s responseId, signature, status, deviceId, useDhcp;
    kIpAddress tempIp;

    kCheck(kMemSet(info, 0, sizeof(info)));

    kTry
    {
        // The size field is included in the message size count stored in the size field.
        kTest(kSerializer_BeginRead(obj->reader, kTypeOf(k64u), kTRUE));

        kTest(kSerializer_Read64s(obj->reader, &responseId));
        // The 5.1 and newer SDK sends both a GET ADDRESS and GET INFO
        // discovery request. A 5.1 and newer sensor firmware version
        // supports both requests and therefore sends back both replies.
        // In the caller to this function, a reply is assumed to be a GET ADDRESS
        // reply and if it is not, then the reply is parsed as a GET
        // INFO reply.
        // So it is valid that the response id is not an GET ADDRESS REPLY so
        // it is not a real error but an expected scenario.
        kTest(responseId == GO_DISCOVERY_GET_ADDRESS_REPLY);

        kTest(kSerializer_Read64s(obj->reader, &status));
        kTest(status);

        kTest(kSerializer_Read64s(obj->reader, &signature));
        kTest(signature == GO_DISOVERY_SIGNATURE);

        kTest(kSerializer_Read64s(obj->reader, &deviceId));
        info->id = (k32u) deviceId;

        kTest(kSerializer_Read64s(obj->reader, &useDhcp));
        info->address.useDhcp = (kBool) useDhcp;

        kTest(GoDiscovery_ReadIpAddress(obj->reader, &info->address.address));
        kTest(GoDiscovery_ReadIpAddress(obj->reader, &info->address.mask));
        kTest(GoDiscovery_ReadIpAddress(obj->reader, &info->address.gateway));
        kTest(GoDiscovery_ReadIpAddress(obj->reader, &tempIp));

        // The ports must be set to a non-zero value to indicate they do not
        // have a valid value. All the remaining fields in "info" can remain as zeroes.
        info->ports.controlPort = k16U_NULL;
        info->ports.dataPort = k16U_NULL;
        info->ports.healthPort = k16U_NULL;
        info->ports.upgradePort = k16U_NULL;
        info->ports.webPort = k16U_NULL;

        info->discoveryServerAddress = remoteEndPoint->address;
    }
    kFinally
    {
        // Automatically flush any unread data and free allocated serializer memory.
        kSerializer_EndRead(obj->reader);

        kEndFinally();
    }

    return kOK;
}

GoFx(kStatus) GoDiscovery_ParseSetAddressReplyCommon(GoDiscovery discovery, k64s replyMsgId, k32u* deviceId)
{
    kObj(GoDiscovery, discovery);
    k64s responseDeviceId;
    k64s responseSignature;
    k64s responseId;
    k64s responseStatus;

    kTry
    {
        // The size field is included in the message size count stored in the size field.
        kTest(kSerializer_BeginRead(obj->reader, kTypeOf(k64u), kTRUE));

        kTest(kSerializer_Read64s(obj->reader, &responseId));
        kTest(responseId == replyMsgId);

        kTest(kSerializer_Read64s(obj->reader, &responseStatus));
        kTest(responseStatus);

        kTest(kSerializer_Read64s(obj->reader, &responseSignature));
        kTest(responseSignature == GO_DISOVERY_SIGNATURE);

        kTest(kSerializer_Read64s(obj->reader, &responseDeviceId));

        *deviceId = (k32u) responseDeviceId;
    }
    kFinally
    {
        // Automatically flush any unread data and free allocated serializer memory.
        kSerializer_EndRead(obj->reader);

        kEndFinally();
    }

    return kOK;
}

GoFx(kStatus) GoDiscovery_ParseSetAddressReply(GoDiscovery discovery, k32u* deviceId)
{
    return(GoDiscovery_ParseSetAddressReplyCommon(discovery, GO_DISCOVERY_SET_ADDRESS_REPLY, deviceId));
}

GoFx(kStatus) GoDiscovery_ParseSetTestReply(GoDiscovery discovery, k32u* deviceId)
{
    return(GoDiscovery_ParseSetAddressReplyCommon(discovery, GO_DISCOVERY_SET_ADDRESS_TEST_REPLY, deviceId));
}

GoFx(kStatus) GoDiscovery_ParseGetInfoReply(GoDiscovery discovery, GoDiscoveryExtInfo* info, kAlloc allocator)
{
    kObj(GoDiscovery, discovery);

    kCheck(GoDiscoveryExtInfo_Construct(info, allocator));
    kCheck(GoDiscoveryExtInfo_Read(*info, obj->reader, allocator));

    return kOK;
}

GoFx(kStatus) GoDiscovery_SetEnumPeriod(GoDiscovery discovery, k64u period)
{
    kObj(GoDiscovery, discovery);

    obj->enumPeriod = period;

    return kOK;
}

GoFx(kStatus) GoDiscovery_SetEnumHandler(GoDiscovery discovery, GoDiscoveryEnumFx function, kPointer receiver)
{
    kObj(GoDiscovery, discovery);

    obj->onEnumerate.function = (kCallbackFx) function;
    obj->onEnumerate.receiver = receiver;

    return kOK;
}

GoFx(kStatus) GoDiscovery_StartEnum(GoDiscovery discovery, kBool waitFirst)
{
    kObj(GoDiscovery, discovery);

    kCheckState(!kIsNull(obj->onEnumerate.function));
    kCheckState(!kPeriodic_Enabled(obj->eventTimer));

    obj->runCount = 0;

    if (waitFirst)
    {
        kCheck(GoDiscovery_Enumerate(discovery, obj->infoList));
        kCheck(obj->onEnumerate.function(obj->onEnumerate.receiver, discovery, obj->infoList));
    }

    kCheck(kPeriodic_Start(obj->eventTimer, 0, GoDiscovery_OnEnumElapsed, discovery));

    return kOK;
}

GoFx(kStatus) GoDiscovery_StopEnum(GoDiscovery discovery)
{
    kObj(GoDiscovery, discovery);

    kCheck(kPeriodic_Stop(obj->eventTimer));

    obj->runCount = 0;

    return kOK;
}

GoFx(kStatus) GoDiscovery_BeginEnum(GoDiscovery discovery)
{
    kObj(GoDiscovery, discovery);
    kSize i;

    for (i = 0; i < kArrayList_Count(obj->interfaces); ++i)
    {
        GoDiscoveryInterface* iface = kArrayList_AtT(obj->interfaces, i, GoDiscoveryInterface);

        if (iface->enabled)
        {
            if (kAtomic32s_Get(&obj->compatEnabled) == 1)
            {
                kCheck(GoDiscovery_SendGetAddress(discovery, iface));
            }

            kCheck(GoDiscovery_SendGetExtendedInfo(discovery, iface));
        }
    }

    return kOK;
}

GoFx(kStatus) GoDiscovery_EndEnum(GoDiscovery discovery, kArrayList list)
{
    kObj(GoDiscovery, discovery);
    GoDiscoveryExtInfo extInfo = kNULL;
    kMapItem mapItem;

    kCheck(kMap_Clear(obj->sensorInfoMap));

    kTry
    {
        while (kSuccess(kSocket_Wait(kUdpClient_Socket(obj->receiver), 0)))
        {
            kIpEndPoint remoteEndPoint;

            kTest(GoDiscovery_ReceiveReply(discovery, &remoteEndPoint, 1000000));

            if (remoteEndPoint.port == GO_DISCOVERY_PORT)
            {
                GoDiscoveryInfo info;

                GoDiscoveryInfo_Init(&info);

                // When compatibility mode is on, the reply could either be a GetAddress reply, or
                // a GetExtendedInfo reply. Rather than restructuring the code to read reply ID first,
                // we take a shortcut to attempt to read the reply as both types if needed.
                if (kSuccess(GoDiscovery_ParseGetAddressReply(discovery, &info, &remoteEndPoint)))
                {
                    kTest(GoDiscovery_ProcessGetAddressReply(discovery, &info));

#ifdef DEBUG_GO_DISCOVERY
                    DEBUG_PRINT("%s: sensor %u GetAddressReply remote end point\n", __FUNCTION__, info.id);
                    PrintEndPoint(&remoteEndPoint);
#endif
                }
                else
                {
                    kTest(kStream_Seek(obj->readerBuffer, 0, kSEEK_ORIGIN_BEGIN));

                    if (kSuccess(GoDiscovery_ParseGetInfoReply(discovery, &extInfo, kObject_Alloc(discovery))))
                    {
                        kTest(GoDiscovery_ProcessGetExtendedInfoReply(discovery, extInfo, &remoteEndPoint));

#ifdef DEBUG_GO_DISCOVERY
                        DEBUG_PRINT("%s: sensor %u ExtInfoReply remote end point\n", __FUNCTION__, GoDiscoveryExtInfo_Id(extInfo));
                        PrintEndPoint(&remoteEndPoint);
#endif

                        kTest(kDisposeRef(&extInfo));
                    }
                }
            }
        }
    }
    kFinally
    {
        kDisposeRef(&extInfo);
        kEndFinally();
    }

    kCheck(kArrayList_Clear(list));

    for (mapItem = kMap_First(obj->sensorInfoMap);
        mapItem != kNULL;
        mapItem = kMap_Next(obj->sensorInfoMap, mapItem))
    {
        kCheck(kArrayList_Add(list, kMap_Value(obj->sensorInfoMap, mapItem)));
    }

    return kOK;
}

GoFx(kStatus) GoDiscovery_ProcessGetAddressReply(GoDiscovery discovery, const GoDiscoveryInfo* info)
{
    kObj(GoDiscovery, discovery);

    if (!kSuccess(kMap_Find(obj->sensorInfoMap, &info->id, kNULL)))
    {
        kCheck(kMap_Add(obj->sensorInfoMap, &info->id, info));
    }

    return kOK;
}

GoFx(kStatus) GoDiscovery_ProcessGetExtendedInfoReply(GoDiscovery discovery, GoDiscoveryExtInfo extInfo, kIpEndPoint* remoteEndPoint)
{
    kObj(GoDiscovery, discovery);
    GoDiscoveryInfo info;

    kCheck(kMemSet(&info, 0, sizeof(info)));

    // Copy most of the extended discovery information so that the sensor
    // object can be populated with the information later in the enumeration
    // process.
    info.id         = GoDiscoveryExtInfo_Id(extInfo);
    info.address    = GoDiscoveryExtInfo_Address(extInfo);
    info.ports      = GoDiscoveryExtInfo_Ports(extInfo);
    info.version    = GoDiscoveryExtInfo_Version(extInfo);
    info.opMode     = GoDiscoveryExtInfo_OpMode(extInfo);
    info.accelSensorIpAddress = GoDiscoveryExtInfo_AccelSensorIpAddress(extInfo);
    info.discoveryServerAddress = remoteEndPoint->address;

    // Call to GoDiscoveryExtInfo_CompleteModel() is deprecated; we more clearly provide 
    // PartNumber, ModelNumber, and ModelDisplayName instead.
    // NOTE: the below functions may not fill in the fields if they are misssing from
    // the discovery protocol.
    kCheck(GoDiscoveryExtInfo_PartNumber(extInfo, info.partNumber, kCountOf(info.partNumber)));
    kCheck(GoDiscoveryExtInfo_ModelNumber(extInfo, info.modelNumber, kCountOf(info.modelNumber)));
    kCheck(GoDiscoveryExtInfo_ModelDisplayName(extInfo, info.modelDisplayName, kCountOf(info.modelDisplayName)));
    kCheck(GoDiscoveryExtInfo_Family(extInfo, info.family, kCountOf(info.family)));

    kCheck(kMap_Replace(obj->sensorInfoMap, &info.id, &info));

    return kOK;
}

GoFx(kStatus) GoDiscovery_SendGetAddress(GoDiscovery discovery, GoDiscoveryInterface* iface)
{
    kCheck(kSerializer_Write64s(iface->writer, GO_DISCOVERY_GET_ADDRESS_SIZE));
    kCheck(kSerializer_Write64s(iface->writer, GO_DISCOVERY_GET_ADDRESS));
    kCheck(kSerializer_Write64s(iface->writer, GO_DISOVERY_SIGNATURE));
    kCheck(kSerializer_Write64s(iface->writer, 0));

    kCheck(kSerializer_Flush(iface->writer));

    // Don't care about errors; some interfaces may not allow sending.
    (void)kUdpClient_Send(iface->client, kIpAddress_BroadcastV4(), GO_DISCOVERY_PORT, GO_DISCOVERY_GET_ADDRESS_TIMEOUT, kTRUE);

    return kOK;
}

GoFx(kStatus) GoDiscovery_SendGetExtendedInfo(GoDiscovery discovery, GoDiscoveryInterface* iface)
{
    kCheck(kSerializer_Write64s(iface->writer, GO_DISCOVERY_GET_INFO_SIZE));
    kCheck(kSerializer_Write64s(iface->writer, GO_DISCOVERY_GET_INFO));
    kCheck(kSerializer_Write64s(iface->writer, GO_DISOVERY_SIGNATURE));
    kCheck(kSerializer_Write64s(iface->writer, 0));

    kCheck(kSerializer_Flush(iface->writer));

    // Don't care about errors; some interfaces may not allow sending.
    (void)kUdpClient_Send(iface->client, kIpAddress_BroadcastV4(), GO_DISCOVERY_PORT, GO_DISCOVERY_GET_INFO_TIMEOUT, kTRUE);

    return kOK;
}

GoFx(kStatus) GoDiscovery_OnEnumElapsed(GoDiscovery discovery, kPeriodic timer)
{
    kObj(GoDiscovery, discovery);

    if (!obj->enumPending)
    {
        kStatus status = kOK;

        kCheck(kLock_Enter(obj->enumLock));

        kTry
        {
            kTest(kTimer_Start(obj->stopwatch, obj->enumPeriod));
            kTest(GoDiscovery_BeginEnum(discovery));
            obj->enumPending = kTRUE;

            kTest(kPeriodic_Start(obj->eventTimer, GO_DISCOVERY_GET_ADDRESS_TIMEOUT, GoDiscovery_OnEnumElapsed, discovery));
        }
        kCatch(&status)
        {
            kCheck(kLock_Exit(obj->enumLock));
            kEndCatch(status);
        }
    }
    else
    {
        kCheck(GoDiscovery_OnEnumElapsedRecv(discovery, timer));
    }

    return kOK;
}

GoFx(kStatus) GoDiscovery_OnEnumElapsedRecv(GoDiscovery discovery, kPeriodic timer)
{
    kObj(GoDiscovery, discovery);

    kTry
    {
        kTest(GoDiscovery_EndEnum(discovery, obj->infoList));
        kTest(obj->onEnumerate.function(obj->onEnumerate.receiver, discovery, obj->infoList));

        obj->enumPending = kFALSE;

        kTest(kPeriodic_Start(obj->eventTimer, kTimer_Remaining(obj->stopwatch), GoDiscovery_OnEnumElapsed, discovery));

        obj->runCount++;
    }
    kFinally
    {
        kCheck(kLock_Exit(obj->enumLock));
        kEndFinally();
    }

    return kOK;
}

GoFx(kStatus) GoDiscovery_SetOneInterface(GoDiscovery discovery, kIpAddress* address, kBool enable)
{
    kObj(GoDiscovery, discovery);
    kSize i;

    for (i = 0; i < kArrayList_Count(obj->interfaces); ++i)
    {
        GoDiscoveryInterface* iface = kArrayList_AtT(obj->interfaces, i, GoDiscoveryInterface);

        if (kIpAddress_Equals(*address, iface->address))
        {
            iface->enabled = enable;
        }
    }

    return kOK;
}

GoFx(kStatus) GoDiscovery_SetAllInterface(GoDiscovery discovery, kBool enable)
{
    kObj(GoDiscovery, discovery);
    kSize i;

    for (i = 0; i < kArrayList_Count(obj->interfaces); ++i)
    {
        GoDiscoveryInterface* iface = kArrayList_AtT(obj->interfaces, i, GoDiscoveryInterface);

        iface->enabled = enable;
    }

    return kOK;
}

GoFx(kStatus) GoDiscovery_EnableCompatMode(GoDiscovery discovery, kBool enable)
{
    kObj(GoDiscovery, discovery);

    kAtomic32s_Exchange(&obj->compatEnabled, enable ? 1 : 0);

    return kOK;
}

GoFx(kBool) GoDiscovery_CompatModeEnabled(GoDiscovery discovery)
{
    kObj(GoDiscovery, discovery);

    return kAtomic32s_Get(&obj->compatEnabled) == 1;
}


