// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
#pragma once

#include <frantic/max3d/exception.hpp>
#include <frantic/max3d/fnpublish/Helpers.hpp>
#include <frantic/max3d/fnpublish/Traits.hpp>

#pragma warning( push, 3 )
#include <ifnpub.h>
#pragma warning( pop )

#include <boost/function.hpp>

#include <vector>

namespace frantic {
namespace max3d {
namespace fnpublish {

/**
 * This struct wraps a TimeValue parameter in order to differentiate a time parameter from an int (since TimeValue is a
 * typedef of int). Use this type as the last parameter to a published function to request the 3ds Max time that the
 * function was called at. This can be different from the scene time obtained via Interface::GetTime() for several
 * reasons (ex. using MAXScript 'at time T( functionCall() )').
 */
struct TimeWrapper {
    TimeValue ticks;

    explicit TimeWrapper( TimeValue t_ )
        : ticks( t_ ) {}

    operator TimeValue() const { return ticks; }
};

/**
 * This struct is used to differentiate explicit time parameters from integers. Its use is similar to TimeWrapper,
 * except it connects to an argument instead of an implicit value.
 */
struct TimeParameter {
    TimeValue ticks;

    explicit TimeParameter( TimeValue t_ )
        : ticks( t_ ) {}

    operator TimeValue() const { return ticks; }
};

/**
 * This template class associates a numeric runtime-ID with an enumeration type. Implementors should expose a unique
 * (per publishing interface), constant EnumID. Ex. template<> struct enum_id<MyEnumType>{ static const EnumID ID = -1;
 * };
 */
template <class EnumType>
struct enum_id {
    inline static EnumID get_id() {
        // Use argument dependent lookup to find a get_id() function in the enclosing namespace of EnumType.
        return get_enum_id( EnumType() );

        // NOTE:
        // If you are getting C3861: 'get_enum_id' identifier not found, this means you need to correctly implement
        // either a specialization of frantic::max3d::fnpublish::enum_id<EnumType> or a function in the namespace of
        // EnumType like so: EnumID get_enum_id( EnumType ){ return THE_ID; } which will be found via ADL.
    }
};

namespace detail {
/**
 * Since template typedefs aren't a thing, this class acts similarly. The contained 'type' typedef is the type of
 * callable object that published functions are stored as in InterfaceDesc<T>.
 */
template <class T>
struct invoke_type {
    typedef boost::function<void( TimeValue, typename RemoveConst<T>::type*, FPValue&, FPParams* )> type;
};
} // namespace detail

/**
 * Exposes metadata to 3ds Max that allows other plugins and MAXScript to call published functions. An instance of this
 * class should be held statically for each object that publishes functions. \tparam T The class exposing fucntions.
 */
template <class T>
class InterfaceDesc : public ::FPInterfaceDesc {
  public:
    typedef typename detail::invoke_type<T>::type invoke_type;

  protected:
    /**
     * Constructor. Protected so this class cannot be directly instantiated.
     * \param id The id of the interface publishing functions. Must be globally unique within 3ds Max.
     * \param szName The name of interface publishing functions.
     * \param i18nDesc The localized description of the interface.
     * \param cd The ClassDesc* of the 3ds Max object that exposes this interface. Should be NULL if this is not used
     * with a FPMixinInterface. \param flags Indicates the type of interface being described. Only FP_MIXIN and FP_CORE
     * (for static, singleton objects) are supported.
     */
    InterfaceDesc( Interface_ID id, const MCHAR* szName, StringResID i18nDesc = 0, ClassDesc* cd = NULL,
                   ULONG flags = FP_MIXIN );

  public:
    /**
     * Returns true if no functions are published by the described interface.
     */
    bool empty() const;

    /**
     * Invokes a function on the provided object instance. Any exception thrown by the published function will be
     * captured and rethrown as a MAXException. \param fid The id of the published function to invoke. \param t The time
     * to invoke the function at. Ignored if the function doesn't take a TimeWrapper final parameter. \param pSelf The
     * object to invoke the function on. \param result The return value of the function (if non-void) will be stored
     * here. \param p The collection of parameters passed to the invoked function. \return FPS_NO_SUCH_FUNCTION if the
     * fid doesn't refer to a real function, or FPS_OK otherwise.
     */
    FPStatus invoke_on( FunctionID fid, TimeValue t, T* pSelf, FPValue& result, FPParams* p );

    /**
     * Publishes a property (ie. accessed via Object.PropertyName syntax in MAXScript) with read and write accessor
     * functions. \param szName The name of the property to expose. \param pGetFn The member function ptr to invoke in
     * order to retrieve the property's value. \param pSetFn The member function ptr to invoke in order to assign the
     * proepery a new value.
     */
    template <class R>
    void read_write_property( const MCHAR* szName, R ( T::*pGetFn )( void ), void ( T::*pSetFn )( R ) );

    /**
     * Publishes a property (ie. accessed via Object.PropertyName syntax in MAXScript) that cannot be modified.
     * \param szName The name of the property to expose.
     * \param pGetFn The member function ptr to invoke in order to retrieve the property's value.
     */
    template <class R>
    void read_only_property( const MCHAR* szName, R ( T::*pGetFn )( void ) );

    /**
     * Publishes a function.
     * \tparam MemFnPtrType Any member function pointer type for class T that consists of supported return and parameter
     * types. There is an undefined maximum number of parameters that the function may have.
     *
     * \note If the signature of MemFnPtrType includes a TimeWrapper as the last parameter, it is automatically
     * connected to the 3ds Max time that the function is evaluated at. It will not be published as a formal parameter.
     *
     * \param szName The name of the published function.
     * \param pFn A pointer to a member function of T to expose.
     * \return An object that allows sequential specification of parameter names and default values.
     */
    template <class MemFnPtrType>
    FunctionDesc function( const MCHAR* szName, MemFnPtrType pFn );

    /**
     * Declares a new enumeration. In order to use this you MUST provide a specialization for enum_id<T> that exposes a
     * const EnumID called ID that uniquely identifies the enumeration type for the publishing interface. The returned
     * object allows enum name/value pairs to be added. \return An object that allows sequential specification of
     * enumeration values.
     */
    template <class EnumType>
    EnumDesc<EnumType> enumeration();

  private:
    std::vector<invoke_type> m_dispatchFns;
};

} // namespace fnpublish
} // namespace max3d
} // namespace frantic

// Include the implementation file here. Don't be alarmed, this is not bizarre when it comes to templates and inline
// stuff.
#include <frantic/max3d/fnpublish/InterfaceDesc.inl>

namespace frantic {
namespace max3d {

using frantic::max3d::fnpublish::TimeWrapper;

}
} // namespace frantic