text: #pragma once
  
  // Only there to split the client.cpp file
  // Should be included only there
  
  #include <boost/version.hpp>
  
  #include <deque>
  #include <atomic>
  
  namespace beauty
  {
  // --------------------------------------------------------------------------
  template<bool SSL>
  class session_client : public std::enable_shared_from_this<session_client<SSL>>
  {
  public:
      using stream_type = std::conditional_t<SSL,
              asio::ssl::stream<asio::ip::tcp::socket>,
              void*>;
  
  private:
      // Store all informations on a request
      struct request_context {
          explicit request_context(asio::io_context& ioc) :
              timer(ioc)
          {
              response.body_limit(1024 * 1024 * 1024); // 1Go
          }
          asio::steady_timer      timer;
          bool                    too_late{false};
  
          beauty::request         request;
          beast::http::response_parser<beast::http::string_body> response;
  
          client::client_cb       cb;
  
          static
          std::shared_ptr<request_context> Create(asio::io_context& ioc, beauty::request&& req,
                  const beauty::url& url, const beauty::duration& d, std::optional<client::client_cb> cb = {}) {
  
              // Create a request context to pass on each callback
              auto req_ctx = std::make_shared<request_context>(ioc);
  
              // Set up an HTTP GET request message
              req_ctx->request = std::move(req);
              req_ctx->request.version(11);
              req_ctx->request.target(std::string(url.path()) + std::string(url.query()));
              req_ctx->request.set(beast::http::field::host, url.host());
              req_ctx->request.set(beast::http::field::user_agent, BEAUTY_PROJECT_VERSION);
              req_ctx->request.prepare_payload();
  
              if (cb) {
                  req_ctx->cb = std::move(*cb);
              }
  
              if (d.count()) {
                  req_ctx->timer.expires_after(d);
              }
  
              return req_ctx;
          }
      };
  
  public:
      template<bool U = SSL, typename std::enable_if_t<!U, int> = 0>
      explicit session_client(asio::io_context& ioc) :
              _ioc(ioc),
              _resolver(_ioc),
              _socket(_ioc),
  #if   (BOOST_VERSION < 107000)
            _strand(_socket.get_executor())
  #else
            _strand(asio::make_strand(ioc))
  #endif
      {}
  
      template<bool U = SSL, typename std::enable_if_t<U, int> = 0>
      session_client(asio::io_context& ioc, asio::ssl::context& ctx) :
              _ioc(ioc),
              _resolver(_ioc),
              _socket(_ioc),
              _stream(ioc, ctx),
  #if   (BOOST_VERSION < 107000)
            _strand(_socket.get_executor())
  #else
            _strand(asio::make_strand(ioc))
  #endif
      {}
  
      ~session_client() {
          // Gracefully close the socket
          boost::system::error_code ec;
          if constexpr(SSL) {
              _stream.shutdown(ec);
          }
          else {
              _socket.shutdown(asio::ip::tcp::socket::shutdown_both, ec);
          }
      }
  
      void run(beauty::request&& req, const beauty::url& url, const beauty::duration& d)
      {
          // Create a request context to pass on each callback
          auto req_ctx = request_context::Create(_ioc, std::move(req), url, d);
  
          run(req_ctx, url);
      }
  
      // Start the asynchronous request
      void run(beauty::request&& req, const beauty::url& url, const beauty::duration& d,
              client::client_cb&& cb)
      {
          // Create a request context to pass on each callback
          auto req_ctx = request_context::Create(_ioc, std::move(req), url, d, std::move(cb));
  
          run(req_ctx, url);
      }
  
      // Start the asynchronous request
      void run(std::shared_ptr<request_context> req_ctx, const beauty::url& url)
      {
          if (req_ctx->timer.expiry() != asio::steady_timer::time_point()) {
              req_ctx->timer.async_wait(
                  [me = this->shared_from_this(), req_ctx](const boost::system::error_code& ec) {
                      me->on_timer(ec, req_ctx);
                  });
          }
  
          std::lock_guard guard{_requests_mtx};
          bool connection_required = false;
          if constexpr(SSL) {
              connection_required = (_requests.empty() && !_stream.next_layer().is_open());
          } else {
              connection_required = (_requests.empty() && !_socket.is_open());
          }
          //std::cout << "Start a request" << std::endl;
          _requests.push_back(req_ctx);
  
          if (connection_required) {
              //std::cout << "Try to resolve the host" << std::endl;
  
              // Set SNI Hostname (many hosts need this to handshake successfully)
              if constexpr(SSL) {
                  std::string host{url.host()};
                  if (!SSL_set_tlsext_host_name(_stream.native_handle(), host.c_str()))
                  {
                      boost::system::error_code ec{static_cast<int>(::ERR_get_error()),
                              boost::asio::error::get_ssl_category()};
                      return fail(**_requests.begin(), ec, "set_tlsext_hostname");
                  }
              }
  
              _pending_request = true;
  
              // Look up the domain name
              _resolver.async_resolve(
                  url.host(),
                  url.port_view(),
                  [me = this->shared_from_this()](const boost::system::error_code& ec,
                          asio::ip::tcp::resolver::results_type results) {
                      me->on_resolve(ec, results);
                  });
          }
  
          if (!_pending_request) {
              //std::cout << "should do something to rearm" << std::endl;
              do_write();
          } else {
              //std::cout << "nothing to do to rearm" << std::endl;
          }
      }
  
      void on_resolve(const boost::system::error_code& ec,
              asio::ip::tcp::resolver::results_type results)
      {
          //std::cout << "on resolve" << std::endl;
          if(ec) {
              std::lock_guard guard{_requests_mtx};
              return fail(**_requests.begin(), ec, "resolve");
          }
  
          // Make the connection on the IP address we get from a lookup
          if constexpr(SSL) {
              asio::async_connect(
                  _stream.next_layer(),
                  results.begin(),
                  results.end(),
                  [me = this->shared_from_this()](const boost::system::error_code& ec,
                          asio::ip::tcp::resolver::iterator it) {
                      me->on_connect(ec);
                  }
              );
          }
          else {
              asio::async_connect(
                  _socket,
                  results.begin(),
                  results.end(),
                  [me = this->shared_from_this()](const boost::system::error_code& ec,
                          asio::ip::tcp::resolver::iterator it) {
                      me->on_connect(ec);
                  }
              );
          }
      }
  
      void
      on_connect(const boost::system::error_code& ec)
      {
          //std::cout << "on connect" << std::endl;
          if(ec) {
              std::lock_guard guard{_requests_mtx};
              return fail(**_requests.begin(), ec, "connect");
          }
  
          if constexpr(SSL) {
              // Perform the SSL handshake
              _stream.async_handshake(
                  asio::ssl::stream_base::client,
                  [me = this->shared_from_this()](boost::system::error_code ec) {
                      me->on_handshake(ec);
                  });
          }
          else {
              std::lock_guard guard{_requests_mtx};
              do_write();
          }
      }
  
      void
      on_handshake(boost::system::error_code ec) {
          if(ec) {
              std::lock_guard guard{_requests_mtx};
              return fail(**_requests.begin(), ec, "handshake");
          }
  
          std::lock_guard guard{_requests_mtx};
          do_write();
      }
  
      void
      do_write() {
          //std::cout << "do write" << std::endl;
  
          if (_requests.empty()) {
              //std::cout << " ... nothing to do" << std::endl;
              _pending_request = false;
              return;
          }
  
          auto req_ctx = *_requests.begin();
         _requests.pop_front();
  
         //std::cout << "start a write" << std::endl;
         _pending_request = true;
  
         // Send the HTTP request to the remote host
         if constexpr(SSL) {
             beast::http::async_write(_stream, req_ctx->request,
                 asio::bind_executor(_strand,
                         [me = this->shared_from_this(), req_ctx](boost::system::error_code ec,
                                 std::size_t bytes_transferred) {
                             me->on_write(ec, bytes_transferred, req_ctx);
                          })
              );
         }
         else {
             beast::http::async_write(_socket, req_ctx->request,
                 asio::bind_executor(_strand,
                         [me = this->shared_from_this(), req_ctx](boost::system::error_code ec,
                                 std::size_t bytes_transferred) {
                             me->on_write(ec, bytes_transferred, req_ctx);
                          })
              );
         }
      }
  
      void
      on_write(boost::system::error_code ec, std::size_t, std::shared_ptr<request_context> req_ctx)
      {
          //std::cout << "on write" << std::endl;
          if(ec) {
              return fail(*req_ctx, ec, "write");
          }
  
          // Receive the HTTP response
          if constexpr(SSL) {
              beast::http::async_read(_stream, _buffer, req_ctx->response,
                      asio::bind_executor(_strand,
                          [me = this->shared_from_this(), req_ctx](boost::system::error_code ec,
                                  std::size_t bytes_transferred) {
                      me->on_read(ec, req_ctx);
                  })
              );
          }
          else {
              beast::http::async_read(_socket, _buffer, req_ctx->response,
                      asio::bind_executor(_strand,
                          [me = this->shared_from_this(), req_ctx](boost::system::error_code ec,
                                  std::size_t bytes_transferred) {
                      me->on_read(ec, req_ctx);
                  })
              );
          }
      }
  
      void
      on_read(boost::system::error_code ec, std::shared_ptr<request_context> req_ctx)
      {
          //std::cout << "on read" << std::endl;
          if(ec) {
              return fail(*req_ctx, ec, "read");
          }
  
          req_ctx->timer.cancel(); // will call on_timer with operator_cancelled
  
          if (req_ctx->cb) {
              if (!req_ctx->too_late) {
                  _response = req_ctx->response.release();
                  req_ctx->cb(ec, std::move(_response));
              }
          } else {
              _response = req_ctx->response.release();
          }
  
          std::lock_guard guard{_requests_mtx};
          do_write();
      }
  
      void
      on_timer(boost::system::error_code ec, std::shared_ptr<request_context> req_ctx)
      {
          //std::cout << "on timer" << std::endl;
          if (!ec && !req_ctx->too_late) {
              fail(*req_ctx, boost::system::error_code(boost::system::errc::timed_out,
                      boost::system::system_category()),
                      "timeout");
  
              req_ctx->too_late = true;
          }
      }
  
      beauty::response& response() { return _response; }
  
  private:
      asio::io_context&       _ioc;
      asio::ip::tcp::resolver _resolver;
      asio::ip::tcp::socket   _socket;
      stream_type                                   _stream = {};
      asio::strand<asio::io_context::executor_type> _strand;
  
      beast::flat_buffer      _buffer;
  
      // Synchronous response
      beauty::response        _response;
  
      // Asynchronous request - wait for connection
      std::mutex              _requests_mtx;
      std::deque<std::shared_ptr<request_context>> _requests;
      std::atomic_bool        _pending_request{false};
  
  private:
      void fail(const request_context& req_ctx, boost::system::error_code ec, const char* /* msg not used */) {
          if (req_ctx.cb) {
              //std::cout << " !!! FAILED !!! " << ec << " with " << msg << std::endl;
              if (!req_ctx.too_late) {
                  req_ctx.cb(ec, {});
              }
          } else {
              throw boost::system::system_error(ec);
          }
      }
  };
  
  }
  