//| This file is a part of the sferes2 framework.
//| Copyright 2009, ISIR / Universite Pierre et Marie Curie (UPMC)
//| Main contributor(s): Jean-Baptiste Mouret, mouret@isir.fr
//|
//| This software is a computer program whose purpose is to facilitate
//| experiments in evolutionary computation and evolutionary robotics.
//| 
//| This software is governed by the CeCILL license under French law
//| and abiding by the rules of distribution of free software.  You
//| can use, modify and/ or redistribute the software under the terms
//| of the CeCILL license as circulated by CEA, CNRS and INRIA at the
//| following URL "http://www.cecill.info".
//| 
//| As a counterpart to the access to the source code and rights to
//| copy, modify and redistribute granted by the license, users are
//| provided only with a limited warranty and the software's author,
//| the holder of the economic rights, and the successive licensors
//| have only limited liability.
//|
//| In this respect, the user's attention is drawn to the risks
//| associated with loading, using, modifying and/or developing or
//| reproducing the software by the user in light of its specific
//| status of free software, that may mean that it is complicated to
//| manipulate, and that also therefore means that it is reserved for
//| developers and experienced professionals having in-depth computer
//| knowledge. Users are therefore encouraged to load and test the
//| software's suitability as regards their requirements in conditions
//| enabling the security of their systems and/or data to be ensured
//| and, more generally, to use and operate it in the same conditions
//| as regards security.
//|
//| The fact that you are presently reading this means that you have
//| had knowledge of the CeCILL license and that you accept its terms.

#ifndef DNN_HPP_
#define DNN_HPP_

#include <bitset>
#include <boost/serialization/serialization.hpp>
#include <boost/archive/text_iarchive.hpp>
#include <boost/archive/text_oarchive.hpp>
#include <boost/serialization/base_object.hpp>
#include <boost/serialization/vector.hpp>
#include <boost/serialization/shared_ptr.hpp>
#include <boost/serialization/map.hpp>
#include <boost/serialization/utility.hpp>
#include <boost/graph/graph_traits.hpp>
#include <boost/graph/iteration_macros.hpp>
#include <boost/foreach.hpp>
#include <sferes/dbg/dbg.hpp>
#include <sferes/misc.hpp>

#include "nn.hpp"
#include "trait.hpp"


namespace sferes
{
namespace gen
{

template <class Graph>
typename boost::graph_traits<Graph>::vertex_descriptor
random_vertex(Graph& g)
{
    assert(num_vertices(g));
    using namespace boost;
    if (num_vertices(g) > 1)
    {
        std::size_t n = misc::rand(num_vertices(g));
        typename graph_traits<Graph>::vertex_iterator i = vertices(g).first;
        while (n-- > 0) ++i;
        return *i;
    }
    else
        return *vertices(g).first;
}

template <class Graph>
typename boost::graph_traits<Graph>::edge_descriptor
random_edge(Graph& g)
{
    assert(num_edges(g));
    using namespace boost;
    if (num_edges(g) > 1)
    {
        std::size_t n = misc::rand(num_edges(g));
        typename graph_traits<Graph>::edge_iterator i = edges(g).first;
        while (n-- > 0) ++i;
        return *i;
    }
    else
        return *edges(g).first;
}

namespace dnn
{
enum init_t { ff = 0, random_topology };
}

template<typename N, typename C, typename Params>
class Dnn : public nn::NN<N, C>
{
public:
    typedef nn::NN<N, C> nn_t;
    typedef N neuron_t;
    typedef C conn_t;
    typedef typename nn_t::io_t io_t;
    typedef typename nn_t::weight_t weight_t;
    typedef typename nn_t::vertex_desc_t vertex_desc_t;
    typedef typename nn_t::edge_desc_t edge_desc_t;
    typedef typename nn_t::adj_it_t adj_it_t;
    typedef typename nn_t::graph_t graph_t;
    typedef typename neuron_t::af_t::params_t af_params_t;
    Dnn():
        nn::NN<N, C>(),
        developed(false)
    {
        
    }

    void init() {
        dbg::trace trace("gendnn", DBG_HERE);
        assert(developed);
        this->_init(); 
    }

    void develop(){
        dbg::trace trace("gendnn", DBG_HERE);
          BGL_FORALL_VERTICES_T(v, this->get_graph(), 
                                typename nn_t::graph_t)
            {
              this->get_graph()[v].get_afparams().develop();
              this->get_graph()[v].get_pfparams().develop();
            }
          BGL_FORALL_EDGES_T(e, this->get_graph(), 
                             typename nn_t::graph_t)
            {
              this->get_graph()[e].get_weight().develop();
            }
        developed = true;
    }

    void random()
    {
        if (Params::dnn::init == dnn::ff)
            _random_ff(Params::dnn::nb_inputs, Params::dnn::nb_outputs);
        else
            _random(Params::dnn::nb_inputs, Params::dnn::nb_outputs,
                    Params::dnn::min_nb_neurons, Params::dnn::max_nb_neurons,
                    Params::dnn::min_nb_conns, Params::dnn::max_nb_conns);
    }

    void mutate()
    {
        _change_conns();

        _change_neurons();

        if (misc::rand<float>() < Params::dnn::m_rate_add_conn)
            _add_conn_nodup();

        if (misc::rand<float>() < Params::dnn::m_rate_del_conn)
            _del_conn();

        if (misc::rand<float>() < Params::dnn::m_rate_add_neuron)
            _add_neuron_on_conn();

        if (misc::rand<float>() < Params::dnn::m_rate_del_neuron)
            _del_neuron();

    }
    void cross(const Dnn& o, Dnn& c1, Dnn& c2)
    {
        // if (misc::flip_coin())
        {
            c1 = *this;
            c2 = o;
        }
        //else
        //{
        //c2 = *this;
        //c1 = o;
        //}
    }

    // serialize the graph "by hand"...
    template<typename Archive>
    void save(Archive& a, const unsigned v) const
    {
        dbg::trace t1("nn", DBG_HERE);
        std::vector<int> inputs;
        std::vector<int> outputs;
        std::vector<typename neuron_t::af_t::params_t> afparams;
        std::vector<typename neuron_t::pf_t::params_t> pfparams;
        std::map<vertex_desc_t, int> nmap;
        std::vector<std::pair<int, int> > conns;
        std::vector<weight_t> weights;

        BGL_FORALL_VERTICES_T(v, this->_g, graph_t)
        {
            if (this->is_input(v))
                inputs.push_back(afparams.size());
            if (this->is_output(v))
                outputs.push_back(afparams.size());
            nmap[v] = afparams.size();
            afparams.push_back(this->_g[v].get_afparams());
            pfparams.push_back(this->_g[v].get_pfparams());
        }
        BGL_FORALL_EDGES_T(e, this->_g, graph_t)
        {
            conns.push_back(std::make_pair(nmap[source(e, this->_g)],
                    nmap[target(e, this->_g)]));
            weights.push_back(this->_g[e].get_weight());
        }
        assert(pfparams.size() == afparams.size());
        assert(weights.size() == conns.size());

        a & BOOST_SERIALIZATION_NVP(afparams);
        a & BOOST_SERIALIZATION_NVP(pfparams);
        a & BOOST_SERIALIZATION_NVP(weights);
        a & BOOST_SERIALIZATION_NVP(conns);
        a & BOOST_SERIALIZATION_NVP(inputs);
        a & BOOST_SERIALIZATION_NVP(outputs);
    }

    template<typename Archive>
    void load(Archive& a, const unsigned v)
    {
        dbg::trace t1("nn", DBG_HERE);
        std::vector<int> inputs;
        std::vector<int> outputs;
        std::vector<typename neuron_t::af_t::params_t> afparams;
        std::vector<typename neuron_t::pf_t::params_t> pfparams;
        std::map<size_t, vertex_desc_t> nmap;
        std::vector<std::pair<int, int> > conns;
        std::vector<weight_t> weights;

        dbg::out(dbg::info, "serialize") << "Serializing: afparams" << std::endl;
        a & BOOST_SERIALIZATION_NVP(afparams);
        dbg::out(dbg::info, "serialize") << "Serializing: pfparams" << std::endl;
        a & BOOST_SERIALIZATION_NVP(pfparams);
        dbg::out(dbg::info, "serialize") << "Serializing: weights" << std::endl;
        a & BOOST_SERIALIZATION_NVP(weights);
        dbg::out(dbg::info, "serialize") << "Serializing: conns" << std::endl;
        a & BOOST_SERIALIZATION_NVP(conns);
        dbg::out(dbg::info, "serialize") << "Serializing: inputs" << std::endl;
        a & BOOST_SERIALIZATION_NVP(inputs);
        dbg::out(dbg::info, "serialize") << "Serializing: outputs" << std::endl;
        a & BOOST_SERIALIZATION_NVP(outputs);

        assert(pfparams.size() == afparams.size());
        assert(weights.size() == conns.size());

        dbg::out(dbg::info, "serialize") << "Building network" << std::endl;
        this->set_nb_inputs(inputs.size());
        this->set_nb_outputs(outputs.size());
        for (size_t i = 0; i < this->get_nb_inputs(); ++i)
            nmap[inputs[i]] = this->get_input(i);
        for (size_t i = 0; i < this->get_nb_outputs(); ++i)
            nmap[outputs[i]] = this->get_output(i);

        for (size_t i = 0; i < afparams.size(); ++i)
            if (std::find(inputs.begin(), inputs.end(), i) == inputs.end()
                    && std::find(outputs.begin(), outputs.end(), i) == outputs.end())
                nmap[i] = this->add_neuron("n", pfparams[i], afparams[i]);
            else
            {
                this->_g[nmap[i]].set_pfparams(pfparams[i]);
                this->_g[nmap[i]].set_afparams(afparams[i]);
            }


        //assert(nmap.size() == num_vertices(this->_g));
        for (size_t i = 0; i < conns.size(); ++i)
            this->add_connection(nmap[conns[i].first], nmap[conns[i].second], weights[i]);
    }
    BOOST_SERIALIZATION_SPLIT_MEMBER();

    void random_neuron_params(){
        _random_neuron_params();
    }
protected:
    bool developed;

    void _random_neuron_params()
    {
        BGL_FORALL_VERTICES_T(v, this->_g, graph_t)
			        {
            this->_g[v].get_pfparams().random();
            this->_g[v].get_afparams().random();
			        }
    }
    // we start with a fully connected 0-layer perceptron with
    // random weights
    void _random_ff(size_t nb_inputs, size_t nb_outputs)
    {
        this->set_nb_inputs(nb_inputs);
        this->set_nb_outputs(nb_outputs);

        BOOST_FOREACH(vertex_desc_t& i, this->_inputs){
            BOOST_FOREACH(vertex_desc_t& o, this->_outputs){
                this->add_connection(i, o, _random_weight());
            }
        }
        _random_neuron_params();
    }

    void _random(size_t nb_inputs, size_t nb_outputs,
            size_t min_nb_neurons, size_t max_nb_neurons,
            size_t min_nb_conns, size_t max_nb_conns)
    {
        // io
        this->set_nb_inputs(nb_inputs);
        this->set_nb_outputs(nb_outputs);
        _random_neuron_params();

        // neurons
        size_t nb_neurons = misc::rand(min_nb_neurons, max_nb_neurons);
        for (size_t i = 0; i < nb_neurons; ++i)
            _add_neuron();//also call the random params

        // conns
        size_t nb_conns = misc::rand(min_nb_conns, max_nb_conns);
        for (size_t i = 0; i < nb_conns; ++i)
            _add_conn_nodup();

        this->simplify();
    }

    vertex_desc_t _random_tgt()
    {
        vertex_desc_t v;
        do
            v = random_vertex(this->_g);
        while (this->is_input(v));
        return v;
    }
    vertex_desc_t _random_src()
    {
        dbg::trace trace("mutate", DBG_HERE);
        vertex_desc_t v;
        do{
            v = random_vertex(this->_g);
        } while (this->is_output(v));
        return v;
    }

    vertex_desc_t _add_neuron()
    {
        vertex_desc_t v = this->add_neuron("n");
        this->_g[v].get_pfparams().random();
        this->_g[v].get_afparams().random();
        return v;
    }

    vertex_desc_t _add_neuron_on_conn()
    {
        dbg::trace trace("mutate", DBG_HERE);
        if (!num_edges(this->_g))
            return (vertex_desc_t)0x0;
        edge_desc_t e = random_edge(this->_g);
        vertex_desc_t src = source(e, this->_g);
        vertex_desc_t tgt = target(e, this->_g);
        typename nn_t::weight_t w = this->_g[e].get_weight();
        vertex_desc_t n = this->add_neuron("n");
        this->_g[n].get_pfparams().random();
        this->_g[n].get_afparams().random();
        //
        remove_edge(e, this->_g);
        this->add_connection(src, n, w);// todo : find a kind of 1 ??
        this->add_connection(n, tgt, w);

#if defined(DBG_ENABLED)
        w.develop();
        dbg::out(dbg::info, "random") << "neuron added - between: " << this->_g[src].get_id()
				          << " to: " << this->_g[tgt].get_id()
				          << " id: " << this->_g[n].get_id()
				          << " " << this->_g[n].get_afparams()
				          << " weight: " << nn::trait<weight_t>::single_value(w) << std::endl;
#endif

        return n;
    }

    void _del_neuron()
    {
        dbg::trace trace("mutate", DBG_HERE);
        assert(num_vertices(this->_g));

        if (this->get_nb_neurons() <= this->get_nb_inputs() + this->get_nb_outputs())
            return;
        vertex_desc_t v;
        do
            v = random_vertex(this->_g);
        while (this->is_output(v) || this->is_input(v));

        dbg::out(dbg::info, "random") << "neuron deleted: " << this->_g[v].get_id()
				          << " " << this->_g[v].get_afparams() << std::endl;

        clear_vertex(v, this->_g);
        remove_vertex(v, this->_g);
    }
    typename nn_t::weight_t _random_weight()
    {
        typename nn_t::weight_t w;
        w.random();
        return w;
    }
    void _add_conn()
    {
        dbg::trace trace("mutate", DBG_HERE);
        this->add_connection(_random_src(), _random_tgt(), _random_weight());
    }
    // add a random connection by avoiding to duplicate an existent connection
    void _add_conn_nodup()
    {
        dbg::trace trace("mutate", DBG_HERE);
        vertex_desc_t src, tgt;
        // this is only an upper bound; a connection might of course
        // be possible even after max_tries tries.
        size_t max_tries = num_vertices(this->_g) * num_vertices(this->_g),
                nb_tries = 0;
        do
        {
            src = _random_src();
            tgt = _random_tgt();
        }
        while (is_adjacent(this->_g, src, tgt) && ++nb_tries < max_tries);
        if (nb_tries < max_tries)
        {
            typename nn_t::weight_t w;
            w.random();
            this->add_connection(src, tgt, w);
        }
    }
    void _del_conn()
    {
        dbg::trace trace("mutate", DBG_HERE);
        if (!this->get_nb_connections())
            return;
        typename boost::graph_traits<graph_t>::edge_descriptor edge = random_edge(this->_g);
        vertex_desc_t src = boost::source(edge, this->_g);
        vertex_desc_t tgt = boost::target(edge, this->_g);

#if defined(DBG_ENABLED)
        this->_g[edge].get_weight().develop();
        dbg::out(dbg::info, "random") << "conn deleted - from: " << this->_g[src].get_id()
				          << " to: " << this->_g[tgt].get_id()
				          << " weight: " << nn::trait<weight_t>::single_value(this->_g[edge].get_weight()) << std::endl;
#endif

        remove_edge(edge, this->_g);
    }
    void _change_neurons(){
        dbg::trace trace("mutate", DBG_HERE);
        BGL_FORALL_VERTICES_T(v, this->_g, graph_t){
            this->_g[v].get_afparams().mutate();
            this->_g[v].get_pfparams().mutate();
        }
    }

    // No dup version
    void _change_conns()
    {
        dbg::trace trace("mutate", DBG_HERE);
        BGL_FORALL_EDGES_T(e, this->_g, graph_t){
            this->_g[e].get_weight().mutate();
        }

        BGL_FORALL_EDGES_T(e, this->_g, graph_t){
            if (misc::rand<float>() < Params::dnn::m_rate_change_conn)
            {
                vertex_desc_t src = source(e, this->_g);
                vertex_desc_t tgt = target(e, this->_g);
                typename nn_t::weight_t w = this->_g[e].get_weight();
                remove_edge(e, this->_g);
                int max_tries = num_vertices(this->_g) * num_vertices(this->_g),
                        nb_tries = 0;
                if (misc::flip_coin())
                    do
                        src = _random_src();
                    while(++nb_tries < max_tries && is_adjacent(this->_g, src, tgt));
                else
                    do
                        tgt = _random_tgt();
                    while(++nb_tries < max_tries && is_adjacent(this->_g, src, tgt));
                if (nb_tries < max_tries)
                    this->add_connection(src, tgt, w);
                return;
            }
        }
    }
};
}
}

#endif
