#include <vector>
#include <map>
#include <set>
#include <string>

#include "utils.h"
#include "range_structures.h"

using namespace std;

static vector<uint64_t> findParticularSolution(const ArrayDimension& dim1, const ArrayDimension& dim2)
{
    for (uint64_t i = 0; i < dim1.tripCount; i++)
    {
        uint64_t leftPart = dim1.start + i * dim1.step;
        for (uint64_t j = 0; j < dim2.tripCount; j++)
        {
            uint64_t rightPart = dim2.start + j * dim2.step;
            if (leftPart == rightPart)
                return { i, j };
        }
    }
    return {};
}

/* dim1 /\ dim2 */
static ArrayDimension* dimensionIntersection(const ArrayDimension& dim1, const ArrayDimension& dim2)
{
    vector<uint64_t> partSolution = findParticularSolution(dim1, dim2);
    if (partSolution.empty())
        return NULL;

    int64_t x0 = partSolution[0], y0 = partSolution[1];
    /* x = x_0 + c * t */
    /* y = y_0 + d * t */
    int64_t c = dim2.step / gcd(dim1.step, dim2.step);
    int64_t d = dim1.step / gcd(dim1.step, dim2.step);
    int64_t tXMin, tXMax, tYMin, tYMax;
    tXMin = -x0 / c;
    tXMax = (dim1.tripCount - 1 - x0) / c;
    tYMin = -y0 / d;
    tYMax = (dim2.tripCount - 1 - y0) / d;
    int64_t tMin = max(tXMin, tYMin);
    uint64_t tMax = min(tXMax, tYMax);
    if (tMin > tMax)
        return NULL;

    uint64_t start3 = dim1.start + x0 * dim1.step;
    uint64_t step3 = c * dim1.step;
    ArrayDimension* result = new(ArrayDimension){ start3, step3, tMax + 1 , dim1.array };
    return result;
}

/* dim1 / dim2 */
static vector<ArrayDimension> dimensionDifference(const ArrayDimension& dim1, const ArrayDimension& dim2)
{
    ArrayDimension* intersection = dimensionIntersection(dim1, dim2);
    if (!intersection)
        return { dim1 };

    vector<ArrayDimension> result;
    /* add the part before intersection */
    if (dim1.start < intersection->start)
        result.push_back({ dim1.start, dim1.step, (intersection->start - dim1.start) / dim1.step, dim1.array });

    /* add the parts between intersection steps */
    if (intersection->step > dim1.step)
    {
        uint64_t start = (intersection->start - dim1.start) / dim1.step;
        uint64_t interValue = intersection->start;
        for (int64_t i = start; interValue <= intersection->start + intersection->step * (intersection->tripCount - 1); i++)
        {
            result.push_back({ interValue + dim1.step, dim1.step, intersection->step / dim1.step, dim1.array });
            interValue += intersection->step;
        }
    }
    /* add the part after intersection */
    if (intersection->start + intersection->step * (intersection->tripCount - 1) < dim1.start + dim1.step * (dim1.tripCount - 1))
    {
        /* first value after intersection */
        uint64_t right_start = intersection->start + intersection->step * (intersection->tripCount - 1) + dim1.step;
        uint64_t tripCount = (dim1.start + dim1.step * dim1.tripCount - right_start) / dim1.step;
        result.push_back({ right_start, dim1.step, tripCount, dim1.array });
    }
    delete(intersection);
    return result;
}


static vector<ArrayDimension> dimensionUnion(const ArrayDimension& dim1, const ArrayDimension& dim2)
{
    vector<ArrayDimension> res;
    ArrayDimension* inter = dimensionIntersection(dim1, dim2);
    if (!inter)
        return { dim1, dim2 };

    res.push_back(*inter);
    delete(inter);

    vector<ArrayDimension> diff1, diff2;
    diff1 = dimensionDifference(dim1, dim2);
    diff2 = dimensionDifference(dim2, dim1);
    res.insert(res.end(), diff1.begin(), diff1.end());
    res.insert(res.end(), diff2.begin(), diff2.end());
    return res;
}

static vector<ArrayDimension> elementsIntersection(const vector<ArrayDimension>& firstElement, const vector<ArrayDimension>& secondElement)
{
    if (firstElement.empty() || secondElement.empty())
        return {};

    size_t dimAmount = firstElement.size();
    /* check if there is no intersecction */
    for (size_t i = 0; i < dimAmount; i++)
        if (findParticularSolution(firstElement[i], secondElement[i]).empty())
            return {};

    vector<ArrayDimension> result(dimAmount);
    for (size_t i = 0; i < dimAmount; i++)
    {
        ArrayDimension* resPtr = dimensionIntersection(firstElement[i], secondElement[i]);
        if (resPtr)
            result[i] = *resPtr;
        else
            return {};

    }
    return result;
}

static vector<vector<ArrayDimension>> elementsDifference(const vector<ArrayDimension>& firstElement,
    const vector<ArrayDimension>& secondElement)
{
    if (firstElement.empty())
        return {};
    if (secondElement.empty())
        return { firstElement };

    vector<ArrayDimension> intersection = elementsIntersection(firstElement, secondElement);
    vector<vector<ArrayDimension>> result;
    if (intersection.empty())
        return { firstElement };

    for (int i = 0; i < firstElement.size(); i++)
    {
        auto dimDiff = dimensionDifference(firstElement[i], secondElement[i]);
        if (!dimDiff.empty())
        {
            vector<ArrayDimension> firstCopy = firstElement;
            for (const auto& range : dimDiff)
            {
                firstCopy[i] = range;
                result.push_back(firstCopy);
            }
        }
    }
    return result;
}

static void elementsUnion(const vector<ArrayDimension>& firstElement, const vector<ArrayDimension>& secondElement,
    vector<vector<ArrayDimension>>& lc, vector<vector<ArrayDimension>>& rc,
    vector<ArrayDimension>& intersection)
{
    /* lc(rc) is a set of ranges, which only exist in first(second) element*/
    intersection = elementsIntersection(firstElement, secondElement);
    lc = elementsDifference(firstElement, intersection);
    rc = elementsDifference(secondElement, intersection);
}

void AccessingSet::FindUncovered(const vector<ArrayDimension>& element, vector<vector<ArrayDimension>>& result) const {
    vector<vector<ArrayDimension>> newTails;
    result.push_back(element);
    for (const auto& currentElement : allElements)
    {
        for (const auto& tailLoc : result)
        {
            auto intersection = elementsIntersection(tailLoc, currentElement);
            auto diff = elementsDifference(tailLoc, intersection);
            if (!diff.empty()) {
                newTails.insert(newTails.end(), diff.begin(), diff.end());
            }
        }
        result = newTails;
        newTails.clear();
    }
}

bool AccessingSet::ContainsElement(const vector<ArrayDimension>& element) const
{
    vector<vector<ArrayDimension>> tails;
    FindUncovered(element, tails);
    return tails.empty();
}

void AccessingSet::FindCoveredBy(const vector<ArrayDimension>& element, vector<vector<ArrayDimension>>& result) const
{
    for (const auto& currentElement : allElements)
    {
        auto intersection = elementsIntersection(element, currentElement);
        if (!intersection.empty())
            result.push_back(intersection);
    }
}

const vector<vector<ArrayDimension>>& AccessingSet::GetElements() const { return allElements; }

void AccessingSet::Insert(const vector<ArrayDimension>& element)
{
    vector<vector<ArrayDimension>> tails;
    FindUncovered(element, tails);
    allElements.insert(allElements.end(), tails.begin(), tails.end());
}

AccessingSet AccessingSet::Union(const AccessingSet& source) {
    if (source.GetElements().empty())
        return *this;
    if (allElements.empty())
        return source;
    AccessingSet result;
    for (auto& element : source.GetElements())
        result.Insert(element);

    for (auto& element : allElements)
        result.Insert(element);
    return result;
}

AccessingSet AccessingSet::Intersect(const AccessingSet& secondSet) const
{
    vector<vector<ArrayDimension>> result;
    if (secondSet.GetElements().empty() || this->allElements.empty())
        return AccessingSet(result);

    for (const auto& element : allElements)
    {
        if (secondSet.ContainsElement(element))
            result.push_back(element);
        else
        {
            vector<vector<ArrayDimension>> coveredBy;
            secondSet.FindCoveredBy(element, coveredBy);
            if (!coveredBy.empty())
                result.insert(result.end(), coveredBy.begin(), coveredBy.end());
        }
    }

    return AccessingSet(result);
}

AccessingSet AccessingSet::Diff(const AccessingSet& secondSet) const
{
    if (secondSet.GetElements().empty() || allElements.empty())
        return *this;

    AccessingSet intersection = this->Intersect(secondSet);
    vector<vector<ArrayDimension>> uncovered;
    for (const auto& element : allElements)
    {
        vector<vector<ArrayDimension>> current_uncovered;
        intersection.FindUncovered(element, current_uncovered);
        uncovered.insert(uncovered.end(),
            std::move_iterator(current_uncovered.begin()),
            std::move_iterator(current_uncovered.end())
        );
    }
    return uncovered;
}

bool operator!=(const ArrayDimension& lhs, const ArrayDimension& rhs)
{
    return !(lhs.start == rhs.start && lhs.step == rhs.step && lhs.tripCount == rhs.tripCount);
}


bool operator!=(const AccessingSet& lhs, const AccessingSet& rhs)
{
    for (size_t i = 0; i < lhs.allElements.size(); i++)
        for (size_t j = 0; j < lhs.allElements[i].size(); j++)
            if (lhs.allElements[i][j] != rhs.allElements[i][j])
                return true;

    return false;
}

bool operator!=(const arrayAccessingIndexes& lhs, const arrayAccessingIndexes& rhs)
{
    if (lhs.size() != rhs.size())
        return true;

    for (auto& [key, value] : lhs)
        if (rhs.find(key) == rhs.end())
            return true;

    return false;
}