/****************************************************************************
 *   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 Template implementation of FMDP/FMDP.h classes.
 *
 * @author Pierre-Henri WUILLEMIN(_at_LIP6) and Jean-Christophe MAGNAN and Christophe
 * GONZALES(_at_AMU)
 */

//======================================================================
#include <cstdio>
#include <iostream>
//======================================================================
#include <agrum/FMDP/fmdp.h>

//======================================================================

namespace gum {


  /* **************************************************************************************************
   * **/
  /* ** **/
  /* **                               Constructors / Destructors **/
  /* ** **/
  /* **************************************************************************************************
   * **/

  // ===========================================================================
  // Default constructor.
  // ===========================================================================
  template < typename GUM_ELEMENT >
  FMDP< GUM_ELEMENT >::FMDP(bool onDestructionDeleteVar) {
    GUM_CONSTRUCTOR(FMDP);
    _onDestructionDeleteVars_ = onDestructionDeleteVar;

    // Default Action initialisation
    _actionMap_.insert(0, new std::string("DEFAULT"));
    _actionTransitionTable_.insert(0, new VarTransitionTable< GUM_ELEMENT >());
    _actionCostTable_.insert(0, nullptr);
    _actionRewardTable_.insert(0, nullptr);
  }

  // ===========================================================================
  // Destructor.
  // ===========================================================================
  template < typename GUM_ELEMENT >
  FMDP< GUM_ELEMENT >::~FMDP() {
    // Action Transition Graph deletion
    for (auto iterA = _actionTransitionTable_.beginSafe();
         iterA != _actionTransitionTable_.endSafe();
         ++iterA) {
      if (iterA.val()) {
        for (auto iterH = (iterA.val())->beginSafe(); iterH != (iterA.val())->endSafe(); ++iterH)
          if (iterH.val()) delete iterH.val();
        delete iterA.val();
      }
    }

    // Action cost graph deletion
    for (auto iterA = _actionCostTable_.beginSafe(); iterA != _actionCostTable_.endSafe(); ++iterA)
      if (iterA.val()) delete iterA.val();

    // Action reward graph deletion
    for (auto iterA = _actionRewardTable_.beginSafe(); iterA != _actionRewardTable_.endSafe();
         ++iterA)
      if (iterA.val()) delete iterA.val();

    // Action Name deletion
    for (auto iterId = _actionMap_.beginSafe(); iterId != _actionMap_.endSafe(); ++iterId)
      delete iterId.second();

    // Primed Variables deletion
    for (auto varIter = _main2primed_.beginSafe(); varIter != _main2primed_.endSafe(); ++varIter) {
      delete varIter.second();
      if (_onDestructionDeleteVars_) delete varIter.first();
    }

    GUM_DESTRUCTOR(FMDP);
  }

  /* **************************************************************************************************
   * **/
  /* ** **/
  /* **                               Variable Handling Methods. **/
  /* ** **/
  /* **************************************************************************************************
   * **/

  // ===========================================================================
  // Adds a variable to FMDP description
  // @throw DuplicateElement if a similar variable already exists
  // ===========================================================================
  template < typename GUM_ELEMENT >
  void FMDP< GUM_ELEMENT >::addVariable(const DiscreteVariable* var) {
    if (_varSeq_.exists(var))
      GUM_ERROR(DuplicateElement,
                " Variable " << var->name() << " has already been inserted in FMDP.");


    _varSeq_.insert(var);

    // Prime version creation
    DiscreteVariable* primeVar = var->clone();
    primeVar->setName(var->name() + "'");
    _main2primed_.insert(var, primeVar);
  }

  /* **************************************************************************************************
   * **/
  /* ** **/
  /* **                               Variable Handling Methods. **/
  /* ** **/
  /* **************************************************************************************************
   * **/

  // ===========================================================================
  // Adds an action to FMDP description
  // @throw DuplicateElement if an action with same name already exists
  // ===========================================================================
  template < typename GUM_ELEMENT >
  void FMDP< GUM_ELEMENT >::addAction(Idx actionId, std::string_view action) {
    if (actionId == 0) GUM_ERROR(DuplicateElement, " Action Id 0 is reserved.")

    for (BijectionIteratorSafe< Idx, const std::string* > actIter = _actionMap_.beginSafe();
         actIter != _actionMap_.endSafe();
         ++actIter)
      if (*(actIter.second()) == action)
        GUM_ERROR(DuplicateElement,
                  " Action " << action << " has already been inserted in FMDP with this name.");

    if (_actionMap_.existsFirst(actionId))
      GUM_ERROR(DuplicateElement,
                " An action with same id (" << actionId << ") has already been inserted.");

    _actionMap_.insert(actionId, new std::string(action));

    _actionTransitionTable_.insert(actionId, new VarTransitionTable< GUM_ELEMENT >());
    _actionCostTable_.insert(actionId, nullptr);
    _actionRewardTable_.insert(actionId, nullptr);

    _actionSeq_.insert(actionId);
  }

  /* **************************************************************************************************
   * **/
  /* ** **/
  /* **                               Transition methods. **/
  /* ** **/
  /* **************************************************************************************************
   * **/

  // ===========================================================================
  // Adds a variable transition table to specified action
  // @throw NotFound if action or var does not exists
  // @throw DuplicateElement if variable already has a transition for this
  // action
  // ===========================================================================
  template < typename GUM_ELEMENT >
  void FMDP< GUM_ELEMENT >::addTransitionForAction(
      Idx                                          actionId,
      const DiscreteVariable*                      var,
      const MultiDimImplementation< GUM_ELEMENT >* transition) {
    if (!_varSeq_.exists(var))
      GUM_ERROR(NotFound, " Variable " << var->name() << " has not been declared before.")

    if (!_actionTransitionTable_.exists(actionId))
      GUM_ERROR(NotFound, " Action " << actionName(actionId) << " has not been declared before.");

    if (_actionTransitionTable_[actionId]->exists(var))
      GUM_ERROR(DuplicateElement,
                " Variable " << var->name() << " already has a transition table in " << actionId
                             << " table.");

    _actionTransitionTable_[actionId]->insert(var, transition);
  }

  // ===========================================================================
  // Returns transition associated to given in parameter variable and given
  // action
  // ===========================================================================
  template < typename GUM_ELEMENT >
  const MultiDimImplementation< GUM_ELEMENT >*
      FMDP< GUM_ELEMENT >::transition(Idx actionId, const DiscreteVariable* v) const {
    if (!_actionTransitionTable_.exists(actionId))
      GUM_ERROR(NotFound, " Action " << actionName(actionId) << " has not been declared before.");

    if (_actionTransitionTable_[actionId]->exists(v))
      return (*_actionTransitionTable_[actionId])[v];
    else
      return (*_actionTransitionTable_[0]).exists(v) ? (*_actionTransitionTable_[0])[v] : nullptr;
  }

  /* **************************************************************************************************
   * **/
  /* ** **/
  /* **                               Cost methods. **/
  /* ** **/
  /* **************************************************************************************************
   * **/

  // ===========================================================================
  // Adds a cost table to specified action
  // @throw NotFound if action does not exists
  // @throw DuplicateElement if action already has a cost
  // ===========================================================================
  template < typename GUM_ELEMENT >
  void FMDP< GUM_ELEMENT >::addCostForAction(Idx                                          actionId,
                                             const MultiDimImplementation< GUM_ELEMENT >* cost) {
    if (!_actionCostTable_.exists(actionId))
      GUM_ERROR(NotFound, " Action " << actionName(actionId) << " has not been declared before.");

    if (_actionCostTable_[actionId] != nullptr)
      GUM_ERROR(DuplicateElement, " Action " << actionName(actionId) << " already has a cost table")

    _actionCostTable_[actionId] = cost;
  }

  // ===========================================================================
  // Returns transition associated to given in parameter variable and given
  // action
  // ===========================================================================
  template < typename GUM_ELEMENT >
  const MultiDimImplementation< GUM_ELEMENT >* FMDP< GUM_ELEMENT >::cost(Idx actionId) const {
    if (!_actionCostTable_.exists(actionId))
      GUM_ERROR(NotFound, " Action " << actionName(actionId) << " has not been declared before.");

    if (_actionCostTable_[actionId]) return _actionCostTable_[actionId];
    return _actionCostTable_[0];
  }

  /* **************************************************************************************************
   * **/
  /* ** **/
  /* **                               Cost methods. **/
  /* ** **/
  /* **************************************************************************************************
   * **/

  // ===========================================================================
  // Adds a default variable reward
  // @throw DuplicateElement if a default reward exists already
  // ===========================================================================
  template < typename GUM_ELEMENT >
  void
      FMDP< GUM_ELEMENT >::addRewardForAction(Idx                                          actionId,
                                              const MultiDimImplementation< GUM_ELEMENT >* reward) {
    if (!_actionRewardTable_.exists(actionId))
      GUM_ERROR(NotFound, " Action " << actionName(actionId) << " has not been declared before.");

    if (_actionRewardTable_[actionId] != nullptr)
      GUM_ERROR(DuplicateElement,
                " Action " << actionName(actionId) << " already has a reward table");

    _actionRewardTable_[actionId] = reward;
  }

  // ===========================================================================
  // Returns transition associated to given in parameter variable and given
  // action
  // ===========================================================================
  template < typename GUM_ELEMENT >
  const MultiDimImplementation< GUM_ELEMENT >* FMDP< GUM_ELEMENT >::reward(Idx actionId) const {
    if (!_actionRewardTable_.exists(actionId))
      GUM_ERROR(NotFound, " Action " << actionName(actionId) << " has not been declared before.");

    if (_actionRewardTable_[actionId]) return _actionRewardTable_[actionId];
    return _actionRewardTable_[0];
  }

  /* **************************************************************************************************
   * **/
  /* ** **/
  /* **                               Miscelleanous methods. **/
  /* ** **/
  /* **************************************************************************************************
   * **/

  // ===========================================================================
  // Returns name of action given in parameter
  // ===========================================================================
  template < typename GUM_ELEMENT >
  const std::string& FMDP< GUM_ELEMENT >::actionName(Idx actionId) const {
    if (!_actionMap_.existsFirst(actionId))
      GUM_ERROR(NotFound, "No action with " << actionId << " as identifiant.")

    return *(_actionMap_.second(actionId));
  }

  // ===========================================================================
  // Returns action id
  // ===========================================================================
  template < typename GUM_ELEMENT >
  Idx FMDP< GUM_ELEMENT >::actionId(std::string_view action) const {
    for (BijectionIterator< Idx, const std::string* > actIter = _actionMap_.begin();
         actIter != _actionMap_.end();
         ++actIter)
      if (*(actIter.second()) == action) { return actIter.first(); }

    GUM_ERROR(NotFound, " Action " << action << " has not been declared before.")
  }

  template < typename GUM_ELEMENT >
  std::string FMDP< GUM_ELEMENT >::toString() const {
    std::string fmdpCore;

    for (auto actionIter = beginActions(); actionIter != endActions(); ++actionIter) {
      for (auto varIter = beginVariables(); varIter != endVariables(); ++varIter)
        if (this->transition(*actionIter, *varIter))
          fmdpCore += static_cast< const MultiDimFunctionGraph< GUM_ELEMENT >* >(
                          this->transition(*actionIter, *varIter))
                          ->toDot()
                    + '\n';
      if (this->reward(*actionIter))
        fmdpCore
            += static_cast< const MultiDimFunctionGraph< GUM_ELEMENT >* >(this->reward(*actionIter))
                   ->toDot()
             + '\n';
    }

    for (auto varIter = beginVariables(); varIter != endVariables(); ++varIter)
      if (this->transition(0, *varIter))
        fmdpCore += static_cast< const MultiDimFunctionGraph< GUM_ELEMENT >* >(
                        this->transition(0, *varIter))
                        ->toDot()
                  + '\n';
    if (this->reward())
      fmdpCore
          += static_cast< const MultiDimFunctionGraph< GUM_ELEMENT >* >(this->reward())->toDot()
           + '\n';
    return fmdpCore;
  }

  template < typename GUM_ELEMENT >
  Size FMDP< GUM_ELEMENT >::size() const {
    Size s = 0;
    for (auto actionIter = beginActions(); actionIter != endActions(); ++actionIter) {
      for (auto varIter = beginVariables(); varIter != endVariables(); ++varIter)
        if (this->transition(*actionIter, *varIter))
          s += this->transition(*actionIter, *varIter)->realSize();
      if (this->reward(*actionIter)) s += this->reward(*actionIter)->realSize();
    }

    for (auto varIter = beginVariables(); varIter != endVariables(); ++varIter)
      if (this->transition(0, *varIter)) s += this->transition(0, *varIter)->realSize();
    if (this->reward()) s += this->reward()->realSize();
    return s;
  }

  template < typename GUM_ELEMENT >
  SequenceIteratorSafe< const DiscreteVariable* > FMDP< GUM_ELEMENT >::beginVariables() const {
    return _varSeq_.beginSafe();
  }

  template < typename GUM_ELEMENT >
  SequenceIteratorSafe< const DiscreteVariable* > FMDP< GUM_ELEMENT >::endVariables() const {
    return _varSeq_.endSafe();
  }

  template < typename GUM_ELEMENT >
  const DiscreteVariable* FMDP< GUM_ELEMENT >::main2prime(const DiscreteVariable* mainVar) const {
    return _main2primed_.second(mainVar);
  }

  template < typename GUM_ELEMENT >
  const Bijection< const DiscreteVariable*, const DiscreteVariable* >&
      FMDP< GUM_ELEMENT >::mapMainPrime() const {
    return _main2primed_;
  }

  template < typename GUM_ELEMENT >
  SequenceIteratorSafe< Idx > FMDP< GUM_ELEMENT >::beginActions() const {
    return _actionSeq_.beginSafe();
  }

  template < typename GUM_ELEMENT >
  SequenceIteratorSafe< Idx > FMDP< GUM_ELEMENT >::endActions() const {
    return _actionSeq_.endSafe();
  }

  template < typename GUM_ELEMENT >
  void FMDP< GUM_ELEMENT >::addTransition(const DiscreteVariable*                      var,
                                          const MultiDimImplementation< GUM_ELEMENT >* transition) {
    this->addTransitionForAction(0, var, transition);
  }

  template < typename GUM_ELEMENT >
  void FMDP< GUM_ELEMENT >::addCost(const MultiDimImplementation< GUM_ELEMENT >* cost) {
    this->addCostForAction(0, cost);
  }

  template < typename GUM_ELEMENT >
  void FMDP< GUM_ELEMENT >::addReward(const MultiDimImplementation< GUM_ELEMENT >* reward) {
    this->addRewardForAction(0, reward);
  }

  template < typename GUM_ELEMENT >
  void FMDP< GUM_ELEMENT >::setDeleteVariablesOnDestruction(bool b) {
    _onDestructionDeleteVars_ = b;
  }

  template < typename GUM_ELEMENT >
  void FMDP< GUM_ELEMENT >::setDiscount(GUM_ELEMENT discount) {
    _discount_ = discount;
  }

  template < typename GUM_ELEMENT >
  GUM_ELEMENT FMDP< GUM_ELEMENT >::discount() const {
    return _discount_;
  }

}   // namespace gum
