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

#include "chpp/app.h"

#include <inttypes.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>

#include "chpp/clients.h"
#include "chpp/clients/discovery.h"
#include "chpp/services.h"
#ifdef CHPP_CLIENT_ENABLED_LOOPBACK
#include "chpp/clients/loopback.h"
#endif
#ifdef CHPP_CLIENT_ENABLED_TIMESYNC
#include "chpp/clients/timesync.h"
#endif
#include "chpp/log.h"
#include "chpp/macros.h"
#include "chpp/notifier.h"
#include "chpp/pal_api.h"
#include "chpp/services.h"
#include "chpp/services/discovery.h"
#include "chpp/services/loopback.h"
#include "chpp/services/nonhandle.h"
#include "chpp/services/timesync.h"
#include "chpp/time.h"

/************************************************
 *  Prototypes
 ***********************************************/

static bool chppProcessPredefinedClientRequest(struct ChppAppState *context,
                                               uint8_t *buf, size_t len);
static bool chppProcessPredefinedServiceResponse(struct ChppAppState *context,
                                                 uint8_t *buf, size_t len);

static bool chppDatagramLenIsOk(struct ChppAppState *context,
                                const struct ChppAppHeader *rxHeader,
                                size_t len);
static ChppDispatchFunction *chppGetDispatchFunction(
    struct ChppAppState *context, uint8_t handle, enum ChppMessageType type);
#ifdef CHPP_CLIENT_ENABLED_DISCOVERY
static ChppNotifierFunction *chppGetClientResetNotifierFunction(
    struct ChppAppState *context, uint8_t index);
#endif  // CHPP_CLIENT_ENABLED_DISCOVERY
static ChppNotifierFunction *chppGetServiceResetNotifierFunction(
    struct ChppAppState *context, uint8_t index);
static inline const struct ChppService *chppServiceOfHandle(
    struct ChppAppState *appContext, uint8_t handle);
static inline const struct ChppClient *chppClientOfHandle(
    struct ChppAppState *appContext, uint8_t handle);
static inline struct ChppEndpointState *chppServiceStateOfHandle(
    struct ChppAppState *appContext, uint8_t handle);
static inline struct ChppEndpointState *chppClientStateOfHandle(
    struct ChppAppState *appContext, uint8_t handle);
static struct ChppEndpointState *chppClientOrServiceStateOfHandle(
    struct ChppAppState *appContext, uint8_t handle, enum ChppMessageType type);

static void chppProcessPredefinedHandleDatagram(struct ChppAppState *context,
                                                uint8_t *buf, size_t len);
static void chppProcessNegotiatedHandleDatagram(struct ChppAppState *context,
                                                uint8_t *buf, size_t len);

/************************************************
 *  Private Functions
 ***********************************************/

/**
 * Processes a client request that is determined to be for a predefined CHPP
 * service.
 *
 * @param context State of the app layer.
 * @param buf Input data. Cannot be null.
 * @param len Length of input data in bytes.
 *
 * @return False if handle is invalid. True otherwise.
 */
static bool chppProcessPredefinedClientRequest(struct ChppAppState *context,
                                               uint8_t *buf, size_t len) {
  const struct ChppAppHeader *rxHeader = (struct ChppAppHeader *)buf;
  bool handleValid = true;
  bool dispatchResult = true;

  switch (rxHeader->handle) {
    case CHPP_HANDLE_LOOPBACK: {
      dispatchResult = chppDispatchLoopbackClientRequest(context, buf, len);
      break;
    }

    case CHPP_HANDLE_TIMESYNC: {
      dispatchResult = chppDispatchTimesyncClientRequest(context, buf, len);
      break;
    }

    case CHPP_HANDLE_DISCOVERY: {
      dispatchResult = chppDispatchDiscoveryClientRequest(context, buf, len);
      break;
    }

    default: {
      handleValid = false;
    }
  }

  if (dispatchResult == false) {
    CHPP_LOGE("H#%" PRIu8 " unknown request. cmd=%#x, ID=%" PRIu8,
              rxHeader->handle, rxHeader->command, rxHeader->transaction);
  }

  return handleValid;
}

/**
 * Processes a service response that is determined to be for a predefined CHPP
 * client.
 *
 * @param context State of the app layer.
 * @param buf Input data. Cannot be null.
 * @param len Length of input data in bytes.
 *
 * @return False if handle is invalid. True otherwise.
 */
static bool chppProcessPredefinedServiceResponse(struct ChppAppState *context,
                                                 uint8_t *buf, size_t len) {
  CHPP_DEBUG_NOT_NULL(buf);
  // Possibly unused if compiling without the clients below enabled
  UNUSED_VAR(context);
  UNUSED_VAR(len);

  const struct ChppAppHeader *rxHeader = (struct ChppAppHeader *)buf;
  bool handleValid = true;
  bool dispatchResult = true;

  switch (rxHeader->handle) {
#ifdef CHPP_CLIENT_ENABLED_LOOPBACK
    case CHPP_HANDLE_LOOPBACK: {
      dispatchResult = chppDispatchLoopbackServiceResponse(context, buf, len);
      break;
    }
#endif  // CHPP_CLIENT_ENABLED_LOOPBACK

#ifdef CHPP_CLIENT_ENABLED_TIMESYNC
    case CHPP_HANDLE_TIMESYNC: {
      dispatchResult = chppDispatchTimesyncServiceResponse(context, buf, len);
      break;
    }
#endif  // CHPP_CLIENT_ENABLED_TIMESYNC

#ifdef CHPP_CLIENT_ENABLED_DISCOVERY
    case CHPP_HANDLE_DISCOVERY: {
      dispatchResult = chppDispatchDiscoveryServiceResponse(context, buf, len);
      break;
    }
#endif  // CHPP_CLIENT_ENABLED_DISCOVERY

    default: {
      handleValid = false;
    }
  }

  if (dispatchResult == false) {
    CHPP_LOGE("H#%" PRIu8 " unknown response. cmd=%#x, ID=%" PRIu8
              ", len=%" PRIuSIZE,
              rxHeader->handle, rxHeader->command, rxHeader->transaction, len);
  }

  return handleValid;
}

/**
 * Verifies if the length of a Rx Datagram from the transport layer is
 * sufficient for the associated service/client.
 *
 * @param context State of the app layer.
 * @param rxHeader The pointer to the datagram RX header.
 * @param len Length of the datagram in bytes.
 *
 * @return true if length is ok.
 */
static bool chppDatagramLenIsOk(struct ChppAppState *context,
                                const struct ChppAppHeader *rxHeader,
                                size_t len) {
  CHPP_DEBUG_NOT_NULL(context);
  CHPP_DEBUG_NOT_NULL(rxHeader);

  size_t minLen = SIZE_MAX;
  uint8_t handle = rxHeader->handle;

  if (handle < CHPP_HANDLE_NEGOTIATED_RANGE_START) {  // Predefined
    switch (handle) {
      case CHPP_HANDLE_NONE:
        minLen = sizeof_member(struct ChppAppHeader, handle);
        break;

      case CHPP_HANDLE_LOOPBACK:
        minLen = sizeof_member(struct ChppAppHeader, handle) +
                 sizeof_member(struct ChppAppHeader, type);
        break;

      case CHPP_HANDLE_TIMESYNC:
      case CHPP_HANDLE_DISCOVERY:
        minLen = sizeof(struct ChppAppHeader);
        break;

      default:
        // len remains SIZE_MAX
        CHPP_LOGE("Invalid H#%" PRIu8, handle);
        return false;
    }

  } else {  // Negotiated
    enum ChppMessageType messageType =
        CHPP_APP_GET_MESSAGE_TYPE(rxHeader->type);

    switch (messageType) {
      case CHPP_MESSAGE_TYPE_CLIENT_REQUEST:
      case CHPP_MESSAGE_TYPE_CLIENT_RESPONSE:
      case CHPP_MESSAGE_TYPE_CLIENT_NOTIFICATION: {
        const struct ChppService *service =
            chppServiceOfHandle(context, handle);
        if (service != NULL) {
          minLen = service->minLength;
        }
        break;
      }
      case CHPP_MESSAGE_TYPE_SERVICE_RESPONSE:
      case CHPP_MESSAGE_TYPE_SERVICE_REQUEST:
      case CHPP_MESSAGE_TYPE_SERVICE_NOTIFICATION: {
        const struct ChppClient *client = chppClientOfHandle(context, handle);
        if (client != NULL) {
          minLen = client->minLength;
        }
        break;
      }
      default:
        CHPP_LOGE("Invalid type=%d or H#%" PRIu8, messageType, handle);
        return false;
    }
  }

  if (len < minLen) {
    CHPP_LOGE("Datagram len=%" PRIuSIZE " < %" PRIuSIZE " for H#%" PRIu8, len,
              minLen, handle);
    return false;
  }

  return true;
}

/**
 * Returns the dispatch function of a particular negotiated client/service
 * handle and message type.
 *
 * Returns null if it is unsupported by the service.
 *
 * @param context State of the app layer.
 * @param handle Handle number for the client/service.
 * @param type Message type.
 *
 * @return Pointer to a function that dispatches incoming datagrams for any
 * particular client/service.
 */
static ChppDispatchFunction *chppGetDispatchFunction(
    struct ChppAppState *context, uint8_t handle, enum ChppMessageType type) {
  CHPP_DEBUG_NOT_NULL(context);
  // chppDatagramLenIsOk() has already confirmed that the handle # is valid.
  // Therefore, no additional checks are necessary for chppClientOfHandle(),
  // chppServiceOfHandle(), or chppClientOrServiceStateOfHandle().

  // Make sure the client is open before it can receive any message:
  switch (CHPP_APP_GET_MESSAGE_TYPE(type)) {
    case CHPP_MESSAGE_TYPE_SERVICE_RESPONSE:
    case CHPP_MESSAGE_TYPE_SERVICE_REQUEST:
    case CHPP_MESSAGE_TYPE_SERVICE_NOTIFICATION: {
      struct ChppEndpointState *clientState =
          chppClientStateOfHandle(context, handle);
      if (clientState->openState == CHPP_OPEN_STATE_CLOSED) {
        CHPP_LOGE("RX service response but client closed");
        return NULL;
      }
      break;
    }
    default:
      // no check needed on the service side
      break;
  }

  switch (CHPP_APP_GET_MESSAGE_TYPE(type)) {
    case CHPP_MESSAGE_TYPE_CLIENT_REQUEST:
      return chppServiceOfHandle(context, handle)->requestDispatchFunctionPtr;
    case CHPP_MESSAGE_TYPE_SERVICE_RESPONSE:
      return chppClientOfHandle(context, handle)->responseDispatchFunctionPtr;
    case CHPP_MESSAGE_TYPE_SERVICE_REQUEST:
      return chppClientOfHandle(context, handle)->requestDispatchFunctionPtr;
    case CHPP_MESSAGE_TYPE_CLIENT_RESPONSE:
      return chppServiceOfHandle(context, handle)->responseDispatchFunctionPtr;
    case CHPP_MESSAGE_TYPE_CLIENT_NOTIFICATION:
      return chppServiceOfHandle(context, handle)
          ->notificationDispatchFunctionPtr;
    case CHPP_MESSAGE_TYPE_SERVICE_NOTIFICATION:
      return chppClientOfHandle(context, handle)
          ->notificationDispatchFunctionPtr;
  }

  return NULL;
}

#ifdef CHPP_CLIENT_ENABLED_DISCOVERY
/**
 * Returns the reset notification function pointer of a particular negotiated
 * client.
 *
 * Returns null for clients that do not need or support a reset notification.
 *
 * @param context State of the app layer.
 * @param index Index of the registered client.
 *
 * @return Pointer to the reset notification function.
 */
static ChppNotifierFunction *chppGetClientResetNotifierFunction(
    struct ChppAppState *context, uint8_t index) {
  CHPP_DEBUG_NOT_NULL(context);
  return context->registeredClients[index]->resetNotifierFunctionPtr;
}
#endif  // CHPP_CLIENT_ENABLED_DISCOVERY

/**
 * Returns the reset function pointer of a particular registered service.
 *
 * Returns null for services that do not need or support a reset notification.
 *
 * @param context State of the app layer.
 * @param index Index of the registered service.
 *
 * @return Pointer to the reset function.
 */
ChppNotifierFunction *chppGetServiceResetNotifierFunction(
    struct ChppAppState *context, uint8_t index) {
  CHPP_DEBUG_NOT_NULL(context);
  return context->registeredServices[index]->resetNotifierFunctionPtr;
}

/**
 * Returns a pointer to the ChppService struct of the service matched to a
 * negotiated handle.
 *
 * Returns null if a service doesn't exist for the handle.
 *
 * @param context State of the app layer.
 * @param handle Handle number.
 *
 * @return Pointer to the ChppService struct of a particular service handle.
 */
static inline const struct ChppService *chppServiceOfHandle(
    struct ChppAppState *context, uint8_t handle) {
  CHPP_DEBUG_NOT_NULL(context);
  uint8_t serviceIndex = CHPP_SERVICE_INDEX_OF_HANDLE(handle);
  if (serviceIndex < context->registeredServiceCount) {
    return context->registeredServices[serviceIndex];
  }

  return NULL;
}

/**
 * Returns a pointer to the ChppClient struct of the client matched to a
 * negotiated handle.
 *
 * Returns null if a client doesn't exist for the handle.
 *
 * @param context State of the app layer.
 * @param handle Handle number.
 *
 * @return Pointer to the ChppClient struct matched to a particular handle.
 */
static inline const struct ChppClient *chppClientOfHandle(
    struct ChppAppState *context, uint8_t handle) {
  CHPP_DEBUG_NOT_NULL(context);
  uint8_t serviceIndex = CHPP_SERVICE_INDEX_OF_HANDLE(handle);
  if (serviceIndex < context->discoveredServiceCount) {
    uint8_t clientIndex = context->clientIndexOfServiceIndex[serviceIndex];
    if (clientIndex < context->registeredClientCount) {
      return context->registeredClients[clientIndex];
    }
  }

  return NULL;
}

/**
 * Returns the service state for a given handle.
 *
 * The caller must pass a valid handle.
 *
 * @param context State of the app layer.
 * @param handle Handle number for the service.
 *
 * @return Pointer to a ChppEndpointState.
 */
static inline struct ChppEndpointState *chppServiceStateOfHandle(
    struct ChppAppState *context, uint8_t handle) {
  CHPP_DEBUG_NOT_NULL(context);
  CHPP_DEBUG_ASSERT(CHPP_SERVICE_INDEX_OF_HANDLE(handle) <
                    context->registeredServiceCount);

  const uint8_t serviceIdx = CHPP_SERVICE_INDEX_OF_HANDLE(handle);
  return context->registeredServiceStates[serviceIdx];
}

/**
 * Returns a pointer to the client state for a given handle.
 *
 * The caller must pass a valid handle.
 *
 * @param context State of the app layer.
 * @param handle Handle number for the service.
 *
 * @return Pointer to the endpoint state.
 */
static inline struct ChppEndpointState *chppClientStateOfHandle(
    struct ChppAppState *context, uint8_t handle) {
  CHPP_DEBUG_NOT_NULL(context);
  CHPP_DEBUG_ASSERT(CHPP_SERVICE_INDEX_OF_HANDLE(handle) <
                    context->registeredClientCount);
  const uint8_t serviceIdx = CHPP_SERVICE_INDEX_OF_HANDLE(handle);
  const uint8_t clientIdx = context->clientIndexOfServiceIndex[serviceIdx];
  return context->registeredClientStates[clientIdx]->context;
}

/**
 * Returns a pointer to the client or service state for a given handle.
 *
 * The caller must pass a valid handle.
 *
 * @param appContext State of the app layer.
 * @param handle Handle number for the service.
 * @param type Message type (indicates if this is for a client or service).
 *
 * @return Pointer to the endpoint state (NULL if wrong type).
 */
static struct ChppEndpointState *chppClientOrServiceStateOfHandle(
    struct ChppAppState *appContext, uint8_t handle,
    enum ChppMessageType type) {
  switch (CHPP_APP_GET_MESSAGE_TYPE(type)) {
    case CHPP_MESSAGE_TYPE_CLIENT_REQUEST:
    case CHPP_MESSAGE_TYPE_CLIENT_RESPONSE:
    case CHPP_MESSAGE_TYPE_CLIENT_NOTIFICATION:
      return chppServiceStateOfHandle(appContext, handle);
    case CHPP_MESSAGE_TYPE_SERVICE_REQUEST:
    case CHPP_MESSAGE_TYPE_SERVICE_RESPONSE:
    case CHPP_MESSAGE_TYPE_SERVICE_NOTIFICATION:
      return chppClientStateOfHandle(appContext, handle);
    default:
      CHPP_LOGE("Unknown type=0x%" PRIx8 " (H#%" PRIu8 ")", type, handle);
      return NULL;
  }
}

/**
 * Processes a received datagram that is determined to be for a predefined CHPP
 * service. Responds with an error if unsuccessful.
 *
 * Predefined requests are only sent by the client side.
 * Predefined responses are only sent by the service side.
 *
 * @param context State of the app layer.
 * @param buf Input data. Cannot be null.
 * @param len Length of input data in bytes.
 */
static void chppProcessPredefinedHandleDatagram(struct ChppAppState *context,
                                                uint8_t *buf, size_t len) {
  CHPP_DEBUG_NOT_NULL(context);
  CHPP_DEBUG_NOT_NULL(buf);

  struct ChppAppHeader *rxHeader = (struct ChppAppHeader *)buf;
  bool success = false;

  switch (CHPP_APP_GET_MESSAGE_TYPE(rxHeader->type)) {
    case CHPP_MESSAGE_TYPE_CLIENT_REQUEST: {
      success = chppProcessPredefinedClientRequest(context, buf, len);
      break;
    }
    case CHPP_MESSAGE_TYPE_SERVICE_RESPONSE: {
      success = chppProcessPredefinedServiceResponse(context, buf, len);
      break;
    }
    default:
      // Predefined client/services do not use
      // - notifications,
      // - service requests / client responses
      break;
  }

  if (!success) {
    CHPP_LOGE("H#%" PRIu8 " undefined msg type=0x%" PRIx8 " (len=%" PRIuSIZE
              ", ID=%" PRIu8 ")",
              rxHeader->handle, rxHeader->type, len, rxHeader->transaction);
    chppEnqueueTxErrorDatagram(context->transportContext,
                               CHPP_TRANSPORT_ERROR_APPLAYER);
  }
}

/**
 * Processes a received datagram that is determined to be for a negotiated CHPP
 * client or service.
 *
 * The datagram is processed by the dispatch function matching the datagram
 * type. @see ChppService and ChppClient.
 *
 * If a request dispatch function returns an error (anything different from
 * CHPP_APP_ERROR_NONE) then an error response is automatically sent back to the
 * remote endpoint.
 *
 * @param appContext State of the app layer.
 * @param buf Input data. Cannot be null.
 * @param len Length of input data in bytes.
 */
static void chppProcessNegotiatedHandleDatagram(struct ChppAppState *appContext,
                                                uint8_t *buf, size_t len) {
  CHPP_DEBUG_NOT_NULL(appContext);
  CHPP_DEBUG_NOT_NULL(buf);

  const struct ChppAppHeader *rxHeader = (struct ChppAppHeader *)buf;
  enum ChppMessageType messageType = CHPP_APP_GET_MESSAGE_TYPE(rxHeader->type);

  // Could be either the client or the service state depending on the message
  // type.
  struct ChppEndpointState *endpointState = chppClientOrServiceStateOfHandle(
      appContext, rxHeader->handle, messageType);
  if (endpointState == NULL) {
    CHPP_LOGE("H#%" PRIu8 " missing ctx (msg=0x%" PRIx8 " len=%" PRIuSIZE
              ", ID=%" PRIu8 ")",
              rxHeader->handle, rxHeader->type, len, rxHeader->transaction);
    chppEnqueueTxErrorDatagram(appContext->transportContext,
                               CHPP_TRANSPORT_ERROR_APPLAYER);
    CHPP_DEBUG_ASSERT(false);
    return;
  }

  ChppDispatchFunction *dispatchFunc =
      chppGetDispatchFunction(appContext, rxHeader->handle, messageType);
  if (dispatchFunc == NULL) {
    CHPP_LOGE("H#%" PRIu8 " unsupported msg=0x%" PRIx8 " (len=%" PRIuSIZE
              ", ID=%" PRIu8 ")",
              rxHeader->handle, rxHeader->type, len, rxHeader->transaction);
    chppEnqueueTxErrorDatagram(appContext->transportContext,
                               CHPP_TRANSPORT_ERROR_APPLAYER);
    return;
  }

  // All good. Dispatch datagram and possibly notify a waiting client
  enum ChppAppErrorCode error = dispatchFunc(endpointState->context, buf, len);

  if (error != CHPP_APP_ERROR_NONE) {
    CHPP_LOGE("RX dispatch err=0x%" PRIx16 " H#%" PRIu8 " type=0x%" PRIx8
              " ID=%" PRIu8 " cmd=0x%" PRIx16 " len=%" PRIuSIZE,
              error, rxHeader->handle, rxHeader->type, rxHeader->transaction,
              rxHeader->command, len);

    // Requests require a dispatch failure response.
    if (messageType == CHPP_MESSAGE_TYPE_CLIENT_REQUEST ||
        messageType == CHPP_MESSAGE_TYPE_SERVICE_REQUEST) {
      struct ChppAppHeader *response =
          chppAllocResponseFixed(rxHeader, struct ChppAppHeader);
      if (response != NULL) {
        response->error = (uint8_t)error;
        chppEnqueueTxDatagramOrFail(appContext->transportContext, response,
                                    sizeof(*response));
      }
    }
    return;
  }

  // Datagram is a response.
  // Check for synchronous operation and notify waiting endpoint if needed.
  if (messageType == CHPP_MESSAGE_TYPE_SERVICE_RESPONSE ||
      messageType == CHPP_MESSAGE_TYPE_CLIENT_RESPONSE) {
    struct ChppSyncResponse *syncResponse = &endpointState->syncResponse;
    chppMutexLock(&syncResponse->mutex);
    syncResponse->ready = true;
    CHPP_LOGD("Finished dispatching a response -> synchronous notification");
    chppConditionVariableSignal(&syncResponse->condVar);
    chppMutexUnlock(&syncResponse->mutex);
  }
}

/************************************************
 *  Public Functions
 ***********************************************/

void chppAppInit(struct ChppAppState *appContext,
                 struct ChppTransportState *transportContext) {
  // Default initialize all service/clients
  struct ChppClientServiceSet set;
  memset(&set, 0xff, sizeof(set));  // set all bits to 1

  chppAppInitWithClientServiceSet(appContext, transportContext, set);
}

void chppAppInitWithClientServiceSet(
    struct ChppAppState *appContext,
    struct ChppTransportState *transportContext,
    struct ChppClientServiceSet clientServiceSet) {
  CHPP_NOT_NULL(appContext);
  CHPP_DEBUG_NOT_NULL(transportContext);

  CHPP_LOGD("App Init");

  memset(appContext, 0, sizeof(*appContext));

  appContext->clientServiceSet = clientServiceSet;
  appContext->transportContext = transportContext;
  appContext->nextClientRequestTimeoutNs = CHPP_TIME_MAX;
  appContext->nextServiceRequestTimeoutNs = CHPP_TIME_MAX;

  chppPalSystemApiInit(appContext);

#ifdef CHPP_SERVICE_ENABLED
  chppRegisterCommonServices(appContext);
#endif

#ifdef CHPP_CLIENT_ENABLED
  chppRegisterCommonClients(appContext);
  chppInitBasicClients(appContext);
#endif
}

void chppAppDeinit(struct ChppAppState *appContext) {
  CHPP_LOGD("App deinit");

#ifdef CHPP_CLIENT_ENABLED
  chppDeinitMatchedClients(appContext);
  chppDeinitBasicClients(appContext);
  chppDeregisterCommonClients(appContext);
#endif

#ifdef CHPP_SERVICE_ENABLED
  chppDeregisterCommonServices(appContext);
#endif

  chppPalSystemApiDeinit(appContext);
}

void chppAppProcessRxDatagram(struct ChppAppState *context, uint8_t *buf,
                              size_t len) {
  CHPP_DEBUG_NOT_NULL(context);
  CHPP_DEBUG_NOT_NULL(buf);

  const struct ChppAppHeader *rxHeader = (struct ChppAppHeader *)buf;

  if (len == 0) {
    CHPP_DEBUG_ASSERT_LOG(false, "App rx w/ len 0");

  } else if (len < sizeof(struct ChppAppHeader)) {
    uint8_t *handle = (uint8_t *)buf;
    CHPP_LOGD("RX datagram len=%" PRIuSIZE " H#%" PRIu8, len, *handle);

  } else if (rxHeader->error != CHPP_APP_ERROR_NONE) {
    CHPP_LOGE("RX datagram len=%" PRIuSIZE " H#%" PRIu8 " type=0x%" PRIx8
              " ID=%" PRIu8 " ERR=%" PRIu8 " cmd=0x%" PRIx16,
              len, rxHeader->handle, rxHeader->type, rxHeader->transaction,
              rxHeader->error, rxHeader->command);
  } else {
    CHPP_LOGD("RX datagram len=%" PRIuSIZE " H#%" PRIu8 " type=0x%" PRIx8
              " ID=%" PRIu8 " err=%" PRIu8 " cmd=0x%" PRIx16,
              len, rxHeader->handle, rxHeader->type, rxHeader->transaction,
              rxHeader->error, rxHeader->command);
  }

  if (!chppDatagramLenIsOk(context, rxHeader, len)) {
    chppEnqueueTxErrorDatagram(context->transportContext,
                               CHPP_TRANSPORT_ERROR_APPLAYER);

  } else {
    if (rxHeader->handle == CHPP_HANDLE_NONE) {
      chppDispatchNonHandle(context, buf, len);

    } else if (rxHeader->handle < CHPP_HANDLE_NEGOTIATED_RANGE_START) {
      chppProcessPredefinedHandleDatagram(context, buf, len);

    } else {
      chppProcessNegotiatedHandleDatagram(context, buf, len);
    }
  }

  chppDatagramProcessDoneCb(context->transportContext, buf);
}

void chppAppProcessReset(struct ChppAppState *context) {
  CHPP_DEBUG_NOT_NULL(context);

#ifdef CHPP_CLIENT_ENABLED_DISCOVERY
  if (!context->isDiscoveryComplete) {
    chppInitiateDiscovery(context);

  } else {
    // Notify matched clients that a reset happened
    for (uint8_t i = 0; i < context->discoveredServiceCount; i++) {
      uint8_t clientIndex = context->clientIndexOfServiceIndex[i];
      if (clientIndex != CHPP_CLIENT_INDEX_NONE) {
        // Discovered service has a matched client
        ChppNotifierFunction *ResetNotifierFunction =
            chppGetClientResetNotifierFunction(context, clientIndex);

        CHPP_LOGD("Client #%" PRIu8 " (H#%d) reset notifier found=%d",
                  clientIndex, CHPP_SERVICE_HANDLE_OF_INDEX(i),
                  (ResetNotifierFunction != NULL));

        if (ResetNotifierFunction != NULL) {
          ResetNotifierFunction(
              context->registeredClientStates[clientIndex]->context);
        }
      }
    }
  }
#endif  // CHPP_CLIENT_ENABLED_DISCOVERY

  // Notify registered services that a reset happened
  for (uint8_t i = 0; i < context->registeredServiceCount; i++) {
    ChppNotifierFunction *ResetNotifierFunction =
        chppGetServiceResetNotifierFunction(context, i);

    CHPP_LOGD("Service #%" PRIu8 " (H#%d) reset notifier found=%d", i,
              CHPP_SERVICE_HANDLE_OF_INDEX(i), (ResetNotifierFunction != NULL));

    if (ResetNotifierFunction != NULL) {
      ResetNotifierFunction(context->registeredServiceStates[i]->context);
    }
  }

#ifdef CHPP_CLIENT_ENABLED_TIMESYNC
  // Reinitialize time offset
  chppTimesyncClientReset(context);
#endif
}

void chppUuidToStr(const uint8_t uuid[CHPP_SERVICE_UUID_LEN],
                   char strOut[CHPP_SERVICE_UUID_STRING_LEN]) {
  snprintf(
      strOut, CHPP_SERVICE_UUID_STRING_LEN,
      "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x",
      uuid[0], uuid[1], uuid[2], uuid[3], uuid[4], uuid[5], uuid[6], uuid[7],
      uuid[8], uuid[9], uuid[10], uuid[11], uuid[12], uuid[13], uuid[14],
      uuid[15]);
}

uint8_t chppAppErrorToChreError(uint8_t chppError) {
  switch (chppError) {
    case CHPP_APP_ERROR_NONE:
    case CHPP_APP_ERROR_INVALID_ARG:
    case CHPP_APP_ERROR_BUSY:
    case CHPP_APP_ERROR_OOM:
    case CHPP_APP_ERROR_UNSUPPORTED:
    case CHPP_APP_ERROR_TIMEOUT:
    case CHPP_APP_ERROR_DISABLED:
    case CHPP_APP_ERROR_RATELIMITED: {
      // CHRE and CHPP error values are identical in these cases
      return chppError;
    }
    default: {
      return CHRE_ERROR;
    }
  }
}

uint8_t chppAppShortResponseErrorHandler(uint8_t *buf, size_t len,
                                         const char *responseName) {
  CHPP_DEBUG_NOT_NULL(buf);
  CHPP_DEBUG_NOT_NULL(responseName);

  CHPP_ASSERT(len >= sizeof(struct ChppAppHeader));
  const struct ChppAppHeader *rxHeader = (struct ChppAppHeader *)buf;

  if (rxHeader->error == CHPP_APP_ERROR_NONE) {
    CHPP_LOGE("%s resp short len=%" PRIuSIZE, responseName, len);
    return CHRE_ERROR;
  }

  CHPP_LOGD("%s resp short len=%" PRIuSIZE, responseName, len);
  return chppAppErrorToChreError(rxHeader->error);
}

struct ChppAppHeader *chppAllocNotification(uint8_t type, size_t len) {
  CHPP_ASSERT(len >= sizeof(struct ChppAppHeader));
  CHPP_ASSERT(type == CHPP_MESSAGE_TYPE_CLIENT_NOTIFICATION ||
              type == CHPP_MESSAGE_TYPE_SERVICE_NOTIFICATION);

  struct ChppAppHeader *notification = chppMalloc(len);
  if (notification != NULL) {
    notification->type = type;
    notification->handle = CHPP_HANDLE_NONE;
    notification->transaction = 0;
    notification->error = CHPP_APP_ERROR_NONE;
    notification->command = CHPP_APP_COMMAND_NONE;
  } else {
    CHPP_LOG_OOM();
  }
  return notification;
}

struct ChppAppHeader *chppAllocRequest(uint8_t type,
                                       struct ChppEndpointState *endpointState,
                                       size_t len) {
  CHPP_ASSERT(len >= sizeof(struct ChppAppHeader));
  CHPP_ASSERT(type == CHPP_MESSAGE_TYPE_CLIENT_REQUEST ||
              type == CHPP_MESSAGE_TYPE_SERVICE_REQUEST);
  CHPP_DEBUG_NOT_NULL(endpointState);

  struct ChppAppHeader *request = chppMalloc(len);
  if (request != NULL) {
    request->handle = endpointState->handle;
    request->type = type;
    request->transaction = endpointState->transaction;
    request->error = CHPP_APP_ERROR_NONE;
    request->command = CHPP_APP_COMMAND_NONE;

    endpointState->transaction++;
  } else {
    CHPP_LOG_OOM();
  }
  return request;
}

struct ChppAppHeader *chppAllocResponse(
    const struct ChppAppHeader *requestHeader, size_t len) {
  CHPP_ASSERT(len >= sizeof(struct ChppAppHeader));
  CHPP_DEBUG_NOT_NULL(requestHeader);
  uint8_t type = requestHeader->type;
  CHPP_ASSERT(type == CHPP_MESSAGE_TYPE_CLIENT_REQUEST ||
              type == CHPP_MESSAGE_TYPE_SERVICE_REQUEST);

  struct ChppAppHeader *response = chppMalloc(len);
  if (response != NULL) {
    *response = *requestHeader;
    response->type = type == CHPP_MESSAGE_TYPE_CLIENT_REQUEST
                         ? CHPP_MESSAGE_TYPE_SERVICE_RESPONSE
                         : CHPP_MESSAGE_TYPE_CLIENT_RESPONSE;
    response->error = CHPP_APP_ERROR_NONE;
  } else {
    CHPP_LOG_OOM();
  }
  return response;
}

void chppTimestampIncomingRequest(struct ChppIncomingRequestState *inReqState,
                                  const struct ChppAppHeader *requestHeader) {
  CHPP_DEBUG_NOT_NULL(inReqState);
  CHPP_DEBUG_NOT_NULL(requestHeader);
  if (inReqState->responseTimeNs == CHPP_TIME_NONE &&
      inReqState->requestTimeNs != CHPP_TIME_NONE) {
    CHPP_LOGE("RX dupe req t=%" PRIu64,
              inReqState->requestTimeNs / CHPP_NSEC_PER_MSEC);
  }
  inReqState->requestTimeNs = chppGetCurrentTimeNs();
  inReqState->responseTimeNs = CHPP_TIME_NONE;
  inReqState->transaction = requestHeader->transaction;
}

void chppTimestampOutgoingRequest(struct ChppAppState *appState,
                                  struct ChppOutgoingRequestState *outReqState,
                                  const struct ChppAppHeader *requestHeader,
                                  uint64_t timeoutNs) {
  CHPP_DEBUG_NOT_NULL(appState);
  CHPP_DEBUG_NOT_NULL(outReqState);
  CHPP_DEBUG_NOT_NULL(requestHeader);
  enum ChppMessageType msgType = requestHeader->type;
  enum ChppEndpointType endpointType =
      msgType == CHPP_MESSAGE_TYPE_CLIENT_REQUEST ? CHPP_ENDPOINT_CLIENT
                                                  : CHPP_ENDPOINT_SERVICE;

  CHPP_ASSERT(msgType == CHPP_MESSAGE_TYPE_CLIENT_REQUEST ||
              msgType == CHPP_MESSAGE_TYPE_SERVICE_REQUEST);

  // Hold the mutex to avoid concurrent read of a partially modified outReqState
  // structure by the RX thread
  chppMutexLock(&appState->transportContext->mutex);

  if (outReqState->requestState == CHPP_REQUEST_STATE_REQUEST_SENT) {
    CHPP_LOGE("Dupe req ID=%" PRIu8 " existing ID=%" PRIu8 " from t=%" PRIu64,
              requestHeader->transaction, outReqState->transaction,
              outReqState->requestTimeNs / CHPP_NSEC_PER_MSEC);

    // Clear a possible pending timeout from the previous request
    outReqState->responseTimeNs = CHPP_TIME_MAX;
    chppRecalculateNextTimeout(appState, endpointType);
  }

  outReqState->requestTimeNs = chppGetCurrentTimeNs();
  outReqState->requestState = CHPP_REQUEST_STATE_REQUEST_SENT;
  outReqState->transaction = requestHeader->transaction;

  uint64_t *nextRequestTimeoutNs =
      getNextRequestTimeoutNs(appState, endpointType);

  if (timeoutNs == CHPP_REQUEST_TIMEOUT_INFINITE) {
    outReqState->responseTimeNs = CHPP_TIME_MAX;

  } else {
    outReqState->responseTimeNs = timeoutNs + outReqState->requestTimeNs;

    *nextRequestTimeoutNs =
        MIN(*nextRequestTimeoutNs, outReqState->responseTimeNs);
  }

  chppMutexUnlock(&appState->transportContext->mutex);

  CHPP_LOGD("Timestamp req ID=%" PRIu8 " at t=%" PRIu64 " timeout=%" PRIu64
            " (requested=%" PRIu64 "), next timeout=%" PRIu64,
            outReqState->transaction,
            outReqState->requestTimeNs / CHPP_NSEC_PER_MSEC,
            outReqState->responseTimeNs / CHPP_NSEC_PER_MSEC,
            timeoutNs / CHPP_NSEC_PER_MSEC,
            *nextRequestTimeoutNs / CHPP_NSEC_PER_MSEC);
}

bool chppTimestampIncomingResponse(struct ChppAppState *appState,
                                   struct ChppOutgoingRequestState *outReqState,
                                   const struct ChppAppHeader *responseHeader) {
  CHPP_DEBUG_NOT_NULL(appState);
  CHPP_DEBUG_NOT_NULL(outReqState);
  CHPP_DEBUG_NOT_NULL(responseHeader);

  uint8_t type = responseHeader->type;

  CHPP_ASSERT(type == CHPP_MESSAGE_TYPE_CLIENT_RESPONSE ||
              type == CHPP_MESSAGE_TYPE_SERVICE_RESPONSE);

  bool success = false;
  uint64_t responseTime = chppGetCurrentTimeNs();

  switch (outReqState->requestState) {
    case CHPP_REQUEST_STATE_NONE: {
      CHPP_LOGE("Resp with no req t=%" PRIu64,
                responseTime / CHPP_NSEC_PER_MSEC);
      break;
    }

    case CHPP_REQUEST_STATE_RESPONSE_RCV: {
      CHPP_LOGE("Extra resp at t=%" PRIu64 " for req t=%" PRIu64,
                responseTime / CHPP_NSEC_PER_MSEC,
                outReqState->requestTimeNs / CHPP_NSEC_PER_MSEC);
      break;
    }

    case CHPP_REQUEST_STATE_RESPONSE_TIMEOUT: {
      CHPP_LOGE("Late resp at t=%" PRIu64 " for req t=%" PRIu64,
                responseTime / CHPP_NSEC_PER_MSEC,
                outReqState->requestTimeNs / CHPP_NSEC_PER_MSEC);
      break;
    }

    case CHPP_REQUEST_STATE_REQUEST_SENT: {
      if (responseHeader->transaction != outReqState->transaction) {
        CHPP_LOGE("Invalid resp ID=%" PRIu8 " at t=%" PRIu64
                  " expected=%" PRIu8,
                  responseHeader->transaction,
                  responseTime / CHPP_NSEC_PER_MSEC, outReqState->transaction);
      } else {
        outReqState->requestState = (responseTime > outReqState->responseTimeNs)
                                        ? CHPP_REQUEST_STATE_RESPONSE_TIMEOUT
                                        : CHPP_REQUEST_STATE_RESPONSE_RCV;
        success = true;

        CHPP_LOGD(
            "Timestamp resp ID=%" PRIu8 " req t=%" PRIu64 " resp t=%" PRIu64
            " timeout t=%" PRIu64 " (RTT=%" PRIu64 ", timeout = %s)",
            outReqState->transaction,
            outReqState->requestTimeNs / CHPP_NSEC_PER_MSEC,
            responseTime / CHPP_NSEC_PER_MSEC,
            outReqState->responseTimeNs / CHPP_NSEC_PER_MSEC,
            (responseTime - outReqState->requestTimeNs) / CHPP_NSEC_PER_MSEC,
            (responseTime > outReqState->responseTimeNs) ? "yes" : "no");
      }
      break;
    }

    default: {
      CHPP_DEBUG_ASSERT_LOG(false, "Invalid req state");
    }
  }

  if (success) {
    // When the received request is the next one that was expected
    // to timeout we need to recompute the timeout considering the
    // other pending requests.
    enum ChppEndpointType endpointType =
        type == CHPP_MESSAGE_TYPE_SERVICE_RESPONSE ? CHPP_ENDPOINT_CLIENT
                                                   : CHPP_ENDPOINT_SERVICE;
    if (outReqState->responseTimeNs ==
        *getNextRequestTimeoutNs(appState, endpointType)) {
      chppRecalculateNextTimeout(appState, endpointType);
    }
    outReqState->responseTimeNs = responseTime;
  }
  return success;
}

uint64_t chppTimestampOutgoingResponse(
    struct ChppIncomingRequestState *inReqState) {
  CHPP_DEBUG_NOT_NULL(inReqState);

  uint64_t previousResponseTime = inReqState->responseTimeNs;
  inReqState->responseTimeNs = chppGetCurrentTimeNs();
  return previousResponseTime;
}

bool chppSendTimestampedResponseOrFail(
    struct ChppAppState *appState, struct ChppIncomingRequestState *inReqState,
    void *buf, size_t len) {
  CHPP_DEBUG_NOT_NULL(appState);
  CHPP_DEBUG_NOT_NULL(inReqState);
  CHPP_DEBUG_NOT_NULL(buf);
  uint64_t previousResponseTime = chppTimestampOutgoingResponse(inReqState);

  if (inReqState->requestTimeNs == CHPP_TIME_NONE) {
    CHPP_LOGE("TX response w/ no req t=%" PRIu64,
              inReqState->responseTimeNs / CHPP_NSEC_PER_MSEC);

  } else if (previousResponseTime != CHPP_TIME_NONE) {
    CHPP_LOGW("TX additional response t=%" PRIu64 " for req t=%" PRIu64,
              inReqState->responseTimeNs / CHPP_NSEC_PER_MSEC,
              inReqState->requestTimeNs / CHPP_NSEC_PER_MSEC);

  } else {
    CHPP_LOGD("Sending initial response at t=%" PRIu64
              " for request at t=%" PRIu64 " (RTT=%" PRIu64 ")",
              inReqState->responseTimeNs / CHPP_NSEC_PER_MSEC,
              inReqState->requestTimeNs / CHPP_NSEC_PER_MSEC,
              (inReqState->responseTimeNs - inReqState->requestTimeNs) /
                  CHPP_NSEC_PER_MSEC);
  }

  return chppEnqueueTxDatagramOrFail(appState->transportContext, buf, len);
}

bool chppSendTimestampedRequestOrFail(
    struct ChppEndpointState *endpointState,
    struct ChppOutgoingRequestState *outReqState, void *buf, size_t len,
    uint64_t timeoutNs) {
  CHPP_DEBUG_NOT_NULL(outReqState);
  CHPP_DEBUG_NOT_NULL(buf);
  CHPP_ASSERT(len >= sizeof(struct ChppAppHeader));

  if (timeoutNs < CHPP_TRANSPORT_TX_TIMEOUT_NS) {
    // The app layer sits above the transport layer.
    // Request timeout (app layer) should be longer than the transport timeout.
    CHPP_LOGW("Request timeout (%" PRIu64
              "ns) should be longer than the transport timeout (%" PRIu64 "ns)",
              timeoutNs, (uint64_t)CHPP_TRANSPORT_TX_TIMEOUT_NS);
  }

  chppTimestampOutgoingRequest(endpointState->appContext, outReqState, buf,
                               timeoutNs);
  endpointState->syncResponse.ready = false;

  bool success = chppEnqueueTxDatagramOrFail(
      endpointState->appContext->transportContext, buf, len);

  // Failure to enqueue a TX datagram means that a request was known to be not
  // transmitted. We explicitly set requestState to be in the NONE state, so
  // that unintended app layer timeouts do not occur.
  if (!success) {
    outReqState->requestState = CHPP_REQUEST_STATE_NONE;
  }

  return success;
}

bool chppWaitForResponseWithTimeout(
    struct ChppSyncResponse *syncResponse,
    struct ChppOutgoingRequestState *outReqState, uint64_t timeoutNs) {
  CHPP_DEBUG_NOT_NULL(syncResponse);
  CHPP_DEBUG_NOT_NULL(outReqState);

  bool result = true;

  chppMutexLock(&syncResponse->mutex);

  while (result && !syncResponse->ready) {
    result = chppConditionVariableTimedWait(&syncResponse->condVar,
                                            &syncResponse->mutex, timeoutNs);
  }
  if (!syncResponse->ready) {
    outReqState->requestState = CHPP_REQUEST_STATE_RESPONSE_TIMEOUT;
    CHPP_LOGE("Response timeout after %" PRIu64 " ms",
              timeoutNs / CHPP_NSEC_PER_MSEC);
    result = false;
  }

  chppMutexUnlock(&syncResponse->mutex);

  return result;
}

struct ChppEndpointState *getRegisteredEndpointState(
    struct ChppAppState *appState, uint8_t index, enum ChppEndpointType type) {
  CHPP_DEBUG_NOT_NULL(appState);
  CHPP_DEBUG_ASSERT(index < getRegisteredEndpointCount(appState, type));

  return type == CHPP_ENDPOINT_CLIENT
             ? appState->registeredClientStates[index]
             : appState->registeredServiceStates[index];
}

uint16_t getRegisteredEndpointOutReqCount(struct ChppAppState *appState,
                                          uint8_t index,
                                          enum ChppEndpointType type) {
  CHPP_DEBUG_NOT_NULL(appState);
  CHPP_DEBUG_ASSERT(index < getRegisteredEndpointCount(appState, type));

  return type == CHPP_ENDPOINT_CLIENT
             ? appState->registeredClients[index]->outReqCount
             : appState->registeredServices[index]->outReqCount;
}

uint8_t getRegisteredEndpointCount(struct ChppAppState *appState,
                                   enum ChppEndpointType type) {
  return type == CHPP_ENDPOINT_CLIENT ? appState->registeredClientCount
                                      : appState->registeredServiceCount;
}

void chppRecalculateNextTimeout(struct ChppAppState *appState,
                                enum ChppEndpointType type) {
  CHPP_DEBUG_NOT_NULL(appState);

  uint64_t timeoutNs = CHPP_TIME_MAX;

  const uint8_t endpointCount = getRegisteredEndpointCount(appState, type);

  for (uint8_t endpointIdx = 0; endpointIdx < endpointCount; endpointIdx++) {
    uint16_t reqCount =
        getRegisteredEndpointOutReqCount(appState, endpointIdx, type);
    struct ChppEndpointState *endpointState =
        getRegisteredEndpointState(appState, endpointIdx, type);
    struct ChppOutgoingRequestState *reqStates = endpointState->outReqStates;
    for (uint16_t cmdIdx = 0; cmdIdx < reqCount; cmdIdx++) {
      struct ChppOutgoingRequestState *reqState = &reqStates[cmdIdx];

      if (reqState->requestState == CHPP_REQUEST_STATE_REQUEST_SENT) {
        timeoutNs = MIN(timeoutNs, reqState->responseTimeNs);
      }
    }
  }

  CHPP_LOGD("nextReqTimeout=%" PRIu64, timeoutNs / CHPP_NSEC_PER_MSEC);

  if (type == CHPP_ENDPOINT_CLIENT) {
    appState->nextClientRequestTimeoutNs = timeoutNs;
  } else {
    appState->nextServiceRequestTimeoutNs = timeoutNs;
  }
}

uint64_t *getNextRequestTimeoutNs(struct ChppAppState *appState,
                                  enum ChppEndpointType type) {
  return type == CHPP_ENDPOINT_CLIENT ? &appState->nextClientRequestTimeoutNs
                                      : &appState->nextServiceRequestTimeoutNs;
}

void chppCloseOpenRequests(struct ChppEndpointState *endpointState,
                           enum ChppEndpointType type, bool clearOnly) {
  CHPP_DEBUG_NOT_NULL(endpointState);

  bool recalcNeeded = false;

  struct ChppAppState *appState = endpointState->appContext;
  const uint8_t enpointIdx = endpointState->index;
  const uint16_t cmdCount =
      getRegisteredEndpointOutReqCount(appState, enpointIdx, type);

  for (uint16_t cmdIdx = 0; cmdIdx < cmdCount; cmdIdx++) {
    if (endpointState->outReqStates[cmdIdx].requestState ==
        CHPP_REQUEST_STATE_REQUEST_SENT) {
      recalcNeeded = true;

      CHPP_LOGE("Closing open req #%" PRIu16 " clear %d", cmdIdx, clearOnly);

      if (clearOnly) {
        endpointState->outReqStates[cmdIdx].requestState =
            CHPP_REQUEST_STATE_RESPONSE_TIMEOUT;
      } else {
        struct ChppAppHeader *response =
            chppMalloc(sizeof(struct ChppAppHeader));
        if (response == NULL) {
          CHPP_LOG_OOM();
        } else {
          // Simulate receiving a timeout response.
          response->handle = endpointState->handle;
          response->type = type == CHPP_ENDPOINT_CLIENT
                               ? CHPP_MESSAGE_TYPE_SERVICE_RESPONSE
                               : CHPP_MESSAGE_TYPE_CLIENT_RESPONSE;
          response->transaction =
              endpointState->outReqStates[cmdIdx].transaction;
          response->error = CHPP_APP_ERROR_TIMEOUT;
          response->command = cmdIdx;

          chppAppProcessRxDatagram(appState, (uint8_t *)response,
                                   sizeof(struct ChppAppHeader));
        }
      }
    }
  }
  if (recalcNeeded) {
    chppRecalculateNextTimeout(appState, type);
  }
}