//    OpenVPN -- An application to securely tunnel IP networks
//               over a single port, with support for SSL/TLS-based
//               session authentication and key exchange,
//               packet encryption, packet authentication, and
//               packet compression.
//
//    Copyright (C) 2012- OpenVPN Inc.
//
//    SPDX-License-Identifier: MPL-2.0 OR AGPL-3.0-only WITH openvpn3-openssl-exception
//

// High-performance functor with move-only semantics.

#ifndef OPENVPN_COMMON_FUNCTION_H
#define OPENVPN_COMMON_FUNCTION_H

#include <cstddef> // for std::size_t
#include <utility> // for std::move
#include <type_traits>
#include <new>

namespace openvpn {
// F -- function type (usually a lambda expression)
// N (default=3) -- max size of functor in void* machine words before we overflow to dynamic allocation
// INTERN_ONLY (default=false) -- if true, throw a static assertion if functor cannot be stored internally
template <typename F, std::size_t N = 3, bool INTERN_ONLY = false>
class Function;

template <typename R, typename... A, std::size_t N, bool INTERN_ONLY>
class Function<R(A...), N, INTERN_ONLY>
{
  public:
    Function() noexcept
    {
        methods = nullptr;
    }

    template <typename T>
    Function(T &&functor) noexcept
    {
        construct(std::move(functor));
    }

    Function(Function &&other) noexcept
    {
        methods = other.methods;
        other.methods = nullptr;
        if (methods)
            methods->move(data, other.data);
    }

    Function &operator=(Function &&other) noexcept
    {
        if (methods)
            methods->destruct(data);
        methods = other.methods;
        other.methods = nullptr;
        if (methods)
            methods->move(data, other.data);
        return *this;
    }

    ~Function()
    {
        if (methods)
            methods->destruct(data);
    }

    template <typename T>
    void reset(T &&functor) noexcept
    {
        if (methods)
            methods->destruct(data);
        construct(std::move(functor));
    }

    void reset() noexcept
    {
        if (methods)
        {
            methods->destruct(data);
            methods = nullptr;
        }
    }

    R operator()(A... args) const
    {
        return methods->invoke(data, std::forward<A>(args)...);
    }

    explicit operator bool() const noexcept
    {
        return methods != nullptr;
    }

  private:
#ifdef _MSC_VER
    template <typename T>
    void construct(T &&functor) noexcept
    {
        constexpr bool is_intern = (sizeof(Intern<T>) <= sizeof(data));
        static_assert(!INTERN_ONLY || is_intern, "Function: Intern<T> doesn't fit in data[] and INTERN_ONLY=true");
        static_assert(sizeof(Extern<T>) <= sizeof(data), "Function: Extern<T> doesn't fit in data[]");

        if (is_intern)
        {
            // store functor internally (in data)
            setup_methods_intern<T>();
            new (data) Intern<T>(std::move(functor));
        }
        else
        {
            // store functor externally (using new)
            setup_methods_extern<T>();
            new (data) Extern<T>(std::move(functor));
        }
    }
#else
    template <typename T>
    static constexpr bool is_intern()
    {
        return sizeof(Intern<T>) <= sizeof(data);
    }

    template <typename T,
              typename std::enable_if<is_intern<T>(), int>::type = 0>
    void construct(T &&functor) noexcept
    {
        // store functor internally (in data)
        setup_methods_intern<T>();
        new (data) Intern<T>(std::move(functor));
    }

    template <typename T,
              typename std::enable_if<!is_intern<T>(), int>::type = 0>
    void construct(T &&functor) noexcept
    {
        static_assert(!INTERN_ONLY, "Function: Intern<T> doesn't fit in data[] and INTERN_ONLY=true");
        static_assert(sizeof(Extern<T>) <= sizeof(data), "Function: Extern<T> doesn't fit in data[]");

        // store functor externally (using new)
        setup_methods_extern<T>();
        new (data) Extern<T>(std::move(functor));
    }
#endif

    struct Methods
    {
        R(*invoke)
        (void *, A &&...);
        void (*move)(void *, void *);
        void (*destruct)(void *);
    };

    template <typename T>
    void setup_methods_intern()
    {
        static const struct Methods m = {
            &Intern<T>::invoke,
            &Intern<T>::move,
            &Intern<T>::destruct,
        };
        methods = &m;
    }

    template <typename T>
    void setup_methods_extern()
    {
        static const struct Methods m = {
            &Extern<T>::invoke,
            &Extern<T>::move,
            &Extern<T>::destruct,
        };
        methods = &m;
    }

    // store functor internally (in data)
    template <typename T>
    class Intern
    {
      public:
        Intern(T &&functor) noexcept
            : functor_(std::move(functor))
        {
        }

        static R invoke(void *ptr, A &&...args)
        {
            Intern *self = reinterpret_cast<Intern *>(ptr);
            return self->functor_(std::forward<A>(args)...);
        }

        static void move(void *dest, void *src)
        {
            Intern *s = reinterpret_cast<Intern *>(src);
            new (dest) Intern(std::move(*s));
        }

        static void destruct(void *ptr)
        {
            Intern *self = reinterpret_cast<Intern *>(ptr);
            self->~Intern();
        }

      private:
        T functor_;
    };

    // store functor externally (using new)
    template <typename T>
    class Extern
    {
      public:
        Extern(T &&functor) noexcept
            : functor_(new T(std::move(functor)))
        {
        }

        static R invoke(void *ptr, A &&...args)
        {
            Extern *self = reinterpret_cast<Extern *>(ptr);
            return (*self->functor_)(std::forward<A>(args)...);
        }

        static void move(void *dest, void *src)
        {
            Extern *d = reinterpret_cast<Extern *>(dest);
            Extern *s = reinterpret_cast<Extern *>(src);
            d->functor_ = s->functor_;
            // no need to set s->functor_=nullptr because parent will not destruct src after move
        }

        static void destruct(void *ptr)
        {
            Extern *self = reinterpret_cast<Extern *>(ptr);
            delete self->functor_;
        }

      private:
        T *functor_;
    };

    const Methods *methods;
    mutable void *data[N];
};
} // namespace openvpn

#endif
