/*
 *       File name:  aghermann/rk1968/rk1968.cc
 *         Project:  Aghermann
 *          Author:  Andrei Zavada <johnhommer@gmail.com>
 * Initial version:  2010-11-09
 *
 *         Purpose:  scoring assistant
 *
 *         License:  GPL
 */


#include "rk1968.hh"

#include <forward_list>
#include <sstream>
#include <fstream>

extern "C" {
#include <lua.h>
#include <lualib.h>
#include <lauxlib.h>
}

#include "libmetrics/bands.hh"
#include "aghermann/globals.hh"
#include "libsigfile/page.hh"
#include "libsigfile/typed-source.hh"
#include "aghermann/expdesign/recording.hh"
#include "aghermann/expdesign/subject.hh"
#include "aghermann/expdesign/expdesign.hh"


using namespace std;
using namespace agh::rk1968;



CScoreAssistant::
CScoreAssistant (const string& name_,
                 TExpDirLevel level_, CExpDesign& ED_, const SExpDirLevelId& level_id_)
      : CStorablePPack (common_subdir, name_, level_, ED_, level_id_),
        status (0),
        lua_state (nullptr)
{
        if ( load() )
                APPLOG_WARN ("no script loaded from \"%s\"", path().c_str());
}

CScoreAssistant::
~CScoreAssistant ()
{
        save();
        if ( lua_state )
                lua_close( lua_state);
}



int
CScoreAssistant::
load()
{
        script_contents.clear();

        string full_path = path();
        ifstream oleg (full_path);
        char b[888];
        while ( oleg.good() ) {
                auto n = oleg.readsome( b, 888);
                if ( n == 0 )
                        break;
                b[n] = 0;
                script_contents += b;
        }

        return script_contents.empty()
                ? (status |= TFlags::enofile, -1)
                : (status &= ~TFlags::enofile, 0);
}

int
CScoreAssistant::
save() const
{
        string p = path();
        if ( fs::mkdir_with_parents( fs::dirname(p)) ) {
                APPLOG_WARN ("mkdir(\"%s\") failed: %s", p.c_str(), strerror(errno));
                return -1;
        }

        ofstream oleg (p);
        oleg << script_contents;

        return 0;
}


void
CScoreAssistant::
lua_init()
{
        if ( !lua_state ) {
                lua_state = luaL_newstate();
                luaL_openlibs( lua_state);
        }
}


int
CScoreAssistant::
compile()
{
        if ( script_contents.empty() ) {
                APPLOG_WARN ("no script to compile");
                status |= TFlags::ecompile;
                return -1;
        }

        lua_init();
        lua_settop( lua_state, 0);
        int ret1 = luaL_loadbuffer(
                lua_state,
                script_contents.c_str(),
                script_contents.size(),
                path().c_str());
        if ( ret1 ) {
                const char* errmsg = lua_tostring( lua_state, -1);
                specific_error.assign( errmsg);
                APPLOG_WARN ("compilation failed: %s (%d)", errmsg, ret1);
                status |= TFlags::ecompile;
        } else
                status &= ~TFlags::ecompile;

        return ret1;
}


extern "C" {
static int host_get_data( lua_State*);
static int host_mark_page( lua_State*);
}



CScoreAssistant::TScoreErrors
CScoreAssistant::
score( agh::SEpisode& E, int* n_pages_scored_p)
{
        sepisodep = &E;

        if ( compile() )
                return TScoreErrors::no_script;

        if ( !lua_checkstack( lua_state, 3) ) {
                APPLOG_WARN ("Failed to grow stack for 3 elements");
                return TScoreErrors::no_script;
        }

        lua_pushlightuserdata( lua_state, this);
        lua_pushcfunction( lua_state, host_get_data);
        lua_pushcfunction( lua_state, host_mark_page);

        int call_result = lua_pcall(
                lua_state,
                3, // nargsin
                1, // nargsout, expecting the number of pages scored (positive), or some error code (negative)
                0);
        if ( call_result ) {
                APPLOG_WARN ("script call failed (%d)", call_result);
                return TScoreErrors::lua_call_error;
        }

        int ret2 = lua_tointeger( lua_state, -1);
        //lua_pop( lua_state, 1);  // zapping stack anyway

        if ( ret2 >= 0 ) {
                if ( n_pages_scored_p )
                        *n_pages_scored_p = ret2;
        } else {
                specific_error = agh::str::sasprintf("Script signalled error %d", ret2);
                return TScoreErrors::script_error;
        }

        return TScoreErrors::ok;
}



enum TOpcode {
        op_noop = 0,  // 0 is sort of invalid in lua

        // 1. Informational.
        //    The script, it is assumed, will have some storage to
        //    save channel setup data between calls.
        op_get_channel_list,
        op_get_channel_list_of_type,
        op_get_channel_data,

        // 2. Metrics describing a page.
        //    This is supposed to convey, to the script, what a
        //    properly RK1968-trained technician "sees".
        op_get_psd = 100,
        op_get_mc,
        op_get_swu,

        op_get_signal_envelope,
        op_get_page_dirty,
};



extern "C" {

static int
host_get_data( lua_State *L)
{
        int nargsin = lua_gettop(L) - 2;  // the first two being, a CScoreAssistant* and opcode

        auto this_p = (CScoreAssistant*)lua_touserdata( L, 1);
        if ( !this_p ) {
                lua_pushboolean( L, false);
                lua_pushfstring( L, "Target CScoreAssistant object is NULL");
                return 2;
        }
        auto& E = *this_p->sepisodep;

        int opcode = lua_tointeger( L, 2);

        auto make_arity_mismatch_return = [&L, &nargsin, &opcode] ( int correct_nargsin) -> int
                {
                        lua_settop( L, 0);  // now we can push
                        lua_pushboolean( L, false);
                        APPLOG_WARN ("Bad arity for opcode %d (need %d, got %d)", opcode, correct_nargsin, nargsin);
                        lua_pushfstring( L, "Bad arity for opcode %d (need %d, got %d)", opcode, correct_nargsin, nargsin);
                        return 2;
                };
        auto make_error_return = [&L] ( const char* fmt, ...) __attribute__ ((format (printf, 2, 3))) -> int
                {
                        lua_settop( L, 0);  // now we can push
                        lua_pushboolean( L, false);
                        va_list ap;
                        va_start (ap, fmt);
                        string E = agh::str::svasprintf( fmt, ap);
                        APPLOG_WARN ("%s", E.c_str());
                        lua_pushstring( L, E.c_str());
                        va_end (ap);
                        return 2;
                };
#define NEED_ARITY_ATLEAST(A) \
        if ( nargsin < A ) { return make_arity_mismatch_return(A); }
#define NEED_ARITY_EXACT(A) \
        if ( nargsin != A ) { return make_arity_mismatch_return(A); }

        switch ( opcode ) {
        case op_noop:
                return 0;

        case op_get_channel_list: {
                NEED_ARITY_EXACT(0);

                if ( !lua_checkstack( L, 1 + E.recordings.size()) )
                        return make_error_return( "Failed to grow stack for %d elements", 1 + (int)E.recordings.size());

                lua_settop( L, 0);  // now we can push
                lua_pushinteger( L, E.recordings.size());
                for ( auto& H : E.recordings )
                        lua_pushstring( L, H.first.c_str());

                return 1 + E.recordings.size();
        }

        case op_get_channel_list_of_type: {
                NEED_ARITY_EXACT(1);

                const char* type = lua_tostring( L, 3);
                lua_settop( L, 0);

                size_t hh_of_type = 0;
                for ( auto& H : E.recordings )
                        if ( 0 == strcasecmp( H.first.type_s(), type) )
                                ++hh_of_type;
                if ( !lua_checkstack( L, 1 + hh_of_type) )
                        return make_error_return( "Failed to grow stack for %d elements", 1 + (int)hh_of_type);

                lua_pushinteger( L, hh_of_type);
                for ( auto& H : E.recordings )
                        if ( 0 == strcasecmp( H.first.type_s(), type) )
                                lua_pushstring( L, H.first.c_str());

                return 1 + hh_of_type;
        }

        case op_get_channel_data: {
                NEED_ARITY_EXACT(1);

                const char* channel = lua_tostring( L, 3);
                lua_settop( L, 0);

                auto Hi = E.recordings.find( sigfile::SChannel (channel));
                if ( Hi == E.recordings.end() ) {
                        return make_error_return( "No such channel (%s)", channel);
                } else {
                        auto& R = Hi->second;
                        if ( !lua_checkstack( L, 4) )
                                return make_error_return( "Failed to grow stack for 4 elements");
                        lua_pushinteger( L, R.full_pages());
                        lua_pushinteger( L, R.total_pages());
                        lua_pushinteger( L, R.pagesize());
                        lua_pushinteger( L, R.F().samplerate(R.h()));
                        return 4;
                }
        }

        case op_get_psd:
        case op_get_mc :
        case op_get_swu: {
                NEED_ARITY_EXACT(5);

                const char* channel = lua_tostring( L, 3);
                auto Hi = E.recordings.find( sigfile::SChannel (channel));
                if ( Hi == E.recordings.end() ) {
                        return make_error_return( "No such channel (%s)", channel);
                } else if ( !Hi->first.is_fftable() ) {
                        return make_error_return( "Request of profile metric from a non-EEG channel channel %s", channel);
                } else {
                        auto& R = Hi->second;
                        int pa = lua_tointeger( L, 4);
                        int pz = lua_tointeger( L, 5);
                        if ( !(pa <= pz) )
                                return make_error_return( "Invalid (inclusive) range: %d..%d", pa, pz);
                        if ( pa < 0 || (size_t)pa >= R.full_pages() )
                                return make_error_return( "Page %d out of valid range (%zu is last full)", pa, R.full_pages());
                        if ( pz < 0 || (size_t)pz >= R.full_pages() )
                                return make_error_return( "Page %d out of valid range (%zu is last full)", pz, R.full_pages());
                        double fa = lua_tonumber( L, 6);
                        double fz = lua_tonumber( L, 7);
                        if ( fa >= fz )
                                return make_error_return( "Ill-formed frequency range");
                        lua_settop( L, 0);

                        auto C =
                                (opcode == op_get_psd)
                                ? R.psd_profile.course( fa, fz)
                                : (opcode == op_get_mc)
                                ? R.mc_profile.course( fa)
                                : R.swu_profile.course( fa);
                        auto p = pa;
                        if ( !lua_checkstack( L, pz - pa) )
                                return make_error_return( "Failed to grow stack for %d numbers", pz - pa);

                        while ( p <= pz )
                                lua_pushnumber( L, C[p++]);
                        return pz - pa;
                }
        }

        case op_get_signal_envelope: {
                NEED_ARITY_EXACT(4);

                const char* channel = lua_tostring( L, 3);

                auto Hi = E.recordings.find( sigfile::SChannel (channel));
                if ( Hi == E.recordings.end() ) {
                        return make_error_return( "No such channel (%s)", channel);
                } else {
                        auto& R = Hi->second;
                        int p = lua_tointeger( L, 4);
                        if ( p < 0 || (size_t)p >= R.full_pages() )
                                return make_error_return( "Page %d out of valid range (%zu is last full)", p, R.full_pages());

                        double dh = lua_tonumber( L, 5);
                        if ( dh <= 0. || dh > 1e2 )
                                return make_error_return( "Bad scope parameter (%g; expecting it to be in range 0..1e2)", dh);
                        double dt = lua_tonumber( L, 6);
                        if ( dt <= 0. || dt > R.pagesize() )
                                return make_error_return( "Bad dt parameter (%g; expecting it to be in range 0..%zu)", dt, R.pagesize());
                        lua_settop( L, 0);

                        auto sr = R.F().samplerate(R.h());
                        auto raw_profile = sigproc::raw_signal_profile<TFloat>(
                                sigproc::SSignalRef<TFloat> (
                                        R.F().get_region_filtered_smpl(
                                                R.h(),
                                                p * R.pagesize() * sr, (p+1) * R.pagesize() * sr),
                                        sr),
                                dh, dt);

                        if ( !lua_checkstack( L, raw_profile.size()) )
                                return make_error_return( "Failed to grow stack for %zu numbers", raw_profile.size());

                        auto b = 0u;
                        while ( b < raw_profile.size() )
                                lua_pushnumber( L, raw_profile[b++]);
                        return (int)raw_profile.size();
                }
        }

        case op_get_page_dirty: {
                NEED_ARITY_EXACT(2);

                const char* channel = lua_tostring( L, 3);
                auto Hi = E.recordings.find( sigfile::SChannel (channel));
                if ( Hi == E.recordings.end() ) {
                        return make_error_return( "No such channel (%s)", channel);
                } else {
                        auto& R = Hi->second;
                        int p = lua_tointeger( L, 4);
                        lua_settop( L, 0);

                        if ( p < 0 || (size_t)p >= R.full_pages() )
                                return make_error_return( "Page %d out of valid range (%zu is last full)", p, R.full_pages());

                        lua_pushnumber(
                                L,
                                R.F().artifacts(R.h()).region_dirty_fraction(
                                        (p+0) * R.pagesize(),
                                        (p+1) * R.pagesize()));
                        return 1;
                }
        }

        default:
                return make_error_return( "Invalid host opcode %d", opcode);
        }
#undef NEED_ARITY_EXACT
#undef NEED_ARITY_ATLEAST

        return 0;
}




static int
host_mark_page( lua_State *L)
{
        //int nargsin = lua_gettop(L);
        auto this_p = (CScoreAssistant*)lua_touserdata( L, 1);
        if ( !this_p ) {
                lua_settop( L, 0);
                lua_pushboolean( L, false);
                lua_pushfstring( L, "Target CScoreAssistant object is NULL");
                return 2;
        }

        int page      = lua_tonumber( L, 2);
        int scorecode = lua_tonumber( L, 3);

        auto& E = *this_p->sepisodep;
        using namespace sigfile;
        CHypnogram& F1 = E.sources.front();
        if ( page < 0 || (size_t)page >= F1.n_pages() ) {
                APPLOG_WARN ("Page %d out of valid range (%zu is last)", page, F1.n_pages()-1);
                lua_settop( L, 0);
                lua_pushboolean( L, false);
                lua_pushfstring( L, "Invalid page number (%d)", page);
                return 2;
        }
        if ( SPage::score_code((SPage::TScore)scorecode) == '?' ) {
                lua_settop( L, 0);
                lua_pushboolean( L, false);
                lua_pushfstring( L, "Invalid score code (%d)", scorecode);
                return 2;
        }

        F1[page].mark( (SPage::TScore)scorecode);

        return 0;
}

} // extern "C"

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