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

//Project includes
#include "Renderer.h"
#include "Maths.h"
#include "HitTest.h"
#include "utils.h"

using namespace dae;

Renderer::Renderer(SDL_Window *pWindow) : m_pWindow(pWindow) {
    //Initialize
    SDL_GetWindowSize(pWindow, &m_Width, &m_Height);

    //Create Buffers
    m_pFrontBuffer = SDL_GetWindowSurface(pWindow);
    m_pBackBuffer = SDL_CreateRGBSurface(0, m_Width, m_Height, 32, 0, 0, 0, 0);
    m_pBackBufferPixels = (uint32_t *) m_pBackBuffer->pixels;

    m_pDepthBufferPixels = new float[m_Width * m_Height];

    //Initialize Camera
    m_aspectRatio = static_cast<float>(m_Width) / static_cast<float>(m_Height);
    m_Camera.Initialize(45.f, {.0f, 5.0f, -64.f}, m_aspectRatio);

    m_currentDiffuse = Texture::LoadFromFile("./Resources/vehicle_diffuse.png");
    m_currentGloss = Texture::LoadFromFile("./Resources/vehicle_gloss.png");
    m_currentNormal = Texture::LoadFromFile("./Resources/vehicle_normal.png");
    m_currentSpecular = Texture::LoadFromFile("./Resources/vehicle_specular.png");

    Utils::ParseOBJ("./Resources/vehicle.obj", m_mesh.vertices, m_mesh.indices, false);
    m_worldMeshes.push_back(m_mesh);
}

Renderer::~Renderer() {
    delete[] m_pDepthBufferPixels;
}

void Renderer::Update(Timer *pTimer) {
    m_Camera.Update(pTimer);

    //Rotate the mesh
    if(m_isRotating){
        m_worldMeshes[0].worldMatrix = Matrix::CreateRotationY(SDL_GetTicks() / 1000.f);
    }
}

void Renderer::Render() {
    //@START
    //Lock BackBuffer
    //Random color
    SDL_FillRect(m_pBackBuffer, nullptr, m_ClearColor);
    SDL_LockSurface(m_pBackBuffer);
    std::fill_n(m_pDepthBufferPixels, m_Width * m_Height, std::numeric_limits<float>::max());

    //Clear
    SDL_FillRect(m_pBackBuffer, nullptr, m_ClearColor);


    ColorRGB finalColor{};
    constexpr int numVerticies = 3;

    std::vector<Vertex> verticiesScreenSpace{};

    for (const Mesh &currentMesh: m_worldMeshes) {

        const Matrix worldViewProjectionMatrix{ currentMesh.worldMatrix * m_Camera.viewMatrix * m_Camera.ProjectionMatrix };
        VertexTransformationFunction(worldViewProjectionMatrix, currentMesh, currentMesh.vertices, verticiesScreenSpace);

        int numTriangles{};

        switch (currentMesh.primitiveTopology) {
            case PrimitiveTopology::TriangleList:
                numTriangles = static_cast<int>(currentMesh.indices.size()) / numVerticies;
                break;
            case PrimitiveTopology::TriangleStrip:
                numTriangles = static_cast<int>(currentMesh.indices.size()) - 2;
                break;
        }


        for (int triangleIndex{}; triangleIndex < numTriangles; ++triangleIndex) {

            Vertex vertex0, vertex1, vertex2;

            switch (currentMesh.primitiveTopology) {
                case PrimitiveTopology::TriangleList:
                    vertex0 = verticiesScreenSpace[currentMesh.indices[triangleIndex * numVerticies + 0]];
                    vertex1 = verticiesScreenSpace[currentMesh.indices[triangleIndex * numVerticies + 1]];
                    vertex2 = verticiesScreenSpace[currentMesh.indices[triangleIndex * numVerticies + 2]];
                    break;
                case PrimitiveTopology::TriangleStrip:
                    vertex0 = verticiesScreenSpace[currentMesh.indices[triangleIndex + 0]];
                    vertex1 = verticiesScreenSpace[currentMesh.indices[triangleIndex + 1]];
                    vertex2 = verticiesScreenSpace[currentMesh.indices[triangleIndex + 2]];

                    if (triangleIndex % 2 == 1) {
                        std::swap(vertex1, vertex2);
                    }

                    if (vertex0.position == vertex1.position ||
                        vertex0.position == vertex2.position ||
                        vertex1.position == vertex2.position) {
                        continue;
                    }
                    break;
            }
            if (!vertex0.valid and !vertex1.valid and !vertex2.valid) {
                continue;
            }


            const float minX{std::min(vertex0.position.x, std::min(vertex1.position.x, vertex2.position.x))};
            const float minY{std::min(vertex0.position.y, std::min(vertex1.position.y, vertex2.position.y))};

            const float maxX{std::max(vertex0.position.x, std::max(vertex1.position.x, vertex2.position.x))};
            const float maxY{std::max(vertex0.position.y, std::max(vertex1.position.y, vertex2.position.y))};


            const Vector3 side1{vertex1.position - vertex0.position};
            const Vector3 side2{vertex2.position - vertex0.position};

            const float totalArea{Vector3::Cross(side1, side2).z};

            const int startX = std::max(0, static_cast<int>(minX)) - 1;
            const int endX = std::min(m_Width - 1, static_cast<int>(maxX)) + 1;

            const int startY = std::max(0, static_cast<int>(minY)) - 1;
            const int endY = std::min(m_Height - 1, static_cast<int>(maxY)) + 1;

            for (int px{startX}; px < endX; ++px) {
                for (int py{startY}; py < endY; ++py) {

                    if (px < 0 || px >= m_Width || py < 0 || py >= m_Height) {
                        //TEMP fix for out of bounds
                        //This is to remove triangles having an incorrect edge on their bounding box
                        continue;
                    }

                    Vector3 P{static_cast<float>(px) + 0.5f, static_cast<float>(py) + 0.5f, 1.f};

                    auto sample{ HitTest::TriangleHitTest(P, vertex0, vertex1, vertex2) };

                    if (!sample.has_value()) {
                        continue;
                    }

                    const Vector3 fragPos{ static_cast<float>(px) + 0.5f, static_cast<float>(py) + 0.5f, 1.f };

                    int depthBufferIndex{ px + (py * m_Width) };

                    float min{.985f};
                    float max{1.f};
                    float depthBuffer{(sample.value().depth - min) * (max - min)};
                    float currentDepth = sample.value().depth;

                    if (m_pDepthBufferPixels[depthBufferIndex] > currentDepth) {
                        m_pDepthBufferPixels[depthBufferIndex] = currentDepth;

                        if (m_isDepthBuffer) {
                            finalColor = ColorRGB{depthBuffer, depthBuffer, depthBuffer};
                        } else {
//                            finalColor = m_pTexture->Sample(UvCoords);
                            finalColor = shadePixel(sample.value());
                        }

                        finalColor.MaxToOne();

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

                    }



                }
            }
        }
    }



//RENDER LOGIC

    SDL_UnlockSurface(m_pBackBuffer);
    SDL_BlitSurface(m_pBackBuffer,
                    nullptr, m_pFrontBuffer, nullptr);
    SDL_UpdateWindowSurface(m_pWindow);
}

void Renderer::VertexTransformationFunction(const Matrix& WorldViewProjectionMatrix, const Mesh& mesh, const std::vector<Vertex> &vertices_in,
                                            std::vector<Vertex> &vertices_out) const {
//    vertices_out.clear();
    for (const Vertex& vert : vertices_in)
    {
        Vertex vertex_out{};

        Vector4 vertPos{ WorldViewProjectionMatrix.TransformPoint({vert.position.GetXYZ(), 1}) };

        const Vector3 normal{ mesh.worldMatrix.TransformVector(vert.normal) };
        const Vector3 tangent{ mesh.worldMatrix.TransformVector(vert.tangent) };



        vertPos.x /= vertPos.w;
        vertPos.y /= vertPos.w;
        vertPos.z /= vertPos.w;

        bool isValid{ true };

        //Check if the vertex is inside the screen
        if (vertPos.x < -1.f || vertPos.x > 1.f ||
            vertPos.y < -1.f || vertPos.y > 1.f ||
            vertPos.z <  0.f || vertPos.z > 1.f)
            isValid = false;

        vertPos.x = ((vertPos.x + 1.f) / 2.f) * static_cast<float>(m_Width);
        vertPos.y = ((1.f - vertPos.y) / 2.f) * static_cast<float>(m_Height);

        vertex_out.position = vertPos;
        vertex_out.color = vert.color;
        vertex_out.uv = vert.uv;
        vertex_out.normal = normal;
        vertex_out.tangent = tangent;
        vertex_out.viewDir = WorldViewProjectionMatrix.TransformVector(vert.viewDir);
        vertex_out.valid = isValid;

        vertices_out.push_back(vertex_out);
    }

}

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

ColorRGB Renderer::shadePixel(const Sample &sample) {
    Vector3 lightDirection = { .577f, -.577f, .577f};
    Vector3 normal = sample.normal.Normalized();
    constexpr float lightIntensity{ 7.f };

    ColorRGB color{ 1, 1, 1 };
    constexpr ColorRGB ambient{ .03f, .03f, .03f};

    if(m_useNormals){
        const ColorRGB normalSample{ m_currentNormal->Sample(sample.uv) };
        const Vector4 normalMapSample{
            2.f * normalSample.r - 1.f,
            2.f * normalSample.g - 1.f,
            2.f * normalSample.b - 1.f,
            0.f
        };

        const Vector3 biNormal{ Vector3::Cross(normal, sample.tangent) };
        const Matrix tangentToWorld{
            Vector4{ sample.tangent, 0.f },
            Vector4{ biNormal, 0.f },
            Vector4{ normal, 0.f },
            Vector4{ 0.f, 0.f, 0.f, 1.f }
        };
        normal = tangentToWorld.TransformVector(normalMapSample).Normalized();
    }


    const ColorRGB diffuseSample{ m_currentDiffuse->Sample(sample.uv) };
    double invPi = 1.0 / PI;
    const ColorRGB lambert{ diffuseSample * lightIntensity * invPi };

    //TODO: ask why deviding by PI causses Segmentation fault
//    const ColorRGB lambert{ diffuseSample * lightIntensity / PI };


    float specularReflectance{ 1.f };
    float shininess{ 25.f };

    specularReflectance *= m_currentGloss->Sample(sample.uv).r;
    shininess *= m_currentSpecular->Sample(sample.uv).r;

    const float cosAngle = Vector3::Dot(normal, -lightDirection);

    const ColorRGB specular = specularReflectance * powf(cosAngle, shininess) * colors::White;

    if (cosAngle < 0) {
		return ambient;
    }

    switch(m_ShadeMode){
        case ShadeMode::ObservedArea:
            break;
        case ShadeMode::Diffuse:
            color = lambert;
            break;
		case ShadeMode::Specular:
			color = specular;
			break;
		case ShadeMode::Combined:
			color = lambert + specular + ambient;
			break;
    }

    color *= ColorRGB{ cosAngle, cosAngle, cosAngle };

    return color;
}
