/*=========================================================================

  Library:   CTK

  Copyright (c) Kitware Inc.

  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
  You may obtain a copy of the License at

      http://www.commontk.org/LICENSE

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.

=========================================================================*/

// Qt includes
#include <QDebug>
#include <QPointer>
#include <QRegExp>
#include <QString>
#include <QTextStream>

// CTK includes
#include "ctkVTKConnection.h"

// VTK includes
#include <vtkObject.h>
#include <vtkSmartPointer.h>
#include <vtkCallbackCommand.h>

//-----------------------------------------------------------------------------
QString convertPointerToString(void* pointer)
{
  QString pointerAsString;
  QTextStream(&pointerAsString) << pointer;
  return pointerAsString;
}

//-----------------------------------------------------------------------------
class ctkVTKConnectionPrivate
{
  Q_DECLARE_PUBLIC(ctkVTKConnection);
protected:
  ctkVTKConnection* const q_ptr;
public:
  enum
    {
    ARG_UNKNOWN = 0,
    ARG_VTKOBJECT_AND_VTKOBJECT,
    ARG_VTKOBJECT_VOID_ULONG_VOID
    };

  typedef ctkVTKConnectionPrivate Self;
  ctkVTKConnectionPrivate(ctkVTKConnection& object);
  ~ctkVTKConnectionPrivate();

  void connect();
  void disconnect();

  /// 
  /// VTK Callback
  static void DoCallback(vtkObject* vtk_obj, unsigned long event,
                         void* client_data, void* call_data);

  /// 
  /// Called by 'DoCallback' to emit signal
  void execute(vtkObject* vtk_obj, unsigned long vtk_event, void* client_data, void* call_data);

  vtkSmartPointer<vtkCallbackCommand> Callback;
  vtkObject*                          VTKObject;
  const QObject*                      QtObject;
  unsigned long                       VTKEvent;
  QString                             QtSlot;
  float                               Priority;
  int                                 SlotType;
  bool                                Connected;
  bool                                Blocked;
  QString                             Id;
  bool                                ObserveDeletion;
};

//-----------------------------------------------------------------------------
// ctkVTKConnectionPrivate methods

//-----------------------------------------------------------------------------
ctkVTKConnectionPrivate::ctkVTKConnectionPrivate(ctkVTKConnection& object)
  :q_ptr(&object)
{
  this->Callback    = vtkSmartPointer<vtkCallbackCommand>::New();
  this->Callback->SetCallback(ctkVTKConnectionPrivate::DoCallback);
  this->Callback->SetClientData(this);
  this->VTKObject   = 0;
  this->QtObject    = 0;
  this->VTKEvent    = vtkCommand::NoEvent;
  this->Priority    = 0.0;
  this->SlotType    = ARG_UNKNOWN;
  this->Connected   = false;
  this->Blocked     = false;
  this->Id          = convertPointerToString(this);
  this->ObserveDeletion = false;
}

//-----------------------------------------------------------------------------
ctkVTKConnectionPrivate::~ctkVTKConnectionPrivate()
{

}

//-----------------------------------------------------------------------------
void ctkVTKConnectionPrivate::connect()
{
  Q_Q(ctkVTKConnection);
  
  if (this->Connected)
    {
    qDebug() << "ctkVTKConnection already connected.";
    return;
    }

  switch (this->SlotType)
    {
    case ctkVTKConnectionPrivate::ARG_VTKOBJECT_AND_VTKOBJECT:
      QObject::connect(q, SIGNAL(emitExecute(vtkObject*, vtkObject*)),
        this->QtObject, this->QtSlot.toLatin1().data(), Qt::AutoConnection);
      break;
    case ctkVTKConnectionPrivate::ARG_VTKOBJECT_VOID_ULONG_VOID:
      QObject::connect(q, SIGNAL(emitExecute(vtkObject*, void*, unsigned long, void*)),
                       this->QtObject, this->QtSlot.toLatin1().data(), Qt::AutoConnection);
      break;
    default:
      Q_ASSERT(false);
      qCritical() << "Failed to connect - "
                  << "The slot (" << this->QtSlot <<  ") owned by "
                  << "QObject(" << this->QtObject->objectName() << ")"
                  << " seems to have a wrong signature.";
      break;
    }

  // Make a connection between this and the vtk object
  this->VTKObject->AddObserver(this->VTKEvent, this->Callback, this->Priority);

  // If necessary, observe vtk DeleteEvent
  if(this->ObserveDeletion)
    {
    // don't observe it twice
    if (this->VTKEvent != vtkCommand::DeleteEvent)
      {
      this->VTKObject->AddObserver(vtkCommand::DeleteEvent, this->Callback);
      }
    // Remove itself from its parent when vtkObject is deleted
    QObject::connect(this->QtObject, SIGNAL(destroyed(QObject*)), 
                     q, SLOT(qobjectDeleted()));
    }
  this->Connected = true;
}

//-----------------------------------------------------------------------------
void ctkVTKConnectionPrivate::disconnect()
{
  Q_Q(ctkVTKConnection);
  
  if (!this->Connected) 
    { 
    return; 
    }

  if (this->QtObject)
    {
    switch (this->SlotType)
      {
      case ctkVTKConnectionPrivate::ARG_VTKOBJECT_AND_VTKOBJECT:
        QObject::disconnect(q, SIGNAL(emitExecute(vtkObject*, vtkObject*)),
                            this->QtObject,this->QtSlot.toLatin1().data());
        break;
      case ctkVTKConnectionPrivate::ARG_VTKOBJECT_VOID_ULONG_VOID:
        QObject::disconnect(q, SIGNAL(emitExecute(vtkObject*, void*, unsigned long, void*)),
                            this->QtObject, this->QtSlot.toLatin1().data());
        break;
      default:
        Q_ASSERT(false);
        qCritical() << "Failed to disconnect - "
                    << "The slot (" << this->QtSlot <<  ") owned by "
                    << "QObject(" << this->QtObject->objectName() << ")"
                    << " seems to have a wrong signature.";
        break;
      }
    }

  if (this->VTKObject)
    {
    this->VTKObject->RemoveObserver(this->Callback);
    }

  if (this->ObserveDeletion && this->QtObject)
    {
    //this->VTKObject->AddObserver(vtkCommand::DeleteEvent, this->Callback); has already been removed
    QObject::disconnect(this->QtObject, SIGNAL(destroyed(QObject*)),
                        q, SIGNAL(isBroke()));
    }

  this->Connected = false;
}

//-----------------------------------------------------------------------------
// ctkVTKConnection methods

//-----------------------------------------------------------------------------
ctkVTKConnection::ctkVTKConnection(QObject* _parent):
  Superclass(_parent)
  , d_ptr(new ctkVTKConnectionPrivate(*this))
{
}

//-----------------------------------------------------------------------------
ctkVTKConnection::~ctkVTKConnection()
{
  Q_D(ctkVTKConnection);
  if (d->ObserveDeletion)
    {
    d->disconnect();
    }
}

//-----------------------------------------------------------------------------
QString ctkVTKConnection::id()const
{
  Q_D(const ctkVTKConnection);
  return d->Id;
}

//-----------------------------------------------------------------------------
QObject* ctkVTKConnection::object()const
{
  Q_D(const ctkVTKConnection);
  return const_cast<QObject*>(d->QtObject);
}

//-----------------------------------------------------------------------------
QDebug operator<<(QDebug dbg, const ctkVTKConnection& connection)
{
  const ctkVTKConnectionPrivate* d = connection.d_func();
  dbg.nospace() << "ctkVTKConnection:" << &connection << endl
                << "Id:" << d->Id << endl
                << " VTKObject:" << d->VTKObject->GetClassName()
                << "(" << d->VTKObject << ")" << endl
                << " QtObject:" << d->QtObject << endl
                << " VTKEvent:" << d->VTKEvent << endl
                << " QtSlot:" << d->QtSlot << endl
                << " SlotType:" << d->SlotType << endl
                << " Priority:" << d->Priority << endl
                << " Connected:" << d->Connected << endl
                << " Blocked:" << d->Blocked;
  return dbg.space();
}

//-----------------------------------------------------------------------------
QString ctkVTKConnection::shortDescription()
{
  Q_D(ctkVTKConnection);
  
  return ctkVTKConnection::shortDescription(d->VTKObject, d->VTKEvent, d->QtObject, d->QtSlot);
}

//-----------------------------------------------------------------------------
QString ctkVTKConnection::shortDescription(vtkObject* vtk_obj, unsigned long vtk_event,
    const QObject* qt_obj, QString qt_slot)
{
  QString ret;
  QTextStream ts( &ret );
  ts << (vtk_obj ? vtk_obj->GetClassName() : "NULL") << " "
     << vtk_event << " " << qt_obj << " " << qt_slot;
  return ret;
}

//-----------------------------------------------------------------------------
bool ctkVTKConnection::isValid(vtkObject* vtk_obj, unsigned long vtk_event,
                               const QObject* qt_obj, QString qt_slot)
{
  Q_UNUSED(vtk_event);
  if (!vtk_obj)
    {
    return false;
    }
  if (!qt_obj)
    {
    return false;
    }
  if (qt_slot.isEmpty())
    {
    return false;
    }
  return true;
}

//-----------------------------------------------------------------------------
void ctkVTKConnection::setup(vtkObject* vtk_obj, unsigned long vtk_event,
                             const QObject* qt_obj, QString qt_slot, 
                             float priority)
{
  Q_D(ctkVTKConnection);
  
  if (!ctkVTKConnection::isValid(vtk_obj, vtk_event, qt_obj, qt_slot)) 
    { 
    return; 
    }

  d->VTKObject = vtk_obj;
  d->QtObject = qt_obj;
  d->VTKEvent = vtk_event;
  d->QtSlot = qt_slot;
  d->Priority = priority;

  if (qt_slot.contains(QRegExp(QString("\\( ?vtkObject ?\\* ?, ?vtkObject ?\\* ?\\)"))))
    {
    d->SlotType = ctkVTKConnectionPrivate::ARG_VTKOBJECT_AND_VTKOBJECT;
    }
  else
    {
    d->SlotType = ctkVTKConnectionPrivate::ARG_VTKOBJECT_VOID_ULONG_VOID;
    }
  d->connect();
}

//-----------------------------------------------------------------------------
void ctkVTKConnection::setBlocked(bool block)
{
  Q_D(ctkVTKConnection);
  d->Blocked = block;
}

//-----------------------------------------------------------------------------
bool ctkVTKConnection::isBlocked()const
{
  Q_D(const ctkVTKConnection);
  return d->Blocked;
}

//-----------------------------------------------------------------------------
bool ctkVTKConnection::isEqual(vtkObject* vtk_obj, unsigned long vtk_event,
    const QObject* qt_obj, QString qt_slot)const
{
  Q_D(const ctkVTKConnection);
  
  if (vtk_obj && d->VTKObject != vtk_obj)
    {
    return false;
    }
  if (vtk_event != vtkCommand::NoEvent && d->VTKEvent != vtk_event)
    {
    return false;
    }
  if (qt_obj && d->QtObject != qt_obj)
    {
    return false;
    }
  if (!qt_slot.isEmpty() && 
      (QString(d->QtSlot).remove(' ').compare(
        QString(qt_slot).remove(' ')) != 0))
    {
    return false;
    }
  return true;
}

//-----------------------------------------------------------------------------
void ctkVTKConnectionPrivate::DoCallback(vtkObject* vtk_obj, unsigned long event,
                                 void* client_data, void* call_data)
{
  ctkVTKConnectionPrivate* conn = reinterpret_cast<ctkVTKConnectionPrivate*>(client_data);
  Q_ASSERT(conn);
  conn->execute(vtk_obj, event, client_data, call_data);
}

//-----------------------------------------------------------------------------
// callback from VTK to emit signal
void ctkVTKConnectionPrivate::execute(vtkObject* vtk_obj, unsigned long vtk_event,
  void* client_data, void* call_data)
{
  Q_Q(ctkVTKConnection);
  
  Q_ASSERT(this->Connected);
  if (this->Blocked) 
    { 
    return; 
    }

  QPointer<ctkVTKConnection> connection(q);
  if(!this->ObserveDeletion ||
     vtk_event != vtkCommand::DeleteEvent ||
     this->VTKEvent == vtkCommand::DeleteEvent)
    {
    vtkObject* callDataAsVtkObject = 0;
    switch (this->SlotType)
      {
      case ctkVTKConnectionPrivate::ARG_VTKOBJECT_AND_VTKOBJECT:
        if (this->VTKEvent == vtk_event)
          {
          callDataAsVtkObject = reinterpret_cast<vtkObject*>( call_data );
          if (!callDataAsVtkObject)
            {
            qCritical() << "The VTKEvent(" << this->VTKEvent<< ") triggered by vtkObject("
              << this->VTKObject->GetClassName() << ") "
              << "doesn't return data of type vtkObject." << endl
              << "The slot (" << this->QtSlot <<  ") owned by "
              << "QObject(" << this->QtObject->objectName() << ")"
              << " may be incorrect.";
            }
          emit q->emitExecute(vtk_obj, callDataAsVtkObject);
          }
        break;
      case ctkVTKConnectionPrivate::ARG_VTKOBJECT_VOID_ULONG_VOID:
        emit q->emitExecute(vtk_obj, call_data, vtk_event, client_data);
        break;
      default:
        // Should never reach
        qCritical() << "Unknown SlotType:" << this->SlotType;
        return;
        break;
      }
    }

  if (!connection.isNull() &&
      vtk_event == vtkCommand::DeleteEvent)
    {
    q->vtkObjectDeleted();
    }
}

void ctkVTKConnection::observeDeletion(bool enable)
{
  Q_D(ctkVTKConnection);
  d->ObserveDeletion = enable;
}

bool ctkVTKConnection::deletionObserved()const
{
  Q_D(const ctkVTKConnection);
  return d->ObserveDeletion;
}

void ctkVTKConnection::vtkObjectDeleted()
{
  Q_D(ctkVTKConnection);
  d->VTKObject = 0;
  d->disconnect();
  emit isBroke();
}

void ctkVTKConnection::qobjectDeleted()
{
  Q_D(ctkVTKConnection);
  d->QtObject = 0;
  d->disconnect();
  emit isBroke();
}
