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 DataSurface
{
    /// <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 = Math.PI / 4.0;       // 45 degrees
        private double CameraTheta = Math.PI / 3.0;     // 60 degrees
        private double CameraR = 16.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 data.
            double[,] values = MakeData();

            // Create an altitude map.
            WriteableBitmap wbm = CreateAltitudeMap(values);
            //wbm.Save("Texture.png");

            // Create the model.
            DefineModel(MainModel3Dgroup, values, wbm);

            // 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 directionalLight =
                new DirectionalLight(Colors.Gray, new Vector3D(-1.0, -3.0, -2.0));
            MainModel3Dgroup.Children.Add(ambientLight);
            MainModel3Dgroup.Children.Add(directionalLight);
        }

        // Create the altitude map texture bitmap.
        private WriteableBitmap CreateAltitudeMap(double[,] values)
        {
            // Calculate the function's value over the area.
            int xwidth = values.GetUpperBound(0) + 1;
            int zwidth = values.GetUpperBound(1) + 1;

            // Get the upper and lower bounds on the values.
            var valueQuery =
                from double value in values
                select value;
            double ymin = valueQuery.Min();
            double ymax = valueQuery.Max();

            // Make the BitmapMaker.
            BitmapMaker bmMaker = new BitmapMaker(xwidth, zwidth);

            // Set the pixel colors.
            for (int ix = 0; ix < xwidth; ix++)
            {
                for (int iz = 0; iz < zwidth; iz++)
                {
                    byte red, green, blue;
                    MapRainbowColor(values[ix, iz], ymin, ymax,
                        out red, out green, out blue);
                    bmMaker.SetPixel(ix, iz, red, green, blue, 255);
                }
            }

            // Convert the BitmapMaker into a WriteableBitmap and return it.
            return bmMaker.MakeBitmap(96, 96);
        }

        // Map a value to a rainbow color.
        private void MapRainbowColor(double value, double minValue, double maxValue,
            out byte red, out byte green, out byte blue)
        {
            // Convert into a value between 0 and 1023.
            int intValue = (int)(1023 * (value - minValue) / (maxValue - minValue));

            // Map different color bands.
            if (intValue < 256)
            {
                // Red to yellow. (255, 0, 0) to (255, 255, 0).
                red = 255;
                green = (byte)intValue;
                blue = 0;
            }
            else if (intValue < 512)
            {
                // Yellow to green. (255, 255, 0) to (0, 255, 0).
                intValue -= 256;
                red = (byte)(255 - intValue);
                green = 255;
                blue = 0;
            }
            else if (intValue < 768)
            {
                // Green to aqua. (0, 255, 0) to (0, 255, 255).
                intValue -= 512;
                red = 0;
                green = 255;
                blue = (byte)intValue;
            }
            else
            {
                // Aqua to blue. (0, 255, 255) to (0, 0, 255).
                intValue -= 768;
                red = 0;
                green = (byte)(255 - intValue);
                blue = 255;
            }
        }

        // Add the model to the Model3DGroup.
        private void DefineModel(Model3DGroup modelGroup, double[,] values, WriteableBitmap wbm)
        {
            MakeMesh(modelGroup, values, wbm);
        }

        // Cooordinate bounds.
        private int xmin, xmax, dx, zmin, zmax, dz;
        private double textureXscale, textureZscale;

        // Make the smooth surface.
        private void MakeMesh(Model3DGroup modelGroup, double[,] values, WriteableBitmap wbm)
        {
            // Make a mesh to hold the surface.
            MeshGeometry3D mesh = new MeshGeometry3D();

            // Create the surface's triangles.
            float offsetX = xmax / 2f;
            float offsetZ = zmax / 2f;
            for (int x = xmin; x <= xmax - dx; x += dx)
            {
                for (int z = zmin; z <= zmax - dz; z += dx)
                {
                    // Make points at the corners of the surface
                    // over (x, z) - (x + dx, z + dz).
                    Point3D p00 = new Point3D(x - offsetX, values[x, z], z - offsetZ);
                    Point3D p10 = new Point3D(x - offsetX + dx, values[x + dx, z], z - offsetZ);
                    Point3D p01 = new Point3D(x - offsetX, values[x, z + dz], z - offsetZ + dz);
                    Point3D p11 = new Point3D(x - offsetX + dx, values[x + dx, z + dz], z - offsetZ + dz);

                    // Add the triangles.
                    AddTriangle(mesh, p00, p01, p11);
                    AddTriangle(mesh, p00, p11, p10);
                }
            }
            Console.WriteLine(mesh.Positions.Count + " points");
            Console.WriteLine(mesh.TriangleIndices.Count / 3 + " triangles");
            Console.WriteLine();

            // Make the texture brush.
            ImageBrush textureBrush = new ImageBrush(wbm);

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

            // 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 the data.
        private double[,] MakeData()
        {
            double[,] values =
            {
                {0,0,0,1,2,2,1,0,0,0},
                {0,0,2,3,3,3,3,2,0,0},
                {0,2,3,4,5,5,4,3,2,0},
                {2,3,4,5,5,5,5,4,3,2},
                {3,4,4,6,7,7,6,4,4,3},
                {3,4,4,6,7,7,6,4,4,3},
                {2,3,4,4,5,5,4,4,3,2},
                {0,2,3,4,4,4,4,3,2,0},
                {0,0,2,3,3,3,3,2,0,0},
                {0,0,0,1,2,2,1,0,0,0}
            };
            // Move it down a little.
            for (int i = 0; i <= values.GetUpperBound(0); i++)
                for (int j = 0; j <= values.GetUpperBound(1); j++)
                    values[i, j] -= 2;

            xmin = 0;
            xmax = values.GetUpperBound(0);
            dx = 1;
            zmin = 0;
            zmax = values.GetUpperBound(1);
            dz = 1;

            textureXscale = (xmax - xmin);
            textureZscale = (zmax - zmin);

            return values;
        }

        // 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, mesh.TextureCoordinates, point1);
            int index2 = AddPoint(mesh.Positions, mesh.TextureCoordinates, point2);
            int index3 = AddPoint(mesh.Positions, mesh.TextureCoordinates, point3);

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

        // A dictionary to hold points for fast lookup.
        private Dictionary<Point3D, int> PointDictionary =
            new Dictionary<Point3D, int>();

        // If the point already exists, return its index.
        // Otherwise create the point and return its new index.
        private int AddPoint(Point3DCollection points, PointCollection positions, Point3D point)
        {
            // If the point is in the point dictionary,
            // return its saved index.
            if (PointDictionary.ContainsKey(point))
                return PointDictionary[point];

            // We didn't find the point. Create it.
            points.Add(point);
            PointDictionary.Add(point, points.Count - 1);

            // Set the point's texture coordinates.
            positions.Add(
                new Point(
                    (point.X - xmin) * textureXscale,
                    (point.Z - zmin) * textureZscale));

            // Return the new point's index.
            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();
        }
    }
}
