using System;
using System.Collections.Generic;
using System.Text;
using nft.core.geometry;
using nft.impl.game;
using nft.core;
using System.Drawing;
using nft.framework.drawing;
using System.Diagnostics;
using Geocon = nft.core.geometry.GeometricConstants;
using nft.core.game;

namespace nft.impl.view
{
    public class SceneBuilder : IEnumerable<I3DObject>
    {
        public delegate void TerrainPlateModifier(ITerrainPlate plate, ITerrainMap map, int x, int y, TerrainPieceTemplate.TerrainPolygonSet tpset);

        CoordinationUtil cdUtil = null;
        Scaler scaler;
        InterCardinalDirection upperDir;

        IApparentAssignor apparents;
        GameImpl game;
        ISurface surface;
        Point viewpos;
        int voffset;

        public SceneBuilder(GameImpl g) {
            this.game = g;
            SetViewFactor(new ViewFactor(InterCardinalDirection.NORTHEAST));
        }

        public SceneBuilder(GameImpl g, ISurface s) : this(g){
            AttachSurface(s);
        }

        public void AttachSurface(ISurface s) {
            this.surface = s;
            surface.Objects = this;
            apparents = DefaultApparentAssignor.TheInstance;
            viewpos = new Point(0, ContentSize.Height / 2);
            voffset = game.TerrainMap.Size.sz * Geocon.UnitHeightPixel;
        }

        public void SetViewFactor(ViewFactor vf) {
            if (cdUtil != null) {
                if (vf.Scaler.Equals(this.scaler) && vf.ViewDirection.Equals(this.upperDir)) {
                    return;
                }
            }
            this.cdUtil = CoordinationUtil.Create(game.TerrainMap.Size, vf);
            this.scaler = vf.Scaler;
            this.upperDir = vf.ViewDirection;
        }

        public InterCardinalDirection UpperDirection {
            get { return this.upperDir; }
            set { SetViewFactor(new ViewFactor(value)); }
        }

        /// <summary>
        /// Scroll position (2D).
        /// </summary>
        public Point ViewPosition {
            get { return viewpos; }
            set {
                viewpos = value;
            }
        }

        public Size ContentSize {
            get { return game.TerrainMap.Size.ProjectionSize; }
        }

        protected PointF3D CalcCameraPosition() {
            Point pos = ViewPosition;
            return new PointF3D(pos.X, pos.Y, pos.Y - 80  /*+ voffset*/);
        }

        protected void SetCameraPositon(PointF3D campos) {
            Point p = new Point((int)campos.X, (int)campos.Y);
            viewpos = p;
        }

        Stopwatch watch0 = new Stopwatch();
        int xxx = 0;
        public IEnumerator<I3DObject> GetEnumerator() {
            InterCardinalDirection dir = upperDir;
            TerrainMapImpl map = game.TerrainMap;
            Size3D sz = map.Size;

            // terrain assembl loop
            watch0.Reset();
            IEnumerator<Point> en = GetGroundEnumerator();
            while (en.MoveNext()) {
                Point uv = en.Current;
                Point3DV pv = new Point3DV(uv.X * Geocon.UnitWidthPixel, uv.Y * Geocon.UnitWidthPixel, 0);
                Location l = cdUtil.IsometricGridToLocation(uv.X, uv.Y, 0);
                int x = l.X;
                int y = l.Y;
                if (x < 0 || y < 0 || x >= sz.sx || y >= sz.sy) {
                    // out of map
                    continue;
                }
                if (!watch0.IsRunning)
                    watch0.Start();

                ITerrainPlate plate;
                int hgap;
                foreach (ITerrainPiece p in map[x, y, dir]) {
                    pv.VZ = p.BaseHeight * Geocon.UnitHeightPixel;
                    //pv = cdUtil.RotatedForViewAxis(l);
                    TerrainPieceTemplate.TerrainPolygonSet tpset = p.Template.GetPolygons(dir);
                    if (tpset.Ground.IsVisible) {
                        plate = apparents.DefaultLandTexture.Get(l, tpset.Ground, pv);
                        plate.Location = new PointF3D(pv.VX, pv.VY, pv.VZ);
                        watch0.Stop();
                        yield return plate;
                        if (marker0 != null && marker0.Same(uv, tpset)) {
                            //Debug.WriteLine("Hit("+x+","+y+")");
                            PointF3D pmk = plate.Location;
                            pmk.Z += 0.5f;
                            plate.Location = pmk;
                            plate.Effect = GraphicManagerEx.GraphicManager.GetFilterByUsage();
                            yield return plate;
                        }
                    }
                    ViewDirection8 dside = tpset.DiagonalSide;
                    if (dside != ViewDirection8.FRONT) {
                        if (dside != ViewDirection8.LEFT && cdUtil.IsLeftCliffVisible(map, l, out hgap)) {
                            // Process Left Cliff
                            hgap *= Geocon.UnitHeightPixel;
                            CliffPolygon cp = tpset.CliffLeft;
                            int pvz = pv.VZ - hgap;
                            if (cp == null && hgap > 0) {
                                cp = CliffPolygon.LeftUnitCliff;
                                hgap -= Geocon.TerrainStepHeightInUnit;
                                pvz -= Geocon.TerrainStepHeightInUnit;
                            }
                            if (cp != null) {
                                plate = apparents.DefaultCliffTexture.Get(cp, hgap, pv);
                                plate.Location = new PointF3D(pv.VX, pv.VY, pvz);
                                yield return plate;
                            }
                        }
                        if (dside != ViewDirection8.RIGHT && cdUtil.IsRightCliffVisible(map, l, out hgap)) {
                            // Process Right Cliff
                            hgap *= Geocon.UnitHeightPixel;
                            CliffPolygon cp = tpset.CliffRight;
                            int pvz = pv.VZ - hgap;
                            if (cp == null && hgap > 0) {
                                cp = CliffPolygon.RightUnitCliff;
                                hgap -= Geocon.TerrainStepHeightInUnit;
                                pvz -= Geocon.TerrainStepHeightInUnit;
                            }
                            if (cp != null) {
                                plate = apparents.DefaultCliffTexture.Get(cp, hgap, pv);
                                plate.Location = new PointF3D(pv.VX, pv.VY, pvz);
                                yield return plate;
                            }
                        }
                    } else {
                        if (cdUtil.IsDiagonalCliffVisible(map, l, out hgap)) {
                            // Process Diagonal Cliff
                            hgap *= Geocon.UnitHeightPixel;
                            CliffPolygon cp = tpset.CliffDiagonal;
                            int pvz = pv.VZ - hgap;
                            if (cp == null && hgap > 0) {
                                cp = CliffPolygon.DiagonalUnitCliff;
                                hgap -= Geocon.TerrainStepHeightInUnit;
                                pvz -= Geocon.TerrainStepHeightInUnit;
                            }
                            if (cp != null) {
                                plate = apparents.DefaultCliffTexture.Get(cp, hgap, pv);
                                plate.Location = new PointF3D(pv.VX, pv.VY, pvz);
                                yield return plate;
                            }
                        }
                    }
                }
            }
            if (watch0.IsRunning)
                watch0.Stop();
            Debug.WriteLine(string.Format("time={0}ms", watch0.ElapsedMilliseconds));
        }

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

        protected IEnumerator<Point> GetGroundEnumerator() {
            //InterCardinalDirection dir = upperDir;
            Size3D sz = game.TerrainMap.Size;

            // something like magic number. large map will be drawn farther.
            // optimize to reduce skip count without undrawn terrains in the viewport.
            int shift = (sz.sx + sz.sy) >> 4;
            
            PointF3D campos = CalcCameraPosition();
            surface.CameraPosition = campos;
            Size csz = surface.Size;

            // calculates visible cell-grid region in the map.
            int wm = ((csz.Width / Geocon.UnitWidthPixel) >> 2) + 2;
            int hm = ((csz.Height / Geocon.UnitWidthPixel)) + 4;
            int cy = (int)(campos.Y / Geocon.HalfOf_UnitWidthPixel) - 10;
            int cx = (int)(campos.X / Geocon.UnitWidthPixel) / 2;
            int hmin = cy - hm;
            int hmax = cy + hm + shift;
            int wmin = cx - wm;
            int wmax = cx + wm;
#if false//DEBUG
            Debug.Write("drawgrid=("+wmin+","+hmin+")-("+wmax+","+hmax+")");
            Point p = new Point(hmin/2-wmin+(hmin&1),hmin/2+wmin);
            //Debug.Write(" first pos=("+p.X+","+p.Y+")");
            Location l = cdUtil.IsometricGridToLocation(p.X, p.Y, 0);
            Debug.WriteLine("first loc=("+l.X+","+l.Y+")");
#endif
            for (int h = hmin; h <= hmax; h++) {
                for (int w = wmin; w <= wmax; w++) {
                    yield return ViewToIsometricPlane(w, h);
                }
            }
        }

        // view grid (cell sized) to isometric grid
        internal protected Point ViewToIsometricPlane(int vx, int vy) {
            int v2 = vy / 2;
            int x = v2 - vx + (vy & 1);
            int y = v2 + vx;
            return new Point(x, y);
        }


        // isometric grid to view positon (in pixel).
        internal protected Point IsometricGridToView(int isx, int isy) {
            int x = (isy - isx)/2;
            int y = (isx + isy)<<1;
            return new Point(x * Geocon.UnitWidthPixel, y * Geocon.HalfOf_UnitWidthPixel);
        }

        public object GetObjectAt(Point spos) {
            PointF3D p3d = surface.ToWorldPosition(spos);
#if DEBUG
            Debug.Write("mouse(" + spos.X + "," + spos.Y + ")");
            //Debug.Write("world(" + (int)p3d.X + "," + (int)p3d.Y + "," + (int)p3d.Z + ")");
#endif
            int isx = (int)Math.Floor(p3d.X / Geocon.UnitWidthPixel);
            int isy = (int)Math.Floor(p3d.Y / Geocon.UnitWidthPixel);
            object ret = null;
            TerrainMapImpl map = game.TerrainMap;
            // 標高の高い地表セルは画面上方に表示される。
            // ヒットテストの再、標高によってヒットする可能性がある一番手前のセルを探す。
            Size3D sz = map.Size;
            double hmax = sz.sz * Geocon.UnitHeightPixel;
            int d = (int)Math.Ceiling(hmax / Geocon.HalfOf_UnitWidthPixel);
            d = Math.Min(d, Math.Min(isx, isy));
            Point l1 = new Point(isx - d, isy - d);
            // 等角投影図なので、縦方向に走査すると２列の菱形をジグザグに調べる必要がある。
            Point l2,l3;
            Point rep = surface.ToScreenPosition(new PointF3D(isx * Geocon.UnitWidthPixel, isy * Geocon.UnitWidthPixel, 0));
            if (spos.X - rep.X > 0)
                l2 = new Point(l1.X, l1.Y - 1);
            else
                l2 = new Point(l1.X - 1, l1.Y);

#if false //DEBUG
            Debug.Write(":r(" + (spos.X- rep.X) + "," + (spos.Y-rep.Y) + ")");
            Debug.WriteLine("grid(" + isx+ "," + isy +")");
            Debug.Write("(" + l1.X + "," + l1.Y + ")");
            Debug.Write("(" + l2.X + "," + l2.Y + ")");
#endif
            marker0 = null;
            while(true){
                //Debug.Write("(" + l1.X + "," + l1.Y + ")");
                if (l2.X >= 0 && l2.Y >= 0 && l2.X < sz.sx && l2.Y < sz.sy) {
                    HitTest(l2, spos, out ret);
                    if (ret != null) break;
                }
                if (l1.X >= 0 && l1.Y >= 0 && l1.X < sz.sx && l1.Y < sz.sy) {
                    if (HitTest(l1, spos, out ret)) {
                        break;
                    }
                }
                if (isx < l1.X) {
                    //Debug.Write("exit3");
                    break;
                }
                l2.X++; l2.Y++;
                l1.X++; l1.Y++;
            }
            Debug.WriteLine("obj=" + ret);
            return ret;
        }

        class TerrainMarker
        {
            int x, y;
            ushort id;
            bool trianglehit;
            public TerrainMarker(Point pt, TerrainPieceTemplate.TerrainPolygonSet tpset, bool inner_triangle) {
                this.x = pt.X;
                this.y = pt.Y;
                this.id = tpset.Ground.ID;
                this.trianglehit = inner_triangle;
            }
            public bool Same(Point pt, TerrainPieceTemplate.TerrainPolygonSet tpset) {
                return pt.X == x && pt.Y == y && tpset.Ground.ID == id;
            }
        }
        TerrainMarker marker0;
        protected bool HitTest(Point iso_p, Point vw_p, out object ret){
            //Debug.Write("HitTest at="+iso_p);
            ret = null;
            TerrainMapImpl map = game.TerrainMap;
            PointF3D p3d = new PointF3D(iso_p.X * Geocon.UnitWidthPixel, iso_p.Y * Geocon.UnitWidthPixel, 0);
            Point rep = surface.ToScreenPosition(p3d);
            //Debug.Write(":rep(" + rep.X + "," + rep.Y + ")");
            // ビューのY座標は上に行くほど小さくなることに注意
            //Debug.Write(":h="+h);
            if (rep.Y < vw_p.Y) { // 指定ポイントより上＝絶対ヒットしない
                //Debug.Write("HitTest at=" + iso_p);
                //Debug.WriteLine(":e1=" + (rep.Y - vw_p.Y));
                return true;
            }
            int xoff = rep.X - vw_p.X;
            int yoff = 0;
            //Debug.WriteLine("HitTest at=" + iso_p);
            bool cliff = false;
            //rep.Y -= (Geocon.TerrainMaxHeightInPixel + Geocon.HalfOf_UnitWidthPixel);
            foreach (ITerrainPiece p in map[iso_p.X, iso_p.Y, upperDir]) {
                int h1 = p.BaseHeight * Geocon.UnitHeightPixel;
                yoff = vw_p.Y - rep.Y + h1;
                //Debug.Write(",off(" + xoff + "," + yoff + ")");
                //Debug.Write(",h1="+h1);
                if (yoff > 0 ) {
                    cliff = true; //おそらく手前の崖を差している
                    continue;
                }else if(yoff + Geocon.TerrainMaxHeightInPixel + Geocon.UnitWidthPixel < 0) {
                    continue;
                }
                TerrainPieceTemplate.TerrainPolygonSet tpset = p.Template.GetPolygons(upperDir);
                if (tpset.Ground.IsVisible) {
                    //Debug.Write("!");
                    if (TerrainUtil.IsInnerPoint2D(tpset.Ground.GetVerticis(scaler), new Point(xoff,yoff))) {
                        ret = tpset.Ground;
                        //Debug.WriteLine("");
                        marker0 = new TerrainMarker(iso_p, tpset, true);
                        return true;
                    }
                }
                // TODO Cliffs
            }
            //Debug.WriteLine(":e0");
            return cliff;
        }

    }
}
