using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

using System.Windows.Media.Media3D;

namespace LineSegments
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        // The main object model group.
        private Model3DGroup MainModel3Dgroup = new Model3DGroup();

        // The camera.
        private PerspectiveCamera TheCamera;

        // The camera's current location.
        private double CameraPhi = 0.62;
        private double CameraTheta = 0.95;
        private double CameraR = 4.0;

        // The change in CameraPhi when you press the up and down arrows.
        private const double CameraDPhi = 0.1;

        // The change in CameraTheta when you press the left and right arrows.
        private const double CameraDTheta = 0.1;

        // The change in CameraR when you press + or -.
        private const double CameraDR = 0.1;

        // Create the scene.
        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            // Give the camera its initial position.
            TheCamera = new PerspectiveCamera();
            TheCamera.FieldOfView = 60;
            MainViewport.Camera = TheCamera;
            PositionCamera();

            // Define lights.
            DefineLights();

            // Create the model.
            DefineModel(MainModel3Dgroup);

            // Add the group of models to a ModelVisual3D.
            ModelVisual3D modelVisual = new ModelVisual3D();
            modelVisual.Content = MainModel3Dgroup;

            // Display the main visual to the viewportt.
            MainViewport.Children.Add(modelVisual);
        }

        // Position the camera.
        private void PositionCamera()
        {
            // Calculate the camera's position in Cartesian coordinates.
            double y = CameraR * Math.Sin(CameraPhi);
            double hyp = CameraR * Math.Cos(CameraPhi);
            double x = hyp * Math.Cos(CameraTheta);
            double z = hyp * Math.Sin(CameraTheta);
            TheCamera.Position = new Point3D(x, y, z);

            // Look toward the origin.
            TheCamera.LookDirection = new Vector3D(-x, -y, -z);

            // Set the Up direction.
            TheCamera.UpDirection = new Vector3D(0, 1, 0);
        }

        // Define the lights.
        private void DefineLights()
        {
            AmbientLight ambientLight = new AmbientLight(Colors.Gray);
            DirectionalLight directionalLight1 =
                new DirectionalLight(Colors.DarkGray, new Vector3D(-1.0, -3.0, -2.0));
            DirectionalLight directionalLight2 =
                new DirectionalLight(Colors.DarkGray, new Vector3D(1.0, 2.0, 2.0));
            MainModel3Dgroup.Children.Add(ambientLight);
            MainModel3Dgroup.Children.Add(directionalLight1);
            MainModel3Dgroup.Children.Add(directionalLight2);
        }

        // Add the model to the Model3DGroup.
        private void DefineModel(Model3DGroup modelGroup)
        {
            MakeTetrahedrons(modelGroup);
            MakeCage(modelGroup);
        }

        // Make two interlocking tetrahedrons.
        private void MakeTetrahedrons(Model3DGroup modelGroup)
        {
            MeshGeometry3D mesh1 = new MeshGeometry3D();

            // Create the first tetrahedron.
            AddTriangle(mesh1,
                new Point3D(1, 1, 1),
                new Point3D(-1, -1, 1),
                new Point3D(1, -1, -1));
            AddTriangle(mesh1,
                new Point3D(1, 1, 1),
                new Point3D(-1, 1, -1),
                new Point3D(-1, -1, 1));
            AddTriangle(mesh1,
                new Point3D(1, 1, 1),
                new Point3D(1, -1, -1),
                new Point3D(-1, 1, -1));
            AddTriangle(mesh1,
                new Point3D(-1, -1, 1),
                new Point3D(-1, 1, -1),
                new Point3D(1, -1, -1));
            SolidColorBrush brush1 = Brushes.LightGreen;
            DiffuseMaterial material1 = new DiffuseMaterial(brush1);
            GeometryModel3D model1 = new GeometryModel3D(mesh1, material1);
            modelGroup.Children.Add(model1);

            // Create the second tetrahedron.
            MeshGeometry3D mesh2 = new MeshGeometry3D();
            AddTriangle(mesh2,
                new Point3D(-1, -1, -1),
                new Point3D(-1, 1, 1),
                new Point3D(1, 1, -1));
            AddTriangle(mesh2,
                new Point3D(-1, -1, -1),
                new Point3D(1, -1, 1),
                new Point3D(-1, 1, 1));
            AddTriangle(mesh2,
                new Point3D(-1, -1, -1),
                new Point3D(1, 1, -1),
                new Point3D(1, -1, 1));
            AddTriangle(mesh2,
                new Point3D(1, -1, 1),
                new Point3D(1, 1, -1),
                new Point3D(-1, 1, 1));
            SolidColorBrush brush2 = Brushes.LightBlue;
            DiffuseMaterial material2 = new DiffuseMaterial(brush2);
            GeometryModel3D model2 = new GeometryModel3D(mesh2, material2);
            modelGroup.Children.Add(model2);
        }

        // Make the cage.
        private void MakeCage(Model3DGroup modelGroup)
        {
            // Make a mesh to hold the surface.
            MeshGeometry3D mesh = new MeshGeometry3D();

            // Make the cage corners.
            Vector3D vx = new Vector3D(2, 0, 0);
            Vector3D vy = new Vector3D(0, 2, 0);
            Vector3D vz = new Vector3D(0, 0, 2);
            Point3D A = new Point3D(-1, -1, -1);
            Point3D B = A + vx;
            Point3D C = B + vz;
            Point3D D = C - vx;
            Point3D E = A + vy;
            Point3D F = B + vy;
            Point3D G = C + vy;
            Point3D H = D + vy;

            // Bottom.
            const double thickness = 0.05;
            AddSegment(mesh, thickness, A, B, true);
            AddSegment(mesh, thickness, B, C, true);
            AddSegment(mesh, thickness, C, D, true);
            AddSegment(mesh, thickness, D, A, true);

            // Top.
            AddSegment(mesh, thickness, E, F, true);
            AddSegment(mesh, thickness, F, G, true);
            AddSegment(mesh, thickness, G, H, true);
            AddSegment(mesh, thickness, H, E, true);

            // Sides.
            AddSegment(mesh, thickness, A, E, true);
            AddSegment(mesh, thickness, B, F, true);
            AddSegment(mesh, thickness, C, G, true);
            AddSegment(mesh, thickness, D, H, true);

            // Make the material.
            DiffuseMaterial surfaceMaterial = new DiffuseMaterial(Brushes.Pink);

            // Make the mesh's model.
            GeometryModel3D surfaceModel = new GeometryModel3D(mesh, surfaceMaterial);

            // Make the surface visible from both sides.
            surfaceModel.BackMaterial = surfaceMaterial;

            // Add the model to the model groups.
            modelGroup.Children.Add(surfaceModel);
        }

        // Make a segment between two points.
        private void AddSegment(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 = ScaleVector(v, 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 = ScaleVector(up, thickness);

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

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

        // Set the vector's length.
        private Vector3D ScaleVector(Vector3D vector, double length)
        {
            return vector / vector.Length * length;
        }

        // Make a prism.
        private void AddPrism(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;

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

        // Add a rectangle to the mesh.
        private void AddRectangle(MeshGeometry3D mesh,
            Point3D llCorner, Vector3D right, Vector3D up)
        {
            // Get the index of the first point.
            int i = mesh.Positions.Count;

            // Add the points.
            mesh.Positions.Add(llCorner);               // A = lower left corner.
            mesh.Positions.Add(llCorner + right);       // B = lower right corner.
            mesh.Positions.Add(llCorner + right + up);  // C = upper right corner.
            mesh.Positions.Add(llCorner + up);          // D = upper left corner.

            // Set texture coordinates.
            mesh.TextureCoordinates.Add(new Point(0, 1));   // A
            mesh.TextureCoordinates.Add(new Point(1, 1));   // B
            mesh.TextureCoordinates.Add(new Point(1, 0));   // C
            mesh.TextureCoordinates.Add(new Point(0, 0));   // D

            // Define triangle ABC.
            mesh.TriangleIndices.Add(i);
            mesh.TriangleIndices.Add(i + 1);
            mesh.TriangleIndices.Add(i + 2);

            // Define triangle ACD.
            mesh.TriangleIndices.Add(i);
            mesh.TriangleIndices.Add(i + 2);
            mesh.TriangleIndices.Add(i + 3);
        }

        // Add a triangle to the indicated mesh reusing existing points.
        private void AddTriangle(MeshGeometry3D mesh, Point3D point1, Point3D point2, Point3D point3)
        {
            // Get the points' indices.
            int index1 = AddPoint(mesh.Positions, point1);
            int index2 = AddPoint(mesh.Positions, point2);
            int index3 = AddPoint(mesh.Positions, point3);

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

        // Add the point without sharing.
        private int AddPoint(Point3DCollection points, Point3D point)
        {
            points.Add(point);
            return points.Count - 1;
        }

        // Adjust the camera's position.
        private void Window_KeyDown(object sender, KeyEventArgs e)
        {
            switch (e.Key)
            {
                case Key.Up:
                    CameraPhi += CameraDPhi;
                    if (CameraPhi > Math.PI / 2.0) CameraPhi = Math.PI / 2.0;
                    break;
                case Key.Down:
                    CameraPhi -= CameraDPhi;
                    if (CameraPhi < -Math.PI / 2.0) CameraPhi = -Math.PI / 2.0;
                    break;
                case Key.Left:
                    CameraTheta += CameraDTheta;
                    break;
                case Key.Right:
                    CameraTheta -= CameraDTheta;
                    break;
                case Key.Add:
                case Key.OemPlus:
                    CameraR -= CameraDR;
                    if (CameraR < CameraDR) CameraR = CameraDR;
                    break;
                case Key.Subtract:
                case Key.OemMinus:
                    CameraR += CameraDR;
                    break;
            }

            // Update the camera's position.
            PositionCamera();
        }
    }
}
