#include "fast_float/fast_float.h"

#include <cstdint>
#include <ios>
#include <iostream>
#include <random>
#include <system_error>
#include <utility>

#if defined(__CYGWIN__) || defined(__MINGW32__) || defined(__MINGW64__) ||     \
    defined(sun) || defined(__sun)
// Anything at all that is related to cygwin, msys and so forth will
// always use this fallback because we cannot rely on it behaving as normal
// gcc.
#include <locale>
#include <sstream>

// workaround for CYGWIN
double cygwin_strtod_l(char const *start, char **end) {
  double d;
  std::stringstream ss;
  ss.imbue(std::locale::classic());
  ss << start;
  ss >> d;
  if (ss.fail()) {
    *end = nullptr;
  }
  if (ss.eof()) {
    ss.clear();
  }
  auto nread = ss.tellg();
  *end = const_cast<char *>(start) + nread;
  return d;
}

float cygwin_strtof_l(char const *start, char **end) {
  float d;
  std::stringstream ss;
  ss.imbue(std::locale::classic());
  ss << start;
  ss >> d;
  if (ss.fail()) {
    *end = nullptr;
  }
  if (ss.eof()) {
    ss.clear();
  }
  auto nread = ss.tellg();
  *end = const_cast<char *>(start) + nread;
  return d;
}
#endif

class RandomEngine {
public:
  RandomEngine() = delete;
  RandomEngine(uint64_t new_seed) : wyhash64_x_(new_seed){};

  uint64_t next() {
    // Adapted from https://github.com/wangyi-fudan/wyhash/blob/master/wyhash.h
    // Inspired from
    // https://github.com/lemire/testingRNG/blob/master/source/wyhash.h
    wyhash64_x_ += UINT64_C(0x60bee2bee120fc15);
    fast_float::value128 tmp = fast_float::full_multiplication(
        wyhash64_x_, UINT64_C(0xa3b195354a39b70d));
    uint64_t m1 = (tmp.high) ^ tmp.low;
    tmp = fast_float::full_multiplication(m1, UINT64_C(0x1b03738712fad5c9));
    uint64_t m2 = (tmp.high) ^ tmp.low;
    return m2;
  }

  bool next_bool() { return (next() & 1) == 1; }

  int next_int() { return static_cast<int>(next()); }

  char next_char() { return static_cast<char>(next()); }

  double next_double() { return static_cast<double>(next()); }

  int next_ranged_int(int min, int max) { // min and max are included
    // Adapted from
    // https://lemire.me/blog/2019/06/06/nearly-divisionless-random-integer-generation-on-various-systems/
    /*  if (min == max) {
         return min;
    }*/
    uint64_t s = uint64_t(max - min + 1);
    uint64_t x = next();
    fast_float::value128 m = fast_float::full_multiplication(x, s);
    uint64_t l = m.low;
    if (l < s) {
      uint64_t t = -s % s;
      while (l < t) {
        x = next();
        m = fast_float::full_multiplication(x, s);
        l = m.low;
      }
    }
    return int(m.high) + min;
  }

  int next_digit() { return next_ranged_int(0, 9); }

private:
  uint64_t wyhash64_x_;
};

size_t build_random_string(RandomEngine &rand, char *buffer) {
  size_t pos{0};
  if (rand.next_bool()) {
    buffer[pos++] = '-';
  }
  int number_of_digits = rand.next_ranged_int(1, 100);
  if (number_of_digits == 100) {
    // With low probability, we want to allow very long strings just to stress
    // the system.
    number_of_digits = rand.next_ranged_int(1, 2000);
  }
  int location_of_decimal_separator = rand.next_ranged_int(1, number_of_digits);
  for (size_t i = 0; i < size_t(number_of_digits); i++) {
    if (i == size_t(location_of_decimal_separator)) {
      buffer[pos++] = '.';
    }
    buffer[pos] = char(rand.next_digit() + '0');
    // We can have a leading zero only if location_of_decimal_separator = 1.
    while (i == 0 && 1 != size_t(location_of_decimal_separator) &&
           buffer[pos] == '0') {
      buffer[pos] = char(rand.next_digit() + '0');
    }
    pos++;
  }
  if (rand.next_bool()) {
    if (rand.next_bool()) {
      buffer[pos++] = 'e';
    } else {
      buffer[pos++] = 'E';
    }
    if (rand.next_bool()) {
      buffer[pos++] = '-';
    } else {
      if (rand.next_bool()) {
        buffer[pos++] = '+';
      }
    }
    number_of_digits = rand.next_ranged_int(1, 3);
    for (size_t i = 0; i < size_t(number_of_digits); i++) {
      buffer[pos++] = char(rand.next_digit() + '0');
    }
  }
  buffer[pos] = '\0'; // null termination
  return pos;
}

std::pair<double, bool> strtod_from_string(char *st) {
  double d;
  char *pr;
#if defined(__CYGWIN__) || defined(__MINGW32__) || defined(__MINGW64__) ||     \
    defined(sun) || defined(__sun)
  d = cygwin_strtod_l(st, &pr);
#elif defined(_WIN32)
  static _locale_t c_locale = _create_locale(LC_ALL, "C");
  d = _strtod_l(st, &pr, c_locale);
#else
  static locale_t c_locale = newlocale(LC_ALL_MASK, "C", NULL);
  d = strtod_l(st, &pr, c_locale);
#endif
  if (st == pr) {
    std::cerr << "strtod_l could not parse '" << st << std::endl;
    return std::make_pair(0, false);
  }
  return std::make_pair(d, true);
}

std::pair<float, bool> strtof_from_string(char *st) {
  float d;
  char *pr;
#if defined(__CYGWIN__) || defined(__MINGW32__) || defined(__MINGW64__) ||     \
    defined(sun) || defined(__sun)
  d = cygwin_strtof_l(st, &pr);
#elif defined(_WIN32)
  static _locale_t c_locale = _create_locale(LC_ALL, "C");
  d = _strtof_l(st, &pr, c_locale);
#else
  static locale_t c_locale = newlocale(LC_ALL_MASK, "C", NULL);
  d = strtof_l(st, &pr, c_locale);
#endif
  if (st == pr) {
    std::cerr << "strtof_l could not parse '" << st << std::endl;
    return std::make_pair(0.0f, false);
  }
  return std::make_pair(d, true);
}

/**
 * We generate random strings and we try to parse them with both strtod/strtof,
 * and we verify that we get the same answer with with fast_float::from_chars.
 */
bool tester(uint64_t seed, size_t volume) {
  char buffer[4096]; // large buffer (can't overflow)
  RandomEngine rand(seed);
  for (size_t i = 0; i < volume; i++) {
    if ((i % 100000) == 0) {
      std::cout << ".";
      std::cout.flush();
    }
    size_t length = build_random_string(rand, buffer);
    std::pair<double, bool> expected_double = strtod_from_string(buffer);
    if (expected_double.second) {
      double result_value;
      auto result =
          fast_float::from_chars(buffer, buffer + length, result_value);
      if (result.ec != std::errc() &&
          result.ec != std::errc::result_out_of_range) {
        printf("parsing %.*s\n", int(length), buffer);
        std::cerr << " I could not parse " << std::endl;
        return false;
      }
      if (result.ptr != buffer + length) {
        printf("parsing %.*s\n", int(length), buffer);
        std::cerr << " Did not get to the end " << std::endl;
        return false;
      }
      if (result_value != expected_double.first) {
        printf("parsing %.*s\n", int(length), buffer);
        std::cerr << std::hexfloat << result_value << std::endl;
        std::cerr << std::hexfloat << expected_double.first << std::endl;
        std::cerr << " Mismatch " << std::endl;
        return false;
      }
    }
    std::pair<float, bool> expected_float = strtof_from_string(buffer);
    if (expected_float.second) {
      float result_value;
      auto result =
          fast_float::from_chars(buffer, buffer + length, result_value);
      if (result.ec != std::errc() &&
          result.ec != std::errc::result_out_of_range) {
        printf("parsing %.*s\n", int(length), buffer);
        std::cerr << " I could not parse " << std::endl;
        return false;
      }
      if (result.ptr != buffer + length) {
        printf("parsing %.*s\n", int(length), buffer);
        std::cerr << " Did not get to the end " << std::endl;
        return false;
      }
      if (result_value != expected_float.first) {
        printf("parsing %.*s\n", int(length), buffer);
        std::cerr << std::hexfloat << result_value << std::endl;
        std::cerr << std::hexfloat << expected_float.first << std::endl;
        std::cerr << " Mismatch " << std::endl;
        return false;
      }
    }
  }
  return true;
}

int main() {

#if defined(__CYGWIN__) || defined(__MINGW32__) || defined(__MINGW64__) ||     \
    defined(sun) || defined(__sun)
  std::cout << "Warning: msys/cygwin or solaris detected." << std::endl;
  return EXIT_SUCCESS;
#else
  if (tester(1234344, 100000000)) {
    std::cout << "All tests ok." << std::endl;
    return EXIT_SUCCESS;
  }
  std::cout << "Failure." << std::endl;
  return EXIT_FAILURE;

#endif
}
