/****************************************************************************
 *   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 BN/MarkovRandomField.h class.
 *
 * @author Pierre-Henri WUILLEMIN(_at_LIP6) & Christophe GONZALES(_at_AMU)
 */

#include <algorithm>
#include <limits>
#include <set>

#include <agrum/base/multidim/aggregators/amplitude.h>
#include <agrum/base/multidim/aggregators/and.h>
#include <agrum/base/multidim/aggregators/count.h>
#include <agrum/base/multidim/aggregators/exists.h>
#include <agrum/base/multidim/aggregators/forall.h>
#include <agrum/base/multidim/aggregators/max.h>
#include <agrum/base/multidim/aggregators/median.h>
#include <agrum/base/multidim/aggregators/min.h>
#include <agrum/base/multidim/aggregators/or.h>
#include <agrum/base/multidim/ICIModels/multiDimLogit.h>
#include <agrum/base/multidim/ICIModels/multiDimNoisyAND.h>
#include <agrum/base/multidim/ICIModels/multiDimNoisyORCompound.h>
#include <agrum/base/multidim/ICIModels/multiDimNoisyORNet.h>
#include <agrum/base/variables/allDiscreteVariables.h>
#include <agrum/BN/generator/simpleCPTGenerator.h>
#include <agrum/MRF/MarkovRandomField.h>

#include <agrum/base/core/utils_string.h>

namespace gum {
  template < GUM_Numeric GUM_SCALAR >
  NodeId build_node_for_MN(MarkovRandomField< GUM_SCALAR >& mn,
                           std::string_view                 node,
                           std::string_view                 default_domain) {
    auto v = fastVariable< GUM_SCALAR >(std::string{node}, default_domain);

    NodeId res;
    if (mn.exists(v->name())) res = mn.idFromName(v->name());
    else res = mn.add(*v);
    return res;
  }

  template < GUM_Numeric GUM_SCALAR >
  MarkovRandomField< GUM_SCALAR >
      MarkovRandomField< GUM_SCALAR >::fastPrototype(std::string_view dotlike, Size domainSize) {
    return fastPrototype(dotlike, "[" + std::to_string(domainSize) + "]");
  }

  template < GUM_Numeric GUM_SCALAR >
  MarkovRandomField< GUM_SCALAR >
      MarkovRandomField< GUM_SCALAR >::fastPrototype(std::string_view dotlike,
                                                     std::string_view domain) {
    MarkovRandomField< GUM_SCALAR > mn;


    for (const auto& clikchain: split(remove_newline(dotlike), ";")) {
      NodeSet cliq;
      for (auto& node: split(clikchain, "--")) {
        auto idVar = build_node_for_MN(mn, node, domain);
        cliq.insert(idVar);
      }
      mn.addFactor(cliq);
    }
    mn.generateFactors();
    mn.setProperty("name", "anonymousMRF");
    return mn;
  }

  template < GUM_Numeric GUM_SCALAR >
  MarkovRandomField< GUM_SCALAR >
      MarkovRandomField< GUM_SCALAR >::fromBN(const BayesNet< GUM_SCALAR >& bn) {
    MarkovRandomField< GUM_SCALAR > mn;
    for (NodeId nod: bn.nodes()) {
      mn.add(bn.variable(nod), nod);
    }
    mn.beginTopologyTransformation();
    for (NodeId nod: bn.nodes()) {
      mn.addFactor(bn.cpt(nod));
    }
    mn.endTopologyTransformation();
    mn.setProperty("name", bn.propertyWithDefault("name", "noname"));
    return mn;
  }

  template < GUM_Numeric GUM_SCALAR >
  MarkovRandomField< GUM_SCALAR >::MarkovRandomField() :
      IMarkovRandomField< GUM_SCALAR >(), _topologyTransformationInProgress_(false) {
    GUM_CONSTRUCTOR(MarkovRandomField);
  }

  template < GUM_Numeric GUM_SCALAR >
  MarkovRandomField< GUM_SCALAR >::MarkovRandomField(std::string_view name) :
      IMarkovRandomField< GUM_SCALAR >(name), _topologyTransformationInProgress_(false) {
    GUM_CONSTRUCTOR(MarkovRandomField);
  }

  template < GUM_Numeric GUM_SCALAR >
  MarkovRandomField< GUM_SCALAR >::MarkovRandomField(
      const MarkovRandomField< GUM_SCALAR >& source) :
      IMarkovRandomField< GUM_SCALAR >(source), _topologyTransformationInProgress_(false) {
    GUM_CONS_CPY(MarkovRandomField);
    _copyFactors_(source);
  }

  template < GUM_Numeric GUM_SCALAR >
  MarkovRandomField< GUM_SCALAR >&
      MarkovRandomField< GUM_SCALAR >::operator=(const MarkovRandomField< GUM_SCALAR >& source) {
    if (this != &source) {
      IMarkovRandomField< GUM_SCALAR >::operator=(source);
      _topologyTransformationInProgress_ = false;
      _copyFactors_(source);
    }

    return *this;
  }

  template < GUM_Numeric GUM_SCALAR >
  MarkovRandomField< GUM_SCALAR >& MarkovRandomField< GUM_SCALAR >::operator=(
      MarkovRandomField< GUM_SCALAR >&& source) noexcept {
    if (this != &source) {
      _clearFactors_();
      UGmodel::operator=(std::move(source));
      _topologyTransformationInProgress_ = false;
      _factors_                          = std::move(source._factors_);
      source._rebuildGraph_();
      GUM_OP_MOV(MarkovRandomField);
    }
    return *this;
  }

  template < GUM_Numeric GUM_SCALAR >
  MarkovRandomField< GUM_SCALAR >::~MarkovRandomField() {
    _clearFactors_();
    GUM_DESTRUCTOR(MarkovRandomField);
  }

  template < GUM_Numeric GUM_SCALAR >
  const DiscreteVariable& MarkovRandomField< GUM_SCALAR >::variable(std::string_view name) const {
    return variable(idFromName(name));
  }

  template < GUM_Numeric GUM_SCALAR >
  void MarkovRandomField< GUM_SCALAR >::changeVariableName(NodeId id, std::string_view new_name) {
    this->varMap_.changeName(id, new_name);
  }

  template < GUM_Numeric GUM_SCALAR >
  void MarkovRandomField< GUM_SCALAR >::changeVariableName(std::string_view name,
                                                           std::string_view new_name) {
    changeVariableName(idFromName(name), new_name);
  }

  template < GUM_Numeric GUM_SCALAR >
  void MarkovRandomField< GUM_SCALAR >::changeVariableLabel(std::string_view name,
                                                            std::string_view old_label,
                                                            std::string_view new_label) {
    changeVariableLabel(idFromName(name), old_label, new_label);
  }

  template < GUM_Numeric GUM_SCALAR >
  void MarkovRandomField< GUM_SCALAR >::changeVariableLabel(NodeId           id,
                                                            std::string_view old_label,
                                                            std::string_view new_label) {
    if (variable(id).varType() != VarType::LABELIZED) {
      GUM_ERROR(NotFound, "Variable " << id << " is not a LabelizedVariable.")
    }
    auto* var = dynamic_cast< LabelizedVariable* >(const_cast< DiscreteVariable* >(&variable(id)));
    if (var == nullptr) GUM_ERROR(TypeError, "Variable " << id << " is not a LabelizedVariable.")

    var->changeLabel(var->posLabel(old_label), new_label);
  }

  template < GUM_Numeric GUM_SCALAR >
  const Tensor< GUM_SCALAR >& MarkovRandomField< GUM_SCALAR >::factor(const NodeSet& varIds) const {
    return *_factors_[varIds];
  }

  template < GUM_Numeric GUM_SCALAR >
  const NodeSet& MarkovRandomField< GUM_SCALAR >::smallestFactorFromNode(NodeId node) const {
    const NodeSet* res      = nullptr;
    Size           smallest = size() + 1;
    for (const auto& kv: factors()) {
      const auto& fact = kv.first;
      if (fact.contains(node))
        if (smallest > fact.size()) {
          res      = &fact;
          smallest = fact.size();
        }
    }
    if (res == nullptr) {
      GUM_ERROR(NotFound, "No factor containing node " << node)
    } else {
      return *res;
    }
  }

  template < GUM_Numeric GUM_SCALAR >
  const Tensor< GUM_SCALAR >&
      MarkovRandomField< GUM_SCALAR >::factor(const std::vector< std::string >& varnames) const {
    return factor(this->nodeset(varnames));
  }

  template < GUM_Numeric GUM_SCALAR >
  const FactorTable< GUM_SCALAR >& MarkovRandomField< GUM_SCALAR >::factors() const {
    return _factors_;
  }

  template < GUM_Numeric GUM_SCALAR >
  NodeId MarkovRandomField< GUM_SCALAR >::add(std::string_view fast_description,
                                              unsigned int     default_nbrmod) {
    auto v = fastVariable< GUM_SCALAR >(std::string{fast_description}, default_nbrmod);
    if (v->domainSize() < 2) GUM_ERROR(OperationNotAllowed, v->name() << " has a domain size <2")
    return add(*v);
  }

  template < GUM_Numeric GUM_SCALAR >
  void MarkovRandomField< GUM_SCALAR >::_rebuildGraph_() {
    if (_topologyTransformationInProgress_) return;

    this->graph_.clearEdges();

    for (const auto& kv: _factors_) {
      auto& c = *kv.second;
      for (Idx i = 0; i < c.nbrDim(); i++)
        for (Idx j = i + 1; j < c.nbrDim(); j++)
          this->graph_.addEdge(this->varMap_.get(c.variable(i)), this->varMap_.get(c.variable(j)));
    }
  }

  template < GUM_Numeric GUM_SCALAR >
  NodeId MarkovRandomField< GUM_SCALAR >::add(const DiscreteVariable& var) {
    return add(var, graph().nextNodeId());
  }

  template < GUM_Numeric GUM_SCALAR >
  NodeId MarkovRandomField< GUM_SCALAR >::add(const DiscreteVariable& var, NodeId id) {
    this->varMap_.insert(id, var);
    this->graph_.addNodeWithId(id);
    return id;
  }

  template < GUM_Numeric GUM_SCALAR >
  void MarkovRandomField< GUM_SCALAR >::erase(const DiscreteVariable& var) {
    erase(this->varMap_.get(var));
  }

  template < GUM_Numeric GUM_SCALAR >
  void MarkovRandomField< GUM_SCALAR >::erase(std::string_view name) {
    erase(idFromName(name));
  }

  template < GUM_Numeric GUM_SCALAR >
  void MarkovRandomField< GUM_SCALAR >::erase(NodeId varId) {
    if (!this->varMap_.exists(varId)) {
      GUM_ERROR(InvalidArgument, "No node with id " << varId << ".")
    }
    this->varMap_.erase(varId);
    this->graph_.eraseNode(varId);

    std::vector< NodeSet > vs;
    for (const auto& kv: _factors_) {
      if (kv.first.contains(varId)) { vs.push_back(kv.first); }
    }
    for (const auto& ns: vs) {
      _eraseFactor_(ns);
    }
    for (const auto& ns: vs) {
      NodeSet nv = ns;
      nv.erase(varId);
      if (nv.size() > 1) addFactor(nv);
    }
    _rebuildGraph_();
  }

  template < GUM_Numeric GUM_SCALAR >
  void MarkovRandomField< GUM_SCALAR >::clear() {
    if (!this->empty()) {
      auto l = this->nodes();
      for (const auto no: l) {
        this->erase(no);
      }
    }
    _rebuildGraph_();
  }

  template < GUM_Numeric GUM_SCALAR >
  std::ostream& operator<<(std::ostream& output, const MarkovRandomField< GUM_SCALAR >& mn) {
    output << mn.toString();
    return output;
  }

  template < GUM_Numeric GUM_SCALAR >
  Tensor< GUM_SCALAR >&
      MarkovRandomField< GUM_SCALAR >::_addFactor_(const std::vector< NodeId >& ordered_nodes) {
    NodeSet vars;
    for (auto node: ordered_nodes)
      vars.insert(node);

    if (vars.size() == 0) { GUM_ERROR(InvalidArgument, "Empty factor cannot be added.") }

    if (_factors_.exists(vars)) {
      GUM_ERROR(InvalidArgument, "A factor for (" << this->names(vars) << ") already exists.")
    }

    Tensor< GUM_SCALAR >* factor = new Tensor< GUM_SCALAR >();

    for (auto node: ordered_nodes) {
      factor->add(variable(node));
    }

    _factors_.insert(vars, factor);
    _rebuildGraph_();

    return *factor;
  }

  template < GUM_Numeric GUM_SCALAR >
  const Tensor< GUM_SCALAR >& MarkovRandomField< GUM_SCALAR >::addFactor(const NodeSet& vars) {
    // in order to be deterministic, the Tensor contains all the vars sorted by id.
    std::vector< NodeId > sorted_nodes;
    for (auto node: vars) {
      sorted_nodes.push_back(node);
    }
    std::sort(sorted_nodes.begin(), sorted_nodes.end());

    return _addFactor_(sorted_nodes);
  }

  template < GUM_Numeric GUM_SCALAR >
  const Tensor< GUM_SCALAR >&
      MarkovRandomField< GUM_SCALAR >::addFactor(const std::vector< std::string >& varnames) {
    std::vector< NodeId > sorted_nodes;
    for (const auto& v: varnames) {
      sorted_nodes.push_back(idFromName(v));
    }

    return _addFactor_(sorted_nodes);
  }

  template < GUM_Numeric GUM_SCALAR >
  const Tensor< GUM_SCALAR >&
      MarkovRandomField< GUM_SCALAR >::addFactor(const Tensor< GUM_SCALAR >& factor) {
    std::vector< NodeId > sorted_nodes;
    for (Idx i = 0; i < factor.nbrDim(); i++) {
      sorted_nodes.push_back(idFromName(factor.variable(i).name()));
    }
    auto& res = _addFactor_(sorted_nodes);
    res.fillWith(factor);

    return res;
  }

  template < GUM_Numeric GUM_SCALAR >
  void MarkovRandomField< GUM_SCALAR >::generateFactors() const {
    for (const auto& elt: _factors_) {
      elt.second->random();
    }
  }

  template < GUM_Numeric GUM_SCALAR >
  void MarkovRandomField< GUM_SCALAR >::generateFactor(const NodeSet& vars) const {
    _factors_[vars]->random();
  }

  template < GUM_Numeric GUM_SCALAR >
  void MarkovRandomField< GUM_SCALAR >::eraseFactor(const NodeSet& vars) {
    if (_factors_.exists(vars)) {
      _eraseFactor_(vars);
      _rebuildGraph_();
    } else {
      GUM_ERROR(InvalidArgument, "No factor for " << vars << ".")
    }
  }

  template < GUM_Numeric GUM_SCALAR >
  void MarkovRandomField< GUM_SCALAR >::eraseFactor(const std::vector< std::string >& varnames) {
    auto vars = this->nodeset(varnames);
    if (_factors_.exists(vars)) {
      _eraseFactor_(vars);
      _rebuildGraph_();
    } else {
      GUM_ERROR(InvalidArgument, "No factor for " << varnames << ".")
    }
  }

  template < GUM_Numeric GUM_SCALAR >
  void MarkovRandomField< GUM_SCALAR >::_eraseFactor_(const NodeSet& vars) {
    delete _factors_[vars];
    _factors_.erase(vars);
  }

  template < GUM_Numeric GUM_SCALAR >
  void MarkovRandomField< GUM_SCALAR >::_clearFactors_() {
    for (const auto& kv: _factors_) {
      delete kv.second;
    }
    _factors_.clear();
    _rebuildGraph_();
  }

  template < GUM_Numeric GUM_SCALAR >
  void MarkovRandomField< GUM_SCALAR >::_copyFactors_(
      const MarkovRandomField< GUM_SCALAR >& source) {
    _clearFactors_();
    for (const auto& pf: source.factors()) {
      addFactor(*pf.second);
    }
    _rebuildGraph_();
  }

  template < GUM_Numeric GUM_SCALAR >
  void MarkovRandomField< GUM_SCALAR >::beginTopologyTransformation() {
    _topologyTransformationInProgress_ = true;
  }

  template < GUM_Numeric GUM_SCALAR >
  void MarkovRandomField< GUM_SCALAR >::endTopologyTransformation() {
    if (_topologyTransformationInProgress_) {
      _topologyTransformationInProgress_ = false;   // before rebuildGraph of course
      _rebuildGraph_();
    }
  }
} /* namespace gum */
