#pragma once
#include <cctype>
#include <string_view>

#include "utils.hpp"

namespace cinatra {
// most of this code is from cpprestsdk
class uri_t {
 public:
  std::string_view schema;
  std::string_view uinfo;
  std::string_view host;
  std::string_view port;
  std::string_view path;
  std::string_view query;
  std::string_view fragment;
  bool is_ssl;

  bool is_unreserved(int c) {
    return std::isalnum((char)c) || c == '-' || c == '.' || c == '_' ||
           c == '~';
  }

  bool is_sub_delim(int c) {
    switch (c) {
      case '!':
      case '$':
      case '&':
      case '\'':
      case '(':
      case ')':
      case '*':
      case '+':
      case ',':
      case ';':
      case '=':
        return true;
      default:
        return false;
    }
  }

  bool is_websocket() { return schema == "ws"sv || schema == "wss"sv; }

  bool is_user_info_character(int c) {
    return is_unreserved(c) || is_sub_delim(c) || c == '%' || c == ':';
  }
  bool is_path_character(int c) {
    return is_unreserved(c) || is_sub_delim(c) || c == '%' || c == '/' ||
           c == ':' || c == '@';
  }
  bool is_query_character(int c) { return is_path_character(c) || c == '?'; }

  bool is_authority_character(int c) {
    return is_unreserved(c) || is_sub_delim(c) || c == '%' || c == '@' ||
           c == ':' || c == '[' || c == ']';
  }
  bool is_fragment_character(int c) {
    // this is intentional, they have the same set of legal characters
    return is_query_character(c);
  }

  bool is_scheme_character(int c) {
    return std::isalnum((char)c) || c == '+' || c == '-' || c == '.';
  }

  bool parse_from(const char *encoded) {
    const char *p = encoded;

    // IMPORTANT -- A uri may either be an absolute uri, or an
    // relative-reference Absolute: 'http://host.com' Relative-Reference:
    // '//:host.com', '/path1/path2?query', './path1:path2' A Relative-Reference
    // can be disambiguated by parsing for a ':' before the first slash

    bool is_relative_reference = true;
    const char *p2 = p;
    for (; *p2 != ('/') && *p2 != ('\0'); p2++) {
      if (*p2 == (':')) {
        // found a colon, the first portion is a scheme
        is_relative_reference = false;
        break;
      }
    }

    if (!is_relative_reference) {
      // the first character of a scheme must be a letter
      if (!isalpha(*p)) {
        return false;
      }

      // start parsing the scheme, it's always delimited by a colon (must be
      // present)
      auto scheme_begin = p++;
      for (; *p != ':'; p++) {
        if (!is_scheme_character(*p)) {
          return false;
        }
      }
      auto scheme_end = p;
      schema = std::string_view(scheme_begin, scheme_end - scheme_begin);
      is_ssl = (schema == "https" || schema == "wss");

      // skip over the colon
      p++;
    }

    // the uri must have http or https
    if (schema.empty()) {
      return false;
    }

    // if we see two slashes next, then we're going to parse the authority
    // portion later on we'll break up the authority into the port and host
    const char *authority_begin = nullptr;
    const char *authority_end = nullptr;
    if (*p == ('/') && p[1] == ('/')) {
      // skip over the slashes
      p += 2;
      authority_begin = p;

      // the authority is delimited by a slash (resource), question-mark (query)
      // or octothorpe (fragment) or by EOS. The authority could be empty
      // ('file:///C:\file_name.txt')
      for (; *p != ('/') && *p != ('?') && *p != ('#') && *p != ('\0'); p++) {
        // We're NOT currently supporting IPvFuture or username/password in
        // authority IPv6 as the host (i.e. http://[:::::::]) is allowed as
        // valid URI and passed to subsystem for support.
        if (!is_authority_character(*p)) {
          return false;
        }
      }
      authority_end = p;

      // now lets see if we have a port specified -- by working back from the
      // end
      if (authority_begin != authority_end) {
        // the port is made up of all digits
        const char *port_begin = authority_end - 1;
        for (; isdigit(*port_begin) && port_begin != authority_begin;
             port_begin--) {
        }

        const char *host_begin = nullptr;
        const char *host_end = nullptr;
        if (*port_begin == (':')) {
          // has a port
          host_begin = authority_begin;
          host_end = port_begin;

          // skip the colon
          port_begin++;

          port = std::string_view(port_begin, authority_end - port_begin);
        }
        else {
          // no port
          host_begin = authority_begin;
          host_end = authority_end;
        }

        // look for a user_info component
        const char *u_end = host_begin;
        for (; is_user_info_character(*u_end) && u_end != host_end; u_end++) {
        }

        if (*u_end == ('@')) {
          host_begin = u_end + 1;
          auto uinfo_begin = authority_begin;
          auto uinfo_end = u_end;
          uinfo = std::string_view(uinfo_begin, uinfo_end - uinfo_begin);
        }
        host = std::string_view(host_begin, host_end - host_begin);
      }
    }

    // if we see a path character or a slash, then the
    // if we see a slash, or any other legal path character, parse the path next
    if (*p == ('/') || is_path_character(*p)) {
      auto path_begin = p;

      // the path is delimited by a question-mark (query) or octothorpe
      // (fragment) or by EOS
      for (; *p != ('?') && *p != ('#') && *p != ('\0'); p++) {
        if (!is_path_character(*p)) {
          return false;
        }
      }
      auto path_end = p;
      path = std::string_view(path_begin, path_end - path_begin);
    }

    // if we see a ?, then the query is next
    if (*p == ('?')) {
      // skip over the question mark
      p++;
      auto query_begin = p;

      // the query is delimited by a '#' (fragment) or EOS
      for (; *p != ('#') && *p != ('\0'); p++) {
        if (!is_query_character(*p)) {
          return false;
        }
      }
      auto query_end = p;
      query = std::string_view(query_begin, query_end - query_begin);
    }

    // if we see a #, then the fragment is next
    if (*p == ('#')) {
      // skip over the hash mark
      p++;
      auto fragment_begin = p;

      // the fragment is delimited by EOS
      for (; *p != ('\0'); p++) {
        if (!is_fragment_character(*p)) {
          return false;
        }
      }
      auto fragment_end = p;
      fragment =
          std::string_view(fragment_begin, fragment_end - fragment_begin);
    }

    return true;
  }

  std::string get_host() const { return std::string(host); }

  std::string get_port() const {
    std::string port_str;
    if (is_ssl) {
      if (port.empty()) {
        port_str = "https";
      }
      else {
        port_str = std::string(port);
      }
    }
    else {
      if (port.empty()) {
        port_str = "http";
      }
      else {
        port_str = std::string(port);
      }
    }

    return port_str;
  }

  std::string get_path() const {
    if (path.empty())
      return "/";

    return std::string(path);
  }

  std::string get_query() const { return std::string(query); }
};

inline std::string url_encode(const std::string &str) {
  std::string new_str = "";
  char c;
  int ic;
  const char *chars = str.c_str();
  char buf_hex[10];
  size_t len = strlen(chars);

  for (size_t i = 0; i < len; i++) {
    c = chars[i];
    ic = c;
    // uncomment this if you want to encode spaces with +
    /*if (c==' ') new_str += '+';
    else */
    if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~')
      new_str += c;
    else {
      sprintf(buf_hex, "%X", c);
      if (ic < 16)
        new_str += "%0";
      else
        new_str += "%";
      new_str += buf_hex;
    }
  }
  return new_str;
}

struct context {
  std::string host;
  std::string port;
  std::string path;
  std::string query;
  std::string body;
  http_method method = http_method::UNKNOW;

  context() = default;
  context(const uri_t &u, http_method mthd)
      : host(u.get_host()),
        port(u.get_port()),
        path(u.get_path()),
        query(u.get_query()),
        method(mthd) {}
  context(const uri_t &u, http_method mthd, std::string b)
      : host(u.get_host()),
        port(u.get_port()),
        path(u.get_path()),
        query(u.get_query()),
        method(mthd),
        body(std::move(b)) {}
};
}  // namespace cinatra