#pragma once

extern int enzyme_interleave;

extern int enzyme_dup;
extern int enzyme_dupnoneed;
extern int enzyme_out;
extern int enzyme_const;

extern int enzyme_const_return;
extern int enzyme_active_return;
extern int enzyme_dup_return;

extern int enzyme_primal_return;
extern int enzyme_noret;

template<typename Return, typename... T>
Return __enzyme_autodiff(T...);

template<typename Return, typename... T>
Return __enzyme_fwddiff(T...);

#include <enzyme/tuple>

namespace enzyme {

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wmissing-braces"

    struct nodiff{};

    template<bool ReturnPrimal = false>
    struct ReverseMode {

    };
    using Reverse = ReverseMode<false>;
    using ReverseWithPrimal = ReverseMode<true>;

    struct ForwardMode {

    };
    using Forward = ForwardMode;

    template < typename T >
    struct Active{
      static_assert(
        !std::is_reference_v<T> && !std::is_pointer_v<T>,
        "Reference/pointer active arguments don't make sense for AD!"
      );
      T value;
      Active(const T& v) : value(v) {};
      Active(T&& v) : value(std::move(v)) {};
      Active(const Active<T>&) = default;
      Active(Active<T>&&) = default;
      operator T&() { return value; }
    };

    namespace detail {

        /**
         * @brief Helper base for all `Duplicated`-type arguments
         *
         * Due to essentially the same implementation, it prevents code
         * duplication
         */
        template < typename T >
        struct DupArg {
            // Argument as regular value. Also works for pointer types
            T value;
            T shadow;
            DupArg(const T& v, const T& s) : value(v), shadow(s) {};
            DupArg(T&& v, T&& s) : value(v), shadow(s) {};
            DupArg(const DupArg<T>&) = default;
            DupArg(DupArg<T>&&) = default;
        };

        template < typename T >
        struct DupArg<T&> {
            T* value;
            T* shadow;
            DupArg(T& v, T& s) : value(&v), shadow(&s) {}
            DupArg(const DupArg<T&>&) = default;
            DupArg(DupArg<T&>&&) = default;
        };

    } // <-- namespace detail

    template < typename T >
    struct Duplicated : public detail::DupArg<T> {
      using detail::DupArg<T>::DupArg;
    };

    template < typename T >
    struct DuplicatedNoNeed : public detail::DupArg<T> {
      using detail::DupArg<T>::DupArg;
    };

    template < typename T >
    struct Const{
      T value;
      Const(const T& v) : value(v) {};
      Const(T&& v) : value(std::move(v)) {};
      Const(const Const<T>&) = default;
      Const(Const<T>&&) = default;
      operator T&() { return value; }
    };

    template < typename T >
    struct Const<T&>{
        T* value;
        Const(T& v) : value(&v) {}
        Const(const Const<T&>&) = default;
        Const(Const<T&>&&) = default;
        operator T&() { return *value; }
    };

    // CTAD available in C++17 or later
    #if __cplusplus >= 201703L
      template < typename T >
      Active(T) -> Active<T>;

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

      template < typename T >
      Duplicated(T,T) -> Duplicated<T>;

      template < typename T >
      DuplicatedNoNeed(T,T) -> DuplicatedNoNeed<T>;
    #endif

    template < typename T >
    struct type_info {
      static constexpr bool is_active     = false;
      static constexpr bool is_duplicated = false;
      using type = nodiff;
    };

    template < typename T >
    struct type_info < Active<T> >{
        static constexpr bool is_active     = true;
        static constexpr bool is_duplicated = false;
        using type = T;
    };

    template < typename T >
    struct type_info < Duplicated<T> >{
        static constexpr bool is_active     = false;
        static constexpr bool is_duplicated = true;
        using type = T;
    };

    template < typename T >
    struct type_info < DuplicatedNoNeed<T> >{
        static constexpr bool is_active     = false;
        static constexpr bool is_duplicated = true;
        using type = T;
    };

    template < typename ... T >
    struct concatenated;

    template < typename ... S, typename T, typename ... rest >
    struct concatenated < tuple < S ... >, T, rest ... > {
        using type = typename concatenated< tuple< S ..., T>, rest ... >::type;
    };

    template < typename T >
    struct concatenated < T > {
        using type = T;
    };

    // Yikes!
    // slightly cleaner in C++20, with std::remove_cvref
    template < typename ... T >
    struct autodiff_return;

    template < typename RetType, typename ... T >
    struct autodiff_return<ReverseMode<false>, RetType, T...>
    {
        using type = tuple<typename concatenated< tuple< >,
            typename type_info<
                typename remove_cvref< T >::type
            >::type ...
        >::type>;
    };

    template < typename RetType, typename ... T >
    struct autodiff_return<ReverseMode<true>, RetType, T...>
    {
        using type = tuple<
            typename type_info<RetType>::type,
            typename concatenated< tuple< >,
                typename type_info<
                    typename remove_cvref< T >::type
                >::type ...
            >::type
        >;
    };

    template < typename T0, typename ... T >
    struct autodiff_return<ForwardMode, Const<T0>, T...>
    {
        using type = tuple<T0>;
    };

    template < typename T0, typename ... T >
    struct autodiff_return<ForwardMode, Duplicated<T0>, T...>
    {
        using type = tuple<T0, T0>;
    };

    template < typename T0, typename ... T >
    struct autodiff_return<ForwardMode, DuplicatedNoNeed<T0>, T...>
    {
        using type = tuple<T0>;
    };

    template < typename T >
    __attribute__((always_inline))
    auto expand_args(const enzyme::Duplicated<T> & arg) {
        return enzyme::tuple{enzyme_dup, arg.value, arg.shadow};
    }

    template < typename T >
    __attribute__((always_inline))
    auto expand_args(const enzyme::DuplicatedNoNeed<T> & arg) {
        return enzyme::tuple{enzyme_dupnoneed, arg.value, arg.shadow};
    }

    template < typename T >
    __attribute__((always_inline))
    auto expand_args(const enzyme::Active<T> & arg) {
        return enzyme::tuple<int, T>{enzyme_out, arg.value};
    }

    template < typename T >
    __attribute__((always_inline))
    auto expand_args(const enzyme::Const<T> & arg) {
        return enzyme::tuple{enzyme_const, arg.value};
    }

    template < typename T >
    __attribute__((always_inline))
    auto expand_primals(const enzyme::Duplicated<T> & arg) {
        return enzyme::tuple{enzyme_dup, arg.value};
    }

    template < typename T >
    __attribute__((always_inline))
    auto expand_primals(const enzyme::DuplicatedNoNeed<T> & arg) {
        return enzyme::tuple{enzyme_dupnoneed, arg.value};
    }

    template < typename T >
    __attribute__((always_inline))
    auto expand_primals(const enzyme::Active<T> & arg) {
        return enzyme::tuple<int, T>{enzyme_out, arg.value};
    }

    template < typename T >
    __attribute__((always_inline))
    auto expand_primals(const enzyme::Const<T> & arg) {
        return enzyme::tuple{enzyme_const, arg.value};
    }

    template < typename T >
    __attribute__((always_inline))
    auto expand_shadows(const enzyme::Duplicated<T> & arg) {
        return enzyme::tuple{arg.shadow};
    }

    template < typename T >
    __attribute__((always_inline))
    auto expand_shadows(const enzyme::DuplicatedNoNeed<T> & arg) {
        return enzyme::tuple{arg.shadow};
    }

    template < typename T >
    __attribute__((always_inline))
    auto expand_shadows(const enzyme::Active<T> & arg) {
        return enzyme::tuple{};
    }

    template < typename T >
    __attribute__((always_inline))
    auto expand_shadows(const enzyme::Const<T> & arg) {
        return enzyme::tuple{};
    }

    template < typename T >
    __attribute__((always_inline))
    auto primal_args(const enzyme::Duplicated<T> & arg) {
        return enzyme::tuple<T>{arg.value};
    }

    template < typename T >
    __attribute__((always_inline))
    auto primal_args(const enzyme::DuplicatedNoNeed<T> & arg) {
        return enzyme::tuple<T>{arg.value};
    }

    template < typename T >
    __attribute__((always_inline))
    auto primal_args(const enzyme::Active<T> & arg) {
        return enzyme::tuple<T>{arg.value};
    }

    template < typename T >
    __attribute__((always_inline))
    auto primal_args(const enzyme::Const<T> & arg) {
        return enzyme::tuple<T>{arg.value};
    }

    template < template <typename> class Template, typename Arg >
    Arg arg_type(const Template<Arg>&);

    namespace detail {
        template < typename RetType, typename ... T >
        struct function_type
        {
            using type = RetType(T...);
        };

        template<typename function, typename prevfunc>
        struct templated_call {

        };

        template<typename function, typename RT, typename ...T>
        struct templated_call<function, RT(T...)> {
            static RT wrap(function* __restrict__ f, T... args) {
                return (*f)(args...);
            }
        };




        template<typename T>
        __attribute__((always_inline))
        constexpr decltype(auto) push_return_last(T &&t);

        template<typename ...T>
        __attribute__((always_inline))
        constexpr decltype(auto) push_return_last(tuple<tuple<T...>> &&t) {
          return tuple<tuple<T...>>{get<0>(t)};
        }

        template<typename ...T, typename R>
        __attribute__((always_inline))
        constexpr decltype(auto) push_return_last(tuple<R, tuple<T...>> &&t) {
          return tuple{get<1>(t), get<0>(t)};
        }

        template <typename Mode>
        struct autodiff_apply {};

        template <bool Mode>
        struct autodiff_apply<ReverseMode<Mode>> {
        template <class return_type, class Tuple, std::size_t... I, typename... ExtraArgs>
        __attribute__((always_inline))
        static constexpr decltype(auto) impl(void* f, int* ret_attr, Tuple&& t, std::index_sequence<I...>, ExtraArgs... args) {
            return push_return_last(__enzyme_autodiff<return_type>(f, ret_attr, args..., enzyme::get<I>(impl::forward<Tuple>(t))...));
        }
        };

        template <>
        struct autodiff_apply<ForwardMode> {
        template <class return_type, class Tuple, std::size_t... I, typename... ExtraArgs>
        __attribute__((always_inline))
        static constexpr return_type impl(void* f, int* ret_attr, Tuple&& t, std::index_sequence<I...>, ExtraArgs... args) {
            return __enzyme_fwddiff<return_type>(f, ret_attr, args..., enzyme::get<I>(impl::forward<Tuple>(t))...);
        }
        };

        template <typename function, class Tuple, std::size_t... I>
        __attribute__((always_inline))
        constexpr decltype(auto) primal_apply_impl(function &&f, Tuple&& t, std::index_sequence<I...>) {
            return f(enzyme::get<I>(impl::forward<Tuple>(t))...);
        }

        template < typename Mode, typename T >
        struct default_ret_activity {
          using type = Const<T>;
        };

        template <bool prim>
        struct default_ret_activity<ReverseMode<prim>, float> {
          using type = Active<float>;
        };

        template <bool prim>
        struct default_ret_activity<ReverseMode<prim>, double> {
          using type = Active<double>;
        };

        template<>
        struct default_ret_activity<ForwardMode, float> {
          using type = DuplicatedNoNeed<float>;
        };

        template<>
        struct default_ret_activity<ForwardMode, double> {
          using type = DuplicatedNoNeed<double>;
        };

        template < typename T >
        struct ret_global;

        template<typename T>
        struct ret_global<Const<T>> {
          static constexpr int* value = &enzyme_const_return;
        };

        template<typename T>
        struct ret_global<Active<T>> {
          static constexpr int* value = &enzyme_active_return;
        };

        template<typename T>
        struct ret_global<Duplicated<T>> {
          static constexpr int* value = &enzyme_dup_return;
        };

        template<typename T>
        struct ret_global<DuplicatedNoNeed<T>> {
          static constexpr int* value = &enzyme_dup_return;
        };

        template<typename Mode, typename RetAct>
        struct ret_used;

        template<typename RetAct>
        struct ret_used<ReverseMode<true>, RetAct> {
          static constexpr int* value = &enzyme_primal_return;
        };

        template<typename RetAct>
        struct ret_used<ReverseMode<false>, RetAct> {
          static constexpr int* value = &enzyme_noret;
        };

        template<typename T>
        struct ret_used<ForwardMode, DuplicatedNoNeed<T>> {
          static constexpr int* value = &enzyme_noret;
        };
        template<typename T>
        struct ret_used<ForwardMode, Const<T>> {
          static constexpr int* value = &enzyme_primal_return;
        };
        template<typename T>
        struct ret_used<ForwardMode, Duplicated<T>> {
          static constexpr int* value = &enzyme_primal_return;
        };

        template <typename T, typename... Args>
        struct verify_dup_args {
          static constexpr bool value = true;
        };

        template <typename... Args>
        struct verify_dup_args<enzyme::Reverse, Args...> {
          static constexpr bool value = (
            (
              !type_info<Args>::is_duplicated
              || std::is_reference_v<typename type_info<Args>::type>
              || std::is_pointer_v<typename type_info<Args>::type>
            ) && ...
          );
        };

        template<class T>
        struct is_function_or_function_ptr : std::is_function<T> {};
         
        template<class T>
        struct is_function_or_function_ptr<T*> : std::is_function<T> {};
    }  // namespace detail

    template < typename return_type, typename function, typename ... enz_arg_types >
    __attribute__((always_inline))
    auto primal_impl(function && f, enzyme::tuple< enz_arg_types ... > && arg_tup) {
      using Tuple = enzyme::tuple< enz_arg_types ... >;
      return detail::primal_apply_impl<return_type>(std::move(f), impl::forward<Tuple>(arg_tup), std::make_index_sequence<enzyme::tuple_size_v<Tuple>>{});
    }

    template < typename function, typename ... arg_types>
    auto primal_call(function && f, arg_types && ... args) {
        return primal_impl<function>(impl::forward<function>(f), enzyme::tuple_cat(primal_args(args)...));
    }

    template < typename return_type, typename DiffMode, typename function, typename functy, typename RetActivity, typename ... enz_arg_types, std::enable_if_t<!detail::is_function_or_function_ptr<typename remove_cvref< function >::type>::value, int> = 0>
    __attribute__((always_inline))
    auto autodiff_impl(function && f, enzyme::tuple< enz_arg_types ... > && arg_tup) {
      using Tuple = enzyme::tuple< enz_arg_types ... >;
      return detail::autodiff_apply<DiffMode>::template impl<return_type>((void*)&detail::templated_call<function, functy>::wrap, detail::ret_global<RetActivity>::value,
            impl::forward<Tuple>(arg_tup),
            std::make_index_sequence<enzyme::tuple_size_v<Tuple>>{}, &enzyme_const, &f);
    }

    template < typename return_type, typename DiffMode, typename function, typename functy, typename RetActivity, typename ... enz_arg_types, std::enable_if_t<detail::is_function_or_function_ptr<typename remove_cvref< function >::type>::value, int> = 0>
    __attribute__((always_inline))
    auto autodiff_impl(function && f, enzyme::tuple< enz_arg_types ... > && arg_tup) {
      using Tuple = enzyme::tuple< enz_arg_types ... >;
      return detail::autodiff_apply<DiffMode>::template impl<return_type>((void*)static_cast<functy*>(f), detail::ret_global<RetActivity>::value, impl::forward<Tuple>(arg_tup), std::make_index_sequence<enzyme::tuple_size_v<Tuple>>{});
    }

    template < typename DiffMode, typename RetActivity, typename function, typename ... arg_types>
    __attribute__((always_inline))
    auto autodiff(function && f, arg_types && ... args) {
        static_assert(
            detail::verify_dup_args<DiffMode, arg_types...>::value,
            "Non-reference/pointer Duplicated/DuplicatedNoNeed args don't make "
            "sense for Reverse mode AD"
        );
        using primal_return_type = decltype(f(arg_type(args)...));
        using functy = typename detail::function_type<primal_return_type, decltype(arg_type(args))...>::type;
        using return_type = typename autodiff_return<DiffMode, RetActivity, arg_types...>::type;
        return autodiff_impl<return_type, DiffMode, function, functy, RetActivity>(impl::forward<function>(f), enzyme::tuple_cat(enzyme::tuple{detail::ret_used<DiffMode, RetActivity>::value}, expand_primals(args)..., enzyme::tuple{enzyme_interleave}, expand_shadows(args)...));
    }

    template < typename DiffMode, typename function, typename ... arg_types>
    __attribute__((always_inline))
    auto autodiff(function && f, arg_types && ... args) {
        static_assert(
            detail::verify_dup_args<DiffMode, arg_types...>::value,
            "Non-reference/pointer Duplicated/DuplicatedNoNeed args don't make "
            "sense for Reverse mode AD"
        );
        using primal_return_type = decltype(f(arg_type(args)...));
        using functy = typename detail::function_type<primal_return_type, decltype(arg_type(args))...>::type;
        using RetActivity = typename detail::default_ret_activity<DiffMode, primal_return_type>::type;
        using return_type = typename autodiff_return<DiffMode, RetActivity, arg_types...>::type;
        return autodiff_impl<return_type, DiffMode, function, functy, RetActivity>(impl::forward<function>(f), enzyme::tuple_cat(enzyme::tuple{detail::ret_used<DiffMode, RetActivity>::value}, expand_primals(args)..., enzyme::tuple{enzyme_interleave}, expand_shadows(args)...));
    }
#pragma clang diagnostic pop

} // namespace enzyme
