#include <SWI-Prolog.h>
#include <stdio.h>
#include <string.h>
#include <glpk.h>
#include <math.h>
#include <stdbool.h>



#define FUNCTOR_plus2       PL_new_functor(PL_new_atom("+"), 2)
#define FUNCTOR_minus2      PL_new_functor(PL_new_atom("-"), 2)
#define FUNCTOR_times2      PL_new_functor(PL_new_atom("*"), 2)
#define FUNCTOR_divide2     PL_new_functor(PL_new_atom("/"), 2)
#define FUNCTOR_pow2	    PL_new_functor(PL_new_atom("^"), 2)
#define FUNCTOR_LO	    PL_new_functor(PL_new_atom(">="), 2)
#define FUNCTOR_UP	    PL_new_functor(PL_new_atom("=<"), 2)
#define FUNCTOR_BD 	    PL_new_functor(PL_new_atom("="), 2)


void Type(term_t u) {
    char message[100] = "";

if (PL_is_variable(u)) {
	strcat(message, "Variable, ");
}
if (PL_is_ground(u)) {
	strcat(message, "Ground, ");
}
if (PL_is_atom(u)) {
	strcat(message, "Atom, ");
}
if (PL_is_integer(u)) {
	strcat(message, "Integer, ");
}
if (PL_is_string(u)) {
	strcat(message, "String, ");
}
if (PL_is_float(u)) {
	strcat(message, "Float, ");
}
if (PL_is_list(u)) {
	strcat(message, "List, ");
}
if (PL_is_dict(u)) {
	strcat(message, "Dict, ");
}
if (PL_is_pair(u)) {
	strcat(message, "Pair, ");
}
if (PL_is_atomic(u)) {
	strcat(message, "Atomic, ");
}

if (PL_is_compound(u)) {
	strcat(message, "Compound, ");
}
if (PL_is_acyclic(u)) {
	strcat(message, "Acyclic, ");
}
if (PL_is_callable(u)) {
	strcat(message, "Callable, ");
}
printf("%s\n", message);
}

void Show (term_t t) { 
    functor_t functor;
    int arity, len, n;
    char* name;
  char *s;
  switch( PL_term_type(t) )
  { case PL_VARIABLE: {printf("Variable"); break;}
    case PL_ATOM: {printf("atom"); break;}
    case PL_INTEGER: {printf("integer"); break;}
    case PL_FLOAT: {
      PL_get_chars(t, &s, CVT_ALL);
      printf("%s", s);
      break;}
    case PL_STRING:{
      PL_get_string_chars(t, &s, &len);
      printf("\"%s\"", s);
      break;}
    case PL_TERM:
    { term_t a = PL_new_term_ref();
      PL_get_name_arity(t, &name, &arity);
      printf("%s(", PL_atom_chars(name));
      for(n=1; n<=arity; n++)
      { PL_get_arg(n, t, a);
if ( n > 1 )
          printf(", ");
        pl_display(a);
      }
      printf(")");
      break;
    }
    default:
PL_fail;
    }

  PL_succeed;
}







typedef struct node {
    int is_terminal; 
    int j;
    struct node* edges[10];
} node;

const int inf = 1e9;

node* root = NULL;
size_t maxVarId = 0;
size_t ar_size = 0;
size_t row_number = 0;
double* ar;
int* ia;
int* ja;
glp_prob *lp;
size_t last_symbol = 1;
char* s;
size_t len;

node* newNode() {
  node* ans = (node*)calloc(1, sizeof(node));
  if (ans == NULL) {
      printf("There's no memory");
      PL_fail;
  }
  return ans;
}

void init() {
    root = newNode();
    maxVarId = 0;
    ar_size = 0;
    last_symbol = 1;
    row_number = 0;
    return;
}


void FreeNodes(node* x) {
    if (x == NULL)
        return;
    for (int i = 0; i < 10; ++i) {
        FreeNodes(x->edges[i]);
    }
    free(x);
}




int GetColId(char* nameInProlog) {
    // printf("A quary for \'%s\' name\n", nameInProlog);
    node* itr = root;
    for (size_t i = 1; i < strlen(nameInProlog); ++i) {
        if (nameInProlog[i]-'0' < 0 || nameInProlog[i]-'0' > 9) {
            printf("Error in nameInProlog\n");
            PL_fail;
        }
        if (itr->edges[nameInProlog[i]-'0'] == NULL) {
            itr->edges[nameInProlog[i]-'0'] = newNode();
    	}
    	itr = itr->edges[nameInProlog[i]-'0'];
    }
    if (itr->is_terminal == 0) {
        itr->is_terminal = 1;
        
        itr->j = ++maxVarId;   
        // printf("Added var: %d, %s\n", itr->j, nameInProlog);
        // glp_set_col_bnds(lp, itr->j, GLP_LO, 0.0, 0.0); // all Vars must be >= 0
    }
    return itr->j;
}



char* GetName(term_t term) {
char* name_for_now; // And for ever, since Prolog do not store the variables as X/Y/Z

if (PL_get_chars(term, &name_for_now, CVT_VARIABLE | CVT_WRITE | REP_UTF8)) {
    // printf("Bad Var: %s\n", name_for_now);
    return name_for_now;
} else {
    printf("\nFailed to get variable name.\n");
    PL_fail;
}
}

void ReadVars(term_t itr, int is_obj) {
if (PL_is_compound(itr)) {
    functor_t f;
    if  (PL_get_functor(itr, &f)) {
        if (f == FUNCTOR_times2 || f == FUNCTOR_plus2 || f == FUNCTOR_minus2) {
            term_t arg1 = PL_new_term_ref();
            term_t arg2 = PL_new_term_ref();
            if (PL_get_arg(1, itr, arg1) && PL_get_arg(2, itr, arg2)) {
                ReadVars(arg1, is_obj);
                ReadVars(arg2, is_obj);
                return;
            }
        } else {
            printf("Unknown functor");
        }
    }
}
if (PL_is_variable(itr)) {
    if (!is_obj)
    	++ar_size;
    // printf("Read: %d -> %s\n", ar_size, GetName(itr));
    GetColId(GetName(itr));
}
}


void AddingExprToLP(term_t itr, int row_id, int negative, int is_obj) {
if (PL_is_compound(itr)) {
    functor_t f;
    if  (PL_get_functor(itr, &f)) {
        term_t arg1 = PL_new_term_ref();
        term_t arg2 = PL_new_term_ref();
        if (f == FUNCTOR_times2 ){
            if (PL_get_arg(1, itr, arg1) && PL_get_arg(2, itr, arg2)) {
                if (PL_is_integer(arg1) || PL_is_float(arg1)) {
                    int id = GetColId(GetName(arg2));
                    double val;
                    if (PL_is_integer(arg1)) {
                        int i;
                        PL_get_integer(arg1, &i);
                        val = (double)i;
                    } else 
                        PL_get_float(arg1, &val);
                    if (negative)
                        val *= -1;
                    if (!is_obj) {
                        ia[last_symbol] = row_id;
                        ja[last_symbol] = id;
                        ar[last_symbol] = val;
                        ++last_symbol;
                        // printf("Added: (i: %d, j: %d, v: %lf)\n", row_id, id, val);
                    } else {
                        // printf("%lf x #%d", val, id);
                        glp_set_obj_coef(lp, id, val);
                    }
                } else 
                    printf("Please use const*Var notation");
            }
        } else if (f == FUNCTOR_plus2) {
            if (PL_get_arg(1, itr, arg1) && PL_get_arg(2, itr, arg2)) {
                // printf("(");
                AddingExprToLP(arg1, row_id, 0, is_obj);
                // printf("+");
                AddingExprToLP(arg2, row_id, 0, is_obj);
                // printf(")");
            }
        } else if (f == FUNCTOR_minus2) {
            if (PL_get_arg(1, itr, arg1) && PL_get_arg(2, itr, arg2)) {
                // printf("(");
                AddingExprToLP(arg1, row_id, 0, is_obj);
                // printf("-");
                AddingExprToLP(arg2, row_id, 1, is_obj);
                // printf(")");
            }
        } else {
            printf("Unknown functor");
        }
    }
} else if (PL_is_variable(itr)) {
    int id = GetColId(GetName(itr));
    if (!is_obj) {
        ia[last_symbol] = row_id;
        ja[last_symbol] = id;
        if (negative)
            ar[last_symbol] = -1;
        else 
            ar[last_symbol] = 1;
        ++last_symbol;
    } else {
        if (negative)
            glp_set_obj_coef(lp, id, -1); //, printf("-1*");
        else 
            glp_set_obj_coef(lp, id, 1); //, printf("1*");
        // printf("[%d]", id);
    }
}
}


bool is_digit(char x) {
    return ('0' <= x && x <= '9');
} 

double GetDoubleFromS_ABS(size_t *i) {
    double ans = 0;
    while ((*i) < len && is_digit(s[(*i)])) {
        ans *= 10;
        ans += s[(*i)] - '0';
        ++(*i);
    }
    if ((*i) < len && s[(*i)] == '.') {
        ++(*i);
        double pw = 1./10;
        while ((*i) < len && is_digit(s[(*i)])) {
            ans += pw * (s[(*i)] - '0');
            pw *= 1./10;
            ++(*i);
        }
    }
    return ans;
}



/** 	                list of integers vars       Min Value (return)		  Status from glpk
                list of constrains    |  objective func    | List of asked vars (return)  |
                         |            |         |            |             |		  |
                         V            V         V            V             V              V
*/
foreign_t pl_expr(term_t cons, term_t ints, term_t obj, term_t _mn, term_t _ans, term_t _status) {
    init();
    
    glp_term_out(GLP_OFF);
    term_t head = PL_new_term_ref();
    term_t tail = PL_new_term_ref();
    
    lp = glp_create_prob();
    glp_set_prob_name(lp, "BSP-2");
    glp_set_obj_dir(lp, GLP_MIN);

/// REPLACMENT 
    
    
//////-----------------------------------------------
///     |
///     V

    // 

// printf("Going through cons:\n");
term_t head_cons = PL_new_term_ref();
term_t tail_cons = PL_copy_term_ref(cons);

row_number = 1;
while (PL_get_list(tail_cons, head_cons, tail_cons)) {
    PL_get_string_chars(head_cons, &s, &len);
    
    for (int i = 0; i < len; ++i) {
        if (s[i] == '_') {
            // Find a variable!
            ++ar_size; 
            int beg = i; // Mark '_' as the beginning of the Variavble name
            ++i; // Skip '_'
            while (i < len && is_digit(s[i])) { // Read the Variable name
                ++i;
            }
            // Creating a string that contains the Variable name
            char *var = (char*)malloc((i - beg + 1) * sizeof(char));
            // Putting the Variable name
            strncpy(var, s + beg, (i - beg));
            var[i - beg] = '\0'; // Add null terminator
            // Calling the Trie
            // printf("Read var: %s\n", var);
            GetColId(var);
            // Make i be the pointer to the last read symbol.
            --i;
        } else if (s[i] == ',')
            ++row_number;
    }
    ++row_number;
}
///      ^
///      |
///------------------------------------------------------
    
    ReadVars(obj, 1);

    tail = PL_copy_term_ref(ints);
    while (PL_get_list(tail, head, tail)) {
        if (PL_is_variable(head)) {
                GetColId(GetName(head)); // Just to add vars to Trie
        } else {
            printf("Ints must be the list of variables");
            PL_fail;
        }
    }
    if (!PL_get_nil(tail)) {
        printf("The list is not properly terminated.\n");
        PL_fail;
    }
    
    
    // printf("Cons: %ld, Vars: %ld\n", row_number, maxVarId);
    row_number = (row_number == 0) ? 1 : row_number;
    maxVarId = (maxVarId == 0) ?  1 : maxVarId;
    // This needs to create a problem.
        
    // printf("Row number: %d\n Array size: %d\n, Column size: %d", row_number, ar_size, maxVarId);
    glp_add_rows(lp, row_number);
    
    
    size_t row_id = 1;
    glp_add_cols(lp, maxVarId);
    ia = (int*)malloc((100+ar_size) * sizeof(int));
    ja = (int*)malloc((100+ar_size) * sizeof(int));
    ar = (double*)malloc((100+ar_size) * sizeof(double));
    for (int i = 1; i <= maxVarId; ++i) {
        // printf("Added %d\n", i);
        // glp_set_col_kind(lp, i, GLP_CV);
        glp_set_col_bnds(lp, i, GLP_FR, 0, 0); // Making variables be whatever they want
    }
    
/// Replacment: ---------------------------
    tail_cons = PL_copy_term_ref(cons);
    while (PL_get_list(tail_cons, head_cons, tail_cons)) {
        bool negative = false;
        int flag = 42;
        double bound = 0;
        bool is_RHS = false;
        PL_get_string_chars(head_cons, &s, &len);
        // printf("String got is: %s\n", s);
        for (size_t i = 0; i < len; ++i) {
            // If a new term of a constrint: 
            if (s[i] == '+' || s[i] == '-' || is_digit(s[i]) || s[i] == '_') {
                if (s[i] == '-')
                    negative = true;
                else 
                    negative = false;
                if (s[i] == '+' || s[i] == '-')
                    ++i; // skipping the sign of the number / variable
                // num is -1 if it is on RHS (don't forget, that it could be -_xxx)
                double num = (is_RHS ? -1 : 1) * (negative ? -1 : 1);
                if (s[i] != '_') {
                    // Skipping space from sign till number 
                    while (i < len && !is_digit(s[i])) {
                        ++i;
                    }
                    // Parsing the number
                    num = GetDoubleFromS_ABS(&i);
                    // Applying sign and moving to LHS
                    num *= (negative ? -1 : 1);
                    num *= (is_RHS ? -1 : 1);
                    // Skipping space from number till next char
                    while (i < len && s[i] == ' ') {
                        ++i;
                    }
                    // means if we met number without further multiplied variable 
                    if (s[i] != '*') {
                        // As number now in LHS, to return it to RHS, it's needed to multiply with (-1).
                        num *= (-1);
                        bound += num;
                        --i; // pointing to the last read symbol
                        continue;
                    }
                    ++i; // skipping '*'
                    // Skipping chars till '_'
                    while (i < len && s[i] != '_') {
                        ++i;
                    }
                }
                int beg = i; // Mark '_' as the beginning of the Variavble name
                ++i; // Skip '_'
                while (i < len && is_digit(s[i])) { // Read the Variable name
                    ++i;
                }
                // Creating a string that contains the Variable name
                char *var = (char*)malloc((i - beg) * sizeof(char));
                // Putting the Variable name
                strncpy(var, s + beg, (i - beg));
                var[i - beg] = '\0'; // Add null terminator
                // Calling the Trie
                int id = GetColId(var);
                
                // printf("Added in matrix: i - %d, j - %d, val - %lf\n", row_id, id, num);
                ia[last_symbol] = row_id;
                ja[last_symbol] = id;
                ar[last_symbol] = num;
                ++last_symbol;

                // Make i be the pointer to the last read symbol.
                --i;
            } else if (s[i] == '=' || s[i] == '>') {
                is_RHS = true;
                if (s[i] == '>') 
                    flag = GLP_LO; //, printf("Added LO\n"), ++i;
                else if (s[i] == '=' && (i + 1 < len && s[i + 1] == '<'))
                    flag = GLP_UP; //, printf("Added UP\n"), ++i;
                else 
                    flag = GLP_FX; //, printf("Added FX\n");
                
            } else if (s[i] == ',') {
                // printf("Bound: %lf\n", bound);
                if (flag == 42) {
                    printf("No constraint sign (in constraint no. %d) was found.", row_id);
                    PL_fail;
                }
                glp_set_row_bnds(lp, row_id, flag, bound, bound);
                ++row_id;
                negative = false;
                flag = 42;
                bound = 0;
                is_RHS = false;
            } else if (s[i] != ' ') {
                printf("The constraints are incorrect entered!");
                PL_fail;
            }
        }
        // printf("Bound: %lf\n", bound);
        if (flag == 42) {
            printf("No constraint sign (in constraint no. %d) was found.", row_id);
            PL_fail;
        }
        glp_set_row_bnds(lp, row_id, flag, bound, bound);
        ++row_id;
    }
///-------------------------------------------------
    
    tail = PL_copy_term_ref(ints);
    while (PL_get_list(tail, head, tail)) {
        if (PL_is_variable(head)) {
            int id = GetColId(GetName(head));
            // printf("%s - integer!!!\n", GetName(head));
            glp_set_col_kind(lp, id, GLP_IV);
            // Say to glpk that head must be int.
        } else {
            printf("Ints must be the list of variables");
            PL_fail;
        }
    }
    // printf("number of integer columns - %d\n", glp_get_num_int(lp));
    if (!PL_get_nil(tail)) {
        printf("The list is not properly terminated.\n");
	PL_fail;
    }
    
    // printf("Function to minimize:\n");
    AddingExprToLP(obj, 0, 0, 1);
    // printf("\n");
    
    glp_load_matrix(lp, ar_size, ia, ja, ar);

    
    
    int ret = glp_simplex(lp, NULL);
    if (ret != 0)
    	PL_fail;
    int st = glp_get_status(lp);
    PL_unify_integer(_status, st);
    // printf("!!!!Status::: %d\nVars: \n", st);
    /*for (int i = 1; i <= maxVarId; ++i) {
        printf("%d -> %g\n", i, glp_get_col_prim(lp, i));
    }*/
    // printf("Answer (prim): %lf\n", glp_get_obj_val(lp));
    if (st == 4) {
        printf("LP has No feasuble solution.");
        fflush(stdout);
        PL_succeed;
    }
    
    if (st == 6) {

        printf("As solution is unbounded, all variables are bounded with [-1e9, 1e9]\n");
        for (int i = 1; i <= maxVarId; ++i)
            glp_set_col_bnds(lp, i, GLP_DB, -inf, inf);
        glp_set_obj_dir(lp, GLP_MIN);
        glp_simplex(lp, NULL);
        st = glp_get_status(lp);
    }
    
    if (st == 5) {
    	glp_intopt(lp, NULL);
    	
        int status_mip = glp_mip_status(lp);
        if (status_mip == 4) {
            printf("LP has solution, but it cannot be optimize to mip solution");
            fflush(stdout);
            PL_unify_integer(_status, st);
            PL_succeed;
        }

    	double val = glp_mip_obj_val(lp);
    	// printf("Answer is %lf\n", val);
    	PL_unify_float(_mn, val);
    	tail = PL_copy_term_ref(ints);
        term_t list = PL_new_term_ref(); 
        term_t list_new = PL_new_term_ref();
        PL_put_nil(list);
        while (PL_get_list(tail, head, tail)) {
            if (PL_is_variable(head)) {
                int node_val = floor(glp_mip_col_val(lp, GetColId(GetName(head)))+0.1);
                term_t value = PL_new_term_ref(); 
                PL_put_integer(value, node_val);
                PL_cons_list(list, value, list);

            } else {
                printf("Ints must be the list of variables");
                PL_fail;
            }
        }
        
        if (!PL_get_nil(tail)) {
            printf("The list is not properly terminated.\n");
            PL_fail;
        }
        PL_put_nil(list_new);
        tail = PL_copy_term_ref(list);
        while (PL_get_list(tail, head, tail)) {
            PL_cons_list(list_new, head, list_new);
        }
        PL_unify(_ans, list_new);
    }
    // printf("!!!Mip status: %d\n", glp_mip_status(lp));

    glp_delete_prob(lp);
    // free Trie;
    for (int i = 0; i < 10; ++i) 
        FreeNodes(root->edges[i]); 
    // printf("\nThanks!\n");
    fflush(stdout);
    PL_succeed;
    
}

install_t install() {
    PL_register_foreign("minimize", 6, pl_expr, 0);
}

int main(void) {
    return 0;
}