// 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.Diagnostics;
using System.Drawing;
using System.IO;

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

using Examples.TextureLoaders;

namespace Examples.Tutorial
{
    /// <summary>Demonstrates Swizzled DXT5 Parallax Mapping
    /// The idea is described in more detail right here: http://www.opentk.com/node/394
    /// </summary>
    [Example("Swizzled Parallax Mapping", ExampleCategory.OpenGL, "2.x", Documentation = "SwizzledParallax")]
    public class T12_GLSL_Parallax: GameWindow
    {
        public T12_GLSL_Parallax( )
            : base( 800, 600 )
        {
        }

        #region internal Fields

        // Shader
        int VertexShaderObject, FragmentShaderObject, ProgramObject;
        const string VertexShaderFilename = "Data/Shaders/Parallax_VS.glsl";
        const string FragmentShaderFilename = "Data/Shaders/Parallax_FS.glsl";

        const int AttribTangent = 5; // slot where to pass tangents to VS, not sure which are reserved besides 0
        Vector3 Tangent = new Vector3( 1f, 0f, 0f );
        Vector3 Normal = new Vector3( 0f, 0f, 1f );

        // Material parameter
        //Vector3 MaterialScaleAndBiasAndShininess = new Vector3( 0.07f, 0.0f, 38.0f ); // for Metal tex
         Vector3 MaterialScaleAndBiasAndShininess = new Vector3( 0.04f, 0.0f, 92.0f ); // for Rock tex

        // Textures
        const TextureUnit TMU0_Unit = TextureUnit.Texture0;
        const int TMU0_UnitInteger = 0;
        const string TMU0_Filename = "Data/Textures/swizzled-rock-diffuse-height.dds";
        uint TMU0_Handle;
        TextureTarget TMU0_Target;

        const TextureUnit TMU1_Unit = TextureUnit.Texture1;
        const int TMU1_UnitInteger = 1;
        const string TMU1_Filename = "Data/Textures/swizzled-rock-normal-gloss.dds";
        uint TMU1_Handle;
        TextureTarget TMU1_Target;

        // Camera
        Vector3 EyePos = new Vector3( 0.0f, 0.0f, 3.0f );

        // Light
        Vector3 LightPosition = new Vector3( 0.0f, 1.0f, 1.0f );
        Vector3 LightDiffuse = new Vector3( 0.5f, 0.5f, 0.5f );
        Vector3 LightSpecular = new Vector3( 1f, 1f, 1f );

        #endregion internal Fields

        /// <summary>Setup OpenGL and load resources here.</summary>
        /// <param name="e">Not used.</param>
        protected override void OnLoad(EventArgs e)
        {
            this.VSync = VSyncMode.Off;

            // Check for necessary capabilities:
            string extensions = GL.GetString(StringName.Extensions);
            if ( !GL.GetString(StringName.Extensions).Contains("GL_ARB_shading_language"))
            {
                throw new NotSupportedException(String.Format("This example requires OpenGL 2.0. Found {0}. Aborting.",
                    GL.GetString(StringName.Version).Substring(0, 3)));
            }

            if (!extensions.Contains("GL_ARB_texture_compression") || 
                 !extensions.Contains("GL_EXT_texture_compression_s3tc"))
            {
                throw new NotSupportedException("This example requires support for texture compression. Aborting.");
            }

            int[] temp = new int[1];
            GL.GetInteger( GetPName.MaxTextureImageUnits, out temp[0] );
            Trace.WriteLine( temp[0] + " TMU's for Fragment Shaders found. (2 required)" );

            GL.GetInteger( GetPName.MaxVaryingFloats, out temp[0] );
            Trace.WriteLine( temp[0] + " varying floats between VS and FS allowed. (6 required)" );

            GL.GetInteger( GetPName.MaxVertexUniformComponents, out temp[0] );
            Trace.WriteLine( temp[0] + " uniform components allowed in Vertex Shader. (6 required)" );

            GL.GetInteger( GetPName.MaxFragmentUniformComponents, out temp[0] );
            Trace.WriteLine( temp[0] + " uniform components allowed in Fragment Shader. (11 required)" );
            Trace.WriteLine("" );

            #region GL State

            GL.ClearColor( 0.2f, 0f, 0.4f, 0f );

            GL.PointSize( 10f );

            GL.Disable( EnableCap.Dither );

            GL.FrontFace( FrontFaceDirection.Ccw );
            GL.PolygonMode( MaterialFace.Front, PolygonMode.Fill );
            GL.PolygonMode( MaterialFace.Back, PolygonMode.Line );

            #endregion GL State

            #region Shaders

            string LogInfo;

            // Load&Compile Vertex Shader

            using ( StreamReader sr = new StreamReader( VertexShaderFilename ) )
            {
                VertexShaderObject = GL.CreateShader( ShaderType.VertexShader );
                GL.ShaderSource( VertexShaderObject, sr.ReadToEnd( ) );
                GL.CompileShader( VertexShaderObject );
            }

            GL.GetShaderInfoLog( VertexShaderObject, out LogInfo );
            if ( LogInfo.Length > 0 && !LogInfo.Contains( "hardware" ) )
                Trace.WriteLine( "Vertex Shader failed!\nLog:\n" + LogInfo );
            else
                Trace.WriteLine( "Vertex Shader compiled without complaint." );

            // Load&Compile Fragment Shader

            using ( StreamReader sr = new StreamReader( FragmentShaderFilename ) )
            {
                FragmentShaderObject = GL.CreateShader( ShaderType.FragmentShader );
                GL.ShaderSource( FragmentShaderObject, sr.ReadToEnd( ) );
                GL.CompileShader( FragmentShaderObject );
            }
            GL.GetShaderInfoLog( FragmentShaderObject, out LogInfo );

            if ( LogInfo.Length > 0 && !LogInfo.Contains( "hardware" ) )
                Trace.WriteLine( "Fragment Shader failed!\nLog:\n" + LogInfo );
            else
                Trace.WriteLine( "Fragment Shader compiled without complaint." );

            // Link the Shaders to a usable Program
            ProgramObject = GL.CreateProgram( );
            GL.AttachShader( ProgramObject, VertexShaderObject );
            GL.AttachShader( ProgramObject, FragmentShaderObject );

            // must bind the attribute before linking
            GL.BindAttribLocation( ProgramObject, AttribTangent, "AttributeTangent" );

            // link it all together
            GL.LinkProgram( ProgramObject );

           // flag ShaderObjects for delete when not used anymore
            GL.DeleteShader( VertexShaderObject );
            GL.DeleteShader( FragmentShaderObject );

            GL.GetProgram( ProgramObject, ProgramParameter.LinkStatus, out temp[0] );
            Trace.WriteLine( "Linking Program (" + ProgramObject + ") " + ( ( temp[0] == 1 ) ? "succeeded." : "FAILED!" ) );
            if ( temp[0] != 1 )
            {
                GL.GetProgramInfoLog( ProgramObject, out LogInfo );
                Trace.WriteLine( "Program Log:\n" + LogInfo );
            }

            GL.GetProgram( ProgramObject, ProgramParameter.ActiveAttributes, out temp[0] );
            Trace.WriteLine( "Program registered " + temp[0] + " Attributes. (Should be 4: Pos, UV, Normal, Tangent)" );

            Trace.WriteLine( "Tangent attribute bind location: " + GL.GetAttribLocation( ProgramObject, "AttributeTangent" ) );

            Trace.WriteLine( "End of Shader build. GL Error: " + GL.GetError( ) );

            #endregion Shaders

            #region Textures

            TextureLoaderParameters.MagnificationFilter = TextureMagFilter.Linear;
            TextureLoaderParameters.MinificationFilter = TextureMinFilter.LinearMipmapLinear;
            TextureLoaderParameters.WrapModeS = TextureWrapMode.ClampToBorder;
            TextureLoaderParameters.WrapModeT = TextureWrapMode.ClampToBorder;
            TextureLoaderParameters.EnvMode = TextureEnvMode.Modulate;

            ImageDDS.LoadFromDisk( TMU0_Filename, out TMU0_Handle, out TMU0_Target );
            Trace.WriteLine( "Loaded " + TMU0_Filename + " with handle " + TMU0_Handle + " as " + TMU0_Target );
           
            ImageDDS.LoadFromDisk( TMU1_Filename, out TMU1_Handle, out TMU1_Target );
            Trace.WriteLine( "Loaded " + TMU1_Filename + " with handle " + TMU1_Handle + " as " + TMU1_Target );

            #endregion Textures

            Trace.WriteLine( "End of Texture Loading. GL Error: " + GL.GetError( ) );
            Trace.WriteLine( "");
        }

        protected override void OnUnload(EventArgs e)
        {
            GL.DeleteProgram( ProgramObject );
            GL.DeleteTextures( 1, ref TMU0_Handle );
            GL.DeleteTextures( 1, ref TMU1_Handle );

            base.OnUnload( e );
        }

        /// <summary>Respond to resize events here.</summary>
        /// <param name="e">Contains information on the new GameWindow size.</param>
        /// <remarks>There is no need to call the base implementation.</remarks>
        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, 100.0f);
            GL.LoadMatrix(ref p);

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

            base.OnResize( e );
        }

        /// <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 )
        {
            base.OnUpdateFrame( e );

            if ( Keyboard[OpenTK.Input.Key.Escape] )
                this.Exit( );
            if ( Keyboard[OpenTK.Input.Key.Space] )
                Trace.WriteLine( "GL: " + GL.GetError( ) );
            if ( Keyboard[OpenTK.Input.Key.Q] )
            {
                MaterialScaleAndBiasAndShininess.X += 0.01f;
                Trace.WriteLine( "Scale: " + MaterialScaleAndBiasAndShininess.X + " Bias: " + MaterialScaleAndBiasAndShininess.Y );
            }
            if ( Keyboard[OpenTK.Input.Key.A] )
            {
                MaterialScaleAndBiasAndShininess.X -= 0.01f;
                Trace.WriteLine( "Scale: " + MaterialScaleAndBiasAndShininess.X + " Bias: " + MaterialScaleAndBiasAndShininess.Y );
            }
            if ( Keyboard[OpenTK.Input.Key.W] )
            {
                MaterialScaleAndBiasAndShininess.Y += 0.01f;
                Trace.WriteLine( "Scale: " + MaterialScaleAndBiasAndShininess.X + " Bias: " + MaterialScaleAndBiasAndShininess.Y );
            }
            if ( Keyboard[OpenTK.Input.Key.S] )
            {
                MaterialScaleAndBiasAndShininess.Y -= 0.01f;
                Trace.WriteLine( "Scale: " + MaterialScaleAndBiasAndShininess.X + " Bias: " + MaterialScaleAndBiasAndShininess.Y );
            }
            if ( Keyboard[OpenTK.Input.Key.E] )
            {
                MaterialScaleAndBiasAndShininess.Z += 1f;
                Trace.WriteLine( "Shininess: " + MaterialScaleAndBiasAndShininess.Z );
            }
            if ( Keyboard[OpenTK.Input.Key.D] )
            {
                MaterialScaleAndBiasAndShininess.Z -= 1f;
                Trace.WriteLine( "Shininess: " + MaterialScaleAndBiasAndShininess.Z );
            }

            LightPosition.X = ( -( this.Width / 2 ) + Mouse.X ) / 100f;
            LightPosition.Y = ( ( this.Height / 2 ) - Mouse.Y ) / 100f;

            EyePos.Y = Mouse.Wheel * 0.5f;
        }

        /// <summary>Add your game rendering code here.</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 )
        {
            this.Title = "FPS: " + (1 / e.Time).ToString("0.");

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

            GL.UseProgram( ProgramObject );

            #region Textures

            GL.ActiveTexture( TMU0_Unit );
            GL.BindTexture( TMU0_Target, TMU0_Handle );
            GL.ActiveTexture( TMU1_Unit );
            GL.BindTexture( TMU1_Target, TMU1_Handle );

            #endregion Textures

            #region Uniforms

            // first Material's uniforms      
            GL.Uniform1( GL.GetUniformLocation( ProgramObject, "Material_DiffuseAndHeight" ), TMU0_UnitInteger );
            GL.Uniform1( GL.GetUniformLocation( ProgramObject, "Material_NormalAndGloss" ), TMU1_UnitInteger );
            GL.Uniform3( GL.GetUniformLocation( ProgramObject, "Material_ScaleBiasShininess" ), MaterialScaleAndBiasAndShininess.X, MaterialScaleAndBiasAndShininess.Y, MaterialScaleAndBiasAndShininess.Z );

            // the rest are vectors
            GL.Uniform3( GL.GetUniformLocation( ProgramObject, "Camera_Position" ), EyePos.X, EyePos.Y, EyePos.Z );
            GL.Uniform3( GL.GetUniformLocation( ProgramObject, "Light_Position" ), LightPosition.X, LightPosition.Y, LightPosition.Z );
            GL.Uniform3( GL.GetUniformLocation( ProgramObject, "Light_DiffuseColor" ), LightDiffuse.X, LightDiffuse.Y, LightDiffuse.Z );
            GL.Uniform3( GL.GetUniformLocation( ProgramObject, "Light_SpecularColor" ), LightSpecular.X, LightSpecular.Y, LightSpecular.Z );

            #endregion Uniforms

            GL.PushMatrix( );
            Matrix4 t = Matrix4.LookAt( EyePos, Vector3.Zero, Vector3.UnitY );
            GL.MultMatrix(ref t);

            #region Draw Quad

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

            GL.Begin( BeginMode.Quads );
            {
                GL.Normal3( Normal );
                GL.VertexAttrib3( AttribTangent, ref Tangent );
                GL.MultiTexCoord2( TextureUnit.Texture0, 0f, 1f );
                GL.Vertex3( -1.0f, 1.0f, 0.0f );

                GL.Normal3( Normal );
                GL.VertexAttrib3( AttribTangent, ref Tangent );
                GL.MultiTexCoord2( TextureUnit.Texture0, 0f, 0f );
                GL.Vertex3( -1.0f, -1.0f, 0.0f );

                GL.Normal3( Normal );
                GL.VertexAttrib3( AttribTangent, ref Tangent );
                GL.MultiTexCoord2( TextureUnit.Texture0, 1f, 0f );
                GL.Vertex3( 1.0f, -1.0f, 0.0f );

                GL.Normal3( Normal );
                GL.VertexAttrib3( AttribTangent, ref Tangent );
                GL.MultiTexCoord2( TextureUnit.Texture0, 1f, 1f );
                GL.Vertex3( 1.0f, 1.0f, 0.0f );
            }
            GL.End( );

            #endregion Draw Quad

            GL.UseProgram( 0 );

            // visualize the light position 'somehow'
            GL.Begin( BeginMode.Points );
            {
                GL.Color3( LightSpecular );
                GL.Vertex3( LightPosition );
            }
            GL.End( );

            GL.PopMatrix( );

            this.SwapBuffers( );
        }

        /// <summary>Entry point</summary>
        [STAThread]
        public static void Main( )
        {
            using ( T12_GLSL_Parallax example = new T12_GLSL_Parallax( ) )
            {
                Utilities.SetWindowTitle( example );
                example.Run( 30.0, 0.0 );
            }
        }
    }
}