#include "TcpSocket.hpp"

#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <netdb.h>

#include <string>
#include <functional>
#include <cerrno>
#include <cstring>
#include <thread>

namespace TcpSocket
{

  void setTimeout(int seconds, int sock)
  {
    struct timeval tv;
    tv.tv_sec = seconds;
    tv.tv_usec = 0;

    setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (char *)&tv, sizeof(tv));
    setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, (char *)&tv, sizeof(tv));
  }

  ssize_t sendt(int sock, const void *bytes, size_t byteslength) { return send(sock, bytes, byteslength, 0); }

  ssize_t recvt(int sock, void *bytes, size_t byteslength) { return recv(sock, bytes, byteslength, 0); }

  void closet(int sock)
  {
    shutdown(sock, SHUT_RDWR);
    close(sock);
  }

  std::string remoteAddress(sockaddr_in &address)
  {
    char ip[INET_ADDRSTRLEN];
    inet_ntop(AF_INET, &(address.sin_addr), ip, INET_ADDRSTRLEN);

    return std::string(ip);
  }

  int remotePort(sockaddr_in &address) { return ntohs(address.sin_port); }

  int connectt(const char *host, uint16_t port)
  {
    struct addrinfo hints, *res, *it;
    memset(&hints, 0, sizeof(hints));
    hints.ai_family = AF_INET;
    hints.ai_socktype = SOCK_STREAM;

    // Get address info from DNS
    int status = getaddrinfo(host, NULL, &hints, &res);
    if (status != 0)
    {
      // onError(errno, "Invalid address." + std::string(gai_strerror(status)));
      return -1;
    }

    sockaddr_in address;
    for (it = res; it != NULL; it = it->ai_next)
    {
      if (it->ai_family == AF_INET)
      { // IPv4
        memcpy((void *)(&address), (void *)it->ai_addr, sizeof(sockaddr_in));
        break; // for now, just get the first ip (ipv4).
      }
    }

    freeaddrinfo(res);

    int sock = socket(AF_INET, SOCK_STREAM, 0);

    if (sock == -1)
    {
      // onError(errno, "Socket creating error.");
      return -2;
    }

    address.sin_family = AF_INET;
    address.sin_port = htons(port);

    // setTimeout(5, sock);

    // Try to connect.
    status = connect(sock, (const sockaddr *)&address, sizeof(sockaddr_in));
    if (status == -1)
    {
      // onError(errno, "Connection failed to the host.");
      close(sock);
      return -3;
    }
    return sock;
  }

  int listent(const char *host, uint16_t port, OnNewConnectionCallBack callback)
  {
    sockaddr_in address;

    int sock = socket(AF_INET, SOCK_STREAM, 0);

    if (sock == -1)
    {
      // onError(errno, "Socket creating error.");
      return -1;
    }

    int opt = 1;
    setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(int));
    setsockopt(sock, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(int));

    int status = inet_pton(AF_INET, host, &address.sin_addr);
    switch (status)
    {
    case -1:
      close(sock);
      // onError(errno, "Invalid address. Address type not supported.");
      return -2;
    case 0:
      close(sock);
      // onError(errno, "AF_INET is not supported. Please send message to developer.");
      return -3;
    default:
      break;
    }

    address.sin_family = AF_INET;
    address.sin_port = htons(port);

    if (bind(sock, (const sockaddr *)&address, sizeof(address)) == -1)
    {
      // onError(errno, "Cannot bind the socket.");
      close(sock);
      return -4;
    }
    if (listen(sock, 20) == -1)
    {
      // onError(errno, "Error: Server can't listen the socket.");
      close(sock);
      return -5;
    }

    sockaddr_in newSocketInfo;
    socklen_t newSocketInfoLength = sizeof(newSocketInfo);

    int newSocketFileDescriptor = -1;
    while (true)
    {
      newSocketFileDescriptor = accept(sock, (sockaddr *)&newSocketInfo, &newSocketInfoLength);
      if (newSocketFileDescriptor == -1)
      {
        if (errno == EBADF || errno == EINVAL)
          return -6;

        return -7;
      }
      setTimeout(60, newSocketFileDescriptor);
      std::thread t(callback, newSocketFileDescriptor, newSocketInfo);
      t.detach();
    }

    return 0;
  }

}