/****************************************************************************
 * apps/netutils/libcurl4nx/curl4nx_easy_perform.c
 *
 * 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.
 *
 ****************************************************************************/

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

#include <nuttx/config.h>
#include <nuttx/compiler.h>
#include <debug.h>

#include <sys/socket.h>
#include <sys/time.h>
#include <stdint.h>
#include <stdio.h>
#include <stdbool.h>
#include <stdlib.h>
#include <unistd.h>
#include <netdb.h>
#include <strings.h>
#include <errno.h>

#include <arpa/inet.h>
#include <netinet/in.h>

#include <nuttx/version.h>

#include "netutils/netlib.h"
#include "netutils/curl4nx.h"
#include "curl4nx_private.h"

#if defined(CONFIG_NETUTILS_CODECS)
#  if defined(CONFIG_CODECS_URLCODE)
#    define WGET_USE_URLENCODE 1
#    include "netutils/urldecode.h"
#  endif
#  if defined(CONFIG_CODECS_BASE64)
#    include "netutils/base64.h"
#  endif
#else
#  undef CONFIG_CODECS_URLCODE
#  undef CONFIG_CODECS_BASE64
#endif

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

#define CURL4NX_STATE_STATUSLINE   0
#define CURL4NX_STATE_STATUSCODE   1
#define CURL4NX_STATE_STATUSREASON 2
#define CURL4NX_STATE_HEADERS      3
#define CURL4NX_STATE_DATA_NORMAL  4
#define CURL4NX_STATE_DATA_CHUNKED 5

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

/****************************************************************************
 * Name: curl4nx_easy_perform()
 *
 * Description:
 *  Resolve the hostname to an IP address. There are 3 cases:
 *  - direct IP: inet_aton is used
 *  - using internal curl4nx resolution
 *  - DNS resolution, if available
 ****************************************************************************/

/****************************************************************************
 * Name: curl4nx_resolve
 * Description:
 *   Translate a host name to an IP address (V4 only for the moment)
 *   - either the host is a string with an IP
 *   - either the known hosts were defined by CURL4NXOPT_
 ****************************************************************************/

static int curl4nx_resolve(FAR struct curl4nx_s *handle, FAR char *hostname,
                          FAR struct in_addr *ip)
{
  int ret = inet_aton(hostname, ip);
  if (ret != 0)
    {
      return CURL4NXE_OK; /* IP address is valid */
    }

  curl4nx_warn("Not a valid IP address, trying hostname resolution\n");
  return CURL4NXE_COULDNT_RESOLVE_HOST;
}

/****************************************************************************
 * Name: curl4nx_is_header
 * Description:
 *   Return TRUE if buf contains header, then update off to point at the
 *   beginning of header value
 ****************************************************************************/

static int curl4nx_is_header(FAR char *buf, int len,
                            FAR const char *header, FAR int *off)
{
  if (strncasecmp(buf, header, strlen(header)))
    {
      return false;
    }

  *off = strlen(header);
  while ((*off) < len && buf[*off] == ' ')
    {
      (*off)++;
    }

  return true;
}

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

/****************************************************************************
 * Name: curl4nx_easy_perform()
 ****************************************************************************/

int curl4nx_easy_perform(FAR struct curl4nx_s *handle)
{
  struct sockaddr_in server;
  FILE *             stream;     /* IOSTREAM used to printf in the socket TODO avoid */
  int                rxoff;      /* Current offset within RX buffer */
  char               tmpbuf[16]; /* Buffer to hold small strings */
  int                tmplen;     /* Number of bytes used in tmpbuffer */
  int                state;      /* Current state of the parser */
  int                ret;        /* Return value from internal calls */
  char *             headerbuf;
  int                headerlen;
  char *             end;
  int                cret = CURL4NXE_OK; /* Public return value */
  bool               redirected = false; /* Boolean to manage HTTP redirections */
  bool               chunked = false;
  unsigned long long done = 0;
  int                redircount = 0;

  curl4nx_info("started\n");

  stream = NULL;
  headerbuf = malloc(CONFIG_LIBCURL4NX_MAXHEADERLINE);
  if (!headerbuf)
    {
      cret = CURL4NXE_OUT_OF_MEMORY;
      goto abort;
    }

  if (handle->host[0] == 0 || handle->port == 0)
    {
      /* URL has not been set */

      cret = CURL4NXE_URL_MALFORMAT;
      goto freebuf;
    }

  /* TODO: check that host and port have changed or are the same, so we can
   * recycle the socket for the next request.
   */

  do
    {
      handle->sockfd = socket(AF_INET, SOCK_STREAM, 0);
      if (handle->sockfd < 0)
        {
          /* socket failed.  It will set the errno appropriately */

          curl4nx_err("ERROR: socket failed: %d\n", errno);
          cret = CURL4NXE_COULDNT_CONNECT;
          goto freebuf;
        }

      /* TODO: timeouts */

      server.sin_family = AF_INET;
      server.sin_port   = htons(handle->port);
      cret = curl4nx_resolve(handle, handle->host, &server.sin_addr);
      if (cret != CURL4NXE_OK)
        {
          /* Could not resolve host (or malformed IP address) */

          curl4nx_err("ERROR: Failed to resolve hostname\n");
          cret = CURL4NXE_COULDNT_RESOLVE_HOST;
          goto freebuf;
        }

      /* Connect to server.  First we have to set some fields in the
       * 'server' address structure.  The system will assign me an arbitrary
       * local port that is not in use.
       */

      ret = connect(handle->sockfd,
                    (struct sockaddr *)&server, sizeof(struct sockaddr_in));
      if (ret < 0)
        {
          curl4nx_err("ERROR: connect failed: %d\n", errno);
          cret = CURL4NXE_COULDNT_CONNECT;
          goto close;
        }

      curl4nx_info("Connected...\n");

      stream = fdopen(handle->sockfd, "wb");

      /* Send request */

      fprintf(stream, "%s %s HTTP/%d.%d\r\n",
                      handle->method,
                      handle->path,
                      handle->version >> 4,
                      handle->version & 0x0f);

      /* Send headers */

      fprintf(stream, "Host: %s\r\n", handle->host);

      /* For the moment we do not support compression */

      fprintf(stream, "Content-encoding: identity\r\n");

      /* Send more headers */

      /* End of headers */

      fprintf(stream, "\r\n");

      /* TODO send data */

      fflush(stream);
      curl4nx_info("Request sent\n");

      /* Now wait for the result */

      redirected = false; /* for now */
      state      = CURL4NX_STATE_STATUSLINE;
      tmplen     = 0;

      while (1)
        {
          ret = recv(handle->sockfd, handle->rxbuf, handle->rxbufsize, 0);
          if (ret < 0)
            {
              curl4nx_err("RECV failed, errno=%d\n", errno);
              cret = CURL4NXE_RECV_ERROR;
              goto close;
            }
          else if (ret == 0)
            {
              curl4nx_err("Connection lost\n");
              cret = CURL4NXE_GOT_NOTHING;
              goto close;
            }

          curl4nx_info("Received %d bytes\n", ret);
          rxoff = 0;

          if (state == CURL4NX_STATE_STATUSLINE)
            {
              /* Accumulate HTTP/x.y until space */

              while ((tmplen < sizeof(tmpbuf)) && (rxoff < ret))
                {
                  if (handle->rxbuf[rxoff] == ' ')
                    {
                      /* found space after http version */

                      tmpbuf[tmplen] = 0; /* Finish pending string */
                      rxoff++;
                      curl4nx_info("received version: [%s]\n", tmpbuf);
                      tmplen = 0; /* reset buffer to look at code */
                      state = CURL4NX_STATE_STATUSCODE;
                      break;
                    }

                  /* Not a space: accumulate chars */

                  tmpbuf[tmplen] = handle->rxbuf[rxoff];
                  tmplen++;
                  rxoff++;
                }

              /* Check for overflow,
               * version code should not fill the tmpbuf
               */

              if (tmplen == sizeof(tmpbuf))
                {
                  /* extremely long http version -> invalid response */

                  curl4nx_err("Buffer overflow while reading version\n");
                  cret = CURL4NXE_RECV_ERROR;
                  goto close;
                }

              /* No overflow and no space found: wait for next buffer */
            }

          /* NO ELSE HERE, state may have changed and require new management
           * for the same rx buffer.
           */

          if (state == CURL4NX_STATE_STATUSCODE)
            {
              /* Accumulate response code until space */

              while ((tmplen < sizeof(tmpbuf)) && (rxoff < ret))
                {
                  if (handle->rxbuf[rxoff] == ' ')
                    {
                      /* Found space after http version */

                      tmpbuf[tmplen] = 0; /* Finish pending string */
                      rxoff++;
                      curl4nx_info("received code: [%s]\n", tmpbuf);
                      handle->status = strtol(tmpbuf, &end, 10);

                      if (*end != 0)
                        {
                          curl4nx_err("Bad status code [%s]\n", tmpbuf);
                          cret = CURL4NXE_RECV_ERROR;
                          goto close;
                        }

                      tmplen = 0; /* reset buffer to look at code */
                      state = CURL4NX_STATE_STATUSREASON;
                      break;
                    }

                  /* Not a space: accumulate chars */

                  tmpbuf[tmplen] = handle->rxbuf[rxoff];
                  tmplen++;
                  rxoff++;
                }

              /* Check for overflow,
               * version code should not fill the tmpbuf
               */

              if (tmplen == sizeof(tmpbuf))
                {
                  /* Extremely long http code -> invalid response */

                  curl4nx_err("Buffer overflow while reading code\n");
                  cret = CURL4NXE_RECV_ERROR;
                  goto close;
                }

              /* No overflow and no space found: wait for next buffer */
            }

          if (state == CURL4NX_STATE_STATUSREASON)
            {
              /* Accumulate response code until CRLF */

              while (rxoff < ret)
                {
                  if (handle->rxbuf[rxoff] == 0x0d ||
                     handle->rxbuf[rxoff] == 0x0a)
                    {
                      /* Accumulate all contiguous CR and LF in any order */

                      tmpbuf[tmplen] = handle->rxbuf[rxoff];
                      tmplen++;
                      if (tmplen == 2)
                        {
                          if (tmpbuf[0] == 0x0d && tmpbuf[1] == 0x0a)
                            {
                              headerlen = 0;
                              handle->content_length = 0;
                              state = CURL4NX_STATE_HEADERS;
                              break;
                            }
                          else
                            {
                              tmplen = 0; /* Reset search for CRLF */
                            }
                        }
                    }
                  else
                    {
                      /* This char is not interesting: reset storage */

                      tmplen = 0;

                      /* curl4nx_info("-> %c\n", handle->rxbuf[rxoff]); */
                    }

                  rxoff++;
                }
            }

          if (state == CURL4NX_STATE_HEADERS)
            {
              while (rxoff < ret)
                {
                  if (handle->rxbuf[rxoff] == 0x0d ||
                     handle->rxbuf[rxoff] == 0x0a)
                    {
                      /* Accumulate all contiguous CR and LF in any order */

                      tmpbuf[tmplen] = handle->rxbuf[rxoff];
                      tmplen++;
                      if (tmplen == 2)
                        {
                          if (tmpbuf[0] == 0x0d && tmpbuf[1] == 0x0a)
                            {
                              if (headerlen == 0) /* Found an empty header */
                                {
                                  curl4nx_info("<End of headers>\n");
                                  rxoff++; /* Skip this char */
                                  state = chunked ?
                                           CURL4NX_STATE_DATA_CHUNKED :
                                           CURL4NX_STATE_DATA_NORMAL;
                                  break;
                                }
                              else
                                {
                                  int off;
                                  curl4nx_iofunc_f func;
                                  headerbuf[headerlen] = 0;
                                  func = handle->headerfunc;
                                  if (func == NULL)
                                    {
                                      func = handle->writefunc;
                                    }

                                  func(headerbuf, headerlen, 1,
                                       handle->headerdata);

                                  /* Find the content-length */

                                  if (curl4nx_is_header(headerbuf, headerlen,
                                                        "content-length:",
                                                        &off))
                                    {
                                      handle->content_length =
                                        strtoull(headerbuf + off, &end, 10);
                                      if (*end != 0)
                                        {
                                          curl4nx_err("Stray chars after "
                                                      "content length!\n");
                                          cret = CURL4NXE_RECV_ERROR;
                                          goto close;
                                        }

                                      curl4nx_info("Found content length: "
                                                   "%llu\n",
                                                   handle->content_length);
                                    }

                                  /* Find the transfer encoding */

                                  if (curl4nx_is_header(headerbuf, headerlen,
                                                        "transfer-encoding:",
                                                        &off))
                                    {
                                      chunked = !strncasecmp(headerbuf + off,
                                                             "chunked", 7);
                                      if (chunked)
                                        {
                                          curl4nx_info("Transfer using "
                                                       "chunked format\n");
                                        }
                                    }

                                  /* Find the location */

                                  if (curl4nx_is_header(headerbuf, headerlen,
                                                        "location:",
                                                        &off))
                                    {
                                      /* Parse the new URL if we get a
                                       * redirection code.
                                       */

                                      if (handle->status >= 300 &&
                                          handle->status < 400)
                                        {
                                          if (handle->flags &
                                             CURL4NX_FLAGS_FOLLOWLOCATION)
                                            {
                                              if ((handle->max_redirs > 0) &&
                                                  (redircount >=
                                                   handle->max_redirs))
                                                {
                                                  curl4nx_info(
                                                  "Too many redirections\n");
                                                  cret =
                                                  CURL4NXE_TOO_MANY_REDIRECTS;
                                                  goto close;
                                                }

                                              cret =
                                                curl4nx_easy_setopt(
                                                        handle,
                                                        CURL4NXOPT_URL,
                                                        headerbuf + off);
                                              if (cret != CURL4NXE_OK)
                                                {
                                                  goto close;
                                                }

                                              redirected = true;
                                              redircount += 1;
                                              curl4nx_info("
                                                REDIRECTION (%d) -> %s\n",
                                                redircount,
                                                headerbuf + off);
                                            }
                                        }
                                    }

                                  /* Prepare for next header */

                                  headerlen = 0;
                                  tmplen    = 0; /* Reset search for CRLF */
                                }
                            }
                          else
                            {
                              tmplen = 0; /* Reset search for CRLF */
                            }
                        }
                    }
                  else
                    {
                      tmplen = 0; /* Reset CRLF detection */

                      /* Plus one for final zero */

                      if (headerlen < (CONFIG_LIBCURL4NX_MAXHEADERLINE - 1))
                        {
                          headerbuf[headerlen] = handle->rxbuf[rxoff];
                          headerlen++;
                        }
                      else
                        {
                          curl4nx_warn("prevented header overload\n");
                        }
                    }

                  rxoff++;
                }
            }

          if (state == CURL4NX_STATE_DATA_NORMAL)
            {
              if ((handle->flags & CURL4NX_FLAGS_FAILONERROR) &&
                   handle->status >= 400)
                {
                  cret = CURL4NXE_HTTP_RETURNED_ERROR;
                  goto close;
                }

              curl4nx_info("now in normal data state, rxoff=%d rxlen=%d\n",
                           rxoff, ret);
              done += (ret - rxoff);

              handle->writefunc(handle->rxbuf + rxoff, ret - rxoff, 1,
                                handle->writedata);
              if (handle->content_length != 0)
                {
                  curl4nx_info("Done %llu of %llu\n",
                               done, handle->content_length);

                  if (handle->progressfunc)
                    {
                      handle->progressfunc(handle->progressdata,
                                           handle->content_length,
                                           done, 0, 0);
                    }

                  if (handle->content_length == done)
                    {
                      /* Transfer is complete */

                      goto close;
                    }
                }
            }

          if (state == CURL4NX_STATE_DATA_CHUNKED)
            {
              if ((handle->flags & CURL4NX_FLAGS_FAILONERROR) &&
                  handle->status >= 400)
                {
                  cret = CURL4NXE_HTTP_RETURNED_ERROR;
                  goto close;
                }

              curl4nx_info("now in chunked data state, rxoff=%d rxlen=%d\n",
                           rxoff, ret);
              curl4nx_err("Not supported yet.\n");
              goto close;
            }
        }

      /* Done with this connection - this will also close the socket */

close:
      curl4nx_info("Closing\n");
      if (stream)
        {
          fclose(stream);
        }
    }
  while (redirected);

freebuf:
  free(headerbuf);

abort:
  curl4nx_info("done\n");
  return cret;
}