#pragma once
#include <luna/Badge.h>
#include <luna/Check.h>
#include <luna/Move.h>
#include <luna/PlacementNew.h>

template <typename T> class Result;

template <typename T> class Option
{
  public:
    Option(const T& value) : m_has_value(true)
    {
        m_storage.store_reference(value);
    }

    Option(T&& value) : m_has_value(true)
    {
        m_storage.store_moved_reference(move(value));
    }

    Option(const Option<T>& other) : m_has_value(other.m_has_value)
    {
        if (m_has_value) { m_storage.store_reference(other.m_storage.fetch_reference()); }
    }

    Option(Option<T>&& other) : m_has_value(other.m_has_value)
    {
        other.m_has_value = false;
        if (m_has_value) { m_storage.store_moved_reference(move(other.m_storage.fetch_reference())); }
    }

    Option<T>& operator=(const Option<T>& other)
    {
        if (this == &other) return *this;

        if (m_has_value) m_storage.destroy();
        m_has_value = other.m_has_value;

        if (m_has_value) { m_storage.store_reference(other.m_storage.fetch_reference()); }

        return *this;
    }

    Option<T>& operator=(Option<T>&& other)
    {
        if (this == &other) return *this;

        if (m_has_value) m_storage.destroy();
        m_has_value = other.m_has_value;
        other.m_has_value = false;

        if (m_has_value) { m_storage.store_moved_reference(move(other.m_storage.fetch_reference())); }

        return *this;
    }

    Option() : m_has_value(false)
    {
    }

    bool has_value() const
    {
        return m_has_value;
    }

    T value(SourceLocation caller = SourceLocation::current()) const
    {
        expect_at(has_value(), caller, "Option::value called on an empty Option");
        return m_storage.fetch_reference();
    }

    T unchecked_value(Badge<Result<T>>) const
    {
        return m_storage.fetch_reference();
    }

    T release_value(SourceLocation caller = SourceLocation::current())
    {
        expect_at(has_value(), caller, "Option::release_value called on an empty Option");
        m_has_value = false;
        return move(m_storage.fetch_reference());
    }

    T unchecked_release_value(Badge<Result<T>>)
    {
        m_has_value = false;
        return move(m_storage.fetch_reference());
    }

    T value_or(const T& other) const
    {
        if (has_value()) return m_storage.fetch_reference();
        return other;
    }

    bool try_set_value(T& ref) const
    {
        if (!has_value()) return false;
        ref = m_storage.fetch_reference();
        return true;
    }

    bool try_move_value(T& ref)
    {
        if (!has_value()) return false;
        m_has_value = false;
        ref = move(m_storage.fetch_reference());
        return true;
    }

    // NOTE: This should also exist in Result.
    T* operator->()
    {
        expect(has_value(), "Option::operator-> called on an empty Option");
        return &m_storage.fetch_reference();
    }

    T& operator*()
    {
        expect(has_value(), "Option::operator* called on an empty Option");
        return m_storage.fetch_reference();
    }

    T* value_ptr(SourceLocation caller = SourceLocation::current())
    {
        expect_at(has_value(), caller, "Option::value_ptr called on an empty Option");
        return &m_storage.fetch_reference();
    }

    ~Option()
    {
        if (has_value()) m_storage.destroy();
    }

    // For compatibility with TRY()
    struct ErrorHandle
    {
      private:
        explicit ErrorHandle()
        {
        }

        friend class Option<T>;
    };

    Option(const ErrorHandle&) : m_has_value(false)
    {
    }

    ErrorHandle release_error(SourceLocation caller = SourceLocation::current())
    {
        expect_at(!has_value(), caller, "Option::release_error called on a non-empty Option");
        return ErrorHandle {};
    }

  private:
    struct Storage
    {
        alignas(T) u8 buffer[sizeof(T)];

        T& fetch_reference()
        {
            return *__builtin_launder(reinterpret_cast<T*>(&buffer));
        }

        const T& fetch_reference() const
        {
            return *__builtin_launder(reinterpret_cast<const T*>(&buffer));
        }

        void store_reference(const T& ref)
        {
            new (buffer) T(ref);
        }

        void store_moved_reference(T&& ref)
        {
            new (buffer) T(move(ref));
        }

        void destroy()
        {
            fetch_reference().~T();
        }
    };
    Storage m_storage;
    bool m_has_value { false };
};

template <typename T> inline Option<T*> nonnull_or_empty_option(T* ptr)
{
    if (ptr == nullptr) return {};
    else
        return ptr;
}