/*
 *       File name:  libcn/model-nmlio.cc
 *         Project:  cnrun
 *          Author:  Andrei Zavada <johnhommer@gmail.com>
 * Initial version:  2008-09-02
 *
 *         Purpose:  NeuroML import/export.
 *
 *         License:  GPL-2+
 */

#if HAVE_CONFIG_H && !defined(VERSION)
#  include "config.h"
#endif

#include <string>
#include <iostream>
#include <regex.h>

#include "forward-decls.hh"
#include "model.hh"


using namespace std;

#ifdef LIBXML_READER_ENABLED


int
cnrun::CModel::
import_NetworkML( const string& fname, TNMLImportOption import_option)
{
        // LIBXML_TEST_VERSION;

        xmlDoc *doc = xmlReadFile( fname.c_str(), nullptr, 0);
        if ( !doc )
                return TNMLIOResult::nofile;

        int retval = import_NetworkML( doc, fname, import_option);

        xmlFreeDoc( doc);

        return retval;
}





inline namespace {

xmlNode*
find_named_root_child_elem( xmlNode *node,     // node to start search from
                            const char *elem)  // name of the element searched for
{
        xmlNode *n;
        for ( n = node->children; n; n = n->next ) {
                if ( n->type == XML_ELEMENT_NODE ) {
                        if ( xmlStrEqual( n->name, BAD_CAST elem) )
                                return n;
// the <populations> and <projections> nodes are expected to appear as
// direct children of the root node; so don't go search deeper than that

//                        if ( n->children ) { // go search deeper
//                                ni = find_named_elem( n->children, elem);
//                                if ( ni )
//                                        return ni;
//                        }
                }
        }
        return nullptr;
}

}

int
cnrun::CModel::
import_NetworkML( xmlDoc *doc, const string& fname,
                  TNMLImportOption import_option)
{
        int retval = 0;

        // we pass up on validation (for which we would need to keep a
        // .dtd or Schema at hand), and proceed to extracting elements

        xmlNode *root_node = xmlDocGetRootElement( doc),
                *n;

      // read meta:notes and make out a name for the model
        if ( !root_node ) {
                vp( 0, stderr, "import_NetworkML(\"%s\"): No root element\n", fname.c_str());
                retval = TNMLIOResult::noelem;
                goto out;
        }

      // give it a name: assume it's generated by neuroConstruct for now
        if ( import_option == TNMLImportOption::reset ) {
                reset();
                if ( !(n = find_named_root_child_elem( root_node, "notes")) ) {
                        vp( 1, stderr, "<notes> element not found; model will be unnamed\n");
                        // this is not critical, so just keep the user
                        // informed and proceed
                } else
                        if ( n->type == XML_ELEMENT_NODE ) {  // only concern ourselves with nodes of this type
                                xmlChar *notes_s = xmlNodeGetContent( n);
                                // look for a substring specific to neuroConstruct, which is obviously speculative
                                regex_t RE;
                                regcomp( &RE, ".*project: (\\w*).*", REG_EXTENDED);
                                regmatch_t M[1+1];
                                name = (0 == regexec( &RE, (char*)notes_s, 1+1, M, 0))
                    ? string ((char*)notes_s + M[1].rm_so, M[1].rm_eo - M[1].rm_so)
                    : "(unnamed)";
                                xmlFree( notes_s);
                        } else
                                name = "(unnamed)";
        }

        vp( 1, "Model \"%s\": %s topology from %s\n",
            name.c_str(),
            (import_option == TNMLImportOption::merge) ?"Merging" :"Importing",
            fname.c_str());

        // In the following calls to _process_{populations,instances}
        // functions, the actual order of appearance of these nodes in
        // the xml file doesn't matter, thanks to the xml contents
        // being already wholly read and available to us as a tree.

      // process <populations>
        if ( !(n = find_named_root_child_elem( root_node, "populations")) ) {
                retval = TNMLIOResult::noelem;
                goto out;
        } // assume there is only one <populations> element: don't loop to catch more
        if ( (retval = _process_populations( n->children)) < 0)        // note n->children, which is in fact a pointer to the first child
                goto out;

      // process <projections>
      // don't strictly require any projections as long as there are some neurons
        if ( (n = find_named_root_child_elem( root_node, "projections")) ) {
                if ( (retval = _process_projections( n->children)) < 0 )
                        goto out;
        } else
                vp( 2, "No projections found\n");

out:
        // we are done with topology; now put units' variables on a vector
        finalize_additions();
        // can call time_step only after finalize_additions

        return retval;
}





int
cnrun::CModel::
_process_populations( xmlNode *n)
{
        xmlChar *group_id_s = nullptr,
                *cell_type_s = nullptr;

        int     pop_cnt = 0;

        try {
                for ( ; n; n = n->next ) {  // if is nullptr (parent had no children), we won't do a single loop
                        if ( n->type != XML_ELEMENT_NODE || !xmlStrEqual( n->name, BAD_CAST "population") )
                                continue;

                        group_id_s = xmlGetProp( n, BAD_CAST "name");
                        // BAD_CAST is just a cast to xmlChar*
                        // with a catch that libxml functions
                        // expect strings pointed to to be good UTF
                        if ( !group_id_s ) {
                                vp( 0, stderr, "<population> element missing a \"name\" attribute near line %d\n", n->line);
                                return TNMLIOResult::badattr;
                        }
                      // probably having an unnamed popuation isn't an error so serious as to abort the
                      // operation, but discipline is above all

                        cell_type_s = xmlGetProp( n, BAD_CAST "cell_type");
                        // now we know the type of cells included in this population; remember it to pass on to
                        // _process_population_instances, where it is used to select an appropriate unit type
                        // when actually adding a neuron to the model

                      // but well, let's check if we have units of that species in stock
                        if ( !unit_species_is_neuron((char*)cell_type_s) && !unit_family_is_neuron((char*)cell_type_s) ) {
                                vp( 0, stderr, "Bad cell species or family (\"%s\") in population \"%s\"\n",
                                    (char*)cell_type_s, group_id_s);
                                throw TNMLIOResult::badcelltype;
                        }

                        xmlNode *nin = n->children;  // again, ->children means ->first
                        if ( nin )
                                for ( ; nin; nin = nin->next )  // deal with multiple <instances> nodes
                                        if ( nin->type == XML_ELEMENT_NODE && xmlStrEqual( nin->name, BAD_CAST "instances") ) {
                                                int subretval = _process_population_instances(
                                                        nin->children,
                                                        group_id_s, cell_type_s);
                                                if ( subretval < 0 )
                                                        throw subretval;

                                                vp( 2, " %5d instance(s) of type \"%s\" in population \"%s\"\n",
                                                    subretval, cell_type_s,  group_id_s);
                                                ++pop_cnt;
                                        }

                        xmlFree( cell_type_s), xmlFree( group_id_s);
                }

                vp( 1, "\tTotal %d population(s)\n", pop_cnt);

        } catch (int ex) {
                xmlFree( cell_type_s), xmlFree( group_id_s);

                return ex;
        }

        return pop_cnt;
}






int
cnrun::CModel::
_process_projections( xmlNode *n)
{
        // much the same code as in _process_populations

        xmlChar *prj_name_s = nullptr,
                *prj_src_s = nullptr,
                *prj_tgt_s = nullptr,
                *synapse_type_s = nullptr;

        size_t pop_cnt = 0;

        try {
                for ( ; n; n = n->next ) {
                        if ( n->type != XML_ELEMENT_NODE || !xmlStrEqual( n->name, BAD_CAST "projection") )
                                continue;

                        prj_name_s = xmlGetProp( n, BAD_CAST "name");
                        if ( !prj_name_s ) {
                                fprintf( stderr, "<projection> element missing a \"name\" attribute near line %u\n", n->line);
                                return TNMLIOResult::badattr;
                        }

                        prj_src_s  = xmlGetProp( n, BAD_CAST "source");
                        prj_tgt_s  = xmlGetProp( n, BAD_CAST "target");
                        if ( !prj_src_s || !prj_tgt_s ) {
                                fprintf( stderr, "Projection \"%s\" missing a \"source\" and/or \"target\" attribute near line %u\n",
                                         prj_name_s, n->line);
                                throw TNMLIOResult::badattr;
                        }

                        xmlNode *nin;
                        nin = n->children;
                        if ( !nin )
                                fprintf( stderr, "Empty <projection> node near line %d\n", n->line);

                        for ( ; nin; nin = nin->next )
                                if ( nin->type == XML_ELEMENT_NODE && xmlStrEqual( nin->name, BAD_CAST "synapse_props") ) {
                                        synapse_type_s = xmlGetProp( nin, BAD_CAST "synapse_type");
                                        if ( !unit_species_is_synapse( (char*)synapse_type_s) &&
                                             !unit_family_is_synapse( (char*)synapse_type_s) ) {
                                                fprintf( stderr, "Bad synapse type \"%s\" near line %u\n",
                                                         (char*)synapse_type_s, nin->line);
                                                throw TNMLIOResult::badcelltype;
                                        }
                                }

                        for ( nin = n->children; nin; nin = nin->next )
                                if ( nin->type == XML_ELEMENT_NODE && xmlStrEqual( nin->name, BAD_CAST "connections") ) {
                                        int subretval = _process_projection_connections(
                                                nin->children,
                                                prj_name_s, synapse_type_s,
                                                prj_src_s, prj_tgt_s);
                                        if ( subretval < 0 )
                                                throw subretval;

                                        vp( 2, " %5d connection(s) of type \"%s\" in projection \"%s\"\n",
                                            subretval, synapse_type_s,  prj_name_s);
                                        ++pop_cnt;
                                }
                        xmlFree( prj_name_s), xmlFree( prj_src_s), xmlFree( prj_tgt_s);
                }

                vp( 1, "\tTotal %zd projection(s)\n", pop_cnt);

        } catch (int ex) {
                xmlFree( prj_name_s), xmlFree( prj_src_s), xmlFree( prj_tgt_s);
                return ex;
        }

        return (int)pop_cnt;
}







int
cnrun::CModel::
_process_population_instances(
        xmlNode *n,
        const xmlChar *group_prefix,
        const xmlChar *type_s)
{
        int     retval = 0;  // also keeps a count of added neurons

        double  x, y, z;
        char    cell_id[C_BaseUnit::max_label_size];

        xmlNode *nin;

        xmlChar *id_s = nullptr;
        try {
                for ( ; n; n = n->next ) {
                        if ( n->type != XML_ELEMENT_NODE || !xmlStrEqual( n->name, BAD_CAST "instance") )
                                continue;

                        xmlChar *id_s = xmlGetProp( n, BAD_CAST "id");
                        if ( !id_s ) {
                              // could be less strict here and allow empty ids, which will then be composed
                              // from group_prefix + id (say, "LN0", "LN1" and so on); but then, as
                              // individual <projection>s would have to reference both endpoints by explicit
                              // ids, it is obviously prone to error to have <instance> ids depend solely on
                              // their order of appearance.
                              // So we bark at empty ids.
                                fprintf( stderr, "<instance> element without an \"id\" attribute near line %u\n", n->line);
                                return TNMLIOResult::badattr;
                        }

                        size_t total_len = xmlStrlen( group_prefix) + xmlStrlen( id_s);
                        if ( total_len >= C_BaseUnit::max_label_size ) {
                                fprintf( stderr, "Combined label for an <instance> (\"%s%s\") exceeding %zu characters near line %d\n",
                                         group_prefix, id_s, C_BaseUnit::max_label_size, n->line);
                                throw TNMLIOResult::biglabel;
                        }
                        _longest_label = max(
                                _longest_label,
                                (unsigned short)snprintf(
                                        cell_id, C_BaseUnit::max_label_size-1, "%s.%s",
                                        group_prefix, id_s));  // here, a new instance is given a name
                        xmlFree( id_s);

                        if ( !(nin = n->children) )
                                return retval;

                        for ( ; nin; nin = nin->next ) {
                                if ( !(nin->type == XML_ELEMENT_NODE &&
                                       xmlStrEqual( nin->name, BAD_CAST "location")) )
                                        continue;

                                xmlChar *x_s = xmlGetProp( nin, BAD_CAST "x"),
                                        *y_s = xmlGetProp( nin, BAD_CAST "y"),
                                        *z_s = xmlGetProp( nin, BAD_CAST "z");
                              // here we do actually insert neurons into the model
                                if ( !(x_s && y_s && z_s) )
                                        vp( 1, stderr, "<location> element missing full set of coordinates near line %d\n", nin->line);
                                        // not an error
                                x = strtod( (char*)x_s, nullptr), y = strtod( (char*)y_s, nullptr), z = strtod( (char*)z_s, nullptr);
                                xmlFree( x_s), xmlFree( y_s), xmlFree( z_s);

                                C_BaseNeuron *neu = add_neuron_species(
                                        (char*)type_s, cell_id,
                                        TIncludeOption::is_notlast);

                                if ( !neu || neu->_status & CN_UERROR ) {
                                        if ( neu )
                                                delete neu;
                                        fprintf( stderr, "Failed to add a neuron \"%s\" near line %u\n", cell_id, n->line);
                                        return TNMLIOResult::structerror;
                                } else {
                                        neu->_serial_id = _global_unit_id_reservoir++;
                                        neu->pos = make_tuple( x, y, z);
                                        ++retval;
                                }
                        }
                }
        } catch (int ex) {
                xmlFree( id_s);
                return ex;
        }

        return retval;
}




int
cnrun::CModel::
_process_projection_connections(
        xmlNode *n,
        const xmlChar *synapse_name,
        const xmlChar *type_s,
        const xmlChar *src_grp_prefix,
        const xmlChar *tgt_grp_prefix)
{
        // similar to _process_population_instances, except that we read some more attributes (source and
        // target units)

        int     retval = 0;  // is also a counter of synapses

        char    //synapse_id [C_BaseUnit::max_label_size],
                src_s[C_BaseUnit::max_label_size],
                tgt_s[C_BaseUnit::max_label_size];
        double  weight;

        C_BaseSynapse *y;

        xmlChar *src_cell_id_s = nullptr,
                *tgt_cell_id_s = nullptr,
                *weight_s      = nullptr;
        try {
                for ( ; n; n = n->next ) {
                        if ( n->type != XML_ELEMENT_NODE || !xmlStrEqual( n->name, BAD_CAST "connection") )
                                continue;

                        src_cell_id_s = xmlGetProp( n, BAD_CAST "pre_cell_id"),
                        tgt_cell_id_s = xmlGetProp( n, BAD_CAST "post_cell_id"),
                        weight_s      = xmlGetProp( n, BAD_CAST "weight");
                        if ( /*!synapse_id_s || */ !src_cell_id_s || !tgt_cell_id_s ) {
                                fprintf( stderr, "A <connection> element without \"pre_cell_id\" and/or \"post_cell_id\" attribute near line %u\n", n->line);
                                throw TNMLIOResult::badattr;
                        }

                        snprintf( src_s, C_BaseUnit::max_label_size-1, "%s.%s", src_grp_prefix, src_cell_id_s);
                        snprintf( tgt_s, C_BaseUnit::max_label_size-1, "%s.%s", tgt_grp_prefix, tgt_cell_id_s);

                        if ( !weight_s ) {
                                vp( 3, stderr, "Assuming 0 for a synapse of \"%s.%s\" to \"%s%s\" without a \"weight\" attribute near line %u\n",
                                    src_grp_prefix, src_cell_id_s, tgt_grp_prefix, tgt_cell_id_s, n->line);
                                weight = 0.;
                        }
                        /* xmlFree( synapse_id_s), */ xmlFree( src_cell_id_s), xmlFree( tgt_cell_id_s),
                                xmlFree( weight_s);

                        y = add_synapse_species(
                                (char*)type_s, src_s, tgt_s,
                                weight,
                                TSynapseCloningOption::yes,
                                TIncludeOption::is_notlast);

                        if ( !y || y->_status & CN_UERROR ) {
                                if ( y )
                                        delete y;
                                fprintf( stderr, "Failed to add an \"%s\" synapse from \"%s\" to \"%s\" near line %u\n",
                                         (char*)type_s, src_s, tgt_s, n->line);
                                return TNMLIOResult::structerror;
                        } else
                                ++retval;
                }

        } catch (int ex) {
                /* xmlFree( synapse_id_s), */ xmlFree( src_cell_id_s), xmlFree( tgt_cell_id_s),
                        xmlFree( weight_s);
                return ex;
        }

        return retval;
}



int
cnrun::CModel::
export_NetworkML( const string& fname)
{
        int retval = 0;

        LIBXML_TEST_VERSION;

        fprintf( stderr, "export_NetworkML() not implemented yet\n");

        return retval;
}


#else
# error Need an XMLREADER-enabled libxml2 (>2.6)
#endif // LIBXML_READER_ENABLED

// Local Variables:
// Mode: c++
// indent-tabs-mode: nil
// tab-width: 8
// c-basic-offset: 8
// End:
