/****************************************************************************
 * netutils/ntpclient/ntpclient.c
 *
 *   Copyright (C) 2014, 2016, 2020 Gregory Nutt. All rights reserved.
 *   Author: Gregory Nutt <gnutt@nuttx.org>
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 * 3. Neither the name NuttX nor the names of its contributors may be
 *    used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
 * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 ****************************************************************************/

/****************************************************************************
 * Included Files
 ****************************************************************************/

#include <nuttx/config.h>

#include <sys/socket.h>
#include <sys/time.h>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <sched.h>
#include <errno.h>
#include <debug.h>

#include <netinet/in.h>

#ifdef CONFIG_LIBC_NETDB
#  include <netdb.h>
#  include <arpa/inet.h>
#endif

#include "netutils/ntpclient.h"

#include "ntpv3.h"

/****************************************************************************
 * Pre-processor Definitions
 ****************************************************************************/

/* Configuration ************************************************************/

#if defined(CONFIG_LIBC_NETDB) && !defined(CONFIG_NETUTILS_NTPCLIENT_SERVER)
#  error CONFIG_NETUTILS_NTPCLIENT_SERVER my be provided
#endif

#if !defined(CONFIG_LIBC_NETDB) && !defined(CONFIG_NETUTILS_NTPCLIENT_SERVERIP)
#  error CONFIG_NETUTILS_NTPCLIENT_SERVERIP my be provided
#endif

/* NTP Time is seconds since 1900. Convert to Unix time which is seconds
 * since 1970
 */

#define NTP2UNIX_TRANLSLATION 2208988800u
#define NTP_VERSION          3

/****************************************************************************
 * Private Types
 ****************************************************************************/

/* This enumeration describes the state of the NTP daemon */

enum ntpc_daemon_e
{
  NTP_NOT_RUNNING = 0,
  NTP_STARTED,
  NTP_RUNNING,
  NTP_STOP_REQUESTED,
  NTP_STOPPED
};

/* This type describes the state of the NTP client daemon.  Only one
 * instance of the NTP daemon is permitted in this implementation.
 */

struct ntpc_daemon_s
{
  uint8_t state; /* See enum ntpc_daemon_e */
  sem_t lock;    /* Used to protect the whole structure */
  sem_t sync;    /* Used to synchronize start and stop events */
  pid_t pid;     /* Task ID of the NTP daemon */
};

/****************************************************************************
 * Private Data
 ****************************************************************************/

/* This type describes the state of the NTP client daemon.  Only one
 * instance of the NTP daemon is permitted in this implementation.  This
 * limitation is due only to this global data structure.
 */

static struct ntpc_daemon_s g_ntpc_daemon =
{
  NTP_NOT_RUNNING,
  SEM_INITIALIZER(1),
  SEM_INITIALIZER(0),
  -1
};

/****************************************************************************
 * Private Functions
 ****************************************************************************/

/****************************************************************************
 * Name: ntpc_getuint32
 *
 * Description:
 *   Return the big-endian, 4-byte value in network (big-endian) order.
 *
 ****************************************************************************/

static inline uint32_t ntpc_getuint32(FAR uint8_t *ptr)
{
  /* Network order is big-endian; host order is irrelevant */

  return (uint32_t)ptr[3] |          /* MS byte appears first in data stream */
         ((uint32_t)ptr[2] << 8) |
         ((uint32_t)ptr[1] << 16) |
         ((uint32_t)ptr[0] << 24);
}

/****************************************************************************
 * Name: ntpc_settime
 *
 * Description:
 *   Given the NTP time in seconds, set the system time
 *
 ****************************************************************************/

static void ntpc_settime(FAR uint8_t *timestamp)
{
  struct timespec tp;
  time_t seconds;
  uint32_t frac;
  uint32_t nsec;
#ifdef CONFIG_HAVE_LONG_LONG
  uint64_t tmp;
#else
  uint32_t a16;
  uint32_t b0;
  uint32_t t32;
  uint32_t t16;
  uint32_t t0;
#endif

  /* NTP timestamps are represented as a 64-bit fixed-point number, in
   * seconds relative to 0000 UT on 1 January 1900.  The integer part is
   * in the first 32 bits and the fraction part in the last 32 bits, as
   * shown in the following diagram.
   *
   *    0                   1                   2                   3
   *    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
   *   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   *   |                         Integer Part                          |
   *   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   *   |                         Fraction Part                         |
   *   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   */

  seconds = ntpc_getuint32(timestamp);

  /* Translate seconds to account for the difference in the origin time */

  if (seconds > NTP2UNIX_TRANLSLATION)
    {
      seconds -= NTP2UNIX_TRANLSLATION;
    }

  /* Conversion of the fractional part to nanoseconds:
   *
   *  NSec = (f * 1,000,000,000) / 4,294,967,296
   *       = (f * (5**9 * 2**9) / (2**32)
   *       = (f * 5**9) / (2**23)
   *       = (f * 1,953,125) / 8,388,608
   */

  frac = ntpc_getuint32(timestamp + 4);
#ifdef CONFIG_HAVE_LONG_LONG
  /* if we have 64-bit long long values, then the computation is easy */

  tmp  = ((uint64_t)frac * 1953125) >> 23;
  nsec = (uint32_t)tmp;

#else
  /* If we don't have 64 bit integer types, then the calculation is a little
   * more complex:
   *
   * Let f         = a    << 16 + b
   *     1,953,125 = 0x1d << 16 + 0xcd65
   * NSec << 23 =  ((a << 16) + b) * ((0x1d << 16) + 0xcd65)
   *            = (a << 16) * 0x1d << 16) +
   *              (a << 16) * 0xcd65 +
   *              b         * 0x1d << 16) +
   *              b         * 0xcd65;
   */

  /* Break the fractional part up into two values */

  a16  = frac >> 16;
  b0   = frac & 0xffff;

  /* Get the b32 and b0 terms
   *
   * t32 = (a << 16) * 0x1d << 16)
   * t0  = b * 0xcd65
   */

  t32  = 0x001d * a16;
  t0   = 0xcd65 * b0;

  /* Get the first b16 term
   *
   * (a << 16) * 0xcd65
   */

  t16  = 0xcd65 * a16;

  /* Add the upper 16-bits to the b32 accumulator */

  t32 += (t16 >> 16);

  /* Add the lower 16-bits to the b0 accumulator, handling carry to the b32
   * accumulator
   */

  t16  <<= 16;
  if (t0 > (0xffffffff - t16))
    {
      t32++;
    }

  t0 += t16;

  /* Get the second b16 term
   *
   * b * (0x1d << 16)
   */

  t16  = 0x001d * b0;

  /* Add the upper 16-bits to the b32 accumulator */

  t32 += (t16 >> 16);

  /* Add the lower 16-bits to the b0 accumulator, handling carry to the b32
   * accumulator
   */

  t16  <<= 16;
  if (t0 > (0xffffffff - t16))
    {
      t32++;
    }

  t0 += t16;

  /* t32 and t0 represent the 64 bit product.  Now shift right by 23 bits to
   * accomplish the divide by by 2**23.
   */

  nsec = (t32 << (32 - 23)) + (t0 >> 23);
#endif

  /* Set the system time */

  tp.tv_sec  = seconds;
  tp.tv_nsec = nsec;
  clock_settime(CLOCK_REALTIME, &tp);

  sinfo("Set time to %lu seconds: %d\n", (unsigned long)tp.tv_sec, ret);
}

/****************************************************************************
 * Name: ntpc_daemon
 *
 * Description:
 *   This the NTP client daemon.  This is a *very* minimal
 *   implementation! An NTP request is and the system clock is set when the
 *   response is received
 *
 ****************************************************************************/

static int ntpc_daemon(int argc, char **argv)
{
  struct sockaddr_in server;
  struct ntp_datagram_s pkt;
  struct timeval tv;

#ifdef CONFIG_LIBC_NETDB
  struct hostent *he;
  struct in_addr **addr_list;
#endif

  socklen_t socklen;
  ssize_t nbytes;
  int exitcode = EXIT_SUCCESS;
  int retry = 0;
  int sd;
  int ret;

  /* Indicate that we have started */

  g_ntpc_daemon.state = NTP_RUNNING;
  sem_post(&g_ntpc_daemon.sync);

  /* Create a datagram socket  */

  sd = socket(AF_INET, SOCK_DGRAM, 0);
  if (sd < 0)
    {
      nerr("ERROR: socket failed: %d\n", errno);

      g_ntpc_daemon.state = NTP_STOPPED;
      sem_post(&g_ntpc_daemon.sync);
      return EXIT_FAILURE;
    }

  /* Setup a receive timeout on the socket */

  tv.tv_sec  = 5;
  tv.tv_usec = 0;

  ret = setsockopt(sd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(struct timeval));
  if (ret < 0)
    {
      nerr("ERROR: setsockopt failed: %d\n", errno);

      g_ntpc_daemon.state = NTP_STOPPED;
      sem_post(&g_ntpc_daemon.sync);
      return EXIT_FAILURE;
    }

  /* Setup or sockaddr_in struct with information about the server we are
   * going to ask the time from.
   */

  memset(&server, 0, sizeof(struct sockaddr_in));
  server.sin_family      = AF_INET;
  server.sin_port        = htons(CONFIG_NETUTILS_NTPCLIENT_PORTNO);

#ifndef CONFIG_LIBC_NETDB
  server.sin_addr.s_addr = htonl(CONFIG_NETUTILS_NTPCLIENT_SERVERIP);
#else
  he = gethostbyname(CONFIG_NETUTILS_NTPCLIENT_SERVER);
  if (he != NULL && he->h_addrtype == AF_INET)
    {
      addr_list = (struct in_addr **)he->h_addr_list;
      server.sin_addr.s_addr = addr_list[0]->s_addr;
      ninfo("INFO: '%s' resolved to: %s\n",
            CONFIG_NETUTILS_NTPCLIENT_SERVER,
            inet_ntoa(server.sin_addr));
    }
  else
    {
      nerr("ERROR: Failed to resolve '%s'\n",
           CONFIG_NETUTILS_NTPCLIENT_SERVER);
      return EXIT_FAILURE;
    }
#endif

  /* Here we do the communication with the NTP server.  This is a very simple
   * client architecture.  A request is sent and then a NTP packet is
   * received and used to set the current time.
   *
   * NOTE that the scheduler is locked whenever this loop runs.  That
   * assures both:  (1) that there are no asynchronous stop requests and
   * (2) that we are not suspended while in critical moments when we about
   * to set the new time.  This sounds harsh, but this function is suspended
   * most of the time either: (1) sending a datagram, (2) receiving a
   * datagram, or (3) waiting for the next poll cycle.
   *
   * TODO: The first datagram that is sent is usually lost.  That is because
   * the MAC address of the NTP server is not in the ARP table.  This is
   * particularly bad here because the request will not be sent again until
   * the long delay expires leaving the system with bad time for a long time
   * initially.  Solutions:
   *
   * 1. Fix send logic so that it assures that the ARP request has been
   *    sent and the entry is in the ARP table before sending the packet
   *    (best).
   * 2. Add some ad hoc logic here so that there is no delay until at least
   *    one good time is received.
   */

  sched_lock();
  while (g_ntpc_daemon.state != NTP_STOP_REQUESTED)
    {
      /* Format the transmit datagram */

      memset(&pkt, 0, sizeof(pkt));
      pkt.lvm = MKLVM(0, NTP_VERSION, 3);

      sinfo("Sending a NTP packet\n");

      ret = sendto(sd, &pkt, sizeof(struct ntp_datagram_s),
                   0, (FAR struct sockaddr *)&server,
                   sizeof(struct sockaddr_in));

      if (ret < 0)
        {
          /* Check if we received a signal.  That is not an error but
           * other error events will terminate the client.
           */

          int errval = errno;
          if (errval != EINTR)
            {
              nerr("ERROR: sendto() failed: %d\n", errval);
              exitcode = EXIT_FAILURE;
              break;
            }

          /* Go back to the top of the loop if we were interrupted
           * by a signal.  The signal might mean that we were
           * requested to stop(?)
           */

          continue;
        }

      /* Attempt to receive a packet (with a timeout that was set up via
       * setsockopt() above)
       */

      socklen = sizeof(struct sockaddr_in);
      nbytes = recvfrom(sd, (void *)&pkt, sizeof(struct ntp_datagram_s),
                        0, (FAR struct sockaddr *)&server, &socklen);

      /* Check if the received message was long enough to be a valid NTP
       * datagram.
       */

      if (nbytes >= (ssize_t)NTP_DATAGRAM_MINSIZE)
        {
          sinfo("Setting time\n");
          ntpc_settime(pkt.recvtimestamp);
          retry = 0;
        }

      /* Check for errors.  Note that properly received, short datagrams
       * are simply ignored.
       */

      else if (nbytes < 0)
        {
          /* Check if we received a signal.  That is not an error but
           * other error events will terminate the client.
           */

          int errval = errno;
          if (errval != EINTR)
            {
              /* Allow up to three retries */

              if (++retry < 3)
                {
                  continue;
                }

              /* Then declare the failure */

              nerr("ERROR: recvfrom() failed: %d\n", errval);
              exitcode = EXIT_FAILURE;
              break;
            }
        }

      /* A full implementation of an NTP client would require much more.  I
       * think we can skip most of that here.
       */

      if (g_ntpc_daemon.state == NTP_RUNNING)
        {
          sinfo("Waiting for %d seconds\n",
                CONFIG_NETUTILS_NTPCLIENT_POLLDELAYSEC);

          sleep(CONFIG_NETUTILS_NTPCLIENT_POLLDELAYSEC);
        }
    }

  /* The NTP client is terminating */

  sched_unlock();

  g_ntpc_daemon.state = NTP_STOPPED;
  sem_post(&g_ntpc_daemon.sync);
  return exitcode;
}

/****************************************************************************
 * Public Functions
 ****************************************************************************/

/****************************************************************************
 * Name: ntpc_start
 *
 * Description:
 *   Start the NTP daemon
 *
 * Returned Value:
 *   On success, the non-negative task ID of the NTPC daemon is returned;
 *   On failure, a negated errno value is returned.
 *
 ****************************************************************************/

int ntpc_start(void)
{
  /* Is the NTP in a non-running state? */

  sem_wait(&g_ntpc_daemon.lock);
  if (g_ntpc_daemon.state == NTP_NOT_RUNNING ||
      g_ntpc_daemon.state == NTP_STOPPED)
    {
      /* Start the NTP daemon */

      g_ntpc_daemon.state = NTP_STARTED;
      g_ntpc_daemon.pid =
        task_create("NTP daemon", CONFIG_NETUTILS_NTPCLIENT_SERVERPRIO,
                    CONFIG_NETUTILS_NTPCLIENT_STACKSIZE, ntpc_daemon,
                    NULL);

      /* Handle failures to start the NTP daemon */

      if (g_ntpc_daemon.pid < 0)
        {
          int errval = errno;
          DEBUGASSERT(errval > 0);

          g_ntpc_daemon.state = NTP_STOPPED;
          nerr("ERROR: Failed to start the NTP daemon\n", errval);
          sem_post(&g_ntpc_daemon.lock);
          return -errval;
        }

      /* Wait for any daemon state change */

      do
        {
          sem_wait(&g_ntpc_daemon.sync);
        }
      while (g_ntpc_daemon.state == NTP_STARTED);
    }

  sem_post(&g_ntpc_daemon.lock);
  return g_ntpc_daemon.pid;
}

/****************************************************************************
 * Name: ntpc_stop
 *
 * Description:
 *   Stop the NTP daemon
 *
 * Returned Value:
 *   Zero on success; a negated errno value on failure.  The current
 *   implementation only returns success.
 *
 ****************************************************************************/

int ntpc_stop(void)
{
  int ret;

  /* Is the NTP in a running state? */

  sem_wait(&g_ntpc_daemon.lock);
  if (g_ntpc_daemon.state == NTP_STARTED ||
      g_ntpc_daemon.state == NTP_RUNNING)
    {
      /* Yes.. request that the daemon stop. */

      g_ntpc_daemon.state = NTP_STOP_REQUESTED;

      /* Wait for any daemon state change */

      do
        {
          /* Signal the NTP client */

          ret = kill(g_ntpc_daemon.pid,
                     CONFIG_NETUTILS_NTPCLIENT_SIGWAKEUP);

          if (ret < 0)
            {
              nerr("ERROR: kill pid %d failed: %d\n",
                   g_ntpc_daemon.pid, errno);
              break;
            }

          /* Wait for the NTP client to respond to the stop request */

          sem_wait(&g_ntpc_daemon.sync);
        }
      while (g_ntpc_daemon.state == NTP_STOP_REQUESTED);
    }

  sem_post(&g_ntpc_daemon.lock);
  return OK;
}
