using System.Numerics;
using System.Runtime.ConstrainedExecution;

namespace Viewports
{
  using Afterwarp;
  //  using TextRenderer = Afterwarp.TextRenderer;

  /// A helper class for "Sketchpad" widget to manage rendering of 3D viewport.
  class SketchpadViewport : CriticalFinalizerObject, IDisposable
  {
    public SketchpadViewport(int viewportIndex, ObjectModelView objectModelView, Scene sceneDepthNormals,
      Scene sceneModeling, SceneLights sceneLights, ObjectMaterials materials, ShadowCastingAtlas shadowCastingAtlas)
    {
      this.objectModelView = objectModelView;
      this.sceneDepthNormals = sceneDepthNormals;
      this.sceneModeling = sceneModeling;
      this.sceneLights = sceneLights;
      this.materials = materials;
      this.shadowCastingAtlas = shadowCastingAtlas;
      camera = new (400.0f);
      updateNeeded = true;

      if (viewportIndex == 0)
        camera.SetRotation(new Vector4(MathF.PI * 0.495f, 0.0f, 0.0f, 0.0f));
      else if (viewportIndex == 1)
      {
        camera.SetRotation(new Vector4(MathF.PI * 0.15f, MathF.PI * 0.5f, 0.0f, 0.0f));
        camera.Distance = 500.0f;
      }
      else if (viewportIndex == 2)
      {
        camera.SetRotation(new Vector4(MathF.PI * 0.4f, -MathF.PI * 0.5f, 0.0f, 0.0f));
        camera.Distance = 600.0f;
      }
    }

    protected virtual void Dispose(bool disposing)
    {
      textureCabinet.Dispose();
      camera.Dispose();
    }

    #region IDisposable

    public void Dispose()
    {
      // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
      Dispose(disposing: true);
      GC.SuppressFinalize(this);
    }

    #endregion

    private void CreateTextureCabinet()
    {
      Device device = widget.Manager.TextRenderer.Canvas.Device;
      bool software = (device.Attributes & Device.Attribute.Software) != 0;

      textureCabinet = new TextureCabinet(device, viewportSize, software ? TextureFidelity.Medium :
        TextureFidelity.High, software ? 0 : 8);
    }

    private void UpdateProjection()
    {
      objectModelView.Projection = Utility.MatrixPerspective4x4(MathF.PI * 0.25f, (float)viewportSize.X /
        viewportSize.Y, 10.0f, 1500.0f);
    }

    public void Invalidate()
    {
      updateNeeded = true;
      widget.Invalidate();
    }

    public void Update()
    {
      if (!updateNeeded)
        return;

      updateNeeded = false;
      double scale = widget.Manager.Scale;

      // Perform light assignment for 3D scene.
      sceneLights.CullingMode = ClustersCullingMode.Performance;
      sceneLights.Size = viewportSize;
      sceneLights.ClusterSize = 512;
      sceneLights.DepthSlices = 1;
      sceneLights.Execute(camera.View, objectModelView.Projection);

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

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

        textureCabinet.Attributes = attributes;
      }
      // Update visibility in object model's view.
      objectModelView.View = camera.View;
      objectModelView.Update();

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

      // Depth pre-pass with ambient occlusion.
      textureCabinet.Clear(TextureCabinetPass.Depth);
      textureCabinet.Begin(TextureCabinetPass.Depth);
      try
      {
        sceneDepthNormals.Attributes = sceneAttributes;
        sceneDepthNormals.View = objectModelView.View;
        sceneDepthNormals.Projection = objectModelView.Projection;

        sceneDepthNormals.Begin();
        try
        {
          objectModelView.AutoDraw(sceneDepthNormals);
        }
        finally
        {
          sceneDepthNormals.End();
        }
      }
      finally
      {
        textureCabinet.End();
      }
      textureCabinet.Filter(TextureCabinetFilterType.Occlusion, objectModelView.Projection);

      // Render main scene.
      textureCabinet.Clear(TextureCabinetPass.Color, new FloatColor(0xFF808080u));
      textureCabinet.Begin(TextureCabinetPass.Color);
      try
      {
        sceneModeling.Attributes = sceneAttributes;
        sceneModeling.View = objectModelView.View;
        sceneModeling.Projection = objectModelView.Projection;

        sceneModeling.Lights = sceneLights;
        sceneModeling.Atlas = shadowCastingAtlas;
        sceneModeling.TextureCabinet = textureCabinet;

        sceneModeling.Prepare();
        sceneModeling.Begin();
        try
        {
          objectModelView.AutoDraw(sceneModeling, materials);
        }
        finally
        {
          sceneModeling.End();
        }
      }
      finally
      {
        textureCabinet.End();
      }
      textureCabinet.Filter(TextureCabinetFilterType.Bloom);
    }

    public bool HandleEvent(Widget sender, string eventName)
    {
      WidgetManager manager = sender.Manager;
      TextRenderer textRenderer = manager.TextRenderer;
      Canvas canvas = textRenderer.Canvas;
      float scale = (float)manager.Scale;

      if (string.Equals(eventName, "Draw", StringComparison.OrdinalIgnoreCase))
      {
        Vector2 offset = sender.Get<Vector2>("Transitory.Offset");
        RectF senderRect = sender.Rect;

        if (textureCabinet is not null)
          canvas.Quad(textureCabinet.Texture, new Quad(new RectF(offset, senderRect.Size)).Scale(
            scale, false));
      }
      else if (string.Equals(eventName, "Rect.Changed", StringComparison.OrdinalIgnoreCase))
      {
        viewportSize = new Point((int)MathF.Round(widget.Rect.Width), (int)MathF.Round(widget.Rect.Height));
        if (!viewportSize.Empty)
        {
          if (textureCabinet is not null)
            textureCabinet.Size = viewportSize;
          else
            CreateTextureCabinet();

          UpdateProjection();
        }
        Invalidate();
      }
      else if (string.Equals(eventName, "Mouse", StringComparison.OrdinalIgnoreCase))
      {
        RectF senderRect = sender.Rect;
        MouseEvent mouseEvent = sender.Get<MouseEvent>("Transitory.Event");
        MouseButton mouseButton = sender.Get<MouseButton>("Transitory.Button");
        Vector2 mousePosition = sender.Get<Vector2>("Transitory.Position");

        mousePosition = (sender.ScreenToLocal(mousePosition) - senderRect.TopLeft) * scale;
        Vector2 senderSize = senderRect.Size * scale;

        switch (mouseEvent)
        {
          case MouseEvent.Down:
            if (mouseButton == MouseButton.Left)
              camera.Command(CameraCommand.Rotation, new Vector3(mousePosition, 0.0f), senderSize);
            else if (mouseButton == MouseButton.Right)
              camera.Command(CameraCommand.Dragging, new Vector3(mousePosition, 0.0f), senderSize);
            break;

          case MouseEvent.Move:
            if (camera.Command(CameraCommand.Update, new Vector3(mousePosition, 0.0f), senderSize))
              Invalidate();
            break;

          case MouseEvent.Up:
            camera.Command(CameraCommand.Stop, new Vector3(mousePosition, 0.0f), senderSize);
            break;

          case MouseEvent.WheelUp:
            camera.Zoom(mousePosition, senderSize, objectModelView.Projection, -16.0f);
            Invalidate();
            break;

          case MouseEvent.WheelDown:
            camera.Zoom(mousePosition, senderSize, objectModelView.Projection, 16.0f);
            Invalidate();
            break;
        }
      }
      return true;
    }

    // Widget associated with the viewport.
    public Widget widget;

    // Object/Model view associated with the viewport.
    private readonly ObjectModelView objectModelView;

    // Depth/Normals scene shared among viewports.
    private readonly Scene sceneDepthNormals;

    // Modeling scene shared among viewports.
    private readonly Scene sceneModeling;

    // Scene lights container shared among viewports.
    private readonly SceneLights sceneLights;

    // Materials shared among viewports.
    private readonly ObjectMaterials materials;

    // Shadow-casting atlas shared among viewports.
    private readonly ShadowCastingAtlas shadowCastingAtlas;

    // Texture cabinet managed by the viewport.
    private TextureCabinet textureCabinet;

    // Camera managed by the viewport.
    private readonly ActorCamera camera;

    // Viewport size in pixels.
    private Point viewportSize;

    // Indicates that viewport must be updated.
    private bool updateNeeded;
  }
}
