//-----------------------------------------------------------------------------
// Backend-agnostic rendering interface, and various backends we use.
//
// Copyright 2016 whitequark
//-----------------------------------------------------------------------------
#include "solvespace.h"

namespace SolveSpace {

//-----------------------------------------------------------------------------
// Camera transformations.
//-----------------------------------------------------------------------------

Point2d Camera::ProjectPoint(Vector p) const {
    Vector p3 = ProjectPoint3(p);
    Point2d p2 = { p3.x, p3.y };
    return p2;
}

Vector Camera::ProjectPoint3(Vector p) const {
    double w;
    Vector r = ProjectPoint4(p, &w);
    return r.ScaledBy(scale/w);
}

Vector Camera::ProjectPoint4(Vector p, double *w) const {
    p = p.Plus(offset);

    Vector r;
    r.x = p.Dot(projRight);
    r.y = p.Dot(projUp);
    r.z = p.Dot(projUp.Cross(projRight));

    *w = 1 + r.z*tangent*scale;
    return r;
}

Vector Camera::UnProjectPoint(Point2d p) const {
    Vector orig = offset.ScaledBy(-1);

    // Note that we're ignoring the effects of perspective. Since our returned
    // point has the same component normal to the screen as the offset, it
    // will have z = 0 after the rotation is applied, thus w = 1. So this is
    // correct.
    orig = orig.Plus(projRight.ScaledBy(p.x / scale)).Plus(
                     projUp.   ScaledBy(p.y / scale));
    return orig;
}

Vector Camera::UnProjectPoint3(Vector p) const {
    p.z = p.z / (scale - p.z * tangent * scale);
    double w = 1 + p.z * tangent * scale;
    p.x *= w / scale;
    p.y *= w / scale;

    Vector orig = offset.ScaledBy(-1);
    orig = orig.Plus(projRight.ScaledBy(p.x)).Plus(
                     projUp.   ScaledBy(p.y).Plus(
                     projRight.Cross(projUp). ScaledBy(p.z)));
    return orig;
}

Vector Camera::VectorFromProjs(Vector rightUpForward) const {
    Vector n = projRight.Cross(projUp);

    Vector r = (projRight.ScaledBy(rightUpForward.x));
    r =  r.Plus(projUp.ScaledBy(rightUpForward.y));
    r =  r.Plus(n.ScaledBy(rightUpForward.z));
    return r;
}

Vector Camera::AlignToPixelGrid(Vector v) const {
    if(!hasPixels) return v;

    v = ProjectPoint3(v);
    v.x = floor(v.x) + 0.5;
    v.y = floor(v.y) + 0.5;
    return UnProjectPoint3(v);
}

void Camera::LoadIdentity() {
    offset    = { 0.0, 0.0, 0.0 };
    projRight = { 1.0, 0.0, 0.0 };
    projUp    = { 0.0, 1.0, 0.0 };
    scale     = 1.0;
    tangent   = 0.0;
}

void Camera::NormalizeProjectionVectors() {
    if(projRight.Magnitude() < LENGTH_EPS) {
        projRight = Vector::From(1, 0, 0);
    }

    Vector norm = projRight.Cross(projUp);
    // If projRight and projUp somehow ended up parallel, then pick an
    // arbitrary projUp normal to projRight.
    if(norm.Magnitude() < LENGTH_EPS) {
        norm = projRight.Normal(0);
    }
    projUp = norm.Cross(projRight);

    projUp = projUp.WithMagnitude(1);
    projRight = projRight.WithMagnitude(1);
}

//-----------------------------------------------------------------------------
// Stroke and fill caching.
//-----------------------------------------------------------------------------

bool Canvas::Stroke::Equals(const Stroke &other) const {
    return (layer  == other.layer &&
            zIndex == other.zIndex &&
            color.Equals(other.color) &&
            width == other.width &&
            stipplePattern == other.stipplePattern &&
            stippleScale == other.stippleScale);
}

bool Canvas::Fill::Equals(const Fill &other) const {
    return (layer  == other.layer &&
            zIndex == other.zIndex &&
            color.Equals(other.color) &&
            pattern == other.pattern);
}

Canvas::hStroke Canvas::GetStroke(const Stroke &stroke) {
    for(const Stroke &s : strokes) {
        if(s.Equals(stroke)) return s.h;
    }
    Stroke strokeCopy = stroke;
    return strokes.AddAndAssignId(&strokeCopy);
}

Canvas::hFill Canvas::GetFill(const Fill &fill) {
    for(const Fill &f : fills) {
        if(f.Equals(fill)) return f.h;
    }
    Fill fillCopy = fill;
    return fills.AddAndAssignId(&fillCopy);
}

//-----------------------------------------------------------------------------
// A wrapper around Canvas that simplifies drawing UI in screen coordinates
//-----------------------------------------------------------------------------

void UiCanvas::DrawLine(int x1, int y1, int x2, int y2, RgbaColor color, int width) {
    Vector va = { (double)x1 + 0.5, (double)Flip(y1) + 0.5, 0.0 },
           vb = { (double)x2 + 0.5, (double)Flip(y2) + 0.5, 0.0 };

    Canvas::Stroke stroke = {};
    stroke.layer  = Canvas::Layer::FRONT;
    stroke.width  = (double)width;
    stroke.color  = color;
    Canvas::hStroke hcs = canvas->GetStroke(stroke);

    canvas->DrawLine(va, vb, hcs);
}

void UiCanvas::DrawRect(int l, int r, int t, int b,
                        RgbaColor fillColor, RgbaColor outlineColor) {
    Vector va = { (double)l + 0.5, (double)Flip(b) + 0.5, 0.0 },
           vb = { (double)l + 0.5, (double)Flip(t) + 0.5, 0.0 },
           vc = { (double)r + 0.5, (double)Flip(t) + 0.5, 0.0 },
           vd = { (double)r + 0.5, (double)Flip(b) + 0.5, 0.0 };

    if(!fillColor.IsEmpty()) {
        Canvas::Fill fill = {};
        fill.layer  = Canvas::Layer::FRONT;
        fill.color  = fillColor;
        Canvas::hFill hcf = canvas->GetFill(fill);

        canvas->DrawQuad(va, vb, vc, vd, hcf);
    }

    if(!outlineColor.IsEmpty()) {
        Canvas::Stroke stroke = {};
        stroke.layer  = Canvas::Layer::FRONT;
        stroke.width  = 1.0;
        stroke.color  = outlineColor;
        Canvas::hStroke hcs = canvas->GetStroke(stroke);

        canvas->DrawLine(va, vb, hcs);
        canvas->DrawLine(vb, vc, hcs);
        canvas->DrawLine(vc, vd, hcs);
        canvas->DrawLine(vd, va, hcs);
    }
}

void UiCanvas::DrawPixmap(std::shared_ptr<const Pixmap> pm, int x, int y) {
    Canvas::Fill fill = {};
    fill.layer  = Canvas::Layer::FRONT;
    fill.color  = { 0, 0, 0, 255 };
    Canvas::hFill hcf = canvas->GetFill(fill);

    canvas->DrawPixmap(pm,
                       { (double)x, (double)(flip ? Flip(y) - pm->height : y), 0.0 },
                       { (double)pm->width,  0.0, 0.0 },
                       { 0.0, (double)pm->height, 0.0 },
                       { 0.0, 1.0 },
                       { 1.0, 0.0 },
                       hcf);
}

void UiCanvas::DrawBitmapChar(char32_t codepoint, int x, int y, RgbaColor color) {
    BitmapFont *font = BitmapFont::Builtin();

    Canvas::Fill fill = {};
    fill.layer  = Canvas::Layer::FRONT;
    fill.color  = color;
    Canvas::hFill hcf = canvas->GetFill(fill);

    if(codepoint >= 0xe000 && codepoint <= 0xefff) {
        // Special character, like a checkbox or a radio button
        x -= 3;
    }

    double s0, t0, s1, t1;
    size_t w, h;
    if(font->LocateGlyph(codepoint, &s0, &t0, &s1, &t1, &w, &h)) {
        // LocateGlyph modified the texture, reload it.
        canvas->InvalidatePixmap(font->texture);
    }

    canvas->DrawPixmap(font->texture,
                       { (double)x, (double)Flip(y), 0.0 },
                       { (double)w,  0.0, 0.0 },
                       { 0.0, (double) h, 0.0 },
                       { s0, t1 },
                       { s1, t0 },
                       hcf);
}

void UiCanvas::DrawBitmapText(const std::string &str, int x, int y, RgbaColor color) {
    BitmapFont *font = BitmapFont::Builtin();

    for(char32_t codepoint : ReadUTF8(str)) {
        DrawBitmapChar(codepoint, x, y, color);
        x += font->GetWidth(codepoint) * 8;
    }
}

//-----------------------------------------------------------------------------
// A canvas that performs picking against drawn geometry.
//-----------------------------------------------------------------------------

void ObjectPicker::DoCompare(double distance, int zIndex, int comparePosition) {
    if(distance > selRadius) return;
    if((zIndex == maxZIndex && distance < minDistance) || (zIndex > maxZIndex)) {
        minDistance = distance;
        maxZIndex   = zIndex;
        position    = comparePosition;
    }
}

void ObjectPicker::DoQuad(const Vector &a, const Vector &b, const Vector &c, const Vector &d,
                          int zIndex, int comparePosition) {
    Point2d corners[4] = {
        camera.ProjectPoint(a),
        camera.ProjectPoint(b),
        camera.ProjectPoint(c),
        camera.ProjectPoint(d)
    };
    double minNegative = VERY_NEGATIVE,
           maxPositive = VERY_POSITIVE;
    for(int i = 0; i < 4; i++) {
        Point2d ap = corners[i],
                bp = corners[(i + 1) % 4];
        double distance = point.DistanceToLineSigned(ap, bp.Minus(ap), /*asSegment=*/true);
        if(distance < 0) minNegative = std::max(minNegative, distance);
        if(distance > 0) maxPositive = std::min(maxPositive, distance);
    }

    bool insideQuad = (minNegative == VERY_NEGATIVE || maxPositive == VERY_POSITIVE);
    if(insideQuad) {
        DoCompare(0.0, zIndex, comparePosition);
    } else {
        double distance = std::min(fabs(minNegative), fabs(maxPositive));
        DoCompare(distance, zIndex, comparePosition);
    }
}

void ObjectPicker::DrawLine(const Vector &a, const Vector &b, hStroke hcs) {
    Stroke *stroke = strokes.FindById(hcs);
    Point2d ap = camera.ProjectPoint(a);
    Point2d bp = camera.ProjectPoint(b);
    double distance = point.DistanceToLine(ap, bp.Minus(ap), /*asSegment=*/true);
    DoCompare(distance - stroke->width / 2.0, stroke->zIndex);
}

void ObjectPicker::DrawEdges(const SEdgeList &el, hStroke hcs) {
    Stroke *stroke = strokes.FindById(hcs);
    for(const SEdge &e : el.l) {
        Point2d ap = camera.ProjectPoint(e.a);
        Point2d bp = camera.ProjectPoint(e.b);
        double distance = point.DistanceToLine(ap, bp.Minus(ap), /*asSegment=*/true);
        DoCompare(distance - stroke->width / 2.0, stroke->zIndex, e.auxB);
    }
}

void ObjectPicker::DrawOutlines(const SOutlineList &ol, hStroke hcs, DrawOutlinesAs drawAs) {
    ssassert(false, "Not implemented");
}

void ObjectPicker::DrawVectorText(const std::string &text, double height,
                                  const Vector &o, const Vector &u, const Vector &v,
                                  hStroke hcs) {
    Stroke *stroke = strokes.FindById(hcs);
    double w = VectorFont::Builtin()-> GetWidth(height, text),
           h = VectorFont::Builtin()->GetHeight(height);
    DoQuad(o,
           o.Plus(v.ScaledBy(h)),
           o.Plus(u.ScaledBy(w)).Plus(v.ScaledBy(h)),
           o.Plus(u.ScaledBy(w)),
           stroke->zIndex);
}

void ObjectPicker::DrawQuad(const Vector &a, const Vector &b, const Vector &c, const Vector &d,
                             hFill hcf) {
    Fill *fill = fills.FindById(hcf);
    DoQuad(a, b, c, d, fill->zIndex);
}

void ObjectPicker::DrawPoint(const Vector &o, double s, hFill hcf) {
    Fill *fill = fills.FindById(hcf);
    double distance = point.DistanceTo(camera.ProjectPoint(o)) - s / 2;
    DoCompare(distance, fill->zIndex);
}

void ObjectPicker::DrawPolygon(const SPolygon &p, hFill hcf) {
    ssassert(false, "Not implemented");
}

void ObjectPicker::DrawMesh(const SMesh &m, hFill hcfFront, hFill hcfBack, hStroke hcsTriangles) {
    ssassert(false, "Not implemented");
}

void ObjectPicker::DrawFaces(const SMesh &m, const std::vector<uint32_t> &faces, hFill hcf) {
    ssassert(false, "Not implemented");
}

void ObjectPicker::DrawPixmap(std::shared_ptr<const Pixmap> pm,
                              const Vector &o, const Vector &u, const Vector &v,
                              const Point2d &ta, const Point2d &tb, Canvas::hFill hcf) {
    ssassert(false, "Not implemented");
}

bool ObjectPicker::Pick(std::function<void()> drawFn) {
    minDistance = VERY_POSITIVE;
    drawFn();
    return minDistance < selRadius;
}

}
