/****************************************************************************
 *   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 bns/bayesNet.h classes.
 *
 * @author Pierre-Henri WUILLEMIN(_at_LIP6) and Lionel TORTI
 */

#include <limits>

#include <agrum/base/multidim/tensor.h>
#include <agrum/MRF/IMarkovRandomField.h>

namespace gum {

  // IMarkovRandomField

  template < GUM_Numeric GUM_SCALAR >
  IMarkovRandomField< GUM_SCALAR >::IMarkovRandomField() : UGmodel() {
    GUM_CONSTRUCTOR(IMarkovRandomField);
  }

  template < GUM_Numeric GUM_SCALAR >
  IMarkovRandomField< GUM_SCALAR >::IMarkovRandomField(std::string_view name) : UGmodel() {
    GUM_CONSTRUCTOR(IMarkovRandomField);
    this->setProperty("name", name);
  }

  template < GUM_Numeric GUM_SCALAR >
  IMarkovRandomField< GUM_SCALAR >::IMarkovRandomField(
      const IMarkovRandomField< GUM_SCALAR >& source) : UGmodel(source) {
    GUM_CONS_CPY(IMarkovRandomField);
  }

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

    return *this;
  }

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

  template < GUM_Numeric GUM_SCALAR >
  Size IMarkovRandomField< GUM_SCALAR >::dim() const {
    Size res = 0;
    for (auto f: factors()) {
      res += f.second->domainSize();
    }
    return res;
  }

  template < GUM_Numeric GUM_SCALAR >
  Size IMarkovRandomField< GUM_SCALAR >::maxVarDomainSize() const {
    Size res = 0;
    for (auto node: nodes()) {
      auto v = variable(node).domainSize();
      if (v > res) { res = v; }
    }
    return res;
  }

  template < GUM_Numeric GUM_SCALAR >
  GUM_SCALAR IMarkovRandomField< GUM_SCALAR >::minParam() const {
    GUM_SCALAR res = 1.0;
    for (auto elt: factors()) {
      auto v = elt.second->min();
      if (v < res) { res = v; }
    }
    return res;
  }

  template < GUM_Numeric GUM_SCALAR >
  GUM_SCALAR IMarkovRandomField< GUM_SCALAR >::maxParam() const {
    GUM_SCALAR res = 1.0;
    for (auto elt: factors()) {
      auto v = elt.second->max();
      if (v > res) { res = v; }
    }
    return res;
  }

  template < GUM_Numeric GUM_SCALAR >
  GUM_SCALAR IMarkovRandomField< GUM_SCALAR >::minNonZeroParam() const {
    GUM_SCALAR res = 1.0;
    for (auto elt: factors()) {
      auto v = elt.second->minNonZero();
      if (v < res) { res = v; }
    }
    return res;
  }

  template < GUM_Numeric GUM_SCALAR >
  GUM_SCALAR IMarkovRandomField< GUM_SCALAR >::maxNonOneParam() const {
    GUM_SCALAR res = 0.0;
    for (auto elt: factors()) {
      auto v = elt.second->maxNonOne();
      if (v > res) { res = v; }
    }
    return res;
  }

  template < GUM_Numeric GUM_SCALAR >
  std::string IMarkovRandomField< GUM_SCALAR >::toString() const {
    Size   param = 0;
    double dSize = log10DomainSize();

    for (auto factor: factors())
      param += factor.second->content()->realSize();

    std::stringstream s;
    s << std::format("MRF{{nodes: {}, edges: {}, ", size(), graph().sizeEdges());

    if (dSize > 6) s << std::format("domainSize: 10^{}", dSize);
    else s << std::format("domainSize: {}", std::round(std::pow(10.0, dSize)));

    s << std::format(", dim: {}}}", param);

    return s.str();
  }

  template < GUM_Numeric GUM_SCALAR >
  std::string IMarkovRandomField< GUM_SCALAR >::toDot() const {
    std::stringstream output;

    std::string mn_name = this->propertyWithDefault("name", "no_name");

    output << std::format("graph \"{}\" {{\n", mn_name);
    output << std::format("  graph [bgcolor=transparent,label=\"{}\"];\n", mn_name);
    output << "  node [style=filled fillcolor=\"#ffffaa\"];" << std::endl << std::endl;

    for (auto node: nodes())
      output << std::format("  \"{}\" [comment=\"{}:{}\"];\n",
                            variable(node).name(),
                            node,
                            variable(node).toStringWithDescription());

    output << std::endl;

    std::string tab = "  ";

    for (auto node: nodes()) {
      if (neighbours(node).size() > 0) {
        for (auto nei: neighbours(node)) {
          if (variable(node).name() < variable(nei).name()) {
            output << std::format("  \"{}\" -- \"{}\";\n",
                                  variable(node).name(),
                                  variable(nei).name());
          }
        }
      } else {
        output << std::format("  \"{}\";\n", variable(node).name());
      }
    }

    output << "}" << std::endl;

    return output.str();
  }

  template < GUM_Numeric GUM_SCALAR >
  std::string IMarkovRandomField< GUM_SCALAR >::toDotAsFactorGraph() const {
    std::stringstream output;
    std::string       mn_name = this->propertyWithDefault("name", "no_name");

    output << std::format("graph FG_{} {{\n", mn_name);
    output << "  layout=neato;" << std::endl;
    output << std::format("  graph [bgcolor=transparent,label=\"factor graph for {}\"];\n",
                          mn_name);

    // the variables
    output << "  node [shape=rectangle,margin=0.04,width=0,height=0, "
              "style=filled,color=\"coral\"];"
           << std::endl;
    for (auto nod: nodes()) {
      output << std::format("\"{}\";\n", variable(nod).name());
    }
    output << std::endl;

    // the factor
    output << "node[shape = point,width = 0.1,height = 0.1,style = filled,color = "
              "\"burlywood\"];"
           << std::endl;
    for (const auto& kv: factors()) {
      output << "  \"f";
      for (NodeId nod: kv.first) {
        output << "#" << variable(nod).name();
      }
      output << "\";" << std::endl;
    }

    // the link variable--factors
    output << "  edge[len = 0.7];" << std::endl;
    for (const auto& kv: factors()) {
      std::string clicname = "\"f";
      for (NodeId nod: kv.first) {
        clicname += "#";
        clicname += variable(nod).name();
      }
      clicname += "\"";

      for (NodeId nod: kv.first)
        output << std::format("  {} -- \"{}\";\n", clicname, variable(nod).name());
    }
    output << "}" << std::endl;

    return output.str();
  }

  template < GUM_Numeric GUM_SCALAR >
  bool IMarkovRandomField< GUM_SCALAR >::operator==(const IMarkovRandomField& from) const {
    if (size() != from.size()) { return false; }

    if (sizeEdges() != from.sizeEdges()) { return false; }

    // alignment of variables between the 2 BNs
    Bijection< const DiscreteVariable*, const DiscreteVariable* > alignment;

    for (auto node: nodes()) {
      const auto& v1 = variable(node);
      if (!from.exists(v1.name())) return false;
      const auto& v2 = from.variableFromName(v1.name());
      if (v1 != v2) return false;

      alignment.insert(&variable(node), &from.variableFromName(v1.name()));
    }

    for (const auto& elt: factors()) {
      const auto& key    = elt.first;
      const auto& factor = *elt.second;

      NodeSet fromkey;
      for (const auto n: key)
        fromkey.insert(from.idFromName(variable(n).name()));

      if (!from.factors().exists(fromkey)) { return false; }

      const auto& fromfactor = from.factor(fromkey);

      Instantiation i(factor);
      Instantiation j(fromfactor);
      for (i.setFirst(); !i.end(); i.inc()) {
        for (Idx indice = 0; indice < factor.nbrDim(); ++indice) {
          const DiscreteVariable* p = &(i.variable(indice));
          j.chgVal(*(alignment.second(p)), i.val(*p));
        }

        if (std::pow(factor.get(i) - fromfactor.get(j), (GUM_SCALAR)2) > (GUM_SCALAR)1e-6) {
          return false;
        }
      }
    }
    return true;
  }

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

  template < GUM_Numeric GUM_SCALAR >
  const NodeSet&
      IMarkovRandomField< GUM_SCALAR >::smallestFactorFromNode(std::string_view name) const {
    if (!this->exists(name)) {
      GUM_ERROR(NotFound, "No factor containing the variable <" << name << ">")
    }
    return smallestFactorFromNode(idFromName(name));
  }

  // visit the nodes and add some of node from soids in minimal
  template < GUM_Numeric GUM_SCALAR >
  void IMarkovRandomField< GUM_SCALAR >::_minimalCondSetVisit_(NodeId         node,
                                                               const NodeSet& soids,
                                                               NodeSet&       minimal,
                                                               NodeSet& alreadyVisited) const {
    if (alreadyVisited.contains(node)) return;
    alreadyVisited << node;

    if (soids.contains(node)) {
      minimal << node;
    } else {
      for (auto neig: graph_.neighbours(node))
        _minimalCondSetVisit_(neig, soids, minimal, alreadyVisited);
    }
  }

  template < GUM_Numeric GUM_SCALAR >
  NodeSet IMarkovRandomField< GUM_SCALAR >::minimalCondSet(NodeId         target,
                                                           const NodeSet& soids) const {
    if (soids.contains(target)) return NodeSet({target});

    NodeSet res;
    NodeSet alreadyVisited;
    alreadyVisited << target;

    for (auto neig: graph_.neighbours(target))
      _minimalCondSetVisit_(neig, soids, res, alreadyVisited);
    return res;
  }

  template < GUM_Numeric GUM_SCALAR >
  NodeSet IMarkovRandomField< GUM_SCALAR >::minimalCondSet(const NodeSet& targets,
                                                           const NodeSet& soids) const {
    NodeSet res;
    for (auto node: targets) {
      res += minimalCondSet(node, soids);
    }
    return res;
  }
} /* namespace gum */
