/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "Utils.hpp"

#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <iomanip>

#include <boost/asio.hpp>
#include <boost/dll/import.hpp>
#include <boost/dll/runtime_symbol_info.hpp>
#include <boost/process/detail/config.hpp>

#include "config.h"

#if defined(HAVE_uname)
#include <sys/utsname.h>
#endif

namespace apache {
namespace geode {
namespace client {

std::string Utils::getEnv(const char* varName) {
  std::string env;
  if (const auto varValue = std::getenv(varName)) {
    env = varValue;
  }
  return env;
}

std::error_code Utils::getLastError() {
  if (errno != 0) {
    return std::error_code(errno, std::system_category());
  }

  return boost::process::detail::get_last_error();
}

void Utils::parseEndpointString(const char* endpoints, std::string& host,
                                uint16_t& port) {
  std::string endpointsStr(endpoints);
  LOGFINE("Parsing endpoint string [%s]", endpointsStr.c_str());
  // Parse this string to get all hostnames and port numbers.
  std::string endpoint;
  std::string::size_type length = endpointsStr.size();
  std::string::size_type pos = 0;
  pos = endpointsStr.find(':', 0);
  if (pos != std::string::npos) {
    endpoint = endpointsStr.substr(0, pos);
    pos += 1;  // skip ':'
    length -= (pos);
    endpointsStr = endpointsStr.substr(pos, length);
  } else {
    host = "";
    port = 0;
    return;
  }
  // trim white spaces.
  std::string::size_type wpos = endpoint.find_last_not_of(' ');
  if (wpos != std::string::npos) {
    endpoint.erase(wpos + 1);
    wpos = endpoint.find_first_not_of(' ');
    if (wpos != std::string::npos) endpoint.erase(0, wpos);
  }
  host = endpoint;
  port = atoi(endpointsStr.c_str());
}

std::string Utils::convertHostToCanonicalForm(const std::string& endpoints) {
  using boost::asio::io_service;
  using boost::asio::ip::tcp;

  if (endpoints.empty()) {
    return {};
  }

  auto pos = endpoints.rfind(':');
  if (pos == std::string::npos) {
    return {};
  }

  auto hostname = endpoints.substr(0, pos);
  auto port = endpoints.substr(pos + 1);

  if (hostname == "localhost") {
    hostname = boost::asio::ip::host_name();
  }

  io_service svc;
  boost::system::error_code ec;
  tcp::resolver resolver{svc};
  auto results = resolver.resolve(hostname, ec);

  if (!ec) {
    hostname = results->host_name();
  }

  return hostname + ':' + port;
}

void Utils::parseEndpointNamesString(
    std::string endpoints, std::unordered_set<std::string>& endpointNames) {
  // Parse this string to get all hostnames and port numbers.
  std::string endpoint;
  std::string::size_type length = endpoints.size();
  std::string::size_type pos = 0;
  do {
    pos = endpoints.find(',', 0);
    if (pos != std::string::npos) {
      endpoint = endpoints.substr(0, pos);
      pos += 1;  // skip ','
      length -= (pos);
      endpoints = endpoints.substr(pos, length);
    } else {
      endpoint = endpoints;
    }
    // trim white spaces.
    std::string::size_type wpos = endpoint.find_last_not_of(' ');
    if (wpos != std::string::npos) {
      endpoint.erase(wpos + 1);
      wpos = endpoint.find_first_not_of(' ');
      if (wpos != std::string::npos) endpoint.erase(0, wpos);
      endpointNames.insert(endpoint);
    }
  } while (pos != std::string::npos);
}

char* Utils::copyString(const char* str) {
  char* resStr = nullptr;
  if (str != nullptr) {
    size_t strSize = strlen(str) + 1;
    resStr = new char[strSize];
    memcpy(resStr, str, strSize);
  }
  return resStr;
}

/**
 * Finds, loads and keeps a copy of requested shared library. Future
 * improvements should use the boost::dll::import to maintain references to
 * shared libraries rather than a synchronized global structure.
 *
 * Uses similar options ans search patterns to the original ACE_DLL
 * implementation.
 *
 * @param libraryName to find or load
 * @return found or loaded shared library
 * @throws IllegalArgumentException if library is not found or otherwise
 * unloadable.
 */
const boost::dll::shared_library& getSharedLibrary(
    const std::string& libraryName) {
  static std::mutex sharedLibrariesMutex;
  static std::unordered_map<std::string,
                            std::shared_ptr<boost::dll::shared_library>>
      sharedLibraries;

  std::lock_guard<decltype(sharedLibrariesMutex)> lock(sharedLibrariesMutex);

  const auto& find = sharedLibraries.find(libraryName);
  if (find == sharedLibraries.end()) {
    auto path = libraryName.empty() ? boost::dll::program_location()
                                    : boost::dll::fs::path{libraryName};
    try {
      return *sharedLibraries
                  .emplace(
                      libraryName,
                      std::make_shared<boost::dll::shared_library>(
                          path,
                          boost::dll::load_mode::rtld_global |
                              boost::dll::load_mode::rtld_lazy |
                              boost::dll::load_mode::append_decorations |
                              boost::dll::load_mode::search_system_folders))
                  .first->second;
    } catch (const boost::dll::fs::system_error& e) {
      throw IllegalArgumentException("cannot open library: \"" + path.string() +
                                     "\": reason: " + e.what());
    }
  }

  return *find->second;
}

void* Utils::getFactoryFunctionVoid(const std::string& lib,
                                    const std::string& funcName) {
  try {
    const auto& sharedLibrary = getSharedLibrary(lib);
    return reinterpret_cast<void*>(sharedLibrary.get<void*()>(funcName));
  } catch (const boost::dll::fs::system_error&) {
    std::string location =
        lib.empty() ? "the application" : "library \"" + lib + "\"";
    throw IllegalArgumentException("cannot find factory function " + funcName +
                                   " in " + location);
  }
}

std::string Utils::convertBytesToString(const uint8_t* bytes, size_t length,
                                        size_t maxLength) {
  if (bytes != nullptr) {
    length = std::min(length, maxLength);
    std::stringstream ss;
    ss << std::setfill('0') << std::hex;
    for (size_t i = 0; i < length; ++i) {
      ss << std::setw(2) << static_cast<int>(bytes[i]);
    }
    return ss.str();
  }
  return "";
}

std::string Utils::convertBytesToString(const int8_t* bytes, size_t length,
                                        size_t maxLength) {
  return Utils::convertBytesToString(reinterpret_cast<const uint8_t*>(bytes),
                                     length, maxLength);
}

int64_t Utils::startStatOpTime() {
  return std::chrono::duration_cast<std::chrono::nanoseconds>(
             std::chrono::steady_clock::now().time_since_epoch())
      .count();
}

void Utils::updateStatOpTime(statistics::Statistics* m_regionStats,
                             int32_t statId, int64_t start) {
  m_regionStats->incLong(statId, startStatOpTime() - start);
}

std::string Utils::getSystemInfo() {
  std::string sysname{"Unknown"};
  std::string machine{"Unknown"};
  std::string nodename{"Unknown"};
  std::string release{"Unknown"};
  std::string version{"Unknown"};

#if defined(HAVE_uname)
  struct utsname name;
  auto rc = ::uname(&name);
  if (rc == 0) {
    sysname = name.sysname;
    machine = name.machine;
    nodename = name.nodename;
    release = name.release;
    version = name.version;
  }
#elif defined(_WIN32) /* HAVE_uname */
  sysname = "Win32";

/* Since MS found it necessary to deprecate these. */
#pragma warning(push)
#pragma warning(disable : 4996)
#if defined(__clang__)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
#endif /* __clang__ */
  OSVERSIONINFOA vinfo;
  vinfo.dwOSVersionInfoSize = sizeof(OSVERSIONINFOA);
  ::GetVersionExA(&vinfo);
#if defined(__clang__)
#pragma clang diagnostic pop
#endif /* __clang__ */
#pragma warning(pop)

  if (vinfo.dwPlatformId == VER_PLATFORM_WIN32_NT) {
    // Get information from the two structures
    release = "Windows NT " + std::to_string(vinfo.dwMajorVersion) + '.' +
              std::to_string(vinfo.dwMinorVersion);
    version = "Build " + std::to_string(vinfo.dwBuildNumber) + ' ' +
              vinfo.szCSDVersion;
  }

  {
    HKEY key;
    DWORD size = 0;
    DWORD type = 0;
    auto key_name = "ProcessorNameString";
    auto key_path = "HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0";
    auto rc = ::RegOpenKeyExA(HKEY_LOCAL_MACHINE, key_path, 0, KEY_READ, &key);
    if (rc == ERROR_SUCCESS) {
      rc = ::RegQueryValueExA(key, key_name, nullptr, &type, nullptr, &size);
      if (rc == ERROR_SUCCESS && type == REG_SZ && size > 0) {
        std::vector<char> buffer(size, 0);
        auto ptr = reinterpret_cast<LPBYTE>(buffer.data());
        rc = ::RegQueryValueExA(key, key_name, nullptr, &type, ptr, &size);
        if (rc == ERROR_SUCCESS && buffer[0] != '\0') {
          machine = buffer.data();
        }
      }

      ::RegCloseKey(key);
    }
  }

  nodename = boost::asio::ip::host_name();
#endif /* _WIN32 */

  std::string info = "SystemName=";
  info += sysname;
  info += " Machine=";
  info += machine;
  info += " Host=";
  info += nodename;
  info += " Release=";
  info += release;
  info += " Version=";
  info += version;

  return info;
}

}  // namespace client
}  // namespace geode
}  // namespace apache
