#ifndef PASTELGFX_DRAW_TEXTURED_TRIANGLE_HPP
#define PASTELGFX_DRAW_TEXTURED_TRIANGLE_HPP
#include "pastel/gfx/drawing/draw_textured_triangle.h"
#include "pastel/gfx/color/colormixer/assign_colormixer.h"
#include "pastel/geometry/shape/plane.h"
#include "pastel/geometry/predicates.h"
#include "pastel/sys/syscommon.h"
#include "pastel/sys/vector/vector_tools.h"
#include <vector>
#include <algorithm>
namespace Pastel
{
    namespace DrawTexturedTriangle_
    {
        class TextureVertex
        {
        public:
            TextureVertex()
                : x_(0)
                , y_(0)
                , uv_(0)
            {
            }
            TextureVertex(
                const Vector2& position,
                const Vector2& uv,
                integer index)
                : x_(position[0])
                , y_(position[1])
                , uv_(uv)
                , index_(index)
            {
            }
            dreal x_;
            dreal y_;
            Vector2 uv_;
            integer index_;
        };
        class VerticalComparison
        {
        public:
            bool operator()(
                const TextureVertex& left,
                const TextureVertex& right) const
            {
                if (left.y_ != right.y_)
                {
                    return left.y_ < right.y_;
                }
                if (left.x_ != right.x_)
                {
                    return left.x_ < right.x_;
                }
                return left.index_ < right.index_;
            }
        };
        class HorizontalComparison
        {
        public:
            bool operator()(
                const TextureVertex& left,
                const TextureVertex& right) const
            {
                if (left.x_ != right.x_)
                {
                    return left.x_ < right.x_;
                }
                if (left.y_ != right.y_)
                {
                    return left.y_ < right.y_;
                }
                return left.index_ < right.index_;
            }
        };
    }
    template <typename Type,
        typename Image_View, typename ColorMixer>
    void drawTriangle(
        const Triangle2& triangle,
        const Triangle2& uvTriangle,
        const Texture<Type>& textureSampler,
        const View<2, Type, Image_View>& image,
        const ColorMixer& colorMixer)
    {
        using Vertex = DrawTexturedTriangle_::TextureVertex;
        // Order vertices vertically.
        std::vector<Vertex> yVertex;
        yVertex.reserve(3);
        yVertex.push_back(Vertex(triangle[0], uvTriangle[0], 0));
        yVertex.push_back(Vertex(triangle[1], uvTriangle[1], 1));
        yVertex.push_back(Vertex(triangle[2], uvTriangle[2], 2));
        std::sort(yVertex.begin(), yVertex.end(),
            DrawTexturedTriangle_::VerticalComparison());
        const Vertex& yMinVertex = yVertex[0];
        const Vertex& yMidVertex = yVertex[1];
        const Vertex& yMaxVertex = yVertex[2];
        // Order vertices horizontally.
        std::vector<Vertex> xVertex;
        xVertex.reserve(3);
        xVertex.push_back(Vertex(triangle[0], uvTriangle[0], 0));
        xVertex.push_back(Vertex(triangle[1], uvTriangle[1], 1));
        xVertex.push_back(Vertex(triangle[2], uvTriangle[2], 2));
        std::sort(xVertex.begin(), xVertex.end(),
            DrawTexturedTriangle_::HorizontalComparison());
        const Vertex& xMinVertex = xVertex[0];
        const Vertex& xMidVertex = xVertex[1];
        const Vertex& xMaxVertex = xVertex[2];
        // Cull and clip.
        integer xMin = toPixelSpanPoint(xMinVertex.x_);
        integer xMax = toPixelSpanPoint(xMaxVertex.x_);
        integer yMin = toPixelSpanPoint(yMinVertex.y_);
        integer yMid = toPixelSpanPoint(yMidVertex.y_);
        integer yMax = toPixelSpanPoint(yMaxVertex.y_);
        integer width = image.width();
        integer height = image.height();
        if (xMax <= 0 || xMin >= width ||
            yMax <= 0 || yMin >= height)
        {
            return;
        }
        if (yMin < 0)
        {
            yMin = 0;
        }
        if (yMax > height)
        {
            yMax = height;
        }
        if (yMid < 0)
        {
            yMid = 0;
        }
        if (yMid > height)
        {
            yMid = height;
        }
        // The polygon spans the following pixel y ranges:
        // [yMin, yMid[
        // [yMid, yMax[
        // Compute delta's w.r.t. y.
        dreal xLeftDelta = 0;
        dreal yLeftDelta = 0;
        Vector2 uvLeftDelta;
        dreal xRightDelta = 0;
        dreal yRightDelta = 0;
        Vector2 uvRightDelta;
        bool longLeftEdge =
            side(
            Vector2(yMidVertex.x_, yMidVertex.y_),
            Plane2(
            Vector2(yMinVertex.x_, yMinVertex.y_),
            cross(Vector2(yMaxVertex.x_, yMaxVertex.y_) -
            Vector2(yMinVertex.x_, yMinVertex.y_)))) < 0;
        if (longLeftEdge)
        {
            xLeftDelta = yMaxVertex.x_ - yMinVertex.x_;
            yLeftDelta = yMaxVertex.y_ - yMinVertex.y_;
            uvLeftDelta = yMaxVertex.uv_ - yMinVertex.uv_;
            xRightDelta = yMidVertex.x_ - yMinVertex.x_;
            yRightDelta = yMidVertex.y_ - yMinVertex.y_;
            uvRightDelta = yMidVertex.uv_ - yMinVertex.uv_;
        }
        else
        {
            xLeftDelta = yMidVertex.x_ - yMinVertex.x_;
            yLeftDelta = yMidVertex.y_ - yMinVertex.y_;
            uvLeftDelta = yMidVertex.uv_ - yMinVertex.uv_;
            xRightDelta = yMaxVertex.x_ - yMinVertex.x_;
            yRightDelta = yMaxVertex.y_ - yMinVertex.y_;
            uvRightDelta = yMaxVertex.uv_ - yMinVertex.uv_;
        }
        // Compute delta's w.r.t. x.
        dreal yBottomDelta = 0;
        dreal xBottomDelta = 0;
        Vector2 uvBottomDelta;
        dreal yTopDelta = 0;
        dreal xTopDelta = 0;
        Vector2 uvTopDelta;
        bool longBottomEdge =
            side(
            Vector2(xMidVertex.x_, xMidVertex.y_),
            Plane2(
            Vector2(xMinVertex.x_, xMinVertex.y_),
            cross(Vector2(xMaxVertex.x_, xMaxVertex.y_) -
            Vector2(xMinVertex.x_, xMinVertex.y_)))) < 0;
        if (longBottomEdge)
        {
            yBottomDelta = xMaxVertex.y_ - xMinVertex.y_;
            xBottomDelta = xMaxVertex.x_ - xMinVertex.x_;
            uvBottomDelta = xMaxVertex.uv_ - xMinVertex.uv_;
            yTopDelta = xMidVertex.y_ - xMinVertex.y_;
            xTopDelta = xMidVertex.x_ - xMinVertex.x_;
            uvTopDelta = xMidVertex.uv_ - xMinVertex.uv_;
        }
        else
        {
            yBottomDelta = xMidVertex.y_ - xMinVertex.y_;
            xBottomDelta = xMidVertex.x_ - xMinVertex.x_;
            uvBottomDelta = xMidVertex.uv_ - xMinVertex.uv_;
            yTopDelta = xMaxVertex.y_ - xMinVertex.y_;
            xTopDelta = xMaxVertex.x_ - xMinVertex.x_;
            uvTopDelta = xMaxVertex.uv_ - xMinVertex.uv_;
        }
        // Compute boundary derivatives w.r.t. y.
        dreal dxLeftDy = xLeftDelta;
        Vector2 dUvLeftDy = uvLeftDelta;
        if (yLeftDelta != 0)
        {
            dreal yLeftDeltaInv = inverse(yLeftDelta);
            dxLeftDy *= yLeftDeltaInv;
            dUvLeftDy *= yLeftDeltaInv;
        }
        dreal dxRightDy = xRightDelta;
        Vector2 dUvRightDy = uvRightDelta;
        if (yRightDelta != 0)
        {
            dreal yRightDeltaInv = inverse(yRightDelta);
            dxRightDy *= yRightDeltaInv;
            dUvRightDy *= yRightDeltaInv;
        }
        // Compute boundary derivatives w.r.t. x.
        dreal dyBottomDx = yBottomDelta;
        Vector2 dUvBottomDx = uvBottomDelta;
        if (xBottomDelta != 0)
        {
            dreal xBottomDeltaInv = inverse(xBottomDelta);
            dyBottomDx *= xBottomDeltaInv;
            dUvBottomDx *= xBottomDeltaInv;
        }
        dreal dyTopDx = yTopDelta;
        Vector2 dUvTopDx = uvTopDelta;
        if (xTopDelta != 0)
        {
            dreal xTopDeltaInv = inverse(xTopDelta);
            dyTopDx *= xTopDeltaInv;
            dUvTopDx *= xTopDeltaInv;
        }
        // Offset the start scanline to pixel rows.
        dreal yOffset = ((dreal)yMin + 0.5) - yMinVertex.y_;
        dreal xLeft = yMinVertex.x_ + dxLeftDy * yOffset;
        Vector2 uvLeft = yMinVertex.uv_ + dUvLeftDy * yOffset;
        dreal xRight = yMinVertex.x_ + dxRightDy * yOffset;
        Vector2 uvRight = yMinVertex.uv_ + dUvRightDy * yOffset;
        // Compute data derivatives w.r.t. x.
        Vector2 dUvDx;
        if (longLeftEdge)
        {
            dreal yDelta = yMidVertex.y_ - yMinVertex.y_;
            dreal dx =
                yMidVertex.x_ -
                (yMinVertex.x_ + dxLeftDy * yDelta);
            Vector2 dUv =
                yMidVertex.uv_ -
                (yMinVertex.uv_ + dUvLeftDy * yDelta);
            dreal invDx = inverse(dx);
            dUvDx = dUv * invDx;
        }
        else
        {
            dreal yDelta = yMidVertex.y_ - yMinVertex.y_;
            dreal dx =
                (yMinVertex.x_ + dxRightDy * yDelta) -
                yMidVertex.x_;
            Vector2 dUv =
                (yMinVertex.uv_ + dUvRightDy * yDelta) -
                yMidVertex.uv_;
            dreal invDx = inverse(dx);
            dUvDx = dUv * invDx;
        }
        // Compute data derivatives w.r.t. y.
        Vector2 dUvDy;
        if (longBottomEdge)
        {
            dreal xDelta = xMidVertex.x_ - xMinVertex.x_;
            dreal dy =
                xMidVertex.y_ -
                (xMinVertex.y_ + dyBottomDx * xDelta);
            Vector2 dUv =
                xMidVertex.uv_ -
                (xMinVertex.uv_ + dUvBottomDx * xDelta);
            dreal invDy = inverse(dy);
            dUvDy = dUv * invDy;
        }
        else
        {
            dreal xDelta = xMidVertex.x_ - xMinVertex.x_;
            dreal dy =
                (xMinVertex.y_ + dyTopDx * xDelta) -
                xMidVertex.y_;
            Vector2 dUv =
                (xMinVertex.uv_ + dUvTopDx * xDelta) -
                xMidVertex.uv_;
            dreal invDy = inverse(dy);
            dUvDy = dUv * invDy;
        }
        // Scan convert the bottom part.
        for (integer y = yMin;y < yMid;++y)
        {
            integer xBegin = std::max(toPixelSpanPoint(xLeft), (integer)0);
            integer xEnd = std::min(toPixelSpanPoint(xRight), width);
            if (xEnd - xBegin > 0)
            {
                dreal xOffset = ((dreal)xBegin + 0.5) - xLeft;
                Vector2 uv = uvLeft + dUvDx * xOffset;
                for (integer x = xBegin;x < xEnd;++x)
                {
                    image(x, y) =
                        colorMixer(image(x, y),
                        textureSampler(uv, matrix2x2<dreal>(dUvDx, dUvDy)));
                    uv += dUvDx;
                }
            }
            xLeft += dxLeftDy;
            uvLeft += dUvLeftDy;
            xRight += dxRightDy;
            uvRight += dUvRightDy;
        }
        // Prepare for scan converting the upper part.
        // Only one of the side changes direction.
        if (longLeftEdge)
        {
            yRightDelta = yMaxVertex.y_ - yMidVertex.y_;
            xRightDelta = yMaxVertex.x_ - yMidVertex.x_;
            uvRightDelta = yMaxVertex.uv_ - yMidVertex.uv_;
            dxRightDy = xRightDelta;
            dUvRightDy = uvRightDelta;
            if (yRightDelta != 0)
            {
                dreal yRightDeltaInv = inverse(yRightDelta);
                dxRightDy *= yRightDeltaInv;
                dUvRightDy *= yRightDeltaInv;
            }
            // Offset the scanline to pixel rows.
            dreal yOffset = ((dreal)yMid + 0.5) - yMidVertex.y_;
            xRight = yMidVertex.x_ + dxRightDy * yOffset;
            uvRight = yMidVertex.uv_ + dUvRightDy * yOffset;
        }
        else
        {
            yLeftDelta = yMaxVertex.y_ - yMidVertex.y_;
            xLeftDelta = yMaxVertex.x_ - yMidVertex.x_;
            uvLeftDelta = yMaxVertex.uv_ - yMidVertex.uv_;
            dxLeftDy = xLeftDelta;
            dUvLeftDy = uvLeftDelta;
            if (yLeftDelta != 0)
            {
                dreal yLeftDeltaInv = inverse(yLeftDelta);
                dxLeftDy *= yLeftDeltaInv;
                dUvLeftDy *= yLeftDeltaInv;
            }
            // Offset the scanline to pixel rows.
            dreal yOffset = ((dreal)yMid + 0.5) - yMidVertex.y_;
            xLeft = yMidVertex.x_ + dxLeftDy * yOffset;
            uvLeft = yMidVertex.uv_ + dUvLeftDy * yOffset;
        }
        // Scan convert the top part.
        for (integer y = yMid;y < yMax;++y)
        {
            integer xBegin = std::max(toPixelSpanPoint(xLeft), (integer)0);
            integer xEnd = std::min(toPixelSpanPoint(xRight), width);
            if (xEnd - xBegin > 0)
            {
                dreal xOffset = ((dreal)xBegin + 0.5) - xLeft;
                Vector2 uv = uvLeft + dUvDx * xOffset;
                for (integer x = xBegin;x < xEnd;++x)
                {
                    image(x, y) =
                        colorMixer(image(x, y),
                        textureSampler(uv, matrix2x2<dreal>(dUvDx, dUvDy)));
                    uv += dUvDx;
                }
            }
            xLeft += dxLeftDy;
            uvLeft += dUvLeftDy;
            xRight += dxRightDy;
            uvRight += dUvRightDy;
        }
    }
    template <typename Type,
        typename Image_View>
    void drawTriangle(
        const Triangle2& triangle,
        const Triangle2& uvTriangle,
        const Texture<Type>& textureSampler,
        const View<2, Type, Image_View>& image)
    {
        drawTriangle(triangle, uvTriangle,
            textureSampler, image, assignColorMixer<Type>());
    }
}
#endif