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 CliffPolygon : ITerrainPolygon, ISerializable {
        [Flags]
        public enum Side { Undefined = 0, Right = 0x01, Left = 0x02, Diagonal = 0x03 }

        static Dictionary<ushort, CliffPolygon> stock = new Dictionary<ushort, CliffPolygon>(256);
        static CliffPolygon[] fillers = new CliffPolygon[4];
        // To enhance brightness, use another sun light vector which has only holizontal elements.
        static protected Vect3D SunLight2D;
        static public readonly CliffPolygon LeftUnitCliff;
        static public readonly CliffPolygon RightUnitCliff;
        static public readonly CliffPolygon DiagonalUnitCliff;

        static CliffPolygon() {
#if DEBUG
            // TODO: 太陽光ベクトルをコントラストくっきりするように暫定調整
            SunLight2D = new Vect3D(0.9, 0.2, 0.0/*Geocon.SunLight*/);
#else
            SunLight2D = new Vect3D(Geocon.SunLight);
#endif
            SunLight2D.Z = 0;
            SunLight2D.Normalize();
            int step = Geocon.TerrainStepHeightInUnit;
            initPatterns(step, Geocon.TerrainMaxHeightInUnit);
            LeftUnitCliff = GetPolygon(step, step, -1);
            RightUnitCliff = GetPolygon(step, -1, step);
            DiagonalUnitCliff = GetPolygon(-1, step, step);
        }

        #region initialize polygon pattern table
        private static void initPatterns(int step, int max) {
            for (int p2 = step; p2 <= max; p2 += step) {
                registerTriPattern(0, p2, -1);
                registerTriPattern(0, -1, p2);
                registerTriPattern(-1, 0, p2);
                registerTriPattern(p2, 0, -1);
                registerTriPattern(p2, -1, 0);
                registerTriPattern(-1, p2, 0);
            }
            for (int p1 = step; p1 <= max; p1 += step) {
                for (int p2 = p1; p2 <= max; p2 += step) {
                    registerRectPattern(p1, p2, -1);
                    registerRectPattern(p1, -1, p2);
                    registerRectPattern(-1, p1, p2);
                    registerRectPattern(p2, p1, -1);
                    registerRectPattern(p2, -1, p1);
                    registerRectPattern(-1, p2, p1);
                }
            }
            fillers[(int)Side.Left] = CreateFillerPattern(-1, 0);
            fillers[(int)Side.Right] = CreateFillerPattern(0, -1);
            fillers[(int)Side.Diagonal] = CreateFillerPattern(-1, -1);
        }

        /// <summary>
        /// One of the argument should be negative as it means unused point.
        /// Another one should be zero, and the last one is positive, 
        /// so that they form a triangle with the holizontal line.
        /// </summary>
        /// <param name="near"></param>
        /// <param name="left"></param>
        /// <param name="right"></param>
        /// <param name="far"></param>
        private static void registerTriPattern(int near, int left, int right) {
            ushort id = PrivateMakePolygonID(near, left, right);
            int hp = Geocon.UnitHeightPixel;
            Side s = Side.Undefined;
            if (!stock.ContainsKey(id)) {
                Point3D[] pt = new Point3D[3];
                int idx = 0;
                int id2 = 0;
                if (left >= 0) {
                    s |= Side.Left;
                    pt[idx++] = new Point3D(0, Geocon.UnitWidthPixel, left * hp);
                    if (left > 0) {
                        id2 = idx;
                        pt[idx++] = new Point3D(0, Geocon.UnitWidthPixel, 0);
                    }
                }
                if (near >= 0) {
                    pt[idx++] = new Point3D(0, 0, near * hp);
                    if (near > 0) {
                        id2 = idx;
                        pt[idx++] = new Point3D(0, 0, 0);
                    }
                }
                if (right >= 0) {
                    s |= Side.Right;
                    pt[idx++] = new Point3D(Geocon.UnitWidthPixel, 0,  right * hp);
                    if (right > 0) {
                        id2 = idx;
                        pt[idx++] = new Point3D(Geocon.UnitWidthPixel, 0, 0);
                    }
                }
                if (id2 == 1) {// swap to be clock wise oreder.
                    Point3D tmp = pt[1];
                    pt[1] = pt[2];
                    pt[2] = tmp;
                }
                stock.Add(id, new CliffPolygon(id, pt, s));
            }
        }

        /// <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 registerRectPattern(int near, int left, int right) {
            ushort id = PrivateMakePolygonID(near, left, right);
            int hp = Geocon.UnitHeightPixel;
            Side s = Side.Undefined;
            if (!stock.ContainsKey(id)) {
                Point3D[] pt = new Point3D[6];
                int idx = 0;
                if (left >= 0) {
                    s |= Side.Left;
                    pt[idx++] = new Point3D(0, Geocon.UnitWidthPixel, left * hp);
                    pt[idx] = new Point3D(0, Geocon.UnitWidthPixel, 0);
                    idx += 2;
                }
                if (near >= 0) {
                    pt[idx++] = new Point3D(0, 0, near * hp);
                    pt[idx] = new Point3D(0, 0, 0);
                    idx += 2;
                }
                if (right >= 0) {
                    s |= Side.Right;
                    pt[idx++] = new Point3D(Geocon.UnitWidthPixel, 0, right * hp);
                    pt[idx] = new Point3D(Geocon.UnitWidthPixel, 0, 0);
                    idx += 2;
                }
                // fill indices 2&5 to be a pair of triangles (0-1-2:3-4-5).
                pt[5] = pt[0];
                pt[2] = pt[1]; // swap to be clock wise oreder.
                pt[1] = pt[4];
                stock.Add(id, new CliffPolygon(id, pt, s));
            }
        }

        /// <summary>
        /// Create filler
        /// </summary>
        /// <param name="near"></param>
        /// <param name="left"></param>
        /// <param name="right"></param>
        /// <param name="far"></param>
        private static CliffPolygon CreateFillerPattern(int bl_left, int bl_right) {
            Side s = Side.Undefined;
            const int hmax = 0x7fffffff;
            Point3D[] pt = new Point3D[6];
            int idx = 0;
            
            if (bl_left != 0) {
                s |= Side.Left;
                pt[idx++] = new Point3D(0, Geocon.UnitWidthPixel, hmax);
                pt[idx] = new Point3D(0, Geocon.UnitWidthPixel, 0);
                idx += 2;
            }
            if (bl_left != bl_right) { // front point required
                pt[idx++] = new Point3D(0, 0, hmax);
                pt[idx] = new Point3D(0, 0, 0);
                idx += 2;
            }
            if (bl_right != 0) {
                s |= Side.Right;
                pt[idx++] = new Point3D(Geocon.UnitWidthPixel, 0, hmax);
                pt[idx] = new Point3D(Geocon.UnitWidthPixel, 0, 0);
                idx += 2;
            }
            // fill indices 2&5 to be a pair of triangles (0-1-2:3-4-5).
            //pt[2] = pt[3];
            //pt[5] = pt[4]; // swap to be clock wise oreder.
            //pt[4] = pt[1];
            pt[5] = pt[0];
            pt[2] = pt[1]; // swap to be clock wise oreder.
            pt[1] = pt[4];

            ushort id = PrivateMakeFillerID(0, bl_left, bl_right);
            return new CliffPolygon(id, pt, s);
        }

        /*
        /// <summary>
        /// </summary>
        /// <param name="near"></param>
        /// <param name="left"></param>
        /// <param name="right"></param>
        /// <param name="far"></param>
        private static void registerFillerPattern(int height, int bl_left, int bl_right) {
            ushort id = PrivateMakeFillerID(height, bl_left, bl_right);
            int hp = Geocon.UnitHeightPixel;
            Side s = Side.Undefined;
            if (!stock.ContainsKey(id)) {
                Point3D[] pt = new Point3D[6];
                int idx = 0;
                if (bl_left != 0) {
                    s |= Side.Left;
                    pt[idx++] = new Point3D(0, height * hp, Geocon.UnitWidthPixel);
                    pt[idx] = new Point3D( Geocon.UnitWidthPixel, 0, 0);
                    idx += 2;
                }
                if ((bl_left ^ bl_right)==0) { // front point required
                    pt[idx++] = new Point3D(0, 0, height * hp);
                    pt[idx] = new Point3D(0, 0, 0);
                    idx += 2;
                }
                if (bl_right != 0) {
                    s |= Side.Right;
                    pt[idx++] = new Point3D(0, Geocon.UnitWidthPixel, height * hp);
                    pt[idx] = new Point3D(0, Geocon.UnitWidthPixel, 0);
                    idx += 2;
                }
                // fill indices 2&5 to be a pair of triangles (0-1-2:3-4-5).
                pt[2] = pt[3];
                pt[5] = pt[4]; // swap to be clock wise oreder.
                pt[4] = pt[1];
                stock.Add(id, new CliffPolygon(id, pt, s));
            }
        }

        private static void MakeAliasForFiller(int height, int bl_left, int bl_right) {
            int left = -height * bl_left;
            int right = -height * bl_right;
            int near = height & (bl_right ^ bl_left);
            ushort id1 = PrivateMakePolygonID(near, left, right);
            CliffPolygon v;
            if (stock.TryGetValue(id1, out v)) {
                ushort id2 = PrivateMakeFillerID(height, bl_left, bl_right);
                stock.Add(id2, v);
            }
        }
        */
        #endregion

        public readonly ushort id;
        protected Point[] verticis;
        protected Point3D[] verticis3d;
        protected Rectangle bounds;
        protected Side side;
        /// <summary>
        /// Determined by surface normal angle between the sun light. 255 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 CliffPolygon(ushort id, Point3D[] pts, Side s) {
            this.id = id;
            this.verticis3d = pts;
            this.side = s;
            this.bounds = new Rectangle();
            this.verticis = Point3D.ConvertToQuarterViewPos(pts, ref bounds);
            Vect3D normal = Vect3D.CalcNormalVector(pts);
            double inprd = normal.InnerProduct(SunLight2D);
            if (inprd > 0) {
                // inverted plane
                normal.Multiple(-1.0);
                inprd = -inprd;
            }
            double l = Geocon.EnvironmentLight;
            l += (l - 1.0) * inprd;
            Brightness = (float)l;
        }

        #region ITerrainPolygon メンバ

        public ushort ID {
            get { return id; }
        }

        /// <summary>
        /// Returns 2D bounds rectangle which contains all verticis points.
        /// </summary>
        public Rectangle GetBounds(Scaler sc) {
            Rectangle ret = sc.Scale(bounds);
            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);
            return ret;
        }

        /// <summary>
        /// Create taller mesh using fillers to be a specific height.
        /// Slightly complicated process.
        /// </summary>
        /// <param name="sc"></param>
        /// <param name="baseheight"></param>
        /// <returns></returns>
        public Point3D[] GetVerticis3D_JackedUpTo(int baseheight) {
            int l = verticis3d.Length;
            Point3D[] ret = new Point3D[6 + l];
            CliffPolygon cp;            
            if (baseheight > 0) {
                cp = fillers[(int)this.side];
                for (int j = 0; j < 6; j++) {
                    ret[j] = cp.verticis3d[j];
                    // upper Y value should be -1
                    ret[j].Z &= baseheight;
                }
            }
            for (int j = 0; j < l; j++) {
                ret[6 + j] = verticis3d[j];
                ret[6 + j].Z += baseheight;
            }
            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, CliffPolygon>.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 CliffPolygon TryGetPolygon(ushort id) {
            CliffPolygon 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 CliffPolygon GetPolygon(ushort id) {
            return stock[(ushort)id];
        }

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

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

        private static ushort PrivateMakePolygonID(int front, int left, int right) {
            return (ushort)((right & 0x000f) + ((front & 0x000f) << 4) + ((left & 0x000f) << 8) + 0xf000);
        }

        private static ushort PrivateMakeFillerID(int height, int bl_left, int bl_right) {
            return (ushort)((height & 0x03ff)<<2 + (bl_left & 0x02) + (bl_right & 0x01));
        }

        private static ushort PrivateMakeFillerID(int height, Side side) {
            return (ushort)(((height & 0x03ff)<<2) + side);
        }

        // 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];
            }
        }

    }
}
