/*
 * 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.
//  * Object selection, which uses object's bounding boxes and accompanying voxel representation to
//    determine accurately what object is being pointed at, including approximate 3D location that mouse
//    is pointing on the object's surface.
//  * Hierarchial object structure, where some objects are parented to other objects.
//  * Grapher module for visualizing object's bounding box.

#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 Grapher* grapher = NULL;

static SceneMeshes* meshes = NULL;
static ObjectModels* objects = NULL;
static SceneLights* lights = NULL;
static ObjectMaterials* materials = NULL;

static TextureCabinet* textureCabinet = NULL;
static ShadowCastingAtlas* shadowCastingAtlas = NULL;
static ObjectModelView* shadowCasterView = NULL;

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

static ObjectModelView* viewport = NULL;
static ActorCamera* camera = NULL;
static Matrix projection = {0};
static ObjectModel* selected = NULL;
static Vector mousePos = {0};

static bool shadowMapNeedsUpdate = true;

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 createAndLoadMeshes(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_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;
  }
  grapher = GrapherCreate(device);
  if (!grapher)
  {
    outputError("Could not create grapher 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;
  }
  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;
  }
  shadowCastingAtlas = ShadowCastingAtlasCreate(device, &(Point){256, 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))
  {
    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;
  }
  viewport = ObjectModelsCreateView(objects);
  if (!viewport)
  {
    outputError("Could not create main viewport.");
    return false;
  }
  { // Create and configure a shadow caster.
    bool const depthClipNegative = DeviceGetBehavior(device) & DEVICE_BEHAVIOR_DEPTH_CLIP_NEGATIVE;

    ShadowCaster* const caster = ShadowCastingAtlasAdd(shadowCastingAtlas, &(Point){256, 256}, true);
    if (!caster)
    {
      outputError("Could not create a shadow caster.");
      return false;
    }
    shadowCasterView = ObjectModelsCreateView(objects);
    if (!shadowCasterView)
    {
      outputError("Could not create a view for shadow caster.");
      return false;
    }
    Matrix const view = matrixLookAt(lightPosition, vectorZero, vectorAxisY);
    Matrix const projection = matrixPerspectiveFOVY((float)M_PI * 0.25f, 1.0f, 100.0f, 1400.0f,
      depthClipNegative);

    ObjectModelViewSetView(shadowCasterView, &view);
    ObjectModelViewSetProjection(shadowCasterView, &projection, depthClipNegative);
  }
  camera = ActorCameraCreate(&(Vector){0.0f, 0.0f, -40.0f},
    &(Vector){(float)M_PI * 0.25f, (float)M_PI * -0.25f, 0.0f}, 440.0f);
  if (!camera)
  {
    outputError("Could not create object camera.");
    return false;
  }
  ActorCameraSetConstraints(camera, &(CameraConstraints){
    .positionMin.x = -200.0f, .positionMin.y = -100.0f, .positionMin.z = -200.0f, .positionMin.w =  100.0f,
    .positionMax.x =  200.0f, .positionMax.y =  100.0f, .positionMax.z =  200.0f, .positionMax.w = 1500.0f,
    .rotationMin.x = (float)M_PI * 0.1f, .rotationMax.x = (float)M_PI * 0.4f});

  if (!createAndLoadMeshes())
  {
    outputError("Could not create and/or load 3D meshes.");
    return false;
  }
  createObjects();
  createLightsAndMaterials();
  return true;
}

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

  Matrix view = {0};
  ActorCameraGetView(camera, &view);

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

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

  if (ObjectModelViewUpdate(viewport))
  {
    PointF const targetSize = pointF(displaySize);
    Matrix const viewInverse = matrixInverse(view);
    Vector origin, direction;

    RayCreate(&origin, &direction, (PointF*)&mousePos, &targetSize, &viewInverse, &projection);
    selected = ObjectModelViewSelect(viewport, &origin, &direction, NULL);
  }

  { // 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 (displayScale >= 2.0f)
      attributes |=
        TEXTURE_CABINET_ATTRIBUTE_BLOOM_DOWNSCALE |
        TEXTURE_CABINET_ATTRIBUTE_AMBIENT_OCCLUSION_DOWNSCALE;

    TextureCabinetSetAttributes(textureCabinet, attributes);
  }

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

  if (!SceneLightsExecute(lights, &view, &projection))
    printf("Error updating scene lights.\n");

  // Render shadow maps
  if (shadowMapNeedsUpdate)
  {
    ShadowCaster* const shadowCaster = ShadowCastingAtlasGetCaster(shadowCastingAtlas, 0);

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

        if (SceneBegin(sceneDepthNormals))
        {
          renderObjectsOpaque(sceneDepthNormals, shadowCasterView);
          SceneEnd(sceneDepthNormals);
        }
        ShadowCasterEnd(shadowCaster);
        ShadowCasterFilter(shadowCaster);
      }
    }
    shadowMapNeedsUpdate = false;
  }

  // Prepare scene's attributes.
  SceneAttributes const attributes =
    SCENE_ATTRIBUTE_NORMALS |
    SCENE_ATTRIBUTE_COLORING |
    SCENE_ATTRIBUTE_SHADOWS_CUBIC |
    SCENE_ATTRIBUTE_SINGLE_PASS;

  // Render main scene.
  FloatColor const clearColor = floatColor(0xFFE0E0E0u);
  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);
    SceneSetShadowCastingAtlas(sceneModeling, shadowCastingAtlas);
    SceneSetTextureCabinet(sceneModeling, textureCabinet);

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

  // Render object selection.
  if (selected && TextureCabinetBegin(textureCabinet, TEXTURE_CABINET_PASS_COLOR))
  {
    Matrix const viewProjection = matrixMultiply(view, projection);
    GrapherSetTransform(grapher, &viewProjection);

    Matrix globalVolume;
    ObjectModelGetTransform(selected, MODEL_TRANSFORM_GLOBAL_VOLUME, &globalVolume);

    if (GrapherBegin(grapher))
    {
      RenderingState renderState;
      DeviceGetRenderingState(device, &renderState);

      GrapherBoundingBox(grapher, &globalVolume, 0xFFFFFFFFu, 6.0f, 0.5f, LINE_CAPS_BUTT);
      GrapherFlush(grapher);

      // Enable depth bias to prevent depth fighting between bounding boxes having same depths.
      renderState.depthBias = 5.0f;
      renderState.slopeDepthBias = 5.0f;
      DeviceSetRenderingState(device, &renderState);

      GrapherBoundingBox(grapher, &globalVolume, 0xFF000000u, 3.0f, 0.5f, LINE_CAPS_BUTT);
      GrapherEnd(grapher);
    }
    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);

    TextureCabinetPresent(textureCabinet);

    if (CanvasBegin(canvas))
    {
      // Draw information text.
      CanvasSetContextState(canvas, CANVAS_CONTEXT_STATE_FLAT_TEXT);
      CanvasSetSamplerState(canvas, &(CanvasSamplerState){TEXTURE_FILTER_NEAREST, TEXTURE_FILTER_NEAREST});
      {
        FontParameters parameters = {
          .family = "Segoe UI", .size = 16.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 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:
    ActorCameraZoom(camera, (PointF*)&mousePos, &screenSize, &projection, -20.0f);
    ApplicationInvalidate(application);
    break;

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

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) &&
      (scene != sceneDepthNormals))
      continue; // Do not render transparent objects, except for shadow mapping.

    char name[128] = {0};
    int32_t nameLength = sizeof(name);

    ObjectModelGetName(object, name, &nameLength);

    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)
    {
      RenderingState initialState;
      DeviceGetRenderingState(device, &initialState);

      if (!strcmp(name, "Table"))
      {
        RenderingState renderState = initialState;
        renderState.cullFace = TRIANGLE_FACE_NONE;
        DeviceSetRenderingState(device, &renderState);
      }
      MeshModelDraw(model, SceneGetProgram(scene), PRIMITIVE_TOPOLOGY_TRIANGLES, 0, 0, 0, true);
      DeviceSetRenderingState(device, &initialState);
    }
  }
}

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.75f;

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

bool createAndLoadMeshes(void)
{
  bool created = false;
  { // Create geometric meshes.
    MeshBuffer* meshBuffer = MeshBufferCreate();
    if (!meshBuffer)
      return false;

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

    created = SceneMeshesAddFromBuffer(meshes, "Plane", meshBuffer, NULL, vertexElements,
      vertexElementsCount, NULL, NULL, 0, 0, 1.0f);

    MeshBufferDestroy(meshBuffer);
  }
  if (created)
  { // Load meshes from disk.
    static char const* const basePath = "../../../../Media/Office/";
    static char const* const meshNames[] = {"Chair", "Table", "TableGlass", "FlowerPot", "Laptop",
      "WineGlass"};

    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, 8.0f, NULL, NULL, NULL, NULL))
      {
        created = false;
        break;
      }
    }
  }
  return created;
}

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

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

    FloatColor const color = floatColor(0xFFE0E0E0u);
    ObjectModelSetColor(object, &color);
  }
  ObjectModel* const table = ObjectModelsAdd(objects, "Table", NULL, OBJECT_MODEL_ATTRIBUTE_VISIBLE |
    OBJECT_MODEL_ATTRIBUTE_SELECTABLE);
  Vector tableSize = {0};
  if (table)
  {
    ObjectModelSetMeshName(table, "Table");
    ObjectModelSetMaterial(table, 4);
    ObjectModelGetSize(table, &tableSize);
  }
  object = ObjectModelsAdd(objects, "TableGlass", NULL, OBJECT_MODEL_ATTRIBUTE_SELECTABLE |
    OBJECT_MODEL_ATTRIBUTE_TRANSPARENT | OBJECT_MODEL_ATTRIBUTE_VISIBLE);
  if (object)
  {
    ObjectModelSetParent(object, table);
    ObjectModelSetMeshName(object, "TableGlass");
    ObjectModelSetMaterial(object, 3);

    Matrix const transform = matrixTranslate((Vector){0.0f, 65.5f, 0.0f});
    ObjectModelSetTransform(object, MODEL_TRANSFORM_LOCAL, &transform);
  }
  object = ObjectModelsAdd(objects, "FlowerPot", NULL, OBJECT_MODEL_ATTRIBUTE_VISIBLE |
    OBJECT_MODEL_ATTRIBUTE_SELECTABLE);
  if (object)
  {
    ObjectModelSetParent(object, table);
    ObjectModelSetMeshName(object, "FlowerPot");
    ObjectModelSetMaterial(object, 1);

    Matrix const transform = matrixTranslate((Vector){0.0f, tableSize.y, -50.0f});
    ObjectModelSetTransform(object, MODEL_TRANSFORM_LOCAL, &transform);
  }
  object = ObjectModelsAdd(objects, "Laptop", NULL, OBJECT_MODEL_ATTRIBUTE_VISIBLE |
    OBJECT_MODEL_ATTRIBUTE_SELECTABLE);
  if (object)
  {
    ObjectModelSetParent(object, table);
    ObjectModelSetMeshName(object, "Laptop");
    ObjectModelSetMaterial(object, 1);

    Matrix const transform = matrixTranslate((Vector){36.0f, tableSize.y, 120.0f});
    ObjectModelSetTransform(object, MODEL_TRANSFORM_LOCAL, &transform);
  }
  for (int8_t i = 0; i < 10; ++i)
  {
    char objectName[64];
    snprintf(objectName, sizeof(objectName), "Chair%d", i + 1);

    object = ObjectModelsAdd(objects, objectName, NULL, OBJECT_MODEL_ATTRIBUTE_VISIBLE |
      OBJECT_MODEL_ATTRIBUTE_SELECTABLE);
    if (object)
    {
      static float const chairFront = 80.0f;
      static float const chairSpacing = 80.0f;
      static float const sideSpacing = 210.0f;

      ObjectModelSetMeshName(object, "Chair");
      ObjectModelSetMaterial(object, 1);

      if (i < 8)
      {
        int8_t const index = i % 4;
        int8_t const side = i / 4;

        Matrix const transform = matrixMultiply(
          matrixRotateY((float)((3 + side * 2) * M_PI * 0.5)),
          matrixTranslate((Vector){chairFront - chairFront * side * 2.0f, 0.0f,
            (index - 1.5f) * chairSpacing}));

        ObjectModelSetTransform(object, MODEL_TRANSFORM_LOCAL, &transform);
      }
      else
      {
        int8_t const side = (i - 8) % 2;

        Matrix const transform = matrixMultiply(
          matrixRotateY((float)((2 - side) * M_PI)),
          matrixTranslate((Vector){0.0f, 0.0f, (side - 0.5f) * sideSpacing * 2.0f}));

        ObjectModelSetTransform(object, MODEL_TRANSFORM_LOCAL, &transform);
      }
    }
  }
  for (int8_t i = 0; i < 10; ++i)
  {
    char objectName[64];
    snprintf(objectName, sizeof(objectName), "WineGlass%d", i + 1);

    object = ObjectModelsAdd(objects, objectName, NULL, OBJECT_MODEL_ATTRIBUTE_SELECTABLE |
      OBJECT_MODEL_ATTRIBUTE_TRANSPARENT | OBJECT_MODEL_ATTRIBUTE_VISIBLE);
    if (object)
    {
      static float const glassFront = 40.0f;
      static float const glassSpacing = 80.0f;
      static float const sideSpacing = 170.0f;

      ObjectModelSetMeshName(object, "WineGlass");
      ObjectModelSetMaterial(object, 2);

      if (i < 8)
      {
        int8_t const index = i % 4;
        int8_t const side = i / 4;

        Vector position;
        position.x = glassFront - glassFront * side * 2.0f;
        position.y = tableSize.y;
        position.z = (index - 1.5f) * glassSpacing;

        if (i == 3)
          position.z += 30.0f;

        Matrix const transform = matrixTranslate(position);
        ObjectModelSetTransform(object, MODEL_TRANSFORM_LOCAL, &transform);
      }
      else
      {
        int8_t const side = (i - 8) % 2;

        Matrix const transform = matrixTranslate((Vector){
          0.0f, tableSize.y, (side - 0.5f) * sideSpacing * 2.0f});

        ObjectModelSetTransform(object, MODEL_TRANSFORM_LOCAL, &transform);
      }
    }
  }
}

void createLightsAndMaterials(void)
{
  SceneLight* const light = SceneLightsAdd(lights);
  if (light)
  {
    light->ambientColor = floatColorRGB(0xFFFFFFu);
    light->albedoColor = floatColorRGB(0xFFFFFFu);
    light->specularColor = floatColorRGB(0xFFFFFFu);
    light->specularExponent = 4.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->shadows = 0.5f;
    material->occlusion = 0.5f;
    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.5f};
    material->specularColor = (FloatColorRGB){1.0f, 1.0f, 1.0f};
    material->specularExponent = 4.0f;
    material->shadows = 0.5f;
    material->occlusion = 0.5f;
    material->technique = TECHNIQUE_LIGHTING_PHONG;
  }

  material = ObjectMaterialsAdd(materials);
  if (material)
  { // #2
    material->ambientColor = (FloatColorRGB){0.5f, 0.5f, 0.5f};
    material->albedoColor = (FloatColor){1.0f, 1.0f, 1.0f, 0.25f};
    material->specularColor = (FloatColorRGB){1.0f, 1.0f, 1.0f};
    material->specularExponent = 16.0f;
    material->shadows = 0.0f;
    material->occlusion = 0.0f;
    material->technique = TECHNIQUE_LIGHTING_PHONG;
    material->frostedGlass = 4.0f;
  }

  material = ObjectMaterialsAdd(materials);
  if (material)
  { // #3
    material->ambientColor = (FloatColorRGB){0.25f, 0.25f, 0.25f};
    material->albedoColor = (FloatColor){1.0f, 1.0f, 1.0f, 0.5f};
    material->specularColor = (FloatColorRGB){1.0f, 1.0f, 1.0f};
    material->specularExponent = 4.0f;
    material->shadows = 0.75f;
    material->occlusion = 0.0f;
    material->technique = TECHNIQUE_LIGHTING_PHONG;
    material->frostedGlass = 3.0f;
  }

  material = ObjectMaterialsAdd(materials);
  if (material)
  { // #4
    material->ambientColor = (FloatColorRGB){0.25f, 0.25f, 0.25f};
    material->albedoColor = (FloatColor){1.0f, 1.0f, 1.0f, 0.5f};
    material->specularColor = (FloatColorRGB){0.5f, 0.5f, 0.5f};
    material->specularExponent = 4.0f;
    material->shadows = 0.5f;
    material->occlusion = 0.0f;
    material->technique = TECHNIQUE_LIGHTING_MINNAERT;
  }
}

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

  SetProcessDPIAware();

  char const* windowClassName = "AfterwarpObjectPickingDemo";

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

  displayScale = ApplicationGetWindowScale(application);
  ApplicationSetTitle(application, "Afterwarp: ObjectPicking 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;
}