/*
Copyright 2023, Arani Chakravarti

This file is part of Homoeopim.

Homoeopim is free software: you can redistribute it and/or modify it under
the terms of the GNU Affero General Public License as published by the 
Free Software Foundation, either version 3 of the License or any later
version.

Homoeopim is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
License for more details.

You should have received a copy of the GNU Affero General Public License
along with Homoeopim. If not, see <https://www.gnu.org/licenses/>.
*/

/* This version of chikitsha_suggest contains code to use the `agenda'
function of CLIPS to form a list of the activations so as to allow a 
doctor to see how exactly the remedies line up with the symptoms. These
details will be displayed below the usual sugestions table. This programme
may be considerably slower than the simpler version. */

#include <iostream>
#include <fstream>
#include <cstdio>
#include <cstring>
#include <cstdlib>

#include <myclips/clips.h>
#include <libpq-fe.h>
#include <regex.h>

#ifndef BEGIN
#define BEGIN(i) {
#define END(i) }
#endif

using namespace std;

/* This structure contains the CLIPS results for sorting and display.
primary_score stores the results evaluated with only the primary 
symptoms declared by the patient. This value will be used to emphasise
those remedies which match with the primary symptoms. */

typedef struct {
int remedy_id;
float score;
float primary_score;
} rem_score;

///////////////////////////////////

class clips
{
private:

public:

//---------------------------------

clips() // constructor
{
InitializeEnvironment();
}

//---------------------------------

inline int CL_Load(char *c) { return(Load(c)); }

inline int CL_Bload(char *c) { return(Bload(c)); }

inline void CL_AssertString(char *c) { AssertString(c); }

inline void CL_Facts() { Facts((char*)"wdisplay",NULL,-1,-1,-1); }

inline void CL_Build(char *c) { Build(c); }

inline void CL_Reset() { Reset(); }

inline void CL_Clear() { Clear(); }

inline void CL_Run() { Run(-1L); }

//----------------------------------

// This is our own function - not in the original lib
inline void CL_get_global_value(char *name,char *retvalue,int retvalue_length)
{
GetDefglobalValueForm(retvalue,retvalue_length,FindDefglobal(name));
}

//----------------------------------

// friend declarations!

friend void InitializeEnvironment();

}; // end of class clips

/////////////////////////////////////

class chikitsha : public clips

BEGIN(0)

private:

char facts[1000][200];
char significant_facts[1000][200];

int num_facts;
int num_significant_facts;
char sql_command[300];

rem_score remedy_score[1500]; // The score array for suggesting remedies

PGconn *conn;
PGresult *res,*res1,*res2,*res3;

int user_id, otp;
int no_of_remedies;

DATA_OBJECT theValue; // CLIPS result storage
void **factPtrArray; // array of fact pointers - since garbage collection is
                      // disabled, these will be persistent during the run
void *facto; // to get at the facts
long int fact_index;
char rule_name[100];
char goda_fact_list[10000];
char agenda_list_table_name[200];

regex_t reegex1, reegex2; // these will be used to eliminate some
                    // fact entries from the display

public:

//--------------------------------

chikitsha() // Constructor

BEGIN(chititsha-0)

user_id = atoi(getenv("WWW_user_id")); // get user id
otp = atoi(getenv("WWW_otp")); // get otp
char connection_auth[100];
sprintf(connection_auth,"host=localhost port=5433 dbname=homoeopim user=homoeopim password=%s",getenv("HOMOEOPIM_DATABASE_PASSWORD"));
conn = PQconnectdb(connection_auth);

// Check whether logged out already

sprintf(sql_command,"select otp from otp where otp=%d",otp);
res = PQexec(conn,sql_command);
if(PQntuples(res) == 0)
{
cout << "<html>\n";
cout << "<head>\n";
cout << "<link rel=\"stylesheet\" href=\"/homoeopim/homoeopim.css\">\n";
cout << "</head>\n";
cout << "<body>\n";
cout << "OTP mismatch - you have probably logged out already!\n";
cout << "</body></html>\n";
PQfinish(conn);
exit(0);
}

 // Get the facts and populate facts array

sprintf(sql_command,"select type,significant_text from patient_details where patient_id = %d",user_id);
res = PQexec(conn,sql_command);
num_facts = PQntuples(res); // get number of facts

for(int i=0;i<num_facts;++i)
BEGIN(chikitsha-1)
sprintf(facts[i],"(%s %s)",PQgetvalue(res,i,0),PQgetvalue(res,i,1)); // form the facts strings
END(chikitsha-1)

// Now for the significant facts

sprintf(sql_command,"select type,significant_text from patient_details where patient_id = %d and whether_primary = 'y'",user_id);
res1 = PQexec(conn,sql_command);

num_significant_facts = PQntuples(res1); // get number of significant facts

for(int i=0;i<num_significant_facts;++i)
BEGIN(chikitsha-sig_1)
sprintf(significant_facts[i],"(%s %s)",PQgetvalue(res,i,0),PQgetvalue(res,i,1)); // form the significant_facts strings
END(chikitsha-sig_1)

IncrementGCLocks(); // disable CLIPS garbage collector

regcomp(&reegex1,"(gender",0); // very standard `symptom' - discard!
regcomp(&reegex2,"(rule_firing_flags",0); // internal fact - discard!

END(chikitsha-0)

//--------------------------------

~chikitsha() // Destructor
{ 
DecrementGCLocks(); // enable CLIPS garbage collector
PQfinish(conn);
}

//-------------------------------

void assert_gender()
{
char cl[50];
sprintf(sql_command,"select gender from patient where id=%d",user_id);
res2 = PQexec(conn,sql_command);
sprintf(cl,"(gender %s)",PQgetvalue(res2,0,0));
CL_AssertString(cl);
}

//---------------------

void assert_facts_list()

BEGIN(assert_facts_list-0)

for(int i=0;i<num_facts;++i)
BEGIN(assert_facts_list-1)
CL_AssertString(facts[i]);
END(assert_facts_list-1)

END(assert_facts_list-0)

//--------------------------------

void assert_significant_facts_list()

BEGIN(assert_significant_facts_list-0)

for(int i=0;i<num_significant_facts;++i)
BEGIN(assert_significant_facts_list-1)
CL_AssertString(significant_facts[i]);
END(assert_significant_facts_list-1)

END(assert_significant_facts_list-0)

//--------------------------------

void read_scores(int flag) // form a list of remedy scores, if flag is
	// 0 then save in score, otherwise save in primary_score

BEGIN(read_scores-0)

char retvalue[1000];
char score_variable_name[500];
char dummy[500]; // keeps unused first part of name

sprintf(sql_command,"select id from remedy order by id");
res = PQexec(conn,sql_command);
no_of_remedies = PQntuples(res); // find no. of remedies

for(int i = 0; i < no_of_remedies; ++i) // read in remedy scores
BEGIN(read_scores-1)
sprintf(score_variable_name,"score_remedy_id_%s",PQgetvalue(res,i,0));
CL_get_global_value(score_variable_name,retvalue,100);
remedy_score[i].remedy_id = atoi(PQgetvalue(res,i,0));
if(flag == 0)
{ sscanf(retvalue,"%[^=]= %f",dummy,&remedy_score[i].score); }
else
{ sscanf(retvalue,"%[^=]= %f",dummy,&remedy_score[i].primary_score); }
END(read_scores-1)

END(read_scores-0)

//-----------------------------------------

//simple bubble sort 

void sort_scores()

BEGIN(sort_scores-0)
int start_remedy_num = 0;
int max_iii = start_remedy_num;;
rem_score max,tmp;
while(start_remedy_num < no_of_remedies)
BEGIN(sort_scores-1)
max = remedy_score[start_remedy_num]; // will this simple copy work?
for(int iii=start_remedy_num+1;iii<(no_of_remedies-1);++iii)
BEGIN(sort_scores-2)
if(max.score < remedy_score[iii].score)
BEGIN(sort_scores-3)
max = remedy_score[iii];
max_iii = iii;
END(sort_score-3)
END(sort_score-2) // maximum has been found
// interchange to place max at the top
tmp = remedy_score[start_remedy_num];
remedy_score[start_remedy_num] = max;
remedy_score[max_iii] = tmp;
++start_remedy_num;
END(sort_score-1)

cout << "<table border>";
cout << "<tr><td><i><b>Sl. No.</i></b></td><td><i><b>Remedy id.</b><i></td><td><i><b>Remedy</b></i></td><td><i><b>Score</b></i></td></tr>\n";
int sl_no = 0;
for(int i=0;i<no_of_remedies;++i)
{
if((remedy_score[0].score > 0) && (remedy_score[i].score >= remedy_score[0].score/10.0))
{
sprintf(sql_command,"select remedy from remedy where id = %d",remedy_score[i].remedy_id);
res = PQexec(conn,sql_command);

if(remedy_score[i].primary_score > 0) // emphasise
BEGIN(sort_scores-2)
cout << "<tr><td align=right><font color=DarkOliveGreen>" << ++sl_no << "</font></td><td align=right>" << remedy_score[i].remedy_id << "</td><td><font color=red><blink>" << PQgetvalue(res,0,0) << "</blink></font></td><td>" << remedy_score[i].score << "</td></tr>";
END(sort_scores-2)
else
BEGIN(sort_scores-3)
cout << "<tr><td align=right><font color=DarkOliveGreen>" << ++sl_no << "</font></td><td align=right>" << remedy_score[i].remedy_id << "</td><td>" << PQgetvalue(res,0,0) << "</td><td>" << remedy_score[i].score << "</td></tr>";
END(sort_scores-3)
}
}
cout << "</table>";

END(sort_scores-0)

//--------------------------------

//This is completely experimental!
void get_agenda_in_database()
{
char buf[1000];
void *acto;
char new_sql_command[10100];
char goda_remedy_list[10000];

// This is a bad hack to get at facts via indices - can't find the 
// appropriate function from the CLIPS apg!

GetFactList(&theValue,NULL);
num_facts = GetDOLength(theValue); // How many facts are there?
// There was an error in the index range previously - it was set 
// to num_facts instead of num_facts+1 caused some heartburn!
factPtrArray = new void*[num_facts]; // an array of fact pointers
facto = NULL;
for(int i =0; i<num_facts; ++i)
{
facto = GetNextFact(facto);
factPtrArray[FactIndex(facto)] = facto; // store the pointers in the indexed array
GetFactPPForm(buf,1000,facto);
sscanf(buf,"f-%ld",&fact_index);
}

// checking activations!

// First create a temporary table for keeping the activations
ifstream in("/dev/urandom");
unsigned short int puchkay;
in >> puchkay;
sprintf(agenda_list_table_name,"agenda%hu",puchkay); 
sprintf(sql_command,"create temporary table %s (defrule text,remedy_list text,fact_list text)",agenda_list_table_name);
PQexec(conn,sql_command);

acto = NULL; // get the activation agenda
acto = GetNextActivation(acto);
while(acto != NULL)
{
GetActivationPPForm(buf,1000,acto);
strcpy(rule_name,give_rule_name(buf));
sprintf(new_sql_command,"select defrule,remedy,score from defrule_remedy_map where defrule = '%s' order by remedy",rule_name);
res3 = PQexec(conn,new_sql_command);
goda_remedy_list[0] = '\0'; // initialise empty string;
strcat(goda_remedy_list,"<ul><li>");
for(int i=0;i<(PQntuples(res3)-1);++i) // form the goda remedy name string with scores
{
strcat(goda_remedy_list,PQgetvalue(res3,i,1));
strcat(goda_remedy_list," (");
strcat(goda_remedy_list,PQgetvalue(res3,i,2));
strcat(goda_remedy_list,")<li> ");
}
strcat(goda_remedy_list,PQgetvalue(res3,PQntuples(res3)-1,1));
strcat(goda_remedy_list," (");
strcat(goda_remedy_list,PQgetvalue(res3,PQntuples(res3)-1,2));
strcat(goda_remedy_list,")</ul>");

//debug
sprintf(new_sql_command,"insert into %s (defrule,remedy_list,fact_list) values ('%s','%s','%s')",agenda_list_table_name,rule_name,goda_remedy_list,give_facts_list_pretty(buf));
//sprintf(new_sql_command,"insert into %s (defrule,remedy_list,fact_list) values ('%s','%s','%s')",agenda_list_table_name,rule_name,goda_remedy_list,give_facts_list(buf));
//debug
//printf("give_facts_list_pretty(%s) is %s",buf,give_facts_list_pretty(buf));
//fflush(stdout);
PQexec(conn,new_sql_command);
acto = GetNextActivation(acto);
}
}

//............................

char* give_rule_name(char *buf)
{
char padding[200];
sscanf(buf,"0%[ ]%[^:]",padding,rule_name);
return(rule_name);
}

//..........................

char* give_facts_list(char *buf)
{
int fact_index_start = 0;
while(buf[fact_index_start] != ':')
{ ++fact_index_start; }
return (buf+fact_index_start+1);
}

//..........................

char* give_facts_list_pretty(char *buf)
{
char fList[5000]; // temporary storage for facts
char single_fact[1000]; // space for a single fact
int index; // to keep n from f-n
goda_fact_list[0] = '\0'; // initialise string
strcat(goda_fact_list,"<ul>");
strcpy(fList,give_facts_list(buf)); // make a local copy 
//debug 
//printf("fList after strcpy = %s<br>",fList);
//fflush(stdout);
char faltu[2000]; // just a buffer to throw away the front part
//debug - originally i4 was set to 0 - caused segfault at 427
int i4 = 0;
while(i4 < (strlen(fList)-1))
{
sscanf(fList,"%[^f]f-%d", faltu,&index);
GetFactPPForm(single_fact,1000,factPtrArray[index]);
//debug
//printf("single_fact after GetFactPPForm is %s<br>",single_fact);
//fflush(stdout);
// We discard facts with certain patterns from the display
if((regexec(&reegex1,single_fact,0,NULL,0) != 0) && 
   (regexec(&reegex2,single_fact,0,NULL,0) != 0))
{
strcat(goda_fact_list,"<li>");
strcat(goda_fact_list,single_fact);
//debug
//printf("goda_fact_list is %s<br>",goda_fact_list);
//fflush(stdout);
}
int j;
for(j=0;fList[j] !='f';++j)
{
fList[j] = ' '; 
++i4;
} // clear front portion of fList
fList[j] = ' '; // clear the comma too
++i4;
}
strcat(goda_fact_list,"</ul>");
return(goda_fact_list);
}

//--------------------------

void print_activation_matches()
{
cout << "<table border width=99%>\n";
sprintf(sql_command,"select defrule,remedy_list,fact_list from %s",agenda_list_table_name);
res = PQexec(conn,sql_command);
cout << "<tr><td><i><b>defrule and facts</b></i></td><td><b><i>remedy list</i></b></td></td></tr>\n";
for(int i=0;i<PQntuples(res);++i)
{
cout << "<tr>\n";
cout << "<td valign=top><font color=DarkOliveGreen>" << PQgetvalue(res,i,0) << "</font><br>" << PQgetvalue(res,i,2) << "</td><td valign=top>" << PQgetvalue(res,i,1) << "</td>\n";
cout << "</tr>\n";
}
cout << "</table>\n";
}

//--------------------------

void print_matrix() // completely experimental as per Pook's demand
BEGIN(print_matrix_0)
char tmp_name[200];
char short_name[50];
int start,end;
int j;
int selected_remedy_indices[1000]; // keeps the indices of the displayed remedies
int no_of_selected_remedies; // total number of remedies displayed

cout << "<table border>\n";
cout << "<tr>\n";
cout << "<td>Defrule and facts-list</td>\n";
no_of_selected_remedies = 0;
for(int i=0;i<no_of_remedies;++i)
BEGIN(print_matrix_1)
if((remedy_score[0].score > 0) && (remedy_score[i].score >= remedy_score[0].score/10.0))
BEGIN(print_matrix_2)
selected_remedy_indices[no_of_selected_remedies] = i;
++no_of_selected_remedies;
sprintf(sql_command,"select remedy from remedy where id = %d",remedy_score[i].remedy_id);
res = PQexec(conn,sql_command);
strcpy(tmp_name,PQgetvalue(res,0,0)); // We try to extract the short names
end = strlen(tmp_name)-2; // leave out the closing bracket
start = end;
j = 0;
while(tmp_name[start] != '(')
{ start--; }
start++; // leave out the opening bracket
for(int i=start;i<=end;++i)
BEGIN(print_matrix_3)
short_name[j] = tmp_name[i];
++j;
END(print_matrix_3)
short_name[j+1] = '\0'; // short_name now contains the short name
cout << "<td align=center valign=center><div class=ghur>"<<short_name<<"</div></td>\n";
END(print_matrix_2)
END(print_matrix_1)
cout << "</tr>\n";
sprintf(sql_command,"select defrule,remedy_list,fact_list from %s",agenda_list_table_name);
res = PQexec(conn,sql_command);
int nnnn = PQntuples(res);
for(int i=0;i<nnnn;++i)
BEGIN(print_matrix_4)
cout << "<tr><td>" << PQgetvalue(res,i,2) << "</td>\n"; // fact_list
for(int j=0;j<no_of_selected_remedies;++j)
BEGIN(print_matrix_5)
sprintf(sql_command,"select score from defrule_remedy_map where defrule='%s' and remedy_id=%d",PQgetvalue(res,i,0),remedy_score[selected_remedy_indices[j]].remedy_id);
res1 = PQexec(conn,sql_command);
if(PQntuples(res1) == 0) // no hit
{ cout << "<td>" << /*sql_command <<*/ "</td>\n"; } // leave blank
else
{
int iscore = atoi(PQgetvalue(res1,0,0));
if(iscore < 15)
 { cout << "<td>" << iscore << "</td>"; } // show the score
else if((iscore >= 15) && (iscore < 25))
 { cout << "<td><font color=blue>" << iscore << "</font></td>"; }
else if((iscore > 25) && (iscore < 35))
 { cout << "<td><font color=red>" << iscore << "</font></td>"; }
else if(iscore > 35)
 { cout << "<td><font color=DarkOliveGreen>" << iscore << "</font></td>"; }
}
END(print_matrix_5)
cout << "</tr>\n";
END(print_matrix_4)
cout << "</table>\n";
END(print_matrix_0)

//--------------------------

END(0);


////////////////////////////////////

int main(void)

BEGIN(main-0)

cout << "Content-type: text/html\n\n";

chikitsha daktaar;

// Evaluation

cout << "<html>\n";
cout << "<head>\n";
cout << "<link rel=\"stylesheet\" href=\"/homoeopim/homoeopim.css\">\n";
cout << "<style>\n";
cout << ".ghur {\n";
cout << "height: 30px;\n";
cout << "width: 40px;\n";
cout << "rotate: -90deg;\n";
cout << "}\n";
cout << "body { background:PaleGoldenRod;color:brown }\n";
cout << "</style>\n";
cout << "</head>\n";
cout << "<body>\n";
daktaar.CL_Bload((char*)"/usr/lib/cgi-bin/homoeopim/chikitsha.bclp"); // load clips file
daktaar.CL_Reset(); // allows deffacts to be loaded
daktaar.assert_gender();
daktaar.assert_facts_list(); // assert facts

// Completely experimental section for accessing agenda
daktaar.get_agenda_in_database(); // creates agenda_list

// End of completely experimental section for accessing agenda

daktaar.CL_Run(); // run clips
cout << "<div align=center>\n";
cout << "<h3>Suggested Remedies</h3>";
daktaar.read_scores(0);
daktaar.CL_Reset();
daktaar.assert_gender();
daktaar.assert_significant_facts_list();
daktaar.CL_Run();
daktaar.read_scores(1);
daktaar.sort_scores();
cout << "</div>\n";
cout << "<div align=left>\n";
cout << "<h3>Activation matches</h3>";
//debug -- how to speed up?
daktaar.print_activation_matches(); // completely experimental!
// This is where maximum delay occurs!
daktaar.print_matrix(); // completely experimental!
cout << "</body></html>\n";
END(main-0)
