/*
 * 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 System.IO;

  using Afterwarp;
  using Viewports;

  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 WidgetManager manager;

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

    private ShadowCastingAtlas shadowCastingAtlas;
    private ObjectModelView shadowCasterView;
    private bool shadowMapNeedsUpdate = true;

    private Scene sceneDepthNormals;
    private Scene sceneModeling;
    private static bool constructingUI;
    private static bool recreateTorusKnot;
    private static bool recreateCube;

    private Vector2 displaySize;
    private double displayScale;
    private Vector2 mousePos;

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

    public MainForm()
    {
      InitializeComponent();
      Application.Idle += Application_Idle;
    }

    private void MainForm_Load(object sender, EventArgs e)
    {
      displaySize = new Vector2(ClientSize.Width, ClientSize.Height);
      displayScale = DeviceDpi / 96.0;
      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));

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

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

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

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

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

        shadowCastingAtlas.Add(new Point(256, 256));
        timer = new Timer();

        manager = new(null, textRenderer, "Manager");
        manager.Scale = displayScale;
        manager.Rect = new RectF(0.0f, 0.0f, (float)(displaySize.X / displayScale),
          (float)(displaySize.Y / displayScale));
        manager.ExternalEvent = ManagerExternalEvent;

        CreateLightsAndMaterials(lights, materials);

        try
        {
          constructingUI = true;
          CreateUI();
        }
        finally
        {
          constructingUI = false;
        }

        CreateMeshes();
        RecreateTorusKnot();
        RecreateCube();
        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)
    {
      if (manager is not null)
        ReleaseViewports(manager);

      manager?.Dispose();
      timer?.Dispose();

      objects?.Dispose();
      meshes?.Dispose();
      materials?.Dispose();
      sceneModeling?.Dispose();
      sceneDepthNormals?.Dispose();
      lights?.Dispose();
      shadowCastingAtlas?.Dispose();

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

    private void Application_Idle(object sender, EventArgs e)
    {
      timer.Update();

      if (manager.Update(timer.Latency / (1000.0 * 1000.0)))
        Invalidate();
      else
        Thread.Sleep(5);
    }

    private void MainForm_Resize(object sender, EventArgs e)
    {
      if (WindowState != FormWindowState.Minimized && device is not null)
      {
        displaySize = new Vector2(ClientSize.Width, ClientSize.Height);
        displayScale = DeviceDpi / 96.0;

        swapChain.Resize(new Point(displaySize));

        manager.Scale = displayScale;
        manager.Rect = new RectF(0.0f, 0.0f, (float)(displaySize.X / displayScale),
          (float)(displaySize.Y / displayScale));

        Invalidate();
      }
    }

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

      if (recreateTorusKnot)
      {
        RecreateTorusKnot();
        recreateTorusKnot = false;
      }
      if (recreateCube)
      {
        RecreateCube();
        recreateCube = false;
      }
      ManagerUpdateSettle();

      // Update shadow map. This is only needed when some object has moved.
      if (shadowMapNeedsUpdate)
      {
        shadowMapNeedsUpdate = false;
        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
          {
            shadowCasterView.AutoDraw(sceneDepthNormals);
          }
          finally
          {
            sceneDepthNormals.End();
          }
        }
        finally
        {
          shadowCaster.End();
        }
        shadowCaster.Filter();
      }

      // Update viewport contents.
      UpdateViewports(manager);

      // Render UI.
      manager.GetTexture(WidgetManagerTextureType.Screen).Clear(new FloatColor(0.0f, 1.0f, 0.0f, 1.0f));
      manager.Present();

      // Render on the form.
      swapChain.Begin();
      try
      {
        device.Clear(DeviceClear.Color, new FloatColor(0xFF404040u));

        canvas.Begin();
        try
        {
          // Draw UI composition texture.
          canvas.SamplerState = new CanvasSamplerState(TextureFilter.Nearest, TextureFilter.Nearest);
          canvas.ContextState = CanvasContextState.FlatScene;
          canvas.Attributes = 0;

          canvas.Quad(manager.GetTexture(WidgetManagerTextureType.Screen), new Quad(0.0f, 0.0f,
            displaySize.X, displaySize.Y));

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

          FontParameters parameters = new("Segoe UI", 14.0f * scale, FontWeight.Medium);
          parameters.Effect.FillBrightness = 1.0f;
          parameters.Effect.BorderBrightness = 0.25f;
          parameters.Effect.BorderOpacity = 0.75f;
          parameters.Effect.BorderType = FontBorder.SemiHeavy;
          parameters.Effect.ShadowBrightness = 0.15f;
          parameters.Effect.ShadowOpacity = 0.75f;
          textRenderer.Parameters = parameters;

          textRenderer.DrawAligned(new Vector2(8.0f * scale, 4.0f * scale),
            (device.Technology == DeviceTechnology.Direct3D ?
            $"Direct3D ({device.TechVersion >> 8}.{(device.TechVersion >> 4) & 0xF}), " :
            $"OpenGL ({device.TechVersion >> 8}.{(device.TechVersion >> 4) & 0xF}+), ") +
            $"features: {device.TechFeatureVersion >> 8}.{(device.TechFeatureVersion >> 4) & 0xF}",
            new ColorPair(0xFFFFFFFFu), TextAlignment.Start, TextAlignment.Start);
        }
        finally
        {
          canvas.End();
        }
      }
      finally
      {
        swapChain.End();
      }
      // Update cursor from UI.
      Cursor = Utility.FromUI(manager.Cursor);
    }

    private void MainForm_MouseDown(object sender, MouseEventArgs e)
    {
      mousePos = new Vector2(e.X, e.Y);
      manager.AcceptMouse(MouseEvent.Down, Utility.ToUI(e.Button), mousePos / (float)displayScale);
    }

    private void MainForm_MouseMove(object sender, MouseEventArgs e)
    {
      mousePos = new Vector2(e.X, e.Y);
      manager.AcceptMouse(MouseEvent.Move, MouseButton.None, mousePos / (float)displayScale);
      Cursor = Utility.FromUI(manager.Cursor);
    }

    private void MainForm_MouseUp(object sender, MouseEventArgs e)
    {
      mousePos = new Vector2(e.X, e.Y);
      manager.AcceptMouse(MouseEvent.Up, Utility.ToUI(e.Button), mousePos / (float)displayScale);
    }

    private void MainForm_MouseWheel(object sender, MouseEventArgs e)
    {
      if (e.Delta > 0)
        manager.AcceptMouse(MouseEvent.WheelUp, MouseButton.None, mousePos / (float)displayScale);
      else
        manager.AcceptMouse(MouseEvent.WheelDown, MouseButton.None, mousePos / (float)displayScale);
    }

    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();
      else
        manager.AcceptKey(KeyEvent.Pressed, Utility.ToUI(e.KeyCode), 0);
    }
    private void MainForm_KeyUp(object sender, KeyEventArgs e)
    {
      manager.AcceptKey(KeyEvent.Released, Utility.ToUI(e.KeyCode), 0);
    }

    private void MainForm_KeyPress(object sender, KeyPressEventArgs e)
    {
      manager.AcceptKey(KeyEvent.Typing, Key.Null, e.KeyChar);
    }

    private void EnumeratePropertiesAndSaveToDisk()
    {
      SaveFileDialog dialog = new ();
      dialog.Filter = "Text files (*.txt)|*.txt|All Files (*.*)|*.*";
      dialog.DefaultExt = ".txt";

      if (dialog.ShowDialog() != DialogResult.OK)
        return;

      string[] classNames = {"Widget", "Label", "Button", "Edit", "RadioButton", "CheckBox", "GaugeEdit",
        "SpinEdit", "Placer", "Panel", "TabSheets", "Window", "SketchPad"};

      string text = string.Empty; // System.Environment.NewLine

      foreach (string className in classNames)
      {
        using Widget widget = new(manager, className, "TestWidget");
        text += widget.ClassName + Environment.NewLine;

        // Enumerate all widget's properties and events.
        List<string> propertyNames = new();

        foreach (Widget.PropertyValue property in widget)
        {
          string propertyName = property.Name;

          if (property.Type != WidgetPropertyType.None)
            propertyName += ": " + property.Type.ToString();
          else if (property.Behavior == WidgetPropertyBehavior.Event)
            propertyName += " [event]";

          propertyNames.Add(propertyName);
        }
        // Make sure the properties and events are sorted by their name.
        propertyNames.Sort();

        foreach (string propertyName in propertyNames)
          text += "  " + propertyName + Environment.NewLine;

        text += Environment.NewLine;
      }
      File.WriteAllText(dialog.FileName, text);
    }

    private static void UpdateViewports(Widget parent)
    {
      for (int i = 0; i < parent.ChildCount; ++i)
      {
        Widget child = parent.GetChild(i);
        Utility.RetrievePayload<SketchpadViewport>(child.Payload)?.Update();

        UpdateViewports(child);
      }
    }

    private static void InvalidateViewports(Widget parent)
    {
      for (int i = 0; i < parent.ChildCount; ++i)
      {
        Widget child = parent.GetChild(i);
        Utility.RetrievePayload<SketchpadViewport>(child.Payload)?.Invalidate();

        InvalidateViewports(child);
      }
    }

    private static void ReleaseViewports(Widget parent)
    {
      for (int i = 0; i < parent.ChildCount; ++i)
      {
        Widget child = parent.GetChild(i);
        Utility.FreePayload(child.Payload);

        ReleaseViewports(child);
      }
    }

    private void RecreateTorusKnot()
    {
      const string meshName = "TorusKnot";

      bool exists = RemoveAndUnassignMesh(meshName);
      SceneMesh mesh;

      using (MeshBuffer meshBuffer = new())
      {
        int p = manager.GetWidget("PValueSpinEdit").Get<int>("Value.Last");
        int q = manager.GetWidget("QValueSpinEdit").Get<int>("Value.Last");
        float insideRadiusShape = manager.GetWidget("InsideRadiusShapeGaugeEdit").Get<float>("Value");
        float lateralRotation = manager.GetWidget("LateralRotationGaugeEdit").Get<float>("Value") *
          MathF.PI / 180.0f;

        meshBuffer.TorusKnot(512, 128, default, p, q, new Vector2(20.0f, 20.0f), 100.0f, 0.0f,
          MathF.PI * 2.0f, insideRadiusShape, 0.0f, MathF.PI * 2.0f, null, null, lateralRotation);

        mesh = meshes.AddFromBuffer(meshName, meshBuffer, vertexElements);
      }
      if (exists)
        AssignMeshToObjects(mesh);
    }

    private void RecreateCube()
    {
      const string meshName = "Cube";

      bool exists = RemoveAndUnassignMesh(meshName);
      SceneMesh mesh;

      using (MeshBuffer meshBuffer = new())
      {
        float longitudeShape = manager.GetWidget("LongitudeShapeGaugeEdit").Get<float>("Value");
        float latitudeShape = manager.GetWidget("LatitudeShapeGaugeEdit").Get<float>("Value");

        meshBuffer.SuperEllipse(32, 32, default, new Vector3(4.0f, 4.0f, 4.0f), 0.0f, MathF.PI * 2.0f,
          longitudeShape, -MathF.PI * 0.5f, MathF.PI * 0.5f, latitudeShape);

        // When a surface of super-ellipse begins to have cusps, smooth normals no longer look correctly,
        // no matter how they are calculated. Therefore, opt for flat normals in such cases.
        if (latitudeShape >= 2.5f || longitudeShape > 1.9f)
          meshBuffer.CalculateFlatNormals();

        mesh = meshes.AddFromBuffer(meshName, meshBuffer, vertexElements);
      }
      if (exists)
        AssignMeshToObjects(mesh);
    }

    private void AssignMeshToObjects(SceneMesh mesh)
    {
      bool assigned = false;

      foreach (ObjectModel obj in objects)
        if (obj.Mesh is null)
        {
          obj.Mesh = mesh;
          assigned = true;
        }

      if (assigned)
      {
        InvalidateViewports(manager);
        shadowMapNeedsUpdate = true;
        Invalidate();
      }
    }

    private bool RemoveAndUnassignMesh(string meshName)
    {
      SceneMesh mesh = meshes.TryMesh(meshName);
      if (mesh is not null)
      {
        foreach (ObjectModel obj in objects)
          if (obj.Mesh.Equals(mesh))
          {
            obj.Mesh = null;
            obj.Size = default;
          }

        meshes.Erase(mesh);
        return true;
      }
      else
        return false;
    }

    private Widget CreateUtilityWidget(Widget parent, string className, string baseName, string caption)
    {
      Widget placer = new(manager, "Placer", baseName + "Placer");
      placer.Rect = new RectF(0.0f, 0.0f, 0.0f, 32.0f);
      placer.Parent = parent;
      placer.Alignment = WidgetAlignment.Top;
      placer.Margins = new Margins(4.0f, 4.0f, 8.0f, 0.0f);

      if (className == "SpinEdit")
        placer.Set("Columns", "-0.25 -1 -0.5 -0.75");
      else
        placer.Set("Columns", "-0.25 -1 -1 -0.25");

      Widget label = new(manager, "Label", baseName + "Label");
      label.Style = "@.Label";
      label.Margins = new Margins(0.0f, 0.0f, 8.0f, 0.0f);
      label.Parent = placer;
      label.ParentZone = 1;
      label.Alignment = WidgetAlignment.Client;
      label.Set("Caption.Text.Alignment.Horizontal", TextAlignment.End);
      label.Set("Caption.Text", caption);

      Widget widget = new(manager, className, baseName + className);
      widget.Style = "@." + className;
      widget.Parent = placer;
      widget.ParentZone = 2;
      widget.Alignment = WidgetAlignment.Client;
      return widget;
    }

    private Widget CreateWindowCloseButton(Widget parent, string buttonName)
    {
      Widget button = new(manager, "Button", buttonName);
      button.Style = "@.FlatButton";
      button.Rect = new RectF(0.0f, 0.0f, 24.0f, 0.0f);
      button.Parent = parent;
      button.ParentZone = 1;
      button.Alignment = WidgetAlignment.Right;
      button.Margins = new Margins(4.0f);
      button.Set("Icon", 3);
      button.Set("Icon.Padding", 3.0f);
      button.Set("Focusable", false);
      return button;
    }

    private bool ManagerUpdateSettle()
    {
      // Perform iterative widget accomodation as long as "repaint" is requested. This ensures that all widget
      // children settle on their positioning and size.
      bool repaint = false;

      for (int i = 0; i < 16; ++i)
        if (manager.Update(0.0))
          repaint = true;
        else
          break;

      return repaint;
    }

    private void CreateUI()
    {
      RandomSequence prng = new (2025);

      // Please note that all UI controls have their style set using "@" symbol, which represents style
      // read from the manager. Therefore, if manager style is set to "Winter" and button has style set as
      // "@.Button", then its resulting style will be "Winter.Buttton". Using "@" symbol is easy to quickly
      // change the style of all UI widgets just by adjusting the style of the manager. The manager itself
      // ignores its style value.
      manager.Style = "Winter";

      // Create "placer" is basically a 2x2 grid for 3D viewports.
      Widget viewportPlacer = new(manager, "Placer", "ViewportPlacer");
      viewportPlacer.Parent = manager;
      viewportPlacer.Alignment = WidgetAlignment.Client;

      // Note: when specifying columns and/or rows in 'Placer' widget, negative values represent proportions,
      // whereas positive values represent DPI-independent size units (pixels when Scale = 1).
      viewportPlacer.Set("Columns", "-1 -1");
      viewportPlacer.Set("Rows", "-1 -1");

      // Create 3D viewports.
      for (int i = 0; i < 4; ++i)
      {
        SketchpadViewport viewport = new(i, objects.CreateView(), sceneDepthNormals, sceneModeling, lights,
          materials, shadowCastingAtlas);

        Widget sketchPad = new(manager, "SketchPad", $"Sketchpad{i + 1}", Utility.AssignPayload(viewport));
        sketchPad.Parent = viewportPlacer;
        sketchPad.ParentZone = i;
        sketchPad.Alignment = WidgetAlignment.Client;

        viewport.widget = sketchPad;
      }

      // Window with parameters and controls
      Widget window = new(manager, "Window", "Window1");
      window.Style = "@.Window";
      window.Set("Caption.Text", "Parameters and control (resizeable, moveable)");
      window.Parent = manager;
      window.Rect = new RectF(100.0f, 100.0f, 500.0f, 420.0f);
      window.Set("AllowedSizing", true);
      window.Set("AllowedMovement", true);
      window.Set("Constraints.Width.Minimum", 440.0f);
      window.Set("Constraints.Height.Minimum", 430.0f);

      { // Torus Knot label
        Widget label = new(manager, "Label", "TorusKnotLabel");
        label.Style = "@.Label";
        label.Rect = new RectF(0.0f, 0.0f, 0.0f, 32.0f);
        label.Parent = window;
        label.Alignment = WidgetAlignment.Top;
        label.Margins = new Margins(0.0f, 4.0f, 0.0f, 0.0f);
        label.Set("Caption.Text.Alignment.Horizontal", TextAlignment.Middle);
        label.Set("Caption.Text", "Torus Knot parameters:");
      }
      { // Inside Radius Shape
        Widget gauge = CreateUtilityWidget(window, "GaugeEdit", "InsideRadiusShape", "Inside radius shape:");
        gauge.Set("Value", 0.25);
        gauge.Set("Value.Decimal.Digits", 3);
        gauge.Set("Value.Minimum", 0.05);
        gauge.Set("Value.Maximum", 4.0);
      }
      { // Lateral Rotation
        Widget gauge = CreateUtilityWidget(window, "GaugeEdit", "LateralRotation", "Lateral rotation:");
        gauge.Set("Value", 0.0);
        gauge.Set("Value.Decimal.Digits", 1);
        gauge.Set("Value.Minimum", 0.0);
        gauge.Set("Value.Maximum", 90.0);
      }
      { // P value
        Widget edit = CreateUtilityWidget(window, "SpinEdit", "PValue", "P-value:");
        edit.Set("Value.Format", 3); // Integer
        edit.Set("Value", 3.0);
        edit.Set("Value.Minimum", 1.0);
        edit.Set("Value.Maximum", 4.0);
      }
      { // Q value
        Widget edit = CreateUtilityWidget(window, "SpinEdit", "QValue", "Q-value:");
        edit.Set("Value.Format", 3); // Integer
        edit.Set("Value", 2.0);
        edit.Set("Value.Minimum", 1.0);
        edit.Set("Value.Maximum", 4.0);
      }
      { // Cube label
        Widget label = new(manager, "Label", "CubeLabel");
        label.Style = "@.Label";
        label.Rect = new RectF(0.0f, 0.0f, 0.0f, 32.0f);
        label.Parent = window;
        label.Alignment = WidgetAlignment.Top;
        label.Margins = new Margins(0.0f, 4.0f, 0.0f, 0.0f);
        label.Set("Caption.Text.Alignment.Horizontal", TextAlignment.Middle);
        label.Set("Caption.Text", "Cube parameters:");
      }
      { // Latitude Shape
        Widget gauge = CreateUtilityWidget(window, "GaugeEdit", "LatitudeShape", "Latitude shape:");
        gauge.Set("Value", 0.2);
        gauge.Set("Value.Decimal.Digits", 3);
        gauge.Set("Value.Minimum", 0.05);
        gauge.Set("Value.Maximum", 4.0);
      }
      { // Longitude Shape
        Widget gauge = CreateUtilityWidget(window, "GaugeEdit", "LongitudeShape", "Longitude shape:");
        gauge.Set("Value", 0.2);
        gauge.Set("Value.Decimal.Digits", 3);
        gauge.Set("Value.Minimum", 0.05);
        gauge.Set("Value.Maximum", 4.0);
      }
      { // Show other windows button
        Widget placer = new(manager, "Placer", "ShowOtherWindowsPlacer");
        placer.Rect = new RectF(0.0f, 0.0f, 0.0f, 32.0f);
        placer.Parent = window;
        placer.Alignment = WidgetAlignment.Top;
        placer.Margins = new Margins(0.0f, 16.0f, 0.0f, 0.0f);
        placer.Set("Columns", "-1 200 -1");

        Widget button = new(manager, "Button", "ShowOtherWindowsButton");
        button.Style = "@.Button";
        button.Parent = placer;
        button.ParentZone = 1;
        button.Alignment = WidgetAlignment.Client;
        button.Set("Caption.Text", "Show other windows");
      }
      { // Enumerate properties button
        Widget button = new(manager, "Button", "EnumeratePropertiesButton");
        button.Style = "@.Button";
        button.Rect = new RectF(0.0f, 0.0f, 0.0f, 32.0f);
        button.Parent = window;
        button.Alignment = WidgetAlignment.Top;
        button.Margins = new Margins(20.0f, 8.0f, 20.0f, 0.0f);
        button.Set("Caption.Text", "Enumerate all properties of UI controls and save to disk");
      }
      // Another test window
      window = new(manager, "Window", "Window2");
      window.Style = "@.Window";
      window.Parent = manager;
      window.Rect = new RectF(700.0f, 50.0f, 450.0f, 300.0f);
      window.Set("Caption.Text", "Window with tabs (resizeable, moveable)");
      window.Set("AllowedSizing", true);
      window.Set("AllowedMovement", true);
      window.Set("Constraints.Width.Minimum", 400.0f);
      window.Set("Constraints.Height.Minimum", 200.0f);
      window.Visible = false;

      { // Tab Sheets
        Widget tabSheets = new(manager, "TabSheets", "SomeTabSheets");
        tabSheets.Style = "@.TabSheets";
        tabSheets.Parent = window;
        tabSheets.Alignment = WidgetAlignment.Client;

        // Create two very basic styles for a custom "ExampleTabSheet" that is used below.
        manager.Set("Winter.ExampleTabSheet.Fill.Color", 0xFFE0E0E0u);
        manager.Set("Night.ExampleTabSheet.Fill.Color", 0xFF505050u);

        // ...individual sheets
        for (int i = 0; i < 10; ++i)
        {
          Widget tabSheet = new(manager, "Panel", $"TabSheet{i + 1}");
          tabSheet.Style = "@.ExampleTabSheet";
          tabSheet.Parent = tabSheets;
          tabSheet.Alignment = WidgetAlignment.Client;
          tabSheet.Set("Tab.Caption", $"Sheet{i + 1}");
          tabSheet.Visible = i == 0;

          // Place some random button on the tab sheet.
          Widget button = new(manager, "Button", $"Sheet{i + 1}Button");
          button.Style = "@.Button";
          button.Rect = new RectF(16.0f + MathF.Truncate((prng.Value * 200.0f) / 4.0f) * 4.0f, 16.0f +
            MathF.Truncate((prng.Value * 200.0f) / 4.0f) * 4.0f, 160.0f, 32.0f);
          button.Parent = tabSheet;
          button.Set("Caption.Text", $"Some button {i + 1}");
        }
      }
      // Create button to close the window
      CreateWindowCloseButton(window, "CloseWindowButton2");

      // Some move windows for testing.
      for (int i = 0; i < 3; ++i)
      {
        window = new(manager, "Window", $"Window{3 + i}");
        window.Style = "@.Window";
        window.Parent = manager;
        window.Rect = new RectF(250.0f + i * 100.0f, 250.0f + i * 100.0f, 400.0f, 300.0f);
        window.Set("Caption.Text", "Another window (moveable)");
        window.Set("AllowedMovement", true);
        window.Visible = false;

        CreateWindowCloseButton(window, $"CloseWindowButton{3 + i}");
      }
    }

    public static bool ManagerExternalEvent(Widget sender, string eventName)
    {
      SketchpadViewport viewport = Utility.RetrievePayload<SketchpadViewport>(sender.Payload);
      if (viewport is not null)
        return viewport.HandleEvent(sender, eventName);

      if (constructingUI)
        return true;

      string senderName = sender.Name;

      if ((senderName == "PValueSpinEdit" || senderName == "QValueSpinEdit" ||
        senderName == "InsideRadiusShapeGaugeEdit" || senderName == "LateralRotationGaugeEdit") &&
        string.Equals(eventName, "Value.Changed", StringComparison.OrdinalIgnoreCase))
      {
        recreateTorusKnot = true;
        Application.OpenForms[0].Invalidate();
      }
      else if ((senderName == "LongitudeShapeGaugeEdit" || senderName == "LatitudeShapeGaugeEdit") &&
        string.Equals(eventName, "Value.Changed", StringComparison.OrdinalIgnoreCase))
      {
        recreateCube = true;
        Application.OpenForms[0].Invalidate();
      }
      else if (senderName == "EnumeratePropertiesButton" &&
        string.Equals(eventName, "Action", StringComparison.OrdinalIgnoreCase))
      {
        (Application.OpenForms[0] as MainForm).EnumeratePropertiesAndSaveToDisk();
      }
      else if (senderName.StartsWith("CloseWindowButton") &&
        string.Equals(eventName, "Action", StringComparison.OrdinalIgnoreCase))
      {
        sender.Parent.Visible = false;
      }
      else if (senderName == "ShowOtherWindowsButton" &&
        string.Equals(eventName, "Action", StringComparison.OrdinalIgnoreCase))
      {
        WidgetManager manager = sender.Manager;
        manager.GetWidget("Window2").Visible = true;
        manager.GetWidget("Window3").Visible = true;
        manager.GetWidget("Window4").Visible = true;
        manager.GetWidget("Window5").Visible = true;
        manager.GetWidget("Window5").Focused = true;
      }
      return true;
    }

    private void CreateObjects()
    {
      { // Floor
        ObjectModel obj = objects.Add("Floor");
        obj.MeshName = "Plane";
        obj.SetTransform(ModelTransform.Local, Matrix4x4.CreateTranslation(0.0f, -0.1f, 0.0f));
        obj.Color = new FloatColor(0xFF505050u);
        obj.Material = 0;
      }
      { // Torus Knot
        ObjectModel obj = objects.Add("TorusKnot");
        obj.MeshName = "TorusKnot";
        obj.SetTransform(ModelTransform.Local, Matrix4x4.CreateTranslation(0.0f, 0.0f, 0.0f));
        obj.Color = new FloatColor(0xFF00E000u);
        obj.Material = 1;
      }
      RandomSequence prng = new RandomSequence(2025);
      uint[] colors = {0xFF00FF00u, 0xFFFF0000u, 0xFF0000FFu, 0xFFFF00FFu, 0xFFFFFF00u, 0xFF00FFFFu,
        0xFF808080u, 0xFF4080FFu};

      for (int i = 0; i < 2000; ++i)
      {
        float radius = 240.0f + prng.Gaussian * 80.0f;
        float angle = prng.Value * MathF.PI * 2.0f;

        ObjectModel obj = objects.Add();
        obj.MeshName = "Cube";
        obj.SetTransform(ModelTransform.Local,
          Matrix4x4.CreateScale(MathF.Max(0.75f + prng.Gaussian * 0.5f, 0.25f)) *
          Matrix4x4.CreateRotationY(prng.Value * MathF.PI * 0.5f) *
          Matrix4x4.CreateTranslation(
            Math.Clamp(MathF.Cos(angle) * radius, -360.0f, 360.0f), 0.0f,
            Math.Clamp(MathF.Sin(angle) * radius, -360.0f, 360.0f)));

        obj.Color = new FloatColor(colors[prng.Ranged(colors.Length)]);
        obj.Material = 1;
      }
    }

    private void CreateMeshes()
    {
      using MeshBuffer meshBuffer = new();

      meshBuffer.Plane(1, 1, default, new Vector3(800.0f, 0.0f, 0.0f), new Vector3(0.0f, 0.0f, 800.0f),
        Vector3.UnitY);

      meshes.AddFromBuffer("Plane", meshBuffer, vertexElements);
    }

    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 = 1.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) Object
        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);
      }
    }
  }
}