/*
 * This file is part of Afterwarp Framework (http://afterwarp.io).
 * Copyright (c) 2015 - 2023 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.
 */

#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 Timer* timer = NULL;

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

static TextureCabinet* textureCabinet = NULL;

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

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

static Vector bridgeSize = {0};
static float bridgeLength = 0.0f;

static float velocity = 0.0f;
static int32_t direction = 0;

static int32_t objectsVisible = 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 = -400.f, .y = 800.0f, .z = 400.0f};

// Forward declarations.

static void updateMovement(float latency);
static void renderObjects(Scene* scene, ObjectModelView* view);
static void updateBridges(void);
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_UNKNOWN, PIXEL_FORMAT_UNKNOWN, 0, false);
  if (!swapChain)
  {
    outputError("Could not create rendering swap-chain.");
    return false;
  }
  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, TEXTURE_FIDELITY_HIGH, 8,
    PIXEL_FORMAT_UNKNOWN);
  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;
  }
  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;
  }
  viewport = ObjectModelsCreateView(objects);
  if (!viewport)
  {
    outputError("Could not create main viewport.");
    return false;
  }
  camera = ObjectCameraCreate(&(Vector){0.0f, 0.0f, 0.0f},
    &(Vector){-(float)M_PI * 0.05, 0.0f, (float)M_PI * 0.5f}, 400.0f);
  if (!camera)
  {
    outputError("Could not create object camera.");
    return false;
  }
/*  ObjectCameraSetConstraints(camera, &(ObjectCameraConstraints){
    .minPosition.y = -600.0f, .maxPosition.y = -425.0f,
    .minPosition.z = -20.0f, .maxPosition.z = 20.0f,
    .maxRotation.y = -(float)M_PI * 0.01f,
    .minRotation.y = -(float)M_PI * 0.5f,
    .minDistance = 100.0f, .maxDistance = 1500.0f});*/

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

void applicationDestroy(Application* application, void* user)
{
  ObjectCameraDestroy(camera);
  TimerDestroy(timer);
  SpatialFogDestroy(spatialFog);
  SceneDestroy(sceneModeling);
  SceneDestroy(sceneDepthNormals);
  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);
}

void applicationMouse(Application* application, MouseEvent event, MouseButton button, Point const* position,
  void* user)
{
  PointF const mousePos = pointF(*position);
  PointF const touchSize = pointF(displaySize);
  PointF const touch = pointDivideF(mousePos, touchSize);

  switch (event)
  {
  case MOUSE_EVENT_MOUSE_DOWN:
    if (button == MOUSE_BUTTON_LEFT)
      ObjectCameraStartRotation(camera, &touch);
    else if (button == MOUSE_BUTTON_RIGHT)
      ObjectCameraStartMovement(camera, &touch);
    break;

  case MOUSE_EVENT_MOUSE_MOVE:
    ObjectCameraUpdate(camera, &touch);
    break;

  case MOUSE_EVENT_MOUSE_UP:
    ObjectCameraStop(camera);
    break;

  case MOUSE_EVENT_WHEEL_UP:
    ObjectCameraZoom(camera, &mousePos, &touchSize, &projection, -20.0f);
    break;

  case MOUSE_EVENT_WHEEL_DOWN:
    ObjectCameraZoom(camera, &mousePos, &touchSize, &projection, 20.0f);
    break;
  }
}

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

  if (key == KEY_UP || key == 'w')
  {
    if (direction == 0 && event == KEY_EVENT_PRESSED)
      direction = 1;
    else if (direction == 1 && event == KEY_EVENT_RELEASED)
      direction = 0;
  }
  else if (key == KEY_DOWN || key == 's')
  {
    if (direction == 0 && event == KEY_EVENT_PRESSED)
      direction = -1;
    else if (direction == -1 && event == KEY_EVENT_RELEASED)
      direction = 0;
  }
}

void applicationRender(Application* application, void* user)
{
  static float const farPlane = 4000.0f;
  float const latency = (float)(TimerGetLatency(timer) / (1000.0 * 1000.0));

  // Prepare view and projection matrices.
  bool const depthClipNegative = DeviceGetBehavior(device) & DEVICE_BEHAVIOR_DEPTH_CLIP_NEGATIVE;

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

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

  // Set view/projection matrices and update visible objects.
  ObjectModelViewSetView(viewport, &view);
  ObjectModelViewSetProjection(viewport, &projection, depthClipNegative);
  ObjectModelViewUpdate(viewport);

  { // Update texture cabinet's attributes.
    TextureCabinetAttributes attributes =
      TEXTURE_CABINET_ATTRIBUTE_BLOOM |
      TEXTURE_CABINET_ATTRIBUTE_BLOOM_CUBIC |
      TEXTURE_CABINET_ATTRIBUTE_AMBIENT_OCCLUSION |
      TEXTURE_CABINET_ATTRIBUTE_GLASSY |
      TEXTURE_CABINET_ATTRIBUTE_GLASSY_FOG |
      TEXTURE_CABINET_ATTRIBUTE_LINEAR_DEPTHS;

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

    TextureCabinetSetAttributes(textureCabinet, attributes);

    // Adjust tone-mapping bloom parameters for HDR rendering.
/*    ToneMappingBloom toneMappingBloom = {0};
    TextureCabinetGetToneMappingBloom(textureCabinet, &toneMappingBloom);
    toneMappingBloom.toneWhite = 2.0f;
    toneMappingBloom.bloomThreshold = 1.5f;
    TextureCabinetSetToneMappingBloom(textureCabinet, &toneMappingBloom);*/

    // Adjust ambient occlusion parameters.
    AmbientOcclusionParameters parameters = {0};
    TextureCabinetGetAmbientOcclusionParameters(textureCabinet, &parameters);
//    parameters.radius = 4.0f;
    parameters.kernelBias = 0.1f;
    TextureCabinetSetAmbientOcclusionParameters(textureCabinet, &parameters);
  }

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

  // Render depth pre-pass.
  TextureCabinetClear(textureCabinet, TEXTURE_CABINET_PASS_DEPTH, &(FloatColor){farPlane});

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

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

  // While depth pre-pass is being rendered, synchronize light sources with lamppost positions and move
  // bridges during movement.
  updateMovement(latency);
  updateBridges();

  // Perform light assignment for 3D scene.
  SceneLightsSetViewSize(lights, &displaySize);
  SceneLightsSetCullingMode(lights, CLUSTERS_CULLING_MODE_QUALITY);
  SceneLightsSetClusterSize(lights, 32);
  SceneLightsSetDepthSlices(lights, 24);
  SceneLightsExecute(lights, &view, &projection);

  // Render main scene.
  FloatColor const clearColor = floatColor(0xFF404040u);
  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);
    SceneSetTexture(sceneModeling, TextureCabinetGetTexture(textureCabinet,
      TEXTURE_CABINET_TYPE_AMBIENT_OCCLUSION_FILTERED), SCENE_TEXTURE_TYPE_OCCLUSION);

    if (ScenePrepare(sceneModeling) && SceneBegin(sceneModeling))
    {
      renderObjects(sceneModeling, viewport);
      SceneEnd(sceneModeling);
    }
    TextureCabinetEnd(textureCabinet);
  }

  { // Apply fog.
    float const fade = 2.0f;

    FogParameters parameters = {0};
    parameters.color = (FloatColor){0.75f * fade, 0.85f * fade, 1.0f * fade, 0.0f};
    parameters.opacity = 1.0f;
    parameters.groundPlane = (Vector4){0.0f, 1.0f, 0.0f, -600.0f};
    parameters.scattering = 1.0f;
    parameters.extinction = 0.0003f;
    parameters.densityGround = 0.03f;
    SpatialFogSetParameters(spatialFog, &parameters);

    SpatialFogSetFormula(spatialFog, FOG_FORMULA_EXPONENTIAL);
    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 application's window.
  if (SwapChainBegin(swapChain))
  {
    FloatColor const clearColor = floatColor(0xFF404040u);
    DeviceClear(device, DEVICE_CLEAR_LAYER_COLOR, &clearColor, 1.0f, 0);

    TextureCabinetPresent(textureCabinet);

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

      FontParameters parameters = {
        .family = "Arial", .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);

      TextRendererDrawAligned(textRenderer, &(PointF){displaySize.x * 0.5f, 4.0f * displayScale},
        "Up/Down or W/S keys - move camera along the bridge.",
        &(ColorPair){0xFFA5E7B8u, 0xFF77FFCEu}, TEXT_ALIGNMENT_MIDDLE, TEXT_ALIGNMENT_START,
        0.75f, true, NULL);

      TextRendererDrawAligned(textRenderer, &(PointF){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,
        0.75f, true, NULL);

      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), "Frame Rate: %d", TimerGetFrameRate(timer));

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

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

      TextRendererDrawAligned(textRenderer, &(PointF){displaySize.x - 12.0f * displayScale,
        4.0f * displayScale}, text, &COLOR_PAIR(0xA0FFFFFFu), TEXT_ALIGNMENT_END,
        TEXT_ALIGNMENT_START, 1.0f, true, NULL);

      snprintf(text, sizeof(text), "Active Lights: %d", SceneLightsGetCount(lights));

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

      snprintf(text, sizeof(text), "Visible Objects: %d", objectsVisible);

      TextRendererDrawAligned(textRenderer, &(PointF){displaySize.x - 12.0f * displayScale,
        28.0f * displayScale}, text, &COLOR_PAIR(0xA0FFFFFFu), TEXT_ALIGNMENT_END,
        TEXT_ALIGNMENT_START, 1.0f, true, NULL);

      CanvasEnd(canvas);
    }
    SwapChainEnd(swapChain);
  }
  TimerExecute(timer, true, false);
}

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

void updateMovement(float latency)
{
  if (direction == 1)
    velocity = MIN(velocity + latency, 1.0f);
  else if (direction == -1)
    velocity = MAX(velocity - latency, -1.0f);
  else if (velocity > 0.0f)
    velocity = MAX(velocity - latency, 0.0f);
  else
    velocity = MIN(velocity + latency, 0.0f);

  if (fabsf(velocity) > vectorEpsilon)
  {
    Vector position;
    ObjectCameraGetPosition(camera, &position);

    position.x += velocity * latency * 100.0f;
    ObjectCameraSetPosition(camera, &position);

    SceneLight* light = SceneLightsGetElement(lights, 0);
    light->position.x = lightPosition.x - position.x;
  }
}

void renderObjects(Scene* scene, ObjectModelView* view)
{
  objectsVisible = 0;

  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;

    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);
      material.emissiveColor = floatColorMultiplyRGB(material.emissiveColor, floatColorToRGB(color));
      SceneSetMaterial(scene, &material);
    }

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

    ++objectsVisible;
  }
}

void updateBridges(void)
{
  Vector cameraPos;
  ObjectCameraGetPosition(camera, &cameraPos);

  ObjectModelsIterator* const iterator = ObjectModelsIteratorCreate(objects);
  if (iterator)
  {
    while (ObjectModelsIteratorNext(iterator))
    {
      ObjectModel* const object = ObjectModelsIteratorGet(iterator);

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

      ObjectModelGetMeshName(object, name, &length);

      if (!strcmp(name, "Bridge"))
      {
        Matrix transform;
        ObjectModelGetTransform(object, MODEL_TRANSFORM_LOCAL, &transform);

        float position = transform.data[3][0] + cameraPos.x;

        if (velocity > vectorEpsilon && position > bridgeLength * 4.0f)
        {
          transform = matrixMultiply(transform, matrixTranslate((Vector){-bridgeLength * 8.0, 0.0f, 0.0f}));
          ObjectModelSetTransform(object, MODEL_TRANSFORM_LOCAL, &transform);
        }
        else if (velocity < -vectorEpsilon && position < -bridgeLength * 4.0f)
        {
          transform = matrixMultiply(transform, matrixTranslate((Vector){bridgeLength * 8.0, 0.0f, 0.0f}));
          ObjectModelSetTransform(object, MODEL_TRANSFORM_LOCAL, &transform);
        }
      }
    }
    ObjectModelsIteratorDestroy(iterator);
  }
}

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

    Matrix const transform = matrixScale((Vector){8000.0f, 1.0f, 8000.0f});
    ObjectModelSetTransform(floor, MODEL_TRANSFORM_LOCAL, &transform);
  }
  for (int32_t bridgeIndex = -4; bridgeIndex < 4; ++bridgeIndex)
  {
    ObjectModel* const bridge = ObjectModelsAdd(objects, NULL, NULL);
    if (bridge)
    {
      ObjectModelSetMeshName(bridge, "PowerLines");
      ObjectModelSetMaterial(bridge, 1);
      ObjectModelSetAttributes(bridge, OBJECT_MODEL_ATTRIBUTE_VISIBLE | OBJECT_MODEL_ATTRIBUTE_SELECTABLE |
        OBJECT_MODEL_ATTRIBUTE_HIERARCHYLESS);

      if (vectorEmpty(bridgeSize))
      {
        ObjectModelGetSize(bridge, &bridgeSize);
        bridgeLength = bridgeSize.x - 0.3f;
      }

      Matrix const transform = matrixTranslate((Vector){bridgeLength * bridgeIndex, 0.0f, 0.0f});
      ObjectModelSetTransform(bridge, MODEL_TRANSFORM_LOCAL, &transform);
    }
  }
}

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

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

    created = SceneMeshesAddFromBuffer(meshes, "Plane", meshBuffer, NULL, NULL, NULL, vertexElements,
      vertexElementsCount, 0, 1.0f);
  }
  if (created)
  { // Load meshes from disk.
    static char const* const basePath = "../../../../Media/PowerLines/";
    static char const* const meshNames[] = {"PowerLines"};

    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, ".mesh");

      if (!SceneMeshesAddFromBinaryFile(meshes, meshNames[i], fileName, NULL, vertexElements,
        vertexElementsCount, 0, 30.0f))
      {
        created = false;
        break;
      }
    }
  }
  return created;
}

void createLightsAndMaterials(void)
{
  SceneLight* 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;
  }

  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.75f;
    material->occlusion = 1.0f;
    material->technique = TECHNIQUE_LIGHTING_PHONG;
  }

  material = ObjectMaterialsAdd(materials);
  if (material)
  { // #1 (structures)
    material->ambientColor = (FloatColorRGB){0.15f, 0.15f, 0.15f};
    material->albedoColor = (FloatColor){1.0f, 1.0f, 1.0f, 0.5f};
    material->specularColor = (FloatColorRGB){0.25f, 0.25f, 0.25f};
    material->specularExponent = 16.0f;
    material->shadows = 0.75f;
    material->occlusion = 1.0f;
    material->roughness = (Vector){0.9f, 0.7f, 0.2f};
    material->technique = TECHNIQUE_LIGHTING_COOK_TORRANCE;
  }

  material = ObjectMaterialsAdd(materials);
  if (material)
  { // #2 (lamp)
    material->ambientColor = (FloatColorRGB){0.0f, 0.0f, 0.0f};
    material->albedoColor = (FloatColor){0.0f, 0.0f, 0.0f, 1.0f};
    material->specularColor = (FloatColorRGB){0.0f, 0.0f, 0.0f};
    material->emissiveColor = (FloatColorRGB){1000.0f * 2.0f, 950.0f * 2.0f, 850.0f * 2.0f};

    material->shadows = 0.0f;
    material->occlusion = 0.0f;
    material->technique = TECHNIQUE_LIGHTING_PHONG;
  }
}

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

#else

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

#endif

  ApplicationConfiguration configuration = {0};
  configuration.size = displaySize;
#ifdef _WIN32
  configuration.startup.win.handleInstance = instance;
  configuration.startup.win.handleIcon = LoadIcon(instance, MAKEINTRESOURCE(IDI_NIGHTBRIDGE));

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

  if (displayScale > 1.0f + vectorEpsilon)
    ApplicationSetClientSize(application,
      &(Point){(int32_t)(displaySize.x * displayScale), (int32_t)(displaySize.y * displayScale)});

  ApplicationSetTitle(application, "Afterwarp: Night Bridge Demo");

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