#pragma once

#include "routing/routing_exceptions.hpp"

#include "coding/bit_streams.hpp"
#include "coding/elias_coder.hpp"

#include "base/assert.hpp"
#include "base/bits.hpp"
#include "base/checked_cast.hpp"
#include "base/macros.hpp"

#include <limits>
#include <type_traits>

namespace routing
{
template <typename T, typename Source>
T ReadDelta(BitReader<Source> & reader)
{
  static_assert(std::is_unsigned<T>::value, "T should be an unsigned type");
  uint64_t const decoded = coding::DeltaCoder::Decode(reader);
  if (decoded > std::numeric_limits<T>::max())
  {
    MYTHROW(CorruptedDataException,
            ("Decoded value", decoded, "out of limit", std::numeric_limits<T>::max()));
  }

  return static_cast<T>(decoded);
}

template <typename T, typename Sink>
void WriteDelta(BitWriter<Sink> & writer, T value)
{
  static_assert(std::is_unsigned<T>::value, "T should be an unsigned type");
  ASSERT_GREATER(value, 0, ());

  bool const success = coding::DeltaCoder::Encode(writer, static_cast<uint64_t>(value));
  ASSERT(success, ());
  UNUSED_VALUE(success);
}

template <typename T, typename Source>
T ReadGamma(BitReader<Source> & reader)
{
  static_assert(std::is_unsigned<T>::value, "T should be an unsigned type");
  uint64_t const decoded = coding::GammaCoder::Decode(reader);
  if (decoded > std::numeric_limits<T>::max())
  {
    MYTHROW(CorruptedDataException,
            ("Decoded value", decoded, "out of limit", std::numeric_limits<T>::max()));
  }

  return static_cast<T>(decoded);
}

template <typename T, typename Sink>
void WriteGamma(BitWriter<Sink> & writer, T value)
{
  static_assert(std::is_unsigned<T>::value, "T should be an unsigned type");
  ASSERT_GREATER(value, 0, ());

  bool const success = coding::GammaCoder::Encode(writer, static_cast<uint64_t>(value));
  ASSERT(success, ());
  UNUSED_VALUE(success);
}

// C++ standard says:
// if the value can't be represented in the destination unsigned type,
// the result is implementation-defined.
//
// ModularCast makes unambiguous conversion from unsigned value to signed.
// The resulting value is the least signed integer congruent to the source integer
// (modulo 2^n where n is the number of bits used to represent the unsigned type)
template <typename Unsigned, typename Signed = std::make_signed_t<Unsigned>>
Signed ModularCast(Unsigned value)
{
  static_assert(std::is_unsigned<Unsigned>::value, "T should be an unsigned type");

  if (value <= static_cast<Unsigned>(std::numeric_limits<Signed>::max()))
    return static_cast<Signed>(value);

  auto constexpr minSignedT = std::numeric_limits<Signed>::min();
  return static_cast<Signed>(value - static_cast<Unsigned>(minSignedT)) + minSignedT;
}

// Encodes current as delta compared with prev.
template <typename T, typename UnsignedT = std::make_unsigned_t<T>>
UnsignedT EncodeZigZagDelta(T prev, T current)
{
  static_assert(std::is_integral<T>::value, "T should be an integral type");

  auto const unsignedPrev = static_cast<UnsignedT>(prev);
  auto const unsignedCurrent = static_cast<UnsignedT>(current);
  auto originalDelta = ModularCast(static_cast<UnsignedT>(unsignedCurrent - unsignedPrev));
  static_assert(std::is_same<decltype(originalDelta), std::make_signed_t<T>>::value,
                "It's expected that ModuleCast returns SignedT");

  auto encodedDelta = bits::ZigZagEncode(originalDelta);
  static_assert(std::is_same<decltype(encodedDelta), UnsignedT>::value,
                "It's expected that bits::ZigZagEncode returns UnsignedT");
  return encodedDelta;
}

// Reverse function for EncodeZigZagDelta.
template <typename T, typename UnsignedT = std::make_unsigned_t<T>>
T DecodeZigZagDelta(T prev, UnsignedT delta)
{
  static_assert(std::is_integral<T>::value, "T should be an integral type");

  auto decoded = bits::ZigZagDecode(delta);
  static_assert(std::is_same<decltype(decoded), std::make_signed_t<T>>::value,
                "It's expected that bits::ZigZagDecode returns SignedT");
  return prev + static_cast<T>(decoded);
}
}  // namespace routing
