using System;
using System.Runtime.InteropServices;

using nft.core.geometry;
using nft.core.game;
using System.Runtime.Serialization;
using System.Diagnostics;
using System.Collections.Generic;

using Geocon = nft.core.geometry.GeometricConstants;
using ICDir = nft.core.geometry.InterCardinalDirection;
using nft.framework.drawing;

namespace nft.impl.game {
    /// <summary>
    /// represents the slope shape above BaseHeight.
    /// </summary>
    public class TerrainPieceTemplate : ITerrainPiece, ISerializable {
        static Dictionary<ushort, TerrainPieceTemplate> stock
            = new Dictionary<ushort, TerrainPieceTemplate>(256);

        protected PolygonRotationTable rotateTable;
        public readonly ushort ID;
        protected readonly int[] heights;
        protected readonly int maxHeight;
        protected readonly int meanHeight;
        protected readonly ushort ocmask;

        static TerrainPieceTemplate() {
            initPatterns(Geocon.TerrainStepHeightInUnit, Geocon.TerrainMaxHeightInUnit);
        }
        #region initialize polygon pattern table
        private static void initPatterns(int step, int max) {
            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;
                        registerRotatePatterns(p1, p2, p3);
                    }
                }
            }
        }
        /// <summary>
        /// register 4 triangles rotate toward each ICDirs.
        /// </summary>
        /// <param name="near"></param>
        /// <param name="left"></param>
        /// <param name="right"></param>
        /// <param name="far"></param>
        private static void registerRotatePatterns(int p1, int p2, int p3) {
            ushort[] ids = new ushort[4];
            TerrainPolygonSet[] tps = new TerrainPolygonSet[4];
            ids[Direction.ToZeroBaseIndex(ICDir.NORTHEAST)]
                = MakeTemplateID(-1, p1, p2, p3);
            ids[Direction.ToZeroBaseIndex(ICDir.SOUTHEAST)]
                = MakeTemplateID(p3, -1, p1, p2);
            ids[Direction.ToZeroBaseIndex(ICDir.SOUTHWEST)]
                = MakeTemplateID(p2, p3, -1, p1);
            ids[Direction.ToZeroBaseIndex(ICDir.NORTHWEST)]
                = MakeTemplateID(p1, p2, p3, -1);
            for (int i = 0; i < 4; i++) {
                tps[i] = TerrainPolygonSet.GetInstance(ids[i]);
            }
            foreach (ICDir dir in Enum.GetValues(typeof(ICDir))) {
                int ix = Direction.ToZeroBaseIndex(dir);
                ushort id = ids[ix];
                if (!stock.ContainsKey(id)) {
                    int[] h = new int[4];
                    h[Direction.ToZeroBaseIndex(dir)] = p2;
                    h[Direction.ToZeroBaseIndex(Direction.GetLeftOf(dir))] = p1;
                    h[Direction.ToZeroBaseIndex(Direction.GetRightOf(dir))] = p3;
                    h[Direction.ToZeroBaseIndex(Direction.GetOppositeOf(dir))] = -1;
                    stock.Add(id, new TerrainPieceTemplate(id, h, tps, dir));
                }
            }
        }
        private static void registerPatterns(int[] heights) {

        }
        #endregion

        [Obsolete]
        public static ushort MakeTemplateIDX(int ne, int nw, int sw, int se){
            throw new NotSupportedException();
            return GroundPolygon.MakePolygonID(sw, nw, ne, se);
        }

        public static ushort MakeTemplateID(int near, int left, int far, int right) {
            return GroundPolygon.MakePolygonID(near, left, right, far);
        }

        public static TerrainPieceTemplate GetInstance(ushort id){
            return stock[id];
        }

        private TerrainPieceTemplate(ushort id, int[] heights, TerrainPolygonSet[] tpset, ICDir dir) {
            this.ID = id;
            this.rotateTable = new PolygonRotationTable(tpset, dir);
            this.heights = heights;
            GroundPolygon terrain = tpset[0].Ground;
            int max = 0;
            int mean = 0;
            for (int i = 0; i < 4; i++) {
                if (heights[i] < 0) continue;
                mean += heights[i];
                if (max < heights[i]) {
                    max = heights[i];
                }
            }
            this.maxHeight = max;
            this.meanHeight = mean / 3;
            this.ocmask = OccupationMaskLibrary.GetCellDiagonalMask(dir);
            Debug.Assert(this.GetOffsetHeightAt(Direction.GetOppositeOf(dir)) == -1);
        }

        public TerrainPolygonSet GetPolygons(ICDir viewUpper) {
            return rotateTable.Get(viewUpper);
        }

        internal int[] CloneHeightArray() { return (int[])heights.Clone(); }
        
        #region implementation of ITerrainPiece
        public TerrainPieceTemplate Template {
            get { return this; }
        }

        public int BaseHeight {
            get { return 0; }
            set { throw new InvalidOperationException("cannot set BaseHeight of TerrainPieceTemplate object"); }
        }

        public int MaxHeight { get { return maxHeight; } }

        public int MeanHeight { get { return meanHeight; } }

        public ushort OccupationMask { get { return ocmask; } }

        public int GetHeightAt(ICDir dir) {
            return BaseHeight + heights[Direction.ToZeroBaseIndex(dir)];
        }

        public int GetOffsetHeightAt(ICDir dir) {
            return heights[Direction.ToZeroBaseIndex(dir)];
        }

        #endregion

        #region ISerializable implement
        // 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 GetInstance(id);
            }
        }
        #endregion

        #region internal classes
        protected class PolygonRotationTable {
            private readonly TerrainPolygonSet[] array;
            private readonly int baseIdx;
            internal PolygonRotationTable(TerrainPolygonSet[] array, ICDir dir) {
                this.array = array;
                this.baseIdx = Direction.ToZeroBaseIndex(dir);
            }

            public TerrainPolygonSet Get(ICDir ViewDir) {
                int i = Direction.ToZeroBaseIndex(ViewDir);
                if ((i & 1) == 1) {
                    // なぜかこうしないとパターンが合わない
                    // たぶんビュー視点の左右方向と、
                    // マップ空間における特定方角に向かっての左右が入れ替わるため
                    // I'm not sure why this code is necessary to get proper pattern.
                    // Perhaps its difference between L/R on display and on rotated world direction.
                    i ^= 2;
                }

                i = (i + baseIdx) % 4;
                return array[i];
            }
        }

        public class TerrainPolygonSet {
            static private Dictionary<ushort, TerrainPolygonSet> chache
                = new Dictionary<ushort, TerrainPolygonSet>(256);

            public static TerrainPolygonSet GetInstance(ushort id) {
                TerrainPolygonSet set;
                if (!chache.TryGetValue(id, out set)) {
                    set = new TerrainPolygonSet(id);
                    chache.Add(id, set);
                }
                return set;
            }

            public readonly GroundPolygon Ground;
            public readonly CliffPolygon CliffLeft;
            public readonly CliffPolygon CliffRight;
            public readonly CliffPolygon CliffDiagonal;
            protected readonly ushort[] hcmask;

            /// <summary>
            /// true if this piece occupies a half of holizontaly splitted cell.
            /// (not a half of verticaly splitted cell)
            /// </summary>
            public readonly bool HorzSplitted;

            public ViewDirection8 DiagonalSide{
                get{ return Ground.DiagonalSide; }
            }

            public ushort ID { get { return Ground.ID; } }

            private TerrainPolygonSet(ushort id) {
                // note: will throw an exception if specified ID is not registerd.
                Ground = GroundPolygon.GetPolygon(id);
                CliffLeft = CliffPolygon.TryGetPolygon((ushort)(id | 0xf00f));
                CliffRight = CliffPolygon.TryGetPolygon((ushort)(id | 0xff00));
                CliffDiagonal = CliffPolygon.TryGetPolygon((ushort)(id | 0xf0f0));
#if DEBUG
                Debug.Write("Ground=" + id.ToString("X4"));
                Debug.Write(",Left=" + (CliffLeft != null ? CliffLeft.ID.ToString("X4") : "null"));
                Debug.Write(",Right=" + (CliffRight != null ? CliffRight.ID.ToString("X4") : "null"));
                //Debug.Write(",Diagonal=" + (CliffDiagonal != null ? CliffDiagonal.ID.ToString("X4") : "null"));
                Debug.WriteLine(",Side=" + Ground.DiagonalSide);
#endif
                int u = id;
                int ds = 0;
                int vmax = 0;
                int[] hts = new int[4];
                for (int i = 3; i >= 0; i--) {
                    int v = u & 0xf;
                    u >>= 4;
                    hts[i] = v;
                    if (v == 0xf) ds = i;
                    else if (vmax < v) vmax = v;
                }
                ViewDirection8 diag = Ground.DiagonalSide;
                HorzSplitted = (diag==ViewDirection8.FRONT||diag==ViewDirection8.BEHIND);
                /*
                int hs = Geocon.TerrainStepHeightInUnit;
                int hc = (vmax / hs) - 1;
                if (hc > 0) {
                    this.hcmask = new ushort[hc];
                    int hn = hts[ds ^ 2];
                    int hr = hts[(ds - 1) & 3];
                    int hl = hts[(ds + 1) & 3];
                    for (int i = 0; i < hc; i++) {
                        hn -= hs; hr -= hs; hl -= hs;
                        hcmask[i] = HeightCutMask.FindBestMatchMask(directions[ds^ 2], hr, hn, hl);
                    }
                } else {
                    this.hcmask = new ushort[0];
                }
                 */
                //Debug.WriteLine(String.Format("tid={0:x4}, DiagSide={1}",id,Enum.GetName(typeof(ViewDirection),DiagonalSide)));
            }
        }
        #endregion
    }
}
