// Copyright 2025 Steven Le Rouzic
//
// SPDX-License-Identifier: BSD-3-Clause

#pragma once

#include "asl/base/assert.hpp"
#include "asl/base/meta.hpp"
#include "asl/base/functional.hpp"
#include "asl/base/annotations.hpp"
#include "asl/types/maybe_uninit.hpp"
#include "asl/hashing/hash.hpp"

namespace asl
{

struct nullopt_t {};
static constexpr nullopt_t nullopt{};

// @Todo(option) Reference
// @Todo(option) Function
// @Todo(option) Arrays

template<is_object T> class option;

namespace option_internal
{

template<typename T, typename U>
concept not_constructible_from_option =
    !constructible_from<T, option<U>&> &&
    !constructible_from<T, const option<U>&> &&
    !constructible_from<T, option<U>&&> &&
    !constructible_from<T, const option<U>&&>;

template<typename T, typename U>
concept not_assignable_from_option =
    !assignable_from<T&, option<U>&> &&
    !assignable_from<T&, const option<U>&> &&
    !assignable_from<T&, option<U>&&> &&
    !assignable_from<T&, const option<U>&&>;

template<typename T, typename U>
concept not_constructible_assignable_from_option =
    not_constructible_from_option<T, U> &&
    not_assignable_from_option<T, U>;

} // namespace option_internal

template<typename T>
concept is_option = requires
{
    typename T::type;
    requires same_as<un_cvref_t<T>, option<typename T::type>>;
};

template<is_object T>
class option
{
    static constexpr bool kHasNiche = has_niche<T>;

    using HasValueMarker = select_t<kHasNiche, empty, bool>;

    maybe_uninit<T> m_payload{};
    ASL_NO_UNIQUE_ADDRESS HasValueMarker m_has_value{};

    template<is_object U>
    friend class option;

    template<typename... Args>
    constexpr void construct(Args&&... args)
    {
        ASL_ASSERT(!has_value());

        if constexpr (!kHasNiche)
        {
            m_payload.construct_unsafe(std::forward<Args>(args)...);
            m_has_value = true;
        }
        else
        {
            if constexpr (move_assignable<T>)
            {
                m_payload.assign_unsafe(T{std::forward<Args>(args)...});
            }
            else
            {
                m_payload.destroy_unsafe();
                m_payload.construct_unsafe(std::forward<Args>(args)...);
            }
        }
    }

    template<typename U>
    constexpr void assign(U&& arg)
    {
        ASL_ASSERT(has_value());
        m_payload.assign_unsafe(std::forward<U>(arg));
    }

public:
    using type = T;

    constexpr option() : option{nullopt} {}

     // NOLINTNEXTLINE(*explicit*)
    constexpr option(nullopt_t) requires (!kHasNiche) {}

     // NOLINTNEXTLINE(*explicit*)
    constexpr option(nullopt_t) requires kHasNiche : m_payload{in_place, niche_t{}} {}

    template<typename U = T>
    constexpr explicit (!convertible_to<U&&, T>)
    option(U&& value)
        requires (
            kHasNiche &&
            constructible_from<T, U&&> &&
            !same_as<un_cvref_t<U>, option>
        )
        : m_payload{in_place, std::forward<U>(value)}
    {}

    template<typename U = T>
    constexpr explicit (!convertible_to<U&&, T>)
    option(U&& value)
        requires (
            !kHasNiche &&
            constructible_from<T, U&&> &&
            !is_option<U>
        )
        : m_payload{in_place, std::forward<U>(value)}
        , m_has_value{true}
    {}

    constexpr option(const option& other) requires trivially_copy_constructible<T> = default;
    constexpr option(const option& other) requires (!copy_constructible<T>) = delete;

    constexpr option(const option& other)
        requires copy_constructible<T> && (!trivially_copy_constructible<T>)
        : option{nullopt}
    {
        if (other.has_value())
        {
            construct(other.m_payload.as_init_unsafe());
        }
    }

    constexpr option(option&& other) requires trivially_move_constructible<T> = default;
    constexpr option(option&& other) requires (!move_constructible<T>) = delete;

    constexpr option(option&& other)
        requires move_constructible<T> && (!trivially_move_constructible<T>)
        : option{nullopt}
    {
        if (other.has_value())
        {
            construct(std::move(other.m_payload.as_init_unsafe()));
        }
    }

    template<typename U>
    constexpr explicit (!convertible_to<const U&, T>)
    option(const option<U>& other)
        requires (
            constructible_from<T, const U&> &&
            option_internal::not_constructible_from_option<T, U>
        )
        : option{nullopt}
    {
        if (other.has_value())
        {
            construct(other.m_payload.as_init_unsafe());
        }
    }

    template<typename U>
    constexpr explicit (!convertible_to<U&&, T>)
    option(option<U>&& other)
        requires (
            constructible_from<T, U&&> &&
            option_internal::not_constructible_from_option<T, U>
        )
        : option{nullopt}
    {
        if (other.has_value())
        {
            construct(std::move(other).m_payload.as_init_unsafe());
        }
    }

    constexpr option& operator=(nullopt_t) &
    {
        reset();
        return *this;
    }

    template<typename U = T>
    constexpr option& operator=(U&& value) &
        requires (
            assignable_from<T&, U&&> &&
            constructible_from<T, U&&> &&
            !is_option<U>
        )
    {
        if (has_value())
        {
            assign(std::forward<U>(value));
        }
        else
        {
            construct(std::forward<U>(value));
        }

        return *this;
    }

    constexpr option& operator=(const option& other) &
        requires (!copy_assignable<T>) = delete;

    constexpr option& operator=(const option& other) &
        requires trivially_copy_assignable<T> = default;

    constexpr option& operator=(const option& other) &
        requires copy_assignable<T> && (!trivially_copy_constructible<T>)
    {
        if (&other == this) { return *this; }

        if (other.has_value())
        {
            if (has_value())
            {
                assign(other.m_payload.as_init_unsafe());
            }
            else
            {
                construct(other.m_payload.as_init_unsafe());
            }
        }
        else if (has_value())
        {
            reset();
        }

        return *this;
    }

    constexpr option& operator=(option&& other) &
        requires (!move_assignable<T>) = delete;

    constexpr option& operator=(option&& other) &
        requires trivially_move_assignable<T> = default;

    constexpr option& operator=(option&& other) &
        requires move_assignable<T> && (!trivially_move_constructible<T>)
    {
        if (&other == this) { return *this; }

        if (other.has_value())
        {
            if (has_value())
            {
                assign(std::move(other.m_payload.as_init_unsafe()));
            }
            else
            {
                construct(std::move(other.m_payload.as_init_unsafe()));
            }
        }
        else if (has_value())
        {
            reset();
        }

        return *this;
    }

    template<typename U = T>
    constexpr option& operator=(const option<U>& other) &
        requires (
            constructible_from<T, const U&> &&
            assignable_from<T&, const U&> &&
            option_internal::not_constructible_assignable_from_option<T, U>
        )
    {
        if (other.has_value())
        {
            if (has_value())
            {
                assign(other.m_payload.as_init_unsafe());
            }
            else
            {
                construct(other.m_payload.as_init_unsafe());
            }
        }
        else if (has_value())
        {
            reset();
        }

        return *this;
    }

    template<typename U = T>
    constexpr option& operator=(option<U>&& other) &
        requires (
            constructible_from<T, U&&> &&
            assignable_from<T&, U&&> &&
            option_internal::not_constructible_assignable_from_option<T, U>
        )
    {
        if (other.has_value())
        {
            if (has_value())
            {
                assign(std::move(other).m_payload.as_init_unsafe());
            }
            else
            {
                construct(std::move(other).m_payload.as_init_unsafe());
            }
        }
        else if (has_value())
        {
            reset();
        }

        return *this;
    }

    constexpr ~option() requires trivially_destructible<T> = default;
    constexpr ~option() requires (!trivially_destructible<T>)
    {
        reset();
    }

    constexpr void reset()
    {
        if (!has_value()) { return; }

        if constexpr (kHasNiche)
        {
            if constexpr (move_assignable<T>)
            {
                m_payload.assign_unsafe(std::move(T{niche_t{}}));
            }
            else
            {
                m_payload.destroy_unsafe();
                m_payload.construct_unsafe(niche_t{});
            }
        }
        else
        {
            m_has_value = false;
            m_payload.destroy_unsafe();
        }
    }

    [[nodiscard]] constexpr bool has_value() const
    {
        if constexpr (kHasNiche)
        {
            return m_payload.as_init_unsafe() != niche_t{};
        }
        else
        {
            return m_has_value;
        }
    }

    constexpr auto&& value(this auto&& self)
    {
        ASL_ASSERT_RELEASE(self.has_value());
        return std::forward<decltype(self)>(self).m_payload.as_init_unsafe();
    }

    template<typename U>
    constexpr T value_or(U&& other_value) const&
        requires copy_constructible<T> && convertible_to<U&&, T>
    {
        return has_value() ? value() : static_cast<T>(std::forward<U>(other_value));
    }

    template<typename U>
    constexpr T value_or(U&& other_value) &&
        requires move_constructible<T> && convertible_to<U&&, T>
    {
        return has_value() ? std::move(value()) : static_cast<T>(std::forward<U>(other_value));
    }

    constexpr T& emplace(auto&&... args) &
        requires constructible_from<T, decltype(args)...>
    {
        if (has_value()) { reset(); }
        construct(std::forward<decltype(args)>(args)...);
        return value();
    }

    template<typename F>
    constexpr auto and_then(this auto&& self, F&& f)
    {
        using Result = invoke_result_t<F, copy_cref_t<decltype(self), T>>;
        static_assert(is_option<Result>);

        if (self.has_value())
        {
            return invoke(std::forward<F>(f), std::forward<decltype(self)>(self).value());
        }
        return Result{ asl::nullopt };
    }

    template<typename F>
    constexpr auto transform(this auto&& self, F&& f)
    {
        using Result = invoke_result_t<F, copy_cref_t<decltype(self), T>>;
        if (self.has_value())
        {
            return option<un_cvref_t<Result>>{
                invoke(std::forward<F>(f), std::forward<decltype(self)>(self).value())
            };
        }
        return option<un_cvref_t<Result>>{ asl::nullopt };
    }

    template<typename F>
    constexpr option or_else(F&& f) const&
        requires same_as<un_cvref_t<invoke_result_t<F>>, option>
    {
        return has_value() ? *this : invoke(std::forward<F>(f));
    }

    template<typename F>
    constexpr option or_else(F&& f) &&
        requires same_as<un_cvref_t<invoke_result_t<F>>, option>
    {
        return has_value() ? std::move(*this) : invoke(std::forward<F>(f));
    }

    template<typename H>
    requires (!uniquely_represented<option>) && hashable<T>
    friend H AslHashValue(H h, const option& opt)
    {
        if (!opt.has_value())
        {
            return H::combine(std::move(h), 0);
        }
        return H::combine(std::move(h), 1, opt.value());
    }
};

template<typename T>
requires has_niche<T> && uniquely_represented<T>
struct is_uniquely_represented<option<T>> : true_type {};

template<typename T>
option(T) -> option<T>;

} // namespace asl