using SanAndreasUnity.Behaviours;
using SanAndreasUnity.Importing.Archive;
using SanAndreasUnity.Importing.Collision;
using SanAndreasUnity.Importing.Items.Definitions;
using SanAndreasUnity.Importing.RenderWareStream;
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Profiling;

namespace SanAndreasUnity.Importing.Conversion
{
    [Flags]
    public enum MaterialFlags
    {
        Default = 0,
        NoBackCull = 1,
        Alpha = 2,
        Vehicle = 4,
        OverrideAlpha = 8
    }

    public class Geometry
    {
        private static int _sMainTexId = -1;

        protected static int MainTexId
        {
            get { return _sMainTexId == -1 ? _sMainTexId = Shader.PropertyToID("_MainTex") : _sMainTexId; }
        }

        private static int _sMaskTexId = -1;

        protected static int MaskTexId
        {
            get { return _sMaskTexId == -1 ? _sMaskTexId = Shader.PropertyToID("_MaskTex") : _sMaskTexId; }
        }

        private static int _sMetallicId = -1;

        protected static int MetallicId
        {
            get { return _sMetallicId == -1 ? _sMetallicId = Shader.PropertyToID("_Metallic") : _sMetallicId; }
        }

        private static int _sSmoothnessId = -1;

        protected static int SmoothnessId
        {
            get { return _sSmoothnessId == -1 ? _sSmoothnessId = Shader.PropertyToID("_Smoothness") : _sSmoothnessId; }
        }

        private static int _sCarColorIndexId = -1;

        public static int CarColorIndexId
        {
            get { return _sCarColorIndexId == -1 ? _sCarColorIndexId = Shader.PropertyToID("_CarColorIndex") : _sCarColorIndexId; }
        }

        private static int[] FromTriangleStrip(IList<int> indices)
        {
            var dst = new List<int>((indices.Count - 2) * 3);

            for (var i = 0; i < indices.Count - 2; ++i)
            {
                var a = indices[i];
                var b = indices[i + 1 + (i & 1)];
                var c = indices[i + 2 - (i & 1)];

                if (a == b || b == c || a == c) continue;

                dst.Add(a);
                dst.Add(b);
                dst.Add(c);
            }

            return dst.ToArray();
        }

        private static UnityEngine.Vector3[] CalculateNormals(RenderWareStream.Geometry src, UnityEngine.Vector3[] verts)
        {
            var norms = new UnityEngine.Vector3[src.VertexCount];

            for (var i = 0; i < src.FaceCount; ++i)
            {
                var face = src.Faces[i];

                var a = verts[face.Vertex0];
                var b = verts[face.Vertex1];
                var c = verts[face.Vertex2];

                var v = b - a;
                var w = c - b;

                var norm = new UnityEngine.Vector3(
                    v.y * w.z - v.z * w.y,
                    v.z * w.x - v.x * w.z,
                    v.x * w.y - v.y * w.x).normalized;

                norms[face.Vertex0] -= norm;
                norms[face.Vertex1] -= norm;
                norms[face.Vertex2] -= norm;
            }

            for (var i = 0; i < src.VertexCount; ++i)
            {
                if (norms[i].sqrMagnitude <= 0f)
                {
                    norms[i] = UnityEngine.Vector3.up;
                }
                else
                {
                    norms[i] = norms[i].normalized;
                }
            }

            return norms;
        }

        private static readonly Dictionary<MaterialFlags, Shader> _sShaders
            = new Dictionary<MaterialFlags, Shader>();

        private static Shader GetShaderNoCache(MaterialFlags flags)
        {
            var noBackCull = (flags & MaterialFlags.NoBackCull) == MaterialFlags.NoBackCull;
            var alpha = (flags & MaterialFlags.Alpha) == MaterialFlags.Alpha;
            var vehicle = (flags & MaterialFlags.Vehicle) == MaterialFlags.Vehicle;

            if (vehicle && alpha)
            {
                return Shader.Find("SanAndreasUnity/VehicleTransparent");
            }

            if (vehicle)
            {
                return Shader.Find("SanAndreasUnity/Vehicle");
            }

            if (noBackCull && alpha)
            {
                return Shader.Find("SanAndreasUnity/TransparentNoBackCull");
            }

            if (noBackCull)
            {
                return Shader.Find("SanAndreasUnity/NoBackCull");
            }

            if (alpha)
            {
                return Shader.Find("SanAndreasUnity/Transparent");
            }

            return Shader.Find("SanAndreasUnity/Default");
        }

        private static Shader GetShader(MaterialFlags flags)
        {
            if (_sShaders.ContainsKey(flags)) return _sShaders[flags];

            var shader = GetShaderNoCache(flags);
            _sShaders.Add(flags, shader);
            return shader;
        }

        private static readonly Color32[] _sKeyColors = new[] {
            new Color32(255, 255, 255, 255),

            // Paint job
            new Color32(60, 255, 0, 255),
            new Color32(255, 0, 175, 255),
            new Color32(60, 255, 0, 255),  // TODO
            new Color32(255, 0, 175, 255), // TODO

            // Head lights
            new Color32(255, 175, 0, 255),   // L
            new Color32(0, 255, 200, 255), // R

            // Tail lights
            new Color32(185, 255, 0, 255), // L
            new Color32(255, 60, 0, 255)   // R
        };

        private static LoadedTexture _sWhiteTex;

        private static LoadedTexture WhiteTex
        {
            get { return _sWhiteTex ?? (_sWhiteTex = new LoadedTexture(Texture2D.whiteTexture, false)); }
        }

        private static UnityEngine.Material Convert(RenderWareStream.Material src, TextureDictionary[] txds, MaterialFlags flags)
        {
            LoadedTexture diffuse;
            LoadedTexture mask = null;

            var overrideAlpha = (flags & MaterialFlags.OverrideAlpha) == MaterialFlags.OverrideAlpha;
            var vehicle = (flags & MaterialFlags.Vehicle) == MaterialFlags.Vehicle;

            if (!overrideAlpha && src.Colour.A != 255)
            {
                flags |= MaterialFlags.Alpha;
            }

            if (src.TextureCount > 0)
            {
                var tex = src.Textures[0];
                diffuse = txds.GetDiffuse(tex.TextureName);

                if (src.TextureCount > 1)
                {
                    Debug.LogFormat("Something has {0} textures!", src.TextureCount);
                }

                if (diffuse == null)
                {
                    Debug.LogWarningFormat("Unable to find texture {0}", tex.TextureName);
                }

                if (!string.IsNullOrEmpty(tex.MaskName))
                {
                    mask = txds.GetAlpha(tex.MaskName) ?? diffuse;
                }
                else if (vehicle)
                {
                    mask = diffuse;
                }

                if (!overrideAlpha && mask != null && mask.HasAlpha)
                {
                    flags |= MaterialFlags.Alpha;
                }
            }
            else
            {
                diffuse = WhiteTex;
            }

            var shader = GetShader(flags);
            var mat = new UnityEngine.Material(shader);

            var clr = Types.Convert(src.Colour);

            if (vehicle)
            {
                var found = false;
                for (var i = 1; i < _sKeyColors.Length; ++i)
                {
                    var key = _sKeyColors[i];
                    if (key.r != clr.r || key.g != clr.g || key.b != clr.b) continue;
                    mat.SetInt(CarColorIndexId, i);
                    found = true;
                    break;
                }

                if (found)
                {
                    mat.color = Color.white;
                }
                else
                {
                    mat.color = clr;
                }
            }
            else
            {
                mat.color = clr;
            }

            if (diffuse != null) mat.SetTexture(MainTexId, diffuse.Texture);
            if (mask != null) mat.SetTexture(MaskTexId, mask.Texture);

            mat.SetFloat(MetallicId, src.Specular);
            mat.SetFloat(SmoothnessId, src.Smoothness);

            return mat;
        }

        private static Mesh Convert(RenderWareStream.Geometry src)
        {
            var mesh = new Mesh();

            // ReSharper disable ConvertClosureToMethodGroup
            var meshVertices = src.Vertices;//Utilities.F.ConvertArray( src.Vertices, x => Types.Convert(x));
            mesh.vertices = meshVertices;

            if (src.Normals != null)
            {
                mesh.normals = src.Normals;
            }

            if (src.Colours != null)
            {
                mesh.colors32 = src.Colours;
            }

            if (src.TexCoords != null && src.TexCoords.Length > 0)
            {
                mesh.uv = src.TexCoords[0];
            }
            // ReSharper restore ConvertClosureToMethodGroup

            if (src.Normals == null)
            {
                mesh.normals = CalculateNormals(src, meshVertices);
            }

            mesh.subMeshCount = src.MaterialSplits.Length;

            var isTriangleStrip = (src.Flags & GeometryFlag.TriangleStrips) == GeometryFlag.TriangleStrips;

            var subMesh = 0;
            foreach (var split in src.MaterialSplits)
            {
                var indices = isTriangleStrip
                    ? FromTriangleStrip(split.FaceIndices)
                    : split.FaceIndices;
                mesh.SetIndices(indices, MeshTopology.Triangles, subMesh++);
            }

            mesh.RecalculateBounds();

            return mesh;
        }

        private static GeometryFrame Convert(RenderWareStream.Frame src, IEnumerable<Atomic> atomics)
        {
            var atomic = atomics.FirstOrDefault(x => x.FrameIndex == src.Index);

            return new GeometryFrame(src, atomic);
        }

        private static Transform[] Convert(HierarchyAnimation hAnim, Dictionary<GeometryFrame, Transform> transforms, IEnumerable<GeometryFrame> frames)
        {
            var dict = frames.Where(x => x.Source.HAnim != null)
                .ToDictionary(x => x.Source.HAnim.NodeId, x => transforms[x]);

            return hAnim.Nodes.Select(x => dict[x.NodeId]).ToArray();
        }

        public class GeometryFrame
        {
            public const string DefaultName = "unnamed";

            public readonly RenderWareStream.Frame Source;

            public readonly string Name;
            public readonly UnityEngine.Vector3 Position;
            public readonly UnityEngine.Quaternion Rotation;

            public readonly int ParentIndex;
            public readonly int GeometryIndex;

            public GeometryFrame(RenderWareStream.Frame src, RenderWareStream.Atomic atomic)
            {
                Source = src;

                Name = src.Name != null ? src.Name.Value : DefaultName;
                ParentIndex = src.ParentIndex;
                GeometryIndex = atomic == null ? -1 : (int)atomic.GeometryIndex;

                Position = Types.Convert(src.Position);
                Rotation = UnityEngine.Quaternion.LookRotation(Types.Convert(src.MatrixForward), Types.Convert(src.MatrixUp));
            }

            public override int GetHashCode()
            {
                return Source.Index;
            }
        }

        public class GeometryParts
        {
            private readonly CollisionFile _collisions;
			public CollisionFile Collisions { get { return _collisions; } }

            public readonly string Name;
            public readonly Geometry[] Geometry;
            public readonly GeometryFrame[] Frames;

            public GeometryParts(string name, Clump clump, TextureDictionary[] txds)
            {
                Name = name;

                Geometry = clump.GeometryList.Geometry
                    .Select(x => new Geometry(x, Convert(x), txds))
                    .ToArray();

                Frames = clump.FrameList.Frames
                    .Select(x => Convert(x, clump.Atomics))
                    .ToArray();

                _collisions = clump.Collision;
            }

            public void AttachCollisionModel(Transform destParent, bool forceConvex = false)
            {
				Profiler.BeginSample ("AttachCollisionModel");
                if (_collisions != null)
                {
                    CollisionModel.Load(_collisions, destParent, forceConvex);
                }
                else
                {
                    CollisionModel.Load(Name, destParent, forceConvex);
                }
				Profiler.EndSample ();
            }

            public FrameContainer AttachFrames(Transform destParent, MaterialFlags flags)
            {
                var transforms = Frames.ToDictionary(x => x, x =>
                {
                    var trans = new GameObject(x.Name).transform;
                    trans.localPosition = x.Position;
                    trans.localRotation = x.Rotation;
                    return trans;
                });

                var container = destParent.gameObject.AddComponent<FrameContainer>();

                foreach (var frame in Frames)
                {
                    if (frame.ParentIndex != -1)
                    {
                        transforms[frame].SetParent(transforms[Frames[frame.ParentIndex]], false);
                    }
                    else
                    {
                        transforms[frame].SetParent(destParent, false);
                    }

                    if (frame.GeometryIndex != -1)
                    {
                        var gobj = transforms[frame].gameObject;
                        var geometry = Geometry[frame.GeometryIndex];

                        HierarchyAnimation hAnim = null;
                        var parent = frame;
                        while ((hAnim = parent.Source.HAnim) == null || hAnim.NodeCount == 0)
                        {
                            if (parent.ParentIndex == -1)
                            {
                                hAnim = null;
                                break;
                            }
                            parent = Frames[parent.ParentIndex];
                        }

                        Renderer renderer;
                        if (hAnim != null)
                        {
                            var smr = gobj.AddComponent<SkinnedMeshRenderer>();

                            var bones = Convert(hAnim, transforms, Frames);

                            smr.rootBone = bones[0];
                            smr.bones = bones;

                            smr.sharedMesh = geometry.Mesh;

                            if (smr.sharedMesh != null)
                            {
                                smr.sharedMesh.bindposes = geometry.SkinToBoneMatrices
                                    .Select(x => x.transpose)
                                    .ToArray();
                            }

                            renderer = smr;
                        }
                        else
                        {
                            var mf = gobj.AddComponent<MeshFilter>();
                            mf.sharedMesh = geometry.Mesh;

                            renderer = gobj.AddComponent<MeshRenderer>();
                        }

                        renderer.sharedMaterials = geometry.GetMaterials(flags);

                        // filter these out for now
                        if (frame.Name.EndsWith("_vlo") ||
                            frame.Name.EndsWith("_dam"))
                        {
                            gobj.SetActive(false);
                        }
                    }
                }

                container.Initialize(Frames, transforms);

                return container;
            }
        }


		private static Utilities.AsyncLoader<string, GeometryParts> s_asyncLoader = new Utilities.AsyncLoader<string, GeometryParts> ();

		public static int NumGeometryPartsLoaded { get { return s_asyncLoader.GetNumObjectsLoaded (); } }


        public static GeometryParts Load(string modelName, params string[] texDictNames)
        {
            return Load(modelName, texDictNames.Select(x => TextureDictionary.Load(x)).ToArray());
        }

		public static void LoadAsync(string modelName, string[] texDictNames, System.Action<GeometryParts> onFinish)
		{
			// load each texture asyncly (or load them all at once ?)

			// copy array to local variable
			texDictNames = texDictNames.ToArray ();

			if (0 == texDictNames.Length)
			{
				LoadAsync( modelName, new TextureDictionary[0], onFinish );
				return;
			}


			var loadedTextDicts = new List<TextureDictionary> ();

			for (int i = 0; i < texDictNames.Length; i++)
			{
			//	bool isLast = i == texDictNames.Length - 1;

				TextureDictionary.LoadAsync (texDictNames [i], (texDict) => {
					
					loadedTextDicts.Add (texDict);

					if (loadedTextDicts.Count == texDictNames.Length)
					{
						// finished loading all tex dicts
						LoadAsync (modelName, loadedTextDicts.ToArray (), onFinish);
					}
				});
			}

		}

        public static GeometryParts Load(string modelName, params TextureDictionary[] txds)
        {
            modelName = modelName.ToLower();

			if (s_asyncLoader.IsObjectLoaded(modelName))
            {
				return s_asyncLoader.GetLoadedObject (modelName);
            }

			Profiler.BeginSample ("ReadClump");
            var clump = ArchiveManager.ReadFile<Clump>(modelName + ".dff");
			Profiler.EndSample ();

            if (clump.GeometryList == null)
            {
                throw new Exception("Invalid mesh");
            }

			Profiler.BeginSample ("Create GeometryParts");
            var loaded = new GeometryParts(modelName, clump, txds);
			Profiler.EndSample ();

			s_asyncLoader.AddToLoadedObjects (modelName, loaded);

            return loaded;
        }

		public static void LoadAsync(string modelName, TextureDictionary[] txds, System.Action<GeometryParts> onFinish)
		{
			modelName = modelName.ToLower();

			if (!s_asyncLoader.TryLoadObject (modelName, onFinish))
			{
				// callback is either called or registered
				return;
			}
			

			GeometryParts loadedGeoms = null;

			LoadingThread.RegisterJob (new LoadingThread.Job<Clump> () {
				action = () => {
					// read archive file in background thread
					var clump = ArchiveManager.ReadFile<Clump>(modelName + ".dff");
					return clump;
				},
				callbackSuccess = (Clump clump) => {
					if (clump.GeometryList == null)
					{
						throw new Exception("Invalid mesh");
					}

					// create geometry parts in main thread
					loadedGeoms = new GeometryParts(modelName, clump, txds);

				},
				callbackFinish = (result) => {
					s_asyncLoader.OnObjectFinishedLoading( modelName, loadedGeoms, loadedGeoms != null );
				}
			});

		}

        public readonly Mesh Mesh;

        private readonly RenderWareStream.Geometry _geom;
        public readonly TextureDictionary[] _textureDictionaries;
        private readonly Dictionary<MaterialFlags, UnityEngine.Material[]> _materials;

        public readonly UnityEngine.Matrix4x4[] SkinToBoneMatrices;

        private Geometry(RenderWareStream.Geometry geom, Mesh mesh, TextureDictionary[] textureDictionaries)
        {
            Mesh = mesh;

            if (geom.Skinning != null)
            {
                Mesh.boneWeights = Types.Convert(geom.Skinning.VertexBoneIndices, geom.Skinning.VertexBoneWeights);
                SkinToBoneMatrices = Types.Convert(geom.Skinning.SkinToBoneMatrices);
            }

            _geom = geom;
            _textureDictionaries = textureDictionaries;
            _materials = new Dictionary<MaterialFlags, UnityEngine.Material[]>();
        }

        public UnityEngine.Material[] GetMaterials(ObjectFlag flags)
        {
            return GetMaterials(flags, x => { });
        }

        public UnityEngine.Material[] GetMaterials(ObjectFlag flags,
            Action<UnityEngine.Material> setupMaterial)
        {
            var matFlags = MaterialFlags.Default | MaterialFlags.OverrideAlpha;

            if ((flags & ObjectFlag.NoBackCull) == ObjectFlag.NoBackCull)
            {
                matFlags |= MaterialFlags.NoBackCull;
            }

            if ((flags & (ObjectFlag.Alpha1 | ObjectFlag.Alpha2)) != 0
                && (flags & ObjectFlag.DisableShadowMesh) == ObjectFlag.DisableShadowMesh)
            {
                matFlags |= MaterialFlags.Alpha;
            }

            return GetMaterials(matFlags, setupMaterial);
        }

        public UnityEngine.Material[] GetMaterials(MaterialFlags flags)
        {
            return GetMaterials(flags, x => { });
        }

        public UnityEngine.Material[] GetMaterials(MaterialFlags flags,
            Action<UnityEngine.Material> setupMaterial)
        {
            if (_materials.ContainsKey(flags))
            {
                return _materials[flags];
            }

            var mats = _geom.Materials.Select(x =>
            {
                var mat = Convert(x, _textureDictionaries, flags);
                setupMaterial(mat);
                return mat;
            }).ToArray();

            mats = _geom.MaterialSplits.Select(x => mats[x.MaterialIndex]).ToArray();

            _materials.Add(flags, mats);

            return mats;
        }
    }
}