using System;
using System.Collections.Generic;
using System.Text;
using System.Collections;
using System.Diagnostics;
using System.Drawing;
using System.Runtime.Serialization;
using nft.framework.drawing;

using Geocon = nft.core.geometry.GeometricConstants;

namespace nft.core.geometry {
    public class GroundPolygon : ITerrainPolygon, ISerializable {
        static Dictionary<ushort, GroundPolygon> stock = new Dictionary<ushort, GroundPolygon>(256);
        
        static GroundPolygon() {
            initPatterns(Geocon.TerrainStepHeightInUnit, Geocon.TerrainMaxHeightInUnit);
        }
        #region initialize polygon pattern table
        private static void initPatterns(int step, int max) {
            //registerRectPattern(0, 0, 0, 0);
            for (int p1 = 0; p1 <= max; p1 += step) {
                for (int p2 = 0; p2 <= max; p2 += step) {
                    for (int p3 = 0; p3 <= max; p3 += step) {
                        if (p1 * p2 * p3 > 0) continue;
                        registerTrianglePattern(p1, p2, p3, -1);
                        registerTrianglePattern(p1, p2, -1, p3);
                        registerTrianglePattern(p1, -1, p2, p3);
                        registerTrianglePattern(-1, p1, p2, p3);
                    }
                }
            }
        }
        /// <summary>
        /// Note that normal vector of plane will be determined by first 3 points.
        /// All points should be on the same plane.
        /// </summary>
        /// <param name="near"></param>
        /// <param name="left"></param>
        /// <param name="right"></param>
        /// <param name="far"></param>
        [Obsolete]
        private static void registerRectPattern(int near, int left, int far, int right) {
            ushort id = PrivateMakePolygonID(near, left, right, far);
            int hp = Geocon.UnitHeightPixel;
            if (!stock.ContainsKey(id)) {
                Point3D[] pt = new Point3D[4];
                int idx = 0;
                pt[idx++] = new Point3D(Geocon.UnitWidthPixel, 0, left * hp);
                pt[idx++] = new Point3D(0, 0, near * hp);
                pt[idx++] = new Point3D(0, Geocon.UnitWidthPixel, right * hp);
                pt[idx++] = new Point3D(Geocon.UnitWidthPixel, Geocon.UnitWidthPixel, far * hp);
                stock.Add(id, new GroundPolygon(id, pt,ViewDirection8.UNDEFINED));
            }
        }
        /// <summary>
        /// One of the argument should be negative so that which means unused point.
        /// </summary>
        /// <param name="near"></param>
        /// <param name="left"></param>
        /// <param name="right"></param>
        /// <param name="far"></param>
        private static void registerTrianglePattern(int near, int left, int far, int right) {
            ushort id = PrivateMakePolygonID(near, left, right, far);
            int hp = Geocon.UnitHeightPixel;
            ViewDirection8 diagside = ViewDirection8.UNDEFINED;
            if (!stock.ContainsKey(id)) {
                Point3D[] pt = new Point3D[3];
                int idx = 0;
                if (near >= 0)
                    pt[idx++] = new Point3D(0, 0, near * hp);
                else
                    diagside = ViewDirection8.FRONT;
                if (left >= 0)
                    pt[idx++] = new Point3D(0, Geocon.UnitWidthPixel, left * hp);
                else
                    diagside = ViewDirection8.LEFT;
                if (far >= 0)
                    pt[idx++] = new Point3D(Geocon.UnitWidthPixel, Geocon.UnitWidthPixel, far * hp);
                else
                    diagside = ViewDirection8.BEHIND;
                if (right >= 0)
                    pt[idx++] = new Point3D(Geocon.UnitWidthPixel, 0, right * hp);
                else
                    diagside = ViewDirection8.RIGHT;
                stock.Add(id, new GroundPolygon(id, pt, diagside));
            }
        }
        #endregion

        public readonly ushort id;
        protected Point[] verticis;
        protected Point3D[] verticis3d;
        protected Rectangle bounds;
        protected Vect3D normal;
        protected int product;
        protected ViewDirection8 diagSide;

        /// <summary>
        /// Determined by surface normal angle between the sun light. 1.0f is maximum.
        /// Visible surface has positive value, effected at least by environment light.
        /// Zero value means inverted or perpendicular surface against camera angle.
        /// </summary>
        public readonly float Brightness;

        protected GroundPolygon(ushort id, Point3D[] pts, ViewDirection8 diagonalside) {
            this.id = id;
            this.diagSide = diagonalside;
            this.verticis3d = pts;
            this.bounds = new Rectangle();
            this.verticis = Point3D.ConvertToQuarterViewPos(pts, ref bounds);
            Debug.Assert(diagSide != ViewDirection8.UNDEFINED);
            //Debug.WriteLine("Ground[" + id.ToString("X4") + "] diagside=" + diagSide);
            normal = Vect3D.CalcNormalVector(pts);
            if (normal.Z < 0.0) {
                //Debug.Assert(id!=0, string.Format("Inverted vertex order for GroundTerrain#{0:x4}", id));
                //Point3D p2 = verticis3d[1];
                //verticis3d[1] = verticis3d[0];
                //verticis3d[0] = p2;
                normal.Multiple(-1.0);
            }
            double inprd = normal.InnerProduct(Geocon.CameraEye);
            product = -Math.Sign(inprd);
            if (IsParallelToEye) {
                Point left = verticis[0];
                Point right = verticis[0];
                for (int i = 1; i < verticis.Length; i++) {
                    if (verticis[i].X < left.X)
                        left = verticis[i];
                    if (verticis[i].X > right.X)
                        right = verticis[i];
                }
                this.verticis = new Point[] { left, right };
            }
            inprd = normal.InnerProduct(Geocon.SunLight);
            double l = Geocon.EnvironmentLight;
            l += (l - 1.1) * inprd;
            Brightness = IsVisible ? (float)l : 0f;
        }

        public ViewDirection8 DiagonalSide {
            get {
                return diagSide;
            }
        }

        /// <summary>
        /// Calculate height at specific point on isometric coordination (px,py), in pixel.
        /// Used to determin z-offset of billboard (tree?) on the slope.
        /// </summary>
        /// <param name="px"></param>
        /// <param name="py"></param>
        /// <returns></returns>
        public double CalcHeigthAtInnerPoint(int px, int py) {
            Point3D p = verticis3d[0];
            if (p.X == px && p.Y == py) p = verticis3d[1]; // select another
            return p.Z + (double)(-normal.X * (px - p.X) - normal.Y * (py - p.Y)) / normal.Z;
        }

        #region ITerrainPolygon メンバ

        public ushort ID {
            get { return id; }
        }

        public bool IsVisible { get { return product>=0; } }
        /// <summary>
        /// Returns true if this polygon is parallel to camera eye.
        /// Witch means that the polygon looks like a segment of line.
        /// </summary>
        public bool IsParallelToEye { get { return product == 0; } }

        /// <summary>
        /// Returns 2D bounds rectangle which contains all verticis points.
        /// </summary>
        public Rectangle GetBounds(Scaler sc) {
            Rectangle ret = sc.Scale(bounds);
            if (ret.Height == 0) {
                // If this polygon is perpendicular to the camera angle,
                // make 'thickness' to make visible as thin line.
                ret.Height = 1;
            }
            return ret;
        }

        /// <summary>
        /// Calc verticis according to the ViewFactor. A bit heavy process.
        /// </summary>
        /// <param name="vf"></param>
        /// <returns></returns>
        public Point[] GetVerticis(Scaler sc) {
            Point[] ret = sc.Scale(verticis);
            if (verticis.Length == 2) {
                // If this polygon is perpendicular to the camera angle,
                // make 'thickness' to make visible as thin line.
                Point p0 = new Point(ret[0].X, ret[0].Y - 1);
                Point p1 = new Point(ret[1].X, ret[1].Y - 1);
                return new Point[] { p0, ret[0], ret[1], p1 };
            } else {
                return ret;
            }
        }

        public Point3D[] GetVerticis3D()
        {
            return verticis3d.Clone() as Point3D[];
        }
        #endregion

        /// <summary>
        /// Get total count of stock polygon patterns.
        /// </summary>
        public static int StockCount {
            get { return stock.Count; }
        }

        public static Dictionary<ushort, GroundPolygon>.KeyCollection GetAllID() {
            return stock.Keys;
        }

        /// <summary>
        /// Try to get the polygon associated with an ID.
        /// returns null if the ID is not registerd.
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        public static GroundPolygon TryGetPolygon(ushort id) {
            GroundPolygon v;
            if (stock.TryGetValue(id, out v)) {
                return v;
            } else {
                return null;
            }
        }

        /// <summary>
        /// get the polygon associated with an ID.
        /// throw exception if the ID is not registerd.
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        public static GroundPolygon GetPolygon(ushort id) {
            return stock[id];
        }

        public static GroundPolygon GetPolygon(int near, int left, int right, int far) {
            return stock[MakePolygonID(near, left, right, far)];
        }

        public static ushort MakePolygonID(int near, int left, int right, int far) {
            //Debug.Assert(near < 15 && left < 15 && right < 15 && far < 15);
            ushort id = PrivateMakePolygonID(near, left, right, far);
            if (stock.ContainsKey(id)) {
                return id;
            } else {
                throw new IndexOutOfRangeException("No corresponding id.");
            }
        }

        internal static ushort PrivateMakePolygonID(int near, int left, int right, int far) {
            return (ushort)((right & 0x000f) + ((near & 0x000f) << 4) + ((left & 0x000f) << 8) + ((far & 0x000f) << 12));
        }

        // serialize this object by reference
        public virtual void GetObjectData(SerializationInfo info, StreamingContext context) {
            info.SetType(typeof(ReferenceImpl));
            info.AddValue("id", ID);
        }


        [Serializable]
        internal sealed class ReferenceImpl : IObjectReference {
            private ushort id = 0xffff;
            public object GetRealObject(StreamingContext context) {
                return stock[id];
            }
        }
    }
}
