/****************************************************************************
 *   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 Headers of the MultiDimWithOffset class.
 *
 * @author Pierre-Henri WUILLEMIN(_at_LIP6) & Christophe GONZALES(_at_AMU)
 */

#include <limits>

// to ease IDE parsers...
#include <agrum/base/multidim/implementations/multiDimImplementation.h>
#include <agrum/base/multidim/implementations/multiDimWithOffset.h>

namespace gum {

  // Default constructor: creates an empty null dimensional matrix

  template < typename GUM_ELEMENT >
  MultiDimWithOffset< GUM_ELEMENT >::MultiDimWithOffset() :
      MultiDimImplementation< GUM_ELEMENT >() {
    // for debugging purposes
    GUM_CONSTRUCTOR(MultiDimWithOffset);
  }

  // copy constructor

  template < typename GUM_ELEMENT >
  MultiDimWithOffset< GUM_ELEMENT >::MultiDimWithOffset(
      const MultiDimWithOffset< GUM_ELEMENT >& from) :
      MultiDimImplementation< GUM_ELEMENT >(from), gaps_(from.gaps_) {
    // for debugging purposes
    GUM_CONS_CPY(MultiDimWithOffset);
  }

  // destructor

  template < typename GUM_ELEMENT >
  MultiDimWithOffset< GUM_ELEMENT >::~MultiDimWithOffset() {
    // for debugging purposes
    GUM_DESTRUCTOR(MultiDimWithOffset);
    // no need to unregister all slaves as it will be done by
    // MultiDimImplementation
  }

  // add a new dimension, needed for updating the offsets_ & gaps_

  template < typename GUM_ELEMENT >
  void MultiDimWithOffset< GUM_ELEMENT >::add(const DiscreteVariable& v) {
    Size lg = this->domainSize();

    if (lg > std::numeric_limits< Idx >::max() / v.domainSize()) {
      GUM_ERROR(OutOfBounds, "Out of bounds !")
    }

    MultiDimImplementation< GUM_ELEMENT >::add(v);
    gaps_.insert(&v, lg);
  }

  // removes a dimension, needed for updating the offsets_ & gaps_

  template < typename GUM_ELEMENT >
  void MultiDimWithOffset< GUM_ELEMENT >::erase(const DiscreteVariable& v) {
    Sequence< const DiscreteVariable* > variables = this->variablesSequence();
    Idx                                 pos = variables.pos(&v);   // throw a NotFound if necessary

    if (variables.size() == 1) {
      gaps_.clear();
    } else {
      // update the gaps_
      Size v_size = v.domainSize();
      gaps_.erase(variables[pos]);

      for (Idx i = pos + 1; i < variables.size(); ++i) {
        gaps_[variables[i]] /= v_size;
      }
    }

    MultiDimImplementation< GUM_ELEMENT >::erase(v);
  }

  // listen to change in each recorded Instantiation.

  template < typename GUM_ELEMENT >
  void MultiDimWithOffset< GUM_ELEMENT >::changeNotification(const Instantiation&          i,
                                                             const DiscreteVariable* const var,
                                                             Idx                           oldval,
                                                             Idx                           newval) {
    GUM_ASSERT(offsets_.exists(&i));
    GUM_ASSERT(offsets_[&i] < this->domainSize());
    GUM_ASSERT(newval < var->domainSize());
    GUM_ASSERT(oldval < var->domainSize());

    if (newval >= oldval) {
      offsets_[&i] += gaps_[var] * (newval - oldval);
      GUM_ASSERT(offsets_[&i] < this->domainSize());
    } else {
      GUM_ASSERT(offsets_[&i] >= gaps_[var] * (oldval - newval));
      offsets_[&i] -= gaps_[var] * (oldval - newval);
    }
  }

  // listen to an assignment of a value in a Instantiation

  template < typename GUM_ELEMENT >
  void MultiDimWithOffset< GUM_ELEMENT >::setChangeNotification(const Instantiation& i) {
    GUM_ASSERT(offsets_.exists(&i));
    offsets_[&i] = getOffs_(i);
  }

  // listen to setFirst in each recorded Instantiation.

  template < typename GUM_ELEMENT >
  void MultiDimWithOffset< GUM_ELEMENT >::setFirstNotification(const Instantiation& i) {
    GUM_ASSERT(offsets_.exists(&i));
    offsets_[&i] = 0;
  }

  // listen to setLast in each recorded Instantiation.

  template < typename GUM_ELEMENT >
  void MultiDimWithOffset< GUM_ELEMENT >::setLastNotification(const Instantiation& i) {
    GUM_ASSERT(offsets_.exists(&i));
    offsets_[&i] = this->domainSize() - 1;
  }

  // listen to increment in each recorded Instantiation.

  template < typename GUM_ELEMENT >
  void MultiDimWithOffset< GUM_ELEMENT >::setIncNotification(const Instantiation& i) {
    GUM_ASSERT(offsets_.exists(&i));
    GUM_ASSERT(offsets_[&i] != this->domainSize() - 1);
    ++offsets_[&i];
  }

  // listen to increment in each recorded Instantiation.

  template < typename GUM_ELEMENT >
  void MultiDimWithOffset< GUM_ELEMENT >::setDecNotification(const Instantiation& i) {
    GUM_ASSERT(offsets_.exists(&i));
    GUM_ASSERT(offsets_[&i] != 0);
    --offsets_[&i];
  }

  // add a Instantiation as a slave

  template < typename GUM_ELEMENT >
  bool MultiDimWithOffset< GUM_ELEMENT >::registerSlave(Instantiation& i) {
    if (MultiDimImplementation< GUM_ELEMENT >::registerSlave(i)) {
      GUM_ASSERT(!offsets_.exists(&i));
      offsets_.insert(&i, getOffs_(i));
      return true;
    }

    return false;
  }

  // remove a registered slave instantiation

  template < typename GUM_ELEMENT >
  bool MultiDimWithOffset< GUM_ELEMENT >::unregisterSlave(Instantiation& i) {
    MultiDimImplementation< GUM_ELEMENT >::unregisterSlave(i);
    offsets_.erase(&i);
    return true;
  }

  // Compute the offset of a Instantiation
  /** If a variable present in the MultiDimWithOffset is missing from the
   * instantiation, throws InvalidArgument.
   * for instance : M<<a<<b<<c; with i=b:1|c:2|d:1 then M.getOffs_(i) throws
   * because a is not present in i.
   */

  template < typename GUM_ELEMENT >
  Size MultiDimWithOffset< GUM_ELEMENT >::getOffs_(const Instantiation& i) const {
    Idx off = 0;

    for (auto iter = gaps_.begin(); iter != gaps_.end(); ++iter)
      if (i.contains(iter.key())) off += iter.val() * i.valFromPtr(iter.key());
      else
        GUM_ERROR(InvalidArgument, iter.key()->name() << " not present in the instantiation " << i)

    return off;
  }

  /*  template < typename GUM_ELEMENT >
    Size MultiDimWithOffset< GUM_ELEMENT >::getOffs_(const Instantiation& i) const {
      Idx off = 0;

      for (HashTableConstIteratorSafe< const DiscreteVariable*, Size > iter = gaps_.beginSafe();
           iter != gaps_.endSafe();
           ++iter)
        if (i.contains(iter.key()))
          off += iter.val() * i.valFromPtr(iter.key());
        else
          GUM_ERROR(InvalidArgument, iter.key()->name() << " not present in the instantiation " <<
    i)

      return off;
    }*/

  // For a given indice of a value in the vector values_, this method computes
  // the corresponding instantiation
  /**
   * @param result the result of this methods, we assume that the given
   * instantiation already contains all the variables
   *               contained in the multidimarray (if V is the set of variables
   *               of this tab, V must be a subset of variables in
   *               result or the exact set)
   * @param indice indice in the vector values_
   */

  template < typename GUM_ELEMENT >
  void MultiDimWithOffset< GUM_ELEMENT >::computeInstantiationValue_(Instantiation& result,
                                                                     Size           indice) const {
    for (Idx i = 0; i < this->nbrDim(); ++i) {
      const DiscreteVariable& var        = this->variable(i);
      Idx                     domainSize = var.domainSize();
      result.chgVal(var, indice % domainSize);
      indice = indice / domainSize;
    }

    GUM_ASSERT(indice == 0);
  }

  // string representation of internal data about i in this.
  template < typename GUM_ELEMENT >
  std::string MultiDimWithOffset< GUM_ELEMENT >::toString(const Instantiation* i) const {
    if (i->isMaster(this)) {
      return std::to_string(offsets_[i]);
    } else {
      return "--";
    }
  }

  template < typename GUM_ELEMENT >
  Size MultiDimWithOffset< GUM_ELEMENT >::toOffset(const Instantiation& i) const {
    return getOffs_(i);
  }

  // set the Instantiation to the values corresponding to the offset (in this
  // array)
  template < typename GUM_ELEMENT >
  Instantiation& MultiDimWithOffset< GUM_ELEMENT >::fromOffset(Instantiation& i,
                                                               Size           offset) const {
    this->computeInstantiationValue_(i, offset);
    return i;
  }

} /* namespace gum */
