using HelixToolkit.Wpf;
using SurfacePlot3D.Common;
using SurfacePlot3D.Enumerations;
using SurfacePlot3D.Members;
using SurfacePlot3D.MeshGenerators;
using SurfacePlot3D.Services;
using SurfacePlot3D.Statics;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Media3D;
using System.Xml.Serialization;

namespace SurfacePlot3D
{
    public class SurfacePlotViewModel : CustomBindableBase
    {
        #region Constructor(s)

        public SurfacePlotViewModel() 
        {
            InitializeSurface();        
        }

        #endregion

        #region Private Variables


        private SurfacePlotDataModel _rsd = new SurfacePlotDataModel();
        private Boolean _showGridX = true;
        private Boolean _showGridY = true;
        private Boolean _showGridZ = true;

        private Model3DGroup _mainModel3DGroup = new Model3DGroup();
        private Model3DGroup _textModel3DGroup = new Model3DGroup();
        private Model3DGroup _indicatorModel3DGroup = new Model3DGroup();

        private Double _majorDistanceX = 10D;
        private Double _majorDistanceY = 10D;
        private Double _majorDistanceZ = 10D;

        private Double _minorDistanceX = 5D;
        private Double _minorDistanceY = 5D;
        private Double _minorDistanceZ = 5D;

        private Double _maxX = 0D;
        private Double _maxY = 0D;

        private Double _gridZHeight = 0D;
        private Double y1 = -60;

        private StatePopulationDataItem _selectedDataItem = null;
        private StatePopulationDataItem _selectedState = null;

        #endregion

        #region Private Auto Properties

        private Double MinX { get; set; }
        private Double MinY { get; set; }
        private int Rows { get; set; }
        private int Columns { get; set; }
        private Func<Double, Double, Double> Function { get; set; }

        #endregion

        #region Public Auto Properties

        public Double[,] ColorValues { get; set; }
        public ColorCoding SelectedColorCoding { get; set; } = ColorCoding.ByGradientY;
        public Point3D[,] Data { get; set; }

        #endregion

        #region Private Commands

        private ICommand _readCsvCommandIc = null;

        #endregion

        #region Public Commands

        [XmlIgnore] public ICommand ReadCsvCommandIc => _readCsvCommandIc ?? (_readCsvCommandIc = new RelayCommand<Object>(ReadCsvCommandIcExecuted));

        #endregion

        #region Private Functions

        private List<String> GetColors()
        {
            // Create empty list
            List<string> colorList = new List<string>();

            // Get type of KnownColor enum
            var color = typeof(System.Drawing.KnownColor);

            // Enumerate all known color names in enum
            var colors = Enum.GetValues(color);

            // Remove 27 from beginning
            var from = 27;

            // Remove 7 elements from the end
            var to = colors.Length - 7;

            // Only keep color names and not user interface colors
            for (int i = from; i < to; i++)
            {
                colorList.Add(colors.GetValue(i).ToString());
            }

            // Return filtered color list
            return colorList;
        }

        private StatePopulationDataItem CreateDataItem(Double population, Double xOffset, Double yOffset, Double zOffset)
        {
            StatePopulationDataItem dataItem = new StatePopulationDataItem();
            dataItem.Text = population.ToString();
            dataItem.TextOffsetPosX = xOffset;
            dataItem.TextOffsetPosY = yOffset;
            dataItem.TextOffsetPosZ = zOffset;
            dataItem.RotateTextZ = 90;
            y1 += 2.4;

            return dataItem;
        }

        private Point GetPointFromIndex(int i, int j)
        {
            Double x = (MinX + (double)j / (Columns - 1) * (MaxX - MinX)) - (MaxX / 2);
            Double y = (MinY + (double)i / (Rows - 1) * (MaxY - MinY)) - (MaxY / 2);
            return new Point(x, y);
        }

        private Point3D[,] CreateDataArray(Func<double, double, double> f)
        {
            var data = new Point3D[Rows, Columns];

            for (int i = 0; i < Rows; i++)
            {
                for (int j = 0; j < Columns; j++)
                {
                    var populationCheck = Rsd.StatePopulationDataItems.Where(c => c.Row == i && c.Column == j);

                    var pt = GetPointFromIndex(i, j);
                    Double population = 0;
                    if (populationCheck.Any())
                    {
                        population = populationCheck.FirstOrDefault().Population / PopulationDivide;
                        populationCheck.FirstOrDefault().PosX = pt.X;
                        populationCheck.FirstOrDefault().PosY = pt.Y;
                        populationCheck.FirstOrDefault().PosZ = population;
                    }



                    data[i, j] = new Point3D(pt.X, pt.Y, population); //f(pt.X, pt.Y) //r.Next(1,3) - 6D - .1D
                }

            }
            return data;
        }
        private Double[,] FindGradientY(Point3D[,] data)
        {
            int n = data.GetUpperBound(0) + 1;
            int m = data.GetUpperBound(1) + 1;
            var K = new double[n, m];
            for (int i = 0; i < n; i++)
                for (int j = 0; j < m; j++)
                {
                    // Finite difference approximation
                    var p10 = data[i + 1 < n ? i + 1 : i, j - 1 > 0 ? j - 1 : j];
                    var p00 = data[i - 1 > 0 ? i - 1 : i, j - 1 > 0 ? j - 1 : j];
                    var p11 = data[i + 1 < n ? i + 1 : i, j + 1 < m ? j + 1 : j];
                    var p01 = data[i - 1 > 0 ? i - 1 : i, j + 1 < m ? j + 1 : j];

                    //double dx = p01.X - p00.X;
                    //double dz = p01.Z - p00.Z;
                    //double Fx = dz / dx;

                    double dy = p10.Y - p00.Y;
                    double dz = p10.Z - p00.Z;

                    K[i, j] = dz / dy;
                }
            return K;
        }
        #endregion

        #region Private ICommand Executes

        private void ReadCsvCommandIcExecuted(Object obj)
		{
			UIServices.Busy();

			Rsd = new SurfacePlotDataModel();

			String strCsv = String.Empty;
			String fileName = "historical_state_population_by_year.csv";

            FileInfo fileInfo = new FileInfo(System.Reflection.Assembly.GetExecutingAssembly().Location);
            var executingAssembly = fileInfo.DirectoryName + @"\";
            fileInfo = null;

			String fullFileName = executingAssembly + fileName;

			StreamReader sr = new StreamReader(fullFileName);
            strCsv = sr.ReadToEnd();
			sr.Close();
			sr.Dispose();
			sr = null;
			
			var records = strCsv.Split("\r\r\n");
			foreach(var record in records) 
			{
				if (!String.IsNullOrEmpty(record))
				{
					var dataItem = record.Split(@",");

					StatePopulationDataItem statePopulationDataItem = new StatePopulationDataItem();

					try
					{
						statePopulationDataItem.State = dataItem[0];
						statePopulationDataItem.Year = Convert.ToDouble(dataItem[1]);
						statePopulationDataItem.Population = Convert.ToDouble(dataItem[2]);
					}
					catch (System.Exception ex)
					{
						String err = ex.Message;
					}

					Rsd.StatePopulationDataItems.Add(statePopulationDataItem);
				}
            }

            var stateSet = Rsd.StatePopulationDataItems.DistinctBy(c => c.State).OrderBy(c => c.State);

            int r = 0; 
            foreach (var state in stateSet)
            {
                int c = 0;
                var yearSet = Rsd.StatePopulationDataItems.Where(c=>c.State==state.State).OrderBy(c=>c.Year);
                foreach(var year in yearSet)
                {
                    year.Row = r;
                    year.Column = c;
                    c++;
                }
                r++;
            }

            var populationMin = Rsd.StatePopulationDataItems.Min(c => c.Population);
            var populationMax = Rsd.StatePopulationDataItems.Max(c => c.Population);

			var populationCount = Rsd.StatePopulationDataItems.Count;

			Double index = 0;
            int colorIndex = 0;
			//X = State
			//Y = Year
			//Z = Population
			var states = Rsd.StatePopulationDataItems.DistinctBy(c=>c.State).ToList(); //51
            var years = Rsd.StatePopulationDataItems.DistinctBy(c => c.Year).ToList(); //120

            var gridZHeight = Rsd.StatePopulationDataItems.Max(c => c.Population) / PopulationDivide;
            GridZHeight = gridZHeight;// / 2D;

            Rows = states.Count;
            Columns = years.Count;
            MajorDistanceX = 120;
            MajorDistanceY = 120;
            MajorDistanceZ = 120;

            //BoxMeshGenerator boxMeshGenerator = new BoxMeshGenerator();

            #region Test

            //         var colors = GetColors();
            //         int exit = 4;
            //         int trackingIndex = 0;

            //         GeometryModel3D geometryModel3DNegX = new GeometryModel3D();
            //         geometryModel3DNegX.Material = new DiffuseMaterial(new SolidColorBrush() { Color = Colors.SeaGreen });
            //         geometryModel3DNegX.Geometry = new SphereMeshGenerator() { Radius = .5 }.Geometry;
            //         TranslateTransform3D ttNegX = new TranslateTransform3D() { OffsetX = -5, OffsetY = 0, OffsetZ = 0 };
            //         geometryModel3DNegX.Transform = ttNegX;
            //         MainModel3DGroup.Children.Add(geometryModel3DNegX);

            //         GeometryModel3D geometryModel3DPosX = new GeometryModel3D();
            //         geometryModel3DPosX.Material = new DiffuseMaterial(new SolidColorBrush() { Color = Colors.SeaGreen });
            //         geometryModel3DPosX.Geometry = new SphereMeshGenerator() { Radius = .5 }.Geometry;
            //         TranslateTransform3D ttPosX = new TranslateTransform3D() { OffsetX = 5, OffsetY = 0, OffsetZ = 0 };
            //         geometryModel3DPosX.Transform = ttPosX;
            //         MainModel3DGroup.Children.Add(geometryModel3DPosX);

            //         GeometryModel3D geometryModel3DNegY = new GeometryModel3D();
            //         geometryModel3DNegY.Material = new DiffuseMaterial(new SolidColorBrush() { Color = Colors.DarkRed });
            //         geometryModel3DNegY.Geometry = new SphereMeshGenerator() { Radius = .5 }.Geometry;
            //         TranslateTransform3D ttNegY = new TranslateTransform3D() { OffsetX = 0, OffsetY = -5, OffsetZ = 0 };
            //         geometryModel3DNegY.Transform = ttNegY;
            //         MainModel3DGroup.Children.Add(geometryModel3DNegY);

            //         GeometryModel3D geometryModel3DPosY = new GeometryModel3D();
            //         geometryModel3DPosY.Material = new DiffuseMaterial(new SolidColorBrush() { Color = Colors.DarkRed });
            //         geometryModel3DPosY.Geometry = new SphereMeshGenerator() { Radius = .5 }.Geometry;
            //         TranslateTransform3D ttPosY = new TranslateTransform3D() { OffsetX = 0, OffsetY = 5, OffsetZ = 0 };
            //         geometryModel3DPosY.Transform = ttPosY;
            //         MainModel3DGroup.Children.Add(geometryModel3DPosY);

            //         int yearIndex = 0;
            //         SphereMeshGenerator sphereMeshGenerator = new SphereMeshGenerator() { Slices = 8, Radius = .25 };
            //         foreach (var state in Rsd.StatePopulationDataItems.DistinctBy(c => c.State).OrderBy(c => c.State)) 
            //         {
            //             foreach (var year in Rsd.StatePopulationDataItems.Where(c=>c.State == state.State).DistinctBy(c=>c.Year).OrderBy(c=>c.Year))
            //             {
            //                 foreach (var dataItem in Rsd.StatePopulationDataItems.Where(c => c.State == state.State && c.Year == year.Year))
            //                 {
            //                     GeometryModel3D geometryModel3D = new GeometryModel3D();

            //                     Color color = (Color)ColorConverter.ConvertFromString((String)colors[colorIndex]);
            //                     geometryModel3D.Material = new DiffuseMaterial(new SolidColorBrush() { Color = color });
            //                     geometryModel3D.Geometry = sphereMeshGenerator.Geometry;// boxMeshGenerator.Geometry;

            //                     TranslateTransform3D tt = new TranslateTransform3D() 
            //                     { 
            //                         OffsetX = index,
            //                         OffsetY = yearIndex, //dataItem.Year / 1000,                           
            //                         OffsetZ = dataItem.Population / 100000D 
            //                     };

            //                     geometryModel3D.Transform = tt;
            //                     MainModel3DGroup.Children.Add(geometryModel3D);

            //                 }
            //                 yearIndex++;
            //             }
            //             yearIndex = 0;
            //             colorIndex++;
            //             index++;
            //             trackingIndex++;
            //             //if (trackingIndex == exit) break;
            //         }

            //boxMeshGenerator = null;

            //FileService fs = new FileService();
            //fs.SaveProjectFileToXml<SurfacePlotDataModel>(Rsd, XmlUtil.LoadSaveTypes);
            //fs = null; 

            #endregion

            SelectedDataItem = Rsd.StatePopulationDataItems.FirstOrDefault();

            Render3DText();
        }

        #endregion

        #region Public Clr Properties
        private Double _populationDivide = 750000;

        public Double PopulationDivide { get { return _populationDivide; } set { SetProperty(ref _populationDivide, value); UpdateModel(); } }

        public Double MajorDistanceX { get { return _majorDistanceX; } set { SetProperty(ref _majorDistanceX, value); } }
        public Double MajorDistanceY { get { return _majorDistanceY; } set { SetProperty(ref _majorDistanceY, value); } }
        public Double MajorDistanceZ { get { return _majorDistanceZ; } set { SetProperty(ref _majorDistanceZ, value); } }

        public Double MinorDistanceX { get { return _minorDistanceX; } set { SetProperty(ref _minorDistanceX, value); } }
        public Double MinorDistanceY { get { return _minorDistanceY; } set { SetProperty(ref _minorDistanceY, value); } }
        public Double MinorDistanceZ { get { return _minorDistanceZ; } set { SetProperty(ref _minorDistanceZ, value); } }
        public Double MaxX { get { return _maxX; } set { SetProperty(ref _maxX, value); } }
        public Double MaxY { get { return _maxY; } set { SetProperty(ref _maxY, value); } }
        public Double GridZHeight { get { return _gridZHeight; } set { SetProperty(ref _gridZHeight, value); } }


        public SurfacePlotDataModel Rsd { get { return _rsd; } set { SetProperty(ref _rsd, value); } }

        public Boolean ShowGridX { get { return _showGridX; } set { SetProperty(ref _showGridX, value); } }
        public Boolean ShowGridY { get { return _showGridY; } set { SetProperty(ref _showGridY, value); } }
        public Boolean ShowGridZ { get { return _showGridZ; } set { SetProperty(ref _showGridZ, value); } }

        public Model3DGroup IndicatorModel3DGroup { get { return _mainModel3DGroup; } set { SetProperty(ref _mainModel3DGroup, value); } }

        public Model3DGroup MainModel3DGroup { get { return _mainModel3DGroup; } set { SetProperty(ref _mainModel3DGroup, value); } }
        public Model3DGroup TextModel3DGroup { get { return _textModel3DGroup; } set { SetProperty(ref _textModel3DGroup, value); } }
        public StatePopulationDataItem SelectedDataItem { get { return _selectedDataItem; } set { SetProperty(ref _selectedDataItem, value); NotifyPropertyChanged("FilteredStatePopulationDataItems"); GenerateIndicator(); } }
        public StatePopulationDataItem SelectedState { get { return _selectedState; } set { SetProperty(ref _selectedState, value); Rsd.StateFilter = SelectedState.State; } }

        #endregion

        #region Public Readonly Properties

        public Brush SurfaceBrush
        {
            get
            {
                // Brush = BrushHelper.CreateGradientBrush(Colors.White, Colors.Blue);
                // Brush = GradientBrushes.RainbowStripes;
                // Brush = GradientBrushes.BlueWhiteRed;
                switch (SelectedColorCoding)
                {
                    case ColorCoding.ByGradientY:
                        return BrushHelper.CreateGradientBrush(Colors.Red, Colors.White, Colors.Blue);
                    case ColorCoding.ByLights:
                        return Brushes.White;
                }
                return null;
            }
        }

        #endregion

        #region Private Methods

        private void InitializeSurface()
        {
            ReadCsvCommandIcExecuted(null);
            MinX = 0;
            MaxX = Rows;
            MinY = 0;
            MaxY = Columns;

            //Rows = 10;
            //Columns = 10;

            Function = (x, y) => Math.Sin(x * y) * 0.5;
            SelectedColorCoding = ColorCoding.ByGradientY;
            UpdateModel();
        }

        private void UpdateModel()
        {
            Data = CreateDataArray(Function);
            switch (SelectedColorCoding)
            {
                case ColorCoding.ByGradientY:
                    ColorValues = FindGradientY(Data);
                    break;
                case ColorCoding.ByLights:
                    ColorValues = null;
                    break;
            }
            NotifyPropertyChanged("Data");
            NotifyPropertyChanged("ColorValues");
            NotifyPropertyChanged("SurfaceBrush");
        }

        private void GenerateIndicator()
        {
            IndicatorModel3DGroup.Children.Clear();

            ArrowMeshGenerator arrowMeshGenerator = new ArrowMeshGenerator() 
            { 
                Radius = 2, 
                Point1Y = 5, Point2Y = -5, HeadLength = 1 };
            arrowMeshGenerator.CycleCommandIcExecuted(null);

            GeometryModel3D geometryModel3DIndicator = new GeometryModel3D();           
            geometryModel3DIndicator.Material = new DiffuseMaterial(new SolidColorBrush() { Color = Colors.SeaGreen });
            geometryModel3DIndicator.Geometry = arrowMeshGenerator.Geometry;

            TranslateTransform3D ttIndicator = new TranslateTransform3D() 
            { 
                OffsetX = SelectedDataItem.PosX, 
                OffsetY = SelectedDataItem.PosY, 
                OffsetZ = SelectedDataItem.PosZ + 8 
            };
            geometryModel3DIndicator.Transform = ttIndicator;
            IndicatorModel3DGroup.Children.Add(geometryModel3DIndicator);
        }

        private void Render3DText()
        {
            TextModel3DGroup.Children.Clear();

            var stateSet = Rsd.StatePopulationDataItems.DistinctBy(c => c.State).OrderBy(c => c.State);
            var maxPopulation = Rsd.StatePopulationDataItems.Max(c => c.Population);
            var minPopulation = Rsd.StatePopulationDataItems.DistinctBy(c => c.Population);
            var midPopulation = maxPopulation / 2D;
            var threeQuarterPopulation = (maxPopulation / 4D) * 3D;
            var oneQuarterPopulation = (maxPopulation / 4D) * 1D;

            IList<StatePopulationDataItem> stateText = new List<StatePopulationDataItem>();
            IList<StatePopulationDataItem> populationText = new List<StatePopulationDataItem>() { };
            IList<StatePopulationDataItem> yearText = new List<StatePopulationDataItem>() { };

            populationText.Add(CreateDataItem(maxPopulation, 26, 65, 53));
            populationText.Add(CreateDataItem(maxPopulation, 26, -65, 53));
            populationText.Add(CreateDataItem(midPopulation, 26, 65, 53D / 2D));
            populationText.Add(CreateDataItem(midPopulation, 26, -65, 53D / 2D));
            populationText.Add(CreateDataItem(threeQuarterPopulation, 26, 65, (53D / 4D) * 3));
            populationText.Add(CreateDataItem(threeQuarterPopulation, 26, -65, (53D / 4D) * 3));
            populationText.Add(CreateDataItem(oneQuarterPopulation, 26, 65, (53D / 4D) * 1));
            populationText.Add(CreateDataItem(oneQuarterPopulation, 26, -65, (53D / 4D) * 1));
            //populationText.Add(CreateDataItem(0, 26, 65, 0));
            //populationText.Add(CreateDataItem(0, 26, -65, 0));

            yearText.Add(CreateDataItem(1900, -26, -65, 0));
            yearText.Add(CreateDataItem(1930, -13, -65, 0));
            yearText.Add(CreateDataItem(1960, 0, -65, 0));
            yearText.Add(CreateDataItem(1990, 13, -65, 0));
            yearText.Add(CreateDataItem(2019, 26, -65, 0));

            yearText.Add(CreateDataItem(1900, -26, 65, 0));
            yearText.Add(CreateDataItem(1930, -13, 65, 0));
            yearText.Add(CreateDataItem(1960, 0, 65, 0));
            yearText.Add(CreateDataItem(1990, 13, 65, 0));
            yearText.Add(CreateDataItem(2019, 26, 65, 0));

            #region State Text

            Double y = -60;

            foreach (var state in stateSet)
            {
                StatePopulationDataItem dataItem = new StatePopulationDataItem();
                dataItem.Text = state.State;
                dataItem.TextOffsetPosX = -27D;
                dataItem.TextOffsetPosY = y;
                dataItem.RotateTextZ = 90;
                stateText.Add(dataItem);
                y += 2.4;
            }

            var stateTextDataItems = stateText.Where(c => !String.IsNullOrEmpty(c.Text));
            if (stateTextDataItems.Any()) BuildText3D(stateTextDataItems);

            #endregion

            if (populationText.Any()) BuildText3D(populationText);

            if (yearText.Any()) BuildText3D(yearText);
        }

        private void BuildText3D(IEnumerable<StatePopulationDataItem> textDataItems)
        {
            LetterService letterService = new LetterService();

            foreach (var dataItem in textDataItems)
            {
                var textDataItem = dataItem;
                textDataItem.TextFontSize = dataItem.TextFontSize;
                textDataItem.FontDepth = dataItem.FontDepth;
                TextModel3DGroup.Children.Add(letterService.AddRibbonTextItem(textDataItem));
            }

            letterService = null;
        }

        #endregion
    }
}