/*
 * 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:
//  * Object hierarchial structure, where smaller objects are parented to bigger objects. Therefore, when
//    a bigger object is moved, it also moves all its children with it.
//  * Bone joint connections for rotating and moving portions of robotic arm, and forklift.
//  * Movement waypoints for moving boxes along two pre-defined paths on the bands.
//  * 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.
//  * Object manipulation, where objects can be moved and/or rotated with mouse and keyboard.
//  * "Auto-draw" functionality for rendering objects using automatic instancing, while applying correctly
//    their materials and colors.
//  * Selection highlight effect, where a colored glow outline can be applied to one or more objects.
//  * Signed Distance Field (SDF) text rendering in 3D: a technique to draw good looking text in 3D with
//    any zoom factor and an optional outline.
//  * Volumetric text in 3D, which is represented as a selectable object with the corresponding mesh.
//  * 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 WarehouseObjects
{
  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 Timer timer;

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

    private TextureCabinet textureCabinet;
    private ShadowCastingAtlas atlas;
    private SelectionHighlight highlight;
    private ObjectModelView shadowCasterView;

    private Scene sceneDepthNormals;
    private Scene sceneModeling;

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

    private Vector2 displaySize;
    private Vector2 mousePos;
    private Ray mouseRay;
    private bool rotatingView;
    private ulong timerTicks;
    private int drawCalls;
    private RandomSequence prngBoxes;
    private byte directionPlaneToggle;
    private float directionPlaneAngle;

    private Vector3 selectionPoint;
    private Vector3 objectDragAxis;
    private Vector3 mousePointAtPlane;
    private int objectRotation;
    private ObjectModel selected;

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

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

    private class ObjectMovementTracking
    {
      public ObjectMovementTracking(float distance, byte group, float angle)
      {
        Distance = distance;
        Group = group;
        Angle = angle;
      }

      public float Distance { get; set; }
      public byte Group { get; set; }
      public float Angle { get; set; }
    }

    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));
        timer = new Timer();

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

        atlas = new ShadowCastingAtlas(device, new Point(256, 256));
        highlight = new SelectionHighlight(device, new Point(displaySize));

        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((float)Math.PI * 0.3f, 1.0f, 200.0f,
          1400.0f);

        atlas.Add(new Point(256, 256));

        camera = new ActorCamera(position: new Vector3(-80.0f, 0.0f, 0.0f),
          rotation: new Vector3(0.906f, -0.0192f, 0.0f), distance: 700.0f);

        camera.Constraints = new CameraConstraints(
          positionMin: new Vector4(-400.0f, -100.0f, -400.0f, 200.0f),
          positionMax: new Vector4(400.0f, 100.0f, 400.0f, 1500.0f),
          rotationMin: new Vector4((float)Math.PI * 0.01f, 0.0f, 0.0f, 0.0f),
          rotationMax: new Vector4((float)Math.PI * 0.5f, 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)
    {
      foreach (ObjectModel obj in objects)
        Utility.FreePayload(obj.Payload);

      camera?.Dispose();
      objects?.Dispose();
      meshes?.Dispose();
      materials?.Dispose();

      sceneModeling?.Dispose();
      sceneDepthNormals?.Dispose();
      textureCabinet?.Dispose();
      lights?.Dispose();
      highlight?.Dispose();
      atlas?.Dispose();

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

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

    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);
        highlight.Size = new Point(displaySize);
      }
    }

    private void MainForm_Paint(object sender, PaintEventArgs e)
    {
      UpdateMovement();

      drawCalls = 0;
      float scale = DeviceDpi / 96.0f;

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

      viewport.View = view;
      viewport.Projection = projection;
      viewport.Update();

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

        // Configure Ambient Occlusion effect.
        AmbientOcclusionParameters parameters = textureCabinet.AmbientOcclusionParameters;
        parameters.KernelBias = 0.075f;
        parameters.Strength = 1.0f;
        textureCabinet.AmbientOcclusionParameters = parameters;
      }

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

      if (objectRotation == 0 && objectDragAxis.Length() <= 1e-6f)
      {
        selected = null;

        if (!rotatingView)
        {
          selected = viewport.Select(mouseRay, out float distance);
          if (selected is not null)
            selectionPoint = mouseRay.Origin + mouseRay.Direction * distance;
        }
      }

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

      // Render shadow map.
      atlas.BorderFill();

      ShadowCaster shadowCaster = atlas.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
        {
          // Use "auto-draw" helper to automatically draw all objects visible within the view frustum of
          // shadow caster.
          drawCalls += shadowCasterView.AutoDraw(sceneDepthNormals);
        }
        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 = atlas;
        sceneModeling.TextureCabinet = textureCabinet;

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

        sceneModeling.Prepare();
        sceneModeling.Begin();
        try
        {
          // Use "auto-draw" helper to automatically draw all objects visible within the camera's view frustum.
          drawCalls += viewport.AutoDraw(sceneModeling, materials);
        }
        finally
        {
          sceneModeling.End();
        }
      }
      finally
      {
        textureCabinet.End();
      }
      textureCabinet.Filter(TextureCabinetFilterType.Occlusion, projection);

      // Render text labels.
      textureCabinet.Begin(TextureCabinetPass.Color);
      try
      {
        canvas.Begin();
        try
        {
          RenderTextLabels(camera.View * projection);
        }
        finally
        {
          canvas.End();
        }
      }
      finally
      {
        textureCabinet.End();
      }
      textureCabinet.Filter(TextureCabinetFilterType.Bloom);

      // Render selection highlight effect.
      if (selected is not null)
      {
        highlight.Clear();
        highlight.Begin();
        try
        {
          sceneModeling.Lights = null;
          sceneModeling.Atlas = null;
          sceneModeling.TextureCabinet = null;

          sceneModeling.Attributes = Scene.Attribute.Normals;

          sceneModeling.Prepare();
          sceneModeling.Begin();
          try
          {
            RenderObjectHighlight(sceneModeling, selected);
          }
          finally
          {
            sceneModeling.End();
          }
        }
        finally
        {
          highlight.End();
        }
        highlight.Filter();
      }

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

          if (selected is not null)
          { // Render selection highlight on top of the scene.
            canvas.Quad(highlight.Texture(SelectionHighlightTextureType.Highlight), new Quad(0.0f, 0.0f,
              displaySize.X, displaySize.Y), new ColorRect(0xFFFFFFFFu));

            canvas.Quad(highlight.Texture(SelectionHighlightTextureType.HighlightMask),
              new Quad(0.0f, 0.0f, displaySize.X, displaySize.Y), new ColorRect(0x80FFFFFFu));
          }
          // Render information text.
          canvas.ContextState = CanvasContextState.FlatText;
          { // Draw text hints.
            FontParameters parameters = new("Segoe UI", 14.0f * scale, FontWeight.SemiBold);
            parameters.Effect.FillBrightness = 0.0f;
            parameters.Effect.BorderBrightness = 1.0f;
            parameters.Effect.BorderOpacity = 0.75f;
            parameters.Effect.BorderType = FontBorder.SemiHeavy;
            parameters.Effect.ShadowBrightness = 0.85f;
            parameters.Effect.ShadowOpacity = 0.4f;
            textRenderer.Parameters = parameters;

            textRenderer.DrawAligned(new Vector2(displaySize.X - 8.0f * scale, 4.0f * scale),
              "Shift/Alt + Left/Right mouse buttons: move/rotate objects.",
              new ColorPair(0xFFA5E7B8u, 0xFF77FFCEu), TextAlignment.End, TextAlignment.Start);

            textRenderer.DrawAligned(new Vector2(displaySize.X - 8.0f * scale, displaySize.Y - 4.0f * scale),
              "Left mouse button: rotate scene, right button: move scene, wheel: zoom.",
              new ColorPair(0xFF00E0FFu, 0xFF0040FFu), TextAlignment.End, 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;

            if (device.Technology == DeviceTechnology.Direct3D)
              textRenderer.DrawAligned(new Vector2(8.0f * scale, 4.0f * scale),
                $"Direct3D ({device.TechVersion >> 8}.{(device.TechVersion >> 4) & 0xF}), " +
                $"features: {device.TechFeatureVersion >> 8}.{(device.TechFeatureVersion >> 4) & 0xF}",
                new ColorPair(0xA0FFFFFFu), TextAlignment.Start, TextAlignment.Start);
            else if (device.Technology == DeviceTechnology.OpenGL)
              textRenderer.DrawAligned(new Vector2(8.0f * scale, 4.0f * scale),
                $"OpenGL ({device.TechVersion >> 8}.{(device.TechVersion >> 4) & 0xF}+), " +
                $"features: {device.TechFeatureVersion >> 8}.{(device.TechFeatureVersion >> 4) & 0xF}",
                new ColorPair(0xA0FFFFFFu), TextAlignment.Start, TextAlignment.Start);

            textRenderer.DrawAligned(new Vector2(8.0f * scale, displaySize.Y - 4.0f * scale),
              $"Frame rate: {(int)timer.FrameRate}, draw calls: {drawCalls}",
              new ColorPair(0xA0FFE0E0u, 0xA0FF80A0u), TextAlignment.Start, TextAlignment.End);
          }
        }
        finally
        {
          canvas.End();
        }
      }
      finally
      {
        swapChain.End();
      }
    }

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

      if (ModifierKeys.HasFlag(Keys.Shift) && selected is not null)
      {
        if (e.Button == MouseButtons.Left)
          objectDragAxis = Vector3.UnitY; // Move horizontally.
        else if (Math.Abs(Vector3.Dot(mouseRay.Direction, Vector3.UnitX)) >
          Math.Abs(Vector3.Dot(mouseRay.Direction, Vector3.UnitZ)))
          objectDragAxis = Vector3.UnitX;
        else
          objectDragAxis = Vector3.UnitZ;

        if (mouseRay.IntersectPlane(selectionPoint, objectDragAxis, out mousePointAtPlane, out float _))
          selectionPoint = Vector3.Zero;
        else
          objectDragAxis = Vector3.Zero;
      }
      else if (ModifierKeys.HasFlag(Keys.Alt) && selected is not null)
      {
        selectionPoint = Vector3.Zero;

        if (e.Button == MouseButtons.Left)
          objectRotation = 2;
        else if (Math.Abs(Vector3.Dot(mouseRay.Direction, Vector3.UnitX)) >
          Math.Abs(Vector3.Dot(mouseRay.Direction, Vector3.UnitZ)))
          objectRotation = 1;
        else
          objectRotation = 3;
      }
      else
      {
        if (e.Button == MouseButtons.Left)
          camera.Command(CameraCommand.Rotation, position, displaySize);
        else if (e.Button == MouseButtons.Right)
          camera.Command(CameraCommand.Dragging, position, displaySize);

        rotatingView = e.Button == MouseButtons.Left || e.Button == MouseButtons.Right;
      }
    }

    private void MainForm_MouseMove(object sender, MouseEventArgs e)
    {
      Vector2 prevMousePos = mousePos;
      mousePos = new Vector2(e.X, e.Y);
      Vector3 position = new(mousePos, 0.0f);

      if (objectDragAxis.Length() > 1e-6f)
      {
        if (mouseRay.IntersectPlane(mousePointAtPlane, objectDragAxis, out Vector3 movePoint, out float _))
        {
          Vector3 offset = movePoint - mousePointAtPlane;

          // Adjust offset to ensure movement respective viewer's direction.
          ObjectModel parent = selected.Parent;
          if (parent is not null)
          {
            if (Matrix4x4.Invert(parent.GetTransform(ModelTransform.Local), out Matrix4x4 transform))
              offset = Vector3.TransformNormal(offset, transform);
          }
          selected.SetTransform(ModelTransform.Local,
            selected.GetTransform(ModelTransform.Local) * Matrix4x4.CreateTranslation(offset));

          mousePointAtPlane = movePoint;
        }
      }
      else if (objectRotation != 0)
      {
        float angle = (prevMousePos.X - mousePos.X) * 2.5f / displaySize.X;
        Matrix4x4 rotation = objectRotation switch
        {
          1 => Matrix4x4.CreateRotationX(angle),
          3 => Matrix4x4.CreateRotationZ(angle),
          _ => Matrix4x4.CreateRotationY(angle),
        };
        selected.SetTransform(ModelTransform.Local, rotation *
          selected.GetTransform(ModelTransform.Local));
      }
      else
        camera.Command(CameraCommand.Update, position, displaySize);
    }

    private void MainForm_MouseUp(object sender, MouseEventArgs e)
    {
      mousePos = new Vector2(e.X, e.Y);
      Vector3 position = new(mousePos, 0.0f);

      camera.Command(CameraCommand.Stop, position, displaySize);
      objectDragAxis = Vector3.Zero;
      objectRotation = 0;
      rotatingView = false;
    }

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

    private static void RenderObjectHighlight(Scene scene, ObjectModel selected)
    {
      SceneMesh mesh = selected.Mesh;
      if (mesh is null)
        return;

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

      uint color = 0xFFFFFFFFu;
      string meshName = selected.MeshName;

      if (meshName == "Box")
        color = 0xFF00FF00u;
      else if (meshName.StartsWith("RoboticArm"))
        color = 0xFF0080FFu;
      else if (meshName.StartsWith("Forklift"))
        color = 0xFF10E0A0u;
      else if (meshName == "Text3D")
        color = 0xFFFF0000u;
      else if (meshName == "Pallet")
        color = 0xFF4080A0u;
      else if (meshName == "MultipleBands" || meshName == "DirectionPlane")
        color = 0xFF0000FFu;
      else if (meshName == "Fence" || meshName == "FenceThick")
        color = 0xFFFF4040u;

      mesh.AutoDrawSliced(scene, null, new FloatColor(color), PrimitiveTopology.Triangles, 0,
        AutoDrawOption.ObjectHighlighted);
    }

    private void RenderTextLabels(Matrix4x4 viewProjection)
    {
      { // Configure rendering state to avoid depth fighting and disable depth writes.
        RenderingState renderingState = device.RenderingState;
        renderingState.States &= ~RenderingState.State.DepthWrite;
        renderingState.DepthBias = 5.0f;
        renderingState.SlopeDepthBias = 5.0f;
        device.RenderingState = renderingState;
      }

      // Configure canvas for 3D projected SDF rendering.
      canvas.SignedDistanceField = new SignedDistanceField(SuperSampleSDF.SuperSample16x,
        1.0f, new Vector3(0.0f, 1.0f, 0.0f), 0.15f, 0.45f);

      canvas.Attributes = Canvas.Attribute.Projected | Canvas.Attribute.SDF | Canvas.Attribute.Cubic;
      canvas.ContextState = CanvasContextState.Projected;
      canvas.SamplerState = new CanvasSamplerState(TextureFilter.Linear, TextureFilter.Linear);
      canvas.ViewProjection = viewProjection;

      { // Prepare SDF font.
        FontParameters parameters = new("Cambria", 32.0f, FontWeight.SemiBold);
        parameters.Effect.SignedFieldDistance = 1.0f;
        textRenderer.Parameters = parameters;
      }

      // Prepare SDF font modifiers.
      TextRenderModifiers modifiers = default;
      modifiers.Scale = 0.4f;

      { // Label describing robotic arm
        ObjectModel roboticArm = objects.Object("RoboticArm:Piece0");
        Matrix4x4 transform = roboticArm.GetTransform(ModelTransform.Global);

        canvas.World = Matrix4x4.CreateRotationX((float)Math.PI * 0.5f) *
          Matrix4x4.CreateRotationY(-(float)Math.PI * 0.5f) *
          Matrix4x4.CreateTranslation(40.0f, 0.0f, 0.0f) * transform;

        textRenderer.DrawAligned(default, "Robotic Arm", ColorPair.Black, TextAlignment.Middle,
          TextAlignment.Middle, 1.0f, false, modifiers);
      }
      { // Labels describing the bands
        ObjectModel bands = objects.Object("MultipleBands");
        Matrix4x4 transform = bands.GetTransform(ModelTransform.Global);

        canvas.World = Matrix4x4.CreateRotationX((float)Math.PI * 0.5f) *
          Matrix4x4.CreateTranslation(70.0f, 0.0f, 20.0f) * transform;

        textRenderer.DrawAligned(default, "Band for outcoming packages", ColorPair.Black,
          TextAlignment.Middle, TextAlignment.Middle, 1.0f, false, modifiers);

        // Band 2 label
        canvas.World = Matrix4x4.CreateRotationX((float)Math.PI * 0.5f) *
          Matrix4x4.CreateRotationY(-(float)Math.PI * 0.5f) *
          Matrix4x4.CreateTranslation(-200.0f, 0.0f, -140.0f) * transform;

        textRenderer.DrawAligned(default, "Bands for incoming packages", ColorPair.Black,
          TextAlignment.Middle, TextAlignment.Middle, 1.0f, false, modifiers);
      }
    }

    public void ObjectMove(string objectName, Matrix4x4 transform)
    {
      objects.Object(objectName).SetTransform(ModelTransform.Local, transform);
    }

    private void UpdateMovement()
    {
      timer.Update();

      // Retrieve current latency and update timer ticks.
      double latency = timer.Latency / 1000000.0;
      timerTicks += (uint)timer.ExtractTokens;

      // Move boxes over the bands
      foreach (ObjectModel obj in objects)
      {
        ObjectMovementTracking tracking = Utility.RetrievePayload<ObjectMovementTracking>(obj.Payload);
        if (tracking is null)
          continue;

        ObjectModel parent = obj.Parent;
        if (parent is null)
          continue;

        const float speed = 70.0f;
        float distance = parent.GetWaypointDistance(tracking.Group);

        tracking.Distance += (float)(latency * speed);
        if (tracking.Distance >= distance)
        {
          tracking.Distance = 0.0f;
          tracking.Group ^= 1;
          tracking.Angle = RandomizeBoxAngle();

          directionPlaneToggle = tracking.Group;
          // Note: distance is different between tracking groups, but object is at zero size anyway.
        }

        float scale = 1.0f;
        if (tracking.Distance < 10.0f)
          scale = tracking.Distance / 10.0f;
        else if (tracking.Distance > distance - 10.0f)
          scale = (distance - tracking.Distance) / 10.0f;

        obj.SetTransform(ModelTransform.Local, Matrix4x4.CreateScale(scale) *
          Matrix4x4.CreateRotationY(tracking.Angle) * parent.GetLatchWaypointCoupleMatrix(tracking.Group,
          tracking.Distance));
      }
      // Animate the angle of direction plane.
      if (directionPlaneToggle == 1)
        directionPlaneAngle = (float)Math.Min(directionPlaneAngle + latency * 0.5, 1.0);
      else
        directionPlaneAngle = (float)Math.Max(directionPlaneAngle - latency * 0.5, 0.0);

      { // Move direction plane and all pieces of robotic arm.
        double timing = timerTicks / 60.0;
        float gripperTheta = (float)Math.Sin(timing * 1.2816) * 0.7f;

        ObjectMove("RoboticArm:Piece1", Matrix4x4.CreateRotationY(
          (float)(Math.Sin(timing * 0.5823) * Math.PI * 0.25)));

        ObjectMove("RoboticArm:Piece2", Matrix4x4.CreateRotationZ(
          (float)((Math.Sin(timing * 0.7921) - 0.5) * Math.PI * 0.1)));

        ObjectMove("RoboticArm:Piece3", Matrix4x4.CreateRotationZ(
          (float)((Math.Sin(timing * 1.184) + 0.25) * Math.PI * 0.1)));

        ObjectMove("RoboticArm:Piece4", Matrix4x4.CreateRotationZ(
          (float)((Math.Cos(timing * 1.037) + 0.75) * Math.PI * 0.15)));

        ObjectMove("RoboticArm:Piece5", Matrix4x4.CreateRotationZ(
          (float)(Math.Sin(timing * 0.8) * Math.PI * 0.15)));

        ObjectMove("RoboticArm:Piece6", Matrix4x4.CreateRotationY(
          (float)(Math.Cos(timing * 0.681) * Math.PI * 0.5)));

        ObjectMove("RoboticArm:Gripper0", Matrix4x4.CreateTranslation(
          new Vector3(0.0f, 0.0f, -9.8f + gripperTheta)));

        ObjectMove("RoboticArm:Gripper1", Matrix4x4.CreateTranslation(
          new Vector3(0.0f, 0.0f, 9.8f - gripperTheta)));

        ObjectMove("DirectionPlane", Matrix4x4.CreateRotationY(
          (Utility.SineTransform(directionPlaneAngle) - 0.5f) * (float)Math.PI * 0.2f));
      }
      { // Move lifter and grippers of the forklift.
        double timing = timerTicks / 60.0;
        float gripperTheta = (float)Math.Cos(timing * 0.773) * 9.0f;

        ObjectMove("Forklift:Forklift", Matrix4x4.CreateRotationY(
          (float)Math.PI * 0.5f) * Matrix4x4.CreateTranslation(new Vector3(-320.0f, 0.0f,
          -160.0f + (float)Math.Sin(timing * 1.6102) * 24.0f)));

        ObjectMove("Forklift:Lifter", Matrix4x4.CreateTranslation(new Vector3(0.0f,
          (float)Math.Sin(timing * 0.6102) * 12.0f + 12.0f, 0.0f)));

        ObjectMove("Forklift:GripperL", Matrix4x4.CreateTranslation(new Vector3(0.0f, 0.0f,
          -12.0f + gripperTheta)));

        ObjectMove("Forklift:GripperR", Matrix4x4.CreateTranslation(new Vector3(0.0f, 0.0f,
          12.0f - gripperTheta)));
      }
    }

    private ObjectModel CreateObject(string objectName, string meshName, ObjectModel parent,
      Vector3 position, int material, float angle = 0.0f, uint attributes = ObjectModel.Attribute.Visible |
      ObjectModel.Attribute.Selectable)
    {
      ObjectModel obj = objects.Add(objectName, IntPtr.Zero, attributes);

      if (parent is not null)
        obj.Parent = parent;

      if (!string.IsNullOrEmpty(meshName))
        obj.MeshName = meshName;

      obj.Material = material;

      Matrix4x4 transform = Matrix4x4.CreateTranslation(position);

      if (angle != 0.0f)
        transform = Matrix4x4.CreateRotationY(angle) * transform;

      obj.SetTransform(ModelTransform.Local, transform);

      return obj;
    }

    private float RandomizeBoxAngle()
    {
      return (float)(prngBoxes.Ranged(4) * Math.PI * 0.5 + (prngBoxes.Value - 0.5) * Math.PI * 0.12);
    }

    private void CreateObjects()
    {
      { // Floor
        ObjectModel floor = objects.Add("Floor", IntPtr.Zero, ObjectModel.Attribute.Visible |
          ObjectModel.Attribute.Hierarchyless);
        floor.MeshName = "Plane";
        floor.Material = 0;
        floor.Color = new FloatColor(0.875f, 0.875f, 0.875f, 1.0f);
        floor.SetTransform(ModelTransform.Local, Matrix4x4.CreateTranslation(
          new Vector3(0.0f, -0.01f, 0.0f)));
      }
      const uint attributes = ObjectModel.Attribute.Visible | ObjectModel.Attribute.Selectable;
      const uint attributesMoving = attributes | ObjectModel.Attribute.Hierarchyless;

      { // Robotic arm pieces
        const int materialPieces = 3;

        ObjectModel piece0 = CreateObject("RoboticArm:Piece0", "RoboticArm_Piece0", null,
          new Vector3(-60.0f, 0.0f, -160.0f), materialPieces, (float)Math.PI * 0.25f, attributesMoving);

        ObjectModel piece1 = CreateObject("RoboticArm:Piece1", "RoboticArm_Piece1", piece0, new Vector3(),
          materialPieces, 0.0f, attributesMoving);

        ObjectModel piece2 = CreateObject("RoboticArm:Piece2", "RoboticArm_Piece2", piece1, new Vector3(),
          materialPieces, 0.0f, attributesMoving);

        ObjectModel piece3 = CreateObject("RoboticArm:Piece3", "RoboticArm_Piece3", piece2, new Vector3(),
          materialPieces, 0.0f, attributesMoving);

        ObjectModel piece4 = CreateObject("RoboticArm:Piece4", "RoboticArm_Piece4", piece3, new Vector3(),
          materialPieces, 0.0f, attributesMoving);

        ObjectModel piece5 = CreateObject("RoboticArm:Piece5", "RoboticArm_Piece5", piece4, new Vector3(),
          materialPieces, 0.0f, attributesMoving);

        ObjectModel piece6 = CreateObject("RoboticArm:Piece6", "RoboticArm_Piece6", piece5, new Vector3(),
          materialPieces, 0.0f, attributesMoving);

        ObjectModel gripper0 = CreateObject("RoboticArm:Gripper0", "RoboticArm_Gripper0", piece6,
          new Vector3(), materialPieces, 0.0f, attributesMoving);

        ObjectModel gripper1 = CreateObject("RoboticArm:Gripper1", "RoboticArm_Gripper1", piece6,
          new Vector3(), materialPieces, 0.0f, attributesMoving);

        // Connect each pieces of robotic arm together by using "latches". An object attached to its parent
        // by latch mechanism places and orients the object so that its and parent latches coincide. This
        // enables precise placement and orientation of pieces together. The positions and orientations are
        // calculated according to parent dependency chain. Therefore, moving the base piece (which has no
        // parent) will also move accordingly all its codependent child pieces.
        piece1.ConnectLatches("Out", "In");
        piece2.ConnectLatches("Out", "In");
        piece3.ConnectLatches("Out", "In");
        piece4.ConnectLatches("Out", "In");
        piece5.ConnectLatches("Out", "In");
        piece6.ConnectLatches("Out", "In");
        gripper0.ConnectLatches("Out", "In");
        gripper1.ConnectLatches("Out", "In");
      }
      { // Multiple bands with direction plane
        ObjectModel multipleBands = CreateObject("MultipleBands", "MultipleBands", null,
          new Vector3(0.0f, 0.0f, 40.0f), 1);

        ObjectModel directionPlane = CreateObject("DirectionPlane", "DirectionPlane", multipleBands,
          new Vector3(0.0f, -2.0f, 0.0f), 1, (float)Math.PI * 0.17f, attributesMoving);

        directionPlane.ConnectLatches("DirectionPlane", "Base");

        // Create boxes that will be moved over the bands
        float distance = multipleBands.GetWaypointDistance(0);
        prngBoxes = new RandomSequence();

        for (int i = 0; i < 20; ++i)
        {
          ObjectModel box = objects.Add($"MovingBox_{i}", Utility.AssignPayload(
            new ObjectMovementTracking(distance * i / 32.0f, 0, RandomizeBoxAngle())), attributesMoving);
          box.Parent = multipleBands;
          box.MeshName = "Box";
          box.Material = 2;
        }
      }
      { // Forklift with separate lifter and grippers
        ObjectModel forklift = CreateObject("Forklift:Forklift", "Forklift_Forklift", null,
          new Vector3(-320.0f, 0.0f, -160.0f), 1, (float)Math.PI * 0.5f, attributesMoving);

        ObjectModel lifter = CreateObject("Forklift:Lifter", "Forklift_Lifter", forklift,
          new Vector3(0.0f, 0.0f, 0.0f), 3, 0.0f, attributesMoving);

        lifter.ConnectLatches("Lifter", "Base");

        ObjectModel gripper0 = CreateObject("Forklift:GripperL", "Forklift_GripperL", lifter,
          new Vector3(0.0f, 0.0f, -10.0f), 3, 0.0f, attributesMoving);

        gripper0.ConnectLatches("Gripper", "Lifter");

        ObjectModel gripper1 = CreateObject("Forklift:GripperR", "Forklift_GripperR", lifter,
          new Vector3(0.0f, 0.0f, 10.0f), 3, 0.0f, attributesMoving);

        gripper1.ConnectLatches("Gripper", "Lifter");
      }
      // Fences
      CreateObject(null, "Fence", null, new Vector3(20.0f, 0.0f, 220.0f), 3);
      CreateObject(null, "FenceThick", null, new Vector3(-280.0f, 0.0f, -60.0f), 3, (float)Math.PI * 0.5f);

      { // Stack of pallets behind the fence
        float height = 0.0f;

        for (int i = 0; i < 7; ++i)
        {
          ObjectModel pallet = CreateObject(null, "Pallet", null, new Vector3(-340.0f, height, -28.0f), 2);
          height += pallet.Size.Y;
        }
      }
      // Stack of boxes at the backyard.
      CreateObject(null, "Box", null, new Vector3(-320.0f, 0.0f, 40.0f), 2);
      CreateObject(null, "Box", null, new Vector3(-315.0f, 0.0f, 72.0f), 2, (float)Math.PI * 0.1f);
      CreateObject(null, "Box", null, new Vector3(-345.0f, 0.0f, 65.0f), 2, -(float)Math.PI * 0.1f);

      // Text describing the boxes
      CreateObject("Text3D", "Text3D", null, new Vector3(135.0f, 0.0f, -135.0f), 3,
        (float)Math.PI * -0.24f);

      { // Stack of boxes near the arm.
        RandomSequence prng = new (4007);
        bool[,,] boxes = new bool[3, 3, 7];

        ObjectModel dummy = CreateObject(null, null, null, new Vector3(105.0f, 0.0f, -105.0f), 2,
          -(float)Math.PI * 0.25f);

        for (int k = 0; k < 3; ++k)
          for (int j = 0; j < 3; ++j)
            for (int i = 0; i < 7; ++i)
              if (prng.Ranged(4) != 1 && (k == 0 || boxes[k - 1, j, i]))
              {
                ObjectModel box = objects.Add();
                box.Parent = dummy;
                box.MeshName = "Box";
                box.Material = 2;

                Vector3 size = box.Size;

                box.SetTransform(ModelTransform.Local, Matrix4x4.CreateRotationY(prng.Ranged(3) *
                  (float)Math.PI * 0.5f) * Matrix4x4.CreateTranslation((i - 3) * (size.X + 1.0f),
                  (size.Y + 0.5f) * k, (j - 1) * (size.Z + 1.0f)));

                boxes[k, j, i] = true;
              }
      }
      { // Stand with pallets and boxes
        RandomSequence prng = new(5004);
        ObjectModel stand = CreateObject(null, "Stand", null, new Vector3(60.0f, 0.0f, 200.0f), 1);

        for (int j = 0; j < 3; ++j)
          for (int i = 0; i < 5; ++i)
          {
            ObjectModel pallet = objects.Add();
            pallet.Parent = stand;
            pallet.MeshName = "Pallet";
            pallet.Material = 2;

            float height = j * 46.0f;
            if (j == 2)
              height = 86.5f;

            Vector3 palletSize = pallet.Size;

            pallet.SetTransform(ModelTransform.Local, Matrix4x4.CreateTranslation(
              (i - 2) * (palletSize.X + 7.0f), height, 0.0f));

            for (int kz = 0; kz < 3; ++kz)
              for (int kx = 0; kx < 3; ++kx)
                if (prng.Ranged(2) == 1)
                {
                  ObjectModel box = objects.Add();
                  box.Parent = pallet;
                  box.MeshName = "Box";
                  box.Material = 2;

                  Vector3 boxSize = box.Size;

                  box.SetTransform(ModelTransform.Local, Matrix4x4.CreateRotationY(prng.Ranged(3) *
                    (float)Math.PI * 0.5f) * Matrix4x4.CreateTranslation((kx - 1) * (boxSize.X + 5.0f),
                    palletSize.Y, (kz - 1) * (boxSize.Z + 2.0f)));
                }
          }
      }
    }

    private static void CreateMeshText3D(SceneMeshes meshes, MeshBuffer buffer)
    {
      buffer.Clear();
      using TextModeller modeller = new(meshes.Device);

      modeller.SetFontParameters(new FontParameters("Cambria", 16.0f, FontWeight.Bold));
      modeller.DrawCurved(default, "Boxes waiting to be dispatched", -150.0f, float.NaN, 3.0f,
        ColorPair.White, TextAlignment.Start);
      modeller.CopyToMeshBuffer(buffer);

      // Create a rough voxel representation of 3D text mesh to improve mouse selection accuracy.
      MeshVoxel voxel = buffer.Voxelize(4);

      meshes.AddFromBuffer("Text3D", buffer, vertexElements);
    }

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

        meshes.AddFromBuffer("Plane", buffer, vertexElements);

        CreateMeshText3D(meshes, buffer);
      }
      string basePath = "../../../../Media/Warehouse/";
      string[] meshNames = {"Box", "DirectionPlane", "Fence", "FenceThick", "Forklift", "MultipleBands",
        "Pallet", "RoboticArm", "Stand"};

      bool[] meshVertexColored = {true, true, true, true, false, false, true, false, true};

      for (int i = 0; i < meshNames.Length; ++i)
      {
        uint options = meshVertexColored[i] ? MeshLoadingOption.MaterialVertexColor : 0u;

        meshes.AddFromFile(meshNames[i], basePath + meshNames[i] + ".obj", vertexElements, ref options,
          null, 0, 0, 1.5f);
      }

      // For meshes such as "RoboticArm" and "ForkLift", a split is performed by creating mesh instances
      // that refer to a specific portion of a parent mesh. This is done according to meta-tags that are
      // present in the mesh. An object that refers to one of the sub-meshes will still use original mesh
      // for rendering, while using each sub-mesh latch and voxel mechanisms.
      string[] meshesToSplit = {"RoboticArm", "Forklift"};

      foreach (string meshName in meshesToSplit)
        meshes.Slice(meshes.Mesh(meshName));

      for (int i = 0; i < meshes.Count; ++i)
      {
        SceneMesh mesh = meshes.Mesh(i);
        mesh.GetSlice(out SceneMesh parent, out MeshMetaTag _);

        if (parent is null)
          continue; // Mesh is not a slice.

        // Retrieve the name of the slice and its parent.
        string parentMeshName = parent.Name, meshName = mesh.Name;

        // Load voxel for a particular segment, if voxel file is present.
        mesh.Voxel.TryLoadFromFile(basePath + '/' + parentMeshName + '/' + meshName + ".voxel",
          out int[] _, out byte _, out bool _);

        // Load latches for a particular segment, if latches file is present.
        mesh.Latches.TryLoadFromFile(basePath + '/' + parentMeshName + '/' + meshName + ".latch");
      }
    }

    private static void CreateLightsAndMaterials(SceneLights lights, ObjectMaterials materials)
    {
      { // 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) Floor
        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) Generic
        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) Carton
        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 = 16.0f;
        material.Shadows = 0.5f;
        material.Occlusion = 0.5f;
        material.Roughness = new Vector3(0.3f, 1.2f, 0.4f);
        material.Technique = TechniqueLighting.Minnaert;

        materials.Add(material);
      }
      { // (3) Metal
        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;
        material.Roughness = new Vector3(1.8f, 1.2f, 0.7f);
        material.Technique = TechniqueLighting.CookTorrance;

        materials.Add(material);
      }
    }
  }
}