﻿/*         INFINITY CODE         */
/*   https://infinity-code.com   */

using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

namespace InfinityCode.RealWorldTerrain
{
    /// <summary>
    /// This class contains basic information about the building.
    /// </summary>
    [AddComponentMenu("")]
    [RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
    public class RealWorldTerrainBuilding : MonoBehaviour
    {
        /// <summary>
        /// The height of the walls.
        /// </summary>
        public float baseHeight;

        /// <summary>
        /// Array of base vertices.
        /// </summary>
        public Vector3[] baseVertices;

        /// <summary>
        /// Reference to RealWorldTerrainContainer instance.
        /// </summary>
        public RealWorldTerrainContainer container;

        /// <summary>
        /// ID of the building
        /// </summary>
        public string id;

        /// <summary>
        /// Indicates that roof normals is inverted.
        /// </summary>
        public bool invertRoof;

        /// <summary>
        /// Indicates that walls normals is inverted.
        /// </summary>
        public bool invertWall;

        /// <summary>
        /// Height of roof.
        /// </summary>
        public float roofHeight;

        /// <summary>
        /// Type of roof.
        /// </summary>
        public RealWorldTerrainRoofType roofType;

        /// <summary>
        /// Whether to generate the wall?
        /// </summary>
        public bool generateWall;

        /// <summary>
        /// Material of the roof.
        /// </summary>
        public Material roofMaterial;
        
        public Vector2 roofUVScale = Vector2.one;

        public float startHeight = 0;

        /// <summary>
        /// Size of a tile texture in meters.
        /// </summary>
        public Vector2 tileSize = new Vector2(30, 30);

        public Vector2 uvOffset = Vector2.zero;

        /// <summary>
        /// Material of the wall.
        /// </summary>
        public Material wallMaterial;

        private MeshFilter _meshFilter;

        /// <summary>
        /// Reference to MeshFilter of the building.
        /// </summary>
        public MeshFilter meshFilter
        {
            get
            {
                if (_meshFilter == null) _meshFilter = GetComponent<MeshFilter>();
                return _meshFilter;
            }
        }

        /// <summary>
        /// Reference to MeshFilter of roof.
        /// </summary>
        [Obsolete("Use meshFilter instead.")]
        public MeshFilter roof
        {
            get { return meshFilter; }
        }

        /// <summary>
        /// Reference to MeshFilter of wall.
        /// </summary>
        [Obsolete("Use meshFilter instead.")]
        public MeshFilter wall
        {
            get { return meshFilter; }
        }

        private void CreateRoofDome(List<Vector3> vertices, List<int> triangles)
        {
            Vector3 roofTopPoint = Vector3.zero;
            roofTopPoint = vertices.Aggregate(roofTopPoint, (current, point) => current + point) / vertices.Count;
            roofTopPoint.y = (baseHeight + roofHeight) * container.scale.y;
            int vIndex = vertices.Count;

            for (int i = 0; i < vertices.Count; i++)
            {
                int p1 = i;
                int p2 = i + 1;
                if (p2 >= vertices.Count) p2 -= vertices.Count;

                triangles.AddRange(new[] { p1, p2, vIndex });
            }

            vertices.Add(roofTopPoint);
        }

        private void CreateRoofMesh(List<Vector3> vertices, out List<Vector2> uv, out List<int> triangles)
        {
            List<Vector2> roofPoints = CreateRoofVertices(vertices);
            triangles = CreateRoofTriangles(vertices, roofPoints);

            if (invertRoof) triangles.Reverse();
            
            float minX = float.MaxValue;
            float minZ = float.MaxValue;
            float maxX = float.MinValue;
            float maxZ = float.MinValue;
            
            float maxDistance = 0;
            int maxDistanceIndex = -1;
            int nextIndex = -1;
            
            for (int i = 0; i < vertices.Count; i++)
            {
                Vector3 v = vertices[i];
                if (v.x < minX) minX = v.x;
                if (v.z < minZ) minZ = v.z;
                if (v.x > maxX) maxX = v.x;
                if (v.z > maxZ) maxZ = v.z;
                
                nextIndex = i + 1;
                if (nextIndex >= vertices.Count) nextIndex = 0;
                
                float distance = (v - vertices[nextIndex]).sqrMagnitude;
                if (distance > maxDistance)
                {
                    maxDistance = distance;
                    maxDistanceIndex = i;
                }
            }
            
            nextIndex = maxDistanceIndex + 1;
            if (nextIndex >= vertices.Count) nextIndex = 0;
            
            Vector3 v1 = vertices[maxDistanceIndex];
            Vector3 v2 = vertices[nextIndex];
            float angle = 360 - RealWorldTerrainUtils.Angle2D(v1, v2);
            
            float centerX = (minX + maxX) / 2;
            float centerZ = (minZ + maxZ) / 2;
            
            Matrix4x4 matrix = Matrix4x4.TRS(new Vector3(centerX, 0, centerZ), Quaternion.Euler(0, -angle, 0), Vector3.one);
            Bounds bounds = new Bounds();
            foreach (Vector3 v in vertices)
            {
                Vector3 v1t = matrix.MultiplyPoint(v);
                bounds.Encapsulate(v1t);
            }

            Vector2 roofScale = roofUVScale;
            if (roofScale.x == 0) roofScale.x = 0.001f;
            if (roofScale.y == 0) roofScale.y = 0.001f;
            
            uv = new List<Vector2>();
            foreach (Vector3 v in vertices)
            {
                Vector3 v1t = matrix.MultiplyPoint(v);
                Vector2 uv1 = new Vector2(v1t.x / roofUVScale.x, v1t.z / roofUVScale.y);
                uv.Add(uv1);
            }
        }

        private List<int> CreateRoofTriangles(List<Vector3> vertices, List<Vector2> roofPoints)
        {
            List<int> triangles = new List<int>();
            if (roofType == RealWorldTerrainRoofType.flat)
            {
                int[] trs = RealWorldTerrainTriangulator.Triangulate(roofPoints);
                if (trs != null) triangles.AddRange(trs);
            }
            else if (roofType == RealWorldTerrainRoofType.dome)
            {
                CreateRoofDome(vertices, triangles);
            }
            return triangles;
        }

        private List<Vector2> CreateRoofVertices(List<Vector3> vertices)
        {
            Vector3[] targetVertices = new Vector3[baseVertices.Length];
            Array.Copy(baseVertices, targetVertices, baseVertices.Length);

            if (container.prefs.buildingBottomMode == RealWorldTerrainBuildingBottomMode.followTerrain)
            {
                Vector3 tp = transform.position;
                RealWorldTerrainItem terrainItem = container.GetItemByWorldPosition(baseVertices[0] + tp);
                if (terrainItem != null)
                {
                    TerrainData t = terrainItem.terrainData;

                    Vector3 offset = tp - terrainItem.transform.position;

                    for (int i = 0; i < targetVertices.Length; i++)
                    {
                        Vector3 v = targetVertices[i];
                        Vector3 localPos = offset + v;
                        float y = t.GetInterpolatedHeight(localPos.x / t.size.x, localPos.z / t.size.z);
                        v.y = terrainItem.transform.position.y + y - tp.y;
                        targetVertices[i] = v;
                    }
                }
            }

            List<Vector2> roofPoints = new List<Vector2>();
            float topPoint = targetVertices.Max(v => v.y) + baseHeight * container.scale.y;
            foreach (Vector3 p in targetVertices)
            {
                Vector3 tv = new Vector3(p.x, topPoint, p.z);
                Vector2 rp = new Vector2(p.x, p.z);

                vertices.Add(tv);
                roofPoints.Add(rp);
            }

            return roofPoints;
        }

        private void CreateWallMesh(List<Vector3> vertices, List<Vector2> uv, out List<int> triangles)
        {
            List<Vector3> wv = new List<Vector3>();
            List<Vector2> wuv = new List<Vector2>();
            bool reversed = CreateWallVertices(wv, wuv);
            if (invertWall) reversed = !reversed;
            triangles = CreateWallTriangles(wv, vertices.Count, reversed);
            vertices.AddRange(wv);
            uv.AddRange(wuv);
        }

        private List<int> CreateWallTriangles(List<Vector3> vertices, int offset, bool reversed)
        {
            List<int> triangles = new List<int>();
            for (int i = 0; i < vertices.Count / 4; i++)
            {
                int p1 = i * 4;
                int p2 = i * 4 + 2;
                int p3 = i * 4 + 3;
                int p4 = i * 4 + 1;

                if (p2 >= vertices.Count) p2 -= vertices.Count;
                if (p3 >= vertices.Count) p3 -= vertices.Count;

                p1 += offset;
                p2 += offset;
                p3 += offset;
                p4 += offset;

                if (reversed)
                {
                    triangles.AddRange(new[] { p1, p4, p3, p1, p3, p2 });
                }
                else
                {
                    triangles.AddRange(new[] { p2, p3, p1, p3, p4, p1 });
                }
            }
            return triangles;
        }

        private bool CreateWallVertices(List<Vector3> vertices, List<Vector2> uv)
        {
            Vector3[] targetVertices = new Vector3[baseVertices.Length];
            Array.Copy(baseVertices, targetVertices, baseVertices.Length);

            if (container.prefs.buildingBottomMode == RealWorldTerrainBuildingBottomMode.followTerrain)
            {
                Vector3 tp = transform.position;
                RealWorldTerrainItem terrainItem = container.GetItemByWorldPosition(baseVertices[0] + tp);
                if (terrainItem != null)
                {
                    TerrainData t = terrainItem.terrainData;

                    Vector3 offset = tp - terrainItem.transform.position;

                    for (int i = 0; i < targetVertices.Length; i++)
                    {
                        Vector3 v = targetVertices[i];
                        Vector3 localPos = offset + v;
                        float y = t.GetInterpolatedHeight(localPos.x / t.size.x, localPos.z / t.size.z);
                        v.y = terrainItem.transform.position.y + y - tp.y;
                        targetVertices[i] = v;
                    }
                }
            }

            float topPoint = targetVertices.Max(v => v.y) + baseHeight * container.scale.y;

            float startY = startHeight * container.scale.y;
            float offsetY = startY < 0 ? startY : 0;

            for (int i = 0; i < targetVertices.Length; i++)
            {
                Vector3 p1 = targetVertices[i];
                Vector3 p2 = i < targetVertices.Length - 1 ? targetVertices[i + 1] : targetVertices[0];
                if (p1.y < startY) p1.y = startY;
                if (p2.y < startY) p2.y = startY;
                p1.y += offsetY;
                p2.y += offsetY;
                vertices.Add(p1);
                vertices.Add(new Vector3(p1.x, topPoint, p1.z));
                vertices.Add(p2);
                vertices.Add(new Vector3(p2.x, topPoint, p2.z));
            }

            float totalDistance = 0;
            float bottomPoint = float.MaxValue;

            for (int i = 0; i < vertices.Count / 4; i++)
            {
                int i1 = Mathf.RoundToInt(Mathf.Repeat(i * 4, vertices.Count));
                int i2 = Mathf.RoundToInt(Mathf.Repeat((i + 1) * 4, vertices.Count));
                Vector3 v1 = vertices[i1];
                Vector3 v2 = vertices[i2];
                v1.y = v2.y = 0;
                totalDistance += (v1 - v2).magnitude;
                if (bottomPoint > targetVertices[i].y) bottomPoint = targetVertices[i].y;
            }

            Vector3 lv1 = vertices[vertices.Count - 4];
            Vector3 lv2 = vertices[0];
            lv1.y = lv2.y = 0;
            totalDistance += (lv1 - lv2).magnitude;

            float currentDistance = 0;
            float nextU = 0;
            float uMul = totalDistance / tileSize.x;
            float vMax = topPoint / tileSize.y;
            float vMinMul = container.scale.y * tileSize.y;

            for (int i = 0; i < vertices.Count / 4; i++)
            {
                int i1 = Mathf.RoundToInt(Mathf.Repeat(i * 4, vertices.Count));
                int i2 = Mathf.RoundToInt(Mathf.Repeat((i + 1) * 4, vertices.Count));
                float curU = nextU;
                uv.Add(new Vector2(curU * uMul + uvOffset.x, (vertices[i * 4].y - bottomPoint) / vMinMul + uvOffset.y));
                uv.Add(new Vector2(curU * uMul + uvOffset.x, vMax + uvOffset.y));

                Vector3 v1 = vertices[i1];
                Vector3 v2 = vertices[i2];
                v1.y = v2.y = 0;
                currentDistance += (v1 - v2).magnitude;
                nextU = currentDistance / totalDistance;

                uv.Add(new Vector2(nextU * uMul + uvOffset.x, (vertices[i * 4 + 2].y - bottomPoint) / vMinMul + uvOffset.y));
                uv.Add(new Vector2(nextU * uMul + uvOffset.x, vMax + uvOffset.y)); 
            }

            int southIndex = -1;
            float southZ = float.MaxValue;

            for (int i = 0; i < targetVertices.Length; i++)
            {
                if (targetVertices[i].z < southZ)
                {
                    southZ = targetVertices[i].z;
                    southIndex = i;
                }
            }

            int prevIndex = southIndex - 1;
            if (prevIndex < 0) prevIndex = targetVertices.Length - 1;

            int nextIndex = southIndex + 1;
            if (nextIndex >= targetVertices.Length) nextIndex = 0;

            float angle1 = RealWorldTerrainUtils.Angle2D(targetVertices[southIndex], targetVertices[nextIndex]);
            float angle2 = RealWorldTerrainUtils.Angle2D(targetVertices[southIndex], targetVertices[prevIndex]);

            return angle1 < angle2;
        }

        public void Generate()
        {
            Mesh mesh;
            if (meshFilter.sharedMesh != null) mesh = meshFilter.sharedMesh;
            else
            {
                mesh = new Mesh();
                mesh.name = "Building " + id;
                mesh.subMeshCount = 2;
                meshFilter.sharedMesh = mesh;
            }

            List<Vector3> vertices = new List<Vector3>();
            List<Vector2> uv;
            List<int> roofTriangles;
            List<int> wallTriangles = null;

            CreateRoofMesh(vertices, out uv, out roofTriangles);
            if (generateWall) CreateWallMesh(vertices, uv, out wallTriangles);

            mesh.SetVertices(vertices);
            mesh.SetUVs(0, uv);
            mesh.SetTriangles(roofTriangles, 0);
            if (generateWall) mesh.SetTriangles(wallTriangles, 1);

            mesh.RecalculateNormals();
            mesh.RecalculateBounds();

            GetComponent<MeshRenderer>().materials = new[]
            {
                roofMaterial,
                wallMaterial,
            };
        }
    }
}