#include <SWI-Prolog.h>
#include <stdio.h>
#include <string.h>
#include <glpk.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)




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;
double* ar;
int* ia;
int* ja;
glp_prob *lp;
size_t last_symbol = 1;
size_t row_number = 0;

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) {
    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");
            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;
if (PL_get_chars(term, &name, CVT_VARIABLE | CVT_WRITE | REP_UTF8)) {
    return name;
} 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 {
                        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)) {
                AddingExprToLP(arg1, row_id, 0, is_obj);
                AddingExprToLP(arg2, row_id, 0, is_obj);
            }
        } else if (f == FUNCTOR_minus2) {
            if (PL_get_arg(1, itr, arg1) && PL_get_arg(2, itr, arg2)) {
                AddingExprToLP(arg1, row_id, 0, is_obj);
                AddingExprToLP(arg2, row_id, 1, is_obj);
            }
        } 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);
        else 
            glp_set_obj_coef(lp, id, 1);
    }
}
}


/** 	                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_MAX);

    term_t cons_copy = PL_copy_term_ref(cons);
    while (PL_get_list(cons_copy, head, tail)) {
        term_t expr = PL_new_term_ref();
        PL_get_arg(1, head, expr);
    	ReadVars(expr, 0);
    	++row_number;
    	cons_copy = tail;
    }
    if (!PL_get_nil(cons_copy)) {
        printf("The list is not properly terminated.\n");
	PL_fail;
    }

    
    ReadVars(obj, 1);

    cons_copy = PL_copy_term_ref(ints);
    while (PL_get_list(cons_copy, 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;
        }
        cons_copy = tail;
    }
    if (!PL_get_nil(cons_copy)) {
        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.
        
    glp_add_rows(lp, row_number);
    
    cons_copy = PL_copy_term_ref(cons);
    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
    }
    


    while (PL_get_list(cons_copy, head, tail)) {
        term_t expr = PL_new_term_ref();
        PL_get_arg(1, head, expr);
        // printf("Row(%d):", row_id);
        AddingExprToLP(expr, row_id, 0, 0);
        
        term_t bound = PL_new_term_ref();
        PL_get_arg(2, head, bound);
        double bd;
        if (PL_is_integer(bound)) {
            int i;
            PL_get_integer(bound, &i);
            bd = (float)i;
        } else if (PL_is_float(bound)) {
            PL_get_float(bound, &bd);
        } else {
            printf("Wrong bound number");
            PL_fail;
        }

        
        functor_t f;
        PL_get_functor(head, &f);
        // printf("%d -> ", row_id);
        if (f == FUNCTOR_UP) {
            glp_set_row_bnds(lp, row_id, GLP_UP, 0.0, bd);
            // printf("=< %lf\n", bd);
        } else if (f == FUNCTOR_LO) {
            glp_set_row_bnds(lp, row_id, GLP_LO, bd, 0.0);
            // printf(">= %lf\n", bd);
        } else if (f == FUNCTOR_BD) {
            glp_set_row_bnds(lp, row_id, GLP_FX, bd, bd);
            // printf("= %lf\n", bd);
        } else {
            printf("Please use only =</=>/=");
            PL_fail;
        }
        ++row_id;
        cons_copy = tail;
    }
    if (!PL_get_nil(cons_copy)) {
        printf("The list is not properly terminated.\n");
	PL_fail;
    }
    

  
    
    cons_copy = PL_copy_term_ref(ints);
    while (PL_get_list(cons_copy, 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;
        }
        cons_copy = tail;
    }
    // printf("number of integer columns - %d\n", glp_get_num_int(lp));
    if (!PL_get_nil(cons_copy)) {
        printf("The list is not properly terminated.\n");
	PL_fail;
    }
    
    AddingExprToLP(obj, 0, 0, 1);
    
    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] and the result is the following:\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);
    	cons_copy = PL_copy_term_ref(ints);
        term_t list = PL_new_term_refs(1); 
        PL_put_nil(list);
        while (PL_get_list(cons_copy, head, tail)) {
            if (PL_is_variable(head)) {
                double node_val = glp_mip_col_val(lp, GetColId(GetName(head)));
                term_t value = PL_new_term_ref(); 
                PL_put_float(value, node_val);
                PL_cons_list(list, value, list);
            } else {
                printf("Ints must be the list of variables");
                PL_fail;
            }
            cons_copy = tail;
        }
        PL_unify(_ans, list);
        if (!PL_get_nil(cons_copy)) {
            printf("The list is not properly terminated.\n");
            PL_fail;
        }
    }
    // 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("maximize", 6, pl_expr, 0);
}

int main(void) {
    return 0;
}