/*
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#pragma once

#include <functional>
#include <type_traits>
#include <utility>

#include <folly/Traits.h>
#include <folly/Utility.h>

namespace folly {

template <typename Pointer>
class propagate_const;

template <typename Pointer>
constexpr Pointer& get_underlying(propagate_const<Pointer>& obj) {
  return obj.pointer_;
}

template <typename Pointer>
constexpr Pointer const& get_underlying(propagate_const<Pointer> const& obj) {
  return obj.pointer_;
}

namespace detail {
template <class Pointer>
using is_propagate_const = is_instantiation_of<propagate_const, Pointer>;
template <typename T>
using is_decay_propagate_const = is_propagate_const<std::decay_t<T>>;

namespace propagate_const_adl {
using std::swap;
template <typename T>
auto adl_swap(T& a, T& b) noexcept(noexcept(swap(a, b)))
    -> decltype(swap(a, b)) {
  swap(a, b);
}
} // namespace propagate_const_adl
} // namespace detail

//  mimic: std::experimental::propagate_const, C++ Library Fundamentals TS v2
template <typename Pointer>
class propagate_const {
 public:
  using element_type =
      std::remove_reference_t<decltype(*std::declval<Pointer&>())>;

  constexpr propagate_const() = default;
  constexpr propagate_const(propagate_const&&) = default;
  propagate_const(propagate_const const&) = delete;

  template <
      typename OtherPointer,
      std::enable_if_t<
          std::is_constructible<Pointer, OtherPointer&&>::value &&
              !std::is_convertible<OtherPointer&&, Pointer>::value,
          int> = 0>
  constexpr explicit propagate_const(propagate_const<OtherPointer>&& other)
      : pointer_(static_cast<OtherPointer&&>(other.pointer_)) {}

  template <
      typename OtherPointer,
      std::enable_if_t<
          std::is_constructible<Pointer, OtherPointer&&>::value &&
              std::is_convertible<OtherPointer&&, Pointer>::value,
          int> = 0>
  constexpr propagate_const(propagate_const<OtherPointer>&& other)
      : pointer_(static_cast<OtherPointer&&>(other.pointer_)) {}

  template <
      typename OtherPointer,
      std::enable_if_t<
          !detail::is_decay_propagate_const<OtherPointer>::value &&
              std::is_constructible<Pointer, OtherPointer&&>::value &&
              !std::is_convertible<OtherPointer&&, Pointer>::value,
          int> = 0>
  constexpr explicit propagate_const(OtherPointer&& other)
      : pointer_(static_cast<OtherPointer&&>(other)) {}

  template <
      typename OtherPointer,
      std::enable_if_t<
          !detail::is_decay_propagate_const<OtherPointer>::value &&
              std::is_constructible<Pointer, OtherPointer&&>::value &&
              std::is_convertible<OtherPointer&&, Pointer>::value,
          int> = 0>
  constexpr propagate_const(OtherPointer&& other)
      : pointer_(static_cast<OtherPointer&&>(other)) {}

  constexpr propagate_const& operator=(propagate_const&&) = default;
  propagate_const& operator=(propagate_const const&) = delete;

  template <
      typename OtherPointer,
      typename =
          std::enable_if_t<std::is_convertible<OtherPointer&&, Pointer>::value>>
  constexpr propagate_const& operator=(propagate_const<OtherPointer>&& other) {
    pointer_ = static_cast<OtherPointer&&>(other.pointer_);
  }

  template <
      typename OtherPointer,
      typename = std::enable_if_t<
          !detail::is_decay_propagate_const<OtherPointer>::value &&
          std::is_convertible<OtherPointer&&, Pointer>::value>>
  constexpr propagate_const& operator=(OtherPointer&& other) {
    pointer_ = static_cast<OtherPointer&&>(other);
    return *this;
  }

  constexpr void swap(propagate_const& other) noexcept(
      noexcept(detail::propagate_const_adl::adl_swap(
          std::declval<Pointer&>(), other.pointer_))) {
    detail::propagate_const_adl::adl_swap(pointer_, other.pointer_);
  }

  constexpr element_type* get() { return get_(pointer_); }

  constexpr element_type const* get() const { return get_(pointer_); }

  constexpr explicit operator bool() const {
    return static_cast<bool>(pointer_);
  }

  constexpr element_type& operator*() { return *get(); }

  constexpr element_type const& operator*() const { return *get(); }

  constexpr element_type* operator->() { return get(); }

  constexpr element_type const* operator->() const { return get(); }

  template <
      typename OtherPointer = Pointer,
      typename = std::enable_if_t<
          std::is_pointer<OtherPointer>::value ||
          std::is_convertible<OtherPointer, element_type*>::value>>
  constexpr operator element_type*() {
    return get();
  }

  template <
      typename OtherPointer = Pointer,
      typename = std::enable_if_t<
          std::is_pointer<OtherPointer>::value ||
          std::is_convertible<OtherPointer, element_type const*>::value>>
  constexpr operator element_type const *() const {
    return get();
  }

 private:
  friend constexpr Pointer& get_underlying<>(propagate_const&);
  friend constexpr Pointer const& get_underlying<>(propagate_const const&);
  template <typename OtherPointer>
  friend class propagate_const;

  template <typename T>
  constexpr static T* get_(T* t) {
    return t;
  }
  template <typename T>
  constexpr static auto get_(T& t) -> decltype(t.get()) {
    return t.get();
  }

  Pointer pointer_;
};

template <typename Pointer>
constexpr void swap(
    propagate_const<Pointer>& a,
    propagate_const<Pointer>& b) noexcept(noexcept(a.swap(b))) {
  a.swap(b);
}

template <typename Pointer>
constexpr bool operator==(propagate_const<Pointer> const& a, std::nullptr_t) {
  return get_underlying(a) == nullptr;
}

template <typename Pointer>
constexpr bool operator==(std::nullptr_t, propagate_const<Pointer> const& a) {
  return nullptr == get_underlying(a);
}

template <typename Pointer>
constexpr bool operator!=(propagate_const<Pointer> const& a, std::nullptr_t) {
  return get_underlying(a) != nullptr;
}

template <typename Pointer>
constexpr bool operator!=(std::nullptr_t, propagate_const<Pointer> const& a) {
  return nullptr != get_underlying(a);
}

template <typename Pointer>
constexpr bool operator==(
    propagate_const<Pointer> const& a, propagate_const<Pointer> const& b) {
  return get_underlying(a) == get_underlying(b);
}

template <typename Pointer>
constexpr bool operator!=(
    propagate_const<Pointer> const& a, propagate_const<Pointer> const& b) {
  return get_underlying(a) != get_underlying(b);
}

template <typename Pointer>
constexpr bool operator<(
    propagate_const<Pointer> const& a, propagate_const<Pointer> const& b) {
  return get_underlying(a) < get_underlying(b);
}

template <typename Pointer>
constexpr bool operator<=(
    propagate_const<Pointer> const& a, propagate_const<Pointer> const& b) {
  return get_underlying(a) <= get_underlying(b);
}

template <typename Pointer>
constexpr bool operator>(
    propagate_const<Pointer> const& a, propagate_const<Pointer> const& b) {
  return get_underlying(a) > get_underlying(b);
}

template <typename Pointer>
constexpr bool operator>=(
    propagate_const<Pointer> const& a, propagate_const<Pointer> const& b) {
  return get_underlying(a) >= get_underlying(b);
}

//  Note: contrary to the specification, the heterogeneous comparison operators
//  only participate in overload resolution when the equivalent heterogeneous
//  comparison operators on the underlying pointers, as returned by invocation
//  of get_underlying, would also participate in overload resolution.

template <typename Pointer, typename Other>
constexpr auto operator==(propagate_const<Pointer> const& a, Other const& b)
    -> decltype(get_underlying(a) == b, false) {
  return get_underlying(a) == b;
}

template <typename Pointer, typename Other>
constexpr auto operator!=(propagate_const<Pointer> const& a, Other const& b)
    -> decltype(get_underlying(a) != b, false) {
  return get_underlying(a) != b;
}

template <typename Pointer, typename Other>
constexpr auto operator<(propagate_const<Pointer> const& a, Other const& b)
    -> decltype(get_underlying(a) < b, false) {
  return get_underlying(a) < b;
}

template <typename Pointer, typename Other>
constexpr auto operator<=(propagate_const<Pointer> const& a, Other const& b)
    -> decltype(get_underlying(a) <= b, false) {
  return get_underlying(a) <= b;
}

template <typename Pointer, typename Other>
constexpr auto operator>(propagate_const<Pointer> const& a, Other const& b)
    -> decltype(get_underlying(a) > b, false) {
  return get_underlying(a) > b;
}

template <typename Pointer, typename Other>
constexpr auto operator>=(propagate_const<Pointer> const& a, Other const& b)
    -> decltype(get_underlying(a) >= b, false) {
  return get_underlying(a) >= b;
}

template <typename Other, typename Pointer>
constexpr auto operator==(Other const& a, propagate_const<Pointer> const& b)
    -> decltype(a == get_underlying(b), false) {
  return a == get_underlying(b);
}

template <typename Other, typename Pointer>
constexpr auto operator!=(Other const& a, propagate_const<Pointer> const& b)
    -> decltype(a != get_underlying(b), false) {
  return a != get_underlying(b);
}

template <typename Other, typename Pointer>
constexpr auto operator<(Other const& a, propagate_const<Pointer> const& b)
    -> decltype(a < get_underlying(b), false) {
  return a < get_underlying(b);
}

template <typename Other, typename Pointer>
constexpr auto operator<=(Other const& a, propagate_const<Pointer> const& b)
    -> decltype(a <= get_underlying(b), false) {
  return a <= get_underlying(b);
}

template <typename Other, typename Pointer>
constexpr auto operator>(Other const& a, propagate_const<Pointer> const& b)
    -> decltype(a > get_underlying(b), false) {
  return a > get_underlying(b);
}

template <typename Other, typename Pointer>
constexpr auto operator>=(Other const& a, propagate_const<Pointer> const& b)
    -> decltype(a >= get_underlying(b), false) {
  return a >= get_underlying(b);
}

} // namespace folly

namespace std {

template <typename Pointer>
struct hash<folly::propagate_const<Pointer>> : private hash<Pointer> {
  using hash<Pointer>::hash;

  size_t operator()(folly::propagate_const<Pointer> const& obj) const {
    return hash<Pointer>::operator()(folly::get_underlying(obj));
  }
};

template <typename Pointer>
struct equal_to<folly::propagate_const<Pointer>> : private equal_to<Pointer> {
  using equal_to<Pointer>::equal_to;

  constexpr bool operator()(
      folly::propagate_const<Pointer> const& a,
      folly::propagate_const<Pointer> const& b) {
    return equal_to<Pointer>::operator()(
        folly::get_underlying(a), folly::get_underlying(b));
  }
};

template <typename Pointer>
struct not_equal_to<folly::propagate_const<Pointer>>
    : private not_equal_to<Pointer> {
  using not_equal_to<Pointer>::not_equal_to;

  constexpr bool operator()(
      folly::propagate_const<Pointer> const& a,
      folly::propagate_const<Pointer> const& b) {
    return not_equal_to<Pointer>::operator()(
        folly::get_underlying(a), folly::get_underlying(b));
  }
};

template <typename Pointer>
struct less<folly::propagate_const<Pointer>> : private less<Pointer> {
  using less<Pointer>::less;

  constexpr bool operator()(
      folly::propagate_const<Pointer> const& a,
      folly::propagate_const<Pointer> const& b) {
    return less<Pointer>::operator()(
        folly::get_underlying(a), folly::get_underlying(b));
  }
};

template <typename Pointer>
struct greater<folly::propagate_const<Pointer>> : private greater<Pointer> {
  using greater<Pointer>::greater;

  constexpr bool operator()(
      folly::propagate_const<Pointer> const& a,
      folly::propagate_const<Pointer> const& b) {
    return greater<Pointer>::operator()(
        folly::get_underlying(a), folly::get_underlying(b));
  }
};

template <typename Pointer>
struct less_equal<folly::propagate_const<Pointer>>
    : private less_equal<Pointer> {
  using less_equal<Pointer>::less_equal;

  constexpr bool operator()(
      folly::propagate_const<Pointer> const& a,
      folly::propagate_const<Pointer> const& b) {
    return less_equal<Pointer>::operator()(
        folly::get_underlying(a), folly::get_underlying(b));
  }
};

template <typename Pointer>
struct greater_equal<folly::propagate_const<Pointer>>
    : private greater_equal<Pointer> {
  using greater_equal<Pointer>::greater_equal;

  constexpr bool operator()(
      folly::propagate_const<Pointer> const& a,
      folly::propagate_const<Pointer> const& b) {
    return greater_equal<Pointer>::operator()(
        folly::get_underlying(a), folly::get_underlying(b));
  }
};

} // namespace std
