/************************************************************************
 *
 * Copyright (C) 2009-2023 IRCAD France
 * Copyright (C) 2012-2021 IHU Strasbourg
 *
 * This file is part of Sight.
 *
 * Sight is free software: you can redistribute it and/or modify it under
 * the terms of 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.
 *
 * Sight is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with Sight. If not, see <https://www.gnu.org/licenses/>.
 *
 ***********************************************************************/

// fwTools - copyright (C) 2005 IRCAD.

#pragma once

#include <boost/mpl/accumulate.hpp>
#include <boost/mpl/apply.hpp>
#include <boost/mpl/back_inserter.hpp>
#include <boost/mpl/copy.hpp>
#include <boost/mpl/empty.hpp>
#include <boost/mpl/eval_if.hpp>
#include <boost/mpl/placeholders.hpp>
#include <boost/mpl/push_front.hpp>
#include <boost/mpl/transform.hpp>
#include <boost/mpl/vector.hpp>

namespace sight::core::tools
{

/**
 * @brief   Helper for BinaryCartesianProduct two Set
 *
 * @param   TYPE should be a simple type
 * @param   SET_OF_SET should be a container of type list i.e vector< vector< singleTypes>, vector< singleTypes>, ... >.
 * If SET is empty then vector<vector<TYPE> > is created
 *
 * From a type and a set generate a new set where elements are concatenation of type and element of second set i.e
 * AppendValueFirst ( a , [ [B] , [C] ] ) --> [ [a,B], [b,C] ] and  AppendValueFirst ( a , [ ] ) --> [ [a] ]
 */
struct append_value_first
{
    template<class TYPE, class SET_OF_SET>
    struct apply
    {
        typedef BOOST_DEDUCED_TYPENAME boost::mpl::eval_if<boost::mpl::empty<SET_OF_SET>,
                                                           boost::mpl::vector<boost::mpl::vector<TYPE> >,
                                                           boost::mpl::transform<SET_OF_SET,
                                                                                 boost::mpl::push_front<boost::mpl::
                                                                                                        _1, TYPE> >
        >::type type;
    };
};

/**
 * @brief   MetaFunction which create an boost::boost::mpl::vector
 */
struct make_vector
{
    template<class T>
    struct apply
    {
        typedef BOOST_DEDUCED_TYPENAME boost::mpl::vector<T>::type type;
    };
};

/**
 * @brief   MetaFunction ( used for pseudo Curryfication ) which transform a set where new elements are
 * singleton of 1st set elements i.e { a , b , c } --> { {a}, {b}, {c} }
 **/
struct make_set_of_singletons
{
    template<class set>
    struct apply
    {
        typedef BOOST_DEDUCED_TYPENAME boost::mpl::transform<set, make_vector>::type type;
    };
};

/**
 * @brief   Helper which compute from a set and a multi set
 *
 * Set1: A = { a_1, a_2, ..., a_N }  \n  MultiSet = {   { ... b_i ...} , { ... c_i ...}, ... { z_i ...}  }\n
 * the set
 * {   {a_1, ... b_i ...} , { a_1,  ... c_i ...}, ... {a_1,  ... z_i ...}  ,        {a_2, ... b_i ...} , { a_2,  ... c_i
 *...}, {a_2,  ... z_i ...},
 *   , .... ,  {a_N, ... b_i ...} , { a_N,  ... c_i ...}, ... {a_N,  ... z_i ...}
 */
struct binary_cartesian_product_recurser
{
    template<class set1, class multi_set>
    struct apply
    {
        typedef BOOST_DEDUCED_TYPENAME boost::mpl::accumulate<set1,
                                                              boost::mpl::vector<>,
                                                              boost::mpl::copy<boost::mpl::apply2<append_value_first,
                                                                                                  boost::mpl::_2,
                                                                                                  multi_set>,
                                                                               boost::mpl::back_inserter<boost::mpl
                                                                                                         ::_1> >

        >::type type;
    };
};

/**
 * @brief   Compute Cartesian Product of two set (type list) to generate all possible combination.
 *
 * From two type list generate a new type list where all element is a combination of each set. For example :
 * @code
 *  using namespace boost::mpl;
 *
 * typedef vector< char, short, long > Set1;
 * typedef vector< double, float > Set2;
 *
 *  typedef vector< vector< char,float>, vector<char, double>, vector< short,float>, vector<short, double>, vector<
 * long,float>, vector<long, double> > Wanted;
 *
 *  typedef apply< BinaryCartesianProduct, Set1, Set2 >::type  Result;
 *
 *  BOOST_MPL_ASSERT_RELATION( size<Wanted>::value , == , size<Result>::value  );
 *  BOOST_MPL_ASSERT(( equal< front<Wanted> , front<Result> > ));
 *  BOOST_MPL_ASSERT(( equal< at_c<Wanted,0> , at_c<Result,0> > ));
 *  BOOST_MPL_ASSERT(( equal< at_c<Wanted,1> , at_c<Result,1> > ));
 *  BOOST_MPL_ASSERT(( equal< at_c<Wanted,2> , at_c<Result,2> > ));
 * @endcode
 * This operator can deal with empty set :
 * @code
 *  using namespace boost::mpl;
    typedef vector<> emptySet;
    typedef vector< char, short>  Set1;

    typedef vector< vector<char>, vector<short> >::type Wanted;

    typedef apply< BinaryCartesianProduct, Set1, emptySet >::type  Result;
    BOOST_MPL_ASSERT_RELATION( size<Wanted>::value , == , size<Result>::value  );
    BOOST_MPL_ASSERT(( equal< front<Wanted> , front<Result> > ));
    BOOST_MPL_ASSERT(( equal< at_c<Wanted,0> , at_c<Result,0> > ));
    BOOST_MPL_ASSERT(( equal< at_c<Wanted,1> , at_c<Result,1> > ));
 * @endcode
 */
struct binary_cartesian_product
{
    template<class set1, class set2>
    struct apply
    {
        typedef BOOST_DEDUCED_TYPENAME boost::mpl::apply1<make_set_of_singletons, set2>::type set2with_singletons;

        typedef BOOST_DEDUCED_TYPENAME boost::mpl::apply2<binary_cartesian_product_recurser, set1,
                                                          set2with_singletons>::type type;
    };
};

/**
 * @brief compute the cartesian product of many set
 *
 * @param   MultiSet must be of the following form vector<  vector< ElementaryType1, ElementaryType2,... > , vector<
 *...ElementaryTypes...> ... > where Elementary
 *          types are not boost::mpl::Container : i.e int, classes, std::vector<int> etc...
 *
 * Example
 * @code
 *  using namespace boost::mpl;

    typedef vector< vector<signed char, signed short, signed int>,  vector< float, double >  , vector< std::string,
 * unsigned char >   > ::type MultiSet;

    typedef apply< CartesianProduct, MultiSet>::type Result;

    BOOST_MPL_ASSERT_RELATION( size< Result >::value , == , 12 );

    BOOST_MPL_ASSERT(( equal<  at_c<Result,0>::type, vector<  signed char,  float  , std::string >    > ));
    BOOST_MPL_ASSERT(( equal<  at_c<Result,1>::type, vector<  signed char,  float  , unsigned char >    > ));
    BOOST_MPL_ASSERT(( equal<  at_c<Result,2>::type, vector<  signed char,  double , std::string >    > ));
    BOOST_MPL_ASSERT(( equal<  at_c<Result,3>::type, vector<  signed char,  double , unsigned char >    > ));

    BOOST_MPL_ASSERT(( equal<  at_c<Result,4>::type, vector< signed short,  float  , std::string >    > ));
    BOOST_MPL_ASSERT(( equal<  at_c<Result,5>::type, vector< signed short,  float  , unsigned char >    > ));
    BOOST_MPL_ASSERT(( equal<  at_c<Result,6>::type, vector< signed short,  double , std::string >    > ));
    BOOST_MPL_ASSERT(( equal<  at_c<Result,7>::type, vector< signed short,  double , unsigned char >    > ));

    BOOST_MPL_ASSERT(( equal<  at_c<Result,8>::type, vector<  signed int,  float  , std::string >    > ));
    BOOST_MPL_ASSERT(( equal<  at_c<Result,9>::type, vector<  signed int,  float  , unsigned char >    > ));
    BOOST_MPL_ASSERT(( equal<  at_c<Result,10>::type, vector< signed int,  double , std::string >    > ));
    BOOST_MPL_ASSERT(( equal<  at_c<Result,11>::type, vector< signed int,  double , unsigned char >    > ));
 * @endcode
 **/
struct cartesian_product
{
    template<class multi_set>
    struct apply
    {
        typedef BOOST_DEDUCED_TYPENAME boost::mpl::reverse_fold<multi_set,
                                                                boost::mpl::vector<>,
                                                                boost::mpl::apply2<binary_cartesian_product_recurser,
                                                                                   boost::mpl::_2, boost::mpl::_1>
        >::type type;
    };
};

} //end namespace sight::core::tools
