// Glaze Library
// For the license information refer to glaze.hpp

#pragma once

#include <cstdint>
#include <cstring>

#include "glaze/base64/base64.hpp"
#include "glaze/eetf/ei.hpp"
#include "glaze/eetf/opts.hpp"
#include "glaze/json/write.hpp"

namespace glz
{
   namespace detail
   {
      template <class It0, class It1>
      GLZ_ALWAYS_INLINE size_t get8s(auto&& ctx, It0&& it, It1&& end) noexcept
      {
         if (check_invalid_offset(ctx, it, end, 1)) return 0;
         return std::size_t(static_cast<uint8_t>(*it++));
      }

      template <class It0, class It1>
      GLZ_ALWAYS_INLINE size_t get16be(auto&& ctx, It0&& it, It1&& end) noexcept
      {
         if (check_invalid_offset(ctx, it, end, 2)) return 0;
         const std::size_t b1 = std::size_t(static_cast<uint8_t>(*it++)) << 8;
         return b1 | static_cast<uint8_t>(*it++);
      }

      template <class It0, class It1>
      GLZ_ALWAYS_INLINE size_t get32be(auto&& ctx, It0&& it, It1&& end) noexcept
      {
         if (check_invalid_offset(ctx, it, end, sizeof(uint32_t))) return 0;
         const std::size_t b1 = std::size_t(static_cast<uint8_t>(*it++)) << 24;
         const std::size_t b2 = std::size_t(static_cast<uint8_t>(*it++)) << 16;
         const std::size_t b3 = std::size_t(static_cast<uint8_t>(*it++)) << 8;
         return b1 | b2 | b3 | static_cast<uint8_t>(*it++);
      }

      template <auto Opts, typename I>
      GLZ_ALWAYS_INLINE void term_to_json_number(I&& val, auto&& ctx, auto&& it, auto&& end, auto& out,
                                                 auto&& ix) noexcept
      {
         decode_number(val, ctx, it, end);
         if (bool(ctx.error)) {
            return;
         }
         to<JSON, I>::template op<Opts>(val, ctx, out, ix);
      }

      template <auto Opts>
      GLZ_ALWAYS_INLINE void term_to_json_big_integer(auto&& ctx, auto&& it, auto&& end, auto& out, auto&& ix) noexcept
      {
         auto tit = it;
         ++tit; // skip type
         const auto size = get8s(ctx, tit, end);
         if (bool(ctx.error)) return;
         if (size > 8u) {
            ctx.error = error_code::no_matching_variant_type;
            return;
         }

         if (check_invalid_offset(ctx, tit, end, 1 + size)) return;
         const int sign = *tit++;
         if (sign) {
            term_to_json_number<Opts>(std::int64_t{}, ctx, it, end, out, ix);
         }
         else {
            term_to_json_number<Opts>(std::uint64_t{}, ctx, it, end, out, ix);
         }
      }

      // Key marks the term as an object key rather than a value. JSON keys must be strings, so
      // the atoms that would otherwise emit as bare `true` / `false` stay quoted there.
      template <auto Opts, bool Key = false, class Buffer>
      void term_to_json_value(auto&& ctx, auto&& it, auto&& end, Buffer& out, auto&& ix, uint32_t recursive_depth)
      {
         // Check recursion depth limit
         if (recursive_depth >= max_recursive_depth_limit) [[unlikely]] {
            ctx.error = error_code::exceeded_max_recursive_depth;
            return;
         }

         if (invalid_end(ctx, it, end)) [[unlikely]] {
            return;
         }

         auto write_sequence = [](size_t arity, size_t index, auto&& ctx, auto&& it, auto&& end, Buffer& out, auto&& ix,
                                  auto recursive_depth) {
            std::advance(it, index);
            if (invalid_end(ctx, it, end)) [[unlikely]] {
               return;
            }

            dump('[', out, ix);
            while (arity--) {
               term_to_json_value<Opts>(ctx, it, end, out, ix, recursive_depth);
               // Stop on error instead of spinning the remaining (attacker-declared, up to 2^32)
               // iterations once the input is exhausted.
               if (bool(ctx.error)) [[unlikely]] {
                  return;
               }
               if (arity) {
                  dump(',', out, ix);
                  if constexpr (Opts.prettify) {
                     dump(' ', out, ix);
                  }
               }
            }
            dump(']', out, ix);
         };

         const auto type = uint8_t(*it);
         switch (type) {
         case ERL_SMALL_INTEGER_EXT:
         case ERL_INTEGER_EXT: {
            // ei_decode_long reads the tag plus a fixed payload (1 byte for SMALL_INTEGER_EXT,
            // 4 for INTEGER_EXT) off the raw pointer; bound it before the decode.
            if (check_invalid_offset(ctx, it, end, type == ERL_SMALL_INTEGER_EXT ? 2u : 5u)) return;
            term_to_json_number<Opts>(std::int64_t{}, ctx, it, end, out, ix);
            if (bool(ctx.error)) return;
            break;
         }

         case ERL_SMALL_BIG_EXT: {
            term_to_json_big_integer<Opts>(ctx, it, end, out, ix);
            if (bool(ctx.error)) return;
            break;
         }

         case ERL_FLOAT_EXT:
         case NEW_FLOAT_EXT: {
            // ei_decode_double reads the tag plus a fixed payload (8 bytes for NEW_FLOAT_EXT, the
            // 31-byte ASCII form for the legacy FLOAT_EXT) off the raw pointer; bound it first.
            if (check_invalid_offset(ctx, it, end, type == NEW_FLOAT_EXT ? 9u : 32u)) return;
            term_to_json_number<Opts>(double{}, ctx, it, end, out, ix);
            if (bool(ctx.error)) return;
            break;
         }

         case ERL_STRING_EXT:
         case ERL_ATOM_EXT:
         case ERL_ATOM_UTF8_EXT: {
            ++it; // skip type
            const size_t len = get16be(ctx, it, end);
            if (bool(ctx.error)) return;
            if (check_invalid_offset(ctx, it, end, len)) return;
            const sv value{reinterpret_cast<const char*>(it), len};
            detail::emit_untrusted_string<Opts>(ctx, value, out, ix);
            std::advance(it, len);
            break;
         }

         case ERL_SMALL_ATOM_EXT:
         case ERL_SMALL_ATOM_UTF8_EXT: {
            ++it; // skip type
            const size_t len = get8s(ctx, it, end);
            if (bool(ctx.error)) return;
            if (check_invalid_offset(ctx, it, end, len)) return;
            const sv value{reinterpret_cast<const char*>(it), len};
            // A JSON object key must be a string, so an atom in key position stays quoted.
            // Dumping the bare literal there produced `{true:1}`, which reports success and
            // then fails to re-parse.
            if (not Key && value == "true") {
               dump("true", out, ix);
            }
            else if (not Key && value == "false") {
               dump("false", out, ix);
            }
            else {
               detail::emit_untrusted_string<Opts>(ctx, value, out, ix);
            }
            std::advance(it, len);
            break;
         }

         case ERL_LIST_EXT: {
            // decode_list_header reads the tag plus a 4-byte arity off the raw pointer; bound it.
            if (check_invalid_offset(ctx, it, end, 5u)) return;
            [[maybe_unused]] auto [arity, idx] = decode_list_header(ctx, it);
            if (bool(ctx.error)) {
               return;
            }
            write_sequence(arity, idx, ctx, it, end, out, ix, recursive_depth + 1);
            if (bool(ctx.error)) [[unlikely]] {
               return;
            }

            // handle tail
            if (invalid_end(ctx, it, end)) [[unlikely]] {
               return;
            }
            // A proper list terminates with ERL_NIL_EXT, a single tag byte. Read that tag directly
            // rather than through get_type/ei_get_type, which reads a 2-4 byte length header off the
            // raw pointer (past end when the tail tag is the final byte) for any other term type.
            if (uint8_t(*it) != ERL_NIL_EXT) {
               ctx.error = error_code::array_element_not_found;
               return;
            }
            ++it;
            break;
         }

         case ERL_NIL_EXT: {
            dump("[]", out, ix);
            ++it;
            break;
         }

         case ERL_SMALL_TUPLE_EXT:
         case ERL_LARGE_TUPLE_EXT: {
            // decode_tuple_header reads the tag plus the arity (1 byte for SMALL_TUPLE_EXT, 4 for
            // LARGE_TUPLE_EXT) off the raw pointer; bound it.
            if (check_invalid_offset(ctx, it, end, type == ERL_SMALL_TUPLE_EXT ? 2u : 5u)) return;
            [[maybe_unused]] auto [arity, idx] = decode_tuple_header(ctx, it);
            if (bool(ctx.error)) {
               return;
            }
            write_sequence(arity, idx, ctx, it, end, out, ix, recursive_depth + 1);
            break;
         }

         case ERL_MAP_EXT: {
            // decode_map_header reads the tag plus a 4-byte arity off the raw pointer; bound it.
            if (check_invalid_offset(ctx, it, end, 5u)) return;
            [[maybe_unused]] auto [arity, idx] = decode_map_header(ctx, it);
            if (bool(ctx.error)) {
               return;
            }

            std::advance(it, idx);
            dump('{', out, ix);
            if constexpr (Opts.prettify) {
               ctx.depth += check_indentation_width(Opts);
               dump('\n', out, ix);
               dumpn(check_indentation_char(Opts), ctx.depth, out, ix);
            }

            while (arity--) {
               // write key
               if (invalid_end(ctx, it, end)) [[unlikely]] {
                  return;
               }
               // Read the key tag directly rather than through get_type/ei_get_type, which reads a
               // 2-4 byte length header off the raw pointer (past end when the tag is the final byte).
               // is_string/is_atom accept the raw, un-normalized tag, and term_to_json_value re-reads
               // and bounds-checks the full key below. Widen to int so the tag clears the int_t
               // constraint on is_string/is_atom (uint8_t is a char type and would be rejected).
               const auto key_type = uint8_t(*it);
               // support only string or atom keys in json
               if (!(eetf::is_string(key_type) || eetf::is_atom(key_type) || eetf::is_binary(key_type))) {
                  ctx.error = error_code::syntax_error;
                  ctx.custom_error_message = "unsupported key type";
                  return;
               }
               term_to_json_value<Opts, true>(ctx, it, end, out, ix, recursive_depth + 1);
               if (bool(ctx.error)) return;
               if constexpr (Opts.prettify) {
                  dump(": ", out, ix);
               }
               else {
                  dump(':', out, ix);
               }
               // write value
               term_to_json_value<Opts>(ctx, it, end, out, ix, recursive_depth + 1);
               if (arity) {
                  dump(',', out, ix);
                  if constexpr (Opts.prettify) {
                     dump('\n', out, ix);
                     dumpn(check_indentation_char(Opts), ctx.depth, out, ix);
                  }
               }
            }
            if constexpr (Opts.prettify) {
               ctx.depth -= check_indentation_width(Opts);
               dump('\n', out, ix);
               dumpn(check_indentation_char(Opts), ctx.depth, out, ix);
            }
            dump('}', out, ix);
            break;
         }

         case ERL_BINARY_EXT: {
            ++it; // skip type
            const size_t len = get32be(ctx, it, end);
            if (bool(ctx.error)) [[unlikely]] {
               return;
            }
            // bound binary length
            if (check_invalid_offset(ctx, it, end, len)) return;
            if constexpr (check_binary_as_base64(Opts)) {
               dump('"', out, ix);
               glz::write_base64_to(ctx, reinterpret_cast<const uint8_t*>(it), len, out, ix);
               if (bool(ctx.error)) [[unlikely]] {
                  return;
               }
               dump('"', out, ix);
            }
            else {
               // A binary holds arbitrary bytes by definition, so a control character among them
               // is expected rather than exceptional. binary_as_base64 carries such a payload
               // across without involving the escaping decision at all.
               const sv value{reinterpret_cast<const char*>(it), len};
               detail::emit_untrusted_string<Opts>(ctx, value, out, ix);
            }
            std::advance(it, len);
            break;
         }

         default: {
            ctx.error = error_code::syntax_error;
            ctx.custom_error_message = "unsupported type";
            return;
         }
         }
      }
   } // namespace detail

   template <auto Opts = eetf::eetf_opts{}, contiguous EETFBuffer, class JSONBuffer>
      requires has_value_type<EETFBuffer> && (sizeof(typename EETFBuffer::value_type) == sizeof(char))
   [[nodiscard]] inline error_ctx eetf_to_json(const EETFBuffer& term, JSONBuffer& out)
   {
      size_t ix{}; // write index

      auto* it = term.data();
      auto* end = it + term.size();

      context ctx{};

      // Check format version
      if constexpr (not check_no_header(Opts)) {
         if (it == end) [[unlikely]] {
            return {0, error_code::no_read_input};
         }
         const auto version = decode_version(ctx, it);
         if (eetf_magic_version != version) {
            return {0, error_code::version_mismatch};
         }
      }

      while (it < end) {
         detail::term_to_json_value<Opts>(ctx, it, end, out, ix, 0);
         if (bool(ctx.error)) {
            return {ix, ctx.error};
         }
      }

      if constexpr (resizable<JSONBuffer>) {
         out.resize(ix);
      }

      // count is the number of bytes written. A resizable buffer carries its own size, but a
      // fixed-size one has no other way to learn how much of it now holds JSON.
      return {ix};
   }
} // namespace glz
