/*
 * 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/clients/timesync.h"

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

#include "chpp/app.h"
#include "chpp/clients.h"
#include "chpp/clients/discovery.h"
#include "chpp/common/timesync.h"
#include "chpp/common/event_log.h"
#include "chpp/log.h"
#include "chpp/memory.h"
#include "chpp/time.h"
#include "chpp/transport.h"

/************************************************
 *  Private Definitions
 ***********************************************/

/**
 * Structure to maintain state for the Timesync client and its Request/Response
 * (RR) functionality.
 */
struct ChppTimesyncClientState {
  struct ChppEndpointState client;                // CHPP client state
  struct ChppOutgoingRequestState measureOffset;  // Request response state
  struct ChppTimesyncResult timesyncResult;  // Result of measureOffset
  struct ChppEventLog eventLog;
  uint64_t lastMeasurementTimeNs;  // The last time a timesync was started
  bool isOffsetClipping;  // If the offset was clipped on previous check
};

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

void chppTimesyncClientInit(struct ChppAppState *appState) {
  CHPP_LOGD("Timesync client init");
  CHPP_DEBUG_NOT_NULL(appState);

  appState->timesyncClientContext =
      chppMalloc(sizeof(struct ChppTimesyncClientState));
  CHPP_NOT_NULL(appState->timesyncClientContext);
  struct ChppTimesyncClientState *state = appState->timesyncClientContext;

  memset(state, 0, sizeof(struct ChppTimesyncClientState));

  chppEventLogInit(&state->eventLog, CHPP_EVENT_LOG_TIMESYNC_CAPACITY);

  state->client.appContext = appState;
  state->timesyncResult.error = CHPP_APP_ERROR_NONE;

  chppClientInit(&state->client, CHPP_HANDLE_TIMESYNC);
  state->timesyncResult.error = CHPP_APP_ERROR_UNSPECIFIED;
  state->client.openState = CHPP_OPEN_STATE_OPENED;

  chppLogEvent(&state->eventLog, CHPP_TIMESYNC_CLIENT_INIT);
}

void chppTimesyncClientDeinit(struct ChppAppState *appState) {
  CHPP_LOGD("Timesync client deinit");
  CHPP_DEBUG_NOT_NULL(appState);
  CHPP_NOT_NULL(appState->timesyncClientContext);
  chppClientDeinit(&appState->timesyncClientContext->client);
  chppEventLogDeinit(&appState->timesyncClientContext->eventLog);
  CHPP_FREE_AND_NULLIFY(appState->timesyncClientContext);
}

void chppTimesyncClientReset(struct ChppAppState *appState) {
  CHPP_LOGI("Timesync client reset");
  CHPP_DEBUG_NOT_NULL(appState);
  struct ChppTimesyncClientState *state = appState->timesyncClientContext;
  CHPP_NOT_NULL(state);

  state->timesyncResult.error = CHPP_APP_ERROR_NONE;
  state->timesyncResult.offsetNs = 0;
  state->timesyncResult.rttNs = 0;
  state->timesyncResult.measurementTimeNs = 0;
  state->lastMeasurementTimeNs = 0;

  chppLogEvent(&state->eventLog, CHPP_TIMESYNC_CLIENT_RESET);
}

bool chppDispatchTimesyncServiceResponse(struct ChppAppState *appState,
                                         const uint8_t *buf, size_t len) {
  CHPP_LOGD("Timesync client dispatch service response");
  CHPP_DEBUG_NOT_NULL(appState);
  struct ChppTimesyncClientState *state = appState->timesyncClientContext;
  CHPP_NOT_NULL(state);
  CHPP_NOT_NULL(buf);

  if (len < sizeof(struct ChppTimesyncResponse)) {
    CHPP_LOGE("Timesync resp short len=%" PRIuSIZE, len);
    state->timesyncResult.error = CHPP_APP_ERROR_INVALID_LENGTH;
    return false;
  }

  const struct ChppTimesyncResponse *response =
      (const struct ChppTimesyncResponse *)buf;
  if (chppTimestampIncomingResponse(state->client.appContext,
                                    &state->measureOffset, &response->header)) {
    state->timesyncResult.rttNs = state->measureOffset.responseTimeNs -
                                  state->measureOffset.requestTimeNs;
    int64_t offsetNs =
        (int64_t)(response->timeNs - state->measureOffset.responseTimeNs);
    int64_t offsetChangeNs = offsetNs - state->timesyncResult.offsetNs;

    int64_t clippedOffsetChangeNs = offsetChangeNs;
    if (state->timesyncResult.offsetNs != 0) {
      clippedOffsetChangeNs = MIN(clippedOffsetChangeNs,
                                  (int64_t)CHPP_CLIENT_TIMESYNC_MAX_CHANGE_NS);
      clippedOffsetChangeNs = MAX(clippedOffsetChangeNs,
                                  -(int64_t)CHPP_CLIENT_TIMESYNC_MAX_CHANGE_NS);
    } else {
      CHPP_LOGI("First timesync offset=%" PRId64 "ms at t=%" PRIu64,
                offsetNs / (int64_t)CHPP_NSEC_PER_MSEC,
                state->measureOffset.responseTimeNs / CHPP_NSEC_PER_MSEC);
      chppLogEventInt64(&state->eventLog, CHPP_TIMESYNC_CLIENT_FIRST_OFFSET,
                         offsetNs);
    }

    bool clippingStatusChanged = false;
    state->timesyncResult.offsetNs += clippedOffsetChangeNs;

    if (offsetChangeNs != clippedOffsetChangeNs) {
      if (!state->isOffsetClipping) {
        CHPP_LOGI("Timesync offset newly required clipping");
        state->isOffsetClipping = true;
        clippingStatusChanged = true;
        chppLogEvent(&state->eventLog,
                     CHPP_TIMESYNC_CLIENT_START_OFFSET_CLIPPING);
      }
      CHPP_LOGW("Drift=%" PRId64 " clipped to %" PRId64 " at t=%" PRIu64,
                offsetChangeNs / (int64_t)CHPP_NSEC_PER_MSEC,
                clippedOffsetChangeNs / (int64_t)CHPP_NSEC_PER_MSEC,
                state->measureOffset.responseTimeNs / CHPP_NSEC_PER_MSEC);
    } else {
      if (state->isOffsetClipping) {
        CHPP_LOGI("Timesync offset no longer requires clipping");
        state->isOffsetClipping = false;
        clippingStatusChanged = true;
        chppLogEvent(&state->eventLog,
                     CHPP_TIMESYNC_CLIENT_END_OFFSET_CLIPPING);
      }
      state->timesyncResult.measurementTimeNs =
          state->measureOffset.responseTimeNs;
    }

    state->timesyncResult.error = CHPP_APP_ERROR_NONE;

    if (clippingStatusChanged) {
      CHPP_LOGI("Timesync RTT=%" PRIu64 " correction=%" PRId64
                " offset=%" PRId64 " t=%" PRIu64,
                state->timesyncResult.rttNs / CHPP_NSEC_PER_MSEC,
                clippedOffsetChangeNs / (int64_t)CHPP_NSEC_PER_MSEC,
                offsetNs / (int64_t)CHPP_NSEC_PER_MSEC,
                state->timesyncResult.measurementTimeNs / CHPP_NSEC_PER_MSEC);
    }
  }

  return true;
}

bool chppTimesyncMeasureOffset(struct ChppAppState *appState) {
  const uint64_t kTimeoutNs = 5 * CHPP_NSEC_PER_SEC;
  bool result = false;
  CHPP_LOGD("Measuring timesync t=%" PRIu64,
            chppGetCurrentTimeNs() / CHPP_NSEC_PER_MSEC);
  CHPP_DEBUG_NOT_NULL(appState);
  struct ChppTimesyncClientState *state = appState->timesyncClientContext;
  CHPP_NOT_NULL(state);
  uint64_t nowNs = chppGetCurrentTimeNs();
  if (state->timesyncResult.error == CHPP_APP_ERROR_BUSY) {
    if (nowNs < state->lastMeasurementTimeNs + kTimeoutNs) {
      CHPP_LOGE("Rejecting timesync request: in progress");
      return false;
    } else {
      CHPP_LOGW("Last timesync (%" PRIu64 " ms ago) timed out",
                (nowNs - state->lastMeasurementTimeNs) / CHPP_NSEC_PER_MSEC);
    }
  }

  state->timesyncResult.error =
      CHPP_APP_ERROR_BUSY;  // A measurement is in progress

  struct ChppAppHeader *request = chppAllocClientRequestCommand(
      &state->client, CHPP_TIMESYNC_COMMAND_GETTIME);
  size_t requestLen = sizeof(*request);

  if (request == NULL) {
    state->timesyncResult.error = CHPP_APP_ERROR_OOM;
    CHPP_LOG_OOM();

  // We use an infinite timeout here because timeouts are not well-supported for
  // predefined clients in CHPP today. An opportunistic timeout will be used
  // using the lastMeasurementTimeNs check above.
  } else if (!chppClientSendTimestampedRequestOrFail(
                 &state->client, &state->measureOffset, request, requestLen,
                 CHPP_REQUEST_TIMEOUT_INFINITE)) {
    state->timesyncResult.error = CHPP_APP_ERROR_UNSPECIFIED;

  } else {
    state->lastMeasurementTimeNs = nowNs;
    result = true;
  }

  return result;
}

int64_t chppTimesyncGetOffset(struct ChppAppState *appState,
                              uint64_t maxTimesyncAgeNs) {
  CHPP_DEBUG_NOT_NULL(appState);
  struct ChppTimesyncClientState *state = appState->timesyncClientContext;
  CHPP_DEBUG_NOT_NULL(state);

  bool timesyncNeverDone = state->timesyncResult.offsetNs == 0;
  bool timesyncIsStale =
      chppGetCurrentTimeNs() - state->timesyncResult.measurementTimeNs >
      maxTimesyncAgeNs;

  if (timesyncNeverDone || timesyncIsStale) {
    chppTimesyncMeasureOffset(appState);
  } else {
    CHPP_LOGD("No need to timesync at t~=%" PRIu64 "offset=%" PRId64,
              chppGetCurrentTimeNs() / CHPP_NSEC_PER_MSEC,
              state->timesyncResult.offsetNs / (int64_t)CHPP_NSEC_PER_MSEC);
  }

  return state->timesyncResult.offsetNs;
}

const struct ChppTimesyncResult *chppTimesyncGetResult(
    struct ChppAppState *appState) {
  CHPP_DEBUG_NOT_NULL(appState);
  CHPP_DEBUG_NOT_NULL(appState->timesyncClientContext);
  return &appState->timesyncClientContext->timesyncResult;
}

const struct ChppEventLog *chppTimesyncGetEventLog(struct ChppAppState *appState) {
  CHPP_DEBUG_NOT_NULL(appState);
  CHPP_DEBUG_NOT_NULL(appState->timesyncClientContext);
  return &appState->timesyncClientContext->eventLog;
}


const char* chppGetEventLogName(enum ChppTimesyncClientEventType eventType) {
  switch (eventType) {
    case CHPP_TIMESYNC_CLIENT_INIT:
      return "CHPP_TIMESYNC_CLIENT_INIT";
    case CHPP_TIMESYNC_CLIENT_RESET:
      return "CHPP_TIMESYNC_CLIENT_RESET";
    case CHPP_TIMESYNC_CLIENT_FIRST_OFFSET:
      return "CHPP_TIMESYNC_CLIENT_FIRST_OFFSET";
    case CHPP_TIMESYNC_CLIENT_START_OFFSET_CLIPPING:
      return "CHPP_TIMESYNC_CLIENT_START_OFFSET_CLIPPING";
    case CHPP_TIMESYNC_CLIENT_END_OFFSET_CLIPPING:
      return "CHPP_TIMESYNC_CLIENT_END_OFFSET_CLIPPING";
    default:
      return "UNKNOWN";
  }
}