/*
 * 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:
//  * Drawing a simple textured 3D cube with ambient occlusion (not really visible as the cube is a very
//    flat and simple) and a bloom effect, with a depth pre-pass. This illustrates how the rendering
//    pipeline generally works.

#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 = {800, 600};
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* modelCube = NULL;
static Texture* textureCube = NULL;

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

// Timing ticks that are incremented 60 times per second.
static int32_t ticks = 0;

// 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},
  {"vertexTexCoord", ELEMENT_FORMAT_FLOAT, 2, 0, 0}};

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

// Forward declarations.

static void renderCube(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_LOW, 4, PIXEL_FORMAT_D16);
  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;
  }
  { // Create model of a 3D cube.
    MeshBuffer* meshBuffer = MeshBufferCreate();
    if (!meshBuffer)
    {
      outputError("Could not create mesh buffer object.");
      return false;
    }

    MeshBufferCube(meshBuffer, 1, 1, 1, NULL, &(Vector){100.0f, 0.0f, 0.0f},
      &(Vector){0.0f, 100.0f, 0.0f}, &(Vector){0.0f, 0.0f, 100.0f}, NULL, NULL, NULL, true);

    // Replace above statement with the following to create a 3D sphere instead.
    // MeshBufferSuperEllipse(meshBuffer, 32, 32, NULL, &(Vector){75.0f, 75.0f, 75.0f}, 0.0f,
    //   (float)M_PI * 2.0f, 1.0f, (float)M_PI * -0.5f, (float)M_PI * 0.5f, 1.0f, NULL, NULL, NULL, true);

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

    MeshBufferDestroy(meshBuffer);

    if (!modelCube)
    {
      outputError("Could not create geometry objects.");
      return false;
    }
  }
  textureCube = TextureCreateFromFile(device, "../../../../Media/ForkLift.png", PIXEL_FORMAT_RGBA8,
    TEXTURE_ATTRIBUTE_MIPMAPPING);
  if (!textureCube)
  {
    outputError("Could not load cube texture from disk.");
    return false;
  }
  // Create a light source for illuminating the cube.
  SceneLight* const light = SceneLightsAdd(lights);
  if (light)
  {
    light->ambientColor = floatColorRGB(0xFFFFFFu);
    light->albedoColor = floatColorRGB(0xFFFFFFu);
    light->specularColor = floatColorRGB(0xFFFFFFu);
    light->position = (Vector){.x = 300.f, .y = 800.0f, .z = -300.0f};
  }

  view = matrixLookAt((Vector){0.0f, 50.0f, -300.0f}, vectorZero, vectorAxisY);

  return true;
}

void applicationDestroy(Application* application, void* user)
{
  TextureDestroy(textureCube);
  MeshModelDestroy(modelCube);
  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 world and projection matrix.
  world = matrixMultiply(matrixRotateY(ticks * 0.022f), matrixRotateX(ticks * 0.0371f));
  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))
    {
      renderCube(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.25f, 0.25f, 0.25f, 1.0f});

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

    renderCube(sceneModeling, attributes);

    TextureCabinetEnd(textureCabinet);
  }
  TextureCabinetFilter(textureCabinet, TEXTURE_CABINET_FILTER_TYPE_BLOOM, NULL);

  // Note: for semi-transparent objects, it is possible to do a "glassy" (order-independent transparency)
  // rendering pass here. To see how it is done, please check other examples.

  // 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 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 - 4.0f * displayScale}, text,
        &(ColorPair){0xFF40E0FFu, 0xFF2020FFu}, TEXT_ALIGNMENT_END, TEXT_ALIGNMENT_END, 1.0f, false, NULL);

      CanvasEnd(canvas);
    }
    SwapChainEnd(swapChain);
  }
  TimerUpdate(timer);
  ticks += TimerExtractTokens(timer);
}

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

void renderCube(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(0x404040u);
    material.albedoColor = floatColor(0xFFFFFFFFu);
    material.specularColor = floatColorRGB(0xFFFFFFu);
    material.specularExponent = 16.0;

    // 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(modelCube, 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 = "AfterwarpCubeDemo";

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

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