#ifndef GERMANAIRLINESVA_GACONNECTOR_UTIL_H
#define GERMANAIRLINESVA_GACONNECTOR_UTIL_H

#ifdef IBM
#define WIN32_LEAN_AND_MEAN
#endif

#define BUFSIZE 1024
#define MD5LEN 16
#define EARTH_M 6371000

#ifdef IBM
// clang-format off
#include <wtypes.h>
#include <wincrypt.h>
// clang-format on
#endif
#ifdef APL
#include <CommonCrypto/CommonDigest.h>
#include <sys/mman.h>
#endif
#ifdef LIN
#include <openssl/md5.h>
#include <sys/mman.h>
#endif

#if defined APL || defined LIN
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#endif

#include <algorithm>
#include <fstream>
#include <functional>
#include <sstream>
#include <string>
#include <utility>
#include <vector>


namespace germanairlinesva
{
namespace util
{
  template <typename T>
  static inline std::vector<T>
      select_T(const std::vector<T> &inVec,
               std::function<bool(const T &)> predicate)
  {
    std::vector<T> result;
    copy_if(inVec.begin(), inVec.end(), back_inserter(result), predicate);
    return result;
  }

#if defined APL || defined LIN
  static inline unsigned long get_size_by_fd(int fd)
  {
    struct stat buf {
    };
    if (fstat(fd, &buf) < 0)
      return 0;
    return buf.st_size;
  }
#endif

  static inline void to_hex(const char *hash, char *buffer)
  {
    for (int i = 0; i < MD5LEN; i++) {
      if (buffer != nullptr) {
        sprintf(&buffer[2 * i], "%02x", hash[i] & 0xff);
      }
    }
  }

  static inline bool fileExists(const char *path)
  {
    return static_cast<bool>(std::ifstream(path));
  }

#ifdef IBM
  static inline int
      generateMD5(const char *filepath,
                  char *lastHash,
                  const std::function<void(const std::string)> toLog)
  {
    BOOL bResult = FALSE;
    HCRYPTPROV hProv = 0;
    HCRYPTHASH hHash = 0;
    HANDLE hFile;
    BYTE rgbFile[BUFSIZE] = {0};
    DWORD cbRead = 0;
    BYTE rgbHash[MD5LEN] = {0};
    DWORD cbHash = 0;

    // Logic to check usage goes here.
    hFile = CreateFile(filepath,
                       GENERIC_READ,
                       FILE_SHARE_READ,
                       nullptr,
                       OPEN_EXISTING,
                       FILE_FLAG_SEQUENTIAL_SCAN,
                       nullptr);

    // Get handle to the crypto provider
    if (!CryptAcquireContext(&hProv,
                             nullptr,
                             nullptr,
                             PROV_RSA_FULL,
                             CRYPT_VERIFYCONTEXT)) {
      toLog("CryptAcquireContext returned with error " +
            std::to_string(GetLastError()));

      CloseHandle(hFile);
      return 1;
    }

    if (!CryptCreateHash(hProv, CALG_MD5, 0, 0, &hHash)) {
      toLog("CryptCreateHash returned with error " +
            std::to_string(GetLastError()));

      CloseHandle(hFile);
      CryptReleaseContext(hProv, 0);
      return 1;
    }

    while ((bResult = ReadFile(hFile, rgbFile, BUFSIZE, &cbRead, nullptr))) {
      if (0 == cbRead) {
        break;
      }

      if (!CryptHashData(hHash, rgbFile, cbRead, 0)) {
        toLog("CryptHashData returned with error " +
              std::to_string(GetLastError()));

        CryptReleaseContext(hProv, 0);
        CryptDestroyHash(hHash);
        CloseHandle(hFile);
        return 1;
      }
    }

    if (!bResult) {
      toLog("ReadFile returned with error " + std::to_string(GetLastError()));

      CryptReleaseContext(hProv, 0);
      CryptDestroyHash(hHash);
      CloseHandle(hFile);
      return 1;
    }

    cbHash = MD5LEN;
    if (CryptGetHashParam(hHash, HP_HASHVAL, rgbHash, &cbHash, 0)) {
      to_hex((char *)rgbHash, lastHash);
    } else {
      toLog("CryptGetHashParam returned with error " +
            std::to_string(GetLastError()));
    }

    CryptDestroyHash(hHash);
    CryptReleaseContext(hProv, 0);
    CloseHandle(hFile);

    return 0;
  }
#endif
#ifdef APL
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunused-parameter"
  static inline int
      generateMD5(const char *filepath,
                  char *lastHash,
                  const std::function<void(const std::string)> toLog)
  {
    int file_descript;
    unsigned long file_size;
    char *file_buffer;
    unsigned char result[MD5LEN];

    file_descript = open(filepath, O_RDONLY);
    if (file_descript < 0)
      return 1;

    file_size = get_size_by_fd(file_descript);

    file_buffer =
        (char *)mmap(0, file_size, PROT_READ, MAP_SHARED, file_descript, 0);

    CC_MD5_CTX context;
    CC_MD5_Init(&context);
    CC_MD5_Update(&context, file_buffer, (CC_LONG)file_size);
    CC_MD5_Final(result, &context);

    munmap(file_buffer, file_size);
    close(file_descript);

    to_hex((char *)result, lastHash);
    return 0;
  }
#pragma clang diagnostic pop
#endif
#ifdef LIN
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunused-parameter"
  static inline int
      generateMD5(const char *filepath,
                  char *buffer,
                  const std::function<void(const std::string)> toLog)
  {
    int file_descriptor;
    unsigned long file_size;
    char *file_buffer;
    unsigned char result[MD5LEN];

    file_descriptor = open(filepath, O_RDONLY);
    if (file_descriptor < 0)
      return 1;

    file_size = get_size_by_fd(file_descriptor);
    if (file_size == 0)
      return 1;

    file_buffer = (char *)
        mmap(nullptr, file_size, PROT_READ, MAP_SHARED, file_descriptor, 0);

    MD5((unsigned char *)file_buffer, file_size, result);

    munmap(file_buffer, file_size);
    close(file_descriptor);

    to_hex((char *)result, buffer);
    return 0;
  }
#pragma clang diagnostic pop
#endif
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunused-parameter"
  static inline void setThreadName(const std::string &name)
  {
#ifdef APL
    //
    // Apple reserves 16 bytes for its thread names
    // Notice that the Apple version of pthread_setname_np
    // does not take a pthread_t argument
    //
    pthread_setname_np(name.substr(0, 63).c_str());
#endif
#ifdef LIN
    //
    // Linux only reserves 16 bytes for its thread names
    // See prctl and PR_SET_NAME property in
    // http://man7.org/linux/man-pages/man2/prctl.2.html
    //
    pthread_setname_np(pthread_self(), name.substr(0, 15).c_str());
#endif
  }
#pragma clang diagnostic pop

  // trim from start (in place)
  static inline void ltrim(std::string &s)
  {
    s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) {
              return !std::isspace(ch);
            }));
  }

  // trim from end (in place)
  static inline void rtrim(std::string &s)
  {
    s.erase(std::find_if(s.rbegin(),
                         s.rend(),
                         [](unsigned char ch) { return !std::isspace(ch); })
                .base(),
            s.end());
  }

  static inline std::string rtrim_copy(std::string s)
  {
    rtrim(s);
    return s;
  }

  // trim from both ends (in place)
  static inline void trim(std::string &s)
  {
    ltrim(s);
    rtrim(s);
  }

  static inline std::vector<std::string> split(const std::string &s, char delim)
  {
    std::vector<std::string> result;
    std::stringstream ss(s);
    std::string item;

    while (getline(ss, item, delim)) {
      result.emplace_back(item);
    }

    return result;
  }
} // namespace util
} // namespace germanairlinesva

#endif
