/** @file

  Provides a wrapper for a Unix socket.

  @section license License

  Licensed to the Apache Software Foundation (ASF) under one
  or more contributor license agreements.  See the NOTICE file
  distributed with this work for additional information
  regarding copyright ownership.  The ASF licenses this file
  to you under the Apache License, Version 2.0 (the
  "License"); you may not use this file except in compliance
  with the License.  You may obtain a copy of the License at

      http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.
 */

#pragma once

#include "tscore/ink_apidefs.h"
#include "tscore/ink_assert.h"
#include "tscore/ink_platform.h"
#include "tscore/ink_sock.h"

#include <cstdint>

#define NO_SOCK -1
#define SOCKET  int

#ifndef SOCK_NONBLOCK
#define SOCK_NONBLOCK O_NONBLOCK
#endif

#ifndef SOCK_CLOEXEC
#define SOCK_CLOEXEC O_CLOEXEC
#endif

#ifndef MSG_FASTOPEN
#if defined(__linux__)
#define MSG_FASTOPEN 0x20000000
#else
#define MSG_FASTOPEN 0
#endif
#endif

bool transient_error();

class UnixSocket
{
public:
  UnixSocket(int fd);

  /** Get a new socket.
   *
   * Call is_ok() to determine whether this call succeeded. If the call
   * failed, errno will be set to indicate the error.
   *
   * @see is_ok
   */
  UnixSocket(int domain, int ctype, int protocol);

  int get_fd() const;

  bool is_ok() const;

  int set_nonblocking();

  int bind(struct sockaddr const *name, int namelen);
  int accept4(struct sockaddr *addr, socklen_t *addrlen, int flags) const;
  int connect(struct sockaddr const *addr, socklen_t addrlen);

  std::int64_t read(void *buf, int size) const;

  int recv(void *buf, int size, int flags) const;
  int recvfrom(void *buf, int size, int flags, struct sockaddr *addr, socklen_t *addrlen) const;
  int recvmsg(struct msghdr *m, int flags) const;
#ifdef HAVE_RECVMMSG
  int recvmmsg(struct mmsghdr *msgvec, int vlen, int flags, struct timespec *timeout) const;
#endif

  std::int64_t write(void const *buf, int size) const;

  int send(void const *buf, int size, int flags) const;
  int sendto(void const *buf, int size, int flags, struct sockaddr const *to, int tolen) const;
  int sendmsg(struct msghdr const *m, int flags) const;

  static int poll(struct pollfd *fds, unsigned long nfds, int timeout);

  int getsockname(struct sockaddr *sa, socklen_t *sz) const;

  int get_sndbuf_size() const;
  int get_rcvbuf_size() const;
  int set_sndbuf_size(int bsz);
  int set_rcvbuf_size(int bsz);

  int enable_option(int level, int optname);

  int close();
  int shutdown(int how);

  static bool client_fastopen_supported();

private:
  int fd{NO_SOCK};
};

inline UnixSocket::UnixSocket(int fd) : fd{fd}
{
  // A value of -1 means no socket, and a positive value means a valid
  // file descriptor (unless it's higher than the max file descriptor, but
  // we don't check this). This is intended to catch if we try to use
  // something like `-errno` to initialize the file descriptor.
  ink_assert(fd >= -1);
}

inline UnixSocket::UnixSocket(int domain, int type, int protocol)
{
  this->fd = socket(domain, type, protocol);
}

inline int
UnixSocket::get_fd() const
{
  return this->fd;
}

inline bool
UnixSocket::is_ok() const
{
  return NO_SOCK != this->fd;
}

inline std::int64_t
UnixSocket::read(void *buf, int size) const
{
  std::int64_t r;
  do {
    r = ::read(this->fd, buf, size);
    if (likely(r >= 0)) {
      break;
    }
    r = -errno;
  } while (r == -EINTR);
  return r;
}

inline int
UnixSocket::recv(void *buf, int size, int flags) const
{
  int r;
  do {
    if (unlikely((r = ::recv(this->fd, static_cast<char *>(buf), size, flags)) < 0)) {
      r = -errno;
    }
  } while (r == -EINTR);
  return r;
}

inline int
UnixSocket::recvfrom(void *buf, int size, int flags, struct sockaddr *addr, socklen_t *addrlen) const
{
  int r;
  do {
    r = ::recvfrom(this->fd, static_cast<char *>(buf), size, flags, addr, addrlen);
    if (unlikely(r < 0)) {
      r = -errno;
    }
  } while (r == -EINTR);
  return r;
}

inline int
UnixSocket::recvmsg(struct msghdr *m, int flags) const
{
  int r;
  do {
    if (unlikely((r = ::recvmsg(this->fd, m, flags)) < 0)) {
      r = -errno;
    }
  } while (r == -EINTR);
  return r;
}

#ifdef HAVE_RECVMMSG
inline int
UnixSocket::recvmmsg(struct mmsghdr *msgvec, int vlen, int flags, struct timespec *timeout) const
{
  int r;
  do {
    if (unlikely((r = ::recvmmsg(this->fd, msgvec, vlen, flags, timeout)) < 0)) {
      r = -errno;
      // EINVAL can ocur if timeout is invalid.
    }
  } while (r == -EINTR);
  return r;
}
#endif

inline std::int64_t
UnixSocket::write(void const *buf, int size) const
{
  std::int64_t r;
  do {
    if (likely((r = ::write(this->fd, buf, size)) >= 0)) {
      break;
    }
    r = -errno;
  } while (r == -EINTR);
  return r;
}

inline int
UnixSocket::send(void const *buf, int size, int flags) const
{
  int r;
  do {
    if (unlikely((r = ::send(this->fd, static_cast<char const *>(buf), size, flags)) < 0)) {
      r = -errno;
    }
  } while (r == -EINTR);
  return r;
}

inline int
UnixSocket::sendto(void const *buf, int len, int flags, struct sockaddr const *to, int tolen) const
{
  int r;
  do {
    if (unlikely((r = ::sendto(this->fd, (char *)buf, len, flags, to, tolen)) < 0)) {
      r = -errno;
    }
  } while (r == -EINTR);
  return r;
}

inline int
UnixSocket::sendmsg(struct msghdr const *m, int flags) const
{
  int r;
  do {
    if (unlikely((r = ::sendmsg(this->fd, m, flags)) < 0)) {
      r = -errno;
    }
  } while (r == -EINTR);
  return r;
}

inline int
UnixSocket::poll(struct pollfd *fds, unsigned long nfds, int timeout)
{
  int r;
  do {
    if ((r = ::poll(fds, nfds, timeout)) >= 0) {
      break;
    }
    r = -errno;
  } while (transient_error());
  return r;
}

inline int
UnixSocket::getsockname(struct sockaddr *sa, socklen_t *sz) const
{
  return ::getsockname(this->fd, sa, sz);
}

inline int
UnixSocket::get_sndbuf_size() const
{
  int bsz = 0;
  int bszsz, r;

  bszsz = sizeof(bsz);
  r     = safe_getsockopt(this->fd, SOL_SOCKET, SO_SNDBUF, (char *)&bsz, &bszsz);
  return (r == 0 ? bsz : r);
}

inline int
UnixSocket::get_rcvbuf_size() const
{
  int bsz = 0;
  int bszsz, r;

  bszsz = sizeof(bsz);
  r     = safe_getsockopt(this->fd, SOL_SOCKET, SO_RCVBUF, (char *)&bsz, &bszsz);
  return (r == 0 ? bsz : r);
}

inline int
UnixSocket::set_sndbuf_size(int bsz)
{
  return safe_setsockopt(this->fd, SOL_SOCKET, SO_SNDBUF, (char *)&bsz, sizeof(bsz));
}

inline int
UnixSocket::set_rcvbuf_size(int bsz)
{
  return safe_setsockopt(this->fd, SOL_SOCKET, SO_RCVBUF, (char *)&bsz, sizeof(bsz));
}

inline int
UnixSocket::shutdown(int how)
{
  int res;
  do {
    if (unlikely((res = ::shutdown(this->fd, how)) < 0)) {
      res = -errno;
    }
  } while (res == -EINTR);
  return res;
}

inline bool
transient_error()
{
  bool transient = (errno == EINTR);
#ifdef ENOMEM
  transient = transient || (errno == ENOMEM);
#endif
#ifdef ENOBUFS
  transient = transient || (errno == ENOBUFS);
#endif
  return transient;
}
