/**
 * @file teec_common_tbase.c
 * @brief Common client interface implementation based on t-base client API
 * @author Iaroslav Makarchuk (i.makarchuk@samsung.com)
 * @date Created Oct 3, 2016
 * @par In Samsung Ukraine R&D Center (SURC) under a contract between
 * @par LLC "Samsung Electronics Ukraine Company" (Kiev, Ukraine) and
 * @par "Samsung Elecrtronics Co", Ltd (Seoul, Republic of Korea)
 * @par Copyright: (c) Samsung Electronics Co, Ltd 2015. All rights reserved.
 *
 * This software is proprietary of Samsung Electronics.
 * No part of this software, either material or conceptual may be copied
 * or distributed, transmitted, transcribed, stored in a retrieval system
 * or translated into any human or computer language in any form by any means,
 * electronic, mechanical, manual or otherwise, or disclosed to third parties
 * without the express written permission of Samsung Electronics.
 */

#include "teec_common.h"

#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>
#include <sys/stat.h>

#include <teec_callback_handler.h>
#include <MobiCoreDriverApi.h>
#include <protocol.h>
#include <teec_param_utils.h>
#include <tees_log.h>

#include <stdio.h>
#include <time.h>
#include <errno.h>
#include <string.h>

#define DEVICE_ID  MC_DEVICE_ID_DEFAULT

enum {
  MB_MC_OPEN_DEVICE_NUM_TRIES = 15,        /**< 15 * 0.5 = 7.5 sec */
  MB_GEN_FAIL_RETRY_WAIT_TIME = 250000000, /**< Wait time between retries
                                            *   if GENERAL_FAILURE error happens
                                            * */
  MB_MC_OPEN_DEVICE_WAIT_TIME = 500000000, /**< nanoseconds */
  MB_GET_PROP_VALUE_MAX = 96,
  MB_MAX_NUM_TRY_FOR_FUNC = 5              /**< Number of retries if
                                            * GENERAL_FAILURE error happens */

};

uint32_t g_sessions_count = 0;

typedef struct TbaseSessionMapInfoStruct {
  void *src_address;
  mcBulkMap_t map_info;
} TbaseSessionMapInfo;

typedef struct TbaseSessionStruct {
  mcSessionHandle_t ta_session;
  ProtocolCmd *tci_buffer;
  PersObjectCmd *persistent_object;
  TbaseSessionMapInfo maps[PARAMS_NUM];
  void *parent_context;
  TEEC_UUID uuid;
} TbaseSession;

static void TbaseInitSessionMaps(TbaseSession *session) {
  if (session) {
    memset(session->maps, 0x00, sizeof(session->maps));
  }
}

static void TbaseCleanSessionMaps(TbaseSession *session) {
  int i = 0;

  if (session) {
    for (i = 0; i < PARAMS_NUM; i++) {
      if (session->maps[i].src_address) {
        mcUnmap(&(session->ta_session),
                session->maps[i].src_address, &(session->maps[i].map_info));
      }
    }
    memset(session->maps, 0x00, sizeof(session->maps));
  }
}

static TEEC_Result TeecUuidToMcUuid(const TEEC_UUID *uuid, mcUuid_t *mc_uuid) {
  uint32_t i = 0, j = 0;
  TEEC_Result res = TEEC_SUCCESS;
  uint8_t *uuid_data_ptr = (uint8_t *)mc_uuid;

  if (!uuid || !mc_uuid) {
    res = TEEC_ERROR_BAD_PARAMETERS;
    goto exit;
  }

  uuid_data_ptr[i++] = (uuid->timeLow & 0xFF000000) >> 24;
  uuid_data_ptr[i++] = (uuid->timeLow & 0xFF0000) >> 16;
  uuid_data_ptr[i++] = (uuid->timeLow & 0xFF00) >> 8;
  uuid_data_ptr[i++] = (uuid->timeLow & 0xFF);

  uuid_data_ptr[i++] = (uuid->timeMid & 0xFF00) >> 8;
  uuid_data_ptr[i++] = (uuid->timeMid & 0xFF);

  uuid_data_ptr[i++] = (uuid->timeHiAndVersion & 0xFF00) >> 8;
  uuid_data_ptr[i++] = (uuid->timeHiAndVersion & 0xFF);

  for (j = 0; j < sizeof(uuid->clockSeqAndNode); j++) {
    uuid_data_ptr[i++] = uuid->clockSeqAndNode[j];
  }

exit:
  return res;
}

TEEC_Result TbaseSendCommand(void *ta_session,
                             uint32_t command_id,
                             TEEC_Operation *operation,
                             uint32_t *return_origin,
                             int32_t timeout) {
  mcResult_t mc_res = MC_DRV_OK;
  TEEC_Result ret = TEEC_ERROR_BAD_PARAMETERS;
  TbaseSession *tbase_session = (TbaseSession *)ta_session;
  PersObjectCmd *command = NULL;

  if (!tbase_session) {
    goto exit;
  }

   // tci_buffer is supposed to be not NULL.
  tbase_session->tci_buffer->return_origin = TEEC_ORIGIN_API;

  tbase_session->tci_buffer->cmd_id = command_id;
  tbase_session->tci_buffer->param_types = 0;
  tbase_session->tci_buffer->cmd_ret = TEEC_ERROR_COMMUNICATION;

  TbaseInitSessionMaps(tbase_session);

  if (operation) {
    tbase_session->tci_buffer->param_types = operation->paramTypes;
    ret = FillCommandArgs(tbase_session->tci_buffer, operation, tbase_session);
    PtrToU64(operation,&tbase_session->tci_buffer->teec_oper);
    if (TEEC_SUCCESS != ret) {
      goto exit;
    }
  }

  do {
    /* Clear PO command! */
    tbase_session->persistent_object->cmd_id = 0x00;

    /* Call */
    mc_res = mcNotify(&tbase_session->ta_session);

    // Return origin: "COMMS" to handle several conditions below.
    tbase_session->tci_buffer->return_origin = TEEC_ORIGIN_COMMS;

    if (mc_res != MC_DRV_OK) {
      ret = TEEC_ERROR_COMMUNICATION;
      goto exit;
    }

    mc_res = mcWaitNotification(&tbase_session->ta_session, timeout);

    if (mc_res != MC_DRV_OK) {
      /* Workaround(i.makarchuk): MC_DRV_INFO_NOTIFICATION is returned when TA
       * exits from the main loop right after tlApiNotify(). In the most cases
       * this is a TEE_Panic(), so set proper return value here.
       */ 
      ret = (mc_res == MC_DRV_INFO_NOTIFICATION ?
             TEEC_ERROR_TARGET_DEAD : TEEC_ERROR_COMMUNICATION);
      goto exit;
    }

    // Return origin: "TRUSTED_APP" to handle PO command emulation.
    tbase_session->tci_buffer->return_origin = TEEC_ORIGIN_TRUSTED_APP;

    command = tbase_session->persistent_object;
    // We need intermediate variables to avoid compilation warning when passing
    // a pointer to the field of packed structure.
    uint32_t data_len = command->data_len;
    U64 teec_oper = command->teec_oper;

    if (command->cmd_id) {
      tbase_session->tci_buffer->cmd_ret = (uint32_t)MbHandleSWdCommand(
                                            command->cmd_id,
                                            &tbase_session->uuid,
                                            command->id,
                                            command->id_len,
                                            command->data,
                                            &data_len,
                                            &teec_oper);
      command->data_len = data_len;
      command->teec_oper = teec_oper;
    }
  } while (command->cmd_id);

  // Return origin: "TEE" or "TRUSTED_APP" after communication with TEE.

  /* On Success: */
  ret = tbase_session->tci_buffer->cmd_ret;

  if (TEEC_SUCCESS == ret && operation) {
    ret = FillOperationArgs(operation, tbase_session->tci_buffer);
    if (TEEC_SUCCESS != ret) {
      tbase_session->tci_buffer->return_origin = TEEC_ORIGIN_API;
    }
  }

exit:
  if (return_origin && tbase_session) {
    // tci_buffer is supposed to be not NULL.
    *return_origin = tbase_session->tci_buffer->return_origin;
  }

  TbaseCleanSessionMaps(tbase_session);
  return ret;
}

TEEC_Result TbaseUnloadTA(void *ta_session) {
  mcResult_t mc_res;
  TEEC_Result status = TEEC_SUCCESS;
  TbaseSession *session = (TbaseSession *)ta_session;

  if (!ta_session) {
    status = TEEC_ERROR_BAD_PARAMETERS;
    goto exit;
  }

  mc_res = mcCloseSession(&(session->ta_session));

  if (mc_res != MC_DRV_OK) {
    status = TEEC_ERROR_GENERIC;
  }

  free(session->tci_buffer);
  session->tci_buffer = NULL;
  session->persistent_object = NULL;

  if (g_sessions_count) {
    g_sessions_count--;
  }

  if (g_sessions_count < 1) {
    mc_res = mcCloseDevice(DEVICE_ID);
    if (mc_res != MC_DRV_OK) {
      status = TEEC_ERROR_GENERIC;
    }
  }

exit:
  return status;
}

static void MbSleep(long nsec) {
  struct timespec tim, tim2;

  tim.tv_sec = 0;
  tim.tv_nsec = nsec;

  if (nanosleep(&tim, &tim2) < 0) {
    TEES_LOG(TEES_LOG_LEVEL_ERROR, "Nanosleep system call failed.\n");
  }
}

/*
 * @brief Open a new connection to a t-base device and to try many times with
 * timeout.
 * @details The function is wrapper of mcOpenDevice from tbase SDK.
 * It uses avoid kernel panic, because tbase SDK has bug when it was used
 * open/close device
 * in stress test.
 * @return MC_DRV_OK if operation has been successfully completed, otherwise it
 * has been failed.
 */
static mcResult_t OpenDeviceEx() {
  uint32_t elapsed_tries = 0;
  mcResult_t ret = MC_DRV_ERR_INVALID_OPERATION;

  do {
    ret = mcOpenDevice(DEVICE_ID);

    if (MC_DRV_OK != ret) {
      TEES_LOG(TEES_LOG_LEVEL_DEBUG, "Could not open device: %d.\n", ret);

      ret = mcCloseDevice(DEVICE_ID);
      if (MC_DRV_OK != ret) {
        TEES_LOG(TEES_LOG_LEVEL_DEBUG, "Close Mobicore device failed: %d.\n",
                 ret);
      }

      MbSleep(MB_MC_OPEN_DEVICE_WAIT_TIME);
      elapsed_tries++;
    }
  } while ((ret != MC_DRV_OK) && (elapsed_tries < MB_MC_OPEN_DEVICE_NUM_TRIES));

  if (MC_DRV_OK != ret) {
    TEES_LOG(TEES_LOG_LEVEL_WARNING, "Error opening device: 0x%08X.\n", ret);
  }

  return ret;
}

TEEC_Result TbaseLoadTA(void *ta_session, const TEEC_UUID *uuid) {
  mcResult_t mc_res;
  TEEC_Result status = TEEC_SUCCESS;
  mcUuid_t mc_uuid;
  TbaseSession *session = (TbaseSession *)ta_session;

  if (!ta_session || !uuid) {
    status = TEEC_ERROR_BAD_PARAMETERS;
    goto exit;
  }

  if (g_sessions_count < 1) {
    mc_res = OpenDeviceEx();

    if (MC_DRV_OK != mc_res) {
      status = TEEC_ERROR_GENERIC;
      goto cleanup;
    }
  }

  session->tci_buffer = (ProtocolCmd *)malloc(sizeof(ProtocolCmd) +
                                              sizeof(PersObjectCmd));

  if (!session->tci_buffer) {
    status = TEEC_ERROR_OUT_OF_MEMORY;
    goto cleanup;
  }

  session->persistent_object =
    (PersObjectCmd *)((uint8_t *)session->tci_buffer +
                      sizeof(ProtocolCmd));

  /* Simple conversion from */
  status = TeecUuidToMcUuid(uuid, &mc_uuid);
  if (TEEC_SUCCESS != status) {
    goto cleanup;
  }

  memcpy(&session->uuid, uuid, sizeof(TEEC_UUID));

  // Current SE policy forbides to use mcOpenTrustlet API function.
  // Thus enforcing to turn back to the old version of connection opening.
  mc_res = mcOpenSession(&(session->ta_session),
                         &mc_uuid,
                         (uint8_t *)session->tci_buffer,
                         sizeof(ProtocolCmd) + sizeof(PersObjectCmd));

  if (MC_DRV_OK != mc_res) {
    status = TEEC_ERROR_GENERIC;
  }

cleanup:
  if (TEEC_SUCCESS != status) {
    free(session->tci_buffer);
    session->tci_buffer = NULL;

    if (g_sessions_count < 1) {
      mcCloseDevice(DEVICE_ID);
    }
  } else {
    g_sessions_count++;
  }

exit:
  return status;
}

uint32_t TbaseMapBuffer(void *session, void *ptr, size_t size) {
  uint32_t ret = 0;
  TbaseSession *tbase_session = (TbaseSession *)session;
  int i = 0;

  if (!tbase_session) {
    goto exit;
  }

  for (i = 0; i < PARAMS_NUM; i++) {
    if (!tbase_session->maps[i].src_address) {
      break;
    }
  }

  if (i >= PARAMS_NUM) {
    goto exit;
  }

  if (MC_DRV_OK != mcMap(&(tbase_session->ta_session),
                         ptr,
                         size,
                         &(tbase_session->maps[i].map_info))) {
    goto exit;
  }

  tbase_session->maps[i].src_address = ptr;
  ret = (uint32_t)tbase_session->maps[i].map_info.sVirtualAddr;

exit:
  return ret;
}

PlatformClientImpl g_tbase_platform_impl = {
  TbaseLoadTA,
  TbaseUnloadTA,
  TbaseSendCommand,
  TbaseMapBuffer,
  NULL,
  NULL,
  NULL,
};

PlatformClientImpl *GetPlatformClientImpl(void) {
  return &g_tbase_platform_impl;
}

size_t GetPlatformClientImplSize(void) {
  return sizeof(TbaseSession);
}

void ClientImplSetParentContext(void *session, void *context) {
  TbaseSession *tbase_session = (TbaseSession *)session;

  if (tbase_session) {
    tbase_session->parent_context = context;
  }
}

void *ClientImplGetParentContext(void *session) {
  void *ret = NULL;
  TbaseSession *tbase_session = (TbaseSession *)session;

  if (tbase_session) {
    ret = tbase_session->parent_context;
  }

  return ret;
}
