using System.Collections.Generic;
using System.Linq;
using Assets.AnnotationPass;
using Assets.Common;
using UnityEngine;

namespace Assets.RenderPass
{
    public class LandmassRenderer : IRenderer
    {
        public float UVScale { get; set; } = 1.0f;

        private List<Vector3> _vertices;
        private List<Vector2> _uv;
        private List<Vector3> _normals;
        private List<Color> _colors;
        private List<int> _triangles;
        private List<List<Point>> _edges;
        
        public void Render(Map map, Component component)
        {
            var points = map.Boundaries.Vertices;
            var locations = map.GetProperty<Graph<Location>>(LandmassPass.MapLocationsProperty);

            _vertices = new List<Vector3>();
            _normals = new List<Vector3>();
            _uv = new List<Vector2>();
            _colors = new List<Color>();
            _triangles = new List<int>();

            foreach (var location in locations.Vertices.Skip(1))
            {
                GenerateLocationEdges(location, points);
                GenerateLocationMeshAlt(location, points);
                GenerateLocationWall(location, points);
            }
            
            Mesh mesh = new Mesh();
            mesh.vertices = _vertices.ToArray();
            mesh.uv = _uv.ToArray();
            mesh.normals = _normals.ToArray();
            mesh.triangles = _triangles.ToArray();
            mesh.colors = _colors.ToArray();

            var mf = component.GetComponent<MeshFilter>();
            mf.sharedMesh = mesh;
        }

        private void GenerateLocationEdges(Location location, List<Point> points) 
        {
            foreach (var (a, b) in location.BoundaryEdges.Select(x => (points[x.Item1], points[x.Item2])))
                location.DetailedEdge.AddRange(this.SplitEdge(a, b, 3));
        }

        private IEnumerable<Point> SplitEdge(Point a, Point b, int count)
        {
            var magnitude = Point.Dist(a, b);
            var d = new Point(-(b.y - a.y) / magnitude, (b.x - a.x) / magnitude);

            magnitude *= Random.value * 0.2 - 0.1;
            
            Point c = new Point((a.x + b.x) / 2.0 + d.x * magnitude, (a.y + b.y) / 2.0 + d.y * magnitude);

            if (count > 0)
            {
                foreach (var p in SplitEdge(a, c, count - 1)) 
                    yield return p;
                
                yield return c;
                
                foreach (var p in SplitEdge(c, b, count - 1)) 
                    yield return p;
            }
            else
            {
                yield return c;
            }
        }

        private void GenerateLocationMeshAlt(Location location, IList<Point> points)
        {
            var start = _vertices.Count;
            var count = location.DetailedEdge.Count;
            
            _vertices.AddRange(location.DetailedEdge.Select(p => new Vector3((float)p.x, location.Type.height, (float)p.y)));
            _normals.AddRange(Enumerable.Repeat(Vector3.up, count));
            _colors.AddRange(Enumerable.Repeat(Color.blue, count));
            _uv.AddRange(location.DetailedEdge.Select(p => new Vector2((float)p.x, (float)p.y) / UVScale));
            
            var triangulator = new Triangulator(location.DetailedEdge.Select(v => new Vector2((float)v.x, (float)v.y)).ToArray());
            _triangles.AddRange(triangulator.Triangulate().Select(x => x + start));
        }

        private void GenerateLocationMesh(Location location, IList<Point> points)
        {
            foreach (var vertices in location.Sites.Select(site => site.Edges.Select(x => x.Item1).Reverse()))
            {
                int start = _vertices.Count;
                
                foreach (var i in vertices)
                {
                    var isEdge = location.BoundaryPoints.Contains(i);
                    
                    var vertex = points[i];
                    var v = PointToVector(location, vertex);
                    
                    _vertices.Add(v);
                    _normals.Add(Vector3.up);
                    _uv.Add(new Vector2(v.x, v.z) / UVScale);
                    _colors.Add(isEdge ? Color.red : Color.blue);
                }

                int end = _vertices.Count;
                
                var triangulator = new Triangulator(_vertices.Skip(start).Take(end - start).Select(v => new Vector2(v.x, v.z)).ToArray());
                _triangles.AddRange(triangulator.Triangulate().Select(x => x + start));

            }
        }

        private static Vector3 PointToVector(Location location, Point vertex)
        {
            return new Vector3((float)vertex.x, location.Type.height + Mathf.PerlinNoise((float)vertex.x, (float)vertex.y) / 4, (float)vertex.y);
        }

        private void GenerateLocationWall(Location location, IList<Point> points)
        {
            var length = 0.0;

            var collection = location.DetailedEdge.Append(location.DetailedEdge.First());
            var edges = collection.Zip(collection.Skip(1), (a, b) => (a, b)).Zip(collection.Skip(2), (tuple, c) => (tuple.a, tuple.b, c));

            foreach (var (p, c, n) in edges)
            {
                int start = _vertices.Count;
                var dist = Point.Dist(p, c);

                var veca = PointToVector(location, p);
                var vecb = PointToVector(location, c);
                
                _vertices.Add(veca);
                _vertices.Add(new Vector3((float)p.x, -10,(float)p.y));
                _vertices.Add(vecb);
                _vertices.Add(new Vector3((float)c.x, -10,(float)c.y));

                var normal = new Vector3((float)(p.x + n.x - c.x) / 2.0f, 0, (float)(p.y + n.y - c.y) / 2.0f).normalized;
                
                _normals.AddRange(Enumerable.Repeat(normal, 4));
                _colors.AddRange(Enumerable.Repeat(Color.red, 4));
                
                _triangles.AddRange(new []
                {
                    start, start + 2, start + 1,
                    start + 1, start + 2, start + 3
                });
                
                _uv.AddRange(new []
                {
                    new Vector2((float)length, veca.y) / UVScale, 
                    new Vector2((float)length, -10) / UVScale, 
                    new Vector2((float)(length + dist), vecb.y) / UVScale, 
                    new Vector2((float)(length + dist), -10) / UVScale, 
                });
                
                length += dist;
            }
        }

        public void DrawGizmos(Map map, Component component)
        {
            
        }
    }
}