// This code is in the Public Domain. It is provided "as is"
// without express or implied warranty of any kind.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Threading;
using OpenTK;
using OpenTK.Graphics;
using OpenTK.Graphics.OpenGL;
using OpenTK.Input;

namespace Examples.Tutorial
{
    /// <summary>
    /// Demonstrates how to decouple rendering from the main thread.
    /// Note that all OpenGL function calls should take place at the rendering thread -
    /// OpenGL will not be available on the main thread at all!
    /// </summary>
    [Example("GameWindow Threaded", ExampleCategory.OpenTK, "GameWindow", 3, Documentation = "GameWindowThreaded")]
    public class ThreadedRendering : GameWindow
    {
        bool viewport_changed = true;
        int viewport_width, viewport_height;
        bool position_changed = true;
        int position_x, position_y;
        float position_dx, position_dy;
        bool exit = false;
        Thread rendering_thread;
        object update_lock = new object();
        const float GravityAccel = -9.81f;
        struct Particle
        {
            public Vector2 Position;
            public Vector2 Velocity;
            public Color4 Color;
        }
        List<Particle> Particles = new List<Particle>();
        Random rand = new Random();

        public ThreadedRendering()
            : base(800, 600)
        {
            Keyboard.KeyDown += delegate(object sender, KeyboardKeyEventArgs e)
            {
                if (e.Key == Key.Escape)
                    this.Exit();
            };

            Keyboard.KeyUp += delegate(object sender, KeyboardKeyEventArgs e)
            {
                if (e.Key == Key.F11)
                    if (this.WindowState == WindowState.Fullscreen)
                        this.WindowState = WindowState.Normal;
                    else
                        this.WindowState = WindowState.Fullscreen;
            };

            Resize += delegate(object sender, EventArgs e)
            {
                // Note that we cannot call any OpenGL methods directly. What we can do is set
                // a flag and respond to it from the rendering thread.
                lock (update_lock)
                {
                    viewport_changed = true;
                    viewport_width = Width;
                    viewport_height = Height;
                }
            };

            Move += delegate(object sender, EventArgs e)
            {
                // Note that we cannot call any OpenGL methods directly. What we can do is set
                // a flag and respond to it from the rendering thread.
                lock (update_lock)
                {
                    position_changed = true;
                    position_dx = (position_x - X) / (float)Width;
                    position_dy = (position_y - Y) / (float)Height;
                    position_x = X;
                    position_y = Y;
                }
            };

            // Make sure initial position are correct, otherwise we'll give a huge
            // initial velocity to the balls.
            position_x = X;
            position_y = Y;
        }

        #region OnLoad

        /// <summary>
        /// Setup OpenGL and load resources here.
        /// </summary>
        /// <param name="e">Not used.</param>
        protected override void OnLoad(EventArgs e)
        {
            Context.MakeCurrent(null); // Release the OpenGL context so it can be used on the new thread.

            rendering_thread = new Thread(RenderLoop);
            rendering_thread.IsBackground = true;
            rendering_thread.Start();
        }

        #endregion

        #region OnUnload

        /// <summary>
        /// Release resources here.
        /// </summary>
        /// <param name="e">Not used.</param>
        protected override void OnUnload(EventArgs e)
        {
            exit = true; // Set a flag that the rendering thread should stop running.
            rendering_thread.Join();

            base.OnUnload(e);
        }

        #endregion

        #region OnUpdateFrame

        /// <summary>
        /// Add your game logic here.
        /// </summary>
        /// <param name="e">Contains timing information.</param>
        /// <remarks>There is no need to call the base implementation.</remarks>
        protected override void OnUpdateFrame(FrameEventArgs e)
        {
            // Nothing to do!
        }

        #endregion

        #region OnRenderFrame

        /// <summary>
        /// Ignored. All rendering is performed on our own rendering function.
        /// </summary>
        /// <param name="e">Contains timing information.</param>
        /// <remarks>There is no need to call the base implementation.</remarks>
        protected override void OnRenderFrame(FrameEventArgs e)
        {
            // Nothing to do. Release the CPU to other threads.
            Thread.Sleep(1);
        }

        #endregion

        #region RenderLoop

        void RenderLoop()
        {
            MakeCurrent(); // The context now belongs to this thread. No other thread may use it!

            VSync = VSyncMode.On;

            for (int i = 0; i < 64; i++)
            {
                Particle p = new Particle();
                p.Position = new Vector2((float)rand.NextDouble() * 2 - 1, (float)rand.NextDouble() * 2 - 1);
                p.Color.R = (float)rand.NextDouble();
                p.Color.G = (float)rand.NextDouble();
                p.Color.B = (float)rand.NextDouble();
                Particles.Add(p);
            }

            // Since we don't use OpenTK's timing mechanism, we need to keep time ourselves;
            Stopwatch render_watch = new Stopwatch();
            Stopwatch update_watch = new Stopwatch();
            update_watch.Start();
            render_watch.Start();

            GL.ClearColor(Color.MidnightBlue);
            GL.Enable(EnableCap.DepthTest);
            GL.Enable(EnableCap.PointSmooth);
            GL.PointSize(16);

            while (!exit)
            {
                Update(update_watch.Elapsed.TotalSeconds);
                update_watch.Reset();
                update_watch.Start();

                Render(render_watch.Elapsed.TotalSeconds);
                render_watch.Reset(); //  Stopwatch may be inaccurate over larger intervals.
                render_watch.Start(); // Plus, timekeeping is easier if we always start counting from 0.

                SwapBuffers();
            }

            Context.MakeCurrent(null);
        }

        #endregion

        #region Update

        void Update(double time)
        {
            lock (update_lock)
            {
                // When the user moves the window we make the particles react to
                // this movement. The reaction is semi-random and not physically
                // correct. It looks quite good, however.
                if (position_changed)
                {
                    for (int i = 0; i < Particles.Count; i++)
                    {
                        Particle p = Particles[i];
                        p.Velocity += new Vector2(
                            16 * (position_dx + 0.05f * (float)(rand.NextDouble() - 0.5)),
                            32 * (position_dy + 0.05f * (float)(rand.NextDouble() - 0.5)));
                        Particles[i] = p;
                    }

                    position_changed = false;
                }
            }

            // For simplicity, we use simple Euler integration to simulate particle movement.
            // This is not accurate, especially under varying timesteps (as is the case here).
            // A better solution would have been time-corrected Verlet integration, as
            // described here:
            // http://www.gamedev.net/reference/programming/features/verlet/
            for (int i = 0; i < Particles.Count; i++)
            {
                Particle p = Particles[i];

                p.Velocity.X = Math.Abs(p.Position.X) >= 1 ?-p.Velocity.X * 0.92f : p.Velocity.X * 0.97f;
                p.Velocity.Y = Math.Abs(p.Position.Y) >= 1 ? -p.Velocity.Y * 0.92f : p.Velocity.Y * 0.97f;
                if (p.Position.Y > -0.99)
                {
                    p.Velocity.Y += (float)(GravityAccel * time);
                }
                else
                {
                    if (Math.Abs(p.Velocity.Y) < 0.02)
                    {
                        p.Velocity.Y = 0;
                        p.Position.Y = -1;
                    }
                    else
                    {
                        p.Velocity.Y *= 0.9f;
                    }
                }

                p.Position += p.Velocity * (float)time;
                if (p.Position.Y <= -1)
                    p.Position.Y = -1;

                Particles[i] = p;
            }
        }

        #endregion

        #region Render

        /// <summary>
        /// This is our main rendering function, which executes on the rendering thread.
        /// </summary>
        public void Render(double time)
        {
            lock (update_lock)
            {
                if (viewport_changed)
                {
                    GL.Viewport(0, 0, viewport_width, viewport_height);
                    viewport_changed = false;
                }
            }

            Matrix4 perspective =
                Matrix4.CreateOrthographic(2, 2, -1, 1);
            GL.MatrixMode(MatrixMode.Projection);
            GL.LoadMatrix(ref perspective);

            GL.MatrixMode(MatrixMode.Modelview);
            GL.LoadIdentity();

            GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);

            GL.Begin(BeginMode.Points);
            foreach (Particle p in Particles)
            {
                GL.Color4(p.Color);
                GL.Vertex2(p.Position);
            }
            GL.End();
        }

        #endregion

        #region public static void Main()

        /// <summary>
        /// Entry point of this example.
        /// </summary>
        [STAThread]
        public static void Main()
        {
            using (GameWindow example = new ThreadedRendering())
            {
                // Get the title and category  of this example using reflection.
                Utilities.SetWindowTitle(example);
                example.Run();
            }
        }

        #endregion
    }
}
