// Copyright lowRISC contributors.
// Copyright 2024 Antmicro <www.antmicro.com>
// Licensed under the Apache License, Version 2.0, see LICENSE for details.
// SPDX-License-Identifier: Apache-2.0

#include "tcp_server.h"

#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

/**
 * Simple buffer for passing data between TCP sockets and DPI modules
 */
#define BUFSIZE_BYTE 1024 // FIXME: This must be larger than the remote_bitbang
                          // buffer in OpenOCD. Otherwise deadlock occurs.

struct tcp_buf {
  unsigned int rptr;
  unsigned int wptr;
  char buf[BUFSIZE_BYTE];
};

/**
 * TCP Server thread context structure
 */
struct tcp_server_ctx {
  // Writeable by the host thread
  char *display_name;
  uint16_t listen_port;
  volatile bool socket_run;
  // Writeable by the server thread
  struct tcp_buf *buf_in;
  struct tcp_buf *buf_out;
  int sfd;  // socket fd
  int cfd;  // client fd
  pthread_t sock_thread;
};

static bool tcp_buffer_is_full(struct tcp_buf *buf) {
  if (buf->wptr >= buf->rptr) {
    return (buf->wptr - buf->rptr) == (BUFSIZE_BYTE - 1);
  } else {
    return (buf->rptr - buf->wptr) == 1;
  }
}

static bool tcp_buffer_is_empty(struct tcp_buf *buf) {
  return (buf->wptr == buf->rptr);
}

static void tcp_buffer_put_byte(struct tcp_buf *buf, char dat) {
  bool done = false;
  while (!done) {
    if (!tcp_buffer_is_full(buf)) {
      buf->buf[buf->wptr++] = dat;
      buf->wptr %= BUFSIZE_BYTE;
      done = true;
    }
  }
}

static bool tcp_buffer_get_byte(struct tcp_buf *buf, char *dat) {
  if (tcp_buffer_is_empty(buf)) {
    return false;
  }
  *dat = buf->buf[buf->rptr++];
  buf->rptr %= BUFSIZE_BYTE;
  return true;
}

static struct tcp_buf *tcp_buffer_new(void) {
  struct tcp_buf *buf_new;
  buf_new = (struct tcp_buf *)malloc(sizeof(struct tcp_buf));
  buf_new->rptr = 0;
  buf_new->wptr = 0;
  return buf_new;
}

static void tcp_buffer_free(struct tcp_buf **buf) {
  free(*buf);
  *buf = NULL;
}

/**
 * Start a TCP server
 *
 * This function creates attempts to create a new TCP socket instance. The
 * socket is a non-blocking stream socket, with buffering disabled.
 *
 * @param ctx context object
 * @return 0 on success, -1 in case of an error
 */
static int start(struct tcp_server_ctx *ctx) {
  int rv;

  assert(ctx->sfd == 0 && "Server already started.");

  // create socket
  int sfd = socket(AF_INET, SOCK_STREAM, 0);
  if (sfd == -1) {
    fprintf(stderr, "%s: Unable to create socket: %s (%d)\n", ctx->display_name,
            strerror(errno), errno);
    return -1;
  }

  rv = fcntl(sfd, F_SETFL, O_NONBLOCK);
  if (rv != 0) {
    fprintf(stderr, "%s: Unable to make socket non-blocking: %s (%d)\n",
            ctx->display_name, strerror(errno), errno);
    return -1;
  }

  // reuse existing socket (if existing)
  int reuse_socket = 1;
  rv = setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &reuse_socket, sizeof(int));
  if (rv != 0) {
    fprintf(stderr, "%s: Unable to set socket options: %s (%d)\n",
            ctx->display_name, strerror(errno), errno);
    return -1;
  }

  // stop tcp socket from buffering (buffering prevents timely responses to
  // OpenOCD which severly limits debugging performance)
  int tcp_nodelay = 1;
  rv = setsockopt(sfd, IPPROTO_TCP, TCP_NODELAY, &tcp_nodelay, sizeof(int));
  if (rv != 0) {
    fprintf(stderr, "%s: Unable to set socket nodelay: %s (%d)\n",
            ctx->display_name, strerror(errno), errno);
    return -1;
  }

  // bind server
  struct sockaddr_in addr;
  memset(&addr, 0, sizeof(addr));
  addr.sin_family = AF_INET;
  addr.sin_addr.s_addr = htonl(INADDR_ANY);
  addr.sin_port = htons(ctx->listen_port);

  rv = bind(sfd, (struct sockaddr *)&addr, sizeof(addr));
  if (rv != 0) {
    fprintf(stderr, "%s: Failed to bind socket: %s (%d)\n", ctx->display_name,
            strerror(errno), errno);
    return -1;
  }

  // listen for incoming connections
  rv = listen(sfd, 1);
  if (rv != 0) {
    fprintf(stderr, "%s: Failed to listen on socket: %s (%d)\n",
            ctx->display_name, strerror(errno), errno);
    return -1;
  }

  ctx->sfd = sfd;
  assert(ctx->sfd > 0);

  return 0;
}

/**
 * Accept an incoming connection from a client (nonblocking)
 *
 * The resulting client fd is made non-blocking.
 *
 * @param ctx context object
 * @return 0 on success, any other value indicates an error
 */
static int client_tryaccept(struct tcp_server_ctx *ctx) {
  int rv;

  assert(ctx->sfd > 0);
  assert(ctx->cfd == 0);

  int cfd = accept(ctx->sfd, NULL, NULL);

  if (cfd == -1 && errno == EAGAIN) {
    return -EAGAIN;
  }

  if (cfd == -1) {
    fprintf(stderr, "%s: Unable to accept incoming connection: %s (%d)\n",
            ctx->display_name, strerror(errno), errno);
    return -1;
  }

  rv = fcntl(cfd, F_SETFL, O_NONBLOCK);
  if (rv != 0) {
    fprintf(stderr, "%s: Unable to make client socket non-blocking: %s (%d)\n",
            ctx->display_name, strerror(errno), errno);
    return -1;
  }

  ctx->cfd = cfd;
  assert(ctx->cfd > 0);

  printf("%s: Accepted client connection\n", ctx->display_name);

  return 0;
}

/**
 * Stop the TCP server
 *
 * @param ctx context object
 */
static void stop(struct tcp_server_ctx *ctx) {
  assert(ctx);
  if (!ctx->sfd) {
    return;
  }
  close(ctx->sfd);
  ctx->sfd = 0;
}

/**
 * Receive a byte from a connected client
 *
 * @param ctx context object
 * @param cmd byte received
 * @return true if a byte was read
 */
static bool get_byte(struct tcp_server_ctx *ctx, char *cmd) {
  assert(ctx);

  ssize_t num_read = read(ctx->cfd, cmd, 1);

  if (num_read == 0) {
    return false;
  }
  if (num_read == -1) {
    if (errno == EAGAIN || errno == EWOULDBLOCK) {
      return false;
    } else if (errno == EBADF) {
      // Possibly client went away? Accept a new connection.
      fprintf(stderr, "%s: Client disappeared.\n", ctx->display_name);
      tcp_server_client_close(ctx);
      return false;
    } else {
      fprintf(stderr, "%s: Error while reading from client: %s (%d)\n",
              ctx->display_name, strerror(errno), errno);
      assert(0 && "Error reading from client");
    }
  }
  assert(num_read == 1);
  return true;
}

/**
 * Send a byte to a connected client
 *
 * @param ctx context object
 * @param cmd byte to send
 */
static void put_byte(struct tcp_server_ctx *ctx, char cmd) {
  while (1) {
    ssize_t num_written = send(ctx->cfd, &cmd, sizeof(cmd), MSG_NOSIGNAL);
    if (num_written == -1) {
      if (errno == EAGAIN || errno == EWOULDBLOCK) {
        continue;
      } else if (errno == EPIPE) {
        printf("%s: Remote disconnected.\n", ctx->display_name);
        tcp_server_client_close(ctx);
        break;
      } else {
        fprintf(stderr, "%s: Error while writing to client: %s (%d)\n",
                ctx->display_name, strerror(errno), errno);
        assert(0 && "Error writing to client.");
      }
    }
    if (num_written >= 1) {
      break;
    }
  }
}

/**
 * Cleanup server context
 *
 * @param ctx context object
 */
static void ctx_free(struct tcp_server_ctx *ctx) {
  // Free the buffers
  tcp_buffer_free(&ctx->buf_in);
  tcp_buffer_free(&ctx->buf_out);
  // Free the display name
  free(ctx->display_name);
  // Free the ctx
  free(ctx);
  ctx = NULL;
}

/**
 * Thread function to create a new server instance
 *
 * @param ctx_void context object
 * @return Always returns NULL
 */
static void *server_create(void *ctx_void) {
  // Cast to a server struct
  struct tcp_server_ctx *ctx = (struct tcp_server_ctx *)ctx_void;
  struct timeval timeout;

  // Start the server
  int rv = start(ctx);
  if (rv != 0) {
    fprintf(stderr, "%s: Unable to create TCP server on port %d\n",
            ctx->display_name, ctx->listen_port);
    goto err_cleanup_return;
  }

  // Initialise timeout
  timeout.tv_sec = 0;

  // Initialise fd_set

  // Start waiting for connection / data
  char xfer_data;
  while (ctx->socket_run) {
    // Initialise structure of fds
    fd_set read_fds;
    FD_ZERO(&read_fds);
    if (ctx->sfd) {
      FD_SET(ctx->sfd, &read_fds);
    }
    if (ctx->cfd) {
      FD_SET(ctx->cfd, &read_fds);
    }
    // max fd num
    int mfd = (ctx->cfd > ctx->sfd) ? ctx->cfd : ctx->sfd;

    // Set timeout - 50us gives good performance
    timeout.tv_usec = 50;

    // Wait for socket activity or timeout
    rv = select(mfd + 1, &read_fds, NULL, NULL, &timeout);

    if (rv < 0) {
      printf("%s: Socket read failed, port: %d\n", ctx->display_name,
             ctx->listen_port);
      tcp_server_client_close(ctx);
    }

    // New connection
    if (FD_ISSET(ctx->sfd, &read_fds)) {
      client_tryaccept(ctx);
    }

    // New client data
    if (FD_ISSET(ctx->cfd, &read_fds)) {
      while (get_byte(ctx, &xfer_data)) {
        tcp_buffer_put_byte(ctx->buf_in, xfer_data);
      }
    }

    if (ctx->cfd != 0) {
      while (tcp_buffer_get_byte(ctx->buf_out, &xfer_data)) {
        put_byte(ctx, xfer_data);
      }
    }
  }

err_cleanup_return:

  // Simulation done - clean up
  tcp_server_client_close(ctx);
  stop(ctx);

  return NULL;
}

// Abstract interface functions
struct tcp_server_ctx *tcp_server_create(const char *display_name,
                                         int listen_port) {
  struct tcp_server_ctx *ctx =
      (struct tcp_server_ctx *)calloc(1, sizeof(struct tcp_server_ctx));
  assert(ctx);

  // Create the buffers
  struct tcp_buf *buf_in = tcp_buffer_new();
  struct tcp_buf *buf_out = tcp_buffer_new();
  assert(buf_in);
  assert(buf_out);

  // Populate the struct with buffer pointers
  ctx->buf_in = buf_in;
  ctx->buf_out = buf_out;

  // Set up socket details
  ctx->socket_run = true;
  ctx->listen_port = listen_port;
  ctx->display_name = strdup(display_name);
  assert(ctx->display_name);

  if (pthread_create(&ctx->sock_thread, NULL, server_create, (void *)ctx) !=
      0) {
    fprintf(stderr, "%s: Unable to create TCP socket thread\n",
            ctx->display_name);
    ctx_free(ctx);
    free(ctx);
    return NULL;
  }
  return ctx;
}

bool tcp_server_read(struct tcp_server_ctx *ctx, char *dat) {
  return tcp_buffer_get_byte(ctx->buf_in, dat);
}

void tcp_server_write(struct tcp_server_ctx *ctx, char dat) {
  tcp_buffer_put_byte(ctx->buf_out, dat);
}

void tcp_server_close(struct tcp_server_ctx *ctx) {
  // Shut down the socket thread
  ctx->socket_run = false;
  pthread_join(ctx->sock_thread, NULL);
  ctx_free(ctx);
}

void tcp_server_client_close(struct tcp_server_ctx *ctx) {
  assert(ctx);

  if (!ctx->cfd) {
    return;
  }

  close(ctx->cfd);
  ctx->cfd = 0;
}
