using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using nft.framework.drawing;
using System.Windows.Forms;
using WinFormsGraphicsDevice;
using Microsoft.Xna.Framework.Graphics;

namespace nft.xna
{
    #region arias
    using SysColor = System.Drawing.Color;
    using XnaColor = Microsoft.Xna.Framework.Color;
    using SysRect = System.Drawing.Rectangle;
    using XnaRect = Microsoft.Xna.Framework.Rectangle;
    using Point = System.Drawing.Point;
    using DefaultEffect = StockEffects.DefaultEffect;
    using System.Diagnostics;
    using System.Drawing;
using StockEffects;
    using Microsoft.Xna.Framework;
    #endregion

    public class XnaSurface : ISurface
    {
        static protected Stopwatch watch = new Stopwatch();

        static internal XnaSurface CreateForControl(Control c, SurfaceUsage usage) {
            Form f = c.TopLevelControl as Form;
            if (f == null) {
                f = Application.OpenForms[0];
            }
            return new XnaSurface(c.ClientSize.Width, c.ClientSize.Height, usage, f.Handle, c);
        }

        static protected int AdjustToOddPixels(int i) {
            return ((i + 1) | 1) - 1;
        }

        static protected Size AdjustToOddPixels(int w, int h) {
            return new Size(((w + 1) | 1) - 1, ((h + 1) | 1) - 1);
        }

        static protected Size AdjustToOddPixels(Size sz) {
            return new Size(((sz.Width + 1) | 1) - 1, ((sz.Height + 1) | 1) - 1);
        }

        public XnaSurface(int width, int height, SurfaceUsage usage)
            : this(width, height, usage, IntPtr.Zero, null) {
        }

        protected XnaSurface(int width, int height, SurfaceUsage usage, IntPtr hwnd, Control related) {
            this.control = related;
            this.width = width;
            this.height = height;
            this.srvGraphicsDev = GraphicsDeviceService.AddRef(hwnd, width, height);
            InitForPostProcess(usage);
            if (related != null) {
                NotifyResize(related);
            } else {
                this.szPrefferd = AdjustToOddPixels(width, height);
            }
            if (control == null) {
                target = CreateRenderTarget();
            }
        }

        GraphicsDeviceService srvGraphicsDev;
        protected Control control;
        protected RenderTargetBinding[] workbuffers = null;
        protected RenderTarget2D target = null; // Destination target.
        protected IEnumerable<I3DObject> drawObjects = null;
        protected PointF3D viewpos = new PointF3D();
        protected long frameTicks = 0;
        protected bool clear = true;
        protected XnaColor fillcolor = XnaColor.CornflowerBlue;
        internal protected int width;
        internal protected int height;
        protected Size szPrefferd;
        private int objcount;
        private int mrtcount; //Mulit Render Target count

        protected PostProcessEffect ppEffect;
        protected SpriteBatch spriteBatch;
        #region ISurface implementation

        public IEnumerable<I3DObject> Objects {
            get {
                return drawObjects;
            }
            set {
                drawObjects = value;
            }
        }

        public Size Size{
            get {
                if (control != null) {
                    return control.ClientSize;
                } else {
                    return new Size(width, height);
                }
            }
        }

        internal void NotifyResize(Control c) {
            if (c!=null && WorkBuffersCount>0) {
                szPrefferd = AdjustToOddPixels(c.ClientSize);
                if (workbuffers != null && workbuffers.Length > 0) {
                    RenderTarget2D rt = (RenderTarget2D)workbuffers[0].RenderTarget;
                    if (rt.Width < szPrefferd.Width || rt.Height < szPrefferd.Height) {
                        // Buffer is smaller. need to reconstruct.
                        ClearWorkBuffers(); // may rebuild on next drawing time.
                    }
                }
            }
        }

        internal Size PrefferdBackBufferSize {
            get {
                return szPrefferd;
            }
        }

        RenderTarget2D rtdbg;
        void PrepareDebugMarker() {
            PresentationParameters pp = GraphicsDevice.PresentationParameters;
            rtdbg = new RenderTarget2D(GraphicsDevice, 3, 3, false, pp.BackBufferFormat, pp.DepthStencilFormat);
            GraphicsDevice.SetRenderTarget(rtdbg);
            GraphicsDevice.Clear(XnaColor.Red);
        }

        /// <summary>
        /// Calcurate required RenderTargets count.
        /// </summary>
        /// <param name="usage"></param>
        /// <returns></returns>
        protected void InitForPostProcess(SurfaceUsage usage) {
#if DEBUG
            PrepareDebugMarker();
#endif
            switch (usage) {
                case SurfaceUsage.GameView:
                    mrtcount = 3;
                    break;
                case SurfaceUsage.PreCombinedTexture:
                    mrtcount = 2;
                    break;
                default:
                    mrtcount = 0;
                    break;
            }
            if (mrtcount > 0) {
                ppEffect = new PostProcessEffect(GraphicsDevice);
                spriteBatch = new SpriteBatch(GraphicsDevice);
            }
        }

        public void Draw() {
            watch.Reset();
            watch.Start();
            if (control != null) {
                Size sz = control.ClientSize;
                width = sz.Width;
                height = sz.Height;
            }
            BeginDraw();
            DrawMain();
            PostProcess();
            EndDraw();
            watch.Stop();
        }

        public SysColor FillColor {
            get {
                return XnaUtil.ToSysColor(fillcolor);
            }
            set {
                fillcolor = XnaUtil.ToXnaColor(value);
            }
        }

        public long GameTicks {
            get { return this.frameTicks; }
            set { frameTicks = value; }
        }

        public bool ClearEveryDraw {
            get { return clear; }
            set { clear = value; }
        }

        public IEffectFilter Effect {
            get {
                throw new NotImplementedException();
            }
            set {
                throw new NotImplementedException();
            }
        }

        public PointF3D CameraPosition {
            get {
                return viewpos;
            }
            set {
                viewpos = value;
            }
        }

        public PointF3D ToWorldPosition(Point screenpos) {
            Vector3 vin = new Vector3(screenpos.X, screenpos.Y, 0);
            Vector3 vout = Scene.Unproject(this, vin);
            vout.X += vout.Y;
            vout.Z += vout.Y;
            vout.Y = 0;
            return XnaUtil.ToPointF(vout);
        }

        public Point ToScreenPosition(PointF3D worldpos) {
            Vector3 vin = XnaUtil.ToVector3(worldpos);
            Vector3 vout = Scene.Project(this, vin);
            return new Point((int)vout.X, (int)vout.Y);
        }

        internal int WorkBuffersCount {
            get {
                return mrtcount;
            }
            set {
                if (mrtcount != value) {
                    PrepareWorkBuffers(value);
                }
            }
        }

        internal protected RenderTarget2D WorkTarget(int n) {
            return (RenderTarget2D)workbuffers[n].RenderTarget;
        }

        internal XnaColor GetWorkPixelAt(int vx, int vy, int target_num) {
            if (vx < 0 || vx >= this.width) return XnaColor.Transparent;
            if (vy < 0 || vy >= this.height) return XnaColor.Transparent;
            RenderTarget2D rt = WorkTarget(target_num);
            XnaColor[] ret = new XnaColor[1];
            XnaRect rct = new XnaRect(vx, vy, 1, 1);
            rt.GetData<XnaColor>(0, rct, ret, 0, 1);
            return ret[0];
        }

        public UInt32 HitTestAt(int vx, int vy) {
            XnaColor xc = GetWorkPixelAt(vx, vy, 1);
            Debug.WriteLine("mouse=" + xc);
            return xc.PackedValue&0xffffff;
        }
        #endregion

        #region Drawing core
        Viewport viewport;
        protected void BeginDraw() {
            GraphicsDevice.DepthStencilState = DepthStencilState.Default;
            
            //Debug.WriteLine(string.Format("xna size=({0},{1})",Width, Height));
            // Check device-lost and prepare proper drawing region.
            // Adjust to odd number size to suppress pixel distortion.
            if (srvGraphicsDev.PrepareDrawing(AdjustToOddPixels(width), AdjustToOddPixels(height), out viewport)) {
                ClearWorkBuffers();
            }
            if (mrtcount > 0) {
                PrepareWorkBuffers(mrtcount);
                GraphicsDevice.SetRenderTargets(this.WorkBuffers);
            } else {
                GraphicsDevice.SetRenderTarget(target);
            }
            GraphicsDevice.Viewport = viewport;
        }

        protected virtual void DrawMain() {
            objcount = 0;
            if (clear)
                GraphicsDevice.Clear(fillcolor);
            DefaultEffect ef = Scene.Prepare(this);
            if (drawObjects == null) return;
            foreach (I3DObject o in drawObjects) {
                ((IDrawable)o).RenderSelf(this, ef);
                objcount++;
            }
        }

        /// <summary>
        /// Apply post process effect.
        /// </summary>
        void PostProcess() {
            if (mrtcount > 0) {
                GraphicsDevice.SetRenderTarget(target);
                ppEffect.CurrentTechnique = ppEffect.Techniques["EdgeDetect"];
                ppEffect.DepthTexture = WorkTarget(1);
                // ポストプロセス エフェクトを適用する全画面スプライトを描画します。
                //spriteBatch.Begin(0, BlendState.Opaque);
                spriteBatch.Begin(0, BlendState.Opaque, null, null, null, ppEffect);
                RenderTarget2D finalImage = WorkTarget(0);
                XnaRect rct = viewport.Bounds;
                rct.Width = Math.Min(rct.Width, finalImage.Width);
                rct.Height = Math.Min(rct.Height, finalImage.Height);
                spriteBatch.Draw(finalImage, Vector2.Zero, viewport.Bounds, XnaColor.White);
#if false //DEBUG
                PointF3D v = CameraPosition;
                
                Vector3 cpos = new Vector3(v.X, v.Y,0);
                Debug.Write("campos=" + cpos);
                Vector3 v3;
                v3 = Vector3.Transform(cpos, Scene.LastProject);
                Debug.Write(", vpos=" + v3);
                //v3 = Vector3.Transform(cpos, Matrix.Invert(Scene.LastCamera));
                //Debug.Write(", revvpos=" + v3);
                //v3 = Scene.Project(this, new Vector3(v.X,v.Y,0));
                //v3 = Scene.Unproject(this, v3);
                //Debug.WriteLine(", prjpos="+v3);
                spriteBatch.Draw(rtdbg, new Vector2(v3.X,v3.Y), XnaColor.White);
#endif
                spriteBatch.End();
            }
        }

        /// <summary>
        /// Commit drawing and render image onto proper region of the control.
        /// </summary>
        void EndDraw() {
            GraphicsDevice.SetRenderTargets(null);
            if (control != null) {
                try {
                    XnaRect sourceRectangle = new XnaRect(0, 0, width, height);
                    GraphicsDevice.Present(sourceRectangle, null, control.Handle);
                } catch {
                    // 'Present' may throw exception when device lost.
                    // Device lost will be detected on next BeginDraw, so do nothing here.
                }
            }
        }

        void PrepareWorkBuffers(int n) {
            RenderTargetBinding[] ret;
            int len = 0;
            mrtcount = n;
            if (workbuffers != null) {
                len = workbuffers.Length;
                if (len == n) return;
                ret = new RenderTargetBinding[n];
                if (len > n) {
                    for (int i = len; i < n; i++)
                        workbuffers[i].RenderTarget.Dispose();
                }
                for (int i = 0; i < len; i++) {
                    ret[i] = workbuffers[i];
                }
            } else {
                ret = new RenderTargetBinding[n];
            }
            for (int i = len; i < n; i++) {
                ret[i] = new RenderTargetBinding(CreateRenderTarget());
            }
            workbuffers = ret;
        }

        protected virtual RenderTarget2D CreateRenderTarget() {
            PresentationParameters pp = GraphicsDevice.PresentationParameters;
            Size sz = this.PrefferdBackBufferSize;
            return new RenderTarget2D(GraphicsDevice, sz.Width, sz.Height, false, pp.BackBufferFormat, pp.DepthStencilFormat);
        }

        protected void ClearWorkBuffers() {
            if (workbuffers != null) {
                int l = workbuffers.Length;
                for (int i = 0; i < l; i++)
                    workbuffers[i].RenderTarget.Dispose();
                workbuffers = null;
            }
        }
        #endregion

        internal string GetDrawStatistics() {
            return string.Format("{0} msec for {1} objects.",watch.ElapsedMilliseconds, objcount);
        }

        #region Properties
        /// <summary>
        /// このコントロールへの描画に使用できる GraphicsDevice を取得します。
        /// </summary>
        public GraphicsDevice GraphicsDevice {
            get { return srvGraphicsDev.GraphicsDevice; }
        }

        public RenderTarget2D RenderTarget {
            get { return target; }
        }

        /// <summary>
        /// オフスクリーン描画する場合のRenderTarget。コントロールに描画する場合はnullを返す。
        /// returns null when draw on screen
        /// </summary>
        protected RenderTargetBinding[] WorkBuffers {
            get { return workbuffers; }
        }
        #endregion

        #region IDisposable 定型
        public virtual void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(Boolean disposing)
        {
            if (disposing ) {
                ClearWorkBuffers();
                if (srvGraphicsDev != null) {
                    srvGraphicsDev.Release(disposing);
                    srvGraphicsDev = null;
                }
            }
        }

        public bool IsDisposed { get { return srvGraphicsDev == null; } }

        ~XnaSurface()
        {
            Dispose(false);
        }
        #endregion
    }
}
