/*
 * 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:
//  * Creating vertex and index buffers manually to render a single triangle.
//  * HDR color rendering with bloom effect.
//  * Optional depth pre-pass.

namespace Triangle
{
  using System.Numerics;
  using System.Windows.Forms;
  using Afterwarp;
  using TextRenderer = Afterwarp.TextRenderer;
  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 Buffer bufferVertices;
    private Buffer bufferColors;
    private Buffer bufferIndices;

    private Vector2 displaySize;
    private Matrix4x4 world;
    private Matrix4x4 view;
    private Matrix4x4 projection;

    private readonly VertexElement[] vertexElements = {
      new("vertexPosition", ElementFormat.Float, 3),
      new("vertexColor", ElementFormat.UnsignedByte, 4, 1 | VertexElement.ChannelNormalized)
    };

    private const bool depthPrePass = true;

    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, null, PixelFormat.Unknown, PixelFormat.Unknown, 0, true);
        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, 8,
          PixelFormat.D16);

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

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

        CreateBuffers();

        lights.Add(new SceneLight(
          new FloatColorRGB(0x00FFFFFFu),
          new FloatColorRGB(0x00FFFFFFu),
          new FloatColorRGB(0x00FFFFFFu),
          new Vector3(0.0f, -20.0f, -300.0f),
          Vector3.Zero));

        view = Utility.MatrixLookAt4x4(new Vector3(0.0f, 0.0f, -80.0f), Vector3.Zero, Vector3.UnitY);
      }
      catch (Exception ex)
      {
        MessageBox.Show(ex.Message, "Initialization failed", MessageBoxButtons.OK, MessageBoxIcon.Error);
        Application.Exit();
      }
    }

    private void MainForm_FormClosed(object sender, FormClosedEventArgs e)
    {
      bufferIndices?.Dispose();
      bufferColors?.Dispose();
      bufferVertices?.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;
      double ticks = Utility.SystemTicks / 1000.0;

      // Configure world and projection matrix.
      world = Matrix4x4.CreateRotationY((float)Math.PI * (Utility.SineCycle((float)(ticks * 0.0001)) -
        0.5f) * 0.25f);
      projection = Utility.MatrixPerspective4x4((float)Math.PI * 0.25f, displaySize.X / displaySize.Y,
        10.0f, 2000.0f);

      // Update texture cabinet's attributes.
      textureCabinet.Attributes = TextureCabinet.Attribute.Bloom | TextureCabinet.Attribute.BloomCubic;

      // 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.
      const uint attributes = Scene.Attribute.Coloring;

      if (depthPrePass)
      {
        textureCabinet.Clear(TextureCabinetPass.Depth);
        textureCabinet.Begin(TextureCabinetPass.Depth);
        try
        {
          sceneDepthNormals.Attributes = attributes;
          RenderTriangle(sceneDepthNormals);
        }
        finally
        {
          textureCabinet.End();
        }
        textureCabinet.Filter(TextureCabinetFilterType.Occlusion, projection);
      }

      // Render main scene.
      textureCabinet.Clear(TextureCabinetPass.Color, new FloatColor(0.25f, 0.25f, 0.25f));
      textureCabinet.Begin(TextureCabinetPass.Color);
      try
      {
        sceneModeling.Lights = lights;
        sceneModeling.Attributes = attributes;

        RenderTriangle(sceneModeling);
      }
      finally
      {
        textureCabinet.End();
      }
      textureCabinet.Filter(TextureCabinetFilterType.Bloom);

      // Note: for semi-transparent objects, it is possible to do a "glassy" (order-independent transparency)
      // rendering pass here. To see how it is done, please check other examples.

      // Render on the form.
      swapChain.Begin();
      try
      {
        // Clear the screen to discard previous swap-chain contents, which may improve performance.
        device.Clear(DeviceClear.Color, new FloatColor(0.5f, 0.0f, 0.0f));

        // Present 3D scene on the screen.
        textureCabinet.Present();

        // Draw 2D text on top of 3D scene.
        canvas.Begin();
        try
        {
          canvas.ContextState = CanvasContextState.FlatText;
          canvas.SamplerState = new CanvasSamplerState();

          { // Draw technology string.
            FontParameters parameters = new("Segoe UI", 14.0f * scale, FontWeight.SemiBold);
            parameters.Effect.BorderType = FontBorder.SemiHeavy;
            parameters.Effect.BorderBrightness = 1.0f;
            parameters.Effect.FillBrightness = 0.0f;
            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(0xFFC0FFC0u, 0xFFFF8020u), 1.0f);
          }
        }
        finally
        {
          canvas.End();
        }
      }
      finally
      {
        swapChain.End();
      }
    }

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

    private void RenderTriangle(Scene scene)
    {
      // Set view and projection matrices.
      scene.View = view;
      scene.Projection = projection;

      // Note: the following call is not required for depth/normals scene, so it will fail during depth
      // pre-pass, which is how we detect whether we are rendering to "depth/normals" or "modeling" scene.
      bool modeling = scene.TryPrepare();

      scene.Begin();
      try
      {
        // Set world matrix to position the object accordingly in 3D scene.
        scene.World = world;

        // Prepare material to make sure triangle has some specular highlight and produce bloom.
        // Note: albedo's alpha-channel is actually a bloom coefficient when rendering main scene.
        if (modeling)
        {
          ObjectMaterial material = ObjectMaterial.Default;
          material.AmbientColor = new FloatColorRGB(0x00404040u);
          material.AlbedoColor = new FloatColor(0xB0FFFFFFu);
          material.SpecularColor = new FloatColorRGB(0x00FFFFFFu);
          material.SpecularExponent = 8.0f;
          scene.Material = material;
        }

        // Retrieve shader program from the scene to bind the buffers and issue a draw call.
        var program = scene.Program;

        program.Bind(bufferVertices, 0);
        program.Bind(bufferColors, 1);
        program.Bind(bufferIndices);

        program.DrawIndexed(PrimitiveTopology.Triangles, 3);
      }
      finally
      {
        scene.End();
      }
    }

    private void CreateBuffers()
    {
      Vector3[] vertices = [
        new(0.0f, 32.0f, 0.0f),
        new(-30.0f, -28.0f, 0.0f),
        new(30.0f, -28.0f, 0.0f)];

      uint[] colors = [0xFF00FF00u, 0xFFFF0000u, 0xFF0000FFu];

      ushort[] indices = [0, 1, 2];

      bufferVertices = new Buffer(device, BufferDataType.Vertex, BufferAccessType.Static,
        3 * 12, 12, PixelFormat.Unknown, vertices);

      bufferColors = new Buffer(device, BufferDataType.Vertex, BufferAccessType.Static,
        3 * 4, 4, PixelFormat.Unknown, colors);

      bufferIndices = new Buffer(device, BufferDataType.Index, BufferAccessType.Static,
        3 * 2, 2, PixelFormat.Unknown, indices);
    }
  }
}