/*
 * Project: MoleCuilder
 * Description: creates and alters molecular systems
 * Copyright (C)  2010-2012 University of Bonn. All rights reserved.
 * Copyright (C)  2013 Frederik Heber. All rights reserved.
 * 
 *
 *   This file is part of MoleCuilder.
 *
 *    MoleCuilder is free software: you can redistribute it and/or modify
 *    it under the terms of the GNU General Public License as published by
 *    the Free Software Foundation, either version 2 of the License, or
 *    (at your option) any later version.
 *
 *    MoleCuilder is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    GNU General Public License for more details.
 *
 *    You should have received a copy of the GNU General Public License
 *    along with MoleCuilder.  If not, see .
 */
/*
 * GLMoleculeObject_bond.cpp
 *
 *  Created on: Aug 17, 2011
 *      Author: heber
 */
// include config.h
#ifdef HAVE_CONFIG_H
#include 
#endif
#include "GLMoleculeObject_bond.hpp"
#include 
#include 
#include "CodePatterns/MemDebug.hpp"
#include 
#include "CodePatterns/Assert.hpp"
#include "CodePatterns/Log.hpp"
#include "CodePatterns/Observer/Notification.hpp"
#include "CodePatterns/Observer/ObserverLog.hpp"
#include "Descriptors/AtomIdDescriptor.hpp"
#include "Atom/atom.hpp"
#include "Bond/bond.hpp"
#include "Element/element.hpp"
#include "Helpers/defs.hpp"
#include "LinearAlgebra/Line.hpp"
#include "LinearAlgebra/Vector.hpp"
#include "World.hpp"
GLMoleculeObject_bond::GLMoleculeObject_bond(QGLSceneNode *mesh[], QObject *parent, const bond::ptr bondref, const enum SideOfBond side) :
  GLMoleculeObject(mesh, parent),
  Observer(std::string("GLMoleculeObject_bond")
      +toString(bondref->leftatom->getId())
      +std::string("-")
      +toString(bondref->rightatom->getId())),
  _bond(*bondref),
  leftobservable(bondref->leftatom),
  rightobservable(bondref->rightatom),
  leftatomId(bondref->leftatom->getId()),
  rightatomId(bondref->rightatom->getId()),
  BondSide(side),
  leftobservable_enabled(false),
  rightobservable_enabled(false),
  bond_enabled(false)
{
  // sign on as observer (obtain non-const instance before)
  _bond.signOn(this);
  _bond.signOn(this, BondObservable::BondRemoved);
  _bond.signOn(this, BondObservable::DegreeChanged);
  bond_enabled = true;
  leftobservable->signOn(this);
  leftobservable->signOn(this, AtomObservable::PositionChanged);
  leftobservable->signOn(this, AtomObservable::ElementChanged);
  leftobservable_enabled = true;
  rightobservable->signOn(this);
  rightobservable->signOn(this, AtomObservable::PositionChanged);
  rightobservable->signOn(this, AtomObservable::ElementChanged);
  rightobservable_enabled = true;
  size_t elementno = 0;
  switch (BondSide) {
    case left:
    {
      const atom *_rightatom = World::getInstance().getAtom(AtomById(rightatomId));
      if (_rightatom->getType() != NULL) {
        elementno = _rightatom->getType()->getAtomicNumber();
      } else { // if not element yet set, set to hydrogen
        elementno = 1;
      }
      break;
    }
    case right:
    {
      const atom *_leftatom = World::getInstance().getAtom(AtomById(leftatomId));
      if (_leftatom->getType() != NULL) {
        elementno = _leftatom->getType()->getAtomicNumber();
      } else { // if not element yet set, set to hydrogen
        elementno = 1;
      }
      break;
    }
    default:
      ASSERT(0,
          "GLMoleculeObject_bond::GLMoleculeObject_bond() - side is not a valid argument: "+toString(BondSide)+".");
      break;
  }
  QGLMaterial *elementmaterial = getMaterial(elementno);
  setMaterial(elementmaterial);
  resetPosition();
  resetWidth();
}
GLMoleculeObject_bond::~GLMoleculeObject_bond()
{
  LOG(3, "DEBUG: Destroying  GLMoleculeObject_bond to bond " << &_bond << " and side " << BondSide << ".");
  // signOff() if not already done
  removeLeftAtom();
  removeRightAtom();
  removeBond();
  removeChannels();
}
void GLMoleculeObject_bond::removeLeftAtom()
{
  // at this point both atoms should still be alive, hence we may safely sign off
  // from the AtomObservable itself
  if (leftobservable_enabled) {
    const atom *_leftatom = World::getInstance().getAtom(AtomById(leftatomId));
    if (_leftatom != NULL) {
      _leftatom->signOff(this);
    } else {
      ELOG(2, "Left atom of bond with id "+toString(leftatomId)+" is already gone.");
    }
  }
}
void GLMoleculeObject_bond::removeRightAtom()
{
  // at this point both atoms should still be alive, hence we may safely sign off
  // from the AtomObservable itself
  if (rightobservable_enabled) {
    const atom *_rightatom = World::getInstance().getAtom(AtomById(rightatomId));
    if (_rightatom != NULL) {
      _rightatom->signOff(this);
    } else {
      ELOG(2, "Right atom of bond with id "+toString(rightatomId)+" is already gone.");
    }
  }
}
void GLMoleculeObject_bond::removeBond()
{
  if (bond_enabled)
    _bond.signOff(this);
}
void GLMoleculeObject_bond::removeChannels()
{
  // at this point both atoms should still be alive, hence we may safely sign off
  // from the AtomObservable itself
  if (leftobservable_enabled) {
    const atom *_leftatom = World::getInstance().getAtom(AtomById(leftatomId));
    if (_leftatom != NULL) {
      _leftatom->signOff(this, AtomObservable::PositionChanged);
      _leftatom->signOff(this, AtomObservable::ElementChanged);
    } else {
      ELOG(2, "Left atom of bond with id "+toString(leftatomId)+" is already gone.");
    }
    leftobservable_enabled = false;
  }
  if (rightobservable_enabled) {
    const atom *_rightatom = World::getInstance().getAtom(AtomById(rightatomId));
    if (_rightatom != NULL) {
      _rightatom->signOff(this, AtomObservable::PositionChanged);
      _rightatom->signOff(this, AtomObservable::ElementChanged);
    } else {
      ELOG(2, "Right atom of bond with id "+toString(rightatomId)+" is already gone.");
    }
    rightobservable_enabled = false;
  }
  if (bond_enabled) {
    _bond.signOff(this, BondObservable::BondRemoved);
    _bond.signOff(this, BondObservable::DegreeChanged);
    bond_enabled = false;
  }
}
void GLMoleculeObject_bond::removeMe()
{
  // sign off
  switch (BondSide) {
    case left:
      emit BondRemoved(leftatomId, rightatomId);
      break;
    case right:
      emit BondRemoved(rightatomId, leftatomId);
      break;
    default:
      ASSERT(0,
          "GLMoleculeObject_bond::subjectKilled() - side is not a valid argument: "
          +toString(BondSide)+".");
      break;
  }
}
void GLMoleculeObject_bond::update(Observable *publisher)
{
#ifdef LOG_OBSERVER
  if (publisher == static_cast(&_bond)) {
    observerLog().addMessage() << "++ Update of Observer "
        << observerLog().getName(static_cast(this))
        << " from bond.";
  } else if (publisher == leftobservable) {
    observerLog().addMessage() << "++ Update of Observer "
        << observerLog().getName(static_cast(this))
        << " from leftatom " << leftatomId << ".";
  } else if (publisher == rightobservable) {
    observerLog().addMessage() << "++ Update of Observer " <<
        observerLog().getName(static_cast(this))
        << " from rightatom " << rightatomId << ".";
  } else
    observerLog().addMessage() << "++ Update of Observer " <<
    observerLog().getName(static_cast(this)) << " from unknown source.";
#endif
}
void GLMoleculeObject_bond::subjectKilled(Observable *publisher)
{
  // assume subjectKilled() is from Observable's own subjectKilled(), not notifications
  // but we must always signOff from all other sources!
  if (publisher == static_cast(&_bond)) {
#ifdef LOG_OBSERVER
    observerLog().addMessage() << "++ subjectKilled of Observer "
        << observerLog().getName(static_cast(this))
        << " from bond.";
#endif
    removeLeftAtom();
    removeRightAtom();
  } else if (publisher == leftobservable) {
#ifdef LOG_OBSERVER
    observerLog().addMessage() << "++ subjectKilled of Observer "
        << observerLog().getName(static_cast(this))
        << " from leftatom " << leftatomId << ".";
#endif
    removeRightAtom();
    removeBond();
  } else if (publisher == rightobservable) {
#ifdef LOG_OBSERVER
    observerLog().addMessage() << "++ subjectKilled of Observer " <<
        observerLog().getName(static_cast(this))
        << " from rightatom " << rightatomId << ".";
#endif
    removeLeftAtom();
    removeBond();
  } else {
#ifdef LOG_OBSERVER
    observerLog().addMessage() << "++ subjectKilled of Observer " <<
    observerLog().getName(static_cast(this)) << " from unknown source.";
#endif
  }
  // then indicate to remove us
  removeChannels();
  removeMe();
}
void GLMoleculeObject_bond::recieveNotification(Observable *publisher, Notification_ptr notification)
{
#ifdef LOG_OBSERVER
  if (publisher == static_cast(&_bond)) {
    observerLog().addMessage() << "++ Update of Observer "
        << observerLog().getName(static_cast(this))
        << " received notification from bond for channel "
        << notification->getChannelNo() << ".";
  } else if (publisher == leftobservable) {
    observerLog().addMessage() << "++ Update of Observer "
        << observerLog().getName(static_cast(this))
        << " received notification from leftatom " << leftatomId << " for channel "
        << notification->getChannelNo() << ".";
  } else if (publisher == rightobservable) {
    observerLog().addMessage() << "++ Update of Observer "
        << observerLog().getName(static_cast(this))
        << " received notification from rightatom " << rightatomId << " for channel "
        << notification->getChannelNo() << ".";
  } else
    observerLog().addMessage() << "++ Update of Observer "
        << observerLog().getName(static_cast(this))
        << " received notification from unknown source.";
#endif
  bool DoResetPosition = false;
  bool DoResetWidth = false;
  if (publisher == static_cast(&_bond)){
    switch (notification->getChannelNo()) {
    case BondObservable::BondRemoved:
//      removeMe();
      break;
    case BondObservable::DegreeChanged:
      DoResetWidth = true;
      break;
    default:
      ASSERT(0, "GLMoleculeObject_bond::recieveNotification() - unknown signal.");
      break;
    }
  } else {
    // from an atom
    switch (notification->getChannelNo()) {
    case AtomObservable::PositionChanged:
      LOG(2, "INFO: Received notification of PositionChanged.");
      DoResetPosition = true;
      break;
    case AtomObservable::ElementChanged:
      LOG(2, "INFO: Received notification of ElementChanged.");
      DoResetPosition = true;
      break;
    default:
      break;
    }
  }
  if (DoResetPosition) {
    resetPosition();
    emit changed();
  }
  if (DoResetWidth) {
    resetWidth();
    emit changed();
  }
}
void GLMoleculeObject_bond::resetWidth()
{
  const double factor = 1.0f+.5f*(_bond.getDegree()-1);
  LOG(2, "DEBUG: GLMoleculeObject_bond::resetWidth() - setting bond's width to " << factor << ".");
  setScaleX(factor);
  setScaleY(factor);
}
void GLMoleculeObject_bond::resetPosition()
{
  Vector Position;
  Vector OtherPosition;
  const atom *_leftatom = World::getInstance().getAtom(AtomById(leftatomId));
  Vector LeftPos = _leftatom->getPosition();
  const atom *_rightatom = World::getInstance().getAtom(AtomById(rightatomId));
  Vector RightPos = _rightatom->getPosition();
  switch (BondSide) {
    case left:
      Position = LeftPos;
      OtherPosition = RightPos;
      break;
    case right:
      Position = RightPos; 
      OtherPosition = LeftPos;
      break;
    default:
      ASSERT(0,
          "GLMoleculeObject_bond::resetPosition() - side is not a valid argument: "+toString(BondSide)+".");
      break;
  }
  const double distance =
      Position.distance(OtherPosition)/2.;
  setScaleZ(distance);
  // calculate position
  Vector Z(unitVec[2]); // cylinder are initially aligned along the Z axis
  Vector zeroVec(0.,0.,0.);
  Vector a,b;
  Vector OtherAxis;
  double alpha;
  a = Position - OtherPosition;
  // construct rotation axis
  b = a;
  b.VectorProduct(Z);
  Line axis(zeroVec, b);
  // calculate rotation angle
  alpha = a.Angle(Z);
  // construct other axis to check right-hand rule
  OtherAxis = b;
  OtherAxis.VectorProduct(Z);
  // assure right-hand rule for the rotation
  if (a.ScalarProduct(OtherAxis) < MYEPSILON)
    alpha = M_PI-alpha;
  // check
  Vector a_rotated = axis.rotateVector(a, alpha);
  LOG(3, "INFO: Created cylinder from "// << Position << " to " << OtherPosition
      << a << " to " << a_rotated << " around " << b << " by " << alpha/M_PI*180. << ", respectively.");
  // set position (cylinder offset is in its barymetric center)
  Vector OneFourth(Position - 0.75 * a);
  setPosition(QVector3D(OneFourth[0], OneFourth[1], OneFourth[2]));
  setRotationVector(QVector3D(b[0], b[1], b[2]));
  setRotationAngle(alpha/M_PI*180.);
}