/**
 * @file carts_loader.cpp
 * @author Jan Holáň, xholan11 (xholan11@vutbr.cz)
 * @brief translated with gcc version 12.2.1
 * @version 0.1
 * @date 2023-05-01
 *
 * @copyright Copyright (c) 2023
 *
 */

#include <filesystem>
#include <map>
#include <nlohmann/json.hpp>
#include <time.h>
#include "WarehousePickingOpt.h"
#include "helper.h"
#include "warehouse_picking_opt_debug.h"
#include "stats.h"

#include "grouping_customers.h"
#include "tsp_solver.h"
#include "WarehousePickingOpt.h"

using json = nlohmann::json;

// void final_time_capture(clock_t clock_start)
// {
//     //vypocteni sekund
//     double whole_time = (double)(clock() - clock_start)/CLOCKS_PER_SEC;
//     cout << setprecision(3) << whole_time << " ";
// }

CartsLoader::CartsLoader(Warehouse &warehouse)
{
    this->warehouse = warehouse;
}

CartsLoader::~CartsLoader() {}

void CartsLoader::loadItemsAndSpec(string path_or_text)
{
    // clear item vector
    this->item_vect.clear();
    // get json from path or text
    json item_list_js;
    try
    {
        //******** check whether the strings is a valid path ********
        try
        {
            if (filesystem::exists(path_or_text))
            {
                // read the json from a file
                item_list_js = readJsonFromFile(path_or_text);
            }
            else
                throw exception();
        }
        catch (...)
        {
            // otherwise is expected json-string in path_or_text
            item_list_js = json::parse(path_or_text);
        }

        //***** parse the json to items objects with their specifications *******

        // map for saving which items has been created already
        map<std::pair<int, int>, OItem> tmp_m;
        // parse it into the list of items
        for (auto it = item_list_js.begin(); it != item_list_js.end(); it++)
        {
            // read the current json object, create new OItem object and parse the json to it
            json item_js = it.value();
            int Customer = item_js["Customer"];
            int Product = item_js["Product"];
            string Address = item_js["AddressCode"];
            int Qty = item_js["Qty"];

            OItem item = OItem(Customer, Product, Address, Qty);

            // load the specification to the item
            int Unit = item_js["Unit"];
            int PackageType = item_js["PackageType"];
            int ContentQty = item_js["ContentQty"];
            float X = item_js["X"];
            float Y = item_js["Y"];
            float Z = item_js["Z"];
            float Weight = item_js["Weight"];

            ItemSpecification item_spec(Product, (ItemSpecification::Type)Unit, PackageType, ContentQty, X, Y, Z, Weight);

            // look for the item, if it has been already processed
            auto res = tmp_m.find(make_pair(Customer, Product));
            if (res != tmp_m.end())
            {
                // item found, so just attach the specification
                res->second.addItemSpecification(item_spec);
            }
            else
            {
                item.addItemSpecification(item_spec);
                tmp_m.insert({make_pair(Customer, Product), item});
            }
        }

        // after the cycle, add all the created oitems to proper vector
        for (auto it : tmp_m)
        {
            this->item_vect.push_back(it.second);
        }

        DEBUG_MSG("TOTAL NUMBER OF ITEMS IN ORDER> %li\n", this->item_vect.size());
    }
    catch (exception &e)
    {
        std::cerr << "ERROR> bad path or text or json doesnt include required values" << '\n';
    }
}

void CartsLoader::testUsedData(string path)
{
    map<int, int> counter;
    SECTOR_ADDR_T starting_point, end_point;
    starting_point.section = "VAA";
    starting_point.x_coord = 300;
    starting_point.y_coord = 0;
    end_point.section = "VAA";
    end_point.x_coord = 620;
    end_point.y_coord = 0;

    // get json from path or text
    json item_list_js;
    //******** check whether the strings is a valid path ********
    if (filesystem::exists(path))
    {
        // read the json from a file
        item_list_js = readJsonFromFile(path);

        // vector with cart number and cartOItem
        multimap<int, CartOItem> cart_items_map;
        vector<int> cart_number_vect;
        // from json will be exported just customer, address, cart and
        for (auto it = item_list_js.begin(); it != item_list_js.end(); it++)
        {
            // read the current json object, create new OItem object and parse the json to it
            json item_js = it.value();

            int Customer = item_js["Customer"];
            int Product = item_js["Product"];
            string Address = item_js["AddressCode"];
            int CartNumber = item_js["Cart"];
            int AddressRank = item_js["AddressRank"];
            counter[CartNumber]++;
            OItem oitem = OItem();
            oitem.Address = Address;
            oitem.Customer = Customer;
            oitem.Product = Product;
            oitem.Qty = AddressRank; //! Qty will hold the address rank
            oitem.waddress = new WAddress(Address);
            cart_items_map.insert({CartNumber, CartOItem(oitem)});

            if (find(cart_number_vect.begin(), cart_number_vect.end(), CartNumber) == end(cart_number_vect))
                cart_number_vect.push_back(CartNumber);
        }

        // create vectors suitable for distance measuring and sort them according their address'rank
        // cout << "   CART  "
        //      << "  DST "
        //      << "  ITEMS" << endl;
        int all_dst = 0;
        int all_items = 0;
        for (auto cart_number : cart_number_vect)
        {
            vector<CartOItem> measure_vect;
            for (auto cart = cart_items_map.begin(); cart != end(cart_items_map); cart++)
            {
                if (cart->first == cart_number)
                    measure_vect.push_back(cart->second);
            }
            // sort the item that belong to one cart according to their address rank (stored in Qty)
            sort(measure_vect.begin(), measure_vect.end(), [](const CartOItem &it1, const CartOItem &it2)
                 { return it1.getOItem().Qty > it2.getOItem().Qty; });

            // measure the distance
            int distance = tsp_solver::justMeasureDistance(measure_vect, warehouse, starting_point, end_point);
            // vector<CartOItem> res_vect;
            // int distance = tsp_solver::greedyAlgorithm(measure_vect, warehouse, &res_vect, starting_point, end_point);
            all_dst += distance;
            // print the result
            // cout << cart_number << "    " << distance << "   " << measure_vect.size() << endl;
            all_items += measure_vect.size();
        }

        // cout << "WHOLE DISTANCE: " << all_dst << ", AVG DISTANCE PER CART: " << all_dst / (int)cart_number_vect.size() << endl;
        // cout << "ALL OITEMS: " << all_items << endl;

        // TEST 2 - alg comparsion
        // cout << all_dst << " " << cart_number_vect.size() << " ";
        DEBUG_MSG("WHOLE DISTANCE [dm]: %i, AVG DISTANCE PER CART: %i\n", all_dst, all_dst / (int)cart_number_vect.size());
    }
    else
    {
        throw exception();
    }
}

list<Cart> CartsLoader::getLoadedCartsList()
{
    return this->loaded_cart_list;
}

string CartsLoader::exportCartsToJson(bool export_shelves_boxes, bool export_sorted_items)
{
    // build the final json object
    json result;

    // loop through all the carts and each of them convert to json
    for (auto cart : this->loaded_cart_list)
    {
        json cart_items_js;

        if (export_sorted_items)
        {
            // take the CartOItems, which are sorted to shortest path
            json cart_items_sort_one_by_one_js;
            for (auto item : cart.getCartOItemVector())
            {
                cart_items_sort_one_by_one_js.push_back(item);
            }
            cart_items_js["items"] = cart_items_sort_one_by_one_js;
        }

        if (export_shelves_boxes)
        {
            // take the items as they are in boxes in shelves
            json shelves_js;
            for (auto shelf : cart.getCartShelvesVector())
            {
                json shelf_js;
                // go through boxes
                for (auto box : shelf.getBoxes()->full_box_vect)
                {
                    // go through items in boxes (both full and one-itemized)
                    for (auto item : box.getItemsInTheBox())
                    {
                        shelf_js.push_back(item.second);
                    }
                }
                for (auto box : shelf.getBoxes()->one_item_box_vect)
                {
                    for (auto item : box.getItemsInTheBox())
                    {
                        shelf_js.push_back(item.second);
                    }
                }
                shelves_js.push_back(shelf_js);
            }
            cart_items_js["shelves"] = shelves_js;
        }
        // push everything to main json string
        result.push_back(cart_items_js);
    }
    return result.dump();
}

string CartsLoader::getStats()
{
    string result = "";

    // stats for each cart
    result.append(stats::eachCartStats(this->getLoadedCartsList()));

    return result;
}

short CartsLoader::chooseBestAlgorithm(SolverConfiguration &solver_conf, CartConfiguration &cart_conf, multimap<int, Customer, greater<int>> volume_customer_map)
{
    SECTOR_ADDR_T starting_point, end_point;
    starting_point.section = "VAA";
    starting_point.x_coord = 300;
    starting_point.y_coord = 0;
    end_point.section = "VAA";
    end_point.x_coord = 620;
    end_point.y_coord = 0;

    multimap<int, Customer, greater<int>> volume_customer_map_copy1 = volume_customer_map;
    multimap<int, Customer, greater<int>> volume_customer_map_copy2 = volume_customer_map;
    int best_all_dst = INT32_MAX;
    GroupingCustomersAlgorithms best_alg = GroupingCustomersAlgorithms::AVG_DISTANCES;
    int all_dist = 0;

    /********************************************************************************************/

    DEBUG_MSG("AVG DISTANCES ALGORITHM\n");
    this->loaded_cart_list = grouping_customers::algorithm_AVGDistances(solver_conf, cart_conf, volume_customer_map, this->warehouse);
    // sort the items in carts
    for (auto cart = this->loaded_cart_list.begin(); cart != this->loaded_cart_list.end(); cart++)
    {
        vector<CartOItem> res_vect;
        int walked_dist = 0;
        walked_dist = tsp_solver::greedyAlgorithm(cart->getCartOItemVector(), warehouse, &res_vect, starting_point, end_point);
        cart->addToEstimatedWalkedDistance(walked_dist);
        cart->setCartOItemVector(res_vect);
        all_dist += walked_dist;
    }
    if (all_dist < best_all_dst)
    {
        best_all_dst = all_dist;
        best_alg = GroupingCustomersAlgorithms::AVG_DISTANCES;
    }
    DEBUG_MSG("WHOLE DISTANCE [dm]: %i, AVG DISTANCE PER CART: %i\n", all_dist, all_dist / (int)this->loaded_cart_list.size());
    // TEST 2 - alg comparsion
    // cout << all_dist << " " << this->loaded_cart_list.size() << " ";
    /********************************************************************************************/

    DEBUG_MSG("LEAST SQUARE METHOD\n");
    this->loaded_cart_list = grouping_customers::leastSquareMethod(solver_conf, cart_conf, volume_customer_map_copy1);
    all_dist = 0;
    // sort the items in carts
    for (auto cart = this->loaded_cart_list.begin(); cart != this->loaded_cart_list.end(); cart++)
    {
        vector<CartOItem> res_vect;
        int walked_dist = 0;

        // choose the right algorithm
        walked_dist = tsp_solver::greedyAlgorithm(cart->getCartOItemVector(), warehouse, &res_vect, starting_point, end_point);
        cart->addToEstimatedWalkedDistance(walked_dist);
        cart->setCartOItemVector(res_vect);
        all_dist += walked_dist;
    }
    if (all_dist < best_all_dst)
    {
        best_all_dst = all_dist;
        best_alg = GroupingCustomersAlgorithms::LEAST_SQUARE_APPROXIMATION;
    }
    DEBUG_MSG("WHOLE DISTANCE [dm]: %i, AVG DISTANCE PER CART: %i\n", all_dist, all_dist / (int)this->loaded_cart_list.size());
    // TEST 2 - alg comparsion
    // cout << all_dist << " " << this->loaded_cart_list.size() << " ";
    /********************************************************************************************/
    DEBUG_MSG("K MEANS CLUSTERING\n");
    this->loaded_cart_list = grouping_customers::kMeansClustering(solver_conf, cart_conf, volume_customer_map_copy2);
    all_dist = 0;
    // sort the items in carts
    for (auto cart = this->loaded_cart_list.begin(); cart != this->loaded_cart_list.end(); cart++)
    {
        vector<CartOItem> res_vect;
        int walked_dist = 0;

        // choose the right algorithm
        walked_dist = tsp_solver::greedyAlgorithm(cart->getCartOItemVector(), warehouse, &res_vect, starting_point, end_point);

        cart->addToEstimatedWalkedDistance(walked_dist);
        cart->setCartOItemVector(res_vect);
        all_dist += walked_dist;
    }
    if (all_dist < best_all_dst)
    {
        best_all_dst = all_dist;
        best_alg = GroupingCustomersAlgorithms::K_MEANS_CLUSTERING;
    }
    DEBUG_MSG("WHOLE DISTANCE [dm]: %i, AVG DISTANCE PER CART: %i\n", all_dist, all_dist / (int)this->loaded_cart_list.size());
    // TEST 2 - alg comparsion
    // cout << all_dist << " " << this->loaded_cart_list.size() << " ";
    return best_alg;
}

void CartsLoader::loadingCartsRun(SolverConfiguration &solver_conf, CartConfiguration &cart_conf, BoxConfiguration &box_conf)
{
    if (item_vect.empty())
        return;

    // erase map of customers (just to be sure)
    this->id_customer_map.erase(this->id_customer_map.begin(), this->id_customer_map.end());

    // attach right items to customers according to the Customer number
    // attach item specification to it
    // compute weight and volume of items
    for (auto item = this->item_vect.begin(); item != this->item_vect.end(); item++)
    {
        // count the weight and volume of the current item
        item->countWeightAndVolume();

        // create the customer if doesn't exist yet
        if (this->id_customer_map.find(item->Customer) == this->id_customer_map.end())
        {
            // customer doesnt exist yet
            // create customer object
            this->id_customer_map.insert({item->Customer, Customer(item->Customer)});
        }

        // add item to proper customer
        this->id_customer_map.at(item->Customer).addOItem(*item);
    }

    // compute the total weight and volume of each customer
    // and insert it into new multimap, which has keys of volume of each customer - so it is SORTED
    multimap<int, Customer, greater<int>> volume_customer_map;
    int total_boxes = 0;
    int total_volume = 0;
    for (auto cust = this->id_customer_map.begin(); cust != this->id_customer_map.end(); cust++)
    {
        // compute the volume and weight of customer
        cust->second.countTotalWeightAndVolume();
        total_volume += cust->second.getTotalVolume();
        // decomp customer's item into boxes
        cust->second.decompIntoBoxes(box_conf, solver_conf);
        total_boxes += cust->second.getBoxesDecomp().size();
        // insert the  customer into the new map
        volume_customer_map.insert({cust->second.getTotalVolume(), cust->second});
    }
    // TEST 3
    // cout << total_boxes << "    " << ((float)(total_volume / total_boxes) / box_conf.getFirstBox().getCapacity()) << "      ";
    DEBUG_MSG("TOTAL BOXES AFTER DECOMPOSITION: %i\n", total_boxes);

    // *****************************************************************************
    // =============================ALGORITHM BEGIN=================================
    // *****************************************************************************

    SECTOR_ADDR_T starting_point, end_point;
    starting_point.section = "VAA";
    starting_point.x_coord = 300;
    starting_point.y_coord = 0;
    end_point.section = "VAA";
    end_point.x_coord = 620;
    end_point.y_coord = 0;

    GroupingCustomersAlgorithms chosen_grouping_alg = solver_conf.getGroupingCustomerAlgorithm();
    if (solver_conf.getGroupingCustomerAlgorithm() == GroupingCustomersAlgorithms::AUTO)
        chosen_grouping_alg = (GroupingCustomersAlgorithms)chooseBestAlgorithm(solver_conf, cart_conf, volume_customer_map);

    // group the customers to carts, but items in carts are UNSORTED
    switch (chosen_grouping_alg)
    {
    case GroupingCustomersAlgorithms::K_MEANS_CLUSTERING:
        this->loaded_cart_list = grouping_customers::kMeansClustering(solver_conf, cart_conf, volume_customer_map);
        break;
    case GroupingCustomersAlgorithms::LEAST_SQUARE_APPROXIMATION:
        this->loaded_cart_list = grouping_customers::leastSquareMethod(solver_conf, cart_conf, volume_customer_map);
        break;
    case GroupingCustomersAlgorithms::AVG_DISTANCES:
    default:
        this->loaded_cart_list = grouping_customers::algorithm_AVGDistances(solver_conf, cart_conf, volume_customer_map, this->warehouse);
        break;
    }

    // sort the items in carts
    int all_dist = 0;
    for (auto cart = this->loaded_cart_list.begin(); cart != this->loaded_cart_list.end(); cart++)
    {
        vector<CartOItem> res_vect;
        int walked_dist = 0;

        // choose the right algorithm
        switch (solver_conf.getShortestPathAlgorithm())
        {
        case ShortestPathAlgorithms::GREEDY:
            walked_dist = tsp_solver::greedyAlgorithm(cart->getCartOItemVector(), warehouse, &res_vect, starting_point, end_point);
            break;
        case ShortestPathAlgorithms::GREEDY_BRUTE_FORCE_OPTIMIZATION:
            if (cart->getCartOItemVector().size() < 10)
                walked_dist = tsp_solver::bruteForce(cart->getCartOItemVector(), warehouse, &res_vect, starting_point, end_point);
            else
                walked_dist = tsp_solver::greedyAlgorithm(cart->getCartOItemVector(), warehouse, &res_vect, starting_point, end_point);
            break;
        case ShortestPathAlgorithms::NEAREST_NEIGHBOUR_BRUTE_FORCE_OPTIMIZATION:
            if (cart->getCartOItemVector().size() < 10)
                walked_dist = tsp_solver::bruteForce(cart->getCartOItemVector(), warehouse, &res_vect, starting_point, end_point);
            else
                walked_dist = tsp_solver::nearestNeighbour(cart->getCartOItemVector(), warehouse, &res_vect, starting_point, end_point);
            break;
        case ShortestPathAlgorithms::NEAREST_NEIGHBOUR:
            walked_dist = tsp_solver::nearestNeighbour(cart->getCartOItemVector(), warehouse, &res_vect, starting_point, end_point);
            break;
        case ShortestPathAlgorithms::JUST_MEASURE_DISTANCE:
            walked_dist = tsp_solver::justMeasureDistance(cart->getCartOItemVector(), warehouse, starting_point, end_point);
            res_vect = cart->getCartOItemVector();
            break;
        case ShortestPathAlgorithms::NONE:
        default:
            res_vect = cart->getCartOItemVector();
            break;
        }

        cart->addToEstimatedWalkedDistance(walked_dist);
        cart->setCartOItemVector(res_vect);
        all_dist += walked_dist;
    }

    DEBUG_MSG("WHOLE DISTANCE [dm]: %i, AVG DISTANCE PER CART: %i\n", all_dist, all_dist / (int)this->loaded_cart_list.size());
}
