using UnityEngine;

namespace EzySlice {
    /**
     * Represents a simple 3D Triangle structure with position
     * and UV map. The UV is required if the slicer needs
     * to recalculate the new UV position for texture mapping.
     */
    public struct Triangle {
        // the points which represent this triangle
        // these have to be set and are immutable. Cannot be
        // changed once set
        private readonly Vector3 m_pos_a;
        private readonly Vector3 m_pos_b;
        private readonly Vector3 m_pos_c;

        // the UV coordinates of this triangle
        // these are optional and may not be set
        private bool m_uv_set;
        private Vector2 m_uv_a;
        private Vector2 m_uv_b;
        private Vector2 m_uv_c;

        // the Normals of the Vertices
        // these are optional and may not be set
        private bool m_nor_set;
        private Vector3 m_nor_a;
        private Vector3 m_nor_b;
        private Vector3 m_nor_c;

        // the Tangents of the Vertices
        // these are optional and may not be set
        private bool m_tan_set;
        private Vector4 m_tan_a;
        private Vector4 m_tan_b;
        private Vector4 m_tan_c;

        public Triangle(Vector3 posa,
            Vector3 posb,
            Vector3 posc) {
            this.m_pos_a = posa;
            this.m_pos_b = posb;
            this.m_pos_c = posc;

            this.m_uv_set = false;
            this.m_uv_a = Vector2.zero;
            this.m_uv_b = Vector2.zero;
            this.m_uv_c = Vector2.zero;

            this.m_nor_set = false;
            this.m_nor_a = Vector3.zero;
            this.m_nor_b = Vector3.zero;
            this.m_nor_c = Vector3.zero;

            this.m_tan_set = false;
            this.m_tan_a = Vector4.zero;
            this.m_tan_b = Vector4.zero;
            this.m_tan_c = Vector4.zero;
        }

        public Vector3 positionA {
            get { return this.m_pos_a; }
        }

        public Vector3 positionB {
            get { return this.m_pos_b; }
        }

        public Vector3 positionC {
            get { return this.m_pos_c; }
        }

        public bool hasUV {
            get { return this.m_uv_set; }
        }

        public void SetUV(Vector2 uvA, Vector2 uvB, Vector2 uvC) {
            this.m_uv_a = uvA;
            this.m_uv_b = uvB;
            this.m_uv_c = uvC;

            this.m_uv_set = true;
        }

        public Vector2 uvA {
            get { return this.m_uv_a; }
        }

        public Vector2 uvB {
            get { return this.m_uv_b; }
        }

        public Vector2 uvC {
            get { return this.m_uv_c; }
        }

        public bool hasNormal {
            get { return this.m_nor_set; }
        }

        public void SetNormal(Vector3 norA, Vector3 norB, Vector3 norC) {
            this.m_nor_a = norA;
            this.m_nor_b = norB;
            this.m_nor_c = norC;

            this.m_nor_set = true;
        }

        public Vector3 normalA {
            get { return this.m_nor_a; }
        }

        public Vector3 normalB {
            get { return this.m_nor_b; }
        }

        public Vector3 normalC {
            get { return this.m_nor_c; }
        }

        public bool hasTangent {
            get { return this.m_tan_set; }
        }

        public void SetTangent(Vector4 tanA, Vector4 tanB, Vector4 tanC) {
            this.m_tan_a = tanA;
            this.m_tan_b = tanB;
            this.m_tan_c = tanC;

            this.m_tan_set = true;
        }

        public Vector4 tangentA {
            get { return this.m_tan_a; }
        }

        public Vector4 tangentB {
            get { return this.m_tan_b; }
        }

        public Vector4 tangentC {
            get { return this.m_tan_c; }
        }

        /**
         * Compute and set the tangents of this triangle
         * Derived From https://answers.unity.com/questions/7789/calculating-tangents-vector4.html
         */
        public void ComputeTangents() {
            // computing tangents requires both UV and normals set
            if (!m_nor_set || !m_uv_set) {
                return;
            }

            Vector3 v1 = m_pos_a;
            Vector3 v2 = m_pos_b;
            Vector3 v3 = m_pos_c;

            Vector2 w1 = m_uv_a;
            Vector2 w2 = m_uv_b;
            Vector2 w3 = m_uv_c;

            float x1 = v2.x - v1.x;
            float x2 = v3.x - v1.x;
            float y1 = v2.y - v1.y;
            float y2 = v3.y - v1.y;
            float z1 = v2.z - v1.z;
            float z2 = v3.z - v1.z;

            float s1 = w2.x - w1.x;
            float s2 = w3.x - w1.x;
            float t1 = w2.y - w1.y;
            float t2 = w3.y - w1.y;

            float r = 1.0f / (s1 * t2 - s2 * t1);

            Vector3 sdir = new Vector3((t2 * x1 - t1 * x2) * r, (t2 * y1 - t1 * y2) * r, (t2 * z1 - t1 * z2) * r);
            Vector3 tdir = new Vector3((s1 * x2 - s2 * x1) * r, (s1 * y2 - s2 * y1) * r, (s1 * z2 - s2 * z1) * r);

            Vector3 n1 = m_nor_a;
            Vector3 nt1 = sdir;

            Vector3.OrthoNormalize(ref n1, ref nt1);
            Vector4 tanA = new Vector4(nt1.x, nt1.y, nt1.z, (Vector3.Dot(Vector3.Cross(n1, nt1), tdir) < 0.0f) ? -1.0f : 1.0f);

            Vector3 n2 = m_nor_b;
            Vector3 nt2 = sdir;

            Vector3.OrthoNormalize(ref n2, ref nt2);
            Vector4 tanB = new Vector4(nt2.x, nt2.y, nt2.z, (Vector3.Dot(Vector3.Cross(n2, nt2), tdir) < 0.0f) ? -1.0f : 1.0f);

            Vector3 n3 = m_nor_c;
            Vector3 nt3 = sdir;

            Vector3.OrthoNormalize(ref n3, ref nt3);
            Vector4 tanC = new Vector4(nt3.x, nt3.y, nt3.z, (Vector3.Dot(Vector3.Cross(n3, nt3), tdir) < 0.0f) ? -1.0f : 1.0f);

            // finally set the tangents of this object
            SetTangent(tanA, tanB, tanC);
        }

        /**
         * Calculate the Barycentric coordinate weight values u-v-w for Point p in respect to the provided
         * triangle. This is useful for computing new UV coordinates for arbitrary points.
         */
        public Vector3 Barycentric(Vector3 p) {
            Vector3 a = m_pos_a;
            Vector3 b = m_pos_b;
            Vector3 c = m_pos_c;

            Vector3 m = Vector3.Cross(b - a, c - a);

            float nu;
            float nv;
            float ood;

            float x = Mathf.Abs(m.x);
            float y = Mathf.Abs(m.y);
            float z = Mathf.Abs(m.z);

            // compute areas of plane with largest projections
            if (x >= y && x >= z) {
                // area of PBC in yz plane
                nu = Intersector.TriArea2D(p.y, p.z, b.y, b.z, c.y, c.z);
                // area of PCA in yz plane
                nv = Intersector.TriArea2D(p.y, p.z, c.y, c.z, a.y, a.z);
                // 1/2*area of ABC in yz plane
                ood = 1.0f / m.x;
            } else if (y >= x && y >= z) {
                // project in xz plane
                nu = Intersector.TriArea2D(p.x, p.z, b.x, b.z, c.x, c.z);
                nv = Intersector.TriArea2D(p.x, p.z, c.x, c.z, a.x, a.z);
                ood = 1.0f / -m.y;
            } else {
                // project in xy plane
                nu = Intersector.TriArea2D(p.x, p.y, b.x, b.y, c.x, c.y);
                nv = Intersector.TriArea2D(p.x, p.y, c.x, c.y, a.x, a.y);
                ood = 1.0f / m.z;
            }

            float u = nu * ood;
            float v = nv * ood;
            float w = 1.0f - u - v;

            return new Vector3(u, v, w);
        }

        /**
         * Generate a set of new UV coordinates for the provided point pt in respect to Triangle.
         * 
         * Uses weight values for the computation, so this triangle must have UV's set to return
         * the correct results. Otherwise Vector2.zero will be returned. check via hasUV().
         */
        public Vector2 GenerateUV(Vector3 pt) {
            // if not set, result will be zero, quick exit
            if (!m_uv_set) {
                return Vector2.zero;
            }

            Vector3 weights = Barycentric(pt);

            return (weights.x * m_uv_a) + (weights.y * m_uv_b) + (weights.z * m_uv_c);
        }

        /**
         * Generates a set of new Normal coordinates for the provided point pt in respect to Triangle.
         * 
         * Uses weight values for the computation, so this triangle must have Normal's set to return
         * the correct results. Otherwise Vector3.zero will be returned. check via hasNormal().
         */
        public Vector3 GenerateNormal(Vector3 pt) {
            // if not set, result will be zero, quick exit
            if (!m_nor_set) {
                return Vector3.zero;
            }

            Vector3 weights = Barycentric(pt);

            return (weights.x * m_nor_a) + (weights.y * m_nor_b) + (weights.z * m_nor_c);
        }

        /**
         * Generates a set of new Tangent coordinates for the provided point pt in respect to Triangle.
         * 
         * Uses weight values for the computation, so this triangle must have Tangent's set to return
         * the correct results. Otherwise Vector4.zero will be returned. check via hasTangent().
         */
        public Vector4 GenerateTangent(Vector3 pt) {
            // if not set, result will be zero, quick exit
            if (!m_nor_set) {
                return Vector4.zero;
            }

            Vector3 weights = Barycentric(pt);

            return (weights.x * m_tan_a) + (weights.y * m_tan_b) + (weights.z * m_tan_c);
        }

        /**
         * Helper function to split this triangle by the provided plane and store
         * the results inside the IntersectionResult structure.
         * Returns true on success or false otherwise
         */
        public bool Split(Plane pl, IntersectionResult result) {
            Intersector.Intersect(pl, this, result);

            return result.isValid;
        }

        /**
         * Check the triangle winding order, if it's Clock Wise or Counter Clock Wise 
         */
        public bool IsCW() {
            return SignedSquare(m_pos_a, m_pos_b, m_pos_c) >= float.Epsilon;
        }

        /**
         * Returns the Signed square of a given triangle, useful for checking the
         * winding order
         */
        public static float SignedSquare(Vector3 a, Vector3 b, Vector3 c) {
            return (a.x * (b.y * c.z - b.z * c.y) -
                a.y * (b.x * c.z - b.z * c.x) +
                a.z * (b.x * c.y - b.y * c.x));
        }

        /**
         * Editor only DEBUG functionality. This should not be compiled in the final
         * Version.
         */
        public void OnDebugDraw() {
            OnDebugDraw(Color.white);
        }

        public void OnDebugDraw(Color drawColor) {
#if UNITY_EDITOR
            Color prevColor = Gizmos.color;

            Gizmos.color = drawColor;

            Gizmos.DrawLine(positionA, positionB);
            Gizmos.DrawLine(positionB, positionC);
            Gizmos.DrawLine(positionC, positionA);

            Gizmos.color = prevColor;
#endif
        }
    }
}