// MIT License
//
// Copyright (c) 2017-2018 offa
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

#pragma once

#include "scope_exit.h"
#include "detail/wrapper.h"
#include <utility>
#include <type_traits>

namespace sr
{
    namespace detail
    {
        template<class T, class U,
                    class R = std::conditional_t<std::is_nothrow_constructible_v<T, U>, U&&, U>
                >
        constexpr R forward_if_nothrow_constructible(U&& arg)
        {
            return std::forward<U>(arg);
        }
    }


    template<class R, class D>
    class unique_resource
    {
    public:

        template<class RR, class DD,
                std::enable_if_t<(std::is_constructible_v<R ,RR> && std::is_constructible_v<D, DD>
                                && (std::is_nothrow_constructible_v<R, RR> || std::is_constructible_v<R, RR&>)
                                && (std::is_nothrow_constructible_v<D, DD> || std::is_constructible_v<D, DD&>)), int> = 0
                >
        explicit unique_resource(RR&& r, DD&& d) noexcept((std::is_nothrow_constructible_v<R, RR> || std::is_nothrow_constructible_v<R, RR&>)
                                                            && (std::is_nothrow_constructible_v<D, DD> || std::is_nothrow_constructible_v<D, DD&>))
                                                : resource(detail::forward_if_nothrow_constructible<R, RR>(std::forward<RR>(r)), scope_exit{[&r, &d] { d(r); }}),
                                                deleter(detail::forward_if_nothrow_constructible<D, DD>(std::forward<DD>(d)), scope_exit{[this, &d] { d(get()); }}),
                                                execute_on_destruction(true)
        {
        }

        unique_resource(unique_resource&& other) noexcept(std::is_nothrow_move_constructible_v<R>
                                                            && std::is_nothrow_move_constructible_v<D>)
                                                : resource(std::move_if_noexcept(other.resource.get()), scope_exit{[] { }}),
                                                deleter(std::move_if_noexcept(other.deleter.get()), scope_exit{[&other] {
                                                                                                            other.get_deleter()(other.resource.get());
                                                                                                            other.release(); }}),
                                                execute_on_destruction(std::exchange(other.execute_on_destruction, false))
        {
        }

        unique_resource(const unique_resource&) = delete;

        ~unique_resource()
        {
            reset();
        }


        void reset() noexcept
        {
            if( execute_on_destruction == true )
            {
                execute_on_destruction = false;
                get_deleter()(resource.get());
            }
        }

        template<class RR>
        void reset(RR&& r)
        {
            reset();

            using R1 = typename detail::Wrapper<R>::type;
            auto se = scope_exit{[this, &r] { get_deleter()(r); }};

            if constexpr( std::is_nothrow_assignable_v<R1&, RR> == true )
            {
                resource.reset(std::forward<RR>(r));
            }
            else
            {
                resource.reset(std::as_const(r));
            }

            execute_on_destruction = true;
            se.release();
        }

        void release() noexcept
        {
            execute_on_destruction = false;
        }

        const R& get() const noexcept
        {
            return resource.get();
        }

        template<class RR = R, std::enable_if_t<std::is_pointer_v<RR>, int> = 0>
        RR operator->() const noexcept
        {
            return resource.get();
        }

        template<class RR = R,
            std::enable_if_t<( std::is_pointer_v<RR>
                            && !std::is_void_v<std::remove_pointer_t<RR>>), int> = 0>
        std::add_lvalue_reference_t<std::remove_pointer_t<RR>> operator*() const noexcept
        {
            return *get();
        }

        const D& get_deleter() const noexcept
        {
            return deleter.get();
        }


        template<class RR = R, class DD = D,
            std::enable_if_t<(std::is_nothrow_move_assignable_v<RR>
                                || std::is_nothrow_copy_assignable_v<RR>)
                            && (std::is_nothrow_move_assignable_v<DD>
                                || std::is_nothrow_copy_assignable_v<DD>), int> = 0
            >
        unique_resource& operator=(unique_resource&& other) noexcept(std::is_nothrow_assignable_v<R&, R>
                                                                    && std::is_nothrow_assignable_v<D&, D>)
        {
            if( this != &other )
            {
                reset();

                if constexpr( std::is_nothrow_move_assignable_v<RR> == true )
                {
                    if constexpr( std::is_nothrow_move_assignable_v<DD> == true )
                    {
                        resource.reset(std::move(other.resource));
                        deleter.reset(std::move(other.deleter));
                    }
                    else
                    {
                        deleter.reset(other.deleter);
                        resource.reset(std::move(other.resource));
                    }
                }
                else
                {
                    if constexpr( std::is_nothrow_move_assignable_v<DD> == true )
                    {
                        resource.reset(other.resource);
                        deleter.reset(std::move(other.deleter));
                    }
                    else
                    {
                        resource.reset(other.resource);
                        deleter.reset(other.deleter);
                    }
                }

                execute_on_destruction = std::exchange(other.execute_on_destruction, false);
            }
            return *this;
        }

        unique_resource& operator=(const unique_resource&) = delete;


    private:

        detail::Wrapper<R> resource;
        detail::Wrapper<D> deleter;
        bool execute_on_destruction;
    };


    template<class R, class D>
    unique_resource(R, D) -> unique_resource<R, D>;


    template<class R, class D, class S = std::decay_t<R>>
    unique_resource<std::decay_t<R>, std::decay_t<D>> make_unique_resource_checked(R&& r, const S& invalid, D&& d)
                                                            noexcept(std::is_nothrow_constructible_v<std::decay_t<R>, R>
                                                                    && std::is_nothrow_constructible_v<std::decay_t<D>, D>)
    {
        const bool must_release{r == invalid};
        unique_resource<std::decay_t<R>, std::decay_t<D>> ur{std::forward<R>(r), std::forward<D>(d)};

        if( must_release == true )
        {
            ur.release();
        }

        return ur;
    }

}