#include <cstddef>
#include <cstdint>
#include <array>
#include <cstdlib>

#include "helpers/common.h"
#include "simdutf.h"

constexpr std::array options = {
    simdutf::base64_default,
    simdutf::base64_url,
    simdutf::base64_default_no_padding,
    simdutf::base64_url_with_padding,
};

constexpr std::array last_chunk = {
    simdutf::last_chunk_handling_options::loose,
    simdutf::last_chunk_handling_options::strict,
    simdutf::last_chunk_handling_options::stop_before_partial};

struct decoderesult {
  std::size_t maxbinarylength{};
  simdutf::result convertresult{};
  auto operator<=>(const decoderesult&) const = default;
};

template <typename FromChar>
void decode(std::span<const FromChar> base64_, const auto selected_option,
            const auto last_chunk_option) {
  std::vector<FromChar> base64(begin(base64_), end(base64_));
  const auto implementations = get_supported_implementations();
  std::vector<decoderesult> results;
  results.reserve(implementations.size());
  for (auto impl : implementations) {
    auto& r = results.emplace_back();
    r.maxbinarylength =
        impl->maximal_binary_length_from_base64(base64.data(), base64.size());
    // binary_length_from_base64 is not compared across implementations: for
    // invalid input the implementations may legitimately return different
    // estimates, so keep it as a local rather than a field of decoderesult.
    const std::size_t binarylength =
        impl->binary_length_from_base64(base64.data(), base64.size());
    // binary_length_from_base64 must never exceed the maximal upper bound.
    if (binarylength > r.maxbinarylength) {
      std::cerr << "binary_length_from_base64 (" << binarylength
                << ") > maximal_binary_length_from_base64 ("
                << r.maxbinarylength << ") for impl " << impl->name() << "\n";
      std::abort();
    }
    std::vector<char> output(r.maxbinarylength);
    r.convertresult =
        impl->base64_to_binary(base64.data(), base64.size(), output.data(),
                               selected_option, last_chunk_option);
    // binary_length_from_base64 takes no last_chunk_handling option, so its
    // estimate only equals the decoded size under loose handling. Under strict
    // and stop_before_partial a partial final chunk is dropped, so a SUCCESS
    // can legitimately write fewer bytes than the estimate (e.g. "QQQ" with
    // stop_before_partial reports 2 but decodes 0). Only assert equality for
    // loose to avoid false positives.
    if (last_chunk_option == simdutf::last_chunk_handling_options::loose &&
        r.convertresult.error == simdutf::error_code::SUCCESS &&
        binarylength != r.convertresult.count) {
      std::cerr << "binary_length_from_base64 (" << binarylength
                << ") != decoded count (" << r.convertresult.count
                << ") for impl " << impl->name() << "\n";
      std::abort();
    }
  }
  auto neq = [](const auto& a, const auto& b) { return a != b; };
  if (std::ranges::adjacent_find(results, neq) != results.end()) {
    std::cerr << "output differs between implementations for decode\n";
    const auto implementations = get_supported_implementations();
    std::size_t i = 0;
    for (const auto& r : results) {
      std::cerr << "impl " << implementations[i]->name()
                << " got maxbinarylength=" << r.maxbinarylength
                << " convertresult=" << r.convertresult << "\n";
      ++i;
    }
    std::cerr << "option: " << selected_option << '\n';
    std::cerr << "data: "
              << (std::is_same_v<FromChar, char> ? "char" : "char16_t") << "{";
    for (int v : base64) {
      std::cerr << v << ", ";
    }
    std::cerr << "}\n";
    std::abort();
  }
}

template <typename FromChar>
void decode_safe(std::span<const FromChar> base64_, const auto selected_option,
                 const std::size_t decode_buf_size,
                 const auto last_chunk_option) {
  std::vector<FromChar> base64(begin(base64_), end(base64_));
  std::vector<char> output(decode_buf_size);
  std::size_t outlen = decode_buf_size;
  const auto convertresult = simdutf::base64_to_binary_safe(
      base64.data(), base64.size(), output.data(), outlen, selected_option,
      last_chunk_option);

  // the number of written bytes must always be less than the supplied buffer
  assert(outlen <= decode_buf_size);

  switch (convertresult.error) {
  case simdutf::error_code::OUTPUT_BUFFER_TOO_SMALL: {
    if (!(convertresult.count <= base64.size())) {
      std::cerr << " decode_buf_size=" << decode_buf_size
                << " outlen=" << outlen << " and result=" << convertresult
                << '\n';
      std::abort();
    }
  } break;
  case simdutf::error_code::INVALID_BASE64_CHARACTER: {
    assert(convertresult.count < base64.size());
  } break;
  case simdutf::error_code::BASE64_INPUT_REMAINDER: {
    if (!(convertresult.count <= base64.size())) {
      std::cerr << "on input with size=" << base64.size()
                << ": got BASE64_INPUT_REMAINDER decode_buf_size="
                << decode_buf_size << " outlen=" << outlen
                << " and result=" << convertresult << '\n';
      std::abort();
    }
  } break;
  case simdutf::error_code::SUCCESS: {
    // possibility to compare with the normal function
  } break;
  default:;
  }
}

struct roundtripresult {
  std::size_t length{};
  std::size_t maxbinarylength{};
  std::string outputhash;
  std::size_t written{};
  simdutf::result convertbackresult{};
  auto operator<=>(const roundtripresult&) const = default;
};

/// verifies that base64 with lines is the same as without lines, but with
/// newlines every line_length:th byte
void verify_lines(std::span<const char> without_lines,
                  std::span<const char> with_lines,
                  const std::size_t line_length) {
  // ensure we get the same as output, with a newline every line_length:th
  // byte
  for (std::size_t i = 0, j = 0;;) {
    // check one line
    for (int count = 0; count < line_length && j < with_lines.size(); ++count) {
      if (without_lines[i++] != with_lines[j++]) {
        // unexpected - different content
        std::abort();
      }
    }
    if (j == with_lines.size()) {
      // we are at the end of with_lines
      if (i != without_lines.size()) {
        // unexpected - we are not at the end of without_lines
        std::abort();
      }
      break;
    }
    if (with_lines[j++] != '\n') {
      // unexpected - not a newline
      std::abort();
    }
  }
}

void roundtrip(std::span<const char> binary, const auto selected_option,
               const auto last_chunk_option, const std::size_t line_length) {
  if (last_chunk_option ==
      simdutf::last_chunk_handling_options::stop_before_partial) {
    return; // this is not a valid option for roundtrip
  }
  const auto inputhash = FNV1A_hash::as_str(binary);
  const auto implementations = get_supported_implementations();
  std::vector<roundtripresult> results;
  results.reserve(implementations.size());
  for (auto impl : implementations) {
    auto& r = results.emplace_back();
    r.length = impl->base64_length_from_binary(binary.size(), selected_option);
    std::vector<char> output(r.length);
    r.written = impl->binary_to_base64(binary.data(), binary.size(),
                                       output.data(), selected_option);
    if (r.length != r.written) {
      std::abort();
    }

    // make sure generating base64 with lines gives the expected result
    const auto length_with_lines =
        simdutf::base64_length_from_binary_with_lines(
            binary.size(), selected_option, line_length);
    assert(length_with_lines >= r.length);
    std::string output_with_lines(length_with_lines, '\0');
    const auto nwritten_with_lines = impl->binary_to_base64_with_lines(
        binary.data(), binary.size(), output_with_lines.data(), line_length,
        selected_option);
    if (nwritten_with_lines != length_with_lines) {
      std::cerr << nwritten_with_lines << "!=" << length_with_lines << '\n';
      std::abort();
    }
    verify_lines(output, output_with_lines, line_length);

    r.outputhash = FNV1A_hash::as_str(output);
    // convert back to binary
    r.maxbinarylength =
        impl->maximal_binary_length_from_base64(output.data(), output.size());
    std::vector<char> restored(r.maxbinarylength);
    r.convertbackresult =
        impl->base64_to_binary(output.data(), output.size(), restored.data(),
                               selected_option, last_chunk_option);
    if (const auto restoredhash = FNV1A_hash::as_str(restored);
        inputhash != restoredhash) {
      std::abort();
    }
    if (restored.size() != binary.size()) {
      std::abort();
    }
  }

  auto neq = [](const auto& a, const auto& b) { return a != b; };
  if (std::ranges::adjacent_find(results, neq) != results.end()) {
    std::cerr << "output differs between implementations\n";
    for (const auto& r : results) {
      std::cout << "written=" << r.written << " maxlength=" << r.maxbinarylength
                << " length=" << r.length << '\n';
    }
    std::abort();
  }
}

extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
  // pick one of the function pointers, based on the fuzz data
  // the first byte is which action to take. step forward
  // several bytes so the input is aligned.
  constexpr auto optionbytes = 6u;
  static_assert(optionbytes % 2 == 0,
                "optionbytes must be even to avoid misaligned char16 pointers");

  if (size < optionbytes) {
    return 0;
  }
  constexpr auto Ncases = 5u;
  constexpr auto actionmask = std::bit_ceil(Ncases) - 1;
  const auto action = data[0] & actionmask;

  // pick a random option
  const auto selected_option = [](auto index) {
    if (index >= options.size())
      return options[0];
    else {
      return options[index];
    }
  }(data[1] & (std::bit_ceil(options.size()) - 1));
  const auto selected_last_chunk =
      (selected_option == simdutf::base64_url ||
       selected_option == simdutf::base64_default_no_padding)
          ? simdutf::last_chunk_handling_options::loose
          : [](auto index) {
              if (index >= last_chunk.size())
                return last_chunk[0];
              else {
                return last_chunk[index];
              }
            }(data[2] & (std::bit_ceil(last_chunk.size()) - 1));

  // decode buffer size
  const std::size_t decode_buffer_size = (data[4] << 8) + data[3];

  // line length must be at least 4
  const std::size_t line_length = unsigned{data[5]} + 4u;

  data += optionbytes;
  size -= optionbytes;

  switch (action) {
  case 0: {
    const std::span<const char> chardata{(const char*)data, size};
    roundtrip(chardata, selected_option, selected_last_chunk, line_length);
  } break;
  case 1: {
    const std::span<const char> chardata{(const char*)data, size};
    decode(chardata, selected_option, selected_last_chunk);
  } break;
  case 2: {
    const std::span<const char16_t> chardata{(const char16_t*)data, size / 2};
    decode(chardata, selected_option, selected_last_chunk);
  } break;
  case 3: {
    const std::span<const char> chardata{(const char*)data, size};
    decode_safe(chardata, selected_option, decode_buffer_size,
                selected_last_chunk);
  } break;
  case 4: {
    const std::span<const char16_t> chardata{(const char16_t*)data, size / 2};
    decode_safe(chardata, selected_option, decode_buffer_size,
                selected_last_chunk);
  } break;
  }

  return 0;
}
