/*
 * 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:
//  * Real-time volumetric text rendering in 3D using "TextModeller" module, including "curved" text.
//  * Ground fog effect, which simulates fog densely packed near certain surface under certain height.
//  * Two independent shadow-casting light sources, HDR rendering and bloom.
//  * Timer-based rendering, which ensures fixed-rate rendering playback.
//  * Third-person view camera with location-based zoom (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 <stdlib.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 = {1280, 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 Timer* timer = NULL;

static ObjectModels* objects = NULL;
static SceneLights* lights = NULL;
static TextureCabinet* textureCabinet = NULL;
static ShadowCastingAtlas* shadowCastingAtlas = NULL;
static ObjectModelView* shadowCasterViews[2] = {NULL};
static SpatialFog* spatialFog = NULL;
static MeshModel* modelFloor = NULL;

static Scene* sceneDepthNormals = NULL;
static Scene* sceneModeling = NULL;

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

static int keyAlt = 0, frameIndex = 0;

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(Scene* scene);
static void renderFloorModel(Scene* scene);
static void prepareTextForRendering(void);
static bool createFloorModel(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 text modeling and 3D rendering module.");
    return false;
  }
  lights = SceneLightsCreate(device);
  if (!lights)
  {
    outputError("Could not create 3D scene lights module.");
    return false;
  }
  objects = ObjectModelsCreate(NULL);
  if (!objects)
  {
    outputError("Could not create objects container.");
    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_MEDIUM, 4,
  #else
    TEXTURE_FIDELITY_HIGH, 8,
  #endif
    PIXEL_FORMAT_UNKNOWN);
  if (!textureCabinet)
  {
    outputError("Could not create texture cabinet container.");
    return false;
  }
  { // Update tone-mapping/bloom parameters to adjust for high white level.
    ToneMappingBloom params;
    TextureCabinetGetToneMappingBloom(textureCabinet, &params);
    params.toneWhite = 2.0f;
    TextureCabinetSetToneMappingBloom(textureCabinet, &params);
  }

  shadowCastingAtlas = ShadowCastingAtlasCreate(device, &(Point){512 + 2, 256}, TECHNIQUE_SHADOWS_EVSM, 8, 1);
  if (!shadowCastingAtlas)
  {
    outputError("Could not create shadow casting atlas container.");
    return false;
  }
  sceneDepthNormals = SceneCreateDepthsNormals(device);
  if (!sceneDepthNormals || !SceneSetVertexElements(sceneDepthNormals, 0, vertexElements,
    vertexElementsCount) || !SceneSetVertexElementsFromTextModeller(sceneDepthNormals, 1))
  {
    outputError("Could not create depths/normals rendering scene.");
    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;
  }
  // Create lights and shadow casters.
  bool const depthClipNegative = DeviceGetBehavior(device) & DEVICE_BEHAVIOR_DEPTH_CLIP_NEGATIVE;

  for (int_fast8_t i = 0; i < 2; ++i)
  {
    SceneLight* light = SceneLightsAdd(lights);
    if (!light)
      return false;

    light->ambientColor = floatColorRGB(0xFFFFFFu);
    light->albedoColor = floatColorRGB(0xFFFFFFu);
    light->specularColor = floatColorRGB(0xFFFFFFu);
    light->specularExponent = 4.0f;
    light->intensity = 1.0f;
    light->position = i == 0 ? (Vector){.x = 300.0f, .y = 800.0f, .z = -300.0f} :
      (Vector){.x = -300.0f, .y = 800.0f, .z = 300.0f};
    light->shadowCaster = i;

    shadowCasterViews[i] = ObjectModelsCreateView(objects);

    Matrix const view = matrixLookAt(light->position, vectorZero, vectorAxisY);
    Matrix const projection = matrixPerspectiveFOVY((float)M_PI * 0.3f, 1.0f, 200.0f, 1400.0f,
      depthClipNegative);

    ObjectModelViewSetView(shadowCasterViews[i], &view);
    ObjectModelViewSetProjection(shadowCasterViews[i], &projection, depthClipNegative);

    ShadowCaster* const caster = ShadowCastingAtlasAdd(shadowCastingAtlas, &(Point){256, 256}, true);
    if (!caster)
    {
      outputError("Could not create shadow caster or its associated view.");
      return false;
    }
  }
  spatialFog = SpatialFogCreate(device);
  if (!spatialFog)
  {
    outputError("Could not create spatial fog module.");
    return false;
  }
  timer = TimerCreate();
  if (!timer)
  {
    outputError("Could not create timer module.");
    return false;
  }
  TimerSetSpeed(timer, 60.0); // Enable 60 frames per second processing speed.

  camera = ActorCameraCreate(NULL, &(Vector){(float)M_PI * 0.4f, 0.0f, 0.0f}, 400.0f);
  if (!camera)
  {
    outputError("Could not create actor camera.");
    return false;
  }
  // Limit camera position and rotation to reasonable values.
  ActorCameraSetConstraints(camera, &(CameraConstraints){
    .positionMin.x = -200.0f, .positionMax.x = 200.0f,
    .positionMin.y =  -50.0f, .positionMax.y =  50.0f,
    .positionMin.z = -200.0f, .positionMax.z = 200.0f,
    .positionMin.w =  300.0f, .positionMax.w = 600.0f,
    .rotationMin.x = (float)M_PI * 0.01f,
    .rotationMax.x = (float)M_PI * 0.4f});

  if (!createFloorModel())
  {
    outputError("Could not create floor model.");
    return false;
  }
  return true;
}

void applicationDestroy(Application* application, void* user)
{
  MeshModelDestroy(modelFloor);
  ActorCameraDestroy(camera);
  TimerDestroy(timer);
  SpatialFogDestroy(spatialFog);
  SceneDestroy(sceneModeling);
  SceneDestroy(sceneDepthNormals);
  ShadowCastingAtlasDestroy(shadowCastingAtlas);
  TextureCabinetDestroy(textureCabinet);
  ObjectModelsDestroy(objects);
  SceneLightsDestroy(lights);
  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);

  ApplicationInvalidate(application);
}

void applicationRender(Application* application, void* user)
{
  bool const depthClipNegative = DeviceGetBehavior(device) & DEVICE_BEHAVIOR_DEPTH_CLIP_NEGATIVE;
  displayScale = ApplicationGetWindowScale(application);

  if (frameIndex == 0)
    TimerReset(timer);

  // Configure view and projection matrices.
  Matrix view = {0};
  ActorCameraGetView(camera, &view);

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

  // Perform light assignment for 3D scene.
  SceneLightsSetViewSize(lights, &displaySize);
  SceneLightsSetCullingMode(lights, CLUSTERS_CULLING_MODE_PERFORMANCE);
  SceneLightsSetClusterSize(lights, 256);
  SceneLightsSetDepthSlices(lights, 4);
  SceneLightsExecute(lights, &view, &projection);

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

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

    TextureCabinetSetAttributes(textureCabinet, attributes);
  }

  // Pre-render text in 3D.
  prepareTextForRendering();

  // Render shadow maps.
  ShadowCastingAtlasBorderFill(shadowCastingAtlas);

  for (int32_t i = 0; i < ShadowCastingAtlasGetCount(shadowCastingAtlas); ++i)
  {
    ShadowCaster* const shadowCaster = ShadowCastingAtlasGetCaster(shadowCastingAtlas, i);
    ObjectModelView* const shadowCasterView = shadowCasterViews[i];

    if (shadowCaster && shadowCasterView)
    {
      // Copy view/projection matrix for the shadow caster view into the shadow caster itself.
      Matrix viewProjection;
      ObjectModelViewGetViewProjection(shadowCasterView, &viewProjection);
      ShadowCasterSetViewProjection(shadowCaster, &viewProjection);

      if (ObjectModelViewUpdate(shadowCasterView) && ShadowCasterClear(shadowCaster) &&
        ShadowCasterBegin(shadowCaster))
      {
        Matrix view, projection;
        ObjectModelViewGetView(shadowCasterView, &view);
        ObjectModelViewGetProjection(shadowCasterView, &projection, NULL);

        SceneSetAttributes(sceneDepthNormals, 0);
        SceneSetView(sceneDepthNormals, &view);
        SceneSetProjection(sceneDepthNormals, &projection);

        SceneSetActiveVertexElements(sceneDepthNormals, 0);
        if (SceneBegin(sceneDepthNormals))
        {
          renderFloorModel(sceneDepthNormals);
          SceneEnd(sceneDepthNormals);
        }
        SceneSetActiveVertexElements(sceneDepthNormals, 1);
        if (SceneBegin(sceneDepthNormals))
        {
          renderText(sceneDepthNormals);
          SceneEnd(sceneDepthNormals);
        }
        ShadowCasterEnd(shadowCaster);
        ShadowCasterFilter(shadowCaster);
      }
    }
  }

  // Prepare scene's attributes.
  SceneAttributes const attributes = 0
    | SCENE_ATTRIBUTE_NORMALS
    | SCENE_ATTRIBUTE_SHADOWS_CUBIC
    | SCENE_ATTRIBUTE_DEPTH_PREPASS
    | SCENE_ATTRIBUTE_LINEAR_DEPTHS;

  // Render depth pre-pass.
  TextureCabinetClear(textureCabinet, TEXTURE_CABINET_PASS_DEPTH, NULL);

  if (TextureCabinetBegin(textureCabinet, TEXTURE_CABINET_PASS_DEPTH))
  {
    SceneSetAttributes(sceneDepthNormals, attributes);
    SceneSetView(sceneDepthNormals, &view);
    SceneSetProjection(sceneDepthNormals, &projection);

    SceneSetAttributes(sceneDepthNormals, attributes);
    SceneSetActiveVertexElements(sceneDepthNormals, 0);
    if (SceneBegin(sceneDepthNormals))
    {
      renderFloorModel(sceneDepthNormals);
      SceneEnd(sceneDepthNormals);
    }
    SceneSetAttributes(sceneDepthNormals, attributes | SCENE_ATTRIBUTE_COLORING);
    SceneSetActiveVertexElements(sceneDepthNormals, 1);
    if (SceneBegin(sceneDepthNormals))
    {
      renderText(sceneDepthNormals);
      SceneEnd(sceneDepthNormals);
    }
    TextureCabinetEnd(textureCabinet);
  }
  TextureCabinetFilter(textureCabinet, TEXTURE_CABINET_FILTER_TYPE_OCCLUSION, &projection);

  // In order to ensure rendering exactly 60 frames per second, the following function call waits until next
  // time slice is reached before returning, issuing an appropriate yield and/or sleep command as necessary.
  // Traditionally, this is called at the beginning of rendering call. However, to take better advantage of
  // parallelism with GPU, this function is called here after depth pre-pass, which would start ambient
  // occlusion calculation (a rather heavy-weight GPU task). Therefore, while the current thread waits for
  // next time slice (if such hasn't been reached yet), the GPU continues working.
  TimerNextSlice(timer);

  // Render main scene.
  TextureCabinetClear(textureCabinet, TEXTURE_CABINET_PASS_COLOR, &(FloatColor){2.0f, 2.0f, 2.0f, 1.0f});

  if (TextureCabinetBegin(textureCabinet, TEXTURE_CABINET_PASS_COLOR))
  {
    SceneSetAttributes(sceneModeling, attributes);
    SceneSetView(sceneModeling, &view);
    SceneSetProjection(sceneModeling, &projection);

    SceneSetLights(sceneModeling, lights);
    SceneSetShadowCastingAtlas(sceneModeling, shadowCastingAtlas);
    SceneSetTextureCabinet(sceneModeling, textureCabinet);

    if (ScenePrepare(sceneModeling))
    {
      SceneSetAttributes(sceneModeling, attributes);
      SceneSetActiveVertexElements(sceneModeling, 0);
      if (SceneBegin(sceneModeling))
      {
        renderFloorModel(sceneModeling);
        SceneEnd(sceneModeling);
      }
      SceneSetAttributes(sceneModeling, attributes | SCENE_ATTRIBUTE_COLORING);
      SceneSetActiveVertexElements(sceneModeling, 1);
      if (SceneBegin(sceneModeling))
      {
        renderText(sceneModeling);
        SceneEnd(sceneModeling);
      }
    }
    TextureCabinetEnd(textureCabinet);
  }
  { // Apply fog.
    FogParameters parameters = {0};
    parameters.color = (FloatColor){2.0f, 2.0f, 2.0f, 0.0f};
    parameters.opacity = 1.0f;
    parameters.groundPlane = (Vector4){0.0f, 1.0f, 0.0f, -100.0f};
    parameters.scattering = 1.0f;
    parameters.extinction = 0.003f;
    parameters.densityGround = 0.03f;
    SpatialFogSetParameters(spatialFog, &parameters);

    SpatialFogSetFormula(spatialFog, FOG_FORMULA_GROUND);
    SpatialFogSetDepthDistance(spatialFog, DEPTH_FOG_DISTANCE_EYE_RELATIVE);

    SpatialFogSetView(spatialFog, &view);
    SpatialFogSetProjection(spatialFog, &projection);

    SpatialFogExecute(spatialFog, TextureCabinetGetTexture(textureCabinet, TEXTURE_CABINET_TYPE_COLOR_HDR),
      TextureCabinetGetTexture(textureCabinet, TEXTURE_CABINET_TYPE_LINEAR_DEPTHS));
  }
  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.borderOpacity = 0.75f;
        parameters.effect.fillBrightness = 0.0f;
        parameters.effect.shadowBrightness = 0.85f;
        parameters.effect.shadowOpacity = 0.4f;
        TextRendererSetFontParameters(textRenderer, &parameters);

        TextRendererDrawAligned(textRenderer, &(PointF){truncf(displaySize.x * 0.5f), displaySize.y -
          4.0f * displayScale}, "Rotate and move the camera around to see ground-based fog.",
          &(ColorPair){0xFFFF4040u, 0xFFFFFF40u}, TEXT_ALIGNMENT_MIDDLE, TEXT_ALIGNMENT_END,
          1.0f, true, NULL);
      }
      { // Draw technology string.
        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, scale %1.2f";
        char const* const textOpenGL = "OpenGL (%d.%d+), features: %d.%d, scale %1.2f";

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

        TextRendererDraw(textRenderer, &(PointF){8.0f * displayScale, 4.0f * displayScale}, text,
          &COLOR_PAIR(0xA0FFFFFFu), 1.0f, NULL);
      }
      CanvasEnd(canvas);
    }
    SwapChainEnd(swapChain);
  }
  TimerUpdate(timer);

  // Instead of incrementing frame index by one, it is incremented by the "tokens" extracted from the timer.
  // Extracted "tokens" include frame increment, plus any new skipped frames. As long as the rendering can
  // keep up and no frames are skipped, this will be incremented by one. If target speed cannot be achieved,
  // then this will be incremented accordingly to maintain the speed of animation (for example, video
  // playback) constant independently of rendering speed.
  frameIndex += TimerExtractTokens(timer);
}

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

  switch (event)
  {
  case MOUSE_EVENT_MOUSE_DOWN:
    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 (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, (PointF*)&mousePos, &screenSize, &projection, -10.0f);
    ApplicationInvalidate(application);
    break;

  case MOUSE_EVENT_WHEEL_DOWN:
    ActorCameraZoom(camera, (PointF*)&mousePos, &screenSize, &projection, 10.0f);
    ApplicationInvalidate(application);
    break;
  }
}

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

  if (key == KEY_ALT_LEFT || key == KEY_ALT_RIGHT)
  {
    keyAlt += event == KEY_EVENT_PRESSED ? 1 : -1;
    ApplicationInvalidate(application);
  }
  else if (key == KEY_ESCAPE && event == KEY_EVENT_PRESSED)
    ApplicationTerminate(application);
  else if (key == KEY_RETURN && event == KEY_EVENT_PRESSED && keyAlt)
  {
    ApplicationWindowState state = ApplicationGetWindowState(application);

    if (state != APPLICATION_WINDOW_STATE_FULLSCREEN)
      state = APPLICATION_WINDOW_STATE_FULLSCREEN;
    else
      state = APPLICATION_WINDOW_STATE_WINDOWED;

    ApplicationSetWindowState(application, state);
  }
}

LibraryBool applicationIdle(Application* application, void* user)
{
  ApplicationInvalidate(application);
  return false;
}

void renderText(Scene* scene)
{
  ObjectMaterial material = {0};
  material.ambientColor = (FloatColorRGB){0.25f, 0.25f, 0.25f};
  material.albedoColor = (FloatColor){1.0f, 1.0f, 1.0f, 0.75f};
  material.specularColor = (FloatColorRGB){1.0f, 1.0f, 1.0f};
  material.specularExponent = 4.0f;
  material.shadows = 1.0f;
  material.occlusion = 1.0f;
  material.roughness = (Vector){1.8f, 1.2f, 0.7f};
  material.technique = TECHNIQUE_LIGHTING_COOK_TORRANCE;
  SceneSetMaterial(scene, &material); // Ignored by depths/normals scene.

  SceneSetWorld(scene, &matrixIdentity);
  TextModellerRender(textModeller, SceneGetProgram(scene), 0);
}

void renderFloorModel(Scene* scene)
{
  ObjectMaterial material = {0};
  material.ambientColor = (FloatColorRGB){1.0f, 1.0f, 1.0f};
  material.albedoColor = (FloatColor){0.0f, 0.0f, 0.0f, 0.0f};
  material.specularColor = (FloatColorRGB){0.0f, 0.0f, 0.0f};
  material.specularExponent = 1.0f;
  material.shadows = 1.0f;
  material.occlusion = 1.0f;
  material.technique = TECHNIQUE_LIGHTING_PHONG;
  SceneSetMaterial(scene, &material); // Ignored by depths/normals scene.

  SceneSetWorld(scene, &matrixIdentity);
  MeshModelDraw(modelFloor, SceneGetProgram(scene), PRIMITIVE_TOPOLOGY_TRIANGLES, 0, 0, 0, true);
}

void prepareTextForRendering(void)
{
  TextModellerClear(textModeller);
  TextModellerSetTransform(textModeller, &matrixIdentity);

  // Draw standard extruded 3D text.
  TextModellerSetFontParameters(textModeller, &(FontParameters){
    .family = "Cambria", .size = 24.0f, .weight = FONT_WEIGHT_SEMI_BOLD,
    .stretch = FONT_STRETCH_NORMAL, .slant = FONT_SLANT_NONE, .effect = fontEffectDefault});

  TextModellerDraw(textModeller, &(Vector){.x = 0.0f, .y = 0.0f, .z = 80.0f},
    "What you see is a standard text extruded in 3D.", 3.0f, &(ColorPair){0xFFFF0000u, 0xFFFF00FFu},
    TEXT_ALIGNMENT_MIDDLE, TEXT_ALIGNMENT_MIDDLE, TEXT_ALIGNMENT_START, false, NULL);

  // Draw a text that changes each frame (shows frame number and skipped frames).
  // In terms of performance, there is no difference if text constantly changes or remains static.
  char text[256];
  int const skippedFrames = TimerGetSkippedTimeSlices(timer);

  snprintf(text, sizeof(text), "In real-time, frame index is (%d), skipped frames (%d).", frameIndex,
    skippedFrames);

  TextModellerSetFontParameters(textModeller, &(FontParameters){
    .family = "Calibri", .size = 18.0f, .weight = FONT_WEIGHT_SEMI_BOLD,
    .stretch = FONT_STRETCH_NORMAL, .slant = FONT_SLANT_NONE, .effect = fontEffectDefault});

  TextModellerDraw(textModeller, &(Vector){.x = 0.0f, .y = 0.0f, .z = 40.0f},
    text, 3.0f, &(ColorPair){0xFFFF0000u, 0xFFFF7070u},
    TEXT_ALIGNMENT_MIDDLE, TEXT_ALIGNMENT_MIDDLE, TEXT_ALIGNMENT_START, false, NULL);

  // Draw some more complicated 3D text, in Chinese (Simplified).
  TextModellerSetFontParameters(textModeller, &(FontParameters){
    .family = "Segoe UI", .size = 24.0f, .weight = FONT_WEIGHT_BOLD,
    .stretch = FONT_STRETCH_NORMAL, .slant = FONT_SLANT_NONE, .effect = fontEffectDefault});

  TextModellerDraw(textModeller, &(Vector){.x = 0.0f, .y = 0.0f, .z = -40.0f},
    u8"该文本可以用多种不同的语言编写。", 6.0f, &(ColorPair){0xFF008080u, 0xFF00FF80u},
    TEXT_ALIGNMENT_MIDDLE, TEXT_ALIGNMENT_MIDDLE, TEXT_ALIGNMENT_START, false, NULL);

  // The following uses "italic" font slant.
  TextModellerSetFontParameters(textModeller, &(FontParameters){
    .family = "Calibri", .size = 14.0f, .weight = FONT_WEIGHT_NORMAL,
    .stretch = FONT_STRETCH_NORMAL, .slant = FONT_SLANT_ITALIC, .effect = fontEffectDefault});

  TextModellerDraw(textModeller, &(Vector){.x = 0.0f, .y = 0.025f, .z = 110.0f},
    "Text in 3D can also be flat (no extrusion).", 0.0f, &COLOR_PAIR(0xFF000000u),
    TEXT_ALIGNMENT_MIDDLE, TEXT_ALIGNMENT_MIDDLE, TEXT_ALIGNMENT_START, false, NULL);

  // Draw curved text rotated inside its own plane.
  TextModellerSetFontParameters(textModeller, &(FontParameters){
    .family = "Segoe UI", .size = 24.0f, .weight = FONT_WEIGHT_SEMI_BOLD,
    .stretch = FONT_STRETCH_NORMAL, .slant = FONT_SLANT_NONE, .effect = fontEffectDefault});

  TextModellerDrawCurved(textModeller, &(Vector){.x = 0.0f, .y = 60.0f, .z = 0.0f},
    "This text is rendered at certain angle.", -200.0f, NAN, 5.0f, &(ColorPair){0xFF0000FFu, 0xFF00E0FFu},
    TEXT_ALIGNMENT_START, NULL);

  // Draw curved text rotated around "depth" axis.
  TextModellerDrawDepthCurved(textModeller, &(Vector){.x = 0.0f, .y = 80.0f, .z = 0.0f},
    "Curve rendering test application.", -200.0f, NAN, 5.0f, &(ColorPair){0xFF00FF00u, 0xFFFFFFFFu}, NULL);

  TextModellerPrepare(textModeller);
}

bool createFloorModel(void)
{
  MeshBuffer* meshBuffer = MeshBufferCreate();
  if (!meshBuffer)
    return false;

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

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

  MeshBufferDestroy(meshBuffer);
  return modelFloor;
}

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 = "AfterwarpCurvedText3DExample";

#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;
  events.eventIdle = applicationIdle;
  ApplicationSetEvents(application, &events, NULL);

  displayScale = ApplicationGetWindowScale(application);

  ApplicationSetTitle(application, "Afterwarp: CurvedText3D Example");
  ApplicationSetIconTitle(application, "CurvedText3D");

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