﻿using System;
using System.Collections.Generic;
using Oni.Imaging;

namespace Oni.Akira
{
    internal class Polygon
    {
        #region Private data
        private PolygonMesh mesh;
        private GunkFlags flags;
        private int[] pointIndices;
        private int[] texCoordIndices;
        private int[] normalIndices;
        private Color[] colors;
        private Material material;
        private Plane plane;
        private int objectType = -1;
        private int objectId = -1;
        private int scriptId;
        private string fileName;
        private string objectName;
        private PolygonEdge[] edges;
        private BoundingBox bbox;
        #endregion

        public Polygon(PolygonMesh mesh, int[] pointIndices)
        {
            this.mesh = mesh;
            this.pointIndices = pointIndices;
            this.plane = GetPlane();
            this.bbox = GetBoundingBox();

            BuildFlags();
        }

        public Polygon(PolygonMesh mesh, int[] pointIndices, GunkFlags flags)
            : this(mesh, pointIndices)
        {
            this.flags |= flags;
        }

        public Polygon(PolygonMesh mesh, int[] pointIndices, Plane plane)
        {
            this.mesh = mesh;
            this.pointIndices = pointIndices;
            this.plane = plane;

            BuildFlags();
        }

        public PolygonMesh Mesh => mesh;

        public GunkFlags Flags
        {
            get
            {
                if (material == null)
                    return flags;

                return flags | material.Flags;
            }
            set
            {
                flags = value;
            }
        }

        public bool IsTransparent => (Flags & GunkFlags.Transparent) != 0;
        public bool IsStairs=>(Flags & GunkFlags.Stairs) != 0;

        public Material Material
        {
            get { return material; }
            set { material = value; }
        }

        public int VertexCount => pointIndices.Length;

        public int[] PointIndices => pointIndices;

        public IEnumerable<Vector3> Points
        {
            get
            {
                foreach (int i in pointIndices)
                    yield return mesh.Points[i];
            }
        }

        public int[] TexCoordIndices
        {
            get { return texCoordIndices; }
            set { texCoordIndices = value; }
        }

        public int[] NormalIndices
        {
            get { return normalIndices; }
            set { normalIndices = value; }
        }

        public Color[] Colors
        {
            get { return colors; }
            set { colors = value; }
        }

        public Plane Plane => plane;

        public int ObjectType
        {
            get { return objectType; }
            set { objectType = value; }
        }

        public int ObjectId
        {
            get { return objectId; }
            set { objectId = value; }
        }

        public int ScriptId
        {
            get { return scriptId; }
            set { scriptId = value; }
        }

        public string FileName
        {
            get { return fileName; }
            set { fileName = value; }
        }

        public string ObjectName
        {
            get { return objectName; }
            set { objectName = value; }
        }

        private Plane GetPlane()
        {
            var plane = new Plane(
                mesh.Points[pointIndices[0]],
                mesh.Points[pointIndices[1]],
                mesh.Points[pointIndices[2]]);

            var bbox = GetBoundingBox();
            var bboxSize = bbox.Max - bbox.Min;

            if (Math.Abs(bboxSize.X) < 0.0001f)
            {
                if (plane.Normal.X < 0.0f)
                    plane = new Plane(Vector3.Left, bbox.Min.X);
                else
                    plane = new Plane(Vector3.Right, -bbox.Max.X);
            }
            else if (Math.Abs(bboxSize.Y) < 0.0001f)
            {
                if (plane.Normal.Y < 0.0f)
                    plane = new Plane(Vector3.Down, bbox.Min.Y);
                else
                    plane = new Plane(Vector3.Up, -bbox.Max.Y);
            }
            else if (Math.Abs(bboxSize.Z) < 0.0001f)
            {
                if (plane.Normal.Z < 0.0f)
                    plane = new Plane(Vector3.Forward, bbox.Min.Z);
                else
                    plane = new Plane(Vector3.Backward, -bbox.Max.Z);
            }
            else
            {
                plane.Normal.X = FMath.Round(plane.Normal.X, 4);
                plane.Normal.Y = FMath.Round(plane.Normal.Y, 4);
                plane.Normal.Z = FMath.Round(plane.Normal.Z, 4);
            }

            return plane;
        }

        public BoundingBox BoundingBox => bbox;

        private BoundingBox GetBoundingBox()
        {
            var point = mesh.Points[pointIndices[0]];
            var bbox = new BoundingBox(point, point);

            for (int i = 1; i < pointIndices.Length; i++)
            {
                point = mesh.Points[pointIndices[i]];

                Vector3.Min(ref bbox.Min, ref point, out bbox.Min);
                Vector3.Max(ref bbox.Max, ref point, out bbox.Max);
            }

            return bbox;
        }

        private void BuildFlags()
        {
            SetProjectionPlane();
            SetHorizontalVertical();
        }

        private void SetHorizontalVertical()
        {
            if (Math.Abs(Vector3.Dot(plane.Normal, Vector3.UnitY)) < 0.3420201f)
                flags |= GunkFlags.Vertical;
            else
                flags |= GunkFlags.Horizontal;
        }

        private void SetProjectionPlane()
        {
            var points = new Vector3[pointIndices.Length];

            for (int i = 0; i < pointIndices.Length; i++)
                points[i] = mesh.Points[pointIndices[i]];

            float xyArea = MathHelper.Area(Project(points, PolygonProjectionPlane.XY));
            float yzArea = MathHelper.Area(Project(points, PolygonProjectionPlane.YZ));
            float xzArea = MathHelper.Area(Project(points, PolygonProjectionPlane.XZ));

            var plane = PolygonProjectionPlane.None;

            if (xyArea > yzArea)
            {
                if (xyArea > xzArea)
                    plane = PolygonProjectionPlane.XY;
                else
                    plane = PolygonProjectionPlane.XZ;
            }
            else
            {
                if (yzArea > xzArea)
                    plane = PolygonProjectionPlane.YZ;
                else
                    plane = PolygonProjectionPlane.XZ;
            }

            flags |= (GunkFlags)((int)plane << 25);
        }

        private static Vector2[] Project(Vector3[] points, PolygonProjectionPlane plane)
        {
            var result = new Vector2[points.Length];

            switch (plane)
            {
                case PolygonProjectionPlane.XY:
                    for (int i = 0; i < points.Length; i++)
                    {
                        result[i].X = points[i].X;
                        result[i].Y = points[i].Y;
                    }
                    break;
                case PolygonProjectionPlane.XZ:
                    for (int i = 0; i < points.Length; i++)
                    {
                        result[i].X = points[i].X;
                        result[i].Y = points[i].Z;
                    }
                    break;
                case PolygonProjectionPlane.YZ:
                    for (int i = 0; i < points.Length; i++)
                    {
                        result[i].X = points[i].Z;
                        result[i].Y = points[i].Y;
                    }
                    break;
            }

            return result;
        }

        public PolygonEdge[] Edges
        {
            get
            {
                if (edges == null)
                {
                    edges = new PolygonEdge[pointIndices.Length];

                    for (int i = 0; i < edges.Length; i++)
                        edges[i] = new PolygonEdge(this, i);
                }

                return edges;
            }
        }
    }
}
