/*
 * 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 3D meshes by performing an extrusion over a vector path.
//  * Two independent shadow-casting light sources, HDR rendering and bloom.
//  * HDR lighting, ambient occlusion and bloom effect.
//  * Third-person view camera with location-based zoom (zooms in or out where the mouse is pointing at).

namespace Extrusion
{
  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 ObjectModels objects;
    private SceneLights lights;
    private TextureCabinet textureCabinet;
    private ShadowCastingAtlas shadowCastingAtlas;
    private ObjectModelView[] shadowCasterViews;
    private MeshModel modelFloor;
    private MeshModel modelCircles;
    private MeshModel modelDiamonds;

    private Scene sceneDepthNormals;
    private Scene sceneModeling;

    private ActorCamera camera;
    private Matrix4x4 projection;

    private Vector2 displaySize;
    private Vector2 mousePos;
    private bool circles = true;

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

    public MainForm()
    {
      InitializeComponent();
    }

    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);
        objects = new ObjectModels(null);

        textureCabinet = new TextureCabinet(device, new Point(displaySize), TextureFidelity.High, 8);

        { // Configure tone-mapping/bloom parameters.
          ToneMappingBloom toneMappingBloom = textureCabinet.ToneMappingBloom;
          toneMappingBloom.ToneWhite = 2.0f;
          textureCabinet.ToneMappingBloom = toneMappingBloom;
        }

        shadowCastingAtlas = new ShadowCastingAtlas(device, new Point(512 + 2, 256));

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

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

        shadowCasterViews = new ObjectModelView[2];

        for (int i = 0; i < shadowCasterViews.Length; ++i)
        {
          SceneLight light = SceneLight.Default;
          light.AmbientColor = new FloatColorRGB(0x00FFFFFFu);
          light.AlbedoColor = new FloatColorRGB(0x00FFFFFFu);
          light.SpecularColor = new FloatColorRGB(0x00FFFFFFu);
          light.SpecularExponent = 4.0f;
          light.Intensity = 1.0f;
          light.Position = i == 0 ? new Vector3(300.0f, 600.0f, -300.0f) :
            new Vector3(-300.0f, 600.0f, 300.0f);
          light.ShadowCaster = i;
          lights.Add(light);

          shadowCasterViews[i] = objects.CreateView();
          shadowCasterViews[i].View = Utility.MatrixLookAt4x4(light.Position, Vector3.Zero, Vector3.UnitY);
          shadowCasterViews[i].Projection = Utility.MatrixPerspective4x4(MathF.PI * 0.35f, 1.0f,
            200.0f, 1400.0f);

          shadowCastingAtlas.Add(new Point(256, 256));
        }

        camera = new ActorCamera(position: Vector3.Zero, rotation: new Vector3(MathF.PI * 0.4f, 0.0f, 0.0f),
          distance: 400.0f)
        {
          Constraints = new CameraConstraints(
            positionMin: new Vector4(-100.0f, -50.0f, -100.0f, 200.0f),
            positionMax: new Vector4(100.0f, 50.0f, 100.0f, 500.0f),
            rotationMin: new Vector4(MathF.PI * 0.01f, 0.0f, 0.0f, 0.0f),
            rotationMax: new Vector4(MathF.PI * 0.4f, 0.0f, 0.0f, 0.0f))
        };

        using (MeshBuffer buffer = new())
        {
          // Create floor model.
          buffer.Plane(1, 1, default, new Vector3(1200.0f, 0.0f, 0.0f), new Vector3(0.0f, 0.0f, 1200.0f),
            Vector3.UnitY);

          modelFloor = buffer.Model(device, vertexElements);

          { // Create extruded meshes.
            PathBuilder builder = new();

            // Construct a path with circular holes.
            builder.Rectangle(new RectF(0.0f, 0.0f, 600.0f, 600.0f), true);

            for (int j = 0; j < 11; ++j)
              for (int i = 0; i < 10; ++i)
              {
                if (j % 2 == 1 && i == 9)
                  break;

                builder.Circle(new Vector2(40.0f + i * 58.0f + (j % 2) * 58.0f * 0.5f,
                  40.0f + j * 58.0f / Canvas.HexagonDelta), 28.0f, false);
              }

            buffer.Clear();
            buffer.Extrusion(builder.Elements, 20.0f);
            buffer.Centralize();

            modelCircles = buffer.Model(device, vertexElements);

            // Construct a path with diamond holes.
            builder.Clear();
            builder.Rectangle(new RectF(0.0f, 0.0f, 600.0f, 600.0f), true);

            for (int j = 0; j < 11; ++j)
              for (int i = 0; i < 10; ++i)
              {
                if (j % 2 == 1 && i == 9)
                  break;

                builder.Diamond(new Vector2(40.0f + i * 58.0f + (j % 2) * 58.0f * 0.5f,
                  40.0f + j * 58.0f / Canvas.HexagonDelta), 28.0f, false);
              }

            buffer.Clear();
            buffer.Extrusion(builder.Elements, 20.0f);
            buffer.Centralize();

            modelDiamonds = buffer.Model(device, vertexElements);
          }
        }
        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)
    {
      modelDiamonds?.Dispose();
      modelCircles?.Dispose();
      modelFloor?.Dispose();
      camera?.Dispose();
      sceneModeling?.Dispose();
      sceneDepthNormals?.Dispose();
      shadowCastingAtlas?.Dispose();
      textureCabinet?.Dispose();
      objects?.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);
        Invalidate();
      }
    }

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

      Matrix4x4 view = camera.View;
      projection = Utility.MatrixPerspective4x4(MathF.PI * 0.25f, displaySize.X / displaySize.Y,
        10.0f, 1200.0f);

      // Perform light assignment for 3D scene.
      lights.Size = new Point(displaySize);
      lights.CullingMode = ClustersCullingMode.Performance;
      lights.ClusterSize = 256;
      lights.DepthSlices = 4;
      lights.Execute(view, projection);

      { // Update texture cabinet's attributes.
        uint attributes = 0
          | TextureCabinet.Attribute.Bloom
          | TextureCabinet.Attribute.BloomCubic
          | TextureCabinet.Attribute.AmbientOcclusion;

        if (scale >= 2.0f)
          attributes |= TextureCabinet.Attribute.BloomDownscale |
            TextureCabinet.Attribute.AmbientOcclusionDownscale;

        textureCabinet.Attributes = attributes;
      }

      // Render shadow maps
      shadowCastingAtlas.BorderFill();

      for (int i = 0; i < shadowCasterViews.Length; ++i)
      {
        ShadowCaster shadowCaster = shadowCastingAtlas.Caster(i);
        ObjectModelView shadowCasterView = shadowCasterViews[i];

        shadowCaster.ViewProjection = shadowCasterView.ViewProjection;
        shadowCasterView.Update();

        shadowCaster.Clear();
        shadowCaster.Begin();
        try
        {
          sceneDepthNormals.Attributes = 0;
          sceneDepthNormals.View = shadowCasterView.View;
          sceneDepthNormals.Projection = shadowCasterView.Projection;

          sceneDepthNormals.Begin();
          try
          {
            RenderModels(sceneDepthNormals);
          }
          finally
          {
            sceneDepthNormals.End();
          }
        }
        finally
        {
          shadowCaster.End();
        }
        shadowCaster.Filter();
      }

      // Prepare scene's attributes.
      uint sceneAttributes = 0
        | Scene.Attribute.Normals
        | Scene.Attribute.ShadowsCubic
        | Scene.Attribute.DepthPrePass;

      // 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
        {
          RenderModels(sceneDepthNormals);
        }
        finally
        {
          sceneDepthNormals.End();
        }
      }
      finally
      {
        textureCabinet.End();
      }
      textureCabinet.Filter(TextureCabinetFilterType.Occlusion, projection);

      // Render main scene.
      textureCabinet.Clear(TextureCabinetPass.Color, (new FloatColor(2.0f, 2.0f, 2.0f, 0.0f) * 0.75f));
      textureCabinet.Begin(TextureCabinetPass.Color);
      try
      {
        sceneModeling.Lights = lights;
        sceneModeling.Atlas = shadowCastingAtlas;
        sceneModeling.TextureCabinet = textureCabinet;

        sceneModeling.View = view;
        sceneModeling.Projection = projection;
        sceneModeling.Attributes = sceneAttributes;

        sceneModeling.Prepare();
        sceneModeling.Begin();
        try
        {
          RenderModels(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.FlatText;
          canvas.SamplerState = CanvasSamplerState.Default;
          { // Draw text hints.
            FontParameters parameters = new("Segoe UI", 16.0f * scale, FontWeight.SemiBold);
            parameters.Effect.BorderType = FontBorder.SemiHeavy;
            parameters.Effect.BorderBrightness = 1.0f;
            parameters.Effect.BorderOpacity = 0.75f;
            parameters.Effect.FillBrightness = 0.0f;
            parameters.Effect.ShadowBrightness = 0.85f;
            parameters.Effect.ShadowOpacity = 0.4f;
            textRenderer.Parameters = parameters;

            textRenderer.DrawAligned(new Vector2(displaySize.X - 4.0f * scale, displaySize.Y - 4.0f * scale),
              "Press Space to switch between circular and diamond holes.",
              new ColorPair(0xFF4040FFu, 0xFF40FFFFu), TextAlignment.End, TextAlignment.End);
          }
          { // Draw technology string.
            FontParameters parameters = new("Segoe UI", 16.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(0x80FFFFFFu), 1.0f);
          }
        }
        finally
        {
          canvas.End();
        }
      }
      finally
      {
        swapChain.End();
      }
    }

    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(mousePos, 0.0f), displaySize);
      else if (e.Button == MouseButtons.Right)
        camera.Command(CameraCommand.Dragging, new(mousePos, 0.0f), displaySize);
    }

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

      if (camera.Command(CameraCommand.Update, new(mousePos, 0.0f), displaySize))
        Invalidate();
    }

    private void MainForm_MouseUp(object sender, MouseEventArgs e)
    {
      mousePos = new Vector2(e.X, e.Y);
      camera.Command(CameraCommand.Stop, new(mousePos, 0.0f), displaySize);
    }
    private void MainForm_MouseWheel(object sender, MouseEventArgs e)
    {
      camera.Zoom(mousePos, displaySize, projection, -e.Delta * 0.125f);
      Invalidate();
    }
    private void MainForm_KeyDown(object sender, KeyEventArgs e)
    {
      if (e.KeyCode == Keys.Space)
      {
        circles = !circles;
        Invalidate();
      }
    }

    private void RenderModels(Scene scene)
    {
      if (scene.Equals(sceneModeling))
      {
        ObjectMaterial material = ObjectMaterial.Default;
        material.AmbientColor = new FloatColorRGB(0.75f, 0.75f, 0.75f);
        material.AlbedoColor = FloatColor.TranslucentBlack;
        material.SpecularColor = FloatColorRGB.Black;
        material.Shadows = 1.0f;
        material.Occlusion = 1.0f;
        scene.Material = material;
      }
      scene.World = Matrix4x4.Identity;
      modelFloor.Draw(scene.Program);

      if (scene.Equals(sceneModeling))
      {
        ObjectMaterial material = ObjectMaterial.Default;
        material.AmbientColor = new FloatColorRGB(0.25f, 0.25f, 0.25f);
        material.AlbedoColor = new FloatColor(0.75f, 0.75f, 0.75f, 0.5f);
        material.SpecularColor = new FloatColorRGB(1.0f, 1.0f, 1.0f);
        material.SpecularExponent = 4.0f;
        material.Shadows = 1.0f;
        material.Occlusion = 1.0f;
        material.Technique = TechniqueLighting.CookTorrance;
        material.Roughness = new Vector3(1.8f, 1.2f, 0.7f);
        scene.Material = material;
      }
      scene.World = Matrix4x4.CreateTranslation(0.0f, 10.0f, 0.0f);

      if (circles)
        modelCircles.Draw(scene.Program);
      else
        modelDiamonds.Draw(scene.Program);
    }
  }
}