/*
 * 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:
//  * Parallax Mapping with optional super-sampling to produce realistic looking surfaces using a
//    combination of normal map, displacement map and ambient occlusion map textures. The example shows
//    the difference between different techniques.
//  * 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 types.

// Type of rendering technique.
typedef enum RenderingTechnique
{
  // Textured plane.
  RENDERING_TECHNIQUE_NORMAL,

  // Textured plane with bump-mapping.
  // This uses texture and normal map.
  RENDERING_TECHNIQUE_BUMP_MAPPING,

  // Textured plane with bump-mapping and parallax mapping.
  // This uses texture, normal, displacement and ambient occlusion maps.
  RENDERING_TECHNIQUE_PARALLAX_MAPPING,

  // Textured plane with bump-mapping, parallax mapping and super-sampling.
  // This uses texture, normal, displacement and ambient occlusion maps.
  RENDERING_TECHNIQUE_PARALLAX_MAPPING_SUPER_SAMPLE
} RenderingTechnique;

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

// Modules for 3D scene rendering.
static SceneLights* lights = NULL;
static TextureCabinet* textureCabinet = NULL;
static Scene* sceneDepthNormals = NULL;
static Scene* sceneModeling = NULL;
static MeshModel* modelPlane = NULL;

static Texture* textureAlbedo = NULL;
static Texture* textureNormalMap = NULL;
static Texture* textureNormalAmbientMap = NULL;
static Texture* textureParallaxMap = NULL;

static ActorCamera* camera = NULL;
static int keyAlt = 0;

// World, view and projection matrices.
static Matrix world = {0}, view = {0}, projection = {0};

// Type of rendering technique used.
static RenderingTechnique renderingTechnique = RENDERING_TECHNIQUE_PARALLAX_MAPPING_SUPER_SAMPLE;

// The following controls whether to perform depth pre-pass.
static bool depthPrePass = true;

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

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

// Forward declarations.

static void renderPlane(Scene* scene, SceneAttributes attributes);
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;
  }
  timer = TimerCreate();
  if (!textRenderer)
  {
    outputError("Could not create timer module.");
    return false;
  }
  lights = SceneLightsCreate(device);
  if (!lights)
  {
    outputError("Could not create 3D scene lights module.");
    return false;
  }
  textureCabinet = TextureCabinetCreate(device, &displaySize, TEXTURE_FIDELITY_MEDIUM, 4, PIXEL_FORMAT_D32F);
  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;
  }
  // Configure parallax mapping parameters.
  SceneSetParallaxMappingParameters(sceneModeling, &(ParallaxMappingParameters){0.1f, 8, 32, 1.0f});

  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.1f,
    .rotationMax.x = (float)M_PI * 0.5f});

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

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

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

    MeshBufferDestroy(meshBuffer);

    if (!modelPlane)
    {
      outputError("Could not create geometry objects.");
      return false;
    }
  }
  // Load textures.
  textureAlbedo = TextureCreateFromFile(device, "../../../../Media/Textures/StoneWall-Albedo.jpg",
    PIXEL_FORMAT_RGBA8, TEXTURE_ATTRIBUTE_MIPMAPPING);

  textureNormalMap = TextureCreateFromFile(device, "../../../../Media/Textures/StoneWall-NormalMap.jpg",
    PIXEL_FORMAT_RGBA8, TEXTURE_ATTRIBUTE_MIPMAPPING);

  textureNormalAmbientMap = TextureCreateFromFileNormalsAndOcclusion(device,
    "../../../../Media/Textures/StoneWall-NormalMap.jpg",
    "../../../../Media/Textures/StoneWall-Ambient.jpg",
    TEXTURE_ATTRIBUTE_MIPMAPPING);

  textureParallaxMap = TextureCreateFromFileParallax(device,
    "../../../../Media/Textures/StoneWall-ParallaxMap.png",
    PIXEL_FORMAT_L8, TEXTURE_ATTRIBUTE_MIPMAPPING);

  if (!textureAlbedo || !textureNormalMap ||!textureNormalAmbientMap || !textureParallaxMap)
  {
    outputError("Could not load surface textures from disk.");
    return false;
  }
  // Create a light source for illuminating the surface.
  SceneLight* const light = SceneLightsAdd(lights);
  if (light)
  {
    light->ambientColor = floatColorRGB(0xFFFFFFu);
    light->albedoColor = floatColorRGB(0xFFFFFFu);
    light->specularColor = floatColorRGB(0xFFFFFFu);
    light->position = (Vector){.x = 100.f, .y = 400.0f, .z = -100.0f};
  }
  return true;
}

void applicationDestroy(Application* application, void* user)
{
  TextureDestroy(textureParallaxMap);
  TextureDestroy(textureNormalAmbientMap);
  TextureDestroy(textureNormalMap);
  TextureDestroy(textureAlbedo);
  MeshModelDestroy(modelPlane);
  ActorCameraDestroy(camera);
  SceneDestroy(sceneModeling);
  SceneDestroy(sceneDepthNormals);
  TextureCabinetDestroy(textureCabinet);
  SceneLightsDestroy(lights);
  TimerDestroy(timer);
  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 applicationRender(Application* application, void* user)
{
  bool const depthClipNegative = DeviceGetBehavior(device) & DEVICE_BEHAVIOR_DEPTH_CLIP_NEGATIVE;
  displayScale = ApplicationGetWindowScale(application);

  // Configure matrices.
  world = matrixTranslate((Vector){0.0f, -0.01f, 0.0f});
  ActorCameraGetView(camera, &view);
  projection = matrixPerspectiveFOVY((float)M_PI * 0.25f, (float)displaySize.x / displaySize.y,
    10.0f, 2000.0, depthClipNegative);

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

    if (displayScale >= 2.0f)
      attributes |= TEXTURE_CABINET_ATTRIBUTE_BLOOM_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);

  SceneLightsExecute(lights, &view, &projection);

  // Prepare scene's attributes.
  SceneAttributes const attributes = SCENE_ATTRIBUTE_NORMALS;

  // Make a depth pre-pass. This step is usually necessary for more advanced effects and to reduce
  // rendering overhead during main scene, especially when many light sources are present.
  // Note: rendering scene without a depth pre-pass is also possible, and it even allows effects such
  // as ambient occlusion to be performed, but this feature is highly experimental and thus should be
  // used only when it is indispensable to do so (e.g. the geometry is so complex that it is costly to
  // render it more than once).
  if (depthPrePass)
  {
    TextureCabinetClear(textureCabinet, TEXTURE_CABINET_PASS_DEPTH, NULL);

    if (TextureCabinetBegin(textureCabinet, TEXTURE_CABINET_PASS_DEPTH))
    {
      renderPlane(sceneDepthNormals, attributes | SCENE_ATTRIBUTE_DEPTH_PREPASS);
      TextureCabinetEnd(textureCabinet);
    }
    TextureCabinetFilter(textureCabinet, TEXTURE_CABINET_FILTER_TYPE_OCCLUSION, &projection);
  }
  // Render main scene.
  TextureCabinetClear(textureCabinet, TEXTURE_CABINET_PASS_COLOR, &(FloatColor){0.0f, 0.0f, 0.0f, 1.0f});

  if (TextureCabinetBegin(textureCabinet, TEXTURE_CABINET_PASS_COLOR))
  {
    SceneSetLights(sceneModeling, lights);
    SceneSetTexture(sceneModeling, textureAlbedo, SCENE_TEXTURE_TYPE_ALBEDO);

    if (renderingTechnique >= RENDERING_TECHNIQUE_PARALLAX_MAPPING)
      SceneSetTexture(sceneModeling, textureNormalAmbientMap, SCENE_TEXTURE_TYPE_NORMAL_MAP);
    else if (renderingTechnique >= RENDERING_TECHNIQUE_BUMP_MAPPING)
      SceneSetTexture(sceneModeling, textureNormalMap, SCENE_TEXTURE_TYPE_NORMAL_MAP);
    else
      SceneSetTexture(sceneModeling, NULL, SCENE_TEXTURE_TYPE_NORMAL_MAP);

    if (renderingTechnique >= RENDERING_TECHNIQUE_PARALLAX_MAPPING)
      SceneSetTexture(sceneModeling, textureParallaxMap, SCENE_TEXTURE_TYPE_PARALLAX_MAP);
    else
      SceneSetTexture(sceneModeling, NULL, SCENE_TEXTURE_TYPE_PARALLAX_MAP);

    if (renderingTechnique >= RENDERING_TECHNIQUE_PARALLAX_MAPPING_SUPER_SAMPLE)
    { // Use super-sampling to provide antialiasing of parallax occlusion map.
      // Note: this is rather GPU-intensive.
      RenderingState renderingState;
      DeviceGetRenderingState(device, &renderingState);
      renderingState.states |= DEVICE_STATE_PER_SAMPLE_SHADING;
      DeviceSetRenderingState(device, &renderingState);
    }
    renderPlane(sceneModeling, attributes);

    TextureCabinetEnd(textureCabinet);
  }
  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.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 * 0.5f, 4.0f * displayScale},
          "Press Space to change between techniques",
          &(ColorPair){0xFFA5E7B8u, 0xFF77FFCEu}, TEXT_ALIGNMENT_MIDDLE, TEXT_ALIGNMENT_START,
          1.0f, true, NULL);

        char const* text = NULL;

        switch (renderingTechnique)
        {
        case RENDERING_TECHNIQUE_NORMAL:
          text = "Current technique: Normal (textured plane)";
          break;

        case RENDERING_TECHNIQUE_BUMP_MAPPING:
          text = "Current technique: Bump-Mapping";
          break;

        case RENDERING_TECHNIQUE_PARALLAX_MAPPING:
          text = "Current technique: Parallax Mapping";
          break;

        default:
          text = "Current technique: Parallax Mapping with Super-Sampling";
          break;
        }

        TextRendererDrawAligned(textRenderer, &(PointF){displaySize.x * 0.5f, displaySize.y -
          4.0f * displayScale}, text, &(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,
          &(ColorPair){0xFF40E0FFu, 0xFF2020FFu}, 1.0f, NULL);

        snprintf(text, sizeof(text), "Frame Rate: %1.0f", TimerGetFrameRate(timer));

        TextRendererDrawAligned(textRenderer,
          &(PointF){displaySize.x - 8.0f * displayScale, displaySize.y - 6.0f * displayScale}, text,
          &(ColorPair){0xFF40E0FFu, 0xFF2020FFu}, TEXT_ALIGNMENT_END, TEXT_ALIGNMENT_END, 1.0f, false, NULL);
      }
      CanvasEnd(canvas);
    }
    SwapChainEnd(swapChain);
  }
  TimerUpdate(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, -20.0f);
    ApplicationInvalidate(application);
    break;

  case MOUSE_EVENT_WHEEL_DOWN:
    ActorCameraZoom(camera, (PointF*)&mousePos, &screenSize, &projection, 20.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_SPACE && event == KEY_EVENT_PRESSED)
  {
    renderingTechnique = (renderingTechnique + 1) % 4;
    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 renderPlane(Scene* scene, SceneAttributes attributes)
{
  SceneSetAttributes(scene, attributes);
  SceneSetView(scene, &view);
  SceneSetProjection(scene, &projection);

  // Note: the following call is not required for depth/normals scene, so it will fail during depth pre-pass,
  // but this an expected behavior.
  ScenePrepare(scene);

  if (SceneBegin(scene))
  {
    ObjectMaterial material = {0};
    material.ambientColor = floatColorRGB(0x303030u);
    material.albedoColor = floatColor(0xFFFFFFFFu);
    material.specularColor = floatColorRGB(0x404040u);
    material.specularExponent = 16.0;
    material.technique = TECHNIQUE_LIGHTING_MINNAERT;
    material.roughness = (Vector){0.8f, 1.0f, 0.8f};

    // Note: depths/normals scene does not use material, so the following call would fail during rendering
    // of depth pre-pass, but this is an expected behavior.
    SceneSetMaterial(scene, &material);

    SceneSetWorld(scene, &world);
    MeshModelDraw(modelPlane, SceneGetProgram(scene), PRIMITIVE_TOPOLOGY_TRIANGLES, 0, 0, 0, true);

    SceneEnd(scene);
  }
}

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

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