/*
 * Descent 3
 * Copyright (C) 2024 Parallax Software
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "stdafx.h"
#include "editor.h"
#include "LightingStatus.h"
#include "radiosity.h"
#include "pserror.h"
#include "findintersection.h"
#include "hemicube.h"
#include "descent.h"
#include "rad_cast.h"
#include "ddio.h"
#include "vecmat.h"
#include <stdlib.h>
#include "mem.h"

// Some radiosity globals
int Shoot_method = SM_HEMICUBE;
int Hemicube_resolution = 1024;

int Ignore_terrain = 0;
int Ignore_satellites = 0;

float rad_TotalFlux = 0.0f;
float rad_Convergence = 1.0f;

int rad_NumSurfaces;
int rad_NumElements;

float *rad_FormFactors;

int rad_StepCount = 0;
int rad_MaxStep = 1;
int rad_DoneCalculating = 0;

float rad_TotalUnsent = 0.0f;

rad_surface *rad_MaxSurface = NULL;
rad_surface *rad_Surfaces;

int UseVolumeLights = 0; // User selectable to do volumelights
int Calculate_specular_lighting = 0;

// Specular variable
float *Room_strongest_value[MAX_ROOMS][4];

// Tells radiosity renderer to do volume lighting
int Do_volume_lighting = 0;

volume_element *Volume_elements[MAX_VOLUME_ELEMENTS];

// Shoot_from_patch tells us whether or not we're shooting from the center of
// a surface or if we must shoot from the center of each of its individual elements
// Shoot_from_patch=1 is much faster
int Shoot_from_patch = 1;

int DoRadiosityRun(int method, rad_surface *light_surfaces, int count) {
  float start_time;

  mprintf(0, "Calculating radiosity on %d faces.\n", count);

  rad_Surfaces = light_surfaces;
  rad_NumSurfaces = count;

  Shoot_method = method;

  start_time = timer_GetTime();

  InitRadiosityRun();

  // Setup our window
  CLightingStatus dlg;

  dlg.Create(IDD_LIGHTINGSTATUS);

  CalculateRadiosity();

  dlg.DestroyWindow();
  CloseRadiosityRun();

  // Print time taken
  mprintf(0, "\nLighting took %.4f seconds.\n", timer_GetTime() - start_time);

  return 1;
}

// Sets up our radiosity run
void InitRadiosityRun() {
  rad_TotalFlux = 0.0f;
  rad_StepCount = 0;
  rad_DoneCalculating = 0;

  // Clear key buffer
  //	ddio_KeyFrame();
  ddio_KeyFlush();

  CountElements();
  CalculateArea();
  InitExitance();

  if (Shoot_method == SM_HEMICUBE) {
    SetupFormFactors();
    InitHemicube(Hemicube_resolution);
  }
}

// Initalizes memory for form factors
void SetupFormFactors() {
  ASSERT(rad_NumElements > 0);

  rad_FormFactors = (float *)mem_malloc(rad_NumElements * sizeof(float));
  ASSERT(rad_FormFactors != NULL);
}

void CalculateAreaForSurface(rad_surface *sp) {
  int i;

  vector normal;

  vm_GetPerp(&normal, &sp->verts[0], &sp->verts[1], &sp->verts[2]);
  sp->area = (vm_GetMagnitude(&normal) / 2);

  for (i = 2; i < sp->num_verts - 1; i++) {
    vm_GetPerp(&normal, &sp->verts[0], &sp->verts[i], &sp->verts[i + 1]);
    sp->area += (vm_GetMagnitude(&normal) / 2);
  }

  sp->surface_area = sp->area;
  sp->element_area = sp->area / (sp->xresolution * sp->yresolution);
}

void CalculateAreaForElement(rad_element *ep) {
  int i;

  vector normal;

  if (ep->flags & EF_IGNORE) {
    ep->area = .0000001f;
    return;
  }

  vm_GetPerp(&normal, &ep->verts[0], &ep->verts[1], &ep->verts[2]);
  ep->area = (vm_GetMagnitude(&normal) / 2);

  for (i = 2; i < ep->num_verts - 1; i++) {
    vm_GetPerp(&normal, &ep->verts[0], &ep->verts[i], &ep->verts[i + 1]);
    ep->area += (vm_GetMagnitude(&normal) / 2);
  }

  if (ep->area < .05)
    ep->flags |= EF_SMALL;
  if (ep->area == 0) {
    ep->flags |= EF_IGNORE;
    ep->area = .00000001f;
  }
}

// Calculates the area of the surfaces and elements in our environment
void CalculateArea() {
  rad_surface *surf;
  int i, t;

  for (i = 0; i < rad_NumSurfaces; i++) {
    surf = &rad_Surfaces[i];

    CalculateAreaForSurface(surf);

    for (t = 0; t < surf->xresolution * surf->yresolution; t++) {
      rad_element *ep = &surf->elements[t];
      CalculateAreaForElement(ep);
    }
  }
}

// Counts the total number of elements we have to work with
void CountElements() {
  rad_surface *surf;
  int i;

  rad_NumElements = 0;

  for (i = 0; i < rad_NumSurfaces; i++) {
    surf = &rad_Surfaces[i];

    rad_NumElements += (surf->xresolution * surf->yresolution);
  }
  mprintf(0, "Number of elements=%d\n", rad_NumElements);
}

// Initializes the exitances for all surfaces
void InitExitance() {
  int i;

  for (i = 0; i < rad_NumSurfaces; i++) {
    SetExitanceForSurface(&rad_Surfaces[i]);
  }
}

// Gets the spectral emittance for a surface
void GetEmittance(rad_surface *surf, spectra *dest) { *dest = surf->emittance; }

// Sets all the elements of a surface to their initial unshot exitance values
void SetExitanceForSurface(rad_surface *surf) {
  int i;

  surf->exitance = surf->emittance;

  for (i = 0; i < surf->xresolution * surf->yresolution; i++) {
    if (Shoot_from_patch) {
      surf->elements[i].exitance.r = 0;
      surf->elements[i].exitance.g = 0;
      surf->elements[i].exitance.b = 0;
    } else
      surf->elements[i].exitance = surf->emittance;
  }

  rad_TotalFlux += GetUnsentFlux(surf);
}

// Find the surface we want to shoot from
void UpdateUnsentValues() {
  float cur_unsent;
  float max_unsent = 0.0f;
  float sat_max_unsent = 0.0f;
  int use_sat = 0;
  int i;
  rad_surface *sat_surface;

  static int last_report_time = -10;

  rad_TotalUnsent = 0.0f;
  rad_MaxSurface = NULL;

  // Go through all the surfaces searching for the surface with the greatest
  // exitance yet to be shot

  for (i = 0; i < rad_NumSurfaces; i++) {
    rad_surface *surf = &rad_Surfaces[i];
    cur_unsent = GetUnsentFlux(surf);
    rad_TotalUnsent += cur_unsent;

    if (cur_unsent > max_unsent) {
      max_unsent = cur_unsent;
      rad_MaxSurface = surf;
    }

    // Always give satellites priority
    if (surf->surface_type == ST_SATELLITE && cur_unsent > 0) {
      if (cur_unsent > sat_max_unsent) {
        use_sat = 1;
        sat_max_unsent = cur_unsent;
        sat_surface = surf;
      }
    }
  }

  // Update convergence
  if (rad_TotalFlux > .0001)
    rad_Convergence = fabs(rad_TotalUnsent) / rad_TotalFlux;
  else
    rad_Convergence = 0.0;

  mprintf_at(2, 3, 0, "Left=%f  ", rad_Convergence);

  if (timer_GetTime() - last_report_time > 10.0) {
    mprintf(0, "Percentage left=%f\n", rad_Convergence);
    last_report_time = timer_GetTime();
  }

  if (use_sat)
    rad_MaxSurface = sat_surface;

  if (!use_sat && Shoot_method == SM_SWITCH_AFTER_SATELLITES) {
    SetupFormFactors();
    InitHemicube(Hemicube_resolution);
    Shoot_method = SM_HEMICUBE;
  }

  // No energy left to shoot?
  if (rad_MaxSurface == NULL || rad_TotalUnsent == 0)
    rad_DoneCalculating = 1;
}

// Finds the world coordinate center of a surface
void GetCenterOfSurface(rad_surface *sp, vector *dest) { vm_GetCentroid(dest, sp->verts, sp->num_verts); }

// Finds the world coordinate center of a surface
void GetCenterOfElement(rad_element *ep, vector *dest) { vm_GetCentroid(dest, ep->verts, ep->num_verts); }

void CalculateRadiosity() {
  int key;

  while (!rad_DoneCalculating) {
    if (rad_StepCount >= rad_MaxStep) {
      rad_DoneCalculating = 1;
      break;
    }
    mprintf_at(2, 2, 0, "Lightcount=%d   ", rad_StepCount);

    DoRadiosityIteration();

    rad_StepCount++;

    Descent->defer();
    //		ddio_KeyFrame();
    while ((key = ddio_KeyInKey()) != 0) {
      if (key == KEY_LAPOSTRO) {
        rad_DoneCalculating = 1;
        break;
      }
    }
  }

  // Clear key buffer
  ddio_KeyFlush();
}

// returns the amount of unsent flux from a surface
float GetUnsentFlux(rad_surface *surface) {
  float flux;

  flux = surface->exitance.r + surface->exitance.g + surface->exitance.b;

  if (surface->surface_type != ST_SATELLITE)
    flux *= surface->area;

  return flux;
}

float GetMaxColor(spectra *sp) {
  float m;

  m = std::max<float>(sp->r, sp->g);
  m = std::max<float>(sp->b, m);

  return m;
}

int FixEdges = 0;
extern void AddSpectra(spectra *dest, spectra *a, spectra *b);

void NormalizeExitance() {
  int i, t;
  float rmax = 0.0f;

  for (i = 0; i < rad_NumSurfaces; i++) {
    rad_surface *surf = &rad_Surfaces[i];
    spectra *emit = &surf->emittance;

    for (t = 0; t < surf->xresolution * surf->yresolution; t++) {
      rad_element *ep = &surf->elements[t];

      if (ep->flags & EF_IGNORE)
        continue;

      if (Shoot_from_patch) {
        ep->exitance.r += emit->r;
        ep->exitance.g += emit->g;
        ep->exitance.b += emit->b;
      }

      /*			if (ep->exitance.r>1)
                                      ep->exitance.r=1;
                              if (ep->exitance.g>1)
                                      ep->exitance.g=1;
                              if (ep->exitance.b>1)
                                      ep->exitance.b=1;*/

      rmax = GetMaxColor(&ep->exitance);

      if (rmax > 1.0 && rmax > 0.0) {
        ep->exitance.r /= rmax;
        ep->exitance.g /= rmax;
        ep->exitance.b /= rmax;
      }
    }
  }
}

// Shuts down the radiosity stuff, freeing memory, etc
void CloseRadiosityRun() {

  NormalizeExitance();
  if (Shoot_method == SM_HEMICUBE) {
    mem_free(rad_FormFactors);
    CloseHemicube();
  }
}
void Calculate() {

  if (Shoot_method == SM_HEMICUBE)
    CalculateFormFactorsHemiCube();
  else
    CalculateFormFactorsRaycast();

  // Set unshot exitance for MaxSurface to zero
  rad_MaxSurface->exitance.r = 0;
  rad_MaxSurface->exitance.g = 0;
  rad_MaxSurface->exitance.b = 0;
  rad_MaxSurface->flags &= ~SF_LIGHTSOURCE;
}

// Does one iteration of ray-casting radiosity
int DoRadiosityIteration() {
  UpdateUnsentValues();

  if (!rad_DoneCalculating)
    Calculate();

  return 1;
}
