// Copyright 2016 Pixar
//
// Licensed under the terms set forth in the LICENSE.txt file available at
// https://openusd.org/license.
//
// Modified by Jeremy Retailleau.

#include <pxr/tf/type.h>
#include <pxr/tf/notice.h>
#include <pxr/tf/pyFunction.h>
#include <pxr/tf/pyIdentity.h>
#include <pxr/tf/pyNoticeWrapper.h>
#include <pxr/tf/pyPtrHelpers.h>
#include "./pyWeakObject.h"
#include <pxr/tf/stringUtils.h>
#include <pxr/tf/type.h>

#include <pxr/boost/python/class.hpp>
#include <pxr/boost/python/def.hpp>
#include <pxr/boost/python/handle.hpp>
#include <pxr/boost/python/manage_new_object.hpp>
#include <pxr/boost/python/return_value_policy.hpp>
#include <pxr/boost/python/scope.hpp>

#include <functional>

using std::string;

namespace pxr {

using namespace pxr::boost::python;

class Tf_PyNotice {
public:
    static size_t SendWithType(const TfNotice& notice,
                               const TfType & noticeType,
                               const TfWeakBase* sender,
                               const void *senderUniqueId,
                               const std::type_info & senderType)
    {
        return notice._SendWithType(noticeType,
                                    sender, senderUniqueId, senderType);
    }
};

}  // namespace pxr

using namespace pxr;

namespace {

// TfNotice is passed for both the type and the base to indicate the root of the
// hierarchy.
TF_INSTANTIATE_NOTICE_WRAPPER(pxr::TfNotice, pxr::TfNotice);

class Tf_PyNoticeInternal
{
  public:

    struct Listener : public TfWeakBase {

        typedef void CallbackSig(object const &, handle<> const &);
        typedef std::function<CallbackSig> Callback;

        static Listener *New(TfType const &noticeType,
                             Callback const &callback,
                             TfAnyWeakPtr const &sender)
        {
            if (noticeType.IsA<TfNotice>()) 
                return new Listener(noticeType, callback, sender);

            // Fail -- unknown notice type.
            TfPyThrowTypeError("not registering for '" +
                               noticeType.GetTypeName() +
                               "' because it is not a known TfNotice type");
            // CODE_COVERAGE_OFF The above throws, so this is unreachable.
            return 0;
            // CODE_COVERAGE_ON
        }

        ~Listener() { Revoke(); }
        void Revoke() { TfNotice::Revoke(_key); }

      private:
    
        Listener(TfType const &noticeType,
                 Callback const &callback,
                 TfAnyWeakPtr const &sender) :
            _callback(callback), _noticeType(noticeType) {
            
            _key = TfNotice::Register
                (TfCreateWeakPtr(this), &Listener::_HandleNotice,
                 noticeType, sender);
        }

        object _GetDeliverableNotice(TfNotice const &notice,
                                     TfType const &noticeType) {
            // If the notice type is not wrapped, return the type name in a
            // string.
            TfPyLock lock;
            /// XXX noticeType is incorrect when the notice is
            /// python-implemented.  We should fix this when TfType optimization
            /// work is done.
            object noticeClass = TfPyGetClassObject(typeid(notice));
            if (TfPyIsNone(noticeClass))
                return object(TfType::Find(notice).GetTypeName());

            // If it's a python notice, use the embedded python object.
            if (TfPyNoticeWrapperBase const *pyNotice =
                TfSafeDynamic_cast<TfPyNoticeWrapperBase const *>(&notice))
                return object(pyNotice->GetNoticePythonObject());

            // Otherwise convert the notice to python like normal.  We
            // can't just use object(notice) because that won't produce
            // a notice of the correct derived type.
            return Tf_PyNoticeObjectGenerator::Invoke(notice);
        }
    
        void _HandleNotice(TfNotice const &notice,
                           TfType const &type,
                           TfWeakBase *sender,
                           void const *senderUniqueId,
                           const std::type_info &) {
            TfPyLock lock;
            object pyNotice = _GetDeliverableNotice(notice, type);
            if (!TfPyIsNone(pyNotice)) {
                // Get the python sender.
                handle<> pySender = sender ?
                    handle<>(allow_null(Tf_PyIdentityHelper::
                                        Get(senderUniqueId))) : handle<>();
                _callback(pyNotice, pySender);
            }
        }

        Callback _callback;
        TfNotice::Key _key;
        TfType _noticeType;
    };

    static Listener *
    RegisterWithAnyWeakPtrSender(TfType const &noticeType,
                                 Listener::Callback const &cb,
                                 TfAnyWeakPtr const &sender)
    {
        return Listener::New(noticeType, cb, sender);
    }

    static Listener *
    RegisterWithPythonSender(TfType const &noticeType,
                             Listener::Callback const &cb,
                             object const &sender) {
        Tf_PyWeakObjectPtr weakSender = Tf_PyWeakObject::GetOrCreate(sender);
        if (!weakSender)
            TfPyThrowTypeError("Cannot register to listen to notices from the "
                               "provided sender.  The sender must support "
                               "python weak references.");

        TfAnyWeakPtr holder(weakSender);
        return RegisterWithAnyWeakPtrSender(noticeType, cb, holder);
    }
    
    static Listener *
    RegisterGlobally(TfType const &noticeType, Listener::Callback const &cb) {
        return RegisterWithAnyWeakPtrSender
            (noticeType, cb, TfAnyWeakPtr());
    }

    static size_t
    SendWithAnyWeakPtrSender(TfNotice const &self,
                             TfAnyWeakPtr const &sender) {
        return Tf_PyNotice::SendWithType(self, TfType::Find(&self),
                                         sender.GetWeakBase(),
                                         sender.GetUniqueIdentifier(),
                                         sender.GetTypeInfo());
    }

    static size_t
    SendWithPythonSender(TfNotice const &self, object const &sender) {
        // Get a "WeakObjectPtr" corresponding to the sender -- this is a
        // TfWeakPtr to an object which holds a python weak reference.  This
        // object expires when the python object expires.  This is what lets us
        // use arbitrary python objects as senders in the notice system.
        Tf_PyWeakObjectPtr weakSender = Tf_PyWeakObject::GetOrCreate(sender);
        if (!weakSender)
            TfPyThrowTypeError("Cannot send notice from the provided sender.  "
                               "Sender must support python weak references.");
        TfAnyWeakPtr holder(weakSender);
        return SendWithAnyWeakPtrSender(self, holder);
    }
    
    static size_t SendGlobally(TfNotice const &self) {
        return Tf_PyNotice::SendWithType(self, TfType::Find(&self),
                                         0, 0, typeid(void));
    }

};

} // anonymous namespace 

void wrapNotice()
{
    // Make sure we can pass callbacks from python.
    TfPyFunctionFromPython<Tf_PyNoticeInternal::Listener::CallbackSig>();

    // Passing TfNotice for both T and its base indicates that this is the root
    // of the notice hierarchy.
    scope notice = TfPyNoticeWrapper<TfNotice, TfNotice>::Wrap("Notice")
        .def(init<>())
        
        // We register the method that takes any python object first, as this is
        // the last overload that will be tried.  Thus, it will only be invoked
        // if the python object is not already weak-pointable.
        .def("Register",
             Tf_PyNoticeInternal::RegisterWithPythonSender,
             return_value_policy<manage_new_object>(),
         "Register( noticeType, callback, sender ) -> Listener \n\n"
         "noticeType : Tf.Notice\n"
         "callback : function\n"
         "sender : object\n\n"
         "Register a listener as being interested in a TfNotice  "
         "type from a specific sender.  Notice listener will get sender  "
         "as an argument.   "
         "  "
         "Registration of interest in a notice class N automatically  "
         "registers interest in all classes derived from N.  When a  "
         "notice of appropriate type is received, the listening object's "
         " member-function method is called with the notice. "
         "  "
         "  "
         "To reverse the registration, call Revoke() on the Listener "
         "object returned by this call. "
             )
        .def("Register",
             Tf_PyNoticeInternal::RegisterWithAnyWeakPtrSender,
             return_value_policy<manage_new_object>())
        .staticmethod("Register")
        
        .def("RegisterGlobally",
             Tf_PyNoticeInternal::RegisterGlobally,
             return_value_policy<manage_new_object>(), 
             "RegisterGlobally( noticeType, callback ) -> Listener \n\n"
             "noticeType : Tf.Notice\n"
             "callback : function\n\n"
             "Register a listener as being interested in a TfNotice "
             "type from any sender.  The notice listener does not get sender "
             "as an argument. "
             "")
        .staticmethod("RegisterGlobally")


        // We register the method that takes any python object first, as this is
        // the last overload that will be tried.  Thus, it will only be invoked
        // if the python object is not already weak-pointable.
        .def("Send", &Tf_PyNoticeInternal::SendWithPythonSender,
             "Send(sender) \n\n"
             "sender : object \n\n"
             "Deliver the notice to interested listeners, returning the number "
             "of interested listeners. "
             "This is the recommended form of Send.  It takes the sender as an "
             "argument. "
             "Listeners that registered for the given sender AND listeners "
             "that registered globally will get the notice. ")
        .def("Send", &Tf_PyNoticeInternal::SendWithAnyWeakPtrSender)
        .def("SendGlobally", &Tf_PyNoticeInternal::SendGlobally,
             "SendGlobally() \n\n"
             "Deliver the notice to interested listeners.   "
             "For most clients it is recommended to use the Send(sender) "
             "version of "
             "Send() rather than this one.  Clients that use this form of Send "
             "will prevent listeners from being able to register to receive "
             "notices "
             "based on the sender of the notice. "
             "ONLY listeners that registered globally will get the notice. ") 
        ;

    const char* Listener_string = ""
    "Represents the Notice connection between "
    "senders and receivers of notices.  When a Listener object expires the "
    "connection is broken. "
    "You can also use the Revoke() function to break the connection. "
    "A Listener object is returned from the Register() and  "
    "RegisterGlobally() functions. ";
    class_<Tf_PyNoticeInternal::Listener,
           noncopyable>("Listener", Listener_string, no_init)
        .def("Revoke", &Tf_PyNoticeInternal::Listener::Revoke,
            "Revoke() \n\n"
            "Revoke interest by a notice listener. "
            " "
            "This function revokes interest in the "
            "particular notice type and call-back method that its "
            "Listener object was registered for. "
        )
        ;
}
