/*
 * 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:
//  * Accurate Order-Independent Transparency (OIT) with Frosted Glass effect.
//  * Color dithering to reduce quantization errors when converting from 64-bits per pixel to 32-bit and
//    remove visible banding effects.
//  * Object layers, which define what objects are currently visible. This enables quick switching between
//    different views and/or floors.
//  * Optional orthographic projection, where all objects appear in their original sizes.
//  * 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 <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 SceneMeshes* meshes = NULL;
static ObjectModels* objects = NULL;
static SceneLights* lights = NULL;
static ObjectMaterials* materials = NULL;

static TextureCabinet* textureCabinet = NULL;
static ColorDithering* colorDithering = NULL;

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

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

static Vector mousePos = {0};
static uint64_t layers = 1;
static bool orthographic = false;
static float cameraZoom = 400.0f;
static int keyAlt = 0;

static VertexElement const vertexElements[] = {
  {"vertexPosition", ELEMENT_FORMAT_FLOAT, 3, 0, 0},
  {"vertexNormal", ELEMENT_FORMAT_FLOAT, 3, 0, 0},
  {"vertexColor", ELEMENT_FORMAT_UNSIGNED_BYTE, 4, 0 | VERTEX_CHANNEL_NORMALIZED, 0}};

static int32_t const vertexElementsCount = sizeof(vertexElements) / sizeof(VertexElement);
static Vector const lightPosition = {.x = 300.f, .y = 800.0f, .z = -300.0f};

// Forward declarations.

static void renderObjectsOpaque(Scene* scene, ObjectModelView* view);
static void renderObjectsGlassy(Scene* scene, ObjectModelView* view);
static void createObjects(void);
static bool loadMeshesFromDisk(void);
static void createLightsAndMaterials(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_UNKNOWN, 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;
  }
  meshes = SceneMeshesCreate(device);
  if (!meshes)
  {
    outputError("Could not create meshes container.");
    return false;
  }
  objects = ObjectModelsCreate(meshes);
  if (!objects)
  {
    outputError("Could not create objects container.");
    return false;
  }
  lights = SceneLightsCreate(device);
  if (!lights)
  {
    outputError("Could not create 3D scene lights module.");
    return false;
  }
  materials = ObjectMaterialsCreate();
  if (!materials)
  {
    outputError("Could not create object materials container.");
    return false;
  }

#if (defined(_MSC_VER) && defined(_M_ARM64)) || defined(__aarch64__)
  // Optimize for WoA devices
  if (ApplicationGetWindowScale(application) >= 2.0f)
    textureCabinet = TextureCabinetCreate(device, &displaySize, TEXTURE_FIDELITY_LOW, 0,
      PIXEL_FORMAT_UNKNOWN);
  else
    textureCabinet = TextureCabinetCreate(device, &displaySize, TEXTURE_FIDELITY_MEDIUM, 4,
      PIXEL_FORMAT_UNKNOWN);
#else
  textureCabinet = TextureCabinetCreate(device, &displaySize, TEXTURE_FIDELITY_HIGH, 8,
    PIXEL_FORMAT_UNKNOWN);
#endif

  if (!textureCabinet)
  {
    outputError("Could not create texture cabinet container.");
    return false;
  }
  sceneDepthNormals = SceneCreateDepthsNormals(device);
  if (!sceneDepthNormals || !SceneSetVertexElements(sceneDepthNormals, 0, vertexElements,
    vertexElementsCount))
  {
    outputError("Could not create depths/normals rendering scene.");
    return false;
  }
  sceneModeling = SceneCreateModeling(device);
  if (!sceneModeling || !SceneSetVertexElements(sceneModeling, 0, vertexElements,
    sizeof(vertexElements) / sizeof(VertexElement)))
  {
    outputError("Could not create 3D rendering scene.");
    return false;
  }
  colorDithering = ColorDitheringCreate(device);
  if (!colorDithering)
  {
    outputError("Could not create color dithering module.");
    return false;
  }
  viewport = ObjectModelsCreateView(objects);
  if (!viewport)
  {
    outputError("Could not create main viewport.");
    return false;
  }
  camera = ActorCameraCreate(&(Vector){0.0f, 0.0f, 0.0f},
    &(Vector){(float)M_PI * 0.25f, (float)M_PI * -0.25f, 0.0f}, 400.0f);
  if (!camera)
  {
    outputError("Could not create object camera.");
    return false;
  }
  // Limit camera position and rotation to reasonable values.
  ActorCameraSetConstraints(camera, &(CameraConstraints){
    .positionMin.x = -300.0f, .positionMax.x = 300.0f,
    .positionMin.y = -150.0f,
    .positionMin.z = -300.0f, .positionMax.z = 300.0f,
    .positionMin.w =  100.0f, .positionMax.w = 1500.0f,
    .rotationMin.x = (float)M_PI * 0.01f,
    .rotationMax.x = (float)M_PI * 0.5f});

  if (!loadMeshesFromDisk())
  {
    outputError("Could not load 3D meshes from disk.");
    return false;
  }
  createObjects();
  createLightsAndMaterials();
  return true;
}

void applicationDestroy(Application* application, void* user)
{
  ActorCameraDestroy(camera);
  ColorDitheringDestroy(colorDithering);
  SceneDestroy(sceneModeling);
  SceneDestroy(sceneDepthNormals);
  TextureCabinetDestroy(textureCabinet);
  ObjectMaterialsDestroy(materials);
  SceneLightsDestroy(lights);
  ObjectModelsDestroy(objects);
  SceneMeshesDestroy(meshes);
  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};

  bool const positionChanged = !vectorEquals(prevMousePos, mousePos);
  PointF const screenSize = pointF(displaySize);

  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:
    if (orthographic)
    {
      float const adjustedZoom = MAX(cameraZoom - 20.0f, 100);
      Matrix const adjustedProjection = matrixOrthogonalVOL(adjustedZoom,
        (adjustedZoom * displaySize.y) / displaySize.x, 1.0f, 1800.0f);

      ActorCameraZoomOrtho(camera, (PointF*)&mousePos, &screenSize, cameraZoom, adjustedZoom, &projection,
        &adjustedProjection);

      cameraZoom = adjustedZoom;
    }
    else
    {
      ActorCameraZoom(camera, (PointF*)&mousePos, &screenSize, &projection, -20.0f);
      cameraZoom = ActorCameraGetDistance(camera);
    }
    ApplicationInvalidate(application);
    break;

  case MOUSE_EVENT_WHEEL_DOWN:
    if (orthographic)
    {
      float const adjustedZoom = MIN(cameraZoom + 20.0f, 1500.0f);
      Matrix const adjustedProjection = matrixOrthogonalVOL(adjustedZoom,
        (adjustedZoom * displaySize.y) / displaySize.x, 1.0f, 1800.0f);

      ActorCameraZoomOrtho(camera, (PointF*)&mousePos, &screenSize, cameraZoom, adjustedZoom, &projection,
        &adjustedProjection);

      cameraZoom = adjustedZoom;
    }
    else
    {
      ActorCameraZoom(camera, (PointF*)&mousePos, &screenSize, &projection, 20.0f);
      cameraZoom = ActorCameraGetDistance(camera);
    }
    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_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)
  {
    orthographic = !orthographic;
    if (orthographic)
      ActorCameraSetDistance(camera, 400.0f);
    else
      ActorCameraSetDistance(camera, cameraZoom);

    // Reset camera position and rotation.
    ActorCameraSetPosition(camera, NULL);
    ActorCameraSetRotation(camera, &(Vector4){.x = (float)M_PI * 0.25f, .y = (float)M_PI * -0.25f});

    ApplicationInvalidate(application);
  }
  else if (key == KEY_SPACE && event == KEY_EVENT_PRESSED)
  {
    if (layers == 0x01)
    {
      ObjectModel* const object = ObjectModelsGetObjectByName(objects, "GroundFloorWalls");
      if (object)
        ObjectModelSetAttributes(object, ObjectModelGetAttributes(object) &
          ~OBJECT_MODEL_ATTRIBUTE_TRANSPARENT);

      layers = 0x03;
    }
    else if (layers == 0x03)
      layers = 0x07;
    else if (layers == 0x07)
    {
      ObjectModel* const object = ObjectModelsGetObjectByName(objects, "GroundFloorWalls");
      if (object)
        ObjectModelSetAttributes(object, ObjectModelGetAttributes(object) |
          OBJECT_MODEL_ATTRIBUTE_TRANSPARENT);

      layers = 0x01;
    }
    ApplicationInvalidate(application);
  }
  else if (key == KEY_UP && event == KEY_EVENT_PRESSED)
  {
    ObjectMaterial* material = ObjectMaterialsGetElement(materials, 2);
    material->frostedGlass = MIN(material->frostedGlass + 0.5f, 12.0f);
    ApplicationInvalidate(application);
  }
  else if (key == KEY_DOWN && event == KEY_EVENT_PRESSED)
  {
    ObjectMaterial* material = ObjectMaterialsGetElement(materials, 2);
    material->frostedGlass = MAX(material->frostedGlass - 0.5f, 0.0f);
    ApplicationInvalidate(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);
  }
}

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

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

  if (orthographic)
    projection = matrixOrthogonalVOL(cameraZoom, (cameraZoom * displaySize.y) / displaySize.x,
      10.0f, 1800.0f);
  else
    projection = matrixPerspectiveFOVY((float)M_PI * 0.25, (float)displaySize.x / displaySize.y,
      10.0f, 1800.0, depthClipNegative);

  ObjectModelViewSetView(viewport, &view);
  ObjectModelViewSetProjection(viewport, &projection, depthClipNegative);

  // Update objects according to visible layers.
  ObjectModelViewSetLayers(viewport, layers);
  ObjectModelViewUpdate(viewport);

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

  #if (defined(_MSC_VER) && defined(_M_ARM64)) || defined(__aarch64__)
    // Use an approximated OIT on WoA devices
    attributes = (attributes & ~TEXTURE_CABINET_ATTRIBUTE_GLASSY) | TEXTURE_CABINET_ATTRIBUTE_GLASSY_FAST;
  #endif

    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.radius = 32.0f;
    parameters.kernelBias = 0.05f;
    parameters.strength = 1.0f;

    TextureCabinetSetAmbientOcclusionParameters(textureCabinet, &parameters);
  }

  // Perform light assignment for 3D scene.
  SceneLightsSetViewSize(lights, &displaySize);

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

  SceneLightsExecute(lights, &view, &projection);

  // Prepare scene's attributes.
  SceneAttributes const attributes =
    // Note: "normals" attribute is not enabled because objects have flat normals anyway.
    // SCENE_ATTRIBUTE_NORMALS |
    SCENE_ATTRIBUTE_DEPTH_PREPASS |
    SCENE_ATTRIBUTE_COLORING;

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

    if (SceneBegin(sceneDepthNormals))
    {
      renderObjectsOpaque(sceneDepthNormals, viewport);
      SceneEnd(sceneDepthNormals);
    }
    TextureCabinetEnd(textureCabinet);
  }
  TextureCabinetFilter(textureCabinet, TEXTURE_CABINET_FILTER_TYPE_OCCLUSION, &projection);

  // Render main scene.
  FloatColor const clearColor = floatColor(0xFF303030u);
  TextureCabinetClear(textureCabinet, TEXTURE_CABINET_PASS_COLOR, &clearColor);

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

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

    if (ScenePrepare(sceneModeling) && SceneBegin(sceneModeling))
    {
      renderObjectsOpaque(sceneModeling, viewport);
      SceneEnd(sceneModeling);
    }
    TextureCabinetEnd(textureCabinet);
  }
  TextureCabinetFilter(textureCabinet, TEXTURE_CABINET_FILTER_TYPE_BLOOM, NULL);

  // Render semi-transparent objects.
  if (TextureCabinetClear(textureCabinet, TEXTURE_CABINET_PASS_GLASSY, NULL) &&
    TextureCabinetBegin(textureCabinet, TEXTURE_CABINET_PASS_GLASSY))
  {
    SceneSetAttributes(sceneModeling, attributes | SCENE_ATTRIBUTE_GLASSY);

    if (SceneBegin(sceneModeling))
    {
      renderObjectsGlassy(sceneModeling, viewport);
      SceneEnd(sceneModeling);
    }
    TextureCabinetEnd(textureCabinet);
  }
  TextureCabinetFilter(textureCabinet, TEXTURE_CABINET_FILTER_TYPE_GLASSY, NULL);

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

    // Note: on lower quality panels, using COLOR_DITHERING_FORMAT_RGB6 would lead to better results.
    ColorDitheringSetFormat(colorDithering, COLOR_DITHERING_FORMAT_RGB8);
    ColorDitheringExecute(colorDithering, TextureCabinetGetFinalTexture(textureCabinet));

    // Comment above two lines and uncomment the following to render without dithering:
    // 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.fillOpacity = 1.0f;
        parameters.effect.fillBrightness = 0.0f;
        parameters.effect.borderBrightness = 1.0f;
        parameters.effect.borderOpacity = 0.75f;
        parameters.effect.borderThickness = 1.0f;
        parameters.effect.borderType = FONT_BORDER_SEMI_HEAVY;
        parameters.effect.shadowBrightness = 0.85f;
        parameters.effect.shadowOpacity = 0.4f;
        parameters.effect.shadowDistance = (PointF){2.0f, 2.0f};
        parameters.effect.shadowSmoothness = 2;
        TextRendererSetFontParameters(textRenderer, &parameters);

        TextRendererDrawAligned(textRenderer, &(PointF){displaySize.x - 8.0f * displayScale,
          4.0f * displayScale},
          "Space - switch layers, Enter - orthographic, Up/Down - adjust 'frosted glass' effect.",
          &(ColorPair){0xFFA5E7B8u, 0xFF77FFCEu}, TEXT_ALIGNMENT_END, TEXT_ALIGNMENT_START,
          1.0f, true, NULL);

        TextRendererDrawAligned(textRenderer, &(PointF){truncf(displaySize.x * 0.5f), displaySize.y -
          4.0f * displayScale}, "Left mouse button: rotate scene, right button: move scene, wheel: zoom.",
          &(ColorPair){0xFFA5E7B8u, 0xFF77FFCEu}, 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";
        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);
      }
      CanvasEnd(canvas);
    }
    SwapChainEnd(swapChain);
  }
}

void renderObjectsOpaque(Scene* scene, ObjectModelView* view)
{
  for (int32_t i = 0; i < ObjectModelViewGetObjectCount(view); ++i)
  {
    ObjectModel* const object = ObjectModelViewGetObject(view, i);
    if (!object)
      continue;

    SceneMesh* const mesh = ObjectModelGetMesh(object);
    if (!mesh)
      continue;

    if (ObjectModelGetAttributes(object) & OBJECT_MODEL_ATTRIBUTE_TRANSPARENT)
      continue;

    Matrix globalModel;
    ObjectModelGetTransform(object, MODEL_TRANSFORM_GLOBAL_MODEL, &globalModel);

    SceneSetWorld(scene, &globalModel);

    ObjectMaterial* const objectMaterial = ObjectMaterialsGetElement(materials,
      ObjectModelGetMaterial(object));
    if (objectMaterial)
    {
      FloatColor color;
      ObjectModelGetColor(object, &color);

      ObjectMaterial material = *objectMaterial;
      material.ambientColor = floatColorMultiplyRGB(material.ambientColor, floatColorToRGB(color));
      material.albedoColor = floatColorMultiply(material.albedoColor, color);
      SceneSetMaterial(scene, &material);
    }

    MeshModel* const model = SceneMeshGetModel(mesh);
    if (model)
      MeshModelDraw(model, SceneGetProgram(scene), PRIMITIVE_TOPOLOGY_TRIANGLES, 0, 0, 0, true);
  }
}

void renderObjectsGlassy(Scene* scene, ObjectModelView* view)
{
  for (int32_t i = 0; i < ObjectModelViewGetObjectCount(view); ++i)
  {
    ObjectModel* const object = ObjectModelViewGetObject(view, i);
    if (!object || !(ObjectModelGetAttributes(object) & OBJECT_MODEL_ATTRIBUTE_TRANSPARENT))
      continue;

    SceneMesh* const mesh = ObjectModelGetMesh(object);
    if (!mesh)
      continue;

    Matrix globalModel;
    ObjectModelGetTransform(object, MODEL_TRANSFORM_GLOBAL_MODEL, &globalModel);

    SceneSetWorld(scene, &globalModel);

    ObjectMaterial* const objectMaterial = ObjectMaterialsGetElement(materials,
      ObjectModelGetMaterial(object));
    if (objectMaterial)
    {
      FloatColor color;
      ObjectModelGetColor(object, &color);

      color.alpha = 0.4f;

      ObjectMaterial material = *objectMaterial;
      material.ambientColor = floatColorMultiplyRGB(material.ambientColor, floatColorToRGB(color));
      material.albedoColor = floatColorMultiply(material.albedoColor, color);
      SceneSetMaterial(scene, &material);
    }

    MeshModel* const model = SceneMeshGetModel(mesh);
    if (model)
      MeshModelDraw(model, SceneGetProgram(scene), PRIMITIVE_TOPOLOGY_TRIANGLES, 0, 0, 0, true);
  }
}

void createObjects(void)
{
  ObjectModel* object = ObjectModelsAdd(objects, "HouseFloor", NULL, OBJECT_MODEL_ATTRIBUTE_VISIBLE |
    OBJECT_MODEL_ATTRIBUTE_SELECTABLE | OBJECT_MODEL_ATTRIBUTE_HIERARCHYLESS);
  if (object)
  {
    ObjectModelSetMeshName(object, "HouseFloor");
    ObjectModelSetMaterial(object, 0);

    ObjectModelSetLayers(object, 0x01);
  }
  object = ObjectModelsAdd(objects, "GroundFloor", NULL, OBJECT_MODEL_ATTRIBUTE_VISIBLE |
    OBJECT_MODEL_ATTRIBUTE_SELECTABLE);
  if (object)
  {
    ObjectModelSetMeshName(object, "GroundFloorNoWalls");
    ObjectModelSetMaterial(object, 1);

    ObjectModelSetLayers(object, 0x01);
  }
  object = ObjectModelsAdd(objects, "GroundFloorWalls", NULL, OBJECT_MODEL_ATTRIBUTE_VISIBLE |
    OBJECT_MODEL_ATTRIBUTE_SELECTABLE | OBJECT_MODEL_ATTRIBUTE_TRANSPARENT);
  if (object)
  {
    ObjectModelSetMeshName(object, "GroundFloorWalls");
    ObjectModelSetMaterial(object, 2);

    Matrix const transform = matrixTranslate((Vector){0.55f, 0.0f, -71.6f});
    ObjectModelSetTransform(object, MODEL_TRANSFORM_LOCAL, &transform);

    ObjectModelSetLayers(object, 0x01);
  }
  object = ObjectModelsAdd(objects, "TopFloor", NULL, OBJECT_MODEL_ATTRIBUTE_VISIBLE |
    OBJECT_MODEL_ATTRIBUTE_SELECTABLE);
  if (object)
  {
    ObjectModelSetMeshName(object, "TopFloorNoWalls");
    ObjectModelSetMaterial(object, 1);

    Matrix const transform = matrixTranslate((Vector){0.0f, 15.5f, -280.0});
    ObjectModelSetTransform(object, MODEL_TRANSFORM_LOCAL, &transform);

    ObjectModelSetLayers(object, 0x02);
  }
  object = ObjectModelsAdd(objects, "TopFloorDivision", NULL, OBJECT_MODEL_ATTRIBUTE_VISIBLE |
    OBJECT_MODEL_ATTRIBUTE_SELECTABLE);
  if (object)
  {
    ObjectModelSetMeshName(object, "TopFloorDivision");
    ObjectModelSetMaterial(object, 0);

    Matrix const transform = matrixTranslate((Vector){0.0f, 15.5f, -280.0f});
    ObjectModelSetTransform(object, MODEL_TRANSFORM_LOCAL, &transform);

    ObjectModelSetLayers(object, 0x02);
  }
  object = ObjectModelsAdd(objects, "TopFloorWalls", NULL, OBJECT_MODEL_ATTRIBUTE_VISIBLE |
    OBJECT_MODEL_ATTRIBUTE_SELECTABLE | OBJECT_MODEL_ATTRIBUTE_TRANSPARENT);
  if (object)
  {
    ObjectModelSetMeshName(object, "TopFloorWalls");
    ObjectModelSetMaterial(object, 2);

    Matrix const transform = matrixTranslate((Vector){0.0f, 96.0f, -280.0f});
    ObjectModelSetTransform(object, MODEL_TRANSFORM_LOCAL, &transform);

    ObjectModelSetLayers(object, 0x02);
  }
  object = ObjectModelsAdd(objects, "HouseWalls", NULL, OBJECT_MODEL_ATTRIBUTE_VISIBLE |
    OBJECT_MODEL_ATTRIBUTE_SELECTABLE);
  if (object)
  {
    ObjectModelSetMeshName(object, "HouseWalls");
    ObjectModelSetMaterial(object, 1);

    ObjectModelSetLayers(object, 0x04);
  }
}

bool loadMeshesFromDisk(void)
{
  bool loaded = true;

  static char const* const basePath = "../../../../Media/House/";
  static char const* const meshNames[] = {"GroundFloorNoWalls", "GroundFloorWalls", "TopFloorNoWalls",
    "TopFloorWalls", "TopFloorDivision", "HouseWalls", "HouseFloor"};

  for (size_t i = 0; i < sizeof(meshNames) / sizeof(char const*); ++i)
  {
    char fileName[128] = {0};

    strcpy(fileName, basePath);
    strcat(fileName, meshNames[i]);
    strcat(fileName, ".obj");

    uint32_t options = MESH_LOADING_OPTION_MATERIAL_VERTEX_COLOR;

    if (!SceneMeshesAddFromFile(meshes, meshNames[i], fileName, NULL, vertexElements, vertexElementsCount,
      0, 0, &options, 20.0f, NULL, NULL, NULL, NULL))
    {
      loaded = false;
      break;
    }
  }
  return loaded;
}

void createLightsAndMaterials(void)
{
  SceneLight* const light = SceneLightsAdd(lights);
  if (light)
  {
    light->ambientColor = floatColorRGB(0xFFFFFFu);
    light->albedoColor = floatColorRGB(0xFFFFFFu);
    light->specularColor = floatColorRGB(0xFFFFFFu);
    light->specularExponent = 1.0f;
    light->intensity = 1.0f;
    light->position = lightPosition;
    light->shadowCaster = 0;
  }

  ObjectMaterial* material = ObjectMaterialsAdd(materials);
  if (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->occlusion = 1.0f;
    material->technique = TECHNIQUE_LIGHTING_PHONG;
  }

  material = ObjectMaterialsAdd(materials);
  if (material)
  { // #1
    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->occlusion = 1.0f;
    material->technique = TECHNIQUE_LIGHTING_PHONG;
  }

  material = ObjectMaterialsAdd(materials);
  if (material)
  { // #2
    material->ambientColor = (FloatColorRGB){0.25f, 0.25f, 0.25f};
    material->albedoColor = (FloatColor){0.5f, 0.5f, 0.5f, 1.0f};
    material->specularColor = (FloatColorRGB){0.25f, 0.25f, 0.25f};
    material->specularExponent = 16.0f;
    material->roughness = (Vector){0.3f, 1.2f, 0.4f};
    material->technique = TECHNIQUE_LIGHTING_MINNAERT;
    material->frostedGlass = 4.0f;
  }
}

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

#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: Architecture 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;
}