/* SPDX-License-Identifier: Apache-2.0
 * Copyright 2011-2022 Blender Foundation */

#include <algorithm>

#include "util/foreach.h"
#include "util/ies.h"
#include "util/math.h"
#include "util/string.h"

CCL_NAMESPACE_BEGIN

// NOTE: For some reason gcc-7.2 does not instantiate this version of the
// allocator here (used in IESTextParser). Works fine for gcc-6, gcc-7.3 and gcc-8.
//
// TODO(sergey): Get to the root of this issue, or confirm this is a compiler
// issue.
template class GuardedAllocator<char>;

bool IESFile::load(const string &ies)
{
  clear();
  if (!parse(ies) || !process()) {
    clear();
    return false;
  }
  return true;
}

void IESFile::clear()
{
  intensity.clear();
  v_angles.clear();
  h_angles.clear();
}

int IESFile::packed_size()
{
  if (v_angles.size() && h_angles.size() > 0) {
    return 2 + h_angles.size() + v_angles.size() + h_angles.size() * v_angles.size();
  }
  return 0;
}

void IESFile::pack(float *data)
{
  if (v_angles.size() && h_angles.size()) {
    *(data++) = __int_as_float(h_angles.size());
    *(data++) = __int_as_float(v_angles.size());

    memcpy(data, &h_angles[0], h_angles.size() * sizeof(float));
    data += h_angles.size();
    memcpy(data, &v_angles[0], v_angles.size() * sizeof(float));
    data += v_angles.size();

    for (int h = 0; h < intensity.size(); h++) {
      memcpy(data, &intensity[h][0], v_angles.size() * sizeof(float));
      data += v_angles.size();
    }
  }
}

class IESTextParser {
 public:
  vector<char> text;
  char *data;

  IESTextParser(const string &str) : text(str.begin(), str.end())
  {
    std::replace(text.begin(), text.end(), ',', ' ');
    data = strstr(&text[0], "\nTILT=");
  }

  bool eof()
  {
    return (data == NULL) || (data[0] == '\0');
  }

  double get_double()
  {
    if (eof()) {
      return 0.0;
    }
    char *old_data = data;
    double val = strtod(data, &data);
    if (data == old_data) {
      data = NULL;
      return 0.0;
    }
    return val;
  }

  long get_long()
  {
    if (eof()) {
      return 0;
    }
    char *old_data = data;
    long val = strtol(data, &data, 10);
    if (data == old_data) {
      data = NULL;
      return 0;
    }
    return val;
  }
};

bool IESFile::parse(const string &ies)
{
  if (ies.empty()) {
    return false;
  }

  IESTextParser parser(ies);
  if (parser.eof()) {
    return false;
  }

  /* Handle the tilt data block. */
  if (strncmp(parser.data, "\nTILT=INCLUDE", 13) == 0) {
    parser.data += 13;
    parser.get_double();              /* Lamp to Luminaire geometry */
    int num_tilt = parser.get_long(); /* Amount of tilt angles and factors */
    /* Skip over angles and factors. */
    for (int i = 0; i < 2 * num_tilt; i++) {
      parser.get_double();
    }
  }
  else {
    /* Skip to next line. */
    parser.data = strstr(parser.data + 1, "\n");
  }

  if (parser.eof()) {
    return false;
  }
  parser.data++;

  parser.get_long();                    /* Number of lamps */
  parser.get_double();                  /* Lumens per lamp */
  double factor = parser.get_double();  /* Candela multiplier */
  int v_angles_num = parser.get_long(); /* Number of vertical angles */
  int h_angles_num = parser.get_long(); /* Number of horizontal angles */
  type = (IESType)parser.get_long();    /* Photometric type */

  /* TODO(lukas): Test whether the current type B processing can also deal with type A files.
   * In theory the only difference should be orientation which we ignore anyways, but with IES you
   * never know...
   */
  if (type != TYPE_B && type != TYPE_C) {
    return false;
  }

  parser.get_long();             /* Unit of the geometry data */
  parser.get_double();           /* Width */
  parser.get_double();           /* Length */
  parser.get_double();           /* Height */
  factor *= parser.get_double(); /* Ballast factor */
  factor *= parser.get_double(); /* Ballast-Lamp Photometric factor */
  parser.get_double();           /* Input Watts */

  /* Intensity values in IES files are specified in candela (lumen/sr), a photometric quantity.
   * Cycles expects radiometric quantities, though, which requires a conversion.
   * However, the Luminous efficacy (ratio of lumens per Watt) depends on the spectral distribution
   * of the light source since lumens take human perception into account.
   * Since this spectral distribution is not known from the IES file, a typical one must be
   * assumed. The D65 standard illuminant has a Luminous efficacy of 177.83, which is used here to
   * convert to Watt/sr. A more advanced approach would be to add a Blackbody Temperature input to
   * the node and numerically integrate the Luminous efficacy from the resulting spectral
   * distribution. Also, the Watt/sr value must be multiplied by 4*pi to get the Watt value that
   * Cycles expects for lamp strength. Therefore, the conversion here uses 4*pi/177.83 as a Candela
   * to Watt factor.
   */
  factor *= 0.0706650768394;

  v_angles.reserve(v_angles_num);
  for (int i = 0; i < v_angles_num; i++) {
    v_angles.push_back((float)parser.get_double());
  }

  h_angles.reserve(h_angles_num);
  for (int i = 0; i < h_angles_num; i++) {
    h_angles.push_back((float)parser.get_double());
  }

  intensity.resize(h_angles_num);
  for (int i = 0; i < h_angles_num; i++) {
    intensity[i].reserve(v_angles_num);
    for (int j = 0; j < v_angles_num; j++) {
      intensity[i].push_back((float)(factor * parser.get_double()));
    }
  }

  return !parser.eof();
}

bool IESFile::process_type_b()
{
  vector<vector<float>> newintensity;
  newintensity.resize(v_angles.size());
  for (int i = 0; i < v_angles.size(); i++) {
    newintensity[i].reserve(h_angles.size());
    for (int j = 0; j < h_angles.size(); j++) {
      newintensity[i].push_back(intensity[j][i]);
    }
  }
  intensity.swap(newintensity);
  h_angles.swap(v_angles);

  float h_first = h_angles[0], h_last = h_angles[h_angles.size() - 1];
  if (h_last != 90.0f) {
    return false;
  }

  if (h_first == 0.0f) {
    /* The range in the file corresponds to 90°-180°, we need to mirror that to get the
     * full 180° range. */
    vector<float> new_h_angles;
    vector<vector<float>> new_intensity;
    int hnum = h_angles.size();
    new_h_angles.reserve(2 * hnum - 1);
    new_intensity.reserve(2 * hnum - 1);
    for (int i = hnum - 1; i > 0; i--) {
      new_h_angles.push_back(90.0f - h_angles[i]);
      new_intensity.push_back(intensity[i]);
    }
    for (int i = 0; i < hnum; i++) {
      new_h_angles.push_back(90.0f + h_angles[i]);
      new_intensity.push_back(intensity[i]);
    }
    h_angles.swap(new_h_angles);
    intensity.swap(new_intensity);
  }
  else if (h_first == -90.0f) {
    /* We have full 180° coverage, so just shift to match the angle range convention. */
    for (int i = 0; i < h_angles.size(); i++) {
      h_angles[i] += 90.0f;
    }
  }
  /* To get correct results with the cubic interpolation in the kernel, the horizontal range
   * has to cover all 360°. Therefore, we copy the 0° entry to 360° to ensure full coverage
   * and seamless interpolation. */
  h_angles.push_back(360.0f);
  intensity.push_back(intensity[0]);

  float v_first = v_angles[0], v_last = v_angles[v_angles.size() - 1];
  if (v_last != 90.0f) {
    return false;
  }

  if (v_first == 0.0f) {
    /* The range in the file corresponds to 90°-180°, we need to mirror that to get the
     * full 180° range. */
    vector<float> new_v_angles;
    int hnum = h_angles.size();
    int vnum = v_angles.size();
    new_v_angles.reserve(2 * vnum - 1);
    for (int i = vnum - 1; i > 0; i--) {
      new_v_angles.push_back(90.0f - v_angles[i]);
    }
    for (int i = 0; i < vnum; i++) {
      new_v_angles.push_back(90.0f + v_angles[i]);
    }
    for (int i = 0; i < hnum; i++) {
      vector<float> new_intensity;
      new_intensity.reserve(2 * vnum - 1);
      for (int j = vnum - 2; j >= 0; j--) {
        new_intensity.push_back(intensity[i][j]);
      }
      new_intensity.insert(new_intensity.end(), intensity[i].begin(), intensity[i].end());
      intensity[i].swap(new_intensity);
    }
    v_angles.swap(new_v_angles);
  }
  else if (v_first == -90.0f) {
    /* We have full 180° coverage, so just shift to match the angle range convention. */
    for (int i = 0; i < v_angles.size(); i++) {
      v_angles[i] += 90.0f;
    }
  }

  return true;
}

bool IESFile::process_type_c()
{
  if (h_angles[0] == 90.0f) {
    /* Some files are stored from 90° to 270°, so we just rotate them to the regular 0°-180° range
     * here. */
    for (int i = 0; i < h_angles.size(); i++) {
      h_angles[i] -= 90.0f;
    }
  }

  if (h_angles[0] != 0.0f) {
    return false;
  }

  if (h_angles.size() == 1) {
    h_angles.push_back(360.0f);
    intensity.push_back(intensity[0]);
  }

  if (h_angles[h_angles.size() - 1] == 90.0f) {
    /* Only one quadrant is defined, so we need to mirror twice (from one to two, then to four).
     * Since the two->four mirroring step might also be required if we get an input of two
     * quadrants, we only do the first mirror here and later do the second mirror in either case.
     */
    int hnum = h_angles.size();
    for (int i = hnum - 2; i >= 0; i--) {
      h_angles.push_back(180.0f - h_angles[i]);
      intensity.push_back(intensity[i]);
    }
  }

  if (h_angles[h_angles.size() - 1] == 180.0f) {
    /* Mirror half to the full range. */
    int hnum = h_angles.size();
    for (int i = hnum - 2; i >= 0; i--) {
      h_angles.push_back(360.0f - h_angles[i]);
      intensity.push_back(intensity[i]);
    }
  }

  /* Some files skip the 360° entry (contrary to standard) because it's supposed to be identical to
   * the 0° entry. If the file has a discernible order in its spacing, just fix this. */
  if (h_angles[h_angles.size() - 1] != 360.0f) {
    int hnum = h_angles.size();
    float last_step = h_angles[hnum - 1] - h_angles[hnum - 2];
    float first_step = h_angles[1] - h_angles[0];
    float difference = 360.0f - h_angles[hnum - 1];
    if (last_step == difference || first_step == difference) {
      h_angles.push_back(360.0f);
      intensity.push_back(intensity[0]);
    }
    else {
      return false;
    }
  }

  float v_first = v_angles[0], v_last = v_angles[v_angles.size() - 1];
  if (v_first == 90.0f) {
    if (v_last == 180.0f) {
      /* Flip to ensure that vertical angles always start at 0°. */
      for (int i = 0; i < v_angles.size(); i++) {
        v_angles[i] = 180.0f - v_angles[i];
      }
    }
    else {
      return false;
    }
  }
  else if (v_first != 0.0f) {
    return false;
  }

  return true;
}

bool IESFile::process()
{
  if (h_angles.size() == 0 || v_angles.size() == 0) {
    return false;
  }

  if (type == TYPE_B) {
    if (!process_type_b()) {
      return false;
    }
  }
  else {
    assert(type == TYPE_C);
    if (!process_type_c()) {
      return false;
    }
  }

  assert(v_angles[0] == 0.0f);
  assert(h_angles[0] == 0.0f);
  assert(h_angles[h_angles.size() - 1] == 360.0f);

  /* Convert from deg to rad. */
  for (int i = 0; i < v_angles.size(); i++) {
    v_angles[i] *= M_PI_F / 180.f;
  }
  for (int i = 0; i < h_angles.size(); i++) {
    h_angles[i] *= M_PI_F / 180.f;
  }

  return true;
}

IESFile::~IESFile()
{
  clear();
}

CCL_NAMESPACE_END
