#pragma once

#include <type_traits>
#include <utility>

namespace util {

template <typename E>
constexpr auto underlying_type_cast(const E &value) -> std::underlying_type_t<E>
requires std::is_enum_v<E> {
  return static_cast<std::underlying_type_t<E>>(value);
}
namespace bitflag_operators {

template <typename E>
concept enum_type = std::is_enum_v<E>;

template <enum_type E>
constexpr auto operator&(const E &rhs, const E &lhs) -> E {
  return static_cast<E>(underlying_type_cast(rhs) & underlying_type_cast(lhs));
}

template <enum_type E> constexpr auto operator&=(E &rhs, const E &lhs) -> E & {
  rhs = rhs & lhs;
  return rhs;
}

template <enum_type E>
constexpr auto operator|(const E &rhs, const E &lhs) -> E {
  return static_cast<E>(underlying_type_cast(rhs) | underlying_type_cast(lhs));
}

template <enum_type E> constexpr auto operator|=(E &rhs, const E &lhs) -> E & {
  rhs = rhs | lhs;
  return rhs;
}

template <enum_type E>
constexpr auto operator^(const E &rhs, const E &lhs) -> E {
  return static_cast<E>(underlying_type_cast(rhs) ^ underlying_type_cast(lhs));
}

template <enum_type E> constexpr auto operator^=(E &rhs, const E &lhs) -> E & {
  rhs = rhs ^ lhs;
  return rhs;
}

template <enum_type E> constexpr auto operator~(const E &rhs) -> E {
  return static_cast<E>(~underlying_type_cast(rhs));
}

template <enum_type E> constexpr auto is_empty(const E &value) -> bool {
  return std::to_underlying(value) == 0;
}

} // namespace bitflag_operators

using bitflag_operators::enum_type;

template <typename E>
requires std::is_enum_v<E>
class EnumFlag {
  E flags;

public:
  using U = std::underlying_type_t<E>;
  using value_type = U;

  constexpr EnumFlag() noexcept : flags(static_cast<E>(U(0))) {}
  constexpr EnumFlag(const U &value) noexcept : flags(static_cast<E>(value)) {}
  constexpr EnumFlag(const E &value) noexcept : flags(value) {}

  constexpr auto operator=(const U &value) -> EnumFlag & {
    flags = static_cast<E>(value);
    return *this;
  }
  constexpr auto operator=(const E &value) -> EnumFlag & {
    flags = value;
    return *this;
  }

  constexpr auto as_underlying_type() const -> U {
    return static_cast<U>(flags);
  }
  constexpr auto as_enum_type() const -> E { return flags; }

  /// `union` is reserved
  constexpr auto unison(const EnumFlag &other) const -> EnumFlag {
    return this | other;
  }

  constexpr auto intersection(const EnumFlag &other) const -> EnumFlag {
    return *this & other;
  }

  constexpr auto contains(const EnumFlag &other) const -> bool {
    return intersection(other).as_underlying_type() ==
           other.as_underlying_type();
  }

  constexpr auto intersects(const EnumFlag &other) const -> bool {
    return intersection(other).as_underlying_type() != 0;
  }

  constexpr auto add(const EnumFlag &other) -> EnumFlag & {
    *this |= other;
    return *this;
  }

  constexpr auto remove(const EnumFlag &other) -> EnumFlag & {
    *this &= ~other;
    return *this;
  }

  constexpr operator U() const { return as_underlying_type(); }
  constexpr operator E() const { return as_enum_type(); }

  // auto operator==(const EnumFlag& other) const {
  //   return as_underlying_type() == other.as_underlying_type();
  // }

  constexpr auto operator<=>(const EnumFlag &other) const {
    return other.as_underlying_type() <=> other.as_underlying_type();
  }

  // operator bool() const { return as_underlying_type() != 0; }

  constexpr auto operator|=(const EnumFlag &other) {
    flags = static_cast<E>(as_underlying_type() | other.as_underlying_type());
  }

  constexpr auto operator|(const EnumFlag &other) const -> EnumFlag {
    auto flag = *this;
    flag |= other;

    return flag;
  }

  constexpr auto operator&=(const EnumFlag &other) {
    flags = static_cast<E>(as_underlying_type() & other.as_underlying_type());
  }

  constexpr auto operator&(const EnumFlag &other) const -> EnumFlag {
    auto flag = *this;
    flag &= other;

    return flag;
  }

  constexpr auto operator~() const -> EnumFlag {
    return static_cast<E>(~as_underlying_type());
  }

  constexpr auto operator^=(const EnumFlag &other) {
    flags = static_cast<E>(as_underlying_type() ^ other.as_underlying_type());
  }

  constexpr auto operator^(const EnumFlag &other) const -> EnumFlag {
    auto flag = *this;
    flag ^= other;

    return flag;
  }
};
} // namespace util