#include "HalfCycleFormulation.h"

HalfCycleFormulation::HalfCycleFormulation(const Instance* instance) {
	this->instance = instance;
	this->upperBound = -1;
	this->lowerBound = -1;
	this->time = -1;
}

HalfCycleFormulation::HalfCycleFormulation(const HalfCycleFormulation& hcf) {
	this->lowerBound = hcf.lowerBound;
	this->upperBound = hcf.upperBound;
	this->time = hcf.time;
}

HalfCycleFormulation::~HalfCycleFormulation() {

}

bool operator==(const HalfCycleFormulation& hcf1, const HalfCycleFormulation& hcf2) {
	return false;
}

bool operator!=(const HalfCycleFormulation& hcf1, const HalfCycleFormulation& hcf2) {
	return !(hcf1 == hcf2);
}

HalfCycleFormulation& HalfCycleFormulation::operator=(const HalfCycleFormulation& hcf) {
	this->lowerBound = hcf.lowerBound;
	this->upperBound = hcf.upperBound;
	this->time = hcf.time;
	return *this;
}

void HalfCycleFormulation::buildSolveModel() {
    GRBEnv* env = new GRBEnv();
    GRBModel* model = new GRBModel(*env);
    vector<GRBVar> variables;
    vector<Exchange> halfCycles;
    try {

        halfCycles = buildHalfCycles();

        
        for (int i = 0; i < halfCycles.size(); i++) {
            string name = "x_" + to_string(i);
            variables.push_back(model->addVar(0.0, 1, 0.0, GRB_INTEGER, name));
        }

        GRBLinExpr objective = 0;
        for (int i = 0; i < halfCycles.size(); i++) {
            objective += (halfCycles.at(i).getSequence().size() - 1) * variables.at(i);
        }
        model->setObjective(objective, GRB_MAXIMIZE);

        for (int i = 0; i < this->instance->getIncompatiblePairs().size(); i++) {
            int id = this->instance->getIncompatiblePairs().at(i);
            GRBLinExpr pack = 0;
            for (int j = 0; j < halfCycles.size(); j++) {
                double coeff = halfCycles.at(j).getVisit(id);
                if (halfCycles.at(j).getSequence().front() == id || halfCycles.at(j).getSequence().back() == id) {
                    coeff = 0.5;
                }
                pack += coeff * variables.at(j);
            }
            model->addConstr(pack <= 1, "packing_" + to_string(id));
        }

        for (int i = 0; i < this->instance->getIncompatiblePairs().size() - 1; i++) {
            int id1 = this->instance->getIncompatiblePairs().at(i);
            for (int j = i + 1; j < this->instance->getIncompatiblePairs().size(); j++) {
                int id2 = this->instance->getIncompatiblePairs().at(j);
                GRBLinExpr merge = 0;
                for (int k = 0; k < halfCycles.size(); k++) {
                    if (halfCycles.at(k).getSequence().front() == id1 && halfCycles.at(k).getSequence().back() == id2) {
                        merge += variables.at(k);
                    }
                    if (halfCycles.at(k).getSequence().front() == id2 && halfCycles.at(k).getSequence().back() == id1) {
                        merge -= variables.at(k);
                    }
                }
                model->addConstr(merge == 0, "merge_" + to_string(id1) + "_" + to_string(id2));
            }
        }

        if ((this->instance->getCyclesLength() % 2) > 0.5) {
            double ll = (this->instance->getCyclesLength() - 1) / 2.0;
            for (int k = 0; k < halfCycles.size(); k++) {
                if (halfCycles.at(k).getSequence().front() > halfCycles.at(k).getSequence().back() && halfCycles.at(k).getSequence().size() - 2 == ll) {
                    model->addConstr(variables.at(k) == 0, "discard_" + to_string(k));
                }
            }
        }

        //model->write("halfCycleFormulation_" + ::instanceName + ".lp");
        model->set(GRB_DoubleParam_TimeLimit, 1800.0);
        model->set(GRB_DoubleParam_MemLimit, 16.0);
        model->set(GRB_IntParam_Threads, 1);
        int bab_nodes = 0;
        mycallback_hcf cb = mycallback_hcf(this->upperBound, this->lowerBound, bab_nodes);
        model->setCallback(&cb);
        model->optimize();

        int status = model->get(GRB_IntAttr_Status);
        int solCount = model->get(GRB_IntAttr_SolCount);

        if (solCount > 0) {
            extract_solution_stats(model, halfCycles, variables);
        }

        string line = ::instanceName + "," + to_string(status) + "," + to_string(this->lowerBound) + "," + to_string(this->upperBound) + "," + to_string(this->time) + "\n";
        delete model;
        delete env;
    }
    catch (GRBException e) {
        cout << "[HCF::solve()] - Error code = " << e.getErrorCode() << endl;
        cout << e.getMessage() << endl;
        if (e.getErrorCode() == 10001) {
            

            int status = model->get(GRB_IntAttr_Status);
            int solCount = model->get(GRB_IntAttr_SolCount);

            if (solCount > 0) {
                extract_solution_stats(model, halfCycles, variables);
            }

            string line = ::instanceName + "," + to_string(status) + "," + to_string(this->lowerBound) + "," + to_string(this->upperBound) + "," + to_string(this->time) + "\n";
            delete model;
            delete env;
        }
        else {
            exit(EXIT_FAILURE);
        }
    }
    catch (...) {
        cout << "[Hpief::solve()] - Error during optimization" << endl;
    }

}

void HalfCycleFormulation::extract_solution_stats(const GRBModel* model, const vector<Exchange>& halfCycles, const  vector<GRBVar>& variables) {
    this->lowerBound = model->get(GRB_DoubleAttr_ObjVal);
    this->upperBound = model->get(GRB_DoubleAttr_ObjBound);
    this->time = model->get(GRB_DoubleAttr_Runtime);
    unordered_set<int> solutionsIds;
    for (int k = 0; k < halfCycles.size(); k++) {
        if (variables.at(k).get(GRB_DoubleAttr_X) > 0.5) {
            this->solution.push_back(halfCycles.at(k));
            solutionsIds.insert(halfCycles.at(k).getId());
        }
    }
    ::appendToFile(::instanceName + "_solutionHC.log", this->toString());
    ::appendToFile(::instanceName + "_solutionHC.log", "\n\nMERGE HALFCYCLES IN CYCLES:\n");
    for (int i = 0; i < this->solution.size(); i++) {
        int id1 = this->solution.at(i).getId();
        int first1 = this->solution.at(i).getSequence().front();
        int last1 = this->solution.at(i).getSequence().back();
        for (int j = 0; j < this->solution.size(); j++) {
            int id2 = this->solution.at(j).getId();
            int first2 = this->solution.at(j).getSequence().front();
            int last2 = this->solution.at(j).getSequence().back();
            if (first1 == last2 && last2 == first1) {
                ::appendToFile(::instanceName + "_solutionHC.log", this->solution.at(i).toString() + "\n");
                ::appendToFile(::instanceName + "_solutionHC.log", this->solution.at(j).toString() + "\n\n");
                solutionsIds.erase(id1);
                solutionsIds.erase(id2);
            }
        }
    }
    if (!solutionsIds.empty()) {
        ::appendToFile(::instanceName + "_errors_HC.log", "NOT EVERYTHING IS MERGED\n");
        for (unordered_set<int>::const_iterator it = solutionsIds.begin(); it != solutionsIds.end(); it++) {
            ::appendToFile(::instanceName + "_errors_HC.log", halfCycles.at(*it).toString() + "\n");
        }
    }
}

vector<Exchange> HalfCycleFormulation::buildHalfCycles() {
	int max_length = ceil(this->instance->getCyclesLength() / 2.0);
    vector<Exchange> solution;
    for (int i = 0; i < this->instance->getIncompatiblePairs().size(); i++) {
        int source = this->instance->getIncompatiblePairs().at(i);
        vector<vector<Label>> labels = dynamicProgram(source, max_length);

        //build exchange objects
        for (int l = 0; l < labels.size(); l++) {
            for (int h = 0; h < labels.at(l).size(); h++) {
                const Label* label = &labels.at(l).at(h);
                if (label->getSequence().size() > 1) {
                    Exchange exchange("cycle", static_cast<int>(label->getWeight()), label->getLength(), label->getSequence(), this->instance->getVertexNumber());
                    solution.push_back(exchange);
                    //cout << exchange.toString() << "\n\n";
                }

            }
        }
    }
    return solution;

}

vector<vector<Label>> HalfCycleFormulation::dynamicProgram(const int& source, const int& maxLength) {

    //INITIALIZATION
    Label sourceLabel(source, 0, 0, 0.0);
    sourceLabel.buildSequence({});
    vector<int> ngMemory(this->instance->getWordsNumber(), 0);
    sourceLabel.buildNgSetFullMemory(ngMemory, this->instance->getVertices().at(source).getWord(), this->instance->getVertices().at(source).getBitKey());
    //cout << "INITI\n" << sourceLabel.toString() << endl;

    size_t vSize = this->instance->getVertices().size();
    size_t pairNumber = this->instance->getIncompatiblePairs().size();
    vector<vector<Label>> labels(vSize);
    labels.at(source).push_back(sourceLabel);

    //ITERATIONS
    bool termination = false;

    while (termination != true) {

        termination = true;

        for (int i = 0; i < pairNumber; i++) {

            int id = this->instance->getIncompatiblePairs().at(i);
            const Vertex* v = &this->instance->getVertices().at(id);
            //cout << "VERTEX\n" << v->toString(false) << endl;

            for (int h = 0; h < labels.at(id).size(); h++) {

                const Label* l = &labels.at(id).at(h);

                if (!l->getIsExtended() && l->getLength() < maxLength) {

                    /*const Vertex* v = &this->vertices.at(this->incompatiblePairs.at(i));
                    cout << "VERTEX\n" << v->toString(false) << endl;*/
                    //cout << "LABEL\n" << l->toString() << endl;

                    for (unordered_map<int, int>::const_iterator it = v->getOutGoingArcs().begin(); it != v->getOutGoingArcs().end(); it++) {

                        const Vertex* w = &this->instance->getVertices().at(it->first);
                        /*cout << "TO REACH\n" << w->toString(false) << endl;
                        cout << "IS VISITED? " << l->isVisited(w->getWord(), w->getBitKey()) << endl;*/

                        if (!l->isVisited(w->getWord(), w->getBitKey())) {

                            Label extension(w->getId(), l->getLength() + 1, l->getWeight() + it->second, 0.0);
                            extension.buildSequence(l->getSequence());
                            extension.buildNgSetFullMemory(l->getNgMemory(), w->getWord(), w->getBitKey());
                            labels.at(w->getId()).push_back(extension);
                            //cout << "EXT\n" << extension.toString() << endl;

                            termination = false;

                        }

                    }

                }

                labels.at(id).at(h).setIsExtended(true);

            }

        }

    }

    return labels;

}

const double& HalfCycleFormulation::getLowerBound() const {
	return this->lowerBound;
}
const double& HalfCycleFormulation::getUpperBound() const {
	return this->upperBound;
}
const double& HalfCycleFormulation::getTime() const {
	return this->time;
}

const string HalfCycleFormulation::toString() const {
	string str = "LB: " + to_string(this->lowerBound) + "\tUB: " + to_string(this->upperBound) + "\tTIME: " + to_string(this->time) + "\n\n";
    for (int i = 0; i < this->solution.size(); i++) {
        str += this->solution.at(i).toString() + "\n\n";
    }
	return str;
}
