using System.Collections.Generic;
using UnityEngine;

public class FieldOfView : MonoBehaviour
{
    public float viewRadius;

    [Range(0,360)]
    public float periAngle;

    [Range(0,180)]
    public float foveaAngle;

    public Transform eyeLeft;
    public Transform eyeRight;
    public Transform eyeCenter;

    public LayerMask targetMask;
    public LayerMask obstacleMask;

    public float meshResolution;
    public int edgeResolveIterations;
    public float edgeDistanceThreshold;

    public MeshFilter periMeshFilter;
    public MeshFilter foveaMeshFilter;
    private Mesh periMesh;
    private Mesh foveaMesh;

    private void Start()
    {
        periMesh = new Mesh();
        periMesh.name = "View Mesh";
        periMeshFilter.mesh = periMesh;

        foveaMesh = new Mesh();
        foveaMesh.name = "View Mesh";
        foveaMeshFilter.mesh = foveaMesh;
    }

    private void LateUpdate()
    {
        Vector3 gazeDirection = ((eyeLeft.forward + eyeRight.forward) * 0.5f).normalized;
        eyeCenter.rotation = Quaternion.LookRotation(gazeDirection);
        float gazeAngle = AngleFromDir(gazeDirection, true);
        DrawFOV(transform.eulerAngles.y, periMesh, periAngle);
        DrawFOV(gazeAngle, foveaMesh, foveaAngle);
    }

    public Vector3 DirFromAngle(float angleDeg, bool angleIsGlobal)
    {
        if (!angleIsGlobal)
        {
            angleDeg += transform.eulerAngles.y;
        }
        return new Vector3(Mathf.Sin(angleDeg * Mathf.Deg2Rad), 0, Mathf.Cos(angleDeg * Mathf.Deg2Rad));
    }

    public float AngleFromDir(Vector3 dir, bool angleIsGlobal)
    {
        // Sicherheitsnetz: y‑Komponente ignorieren und Vektor normalisieren
        dir.y = 0;
        if (dir.sqrMagnitude < 0.0001f) return 0f;

        // Atan2: (x, z) → Winkel in Rad, dann nach ° umrechnen
        float angleDeg = Mathf.Atan2(dir.x, dir.z) * Mathf.Rad2Deg;

        // Bereich schön sauber auf 0–360 bringen
        angleDeg = (angleDeg + 360f) % 360f;

        // Soll der Winkel relativ zum Objekt sein?
        if (!angleIsGlobal)
            angleDeg -= transform.eulerAngles.y;

        // Und noch einmal normalisieren, damit auch hier 0–360 garantiert ist
        return (angleDeg + 360f) % 360f;
    }

    void DrawFOV(float startAngle, Mesh viewMesh, float viewAngle)
    {
        int stepCount = Mathf.RoundToInt(viewAngle * meshResolution);
        float stepAngleSize = viewAngle / stepCount;
        List<Vector3> viewPoints = new List<Vector3>();
        ViewCastInfo oldViewCast = new ViewCastInfo();

        for (int i = 0; i <= stepCount; i++)
        {
            float angle = startAngle - viewAngle / 2 + stepAngleSize * i;
            
            Debug.DrawLine(transform.position, transform.position + DirFromAngle(angle, true) * viewRadius, Color.red);

            ViewCastInfo newViewCast = ViewCast(angle);

            if (i > 0)
            {
                bool edgeDistThresholdExeeded = Mathf.Abs(oldViewCast.distance - newViewCast.distance) > edgeDistanceThreshold;
                if (oldViewCast.didHit != newViewCast.didHit || (oldViewCast.didHit && newViewCast.didHit && edgeDistThresholdExeeded))
                {
                    EdgeInfo edge = FingEdge(oldViewCast, newViewCast);
                    if (edge.pointmin != Vector3.zero)
                    {
                        viewPoints.Add(edge.pointmin);
                    }
                    if (edge.pointmax != Vector3.zero)
                    {
                        viewPoints.Add(edge.pointmax);
                    }
                }
            }
            viewPoints.Add(newViewCast.hitPoint);
            oldViewCast = newViewCast;
        }

        int vertexcount = viewPoints.Count + 1;
        Vector3[] vertices = new Vector3[vertexcount];
        int[] triangles = new int[(vertexcount-2) * 3];

        vertices[0] = Vector3.zero;

        for (int i = 0; i < vertexcount-1; i++)
        {
            vertices[i+1] = transform.InverseTransformPoint(viewPoints[i]);

            if (i < vertexcount - 2)
            {
                triangles[i * 3] = 0;
                triangles[i * 3 + 1] = i + 1;
                triangles[i * 3 + 2] = i + 2;
            }
            
        }
        viewMesh.Clear();
        viewMesh.vertices = vertices;
        viewMesh.triangles = triangles;
        viewMesh.RecalculateNormals();
    }

    ViewCastInfo ViewCast(float globalAngle)
    {
        Vector3 direction = DirFromAngle(globalAngle, true);
        RaycastHit hit;

        if (Physics.Raycast(transform.position, direction, out hit, viewRadius, obstacleMask))
        {
            return new ViewCastInfo(true, hit.point, hit.distance, globalAngle);
        }
        else
        {
            return new ViewCastInfo(false, transform.position + direction * viewRadius, viewRadius, globalAngle);
        }
    }

    public struct ViewCastInfo
    {
        public bool didHit;
        public Vector3 hitPoint;
        public float distance;
        public float angle;

        public ViewCastInfo(bool _hit, Vector3 _hitPoint, float _distance, float _angle)
        {
            didHit = _hit;
            hitPoint = _hitPoint;
            distance = _distance;
            angle = _angle;
        }
    }

    public struct EdgeInfo
    {
        public Vector3 pointmin;
        public Vector3 pointmax;

        public EdgeInfo(Vector3 _pointmin, Vector3 _pointmax)
        {
            pointmin = _pointmin;
            pointmax = _pointmax;
        }
    }

    EdgeInfo FingEdge(ViewCastInfo minViewCast,  ViewCastInfo maxViewCast)
    {
        float minAngle = minViewCast.angle;
        float maxAngle = maxViewCast.angle;
        Vector3 minPoint = Vector3.zero;
        Vector3 maxPoint = Vector3.zero;

        for (int i = 0; i < edgeResolveIterations; i++)
        {
            float angle = (minAngle + maxAngle) / 2;
            ViewCastInfo newViewCast = ViewCast(angle);

            bool edgeDistThresholdExeeded = Mathf.Abs(minViewCast.distance - newViewCast.distance) > edgeDistanceThreshold;
            if (newViewCast.didHit == minViewCast.didHit && !edgeDistThresholdExeeded)
            {
                minAngle = angle;
                minPoint = newViewCast.hitPoint;
            }
            else
            {
                maxAngle = angle;
                maxPoint = newViewCast.hitPoint;
            }
        }

        return new EdgeInfo(minPoint, maxPoint);
    }
}
