/*
 * 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:
//  * Manually creating HLSL shaders, vertex and constant buffers.

namespace Triangle
{
  using System.Numerics;
  using System.Runtime.InteropServices;
  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 Texture textureColor;
    private Texture textureDepths;
    private Texture textureColorResolved;

    private Afterwarp.Program programTriangle;

    private Buffer bufferTransform;
    private Buffer bufferMaterial;

    private Buffer bufferVertices;
    private Buffer bufferNormals;
    private Buffer bufferColors;
    private Buffer bufferIndices;

    private Vector2 displaySize;

    // Note: vertex elements describe how vertex buffers are treated and passed to vertex shader.
    // If all elements use the same channel, then vertex buffer is interleaved; otherwise, multiple
    // buffers can be bound to different channels.
    private readonly VertexElement[] vertexElements = [
      new(name: "vertexPosition", format: ElementFormat.Float, count: 3, channel: 0),
      new(name: "vertexNormal", format: ElementFormat.Float, count: 3, channel: 1),
      new(name: "vertexColor", format: ElementFormat.UnsignedByte, count: 4,
        channel: 2 | VertexElement.ChannelNormalized)
    ];

    // Note: program elements describe what type of buffers are passed to the shaders. In this case, only
    // constant buffers are being passed and they have to use unique channel index, where each channel
    // would correspond to a combination of shader type and physical slot (defined in HLSL code).
    private readonly ProgramElement[] programElements = [
      new(name: "BufferTransform", element: ShaderElement.ConstantBuffer, shader: ShaderType.Vertex,
        channel: 0, index: 0, size: (uint)Marshal.SizeOf<BufferTransform>()),
      new(name: "BufferMaterial", element: ShaderElement.ConstantBuffer, shader: ShaderType.Fragment,
        channel: 1, index: 0, size: (uint)Marshal.SizeOf<BufferTransform>())
    ];

    // Structure that describes how to pass parameters to vertex shader.
    private struct BufferTransform
    {
      public Matrix4x4 objectViewProj;
      public Matrix4x4 objectWorldInvTrans;
      public Matrix4x4 objectWorld;
    }

    // Structure that describes how to pass parameters to pixel shader.
    private struct BufferMaterial
    {
      public Vector4 lightPosition;
      public Vector4 eyePosition;
      public Vector4 diffuseColor;
      public Vector4 specularColor;
      public Vector4 ambientColor;
    }

    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));

        bufferTransform = new Buffer(device, BufferDataType.Constant, BufferAccessType.Dynamic,
          (uint)Marshal.SizeOf<BufferTransform>());

        bufferMaterial = new Buffer(device, BufferDataType.Constant, BufferAccessType.Dynamic,
          (uint)Marshal.SizeOf<BufferMaterial>());

        programTriangle = new Afterwarp.Program(device, vertexElements, programElements, null,
          File.ReadAllBytes("..\\..\\..\\shaders\\Triangle.vs.bin"), null, null, null,
          File.ReadAllBytes("..\\..\\..\\shaders\\Triangle.ps.bin"));

        CreateBuffers();
        CreateTextures();
      }
      catch (Exception ex)
      {
        MessageBox.Show(ex.Message, "Initialization failed", MessageBoxButtons.OK, MessageBoxIcon.Error);
        Application.Exit();
      }
    }

    private void MainForm_FormClosed(object sender, FormClosedEventArgs e)
    {
      DestroyTextures();

      programTriangle?.Dispose();

      bufferMaterial?.Dispose();
      bufferTransform?.Dispose();
      bufferIndices?.Dispose();
      bufferColors?.Dispose();
      bufferNormals?.Dispose();
      bufferVertices?.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));

        DestroyTextures();
        try
        {
          CreateTextures();
        }
        catch (Exception ex)
        {
          MessageBox.Show(ex.Message, "Re-creating textures failed.", MessageBoxButtons.OK,
            MessageBoxIcon.Error);
          Application.Exit();
        }
      }
    }

    private void MainForm_Paint(object sender, PaintEventArgs e)
    {
      float scale = DeviceDpi / 96.0f;
      double ticks = Utility.SystemTicks / 1000.0;

      // Configure world and projection matrix.
      Matrix4x4 world = Matrix4x4.CreateRotationY((float)Math.PI *
        (Utility.SineCycle((float)(ticks * 0.0001)) - 0.5f) * 0.25f);

      Vector3 viewerPosition = new(0.0f, 0.0f, -80.0f);

      Matrix4x4 view = Utility.MatrixLookAt4x4(viewerPosition, Vector3.Zero, Vector3.UnitY);

      Matrix4x4 projection = Utility.MatrixPerspective4x4((float)Math.PI * 0.25f, displaySize.X /
        displaySize.Y, 5.0f, 400.0f);

      Matrix4x4.Invert(world, out Matrix4x4 worldInverseTranspose);
      worldInverseTranspose = Matrix4x4.Transpose(worldInverseTranspose);

      { // Update constant buffer for Vertex Shader
        BufferTransform transform = new()
        {
          objectViewProj = view * projection,
          objectWorldInvTrans = worldInverseTranspose,
          objectWorld = world
        };
        bufferTransform!.Update(StructToByteArray(transform));
      }
      { // Update constant buffer for Fragment Shader
        BufferMaterial material = new()
        {
          lightPosition = new Vector4(0.0f, -20.0f, -300.0f, 0.0f),
          eyePosition = new Vector4(viewerPosition, 0.0f),
          diffuseColor = new Vector4(0.0f, 1.0f, 0.0f, 1.0f),
          specularColor = new Vector4(1.0f, 1.0f, 1.0f, 4.0f),
          ambientColor = new Vector4(0.25f, 0.25f, 0.25f, 0.0f)
        };
        bufferMaterial!.Update(StructToByteArray(material));
      }

      // Attach depth to color texture so that 3D rendering uses both color and depth buffers.
      textureColor!.Detach();
      textureColor.Attach(textureDepths!);

      // Start rendering to textures.
      textureColor.Begin();
      try
      {
        // Clear the color texture and attached depth buffer.
        // Note: we use "reversed depth" to be consistent with the framework.
        device!.Clear(DeviceClear.Color | DeviceClear.Depth, new FloatColor(0.25f, 0.25f, 0.25f), 1.0f);

        // Prepare rendering state.
        RenderingState state = RenderingState.Default;
        state.States =
          RenderingState.State.ColorWrite |
          RenderingState.State.DepthWrite |
          RenderingState.State.DepthTest |
          RenderingState.State.Multisampling;

        state.CullFace = TriangleFace.Back;
        state.DepthFunc = ComparisonFunc.LessEqual;
        device.RenderingState = state;

        programTriangle!.Begin();
        try
        {
          // Note: constant buffer channels correspond to those declared above in "programElements".
          programTriangle.Bind(bufferTransform, 0);
          programTriangle.Bind(bufferMaterial, 1);

          // Note: vertex buffer channels correspond to those declared above in "vertexElements".
          programTriangle.Bind(bufferVertices, 0);
          programTriangle.Bind(bufferNormals, 1);
          programTriangle.Bind(bufferColors, 2);
          programTriangle.Bind(bufferIndices);

          // Send the data to GPU.
          programTriangle.DrawIndexed(PrimitiveTopology.Triangles, 3);
        }
        finally
        {
          programTriangle.End();
        }
      }
      finally
      {
        textureColor.End();
      }
      textureColorResolved!.Copy(textureColor); // Resolve MSAA to non-MSAA texture.

      // Render on the form.
      swapChain!.Begin();
      try
      {
        device!.Clear(DeviceClear.Color, new FloatColor(0.5f, 0.0f, 0.0f));
        canvas!.Begin();
        try
        {
          // Draw color texture first.
          canvas.ContextState = CanvasContextState.FlatScene;
          canvas.SamplerState = new CanvasSamplerState();

          canvas.Quad(textureColorResolved, new Quad(0.0f, 0.0f, displaySize.X, displaySize.Y), null, null,
            BlendingEffect.Copy);

          // Draw some information text.
          canvas.ContextState = CanvasContextState.FlatText;

          { // 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 CreateBuffers()
    {
      Vector3[] vertices = [
        new(0.0f, 32.0f, 0.0f),
        new(-30.0f, -28.0f, 0.0f),
        new(30.0f, -28.0f, 0.0f)];

      Vector3[] normals = [
        new(0.0f, 0.0f, -1.0f),
        new(0.0f, 0.0f, -1.0f),
        new(0.0f, 0.0f, -1.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);

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

      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);
    }

    private void CreateTextures()
    {
      TextureParameters parameters = default;
      parameters.Width = (int)displaySize.X;
      parameters.Height = (int)displaySize.Y;
      parameters.Attributes = Texture.Attribute.Drawable;
      parameters.Format = PixelFormat.RGBA8;
      parameters.Multisamples = 8;

      // MSAA Color texture
      textureColor = new Texture(device, parameters);

      // MSAA Depths texture
      parameters.Format = PixelFormat.D32F;

      textureDepths = new Texture(device, parameters);

      // Resolved Color Texture
      parameters.Format = PixelFormat.RGBA8;
      parameters.Multisamples = 0;

      textureColorResolved = new Texture(device, parameters);
    }

    private void DestroyTextures()
    {
      textureColorResolved?.Dispose();
      textureColorResolved = null;
      textureDepths?.Dispose();
      textureDepths = null;
      textureColor?.Dispose();
      textureColor = null;
    }

    /// <summary>
    /// Converts a structure into byte array so that it can be passed as parameter for updating buffers.
    /// </summary>
    /// <typeparam name="T">Type of the structure</typeparam>
    /// <param name="structData">Structure that needs to be converted</param>
    /// <returns>Byte array that can be passed to Buffer.Update function</returns>
    public static byte[] StructToByteArray<T>(T structData) where T : struct
    {
      int structBytes = Marshal.SizeOf<T>();
      byte[] byteArray = new byte[structBytes];

      IntPtr nativeMem = Marshal.AllocHGlobal(structBytes);
      Marshal.StructureToPtr(structData, nativeMem, true);
      Marshal.Copy(nativeMem, byteArray, 0, structBytes);
      Marshal.FreeHGlobal(nativeMem);

      return byteArray;
    }
  }
}