/** @file

  Copyright (c) 2020, Rebecca Cran <rebecca@bsdio.com>
  Copyright (c) 2011 - 2013, Intel Corporation. All rights reserved.<BR>
  Copyright (C) 2013, Red Hat, Inc.
  Copyright (c) 2015, Nahanni Systems.

  SPDX-License-Identifier: BSD-2-Clause-Patent

**/

#include "Uefi.h"
#include <Library/BaseLib.h>
#include <Library/BaseMemoryLib.h>
#include <Library/BhyveFwCtlLib.h>
#include <Library/DebugLib.h>
#include <Library/IoLib.h>
#include <Library/MemoryAllocationLib.h>
#include <Library/UefiBootServicesTableLib.h>

#define FW_PORT   0x510
#define FW_IPORT  0x511

/* Transport protocol basic operations */
#define OP_NULL     1
#define OP_ECHO     2
#define OP_GET      3
#define OP_GET_LEN  4
#define OP_SET      5

/* Transport protocol error returns */
#define T_ESUCCESS  0
#define T_ENOENT    2
#define T_E2BIG     7
#define T_EMSGSIZE  40

#define ROUNDUP(x, y)  ((((x)+((y)-1))/(y))*(y))

STATIC CONST CHAR8  mBhyveSig[4] = { 'B', 'H', 'Y', 'V' };

STATIC BOOLEAN  mBhyveFwCtlSupported = FALSE;

STATIC INT32  mBhyveFwCtlTxid = 0xa5;

/* XXX Maybe a better inbuilt version of this ? */
typedef struct {
  VOID      *Base;
  UINT32    Len;
} BIO_VEC;

typedef struct {
  UINT32    Sz;
  UINT32    Op;
  UINT32    TxId;
  UINT32    Err;
} MSG_RX_HDR;

STATIC
RETURN_STATUS
EFIAPI
BhyveFwCtl_CvtErr (
  IN UINT32  errno
  )
{
  RETURN_STATUS  Status;

  switch (errno) {
    case T_ESUCCESS:
      Status = RETURN_SUCCESS;
      break;
    case T_ENOENT:
      Status = RETURN_NOT_FOUND;
      break;
    case T_E2BIG:
      Status = RETURN_INVALID_PARAMETER;
      break;
    case T_EMSGSIZE:
      Status = RETURN_BUFFER_TOO_SMALL;
      break;
    default:
      Status = RETURN_PROTOCOL_ERROR;
      break;
  }

  return Status;
}

STATIC
UINT32
EFIAPI
BIov_WLen (
  IN BIO_VEC  b[]
  )
{
  UINT32  i;
  UINT32  tLen;

  tLen = 0;

  if (b != NULL) {
    for (i = 0; b[i].Base != NULL; i++) {
      tLen += ROUNDUP (b[i].Len, sizeof (UINT32));
    }
  }

  return tLen;
}

/**
   Utility to send 1-3 bytes of input as a 4-byte value
   with trailing zeroes.
 **/
STATIC
UINT32
BIov_Send_Rem (
  IN UINT32  *Data,
  IN UINT32  Len
  )
{
  union {
    UINT8     c[4];
    UINT32    w;
  } u;
  UINT8   *cdata;
  UINT32  i;

  cdata = (UINT8 *)Data;
  u.w   = 0;

  for (i = 0; i < Len; i++) {
    u.c[i] = *cdata++;
  }

  return u.w;
}

/**
   Send a block of data out the i/o port as 4-byte quantities,
   appending trailing zeroes on the last if required.
 **/
STATIC
VOID
BIov_Send (
  IN char    *Data,
  IN UINT32  Len
  )
{
  UINT32  *LData;

  LData = (UINT32 *)Data;

  while (Len > sizeof (UINT32)) {
    IoWrite32 (FW_PORT, *LData++);
    Len -= sizeof (UINT32);
  }

  if (Len > 0) {
    IoWrite32 (FW_PORT, BIov_Send_Rem (LData, Len));
  }
}

/**
   Send data described by an array of iovecs out the i/o port.
 **/
STATIC
VOID
BIov_SendAll (
  IN BIO_VEC  b[]
  )
{
  INT32  i;

  if (b != NULL) {
    for (i = 0; b[i].Base; i++) {
      BIov_Send (b[i].Base, b[i].Len);
    }
  }
}

/**
   Prepend the transport header to a block of data and send.
 **/
STATIC
VOID
EFIAPI
BhyveFwCtl_MsgSend (
  IN  UINT32   OpCode,
  IN  BIO_VEC  Data[]
  )
{
  BIO_VEC  hIov[4];
  UINT32   Hdr[3];
  UINT32   i;

  /* Set up header as an iovec */
  for (i = 0; i < 3; i++) {
    hIov[i].Base = &Hdr[i];
    hIov[i].Len  = sizeof (Hdr[0]);
  }

  hIov[i].Base = NULL;
  hIov[i].Len  = 0;

  /* Initialize header */
  Hdr[0] = BIov_WLen (hIov) + BIov_WLen (Data);
  Hdr[1] = (UINT32)OpCode;
  Hdr[2] = mBhyveFwCtlTxid;

  /* Send header and data */
  BIov_SendAll (hIov);
  BIov_SendAll (Data);
}

/**
   Read a transport response and optional data from the i/o port.
 **/
STATIC
RETURN_STATUS
EFIAPI
BhyveFwCtl_MsgRecv (
  OUT  MSG_RX_HDR  *Rhdr,
  OUT  BIO_VEC     Data[]
  )
{
  RETURN_STATUS  Status;
  UINT32         *Dp;
  UINT32         Rd;
  UINT32         remLen;
  INT32          oLen;
  INT32          xLen;

  Rd = IoRead32 (FW_PORT);
  if (Rd < sizeof (MSG_RX_HDR)) {
  }

  /* Read in header and setup initial error */
  Rhdr->Sz   = Rd;
  Rhdr->Op   = IoRead32 (FW_PORT);
  Rhdr->TxId = IoRead32 (FW_PORT);
  Rhdr->Err  = IoRead32 (FW_PORT);

  /* Convert transport errno into UEFI error status */
  Status = BhyveFwCtl_CvtErr (Rhdr->Err);

  remLen = Rd - sizeof (MSG_RX_HDR);
  xLen   = 0;

  /*
   * A few cases to handle:
   *  - the user didn't supply a read buffer
   *  - the buffer is too small for the response
   *  - the response is zero-length
   */
  if (Data != NULL) {
    Dp   = (UINT32 *)Data[0].Base;
    oLen = remLen;
    if (remLen > Data[0].Len) {
      Status = RETURN_BUFFER_TOO_SMALL;
      xLen   = remLen - Data[0].Len;
      oLen   = remLen = Data[0].Len;
    }

    while (remLen > 0) {
      *Dp++   = IoRead32 (FW_PORT);
      remLen -= sizeof (UINT32);
    }

    Data[0].Len = oLen;
  } else {
    /* No user data, but data returned - drop */
    if (remLen > 0) {
      Status = RETURN_BUFFER_TOO_SMALL;
      xLen   = remLen;
    }
  }

  /* Drop additional data */
  while (xLen > 0) {
    (void)IoRead32 (FW_PORT);
    xLen -= sizeof (UINT32);
  }

  return Status;
}

STATIC
RETURN_STATUS
EFIAPI
BhyveFwCtl_Msg (
  IN   UINT32   OpCode,
  IN   BIO_VEC  Sdata[],
  OUT  BIO_VEC  Rdata[]
  )
{
  MSG_RX_HDR     Rh;
  RETURN_STATUS  Status;

  Status = RETURN_SUCCESS;

  BhyveFwCtl_MsgSend (OpCode, Sdata);
  Status = BhyveFwCtl_MsgRecv (&Rh, Rdata);

  mBhyveFwCtlTxid++;

  return Status;
}

STATIC
RETURN_STATUS
EFIAPI
BhyveFwCtlGetLen (
  IN CONST CHAR8   *Name,
  IN OUT   UINT32  *Size
  )
{
  BIO_VEC        Req[2], Resp[2];
  RETURN_STATUS  Status;

  Req[0].Base = (VOID *)Name;
  Req[0].Len  = (UINT32)AsciiStrLen (Name) + 1;
  Req[1].Base = NULL;

  Resp[0].Base = Size;
  Resp[0].Len  = sizeof (UINT32);
  Resp[1].Base = NULL;

  Status = BhyveFwCtl_Msg (OP_GET_LEN, Req, Resp);

  return Status;
}

#define FMAXSZ  1024
STATIC struct {
  UINT64    fSize;
  UINT32    fData[FMAXSZ];
} FwGetvalBuf;

STATIC
RETURN_STATUS
EFIAPI
BhyveFwCtlGetVal (
  IN CONST CHAR8   *Name,
  OUT      VOID    *Item,
  IN OUT   UINT32  *Size
  )
{
  BIO_VEC        Req[2];
  BIO_VEC        Resp[2];
  RETURN_STATUS  Status;

  /* Make sure temp buffer is larger than passed-in size */
  if (*Size > sizeof (FwGetvalBuf.fData)) {
    return RETURN_INVALID_PARAMETER;
  }

  Req[0].Base = (VOID *)Name;
  Req[0].Len  = (UINT32)AsciiStrLen (Name) + 1;
  Req[1].Base = NULL;

  Resp[0].Base = &FwGetvalBuf;
  Resp[0].Len  = sizeof (UINT64) + *Size;
  Resp[1].Base = NULL;

  Status = BhyveFwCtl_Msg (OP_GET, Req, Resp);

  /*
   * Copy out data on success (or on a truncated message).
   * XXX This step can be eliminted with Msg() supporting
   *     multiple iovecs.
   */
  if ((Status == RETURN_SUCCESS) || (Status == RETURN_BUFFER_TOO_SMALL)) {
    *Size = (UINT32)FwGetvalBuf.fSize;
    CopyMem (Item, FwGetvalBuf.fData, *Size);
  }

  return Status;
}

/**
   Front end to the internal GET_LEN and GET protocols
 **/
RETURN_STATUS
EFIAPI
BhyveFwCtlGet (
  IN   CONST CHAR8  *Name,
  OUT  VOID         *Item,
  IN OUT  UINTN     *Size
  )
{
  RETURN_STATUS  Status;

  if (mBhyveFwCtlSupported == FALSE) {
    return RETURN_UNSUPPORTED;
  }

  if (Item == NULL) {
    Status = BhyveFwCtlGetLen (Name, (UINT32 *)Size);
  } else {
    Status = BhyveFwCtlGetVal (Name, Item, (UINT32 *)Size);
  }

  return Status;
}

/**
   Library initialization. Probe the host to see if the f/w ctl
   interface is supported.
 **/
RETURN_STATUS
EFIAPI
BhyveFwCtlInitialize (
  VOID
  )
{
  UINT32  i;
  UINT8   ch;

  DEBUG ((DEBUG_INFO, "FwCtlInitialize\n"));

  IoWrite16 (FW_PORT, 0x0000);
  for (i = 0; i < 4; i++) {
    ch = IoRead8 (FW_IPORT);
    if (ch != mBhyveSig[i]) {
      DEBUG ((DEBUG_INFO, "Host f/w sig mismatch %c/%c\n", ch, mBhyveSig[i]));
      return RETURN_SUCCESS;
    }
  }

  mBhyveFwCtlSupported = TRUE;

  return RETURN_SUCCESS;
}
