/**
 * @file core/data/download_file_impl.hpp
 * @author Omar Shrit
 *
 * mlpack Load function that download dataset from a URL.
 *
 * mlpack is free software; you may redistribute it and/or modify it under the
 * terms of the 3-clause BSD license.  You should have received a copy of the
 * 3-clause BSD license along with mlpack.  If not, see
 * http://www.opensource.org/licenses/BSD-3-Clause for more information.
 */
#ifndef MLPACK_CORE_DATA_DOWNLOAD_FILE_IMPL_HPP
#define MLPACK_CORE_DATA_DOWNLOAD_FILE_IMPL_HPP

#include "download_file.hpp"

namespace mlpack {

#ifdef MLPACK_ENABLE_HTTPLIB

inline bool CheckIfURL(const std::string& url)
{
  if (!url.empty())
  {
    if (url.compare(0, 7, "http://") == 0 ||
        url.compare(0, 8, "https://") == 0)
    {
      return true;
    }
  }
  return false;
}

inline bool CheckValidHost(const std::string& host)
{
  if (host.empty())
    return false;
  else if (host.size() > 253)
    return false;

  std::string traillingDots("..");
  if (host.find(traillingDots) != std::string::npos)
    return false;

  // Only lower case alpha, numeric and hyphen are allowed in a hostname.
  struct InvalidHost
  {
    bool operator()(char c)
    {
      if (std::isdigit(c))
        return false;
      else if (std::islower(c))
        return false;
      else if (c == '-' || c== '.')
        return false;
      else
        return true;
    }
  };
  return std::find_if(host.begin(), host.end(), InvalidHost()) == host.end();
}

inline bool IsDigits(const std::string& str)
{
  return std::all_of(str.begin(), str.end(), ::isdigit);
}

inline void ParseURL(const std::string& url, std::string& host,
                     std::string& filename, int& port)
{
  if (!CheckIfURL(url) || url.size() <= 8)
  {
    throw std::runtime_error("ParseURL(): invalid URL '" + url + "' provided."
        " URL should start with 'http://' or 'https://'.");
  }
  size_t pos = url.find("://");
  pos = pos + 3;

  std::string possibleHost = url.substr(pos);
  size_t endHost = possibleHost.find_first_of(":/");
  std::string tmpHost = possibleHost.substr(0, endHost);
  if (CheckValidHost(tmpHost))
    host = tmpHost;
  else
    throw std::runtime_error("ParseURL(): invalid host '" + tmpHost + "'. "
        "Valid hostnames must contain only lowercase letters, digits,"
        " and hyphens.");

  if (endHost != std::string::npos)
  {
    char endChar = possibleHost.at(endHost);
    if (endChar == ':')
    {
      // We need to find the first '/' as we might have several.
      std::string possiblePort = possibleHost.substr(endHost + 1);
      size_t endPort = possiblePort.find_first_of("/");
      possiblePort = possiblePort.substr(0, endPort);
      if (IsDigits(possiblePort))
        port = std::stoi(possiblePort);
    }
  }

  int firstPos = possibleHost.rfind(":");
  int secPos   = possibleHost.rfind("/");
  // Need to be sure that we are comparing valid number since npos is the
  // highest possible value in size_t
  if (firstPos == (int)std::string::npos) firstPos = -1;
  if (secPos == (int)std::string::npos) secPos = -1;
  if (secPos > firstPos)
  {
    size_t filePos = possibleHost.rfind("/");
    // no need to throw an exception, if the file is not found this is not a
    // problem with the URL.
    if (filePos != std::string::npos)
    {
      std::string possibleFilename = possibleHost.substr(filePos + 1);
      size_t posFile = possibleFilename.find_first_of("?#");
      // we assume something is after the file name.
      if (posFile != std::string::npos)
      {
        filename = possibleFilename.substr(0, posFile);
      }
      else
      {
        filename = possibleFilename;
      }
    }
  }
}

inline bool DownloadFile(const std::string& url,
                         std::string& filename)
{
  std::fstream stream;
  int port = -1;
  std::string host;
  ParseURL(url, host, filename, port);

  // Sanity check if in case.
  if (host.empty())
  {
    throw std::runtime_error("DownloadFile(): domain name could not be parsed"
        " from URL '" + url + "'");
  }
#ifdef MLPACK_USE_HTTPS
  if (port == -1)
  {
    port = 443;
  }
  httplib::SSLClient cli(host, port);
#else
  if (port == -1)
  {
    port = 80;
  }
  httplib::Client cli(host, port);
#endif
  cli.set_connection_timeout(2);
  httplib::Result res = cli.Get(url);
  if (!res)
  {
    std::stringstream oss;
    oss << "DownloadFile(): httplib error: " << httplib::to_string(res.error());
    throw std::runtime_error(oss.str());
  }
  if (res->status == 404)
  {
    std::stringstream oss;
    oss << "DownloadFile(): httplib error: Page / File not found: "
        << res->status << " returned.";
    throw std::runtime_error(oss.str());
  }

  std::filesystem::path tmpFilename = TempName();
  // This is necessary to get the extension.
  tmpFilename +=  "." + Extension(filename);

#ifdef  _WIN32 // Always open in binary mode on Windows.
  stream.open(tmpFilename, std::fstream::out
      | std::fstream::binary);
#else
  stream.open(tmpFilename, std::fstream::out);
#endif

  if (!stream.is_open())
  {
    std::stringstream oss;
    oss <<  "DownloadFile(): cannot open temporary file " << tmpFilename
        << " for storing downloaded data. Please check the file path.";
    throw std::runtime_error(oss.str());
  }
  errno = 0;
  stream.write(res->body.data(), res->body.size());
  stream.flush();
  if (!stream.good())
  {
    std::stringstream oss;
    oss << "Error writing to file " << tmpFilename << ": "
        << std::strerror(errno);
    throw std::runtime_error(oss.str());
  }
  stream.close();
  filename = tmpFilename.generic_string();
  return true;
}

#else

inline bool CheckIfURL(const std::string& /* url */)
{
  return false;
}

inline void ParseURL(const std::string& url, std::string& host,
                     std::string& filename, int& port)
{
  host = url;
  filename = "";
  port = -1;
  throw std::runtime_error("ParseURL(): httplib support not enabled; cannot "
      "check provided URL '" + url + "'.  Enable httplib by adding '#define "
      "MLPACK_ENABLE_HTTPLIB' before including mlpack.");
}

inline bool DownloadFile(const std::string& url,
                         std::string& filename)
{
  filename = "";
  throw std::runtime_error("DownloadFile(): httplib support not enabled; cannot"
      " check provided URL '" + url + "'.  Enable httplib by adding '#define "
      "MLPACK_ENABLE_HTTPLIB' before including mlpack.");
  return false;
}

#endif

} // namespace mlpack

#endif
