/****************************************************************************
 *   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                           *
 *                                                                          *
 ****************************************************************************/


/**
 * @file
 * @brief Headers of the MultiDimBucket class.
 *
 * @author Pierre-Henri WUILLEMIN(_at_LIP6) & Christophe GONZALES(_at_AMU)
 * @author Lionel TORTI
 */

#ifndef GUM_MULTIDIM_BUCKET_H
#define GUM_MULTIDIM_BUCKET_H

#include <climits>
#include <sstream>

#include <agrum/base/core/bijection.h>
#include <agrum/base/multidim/implementations/multiDimArray.h>
#include <agrum/base/multidim/implementations/multiDimReadOnly.h>

namespace gum {

  /**
   * @class MultiDimBucket
   * @headerfile multiDimBucket.h <agrum/base/multidim/multiDimBucket.h>
   * @ingroup multidim_group
   *
   * @brief A multidim implementation for buckets.
   *
   * This class behaves in two different ways, depending on the value of memory
   * allowed and the size of the resulting multidimensional table. If the table
   * is above the allowed amount of memory then value are computed when
   * demanded, having a cache for each registered instantiation to not compute
   * several times in a row the same value.
   *
   * If the memory allowed is enough to contain the bucket's table, then the
   * resulting table is computed when a value is demanded for the first time.
   *
   * Since a MultiDimArray is used as a buffer, an instantiation real master
   * will be the internal buffer of a MultiDimBucket. This is why you should
   * always call the Instantiation::isMaster() method with the reference
   * retuned by MultiDimBucket::getMasterRef().
   *
   * TODO handle slave switch between buffer and hashtable.
   *
   * @tparam GUM_ELEMENT The type of scalars stored in this multidimensional
   * table.
   */

  template < typename GUM_ELEMENT >
  class MultiDimBucket final: public MultiDimReadOnly< GUM_ELEMENT > {
    public:
    // ========================================================================
    /// @name Constructor & destructor.
    // ========================================================================
    /// @{

    /**
     * @brief Default constructor.
     *
     * The default amount of memory available for a bucket is 2^16 (65536)
     * elements in the table.
     *
     * @param bufferSize The amount of memory allowed for this bucket.
     */
    explicit MultiDimBucket(Size bufferSize = INT_MAX);

    /**
     * @brief Copy constructor.
     * @param source The MultiDimBucket to copy.
     */
    MultiDimBucket(const MultiDimBucket< GUM_ELEMENT >& source);

    /**
     * @brief Destructor.
     */
    ~MultiDimBucket() override;

    /// @}
    // ========================================================================
    /// @name Bucket methods
    // ========================================================================
    /// @{

    /**
     * @brief Add a MultiDimContainer in the bucket.
     * @param impl The MultiDimContainer to copy.
     * @throw DuplicateElement Raised if impl is already in the bucket.
     */
    void add(const MultiDimContainer< GUM_ELEMENT >& impl);

    /**
     * @brief Add a MultiDimContainer in the bucket.
     * @param impl The MultiDimContainer to add.
     * @throw DuplicateElement Raised if impl is already in the bucket.
     */
    void add(const MultiDimContainer< GUM_ELEMENT >* impl);

    /**
     * @brief Remove a MultiDimContainer from this bucket.
     * @param impl The MultiDimContainer to remove.
     */
    void erase(const MultiDimContainer< GUM_ELEMENT >& impl);

    /**
     * @brief Remove a MultiDimContainer from this bucket.
     * @param impl The MultiDimContainer to remove.
     */
    void erase(const MultiDimContainer< GUM_ELEMENT >* impl);

    /**
     * @brief Returns true if the MultiDimContainer is in this bucket.
     * @param impl The impl to test for existence.
     * @return Returns true if the MultiDimContainer is in this bucket.
     */
    bool contains(const MultiDimContainer< GUM_ELEMENT >& impl) const;

    /**
     * @brief Returns the sequence of all the variables contained in the
     * bucket.
     * @return Returns the sequence of all the variables contained in the
     * bucket.
     */
    const gum::VariableSet& allVariables() const;

    /**
     * @brief Returns the number of MultiDimContainer in this bucket.
     * @return Returns the number of MultiDimContainer in this bucket.
     */
    Size bucketSize() const;

    /**
     * @brief Returns true if this bucket is empty.
     * @return Returns true if this bucket is empty.
     */
    bool isBucketEmpty() const;

    /**
     * @brief Returns true if the bucket need re-computation since the last
     * computation.
     * @return Returns true if the bucket need re-computation since the last
     * computation.
     */
    bool bucketChanged() const;

    /**
     * @brief Returns the MultiDimArray used by this MultiDimBucket.
     * @return Returns the MultiDimArray used by this MultiDimBucket.
     * @throw OperationNotAllowed Raised if the bucket has not been built.
     */
    const MultiDimArray< GUM_ELEMENT >& bucket() const;

    /**
     * @brief Returns the amount of memory allowed for this bucket.
     * @return Returns the amount of memory allowed for this bucket.
     */
    Size bufferSize() const;

    /**
     * @brief Changes the amount of memory allowed for this bucket.
     *
     * If the new amount is not enough for the current size of this bucket,
     * then internal buffer is deleted. In the other case, the internal buffer
     * is created but not computed.
     *
     * @param amount The amount of memory allowed for this bucket.
     */
    void setBufferSize(Size amount);

    /**
     * @brief This method computes the final table of this bucket.
     *
     * A flag is used to prevent unnecessary computation if the table has
     * already been computed.
     *
     * If the size of the final table is above the amount of authorized memory,
     * an OperationNotAllowed is raised.
     *
     * Remember that this method is constant because the content of a
     * multidimensional table is mutable.
     *
     * @param force If true (default set at false) then the final table is
     * re-computed.
     *
     * @throw OperationNotAllowed Raised if the size of the final table is
     * above the authorized amount of memory.
     * @throw SizeError Raised if the bucket is empty.
     */
    void compute(bool force = false) const;

    /// @}
    // ========================================================================
    /// @name Inherited methods
    // ========================================================================
    /// @{

    [[nodiscard]] MultiDimContainer< GUM_ELEMENT >* newFactory() const override;

    const std::string& name() const override;

    void add(const DiscreteVariable& v) override;

    void erase(const DiscreteVariable& v) override;

    Size realSize() const override;

    bool contains(const DiscreteVariable& v) const override;

    GUM_ELEMENT get(const Instantiation& i) const override;

    void changeNotification(const Instantiation&          i,
                            const DiscreteVariable* const var,
                            Idx                           oldval,
                            Idx                           newval) override;

    void setFirstNotification(const Instantiation& i) override;

    void setLastNotification(const Instantiation& i) override;

    void setIncNotification(const Instantiation& i) override;

    void setDecNotification(const Instantiation& i) override;

    void setChangeNotification(const Instantiation& i) override;

    bool registerSlave(Instantiation& i) override;

    bool unregisterSlave(Instantiation& i) override;

    MultiDimAdressable& getMasterRef() override;

    const MultiDimAdressable& getMasterRef() const override;

    std::string toString(const Instantiation* i) const override;

    /// @}

    /**
     * @brief Returns the MultiDimContainer and their respective Instantiation.
     * @return Returns the MultiDimContainer and their respective Instantiation.
     */
    const HashTable< const MultiDimContainer< GUM_ELEMENT >*, Instantiation* >& multidims() const;

    protected:
    void commitMultipleChanges_() override;

    /**
     * @warning This will raise en exception, you should directly use the get()
     * and operator[]() methods.
     */
    GUM_ELEMENT& get_(const Instantiation& i) const override;

    void replace_(const DiscreteVariable* x, const DiscreteVariable* y) override;

    private:
    /// The number of element allowed in  _bucket_.
    Size _bufferSize_;

    /// Bijection between instantiations registered on this and
    /// their equivalent on  _bucket_
    Bijection< Instantiation*, Instantiation* > _instantiations_;

    /// The result table of this bucket.
    MultiDimArray< GUM_ELEMENT >* _bucket_;

    /// The set of MultiDimContainer in this bucket.
    mutable HashTable< const MultiDimContainer< GUM_ELEMENT >*, Instantiation* > _multiDims_;

    /// The set of all variables of the multidims in this bucket.
    gum::VariableSet _allVariables_;

    /// Instantiation over all variable in this
    mutable Instantiation _allVarsInst_;

    /**
     * @brief Add a variable to  _allVariables_, and do nothing if var is already
     * in
     * the set.
     * @param var The DiscreteVariable to add.
     */
    void _addVariable_(const DiscreteVariable* var);

    /**
     * @brief Erase a variable from  _allVariables_ if no other multidimensional
     * table
     * uses it in this bucket.
     * @param var The DiscreteVariable to remove.
     */
    void _eraseVariable_(const DiscreteVariable* var);

    /**
     * @brief Initialize the internal buffer.
     *
     * This method delete  _bucket_ after saving it's slave instantiations.
     */
    void _initializeBuffer_();

    /**
     * @brief Clean the buffer and switch it's instantiation to this bucket.
     */
    void _eraseBuffer_();

    /**
     * @brief Compute the value of the final table of this bucket given i.
     *
     * If i variables are a subset of this bucket, then the missing values are
     * supposed to be at 0.
     *
     * @param value The value to compute.
     * @throw SizeError Raised if the bucket is empty.
     */
    GUM_ELEMENT _computeValue_(const Instantiation& value) const;

    /// Flag used to know if changes has occurred in the bucket since last
    /// computation.
    mutable bool _changed_;

    /// This table is used to keep the last value computed for an instantiation
    /// when the value are computed on the fly.
    mutable HashTable< const Instantiation*, GUM_ELEMENT > _slavesValue_;

    /// The class name.
    std::string _name_;
  };


#ifndef GUM_NO_EXTERN_TEMPLATE_CLASS
  extern template class MultiDimBucket< double >;
#endif


} /* namespace gum */

#include <agrum/base/multidim/implementations/multiDimBucket_tpl.h>

#endif /* GUM_MULTIDIM_BUCKET_H */
