/* 
 * File:   JuliusServer.cpp
 * Author: hara
 * 
 * Created on 2013/08/21, 10:12
 */

#include <stdio.h>
#include <map>

#include <boost/format.hpp>
#include <boost/date_time/posix_time/posix_time.hpp>

#include "Julius.h"
#include "julius_utils.h"

#define JULIUS_SAFEXML 1

namespace Junius {

// static members
std::map<std::string,int> CJulius::japi_table;

void CJulius::JAPISpeechReady(Recog *recog, void *data)
{
    (reinterpret_cast<CJulius *>(data))->cbApiSpeechReady();
}
void CJulius::JAPISpeechStart(Recog *recog, void *data)
{
    (reinterpret_cast<CJulius *>(data))->cbApiSpeechStart();
}
void CJulius::JAPISpeechEnd(Recog *recog, void *data)
{
    (reinterpret_cast<CJulius *>(data))->cbApiSpeechEnd();
}
void CJulius::JAPIResult(Recog *recog, void *data)
{
    (reinterpret_cast<CJulius *>(data))->cbApiResult(recog);
}

static long _getCurrentMillisecond()
{
    boost::posix_time::ptime pt_epoch(boost::gregorian::date(1970,1,1)); 
    boost::posix_time::ptime pt_now = boost::posix_time::microsec_clock::universal_time();
    boost::posix_time::time_duration durr = pt_now - pt_epoch;
    return durr.total_milliseconds();
}

CJulius::CJulius()
{
    if(japi_table.empty()) {
        // Junius API
        japi_table["getVersion"] = JAPI_VERSION;
        japi_table["getStatus"]  = JAPI_STATUS;
        //japi_table["requesDie"]     = JAPI_DIE;
        japi_table["requesPause"]   = JAPI_PAUSE;
        japi_table["requesTerminate"] = JAPI_TERMINATE;
        japi_table["requesResume"]  = JAPI_RESUME;
//        japi_table["reqClose"]   = JAPI_CLOSE;

        // Julius Legacy API
        japi_table["VERSION"] = JAPI_VERSION;
        japi_table["STATUS"] = JAPI_STATUS;
        
        // for debug command
        japi_table["debug"]  = JAPI_DEBUG;
    }
    
    // if debug
    //j_enable_debug_message();
    //j_enable_verbose_message();

    m_outputInst = NULL;
    bRequestStopThread = false;
}

CJulius::CJulius(const CJulius& orig) {
}

CJulius::~CJulius()
{
    fprintf(stderr, "DEBUG: JuliusServer: destruct\n");
    if(m_recog) {
        j_close_stream(m_recog);
        j_recog_free(m_recog);
    }
    m_recog = NULL;
}

bool
CJulius::_init(Jconf *jconf)
{
    m_recog = j_create_instance_from_jconf(jconf);
    if (m_recog == NULL) {
        fprintf(stderr, "Error: CreateInstance: %s#%d\n", __FILE__, __LINE__);
        return false;
    }
    
    callback_add(m_recog, CALLBACK_EVENT_SPEECH_READY, CJulius::JAPISpeechReady, this);
    callback_add(m_recog, CALLBACK_EVENT_SPEECH_START, CJulius::JAPISpeechStart, this);
    callback_add(m_recog, CALLBACK_EVENT_SPEECH_STOP,  CJulius::JAPISpeechEnd, this);

    callback_add(m_recog, CALLBACK_RESULT, CJulius::JAPIResult, this);

    j_recog_info(m_recog);

    return true;
}

/**
 * 
 * @param jconf_filepath
 * @return 
 */
bool
CJulius::init(int argc, char* argv[])
{
    Jconf *jconf = j_config_load_args_new(argc, argv);
    if(!jconf) {
        fprintf(stderr, "Error: LoadJconf: %s#%d\n", __FILE__, __LINE__);
        return false;
    }

    return _init(jconf);
}

/**
 * 
 * @param jconf_filepath
 * @return 
 */
bool
CJulius::init(const char* jconf_filepath)
{
    //j_put_version(stderr);

    Jconf *jconf = j_config_load_file_new(const_cast<char*>(jconf_filepath));
    if(!jconf) {
        fprintf(stderr, "Error: LoadJconf: %s#%d\n", __FILE__, __LINE__);
        return false;
    }
    
    return _init(jconf);
}

bool CJulius::initPushInput()
{
    /* initialize audio input device */
    /* ad-in thread starts at this time for microphone */
    if (j_adin_init(m_recog) == FALSE) { /* error */
        return FALSE;
    }
    // force null input
    m_recog->adin->ad_standby = NULL;
    m_recog->adin->ad_begin = NULL;
    m_recog->adin->ad_end   = NULL;
    m_recog->adin->ad_read  = NULL;
//    m_recog->adin->ad_input_name = "Push function";

    return RealTimeInit(m_recog);
}

uint32_t CJulius::beginPushInput()
{
    uint32_t ret = 0;
    // UPDATE process
    _recognize_stream_prepare(m_recog);
    
    //
    // 1st pass
    //
    // wave-input/1st-pass: begin
    ret |= _recognize_stream_pass1_begin(m_recog);
    if( (ret & JRECOGSTATUS_PASS1_END_FAIL) == JRECOGSTATUS_PASS1_END_FAIL ) {
        return ret;
    }
    
    return ret;
}

void CJulius::addPushInput(SP16* data, int len)
{
    RealTimePipeLine(data, len, m_recog);
}

uint32_t CJulius::endPushInput()
{
    uint32_t ret = 0;

    // wave-input/1st-pass: end
    ret |= _recognize_stream_pass1_end(m_recog);
    
    // Check terminate signal
    if (m_recog->process_want_terminate) {
        result_error(m_recog, J_RESULT_STATUS_TERMINATE);
        return ret | JRECOGSTATUS_END_BY_REQTERM;
    }
    
    //
    // 2nd pass
    //
    ret |= _recognize_stream_pass2_prepare(m_recog);
    ret |= _recognize_stream_pass2_proc(m_recog);
    ret |= _recognize_stream_pass2_finish(m_recog, ret);

    return ret;
}


bool CJulius::startService()
{
    fprintf(stderr, "DEBUG: JuliusServer: startService\n");
    if (j_adin_init(m_recog) == FALSE) {    /* error */
        return false;
    }
    
    //j_open_stream(j_recog, NULL);
    //j_recog->adin->ad_begin(NULL);
    
//    thread_manager.create_thread(boost::bind(&j_recognize_stream, j_recog));
//    CJuliusServer::Run(j_recog, this);
    if(m_recog->jconf->input.speech_input == SP_ADINNET) {
        thread_manager.create_thread(boost::bind(
                &CJulius::runForRealtimeStream, this, m_recog
                ));
    } else {
        thread_manager.create_thread(boost::bind(
                &CJulius::runForBatchStream, this, m_recog
                ));
    }
    fprintf(stderr, "DEBUG: Start Thread.\n");

    return true;
}

void CJulius::stopService()
{
    fprintf(stderr, "DEBUG: JuliusServer: stopService\n");
//    j_request_terminate(j_recog);
    bRequestStopThread = true;

    m_recog->adin->end_of_stream = TRUE;
    m_recog->adin->ad_end();
    if(m_recog->jconf->input.speech_input == SP_ADINNET)
        adin_tcpip_release();

    thread_manager.join_all(); // wait...
}

/**
 * startServiceから呼ばれる。
 * 
 * @param recog
 * @see CJulius::runForBatchStream
 */
void CJulius::runForRealtimeStream(Recog *recog)
{
    int ret = 0;

    while(!bRequestStopThread && (ret >= 0) ) {
        if(j_adin_status(recog, 500) != 0) {
            //fprintf(stderr, "DEBUG: Adinnet-server timeout.\n");
            continue;
        }
        fprintf(stderr, "DEBUG: Ready input stream: \n");
        ret = j_open_stream(recog, NULL);
        fprintf(stderr, "%d\n", ret);

        fprintf(stderr, "DEBUG: Ready recognition\n");
        ret = j_recognize_stream(recog);
        fprintf(stderr, "DEBUG: Finish rezognition for a stream: %d\n", ret);
       
        ret = j_close_stream(recog);
        recog->adin->ad_end(); // why dont call this function in close_stream??
        fprintf(stderr, "DEBUG: Close input stream: \n");
    }
    
    fprintf(stderr, "DEBUG: End of Thread.\n");
}

/**
 * startServiceから呼ばれる。
 * 
 * @param recog
 * @see CJulius::runForRealtimeStream
 */
void CJulius::runForBatchStream(Recog *recog)
{
    int ret = 0;

    while(!bRequestStopThread && (ret >= 0) ) {
        fprintf(stderr, "DEBUG: Ready input stream: \n");
        ret = j_open_stream(recog, NULL);
        fprintf(stderr, "%d\n", ret);
        if(ret < 0) break;

        fprintf(stderr, "DEBUG: Ready recognition\n");
        ret = j_recognize_stream(recog);

        fprintf(stderr, "DEBUG: Finish rezognition for a stream: %d\n", ret);
       
    }
    fprintf(stderr, "DEBUG: End of Thread.\n");
}

std::string CJulius::CallbackReceiveMessage(char* msg, char* param, CJulius* self)
{
    std::map<std::string,int>::iterator ite;
    ite = japi_table.find(msg);
    
    if(ite == japi_table.end()) {
        fprintf(stderr, "DEBUG: JServer: UnknownCommand: [[%s]]\n", msg);
        return "";
    }

    fprintf(stderr, "DEBUG: JServer: Command: [[%s]]\n", msg);
    
    switch(ite->second){
    case JAPI_VERSION:
        return self->getVersion();
        break;
    case JAPI_STATUS:
        return self->getEngineStatus();
        break;
    case JAPI_DIE:
        self->stopService();
        break;
    case JAPI_PAUSE:
        self->requestEnginePause();
        break;
    case JAPI_TERMINATE:
        self->requestEngineTerminate();
        break;
    case JAPI_RESUME:
        self->requestEngineResume();
        break;
    case JAPI_DEBUG:
        self->debug();
        break;
    default:
        fprintf(stderr, "DEBUG: JServer: UnknownCommandNumber: %d", ite->second);
    }
    
    return "";
}

void CJulius::debug(void)
{
        {
            const char* filename = "sample.le";
            SP16 buf[4096];
            size_t nRead = 0;

            beginPushInput();

            fprintf(stderr, "PipeLine\n");
            FILE *fp = fopen(filename, "rb");
            while ((nRead = fread(buf, sizeof (SP16), 4096, fp)) > 0) {
                fprintf(stderr, "%d, ", (int) nRead);
                addPushInput(buf, nRead);
            }
            fclose(fp);

            endPushInput();
        }

}

std::string CJulius::getVersion(void)
{
    printf("<ENGINEINFO TYPE=\"%s\" VERSION=\"%s\" CONF=\"%s\"/>\n",
                JULIUS_PRODUCTNAME, JULIUS_VERSION, JULIUS_SETUP);

    return (boost::format("<ENGINEINFO TYPE=\"%s\" VERSION=\"%s\" CONF=\"%s\"/>\n.\n")
            % JULIUS_PRODUCTNAME % JULIUS_VERSION % JULIUS_SETUP).str();
}

std::string CJulius::getEngineStatus(void)
{
    if (m_recog->process_active) {
        return "<SYSINFO PROCESS=\"ACTIVE\"/>\n.\n";
    } else {
        return "<SYSINFO PROCESS=\"SLEEP\"/>\n.\n";
    }
}

void CJulius::cbApiSpeechReady(void)
{
    long now = _getCurrentMillisecond();
    std::string str = (boost::format("<INPUT STATUS=\"%s\" TIME=\"%.3f\" />\n.\n")
            % "LISTEN" % ((double)now/1000.0) ).str();
    
    if(m_outputInst) m_outputInst->onData(const_cast<char *>(str.c_str()));
    return ;
}

void CJulius::cbApiSpeechStart(void)
{
    long now = _getCurrentMillisecond();
    std::string str = (boost::format("<INPUT STATUS=\"%s\" TIME=\"%.3f\" />\n.\n")
            % "START" % ((double)now/1000.0) ).str();
    
    if(m_outputInst) m_outputInst->onData(const_cast<char *>(str.c_str()));
    return ;
}

void CJulius::cbApiSpeechEnd(void)
{
    long now = _getCurrentMillisecond();
    std::string str = (boost::format("<INPUT STATUS=\"%s\" TIME=\"%.3f\" />\n.\n")
            % "END" % ((double)now/1000.0) ).str();
    
    if(m_outputInst) m_outputInst->onData(const_cast<char *>(str.c_str()));
    return ;
}

static void
_formatResultRejection(RecogProcess *r, std::stringstream& result)
{
    result << "<REJECTED REASON=\"";
    switch(r->result.status) {
    case J_RESULT_STATUS_REJECT_POWER:
        result << "REJECT_BY_POWER";
        break;
    case J_RESULT_STATUS_TERMINATE:
        result << "STATUS_TERMINATE";
        break;
    case J_RESULT_STATUS_ONLY_SILENCE:
        result << "ONLY_SILENCE";
        break;
    case J_RESULT_STATUS_REJECT_GMM:
        result << "REJECT_BY_GMM";
        break;
    case J_RESULT_STATUS_REJECT_SHORT:
        result << "TOO_SHORT_INPUT";
        break;
    case J_RESULT_STATUS_REJECT_LONG:
        result << "TOO_LONG_INPUT";
        break;
    case J_RESULT_STATUS_FAIL:
        result << "NO_CANDIDATE";
        break;
    }

    result << boost::format(" ID=\"SR%02d\" NAME=\"%s\" />\n.\n")
            % r->config->id % r->config->name;
}

static void
_xmlEscape(std::string* str)
{
    using boost::algorithm::replace_all;
    replace_all(*str, "&",  "&amp;");
    replace_all(*str, "\"", "&quot;");
    replace_all(*str, "\'", "&apos;");
    replace_all(*str, "<",  "&lt;");
    replace_all(*str, ">",  "&gt;");
}

static void
_formatResultWord(RecogProcess *r, Sentence *s, int word_pos, std::stringstream& result)
{
    int j;
    static char buf[MAX_HMMNAME_LEN];

    WORD_ID wid = s->word[word_pos];
    WORD_INFO *winfo = r->lm->winfo;
    
    result << "    <WHYPO";
    result << boost::format(" CM=\"%5.3f\"") % s->confidence[word_pos];
#ifdef JULIUS_SAFEXML
    {
        std::string tmp_word(winfo->woutput[wid]);
        _xmlEscape(&tmp_word);
        result << boost::format(" WORD=\"%s\"") % tmp_word;

        std::string tmp_class(winfo->wname[wid]);
        _xmlEscape(&tmp_class);
        result << boost::format(" CLASSID=\"%s\"") % tmp_class;
    }
#else
    result << boost::format(" WORD=\"%s\"") % winfo->woutput[wid];
    result << boost::format(" CLASSID=\"%s\"") % winfo->wname[wid];
#endif
    result << " PHONE=\"";
    for(j=0; j<winfo->wlen[wid]; j++) {
        center_name(winfo->wseq[wid][j]->name, buf);
        if(j!=0) result << " ";
        result << buf;
    }
    result << "\""; // end of PHONE entity

    if (s->align) {
        SentenceAlign *align;
        for (align = s->align; align; align = align->next) {
            switch(align->unittype) {
            case PER_WORD:	// word alignment
                result << boost::format(" BEGINFRAME=\"%d\" ENDFRAME=\"%d\"")
                        % align->begin_frame[word_pos]
                        % align->end_frame[word_pos];
                break;
            case PER_PHONEME:
            case PER_STATE:
                fprintf(stderr, "Error: \"-palign\" and \"-salign\" does not supported for module output\n");
                break;
            }
        }
    }

    result << "/>\n"; // end of WHYPO tag.
}

static void
_formatResultSentence(RecogProcess *r, int nSentence, std::stringstream& result)
{
    int i;
    Sentence *s = &(r->result.sent[nSentence]);

    result << "  <SHYPO";
    result << boost::format(" RANK=\"%d\"") % (nSentence+1);
    result << boost::format(" SCORE=\"%f\"") % s->score;
#ifdef USE_MBR
    if(r->config->mbr.use_mbr == TRUE) {
        result << boost::format(" MBRSCORE=\"%f\"") % s->score_mbr;
    }
#endif
//    if (r->lmtype == LM_PROB && separate_score_flag) {
    if (r->lmtype == LM_PROB) {
        result << boost::format(" AMSCORE=\"%f\" LMSCORE=\"%f\"") % s->score_am % s->score_lm;
    }
    // output which grammar the best hypothesis belongs to
    if (r->lmtype == LM_DFA) {
        result << boost::format(" GRAM=\"%d\"") % s->gram_id;
    }
    result << ">\n"; // end of "<SHYPO ...>" begin tag.

    int nWords = s->word_num;
    for (i=0; i<nWords; ++i) {
        _formatResultWord(r, s, i, result);

    }

    result << "  </SHYPO>\n";
}

void CJulius::cbApiResult(Recog *recog)
{
    RecogProcess *r;
    std::stringstream result;
    
    fprintf(stderr, ">> cbApiResult\n");

    for(r=recog->process_list; r; r=r->next) {
        if (! r->live) continue;
        
        // 認識結果がエラーならここで出力される.
        if (r->result.status < 0) {
            _formatResultRejection(r, result);
            
            continue; // 次のインスタンスへ.
        }
        
        // 認識結果があるので解析していく.

        // Header
        result << boost::format("<RECOGOUT ID=\"SR%02d\" NAME=\"%s\">\n")
            %r->config->id % r->config->name;

        // Body = Sentences
        int n;
        int nSentences = r->result.sentnum;
        for(n=0; n<nSentences; ++n) {
            _formatResultSentence(r, n, result);
        }
            
        // Footer
        result << "</RECOGOUT>\n.\n";
    } // end of "for" loop.

    if(m_outputInst) m_outputInst->onData(const_cast<char *>(result.str().c_str()));
    return ;
}

} // namespace