/*
 * 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:
//  * 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.
//  * Grapher module, which allows drawing lines, arrows and other simple shapes in 3D.
//  * Optional real-time 3D volumetric text.
//  * Third-person view camera with optional location-based zoom, which zooms in or out where the mouse is
//    pointing at.

#ifdef _WIN32
  #include "Resource.h"
  #include <SDKDDKVer.h>

  #define WIN32_LEAN_AND_MEAN
  #include <windows.h>
  // Remove conflicting Windows macros.
  #undef KEY_EXECUTE
#endif

#include <stdio.h>
#include <math.h>

#include "Afterwarp.h"
#include "Afterwarp.Vectors.h"

#define AFTERWARP_VECTORS_IMPLEMENTATION
#include "Afterwarp.Vectors.h"

// Global variables.

// Display characteristics.
static Point displaySize = {960, 720};
static float displayScale = 1.0f;

// Base Afterwarp interfaces.
static Application* application = NULL;
static Device* device = NULL;
static SwapChain* swapChain = NULL;
static Canvas* canvas = NULL;
static TextRenderer* textRenderer = NULL;
static TextModeller* textModeller = NULL;
static Grapher* grapher = NULL;

static SceneLights* lights = NULL;
static TextureCabinet* textureCabinet = NULL;
static Scene* sceneModeling = NULL;

static MeshModel* modelPlane = NULL;
static MeshModel* modelCube = NULL;

static ActorCamera* camera = NULL;
static Matrix projection = {0};
static Matrix viewProjection = {0};
static Matrix viewInverse = {0};

static Vector rayOrigin = {0};
static Vector rayDirection = {0};
static Vector mousePos = {0};

static float axisLength = 0.0f;
static Vector axisFinal[3] = {0};
static float axisThickness = 0.0f;

static Vector location = {0};
static bool locationLocked = false;
static bool keyShift = false;
static bool volumetricText = false;

static VertexElement const vertexElements[] = {
  {"vertexPosition", ELEMENT_FORMAT_FLOAT, 3, 0, 0},
  {"vertexNormal", ELEMENT_FORMAT_FLOAT, 3, 0, 0}};

static int32_t const vertexElementsCount = sizeof(vertexElements) / sizeof(VertexElement);

// Forward declarations.

static void renderText(void);
static void renderText3D(void);
static void renderChart(void);
static void renderCubes(void);
static void setupPlotting(void);
static void outputError(char const* message);

// Implementation.

LibraryBool applicationCreate(Application* application, void* user)
{
  device = DeviceCreate(application, DEVICE_TECHNOLOGY_UNKNOWN, 0);
  if (!device)
  {
    outputError("Could not create graphics device.");
    return false;
  }
  swapChain = SwapChainCreate(device, ApplicationGetWindowHandle(application), &displaySize,
    PIXEL_FORMAT_RGBA8, PIXEL_FORMAT_UNKNOWN, 0, false);
  if (!swapChain)
  {
    outputError("Could not create rendering swap-chain.");
    return false;
  }
#ifndef _WIN32
  {
    char const* fileNames[] = {"../../../../Media/afterwarp.ico"};

    if (!ApplicationSetIconsFromFiles(application, fileNames, sizeof(fileNames) / sizeof(fileNames[0])))
    {
      outputError("Could not load application icon.");
      return false;
    }
  }
#endif
  canvas = CanvasCreate(device);
  if (!canvas)
  {
    outputError("Could not create rendering canvas.");
    return false;
  }
  textRenderer = TextRendererCreate(canvas, &(Point){512, 512}, PIXEL_FORMAT_LA8, false);
  if (!textRenderer)
  {
    outputError("Could not create text rendering module.");
    return false;
  }
  textModeller = TextModellerCreate(device);
  if (!textModeller)
  {
    outputError("Could not create 2D/3D text rendering module.");
    return false;
  }
  grapher = GrapherCreate(device);
  if (!grapher)
  {
    outputError("Could not create grapher module.");
    return false;
  }
  textureCabinet = TextureCabinetCreate(device, &displaySize,
  #if (defined(_MSC_VER) && defined(_M_ARM64)) || defined(__aarch64__)
    // Use lower quality settings on mobile WoA devices.
    TEXTURE_FIDELITY_LOW, 4, PIXEL_FORMAT_D16
  #else
    TEXTURE_FIDELITY_HIGH, 8, PIXEL_FORMAT_D32F
  #endif
  );
  if (!textureCabinet)
  {
    outputError("Could not create texture cabinet container.");
    return false;
  }
  sceneModeling = SceneCreateModeling(device);
  if (!sceneModeling || !SceneSetVertexElements(sceneModeling, 0, vertexElements,
    sizeof(vertexElements) / sizeof(VertexElement)) ||
    !SceneSetVertexElementsFromTextModeller(sceneModeling, 1))
  {
    outputError("Could not create 3D rendering scene.");
    return false;
  }
  lights = SceneLightsCreate(device);
  if (!lights)
  {
    outputError("Could not create 3D scene lights module.");
    return false;
  }
  camera = ActorCameraCreate(&(Vector){3.0f, -2.5f, -0.15f}, &(Vector){0.584f, -0.585f, 0.0f}, 320.0f);
  if (!camera)
  {
    outputError("Could not create object camera.");
    return false;
  }
  // Limit camera position and rotation to reasonable values.
  ActorCameraSetConstraints(camera, &(CameraConstraints){
    .positionMin.x = -100.0f, .positionMax.x = 100.0f,
    .positionMin.y = -100.0f, .positionMax.y = 100.0f,
    .positionMin.z = -100.0f, .positionMax.z = 100.0f,
    .positionMin.w =  150.0f, .positionMax.w = 600.0f,
    .rotationMin.x = (float)M_PI * 0.1f,
    .rotationMax.x = (float)M_PI * 0.4f});

  { // Create model of a 3D plane and cube.
    MeshBuffer* meshBuffer = MeshBufferCreate();
    if (!meshBuffer)
    {
      outputError("Could not create mesh buffer object.");
      return false;
    }

    MeshBufferPlane(meshBuffer, 1, 1, NULL, &(Vector){1.0f, 0.0f, 0.0f}, &(Vector){0.0f, 0.0f, 1.0f},
      &(Vector){0.0f, 1.0f, 0.0f}, NULL, NULL, NULL, true);

    modelPlane = MeshBufferCreateModel(meshBuffer, device, vertexElements, vertexElementsCount,
      0, 0, 0, 0, 0);

    MeshBufferClear(meshBuffer);

    Matrix const transform = matrixRotateX((float)M_PI * 0.5f);
    MeshBufferSetTransform(meshBuffer, &transform);

    MeshBufferSuperEllipse(meshBuffer, 32, 32, NULL, &(Vector){0.5f, 0.5f, 0.5f}, 0.0f,
      (float)M_PI * 2.0f, 0.15f, (float)M_PI * -0.5f, (float)M_PI * 0.5f, 0.15f, NULL, NULL, NULL, true);

    modelCube = MeshBufferCreateModel(meshBuffer, device, vertexElements, vertexElementsCount,
      0, 0, 0, 0, 0);

    MeshBufferDestroy(meshBuffer);

    if (!modelPlane || !modelCube)
    {
      outputError("Could not create geometry objects.");
      return false;
    }
  }
  SceneLight* const light = SceneLightsAdd(lights);
  if (light)
  {
    light->ambientColor = floatColorRGB(0xFFFFFFu);
    light->albedoColor = floatColorRGB(0xFFFFFFu);
    light->specularColor = floatColorRGB(0xFFFFFFu);
    light->position = (Vector){200.0f, 200.0f, -400.0f};
  }
  return true;
}

void applicationDestroy(Application* application, void* user)
{
  MeshModelDestroy(modelCube);
  MeshModelDestroy(modelPlane);
  ActorCameraDestroy(camera);
  SceneLightsDestroy(lights);
  SceneDestroy(sceneModeling);
  TextureCabinetDestroy(textureCabinet);
  GrapherDestroy(grapher);
  TextModellerDestroy(textModeller);
  TextRendererDestroy(textRenderer);
  CanvasDestroy(canvas);
  SwapChainDestroy(swapChain);
  DeviceDestroy(device);
}

void applicationResize(Application* application, void* user)
{
  Rect clientRect = {0};
  ApplicationGetClientRect(application, &clientRect);

  displaySize = rectGetSize(clientRect);

  SwapChainResize(swapChain, &displaySize);
  TextureCabinetSetSize(textureCabinet, &displaySize);
}

void applicationMouse(Application* application, MouseEvent event, MouseButton button, Point const* position,
  void* user)
{
  Vector const prevMousePos = mousePos;
  mousePos = (Vector){.x = position->x, .y = position->y, .z = 0.0f};

  bool const positionChanged = !vectorEquals(prevMousePos, mousePos);

  PointF const screenSize = pointF(displaySize);
  PointF const halfScreenSize = pointRescaleF(screenSize, 0.5f);

  switch (event)
  {
  case MOUSE_EVENT_MOUSE_DOWN:
    if (button == MOUSE_BUTTON_LEFT && keyShift)
    {
      if (locationLocked)
      {
        locationLocked = false;
        location = vectorZero;
      }
      else if (!vectorEmpty(location))
        locationLocked = true;
    }
    else if (button == MOUSE_BUTTON_LEFT)
      ActorCameraCommand(camera, CAMERA_COMMAND_ROTATION, &mousePos, &screenSize, NULL, NULL);
    else if (button == MOUSE_BUTTON_RIGHT)
      ActorCameraCommand(camera, CAMERA_COMMAND_DRAGGING, &mousePos, &screenSize, NULL, NULL);
    break;

  case MOUSE_EVENT_MOUSE_MOVE:
    if (!locationLocked)
    {
      bool valid = false;
      Vector intersection;

      PointF const targetSize = pointF(displaySize);
      RayCreate(&rayOrigin, &rayDirection, (PointF*)&mousePos, &targetSize, &viewInverse, &projection);

      // Calculate intersection point with one of three planes.
      if (RayIntersectPlane(&rayOrigin, &rayDirection, NULL, &vectorAxisY, &intersection, NULL) &&
        intersection.x >= 0.0f && intersection.x < axisLength - 14.0f &&
        intersection.z <= 0.0f && intersection.z > -(axisLength - 12.0f))
        valid = true;
      else if (RayIntersectPlane(&rayOrigin, &rayDirection, NULL, &vectorAxisX, &intersection, NULL) &&
        intersection.y >= 0.0f && intersection.y < axisLength - 14.0f &&
        intersection.z <= 0.0f && intersection.z > -(axisLength - 12.0f))
        valid = true;
      else if (RayIntersectPlane(&rayOrigin, &rayDirection, NULL, &vectorAxisZ, &intersection, NULL) &&
        intersection.x >= 0.0f && intersection.x < axisLength - 14.0f &&
        intersection.y >= 0.0f && intersection.y < axisLength - 12.0f)
        valid = true;

      if (!valid)
        intersection = vectorZero;

      if (!vectorEquals(location, intersection))
      {
        location = intersection;
        ApplicationInvalidate(application);
      }
    }
    if (ActorCameraCommand(camera, CAMERA_COMMAND_UPDATE, &mousePos, &screenSize, NULL, NULL))
      ApplicationInvalidate(application);
    break;

  case MOUSE_EVENT_MOUSE_UP:
    ActorCameraCommand(camera, CAMERA_COMMAND_STOP, &mousePos, &screenSize, NULL, NULL);
    break;

  case MOUSE_EVENT_WHEEL_UP:
    ActorCameraZoom(camera, keyShift ? (PointF*)&mousePos : &halfScreenSize, &screenSize,
      &projection, -20.0f);
    ApplicationInvalidate(application);
    break;

  case MOUSE_EVENT_WHEEL_DOWN:
    ActorCameraZoom(camera, keyShift ? (PointF*)&mousePos : &halfScreenSize, &screenSize,
      &projection, 20.0f);
    ApplicationInvalidate(application);
    break;
  }
  if (positionChanged)
    ApplicationInvalidate(application); // Update and show object selection.
}

void applicationKey(Application* application, KeyEvent event, int32_t virtualKey, uint16_t keyCode,
  void* user)
{
  Key const key = ApplicationTranslateVirtualKey(application, virtualKey);

  if (key == KEY_SHIFT_LEFT || key == KEY_SHIFT_RIGHT)
  {
    keyShift = event == KEY_EVENT_PRESSED;
    ApplicationInvalidate(application);
  }
  else if (key == KEY_SPACE && event == KEY_EVENT_PRESSED)
  {
    volumetricText = !volumetricText;
    ApplicationInvalidate(application);
  }
  else if (key == KEY_ESCAPE && event == KEY_EVENT_PRESSED)
    ApplicationTerminate(application);
}

void applicationRender(Application* application, void* user)
{
  displayScale = ApplicationGetWindowScale(application);
  setupPlotting();

  { // Update texture cabinet's attributes.
    TextureCabinetAttributes attributes =
      TEXTURE_CABINET_ATTRIBUTE_BLOOM |
    #if (!defined(_MSC_VER) || !defined(_M_ARM64)) && !defined(__aarch64__)
      TEXTURE_CABINET_ATTRIBUTE_BLOOM_CUBIC |
    #endif
      TEXTURE_CABINET_ATTRIBUTE_AMBIENT_OCCLUSION;

    if (displayScale >= 2.0f)
      attributes |=
        TEXTURE_CABINET_ATTRIBUTE_BLOOM_DOWNSCALE |
        TEXTURE_CABINET_ATTRIBUTE_AMBIENT_OCCLUSION_DOWNSCALE;

    TextureCabinetSetAttributes(textureCabinet, attributes);

    // Adjust ambient occlusion parameters.
    AmbientOcclusionParameters parameters;
    TextureCabinetGetAmbientOcclusionParameters(textureCabinet, &parameters);

    parameters.kernelBias = 0.05f;
    parameters.strength = 1.0f;
    parameters.radius = 6.0f;

    TextureCabinetSetAmbientOcclusionParameters(textureCabinet, &parameters);
  }

  // Only one light is used for the entire scene, so only minimal light culling is required.
  SceneLightsSetCullingMode(lights, CLUSTERS_CULLING_MODE_PERFORMANCE);
  SceneLightsSetClusterSize(lights, 512);
  SceneLightsSetDepthSlices(lights, 1);
  SceneLightsSetViewSize(lights, &displaySize);
  { // Perform light assignment for 3D scene.
    Matrix view, projection;

    SceneGetView(sceneModeling, &view);
    SceneGetProjection(sceneModeling, &projection);

    SceneLightsExecute(lights, &view, &projection);
  }

  FloatColor const clearColor = floatColor(0xFFFFFFFFu);

  if (TextureCabinetClear(textureCabinet, TEXTURE_CABINET_PASS_COLOR, &clearColor) &&
    TextureCabinetBegin(textureCabinet, TEXTURE_CABINET_PASS_COLOR))
  {
    // Using experimental single-pass rendering (without a depth pre-pass).
    SceneAttributes const attributes = SCENE_ATTRIBUTE_NORMALS | SCENE_ATTRIBUTE_SINGLE_PASS;
    SceneSetLights(sceneModeling, lights);

    if (ScenePrepare(sceneModeling))
    {
      // Render 3D text.
      if (volumetricText)
      {
        SceneSetActiveVertexElements(sceneModeling, 1);
        SceneSetAttributes(sceneModeling, attributes | SCENE_ATTRIBUTE_COLORING);

        if (SceneBegin(sceneModeling))
        {
          renderText3D();
          SceneEnd(sceneModeling);
        }
      }
      // Render 3D cubes.
      SceneSetActiveVertexElements(sceneModeling, 0);
      SceneSetAttributes(sceneModeling, attributes);

      if (SceneBegin(sceneModeling))
      {
        renderCubes();
        SceneEnd(sceneModeling);
      }
    }
    renderChart();

    if (CanvasBegin(canvas))
    {
      renderText();
      CanvasEnd(canvas);
    }
    TextureCabinetEnd(textureCabinet);
  }
  TextureCabinetFilter(textureCabinet, TEXTURE_CABINET_FILTER_TYPE_OCCLUSION, &projection);
  TextureCabinetFilter(textureCabinet, TEXTURE_CABINET_FILTER_TYPE_BLOOM, NULL);

  // Render on the main form.
  if (SwapChainBegin(swapChain))
  {
    FloatColor const clearColor = floatColor(0xFF404040u);
    DeviceClear(device, DEVICE_CLEAR_LAYER_COLOR, &clearColor, 1.0f, 0);

    TextureCabinetPresent(textureCabinet);

    if (CanvasBegin(canvas))
    {
      CanvasSetAttributes(canvas, 0);
      CanvasSetContextState(canvas, CANVAS_CONTEXT_STATE_FLAT_TEXT);
      CanvasSetSamplerState(canvas, &(CanvasSamplerState){TEXTURE_FILTER_NEAREST, TEXTURE_FILTER_NEAREST});

      { // Draw text hints.
        FontParameters parameters = {
          .family = "Segoe UI", .size = 14.0f * displayScale, .weight = FONT_WEIGHT_SEMI_BOLD,
          .stretch = FONT_STRETCH_NORMAL, .slant = FONT_SLANT_NONE, .effect = fontEffectDefault};
        parameters.effect.borderType = FONT_BORDER_SEMI_HEAVY;
        parameters.effect.borderBrightness = 1.0f;
        parameters.effect.fillBrightness = 0.0f;
        parameters.effect.shadowBrightness = 0.75f;
        parameters.effect.shadowOpacity = 1.0f;
        TextRendererSetFontParameters(textRenderer, &parameters);

        char const* const textDirect3D = "Direct3D (%d.%d), features: %d.%d";
        char const* const textOpenGL = "OpenGL (%d.%d+), features: %d.%d";

        char const* const formatText = DeviceGetTechnology(device) == DEVICE_TECHNOLOGY_DIRECT3D ?
          textDirect3D : textOpenGL;

        char text[128];

        snprintf(text, sizeof(text), formatText,
          DeviceGetTechVersion(device) >> 8, (DeviceGetTechVersion(device) >> 4) & 0xF,
          DeviceGetTechFeatureVersion(device) >> 8, (DeviceGetTechFeatureVersion(device) >> 4) & 0xF);

        TextRendererDraw(textRenderer, &(PointF){8.0f * displayScale, 4.0f * displayScale}, text,
          &COLOR_PAIR(0xA0FFFFFFu), 1.0f, NULL);

        if (volumetricText)
          snprintf(text, sizeof(text), "3D volumetric text. Press Space to change.");
        else
          snprintf(text, sizeof(text), "SDF text in 3D. Press Space to change.");

        TextRendererDrawAligned(textRenderer, &(PointF){8.0f * displayScale, displaySize.y -
          4.0f * displayScale}, text, &COLOR_PAIR(0xA0FFFFFFu), TEXT_ALIGNMENT_START, TEXT_ALIGNMENT_END,
          1.0f, false, NULL);
      }
      CanvasEnd(canvas);
    }
    SwapChainEnd(swapChain);
  }
}

void renderText(void)
{
  // How many numbers to show over each axis.
  int32_t const numberCount = (int32_t)floorf(axisLength / 20.0f) - 1;

  // Offset for rendering text relative to axis.
  float const offset = axisThickness * 0.5f + 1.0f;

  // Configure canvas for 3D projected SDF rendering.
  CanvasSetSignedDistanceField(canvas, &(SignedDistanceField){
    SUPER_SAMPLE_SDF_SUPER_SAMPLE_16X, 2.0f, {0.0f, 1.0f, 0.0f}, 0.15f, 0.45f});

  CanvasSetAttributes(canvas, CANVAS_ATTRIBUTE_PROJECTED | CANVAS_ATTRIBUTE_SDF | CANVAS_ATTRIBUTE_CUBIC);
  CanvasSetContextState(canvas, CANVAS_CONTEXT_STATE_PROJECTED);
  CanvasSetSamplerState(canvas, &(CanvasSamplerState){TEXTURE_FILTER_LINEAR, TEXTURE_FILTER_LINEAR});

  CanvasSetViewProjection(canvas, &viewProjection);

  { // Enable depth bias to prevent depth fighting with plane.
    RenderingState renderState;
    DeviceGetRenderingState(device, &renderState);
    renderState.states &= ~DEVICE_STATE_DEPTH_WRITE;
    renderState.depthBias = 5.0f;
    renderState.slopeDepthBias = 5.0f;
    DeviceSetRenderingState(device, &renderState);
  }
  { // Prepare SDF font.
    FontParameters parameters = {
      .family = "Verdana", .size = 32.0f, .weight = FONT_WEIGHT_SEMI_BOLD,
      .stretch = FONT_STRETCH_NORMAL, .slant = FONT_SLANT_NONE, .effect = fontEffectDefault};
    parameters.effect.signedFieldDistance = 2.0f;
    TextRendererSetFontParameters(textRenderer, &parameters);
  }

  // Prepare SDF font modifiers.
  TextRenderModifiers modifiers = {0};
  modifiers.scale = 0.2f;

  // Text container.
  char text[16];

  // "axis1"
  Matrix transform = matrixRotateX((float)M_PI * 0.5f);
  CanvasSetWorld(canvas, &transform);

  TextRendererDrawAligned(textRenderer, &(PointF){axisLength, -offset}, "axis1", &COLOR_PAIR(0xFF000000u),
    TEXT_ALIGNMENT_MIDDLE, TEXT_ALIGNMENT_END, 1.0, false, &modifiers);

  // numbers for "axis2"
  for (int32_t i = 0; i < numberCount; ++i)
  {
    snprintf(text, sizeof(text), "%d", i + 1);

    TextRendererDrawAligned(textRenderer, &(PointF){axisLength + offset, (i + 1) * 20.0f}, text,
      &COLOR_PAIR(0xFF000000u), TEXT_ALIGNMENT_START, TEXT_ALIGNMENT_MIDDLE, 1.0, false, &modifiers);
  }

  // Mouse pointer values.
  if (!vectorEmpty(location) && !volumetricText)
  {
    snprintf(text, sizeof(text), "%1.2f", -location.z / 20.0f);

    if (fabsf(location.y) <= 0.01f)
      TextRendererDrawAligned(textRenderer, &(PointF){-offset, -location.z}, text, &COLOR_PAIR(0xFF505050u),
        TEXT_ALIGNMENT_END, TEXT_ALIGNMENT_MIDDLE, 1.0, false, &modifiers);

    if (fabsf(location.x) <= 0.01f)
      TextRendererDrawAligned(textRenderer, &(PointF){offset, -location.z}, text, &COLOR_PAIR(0xFF505050u),
        TEXT_ALIGNMENT_START, TEXT_ALIGNMENT_MIDDLE, 1.0, false, &modifiers);
  }

  // "axis2"
  transform = matrixRotateX((float)M_PI * 0.5f);
  transform = matrixMultiply(transform, matrixRotateY(-(float)M_PI * 0.5f));
  CanvasSetWorld(canvas, &transform);

  TextRendererDrawAligned(textRenderer, &(PointF){-axisLength, -offset}, "axis2", &COLOR_PAIR(0xFF000000u),
    TEXT_ALIGNMENT_MIDDLE, TEXT_ALIGNMENT_END, 1.0, false, &modifiers);

  // numbers for "axis1"
  for (int32_t i = 0; i < numberCount; ++i)
  {
    snprintf(text, sizeof(text), "%d", (i + 1) * 5);

    TextRendererDrawAligned(textRenderer, &(PointF){-axisLength - offset, (i + 1) * 20.0f}, text,
      &COLOR_PAIR(0xFF000000u), TEXT_ALIGNMENT_END, TEXT_ALIGNMENT_MIDDLE, 1.0, false, &modifiers);
  }

  // Mouse pointer values.
  if (!vectorEmpty(location) && !volumetricText)
  {
    snprintf(text, sizeof(text), "%1.2f", (location.x / 20.0f) * 5.0f);

    if (fabsf(location.y) <= 0.01f)
      TextRendererDrawAligned(textRenderer, &(PointF){offset, location.x}, text, &COLOR_PAIR(0xFF505050u),
        TEXT_ALIGNMENT_START, TEXT_ALIGNMENT_MIDDLE, 1.0, false, &modifiers);

    if (fabsf(location.z) <= 0.01f)
      TextRendererDrawAligned(textRenderer, &(PointF){-offset, location.x}, text, &COLOR_PAIR(0xFF505050u),
        TEXT_ALIGNMENT_END, TEXT_ALIGNMENT_MIDDLE, 1.0, false, &modifiers);
  }

  // "axis3"
  transform = matrixRotateZ((float)M_PI * 0.5f);
  CanvasSetWorld(canvas, &transform);

  TextRendererDrawAligned(textRenderer, &(PointF){axisLength, -offset}, "axis3", &COLOR_PAIR(0xFF000000u),
    TEXT_ALIGNMENT_MIDDLE, TEXT_ALIGNMENT_END, 1.0, false, &modifiers);

  // numbers for "axis3"
  CanvasSetWorld(canvas, &matrixIdentity);

  for (int32_t i = 0; i < numberCount; ++i)
  {
    snprintf(text, sizeof(text), "%d", (int32_t)powf(i + 11.0f, 2.0f) * 37);

    TextRendererDrawAligned(textRenderer, &(PointF){-offset, -(i + 1) * 20.0f}, text,
      &COLOR_PAIR(0xFF000000u), TEXT_ALIGNMENT_END, TEXT_ALIGNMENT_MIDDLE, 1.0, false, &modifiers);
  }

  // Mouse pointer values.
  if (!vectorEmpty(location) && !volumetricText)
  {
    transform = matrixRotateY(-(float)M_PI * 0.5f);
    CanvasSetWorld(canvas, &transform);

    snprintf(text, sizeof(text), "%d", (int32_t)roundf(powf((location.y / 20.0f) + 10.0f, 2.0f) * 37.0f));

    if (fabsf(location.x) <= 0.01f || fabsf(location.z) <= 0.01f)
      TextRendererDrawAligned(textRenderer, &(PointF){offset, -location.y}, text, &COLOR_PAIR(0xFF505050u),
        TEXT_ALIGNMENT_START, TEXT_ALIGNMENT_MIDDLE, 1.0, false, &modifiers);
  }
  if (!volumetricText)
  {
    // Enable Outline in SDF settings.
    CanvasSetSignedDistanceField(canvas, &(SignedDistanceField){
      SUPER_SAMPLE_SDF_SUPER_SAMPLE_16X, 2.0f, {-0.5f, 1.0f, -1.0f}, 0.15f, 0.45f});

    CanvasSetAttributes(canvas, CANVAS_ATTRIBUTE_PROJECTED | CANVAS_ATTRIBUTE_SDF |
      CANVAS_ATTRIBUTE_OUTLINE_SDF | CANVAS_ATTRIBUTE_CUBIC);

    // Update SDF font modifiers for Outline.
    modifiers.scale = 0.3f;

    // "chart 2"
    transform = matrixRotateX((float)M_PI * 0.5f);
    CanvasSetWorld(canvas, &transform);

    TextRendererDrawAligned(textRenderer, &(PointF){40.0f, 65.0f}, "Chart 2", &COLOR_PAIR(0xFF29D48Au),
      TEXT_ALIGNMENT_MIDDLE, TEXT_ALIGNMENT_START, 1.0, false, &modifiers);

    // Change SDF outline color.
    CanvasSetSignedDistanceField(canvas, &(SignedDistanceField){
      SUPER_SAMPLE_SDF_SUPER_SAMPLE_16X, 2.0f, {-0.5f, 1.0f, 1.0f}, 0.15f, 0.45f});

    // "chart 1"
    CanvasSetWorld(canvas, &matrixIdentity);

    TextRendererDrawAligned(textRenderer, &(PointF){40.0f, -80.0f}, "Chart 1", &COLOR_PAIR(0xFF0177FFu),
      TEXT_ALIGNMENT_MIDDLE, TEXT_ALIGNMENT_END, 1.0, false, &modifiers);
  }
}

void renderText3D(void)
{
  SceneSetWorld(sceneModeling, &matrixIdentity);

  ObjectMaterial material = {0};
  material.ambientColor = floatColorRGB(0x404040u);
  material.albedoColor = floatColor(0xD0FFFFFFu);
  material.specularColor = floatColorRGB(0xFFFFFFu);
  material.specularExponent = 8.0f;
  SceneSetMaterial(sceneModeling, &material);

  TextModellerClear(textModeller);

  { // Specify font parameters for 3D text.
    FontParameters parameters = {
      .family = "Verdana", .size = 32.0f, .weight = FONT_WEIGHT_SEMI_BOLD,
      .stretch = FONT_STRETCH_NORMAL, .slant = FONT_SLANT_NONE};
    TextModellerSetFontParameters(textModeller, &parameters);
  }

  TextRenderModifiers modifiers = {0};
  modifiers.scale = 0.3f;

  TextModellerSetTransform(textModeller, &matrixIdentity);

  TextModellerDraw(textModeller,
    &(Vector){.x = 40.0f, .y = 0.0f, .z = -80.0f},
    "Chart 2",
    10.0f,
    &COLOR_PAIR(0xFF29D48Au),
    TEXT_ALIGNMENT_MIDDLE,
    TEXT_ALIGNMENT_END,
    TEXT_ALIGNMENT_START,
    false,
    &modifiers);

  Matrix transform = matrixRotateX((float)-M_PI * 0.5f);
  TextModellerSetTransform(textModeller, &transform);

  TextModellerDraw(textModeller,
    &(Vector){.x = 40.0f, .y = 0.0f, .z = 80.0f},
    "Chart 1",
    10.0f,
    &COLOR_PAIR(0xFF0177FFu),
    TEXT_ALIGNMENT_MIDDLE,
    TEXT_ALIGNMENT_END,
    TEXT_ALIGNMENT_START,
    false,
    &modifiers);

  TextModellerPrepare(textModeller);
  TextModellerRender(textModeller, SceneGetProgram(sceneModeling), 0);
}

void renderChart(void)
{
  // Calculate number of lines in 3D graph.
  int32_t const lineCount = (int32_t)ceilf(axisLength / 20.0f);

  // Make own version of axes that is multiple of line count.
  Vector axisDots[3];

  for (int_fast8_t i = 0; i < 3; ++i)
    axisDots[i] = vectorRescale(vectorNormalize(axisFinal[i]), lineCount * 20.0f);

  RenderingState initialState;
  DeviceGetRenderingState(device, &initialState);

  { // Enable depth bias to prevent depth fighting with plane.
    RenderingState renderState = initialState;
    renderState.states &= ~DEVICE_STATE_DEPTH_WRITE;
    renderState.depthBias = 5.0f;
    renderState.slopeDepthBias = 5.0f;
    DeviceSetRenderingState(device, &renderState);
  }
  GrapherSetTransform(grapher, &viewProjection);

  if (GrapherBegin(grapher))
  {
    PointF const targetSize = pointF(displaySize);

    // Axis arrows.
    for (int_fast8_t i = 0; i < 3; ++i)
      GrapherArrow(grapher, &targetSize, NULL, &axisFinal[i], 0xFF000000u, axisThickness,
        axisThickness * 5.0f, LINE_CAPS_BUTT);

    // Separation dots.
    for (int32_t line = 1; line < lineCount; ++line)
      for (int_fast8_t axis = 0; axis < 3; ++axis)
      {
        float const lineTheta = 1.0f - ((float)line / lineCount);
        int_fast8_t const axisNext = (axis + 2) % 3;
        {
          Vector const position1 = vectorLerp(axisDots[axisNext], vectorZero, lineTheta);
          Vector const position2 = vectorAdd(position1, vectorRescale(vectorNormalize(axisDots[axis]),
            axisLength));

          GrapherDottedLine(grapher, &position2, &position1, 0xFF808080u, 0xFF808080u, axisThickness * 0.75f,
            axisThickness * 0.75f, 4.0f, POINT_SHAPE_ROUND, (float)M_PI * 0.15f);
        }
        {
          Vector const position1 = vectorLerp(axisDots[axis], vectorZero, lineTheta);
          Vector const position2 = vectorAdd(position1, vectorRescale(vectorNormalize(axisDots[axisNext]),
            axisLength));

          GrapherDottedLine(grapher, &position2, &position1, 0xFF808080u, 0xFF808080u, axisThickness * 0.75f,
            axisThickness * 0.75f, 4.0f, POINT_SHAPE_ROUND, (float)M_PI * 0.15f);
        }
      }

    // Selection markers for mouse pointer.
    if (!vectorEmpty(location) && !volumetricText)
    {
      static Color const selectColor = 0xFF808080u;

      if (fabs(location.y) <= 0.01f)
      {
        GrapherLine(grapher, &(Vector){0.0f, 0.0f, location.z}, &(Vector){location.x, 0.0f, location.z},
          selectColor, selectColor, axisThickness * 0.75f, axisThickness * 0.75f, LINE_CAPS_BUTT);

        GrapherLine(grapher, &(Vector){location.x, 0.0f, 0.0f}, &(Vector){location.x, 0.0f, location.z},
          selectColor, selectColor, axisThickness * 0.75f, axisThickness * 0.75f, LINE_CAPS_BUTT);
      }
      else if (fabs(location.x) <= 0.01f)
      {
        GrapherLine(grapher, &(Vector){0.0f, 0.0f, location.z}, &(Vector){0.0f, location.y, location.z},
          selectColor, selectColor, axisThickness * 0.75f, axisThickness * 0.75f, LINE_CAPS_BUTT);

        GrapherLine(grapher, &(Vector){0.0f, location.y, 0.0f}, &(Vector){0.0f, location.y, location.z},
          selectColor, selectColor, axisThickness * 0.75f, axisThickness * 0.75f, LINE_CAPS_BUTT);
      }
      else if (fabs(location.z) <= 0.01f)
      {
        GrapherLine(grapher, &(Vector){0.0f, location.y, 0.0f}, &(Vector){location.x, location.y, 0.0f},
          selectColor, selectColor, axisThickness * 0.75f, axisThickness * 0.75f, LINE_CAPS_BUTT);

        GrapherLine(grapher, &(Vector){location.x, 0.0f, 0.0f}, &(Vector){location.x, location.y, 0.0f},
          selectColor, selectColor, axisThickness * 0.75f, axisThickness * 0.75f, LINE_CAPS_BUTT);
      }
    }
    GrapherEnd(grapher);
  }
  DeviceSetRenderingState(device, &initialState);
}

void renderCubes(void)
{
  static float const heights[2][4] = {{20.0f, 60.0f, 40.0f, 80.0f}, {10.0f, 30.0f, 60.0f, 40.0f}};

  ObjectMaterial material = {0};
  material.ambientColor = floatColorRGB(0x203D53u);
  material.albedoColor = floatColor(0xD00083E4u);
  material.specularColor = floatColorRGB(0xFFFFFFu);
  material.specularExponent = 8.0f;

  SceneSetMaterial(sceneModeling, &material);

  // Render first row of cubes.
  for (int32_t i = 0; i < 4; ++i)
  {
    Matrix const transform =
      matrixMultiply(
        matrixScale((Vector){20.0f, heights[0][i], 20.0f}),
        matrixTranslate((Vector){10.0f + i * 20.0f, heights[0][i] * 0.5f, -10.0f}));

    SceneSetWorld(sceneModeling, &transform);

    MeshModelDraw(modelCube, SceneGetProgram(sceneModeling), PRIMITIVE_TOPOLOGY_TRIANGLES, 0, 0, 0, true);
  }

  // Update material for another set of cubes.
  material.ambientColor = floatColorRGB(0x235240u);
  material.albedoColor = floatColor(0xD000D784u);
  SceneSetMaterial(sceneModeling, &material);

  // Render second row of cubes.
  for (int32_t i = 0; i < 4; ++i)
  {
    Matrix const transform =
      matrixMultiply(
        matrixScale((Vector){20.0f, heights[1][i], 20.0f}),
        matrixTranslate((Vector){10.0f + i * 20.0f, heights[1][i] * 0.5f, -50.0f}));

    SceneSetWorld(sceneModeling, &transform);

    MeshModelDraw(modelCube, SceneGetProgram(sceneModeling), PRIMITIVE_TOPOLOGY_TRIANGLES, 0, 0, 0, true);
  }

  // Draw a plane on bottom to make ambient occlusion more apparent.
  material.ambientColor = floatColorRGB(0xFFFFFFu);
  material.albedoColor = floatColorTraslucentBlack;
  material.specularColor = floatColorBlackRGB;
  SceneSetMaterial(sceneModeling, &material);

  Matrix transform = matrixMultiply(
    matrixScale((Vector){.x = 400.0f, .y = 1.0f, .z = 400.0f}),
    matrixTranslate((Vector){0.0f, -0.1f, 0.0f}));

  SceneSetWorld(sceneModeling, &transform);
  MeshModelDraw(modelPlane, SceneGetProgram(sceneModeling), PRIMITIVE_TOPOLOGY_TRIANGLES, 0, 0, 0, true);

  if (volumetricText)
  {
    // Draw another plane on the back of chart to enable ambient occlusion on 3D text.
    transform = matrixMultiply(
      matrixScale((Vector){.x = 90.0f, .y = 1.0f, .z = 100.0f}),
      matrixMultiply(
        matrixRotateX((float)-M_PI * 0.5f),
        matrixTranslate((Vector){45.0f, 50.0f, 0.5f})));

    SceneSetWorld(sceneModeling, &transform);
    MeshModelDraw(modelPlane, SceneGetProgram(sceneModeling), PRIMITIVE_TOPOLOGY_TRIANGLES, 0, 0, 0, true);
  }
}

void setupPlotting(void)
{
  bool const depthClipNegative = DeviceGetBehavior(device) & DEVICE_BEHAVIOR_DEPTH_CLIP_NEGATIVE;
  float const cameraDistance = ActorCameraGetDistance(camera);

  axisThickness = MAX((displaySize.y * 3.0f / 600.0f) * 320.0f / cameraDistance, 3.0f) * displayScale;
  axisLength = MAX(110.0f * cameraDistance / 320.0f, 110.0f);

  axisFinal[0] = (Vector){axisLength, 0.0f, 0.0f};
  axisFinal[1] = (Vector){0.0f, axisLength, 0.0f};
  axisFinal[2] = (Vector){0.0f, 0.0f, -axisLength};

  // View matrix is extracted directly from object manipulation camera.
  Matrix view;
  ActorCameraGetView(camera, &view);

  // Build projection matrix.
  projection = matrixPerspectiveFOVY((float)M_PI * 0.25f, (float)displaySize.x / displaySize.y,
    10.0f, 1400.0, depthClipNegative);

  // Set the matrices to the modeling scene.
  SceneSetView(sceneModeling, &view);
  SceneSetProjection(sceneModeling, &projection);

  // Precalculate view/projection and inverse view matrices.
  viewProjection = matrixMultiply(view, projection);
  viewInverse = matrixInverse(view);

  // Calculate mouse pointing ray.
  PointF const targetSize = pointF(displaySize);
  RayCreate(&rayOrigin, &rayDirection, (PointF*)&mousePos, &targetSize, &viewInverse, &projection);
}

void outputError(char const* message)
{
#ifdef _WIN32
  static char const* initErrorTitle = "Initialization Error";
  MessageBoxA(NULL, message, initErrorTitle, MB_ICONERROR | MB_OK | MB_DEFAULT_DESKTOP_ONLY);
#else
  printf("%s\n", message);
#endif
}

// Main application entry.

#ifdef _WIN32

int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prevInstance, _In_ LPWSTR cmdLine,
  _In_ int showCmd)
{
  UNREFERENCED_PARAMETER(prevInstance);
  UNREFERENCED_PARAMETER(cmdLine);
  UNREFERENCED_PARAMETER(showCmd);

  char const* windowClassName = "AfterwarpChard3DDemo";

#else

int main(int argc, char **argv)
{

#endif

  ApplicationConfiguration configuration = {0};
  configuration.size = displaySize;
  configuration.position = (Point){.x = INT32_MAX, .y = INT32_MAX}; // default position
#ifdef _WIN32
  configuration.startup.win.handleInstance = instance;
  configuration.startup.win.handleIconSmall = LoadIcon(instance, MAKEINTRESOURCE(IDI_AFTERWARP));
  configuration.startup.win.handleIconBig = LoadIcon(instance, MAKEINTRESOURCE(IDI_AFTERWARP));

  strncpy(configuration.startup.win.windowClassName, windowClassName,
    sizeof(configuration.startup.win.windowClassName));
#else
  configuration.startup.nx.parameterCount = argc;
  configuration.startup.nx.parameterStrings = argv;
#endif

  application = ApplicationCreate(&configuration);
  if (!application)
  {
    outputError("Could not create Application object.");
    return -1;
  }

  ApplicationEvents events = {0};
  events.eventCreate = applicationCreate;
  events.eventDestroy = applicationDestroy;
  events.eventResize = applicationResize;
  events.eventMouse = applicationMouse;
  events.eventKey = applicationKey;
  events.eventRender = applicationRender;
  ApplicationSetEvents(application, &events, NULL);

  displayScale = ApplicationGetWindowScale(application);
  ApplicationSetTitle(application, "Afterwarp: Chart3D example");

  { // Retrieve client size back in case it has changed since startup.
    Rect clientRect = {0};
    ApplicationGetClientRect(application, &clientRect);

    displaySize = rectGetSize(clientRect);
  }

  bool const executed = ApplicationExecute(application);
  ApplicationDestroy(application);

  return executed ? 0 : -1;
}