/**
 * @file grouping_customers.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 "grouping_customers.h"
#include <math.h>
#include "helper.h"

list<Cart> grouping_customers::algorithm_AVGDistances(SolverConfiguration &sc, CartConfiguration &cart_conf, multimap<int, Customer, greater<int>> &volume_customer_map, Warehouse &warehouse)
{
    // create list of carts
    list<Cart> loaded_cart_list;

    // take customers one by one, from the biggest to the smallest (by volume), and create Carts
    for (auto cust = volume_customer_map.begin(); cust != volume_customer_map.end();)
    {
        // save the customer currently in iteration
        Customer chosen_customer = cust->second;

        // create first cart
        Cart cart(cart_conf.getCartPattern());

        // load the customer to it or create more carts
        vector<Box> non_loaded_boxes;
        while (!cart.loadCustomer(sc, chosen_customer, true, &non_loaded_boxes))
        {
            // when the customer hasn't been loaded
            // push the cart, create new one and go next cycle
            loaded_cart_list.push_back(cart);
            cart = Cart(cart_conf.getCartPattern());

            chosen_customer.setBoxesDecomp(non_loaded_boxes);
            non_loaded_boxes.clear();
        }
        // erase the currently loaded customer
        volume_customer_map.erase(cust);

        //* ALGORITHM FOR AVG DISTANCES
        multimap<int, pair<Customer, multimap<int, Customer, less<int>>::iterator>, less<int>> possible_customers_by_avg;
        // choose the best customer according to its avg distance from the main customer (chosen right above)
        // exclude all customers, that have been tried already
        // loop through the map, finding the proper customers to the remaining space in the cart

        // for (auto next_cust = volume_customer_map.lower_bound(cart.getCartFreeSpace()); next_cust != volume_customer_map.end(); next_cust++)
        for (auto next_cust = volume_customer_map.begin(); next_cust != volume_customer_map.end(); next_cust++)
        {
            // for each customer which seems to fit count its avg distance
            int avg_distance = 0;
            int count_of_computed_distances = 0;
            for (auto item : next_cust->second.item_vect)
            {
                for (auto item_m : chosen_customer.item_vect)
                {
                    avg_distance += warehouse.getDistanceBetweenAddresses(*item.waddress, *item_m.waddress);
                    count_of_computed_distances++;
                }
            }
            avg_distance = avg_distance / count_of_computed_distances;
            possible_customers_by_avg.insert({avg_distance, {next_cust->second, next_cust}});
        }
        //* CUSTOMERS HAVE BEEN SORT ACCORDING TO THE DISTANCES

        // loop thourgh the map of sorted customers by avg distance and find whether they really fits to the cart
        for (auto best_cust : possible_customers_by_avg)
        {
            // check whether the customer fits the volume requirements on cart and then load it to the cart
            if (cart.loadCustomer(sc, best_cust.second.first))
            {
                // load customer to cart -> it is loaded yet, just remove from the map (to be sure not load the same customer again)
                volume_customer_map.erase(best_cust.second.second);
            }
        }

        // load the next customer for the next iteration
        cust = volume_customer_map.begin();

        // insert the created cart to the list
        loaded_cart_list.push_back(cart);
    }

    return loaded_cart_list;
}

struct line_pattern_t
{
    // these two points stands for the imaginary boundaries in 2D (x,y) space
    // - the most left and the most right
    float min_x, min_y;
    float max_x, max_y;
    // values defining directive pattern of a line
    float a;
    float b;
};

list<Cart> grouping_customers::leastSquareMethod(SolverConfiguration &sc, CartConfiguration &cart_conf, multimap<int, Customer, greater<int>> &volume_customer_map)
{
    list<Cart> loaded_cart_list;
    for (auto customer_it = volume_customer_map.begin(); customer_it != volume_customer_map.end();)
    {
        // save the customer currently in iteration
        Customer chosen_customer = customer_it->second;

        // load the customer
        // create first cart
        Cart cart(cart_conf.getCartPattern());

        // load the customer to it or create more carts
        vector<Box> non_loaded_boxes;
        while (!cart.loadCustomer(sc, chosen_customer, true, &non_loaded_boxes))
        {
            // when the customer hasn't been loaded
            // push the cart, create new one and go next cycle
            loaded_cart_list.push_back(cart);
            cart = Cart(cart_conf.getCartPattern());
            chosen_customer.setBoxesDecomp(non_loaded_boxes);
            non_loaded_boxes.clear();
        }
        // erase the currently loaded customer
        volume_customer_map.erase(customer_it);

        //* SORTING THE CUSTOMERS THAT CAN FIT THE CART ACCORDING TO THE LEAST SQUARE LINE APPROXIMATION
        multimap<float, pair<Customer, multimap<int, Customer, less<int>>::iterator>, less<int>> possible_customers;
        // exclude all customers, that have been tried already
        // loop through the map, finding the proper customers to the remaining space in the cart
        // sort the fitable customers by their relation to the line of the chosen_customer

        // for (auto next_cust = volume_customer_map.lower_bound(cart.getCartFreeSpace()); next_cust != volume_customer_map.end(); next_cust++)
        for (auto next_cust = volume_customer_map.begin(); next_cust != volume_customer_map.end(); next_cust++)
        {
            // regrese--------------------------------
            float sumx = 0, sumy = 0, sumxx = 0, sumxy = 0;
            vector<OItem> both_items = vector<OItem>(chosen_customer.item_vect);
            both_items.insert(both_items.end(), next_cust->second.item_vect.begin(), next_cust->second.item_vect.end());
            int n = both_items.size();
            for (int item = 0; item < n; item++)
            {
                int x = both_items[item].waddress->getWarehouseWAddress().x_coord;
                int y = both_items[item].waddress->getWarehouseWAddress().y_coord;
                sumx += x;
                sumy += y;
                sumxx += x * x;
                sumxy += x * y;
            }
            float a = (n * sumxy - sumx * sumy) / (n * sumxx - sumx * sumx);
            float b = (sumy - a * sumx) / n;
            // count all distances
            int dst = 0;
            for (auto item : both_items)
                dst += abs(-1 * a * item.waddress->getWarehouseWAddress().x_coord + 1 * item.waddress->getWarehouseWAddress().y_coord + -1 * b) / sqrt(pow(a, 2) + 1 * 1);
            possible_customers.insert({(float)dst / n, {next_cust->second, next_cust}});
            //---------------------------------------
        }

        //* CUSTOMERS HAVE BEEN SORT ACCORDING TO THE ALGORITHM
        // loop thourgh the map of sorted customers by avg distance and find whether they really fits to the cart
        for (auto best_cust : possible_customers)
        {
            // check whether the customer fits the volume requirements on cart and then load it to the cart
            if (cart.loadCustomer(sc, best_cust.second.first))
            {
                // load customer to cart -> it is loaded yet, just remove from the map (to be sure not load the same customer again)
                volume_customer_map.erase(best_cust.second.second);
            }
        }

        // load the next customer for the next iteration
        customer_it = volume_customer_map.begin();

        // insert the created cart to the list
        loaded_cart_list.push_back(cart);
    }

    return loaded_cart_list;
}

struct Point
{
    // the center point of the customer
    // computed as avg of points belonging to addresses of all items
    int x;
    int y;
};

struct Cluster
{
    int clusterId;
    Point center;
    vector<Point> custpoint_vect;
    multimap<int, Customer, greater<int>> vol_cust_map;
};

list<Cart> grouping_customers::kMeansClustering(SolverConfiguration &sc, CartConfiguration &cart_conf, multimap<int, Customer, greater<int>> &volume_customer_map)
{

    // map with key of free space in cart
    multimap<int, Cart, greater<int>> loaded_cart_map;

    while (volume_customer_map.size() != 0)
    {
        // find the center point of each customer and calculate
        vector<pair<Point, Customer>> cust_center_point_vect;
        int vol_of_all_customers = 0;
        for (auto cust : volume_customer_map)
        {
            vol_of_all_customers += cust.first;
            int sum_x = 0, sum_y = 0;
            for (auto item : cust.second.item_vect)
            {
                sum_x += item.waddress->getWarehouseWAddress().x_coord;
                sum_y += item.waddress->getWarehouseWAddress().y_coord;
            }

            cust_center_point_vect.push_back(
                {Point{
                     .x = sum_x / (int)cust.second.item_vect.size(),
                     .y = sum_y / (int)cust.second.item_vect.size(),
                 },
                 cust.second});
        }

        // calculate approximation for count of clusters
        // int num_of_clusters = vol_of_all_customers / cart_conf.getCartPattern().getCartFreeSpace() + 1;
        int num_of_clusters = int((vol_of_all_customers / cart_conf.getCartPattern().getCartFreeSpace()) * sc.percentage_kmeans);
        num_of_clusters = num_of_clusters == 0 ? 1 : num_of_clusters;

        // init clusters

        vector<Cluster> cluster_vect;
        for (int i = 0; i < num_of_clusters; i++)
        {
            cluster_vect.push_back(Cluster{
                .clusterId = i,
                .center = cust_center_point_vect.at(i).first,
                .custpoint_vect = {},
                .vol_cust_map = {}});
        }

        // do k-means clustering
        int max_cycles = sc.Number_of_K_Means_Iterations;
        while (max_cycles-- != 0)
        {
            // clear the items assigned to clusters
            for (auto &cluster : cluster_vect)
            {
                cluster.custpoint_vect.clear();
                cluster.vol_cust_map.clear();
            }

            // reassign the customers to proper centroids
            for (auto p : cust_center_point_vect)
            {
                // find the nearest cluster and assign item to it
                float nearest_dst = MAXFLOAT;
                int near_clust_idx = 0;
                for (auto &cluster : cluster_vect)
                {
                    float tmp_dst = sqrt(pow(abs(p.first.x - cluster.center.x), 2) + pow(abs(p.first.y - cluster.center.y), 2));
                    if (tmp_dst < nearest_dst)
                    {
                        nearest_dst = tmp_dst;
                        near_clust_idx = cluster.clusterId;
                    }
                }
                cluster_vect.at(near_clust_idx).custpoint_vect.push_back({p.first});
                cluster_vect.at(near_clust_idx).vol_cust_map.insert({p.second.getTotalVolume(), p.second});
            }

            // recompute the centroids coordinates
            bool centroids_are_same = true;
            for (auto &cluster : cluster_vect)
            {
                float sum_x = 0, sum_y = 0;
                for (auto point : cluster.custpoint_vect)
                {
                    sum_x += point.x;
                    sum_y += point.y;
                }
                float new_x = sum_x / cluster.custpoint_vect.size();
                float new_y = sum_y / cluster.custpoint_vect.size();
                if (new_x != cluster.center.x || new_y != cluster.center.y)
                    centroids_are_same = false;
                cluster.center.x = new_x;
                cluster.center.y = new_y;
            }
            // no change - exit
            if (centroids_are_same)
                break;
        }

        // clear the current content of volume_customer_map, because it will be filled with everything currently not in cart
        volume_customer_map.clear();
        // build the carts from items in clusters
        for (auto &cluster : cluster_vect)
        {
            // firstly, try to load the customer to any of the previously created carts
            for (auto customer = begin(cluster.vol_cust_map); customer != end(cluster.vol_cust_map);)
            {
                bool added = false;
                for (auto &cart : loaded_cart_map)
                {
                    if (cart.second.getCartFreeSpace() < customer->first)
                        continue;
                    if (cart.second.loadCustomer(sc, customer->second))
                    {
                        // just remove from the map (to be sure not load the same customer again)
                        auto tmp_iter = customer;
                        customer++;
                        added = true;
                        cluster.vol_cust_map.erase(tmp_iter);
                        break;
                    }
                }
                if (!added)
                    customer++;
            }

            // then loop again through the customers and try to insert them into cart, and when they cannot fit it, save them for the next iteration
            auto customer = begin(cluster.vol_cust_map);
            if (customer == end(cluster.vol_cust_map))
                continue;

            // save the chosen customer
            Customer chosen_customer = customer->second;

            // load the customer
            // create first cart
            Cart cart(cart_conf.getCartPattern());

            // load the customer to it or create more carts
            vector<Box> non_loaded_boxes;
            while (!cart.loadCustomer(sc, chosen_customer, true, &non_loaded_boxes))
            {
                // when the customer hasn't been loaded
                // push the cart, create new one and go next cycle
                loaded_cart_map.insert({cart.getCartFreeSpace(), cart});
                cart = Cart(cart_conf.getCartPattern());
                chosen_customer.setBoxesDecomp(non_loaded_boxes);
                non_loaded_boxes.clear();
            }
            // erase the chosen customer
            cluster.vol_cust_map.erase(customer);

            // go through the rest of the customers in this group and try fit them to current cart
            for (auto next_cust = cluster.vol_cust_map.begin(); next_cust != cluster.vol_cust_map.end();)
            {
                // check whether the customer fits the volume requirements on cart and then load it to the cart
                if (cart.loadCustomer(sc, next_cust->second))
                {
                    // load customer to cart -> it is loaded yet, just remove from the map (to be sure not load the same customer again)
                    auto tmp_iter = next_cust;
                    next_cust++;
                    cluster.vol_cust_map.erase(tmp_iter);
                }
                // when the customer can't be loaded, add him to volume_customer_map
                else
                {
                    volume_customer_map.insert({next_cust->first, next_cust->second});
                    next_cust++;
                }
            }

            loaded_cart_map.insert({cart.getCartFreeSpace(), cart});
        }
    }

    // return the list of created carts
    list<Cart> loaded_cart_list;
    for (auto cart : loaded_cart_map)
        loaded_cart_list.push_back(cart.second);
    return loaded_cart_list;
}
