#include "optimizer/hexalyoptimizer.h"
#include <fstream>
#include <iostream>
#include <vector>

using namespace hexaly;
using namespace std;

class OrderPicking {
public:
    int nbOrders;
    vector<vector<int>> distancesData;
    HexalyOptimizer optimizer;
    HxExpression pickingList;
    HxExpression objective;
    HxExpression indexInitialPosition;

    void readInstance(const string& inputFile) {
        ifstream infile(inputFile);
        if (!infile.is_open()) {
            throw std::runtime_error("Cannot open the file.");
        }
        infile >> nbOrders;
        ++nbOrders;
        distancesData.resize(nbOrders);
        for (int i = 0; i < nbOrders; ++i) {
            distancesData[i].resize(nbOrders);
            for (int j = 0; j < nbOrders; ++j) {
                infile >> distancesData[i][j];
            }
        }
    }

    void solve(int timeLimit) {
        // Declare the model
        HxModel model = optimizer.getModel();

        // Declare the list containing the picking order
        pickingList = model.listVar(nbOrders);

        // All orders must be picked
        model.constraint(model.count(pickingList) == nbOrders);

        // Create an Hexaly array for the distance matrix in order to access it using the "at" operator
        HxExpression distancesMatrix = model.array();
        for (int i = 0; i < nbOrders; ++i) {
            distancesMatrix.addOperand(model.array(distancesData[i].begin(), distancesData[i].end()));
        }

        // Lambda expression to compute the distance to the next order
        HxExpression distanceToNextOrderLambda = model.createLambdaFunction([&](HxExpression i) {
            return model.at(distancesMatrix,  pickingList[i], pickingList[i + 1]);
        });

        // The objective is to minimize the total distance required to pick 
        // all the orders and to go back to the initial position
        objective = model.sum(model.range(0, nbOrders - 1), distanceToNextOrderLambda) 
            + model.at(distancesMatrix, pickingList[nbOrders - 1], pickingList[0]);

        // Store the index of the initial position in the list.
        // It will be used at the end to write the solution starting from the initial point.
        indexInitialPosition = model.indexOf(pickingList, 0);

        model.minimize(objective);

        // End of the model declaration
        model.close();

        optimizer.getParam().setTimeLimit(timeLimit);

        optimizer.solve();
    }

    void writeSolution(const string& outputFile) {
        ofstream outfile;
        outfile.exceptions(ofstream::failbit | ofstream::badbit);
        outfile.open(outputFile.c_str());
        outfile << objective.getValue() << endl;
        HxCollection pickingListCollection = pickingList.getCollectionValue();
        for (int i = 0; i < nbOrders; ++i) {
            int index = ((int)indexInitialPosition.getValue() + i) % nbOrders;
            outfile << pickingListCollection[index] << " ";
        }
        outfile << endl;
    }
};

int main(int argc, char** argv) {
    if (argc < 2) {
        cerr << "order_picking inputFile [outputFile] [timeLimit]" << endl;
        return 1;
    }
    const char* inputFile = argv[1];
    const char* outputFile = argc > 2 ? argv[2] : NULL;
    const char* strTimeLimit = argc > 3 ? argv[3] : "10";
    OrderPicking model;
    model.readInstance(inputFile);
    model.solve(atoi(strTimeLimit));
    if (outputFile != NULL)
        model.writeSolution(outputFile);
    return 0;
}
