using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Autodesk.Revit.DB;
using IDTF.Net;
using Group = IDTF.Net.Group;

namespace PDF3D.Addin
{
    internal class IDTFBuilder
    {
        private readonly Document document;
        private IDTFScene _scene;
        private Dictionary<string, int> _shaderIndex = new Dictionary<string, int>();

        private Parent nullParent = new Parent() { Name = "<NULL>", Transform = new Transform4x4() };

        public IDTFBuilder(Autodesk.Revit.DB.Document document)
        {
            this.document = document;
            this._scene = new IDTFScene();
        }
        private void AddDefault()
        {
            var mat = new MaterialResource()
            {
                Name = "default",
                MaterialDiffuse = System.Drawing.Color.FromArgb(255, 125, 125, 125),
            };
            this._scene.MaterialResources.Add(mat.Name, mat);
            this._scene.ShaderResources.Add(mat.Name, new ShaderResource() { ShaderMaterialName = mat.Name, Name = mat.Name });
            _shaderIndex.Add(mat.Name, this._scene.ShaderResources.Count);
        }

        public bool InsertGroup(string name, string patrent = "")
        {
            if (this._scene.Groups.Any(x => x.Name == name))
            {
                return false;
            }
            else
            {
                var group = new Group() { Name = name, Parents = new List<Parent>() { _scene.Groups.Any(x => x.Name == patrent) ? new Parent(patrent) : nullParent } };
                this._scene.Groups.Add(group);
                return true;
            }
        }
        public bool InsertMaterial(Material material)
        {
            if (material == null)
            {
                if (!this._scene.MaterialResources.ContainsKey("default"))
                {
                    AddDefault();
                }
                return false;
            }
            else if (!this._scene.MaterialResources.ContainsKey(material.Name))
            {
                var mat = new MaterialResource()
                {
                    Name = material.Name,
                    MaterialDiffuse = System.Drawing.Color.FromArgb(255, material.Color.Red, material.Color.Green, material.Color.Blue),
                    MaterialOpacity = (100 - material.Transparency) / 100f
                };
                this._scene.MaterialResources.Add(mat.Name, mat);
                this._scene.ShaderResources.Add(mat.Name, new ShaderResource() { ShaderMaterialName = mat.Name, Name = mat.Name });
                _shaderIndex.Add(mat.Name, this._scene.ShaderResources.Count);
                return true;
            }
            return false;
        }
        public bool InsertElement(Element element)
        {
            Parent parent;
            if (element is FamilyInstance fi)
            {
                this.InsertGroup(fi.Symbol.FamilyName, fi.Category.Name);
                parent = new Parent(fi.Symbol.FamilyName);
            }
            else
            {
                parent = new Parent(element.Category.Name);
            }

            var option = new Options();
            option.ComputeReferences = true;
            option.DetailLevel = ViewDetailLevel.Fine;
            option.IncludeNonVisibleObjects = false;
            GeometryElement geoElement = element.get_Geometry(option);
            var meshData = ConvertGeometryObject(geoElement);
            if (meshData != null && meshData.ModelPositionList.Any())
            {
                var name = element.Name + " " + element.Id.ToString();
                var model = new Model
                {
                    Name = name,
                    Parents = new List<Parent>() { parent },
                    Visibility = ModelVisibility.BOTH
                };
                var modelRes = new ModelResource(ModelType.MESH) { Name = name };
                modelRes.Mesh = meshData;

                model.Resource = modelRes;
                _scene.Models.Add(model);
                _scene.ModelResources.Add(name, modelRes);
            }
            return false;
        }

        private MeshData ConvertGeometryObject(GeometryObject go)
        {
            if (go is GeometryElement ge)
            {
                MeshData meshData = new MeshData();
                foreach (var item in ge)
                {
                    var data = ConvertGeometryObject(item);
                    if (data != null)
                    {
                        CombineMeshdata(meshData, data);
                    }
                }
                return meshData;
            }

            if (go is GeometryInstance gi)
            {
                return ConvertGeometryObject(gi.GetInstanceGeometry());
            }
            if (go is Face face)
            {
                var faceMesh = face.Triangulate(0.4);
                MeshData meshData = new MeshData();
                meshData.ModelPositionList.AddRange(faceMesh.Vertices.Select(p => new Point3((float)p.X * 304.8f, (float)p.Y * 304.8f, (float)p.Z * 304.8f)));
                for (int i = 0; i < faceMesh.NumTriangles; i++)
                {
                    var tri = faceMesh.get_Triangle(i);
                    meshData.MeshFacePositionList.Add(new Int3((int)tri.get_Index(0), (int)tri.get_Index(1), (int)tri.get_Index(2)));
                }
                var material = face?.MaterialElementId.IntegerValue > 0
                                ? (document.GetElement(face?.MaterialElementId) as Autodesk.Revit.DB.Material) : null;
                this.InsertMaterial(material);
                var shaderDesc = material == null ? new ShadingDescription(_shaderIndex["default"]) : new ShadingDescription(_shaderIndex[material.Name]);

                meshData.ShadingDescriptionList.Add(shaderDesc);
                for (int i = 0; i < meshData.MeshFacePositionList.Count; i++)
                {
                    meshData.MeshFaceShadingList.Add(0);
                }

                return meshData;
            }
            if (go is Mesh mesh)
            {
                MeshData meshData = new MeshData();
                meshData.ModelPositionList.AddRange(mesh.Vertices.Select(p => new Point3((float)p.X * 304.8f, (float)p.Y * 304.8f, (float)p.Z * 304.8f)));
                for (int i = 0; i < mesh.NumTriangles; i++)
                {
                    var tri = mesh.get_Triangle(i);
                    meshData.MeshFacePositionList.Add(new Int3((int)tri.get_Index(0), (int)tri.get_Index(1), (int)tri.get_Index(2)));
                }
                var material = mesh?.MaterialElementId.IntegerValue > 0
                ? (document.GetElement(mesh?.MaterialElementId) as Autodesk.Revit.DB.Material) : null;
                this.InsertMaterial(material);

                var shaderDesc = material == null ? new ShadingDescription(_shaderIndex["default"]) : new ShadingDescription(_shaderIndex[material.Name]);

                meshData.ShadingDescriptionList.Add(shaderDesc);
                for (int i = 0; i < meshData.MeshFacePositionList.Count; i++)
                {
                    meshData.MeshFaceShadingList.Add(0);
                }

                return meshData;
            }
            if (go is Solid solid)
            {
                var meshData = new MeshData();

                if (solid.Volume != 0)
                {
                    foreach (Face item in solid.Faces)
                    {
                        var data = ConvertGeometryObject(item);
                        if (data != null)
                        {
                            CombineMeshdata(meshData, data);
                        }
                    }
                }

                return meshData;
            }

            return null;
        }

        private void CombineMeshdata(MeshData data1, MeshData data2)
        {
            var oldCount = data1.ModelPositionList.Count;
            data1.ModelPositionList.AddRange(data2.ModelPositionList);
            data1.MeshFacePositionList.AddRange(data2.MeshFacePositionList.Select(x => new Int3(x.IntArray[0] + oldCount, x.IntArray[1] + oldCount, x.IntArray[2] + oldCount)));
            for (int i = 0; i < data2.MeshFaceDiffuseColorList.Count; i++)
            {
                int index = data2.MeshFaceDiffuseColorList[i].IntArray.ElementAt(0);
                var color = data2.ModelDiffuseColorList[index];
                int colorIndex = data1.ModelDiffuseColorList.IndexOf(color);
                if (colorIndex != -1)
                {
                    data1.MeshFaceDiffuseColorList.Add(new Int3(colorIndex, colorIndex, colorIndex));
                }
                else
                {
                    data1.ModelDiffuseColorList.Add(color);
                    data1.MeshFaceDiffuseColorList.Add(new Int3(data1.ModelDiffuseColorList.Count - 1, data1.ModelDiffuseColorList.Count - 1, data1.ModelDiffuseColorList.Count - 1));
                }
            }
            for (int i = 0; i < data2.MeshFaceShadingList.Count; i++)
            {
                var index = data2.MeshFaceShadingList[i];
                var shader = data2.ShadingDescriptionList[index];
                if (data1.ShadingDescriptionList.Any(x => x.ShaderID == shader.ShaderID))
                {
                    int shaderIndex = data1.ShadingDescriptionList.IndexOf(data1.ShadingDescriptionList.FirstOrDefault(x => x.ShaderID == shader.ShaderID));
                    data1.MeshFaceShadingList.Add(shaderIndex);
                }
                else
                {
                    data1.ShadingDescriptionList.Add(shader);
                    data1.MeshFaceShadingList.Add(data1.ShadingDescriptionList.Count - 1);
                }
            }

        }

        public void Export(string path)
        {
            Distinct();
            _scene.Export(path);
        }

        private void Distinct()
        {
            var comparer = new Point3Comparer();
            foreach (var item in _scene.ModelResources.Values)
            {
                var points = item.Mesh.ModelPositionList.Distinct(comparer).ToList();
                if (points.Count != item.Mesh.ModelPositionList.Count)
                {
                    for (int i = 0; i < item.Mesh.MeshFacePositionList.Count; i++)
                    {
                        Int3 face = item.Mesh.MeshFacePositionList[i];
                        int a = points.IndexOf(item.Mesh.ModelPositionList[face.IntArray[0]]);
                        int b = points.IndexOf(item.Mesh.ModelPositionList[face.IntArray[1]]);
                        int c = points.IndexOf(item.Mesh.ModelPositionList[face.IntArray[2]]);
                        item.Mesh.MeshFacePositionList[i] = new Int3(a, b, c);
                    }
                    item.Mesh.ModelPositionList = points;
                }
                //List<Point3> points = new List<Point3>();
                //foreach (var point in item.Mesh.ModelPositionList)
                //{
                //    var str = point.ToString();
                //    if (!points.Any(x => x.ToString() == str))
                //    {
                //        points.Add(point);
                //    }
                //}
                //if (points.Count != item.Mesh.ModelPositionList.Count)
                //{
                //    var ptStr = points.Select(x => x.ToString()).ToList();
                //    for (int i = 0; i < item.Mesh.MeshFacePositionList.Count; i++)
                //    {
                //        Int3 face = item.Mesh.MeshFacePositionList[i];
                //        int a = ptStr.IndexOf(item.Mesh.ModelPositionList[face.IntArray[0]].ToString());
                //        int b = ptStr.IndexOf(item.Mesh.ModelPositionList[face.IntArray[1]].ToString());
                //        int c = ptStr.IndexOf(item.Mesh.ModelPositionList[face.IntArray[2]].ToString());
                //        item.Mesh.MeshFacePositionList[i] = new Int3(a, b, c);
                //    }
                //    item.Mesh.ModelPositionList = points;
                //}
            }
        }

        internal void InsertView(View3D item)
        {
            IDTF.Net.View view = new IDTF.Net.View() { Name = item.Name };
            ViewData viewData = new ViewData();
            view.ViewData = viewData;
            this._scene.Views.Add(view);
        }
    }
}
