//
//  KRLight.cpp
//  Kraken Engine
//
//  Copyright 2026 Kearwood Gilbert. All rights reserved.
//  
//  Redistribution and use in source and binary forms, with or without modification, are
//  permitted provided that the following conditions are met:
//  
//  1. Redistributions of source code must retain the above copyright notice, this list of
//  conditions and the following disclaimer.
//  
//  2. Redistributions in binary form must reproduce the above copyright notice, this list
//  of conditions and the following disclaimer in the documentation and/or other materials
//  provided with the distribution.
//  
//  THIS SOFTWARE IS PROVIDED BY KEARWOOD GILBERT ''AS IS'' AND ANY EXPRESS OR IMPLIED
//  WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
//  FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL KEARWOOD GILBERT OR
//  CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
//  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
//  SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
//  ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
//  NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
//  ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//  
//  The views and conclusions contained in the software and documentation are those of the
//  authors and should not be interpreted as representing official policies, either expressed
//  or implied, of Kearwood Gilbert.
//


#include "KREngine-common.h"
#include "KRLight.h"

#include "KRNode.h"
#include "KRCamera.h"
#include "KRContext.h"

#include "KRPipelineManager.h"
#include "KRPipeline.h"
#include "KRDirectionalLight.h"
#include "KRSpotLight.h"
#include "KRPointLight.h"
#include "KRRenderPass.h"

using namespace hydra;

/* static */
void KRLight::InitNodeInfo(KrNodeInfo* nodeInfo)
{
  KRNode::InitNodeInfo(nodeInfo);
  nodeInfo->light.casts_shadow = decltype(m_casts_shadow)::defaultVal;
  nodeInfo->light.color = decltype(m_color)::defaultVal;
  nodeInfo->light.decay_start = decltype(m_decayStart)::defaultVal;
  nodeInfo->light.dust_particle_density = decltype(m_dust_particle_density)::defaultVal;
  nodeInfo->light.dust_particle_intensity = decltype(m_dust_particle_density)::defaultVal;
  nodeInfo->light.dust_particle_size = decltype(m_dust_particle_size)::defaultVal;
  nodeInfo->light.flare_occlusion_size = decltype(m_flareOcclusionSize)::defaultVal;
  nodeInfo->light.flare_size = decltype(m_flareSize)::defaultVal;
  nodeInfo->light.flare_texture = -1;
  nodeInfo->light.intensity = decltype(m_intensity)::defaultVal;
  nodeInfo->light.light_shafts = decltype(m_light_shafts)::defaultVal;
}

KRLight::KRLight(KRScene& scene, std::string name)
  : KRNode(scene, name)
{
  m_occlusionQuery = 0;

  // Initialize shadow buffers
  m_cShadowBuffers = 0;
  for (int iBuffer = 0; iBuffer < KRENGINE_MAX_SHADOW_BUFFERS; iBuffer++) {
    shadowFramebuffer[iBuffer] = 0;
    shadowDepthTexture[iBuffer] = 0;
    shadowValid[iBuffer] = false;
  }
}

KRLight::~KRLight()
{
  if (m_occlusionQuery) {
    GLDEBUG(glDeleteQueriesEXT(1, &m_occlusionQuery));
    m_occlusionQuery = 0;
  }
  allocateShadowBuffers(0);
}

tinyxml2::XMLElement* KRLight::saveXML(tinyxml2::XMLNode* parent)
{
  tinyxml2::XMLElement* e = KRNode::saveXML(parent);
  m_intensity.save(e);
  m_color.save(e);
  m_decayStart.save(e);
  m_flareSize.save(e);
  m_flareOcclusionSize.save(e);
  m_casts_shadow.save(e);
  m_light_shafts.save(e);
  m_dust_particle_density.save(e);
  m_dust_particle_size.save(e);
  m_dust_particle_intensity.save(e);
  m_flareTexture.save(e);
  return e;
}

void KRLight::loadXML(tinyxml2::XMLElement* e)
{
  KRNode::loadXML(e);

  m_color.load(e);
  m_intensity.load(e);
  m_decayStart.load(e);
  m_flareSize.load(e);
  m_flareOcclusionSize.load(e);
  m_casts_shadow.load(e);
  m_light_shafts.load(e);
  m_dust_particle_density.load(e);
  m_dust_particle_size.load(e);
  m_dust_particle_intensity.load(e);
  m_flareTexture.load(e);
}

void KRLight::setFlareTexture(std::string flare_texture)
{
  m_flareTexture.val.set(flare_texture);
}

void KRLight::setFlareSize(float flare_size)
{
  m_flareSize = flare_size;
}

void KRLight::setFlareOcclusionSize(float occlusion_size)
{
  m_flareOcclusionSize = occlusion_size;
}

void KRLight::setIntensity(float intensity)
{
  m_intensity = intensity;
}
float KRLight::getIntensity() const
{
  return m_intensity;
}

const Vector3& KRLight::getColor()
{
  return m_color;
}

void KRLight::setColor(const Vector3& color)
{
  m_color = color;
}

void KRLight::setDecayStart(float decayStart)
{
  m_decayStart = decayStart;
}

float KRLight::getDecayStart() const
{
  return m_decayStart;
}

void KRLight::getResourceBindings(std::list<KRResourceBinding*>& bindings)
{
  KRNode::getResourceBindings(bindings);

  bindings.push_back(&m_flareTexture.val);
}

Matrix4 KRLight::getParticleModelMatrix(const KRViewport& viewport) const
{
  Matrix4 particleModelMatrix;
  particleModelMatrix.scale(KRLIGHT_PARTICLE_RANGE);  // Scale the box symetrically to ensure that we don't have an uneven distribution of particles for different angles of the view frustrum
  particleModelMatrix.translate(viewport.getCameraPosition());

  return particleModelMatrix;
}

int KRLight::getParticleCount() const
{
  int particle_count = (int)(m_dust_particle_density * pow(KRLIGHT_PARTICLE_RANGE, 3));
  if (particle_count > KRMeshManager::KRENGINE_MAX_RANDOM_PARTICLES) {
    particle_count = KRMeshManager::KRENGINE_MAX_RANDOM_PARTICLES;
  }

  return particle_count;
}

int KRLight::getSliceCount(const KRCamera* camera) const
{
  int slice_count = (int)(camera->getSettings()->volumetric_environment_quality * 495.0) + 5;
  return slice_count;
}

void KRLight::render(RenderInfo& ri)
{
  KRNode::render(ri);
  ri.reflectedObjects.push_back(this);

  if (ri.renderPass->getType() == RenderPassType::RENDER_PASS_SHADOWMAP && (ri.camera->settings.volumetric_environment_enable || ri.camera->settings.dust_particle_enable || (ri.camera->settings.m_cShadowBuffers > 0 && m_casts_shadow))) {
    allocateShadowBuffers(configureShadowBufferViewports(*ri.viewport));
    renderShadowBuffers(ri);
  }

  if (ri.renderPass->getType() == RenderPassType::RENDER_PASS_ADDITIVE_PARTICLES && ri.camera->settings.dust_particle_enable) {
    // Render brownian particles for dust floating in air
    if (m_cShadowBuffers >= 1 && shadowValid[0] && m_dust_particle_density > 0.0f && m_dust_particle_size > 0.0f && m_dust_particle_intensity > 0.0f) {

      if (ri.viewport->visible(getBounds()) || true) { // FINDME, HACK need to remove "|| true"?
        
        std::vector<KRDirectionalLight*> this_directional_light;
        std::vector<KRSpotLight*> this_spot_light;
        std::vector<KRPointLight*> this_point_light;
        KRDirectionalLight* directional_light = dynamic_cast<KRDirectionalLight*>(this);
        KRSpotLight* spot_light = dynamic_cast<KRSpotLight*>(this);
        KRPointLight* point_light = dynamic_cast<KRPointLight*>(this);
        if (directional_light) {
          this_directional_light.push_back(directional_light);
        }
        if (spot_light) {
          this_spot_light.push_back(spot_light);
        }
        if (point_light) {
          this_point_light.push_back(point_light);
        }
        
        PipelineInfo info{};
        std::string shader_name("dust_particle");
        info.shader_name = &shader_name;
        info.pCamera = ri.camera;
        info.point_lights = &this_point_light;
        info.directional_lights = &this_directional_light;
        info.spot_lights = &this_spot_light;
        info.renderPass = ri.renderPass;
        info.rasterMode = RasterMode::kAdditive;
        info.cullMode = CullMode::kCullNone;
        info.layout = m_pContext->getMeshManager()->KRENGINE_VBO_DATA_RANDOM_PARTICLES.getLayout();
        KRPipeline* pParticleShader = m_pContext->getPipelineManager()->getPipeline(*ri.surface, info);
        if (pParticleShader && pParticleShader->bind(ri, getParticleModelMatrix(*ri.viewport))) { // TODO: Pass light index to shader
          m_pContext->getMeshManager()->bindVBO(ri.commandBuffer, &m_pContext->getMeshManager()->KRENGINE_VBO_DATA_RANDOM_PARTICLES, 1.0f);
          vkCmdDraw(ri.commandBuffer, getParticleCount() *3, 1, 0, 0);
        }
      }
    }
  }


  if (ri.renderPass->getType() == RenderPassType::RENDER_PASS_VOLUMETRIC_EFFECTS_ADDITIVE && ri.camera->settings.volumetric_environment_enable && m_light_shafts) {
    std::string shader_name = ri.camera->settings.volumetric_environment_downsample != 0 ? "volumetric_fog_downsampled" : "volumetric_fog";

    std::vector<KRDirectionalLight*> this_directional_light;
    std::vector<KRSpotLight*> this_spot_light;
    std::vector<KRPointLight*> this_point_light;
    KRDirectionalLight* directional_light = dynamic_cast<KRDirectionalLight*>(this);
    KRSpotLight* spot_light = dynamic_cast<KRSpotLight*>(this);
    KRPointLight* point_light = dynamic_cast<KRPointLight*>(this);
    if (directional_light) {
      this_directional_light.push_back(directional_light);
    }
    if (spot_light) {
      this_spot_light.push_back(spot_light);
    }
    if (point_light) {
      this_point_light.push_back(point_light);
    }

    PipelineInfo info{};
    info.shader_name = &shader_name;
    info.pCamera = ri.camera;
    info.point_lights = &this_point_light;
    info.directional_lights = &this_directional_light;
    info.spot_lights = &this_spot_light;
    info.renderPass = ri.renderPass;
    info.rasterMode = RasterMode::kAdditive;
    info.cullMode = CullMode::kCullNone;
    info.layout = m_pContext->getMeshManager()->KRENGINE_VBO_DATA_VOLUMETRIC_LIGHTING.getLayout();

    KRPipeline* pFogShader = m_pContext->getPipelineManager()->getPipeline(*ri.surface, info);
    if (pFogShader) {
      if (pFogShader->bind(ri, Matrix4())) { // TODO: Pass indexes of lights to shader
        m_pContext->getMeshManager()->bindVBO(ri.commandBuffer, &m_pContext->getMeshManager()->KRENGINE_VBO_DATA_VOLUMETRIC_LIGHTING, 1.0f);
        vkCmdDraw(ri.commandBuffer, getSliceCount(ri.camera) * 6, 1, 0, 0);
      }
    }

  }

  if (ri.renderPass->getType() == RenderPassType::RENDER_PASS_PARTICLE_OCCLUSION) {
    if (m_flareTexture.val.isBound() && m_flareSize > 0.0f) {
      KRMesh* sphereModel = getContext().getMeshManager()->getMesh("__sphere");
      if (sphereModel) {

        Matrix4 occlusion_test_sphere_matrix = Matrix4();
        occlusion_test_sphere_matrix.scale(m_localScale.val * m_flareOcclusionSize);
        occlusion_test_sphere_matrix.translate(m_localTranslation);
        if (m_parentNode) {
          occlusion_test_sphere_matrix *= m_parentNode->getModelMatrix();
        }

        PipelineInfo info{};
        std::string shader_name("occlusion_test");
        info.shader_name = &shader_name;
        info.pCamera = ri.camera;
        info.point_lights = &ri.point_lights;
        info.directional_lights = &ri.directional_lights;
        info.spot_lights = &ri.spot_lights;
        info.renderPass = ri.renderPass;
        info.rasterMode = RasterMode::kAdditive;
        info.cullMode = CullMode::kCullNone;
        info.layout = sphereModel->getLayout(0);

        KRPipeline* pPipeline = getContext().getPipelineManager()->getPipeline(*ri.surface, info);
        if (pPipeline && pPipeline->bind(ri, occlusion_test_sphere_matrix)) {

          GLDEBUG(glGenQueriesEXT(1, &m_occlusionQuery));
#if TARGET_OS_IPHONE || defined(ANDROID)
          GLDEBUG(glBeginQueryEXT(GL_ANY_SAMPLES_PASSED_EXT, m_occlusionQuery));
#else
          GLDEBUG(glBeginQuery(GL_SAMPLES_PASSED, m_occlusionQuery));
#endif

          sphereModel->renderNoMaterials(ri.commandBuffer, ri.renderPass, getName(), "occlusion_test", 1.0f);

#if TARGET_OS_IPHONE || defined(ANDROID)
          GLDEBUG(glEndQueryEXT(GL_ANY_SAMPLES_PASSED_EXT));
#else
          GLDEBUG(glEndQuery(GL_SAMPLES_PASSED));
#endif
        }
      }

    }

    ri.reflectedObjects.pop_back();
  }

  if (ri.renderPass->getType() == RenderPassType::RENDER_PASS_ADDITIVE_PARTICLES) {
    if (m_flareTexture.val.isBound() && m_flareSize > 0.0f) {

      if (m_occlusionQuery) {
        int params = 0;
        GLDEBUG(glGetQueryObjectuivEXT(m_occlusionQuery, GL_QUERY_RESULT_EXT, &params));
        GLDEBUG(glDeleteQueriesEXT(1, &m_occlusionQuery));

        if (params) {
          KRMeshManager::KRVBOData& vertices = getContext().getMeshManager()->KRENGINE_VBO_DATA_2D_SQUARE_VERTICES;

          // Render light flare on transparency pass
          PipelineInfo info{};
          std::string shader_name("flare");
          info.shader_name = &shader_name;
          info.pCamera = ri.camera;
          info.point_lights = &ri.point_lights;
          info.directional_lights = &ri.directional_lights;
          info.spot_lights = &ri.spot_lights;
          info.renderPass = ri.renderPass;
          info.rasterMode = RasterMode::kAdditiveNoTest;
          info.cullMode = CullMode::kCullNone;
          info.layout = vertices.getLayout();

          KRPipeline* pShader = getContext().getPipelineManager()->getPipeline(*ri.surface, info);
          if (pShader && pShader->bind(ri, getModelMatrix())) {
            m_pContext->getMeshManager()->bindVBO(ri.commandBuffer, &vertices, 1.0f);
            vkCmdDraw(ri.commandBuffer, 4, 1, 0, 0);
          }
        }
      }
    }
  }
}

void KRLight::allocateShadowBuffers(int cBuffers)
{
  // First deallocate buffers no longer needed
  for (int iShadow = cBuffers; iShadow < KRENGINE_MAX_SHADOW_BUFFERS; iShadow++) {
    if (shadowDepthTexture[iShadow]) {
      GLDEBUG(glDeleteTextures(1, shadowDepthTexture + iShadow));
      shadowDepthTexture[iShadow] = 0;
    }

    if (shadowFramebuffer[iShadow]) {
      GLDEBUG(glDeleteFramebuffers(1, shadowFramebuffer + iShadow));
      shadowFramebuffer[iShadow] = 0;
    }
  }

  // Allocate newly required buffers
  for (int iShadow = 0; iShadow < cBuffers; iShadow++) {
    Vector2 viewportSize = m_shadowViewports[iShadow].getSize();

    if (!shadowDepthTexture[iShadow]) {
      shadowValid[iShadow] = false;

      GLDEBUG(glGenFramebuffers(1, shadowFramebuffer + iShadow));
      GLDEBUG(glGenTextures(1, shadowDepthTexture + iShadow));
      // ===== Create offscreen shadow framebuffer object =====

      GLDEBUG(glBindFramebuffer(GL_FRAMEBUFFER, shadowFramebuffer[iShadow]));

      // ----- Create Depth Texture for shadowFramebuffer -----
      // TODO - Vulkan Refactoring.  Note: shadowDepthTexture Sampler needs clamp-to-edge and linear filtering
      GLDEBUG(glBindTexture(GL_TEXTURE_2D, shadowDepthTexture[iShadow]));
      GLDEBUG(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST));
      GLDEBUG(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST));
      // m_pContext->getTextureManager()->_setWrapModeS(shadowDepthTexture[iShadow], GL_CLAMP_TO_EDGE);
      // m_pContext->getTextureManager()->_setWrapModeT(shadowDepthTexture[iShadow], GL_CLAMP_TO_EDGE);
#if GL_EXT_shadow_samplers
      GLDEBUG(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE_EXT, GL_COMPARE_REF_TO_TEXTURE_EXT)); // TODO - Detect GL_EXT_shadow_samplers and only activate if available
      GLDEBUG(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_FUNC_EXT, GL_LEQUAL)); // TODO - Detect GL_EXT_shadow_samplers and only activate if available
#endif
      GLDEBUG(glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, (int)viewportSize.x, (int)viewportSize.y, 0, GL_DEPTH_COMPONENT, GL_UNSIGNED_INT, NULL));

      GLDEBUG(glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, shadowDepthTexture[iShadow], 0));
    }
  }

  m_cShadowBuffers = cBuffers;
}


void KRLight::deleteBuffers()
{
  // Called when this light wasn't used in the last frame, so we can free the resources for use by other lights
  allocateShadowBuffers(0);
}

void KRLight::invalidateShadowBuffers()
{
  for (int iShadow = 0; iShadow < m_cShadowBuffers; iShadow++) {
    shadowValid[iShadow] = false;
  }
}

int KRLight::configureShadowBufferViewports(const KRViewport& viewport)
{
  return 0;
}

void KRLight::renderShadowBuffers(RenderInfo& ri)
{
  const KRViewport* prevViewport = ri.viewport;
  for (int iShadow = 0; iShadow < m_cShadowBuffers; iShadow++) {
    if (!shadowValid[iShadow]) {
      shadowValid[iShadow] = true;

      GLDEBUG(glBindFramebuffer(GL_FRAMEBUFFER, shadowFramebuffer[iShadow]));
      GLDEBUG(glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, shadowDepthTexture[iShadow], 0));

      GLDEBUG(glViewport(0, 0, (int)m_shadowViewports[iShadow].getSize().x, (int)m_shadowViewports[iShadow].getSize().y));

      GLDEBUG(glClearDepthf(0.0f));
      GLDEBUG(glClear(GL_DEPTH_BUFFER_BIT));

      GLDEBUG(glViewport(1, 1, (int)m_shadowViewports[iShadow].getSize().x - 2, (int)m_shadowViewports[iShadow].getSize().y - 2));

      GLDEBUG(glClearDepthf(1.0f));

      GLDEBUG(glClear(GL_DEPTH_BUFFER_BIT));

      GLDEBUG(glDisable(GL_DITHER));


      ri.viewport = &m_shadowViewports[iShadow];

      // Use shader program
      PipelineInfo info{};
      std::string shader_name("ShadowShader");
      info.shader_name = &shader_name;
      info.pCamera = ri.camera;
      info.renderPass = ri.renderPass;
      info.rasterMode = RasterMode::kOpaqueLessTest; // TODO - This is sub-optimal.  Evaluate increasing depth buffer resolution instead of disabling depth test.
      info.cullMode = CullMode::kCullNone; // Disabling culling, which eliminates some self-cast shadow artifacts
      KRPipeline* shadowShader = m_pContext->getPipelineManager()->getPipeline(*ri.surface, info);

      
      if (shadowShader && shadowShader->bind(ri, Matrix4())) {
        getScene().render(ri);
      }
    }
  }
  ri.viewport = prevViewport;
}



int KRLight::getShadowBufferCount()
{
  int cBuffers = 0;
  for (int iBuffer = 0; iBuffer < m_cShadowBuffers; iBuffer++) {
    if (shadowValid[iBuffer]) {
      cBuffers++;
    } else {
      break;
    }
  }
  return cBuffers;
}

int* KRLight::getShadowTextures()
{
  return shadowDepthTexture;
}

KRViewport* KRLight::getShadowViewports()
{
  return m_shadowViewports;
}

bool KRLight::getShaderValue(const KRCamera* camera, ShaderValue value, float* output) const
{
  switch (value) {
  case ShaderValue::light_intensity:
    *output = m_intensity;
    return true;
  case ShaderValue::light_decay_start:
    *output = getDecayStart();
    return true;
  case ShaderValue::light_cutoff:
    *output = KRLIGHT_MIN_INFLUENCE;
    return true;
  case ShaderValue::flare_size:
    *output = m_flareSize;
    return true;
  case ShaderValue::dust_particle_size:
    *output = m_dust_particle_size;
    return true;
  default:
    return KRNode::getShaderValue(camera, value, output);
  }
}

bool KRLight::getShaderValue(const KRCamera* camera, ShaderValue value, hydra::Vector2* output) const
{
  if (camera) {
    switch (value) {
    case ShaderValue::slice_depth_scale:
    {
      int slice_count = getSliceCount(camera);

      float slice_near = -camera->getSettings()->getPerspectiveNearZ();
      float slice_far = -camera->getSettings()->volumetric_environment_max_distance;
      float slice_spacing = (slice_far - slice_near) / slice_count;
      *output = Vector2::Create(slice_near, slice_spacing);
      return true;
    }
    break;
    default:
      break;
    }
  }

  return KRNode::getShaderValue(camera, value, output);
}

bool KRLight::getShaderValue(const KRCamera* camera, ShaderValue value, hydra::Vector3* output) const
{
  switch (value) {
  case ShaderValue::light_position:
    *output = m_localTranslation;
    return true;
  case ShaderValue::light_color:
    *output = m_color;
    return true;
  case ShaderValue::particle_origin:
    *output = Matrix4::DotWDiv(Matrix4::Invert(getParticleModelMatrix(*camera->getViewport())), Vector3::Zero());
    return true;
  case ShaderValue::dust_particle_color:
    *output = m_color.val * m_dust_particle_intensity * m_intensity * camera->settings.dust_particle_intensity;
    return true;
  default:
    return KRNode::getShaderValue(camera, value, output);
  }
}

bool KRLight::getImageBinding(const std::string& name, const KRTextureBinding** binding, KRSampler** sample) const
{
  if (name == "flareTexture") {
    *binding = &m_flareTexture.val;
    *sample = getContext().getSamplerManager()->DEFAULT_CLAMPED_SAMPLER;
    return true;
  } else {
    return KRNode::getImageBinding(name, binding, sample);
  }
}