/****************************************************************************
 *   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 Implementation of the non pure virtual methods of class
 * ApproximateInference.
 *
 * @author Paul ALAM & Pierre-Henri WUILLEMIN(_at_LIP6)
 */

#include <agrum/BN/algorithms/barrenNodesFinder.h>
#include <agrum/BN/algorithms/dSeparationAlgorithm.h>
#include <agrum/BN/BayesNetFragment.h>
#include <agrum/BN/inference/tools/samplingInference.h>


#define DEFAULT_MAXITER          10000000
#define DEFAULT_PERIOD_SIZE      100
#define DEFAULT_VERBOSITY        false
#define DEFAULT_TIMEOUT          6000
#define DEFAULT_EPSILON          1e-2
#define DEFAULT_MIN_EPSILON_RATE 1e-5

namespace gum {

  template < GUM_Numeric GUM_SCALAR >
  SamplingInference< GUM_SCALAR >::SamplingInference(const IBayesNet< GUM_SCALAR >* bn) :
      ApproximateInference< GUM_SCALAR >(bn), _estimator_(), _samplingBN_(nullptr) {
    this->setEpsilon(DEFAULT_EPSILON);
    this->setMinEpsilonRate(DEFAULT_MIN_EPSILON_RATE);
    this->setMaxIter(DEFAULT_MAXITER);
    this->setVerbosity(DEFAULT_VERBOSITY);
    this->setPeriodSize(DEFAULT_PERIOD_SIZE);
    this->setMaxTime(DEFAULT_TIMEOUT);
    GUM_CONSTRUCTOR(SamplingInference);
  }

  template < GUM_Numeric GUM_SCALAR >
  SamplingInference< GUM_SCALAR >::~SamplingInference() {
    GUM_DESTRUCTOR(SamplingInference);
    if (_samplingBN_ != nullptr) {
      if (isContextualized) {   // otherwise  _samplingBN_==&BN()
        delete _samplingBN_;
      }
    }
  }

  template < GUM_Numeric GUM_SCALAR >
  const IBayesNet< GUM_SCALAR >& SamplingInference< GUM_SCALAR >::samplingBN() {
    this->prepareInference();
    if (_samplingBN_ == nullptr) return this->BN();
    else return *_samplingBN_;
  }

  template < GUM_Numeric GUM_SCALAR >
  void SamplingInference< GUM_SCALAR >::setEstimatorFromBN_() {
    _estimator_.setFromBN(&samplingBN(), this->hardEvidenceNodes());
    this->isSetEstimator = true;
  }

  template < GUM_Numeric GUM_SCALAR >
  void SamplingInference< GUM_SCALAR >::setEstimatorFromLBP_(
      LoopyBeliefPropagation< GUM_SCALAR >* lbp,
      GUM_SCALAR                            virtualLBPSize) {
    _estimator_.setFromLBP(lbp, this->hardEvidenceNodes(), virtualLBPSize);
    this->isSetEstimator = true;
  }

  template < GUM_Numeric GUM_SCALAR >
  const Tensor< GUM_SCALAR >& SamplingInference< GUM_SCALAR >::currentPosterior(NodeId id) {
    return _estimator_.posterior(this->BN().variable(id));
  }

  template < GUM_Numeric GUM_SCALAR >
  const Tensor< GUM_SCALAR >&
      SamplingInference< GUM_SCALAR >::currentPosterior(std::string_view name) {
    return currentPosterior(this->BN().idFromName(name));
  }

  template < GUM_Numeric GUM_SCALAR >
  const Tensor< GUM_SCALAR >& SamplingInference< GUM_SCALAR >::posterior_(NodeId id) {
    return _estimator_.posterior(this->BN().variable(id));
  }

  template < GUM_Numeric GUM_SCALAR >
  void SamplingInference< GUM_SCALAR >::contextualize() {
    // Finding Barren nodes

    BarrenNodesFinder barr_nodes = BarrenNodesFinder(&this->BN().internalDag());
    barr_nodes.setTargets(&this->targets());
    barr_nodes.setEvidence(&this->hardEvidenceNodes());
    const NodeSet& barren = barr_nodes.barrenNodes();

    // creating BN fragment
    _samplingBN_ = new BayesNetFragment< GUM_SCALAR >(this->BN());
    for (const auto elmt: this->BN().internalDag().asNodeSet() - barren)
      _samplingBN_->installNode(elmt);

    // D-separated nodes

    dSeparationAlgorithm dsep = gum::dSeparationAlgorithm();
    NodeSet              requisite;
    dsep.requisiteNodes(this->BN().internalDag(),
                        this->BN().nodes().asNodeSet(),   // no target for approximateInference
                        this->hardEvidenceNodes(),
                        this->softEvidenceNodes(),        // should be empty
                        requisite);
    requisite += this->hardEvidenceNodes();

    auto nonRequisite = this->BN().internalDag().asNodeSet() - requisite;

    for (const auto elmt: nonRequisite)
      _samplingBN_->uninstallNode(elmt);
    for (const auto hard: this->hardEvidenceNodes()) {
      gum::Instantiation I;
      I.add(this->BN().variable(hard));
      I.chgVal(this->BN().variable(hard), this->hardEvidence()[hard]);

      for (const auto& child: this->BN().children(hard)) {
        _samplingBN_->installCPT(child, this->BN().cpt(child).extract(I));
      }
    }

    this->isContextualized = true;
    this->onContextualize_(_samplingBN_);
  }

  template < GUM_Numeric GUM_SCALAR >
  void SamplingInference< GUM_SCALAR >::makeInference_() {
    if (!isSetEstimator) this->setEstimatorFromBN_();
    loopApproxInference_();
  }

  template < GUM_Numeric GUM_SCALAR >
  void SamplingInference< GUM_SCALAR >::loopApproxInference_() {
    //@todo This should be in  _prepareInference_
    if (!isContextualized) { this->contextualize(); }

    this->initApproximationScheme();
    gum::Instantiation Ip;
    GUM_SCALAR         w = .0;   //

    // Burn in
    Ip = this->burnIn_();
    do {
      Ip = this->draw_(&w, Ip);
      _estimator_.update(Ip, w);
      this->updateApproximationScheme();
    } while (this->continueApproximationScheme(_estimator_.confidence()));

    this->isSetEstimator = false;
  }

  template < GUM_Numeric GUM_SCALAR >
  void SamplingInference< GUM_SCALAR >::addVarSample_(NodeId nod, Instantiation* I) {
    gum::Instantiation Itop = gum::Instantiation(*I);

    I->add(samplingBN().variable(nod));
    I->chgVal(samplingBN().variable(nod), samplingBN().cpt(nod).extract(Itop).draw());
  }

  template < GUM_Numeric GUM_SCALAR >
  void SamplingInference< GUM_SCALAR >::onContextualize_(BayesNetFragment< GUM_SCALAR >* bn) {}

  template < GUM_Numeric GUM_SCALAR >
  void SamplingInference< GUM_SCALAR >::onEvidenceAdded_(const NodeId id, bool isHardEvidence) {
    if (!isHardEvidence) {
      GUM_ERROR(FatalError, "Approximated inference only accept hard evidence")
    }
  }

  template < GUM_Numeric GUM_SCALAR >
  void SamplingInference< GUM_SCALAR >::onEvidenceErased_(const NodeId id, bool isHardEvidence) {}

  template < GUM_Numeric GUM_SCALAR >
  void SamplingInference< GUM_SCALAR >::onAllEvidenceErased_(bool contains_hard_evidence) {}

  template < GUM_Numeric GUM_SCALAR >
  void SamplingInference< GUM_SCALAR >::onEvidenceChanged_(const NodeId id,
                                                           bool         hasChangedSoftHard) {
    if (hasChangedSoftHard) {
      GUM_ERROR(FatalError, "Approximated inference only accept hard evidence")
    }
  }

  template < GUM_Numeric GUM_SCALAR >
  void SamplingInference< GUM_SCALAR >::onModelChanged_(const GraphicalModel* bn) {}

  template < GUM_Numeric GUM_SCALAR >
  void SamplingInference< GUM_SCALAR >::updateOutdatedStructure_() {}

  template < GUM_Numeric GUM_SCALAR >
  void SamplingInference< GUM_SCALAR >::updateOutdatedTensors_() {}

  template < GUM_Numeric GUM_SCALAR >
  void SamplingInference< GUM_SCALAR >::onMarginalTargetAdded_(const NodeId id) {}

  template < GUM_Numeric GUM_SCALAR >
  void SamplingInference< GUM_SCALAR >::onMarginalTargetErased_(const NodeId id) {}

  template < GUM_Numeric GUM_SCALAR >
  void SamplingInference< GUM_SCALAR >::onAllMarginalTargetsAdded_() {}

  template < GUM_Numeric GUM_SCALAR >
  void SamplingInference< GUM_SCALAR >::onAllMarginalTargetsErased_() {}

  template < GUM_Numeric GUM_SCALAR >
  void SamplingInference< GUM_SCALAR >::onStateChanged_() {
    if (this->isInferenceReady()) {
      _estimator_.clear();
      this->initApproximationScheme();
    }
  }
}   // namespace gum
