/*
 *       File name:  libcn/base-unit.cc
 *         Project:  cnrun
 *          Author:  Andrei Zavada <johnhommer@gmail.com>
 *                   building on original work by Thomas Nowotny <tnowotny@ucsd.edu>
 * Initial version:  2008-08-02
 *
 *         Purpose:  unit base class
 *
 *         License:  GPL-2+
 */

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

#include <fcntl.h>
#include <unistd.h>
#include <iostream>
#include <limits>
#include <functional>

#include <gsl/gsl_statistics_double.h>

#include "libstilton/containers.hh"
#include "base-unit.hh"
#include "model.hh"


using namespace std;
using namespace cnrun;
using cnrun::alg::member;

unsigned short cnrun::global::precision = 4;
int cnrun::global::verbosely = 1;

cnrun::C_BaseUnit::
C_BaseUnit (TUnitType type_, const string& label_,
            CModel* M_, int s_mask)
      : precision (global::precision),
        _type (type_), _status (0 |/* CN_UENABLED |*/ s_mask),
        M (M_),
        _binwrite_handle (-1), _listener_disk (nullptr), _listener_mem (nullptr)
{
        memset( _label, 0, max_label_size);
        if ( label_.size() )
                strncpy( _label, label_.c_str(), max_label_size);
        else
                snprintf( _label, max_label_size-1, "fafa%p", this);

        if ( M_ && M_->unit_by_label( label_) ) {
                fprintf( stderr, "Model %s already has a unit labelled \"%s\"\n", M_->name.c_str(), label_.c_str());
                throw "Duplicate unit label";
        }

        reset_params();
        // don't have field idx to do reset_vars() safely
}



void
cnrun::C_BaseUnit::
reset_state()
{
        if ( M )
                M->vp( 3, stderr, "Resetting \"%s\"\n", _label);
        reset_vars();
        if ( is_listening() )
                restart_listening();
}


int
cnrun::C_BaseUnit::
param_idx_by_sym( const string& sym) const
{
        for ( size_t i = 0; i < p_no(); ++i )
                if ( sym == __CNUDT[_type].stock_param_syms[i] )
                        return i;
        return -1;
}

int
cnrun::C_BaseUnit::
var_idx_by_sym( const string& sym) const
{
        for ( size_t i = 0; i < v_no(); ++i )
                if ( sym == __CNUDT[_type].stock_var_syms[i] )
                        return i;
        return -1;
}








void
cnrun::C_BaseUnit::
start_listening( int mask)
{
        if ( !M ) {
                fprintf( stderr, "start_listening() called for an unattached unit \"%s\"\n", _label);
                return;
        }
        if ( _listener_disk || _listener_mem || _binwrite_handle != -1 ) { // listening already; check if user wants us to listen differently
                if ( (_status | (mask & (CN_ULISTENING_DISK | CN_ULISTENING_MEM | CN_ULISTENING_BINARY | CN_ULISTENING_1VARONLY | CN_ULISTENING_DEFERWRITE)))
                     != mask ) {
                        stop_listening();  // this will nullptrify _listener_{mem,disk}, avoiding recursion
                        start_listening( mask);
                        M->vp( 4, stderr, "Unit \"%s\" was already listening\n", _label);
                        return;
                }
        }

      // deferred write implies a mem listener
        if ( mask & CN_ULISTENING_DEFERWRITE && !(mask & CN_ULISTENING_MEM) )
                mask |= CN_ULISTENING_MEM;

        if ( mask & CN_ULISTENING_MEM )
                _listener_mem = new vector<double>;

        if ( mask & CN_ULISTENING_DISK ) {
                if ( M->is_diskless )
                        M->vp( 1, stderr, "Cannot get Unit \"%s\" to listen to disk in a diskless model\n", _label);
                else {
                        _listener_disk = new ofstream( (string(_label)+".var").c_str(), ios_base::trunc);
                        _listener_disk->precision( precision);

                        *_listener_disk << "# " << _label << " variables\n#<time>";
                        if ( mask & CN_ULISTENING_1VARONLY )
                                *_listener_disk << "\t<" << var_sym(0) << ">";
                        else
                                for ( size_t v = 0; v < v_no(); ++v )
                                        *_listener_disk << "\t<" << var_sym(v) << ">";
                        *_listener_disk << endl;
                        M->vp( 4, stderr, "Unit \"%s\" now listening\n", _label);
                }
        }

        if ( mask & CN_ULISTENING_BINARY )
                _binwrite_handle = open( (string(_label)+".varx").c_str(), O_WRONLY|O_CREAT|O_TRUNC, S_IRUSR | S_IWUSR);

        _status |= (mask & (CN_ULISTENING_DISK | CN_ULISTENING_MEM | CN_ULISTENING_BINARY |
                            CN_ULISTENING_1VARONLY | CN_ULISTENING_DEFERWRITE));

      // inform the model
        M->register_listener( this);
}


void
cnrun::C_BaseUnit::
stop_listening()
{
      // do deferred write
        if ( _status & CN_ULISTENING_DEFERWRITE && _listener_mem ) {
                if ( _listener_disk ) {
                        for ( auto mI = _listener_mem->begin(); mI != _listener_mem->end(); ) {
                                *_listener_disk << *(mI++);
                                if ( _status & CN_ULISTENING_1VARONLY )
                                        *_listener_disk << "\t" << *(mI++);
                                else
                                        for ( size_t v = 0; v < v_no(); ++v )
                                                *_listener_disk << "\t" << *(mI++);
                                *_listener_disk << endl;
                        }
                }
                if ( _binwrite_handle != -1 )
                        if ( write( _binwrite_handle, _listener_mem->data(),
                                    sizeof(double) * _listener_mem->size()) < 1 )
                                M->vp( 0, stderr, "write() failed on \"%s.varx\"\n", _label);
        }

        if ( _listener_mem ) {
                delete _listener_mem;
                _listener_mem = nullptr;
        }

        if ( _listener_disk ) {
                _listener_disk->close();
                delete _listener_disk;
                _listener_disk = nullptr;
        }

        if ( _binwrite_handle != -1 ) {
                close( _binwrite_handle);
                _binwrite_handle = -1;
        }

        _status &= ~(CN_ULISTENING_MEM | CN_ULISTENING_DISK | CN_ULISTENING_BINARY);

        if ( M ) {
                M->unregister_listener( this);
                M->vp( 4, stderr, "Unit \"%s\" not listening now\n", _label);
        }

}




void
cnrun::C_BaseUnit::
tell()
{
        if ( _binwrite_handle != -1 && !(_status & CN_ULISTENING_DEFERWRITE) ) {
                if ( write( _binwrite_handle, &M->V[0], sizeof(double)) < 1 ||
                     write( _binwrite_handle, &var_value(0),
                            sizeof(double) * ((_status & CN_ULISTENING_1VARONLY) ? 1 : v_no())) < 1 )
                        M->vp( 0, stderr, "write() failed in tell() for \"%s\"\n", _label);
        }

        if ( _listener_disk && !(_status & CN_ULISTENING_DEFERWRITE) ) {
                *_listener_disk << model_time();
                if ( _status & CN_ULISTENING_1VARONLY )
                        *_listener_disk << "\t" << var_value(0);
                else
                        for ( size_t v = 0; v < v_no(); ++v )
                                *_listener_disk << "\t" << var_value(v);
                *_listener_disk << endl;
        }

        if ( _listener_mem ) {
//                _listener_mem->push_back( 999);
                _listener_mem->push_back( model_time());
                if ( _status & CN_ULISTENING_1VARONLY )
                        _listener_mem->push_back( var_value(0));
                else
                        for ( size_t v = 0; v < v_no(); ++v )
                                _listener_mem->push_back( var_value(v));
        }
}






void
cnrun::C_BaseUnit::
dump( bool with_params, FILE *strm) const
{
        fprintf( strm, "[%lu] (%s) \"%s\"\n", _serial_id, species(), _label);

        if ( with_params ) {
                fprintf( strm, "    Pp: ");
                for ( size_t p = 0; p < p_no(); ++p )
                        if ( *param_sym(p) != '.' || M->options.verbosely > 5 )
                                fprintf( strm, "%s = %g; ", param_sym(p), get_param_value(p));
                fprintf( strm, "\n");
        }
        fprintf( strm, "    Vv: ");
        for ( size_t v = 0; v < v_no(); ++v )
                if ( *var_sym(v) != '.' || M->options.verbosely > 5 )
                        fprintf( strm, "%s = %g; ", var_sym(v), get_var_value(v));
        fprintf( strm, "\n");

        if ( _sources.size() ) {
                fprintf( strm, "   has sources:  ");
                for ( auto &S : _sources )
                        fprintf( strm, "%s << %s;  ",
                                 (S.sink_type == SINK_PARAM) ? param_sym(S.idx) : var_sym(S.idx),
                                 S.source->name());
                fprintf( strm, "\n");
        }

        if ( is_listening() ) {
                fprintf( strm, "   listening to %s%s%s\n",
                         _listener_mem ? "mem" : "",
                         _listener_mem && _listener_disk ? ", " : "",
                         _listener_disk ? "disk" : "");
        }
}






// source interface

void
cnrun::C_BaseUnit::
detach_source( C_BaseSource *s, TSinkType sink_type, size_t idx)
{
        // list <SSourceInterface<C_BaseSource>>::iterator K;
        // while ( (K = find( _sources.begin(), _sources.end(),
        //                    )) != _sources.end() )
        //         _sources.erase( K);
        _sources.remove( SSourceInterface<C_BaseSource> (s, sink_type, idx));
        M->unregister_unit_with_sources( this);
}


void
cnrun::C_BaseUnit::
apprise_from_sources()
{
        for ( auto &S : _sources )
                switch ( S.sink_type ) {
                case SINK_PARAM:
//                        printf( "apprise_from_sources() for %s{%d} = %g\n", _label, S->idx, (*S->source)( model_time()));
                        param_value( S.idx) = (*S.source)( model_time());
                        param_changed_hook();
                    break;
                case SINK_VAR:
                        var_value( S.idx) = (*S.source)( model_time());
                    break;
                }
}


cnrun::C_BaseUnit::
~C_BaseUnit()
{
        if ( M )
                M->vp( 5, "   deleting base unit \"%s\"\n", _label);

        if ( is_listening() ) {
                stop_listening();
                if ( M && M->model_time() == 0. )
                      // nothing has been written yet, delete the files on disk
                        unlink( (string(_label) + ".var").c_str());
        }
        if ( M )
                M->exclude_unit( this, CModel::TExcludeOption::no_delete);
}






// ----- C_BaseNeuron


bool
cnrun::C_BaseNeuron::
connects_to( const C_BaseNeuron &to) const
{
        for ( auto &A : _axonal_harbour )
                if ( A->has_target( to) )
                        return true;
        return false;
}

cnrun::C_BaseSynapse*
cnrun::C_BaseNeuron::
connects_via( const C_BaseNeuron &to,
              SCleft::mapped_type *g_ptr) const
{
        for ( auto &A : _axonal_harbour )
                if ( A->has_target( to) ) {
                        if ( g_ptr )
                                *g_ptr = to._dendrites.at(A);
                        return A;
                }
        if ( g_ptr )
                *g_ptr = NAN;
        return nullptr;
}


void
cnrun::C_BaseNeuron::
dump( bool with_params, FILE *strm) const
{
        C_BaseUnit::dump( with_params);
        if ( _spikelogger_agent && !(_spikelogger_agent->_status & CN_KL_IDLE) )
                fprintf( strm, "   logging spikes at %g:%g\n", _spikelogger_agent->sample_period, _spikelogger_agent->sigma);
        fprintf( strm, "\n");

}


cnrun::C_BaseNeuron::
~C_BaseNeuron()
{
        if ( M )
                M->vp( 4, "  deleting base neuron \"%s\"\n", _label);

      // kill all efferents
        for ( auto Y = _axonal_harbour.rbegin(); Y != _axonal_harbour.rend(); ++Y ) {
                (*Y) -> _source = nullptr;
                delete (*Y);
        }
      // unlink ourselves from all afferents
        for ( auto Y = _dendrites.rbegin(); Y != _dendrites.rend(); ++Y )
                Y->first->_targets.remove( this);

        if ( _spikelogger_agent ) {
                if ( M && !(_spikelogger_agent->_status & CN_KL_IDLE) )
                        M->unregister_spikelogger( this);
                delete _spikelogger_agent;
                _spikelogger_agent = nullptr;
        }
}




// --- SSpikeloggerService

double
cnrun::SSpikeloggerService::
sdf( double at, double sample_width, double sigma, size_t *nspikes) const
{
        if ( nspikes )
                *nspikes = 0;

        double  dt,
                result = 0.;
        for ( auto &T : spike_history ) {
                dt = T - at;
                if ( dt < -sample_width/2. )
                        continue;
                if ( dt >  sample_width/2. )
                        break;
                if ( nspikes )
                        ++(*nspikes);
                result += exp( -dt*dt/(sigma * sigma));
        }
        return result;
}


double
cnrun::SSpikeloggerService::
shf( double at, double sample_width) const
{
        double  dt,
                last_spike_at;
        vector<double>
                intervals;
        bool    counted_one = false;
        for ( auto &T : spike_history ) {
                dt = T - at;
                if ( dt < -sample_width/2. )
                        continue;
                if ( dt >  sample_width/2. )
                        break;

                if ( counted_one )
                        intervals.push_back( last_spike_at - T);
                else
                        counted_one = true;

                last_spike_at = T;
        }

        return (intervals.size() < 3)
                ? 0
                : gsl_stats_sd( intervals.data(), 1, intervals.size());
}


size_t
cnrun::SSpikeloggerService::
get_sxf_vector_custom( vector<double> *sdf_buffer, vector<double> *shf_buffer,
                       vector<size_t> *nspikes_buffer,
                       double sample_period_custom, double sigma_custom,
                       double from, double to) const
{
        if ( to == 0. )
                to = _client->M->model_time();

        if ( sdf_buffer )
                sdf_buffer->clear();
        if ( shf_buffer )
                shf_buffer->clear();
        if ( nspikes_buffer )
                nspikes_buffer->clear();

        for ( double t = from; t <= to; t += sample_period_custom ) {
                size_t  nspikes = 0;
                double  sdf_value = sdf(
                        t, sample_period_custom,
                        sigma_custom, &nspikes);
                if ( sdf_buffer )
                        sdf_buffer->push_back( sdf_value);
                if ( shf_buffer )
                        shf_buffer->push_back( shf( t, sample_period_custom));
                if ( nspikes_buffer )
                        nspikes_buffer->push_back( nspikes);
        }

        return (to - from) / sample_period_custom;
}


void
cnrun::SSpikeloggerService::
sync_history() const
{
        if ( !_client->M || (_client->M && _client->M->is_diskless) )
                return;

        ofstream spikecnt_strm( (string(_client->_label) + ".spikes").c_str());
        spikecnt_strm.precision( _client->precision);
        spikecnt_strm << "#spike time\n";

        for ( auto &V : spike_history )
                spikecnt_strm << V << endl;

        if ( _status & CN_KL_COMPUTESDF ) {
                ofstream sdf_strm( (string(_client->_label) + ".sxf").c_str());
                sdf_strm.precision( _client->precision);
                sdf_strm << "#<time>\t<sdf>\t<shf>\t<nspikes>\n";

                vector<double> sdf_vector, shf_vector;
                vector<size_t> nspikes_vector;
                get_sxf_vector( &sdf_vector, &shf_vector, &nspikes_vector,
                                start_delay, 0);

                double t = start_delay;
                for ( size_t i = 0; i < sdf_vector.size(); ++i, t += sample_period )
                        sdf_strm << t << "\t"
                                 << sdf_vector[i] << "\t"
                                 << shf_vector[i] << "\t"
                                 << nspikes_vector[i] << endl;
        }
}


size_t
cnrun::SSpikeloggerService::
n_spikes_since( double since) const
{
        size_t i = 0;
        for ( auto &K : spike_history )
                if ( K > since )
                        return spike_history.size() - i++;
        return 0;
}



// ----- CSynapse

cnrun::C_BaseSynapse::
C_BaseSynapse( TUnitType intype,
               C_BaseNeuron *insource, C_BaseNeuron *intarget,
               double ing, CModel *inM, int s_mask)
      : C_BaseUnit (intype, "overwrite-me", inM, s_mask),
        _source (insource),
        t_last_release_started (-INFINITY)
{
        if ( M )
                M->vp( 5, "Creating a \"%s\" base synapse\n", species());
        _targets.push_back( intarget);
        intarget->_dendrites[this] = ing;
        _source->_axonal_harbour.push_back( this);
        snprintf( _label, max_label_size-1, "%s:1", _source->_label);
}






cnrun::C_BaseSynapse*
cnrun::C_BaseSynapse::
clone_to_target( C_BaseNeuron *tgt, double g)
{
      // check if we have no existing connection already to tgt
        if ( member( tgt, _targets) ) {
                M->vp( 1, stderr, "Neuron \"%s\" already synapsing onto \"%s\"\n",
                       _source->_label, tgt->_label);
                return nullptr;
        }

        tgt -> _dendrites[this] = g;
        _targets.push_back( tgt);

        snprintf( _label, max_label_size-1, "%s:%zu", _source->_label, _targets.size());

        return this;
}




cnrun::C_BaseSynapse*
cnrun::C_BaseSynapse::
make_clone_independent( C_BaseNeuron *tgt)
{
        double g = g_on_target( *tgt);
        if ( !isfinite(g) || !M )
                return nullptr;

        if ( M )
                M->vp( 4, "promoting a clone of %s synapse from \"%s\" to \"%s\"\n",
                       species(), _label, tgt->_label);
        // if ( unlikely (member( tgt, _targets)) )
        //         fprintf( stderr, "ебать!\n");
        _targets.remove( tgt);

        // if ( unlikely (member( this, tgt->_dendrites)) )
        //         fprintf( stderr, "ебать-колотить!\n");
        tgt -> _dendrites.erase( this);

        snprintf( _label, max_label_size-1, "%s:%zu", _source->_label, _targets.size());

        C_BaseSynapse* ret = M -> add_synapse_species(
                _type, _source, tgt, g,
                CModel::TSynapseCloningOption::no /* prevents re-creation of a clone we have just excised */,
                TIncludeOption::is_last);
        // the newly added synapse has stock paramaters yet: copy ours
        if ( ret ) {
                ret->P = P;
                // also see to vars
                for ( size_t i = 0; i < v_no(); ++i )
                        ret->var_value(i) = get_var_value(i);
                return ret;
        }
        return nullptr;
}






void
cnrun::C_BaseSynapse::
dump( bool with_params, FILE *strm) const
{
        C_BaseUnit::dump( with_params);
        fprintf( strm, "  gsyn on targets (%zu):  ", _targets.size());
        for ( auto &T : _targets )
                fprintf( strm, "%s: %g;  ", T->_label, g_on_target( *T));
        fprintf( strm, "\n\n");
}





cnrun::C_BaseSynapse::
~C_BaseSynapse()
{
        if ( M )
                M->vp( 4, "  deleting base synapse \"%s\"\n", _label);

        for ( auto &T : _targets )
                if ( T )
                        T->_dendrites.erase( this);

        if ( _source ) {
                _source->_axonal_harbour.remove( this);
                if ( M )
                        M->vp( 5, "    removing ourselves from \"%s\" axonals (%zu still there)\n",
                               _source->_label, _source->_axonal_harbour.size());
        }
}

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