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

int tsp_solver::nearestNeighbour(vector<CartOItem> item_vect, Warehouse &warehouse, vector<CartOItem> *res_vect, SECTOR_ADDR_T starting_point, SECTOR_ADDR_T end_point)
{
    // vector where are going to be sorted the items
    vector<CartOItem> sorted_vect;
    int walked_distance = 0;

    CartOItem chosen_item = item_vect[0]; // it is set just for start
    SECTOR_ADDR_T last_added_item_addr = starting_point;
    while (!item_vect.empty())
    {
        int cur_nearest_dist = INT32_MAX;
        vector<CartOItem>::iterator iter_of_chosen_item;

        // go through all the rest items and choose the closest one
        for (auto item = item_vect.begin(); item != item_vect.end(); item++)
        {
            int dist = warehouse.getDistanceBetweenAddresses(last_added_item_addr, item->getOItem().waddress->getWarehouseWAddress());

            if (dist < cur_nearest_dist)
            {
                cur_nearest_dist = dist;
                chosen_item = *item.base();
                iter_of_chosen_item = item;
            }
        }

        // append the best item to the sorted vector
        sorted_vect.push_back(chosen_item);
        walked_distance += cur_nearest_dist;
        // remove the chosen item from the vector of the remaining itmes
        item_vect.erase(iter_of_chosen_item);
        last_added_item_addr = chosen_item.getOItem().waddress->getWarehouseWAddress();
    }

    // finally return to the starting point
    walked_distance += warehouse.getDistanceBetweenAddresses(last_added_item_addr, end_point);

    // return
    *res_vect = sorted_vect;
    return walked_distance;
}

/// @brief overtaken code
/// @copyright mayankdv, Aug 10, 2021, https://leetcode.com/problems/next-permutation/solutions/1394208/c-brute-force-optimal-time-on-auxiliary-space-o1/
/// @link https://leetcode.com/problems/next-permutation/solutions/1394208/c-brute-force-optimal-time-on-auxiliary-space-o1/
class BruteForcePermutation
{
    // Brute Force Solution Time O(N!*N) & Auxiliary Space O(N^2)
    // Recursion & Backtracking
    // TLE 64/265 test cases passed
public:
    void nextPermutation(vector<int> &nums)
    {
        // Optimal Solution Time O(N) & Auxiliary Space O(1)
        // Narayana Panditha's Algorithm
        int len = nums.size(), k = 0, l = 0; // For example, if nums=[2,4,6,5,3]
        for (k = len - 2; k >= 0; k--)
        {
            if (nums[k] < nums[k + 1])
                break; // k will point to 4 in nums=[2,4,6,5,3]
        }
        if (k < 0)
        {
            reverse(nums.begin(), nums.end());
        }
        else
        {
            for (l = len - 1; l > k; l--)
            {
                if (nums[l] > nums[k])
                    break; //  l will point to 5 in nums=[2,4,6,5,3]
            }
            swap(nums[k], nums[l]);                    // nums=[2,5,6,4,3]
            reverse(nums.begin() + k + 1, nums.end()); // nums=[2,5,3,4,6]
        }
    }
};

int tsp_solver::bruteForce(vector<CartOItem> item_vect, Warehouse &warehouse, vector<CartOItem> *res_vect, SECTOR_ADDR_T starting_point, SECTOR_ADDR_T end_point)
{
    // vector where are going to be sorted the items
    vector<CartOItem> sorted_vect;
    int num_of_points = item_vect.size();

    // init class
    BruteForcePermutation bfp;
    // init vector according to the num_of_points
    vector<int> res;
    for (int i = 0; i < num_of_points; i++)
        res.push_back(i);

    // algorithm
    float best_solution_distance = MAXFLOAT;
    vector<CartOItem> best_solution_order;
    for (int i = 0; i < factorial(num_of_points); i++)
    {
        // compute factorial
        bfp.nextPermutation(res);

        // init tmp variables
        float current_solution_distance = 0;
        vector<CartOItem> current_solution_order;

        // accordign to the numbers compute the path in warehouse
        SECTOR_ADDR_T last_added_item_addr = starting_point;

        for (auto d = 0; d < num_of_points; d++)
        {
            // get index of current point according to the number in permutation
            int item_vect_idx = res[d];
            int dist = warehouse.getDistanceBetweenAddresses(last_added_item_addr, item_vect[item_vect_idx].getOItem().waddress->getWarehouseWAddress());
            current_solution_distance += dist;
            current_solution_order.push_back(item_vect[item_vect_idx]);
            // set the last processed addr
            last_added_item_addr = item_vect[item_vect_idx].getOItem().waddress->getWarehouseWAddress();
        }
        // compute return to the starting point
        current_solution_distance += warehouse.getDistanceBetweenAddresses(last_added_item_addr, end_point);

        // compare current and best distances
        if (current_solution_distance < best_solution_distance)
        {
            best_solution_distance = current_solution_distance;
            best_solution_order = current_solution_order;
        }
    }

    // return
    *res_vect = best_solution_order;
    return best_solution_distance;
}

int tsp_solver::justMeasureDistance(vector<CartOItem> item_vect, Warehouse &warehouse, SECTOR_ADDR_T starting_point, SECTOR_ADDR_T end_point)
{
    SECTOR_ADDR_T last_addr = starting_point;
    int dist = 0;
    for (size_t i = 0; i < item_vect.size(); i++)
    {
        dist += warehouse.getDistanceBetweenAddresses(last_addr, item_vect[i].getOItem().waddress->getWarehouseWAddress());
        last_addr = item_vect[i].getOItem().waddress->getWarehouseWAddress();
    }

    dist += warehouse.getDistanceBetweenAddresses(last_addr, end_point);

    return dist;
}

struct Edge
{
    SECTOR_ADDR_T first_point;
    SECTOR_ADDR_T second_point;
    int distance;
    Edge(const SECTOR_ADDR_T _first, const SECTOR_ADDR_T _second, int _dst)
        : first_point(_first), second_point(_second), distance(_dst) {}

    // sorting in ascending order
    bool operator<(const Edge &edge) const
    {
        return (distance < edge.distance);
    }

    bool operator==(const Edge &edge)
    {
        return ((first_point == edge.first_point && second_point == edge.second_point) || (first_point == edge.second_point && second_point == edge.first_point));
    }
};

/// @brief checker for cycle
/// @param edge_vect
/// @return true when cycle exists, otherwise false
bool checkForCycle(vector<Edge> edge_vect, int num_of_points)
{
    // save first point
    SECTOR_ADDR_T mark = edge_vect[0].first_point;

    // save the point from first
    SECTOR_ADDR_T last_point = edge_vect[0].second_point;

    // remove the first edge
    edge_vect.erase(edge_vect.begin());

    int num_of_consecv_edges_found = 1;
    while (1)
    {
        bool consecutive_edge_found = false;
        for (auto edge = edge_vect.begin(); edge != edge_vect.end(); edge++)
        {
            if (last_point == edge->first_point)
            {
                last_point = edge->second_point;
                consecutive_edge_found = true;
                num_of_consecv_edges_found++;
                edge_vect.erase(edge);
                break;
            }
            else if (last_point == edge->second_point)
            {
                last_point = edge->first_point;
                consecutive_edge_found = true;
                num_of_consecv_edges_found++;
                edge_vect.erase(edge);
                break;
            }
        }

        if (consecutive_edge_found)
        {
            // check whether the last point isn't marked point
            if (last_point == mark && num_of_points != num_of_consecv_edges_found)
            {
                // small cycle has been found ,return true
                return true;
            }
            else
                continue;
        }

        // when the consecutive edge hasn't been found take random from the edge vect
        if (!consecutive_edge_found && edge_vect.size() > 0)
        {
            // save first point
            mark = edge_vect[0].first_point;

            // save the point from first
            last_point = edge_vect[0].second_point;
            num_of_consecv_edges_found = 1;

            // remove the first edge
            edge_vect.erase(edge_vect.begin());
        }
        else
            break;
    }

    return false;
}

int tsp_solver::greedyAlgorithm(vector<CartOItem> item_vect, Warehouse &warehouse, vector<CartOItem> *res_vect, SECTOR_ADDR_T starting_point, SECTOR_ADDR_T end_point)
{
    // handle the case when only one item is in the cart
    if (item_vect.size() == 1)
    {
        *res_vect = item_vect;
        return warehouse.getDistanceBetweenAddresses(
                   starting_point,
                   item_vect[0].getOItem().waddress->getWarehouseWAddress()) +
               warehouse.getDistanceBetweenAddresses(item_vect[0].getOItem().waddress->getWarehouseWAddress(), end_point);
    }

    // extract all unique addresses
    vector<SECTOR_ADDR_T> unique_points = {starting_point, end_point};
    vector<pair<int, int>> tmp;
    for (auto item : item_vect)
    {
        int x = item.getOItem().waddress->getWarehouseWAddress().x_coord;
        int y = item.getOItem().waddress->getWarehouseWAddress().y_coord;
        if (find(begin(tmp), end(tmp), pair<int, int>(x, y)) == end(tmp))
        {
            tmp.push_back({x, y});
            unique_points.push_back(item.getOItem().waddress->getWarehouseWAddress());
        }
    }

    // create vector of all edges
    vector<Edge> edge_vect;
    for (size_t i = 0; i < unique_points.size(); i++)
    {
        for (size_t d = i + 1; d < unique_points.size(); d++)
        {
            // create edge
            edge_vect.push_back(Edge(
                unique_points[i],
                unique_points[d],
                warehouse.getDistanceBetweenAddresses(
                    unique_points[i],
                    unique_points[d])));
        }
    }

    // sort the edges from the shortest one (ascending)
    sort(edge_vect.begin(), edge_vect.end(), less<Edge>());
    //====================================================================================

    // do greedy
    Edge begin_end_edge = Edge(starting_point, end_point, warehouse.getDistanceBetweenAddresses(starting_point, end_point));
    vector<Edge> final_vect = {begin_end_edge};
    vector<SECTOR_ADDR_T> used_points_vect = {starting_point, end_point};

    for (auto edge : edge_vect)
    {
        int first_exists = 0, second_exists = 0;
        for (auto ucv : used_points_vect)
        {
            if (ucv == edge.first_point)
                first_exists++;
            else if (ucv == edge.second_point)
                second_exists++;
        }

        // edge contains points, that are connected already, continue
        if (first_exists == 2 || second_exists == 2)
        {
            continue;
        }

        used_points_vect.push_back(edge.first_point);
        used_points_vect.push_back(edge.second_point);
        final_vect.push_back(edge);

        if (final_vect.size() != 1 && checkForCycle(final_vect, unique_points.size()))
        {
            used_points_vect.pop_back();
            used_points_vect.pop_back();
            final_vect.pop_back();
            continue;
        }
    }

    // finally go through the edges and sort their points
    int final_distance = 0;
    SECTOR_ADDR_T last_point = starting_point;
    auto tmp_it = find(final_vect.begin(), final_vect.end(), begin_end_edge);
    if (tmp_it != end(final_vect))
        final_vect.erase(tmp_it);

    // do loop unless one edge remains - because that edge return to first point (saved as nearest), but we need to return to starting point
    while (final_vect.size() > 0)
    {
        for (auto edge = final_vect.begin(); edge != final_vect.end();)
        {
            if (last_point == edge->first_point)
            {
                for (auto item_it = item_vect.begin(); item_it != item_vect.end(); item_it++)
                {
                    if (item_it->getOItem().waddress->getWarehouseWAddress() == last_point)
                    {
                        res_vect->push_back(*item_it.base());
                    }
                }

                final_distance += edge->distance;
                last_point = edge->second_point;
                vector<Edge>::iterator tmp_it = edge;
                edge++;
                final_vect.erase(tmp_it);
                break;
            }
            else if (last_point == edge->second_point)
            {
                for (auto item_it = item_vect.begin(); item_it != item_vect.end(); item_it++)
                {
                    if (item_it->getOItem().waddress->getWarehouseWAddress() == last_point)
                    {
                        res_vect->push_back(*item_it.base());
                    }
                }

                final_distance += edge->distance;
                last_point = edge->first_point;
                vector<Edge>::iterator tmp_it = edge;
                edge++;
                final_vect.erase(tmp_it);
                break;
            }
            edge++;
        }
    }

    return final_distance;
}