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

namespace Puzzy
{
    [RequireComponent(typeof(MeshFilter))]
    [RequireComponent(typeof(MeshRenderer))]
    [RequireComponent(typeof(BoxCollider))]
    [RequireComponent(typeof(Rigidbody))]
    [RequireComponent(typeof(Hoverable))]
    [RequireComponent(typeof(Draggable))]
    public class JigsawPiece : MonoBehaviour
    {
        private const float DefaultPieceThickness = .1f;

        private static readonly int GridSize = Shader.PropertyToID("_GridSize");
        private static readonly int Coordinates = Shader.PropertyToID("_Coordinates");
        private static readonly int EmissionIntensity = Shader.PropertyToID("_EmissionIntensity");

        [SerializeField] private bool _drawOutlineGizmos;
        [SerializeField] private bool _drawMeshGizmos;
        [SerializeField] private Material _topMaterial;
        [SerializeField] private Material _sideMaterial;
        [SerializeField] private Vector2Int _gridSize;
        [SerializeField] private Vector2Int _gridCoordinates;

        private MeshFilter _meshFilter;
        private MeshRenderer _meshRenderer;
        private BoxCollider _boxCollider;
        private Rigidbody _rigidbody;
        private Mesh _mesh;
        private Hoverable _hoverable;
        private Draggable _draggable;
        private float _thickness = DefaultPieceThickness;

        private readonly List<Color> _orderedGizmoColors = new();

        private void Awake()
        {
            _meshFilter = GetComponent<MeshFilter>();
            _meshRenderer = GetComponent<MeshRenderer>();
            _boxCollider = GetComponent<BoxCollider>();
            _boxCollider.size = new Vector3(0.99f, 0.99f, _thickness);
            _boxCollider.center = new Vector3(0.5f, 0.5f, -_thickness / 2f);
            _rigidbody = GetComponent<Rigidbody>();
            _hoverable = GetComponent<Hoverable>();
            _draggable = GetComponent<Draggable>();

            _rigidbody.isKinematic = false;

            GenerateGizmoColorSequence();
        }

        private void Start()
        {
            SetCoordinateInfo(_gridSize, _gridCoordinates);
            _hoverable.HoverStateChanged += HoverableOnHoverStateChanged;
            _draggable.DragStateChanged += DraggableOnDragStateChanged;
        }

        private void HoverableOnHoverStateChanged(bool isHovering)
        {
            if (_draggable.IsDragging)
            {
                return;
            }

            float intensity = isHovering ? 5f : 0f;
            var materialPropertyBlock = new MaterialPropertyBlock();
            _meshRenderer.GetPropertyBlock(materialPropertyBlock);
            materialPropertyBlock.SetFloat(EmissionIntensity, intensity);
            _meshRenderer.SetPropertyBlock(materialPropertyBlock);
        }

        private void DraggableOnDragStateChanged(bool isDragging)
        {
            if (!_draggable.IsDragging)
            {
                return;
            }

            var materialPropertyBlock = new MaterialPropertyBlock();
            _meshRenderer.GetPropertyBlock(materialPropertyBlock);
            materialPropertyBlock.SetFloat(EmissionIntensity, 0f);
            _meshRenderer.SetPropertyBlock(materialPropertyBlock);
        }

        private void OnDrawGizmos()
        {
            if (!_mesh)
            {
                return;
            }

            DrawOutlineGizmos();
            DrawMeshGizmos();
        }

        private void DrawOutlineGizmos()
        {
            if (!_drawOutlineGizmos)
            {
                return;
            }

            for (int i = 0; i < _mesh.vertexCount - 1; i++)
            {
                Gizmos.DrawWireSphere(transform.TransformPoint(_mesh.vertices[i]), 0.01f);
                Gizmos.DrawLine(transform.TransformPoint(_mesh.vertices[i]), transform.TransformPoint(_mesh.vertices[i + 1]));
            }

            Gizmos.DrawWireSphere(transform.TransformPoint(_mesh.vertices[^1]), 0.01f);
        }

        private void DrawMeshGizmos()
        {
            if (!_drawMeshGizmos)
            {
                return;
            }

            for (int i = 0; i < _mesh.triangles.Length; i += 3)
            {
                Gizmos.color = _orderedGizmoColors[i % _orderedGizmoColors.Count];
                Gizmos.DrawLine(transform.TransformPoint(_mesh.vertices[_mesh.triangles[i]]),
                    transform.TransformPoint(_mesh.vertices[_mesh.triangles[i + 1]]));
                Gizmos.DrawLine(transform.TransformPoint(_mesh.vertices[_mesh.triangles[i + 1]]),
                    transform.TransformPoint(_mesh.vertices[_mesh.triangles[i + 2]]));
                Gizmos.DrawLine(transform.TransformPoint(_mesh.vertices[_mesh.triangles[i + 2]]),
                    transform.TransformPoint(_mesh.vertices[_mesh.triangles[i]]));
            }
        }

        private static bool ArePointsCollinear(Vector3 a, Vector3 b, Vector3 c)
        {
            Vector3 ab = b - a;
            Vector3 bc = c - b;
            return Vector3.Cross(ab.normalized, bc.normalized).magnitude < 0.0001f;
        }

        private static List<Vector3> JoinAndSimplifyVertices(IList<Vector3> top, IList<Vector3> right, IList<Vector3> bottom, IList<Vector3> left)
        {
            var vertices = new List<Vector3>();
            vertices.AddRange(top);
            vertices.AddRange(right.Skip(1));
            vertices.AddRange(bottom.Skip(1));
            vertices.AddRange(left.Skip(1));

            var simplified = new List<Vector3>();
            for (int i = 0; i < vertices.Count; i++)
            {
                Vector3 prev = vertices[(i - 1 + vertices.Count) % vertices.Count];
                Vector3 curr = vertices[i];
                Vector3 next = vertices[(i + 1) % vertices.Count];

                if (Vector3.Distance(curr, prev) < 0.0001f)
                {
                    continue;
                }

                if (i == 0 || i == vertices.Count - 1 || !ArePointsCollinear(prev, curr, next))
                {
                    simplified.Add(curr);
                }
            }

            return simplified;
        }

        public static bool FromEdges(
            IList<Vector3> top,
            IList<Vector3> right,
            IList<Vector3> bottom,
            IList<Vector3> left,
            Material newTopMaterial,
            Material newCardboardMaterial,
            out JigsawPiece piece)
        {
            piece = null;
            if (top.Last() != right.First() || right.Last() != bottom.First() ||
                bottom.Last() != left.First() || left.Last() != top.First())
            {
                Debug.LogError("Edges are not properly joined.");
                return false;
            }

            if (top.Count == 0)
            {
                top = new List<Vector3> { Vector3.zero };
            }

            List<Vector3> verts = JoinAndSimplifyVertices(top, right, bottom, left);
            piece = new GameObject("JigsawPiece").AddComponent<JigsawPiece>();
            piece.GenerateMeshFromVertices(verts);
            piece.SetMaterials(newTopMaterial, newCardboardMaterial);
            return true;
        }

        private void SetMaterials(Material newTopMaterial, Material newSideMaterial)
        {
            _topMaterial = newTopMaterial;
            _sideMaterial = newSideMaterial;
            _meshRenderer.SetMaterials(new List<Material> { newTopMaterial, newSideMaterial });
        }

        public void SetCoordinateInfo(Vector2Int gridSize, Vector2Int coordinates)
        {
            _gridSize = gridSize;
            _gridCoordinates = coordinates;

            var materialPropertyBlock = new MaterialPropertyBlock();
            _meshRenderer.GetPropertyBlock(materialPropertyBlock);

            materialPropertyBlock.SetVector(GridSize, new Vector4(gridSize.x, gridSize.y, 0f, 0f));
            materialPropertyBlock.SetVector(Coordinates,
                new Vector4(
                    1f / gridSize.x * coordinates.x,
                    1f / gridSize.y * coordinates.y,
                    0f,
                    0f));

            _meshRenderer.SetPropertyBlock(materialPropertyBlock);
        }

        public void GenerateMeshFromVertices(IList<Vector3> vertices2D, float thickness = DefaultPieceThickness)
        {
            _thickness = thickness;
            _mesh = new Mesh { name = "JigsawPieceMesh" };
            _meshFilter.sharedMesh = _mesh;

            int vertexCount = vertices2D.Count;
            List<Vector3> verts = new();
            List<Vector3> normals = new();
            List<Vector2> uv0S = new(); // Primary UV set for texturing
            List<int> topTris = new();
            List<int> sideTris = new();

            for (int i = 0; i < vertexCount; i++)
            {
                verts.Add(vertices2D[i]);
                normals.Add(Vector3.zero);
                uv0S.Add(new Vector2(vertices2D[i].x, vertices2D[i].y));
            }

            for (int i = 0; i < vertexCount; i++)
            {
                verts.Add(vertices2D[i] + Vector3.back * thickness);
                normals.Add(Vector3.zero);
                uv0S.Add(new Vector2(vertices2D[i].x, vertices2D[i].y));
            }

            // Triangulate top surface
            List<int> tri = vertices2D.Triangulate();
            topTris.AddRange(tri);

            // Triangulate bottom surface
            for (int i = 0; i < tri.Count; i += 3)
            {
                sideTris.Add(tri[i + 2] + vertexCount);
                sideTris.Add(tri[i + 1] + vertexCount);
                sideTris.Add(tri[i] + vertexCount);
            }

            for (int i = 0; i < vertexCount; i++)
            {
                int topCurr = i;
                int topNext = (i + 1) % vertexCount;
                int bottomCurr = i + vertexCount;
                int bottomNext = (i + 1) % vertexCount + vertexCount;

                // First triangle: bottomCurr -> topCurr -> bottomNext
                sideTris.Add(bottomCurr);
                sideTris.Add(topCurr);
                sideTris.Add(bottomNext);

                // Second triangle: topCurr -> topNext -> bottomNext
                sideTris.Add(topCurr);
                sideTris.Add(topNext);
                sideTris.Add(bottomNext);

                float edgeLength = Vector3.Distance(vertices2D[i], vertices2D[topNext]);
                uv0S.Add(new Vector2(0, 0));
                uv0S.Add(new Vector2(edgeLength, 0));
                uv0S.Add(new Vector2(0, 1));
                uv0S.Add(new Vector2(edgeLength, 1));
            }

            for (int i = 0; i < vertexCount; i++)
            {
                int topNext = (i + 1) % vertexCount;
                uv0S[topNext] = new Vector2(Vector3.Distance(vertices2D[i], vertices2D[topNext]), 0); // Update topNext UV
            }

            _mesh.SetVertices(verts);
            _mesh.subMeshCount = 2;
            _mesh.SetTriangles(topTris, 0);
            _mesh.SetTriangles(sideTris, 1);
            _mesh.SetUVs(0, uv0S);
            _mesh.SetNormals(normals);

            ComputeSmoothNormals(_mesh, vertexCount);

            _mesh.RecalculateBounds();
            _mesh.RecalculateTangents();
        }

        private void ComputeSmoothNormals(Mesh mesh, int vertexCount)
        {
            Vector3[] vertices = mesh.vertices;
            var normals = new Vector3[vertices.Length];
            int[] triangles = mesh.GetIndices(0).Concat(mesh.GetIndices(1)).ToArray();
            var normalSums = new Vector3[vertices.Length];
            int[] counts = new int[vertices.Length];

            for (int i = 0; i < triangles.Length; i += 3)
            {
                int i0 = triangles[i];
                int i1 = triangles[i + 1];
                int i2 = triangles[i + 2];
                Vector3 v0 = vertices[i0];
                Vector3 v1 = vertices[i1];
                Vector3 v2 = vertices[i2];
                Vector3 faceNormal = Vector3.Cross(v1 - v0, v2 - v0).normalized;
                normalSums[i0] += faceNormal;
                normalSums[i1] += faceNormal;
                normalSums[i2] += faceNormal;
                counts[i0]++;
                counts[i1]++;
                counts[i2]++;
            }

            for (int i = 0; i < vertices.Length; i++)
            {
                if (counts[i] > 0)
                {
                    normals[i] = (normalSums[i] / counts[i]).normalized;
                }
            }

            mesh.normals = normals;
        }

        private void GenerateGizmoColorSequence()
        {
            const int colorCount = 50;
            for (int i = 0; i < colorCount; i++)
            {
                _orderedGizmoColors.Add(Color.HSVToRGB(i / (colorCount - 1f), 1f, 1f));
            }
        }

        private void RecalculateSideNormals(Mesh mesh, int sideSubMeshIndex, int sideStartVertexIndex)
        {
            int[] triangles = mesh.GetTriangles(sideSubMeshIndex);
            Vector3[] vertices = mesh.vertices;
            Vector3[] normals = mesh.normals;

            var normalSums = new Vector3[vertices.Length];
            int[] counts = new int[vertices.Length];

            for (int i = 0; i < triangles.Length; i += 3)
            {
                int i0 = triangles[i];
                int i1 = triangles[i + 1];
                int i2 = triangles[i + 2];

                Vector3 v0 = vertices[i0];
                Vector3 v1 = vertices[i1];
                Vector3 v2 = vertices[i2];
                Vector3 faceNormal = Vector3.Cross(v1 - v0, v2 - v0).normalized;

                normalSums[i0] += faceNormal;
                normalSums[i1] += faceNormal;
                normalSums[i2] += faceNormal;

                counts[i0]++;
                counts[i1]++;
                counts[i2]++;
            }

            for (int i = sideStartVertexIndex; i < vertices.Length; i++)
            {
                if (counts[i] > 0)
                {
                    normals[i] = (normalSums[i] / counts[i]).normalized;
                }
            }

            mesh.normals = normals;
        }
    }
}