#include "::/Adam/Net/Socket"
#include "::/Adam/Net/UrlParse"

#define HTTP_ECONNECT   (-101)
#define HTTP_EPROTOCOL  (-102)
#define HTTP_EREQUEST   (-103)
#define HTTP_EREDIRECT  (-104)
#define HTTP_EEOF       (-105)
#define HTTP_ECONTENTLENGTH (-106)

#define HTTP_MAX_REDIRECTS  5
#define HTTP_USER_AGENT "Adam/Net/Http ($TX+CX,"TempleOS",D="DD_OS_NAME_VERSION"$)"

/**
 * @param len_out (required) requires the content length, or -1 if unspecified
 * @return socket (>= 0) on success, error code on failure
 */
I64 HttpOpenGet(U8* host, U16 port = 0, U8* path, I64* len_out,
    I64 allowed_redirects = HTTP_MAX_REDIRECTS) {
  U8 line[256];
  I64 error = 0;

  if (!port)
    port = 80;

  // Should this be done here though?
  if (*path == 0)
    path = "/";

  //"Connect(%s:%d)\n", host, port;
  I64 sock = create_connection(host, port);
  //"create_connection: %d\n", sock;
  if (sock >= 0) {
    StrPrint(line, "GET %s HTTP/1.0\r\n", path);
    sendString(sock, line, 0);
    StrPrint(line, "Host: %s\r\n", host);
    sendString(sock, line, 0);
    sendString(sock, "User-Agent: " HTTP_USER_AGENT "\r\n", 0);
    sendString(sock, "\r\n", 0);

    Bool haveHTTP = FALSE;
    U8* location = NULL;

    I64 code = -1;

    *len_out = -1;

    while (1) {
      error = recvLine(sock, line, sizeof(line), 0);

      if (error < 0) {
        break;
      }
      else if (error == 0) {
        if (!haveHTTP)
          error = HTTP_EPROTOCOL;
        break;
      }

      U8* delim;

      //"%s\n", line;
      if (!haveHTTP) {
        delim = StrFirstOcc(line, " ");
        if (delim && StrNCmp(line, "HTTP/", 5) == 0) {
          code = Str2I64(delim + 1);

          if (code >= 200 && code <= 399) {
            haveHTTP = TRUE;
          }
          else {
            error = HTTP_EREQUEST;
            break;
          }
        }
        else {
          error = HTTP_EREQUEST;
          break;
        }
      }
      else {
        delim = StrFirstOcc(line, ":");

        if (!delim) {
          error = HTTP_EPROTOCOL;
          break;
        }

        *delim = 0;

        do { delim++; }
        while (*delim == ' ');

        //"%s=%s\n", line, delim;
        if (!StrCmp(line, "Content-Length")) {
          StrScan(delim, "%d", len_out);
        }
        else if (!StrCmp(line, "Location")) {
          // This will leak on malformed response
          location = StrNew(delim);
        }
      }
    }

    // HTTP Code 3xx -- Redirection
    if (!error && code >= 300 && code <= 399) {
      if (allowed_redirects > 0) {
        CUrl curl;
        UrlInit(&curl);

        if (UrlParse(location, &curl)) {
          if (!StrCmp(curl.protocol, "http")) {
            close(sock);
            sock = HttpOpenGet(curl.host, curl.port, curl.path, len_out, allowed_redirects - 1);

            if (sock < 0)
              error = sock;
          }
          else
            error = HTTP_EPROTOCOL;
        }
        else
          error = HTTP_EREDIRECT;

        UrlFree(&curl);
      }
      else {
        error = HTTP_EREDIRECT;
      }
    }

    Free(location);
  }
  else
    error = HTTP_ECONNECT;

  if (error) {
    close(sock);
    return error;
  }
  else {
    return sock;
  }
}

I64 HttpGet(U8* host, U16 port = 0, U8* path, U8** data_out = NULL, I64* len_out = NULL) {
  I64 error = 0;
  I64 len = 0;
  I64 sock = HttpOpenGet(host, port, path, &len);

  if (sock > 0) {
    if (len >= 0) {
      U8* data = MAlloc(1 + len);
      I64 total = 0;

      while (total < len) {
        I64 step = len - total;

        if (step > 1024)
          step = 1024;

        I64 got = recv(sock, data + total, step, 0);

        if (got <= 0) {
          error = HTTP_EEOF;
          break;
        }

        total += got;
      }

      if (error) {
        Free(data);
      }
      else {
        if (data_out) {
          data[total] = 0;
          *data_out = data;
        }

        if (len_out)
          *len_out = len;
      }
    }
    else
      // We currently don't handle dynamic-length HTTP responses
      error = HTTP_ECONTENTLENGTH;

    close(sock);
  }
  else
    error = sock;

  return error;
}
