#include <cmath>
#include "ball.hpp"
#include "brick_grid.hpp"
#include "paddle.hpp"

// Вспомагательные функции для работы с векторами типа sf::Vector2f
float operator*(const sf::Vector2f& first, const sf::Vector2f& second)
{
    return first.x * second.x + first.y * second.y;
}

float norm(sf::Vector2f a)
{
    return std::sqrt(a.x * a.x + a.y * a.y);
}

float sqnorm(sf::Vector2f a)
{
    return a.x * a.x + a.y * a.y;
}




Ball::Ball(float radius, sf::Vector2f position, sf::Vector2f velocity) : 
    radius(radius), position(position), velocity(velocity) {}

void Ball::update(float dt) 
{
    position += velocity * dt;
}

void Ball::draw(sf::RenderWindow& window) 
{
    static sf::CircleShape shape {};
    shape.setRadius(radius);
    shape.setOrigin(radius, radius);
    shape.setFillColor(color);
    shape.setPosition(position);
    window.draw(shape);
}

std::pair<sf::Vector2f, bool> Ball::findClosestPoint(const sf::FloatRect& rect) const 
{
    float left   = rect.left;
    float right  = rect.left + rect.width;
    float bottom = rect.top + rect.height;
    float top    = rect.top;

    sf::Vector2f d;
    if (position.x < left)
        d.x = left;
    else if (position.x > right)
        d.x = right;
    else
        d.x = position.x;

    if (position.y > bottom)
        d.y = bottom;
    else if (position.y < top)
        d.y = top;
    else
        d.y = position.y;

    d -= position;
    bool isColiding = sqnorm(d) < radius * radius;
    return {d, isColiding};
}

bool Ball::handleRectCollision(const sf::FloatRect& rect)
{
    auto [d, isColiding] = findClosestPoint(rect);
    if (!isColiding)
        return false;

    float closestPointNorm = norm(d);
    // Если расстояние == 0, то это значит, что шарик за 1 фрейм зашёл центром внутрь блока
    // Отражаем шарик от блока
    if (closestPointNorm < 1e-4)
    {
        if (std::fabs(velocity.x) > std::fabs(velocity.y))
            velocity.x *= -1;
        else
            velocity.y *= -1;
    }
    // Если расстояние != 0, но шарик касается блока, то мы можем просчитать отражение более точно
    // Отражение от углов и по касательной.
    else if (closestPointNorm < radius)
    {
        position -= d * ((radius - closestPointNorm) / closestPointNorm);
        velocity -= 2.0f * d * (d * velocity) / (closestPointNorm * closestPointNorm);       
    }
    return true;
}

void Ball::handleWallsCollision(sf::FloatRect boundary)
{
    if (position.x < boundary.left + radius)
    {
        position.x = boundary.left + radius;
        velocity.x *= -1;
    }
    if (position.x > boundary.left + boundary.width - radius)
    {
        position.x = boundary.left + boundary.width - radius;
        velocity.x *= -1;
    }
    if (position.y < boundary.top + radius)
    {
        position.y = boundary.top + radius;
        velocity.y *= -1;
    }
}


std::pair<int, int> Ball::handleBrickGridCollision(const BrickGrid& brickGrid)
{
    auto [gridColumns, gridRows] = brickGrid.getGridSizes();
    auto [brickWidth, brickHeight] = brickGrid.getBrickSizes();
    auto [left, top, width, height] = brickGrid.getBorder();
    
    // Определяем координаты блоков с которыми шарик может пересечься
    int brickColumnStart  = std::clamp(static_cast<int>((position.x - left - radius) / brickWidth),      0, gridColumns);
    int brickColumnFinish = std::clamp(static_cast<int>((position.x - left + radius) / brickWidth) + 1,  0, gridColumns);
    int brickRowStart     = std::clamp(static_cast<int>((position.y - top  - radius) / brickHeight),     0, gridRows);
    int brickRowFinish    = std::clamp(static_cast<int>((position.y - top  + radius) / brickHeight) + 1, 0, gridRows);

    // Если шарик находится вне сетки блоков, то выходим
    if (brickColumnStart == brickColumnFinish || brickRowStart == brickRowFinish)
        return {-1, -1};

    // Находим ближайший к центру шарика активный пересекаемый шариком блок
    float closestSqNorm = width * width + height * height;
    std::pair<int, int> closestBrickIndexes = {-1, -1};
    for (int i = brickColumnStart; i < brickColumnFinish; ++i)
    {
        for (int j = brickRowStart; j < brickRowFinish; ++j)
        {
            if (!brickGrid.isBrickActive({i, j}))
                continue;

            sf::FloatRect rect {left + i * brickWidth, top + j * brickHeight, brickWidth, brickHeight};
            auto [d, isColiding] = findClosestPoint(rect);
            if (!isColiding)
                continue;

            if (sqnorm(d) < closestSqNorm)
            {
                closestSqNorm = sqnorm(d);
                closestBrickIndexes = {i, j};
            }
        }
    }

    // Если такого не нашли, то возвращаем {-1, -1}
    if (closestBrickIndexes.first == -1)
        return closestBrickIndexes;

    // Упруго сталкиваем шарик с найденым блоком
    sf::FloatRect rect {left + closestBrickIndexes.first * brickWidth, top + closestBrickIndexes.second * brickHeight, brickWidth, brickHeight};
    handleRectCollision(rect);

    // Возвращаем координаты блока в сетки блоков
    return closestBrickIndexes;
}

// Обрабатываем столкновения шарика с ракеткой
void Ball::handlePaddleCollision(const Paddle& paddle)
{
    auto [d, isColiding] = findClosestPoint(paddle.getBorder());
    if (!isColiding)
        return;

    // Столкновение не упругое
    // Угол отражения зависит от места на ракетке, куда стукнулся шарик
    // Если шарик стукнулся в левую часть ракетки, то он должен полететь влево.
    // Если в правую часть ракетки, то вправо.
    const float pi = 3.14159265;
    float velocityAngle = (position.x - paddle.position.x) / (paddle.size.x + 2 * radius) * (0.8 * pi) + pi / 2;
    float velocityNorm = norm(velocity);
    velocity.x = - velocityNorm * std::cos(velocityAngle);
    velocity.y = - velocityNorm * std::sin(velocityAngle);
}
