/*
 * This file is part of Afterwarp Framework (http://afterwarp.io).
 * Copyright (c) 2015 - 2025 Dr. Yuriy Kotsarenko. All rights reserved.
 *
 * This example source code is licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of the License at
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License is
 * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and limitations under the License.
 */

// This example illustrates the following features:
//  * Parallax Mapping with optional super-sampling to produce realistic looking surfaces using a
//    combination of normal map, displacement map and ambient occlusion map textures. The example shows
//    the difference between different techniques.
//  * Third-person view camera with location-based zoom (zooms in or out where the mouse is pointing at).

namespace ParallaxMapping
{
  using System.Numerics;
  using System.Windows.Forms;
  using Afterwarp;
  using TextRenderer = Afterwarp.TextRenderer;
  using Timer = Afterwarp.Timer;
  using Application = System.Windows.Forms.Application;

  public partial class MainForm : Form
  {
    private Device device;
    private SwapChain swapChain;
    private Canvas canvas;
    private TextRenderer textRenderer;

    private SceneLights lights;
    private TextureCabinet textureCabinet;
    private Scene sceneDepthNormals;
    private Scene sceneModeling;
    private MeshModel modelPlane;

    private ActorCamera camera;
    private Timer timer;

    private Texture textureAlbedo;
    private Texture textureNormalMap;
    private Texture textureNormalAmbientMap;
    private Texture textureParallaxMap;

    private Matrix4x4 world;
    private Matrix4x4 view;
    private Matrix4x4 projection;

    private Vector2 displaySize;
    private Vector2 mousePos;

    private static readonly VertexElement[] vertexElements = [
      new VertexElement("vertexPosition", ElementFormat.Float, 3),
      new VertexElement("vertexNormal", ElementFormat.Float, 3),
      new VertexElement("vertexTangent", ElementFormat.Float, 3),
      new VertexElement("vertexTexCoord", ElementFormat.Float, 2)
    ];

    private enum RenderingTechnique
    {
      // Textured plane.
      Normal,

      // Textured plane with bump-mapping.
      // This uses texture and normal map.
      BumpMapping,

      // Textured plane with bump-mapping and parallax mapping.
      // This uses texture, normal, displacement and ambient occlusion maps.
      ParallaxMapping,

      // Textured plane with bump-mapping, parallax mapping and super-sampling.
      // This uses texture, normal, displacement and ambient occlusion maps.
      ParallaxMappingSuperSample
    }

    private RenderingTechnique renderingTechnique = RenderingTechnique.ParallaxMappingSuperSample;

    public MainForm()
    {
      InitializeComponent();
      Application.Idle += Application_Idle;
    }

    private void MainForm_Load(object sender, EventArgs e)
    {
      displaySize = new Vector2(ClientSize.Width, ClientSize.Height);
      try
      {
        device = new Device(DeviceTechnology.Direct3D);
        swapChain = new SwapChain(device, Handle);
        canvas = new Canvas(device);
        textRenderer = new TextRenderer(canvas, new Point(512, 512));

        lights = new SceneLights(device);

        textureCabinet = new TextureCabinet(device, new Point(displaySize), TextureFidelity.Medium, 4,
          PixelFormat.D32F)
        {
          Attributes = 0
            | TextureCabinet.Attribute.Bloom
            | TextureCabinet.Attribute.BloomCubic
        };

        sceneDepthNormals = Scene.CreateDepthNormals(device);
        sceneDepthNormals.SetVertexElements(0, vertexElements);

        sceneModeling = Scene.CreateModeling(device);
        sceneModeling.SetVertexElements(0, vertexElements);

        // Configure parallax mapping parameters.
        sceneModeling.ParallaxMapping = new ParallaxMappingParameters(0.1f, 8, 32, 1.0f);

        camera = new ActorCamera(position: Vector3.Zero,
          rotation: new Vector3(MathF.PI * 0.25f, MathF.PI * -0.25f, 0.0f), distance: 400.0f)
        { // Limit camera position and rotation to reasonable values.
          Constraints = new CameraConstraints(
            positionMin: new Vector4(-300.0f, -150.0f, -300.0f,  100.0f),
            positionMax: new Vector4( 300.0f,  150.0f,  300.0f, 1500.0f),
            rotationMin: new Vector4(MathF.PI * 0.1f, 0.0f, 0.0f, 0.0f),
            rotationMax: new Vector4(MathF.PI * 0.5f, 0.0f, 0.0f, 0.0f))
        };
        timer = new Timer();

        // Create model of a 3D plane.
        using (MeshBuffer buffer = new())
        {
          buffer.Plane(1, 1, default, new Vector3(1600.0f, 0.0f, 0.0f), new Vector3(0.0f, 0.0f, 1600.0f),
            Vector3.UnitY, Vector2.Zero, new Vector2(10.0f));

          modelPlane = buffer.Model(device, vertexElements);
        }

        // Load textures.
        textureAlbedo = new Texture(device, "../../../../Media/Textures/StoneWall-Albedo.jpg",
          PixelFormat.RGBA8, Texture.Attribute.MipMapping);

        textureNormalMap = new Texture(device, "../../../../Media/Textures/StoneWall-NormalMap.jpg",
          PixelFormat.RGBA8, Texture.Attribute.MipMapping);

        textureNormalAmbientMap = Texture.CreateNormalsAndOcclusion(device,
          "../../../../Media/Textures/StoneWall-NormalMap.jpg",
          "../../../../Media/Textures/StoneWall-Ambient.jpg",
          Texture.Attribute.MipMapping);

        textureParallaxMap = Texture.CreateParallax(device,
          "../../../../Media/Textures/StoneWall-ParallaxMap.png",
          PixelFormat.L8, Texture.Attribute.MipMapping);

        // Create a light source for illuminating the surface.
        lights.Add(new SceneLight(
          new FloatColorRGB(0x00FFFFFFu),
          new FloatColorRGB(0x00FFFFFFu),
          new FloatColorRGB(0x00FFFFFFu),
          new Vector3(100.0f, 400.0f, -100.0f),
          Vector3.Zero));

        MouseWheel += new MouseEventHandler(MainForm_MouseWheel);
      }
      catch (Exception ex)
      {
        MessageBox.Show(ex.Message, "Initialization failed", MessageBoxButtons.OK, MessageBoxIcon.Error);
        Application.Exit();
      }
      SetStyle(ControlStyles.Opaque, true);
      SetStyle(ControlStyles.UserPaint, true);
      SetStyle(ControlStyles.AllPaintingInWmPaint, true);
      SetStyle(ControlStyles.DoubleBuffer, false);
    }

    private void MainForm_FormClosed(object sender, FormClosedEventArgs e)
    {
      modelPlane?.Dispose();
      textureParallaxMap?.Dispose();
      textureNormalAmbientMap?.Dispose();
      textureNormalMap?.Dispose();
      textureAlbedo?.Dispose();

      timer?.Dispose();
      camera?.Dispose();
      sceneModeling?.Dispose();
      sceneDepthNormals?.Dispose();
      textureCabinet?.Dispose();
      lights?.Dispose();

      textRenderer?.Dispose();
      canvas?.Dispose();
      swapChain?.Dispose();
      device?.Dispose();
    }

    private void MainForm_Resize(object sender, EventArgs e)
    {
      if (WindowState != FormWindowState.Minimized && device is not null)
      {
        displaySize = new Vector2(ClientSize.Width, ClientSize.Height);
        swapChain.Resize(new Point(displaySize));
        textureCabinet.Size = new Point(displaySize);
      }
    }

    private void MainForm_Paint(object sender, PaintEventArgs e)
    {
      float scale = DeviceDpi / 96.0f;

      world = Matrix4x4.CreateTranslation(0.0f, -0.01f, 0.0f);
      view = camera.View;
      projection = Utility.MatrixPerspective4x4(MathF.PI * 0.25f, displaySize.X / displaySize.Y,
        10.0f, 2000.0f);

      // Perform light assignment for 3D scene.
      lights.Size = new Point(displaySize);

      // Only one light is used for the entire scene, so only minimal light culling is required.
      lights.CullingMode = ClustersCullingMode.Performance;
      lights.ClusterSize = 512;
      lights.DepthSlices = 1;

      lights.Execute(view, projection);

      // Prepare scene's attributes.
      uint sceneAttributes = Scene.Attribute.Normals;

      // Render depth pre-pass.
      textureCabinet.Clear(TextureCabinetPass.Depth);
      textureCabinet.Begin(TextureCabinetPass.Depth);
      try
      {
        sceneDepthNormals.Attributes = sceneAttributes;
        sceneDepthNormals.View = view;
        sceneDepthNormals.Projection = projection;

        sceneDepthNormals.Begin();
        try
        {
          RenderPlane(sceneDepthNormals);
        }
        finally
        {
          sceneDepthNormals.End();
        }
      }
      finally
      {
        textureCabinet.End();
      }
      textureCabinet.Filter(TextureCabinetFilterType.Occlusion, projection);

      // Render main scene.
      textureCabinet.Clear(TextureCabinetPass.Color, FloatColor.Black);
      textureCabinet.Begin(TextureCabinetPass.Color);
      try
      {
        sceneModeling.Lights = lights;
        sceneModeling.SetTexture(SceneTextureType.Albedo, textureAlbedo);

        if (renderingTechnique >= RenderingTechnique.ParallaxMapping)
          sceneModeling.SetTexture(SceneTextureType.NormalMap, textureNormalAmbientMap);
        else if (renderingTechnique >= RenderingTechnique.BumpMapping)
          sceneModeling.SetTexture(SceneTextureType.NormalMap, textureNormalMap);
        else
          sceneModeling.SetTexture(SceneTextureType.NormalMap, null);

        if (renderingTechnique >= RenderingTechnique.ParallaxMapping)
          sceneModeling.SetTexture(SceneTextureType.ParallaxMap, textureParallaxMap);
        else
          sceneModeling.SetTexture(SceneTextureType.ParallaxMap, null);

        if (renderingTechnique >= RenderingTechnique.ParallaxMappingSuperSample)
        { // Use super-sampling to provide antialiasing of parallax occlusion map.
          // Note: this is rather GPU-intensive.
          RenderingState renderingState = device.RenderingState;
          renderingState.States |= RenderingState.State.PerSampleShading;
          device.RenderingState = renderingState;
        }
        sceneModeling.Attributes = sceneAttributes;
        sceneModeling.View = view;
        sceneModeling.Projection = projection;

        sceneModeling.Prepare();
        sceneModeling.Begin();
        try
        {
          RenderPlane(sceneModeling);
        }
        finally
        {
          sceneModeling.End();
        }
      }
      finally
      {
        textureCabinet.End();
      }
      textureCabinet.Filter(TextureCabinetFilterType.Bloom);

      // Render on the form.
      swapChain.Begin();
      try
      {
        device.Clear(DeviceClear.Color, new FloatColor(0xFF404040u));
        textureCabinet.Present();

        canvas.Begin();
        try
        {
          canvas.Attributes = 0;
          canvas.ContextState = CanvasContextState.FlatScene;
          canvas.SamplerState = CanvasSamplerState.Default;

          // Render information text.
          canvas.ContextState = CanvasContextState.FlatText;
          {
            FontParameters parameters = new("Segoe UI", 14.0f * scale, FontWeight.SemiBold);
            parameters.Effect.FillOpacity = 1.0f;
            parameters.Effect.FillBrightness = 0.0f;
            parameters.Effect.BorderBrightness = 1.0f;
            parameters.Effect.BorderOpacity = 0.75f;
            parameters.Effect.BorderThickness = 1.0f;
            parameters.Effect.BorderType = FontBorder.SemiHeavy;
            parameters.Effect.ShadowBrightness = 0.85f;
            parameters.Effect.ShadowOpacity = 0.4f;
            parameters.Effect.ShadowDistance = new Vector2(2.0f, 2.0f);
            parameters.Effect.ShadowSmoothness = 2;
            textRenderer.Parameters = parameters;

            textRenderer.DrawAligned(new Vector2(displaySize.X * 0.5f, 4.0f * scale),
              "Press Space to change between techniques",
              new ColorPair(0xFFA5E7B8u, 0xFF77FFCEu), TextAlignment.Middle, TextAlignment.Start);

            string text = renderingTechnique switch
            {
              RenderingTechnique.Normal => "Current technique: Normal (textured plane)",
              RenderingTechnique.BumpMapping => "Current technique: Bump-Mapping",
              RenderingTechnique.ParallaxMapping => "Current technique: Parallax Mapping",
              _ => "Current technique: Parallax Mapping with Super-Sampling",
            };

            textRenderer.DrawAligned(new Vector2(displaySize.X * 0.5f, displaySize.Y - 4.0f * scale),
              text, new ColorPair(0xFFA5E7B8u, 0xFF77FFCEu), TextAlignment.Middle, TextAlignment.End);
          }
          { // Draw technology string.
            FontParameters parameters = new("Segoe UI", 14.0f * scale, FontWeight.SemiBold);
            parameters.Effect.FillBrightness = 0.0f;
            parameters.Effect.BorderBrightness = 1.0f;
            parameters.Effect.BorderType = FontBorder.SemiHeavy;
            parameters.Effect.ShadowBrightness = 0.75f;
            parameters.Effect.ShadowOpacity = 1.0f;

            textRenderer.Parameters = parameters;
            textRenderer.Draw(new Vector2(8.0f * scale, 4.0f * scale),
              string.Format("Direct3D ({0}.{1}), features: {2}.{3}",
              device.TechVersion >> 8, (device.TechVersion >> 4) & 0xF,
              device.TechFeatureVersion >> 8, (device.TechFeatureVersion >> 4) & 0xF),
              new ColorPair(0xFF40E0FFu, 0xFF2020FFu), 1.0f);

            textRenderer.DrawAligned(new Vector2(displaySize.X - 8.0f * scale,
              displaySize.Y - 6.0f * scale), $"Frame Rate: {(int)MathF.Round(timer.FrameRate)}",
              new ColorPair(0xFF40E0FFu, 0xFF2020FFu), TextAlignment.End, TextAlignment.End);
          }
        }
        finally
        {
          canvas.End();
        }
      }
      finally
      {
        swapChain.End();
      }
      timer.Update();
    }

    private void Application_Idle(object sender, EventArgs e)
    {
      if (WindowState != FormWindowState.Minimized)
        Invalidate(); // Continuously repaint the form.
    }

    private void MainForm_MouseDown(object sender, MouseEventArgs e)
    {
      mousePos = new Vector2(e.X, e.Y);

      if (e.Button == MouseButtons.Left)
        camera.Command(CameraCommand.Rotation, new Vector3(mousePos, 0.0f), displaySize);
      else if (e.Button == MouseButtons.Right)
        camera.Command(CameraCommand.Dragging, new Vector3(mousePos, 0.0f), displaySize);
    }

    private void MainForm_MouseMove(object sender, MouseEventArgs e)
    {
      mousePos = new Vector2(e.X, e.Y);
      camera.Command(CameraCommand.Update, new Vector3(mousePos, 0.0f), displaySize);
    }

    private void MainForm_MouseUp(object sender, MouseEventArgs e)
    {
      mousePos = new Vector2(e.X, e.Y);
      camera.Command(CameraCommand.Stop, new Vector3(mousePos, 0.0f), displaySize);
    }

    private void MainForm_MouseWheel(object sender, MouseEventArgs e)
    {
      camera.Zoom(mousePos, displaySize, projection, -e.Delta * 0.25f);
    }

    private void MainForm_KeyDown(object sender, KeyEventArgs e)
    {
      if (e.KeyCode == Keys.Return && e.Modifiers.HasFlag(Keys.Alt))
      {
        if (WindowState == FormWindowState.Maximized)
        {
          FormBorderStyle = FormBorderStyle.Sizable;
          WindowState = FormWindowState.Normal;
        }
        else
        {
          WindowState = FormWindowState.Normal;
          FormBorderStyle = FormBorderStyle.None;
          WindowState = FormWindowState.Maximized;
        }
      }
      else if (e.KeyCode == Keys.Escape)
        Close();
      else if (e.KeyCode == Keys.Space)
        renderingTechnique = (RenderingTechnique)(((int)renderingTechnique + 1) % 4);
    }

    private void RenderPlane(Scene scene)
    {
      if (scene.Equals(sceneModeling))
      {
        ObjectMaterial material = ObjectMaterial.Default;
        material.AmbientColor = new FloatColorRGB(0x00303030u);
        material.AlbedoColor = new FloatColor(0xFFFFFFu);
        material.SpecularColor = new FloatColorRGB(0x00404040u);
        material.SpecularExponent = 16.0f;
        material.Technique = TechniqueLighting.Minnaert;
        material.Roughness = new Vector3(0.8f, 1.0f, 0.8f);
        scene.Material = material;
      }
      scene.World = world;
      modelPlane.Draw(scene.Program);
    }
  }
}