// This code was written for the OpenTK library and has been released
// to the Public Domain.
// It is provided "as is" without express or implied warranty of any kind.

using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Diagnostics;

using OpenTK;
using OpenTK.Input;
using OpenTK.Graphics;
using OpenTK.Graphics.OpenGL;

using Examples.Shapes;

namespace Examples.Tutorial
{

    [Example("Stencil CSG", ExampleCategory.OpenGL, "1.x", Documentation = "StencilCSG")]
    partial class StencilCSG : GameWindow
    {
        #region Model Related
        DrawableShape OperandB;
        DrawableShape OperandA;
        float MySphereZOffset = 0f;
        float MySphereXOffset = 0f;

        int Texture;
        #endregion Model Related

        string WindowTitle;
        bool ShowDebugWireFrame = true;

        float CameraZoom;
        float CameraRotX;
        float CameraRotY;
        Vector3 EyePosition = new Vector3(0f, 0f, 15f);

        #region Window
        public StencilCSG()
            : base(800, 600, new GraphicsMode(new ColorFormat(8, 8, 8, 8), 24, 8)) // request 8-bit stencil buffer
        {
            base.VSync = VSyncMode.Off;
            Keyboard.KeyDown += delegate(object sender, KeyboardKeyEventArgs e)
            {
                switch (e.Key)
                {
                    case Key.Escape: this.Exit(); break;
                    case Key.Space: ShowDebugWireFrame = !ShowDebugWireFrame; break;
                }
            };
        }

        protected override void OnResize(EventArgs e)
        {
            GL.Viewport(0, 0, Width, Height);
            GL.MatrixMode(MatrixMode.Projection);
            Matrix4 p = Matrix4.CreatePerspectiveFieldOfView(MathHelper.PiOver4, Width / (float)Height, 0.1f, 64.0f);
            GL.LoadMatrix(ref p);
        }
        #endregion Window

        protected override void OnLoad(EventArgs e)
        {
            #region Abort on platforms which will not be able to execute the ops properly
            /*
            if (!GL.SupportsExtension("VERSION_1_2"))
            {
                Trace.WriteLine("Aborting. OpenGL 1.2 or later required.");
                this.Exit();
            }

            int[] t = new int[2];
            GL.GetInteger(GetPName.MajorVersion, out t[0]);
            GL.GetInteger(GetPName.MinorVersion, out t[1]);
            Trace.WriteLine("OpenGL Context Version: " + t[0] + "." + t[1]);

            GL.GetInteger(GetPName.DepthBits, out t[0]);
            Trace.WriteLine("Depth Bits: " + t[0]);
            GL.GetInteger(GetPName.StencilBits, out t[1]);
            Trace.WriteLine("Stencil Bits: " + t[1]);

            if (t[0] < 16)
            {
                Trace.WriteLine("Aborting. Need at least 16 depth bits, only " + t[0] + " available.");
                this.Exit();
            }

            if (t[1] < 1)
            {
                Trace.WriteLine("Aborting. Need at least 1 stencil bit, only " + t[1] + " available.");
                this.Exit();
            }
            */
            #endregion Abort on platforms which will not be able to execute the ops properly

            WindowTitle = "Cube-Sphere Stencil CSG  " + GL.GetString(StringName.Renderer) + " (GL " + GL.GetString(StringName.Version) + ")";

            #region GL States
            GL.ClearColor(.08f, .12f, .16f, 1f);

            GL.Enable(EnableCap.DepthTest);
            GL.DepthFunc(DepthFunction.Less);
            GL.ClearDepth(1.0);

            GL.Enable(EnableCap.StencilTest);
            GL.ClearStencil(0);
            GL.StencilMask(0xFFFFFFFF); // read&write

            GL.Enable(EnableCap.CullFace);
            GL.FrontFace(FrontFaceDirection.Ccw);
            GL.CullFace(CullFaceMode.Back);

            GL.PolygonMode(MaterialFace.FrontAndBack, PolygonMode.Fill);

            GL.Color4(1f, 1f, 1f, 1f);

            GL.Enable(EnableCap.Lighting);
            GL.Enable(EnableCap.Light0);
            GL.ShadeModel(ShadingModel.Smooth);

            #endregion GL States

            #region Load Texture
            Bitmap bitmap = new Bitmap("Data/Textures/logo-dark.jpg");
            bitmap.RotateFlip(RotateFlipType.RotateNoneFlipY);

            GL.GenTextures(1, out Texture);
            GL.BindTexture(TextureTarget.Texture2D, Texture);

            BitmapData data = bitmap.LockBits(new System.Drawing.Rectangle(0, 0, bitmap.Width, bitmap.Height), ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
            GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba, data.Width, data.Height, 0, OpenTK.Graphics.OpenGL.PixelFormat.Bgra, PixelType.UnsignedByte, data.Scan0);
            GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear);
            GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear);
            GL.Finish();
            bitmap.UnlockBits(data);
            #endregion Load Texture

            OperandA = new ChamferCube(1.5, 2.0, 2.5, ChamferCube.SubDivs.Four, 0.42, true);
            OperandB = new SlicedSphere(2.0f, Vector3d.Zero,
                                           SlicedSphere.eSubdivisions.Three,
                                           new SlicedSphere.eDir[] { SlicedSphere.eDir.All },
                                           true);

            #region Invert Operand B's Normals
            // only the inside of the operand is ever drawn to color buffers and lighting requires this.
            PrimitiveType tempPrimMode;
            VertexT2dN3dV3d[] tempVertices;
            uint[] tempIndices;

            OperandB.GetArraysforVBO(out tempPrimMode, out tempVertices, out tempIndices);
            OperandB.Dispose();

            for (int i = 0; i < tempVertices.Length; i++)
            {
                tempVertices[i].Normal *= -1.0;
                tempVertices[i].Normal.Normalize();
            }

            OperandB = new VboShape(ref tempPrimMode, ref tempVertices, ref tempIndices, true);
            #endregion Invert Operand B's Normals
        }

        protected override void OnUnload(EventArgs e)
        {
            GL.DeleteTextures(1, ref Texture);

            OperandA.Dispose();
            OperandB.Dispose();

            base.OnUnload(e);
        }

        protected override void OnUpdateFrame(FrameEventArgs e)
        {
            #region Magic numbers for camera
            CameraRotX = -Mouse.X * .5f;
            CameraRotY = Mouse.Y * .5f;
            CameraZoom = Mouse.Wheel * .2f;
            #endregion Magic numbers for camera
        }

        public void DrawOperandB()
        {
            GL.PushMatrix();
            GL.Translate(Math.Cos(MySphereXOffset), -1f, Math.Cos(MySphereZOffset));
            OperandB.Draw();
            GL.PopMatrix();
        }

        public void DrawOperandA()
        {
            GL.Enable(EnableCap.Texture2D);
            OperandA.Draw();
            GL.Disable(EnableCap.Texture2D);
        }

        public void RenderCsg()
        {
            // first pass
            GL.Disable(EnableCap.StencilTest);

            GL.ColorMask(false, false, false, false);
            GL.CullFace(CullFaceMode.Front);
            DrawOperandB();// draw front-faces into depth buffer

            // use stencil plane to find parts of b in a 
            GL.DepthMask(false);
            GL.Enable(EnableCap.StencilTest);
            GL.StencilFunc(StencilFunction.Always, 0, 0);

            GL.StencilOp(StencilOp.Keep, StencilOp.Keep, StencilOp.Incr);
            GL.CullFace(CullFaceMode.Back);
            DrawOperandA(); // increment the stencil where the front face of a is drawn

            GL.StencilOp(StencilOp.Keep, StencilOp.Keep, StencilOp.Decr);
            GL.CullFace(CullFaceMode.Front);
            DrawOperandA(); // decrement the stencil buffer where the back face of a is drawn

            GL.DepthMask(true);
            GL.Disable(EnableCap.DepthTest);

            GL.ColorMask(true, true, true, true);
            GL.StencilFunc(StencilFunction.Notequal, 0, 1);
            DrawOperandB(); // draw the part of b that's in a

            // fix depth
            GL.ColorMask(false, false, false, false);
            GL.Enable(EnableCap.DepthTest);
            GL.Disable(EnableCap.StencilTest);
            GL.DepthFunc(DepthFunction.Always);
            DrawOperandA();
            GL.DepthFunc(DepthFunction.Less);

            // second pass
            GL.CullFace(CullFaceMode.Back);
            DrawOperandA();

            GL.DepthMask(false);
            GL.Enable(EnableCap.StencilTest);

            GL.StencilFunc(StencilFunction.Always, 0, 0);
            GL.StencilOp(StencilOp.Keep, StencilOp.Keep, StencilOp.Incr);
            DrawOperandB(); // increment the stencil where the front face of b is drawn

            GL.StencilOp(StencilOp.Keep, StencilOp.Keep, StencilOp.Decr);
            GL.CullFace(CullFaceMode.Front);
            DrawOperandB(); // decrement the stencil buffer where the back face of b is drawn

            GL.DepthMask(true);
            GL.Disable(EnableCap.DepthTest);

            GL.ColorMask(true, true, true, true);
            GL.StencilFunc(StencilFunction.Equal, 0, 1);
            GL.CullFace(CullFaceMode.Back);
            DrawOperandA(); // draw the part of a that's in b

            GL.Enable(EnableCap.DepthTest);
        }

        protected override void OnRenderFrame(FrameEventArgs e)
        {
            this.Title = WindowTitle + "  FPS: " + (1f / e.Time).ToString("0.");

            MySphereZOffset += (float)(e.Time * 3.1);
            MySphereXOffset += (float)(e.Time * 4.2);

            #region Transform setup
            GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit | ClearBufferMask.StencilBufferBit);

            // Camera
            GL.MatrixMode(MatrixMode.Modelview);
            Matrix4 mv = Matrix4.LookAt(EyePosition, Vector3.Zero, Vector3.UnitY);
            GL.LoadMatrix(ref mv);

            GL.Translate(0f, 0f, CameraZoom);
            GL.Rotate(CameraRotX, Vector3.UnitY);
            GL.Rotate(CameraRotY, Vector3.UnitX);
            #endregion Transform setup

            RenderCsg();

            // ---------------------------------

            if (ShowDebugWireFrame)
            {
                GL.Color3(System.Drawing.Color.LightGray);
                GL.Disable(EnableCap.StencilTest);
                GL.Disable(EnableCap.Lighting);
                //GL.Disable( EnableCap.DepthTest );
                GL.PolygonMode(MaterialFace.Front, PolygonMode.Line);
                DrawOperandB();
                GL.PolygonMode(MaterialFace.Front, PolygonMode.Fill);
                GL.Enable(EnableCap.DepthTest);
                GL.Enable(EnableCap.Lighting);
                GL.Enable(EnableCap.StencilTest);
            }
            this.SwapBuffers();
        }

        [STAThread]
        static void Main()
        {
            using (StencilCSG example = new StencilCSG())
            {
                Utilities.SetWindowTitle(example);
                example.Run(30.0, 0.0);
            }
        }
    }
}