using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace VisiMath.Geometry
{
    /// <summary>
    /// Interface for geometric container
    /// </summary>
    /// <typeparam name="T">Type of the contained geometry</typeparam>
    public interface IGeoContainer<T> : IEnumerable<IGeometry>
        where T : IGeometry
    {
        /// <summary>
        /// Indexing acessors for object
        /// </summary>
        /// <param name="index">index of object</param>
        /// <returns>Geoobject</returns>
        T this[int index] { get; set; }

        /// <summary>
        /// Number of the objects in the container
        /// </summary>
        int Count { get; }

        /// <summary>
        /// Adds new object to the container
        /// </summary>
        /// <param name="geoobject">new object to add</param>
        void Add(T geoobject);
    }

    //Enumerator used for geocontainers
    public class GeoEnumerator<T> : IEnumerator<IGeometry>
        where T : IGeometry
    {
        private int index = 0;
        IGeoContainer<T> parent;

        public GeoEnumerator(IGeoContainer<T> par)
        {
            parent = par;
        }


        #region IEnumerator<IGeometry> Members

        public IGeometry Current
        {
            get { return parent[index]; }
        }

        #endregion

        #region IDisposable Members

        public void Dispose()
        {
        }

        #endregion

        #region IEnumerator Members

        object System.Collections.IEnumerator.Current
        {
            get { return (object)parent[index]; }
        }

        public bool MoveNext()
        {
            index++;
            if (index >= parent.Count)
                return false;
            return true;
        }

        public void Reset()
        {
            index = 0;
        }

        #endregion
    }


    /// <summary>
    /// Generic container with several geometry objects of the same type
    /// </summary>
    /// <typeparam name="T">Required geometry type</typeparam>
    public class GeoContainer<T> : BaseGeometry, IGeoContainer<T>
        where T : IGeometry
    {
        /// <summary>
        /// List of all geometry objects in container
        /// </summary>
        public List<T> GeoObjects;

        /// <summary>
        /// Default constructor
        /// </summary>
        public GeoContainer()
        {
        }

        /// <summary>
        /// XML reader constructor
        /// </summary>
        /// <param name="reader">XML reader</param>
        public GeoContainer(System.Xml.XmlReader reader)
            : base()
        {
            ReadXml(reader);
        }


        #region IGeoContainer<T> Members

        /// <summary>
        /// Indexing acessors for object
        /// </summary>
        /// <param name="index">index of object</param>
        /// <returns>Geoobject</returns>
        public T this[int index]
        {
            get
            {
                return GeoObjects[index];
            }
            set
            {
                GeoObjects[index] = value;
            }
        }

        /// <summary>
        /// Number of the objects in the container
        /// </summary>
        public int Count
        {
            get { return GeoObjects.Count; }
        }

        /// <summary>
        /// Adds new object to the container
        /// </summary>
        /// <param name="geoobject">new object to add</param>
        public virtual void Add(T geoobject)
        {
            GeoObjects.Add(geoobject);
        }

        #endregion

        #region IEnumerable<IGeometry> Members

        public IEnumerator<IGeometry> GetEnumerator()
        {
            return new GeoEnumerator<T>(this);
        }


        #endregion

        #region IEnumerable Members

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return GeoObjects.GetEnumerator();
        }

        #endregion


        /// <summary>
        /// Is object geometric renderable primitive from enumaration PrimitiveTypes?
        /// </summary>
        public override bool IsPrimitive
        { 
            get
            {
                if (Count == 0)
                    return false;
                return this[0].IsPrimitive;
            } 
        }

        /// <summary>
        /// If geometry object is container, it's made of several joined objects
        /// </summary>
        /// <returns>True if container</returns>
        public override bool IsContainer { get { return true; } }

        /// <summary>
        /// Center of the object
        /// </summary>
        public override Vector3 Center
        {
            get
            {
                float x = 0, y = 0, z = 0;
                foreach (var obj in GeoObjects)
                {
                    Vector3 vec = obj.Center;
                    x += vec.X;
                    y += vec.Y;
                    z += vec.Z;
                }

                Vector3 retval = new Vector3(x, y, z);
                if (GeoObjects.Count > 1)
                {
                    retval = retval / GeoObjects.Count;
                }
                return retval;
            }
        }

        /// <summary>
        /// Returns closest distance from the specified point to object
        /// </summary>
        /// <param name="pos">Closest distance to the object</param>
        /// <returns></returns>
        public override float Distance(ref Vector3 pos)
        {
            float maxdist = float.MaxValue;
            foreach (var ob in GeoObjects)
            {
                float dist = ob.Distance(pos);
                if (dist < maxdist)
                    maxdist = dist;
            }
            return maxdist;
        }

        /// <summary>
        /// Returns closest distance to the ray
        /// </summary>
        /// <param name="ray">Ray to check distance to</param>
        /// <param name="frontOnly">Check only objects in front of the origin</param>
        /// <returns>Closest distance to the ray, if frontOnly and object is behind the ray, returns NaN</returns>
        public override float Distance(Ray ray, bool frontOnly = false)
        {
            float maxdist = float.MaxValue;
            foreach (var ob in GeoObjects)
            {
                float dist = ob.Distance(ray, frontOnly);
                if (dist < maxdist)
                    maxdist = dist;
            }
            return maxdist;
        }

        /// <summary>
        /// Returns closest distance from the generic geometric object
        /// </summary>
        /// <param name="obj">Object to check distance with</param>
        /// <returns>Closest distance to the object</returns>
        public override float Distance(IGeometry obj)
        {
            float maxdist = float.MaxValue;
            foreach (var ob in GeoObjects)
            {
                float dist = ob.Distance(obj);
                if (dist < maxdist)
                    maxdist = dist;
            }
            return maxdist;
        }

        /// <summary>
        /// Returns closest distance to the line segment. Generic, from center, OVERRIDE!
        /// </summary>
        ///<param name="pos1">Start of the line</param>
        ///<param name="pos2">end of the line</param>
        /// <returns>Closest distance</returns>
        public override float DistanceLineSegment(ref Vector3 pos1, ref Vector3 pos2)
        {
            float maxdist = float.MaxValue;
            foreach (var ob in GeoObjects)
            {
                float dist = ob.DistanceLineSegment(ref pos1, ref pos2);
                if (dist < maxdist)
                    maxdist = dist;
            }
            return maxdist;
        }

        /// <summary>
        /// Returns closest distance to the sphere
        /// </summary>
        /// <param name="center">center of the sphere</param>
        /// <param name="radius">radius of the sphere</param>
        /// <returns>Closest distance</returns>
        public override float DistanceSphere(ref Vector3 center, float radius)
        {
            float maxdist = float.MaxValue;
            foreach (var ob in GeoObjects)
            {
                float dist = ob.DistanceSphere(ref center, radius);
                if (dist < maxdist)
                    maxdist = dist;
            }
            return maxdist;
        }

        /// <summary>
        /// Returns closest distance to the triangle.
        /// </summary>
        /// <param name="pos1">First corner</param>
        /// <param name="pos2">Second corner</param>
        /// <param name="pos3">Third corner</param>
        /// <returns>Distance</returns>
        public override float DistanceTriangle(ref Vector3 pos1, ref Vector3 pos2, ref Vector3 pos3)
        {
            float maxdist = float.MaxValue;
            foreach (var ob in GeoObjects)
            {
                float dist = ob.DistanceTriangle(ref pos1, ref pos2, ref pos3);
                if (dist < maxdist)
                    maxdist = dist;
            }
            return maxdist;
        }

        /// <summary>
        /// Check for intersection and fills intersection info if intersection is closer to the origin of the ray then value before
        /// </summary>
        /// <param name="inters">Data structure with intersection info</param>
        /// <param name="obj">Ray object</param>
        /// <returns>True if there was an intersection and it was closer then previous stored</returns>
        public override bool GetIntersectionInfo(Ray obj, ref IIntersectionInfo inters)
        {
            bool rval = false;
            IIntersectionInfo inf = IntersectionInfo.CreateNew();
            foreach (var ob in GeoObjects)
            {
                if (GetIntersectionInfo(obj, ref inf))
                {
                    rval = true;
                }
            }
            return rval;
        }

        /// <summary>
        /// Check for intersection and adds intersection info if there was any intersection
        /// </summary>
        /// <param name="ilist">List for intersections</param>
        /// <param name="obj">Ray object</param>
        /// <returns>True if there was an intersection</returns>        
        public override bool GetIntersectionInfo(Ray obj, IntersectionList ilist)
        {
            bool rval = false;
            foreach (var ob in GeoObjects)
            {
                IIntersectionInfo inf = IntersectionInfo.CreateNew();
                if (GetIntersectionInfo(obj, ref inf))
                {
                    ilist.Add(inf);
                    rval = true;
                }
            }
            return rval;
        }



        /// <summary>
        /// Checks for intersection between object and ray
        /// </summary>
        /// <param name="ray"></param>
        /// <returns>True if ray intersects the object</returns>
        public override bool Intersects(Ray ray)
        {
            foreach (var ob in GeoObjects)
            {
                if (ob.Intersects(ray))
                    return true;
            }
            return false;
        }

        /// <summary>
        /// Checks for intersection with generic geo object
        /// </summary>
        /// <param name="obj">Object to check with</param>
        /// <returns>True if intersection exists</returns>
        public override bool Intersects(IGeometry obj)
        {
            foreach (var ob in GeoObjects)
            {
                if (ob.Intersects(obj))
                    return true;
            }
            return false;
        }


        /// <summary>
        /// Checks if specified position is inside the object
        /// </summary>
        /// <param name="pos">Position to check</param>
        /// <returns>True if vector is inside the object</returns>
        public override bool ContainsPosition(Vector3 pos)
        {
                foreach (var ob in GeoObjects)
                {
                    if (ob.ContainsPosition(pos))
                        return true;
                }
                return false;
        }


        /// <summary>
        /// Returns distance to the intersection between object and ray
        /// </summary>
        /// <param name="ray"></param>
        /// <returns>Distance from the origin to the point of intersection or double.NaN if no intersection</returns>
        public override float IntersectionDistance(Ray ray)
        {
            IIntersectionInfo inf = IntersectionInfo.CreateNew();
            if (GetIntersectionInfo(ray, ref inf))
            {
                return inf.Distance;
            }
            return float.NaN;
        }

        /// <summary>
        /// Returns closest point of the intersection between object and ray
        /// </summary>
        /// <param name="ray"></param>
        /// <returns>Point of intersection or invalid Vector3 (with double.NaN as values)</returns>
        public override Vector3 IntersectionPoint(Ray ray)
        {
            IIntersectionInfo inf = IntersectionInfo.CreateNew();
            GetIntersectionInfo(ray, ref inf);
            return inf.IntersectionPoint;
        }

        /// <summary>
        /// Checks for intersection between object and moving sphere
        /// </summary>
        ///<param name="radius">Radius of the sphere</param>
        /// <param name="maxdistance">Maximal distance to travel</param>
        /// <param name="ray">Ray describing movement of the sphere</param>
        /// <returns>Distance to the intersection or -1 if no intersection exists</returns>
        public override float IntersectsMoving(float radius, Ray ray, float maxdistance)
        {
            bool hit = false;

            foreach (var ob in GeoObjects)
            {
                float dist = ob.IntersectsMoving(radius, ray, maxdistance);

                if ((dist >= 0) && (dist < maxdistance))
                {
                    maxdistance = dist;
                    hit = true;
                }
            }

            if (hit)
                return maxdistance;
            return -1;
        }



        #region Conversions

        /// <summary>
        /// Preferred primitive type for rendering
        /// </summary>
        public override PrimitiveTypes PreferredPrimitive
        {
            get
            {
                if (GeoObjects.Count == 0)
                {
                    return PrimitiveTypes.Point;
                }
                return GeoObjects[0].PreferredPrimitive;
            }
        }

        /// <summary>
        /// Preferred type of the vertex
        /// </summary>
        public override Type PreferredVertexType
        {
            get
            {
                if (GeoObjects.Count == 0)
                {
                    return typeof(PositionVertex);
                }
                return GeoObjects[0].PreferredVertexType;
            }
        }

        /// <summary>
        /// Convert object to list of points
        /// </summary>
        /// <returns>New geometry object containing point based definition of the object</returns>
        /// <param name="quality">
        /// Quality for parametric objects (0-1). Zero is simples possible, 1 maximal for the current dimension of the object. 
        /// If vertexlimit parameter is used, quality will be reduced to fullfill maximal vertex budget
        /// </param>
        /// <param name="vertextype">Required type of the vertex</param>
        /// <param name="vertexlimit">Maximal number of allowed vertices. If set to zero, there's no limit. 
        /// Used for fully parametric objects or to reduce number of the vertices in complicated objects.</param>
        public override Point3DList ToPoints(Type vertextype, float quality = 0, int vertexlimit = 0)
        {
            var pointlist = VertexFactory.CreateList(vertextype, 100);            

            vertexlimit = vertexlimit / GeoObjects.Count;

            foreach (var geo in GeoObjects)
            {
                Point3DList plist = geo.ToPoints(vertextype, quality, vertexlimit);
                pointlist.AddArray(plist.Vertices);
            }

            return new Point3DList(pointlist);
        }

        /// <summary>
        /// Convert object to list of lines
        /// </summary>
        /// <returns>New geometry object containing edges of the object</returns>
        /// <param name="quality">
        /// Quality for parametric objects (0-1). Zero is simples possible, 1 maximal for the current dimension of the object. 
        /// If vertexlimit parameter is used, quality will be reduced to fullfill maximal vertex budget
        /// </param>
        /// <param name="vertextype">Required type of the vertex</param>
        /// <param name="vertexlimit">Maximal number of allowed vertices. If set to zero, there's no limit. 
        /// Used for fully parametric objects or to reduce number of the vertices in complicated objects.</param>
        public override LineList ToLines(Type vertextype, float quality = 0, int vertexlimit = 0)
        {
            var pointlist = VertexFactory.CreateList(vertextype, 100);            

            vertexlimit = vertexlimit / GeoObjects.Count;

            foreach (var geo in GeoObjects)
            {
                LineList plist = geo.ToLines(vertextype, quality, vertexlimit);
                pointlist.AddArray(plist.Vertices);
            }

            return new LineList(pointlist);
        }

        /// <summary>
        /// Convert object to list of triangles. Valid only for surface based objects, otherwise returns null.
        /// </summary>
        /// <returns>New geometry object containing triangle faces of the object surface.</returns>
        /// <param name="quality">
        /// Quality for parametric objects (0-1). Zero is simples possible, 1 maximal for the current dimension of the object. 
        /// If vertexlimit parameter is used, quality will be reduced to fullfill maximal vertex budget
        /// </param>
        /// <param name="vertextype">Required type of the vertex</param>
        /// <param name="vertexlimit">Maximal number of allowed vertices. If set to zero, there's no limit. 
        /// Used for fully parametric objects or to reduce number of the vertices in complicated objects.</param>
        public override TriangleList ToTriangles(Type vertextype, float quality = 0, int vertexlimit = 0)
        {
            var pointlist = VertexFactory.CreateList(vertextype, 100);

            vertexlimit = vertexlimit / GeoObjects.Count;

            foreach (var geo in GeoObjects)
            {
                TriangleList plist = geo.ToTriangles(vertextype, quality, vertexlimit);
                pointlist.AddArray(plist.Vertices);
            }

            return new TriangleList(pointlist);
        }

        /// <summary>
        /// Convert object to indexed triangle mesh. Valid only for surface based objects, otherwise returns null.
        /// </summary>
        /// <returns>New geometry object containing triangle faces of the object surface</returns>
        /// <param name="quality">
        /// Quality for parametric objects (0-1). Zero is simples possible, 1 maximal for the current dimension of the object. 
        /// If vertexlimit parameter is used, quality will be reduced to fullfill maximal vertex budget
        /// </param>
        /// <param name="vertextype">Required type of the vertex</param>
        /// <param name="vertexlimit">Maximal number of allowed vertices. If set to zero, there's no limit. 
        /// Used for fully parametric objects or to reduce number of the vertices in complicated objects.</param>
        public override IndexedMesh ToIndexedMesh(Type vertextype, float quality = 0, int vertexlimit = 0)
        {
            //Note!!! optimalize (reduce vertices etc, subobjects might be better with indexeds etc
            TriangleList tl = ToTriangles(vertextype, quality, vertexlimit);
            return tl.ToIndexedMesh(vertextype);
        }


        #endregion

        /// <summary>
        /// Reads container from xml
        /// </summary>
        /// <param name="reader">XML reader</param>
        public override void ReadXml(System.Xml.XmlReader reader)
        {
            if (reader["type"] != this.GetType().Name)
                throw new ArgumentException("IGeometry:ReadXML - wrong xml node type!");
            while (reader.Read())
            {
                if (reader.IsStartElement())
                {
                    T geo = GeoFactory.ReadXml<T>(reader);
                    if (geo != null)
                        Add(geo);
                }
                else return;
            }
        }

        /// <summary>
        /// Writes container to xml
        /// </summary>
        /// <param name="writer">XML writer</param>
        public override void WriteXml(System.Xml.XmlWriter writer)
        {
            writer.WriteAttributeString("type", this.GetType().Name);
            foreach (var geo in GeoObjects)
            {
                writer.WriteStartElement("GeoObject");
                geo.WriteXml(writer);
                writer.WriteEndElement();
            }
        }


        /// <summary>
        /// Returns copy of the geometric data
        /// </summary>
        /// <returns>Full copy of the geometry</returns>
        public override IGeometry GetCopy()
        {
            GeoContainer<T> rval = new GeoContainer<T>();

            foreach (var geo in GeoObjects)
            {
                rval.Add((T)geo.GetCopy());
            }
            return rval;
        }

    }

    /// <summary>
    /// Geometry container with mixed types of geometry objects
    /// </summary>
    public class GeoContainer : GeoContainer<IGeometry>
    {

        /// <summary>
        /// XML reader constructor
        /// </summary>
        /// <param name="reader">XML reader</param>
        public GeoContainer(System.Xml.XmlReader reader)
            : base(reader)
        {
        }

        /// <summary>
        /// Returns true if all objects in container are primitive.
        /// </summary>
        /// <returns>True if geo objects are primitive</returns>
        public override bool IsPrimitive
        {
            get
            {
                foreach (var x in GeoObjects)
                {
                    if (!x.IsPrimitive)
                        return false;
                }
                return true;
            }
        }

        public override PrimitiveTypes PreferredPrimitive
        {
            get
            {
                return this[0].PreferredPrimitive;
            }
        }
    }


}
