#pragma once
#include "memory.h"
#include "string.h"
#include "varint.h"
#include "refcnt.h"
#include "expected.h"
#include <iosfwd>
#include <system_error>

namespace panda { namespace error {

struct ErrorCode : AllocatedObject<ErrorCode> {
    ErrorCode () noexcept {}

    ErrorCode (const ErrorCode& o) noexcept : data(o.data)            {}
    ErrorCode (ErrorCode&&      o) noexcept : data(std::move(o.data)) {}

    ErrorCode (int ec, const std::error_category& cat) noexcept { if (ec) set(std::error_code(ec, cat)); }

    ErrorCode (const std::error_code& ec) noexcept { if (ec) set(ec); }

    template <class N, typename = std::enable_if_t<std::is_error_code_enum<N>::value, void>>
    ErrorCode (N e) noexcept : ErrorCode(std::error_code(e)) {}

    ErrorCode (const std::error_code& ec, const std::error_code& next) noexcept { set(ec, next); }
    ErrorCode (const std::error_code& ec, const ErrorCode&       next) noexcept { set(ec, next); }

    ErrorCode& operator= (const ErrorCode& o) noexcept { data = o.data; return *this; }
    ErrorCode& operator= (ErrorCode&& o)      noexcept { data = std::move(o.data); return *this; }

    ErrorCode& operator= (const std::error_code& ec) {
        if (ec) set(ec);
        else clear();
        return *this;
    }

    template <class N, typename = std::enable_if_t<std::is_error_code_enum<N>::value, void>>
    ErrorCode& operator= (N e) noexcept {
        set(std::error_code(e));
        return *this;
    }

    void clear () noexcept {
        data.reset();
    }

    explicit operator bool () const noexcept { return data; }

    std::error_code code () const noexcept {
        if (!data) return {};
        return std::error_code(data->codes.top(), data->cat->self);
    };

    int value () const noexcept {
        if (!data) return 0;
        return data->codes.top();
    }

    const std::error_category& category () const noexcept {
        if (!data) return std::system_category();
        return data->cat->self;
    }

    std::error_condition default_error_condition () const noexcept {
        return code().default_error_condition();
    }

    std::string message () const;

    ErrorCode next () const noexcept;

    string what () const;

    bool contains (const std::error_code& c) const {
        if (!data) {
            return !c;
        }
        return contains_impl(c);
    }

    // any user can add specialization for his own result and get any data
    template <typename T = void, typename... Args>
    T private_access(Args...);

    template <typename T = void, typename... Args>
    T private_access(Args...) const;

    struct NestedCategory {
        const std::error_category& self;
        const NestedCategory*      next;
    };

private:
    struct Data : Refcnt, AllocatedObject<Data> {
        using CodeStack = VarIntStack;
        CodeStack codes;
        const NestedCategory* cat;
    };

    iptr<Data> data;

    void init ();
    void set  (const std::error_code& ec);
    void set  (const std::error_code& ec, const std::error_code& next);
    void set  (const std::error_code& ec, const ErrorCode& next);
    void push (const std::error_code&);
    bool contains_impl (const std::error_code& c) const;
};

struct StrictErrorCode  {
    StrictErrorCode(const ErrorCode& ec) : base(ec) {}
    const ErrorCode& base;
};

inline bool operator== (const StrictErrorCode& lhs, const StrictErrorCode& rhs) noexcept { return lhs.base.code() == rhs.base.code(); }
inline bool operator!= (const StrictErrorCode& lhs, const StrictErrorCode& rhs) noexcept { return !(lhs == rhs); }

inline bool operator& (const ErrorCode& lhs, const std::error_code& rhs) noexcept { return lhs.contains(rhs); }
inline bool operator& (const std::error_code& lhs, const ErrorCode& rhs) noexcept { return rhs.contains(lhs); }

template <class E, typename = std::enable_if_t<std::is_error_code_enum<E>::value || std::is_error_condition_enum<E>::value, void>>
inline bool operator& (const ErrorCode& ec, E e) noexcept { return ec.contains(make_error_code(e)); }
template <class E, typename = std::enable_if_t<std::is_error_code_enum<E>::value || std::is_error_condition_enum<E>::value, void>>
inline bool operator& (E e, const ErrorCode& ec) noexcept { return ec.contains(make_error_code(e)); }

inline bool operator< (const ErrorCode& lhs, const ErrorCode& rhs) noexcept { return lhs.code() < rhs.code(); }
inline bool operator< (const ErrorCode& lhs, const std::error_code& rhs) noexcept { return lhs.code() < rhs; }
inline bool operator< (const std::error_code& lhs, const ErrorCode& rhs) noexcept { return lhs < rhs.code(); }
template <class E, typename = std::enable_if_t<std::is_error_code_enum<E>::value || std::is_error_condition_enum<E>::value, void>>
inline bool operator< (const ErrorCode& ec, E e) noexcept { return ec.code() < make_error_code(e); }
template <class E, typename = std::enable_if_t<std::is_error_code_enum<E>::value || std::is_error_condition_enum<E>::value, void>>
inline bool operator< (E e, const ErrorCode& ec) noexcept { return make_error_code(e) < ec.code(); }

std::ostream& operator<< (std::ostream&, const ErrorCode&);

namespace details {
    inline string error_message(const ErrorCode& e) {
        return e.what();
    }

    inline string error_message(const std::error_code& e) {
        auto r = e.message();
        return string(r.data(), r.size());
    }

    template <typename E>
    struct bad_expected_access_code : std::exception {
        explicit bad_expected_access_code (E e) : _val(std::move(e)) {}

        virtual const char* what () const noexcept override {
            if (_message.empty()) _message = error_message(_val);
            return _message.c_str();
        }

        const E&  error () const &  { return _val; }
        const E&& error () const && { return std::move(_val); }

        E&  error () &  { return _val; }
        E&& error () && { return std::move(_val); }

    private:
        E _val;
        mutable std::string _message;
    };
}

}}

namespace panda {
    using ErrorCode = error::ErrorCode;

    template <>
    struct bad_expected_access<ErrorCode> : error::details::bad_expected_access_code<ErrorCode> {
        using bad_expected_access_code::bad_expected_access_code;
    };

    template <>
    struct bad_expected_access<std::error_code> : error::details::bad_expected_access_code<std::error_code> {
        using bad_expected_access_code::bad_expected_access_code;
    };
}

namespace std {
    template<> struct hash<panda::ErrorCode> {
        typedef panda::ErrorCode argument_type;
        typedef std::size_t result_type;

        result_type operator()(argument_type const& c) const noexcept { return std::hash<std::error_code>{}(c.code()); }
    };
}