/*
 * CMaze.cpp
 *
 *  Created on: 26 Oct 2023
 *      Author: max.petrak
 */

#include "CGame.h"

#include "eC_Math.h"

#include "GUITimer.h"
#include "GUIRect.h"

#include "CHardware.h"
#include "CLevel.h"

CGame::CGame(CHardware* pkHardware) :
        m_pkHardware(pkHardware),
        m_pkGUIBall(NULL),
        m_pkField(NULL),
        m_pkLevel(NULL),
        m_pkObserver(NULL)
{
    m_eGameState = GAME_PRESTART;
    m_kBallPos.m_vX = 0;
    m_kBallPos.m_vY = 0;
    m_vBallSize = 0;

    m_iSampleRate = 25;

    m_vSmallestMovement = 1;
}

CGame::~CGame()
{
    if (NULL != m_pkField)
    {
        delete m_pkField;
        m_pkField = NULL;
    }
}

void CGame::Start()
{
    if (!GETTIMER.IsAnimating(this))
    {
        GETTIMER.AddAnimationCallback(m_iSampleRate, this);
    }
}

void CGame::Stop()
{
    if (GETTIMER.IsAnimating(this))
    {
        GETTIMER.RemoveAnimationCallback(this);
    }
}

void CGame::SetSampleRate(eC_Int iSampleRate)
{
    m_iSampleRate = iSampleRate;
}
void CGame::SetLevel(CLevel* kLevel)
{
    m_pkLevel = kLevel;
    // in case the ball is not round, make it so
    m_pkGUIBall = m_pkLevel->GetBall();
    if (NULL != m_pkGUIBall)
    {
        m_vBallSize = m_pkGUIBall->GetHeight();
        m_pkGUIBall->SetWidth(m_vBallSize);
    }
    ChangeGameState(GAME_PRESTART);
    Reset();
}

eC_Int CGame::GetSampleRate()
{
    return m_iSampleRate;
}

void CGame::SetField(eC_Value vX1, eC_Value vY1, eC_Value vX2, eC_Value vY2)
{
    m_pkField = new CGUIRect(vX1, vY1, vX2, vY2);
}

void CGame::SetObserver(CGameObserver* pkObserver)
{
    m_pkObserver = pkObserver;
}

void CGame::ChangeGameState(GameState_t eGameState)
{
    m_eGameState = eGameState;
    m_pkObserver->OnGameStateChange(eGameState);
}

GameState_t CGame::GetGameState()
{
    return m_eGameState;
}

eC_Bool CGame::CheckCollisionBallRect(CGUIRect kBall, CGUIRect kRect)
{
    return Distance(kBall.GetCenter(),
        ClosestPointToPointOnRect(kRect, kBall.GetCenter()))
        < eC_Div(kBall.GetHeight(), 2);
}

// returns the closest point on the rect
CGUIPoint CGame::ClosestPointToPointOnRect(CGUIRect kRect, CGUIPoint kPoint)
{
    CGUIPoint kRectHalfExtends(eC_Div(kRect.GetWidth(), 2),
        eC_Div(kRect.GetHeight(), 2));
    CGUIPoint kRectCenter = kRect.GetCenter();
    CGUIPoint kDifference = PointMinusPoint(kPoint, kRectCenter);
    CGUIPoint kClamped = ClampPoint(kDifference, NegatePoint(kRectHalfExtends),
        kRectHalfExtends);
    CGUIPoint kClosestPoint = PointPlusPoint(kRectCenter, kClamped);

    return kClosestPoint;
}

// returns the distance between two points
eC_Value CGame::Distance(CGUIPoint kPoint1, CGUIPoint kPoint2)
{
    return eC_Sqrt(
        eC_Mul((kPoint2.m_vX - kPoint1.m_vX), (kPoint2.m_vX - kPoint1.m_vX)) + eC_Mul((kPoint2.m_vY - kPoint1.m_vY), (kPoint2.m_vY - kPoint1.m_vY)));
}

eC_Value CGame::Clamp(eC_Value vValue, eC_Value vMin, eC_Value vMax)
{
    return eC_Max(vMin, eC_Min(vMax, vValue));
}

CGUIPoint CGame::ClampPoint(CGUIPoint kPoint, CGUIPoint kMin, CGUIPoint kMax)
{
    return CGUIPoint(Clamp(kPoint.m_vX, kMin.m_vX, kMax.m_vX),
        Clamp(kPoint.m_vY, kMin.m_vY, kMax.m_vY));
}
CGUIPoint CGame::PointPlusPoint(CGUIPoint kPoint1, CGUIPoint kPoint2)
{
    return CGUIPoint(kPoint1.m_vX + kPoint2.m_vX, kPoint1.m_vY + kPoint2.m_vY);
}
CGUIPoint CGame::PointMinusPoint(CGUIPoint kPoint1, CGUIPoint kPoint2)
{
    return CGUIPoint(kPoint1.m_vX - kPoint2.m_vX, kPoint1.m_vY - kPoint2.m_vY);
}
CGUIPoint CGame::PointPlusValue(CGUIPoint kPoint, eC_Value vValue)
{
    return CGUIPoint(kPoint.m_vX + vValue, kPoint.m_vY + vValue);
}
CGUIPoint CGame::PointTimesValue(CGUIPoint kPoint, eC_Value vValue)
{
    return CGUIPoint(eC_Mul(kPoint.m_vX, vValue), eC_Mul(kPoint.m_vY, vValue));
}
CGUIPoint CGame::NegatePoint(CGUIPoint kPoint)
{
    return CGUIPoint(-kPoint.m_vX, -kPoint.m_vY);
}
eC_Value CGame::DotProduct(CGUIPoint kPoint1, CGUIPoint kPoint2)
{
    return (eC_Mul(kPoint1.m_vX, kPoint2.m_vX)
        + eC_Mul(kPoint1.m_vY, kPoint2.m_vY));
}
CGUIPoint CGame::NormalizeVector(CGUIPoint kPoint)
{
    return PointTimesValue(kPoint, eC_Div(1, Distance(CGUIPoint(0, 0), kPoint)));
}

void CGame::Reset()
{
    if (NULL != m_pkGUIBall)
    {
        m_pkGUIBall->SetAlpha(255);
    }
    m_kBallPos = m_pkLevel->GetStartingPoint();
    m_kBallVelocity = CGUIPoint(0, 0);

    if (GAME_PRESTART != m_eGameState)
    {
        Start();
        if (NULL != m_pkObserver)
        {
            ChangeGameState(GAME_PLAYING);
        }
    }
}

void CGame::DoAnimate(const eC_Value& vTimes)
{
    eC_Value vDeltaTime = eC_Mul(vTimes, m_iSampleRate);

    eC_Int iX = 0;
    eC_Int iY = 0;
    eC_Int iZ = 0;

    if (NULL != m_pkHardware)
        m_pkHardware->GetAccelerometerData(iX, iY, iZ);
    else
    {
        GUILOGMESSAGE("No hardware found, stopping the game\n");
        Stop();
        return;
    }

    m_kBallVelocity.m_vX += eC_Div(eC_Mul(eC_Div(iX, 100), vDeltaTime), 5000);
    m_kBallVelocity.m_vY -= eC_Div(eC_Mul(eC_Div(iY, 100), vDeltaTime), 5000);

    CGUIPoint kVectorOfBallMovement = PointMinusPoint(m_kBallPos,
        PointPlusPoint(m_kBallPos, m_kBallVelocity));

    // Save current ball position
    CGUIPoint kCurrentBallPos;
    if (NULL != m_pkGUIBall)
    {
        kCurrentBallPos = m_pkGUIBall->GetRelRect().GetTopLeft();
    }
    else
    {
        GUILOGMESSAGE("No ball found, stopping the game\n");
        Stop();
        return;
    }

    // Set potential new ball position
    m_kBallPos = PointPlusPoint(m_kBallPos, m_kBallVelocity);

    // Check all Obstacles for Collision
    if (NULL != m_pkLevel)
    {
        const CGUICompositeObject& pConstObstacleComposite =
            *(m_pkLevel->GetObstacles());
        ObjectPtrList kList = pConstObstacleComposite.GetChildObjectsList();
        ObjectPtrList::Iterator kIter;

        FOR_ALL_FORWARD(kIter, kList)
        {
            CGUIObject* pkObstacle = *kIter;
            if (NULL != pkObstacle)
            {

                CGUIPoint kBallCenter(m_kBallPos.m_vX + eC_Div(m_vBallSize, 2),
                    m_kBallPos.m_vY + eC_Div(m_vBallSize, 2));

                CGUIPoint kClosestPointOnRect = ClosestPointToPointOnRect(
                    pkObstacle->GetCurrentAbsRect(), kBallCenter);
                eC_Value vDistanceBallCenterRect = Distance(kClosestPointOnRect,
                    kBallCenter);
                eC_Value vRatioBallDistance = eC_Div(eC_Div(m_vBallSize, 2),
                    vDistanceBallCenterRect);

                // if the distance is shorter than the ballRadius
                if (1 < vRatioBallDistance)
                {
                    CGUIPoint kVectorClosestPointToBall = PointMinusPoint(
                        PointPlusValue(m_kBallPos, eC_Div(m_vBallSize, 2)),
                        kClosestPointOnRect);
                    CGUIPoint kOffset = PointPlusValue(
                        PointTimesValue(kVectorClosestPointToBall,
                            vRatioBallDistance), eC_Div(-m_vBallSize, 2));
                    m_kBallPos = PointPlusPoint(kClosestPointOnRect, kOffset);

                    // if the ball hit on top
                    if (ValuesClose(kClosestPointOnRect.m_vY,
                        pkObstacle->GetAbsYPos(), 5) && 0 < m_kBallVelocity.m_vY
                        && !(ValuesClose(kClosestPointOnRect.m_vX,
                            pkObstacle->GetAbsXPos(), 5)
                            || ValuesClose(kClosestPointOnRect.m_vX,
                                pkObstacle->GetAbsXPosREdge(), 5)))
                    {
                        m_kBallVelocity.m_vY = eC_Div(-m_kBallVelocity.m_vY, 2);
                    }
                    // if the ball hit left
                    else if (ValuesClose(kClosestPointOnRect.m_vX,
                        pkObstacle->GetAbsXPos(), 5) && 0 < m_kBallVelocity.m_vX
                        && !(ValuesClose(kClosestPointOnRect.m_vY,
                            pkObstacle->GetAbsYPos(), 5)
                            || ValuesClose(kClosestPointOnRect.m_vY,
                                pkObstacle->GetAbsYPosBEdge(), 5)))
                    {
                        m_kBallVelocity.m_vX = eC_Div(-m_kBallVelocity.m_vX, 2);
                    }
                    // if the ball hit right
                    else if (ValuesClose(kClosestPointOnRect.m_vX,
                        pkObstacle->GetAbsXPosREdge(), 5)
                        && 0 > m_kBallVelocity.m_vX
                        && !(ValuesClose(kClosestPointOnRect.m_vY,
                            pkObstacle->GetAbsYPos(), 5)
                            || ValuesClose(kClosestPointOnRect.m_vY,
                                pkObstacle->GetAbsYPosBEdge(), 5)))
                    {
                        m_kBallVelocity.m_vX = eC_Div(-m_kBallVelocity.m_vX, 2);
                    }
                    // if the ball hit on bottom
                    else if (ValuesClose(kClosestPointOnRect.m_vY,
                        pkObstacle->GetAbsYPosBEdge(), 5)
                        && 0 > m_kBallVelocity.m_vY
                        && !(ValuesClose(kClosestPointOnRect.m_vX,
                            pkObstacle->GetAbsXPos(), 5)
                            || ValuesClose(kClosestPointOnRect.m_vX,
                                pkObstacle->GetAbsXPosREdge(), 5)))
                    {
                        m_kBallVelocity.m_vY = eC_Div(-m_kBallVelocity.m_vY, 2);
                    }
                }
            }
        }

            CGUIPoint kBallCenter(m_kBallPos.m_vX + eC_Div(m_vBallSize, 2),
                m_kBallPos.m_vY + eC_Div(m_vBallSize, 2));

        const CGUICompositeObject* kConstHolesComposite =
            (m_pkLevel->GetCurrentHoles());
        if (NULL != kConstHolesComposite)
        {
            kList = kConstHolesComposite->GetChildObjectsList();

            FOR_ALL_FORWARD(kIter, kList)
            {
                CGUIObject* pkHole = *kIter;
                if (NULL != pkHole)
                {
                    if (Distance(kBallCenter,
                        pkHole->GetCurrentAbsRect().GetCenter()) < eC_Div(pkHole->GetCurrentAbsRect().GetHeight(), 2))
                    {
                        m_kBallPos = PointPlusValue(
                            pkHole->GetCurrentAbsRect().GetCenter(),
                            eC_Div(-m_vBallSize, 2));
                        m_kBallVelocity = CGUIPoint(0, 0);
                        if (NULL != m_pkGUIBall)
                        {
                            m_pkGUIBall->SetAlpha(128);
                        }
                        if (NULL != m_pkObserver)
                            {
                                ChangeGameState(GAME_LOST);
                            }
                    }
                }
            }
        }
        if (NULL != m_pkLevel->GetGoal())
        {
            // Check if the ball is in the GOAL
            CGUIPoint kGoalCenter(m_pkLevel->GetGoal()->GetAbsXPosCenter(),
                m_pkLevel->GetGoal()->GetAbsYPosCenter());
            // if the distance is shorter than the ballRadius
            if (Distance(kGoalCenter, kBallCenter) < eC_Div(m_vBallSize, 2))
            {
                if (NULL != m_pkObserver)
                    {
                        ChangeGameState(GAME_WON);
                    }
            }
        }
    }

    if (NULL != m_pkGUIBall && (Distance(kCurrentBallPos, m_kBallPos) > m_vSmallestMovement))
    {
        m_pkGUIBall->InvalidateArea();
        m_pkGUIBall->SetRelXPos(m_kBallPos.m_vX);
        m_pkGUIBall->SetRelYPos(m_kBallPos.m_vY);
        CGUIObject* pkBallShine = m_pkGUIBall->GetObjectByID(BALLSHINE);
        if (NULL != pkBallShine)
        {
            pkBallShine->SetRelXPos(eC_Div(m_vBallSize, eC_FromFloat(1.25)) - pkBallShine->GetWidth() - eC_Mul(eC_Div(m_kBallPos.m_vX, m_pkField->GetWidth()), eC_Div(m_vBallSize, eC_FromFloat(1.5)) - pkBallShine->GetWidth()));
            pkBallShine->SetRelYPos(eC_Div(m_vBallSize, eC_FromFloat(1.25)) - pkBallShine->GetHeight() - eC_Mul(eC_Div(m_kBallPos.m_vY, m_pkField->GetHeight()), eC_Div(m_vBallSize, eC_FromFloat(1.5)) - pkBallShine->GetHeight()));
        }
        m_pkGUIBall->InvalidateArea();
    }
}

eC_Bool CGame::ValuesClose(eC_Value vValue1, eC_Value vValue2, eC_Value vLeeway)
{
    return (vValue1 - vLeeway < vValue2 && vValue2 < vValue1 + vLeeway)
        || (vValue2 - vLeeway < vValue1 && vValue1 < vValue2 + vLeeway);
}
