//External includes
#include <iostream>
#include "SDL.h"
#include "SDL_surface.h"

//Project includes
#include "Renderer.h"
#include "Maths.h"
#include "Matrix.h"
#include "Material.h"
#include "Scene.h"
#include "Utils.h"

using namespace dae;

Renderer::Renderer(SDL_Window *pWindow) :
        m_pWindow(pWindow),
        m_pBuffer(SDL_GetWindowSurface(pWindow)) {
    //Initialize
    SDL_GetWindowSize(pWindow, &m_Width, &m_Height);
    m_pBufferPixels = static_cast<uint32_t *>(m_pBuffer->pixels);

    m_xvalues.reserve(m_Width);
    for (uint16_t x{}; x < m_Width; ++x) {
        m_xvalues.push_back(x);
    }

}

void Renderer::Render(Scene *pScene) const {
    Camera &camera = pScene->GetCamera();
    auto &materials = pScene->GetMaterials();
    auto &lights = pScene->GetLights();

    Matrix CameraToWorld = camera.CalculateCameraToWorld();

    float apsectRatio = static_cast<float>(m_Width) / m_Height;
    float fov = tan((camera.fovAngle * (PI / 180.f)) / 2);
    for (int px{}; px < m_Width; px += 1) {
        for (int py{}; py < m_Height; py += 1) {

            int actualPx = px;
//            if (py % 2 != 0) {
//                actualPx = px + 1;
//            }

            float NDCx = ((2 * ((actualPx + 0.5) / m_Width) - 1) * apsectRatio) * fov;
            float NDCy = (1 - 2 * ((py + 0.5) / m_Height)) * fov;


            Vector3 rayDir = Vector3{NDCx, NDCy, 1};
            rayDir = CameraToWorld.TransformVector((rayDir));
            rayDir.Normalize();

            Ray ray{camera.origin, rayDir};


            //set the color based on the ray direction
            ColorRGB finalColor{0.f, 0.f, 0.f};

            HitRecord closestHit{};
            pScene->GetClosestHit(ray, closestHit);


            if (closestHit.didHit) {
                for (const Light &light: lights) {
                    Vector3 lightDirection{LightUtils::GetDirectionToLight(light, closestHit.origin)};
                    float rayMaxValue = lightDirection.Normalize();
                    Ray lightRay = Ray{closestHit.origin + closestHit.normal * 0.01f, lightDirection, 0.0001f,
                                       rayMaxValue};
                    bool doesLightHit = pScene->DoesHit(lightRay);


                    if (m_CurrentLightingMode == LightingMode::Radiance) {
                        if (!m_ShadowsEnabled or (m_ShadowsEnabled and !doesLightHit)) {
                            auto radiance = LightUtils::GetRadiance(light, closestHit.origin);
                            finalColor += radiance;
                        }


                    } else if (m_CurrentLightingMode == LightingMode::ObservedArea) {
                        if (!m_ShadowsEnabled or (m_ShadowsEnabled and !doesLightHit)) {
//                            float observedArea = std::clamp(
//                                    LightUtils::LambertCosineLaw(closestHit.normal, lightDirection), 0.0f, 1.0f);
//                            finalColor.r += observedArea;
//                            finalColor.g += observedArea;
//                            finalColor.b += observedArea;
                        }
                    } else if (m_CurrentLightingMode == LightingMode::Combined) {
                        if (!m_ShadowsEnabled or (m_ShadowsEnabled and !doesLightHit)) {
                            auto radiance = LightUtils::GetRadiance(light, closestHit.origin);
                            float observedArea = std::clamp(
                                    LightUtils::LambertCosineLaw(closestHit.normal, lightDirection), 0.0f, 1.0f);

                            finalColor += radiance * observedArea *
                                          materials[closestHit.materialIndex]->Shade(closestHit, lightDirection,
                                                                                     rayDir);
                        }
                    } else if (m_CurrentLightingMode == LightingMode::BRDF) {
                        if (!m_ShadowsEnabled or (m_ShadowsEnabled and !doesLightHit)) {
                            auto radiance = LightUtils::GetRadiance(light, closestHit.origin);
                            float observedArea = std::clamp(
                                    LightUtils::LambertCosineLaw(closestHit.normal, lightDirection), 0.0f, 1.0f);

                            finalColor += materials[closestHit.materialIndex]->Shade(closestHit, lightDirection, ray.direction);
                        }
                    }

                }
            }


//            if (m_CurrentLightingMode == LightingMode::ObservedArea) {
//                if (closestHit.didHit) {
//                    for (const Light &light: lights) {
//                        Vector3 lightDirection{LightUtils::GetDirectionToLight(light, closestHit.origin)};
//                        const float lightDistance = lightDirection.Normalize();
//
//                        float law = std::clamp(LightUtils::LambertCosineLaw(closestHit.normal, lightDirection), 0.0f, 1.0f);
////                        finalColor += materials[closestHit.materialIndex]->Shade() * law;
//                        finalColor += ColorRGB{law, law, law};
//
//                    }
//                }
//            }
//
//            if (m_ShadowsEnabled) {
//
//                if (closestHit.didHit) {
////                    finalColor = materials[closestHit.materialIndex]->Shade();
//                    if (m_ShadowsEnabled) {
//                        for (const Light &light: lights) {
//                            Vector3 lightDirection{LightUtils::GetDirectionToLight(light, closestHit.origin)};
//                            const float lightDistance = lightDirection.Normalize();
//
//                            const Ray lightRay{
//                                    closestHit.origin + closestHit.normal * 0.01f,
//                                    lightDirection, 0.0001f, lightDistance
//                            };
//
//                            if (pScene->DoesHit(lightRay)) {
//                                finalColor *= 0.5;
//                                continue;
//                            }
//                        }
//                    }
//                }
//            }

            finalColor.MaxToOne();

            m_pBufferPixels[actualPx + (py * m_Width)] = SDL_MapRGB(m_pBuffer->format,
                                                                    static_cast<uint8_t>(finalColor.r * 255),
                                                                    static_cast<uint8_t>(finalColor.g * 255),
                                                                    static_cast<uint8_t>(finalColor.b * 255));

        }
    }

    // go over the pixels and avg the pixels i skipped
//    for (int px2{1}; px2 < m_Width; px2 += 2) {
//        for (int py{1}; py < m_Height; py += 1) {
//            if (px2 == m_Width - 1 || py == m_Height - 1) continue;
//            if (px2 == 0 || py == 0) continue;
//
//            int px = px2;
//            if (py % 2 != 0) {
//                px = px2 + 1;
//            }
//
//
//            uint8_t r = 0;
//            uint8_t g = 0;
//            uint8_t b = 0;
//            SDL_GetRGB(m_pBufferPixels[px + (py * m_Width)], m_pBuffer->format, &r, &g, &b);
//            ColorRGB currentColor = ColorRGB{r / 255.f, g / 255.f, b / 255.f};
//
//            SDL_GetRGB(m_pBufferPixels[(px - 1) + (py * m_Width)], m_pBuffer->format, &r, &g, &b);
//            ColorRGB leftColor = ColorRGB{r / 255.f, g / 255.f, b / 255.f};
//
//            SDL_GetRGB(m_pBufferPixels[(px + 1) + (py * m_Width)], m_pBuffer->format, &r, &g, &b);
//            ColorRGB rightColor = ColorRGB{r / 255.f, g / 255.f, b / 255.f};
//
//            SDL_GetRGB(m_pBufferPixels[px + ((py - 1) * m_Width)], m_pBuffer->format, &r, &g, &b);
//            ColorRGB upColor = ColorRGB{r / 255.f, g / 255.f, b / 255.f};
//
//            SDL_GetRGB(m_pBufferPixels[px + ((py + 1) * m_Width)], m_pBuffer->format, &r, &g, &b);
//            ColorRGB downColor = ColorRGB{r / 255.f, g / 255.f, b / 255.f};
//
//            float avgR = (leftColor.r + rightColor.r + upColor.r + downColor.r) / 4;
//            float avgG = (leftColor.g + rightColor.g + upColor.g + downColor.g) / 4;
//            float avgB = (leftColor.b + rightColor.b + upColor.b + downColor.b) / 4;
//            ColorRGB avgColor = ColorRGB{avgR, avgG, avgB};
//
//            m_pBufferPixels[px + (py * m_Width)] = SDL_MapRGB(m_pBuffer->format,
//                                                              static_cast<uint8_t>(avgColor.r * 255),
//                                                              static_cast<uint8_t>(avgColor.g * 255),
//                                                              static_cast<uint8_t>(avgColor.b * 255));
//        }
//    }

    //@END
    //Update SDL Surface
    SDL_UpdateWindowSurface(m_pWindow);
}

ColorRGB Renderer::Raytrace(const dae::Ray &viewRay, dae::Scene *pScene) const {
    ColorRGB finalColor{0.f, 0.f, 0.f};

    auto &materials = pScene->GetMaterials();
    Camera &camera = pScene->GetCamera();
    auto &lights = pScene->GetLights();

    HitRecord closestHit{};
    pScene->GetClosestHit(viewRay, closestHit);

    if (closestHit.didHit) {

        for (const Light &light: lights) {

            Vector3 lightDirection{LightUtils::GetDirectionToLight(light, closestHit.origin)};
            const float lightDistance = lightDirection.Normalize();

            const Ray lightRay{
                    closestHit.origin + closestHit.normal * 0.0001f,
                    lightDirection, 0.0001f, lightDistance
            };

            if (m_ShadowsEnabled && pScene->DoesHit(lightRay))
                continue;

            float cosAngle = LightUtils::LambertCosineLaw(closestHit.normal, lightDirection);


            return finalColor;
        }


    }
}


bool Renderer::SaveBufferToImage() const {
    return SDL_SaveBMP(m_pBuffer, "RayTracing_Buffer.bmp");
}


void Renderer::CycleLightingMode() {
    switch (m_CurrentLightingMode) {
        case LightingMode::ObservedArea:
            m_CurrentLightingMode = LightingMode::Radiance;
            std::cout << "Radiance" << std::endl;
            break;
        case LightingMode::Radiance:
            m_CurrentLightingMode = LightingMode::BRDF;
            std::cout << "BRDF" << std::endl;
            break;
        case LightingMode::BRDF:
            m_CurrentLightingMode = LightingMode::Combined;
            std::cout << "Combined" << std::endl;
            break;
        case LightingMode::Combined:
            m_CurrentLightingMode = LightingMode::ObservedArea;
            std::cout << "Observed Area" << std::endl;
            break;
    }

}
