// This class provides methods to draw spheres.  The shape is represented
// as a "geodesic" mesh of triangles generated by subviding an icosahedron
// until an edge length criteria is met.  Supports wireframe and unshaded
// triangle drawing styles.  Provides front/back/both culling of faces.
//
// see drawSphere below
//
class DrawSphereHelper
{
public:
    Vec3 center;
    float radius;
    float maxEdgeLength;
    bool filled;
    Vec3 color;
    bool drawFrontFacing;
    bool drawBackFacing;
    Vec3 viewpoint;

    // default constructor (at origin, radius=1, white, wireframe, nocull)
    DrawSphereHelper ()
        : center (Vec3::zero),
          radius (1.0f),
          maxEdgeLength (1.0f),
          filled (false),
          color (gWhite),
          drawFrontFacing (true),
          drawBackFacing (true),
          viewpoint (Vec3::zero)
    {}

    // "kitchen sink" constructor (allows specifying everything)
    DrawSphereHelper (const Vec3 _center,
                      const float _radius,
                      const float _maxEdgeLength,
                      const bool _filled,
                      const Vec3& _color,
                      const bool _drawFrontFacing,
                      const bool _drawBackFacing,
                      const Vec3& _viewpoint)
        : center (_center),
          radius (_radius),
          maxEdgeLength (_maxEdgeLength),
          filled (_filled),
          color (_color),
          drawFrontFacing (_drawFrontFacing),
          drawBackFacing (_drawBackFacing),
          viewpoint (_viewpoint)
    {}

    // draw as an icosahedral geodesic sphere
    void draw (void) const
    {
        // Geometry based on Paul Bourke's excellent article:
        //   Platonic Solids (Regular polytopes in 3D)
        //   http://astronomy.swin.edu.au/~pbourke/polyhedra/platonic/
        const float sqrt5 = sqrt (5.0f);
        const float phi = (1.0f + sqrt5) * 0.5f; // "golden ratio"
        // ratio of edge length to radius
        const float ratio = sqrt (10.0f + (2.0f * sqrt5)) / (4.0f * phi);
        const float a = (radius / ratio) * 0.5;
        const float b = (radius / ratio) / (2.0f * phi);

        // define the icosahedron's 12 vertices:
        const Vec3 v1  = center + Vec3 ( 0,  b, -a);
        const Vec3 v2  = center + Vec3 ( b,  a,  0);
        const Vec3 v3  = center + Vec3 (-b,  a,  0);
        const Vec3 v4  = center + Vec3 ( 0,  b,  a);
        const Vec3 v5  = center + Vec3 ( 0, -b,  a);
        const Vec3 v6  = center + Vec3 (-a,  0,  b);
        const Vec3 v7  = center + Vec3 ( 0, -b, -a);
        const Vec3 v8  = center + Vec3 ( a,  0, -b);
        const Vec3 v9  = center + Vec3 ( a,  0,  b);
        const Vec3 v10 = center + Vec3 (-a,  0, -b);
        const Vec3 v11 = center + Vec3 ( b, -a,  0);
        const Vec3 v12 = center + Vec3 (-b, -a,  0);

        // draw the icosahedron's 20 triangular faces:
        drawMeshedTriangleOnSphere (v1, v2, v3);
        drawMeshedTriangleOnSphere (v4, v3, v2);
        drawMeshedTriangleOnSphere (v4, v5, v6);
        drawMeshedTriangleOnSphere (v4, v9, v5);
        drawMeshedTriangleOnSphere (v1, v7, v8);
        drawMeshedTriangleOnSphere (v1, v10, v7);
        drawMeshedTriangleOnSphere (v5, v11, v12);
        drawMeshedTriangleOnSphere (v7, v12, v11);
        drawMeshedTriangleOnSphere (v3, v6, v10);
        drawMeshedTriangleOnSphere (v12, v10, v6);
        drawMeshedTriangleOnSphere (v2, v8, v9);
        drawMeshedTriangleOnSphere (v11, v9, v8);
        drawMeshedTriangleOnSphere (v4, v6, v3);
        drawMeshedTriangleOnSphere (v4, v2, v9);
        drawMeshedTriangleOnSphere (v1, v3, v10);
        drawMeshedTriangleOnSphere (v1, v8, v2);
        drawMeshedTriangleOnSphere (v7, v10, v12);
        drawMeshedTriangleOnSphere (v7, v11, v8);
        drawMeshedTriangleOnSphere (v5, v12, v6);
        drawMeshedTriangleOnSphere (v5, v9, v11);
    }

    // given two points, take midpoint and project onto this sphere
    inline Vec3 midpointOnSphere (const Vec3& a, const Vec3& b) const
    {
        const Vec3 midpoint = (a + b) * 0.5f;
        const Vec3 unitRadial = (midpoint - center).normalize ();
        return center + (unitRadial * radius);
    }

    // given three points on the surface of this sphere, if the triangle
    // is "small enough" draw it, otherwise subdivide it into four smaller
    // triangles and recursively draw each of them.
    void drawMeshedTriangleOnSphere (const Vec3& a,
                                     const Vec3& b,
                                     const Vec3& c) const
    {
        // if all edges are short enough
        if ((((a - b).length ()) < maxEdgeLength) &&
            (((b - c).length ()) < maxEdgeLength) &&
            (((c - a).length ()) < maxEdgeLength))
        {
            // draw triangle
            drawTriangleOnSphere (a, b, c);
        }
        else // otherwise subdivide and recurse
        {
            // find edge midpoints
            const Vec3 ab = midpointOnSphere (a, b);
            const Vec3 bc = midpointOnSphere (b, c);
            const Vec3 ca = midpointOnSphere (c, a);

            // recurse on four sub-triangles
            drawMeshedTriangleOnSphere ( a, ab, ca);
            drawMeshedTriangleOnSphere (ab,  b, bc);
            drawMeshedTriangleOnSphere (ca, bc,  c);
            drawMeshedTriangleOnSphere (ab, bc, ca);
        }
    }

    // draw one mesh element for drawMeshedTriangleOnSphere
    void drawTriangleOnSphere (const Vec3& a,
                               const Vec3& b,
                               const Vec3& c) const
    {
        // draw triangle, subject to the camera orientation criteria
        // (according to drawBackFacing and drawFrontFacing)
        const Vec3 triCenter = (a + b + c) / 3.0f;
        const Vec3 triNormal = triCenter - center; // not unit length
        const Vec3 view = triCenter - viewpoint;
        const float dot = view.dot (triNormal); // project normal on view
        const bool seen = ((dot>0.0f) ? drawBackFacing : drawFrontFacing);
        if (seen)
        {
            if (filled)
            {
                // draw filled triangle
                if (drawFrontFacing)
                    drawTriangle (c, b, a, color);
                else
                    drawTriangle (a, b, c, color);
            }
            else
            {
                // draw triangle edges (use trick to avoid drawing each
                // edge twice (for each adjacent triangle) unless we are
                // culling and this tri is near the sphere's silhouette)
                const float unitDot = view.dot (triNormal.normalize ());
                const float t = 0.05f; // near threshold
                const bool nearSilhouette = (unitDot<t) || (unitDot>-t);
                if (nearSilhouette && !(drawBackFacing&&drawFrontFacing))
                {
                    drawLine (a, b, color);
                    drawLine (b, c, color);
                    drawLine (c, a, color);
                }
                else
                {
                    drawMeshedTriangleLine (a, b, color);
                    drawMeshedTriangleLine (b, c, color);
                    drawMeshedTriangleLine (c, a, color);
                }
            }
        }
    }

    // Draws line from A to B but not from B to A: assumes each edge
    // will be drawn in both directions, picks just one direction for
    // drawing according to an arbitary but reproducable criterion.
    void drawMeshedTriangleLine (const Vec3& a,
                                 const Vec3& b,
                                 const Vec3& color) const
    {
        if (a.x != b.x)
        {
            if (a.x > b.x) drawLine (a, b, color);
        }
        else
        {
            if (a.y != b.y)
            {
                if (a.y > b.y) drawLine (a, b, color);
            }
            else
            {
                if (a.z > b.z) drawLine (a, b, color);
            }
        }
    }

};

// utility to draw a sphere
void drawSphere (const Vec3 center,
                 const float radius,
                 const float maxEdgeLength,
                 const bool filled,
                 const Vec3& color,
                 const bool drawFrontFacing,
                 const bool drawBackFacing,
                 const Vec3& viewpoint)
{
    const DrawSphereHelper s (center, radius, maxEdgeLength, filled, color,
                              drawFrontFacing, drawBackFacing, viewpoint);
    s.draw ();
}


