// Solutions to the Advent Of Code 2024.
// Copyright (C) 2025  Stefan Müller
//
// This program is free software: you can redistribute it and/or modify it under
// the terms of the GNU General Public License as published by the Free Software
// Foundation, either version 3 of the License, or (at your option) any later
// version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
// FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// this program.  If not, see <http://www.gnu.org/licenses/>.

#include <aoc/RestroomRedoubt.hpp>

#include <algorithm>
#include <array>
#include <functional>
#include <numeric>
#include <sstream>

#include <aoc/common/Math.hpp>
#include <aoc/common/Point2.hpp>

RestroomRedoubt::RestroomRedoubt(const int width, const int height, const bool runPart2)
    : width_{ width }, height_{ height }, halfWidth_{ width / 2 }, halfHeight_{ height / 2 }, runPart2_{ runPart2 }
{
    quadrants_.fill(0);
}

const std::string RestroomRedoubt::getPuzzleName() const
{
    return "Restroom Redoubt";
}

const int RestroomRedoubt::getPuzzleDay() const
{
    return 14;
}

void RestroomRedoubt::processDataLine(const std::string& line)
{
    std::istringstream stream{ line };
    char c;
    Point2 p, v;
    stream >> c >> c >> p.x >> c >> p.y >> c >> c >> v.x >> c >> v.y;

    // Calculates the resulting position, and shifts negative results back into the target range.
    Point2 position = p + v * getNPredictionSeconds();
    position.x %= width_;
    if (position.x < 0)
    {
        position.x += width_;
    }
    position.y %= height_;
    if (position.y < 0)
    {
        position.y += height_;
    }

    // Determines the resulting quadrant from the position.
    if (position.x < halfWidth_)
    {
        if (position.y < halfHeight_)
        {
            quadrants_[0]++;
        }
        else if (position.y > halfHeight_)
        {
            quadrants_[1]++;
        }
    }
    else if (position.x > halfWidth_)
    {
        if (position.y < halfHeight_)
        {
            quadrants_[2]++;
        }
        else if (position.y > halfHeight_)
        {
            quadrants_[3]++;
        }
    }

    // Keeps track of the robot for part 2.
    robots_.push_back({ p, v });
}

void RestroomRedoubt::finish()
{
    part1 = std::accumulate(quadrants_.cbegin(), quadrants_.cend(), 1, std::multiplies<int>{});

    if (runPart2_)
    {
        findEasterEgg();
    }
}

constexpr int RestroomRedoubt::getNPredictionSeconds()
{
    return 100;
}

constexpr int RestroomRedoubt::getEasterEggThreshold()
{
    return 25;
}

void RestroomRedoubt::findEasterEgg()
{
    std::array<int, 2> patterns{ 0, 0 };
    std::array<int, 2> sizes{ width_, height_ };
    for (size_t i = 0; i < 2; i++)
    {
        int seconds{ 0 };
        while (patterns[i] == 0)
        {
            std::vector<int> buckets(sizes[i], 0);
            for (auto& robot : robots_)
            {
                buckets[robot.first[i]]++;
                robot.first[i] += robot.second[i];
                if (robot.first[i] < 0)
                {
                    robot.first[i] += sizes[i];
                }
                else if (robot.first[i] >= sizes[i])
                {
                    robot.first[i] -= sizes[i];
                }
            }
            auto it = std::find_if(buckets.cbegin(), buckets.cend(), [](int x) { return x > getEasterEggThreshold(); });
            if (it != buckets.cend())
            {
                patterns[i] = seconds;
            }
            seconds++;
        }
    }

    // Determines the iteration where the repeating patterns of horizontal and vertical bands overlap, that is
    //     patterns_0 + width * x = patterns_1 + height * y
    // for two positive integers 'x' and 'y'. This is equivalent to finding 'x' such that
    //     width * x ≡ patterns_1 - patterns_0 (modulo height)
    // We can determine the modular multiplicative inverse of 'width' modulo 'height' with the extended Euclidean
    // algorithm.
    const auto [gcd, invHeight, invWidth] = Math::extendedEuclid(height_, width_);
    // With that we can find 'x'.
    int x{ (invWidth * (patterns[1] - patterns[0])) % height_ };
    // And calculate when the overlap occurs.
    part2 = patterns[0] + width_ * x;
}