/**
 * @file tee_client_api.c
 * @brief GlobalPlatform client API implementation
 * @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 <tee_client_api.h>

#include <errno.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <stdio.h>
#include <stdlib.h>

#include <protocol.h>
#include <teec_common.h>
#include <tees_log.h>
#include <string.h>

#include "mb_list.h"

#define ALLOCATED_MAGIC      0xFFA110CD
#define CONTEXT_INITIALIZED  0xA7A6A5A4

#define TEEC_PARAM_TYPE_GET(t, i)  (((t) >> (i * 4)) & 0xF)

#define TEEC_PARAM_TYPE_INVALID           0x00000004
#define TEEC_PARAM_TYPE_VALID_BEFORE_GAP  TEEC_MEMREF_TEMP_INOUT
#define TEEC_PARAM_TYPE_VALID_AFTER_GAP   TEEC_MEMREF_WHOLE


typedef struct SessionListStruct {
  struct list_head list;
  TEEC_Session *session;
} SessionList;

typedef struct TeecContextStruct {
  uint32_t initialized;
  SessionList session_list;
  uint32_t sessions_count;
} TeecContext;


static CancelRequestList g_CRList = { PTHREAD_MUTEX_INITIALIZER, 0, {{NULL, NULL}, NULL}, 0, 0 };

static TEEC_Result AddOperationToCRList(TEEC_Operation *oper) {
  TEEC_Result ret = TEEC_ERROR_BAD_PARAMETERS;

  if (!oper) {
    goto exit;
  }

  OperationList *node = (OperationList *)malloc(sizeof(OperationList));
  if (!node) {
    ret = TEEC_ERROR_OUT_OF_MEMORY;
    goto exit;
  }
  pthread_mutex_lock(&g_CRList.mtx);
  node->oper = oper;
  list_add_tail(&node->list, &g_CRList.operation_list.list);
  g_CRList.operations_count++;
  pthread_mutex_unlock(&g_CRList.mtx);
  ret = TEEC_SUCCESS;

exit:
  return ret;
}

static void RemoveOperationFromCRList(TEEC_Operation *oper) {
  struct list_head *pos = NULL, *q = NULL;

  if (!oper) {
    goto exit;
  }
  pthread_mutex_lock(&g_CRList.mtx);
  list_for_each_safe(pos, q, &g_CRList.operation_list.list)
  {
    OperationList *node = list_entry(pos, OperationList, list);

    if (node->oper == oper) {
      list_del(pos);
      free(node);
      g_CRList.operations_count--;
      break;
    }
  }
  pthread_mutex_unlock(&g_CRList.mtx);
exit:
  return;
}

uint8_t IsOperationInCRList(TEEC_Operation *oper) {
  struct list_head *pos = NULL, *q = NULL;
  uint8_t res = 0;

  if (!oper) {
    goto exit;
  }
  pthread_mutex_lock(&g_CRList.mtx);
  list_for_each_safe(pos, q, &g_CRList.operation_list.list) {
    OperationList *node = list_entry(pos, OperationList, list);

    if (node->oper == oper) {
      res = 1;
      break;
    }
  }
  pthread_mutex_unlock(&g_CRList.mtx);
exit:

  return res;
}

static void InitializeContextCRList() {
  pthread_mutex_lock(&g_CRList.mtx);
  if (CONTEXT_INITIALIZED != g_CRList.initialized) {
    INIT_LIST_HEAD(&g_CRList.operation_list.list);
    g_CRList.initialized = CONTEXT_INITIALIZED;
    g_CRList.operations_count = 0;
  }
  g_CRList.usage_count++;
  pthread_mutex_unlock(&g_CRList.mtx);

}

void FinalizeContextCRList() {
  struct list_head *pos = NULL, *q = NULL;

  pthread_mutex_lock(&g_CRList.mtx);
  g_CRList.usage_count--;
  if (!g_CRList.usage_count) {
    list_for_each_safe(pos, q, &g_CRList.operation_list.list) {
      OperationList *node = list_entry(pos, OperationList, list);

      list_del(pos);
      free(node);
      g_CRList.operations_count--;
      break;
    }
  }
  pthread_mutex_unlock(&g_CRList.mtx);

}


static inline int GetSessionsCount(TeecContext *context) {
  return context ? context->sessions_count : 0;
}

static TEEC_Result AddSessionToList(TeecContext *context,
                                    TEEC_Session *session) {
  TEEC_Result ret = TEEC_ERROR_OUT_OF_MEMORY;

  SessionList *node = (SessionList *)malloc(sizeof(SessionList));

  if (!node) {
    goto exit;
  }

  node->session = session;

  list_add_tail(&node->list, &context->session_list.list);

  ret = TEEC_SUCCESS;
  context->sessions_count++;
exit:
  return ret;
}

static void RemoveSessionFromList(TeecContext *context, TEEC_Session *session) {
  struct list_head *pos, *q;

  if (!context || !session) {
    goto exit;
  }

  list_for_each_safe(pos, q, &context->session_list.list) {
    SessionList *node = list_entry(pos, SessionList, list);

    if (node->session == session) {
      list_del(pos);
      free(node);
      // todo - add delete for all CR in session
      if (context->sessions_count) {
        context->sessions_count--;
      }
      break;
    }
  }

exit:
  return;
}

static TEEC_Result check_param_types(uint32_t param_types) {
  int i;
  uint32_t curr_param_type = 0;

  if (param_types & 0xFFFF0000) {
    TEES_LOG(TEES_LOG_LEVEL_ERROR,
             "Unused bits in operation->paramTypes must be set to zero\n");
    return TEEC_ERROR_BAD_PARAMETERS;
  }

  for (i = 0; i < 4; i++) {
    curr_param_type = TEEC_PARAM_TYPE_GET(param_types, i);
    if (curr_param_type == TEEC_PARAM_TYPE_INVALID
        || (curr_param_type > TEEC_PARAM_TYPE_VALID_BEFORE_GAP
            && curr_param_type < TEEC_PARAM_TYPE_VALID_AFTER_GAP)) {
      TEES_LOG(TEES_LOG_LEVEL_ERROR,
               "Unknown paramType (0x%08x) for param %d\n", curr_param_type, i);
      return TEEC_ERROR_BAD_PARAMETERS;
    }
  }
  return TEEC_SUCCESS;
}

TEEC_Result TEEC_InvokeCommand(TEEC_Session *session,
                               uint32_t commandID,
                               TEEC_Operation *operation,
                               uint32_t *returnOrigin) {
  TEEC_Result ret = TEEC_ERROR_BAD_PARAMETERS;
  PlatformClientImpl *client_impl = GetPlatformClientImpl();

  if (!session || !session->imp) {
    goto exit;
  }

  if (returnOrigin) {
    *returnOrigin = TEEC_ORIGIN_API;
  }

  if (operation) {
    if (check_param_types(operation->paramTypes) != TEEC_SUCCESS) {
      TEES_LOG(TEES_LOG_LEVEL_ERROR,
               "Invalid operation->paramTypes (0x%08x)\n",
               operation->paramTypes);
      ret = TEEC_ERROR_BAD_PARAMETERS;
      goto exit;
    }
  }

  if (!client_impl || !client_impl->SendCommand) {
    ret = TEEC_ERROR_NOT_IMPLEMENTED;
    goto exit;
  }

  ret = client_impl->SendCommand(session->imp,
                                 commandID, operation, returnOrigin,
                                 MB_TIMEOUT_INTERRUPTIBLE);
  RemoveOperationFromCRList(operation);

exit:
  // In a case of TEEC_ERROR_TARGET_DEAD the client must close the session
  // manually. For a while the client will receive TEEC_ERROR_TARGET_DEAD.
  return ret;
}

TEEC_Result TEEC_InitializeContext(const char *name, TEEC_Context *context) {
  TEEC_Result res = TEEC_ERROR_BAD_PARAMETERS;

  (void)name;
  if (context) {
    res = TEEC_ERROR_OUT_OF_MEMORY;

    TeecContext *teec_ctx = (TeecContext *)calloc(1, sizeof(TeecContext));
    if (teec_ctx) {
      memset(teec_ctx, 0x00, sizeof(TeecContext));
      teec_ctx->initialized = CONTEXT_INITIALIZED;
      INIT_LIST_HEAD(&teec_ctx->session_list.list);
      teec_ctx->sessions_count = 0;
      context->imp = (void *)teec_ctx;
      res = TEEC_SUCCESS;
      InitializeContextCRList();
    } else {
      context->imp = NULL;
    }
  }

  return res;
}

void TEEC_FinalizeContext(TEEC_Context *context) {
  if (context) {
    TeecContext *teec_ctx = (TeecContext *)context->imp;
    if (teec_ctx) {
      teec_ctx->initialized = 0;
      free(context->imp);
      context->imp = NULL;
      FinalizeContextCRList();
    }
  }
}

TEEC_Result TEEC_OpenSession(TEEC_Context *context,
                             TEEC_Session *session,
                             const TEEC_UUID *destination,
                             uint32_t connectionMethod,
                             const void *connectionData,
                             TEEC_Operation *operation,
                             uint32_t *returnOrigin) {
  TEEC_Result ret = TEEC_ERROR_BAD_PARAMETERS;
  PlatformClientImpl *client_impl = GetPlatformClientImpl();
  TeecContext *teec_ctx = NULL;

  if (!context || !session || !destination) {
    goto exit;
  }

  if (returnOrigin) {
    *returnOrigin = TEEC_ORIGIN_API;
  }

  if (operation) {
    if (check_param_types(operation->paramTypes) != TEEC_SUCCESS) {
      TEES_LOG(TEES_LOG_LEVEL_ERROR,
               "Invalid operation->paramTypes (0x%08x)\n",
               operation->paramTypes);
      ret = TEEC_ERROR_BAD_PARAMETERS;
      goto exit;
    }
  }

  teec_ctx = (TeecContext *)context->imp;
  if (!teec_ctx || teec_ctx->initialized != CONTEXT_INITIALIZED) {
    goto exit;
  }

  switch (connectionMethod) {
    case TEEC_LOGIN_PUBLIC:
    case TEEC_LOGIN_USER:
    case TEEC_LOGIN_APPLICATION:
    case TEEC_LOGIN_USER_APPLICATION: {
      if (connectionData) {
        goto exit;
      }
      break;
    }
    case TEEC_LOGIN_GROUP:
    case TEEC_LOGIN_GROUP_APPLICATION: {
      if (!connectionData) {
        goto exit;
      }
      break;
    }
    default: {
      goto exit;
    }
  }

  if (!client_impl || !client_impl->SendCommand || !client_impl->LoadTA) {
    ret = TEEC_ERROR_NOT_IMPLEMENTED;
    goto exit;
  }

  session->imp = malloc(GetPlatformClientImplSize());

  if (!session->imp) {
    ret = TEEC_ERROR_OUT_OF_MEMORY;
    goto exit;
  }

  memset(session->imp, 0x00, GetPlatformClientImplSize());

  ret = client_impl->LoadTA(session->imp, destination);

  if (TEEC_SUCCESS != ret) {
    goto cleanup;
  }

  /* Fixed waiting initialization of DCI thread in TBase driver */
  const uint32_t kNumTries = 5;
  const int32_t kTimeoutMs = 1000;
  uint32_t elapsed_tries = 0;

  do {
    ret = client_impl->SendCommand(session->imp, PROTOCOL_COMMAND_LOAD,
                                   operation, returnOrigin, kTimeoutMs);

    if (TEEC_SUCCESS != ret) {
      TEES_LOG(TEES_LOG_LEVEL_DEBUG,
               "Try to send %u of %u command in driver: 0x%x.\n",
               elapsed_tries + 1, kNumTries, ret);
    }

  } while ((ret != TEEC_SUCCESS) && (++elapsed_tries < kNumTries));

  if (TEEC_SUCCESS != ret) {
    TEES_LOG(TEES_LOG_LEVEL_ERROR,
             "Could not send %d commands in driver: 0x%x.\n", kNumTries, ret);
    client_impl->SendCommand(session->imp, PROTOCOL_COMMAND_UNLOAD, NULL,
                             returnOrigin, MB_TIMEOUT_INTERRUPTIBLE);
    client_impl->UnloadTA(session->imp);
  }

cleanup:
  if (TEEC_SUCCESS != ret && session) {
    free(session->imp);
    session->imp = NULL;
  }

exit:
  if (TEEC_SUCCESS == ret) {
    ClientImplSetParentContext(session->imp, teec_ctx);
    ret = AddSessionToList(teec_ctx, session);

    if (TEEC_SUCCESS != ret) {
      TEEC_CloseSession(session);
    }
  }

  return ret;
}

void TEEC_CloseSession(TEEC_Session *session) {
  uint32_t return_origin;
  PlatformClientImpl *client_impl = GetPlatformClientImpl();

  if (session && session->imp) {
    if (client_impl && client_impl->SendCommand && client_impl->UnloadTA) {
      client_impl->SendCommand(session->imp, PROTOCOL_COMMAND_UNLOAD, NULL,
                               &return_origin, MB_TIMEOUT_INTERRUPTIBLE);
      client_impl->UnloadTA(session->imp);
    }
    RemoveSessionFromList(ClientImplGetParentContext(session->imp), session);

    free(session->imp);
    session->imp = NULL;
  }
}

void TEEC_RequestCancellation(TEEC_Operation *operation) {
  if (!operation) {
    goto exit;
  }
  AddOperationToCRList(operation);
exit:
  return;
}

TEEC_Result TEEC_RegisterSharedMemory(TEEC_Context *context,
                                      TEEC_SharedMemory *sharedMem) {
  TEEC_Result result = TEEC_ERROR_BAD_PARAMETERS;
  TeecContext *teec_ctx = NULL;

  if (!context || !sharedMem) {
    goto exit;
  }

  teec_ctx = (TeecContext *)context->imp;
  if (!teec_ctx || teec_ctx->initialized != CONTEXT_INITIALIZED) {
    goto exit;
  }

  if (sharedMem->size > MAX_SHAREDMEM_SIZE) {
    goto exit;
  }

  if (!sharedMem->buffer) {
    result = TEEC_ERROR_NO_DATA;
    goto exit;
  }
  sharedMem->imp.context = context;
  sharedMem->imp.allocated = 0;
  result = TEEC_SUCCESS;
exit:
  return result;
}

static TEEC_Result AllocateSharedMemory(TEEC_Context *context,
                                        TEEC_Session *session,
                                        TEEC_SharedMemory *sharedMem) {
  TEEC_Result result = TEEC_ERROR_BAD_PARAMETERS;
  PlatformClientImpl *client_impl = GetPlatformClientImpl();
  TeecContext *teec_ctx = (TeecContext *)context->imp;

  if (!teec_ctx || teec_ctx->initialized != CONTEXT_INITIALIZED) {
    goto exit;
  }

  result = TEEC_ERROR_OUT_OF_MEMORY;
  if (sharedMem->size > MAX_SHAREDMEM_SIZE) {
    goto exit;
  }

  if (client_impl && client_impl->AllocateBuffer && session) {
    sharedMem->buffer = client_impl->AllocateBuffer(session->imp,
                                                    sharedMem->size);
  } else {
    sharedMem->buffer = malloc(sharedMem->size);
  }

  if (!sharedMem->buffer) {
    goto exit;
  }

  sharedMem->imp.allocated = ALLOCATED_MAGIC;
  sharedMem->imp.context = context;
  result = TEEC_SUCCESS;

exit:
  return result;
}

TEEC_Result TEEC_AllocateSharedMemory(TEEC_Context *context,
                                      TEEC_SharedMemory *sharedMem) {
  TEEC_Result result = TEEC_ERROR_OUT_OF_MEMORY;
  TEEC_Session *session = NULL;
  TeecContext *teec_ctx = NULL;
  PlatformClientImpl *client_impl = GetPlatformClientImpl();

  if (!context || !context->imp || !sharedMem ||
      CONTEXT_INITIALIZED != ((TeecContext *)(context->imp))->initialized) {
    result = TEEC_ERROR_BAD_PARAMETERS;
    goto exit;
  }

  teec_ctx = (TeecContext *)context->imp;

  if (!teec_ctx) {
    goto exit;
  }

  if (client_impl && client_impl->AllocateBuffer &&
      1 == GetSessionsCount(teec_ctx)) {
    SessionList *node = list_first_entry(&teec_ctx->session_list.list,
                                         SessionList, list);
    session = node ? node->session : NULL;
  }

  result = AllocateSharedMemory(context, session, sharedMem);
exit:
  return result;
}

TEEC_Result TEECS_AllocateSessionSharedMemory(TEEC_Context *context,
                                              TEEC_Session *session,
                                              TEEC_SharedMemory *sharedMem) {
  TEEC_Result result = TEEC_ERROR_OUT_OF_MEMORY;

  if (!context || !context->imp || !sharedMem || !session ||
      CONTEXT_INITIALIZED != ((TeecContext *)(context->imp))->initialized) {
    result = TEEC_ERROR_BAD_PARAMETERS;
    goto exit;
  }

  result = AllocateSharedMemory(context, session, sharedMem);
exit:
  return result;
}

static int ReleaseBuffer(TeecContext *teec_ctx, void *ptr) {
  int released = 0;
  struct list_head *pos, *q;
  PlatformClientImpl *client_impl = GetPlatformClientImpl();

  if (client_impl && client_impl->CanRelease && client_impl->FreeBuffer) {

    list_for_each_safe(pos, q, &teec_ctx->session_list.list) {
      SessionList *node = list_entry(pos, SessionList, list);

      if (client_impl->CanRelease(node->session->imp, ptr)) {
        released++;
        client_impl->FreeBuffer(node->session->imp, ptr);
        break;
      }
    }

  }

  return released;
}

void TEEC_ReleaseSharedMemory(TEEC_SharedMemory *sharedMem) {
  if (sharedMem && sharedMem->imp.context && sharedMem->imp.context->imp) {
    if (ALLOCATED_MAGIC == sharedMem->imp.allocated) {
      /* Try to free buffer using first session binding if possible */
      if (!ReleaseBuffer((TeecContext *)(sharedMem->imp.context->imp),
                         sharedMem->buffer)) {
        free(sharedMem->buffer);
      }
      sharedMem->imp.allocated = 0;
    }
  }
}

