using System;
using System.Collections.Generic;
using Assets.Scripts.Gameplay;
using Assets.Scripts.TerrainAi;
using UnityEngine;
using Random = UnityEngine.Random;

namespace Assets.Scripts.Terrain
{
    public class TerrainGenerator : MonoBehaviour
    {
        public static TerrainGenerator Instance;
        [SerializeField] public Noise PerlinNoise;

        [Header("Terrain Display")]
        public int Width;
        public int Length;
        public float MountainModifer;

        public GameObject Player;
        public GameObject Camera;
        public Color[] ColorType;       //This has to be four or else. Too tired to be bothered protecting this. Just leave it at 4
        private Material _terrainMaterial;

        private MeshFilter _filter;
        private MeshRenderer _renderer;
        private MeshCollider _collider;

        private PolyData _meshData;

        private VisibleRoad _road;
        public Vector2 _position;
        private Vector3 _cameraPosition;
        private List<Vector3> _navTargets;
        private List<GameObject> _roadMarkers;
        private List<VehicleData> _vehicles;

        private bool _ready;            //This is a cheat
        private bool _redraw;
        private bool _lockMovement;
        private bool _regen2;
        private bool _redrawRoad;
        private IntVector2 _cameraPos;

        private GameObject _areaExit;

        void Awake()
        {
            Instance = this;

            _areaExit = new GameObject("Bounds");
            _areaExit.transform.parent = transform;
            var boundCollider = _areaExit.gameObject.AddComponent<BoxCollider>();
            boundCollider.center = transform.position + new Vector3((Width * PerlinNoise.HorizontalSpacing) /2f, 20f, (Length * PerlinNoise.HorizontalSpacing) /2f);
            boundCollider.size = new Vector3((Width - 5) * PerlinNoise.HorizontalSpacing, 150f, (Length - 5) * PerlinNoise.VerticalSpacing);
            boundCollider.isTrigger = true;

            _meshData = new PolyData()
            {
                Terrain = new PolyMeshData[Width, Length],
                Road = new PolyMeshData[Width, Length]
            };

            _filter = gameObject.AddComponent<MeshFilter>();
            _renderer = gameObject.AddComponent<MeshRenderer>();
            _collider = gameObject.AddComponent<MeshCollider>();

            _vehicles = new List<VehicleData>();

            if (PerlinNoise.Seed == 0) PerlinNoise.Seed = (int)DateTime.UtcNow.Ticks;
            Random.InitState(PerlinNoise.Seed);
            PerlinNoise.Offset = new Vector2(Random.Range(-100000, 100000), Random.Range(-100000, 1000000));

            if (MountainModifer <= 0) MountainModifer = Random.Range(0f, 1f); 
            if (Width == 0) Width = 40;
            if (Length == 0) Length = 40;
            if (PerlinNoise.HorizontalSpacing <= 0) PerlinNoise.HorizontalSpacing = 1f;
            if (PerlinNoise.VerticalSpacing <= 0) PerlinNoise.VerticalSpacing = 1f;
            _position = Vector2.zero;
            _road = gameObject.AddComponent<VisibleRoad>();

            _terrainMaterial = CreateMaterial();
        }

        private Material CreateMaterial()
        {
            var size = 25;
            //grass, sand, rock, road
            var colours = new Color[(size * 2) * (size * 2)];
            //var colourType = new[] { new Color(0.4f, 0.35f, 0.2f), new Color(0.6f, 0.2f, 0.1f), new Color(0.7f, 0.3f, 0.1f), Color.black };   //base color
            //todo: Highlight speckled
            for (var y = 0; y < size * 2; ++y)
            {
                for (var x = 0; x < size * 2; ++x)
                {
                    var type = 0;
                    if (x > size) type = 1;
                    if (y > size) type += 2;
                    colours[y * (size * 2) + x] = ColorType[type];
                }
            }

            var texture = new Texture2D(size * 2, size * 2, TextureFormat.ARGB32, false) { filterMode = FilterMode.Point };
            texture.SetPixels(colours);
            texture.Apply();

            var material = new Material(Shader.Find("Standard")) { mainTexture = texture };
            //material.EnableKeyword("_NORMALMAP");
            //material.SetTexture("_BumpMap", normalMap);

            return material;
        }

        void Start ()
        {
            _roadMarkers = new List<GameObject>();
            GenerateTerrain();
        }

        void OnValidate()
        {
            if (!_ready) return;
            _redraw = true;
        }

        void ShowPolyMesh(PolyData data)
        {
            for (var i = 0; i < _vehicles.Count; ++i)
            {
                Destroy(_vehicles[i].Obj);
            }
            _vehicles.Clear();

            var vertices = new List<Vector3>();
            var tris = new List<int>();
            var uv = new List<Vector2>();


            DrawTerrain(vertices, tris, uv, data.Terrain);
            DrawRoad(vertices, tris, uv, data.Road);

            var mesh = new Mesh
            {
                vertices = vertices.ToArray(),
                triangles = tris.ToArray(),
                uv = uv.ToArray()
            };

            mesh.RecalculateNormals();
            mesh.RecalculateTangents();

            _filter.mesh = mesh;
            _renderer.material = _terrainMaterial;
            _collider.sharedMesh = mesh;

            SpawnVehicles();

            _ready = true;
        }

        void DrawTerrain(List<Vector3> vertices, List<int> tris, List<Vector2> uv, PolyMeshData[,] terrain)
        {
            var offset = Vector3.zero;
            for (var y = 0; y < Length; ++y)
            {
                for (var x = 0; x < Width; ++x)
                {
                    offset = Vector3.zero;

                    if (terrain[x, y].Type == 4)
                        offset = new Vector3(0, -1.5f, 0f);
                    
                    var index = vertices.Count;

                    var bottomLeft = terrain[x, y].BottomLeft;
                    var topLeft = terrain[x, y].TopLeft;
                    var topRight = terrain[x, y].TopRight;
                    var bottomRight = terrain[x, y].BottomRight;

                    if (x > 0 && y > 0)
                        bottomLeft = terrain[x - 1, y - 1].TopRight;

                    if (x > 0)
                        topLeft = terrain[x - 1, y].TopRight;

                    if (y > 0)
                        bottomRight = terrain[x, y - 1].TopRight;

                    vertices.Add(bottomLeft + offset);
                    vertices.Add(topLeft + offset);
                    vertices.Add(topRight + offset);
                    vertices.Add(bottomRight + offset);

                    tris.Add(index + 0);
                    tris.Add(index + 1);
                    tris.Add(index + 2);

                    tris.Add(index + 0);
                    tris.Add(index + 2);
                    tris.Add(index + 3);

                    uv.Add(terrain[x, y].blUv);
                    uv.Add(terrain[x, y].tlUv);
                    uv.Add(terrain[x, y].trUv);
                    uv.Add(terrain[x, y].brUv);
                }
            }
        }

        void DrawRoad(List<Vector3> vertices, List<int> tris, List<Vector2> uv, PolyMeshData[,] road)
        {
            _navTargets.Clear();
            var offset = new Vector3(0, 0.05f, 0f);

            for (var y = 0; y < Length; ++y)
            {
                for (var x = 0; x < Width; ++x)
                {
                    if (road[x, y] == null) continue;

                    var index = vertices.Count;

                    var bottomLeft = road[x, y].BottomLeft;
                    var bottomRight = (x + 1 < Width && road[x + 1, y] != null) ? road[x + 1, y].BottomLeft : road[x, y].BottomRight;
                    var topRight = road[x, y].TopRight;
                    if (x + 1 < Width && y + 1 < Length && road[x + 1, y + 1] != null)
                        topRight = road[x + 1, y + 1].BottomLeft;
                    else
                    {
                        if (x + 1 < Width && road[x + 1, y] != null) topRight = road[x + 1, y].TopLeft;
                        if (y + 1 < Length && road[x, y + 1] != null) topRight = road[x, y + 1].BottomRight;
                    }

                    var topLeft = road[x, y].TopLeft;
                    if (y + 1 < Length && road[x, y + 1] != null)
                        topLeft = road[x, y + 1].BottomLeft;
                    else if (x - 1 > Width && y + 1 < Length && road[x - 1, y + 1] != null)
                        topLeft = road[x - 1, y + 1].BottomRight;

                    if (road[x, y].Center)
                    {
                        var centerPoint = ((bottomLeft + offset) * 0.25f);
                        centerPoint += ((topLeft + offset) * 0.25f);
                        centerPoint += ((topRight + offset) * 0.25f);
                        centerPoint += ((bottomRight + offset) * 0.25f);
                        road[x, y].CenterPosition = centerPoint;

                        _navTargets.Add(centerPoint);
                        if (_roadMarkers.Count <= _navTargets.Count)
                        {
                            var go = GameObject.CreatePrimitive(PrimitiveType.Cube);
                            go.transform.localScale = new Vector3(0.5f, 0.25f, 1f);
                            go.transform.position = centerPoint;
                            if (_navTargets.Count > 1) go.transform.LookAt(_navTargets[_navTargets.Count - 2]);
                            _roadMarkers.Add(go);
                            go.transform.parent = transform;
                        }
                        else
                        {
                            var go = _roadMarkers[_navTargets.Count - 1];
                            go.transform.position = centerPoint;
                            go.transform.parent = transform;
                            if (_navTargets.Count > 1) go.transform.LookAt(_navTargets[_navTargets.Count - 2]);
                        }

                        //todo: find me for car ai spawner
                        //Also this shouldnt be here.
                        //This is stupid and shit. Better off to find all points in front. We got an hour and a half left, so this. all this.
                        for (var w = 0; w < _vehicles.Count - 1; ++w)
                        {
                            _vehicles[w].AiPoints.Add(centerPoint + new Vector3(Random.Range(-2, 2), 2, Random.Range(-2, 2)));

                        }

                        if (y > 15)
                        {
                            //spawn vehicle
                            if (Random.Range(0, 100) > 75)
                            {
                                _vehicles.Add(new VehicleData()
                                {
                                    Position = centerPoint + new Vector3(Random.Range(-50, 50), 5, Random.Range(-50, 50)),       //+ random
                                    AiPoints = new List<Vector3>()
                                });
                            }
                        }
                    }


                    vertices.Add(bottomLeft + offset);
                    vertices.Add(topLeft + offset);
                    vertices.Add(topRight + offset);
                    vertices.Add(bottomRight + offset);

                    tris.Add(index + 0);
                    tris.Add(index + 1);
                    tris.Add(index + 2);

                    tris.Add(index + 0);
                    tris.Add(index + 2);
                    tris.Add(index + 3);

                    uv.Add(road[x, y].blUv);
                    uv.Add(road[x, y].tlUv);
                    uv.Add(road[x, y].trUv);
                    uv.Add(road[x, y].brUv);

                    if (y > 0)
                    {
                        if (road[x, y - 1] == null)
                        {
                            if (x + 1 < Width && road[x + 1, y - 1] != null)
                            {
                                var mergeTop= road[x, y].TopRight;
                                if (road[x + 1, y] != null) mergeTop = road[x + 1, y].BottomLeft;
                                CreateMergingTri(vertices, tris, uv, mergeTop, road[x + 1, y - 1].BottomLeft, road[x, y].BottomLeft);
                            }
                            else if (x - 1 > 0 && road[x - 1, y - 1] != null)
                            {
                                CreateMergingTri(vertices, tris, uv,
                                    road[x, y].BottomRight,
                                    road[x - 1, y - 1].BottomRight,
                                    road[x, y].BottomLeft
                                );
                            }
                        }
                    }

                    if (y + 1 < Length)
                    {
                        if (road[x, y + 1] == null)
                        {
                            if (x + 1 < Width && road[x + 1, y + 1] != null)
                            {
                                var mergeTopLeft = road[x + 1, y + 1].TopLeft;
                                if (x + 1 < Width && y + 2 < Length && road[x + 1, y + 2] != null) mergeTopLeft = road[x + 1, y + 2].BottomLeft;

                                CreateMergingTri(vertices, tris, uv,
                                    mergeTopLeft,
                                    road[x + 1, y + 1].BottomLeft,
                                    road[x, y].TopLeft
                                );

                            }
                            else if (x - 1 > 0 && road[x - 1, y + 1] != null)
                            {
                                CreateMergingTri(vertices, tris, uv,
                                    road[x, y].TopRight,
                                    road[x - 1, y + 1].BottomRight,
                                    road[x - 1, y + 1].TopRight
                                );
                            }
                        }
                    }
                }
            }
        }

        void CreateMergingTri(List<Vector3> vertices, List<int> tris, List<Vector2> uv, Vector3 topLeft, Vector3 bottomLeft, Vector3 left)
        {
            var offset = new Vector3(0, 0.05f, 0f);

            var index = vertices.Count;
            vertices.Add(left + offset);
            vertices.Add(topLeft + offset);
            vertices.Add(bottomLeft + offset);

            tris.Add(index + 0);
            tris.Add(index + 1);
            tris.Add(index + 2);

            uv.Add(new Vector2(0.55f, 0.95f));
            uv.Add(new Vector2(0.95f, 0.95f));
            uv.Add(new Vector2(0.95f, 0.55f));
        }
        

        void Update ()
        {
            if (Input.GetKeyDown(KeyCode.P))
            {
                PerlinNoise.Seed = (int) DateTime.UtcNow.Ticks;
                GenerateTerrain();
                return;
            }

            if (!_redraw) return;
            _navTargets = new List<Vector3>();
            Random.InitState(PerlinNoise.Seed);
            PerlinNoise.Offset = new Vector2(Random.Range(-100000, 100000), Random.Range(-100000, 1000000));
            ShowPolyMesh(_meshData);
            _redraw = false;
        }

        void GenerateTerrain()
        {
            _navTargets = new List<Vector3>();
            Random.InitState(PerlinNoise.Seed);
            PerlinNoise.Offset = new Vector2(Random.Range(-100000, 100000), Random.Range(-100000, 1000000));
            _road.CreatePolyTerrain(_meshData, Width, Length, _position, PerlinNoise, _navTargets, MountainModifer);
            ShowPolyMesh(_meshData);
            FindCameraPosition(_meshData);
        }

        void ShiftMapData()
        {
            var height = Length / 2;
            var lengthOffset = height * PerlinNoise.VerticalSpacing;

            for (var y = 0; y < height; ++y)
            {
                for (var x = 0; x < Width; ++x)
                {
                    _meshData.Terrain[x, y] = ModifyPoints(_meshData.Terrain[x, y + height], lengthOffset);

                    //Roads
                    if (_meshData.Road[x, y + height] == null)
                    {
                        _meshData.Road[x, y] = null;
                        continue;
                    }
                    _meshData.Road[x, y] = ModifyPoints(_meshData.Road[x, y + height], lengthOffset);
                    _meshData.Road[x, y + height] = null;
                }
            }

            PerlinNoise.Offset += new Vector2(0, height);
        }

        PolyMeshData ModifyPoints(PolyMeshData cell, float lengthOffset)
        {
            var offset = new Vector3(0, 0, -lengthOffset);
            var result = new PolyMeshData()
            {
                BottomLeft = cell.BottomLeft + offset,
                BottomRight = cell.BottomRight + offset,
                TopLeft = cell.TopLeft + offset,
                TopRight = cell.TopRight + offset,
                CenterPosition = cell.CenterPosition + offset,
                Height = cell.Height,
                Type = cell.Type,
                Center = cell.Center,
                blUv = cell.blUv,
                brUv = cell.brUv,
                tlUv = cell.tlUv,
                trUv =  cell.trUv,
                brIndex = cell.brIndex,
                trIndex = cell.trIndex,
                tlIndex = cell.tlIndex,
                blIndex = cell.blIndex,
            };

            return result;
        }

        void UpdateCamera()
        {
            if (_lockMovement) return;

            //todo: store this
            var distance = float.MaxValue;
            var markerId = -1;
            for (var i = 0; i < _navTargets.Count; ++i)
            {
                var markerDistance = Vector3.Distance(Camera.transform.position, _navTargets[i]);
                if (markerDistance >= distance) continue;
                markerId = i;
                distance = markerDistance;
            }

            if (markerId == -1) return;
            if (markerId + 1 >= _navTargets.Count) return;
            Camera.transform.position =  Vector3.MoveTowards(Camera.transform.position, 
                _navTargets[markerId + 1] + new Vector3(0, 1.5f, 0), 
                Time.deltaTime * 100f);

            //Camera.transform.LookAt(_navTargets[markerId + 1] + new Vector3(0, 1.5f, 0));
        }

        public void RegenMap()
        {
            PerlinNoise.Seed = (int)DateTime.UtcNow.Ticks;
            GenerateTerrain();
        }

        public void CheckCarPosition(Vector3 carPos, float speed)
        {
            var maxDistance = float.MaxValue;
            for (var y = 0; y < Length; ++y)
            {
                for (var x = 0; x < Length; ++x)
                {
                    if (_meshData.Road[x, y] == null) continue;
                    var distance = Vector3.Distance(_meshData.Road[x, y].CenterPosition, carPos);
                    if (distance < maxDistance) maxDistance = distance;
                }
            }

            if (maxDistance > 100 && speed < 10f) RegenMap();
        }

        void CheckCameraPosition()
        {
            return;
            var camDistance = Vector3.Distance(Camera.transform.position, _cameraPosition);
            if (camDistance < 3f) return;
            _cameraPosition = Camera.transform.position;

            var found = false;
            _cameraPos = new IntVector2() { X = -1, Y = -1 };
            var maxDistance = float.MaxValue;
            for (var y = 0; y < Length; ++y)
            {
                for (var x = 0; x < Length; ++x)
                {
                    var distance = Vector3.Distance(Camera.transform.position, _meshData.Terrain[x, y].CenterPosition);
                    if (distance > maxDistance) continue;
                    maxDistance = distance;
                    _cameraPos = new IntVector2() { X = x, Y = y };
                    found = true;
                }
            }

            if (!found) return;
            if (_cameraPos.Y > 50)
            {
                _lockMovement = true;
            }
        }

        void SpawnVehicles()
        {
            for (var i = 0; i < _vehicles.Count; ++i)
            {
                _vehicles[i].Obj = AiControllers.Instance.GetCar(_vehicles[i].Position, _vehicles[i].AiPoints, i);
            }

            var waypoints = AiControllers.Instance.CreateWaypoints(_vehicles[0].AiPoints);
            for (var i = 0; i < _vehicles.Count; ++i)
            {
                var script = _vehicles[i].Obj.GetComponent<AIWaypointFollower>();
                script.waypoints = waypoints;
                //_vehicles[i].Obj = AiControllers.Instance.GetCar(_vehicles[i].Position, _vehicles[i].AiPoints, i);
            }
        }

        void FindCameraPosition(PolyData data)
        {
            var start = -1;
            var end = 0;
            var height = 0f;
            var count = 0;
            var startY = 7;
            for (var x = 0; x < Width; ++x)
            {
                if (data.Road[x, startY] == null) continue;
                height += data.Road[x, startY].Height;
                if (start == -1) start = x;
                end = x;
                count++;
            }

            height /= count;
            var spawnPosition = new Vector3(start * PerlinNoise.HorizontalSpacing + (((end - start) / 2f) * PerlinNoise.HorizontalSpacing), height + 4f, 3 * PerlinNoise.HorizontalSpacing + 2f);
            Player.transform.position = spawnPosition;
            Player.transform.eulerAngles = new Vector3(0, 0, 0);

            Camera.transform.position = spawnPosition + new Vector3(0, 5f, -5f);
        }

        public class VehicleData
        {
            public GameObject Obj;
            public Vector3 Position;
            public List<Vector3> AiPoints;
        }
    }
}
