//
// request_parser.cpp
// ~~~~~~~~~~~~~~~~~~
//
// Copyright (c) 2003-2020 Christopher M. Kohlhoff (chris at kohlhoff dot com)
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//

#include "request_parser.hpp"
#include <algorithm>
#include <cctype>
#include <boost/lexical_cast.hpp>
#include "request.hpp"

namespace http {
namespace server4 {

// Enable the pseudo-keywords reenter, yield and fork.
#include <asio/yield.hpp>

std::string request_parser::content_length_name_ = "Content-Length";

boost::tribool request_parser::consume(request& req, char c)
{
  reenter (this)
  {
    req.method.clear();
    req.uri.clear();
    req.http_version_major = 0;
    req.http_version_minor = 0;
    req.headers.clear();
    req.content.clear();
    content_length_ = 0;

    // Request method.
    while (is_char(c) && !is_ctl(c) && !is_tspecial(c) && c != ' ')
    {
      req.method.push_back(c);
      yield return boost::indeterminate;
    }
    if (req.method.empty())
      return false;

    // Space.
    if (c != ' ') return false;
    yield return boost::indeterminate;

    // URI.
    while (!is_ctl(c) && c != ' ')
    {
      req.uri.push_back(c);
      yield return boost::indeterminate;
    }
    if (req.uri.empty()) return false;

    // Space.
    if (c != ' ') return false;
    yield return boost::indeterminate;

    // HTTP protocol identifier.
    if (c != 'H') return false;
    yield return boost::indeterminate;
    if (c != 'T') return false;
    yield return boost::indeterminate;
    if (c != 'T') return false;
    yield return boost::indeterminate;
    if (c != 'P') return false;
    yield return boost::indeterminate;

    // Slash.
    if (c != '/') return false;
    yield return boost::indeterminate;

    // Major version number.
    if (!is_digit(c)) return false;
    while (is_digit(c))
    {
      req.http_version_major = req.http_version_major * 10 + c - '0';
      yield return boost::indeterminate;
    }

    // Dot.
    if (c != '.') return false;
    yield return boost::indeterminate;

    // Minor version number.
    if (!is_digit(c)) return false;
    while (is_digit(c))
    {
      req.http_version_minor = req.http_version_minor * 10 + c - '0';
      yield return boost::indeterminate;
    }

    // CRLF.
    if (c != '\r') return false;
    yield return boost::indeterminate;
    if (c != '\n') return false;
    yield return boost::indeterminate;

    // Headers.
    while ((is_char(c) && !is_ctl(c) && !is_tspecial(c) && c != '\r')
        || (c == ' ' || c == '\t'))
    {
      if (c == ' ' || c == '\t')
      {
        // Leading whitespace. Must be continuation of previous header's value.
        if (req.headers.empty()) return false;
        while (c == ' ' || c == '\t')
          yield return boost::indeterminate;
      }
      else
      {
        // Start the next header.
        req.headers.push_back(header());

        // Header name.
        while (is_char(c) && !is_ctl(c) && !is_tspecial(c) && c != ':')
        {
          req.headers.back().name.push_back(c);
          yield return boost::indeterminate;
        }

        // Colon and space separates the header name from the header value.
        if (c != ':') return false;
        yield return boost::indeterminate;
        if (c != ' ') return false;
        yield return boost::indeterminate;
      }

      // Header value.
      while (is_char(c) && !is_ctl(c) && c != '\r')
      {
        req.headers.back().value.push_back(c);
        yield return boost::indeterminate;
      }

      // CRLF.
      if (c != '\r') return false;
      yield return boost::indeterminate;
      if (c != '\n') return false;
      yield return boost::indeterminate;
    }

    // CRLF.
    if (c != '\r') return false;
    yield return boost::indeterminate;
    if (c != '\n') return false;

    // Check for optional Content-Length header.
    for (std::size_t i = 0; i < req.headers.size(); ++i)
    {
      if (headers_equal(req.headers[i].name, content_length_name_))
      {
        try
        {
          content_length_ =
            boost::lexical_cast<std::size_t>(req.headers[i].value);
        }
        catch (boost::bad_lexical_cast&)
        {
          return false;
        }
      }
    }

    // Content.
    while (req.content.size() < content_length_)
    {
      yield return boost::indeterminate;
      req.content.push_back(c);
    }
  }

  return true;
}

// Disable the pseudo-keywords reenter, yield and fork.
#include <asio/unyield.hpp>

bool request_parser::is_char(int c)
{
  return c >= 0 && c <= 127;
}

bool request_parser::is_ctl(int c)
{
  return (c >= 0 && c <= 31) || (c == 127);
}

bool request_parser::is_tspecial(int c)
{
  switch (c)
  {
  case '(': case ')': case '<': case '>': case '@':
  case ',': case ';': case ':': case '\\': case '"':
  case '/': case '[': case ']': case '?': case '=':
  case '{': case '}': case ' ': case '\t':
    return true;
  default:
    return false;
  }
}

bool request_parser::is_digit(int c)
{
  return c >= '0' && c <= '9';
}

bool request_parser::tolower_compare(char a, char b)
{
  return std::tolower(a) == std::tolower(b);
}

bool request_parser::headers_equal(const std::string& a, const std::string& b)
{
  if (a.length() != b.length())
    return false;

  return std::equal(a.begin(), a.end(), b.begin(),
      &request_parser::tolower_compare);
}

} // namespace server4
} // namespace http
