/****************************************************************************
 *   This file is part of the aGrUM/pyAgrum library.                        *
 *                                                                          *
 *   Copyright (c) 2005-2026 by                                             *
 *       - Pierre-Henri WUILLEMIN(_at_LIP6)                                 *
 *       - Christophe GONZALES(_at_AMU)                                     *
 *                                                                          *
 *   The aGrUM/pyAgrum library is free software; you can redistribute it    *
 *   and/or modify it under the terms of either :                           *
 *                                                                          *
 *    - the GNU Lesser General Public License as published by               *
 *      the Free Software Foundation, either version 3 of the License,      *
 *      or (at your option) any later version,                              *
 *    - the MIT license (MIT),                                              *
 *    - or both in dual license, as here.                                   *
 *                                                                          *
 *   (see https://agrum.gitlab.io/articles/dual-licenses-lgplv3mit.html)    *
 *                                                                          *
 *   This aGrUM/pyAgrum library is distributed in the hope that it will be  *
 *   useful, but WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,          *
 *   INCLUDING BUT NOT LIMITED TO THE WARRANTIES MERCHANTABILITY or FITNESS *
 *   FOR A PARTICULAR PURPOSE  AND NONINFRINGEMENT. IN NO EVENT SHALL THE   *
 *   AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER *
 *   LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,        *
 *   ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR  *
 *   OTHER DEALINGS IN THE SOFTWARE.                                        *
 *                                                                          *
 *   See LICENCES for more details.                                         *
 *                                                                          *
 *   SPDX-FileCopyrightText: Copyright 2005-2026                            *
 *       - Pierre-Henri WUILLEMIN(_at_LIP6)                                 *
 *       - Christophe GONZALES(_at_AMU)                                     *
 *   SPDX-License-Identifier: LGPL-3.0-or-later OR MIT                      *
 *                                                                          *
 *   Contact  : info_at_agrum_dot_org                                       *
 *   homepage : http://agrum.gitlab.io                                      *
 *   gitlab   : https://gitlab.com/agrumery/agrum                           *
 *                                                                          *
 ****************************************************************************/

#pragma once


/**
 * @file
 * @brief Inline implementation of gum::prm::PRMInterface
 *
 * @author Lionel TORTI and Pierre-Henri WUILLEMIN(_at_LIP6)
 */
#include <agrum/PRM/elements/PRMClass.h>
#include <agrum/PRM/elements/PRMInterface.h>

namespace gum {
  namespace prm {

    template < GUM_Numeric GUM_SCALAR >
    PRMInterface< GUM_SCALAR >::PRMInterface(std::string_view name) :
        PRMClassElementContainer< GUM_SCALAR >(name), _superInterface_(0) {
      GUM_CONSTRUCTOR(PRMInterface);
    }

    template < GUM_Numeric GUM_SCALAR >
    PRMInterface< GUM_SCALAR >::PRMInterface(std::string_view            name,
                                             PRMInterface< GUM_SCALAR >& super,
                                             bool                        delayInheritance) :
        PRMClassElementContainer< GUM_SCALAR >(name), _superInterface_(&super) {
      GUM_CONSTRUCTOR(PRMInterface);
      if (!delayInheritance) { _inheritInterface_(super); }
    }

    template < GUM_Numeric GUM_SCALAR >
    PRMInterface< GUM_SCALAR >::PRMInterface(const PRMInterface< GUM_SCALAR >& source) :
        PRMClassElementContainer< GUM_SCALAR >(source.name()), _dag_(source._dag_),
        _superInterface_(source._superInterface_) {
      GUM_CONS_CPY(PRMInterface);
      GUM_ERROR(FatalError, "don't copy an interface")
    }

    template < GUM_Numeric GUM_SCALAR >
    PRMInterface< GUM_SCALAR >::~PRMInterface() {
      GUM_DESTRUCTOR(PRMInterface);

      for (const auto& elt: _nodeIdMap_) {
        delete elt.second;
      }
    }

    template < GUM_Numeric GUM_SCALAR >
    void PRMInterface< GUM_SCALAR >::inheritInterface() {
      if (_superInterface_ != nullptr) { _inheritInterface_(*_superInterface_); }
    }

    template < GUM_Numeric GUM_SCALAR >
    void PRMInterface< GUM_SCALAR >::_inheritInterface_(const PRMInterface< GUM_SCALAR >& i) {
      // Copying attributes
      for (const auto i_attr: i._attributes_) {
        auto attr = new PRMScalarAttribute< GUM_SCALAR >(i_attr->name(), i_attr->type());
        attr->setId(i_attr->id());
        _nodeIdMap_.insert(attr->id(), attr);
        _attributes_.insert(attr);

        if (i._nameMap_[i_attr->name()] == i._nameMap_[i_attr->safeName()]) {
          _nameMap_.insert(attr->name(), attr);
        }

        _nameMap_.insert(attr->safeName(), attr);
        _dag_.addNodeWithId(attr->id());
      }

      // Copying reference slots
      for (const auto i_ref: i._referenceSlots_) {
        auto ref = new PRMReferenceSlot< GUM_SCALAR >(
            i_ref->name(),
            const_cast< PRMClassElementContainer< GUM_SCALAR >& >(i_ref->slotType()),
            i_ref->isArray());

        ref->setId(i_ref->id());
        _nodeIdMap_.insert(ref->id(), ref);
        _referenceSlots_.insert(ref);

        if (i._nameMap_.exists(ref->name())) { _nameMap_.insert(ref->name(), ref); }

        _nameMap_.insert(ref->safeName(), ref);
        _dag_.addNodeWithId(ref->id());
      }
    }

    template < GUM_Numeric GUM_SCALAR >
    NodeId PRMInterface< GUM_SCALAR >::add(PRMClassElement< GUM_SCALAR >* elt) {
      if (_nameMap_.exists(elt->name())) {
        GUM_ERROR(DuplicateElement,
                  "name '" << elt->name() << "' is already used by another ClassElement");
      }

      if (PRMClassElement< GUM_SCALAR >::isAttribute(*elt)) {
        PRMAttribute< GUM_SCALAR >* attr = static_cast< PRMAttribute< GUM_SCALAR >* >(elt);
        _nameMap_.insert(attr->name(), attr);

        while (true) {
          attr->setId(nextNodeId());
          _dag_.addNodeWithId(attr->id());
          _nodeIdMap_.insert(attr->id(), attr);
          _nameMap_.insert(attr->safeName(), attr);
          _attributes_.insert(attr);

          if (attr->type().isSubType()) {
            attr = attr->getCastDescendant();
          } else {
            break;
          }
        }
      } else if (PRMClassElement< GUM_SCALAR >::isReferenceSlot(*elt)) {
        elt->setId(nextNodeId());
        _dag_.addNodeWithId(elt->id());
        _nodeIdMap_.insert(elt->id(), elt);
        _referenceSlots_.insert(static_cast< PRMReferenceSlot< GUM_SCALAR >* >(elt));
        _nameMap_.insert(elt->name(), elt);
        _nameMap_.insert(elt->safeName(), elt);
      } else {
        GUM_ERROR(WrongClassElement, "illegal ClassElement<GUM_SCALAR> for an Interface")
      }

      return elt->id();
    }

    template < GUM_Numeric GUM_SCALAR >
    NodeId PRMInterface< GUM_SCALAR >::overload(PRMClassElement< GUM_SCALAR >* overloader) {
      if (!_superInterface_)
        GUM_ERROR(OperationNotAllowed, "overload is possible only with sub interfaces")

      if (!super().exists(overloader->name()))
        GUM_ERROR(OperationNotAllowed, "found no ClassElement<GUM_SCALAR> to overload")

      PRMClassElement< GUM_SCALAR >* overloaded = _nameMap_[overloader->name()];
      if (overloaded == overloader)
        GUM_ERROR(DuplicateElement, "duplicate ClassElement '" << overloader->name() << "'")

      if (!_checkOverloadLegality_(overloaded, overloader))
        GUM_ERROR(OperationNotAllowed, "illegal overload")

      switch (overloader->elt_type()) {
        case PRMClassElement< GUM_SCALAR >::prm_attribute : {
          auto attr_overloader = static_cast< PRMAttribute< GUM_SCALAR >* >(overloader);
          auto attr_overloaded = static_cast< PRMAttribute< GUM_SCALAR >* >(overloaded);
          _overloadAttribute_(attr_overloader, attr_overloaded);
          break;
        }

        case PRMClassElement< GUM_SCALAR >::prm_refslot : {
          auto ref_overloader = static_cast< PRMReferenceSlot< GUM_SCALAR >* >(overloader);
          auto ref_overloaded = static_cast< PRMReferenceSlot< GUM_SCALAR >* >(overloaded);
          _overloadReferenceSlot_(ref_overloader, ref_overloaded);
          break;
        }

        case PRMClassElement< GUM_SCALAR >::prm_aggregate :
        case PRMClassElement< GUM_SCALAR >::prm_slotchain :
          GUM_ERROR(OperationNotAllowed,
                    "Element " << overloader->name() << " can not be overloaded")
        default :
          GUM_ERROR(FatalError, "Unknown ClassElement<GUM_SCALAR> type for " << overloader->name())
      }

      return overloader->id();
    }

    template < GUM_Numeric GUM_SCALAR >
    void PRMInterface< GUM_SCALAR >::_overloadAttribute_(PRMAttribute< GUM_SCALAR >* overloader,
                                                         PRMAttribute< GUM_SCALAR >* overloaded) {
      if (overloader->type() != overloaded->type()) {
        overloader->setId(nextNodeId());
        _dag_.addNodeWithId(overloader->id());
        _nodeIdMap_.insert(overloader->id(), overloader);
        _nameMap_[overloader->name()] = overloader;
        _nameMap_.insert(overloader->safeName(), overloader);
        _attributes_.insert(overloader);
        _addCastDescendants_(overloader, overloaded);
      } else {
        overloader->setId(overloaded->id());
        _nodeIdMap_[overloader->id()]     = overloader;
        _nameMap_[overloader->name()]     = overloader;
        _nameMap_[overloader->safeName()] = overloader;
        _attributes_.erase(overloaded);
        _attributes_.insert(overloader);
        // Swapping types, ugly but necessary to preserve the
        // PRMType<GUM_SCALAR>
        // pointer of overloaded
        overloader->overload(overloaded);
        delete overloaded;
      }
    }

    template < GUM_Numeric GUM_SCALAR >
    void PRMInterface< GUM_SCALAR >::_overloadReferenceSlot_(
        PRMReferenceSlot< GUM_SCALAR >* overloader,
        PRMReferenceSlot< GUM_SCALAR >* overloaded) {
      // Adding overloading reference
      overloader->setId(overloaded->id());
      _nodeIdMap_[overloader->id()] = overloader;
      _nameMap_[overloader->name()] = overloader;
      _nameMap_.insert(overloader->safeName(), overloader);
      _referenceSlots_.insert(overloader);
      // Removing overloaded PRMReferenceSlot<GUM_SCALAR>
      _referenceSlots_.erase(overloaded);
      _nameMap_.erase(overloaded->safeName());
      delete overloaded;
    }

    template < GUM_Numeric GUM_SCALAR >
    void PRMInterface< GUM_SCALAR >::_addCastDescendants_(PRMAttribute< GUM_SCALAR >* start,
                                                          PRMAttribute< GUM_SCALAR >* end) {
      PRMAttribute< GUM_SCALAR >* parent = start;
      PRMAttribute< GUM_SCALAR >* child  = nullptr;

      while (parent->type().superType() != end->type()) {
        child = parent->getCastDescendant();
        child->setId(nextNodeId());
        _dag_.addNodeWithId(child->id());
        _nodeIdMap_.insert(child->id(), child);
        // Only use child's safe name when adding to the name map!
        _nameMap_.insert(child->safeName(), child);
        _attributes_.insert(child);
        // Do ! use Class<GUM_SCALAR>::insertArc(), child's CPF is already
        // initialized properly
        parent = child;
      }

      parent->setAsCastDescendant(end);
    }

    template < GUM_Numeric GUM_SCALAR >
    bool PRMInterface< GUM_SCALAR >::_checkOverloadLegality_(
        const PRMClassElement< GUM_SCALAR >* overloaded,
        const PRMClassElement< GUM_SCALAR >* overloader) {
      if (overloaded->elt_type() != overloader->elt_type()) { return false; }

      if (overloaded->elt_type() == PRMClassElement< GUM_SCALAR >::prm_attribute) {
        if (!overloader->type().isSubTypeOf(overloaded->type())) { return false; }
      } else if (overloaded->elt_type() == PRMClassElement< GUM_SCALAR >::prm_refslot) {
        auto ref_overloader = static_cast< const PRMReferenceSlot< GUM_SCALAR >* >(overloader);
        auto ref_overloaded = static_cast< const PRMReferenceSlot< GUM_SCALAR >* >(overloaded);
        if (!ref_overloader->slotType().isSubTypeOf(ref_overloaded->slotType())) { return false; }
      } else {
        return false;
      }
      return true;
    }

    template < GUM_Numeric GUM_SCALAR >
    bool PRMInterface< GUM_SCALAR >::isSubTypeOf(
        const PRMClassElementContainer< GUM_SCALAR >& cec) const {
      switch (cec.obj_type()) {
        case PRMClassElement< GUM_SCALAR >::prm_type::CLASS : {
          return false;
        }

        case PRMClassElement< GUM_SCALAR >::prm_type::PRM_INTERFACE : {
          const PRMInterface* current = this;

          while (current != 0) {
            if (current == &(cec)) return true;

            current = current->_superInterface_;
          }

          return false;
        }

        default : {
          GUM_ERROR(FatalError, "unknown ClassElementContainer<GUM_SCALAR>")
        }
      }
    }

    template < GUM_Numeric GUM_SCALAR >
    void PRMInterface< GUM_SCALAR >::updateDescendants_(const PRMClassElement< GUM_SCALAR >& elt) {
      // for ( const auto ext :  _extensions_ )
      //  if ( !ext->isOutputNode( elt ) ) ext->setOutputNode( elt, true );

      // for ( const auto impl :  _implementations_ ) {
      //  // Because of cyclic dependencies we must use a reinterpret cast.
      //  PRMClassElementContainer<GUM_SCALAR>* c =
      //      reinterpret_cast<PRMClassElementContainer<GUM_SCALAR>*>( impl );

      //  if ( ! c->isOutputNode( elt ) ) c->setOutputNode( elt, true );
      //}
    }

    template < GUM_Numeric GUM_SCALAR >
    typename PRMInterface< GUM_SCALAR >::ClassEltIterator PRMInterface< GUM_SCALAR >::begin() {
      return _nodeIdMap_.begin();
    }

    template < GUM_Numeric GUM_SCALAR >
    const typename PRMInterface< GUM_SCALAR >::ClassEltIterator& PRMInterface< GUM_SCALAR >::end() {
      return _nodeIdMap_.end();
    }

    template < GUM_Numeric GUM_SCALAR >
    typename PRMInterface< GUM_SCALAR >::const_ClassEltIterator
        PRMInterface< GUM_SCALAR >::begin() const {
      return _nodeIdMap_.begin();
    }

    template < GUM_Numeric GUM_SCALAR >
    const typename PRMInterface< GUM_SCALAR >::const_ClassEltIterator&
        PRMInterface< GUM_SCALAR >::end() const {
      return _nodeIdMap_.end();
    }

    template < GUM_Numeric GUM_SCALAR >
    void PRMInterface< GUM_SCALAR >::addArc(std::string_view tail, std::string_view head) {
      GUM_ERROR(OperationNotAllowed, "an Interface does ! have arcs")
    }

    template < GUM_Numeric GUM_SCALAR >
    PRMInterface< GUM_SCALAR >& PRMInterface< GUM_SCALAR >::super() {
      if (_superInterface_) return *_superInterface_;
      else GUM_ERROR(NotFound, "this Interface is ! a sub interface")
    }

    template < GUM_Numeric GUM_SCALAR >
    const PRMInterface< GUM_SCALAR >& PRMInterface< GUM_SCALAR >::super() const {
      if (_superInterface_) return *_superInterface_;
      else GUM_ERROR(NotFound, "this Interface is ! a sub interface")
    }

    template < GUM_Numeric GUM_SCALAR >
    void PRMInterface< GUM_SCALAR >::_addImplementation_(PRMClass< GUM_SCALAR >* c) {
      _implementations_.insert(c);
    }

    template < GUM_Numeric GUM_SCALAR >
    void PRMInterface< GUM_SCALAR >::_addExtension_(PRMInterface< GUM_SCALAR >* i) {
      _extensions_.insert(i);
    }

    template < GUM_Numeric GUM_SCALAR >
    PRMClassElement< GUM_SCALAR >& PRMInterface< GUM_SCALAR >::operator[](NodeId id) {
      return get(id);
    }

    template < GUM_Numeric GUM_SCALAR >
    const PRMClassElement< GUM_SCALAR >& PRMInterface< GUM_SCALAR >::operator[](NodeId id) const {
      return get(id);
    }

    template < GUM_Numeric GUM_SCALAR >
    PRMClassElement< GUM_SCALAR >& PRMInterface< GUM_SCALAR >::operator[](std::string_view name) {
      return get(name);
    }

    template < GUM_Numeric GUM_SCALAR >
    const PRMClassElement< GUM_SCALAR >&
        PRMInterface< GUM_SCALAR >::operator[](std::string_view name) const {
      return get(name);
    }

    template < GUM_Numeric GUM_SCALAR >
    typename PRMObject::prm_type PRMInterface< GUM_SCALAR >::obj_type() const {
      return PRMObject::prm_type::PRM_INTERFACE;
    }

    template < GUM_Numeric GUM_SCALAR >
    const DAG& PRMInterface< GUM_SCALAR >::dag_() const {
      return _dag_;
    }

    template < GUM_Numeric GUM_SCALAR >
    DAG& PRMInterface< GUM_SCALAR >::dag_() {
      return _dag_;
    }

    template < GUM_Numeric GUM_SCALAR >
    PRMClassElement< GUM_SCALAR >& PRMInterface< GUM_SCALAR >::get(NodeId id) {
      auto p = _nodeIdMap_.tryGet(id);
      if (!p) { GUM_ERROR(NotFound, "no ClassElement<GUM_SCALAR> with the given NodeId") }
      return **p;
    }

    template < GUM_Numeric GUM_SCALAR >
    const PRMClassElement< GUM_SCALAR >& PRMInterface< GUM_SCALAR >::get(NodeId id) const {
      auto p = _nodeIdMap_.tryGet(id);
      if (!p) { GUM_ERROR(NotFound, "no ClassElement<GUM_SCALAR> with the given NodeId") }
      return **p;
    }

    template < GUM_Numeric GUM_SCALAR >
    PRMClassElement< GUM_SCALAR >& PRMInterface< GUM_SCALAR >::get(std::string_view name) {
      auto p = _nameMap_.tryGet(name);
      if (!p) { GUM_ERROR(NotFound, "no ClassElement<GUM_SCALAR> with the given name") }
      return **p;
    }

    template < GUM_Numeric GUM_SCALAR >
    const PRMClassElement< GUM_SCALAR >&
        PRMInterface< GUM_SCALAR >::get(std::string_view name) const {
      auto p = _nameMap_.tryGet(name);
      if (!p) { GUM_ERROR(NotFound, "no ClassElement<GUM_SCALAR> with the given name") }
      return **p;
    }

    template < GUM_Numeric GUM_SCALAR >
    const Set< PRMAttribute< GUM_SCALAR >* >& PRMInterface< GUM_SCALAR >::attributes() const {
      return _attributes_;
    }

    template < GUM_Numeric GUM_SCALAR >
    const Set< PRMReferenceSlot< GUM_SCALAR >* >&
        PRMInterface< GUM_SCALAR >::referenceSlots() const {
      return _referenceSlots_;
    }

    template < GUM_Numeric GUM_SCALAR >
    Set< PRMClass< GUM_SCALAR >* >& PRMInterface< GUM_SCALAR >::implementations() {
      return _implementations_;
    }

    template < GUM_Numeric GUM_SCALAR >
    const Set< PRMClass< GUM_SCALAR >* >& PRMInterface< GUM_SCALAR >::implementations() const {
      return _implementations_;
    }

    template < GUM_Numeric GUM_SCALAR >
    void PRMInterface< GUM_SCALAR >::findAllSubtypes_(
        Set< PRMClassElementContainer< GUM_SCALAR >* >& set) {
      for (const auto impl: _implementations_) {
        set.insert(impl);
        impl->findAllSubtypes_(set);
      }

      for (const auto ext: _extensions_) {
        set.insert(ext);
        ext->findAllSubtypes_(set);
      }
    }

    template < GUM_Numeric GUM_SCALAR >
    bool PRMInterface< GUM_SCALAR >::isOutputNode(const PRMClassElement< GUM_SCALAR >& elt) const {
      try {
        if (!this->getIOFlag_(elt).second) {
          for (auto i: _implementations_) {
            if (i->isOutputNode(elt)) { return true; }
          }

          if (_superInterface_ && _superInterface_->isOutputNode(elt)) { return true; }

        } else {
          return true;
        }
      } catch (NotFound const&) {}
      return false;
    }
  } /* namespace prm */
} /* namespace gum */
