/*
 * Copyright 2020 faddenSoft
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
using System;
using System.Collections.Generic;
using System.Diagnostics;

using PluginCommon;

namespace SourceGen {
    /// <summary>
    /// Renders a wireframe visualization, generating a collection of line segments in clip space.
    /// </summary>
    public class WireframeObject {
        /// <summary>
        /// Line segment.
        /// </summary>
        public class LineSeg {
            public double X0 { get; private set; }
            public double Y0 { get; private set; }
            public double X1 { get; private set; }
            public double Y1 { get; private set; }

            public LineSeg(double x0, double y0, double x1, double y1) {
                if (double.IsNaN(x0) || double.IsNaN(y0) || double.IsNaN(x1) || double.IsNaN(y1)) {
                    throw new Exception("Invalid LineSeg: x0=" + x0 + " y0=" + y0 +
                        " x1=" + x1 + " y1=" + y1);
                }
                X0 = x0;
                Y0 = y0;
                X1 = x1;
                Y1 = y1;
            }
        }

        private class Vertex {
            public Vector3 Vec { get; private set; }
            public List<Face> Faces { get; private set; }
            public bool IsExcluded { get; private set; }

            public Vertex(double x, double y, double z, bool isExcluded) {
                Vec = new Vector3(x, y, z);
                Faces = new List<Face>();
                IsExcluded = isExcluded;
            }

            public override string ToString() {
                return Vec.ToString() + " + " + Faces.Count + " faces";
            }
        }

        private class Edge {
            public Vertex Vertex0 { get; private set; }
            public Vertex Vertex1 { get; private set; }
            public List<Face> Faces { get; private set; }
            public bool IsExcluded { get; private set; }

            public Edge(Vertex v0, Vertex v1, bool isExcluded) {
                Vertex0 = v0;
                Vertex1 = v1;
                Faces = new List<Face>();
                IsExcluded = isExcluded;
            }
        }

        private class Face {
            // Surface normal.
            public Vector3 Normal { get; private set; }
            // One vertex on the face, for BFC.
            public Vertex Vert { get; set; }
            // Flag set during BFC calculation.
            public bool IsVisible { get; set; }

            public Face(double x, double y, double z) {
                Normal = new Vector3(x, y, z);
                Normal.Normalize();     // not necessary, but easier to read in debug output
                IsVisible = true;
            }
        }

        public bool VerboseDebug { get; set; }

        private bool mIs2d = false;
        private List<Vertex> mVertices = new List<Vertex>();
        private List<Vertex> mPoints = new List<Vertex>();
        private List<Edge> mEdges = new List<Edge>();
        private List<Face> mFaces = new List<Face>();
        private double mBigMag = -1.0;
        private double mBigMagRc = -1.0;
        private double mCenterAdjX, mCenterAdjY;


        // private constructor; use Create()
        private WireframeObject() { }

        /// <summary>
        /// Creates a new object from a wireframe visualization.
        /// </summary>
        /// <param name="visWire">Visualization object.</param>
        /// <returns>New object, or null if visualization data fails validation.</returns>
        public static WireframeObject Create(IVisualizationWireframe visWire) {
            if (!visWire.Validate(out string msg)) {
                // Should not be here -- visualizer should have checked validation and
                // reported an error.
                Debug.WriteLine("Wireframe validation failed: " + msg);
                return null;
            }

            WireframeObject wireObj = new WireframeObject();

            wireObj.mIs2d = visWire.Is2d;

            //
            // Start by extracting data from the visualization object.  Everything stored
            // there is loaded into this object.  The VisWireframe validator will have
            // ensured that all the indices are in range.
            //
            // IMPORTANT: do not retain "visWire", as it may be a proxy for an object with a
            // limited lifespan.
            //

            float[] normalsX = visWire.GetNormalsX();
            if (normalsX.Length > 0) {
                float[] normalsY = visWire.GetNormalsY();
                float[] normalsZ = visWire.GetNormalsZ();

                for (int i = 0; i < normalsX.Length; i++) {
                    wireObj.mFaces.Add(new Face(normalsX[i], normalsY[i], normalsZ[i]));
                }
            }

            float[] verticesX = visWire.GetVerticesX();
            float[] verticesY = visWire.GetVerticesY();
            float[] verticesZ = visWire.GetVerticesZ();
            int[] excludedVertices = visWire.GetExcludedVertices();

            // Compute min/max for X/Y for 2d re-centering.  The trick is that we only want
            // to use vertices that are visible.  If the shape starts with a huge move off to
            // the left, we don't want to include (0,0).
            double xmin, xmax, ymin, ymax;
            xmin = ymin = 10e9;
            xmax = ymax = -10e9;

            for (int i = 0; i < verticesX.Length; i++) {
                wireObj.mVertices.Add(new Vertex(verticesX[i], verticesY[i], verticesZ[i],
                    HasIndex(excludedVertices, i)));
            }

            int[] points = visWire.GetPoints();
            for (int i = 0; i < points.Length; i++) {
                Vertex vert = wireObj.mVertices[points[i]];
                wireObj.mPoints.Add(vert);
                UpdateMinMax(vert, ref xmin, ref xmax, ref ymin, ref ymax);
            }

            IntPair[] edges = visWire.GetEdges();
            int[] excludedEdges = visWire.GetExcludedEdges();
            for (int i = 0; i < edges.Length; i++) {
                int v0index = edges[i].Val0;
                int v1index = edges[i].Val1;

                //if (v0index < 0 || v0index >= wireObj.mVertices.Count ||
                //        v1index < 0 || v1index >= wireObj.mVertices.Count) {
                //    Debug.Assert(false);
                //    return null;
                //}

                Vertex vert0 = wireObj.mVertices[v0index];
                Vertex vert1 = wireObj.mVertices[v1index];
                wireObj.mEdges.Add(new Edge(vert0, vert1, HasIndex(excludedEdges, i)));

                UpdateMinMax(vert0, ref xmin, ref xmax, ref ymin, ref ymax);
                UpdateMinMax(vert1, ref xmin, ref xmax, ref ymin, ref ymax);
            }

            IntPair[] vfaces = visWire.GetVertexFaces();
            for (int i = 0; i < vfaces.Length; i++) {
                int vindex = vfaces[i].Val0;
                int findex = vfaces[i].Val1;

                //if (vindex < 0 || vindex >= wireObj.mVertices.Count ||
                //        findex < 0 || findex >= wireObj.mFaces.Count) {
                //    Debug.Assert(false);
                //    return null;
                //}

                Face face = wireObj.mFaces[findex];
                wireObj.mVertices[vindex].Faces.Add(face);
                if (face.Vert == null) {
                    face.Vert = wireObj.mVertices[vindex];
                }
            }

            IntPair[] efaces = visWire.GetEdgeFaces();
            for (int i = 0; i < efaces.Length; i++) {
                int eindex = efaces[i].Val0;
                int findex = efaces[i].Val1;

                //if (eindex < 0 || eindex >= wireObj.mEdges.Count ||
                //        findex < 0 || findex >= wireObj.mFaces.Count) {
                //    Debug.Assert(false);
                //    return null;
                //}

                Face face = wireObj.mFaces[findex];
                wireObj.mEdges[eindex].Faces.Add(face);
                if (face.Vert == null) {
                    face.Vert = wireObj.mEdges[eindex].Vertex0;
                }
            }

            //
            // All data has been loaded into friendly classes.
            //

            // Compute center of visible vertices.
            wireObj.mCenterAdjX = -(xmin + xmax) / 2;
            wireObj.mCenterAdjY = -(ymin + ymax / 2);

            // Compute the magnitude of the largest vertex, for scaling.
            double bigMag = -1.0;
            double bigMagRc = -1.0;
            for (int i = 0; i < wireObj.mVertices.Count; i++) {
                Vector3 vec = wireObj.mVertices[i].Vec;
                double mag = vec.Magnitude();
                if (bigMag < mag) {
                    bigMag = mag;
                }

                // Repeat the operation with recentering.  This isn't quite right as we're
                // including all vertices, not just the visible ones.
                mag = new Vector3(vec.X + wireObj.mCenterAdjX,
                    vec.Y + wireObj.mCenterAdjY, vec.Z).Magnitude();
                if (bigMagRc < mag) {
                    bigMagRc = mag;
                }
            }

            // Avoid divide-by-zero.
            if (bigMag == 0) {
                Debug.WriteLine("NOTE: wireframe magnitude was zero");
                bigMag = 1;
            }
            if (bigMagRc == 0) {
                bigMagRc = 1;
            }
            wireObj.mBigMag = bigMag;
            wireObj.mBigMagRc = bigMagRc;

            return wireObj;
        }

        private static void UpdateMinMax(Vertex vert, ref double xmin, ref double xmax,
                ref double ymin, ref double ymax) {
            if (vert.Vec.X < xmin) {
                xmin = vert.Vec.X;
            }
            if (vert.Vec.X > xmax) {
                xmax = vert.Vec.X;
            }
            if (vert.Vec.Y < ymin) {
                ymin = vert.Vec.Y;
            }
            if (vert.Vec.Y > ymax) {
                ymax = vert.Vec.Y;
            }
        }

        private static bool HasIndex(int[] arr, int val) {
            for (int i = 0; i < arr.Length; i++) {
                if (arr[i] == val) {
                    return true;
                }
            }
            return false;
        }

        /// <summary>
        /// Generates a list of line segments for the wireframe data and the specified
        /// parameters.
        /// </summary>
        /// <param name="eulerX">Rotation about X axis.</params>
        /// <param name="eulerY">Rotation about Y axis.</params>
        /// <param name="eulerZ">Rotation about Z axis.</params>
        /// <param name="doPersp">Perspective or othographic projection?</param>
        /// <param name="doBfc">Perform backface culling?</param>
        /// <param name="doRecenter">Re-center 2D renderings?</param>
        /// <returns>List a of line segments, which could be empty if backface culling
        ///   was especially successful.  All segment coordinates are in the range
        ///   [-1,1].</returns>
        public List<LineSeg> Generate(int eulerX, int eulerY, int eulerZ,
                bool doPersp, bool doBfc, bool doRecenter) {
            if (VerboseDebug) {
                Debug.WriteLine("Found center=" + mCenterAdjX + "," + mCenterAdjY);
                Debug.WriteLine("  bigMag=" + mBigMag + " / " + mBigMagRc);
            }

            // overrule flags that don't make sense
            if (mIs2d) {
                doPersp = doBfc = false;
            } else {
                doRecenter = false;
            }

            List<LineSeg> segs = new List<LineSeg>(mEdges.Count);

            // Camera Z coordinate adjustment, used to control how perspective projections
            // appear.  The larger the value, the farther the object appears to be.  Very
            // large values approximate an orthographic projection.
            const double zadj = 3.0;

            // Scale coordinate values to [-1,1].
            double scale;
            if (doRecenter) {
                scale = 1.0 / mBigMagRc;
            } else {
                scale = 1.0 / mBigMag;
            }
            if (doPersp) {
                // objects closer to camera are bigger; reduce scale slightly
                scale = (scale * zadj) / (zadj + 0.3);
            }

            // Configure X/Y translation for 2D wireframes.
            double transX = 0;
            double transY = 0;
            if (doRecenter) {
                transX = mCenterAdjX;
                transY = mCenterAdjY;
            }

            // In a left-handed coordinate system, +Z is away from the viewer.  The
            // visualizer expects a left-handed system with the "nose" aimed toward +Z,
            // which leaves us looking at the back end of things.  We can add a 180 degree
            // rotation about Y so we're looking at the front instead, though this
            // effectively reverses the direction of rotation about X.  We can compensate
            // for it by reversing the handedness of the X rotation.
            //eulerY = (eulerY + 180) % 360;

            // Form rotation matrix.
            Matrix33 rotMat = new Matrix33();
            rotMat.SetRotationEuler(eulerX, eulerY, eulerZ, Matrix33.RotMode.ZYX_LLL);
            //Debug.WriteLine("ROT: " + rotMat);

            if (doBfc) {
                // Mark faces as visible or not.  This is determined with the surface normal,
                // rather than by checking whether a transformed triangle is clockwise.
                foreach (Face face in mFaces) {
                    // Transform the surface normal.
                    Vector3 rotNorm = rotMat.Multiply(face.Normal);
                    if (doPersp) {
                        // Transform one vertex to get a vector from the camera to the
                        // surface.  We want (V0 - C), where C is the camera; since we're
                        // at the origin, we just need -C.
                        if (face.Vert == null) {
                            Debug.WriteLine("GLITCH: no vertex for face");
                            face.IsVisible = true;
                            continue;
                        }
                        Vector3 camVec = rotMat.Multiply(face.Vert.Vec);    // transform
                        camVec = camVec.Multiply(-scale);   // scale to [-1,1] and negate to get -C
                        camVec = camVec.Add(new Vector3(0, 0, -zadj));      // translate

                        // Now compute the dot product of the camera vector.
                        double dot = Vector3.Dot(camVec, rotNorm);
                        face.IsVisible = (dot >= 0);
                        //Debug.WriteLine(string.Format(
                        //    "Face {0} vis={1,-5} dot={2,-8:N2}: camVec={3} rotNorm={4}",
                        //    index++, face.IsVisible, dot, camVec, rotNorm));
                    } else {
                        // For orthographic projection, the camera is essentially looking
                        // down the Z axis at every X,Y, so we can trivially check the
                        // value of Z in the transformed normal.
                        face.IsVisible = (rotNorm.Z <= 0);
                    }
                }
            }

            foreach (Vertex point in mPoints) {
                // There are no "point faces" at the moment, so no BFC is applied.
                Vector3 vec = point.Vec;
                if (doRecenter) {
                    vec = new Vector3(vec.X + transX, vec.Y + transY, vec.Z);
                }
                Vector3 trv = rotMat.Multiply(vec);

                double xc, yc;
                if (doPersp) {
                    double zc = trv.Z * scale;
                    xc = (trv.X * scale * zadj) / (zadj + zc);
                    yc = (trv.Y * scale * zadj) / (zadj + zc);
                } else {
                    xc = trv.X * scale;
                    yc = trv.Y * scale;
                }

                //Debug.WriteLine("POINT " + xc + "," + yc);

                // Zero-length line segments don't do anything.  Try a '+'.
                const double dist = 1 / 64.0;
                double x0 = Math.Max(-1.0, xc - dist);
                double x1 = Math.Min(xc + dist, 1.0);
                segs.Add(new LineSeg(x0, yc, x1, yc));
                double y0 = Math.Max(-1.0, yc - dist);
                double y1 = Math.Min(yc + dist, 1.0);
                segs.Add(new LineSeg(xc, y0, xc, y1));
            }

            foreach (Edge edge in mEdges) {
                if (doBfc) {
                    // To be visible, vertices and edges must either not specify any
                    // faces, or must specify a visible face.  They can also be hidden
                    // by the level-of-detail exclusion mechanism.
                    if (!IsVertexVisible(edge.Vertex0) || edge.Vertex0.IsExcluded ||
                            !IsVertexVisible(edge.Vertex1) || edge.Vertex1.IsExcluded ||
                            !IsEdgeVisible(edge) || edge.IsExcluded) {
                        continue;
                    }
                }

                Vector3 vec0 = edge.Vertex0.Vec;
                Vector3 vec1 = edge.Vertex1.Vec;
                if (doRecenter) {
                    vec0 = new Vector3(vec0.X + transX, vec0.Y + transY, vec0.Z);
                    vec1 = new Vector3(vec1.X + transX, vec1.Y + transY, vec1.Z);
                }
                Vector3 trv0 = rotMat.Multiply(vec0);
                Vector3 trv1 = rotMat.Multiply(vec1);
                double x0, y0, x1, y1;

                if (doPersp) {
                    // Left-handed system, so +Z is away from viewer.
                    double z0 = trv0.Z * scale;
                    double z1 = trv1.Z * scale;
                    x0 = (trv0.X * scale * zadj) / (zadj + z0);
                    y0 = (trv0.Y * scale * zadj) / (zadj + z0);
                    x1 = (trv1.X * scale * zadj) / (zadj + z1);
                    y1 = (trv1.Y * scale * zadj) / (zadj + z1);
                } else {
                    x0 = trv0.X * scale;
                    y0 = trv0.Y * scale;
                    x1 = trv1.X * scale;
                    y1 = trv1.Y * scale;
                }

                segs.Add(new LineSeg(x0, y0, x1, y1));
            }

            return segs;
        }

        private bool IsVertexVisible(Vertex vert) {
            if (vert.Faces.Count == 0) {
                return true;
            }
            foreach (Face face in vert.Faces) {
                if (face.IsVisible) {
                    return true;
                }
            }
            return false;
        }

        private bool IsEdgeVisible(Edge edg) {
            if (edg.Faces.Count == 0) {
                return true;
            }
            foreach (Face face in edg.Faces) {
                if (face.IsVisible) {
                    return true;
                }
            }
            return false;
        }
    }
}
