﻿using FSO.SimAntics.Model.Routing;
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;

namespace FSO.Common.WorldGeometry
{
    /// <summary>
    /// Projects one mesh onto another mesh on a given axis, potentially with an offset from the surface.
    /// Example use case: Projecting a road onto a terrain mesh
    /// </summary>
    public class MeshProjector
    {
        public MeshProjector(IEnumerable<BaseMeshTriangle> baseMesh, IEnumerable<MeshTriangle> projMesh)
        {
            foreach (var tri in baseMesh) tri.GenBounds();
            foreach (var tri in projMesh) tri.GenBounds();

            BaseMesh = baseMesh;
            ProjectMesh = projMesh;

            BaseSet = BaseTriangleSet.RoughBalanced(baseMesh.ToList());
        }

        IEnumerable<BaseMeshTriangle> BaseMesh;
        BaseTriangleSet BaseSet;
        IEnumerable<MeshTriangle> ProjectMesh;

        public List<int> Indices;
        public List<MeshPoint> Vertices;

        public void Project()
        {
            Indices = new List<int>();
            Vertices = new List<MeshPoint>();
            //find list of potential intersect tris for a projtri
            //build clipping edges for projtri

            foreach (var projTri in ProjectMesh)
            {
                //find candidate baseTris
                var candidates = BaseSet.AllIntersect(projTri);
                foreach (var baseTri in candidates)
                {
                    //if (projTri.RoughIntersects(baseTri))
                    //{
                        ClipTriangles(baseTri, projTri, Vertices, Indices);
                    //}
                }
            }
        }

        private void ClipTriangles(BaseMeshTriangle baseTri, MeshTriangle projTri, List<MeshPoint> outverts, List<int> inds)
        {
            //Sutherland–Hodgman algorithm
            //clip a triangle against another by iteratively clipping each edge of the second one

            //we want to clip against base tri
            var outputList = new MeshPolygon(projTri);
            var basePlane = new Plane(baseTri.Vertices[0], baseTri.Vertices[1], baseTri.Vertices[2]);
            
            for (int i=0; i<3; i++)
            {
                if (outputList.Points.Count == 0) return;
                var inputList = outputList;
                var edge = new ClipEdge(baseTri.Vertices[i], baseTri.Vertices[(i + 1) % 3]);
                outputList = new MeshPolygon();
                var lastPoint = inputList.Points.Last();
                int j = inputList.Points.Count-1;
                foreach (var point in inputList.Points)
                {
                    if (!edge.ShouldClip(point.Position))
                    {
                        if (edge.ShouldClip(lastPoint.Position))
                        {
                            outputList.Points.Add(edge.IntersectLine(inputList, j));
                        }
                        //we still need to project the point onto the surface...
                        var ray = new Ray(point.Position, new Vector3(0, -1, 0));
                        var intersect2 = ray.Intersects(basePlane);
                        if (intersect2 == null) {
                            ray.Direction *= -1;
                            intersect2 = ray.Intersects(basePlane);
                            if (intersect2 == null) { }
                            intersect2 = -(intersect2 ?? 0f);
                        }
                        point.Position.Y -= intersect2.Value;
                        outputList.Points.Add(point);
                    } else
                    {
                        if (!edge.ShouldClip(lastPoint.Position))
                        {
                            outputList.Points.Add(edge.IntersectLine(inputList, j));
                        }
                    }
                    j = (j + 1) % inputList.Points.Count;
                    lastPoint = point;
                }
            }

            if (outputList.Points.Count < 3) return; //?

            outputList.Triangulate(outverts, inds);
        }
    }

    public class ClipEdge
    {
        Vector3 EdgeVec;
        Vector2 DotVec;
        Vector3 EdgePos;
        Vector2 EdgePos2;

        public ClipEdge(Vector3 from, Vector3 to)
        {
            //xz
            //we assume the triangle is winding clockwise, so points on the left should be clipped

            EdgeVec = to - from;
            EdgePos = from;
            EdgePos2 = new Vector2(from.X, from.Z);
            DotVec = new Vector2(-EdgeVec.Z, EdgeVec.X);
        }

        public bool ShouldClip(Vector3 pos)
        {
            return (Vector2.Dot(DotVec, new Vector2(pos.X, pos.Z) - EdgePos2) < 0);
        }

        public MeshPoint IntersectLine(MeshPolygon tri, int lineInd)
        {
            var points = tri.Points;
            var lineInd2 = (lineInd + 1) % points.Count;
            var pt1 = tri.Points[lineInd];
            var pt2 = tri.Points[lineInd2];

            Vector3 a = EdgeVec; //line 1
            Vector3 b = pt2.Position - pt1.Position; //line 2
            Vector3 c = EdgePos - pt1.Position; //vec between starts

            //percent of line 1 where we intersect with line 2
            float ip = 1 / (-b.X * a.Z + a.X * b.Z); //projection
            float t = (b.X * c.Z - b.Z * c.X) * ip;

            //percent of line 2 where we intersect line 1
            float ip2 = 1 / (-a.X * b.Z + b.X * a.Z);
            float s = (a.X * (-c.Z) - a.Z * (-c.X)) * ip2;

            //pos + vec * t = pos2 + vec2 * s
            //vec * t - vec2 * s = pos2 - pos1
            float[] newTC = new float[pt1.TexCoords.Length];
            float ms = 1 - s;
            for (int i=0; i<newTC.Length; i++)
            {
                newTC[i] = pt1.TexCoords[i] * ms + pt2.TexCoords[i] * s;
            }

            return new MeshPoint(
                //position from the clip triangle (use t)
                EdgePos + EdgeVec * t,
                //texcoords from the two points in the poly (use s)
                newTC
            );

        }
    }

    public class BaseMeshTriangle
    {
        public float x1;
        public float y1;
        public float x2;
        public float y2;

        public Vector3[] Vertices;

        public void GenBounds()
        {
            x1 = Vertices[0].X;
            y1 = Vertices[0].Z;
            x2 = x1;
            y2 = y1;
            for (int i=1; i<Vertices.Length; i++)
            {
                var v = Vertices[i];
                if (v.X < x1) x1 = v.X;
                if (v.Z < y1) y1 = v.Z;
                if (v.X > x2) x2 = v.X;
                if (v.Z > y2) y2 = v.Z;
            }
        }

        public bool RoughIntersects(BaseMeshTriangle other)
        {
            return !(x1 > other.x2 || x2 < other.x1 || y1 > other.y2 || y2 < other.y1);
        }
    }

    public class MeshTriangle : BaseMeshTriangle
    {
        public float[][] TexCoords;
    }

    public class MeshPoint
    {
        public Vector3 Position;
        public float[] TexCoords;

        public MeshPoint(Vector3 pos, float[] texCoords)
        {
            Position = pos;
            TexCoords = texCoords;
        }

        public MeshPoint(Vector3 pos, Vector2 texCoords)
        {
            Position = pos;
            TexCoords = new float[] { texCoords.X, texCoords.Y };
        }
    }

    public class MeshPolygon {
        public List<MeshPoint> Points;

        public MeshPolygon()
        {
            Points = new List<MeshPoint>();
        }

        public MeshPolygon(MeshTriangle tri)
        {
            Points = new List<MeshPoint>();
            for (int i=0; i<3; i++)
            {
                Points.Add(new MeshPoint(tri.Vertices[i], tri.TexCoords[i]));
            }
        }

        public void Triangulate(List<MeshPoint> outverts, List<int> inds)
        {
            //simple fan triangle fill
            var baseInd = outverts.Count;
            outverts.AddRange(Points);

            for (int i=2; i<Points.Count; i++)
            {
                inds.Add(baseInd);
                inds.Add(baseInd+i-1);
                inds.Add(baseInd+i);
            }
        }
    }
}
