// Copyright (c) 2022 Dominic Masters
// 
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT

#include "RenderPipeline.hpp"
#include "game/DawnGame.hpp"
#include "scene/components/scene/SubSceneController.hpp"

#if DAWN_DEBUG_BUILD
  #include "scene/debug/SceneDebugLine.hpp"
#endif

using namespace Dawn;

RenderPipeline::RenderPipeline(RenderManager *renderManager) {
  assertNotNull(renderManager, "RenderPipeline::RenderPipeline: RenderManager cannot be null");
  this->renderManager = renderManager;
}

void RenderPipeline::init() {
  this->shaderBuffer.init();
}

void RenderPipeline::render() {
  if(this->renderManager->game->scene != nullptr) {
    this->renderScene(this->renderManager->game->scene);
  }
}

void RenderPipeline::renderScene(Scene *scene) {
  assertNotNull(scene, "RenderPipeline::renderScene: Scene cannot be null");

  // Render subscenes first.
  auto subSceneControllers = scene->findComponents<SubSceneController>();
  auto itSubScene = subSceneControllers.begin();
  while(itSubScene != subSceneControllers.end()) {
    Scene *subScene = (Scene *)((*itSubScene)->subScene);
    if(subScene == nullptr) {
      ++itSubScene;
      continue;
    }

    if((*itSubScene)->onlyUpdateUnpaused && scene->game->timeManager.isPaused) {
      ++itSubScene;
      continue;
    }

    this->renderScene(subScene);
    ++itSubScene;
  }

  // Now render backbuffers.
  auto backBuffer = this->renderManager->getBackBuffer();
  auto cameras = scene->findComponents<Camera>();
  Camera *backBufferCamera = nullptr;

  // First, render all non-backbuffer cameras.
  auto it = cameras.begin();
  while(it != cameras.end()) {
    RenderTarget *cameraTarget = (*it)->getRenderTarget();

    // Leave the backbuffer camera(s) to last, so we skip them. we do this so
    // that children framebuffers contain the CURRENT frame, not LAST frame.
    if(cameraTarget == nullptr) {
      ++it;
      continue;
    } else if(cameraTarget == backBuffer) {
      backBufferCamera = *it;
    } else {
      this->renderSceneCamera(scene, *it);
    }

    ++it;
  }

  // Now render the backbuffer camera.
  if(backBufferCamera == nullptr) return;
  this->renderSceneCamera(scene, backBufferCamera);
}

void RenderPipeline::renderSceneCamera(Scene *scene, Camera *camera) {
  std::vector<struct ShaderPassItem>::iterator itPassItem;

  assertNotNull(scene, "RenderPipeline::renderSceneCamera: Scene cannot be null");
  assertNotNull(camera, "RenderPipeline::renderSceneCamera: Camera cannot be null");

  // Create a new render ID. Long story short this is a really dirty way of 
  // not sending parameters to shaders more than we need.
  this->renderId--;

  // Get the render target.
  auto renderTarget = camera->getRenderTarget();
  assertNotNull(renderTarget, "RenderPipeline::renderSceneCamera: Camera must have a render target");

  // Update shader parameter buffers with current knowledge
  struct RenderPipelineShaderBufferData shaderBufferData;
  shaderBufferData.projection = camera->getProjection();
  shaderBufferData.view = camera->transform->getWorldTransform();
  this->shaderBuffer.buffer(&shaderBufferData);

  // Prepare a render context. This is just a nice way of letting renderables 
  // know a bit about what is happening, they can choose to use this data to
  // render themselves differently.
  struct IRenderableContext context = {
    scene,
    camera,
    this
  };

  // Get the list of things to render first.
  std::vector<struct ShaderPassItem> shaderPassItems;

  // Renderables
  auto renderables = scene->findComponents<IRenderable>();
  auto itRenderables = renderables.begin();
  while(itRenderables != renderables.end()) {
    vectorAppend(&shaderPassItems, (*itRenderables)->getRenderPasses(context));
    ++itRenderables; 
  }

  // Debug Lines
  #if DAWN_DEBUG_BUILD
    Mesh lineMesh;
    if(scene->debugLines.size() > 0) {
      int32_t lineIndex = 0;
      lineMesh.createBuffers(
        scene->debugLines.size() * SCENE_DEBUG_LINE_VERTICE_COUNT,
        scene->debugLines.size() * SCENE_DEBUG_LINE_INDICE_COUNT
      );
      auto itDebugLine = scene->debugLines.begin();
      while(itDebugLine != scene->debugLines.end()) {
        auto item = itDebugLine->createShaderItem(
          &lineMesh,
          &lineIndex,
          camera,
          this->renderManager->simpleTexturedShader
        );
        shaderPassItems.push_back(item);
        itDebugLine = scene->debugLines.erase(itDebugLine);
        lineIndex++;
      }
    }
  #endif

  // Now we've queued everything, let's sort the rendering queue by the priority
  std::sort(
    shaderPassItems.begin(),
    shaderPassItems.end(),
    [](struct ShaderPassItem &a, struct ShaderPassItem  &b) {
      if(a.priority == b.priority) {
        return a.w < b.w;
      }
      return a.priority < b.priority;
    }
  );

  // Now we've sorted everything! Let's actually start rendering.
  Shader *boundShader = nullptr;
  std::map<textureslot_t, Texture*> boundTextures;
  std::map<shaderbufferlocation_t, shaderbufferslot_t> boundBuffers;
  shaderbufferslot_t slot;
  
  // TODO: This will be editable!
  renderTarget->bind();
  renderTarget->clear(
    RENDER_TARGET_CLEAR_FLAG_DEPTH |
    RENDER_TARGET_CLEAR_FLAG_COLOR
  );

  // Shader items
  itPassItem = shaderPassItems.begin();
  while(itPassItem != shaderPassItems.end()) {
    auto item = *itPassItem;

    // Bind the program.
    if(boundShader != item.shader) {
      boundShader = item.shader;
      boundShader->bind();
    }

    // Bind the textures to the slots
    auto itTextureSlot = item.textureSlots.begin();
    while(itTextureSlot != item.textureSlots.end()) {
      // Assert texture isn't null, just don't include it.
      assertNotNull(itTextureSlot->second, "RenderPipeline::renderSceneCamera: Texture cannot be null (omit texture instead)");

      if(boundTextures[itTextureSlot->first] != itTextureSlot->second) {
        itTextureSlot->second->bind(itTextureSlot->first);
        boundTextures[itTextureSlot->first] = itTextureSlot->second;
      }
      ++itTextureSlot;
    }

    // Bind the buffers to their slots
    slot = 0;
    auto itBufferSlot = item.parameterBuffers.begin();
    while(itBufferSlot != item.parameterBuffers.end()) {
      auto location = itBufferSlot->first;
      auto buff = itBufferSlot->second;
      boundBuffers[itBufferSlot->first] = slot;
      buff->bind(slot);
      slot++;
      ++itBufferSlot;
    }

    // Now set each of the parameters. Nothing exciting here.
    auto itColors = item.colorValues.begin();
    while(itColors != item.colorValues.end()) {
      item.shader->setColor(itColors->first, itColors->second);
      ++itColors;
    }

    auto itBool = item.boolValues.begin();
    while(itBool != item.boolValues.end()) {
      item.shader->setBoolean(itBool->first, itBool->second);
      ++itBool;
    }

    auto itMat = item.matrixValues.begin();
    while(itMat != item.matrixValues.end()) {
      item.shader->setMatrix(itMat->first, itMat->second);
      ++itMat;
    }

    auto itVec3 = item.vec3Values.begin();
    while(itVec3 != item.vec3Values.end()) {
      item.shader->setVector3(itVec3->first, itVec3->second);
      ++itVec3;
    }

    auto itText = item.textureValues.begin();
    while(itText != item.textureValues.end()) {
      item.shader->setTexture(itText->first, itText->second);
      ++itText;
    }

    auto itBuffer = item.parameterBuffers.begin();
    while(itBuffer != item.parameterBuffers.end()) {
      item.shader->setParameterBuffer(itBuffer->first, boundBuffers[itBuffer->first]);
      ++itBuffer;
    }

    auto itFloat = item.floatValues.begin();
    while(itFloat != item.floatValues.end()) {
      item.shader->setFloat(itFloat->first, itFloat->second);
      ++itFloat;
    }

    // Set Render flags
    this->renderManager->setRenderFlags(item.renderFlags);

    // Thank god that's done, now just draw the damn mesh.
    item.mesh->draw(item.drawMode, item.start, item.count);
    ++itPassItem;
  }
}

RenderPipeline::~RenderPipeline() {
}