/*
 * 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:
//  * Accurate Order-Independent Transparency (OIT) with Frosted Glass effect.
//  * Object selection, which uses object's bounding boxes and accompanying voxel representation to
//    determine accurately what object is being pointed at, including approximate 3D location that mouse
//    is pointing on the object's surface.
//  * Hierarchial object structure, where some objects are parented to other objects.
//  * Grapher module for visualizing object's bounding box.

namespace ObjectPicking
{
  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 Grapher grapher;

    private SceneMeshes meshes;
    private ObjectModels objects;
    private SceneLights lights;
    private ObjectMaterials materials;

    private TextureCabinet textureCabinet;
    private ShadowCastingAtlas shadowCastingAtlas;
    private ObjectModelView shadowCasterView;

    private Scene sceneDepthNormals;
    private Scene sceneModeling;

    private ActorCamera camera;
    private ObjectModelView viewport;
    private Matrix4x4 projection;
    private ObjectModel selected;

    private Vector2 displaySize;
    private Vector2 mousePos;
    private Ray mouseRay;

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

    private static readonly Vector3 lightPosition = new(300.0f, 800.0f, -300.0f);

    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));
        grapher = new Grapher(device);

        meshes = new SceneMeshes(device);
        objects = new ObjectModels(meshes);
        lights = new SceneLights(device);
        materials = new ObjectMaterials();

        textureCabinet = new TextureCabinet(device, new Point(displaySize), TextureFidelity.High, 8);
        { // Configure Ambient Occlusion effect.
          AmbientOcclusionParameters parameters = textureCabinet.AmbientOcclusionParameters;
          parameters.KernelBias = 0.05f;
          parameters.Strength = 1.0f;
          textureCabinet.AmbientOcclusionParameters = parameters;
        }

        shadowCastingAtlas = new ShadowCastingAtlas(device, new Point(256, 256));
        shadowCastingAtlas.Add(new Point(256, 256));

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

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

        viewport = objects.CreateView();

        shadowCasterView = objects.CreateView();
        shadowCasterView.View = Utility.MatrixLookAt4x4(lightPosition, Vector3.Zero, Vector3.UnitY);
        shadowCasterView.Projection = Utility.MatrixPerspective4x4(MathF.PI * 0.3f, 1.0f, 200.0f, 1400.0f);

        camera = new ActorCamera(position: new Vector3(0.0f, 0.0f, -40.0f),
          rotation: new Vector3(MathF.PI * 0.25f, MathF.PI * -0.25f, 0.0f), distance: 440.0f)
        {
          Constraints = new CameraConstraints(
            positionMin: new Vector4(-200.0f, -100.0f, -200.0f, 100.0f),
            positionMax: new Vector4(200.0f, 100.0f, 200.0f, 1400.0f),
            rotationMin: new Vector4(MathF.PI * 0.1f, 0.0f, 0.0f, 0.0f),
            rotationMax: new Vector4(MathF.PI * 0.4f, 0.0f, 0.0f, 0.0f))
        };

        CreateAndLoadMeshes(meshes);
        CreateLightsAndMaterials(lights, materials);
        CreateObjects();

        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)
    {
      camera?.Dispose();
      objects?.Dispose();
      meshes?.Dispose();
      materials?.Dispose();

      sceneModeling?.Dispose();
      sceneDepthNormals?.Dispose();
      textureCabinet?.Dispose();
      lights?.Dispose();
      shadowCastingAtlas?.Dispose();

      grapher?.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;

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

      viewport.View = view;
      viewport.Projection = projection;

      viewport.Update();

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

      // Perform object selection.
      if (!Matrix4x4.Invert(view, out Matrix4x4 viewInverse))
        viewInverse = Matrix4x4.Identity;

      mouseRay = new(mousePos, displaySize, viewInverse, projection);
      selected = viewport.Select(mouseRay, out float _);

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

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

        textureCabinet.Attributes = attributes;
      }

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

      { // Render shadow maps
        shadowCastingAtlas.BorderFill();

        ShadowCaster shadowCaster = shadowCastingAtlas.Caster(0);
        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
          {
            RenderObjects(sceneDepthNormals, shadowCasterView);
          }
          finally
          {
            sceneDepthNormals.End();
          }
        }
        finally
        {
          shadowCaster.End();
        }
        shadowCaster.Filter();
      }

      // Render main scene.
      textureCabinet.Clear(TextureCabinetPass.Color, new FloatColor(0xFFE0E0E0u));
      textureCabinet.Begin(TextureCabinetPass.Color);
      try
      {
        sceneModeling.Lights = lights;
        sceneModeling.Atlas = shadowCastingAtlas;
        sceneModeling.TextureCabinet = textureCabinet;

        sceneModeling.Attributes = sceneAttributes;
        sceneModeling.View = viewport.View;
        sceneModeling.Projection = viewport.Projection;

        sceneModeling.Prepare();
        sceneModeling.Begin();
        try
        {
          RenderObjects(sceneModeling, viewport);
        }
        finally
        {
          sceneModeling.End();
        }
      }
      finally
      {
        textureCabinet.End();
      }
      textureCabinet.Filter(TextureCabinetFilterType.Occlusion, projection);

      // Render object selection.
      if (selected is not null)
      {
        textureCabinet.Begin(TextureCabinetPass.Color);
        try
        {
          grapher.Transform = view * projection;

          Matrix4x4 globalVolume = selected.GetTransform(ModelTransform.GlobalVolume);

          grapher.Begin();
          try
          {
            RenderingState renderingState = device.RenderingState;

            grapher.BoundingBox(globalVolume, 0xFFFFFFFFu, 6.0f, 0.5f);
            grapher.Flush();

            // Enable depth bias to prevent depth fighting between bounding boxes having same depths.
            renderingState.DepthBias = 5.0f;
            renderingState.SlopeDepthBias = 5.0f;
            device.RenderingState = renderingState;

            grapher.BoundingBox(globalVolume, 0xFF000000u, 3.0f, 0.5f);
          }
          finally
          {
            grapher.End();
          }
        }
        finally
        {
          textureCabinet.End();
        }
      }
      textureCabinet.Filter(TextureCabinetFilterType.Bloom);

      // Render semi-transparent objects.
      textureCabinet.Clear(TextureCabinetPass.Glassy);
      textureCabinet.Begin(TextureCabinetPass.Glassy);
      try
      {
        sceneModeling.Attributes = sceneAttributes | Scene.Attribute.Glassy;

        sceneModeling.Begin();
        try
        {
          RenderObjectsGlassy(sceneModeling, viewport);
        }
        finally
        {
          sceneModeling.End();
        }
      }
      finally
      {
        textureCabinet.End();
      }
      textureCabinet.Filter(TextureCabinetFilterType.Glassy);

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

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

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

          FontParameters parameters = new("Segoe UI", 16.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(0xA0FFFFFFu), 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 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)
    {
      Vector2 prevMousePos = mousePos;
      mousePos = new Vector2(e.X, e.Y);

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

      if (prevMousePos != mousePos)
        Invalidate();
    }

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

    private void RenderObjects(Scene scene, ObjectModelView view)
    {
      for (int i = 0; i < view.ObjectCount; ++i)
      {
        ObjectModel obj = view.Object(i);
        if ((obj.Attributes & ObjectModel.Attribute.Transparent) != 0u)
          continue; // Skip transparent objects.

        string name = obj.Name;

        scene.World = obj.GetTransform(ModelTransform.GlobalModel);

        if ((scene.Attributes & Scene.Attribute.Modeling) != 0)
        {
          ObjectMaterial material = materials.GetMaterial(obj.Material);
          material.AmbientColor *= obj.Color.RGB;
          material.AlbedoColor *= obj.Color;
          scene.Material = material;
        }

        RenderingState renderingState = device.RenderingState;
        if (name == "Table")
        {
          renderingState.CullFace = TriangleFace.None;
          device.RenderingState = renderingState;
        }
        obj.Mesh.Model.Draw(scene.Program);
      }
    }

    private void RenderObjectsGlassy(Scene scene, ObjectModelView view)
    {
      for (int i = 0; i < view.ObjectCount; ++i)
      {
        ObjectModel obj = view.Object(i);
        if ((obj.Attributes & ObjectModel.Attribute.Transparent) == 0u)
          continue;

        scene.World = obj.GetTransform(ModelTransform.GlobalModel);

        if (materials is not null)
        {
          FloatColor color = obj.Color;
          color.Alpha = 0.75f;

          ObjectMaterial material = materials.GetMaterial(obj.Material);
          material.AmbientColor *= obj.Color.RGB;
          material.AlbedoColor *= obj.Color;
          scene.Material = material;
        }
        obj.Mesh.Model.Draw(scene.Program);
      }
    }

    private void CreateObjects()
    {
      Vector3 tableSize;

      ObjectModel floor = objects.Add("Floor");
      floor.MeshName = "Plane";
      floor.Material = 0;
      floor.Attributes = ObjectModel.Attribute.Visible;
      floor.Color = new FloatColor(0xFFE0E0E0u);
      floor.SetTransform(ModelTransform.Local, Matrix4x4.CreateTranslation(0.0f, -0.01f, 0.0f));

      ObjectModel table = objects.Add("Table");
      table.MeshName = "Table";
      table.Material = 4;

      tableSize = table.Size;

      ObjectModel tableGlass = objects.Add("TableGlass");
      tableGlass.Parent = table;
      tableGlass.MeshName = "TableGlass";
      tableGlass.Material = 3;
      tableGlass.Attributes = ObjectModel.Attribute.Visible | ObjectModel.Attribute.Selectable |
        ObjectModel.Attribute.Transparent;
      tableGlass.SetTransform(ModelTransform.Local, Matrix4x4.CreateTranslation(0.0f, 65.5f, 0.0f));

      ObjectModel flowerPot = objects.Add("FlowerPot");
      flowerPot.Parent = table;
      flowerPot.MeshName = "FlowerPot";
      flowerPot.Material = 1;
      flowerPot.SetTransform(ModelTransform.Local, Matrix4x4.CreateTranslation(0.0f, tableSize.Y, -50.0f));

      ObjectModel laptop = objects.Add("Laptop");
      laptop.Parent = table;
      laptop.MeshName = "Laptop";
      laptop.Material = 1;
      laptop.SetTransform(ModelTransform.Local, Matrix4x4.CreateTranslation(36.0f, tableSize.Y, 120.0f));

      for (int i = 0; i < 10; ++i)
      {
        const float chairFront = 80.0f;
        const float chairSpacing = 80.0f;
        const float sideSpacing = 210.0f;

        ObjectModel chair = objects.Add($"Chair{i + 1}");
        chair.MeshName = "Chair";
        chair.Material = 1;

        if (i < 8)
        {
          int index = i % 4;
          int side = i / 4;

          chair.SetTransform(ModelTransform.Local,
            Matrix4x4.CreateRotationY((float)((3 + side * 2) * Math.PI * 0.5)) *
            Matrix4x4.CreateTranslation(chairFront - chairFront * side * 2.0f, 0.0f,
              (index - 1.5f) * chairSpacing));
        }
        else
        {
          int side = (i - 8) % 2;

          chair.SetTransform(ModelTransform.Local,
            Matrix4x4.CreateRotationY((float)((2 - side) * Math.PI)) *
            Matrix4x4.CreateTranslation(0.0f, 0.0f, (side - 0.5f) * sideSpacing * 2.0f));
        }
      }
      for (int i = 0; i < 10; ++i)
      {
        const float glassFront = 40.0f;
        const float glassSpacing = 80.0f;
        const float sideSpacing = 170.0f;

        ObjectModel wineGlass = objects.Add($"WineGlass{i + 1}");
        wineGlass.MeshName = "WineGlass";
        wineGlass.Material = 2;
        wineGlass.Attributes = ObjectModel.Attribute.Visible | ObjectModel.Attribute.Selectable |
          ObjectModel.Attribute.Transparent;

        if (i < 8)
        {
          int index = i % 4;
          int side = i / 4;

          Vector3 position;
          position.X = glassFront - glassFront * side * 2.0f;
          position.Y = tableSize.Y;
          position.Z = (index - 1.5f) * glassSpacing;

          if (i == 3)
            position.Z += 30.0f;

          wineGlass.SetTransform(ModelTransform.Local, Matrix4x4.CreateTranslation(position));
        }
        else
        {
          int side = (i - 8) % 2;

          wineGlass.SetTransform(ModelTransform.Local, Matrix4x4.CreateTranslation(0.0f, tableSize.Y,
            (side - 0.5f) * sideSpacing * 2.0f));
        }
      }
    }

    private static void CreateLightsAndMaterials(SceneLights lights, ObjectMaterials materials)
    {
      { // Create default light.
        SceneLight light = SceneLight.Default;
        light.AmbientColor = new FloatColorRGB(0x00FFFFFFu);
        light.AlbedoColor = new FloatColorRGB(0x00FFFFFFu);
        light.SpecularColor = new FloatColorRGB(0x00FFFFFFu);
        light.SpecularExponent = 4.0f;
        light.Position = lightPosition;
        light.ShadowCaster = 0;

        lights.Add(light);
      }
      { // (0)
        ObjectMaterial material = ObjectMaterial.Default;
        material.AmbientColor = new FloatColorRGB(1.0f, 1.0f, 1.0f);
        material.AlbedoColor = new FloatColor(0.0f, 0.0f, 0.0f, 0.0f);
        material.SpecularColor = new FloatColorRGB(0.0f, 0.0f, 0.0f);
        material.Shadows = 0.5f;
        material.Occlusion = 0.5f;

        materials.Add(material);
      }
      { // (1)
        ObjectMaterial material = ObjectMaterial.Default;
        material.AmbientColor = new FloatColorRGB(0.25f, 0.25f, 0.25f);
        material.AlbedoColor = new FloatColor(1.0f, 1.0f, 1.0f, 0.5f);
        material.SpecularColor = new FloatColorRGB(1.0f, 1.0f, 1.0f);
        material.SpecularExponent = 4.0f;
        material.Shadows = 0.5f;
        material.Occlusion = 0.5f;

        materials.Add(material);
      }
      { // (2)
        ObjectMaterial material = ObjectMaterial.Default;
        material.AmbientColor = new FloatColorRGB(0.5f, 0.5f, 0.5f);
        material.AlbedoColor = new FloatColor(1.0f, 1.0f, 1.0f, 0.25f);
        material.SpecularColor = new FloatColorRGB(1.0f, 1.0f, 1.0f);
        material.SpecularExponent = 16.0f;
        material.FrostedGlass = 4.0f;

        materials.Add(material);
      }
      { // (3)
        ObjectMaterial material = ObjectMaterial.Default;
        material.AmbientColor = new FloatColorRGB(0.25f, 0.25f, 0.25f);
        material.AlbedoColor = new FloatColor(1.0f, 1.0f, 1.0f, 0.5f);
        material.SpecularColor = new FloatColorRGB(1.0f, 1.0f, 1.0f);
        material.SpecularExponent = 4.0f;
        material.Shadows = 0.75f;
        material.FrostedGlass = 3.0f;

        materials.Add(material);
      }
      { // (4)
        ObjectMaterial material = ObjectMaterial.Default;
        material.AmbientColor = new FloatColorRGB(0.25f, 0.25f, 0.25f);
        material.AlbedoColor = new FloatColor(1.0f, 1.0f, 1.0f, 0.5f);
        material.SpecularColor = new FloatColorRGB(0.5f, 0.5f, 0.5f);
        material.SpecularExponent = 4.0f;
        material.Shadows = 0.5f;
        material.Occlusion = 0.5f;
        material.Technique = TechniqueLighting.Minnaert;

        materials.Add(material);
      }
    }

    private static void CreateAndLoadMeshes(SceneMeshes meshes)
    {
      using (MeshBuffer buffer = new())
      {
        buffer.Plane(1, 1, default, new Vector3(400.0f, 0.0f, 0.0f), new Vector3(0.0f, 0.0f, 800.0f),
          Vector3.UnitY);

        meshes.AddFromBuffer("Plane", buffer, vertexElements);
      }
      string basePath = "../../../../Media/Office/";
      string[] meshNames = ["Chair", "Table", "TableGlass", "FlowerPot", "Laptop", "WineGlass"];

      foreach (string meshName in meshNames)
      {
        uint options = MeshLoadingOption.MaterialVertexColor;
        meshes.AddFromFile(meshName, basePath + meshName + ".obj", vertexElements, ref options, null,
          0, 0, 8.0f);
      }
    }
  }
}