/*
 *       File name:  aghermann/expdesign/expdesign.cc
 *         Project:  Aghermann
 *          Author:  Andrei Zavada <johnhommer@gmail.com>
 * Initial version:  2010-04-28
 *
 *         Purpose:  experimental design primary classes: CSubject & CExpDesign
 *
 *         License:  GPL
 */


#include <stdarg.h>
#include <string>

#ifdef _OPENMP
# include <omp.h>
#endif

#include "aghermann/globals.hh"
#include "common/config-validate.hh"
#include "expdesign.hh"


using namespace std;
using namespace agh;


const char
        *const agh::CExpDesign::FreqBandNames[metrics::TBand::TBand_total] = {
        "Delta", "Theta", "Alpha", "Beta", "Gamma",
};

double
        agh::CExpDesign::freq_bands[metrics::TBand::TBand_total][2] = {
        {  1.5,  4.0 },
        {  4.0,  8.0 },
        {  8.0, 12.0 },
        { 15.0, 30.0 },
        { 30.0, 40.0 },
};


using agh::confval::SValidator;

agh::CExpDesign::
CExpDesign (const string& session_dir_,
            TMsmtCollectProgressIndicatorFun progress_fun)
      : num_threads              (0),
        // fft_params ({30., 30,}, .25, sigproc::TWinType::hanning, metrics::psd::TFFTWPlanType::estimate),
        // swu_params ({30., 30,}, 1.),
        // mc_params  ({30., 30,}, 30 / 6., .8, 1.5, 0.5, 0.0, 0, .5, .5, 5),
        af_dampen_window_type    (sigproc::TWinType::welch),
        af_dampen_factor         (.95),
        tunables0                (tstep, tlo, thi), // only references here, don't worry
        strict_subject_id_checks (false),
        _id_pool                 (0)
{
        config
                ("ctl_param.step_size",                  &ctl_params0.siman_params.step_size)
                ("ctl_param.boltzmann_k",                &ctl_params0.siman_params.k,                          SValidator<double>::SVFRangeEx( DBL_MIN, 1e9))
                ("ctl_param.t_initial",                  &ctl_params0.siman_params.t_initial,                  SValidator<double>::SVFRangeEx( DBL_MIN, 1e9))
                ("ctl_param.damping_mu",                 &ctl_params0.siman_params.mu_t,                       SValidator<double>::SVFRangeEx( DBL_MIN, 1e9))
                ("ctl_param.t_min",                      &ctl_params0.siman_params.t_min,                      SValidator<double>::SVFRangeEx( DBL_MIN, 1e9))
                ("profile.req_scored_pc",                &profile_common_params0.req_percent_scored,           SValidator<double>::SVFRangeIn( 80., 100.))
                ("psd_param.pagesize",                   &psd_params.pagesize,                                 SValidator<double>::SVFRangeIn( 4., 120.))
                ("psd_param.binsize",                    &psd_params.binsize,                                  SValidator<double>::SVFRangeIn( .125, 1.))
                ("artifacts.dampen_factor",              &af_dampen_factor,                                    SValidator<double>::SVFRangeIn( 0., 1.))
                ("mc_param.mc_gain",                     &mc_params.mc_gain,                                   SValidator<double>::SVFRangeIn( 0., 100.))
                ("mc_param.f0fc",                        &mc_params.f0fc,                                      SValidator<double>::SVFRangeEx( 0., 80.))
                ("mc_param.bandwidth",                   &mc_params.bandwidth,                                 SValidator<double>::SVFRangeIn( 0.125, 2.))
                ("mc_param.iir_backpolate",              &mc_params.iir_backpolate,                            SValidator<double>::SVFRangeIn( 0., 1.))
                ("swu_param.min_upswing_duration",       &swu_params.min_upswing_duration,                     SValidator<double>::SVFRangeIn( 0.01, 1.))
                ("psd_param.welch_window_type",  (int*)  &psd_params.welch_window_type,                        SValidator<int>::SVFRangeIn( 0, (int)sigproc::TWinType_total - 1))
                ("psd_param.plan_type",          (int*)  &psd_params.plan_type,                                SValidator<int>::SVFRangeIn( 0, (int)metrics::psd::TFFTWPlanType_total - 1))
                ("artifacts.dampen_window_type", (int*)  &af_dampen_window_type,                               SValidator<int>::SVFRangeIn( 0, (int)sigproc::TWinType_total - 1))
                ("ctl_param.iters_fixed_t",              &ctl_params0.siman_params.iters_fixed_T,              SValidator<int>::SVFRangeIn( 1, 1000000))
                ("ctl_param.n_tries",                    &ctl_params0.siman_params.n_tries,                    SValidator<int>::SVFRangeIn( 1, 10000))
                ("smp.num_threads",                      &num_threads,                                         SValidator<size_t>::SVFRangeIn( 0, 20))
                ("mc_params.n_bins",                     &mc_params.n_bins,                                    SValidator<size_t>::SVFRangeIn( 1, 100))
                ("profile.swa_laden_pages_before_SWA_0", &profile_common_params0.swa_laden_pages_before_SWA_0, SValidator<size_t>::SVFRangeIn( 1, 100))
                ("mc_param.smooth_side",                 &mc_params.smooth_side,                               SValidator<size_t>::SVFRangeIn( 0, 5))
                ("ctl_param.DBAmendment1",               &ctl_params0.DBAmendment1)
                ("ctl_param.DBAmendment2",               &ctl_params0.DBAmendment2)
                ("ctl_param.AZAmendment1",               &ctl_params0.AZAmendment1)
                ("ctl_param.AZAmendment2",               &ctl_params0.AZAmendment2)
                ("profile.score_unscored_as_wake",       &profile_common_params0.score_unscored_as_wake)
                ("StrictSubjectIdChecks",                &strict_subject_id_checks)
                ("LastUsedVersion",                      &last_used_version);

        char *tmp = canonicalize_file_name(session_dir_.c_str());
        if ( !tmp ) // does not exist
                throw invalid_argument (string ("Failed to canonicalize dir: ") + session_dir_);

        if ( session_dir_ != tmp ) {
                APPLOG_INFO( "canonicalized session_dir \"%s\" to \"%s\"", session_dir_.c_str(), tmp);
                _session_dir.assign( tmp);
        } else
                _session_dir = session_dir_;
        free( tmp);

        if ( _session_dir.size() > 1 && _session_dir[_session_dir.size()-1] == '/' )
                _session_dir.erase( _session_dir.size()-1, 1);

        if ( fs::exists_and_is_writable( session_dir()) == false )
                throw invalid_argument (agh::str::sasprintf( "Experiment directory \"%s\" does not exist or is not writable", _session_dir.c_str()));

        if ( chdir( session_dir()) == -1 )
                throw invalid_argument (agh::str::sasprintf( "Failed to cd to \"%s\"", _session_dir.c_str()));

        load_settings();

        // that's pretty important: scope is not itself exposed to the user
        mc_params.scope = psd_params.pagesize;
        // also, ensure step is equal to pagesize, in all profile params
        psd_params.step = mc_params.step = swu_params.step =
                psd_params.pagesize;

#ifdef _OPENMP
        omp_set_num_threads( (num_threads == 0) ? global::num_procs : num_threads);
        APPLOG_INFO( "SMP enabled with %d threads", omp_get_max_threads());
#endif
        if ( last_used_version != VERSION ) {
                APPLOG_INFO( "Purging old files as we are upgrading from version %s to %s", last_used_version.c_str(), VERSION);
                purge_cached_profiles();
        }
        // last_used_version = VERSION;
        /// leave it so SExpDesignUI::populate will see it and pop the changelog

        scan_tree( progress_fun);
}


void
agh::CExpDesign::
log_message( TLogEntryStyle style, const char* fmt, ...)
{
        va_list ap;
        va_start (ap, fmt);

        string line = str::svasprintf( fmt, ap);
        _error_log.emplace_back( line, style);

        va_end (ap);
}


string
agh::CExpDesign::
error_log_serialize() const
{
        string ret;
        for ( const auto& E : _error_log )
                ret += E.first + '\n';
        return move(ret);
}



void
agh::CExpDesign::
for_all_subjects( const TSubjectOpFun& F, const TSubjectReportFun& report, const TSubjectFilterFun& filter)
{
        vector<tuple<CJGroup*,
                     CSubject*>> v;
        for ( auto& G : groups )
                for ( auto& J : G.second )
                        if ( filter(J) )
                                v.emplace_back( make_tuple(&G.second, &J));
        size_t global_i = 0;
#ifdef _OPENMP
#pragma omp parallel for schedule(guided)
#endif
        for ( size_t i = 0; i < v.size(); ++i ) {
#ifdef _OPENMP
#pragma omp critical
#endif
                {
                        report( *get<0>(v[i]), *get<1>(v[i]),
                                ++global_i, v.size());
                }
// rest of loop, expressly not critical, and so, will be eligible for OMP
                F( *get<1>(v[i]));
        }
}



string
CExpDesign::
make_dirname( TExpDirLevel level, const SExpDirLevelId& level_id) const
{
        switch (level) {
        case TExpDirLevel::transient:
                return "/tmp";

        case TExpDirLevel::session: {
                TJGroups::const_iterator Gi;
                const CSubject& J = subject_by_x(level_id.j, &Gi);
                if ( level_id.g != Gi->first || !J.have_session(level_id.d) )
                        throw invalid_argument ("bad group/subject/session");
                return move(str::sasprintf( "%s/%s/%s/%s", _session_dir.c_str(), Gi->first.c_str(), J.id.c_str(), level_id.d.c_str()));
        }

        case TExpDirLevel::subject: {
                TJGroups::const_iterator Gi;
                const CSubject& J = subject_by_x(level_id.j, &Gi);
                if ( level_id.g != Gi->first )
                        throw invalid_argument ("bad group/subject");
                return move(str::sasprintf( "%s/%s/%s", _session_dir.c_str(), Gi->first.c_str(), J.id.c_str()));
        }

        case TExpDirLevel::group:
                if ( not have_group( level_id.g) )
                        throw invalid_argument ("bad group");
                return move(str::sasprintf( "%s/%s", _session_dir.c_str(), level_id.g.c_str()));

        case TExpDirLevel::experiment:
                return make_dirname();

        case TExpDirLevel::user:
                return move(str::sasprintf( "%s/.local/share/aghermann", getenv("HOME")));

        case TExpDirLevel::system:
                return PACKAGE_DATADIR "/aghermann/";

        default:
                throw invalid_argument ("level not appropriate");
        }
}




void
agh::CExpDesign::
for_all_episodes( const TEpisodeOpFun& F, const TEpisodeReportFun& report, const TEpisodeFilterFun& filter)
{
        vector<tuple<CJGroup*,
                     CSubject*,
                     const string*,
                     SEpisode*>> v;
        for ( auto& G : groups )
                for ( auto& J : G.second )
                        for ( auto& M : J.measurements )
                                for ( auto& E : M.second.episodes )
                                        if ( filter(E) )
                                                v.emplace_back( make_tuple(&G.second, &J, &M.first, &E));
        size_t global_i = 0;
#ifdef _OPENMP
#pragma omp parallel for schedule(guided)
#endif
        for ( size_t i = 0; i < v.size(); ++i ) {
#ifdef _OPENMP
#pragma omp critical
#endif
                {
                        report( *get<0>(v[i]), *get<1>(v[i]), *get<2>(v[i]), *get<3>(v[i]),
                                ++global_i, v.size());
                }
                F( *get<3>(v[i]));
        }
}


void
agh::CExpDesign::
for_all_recordings( const TRecordingOpFun& F, const TRecordingReportFun& report, const TRecordingFilterFun& filter)
{
        vector<tuple<CJGroup*,
                     CSubject*,
                     const string*,
                     SEpisode*,
                     CRecording*>> v;
        for ( auto& G : groups )
                for ( auto& J : G.second )
                        for ( auto& D : J.measurements )
                                for ( auto& E : D.second.episodes )
                                        for ( auto &R : E.recordings )
                                                if ( filter(R.second) )
                                                        v.emplace_back(
                                                                make_tuple (&G.second, &J, &D.first,
                                                                            &E,
                                                                            &R.second));
        size_t global_i = 0;
#ifdef _OPENMP
// read that man, bro
#pragma omp parallel for schedule(guided)
#endif
        for ( size_t i = 0; i < v.size(); ++i ) {
#ifdef _OPENMP
#pragma omp critical
#endif
                {
                        report( *get<0>(v[i]), *get<1>(v[i]), *get<2>(v[i]), *get<3>(v[i]), *get<4>(v[i]),
                                ++global_i, v.size());
                }
                F( *get<4>(v[i]));
        }
}

void
agh::CExpDesign::
for_all_modruns( const TModelRunOpFun& F, const TModelRunReportFun& report, const TModelRunFilterFun& filter)
{
        vector<tuple<CJGroup*,
                     CSubject*,
                     const string*,
                     const SProfileParamSet*,
                     const string*,
                     ach::CModelRun*>> v;
        for ( auto& G : groups )
                for ( auto& J : G.second )
                        for ( auto& D : J.measurements )
                                for ( auto& T : D.second.modrun_sets )
                                        for ( auto& H : T.second )
                                                if ( filter(H.second) )
                                                        v.emplace_back(
                                                                make_tuple (
                                                                        &G.second, &J, &D.first,
                                                                        &T.first,
                                                                        &H.first,
                                                                        &H.second));
        size_t global_i = 0;
#ifdef _OPENMP
#pragma omp parallel for schedule(guided)
#endif
        for ( size_t i = 0; i < v.size(); ++i ) {
#ifdef _OPENMP
#pragma omp critical
#endif
                {
                        report( *get<0>(v[i]), *get<1>(v[i]), *get<2>(v[i]), *get<3>(v[i]), *get<4>(v[i]), *get<5>(v[i]),
                                ++global_i, v.size());
                }
                F( *get<5>(v[i]));
        }
}



list<string>
agh::CExpDesign::
enumerate_groups() const
{
        list<string> recp;
        for ( auto &G : groups )
                recp.push_back( G.first);
        return move(recp);
}

list<string>
agh::CExpDesign::
enumerate_subjects() const
{
        list<string> recp;
        for ( auto &G : groups )
                for ( auto &J : G.second )
                        recp.push_back( J.id);
        return move(recp);
}


list<string>
agh::CExpDesign::
enumerate_sessions() const
{
        list<string> recp;
        for ( auto &G : groups )
                for ( auto &J : G.second )
                        for ( auto &D : J.measurements )
                                recp.push_back( D.first);
        recp.sort();
        recp.unique();
        return move(recp);
}

list<string>
agh::CExpDesign::
enumerate_episodes() const
{
        list<string> recp;
        for ( auto &G : groups )
                for ( auto &J : G.second )
                        for ( auto &D : J.measurements )
                                for ( auto &E : D.second.episodes )
                                        recp.push_back( E.name());
        recp.sort();
        recp.unique();
        return move(recp);
}

list<sigfile::SChannel>
agh::CExpDesign::
enumerate_eeg_channels() const
{
        list<sigfile::SChannel> recp;
/// sigfile::SChannel will rightly not count oddly named channels
/// which, still, were additionally qualified as EEG in the EDF
/// header, so we'd better walk it again and look at signal type
        for ( auto &G : groups )
                for ( auto &J : G.second )
                        for ( auto &D : J.measurements )
                                for ( auto &E : D.second.episodes )
                                        for ( auto &F : E.sources )
                                                for ( size_t h = 0; h < F().n_channels(); ++h )
                                                        if ( F().signal_type(h) == sigfile::SChannel::TType::eeg )
                                                                recp.push_back( F().channel_by_id(h));
        recp.sort();
        recp.unique();
        return move(recp);
}

list<sigfile::SChannel>
agh::CExpDesign::
enumerate_all_channels() const
{
        list<sigfile::SChannel> recp;
        for ( auto &G : groups )
                for ( auto &J : G.second )
                        for ( auto &D : J.measurements )
                                for ( auto &E : D.second.episodes )
                                        for ( auto &F : E.sources ) {
                                                auto Ins = F().channel_list();
                                                recp.insert( recp.end(),
                                                             Ins.begin(), Ins.end());
                                        }
        recp.sort();
        recp.unique();
        return move(recp);
}


list<size_t>
agh::CExpDesign::
used_samplerates( sigfile::SChannel::TType type) const
{
        list<size_t> recp;
        for ( auto &G : groups )
                for ( auto &J : G.second )
                        for ( auto &D : J.measurements )
                                for ( auto &E : D.second.episodes )
                                        for ( auto &F : E.sources )
                                                for ( size_t h = 0; h < F().n_channels(); ++h )
                                                        if ( type == sigfile::SChannel::TType::other or
                                                             type == F().signal_type(h) ) {
                                                                recp.push_back( F().samplerate(h));
                                                        }
        recp.sort();
        recp.unique();
        return move(recp);
}











int
agh::CExpDesign::
setup_modrun( const string& j, const string& d, const string& h,
              const SProfileParamSet& profile_params0,
              agh::ach::CModelRun** Rpp)
{
        try {
                CSubject& J = subject_by_x(j);

                if ( J.measurements[d].size() == 1 && ctl_params0.DBAmendment2 )
                        return CProfile::TFlags::eamendments_ineffective;

                if ( J.measurements[d].size() == 1 && tstep[ach::TTunable::rs] > 0. )
                        return CProfile::TFlags::ers_nonsensical;

                J.measurements[d].modrun_sets[profile_params0].insert(
                        pair<string, ach::CModelRun> (
                                h,
                                ach::CModelRun (
                                        J, d, h,
                                        profile_params0,
                                        ctl_params0,
                                        tunables0))
                        );
                if ( Rpp )
                        *Rpp = &J.measurements[d]
                                . modrun_sets[profile_params0][h];

        } catch (invalid_argument ex) { // thrown by CProfile ctor
                APPLOG_WARN ("setup_modrun( %s, %s, %s): %s", j.c_str(), d.c_str(), h.c_str(), ex.what());
                return -1;
        } catch (out_of_range ex) {
                APPLOG_WARN ("setup_modrun( %s, %s, %s): %s", j.c_str(), d.c_str(), h.c_str(), ex.what());
                return -1;
        } catch (int ex) { // thrown by CModelRun ctor
                APPLOG_WARN ("setup_modrun( %s, %s, %s): %s", j.c_str(), d.c_str(), h.c_str(), CProfile::explain_status(ex).c_str());
                log_message( TLogEntryStyle::plain, "setup_modrun( %s, %s, %s): %s", j.c_str(), d.c_str(), h.c_str(), CProfile::explain_status(ex).c_str());
                return ex;
        }

        return 0;
}


void
agh::CExpDesign::
prune_untried_modruns()
{
        for ( auto& G : groups )
                for ( auto& J : G.second )
                        for ( auto& D : J.measurements )
                        retry_modruns:
                                for ( auto RSt = D.second.modrun_sets.begin(); RSt != D.second.modrun_sets.end(); ++RSt )
                                        for ( auto Ri = RSt->second.begin(); Ri != RSt->second.end(); ++Ri ) {
                                                if ( !(Ri->second.status & ach::CModelRun::modrun_tried) ) {
                                                        RSt->second.erase( Ri);
                                                        if ( RSt->second.empty() )
                                                                D.second.modrun_sets.erase(RSt);
                                                        goto retry_modruns;
                                                }
                                        }
}

void
agh::CExpDesign::
remove_all_modruns()
{
        for ( auto &G : groups )
                for ( auto &J : G.second )
                        for ( auto &D : J.measurements )
                                D.second.modrun_sets.clear();
}


void
agh::CExpDesign::
export_all_modruns( const string& fname) const
{
        FILE *f = fopen( fname.c_str(), "w");
        if ( !f )
                return;

        size_t t = (size_t)ach::TTunable::rs;
        fprintf( f, "#");
        for ( ; t < ach::TTunable::_all_tunables; ++t )
                fprintf( f, "%s%s", (t == 0) ? "" : "\t", ach::tunable_name(t).c_str());
        fprintf( f, "\n");

        for ( auto &G : groups )
                for ( auto &J : G.second )
                        for ( auto &D : J.measurements )
                                for ( auto &RS : D.second.modrun_sets )
                                        for ( auto &R : RS.second )
                                                if ( R.second.status & ach::CModelRun::modrun_tried ) {
                                                        auto& M = R.second;
                                                        string extra;
                                                        switch ( M.P().metric ) {
                                                        case metrics::TType::psd:
                                                                extra = agh::str::sasprintf( "%g-%g Hz", M.P().P.psd.freq_from, M.P().P.psd.freq_upto);
                                                                break;
                                                        case metrics::TType::swu:
                                                                extra = agh::str::sasprintf( "%g Hz", M.P().P.swu.f0);
                                                                break;
                                                        case metrics::TType::mc:
                                                                extra = agh::str::sasprintf( "%g Hz", M.P().P.mc.f0);
                                                                break;
                                                        default:
                                                                throw runtime_error ("What metric?");
                                                        }
                                                        fprintf( f, "# ----- Subject: %s;  Session: %s;  Channel: %s;  Metric: %s (%s)\n",
                                                                 M.subject(), M.session(), M.channel(),
                                                                 M.P().metric_name(), extra.c_str());
                                                        t = ach::TTunable::rs;
                                                        do {
                                                                fprintf( f, "%g%s", M.tx[t] * ach::stock[t].display_scale_factor,
                                                                         (t < M.tx.size()-1) ? "\t" : "\n");
                                                        } while ( t++ < M.tx.size()-1 );
                                                }

        fclose( f);
}






void
agh::CExpDesign::
sync()
{
        for ( auto &G : groups )
                for ( auto &J : G.second )
                        for ( auto &D : J.measurements )
                                for ( auto &E : D.second.episodes )
                                        for ( auto &F : E.sources )
                                                F().save_ancillary_files();
}



int
agh::CExpDesign::
purge_cached_profiles()
{
        return system(
                agh::str::sasprintf(
                        "find '%s' \\( -name '.*.psd' -or -name '.*.mc' -or -name '.*.swu' \\) -delete",
                        session_dir())
                .c_str());
}

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