using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using System.Windows.Media.Media3D;

namespace Wireframe
{
    public static class MeshExtensions
    {
        // Return a MeshGeometry3D representing this mesh's wireframe.
        public static MeshGeometry3D ToWireframe(this MeshGeometry3D sourceMesh, double thickness)
        {
            // Make a mesh to hold the wireframe.
            MeshGeometry3D wireframe = new MeshGeometry3D();

            // Make a dictionary for segment sharing.
            Dictionary<int, int> segmentDict = new Dictionary<int, int>();

            // Loop through the mesh's triangles.
            for (int triangle = 0; triangle < sourceMesh.TriangleIndices.Count; triangle += 3)
            {
                // Get the source triangle's point indices.
                int index1 = sourceMesh.TriangleIndices[triangle];
                int index2 = sourceMesh.TriangleIndices[triangle + 1];
                int index3 = sourceMesh.TriangleIndices[triangle + 2];

                // Get the source triangle's corner points.
                Point3D point1 = sourceMesh.Positions[index1];
                Point3D point2 = sourceMesh.Positions[index2];
                Point3D point3 = sourceMesh.Positions[index3];

                // Make the triangle's three segments.
                wireframe.AddUniqueSegment(segmentDict,
                    index1, index2, point1, point2, thickness, true);
                wireframe.AddUniqueSegment(segmentDict,
                    index2, index3, point2, point3, thickness, true);
                wireframe.AddUniqueSegment(segmentDict,
                    index3, index1, point3, point1, thickness, true);
            }

            return wireframe;
        }

        // Add a segment if it is not already in the dictionary.
        public static void AddUniqueSegment(this MeshGeometry3D mesh,
            Dictionary<int, int> segmentDict,
            int index1, int index2, Point3D point1, Point3D point2,
            double thickness, bool extend = false)
        {
            // Get a unique ID for a segment connecting the two points.
            if (index1 > index2)
            {
                int temp = index1;
                index1 = index2;
                index2 = temp;
            }
            int segment_id = index1 * 10000 + index2;

            // If we've already added this segment, do nothing.
            if (segmentDict.ContainsKey(segment_id)) return;
            segmentDict.Add(segment_id, segment_id);

            // Create the segment.
            AddSegment(mesh, thickness, point1, point2, extend);
        }

        // Add a triangle to the indicated mesh.
        // If pointDictionary is not null, reuse points.
        public static void AddTriangle(this MeshGeometry3D mesh,
            Point3D point1, Point3D point2, Point3D point3,
            Dictionary<Point3D, int> pointDictionary = null)
        {
            // Get the points' indices.
            int index1 = mesh.AddPoint(point1, pointDictionary);
            int index2 = mesh.AddPoint(point2, pointDictionary);
            int index3 = mesh.AddPoint(point3, pointDictionary);

            // Create the triangle.
            mesh.TriangleIndices.Add(index1);
            mesh.TriangleIndices.Add(index2);
            mesh.TriangleIndices.Add(index3);
        }

        // Add the point. If pointDictionary is not null, reuse points.
        public static int AddPoint(this MeshGeometry3D mesh, Point3D point,
            Dictionary<Point3D, int> pointDictionary = null)
        {
            if (pointDictionary != null)
            {
                // If the point is in the dictionary, reuse it.
                if (pointDictionary.ContainsKey(point))
                    return pointDictionary[point];

                // It doesn't exit. Save its new index.
                pointDictionary.Add(point, mesh.Positions.Count);
            }

            // Create the point.
            mesh.Positions.Add(point);
            return mesh.Positions.Count - 1;
        }

        // Make a segment between two points.
        public static void AddSegment(this MeshGeometry3D mesh, double thickness,
            Point3D point1, Point3D point2, bool extend = false)
        {
            // Get the segment's vector.
            Vector3D v = point2 - point1;

            // Extend the vector if desired.
            if (extend)
            {
                // Increase the segment's length on both ends by thickness / 2.
                Vector3D n = v.Scale(thickness / 2.0);
                point1 -= n;
                point2 += n;
                v = point2 - point1;
            }

            // Get an up vector.
            Vector3D up = new Vector3D(0, 1, 0);    // Y axis.

            // See if up and v are more or less parallel.
            Vector3D vn = v;
            vn.Normalize();
            if (Vector3D.CrossProduct(up, vn).Length < 0.1)
            {
                // Use the X axis instead.
                up = new Vector3D(1, 0, 0);
            }

            // Get the scaled up vector.
            Vector3D n1 = up.Scale(thickness);

            // Get another scaled perpendicular vector.
            Vector3D n2 = Vector3D.CrossProduct(v, n1);
            n2 = n2.Scale(thickness);

            // Make a skinny box.
            mesh.AddPrism(point1 - n1 / 2.0 - n2 / 2.0, n1, v, n2);
        }

        // Set the vector's length.
        public static Vector3D Scale(this Vector3D vector, double length)
        {
            return vector / vector.Length * length;
        }

        // Make a prism.
        public static void AddPrism(this MeshGeometry3D mesh,
            Point3D corner, Vector3D v1, Vector3D v2, Vector3D v3)
        {
            // Make the prism.
            Point3D A = corner;
            Point3D B = A + v1;
            Point3D C = B + v3;
            Point3D D = C - v1;
            Point3D E = A + v2;
            Point3D F = B + v2;
            Point3D G = C + v2;
            Point3D H = D + v2;

            mesh.AddRectangle(D, v1, v2);      // Front.
            mesh.AddRectangle(A, v3, v2);      // Left.
            mesh.AddRectangle(C, -v3, v2);     // Right.
            mesh.AddRectangle(B, -v1, v2);     // Back.
            mesh.AddRectangle(G, -v3, -v1);    // Top.
            mesh.AddRectangle(A, v1, v3);      // Bottom.
        }

        // Add a rectangle to the mesh.
        public static void AddRectangle(this MeshGeometry3D mesh,
            Point3D llCorner, Vector3D right, Vector3D up,
            Dictionary<Point3D, int> pointDictionary = null)
        {
            // Get the points' indices.
            int index1 = mesh.AddPoint(llCorner, pointDictionary);              // A = lower left corner.
            int index2 = mesh.AddPoint(llCorner + right, pointDictionary);      // B = lower right corner.
            int index3 = mesh.AddPoint(llCorner + right + up, pointDictionary); // C = upper right corner.
            int index4 = mesh.AddPoint(llCorner + up, pointDictionary);         // D = upper left corner.

            // Define triangle ABC.
            mesh.TriangleIndices.Add(index1);
            mesh.TriangleIndices.Add(index2);
            mesh.TriangleIndices.Add(index3);

            // Define triangle ACD.
            mesh.TriangleIndices.Add(index1);
            mesh.TriangleIndices.Add(index3);
            mesh.TriangleIndices.Add(index4);
        }
    }
}
