// ########################## OPEN3D ORIGINAL WORK ############################
// ----------------------------------------------------------------------------
// -                        Open3D: www.open3d.org                            -
// ----------------------------------------------------------------------------
// The MIT License (MIT)
//
// Copyright (c) 2018 www.open3d.org
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
// ----------------------------------------------------------------------------
//
//
//
// ########################## PROCARE MODIFIED WORK ############################
// Mofifications: block of codes specified by /*kimeguida*/
// -----------------------------------------------------------------------------
// <                                  ProCare                                  >
// -----------------------------------------------------------------------------
// The MIT License (MIT)
//
// Copyright (c) 2020 Merveille Eguida
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.

#include "Feature.h"

#include <Eigen/Dense>
#include <Core/Utility/Console.h>
#include <Core/Geometry/PointCloud.h>
#include <Core/Geometry/KDTreeFlann.h>

namespace open3d {

namespace {

Eigen::Vector4d ComputePairFeatures(const Eigen::Vector3d &p1,
                                    const Eigen::Vector3d &n1,
                                    const Eigen::Vector3d &p2,
                                    const Eigen::Vector3d &n2) {
    Eigen::Vector4d result;
    Eigen::Vector3d dp2p1 = p2 - p1;
    result(3) = dp2p1.norm();
    if (result(3) == 0.0) {
        return Eigen::Vector4d::Zero();
    }
    auto n1_copy = n1;
    auto n2_copy = n2;
    double angle1 = n1_copy.dot(dp2p1) / result(3);
    double angle2 = n2_copy.dot(dp2p1) / result(3);
    if (acos(fabs(angle1)) > acos(fabs(angle2))) {
        n1_copy = n2;
        n2_copy = n1;
        dp2p1 *= -1.0;
        result(2) = -angle2;
    } else {
        result(2) = angle1;
    }
    auto v = dp2p1.cross(n1_copy);
    double v_norm = v.norm();
    if (v_norm == 0.0) {
        return Eigen::Vector4d::Zero();
    }
    v /= v_norm;
    auto w = n1_copy.cross(v);
    result(1) = v.dot(n2_copy);
    result(0) = atan2(w.dot(n2_copy), n1_copy.dot(n2_copy));
    return result;
}

std::shared_ptr<Feature> ComputeSPFHFeature(
        const PointCloud &input,
        const KDTreeFlann &kdtree,
        const KDTreeSearchParam &search_param) {
    auto feature = std::make_shared<Feature>();
    feature->Resize(33, (int)input.points_.size());
#ifdef _OPENMP
#pragma omp parallel for schedule(static)
#endif
    for (int i = 0; i < (int)input.points_.size(); i++) {
        const auto &point = input.points_[i];
        const auto &normal = input.normals_[i];
        std::vector<int> indices;
        std::vector<double> distance2;
        if (kdtree.Search(point, search_param, indices, distance2) > 1) {
            // only compute SPFH feature when a point has neighbors
            double hist_incr = 100.0 / (double)(indices.size() - 1);
            for (size_t k = 1; k < indices.size(); k++) {
                // skip the point itself, compute histogram
                auto pf = ComputePairFeatures(point, normal,
                                              input.points_[indices[k]],
                                              input.normals_[indices[k]]);
                int h_index = (int)(floor(11 * (pf(0) + M_PI) / (2.0 * M_PI)));
                if (h_index < 0) h_index = 0;
                if (h_index >= 11) h_index = 10;
                feature->data_(h_index, i) += hist_incr;
                h_index = (int)(floor(11 * (pf(1) + 1.0) * 0.5));
                if (h_index < 0) h_index = 0;
                if (h_index >= 11) h_index = 10;
                feature->data_(h_index + 11, i) += hist_incr;
                h_index = (int)(floor(11 * (pf(2) + 1.0) * 0.5));
                if (h_index < 0) h_index = 0;
                if (h_index >= 11) h_index = 10;
                feature->data_(h_index + 22, i) += hist_incr;
            }
        }
    }
    return feature;
}

/*kimeguida*/
std::shared_ptr<Feature> ComputeCSPFHFeature(
        const PointCloud &input,
        const KDTreeFlann &kdtree,
        const KDTreeSearchParam &search_param) {
    auto feature = std::make_shared<Feature>();
    feature->Resize(41, (int)input.points_.size());
#ifdef _OPENMP
#pragma omp parallel for schedule(static)
#endif
    for (int i = 0; i < (int)input.points_.size(); i++) {
        const auto &point = input.points_[i];
        const auto &normal = input.normals_[i];
        std::vector<int> indices;
        std::vector<double> distance2;
        if (kdtree.Search(point, search_param, indices, distance2) > 1) {
            // only compute CSPFH feature when a point has neighbors
            double hist_incr = 100.0 / (double)(indices.size() - 1);
            for (size_t k = 1; k < indices.size(); k++) {
                // skip the point itself, compute histogram
                auto pf = ComputePairFeatures(point, normal,
                                              input.points_[indices[k]],
                                              input.normals_[indices[k]]);
                int h_index = (int)(floor(11 * (pf(0) + M_PI) / (2.0 * M_PI)));
                if (h_index < 0) h_index = 0;
                if (h_index >= 11) h_index = 10;
                feature->data_(h_index, i) += hist_incr;
                h_index = (int)(floor(11 * (pf(1) + 1.0) * 0.5));
                if (h_index < 0) h_index = 0;
                if (h_index >= 11) h_index = 10;
                feature->data_(h_index + 11, i) += hist_incr;
                h_index = (int)(floor(11 * (pf(2) + 1.0) * 0.5));
                if (h_index < 0) h_index = 0;
                if (h_index >= 11) h_index = 10;
                feature->data_(h_index + 22, i) += hist_incr;
            }

            double c_hist_incr = 100.0 / (double)(indices.size());
            const char *CA_c = "16741671";
            const char *CZ_c = "4646984";
            const char *O_c = "15219528";
            const char *OD1_c = "0";
            const char *OG_c = "8204959";
            const char *N_c = "30894";
            const char *NZ_c = "15231913";
            const char *DU_c = "7566712";

            Eigen::Vector3d CA_rgb = ASCIIPCDColorToRGB(CA_c, 'F', 4);
            Eigen::Vector3d CZ_rgb = ASCIIPCDColorToRGB(CZ_c, 'F', 4);
            Eigen::Vector3d O_rgb = ASCIIPCDColorToRGB(O_c, 'F', 4);
            Eigen::Vector3d OD1_rgb = ASCIIPCDColorToRGB(OD1_c, 'F', 4);
            Eigen::Vector3d OG_rgb = ASCIIPCDColorToRGB(OG_c, 'F', 4);
            Eigen::Vector3d N_rgb = ASCIIPCDColorToRGB(N_c, 'F', 4);
            Eigen::Vector3d NZ_rgb = ASCIIPCDColorToRGB(NZ_c, 'F', 4);
            Eigen::Vector3d DU_rgb = ASCIIPCDColorToRGB(DU_c, 'F', 4);

            for (size_t k = 0; k < indices.size(); k++) {
                // include the point itself, compute histogram
                if (input.colors_[indices[k]] == CA_rgb) {
                    int c_h_index = 33;
                    feature->data_(c_h_index, i) += c_hist_incr;
                }
                if (input.colors_[indices[k]] == CZ_rgb) {
                    int c_h_index = 34;
                    feature->data_(c_h_index, i) += c_hist_incr;
                }
                if (input.colors_[indices[k]] == O_rgb) {
                    int c_h_index = 35;
                    feature->data_(c_h_index, i) += c_hist_incr;
                }
                if (input.colors_[indices[k]] == OD1_rgb) {
                    int c_h_index = 36;
                    feature->data_(c_h_index, i) += c_hist_incr;
                }
                if (input.colors_[indices[k]] == OG_rgb) {
                    int c_h_index = 37;
                    feature->data_(c_h_index, i) += c_hist_incr;
                }
                if (input.colors_[indices[k]] == N_rgb) {
                    int c_h_index = 38;
                    feature->data_(c_h_index, i) += c_hist_incr;
                }
                if (input.colors_[indices[k]] == NZ_rgb) {
                    int c_h_index = 39;
                    feature->data_(c_h_index, i) += c_hist_incr;
                }
                if (input.colors_[indices[k]] == DU_rgb) {
                    int c_h_index = 40;
                    feature->data_(c_h_index, i) += c_hist_incr;
                }
            }
        }
    }
    return feature;
}
/*kimeguida*/

}  // unnamed namespace

std::shared_ptr<Feature> ComputeFPFHFeature(
        const PointCloud &input,
        const KDTreeSearchParam &search_param /* = KDTreeSearchParamKNN()*/) {
    auto feature = std::make_shared<Feature>();
    feature->Resize(33, (int)input.points_.size());
    if (input.HasNormals() == false) {
        PrintDebug(
                "[ComputeFPFHFeature] Failed because input point cloud has no "
                "normal.\n");
        return feature;
    }
    KDTreeFlann kdtree(input);
    auto spfh = ComputeSPFHFeature(input, kdtree, search_param);
#ifdef _OPENMP
#pragma omp parallel for schedule(static)
#endif
    for (int i = 0; i < (int)input.points_.size(); i++) {
        const auto &point = input.points_[i];
        std::vector<int> indices;
        std::vector<double> distance2;
        if (kdtree.Search(point, search_param, indices, distance2) > 1) {
            double sum[3] = {0.0, 0.0, 0.0};
            for (size_t k = 1; k < indices.size(); k++) {
                // skip the point itself
                double dist = distance2[k];
                if (dist == 0.0) continue;
                for (int j = 0; j < 33; j++) {
                    double val = spfh->data_(j, indices[k]) / dist;
                    sum[j / 11] += val;
                    feature->data_(j, i) += val;
                }
            }
            for (int j = 0; j < 3; j++)
                if (sum[j] != 0.0) sum[j] = 100.0 / sum[j];
            for (int j = 0; j < 33; j++) {
                feature->data_(j, i) *= sum[j / 11];
                // The commented line is the fpfh function in the paper.
                // But according to PCL implementation, it is skipped.
                // Our initial test shows that the full fpfh function in the
                // paper seems to be better than PCL implementation. Further
                // test required.
                feature->data_(j, i) += spfh->data_(j, i);
            }
        }
    }
    return feature;
}

/*kimeguida*/
Eigen::Vector3d ASCIIPCDColorToRGB(const char *color_ptr,
                                   const char type,
                                   const int size) {
    if ((size == 4) && (type == 'F')) {
        std::uint8_t c_data[4] = {0, 0, 0, 0};
        char *end;
        std::float_t c_value = std::strtof(color_ptr, &end);
        memcpy(c_data, &c_value, 4);
        return Eigen::Vector3d((double)c_data[2] / 255.0,
                               (double)c_data[1] / 255.0,
                               (double)c_data[0] / 255.0);
    } else {
        return Eigen::Vector3d::Zero();
    }
}

std::shared_ptr<Feature> ComputeCFPFHFeature(
        const PointCloud &input,
        const KDTreeSearchParam
                &search_param /* = geometry::KDTreeSearchParamKNN()*/) {
    auto feature = std::make_shared<Feature>();
    feature->Resize(41, (int)input.points_.size());
    if (input.HasNormals() == false) {
        PrintDebug(
                "[ComputeCFPFHFeature] Failed because input point cloud has no "
                "normal.\n");
        return feature;
    }
    if (input.HasColors() == false) {
        PrintDebug(
                "[ComputeCFPFHFeature] Failed because input point cloud has no "
                "color.\n");
        return feature;
    }
    KDTreeFlann kdtree(input);
    auto cspfh = ComputeCSPFHFeature(input, kdtree, search_param);
#ifdef _OPENMP
#pragma omp parallel for schedule(static)
#endif
    for (int i = 0; i < (int)input.points_.size(); i++) {
        const auto &point = input.points_[i];
        std::vector<int> indices;
        std::vector<double> distance2;
        if (kdtree.Search(point, search_param, indices, distance2) > 1) {
            double sum[3] = {0.0, 0.0, 0.0};
            double c_sum = 0;
            for (size_t k = 1; k < indices.size(); k++) {
                // skip the point itself
                double dist = distance2[k];
                if (dist == 0.0) continue;
                for (int j = 0; j < 33; j++) {
                    double val = cspfh->data_(j, indices[k]) / dist;
                    sum[j / 11] += val;
                    feature->data_(j, i) += val;
                }
                for (int j = 33; j < 41; j++) {
                    double val = cspfh->data_(j, indices[k]) / dist;
                    c_sum += val;
                    feature->data_(j, i) += val;
                }
            }
            for (int j = 0; j < 3; j++)
                if (sum[j] != 0.0) sum[j] = 100.0 / sum[j];
            for (int j = 0; j < 33; j++) {
                feature->data_(j, i) *= sum[j / 11];
                // The commented line is the fpfh function in the paper.
                // But according to PCL implementation, it is skipped.
                // Our initial test shows that the full fpfh function in the
                // paper seems to be better than PCL implementation. Further
                // test required.
                feature->data_(j, i) += cspfh->data_(j, i);
            }

            if (c_sum != 0.0) c_sum = 100.0 / c_sum;

            for (int j = 33; j < 41; j++) {
                feature->data_(j, i) *= c_sum;
                feature->data_(j, i) += cspfh->data_(j, i);
            }
        }
    }
    return feature;
}
/*kimeguida*/
}  // namespace open3d


#pragma once

// deal.II includes
#include <deal.II/base/exceptions.h>
#include <deal.II/base/parameter_handler.h>
#include <deal.II/dofs/dof_renumbering.h>
#include <deal.II/grid/grid_generator.h>
#include <deal.II/grid/grid_in.h>
#include <deal.II/grid/grid_tools.h>
#include <deal.II/grid/tria_boundary_lib.h>
#include <deal.II/lac/sparsity_pattern.h>
#include <deal.II/numerics/data_out.h>
#include <deal.II/numerics/vector_tools.h>
#include <yaml-cpp/yaml.h>
#include <cctype>
// own includes
#include "grid_handler_base.hpp"

namespace boltzmann {
// ----------------------------------------------------------------------

/**
 * @brief takes into account *all* ajdacent vertices
 *        when constructing the domain decomposition with METIS
 *        for *periodic* boundary conditions.
 *
 */
template <int dim>
class GridHandlerPeriodic : public GridHandlerBase<dim>
{
 private:
  typedef GridHandlerBase<dim> base_type;

 public:
  GridHandlerPeriodic();
  void init_square(int nref, int nprocs = 1);
  void init(dealii::ParameterHandler& param_handler, int nprocs = 1);
  void init(YAML::Node& config, int nprocs = 1);

 private:
  void make_graph();

 private:
  void load_msh(std::string fname);
  bool is_initialized;
  dealii::SparsityPattern graph;
  using base_type::triangulation_;
  using base_type::dofhandler_;
};

// ----------------------------------------------------------------------
template <int dim>
GridHandlerPeriodic<dim>::GridHandlerPeriodic()
    : GridHandlerBase<dim>()
    , is_initialized(false)
{
  static_assert(dim == 2, "dim must be 2, 3d code is not implemented");
}

// ----------------------------------------------------------------------
template <int dim>
void
GridHandlerPeriodic<dim>::init(dealii::ParameterHandler& param_handler, int nprocs)
{
  int n_refine = param_handler.get_integer("refine phys");
  std::string mesh = param_handler.get("mesh");
  if (!mesh.compare("")) {
    this->init_square(n_refine, nprocs);
  } else {
    throw std::runtime_error(
        "Grids for periodic boundary conditions should be generated and not loaded from file!");
    // load mesh grom file
    this->load_msh(mesh);
    this->dofhandler_.distribute_dofs(this->fe);
    if (nprocs > 1) {
      dealii::GridTools::partition_triangulation(nprocs, triangulation_);
      dealii::DoFRenumbering::subdomain_wise(dofhandler_);
    } else {
      dealii::DoFRenumbering::boost::Cuthill_McKee(dofhandler_);
    }
    is_initialized = true;
  }
}

// ----------------------------------------------------------------------
template <int dim>
void
GridHandlerPeriodic<dim>::init(YAML::Node& config, int nprocs)
{
  if (!config["Mesh"]["file"]) {
    int n_refine = config["Mesh"]["nref"].as<int>();
    this->init_square(n_refine, nprocs);
  } else {
    throw std::runtime_error(
        "Grids for periodic boundary conditions should be generated and not loaded from file!");
    std::string mesh = config["Mesh"]["file"].as<std::string>();
    // load mesh grom file
    this->load_msh(mesh);
    this->dofhandler_.distribute_dofs(this->fe);
    if (nprocs > 1) {
      dealii::GridTools::partition_triangulation(nprocs, triangulation_);
      dealii::DoFRenumbering::boost::Cuthill_McKee(dofhandler_);
      dealii::DoFRenumbering::subdomain_wise(dofhandler_);
    } else {
      dealii::DoFRenumbering::boost::Cuthill_McKee(dofhandler_);
    }
  }
  is_initialized = true;
}

// ----------------------------------------------------------------------
template <int dim>
void
GridHandlerPeriodic<dim>::init_square(int nref, int nprocs)
{
  AssertThrow(!is_initialized, dealii::ExcMessage("Attempt to initialize GridHandler twice"));
  bool colorize = true;
  dealii::GridGenerator::hyper_cube(triangulation_, 0, 1, colorize);
  triangulation_.refine_global(nref);
  dofhandler_.distribute_dofs(this->fe);
  if (nprocs > 1) {
    this->make_graph();
    dealii::GridTools::partition_triangulation(nprocs, graph, triangulation_);
    dealii::DoFRenumbering::boost::Cuthill_McKee(dofhandler_);
    dealii::DoFRenumbering::subdomain_wise(dofhandler_);
  } else {
    dealii::DoFRenumbering::boost::Cuthill_McKee(dofhandler_);
  }
  is_initialized = true;
}

// ----------------------------------------------------------------------
template <int dim>
void
GridHandlerPeriodic<dim>::make_graph()
{
  typedef long int lint;
  lint FUZZY = 1e6;
  typedef typename dealii::Triangulation<dim>::cell_iterator cell_t;
  //  typedef std::pair<cell_t, int> cell_face_pair_t;
  typedef std::map<lint, int> map_t;
  map_t hmap;
  map_t vmap;
  int nfaces = 4;
  dealii::DynamicSparsityPattern csp(triangulation_.n_active_cells());
  for (auto cell = triangulation_.begin_active(); cell != triangulation_.end(); ++cell) {
    for (int i = 0; i < nfaces; ++i) {
      auto neighbor = cell->neighbor(i);
      if (neighbor != triangulation_.end()) {
        csp.add(neighbor->index(), cell->index());
        csp.add(cell->index(), neighbor->index());
      }
    }
    if (cell->at_boundary()) {
      // vertical faces
      for (int f : {0, 1}) {
        if (cell->face(f)->at_boundary()) {
          auto center = cell->face(f)->center();
          // find horizontal neighbor
          auto it = vmap.find(lint(FUZZY * center[1]));
          if (it != vmap.end()) {
            // element found
            int index_other = it->second;
            csp.add(cell->index(), index_other);
            csp.add(index_other, cell->index());
            vmap.erase(it);
          } else {
            vmap.insert(it, std::make_pair(lint(center[1] * FUZZY), cell->index()));
          }
        }
      }
      // horizontal faces
      for (int f : {2, 3}) {
        if (cell->face(f)->at_boundary()) {
          auto center = cell->face(f)->center();
          // find horizontal neighbor
          auto it = hmap.find(lint(FUZZY * center[0]));
          if (it != hmap.end()) {
            // element found
            int index_other = it->second;
            csp.add(cell->index(), index_other);
            csp.add(index_other, cell->index());
            hmap.erase(it);
          } else {
            hmap.insert(it, std::make_pair(lint(center[0] * FUZZY), cell->index()));
          }
        }
      }
    }
  }
  // no single boundary faces
  AssertThrow(hmap.empty(), dealii::ExcMessage("unpaired horizontal face"));
  AssertThrow(vmap.empty(), dealii::ExcMessage("unpaired vertical face"));
  //  graph.reinit(triangulation_.n_active_cells());
  csp.compress();
  graph.copy_from(csp);
}

// ------------------------------------------------------------------------
template <int dim>
void
GridHandlerPeriodic<dim>::load_msh(std::string fname)
{
  dealii::GridIn<2> gridin;
  gridin.attach_triangulation(triangulation_);
  std::ifstream f(fname);
  gridin.read_msh(f);
  std::cout << "Number of active cells: " << triangulation_.n_active_cells() << std::endl;
  std::cout << "Total number of cells: " << triangulation_.n_cells() << std::endl;
}

}  // end namespace boltzmann


#pragma once

#include <cstdlib>
#include <utility>
#include <tuple>
#include <numeric>

#include <boost/unordered_map.hpp>
#include "types.hh"
#include "zone.hh"

/*!
  @brief Generate a zone automaton from a timed automaton
  @tparam NVar the number of variable in TA

  TA to ZA adds states with BFS. Initial configuration is the initial states of ZA. The ZA contain only the states reachable from initial states.
 */
template <int NVar>
void ta2za (const TimedAutomaton<NVar> &TA,ZoneAutomaton &ZA, Zone initialZone = Zone::zero(NVar + 1))
{
  auto initialStates = TA.initialStates;
  if (!ZA.abstractedStates.empty()) {
    for (auto it = initialStates.begin(); it != initialStates.end();) {
      if (std::find(ZA.abstractedStates.begin(), ZA.abstractedStates.end(),std::pair<ZAState,Zone>{TAState(*it),initialZone}) != ZA.abstractedStates.end()) {
        it = initialStates.erase(it);
      } else {
        it++;
      }
    }
  }
  if (initialStates.empty()) {
    return;
  }

  ZA.numOfVariables = NVar;
  //! number of states of ZA
  const State numOfStatesInitial = ZA.abstractedStates.size();
  State numOfStates = numOfStatesInitial;
  if (NVar > 0) {
    initialZone.M = Bounds{*std::max_element(TA.max_constraints.begin(), TA.max_constraints.end()), true};
  } else {
    initialZone.M = Bounds(0, true);
  }

  // make initial state
  numOfStates += initialStates.size();
  ZA.edges.resize(numOfStates);
  const State numOfInitialStatesInitial = ZA.initialStates.size();
  ZA.initialStates.resize (numOfStates);
  iota (ZA.initialStates.begin() + numOfInitialStatesInitial, ZA.initialStates.end(),numOfStatesInitial);

  ZA.abstractedStates.reserve (numOfStates);
  for (const auto &conf : initialStates ) {
    ZA.abstractedStates.push_back (std::make_pair(conf,initialZone));
  }

  /*!
    @brief Current configuration of BFS

    A configuration consistes of a tuple ((conf,\alpha),conf,alpha). Remark (conf,\alpha) is not a pair but just a number representing a state of RA.
   */
  std::vector<std::tuple<RAState,TAState,Zone> > nextConf;
  nextConf.resize (initialStates.size());
  for (std::size_t i = 0; i < nextConf.size();i++) {
    nextConf[i] = std::make_tuple(i + numOfStatesInitial,
                                  ZA.abstractedStates[i+numOfStatesInitial].first,
                                  ZA.abstractedStates[i+numOfStatesInitial].second);
  }

  /*!
    @brief translater from TAState and Region to its corresponding state in RA.

    The type is like this.
    (TAState,Zone) -> RAState
  */
  while (!nextConf.empty ()) {
    std::vector<std::tuple<RAState,TAState, Zone> > currentConf = nextConf;
    nextConf.clear();
    for (const auto &conf : currentConf) {
      Zone nowZone = std::get<2>(conf);
      nowZone.elapse();
      for (const auto &edge : TA.edges.at(std::get<1>(conf))) {
        Zone nextZone = nowZone;

        for (const auto &delta : edge.guard) {
          switch (delta.odr) {
          case Constraint::Order::lt:
            nextZone.tighten(delta.x,-1,{delta.c, false});
            break;
          case Constraint::Order::le:
            nextZone.tighten(delta.x,-1,{delta.c, true});
            break;
          case Constraint::Order::gt:
            nextZone.tighten(-1,delta.x,{-delta.c, false});
            break;
          case Constraint::Order::ge:
            nextZone.tighten(-1,delta.x,{-delta.c, true});
            break;
          }
        }

        if (nextZone.isSatisfiable()) {
          for (auto x : edge.resetVars) {
            nextZone.reset(x);
          }
          nextZone.abstractize();
          nextZone.canonize();
          // nextRegion state is new
          const auto targetStateInZA = ZA.zones_in_za.find(std::make_pair(edge.target, nextZone.toTuple()));

          // targetRegionState is already added
          if (targetStateInZA != ZA.zones_in_za.end()) {
            const NFA::Edge newEdge = {std::get<0>(conf),targetStateInZA->second,edge.c};
            ZA.edges[std::get<0>(conf)].push_back (newEdge);
            ZoneAutomaton::TAEdge taEdge = {edge.source, edge.target, edge.c, edge.resetVars, edge.guard};
            ZA.edgeMap[newEdge.toTuple()] = taEdge;

          } else {
            const NFA::Edge newEdge = {std::get<0>(conf),static_cast<State>(numOfStates),edge.c};
            ZA.edges[std::get<0>(conf)].push_back (newEdge);
            ZoneAutomaton::TAEdge taEdge = {edge.source, edge.target, edge.c, edge.resetVars, edge.guard};
            ZA.edgeMap[newEdge.toTuple()] = taEdge;

            if (binary_search (TA.acceptingStates.begin(),
                               TA.acceptingStates.end (),edge.target)) {
              ZA.acceptingStates.push_back (numOfStates);
            }
            ZA.zones_in_za[std::make_pair(edge.target, nextZone.toTuple())] = numOfStates;
            ZA.abstractedStates.push_back (std::make_pair (edge.target,nextZone));
            nextConf.push_back (std::make_tuple (numOfStates,edge.target, nextZone));
            numOfStates++;
            ZA.edges.resize(numOfStates);
          }
        }
      }
    }
  }
}


#include <kazen/bsdf.h>
#include <kazen/mesh.h>
#include <kazen/frame.h>
#include <kazen/warp.h>
#include <kazen/texture.h>
#include <kazen/mircofacet.h>
#include <kazen/proplist.h>
#include <Eigen/Geometry> // cross()
#include <OpenImageIO/texture.h>
#include <OpenImageIO/ustring.h>
#include <filesystem/resolver.h>

NAMESPACE_BEGIN(kazen)

/**
 * \brief Diffuse / Lambertian BRDF model
 */
class Diffuse : public BSDF {
public:
    Diffuse(const PropertyList &propList) {
        m_albedo = propList.getColor("albedo", Color3f(0.5f));
    }

    /// Evaluate the BRDF model
    Color3f eval(const BSDFQueryRecord &bRec) const {
        /* This is a smooth BRDF -- return zero if the measure
           is wrong, or when queried for illumination on the backside */
        if (bRec.measure != ESolidAngle
            || Frame::cosTheta(bRec.wi) <= 0
            || Frame::cosTheta(bRec.wo) <= 0)
            return Color3f(0.0f);

        /* The BRDF is simply the albedo / pi */
        return m_albedo * INV_PI * Frame::cosTheta(bRec.wo);
    }

    /// Compute the density of \ref sample() wrt. solid angles
    float pdf(const BSDFQueryRecord &bRec) const {
        /* This is a smooth BRDF -- return zero if the measure
           is wrong, or when queried for illumination on the backside */
        if (bRec.measure != ESolidAngle
            || Frame::cosTheta(bRec.wi) <= 0
            || Frame::cosTheta(bRec.wo) <= 0)
            return 0.0f;

        /* Importance sampling density wrt. solid angles:
           cos(theta) / pi.

           Note that the directions in 'bRec' are in local coordinates,
           so Frame::cosTheta() actually just returns the 'z' component.
        */
        return INV_PI * Frame::cosTheta(bRec.wo);
    }

    /// Draw a a sample from the BRDF model
    Color3f sample(BSDFQueryRecord &bRec, const Point2f &sample) const {
        if (Frame::cosTheta(bRec.wi) <= 0)
            return Color3f(0.0f);

        bRec.measure = ESolidAngle;

        /* Warp a uniformly distributed sample on [0,1]^2
           to a direction on a cosine-weighted hemisphere */
        bRec.wo = Warp::squareToCosineHemisphere(sample);

        /* Relative index of refraction: no change */
        bRec.eta = 1.0f;

        /* eval() / pdf() * cos(theta) = albedo. There
           is no need to call these functions. */
        return m_albedo;
    }

    bool isDiffuse() const {
        return true;
    }

    /// Return a human-readable summary
    std::string toString() const {
        return fmt::format(
            "Diffuse[\n"
            "  albedo = {}\n"
            "]", m_albedo.toString());
    }

    EClassType getClassType() const { return EBSDF; }
private:
    Color3f m_albedo;
};

/**
 * \brief Dielectric / Ideal dielectric BSDF
 */
class Dielectric : public BSDF {
public:
    Dielectric(const PropertyList &propList) {
        /* Interior IOR (default: BK7 borosilicate optical glass) */
        m_intIOR = propList.getFloat("intIOR", 1.5046f);

        /* Exterior IOR (default: air) */
        m_extIOR = propList.getFloat("extIOR", 1.000277f);
    }

    Color3f eval(const BSDFQueryRecord &) const {
        /* Discrete BRDFs always evaluate to zero in kazen */
        return Color3f(0.0f);
    }

    float pdf(const BSDFQueryRecord &) const {
        /* Discrete BRDFs always evaluate to zero in kazen */
        return 1.0f;
    }

    Color3f sample(BSDFQueryRecord &bRec, const Point2f &sample) const {
        bRec.measure = EDiscrete;

        auto cosThetaI = Frame::cosTheta(bRec.wi);
        auto fresnelTerm = fresnel(cosThetaI, m_extIOR, m_intIOR);

        if (sample.x() < fresnelTerm) {
            /* Reflect wo = -wi + 2*dot(wi, n)*n. In this case is much simpler:
            https://www.pbr-book.org/3ed-2018/Reflection_Models/Specular_Reflection_and_Transmission*/
            bRec.wo = Vector3f(-bRec.wi.x(), -bRec.wi.y(), bRec.wi.z());
            bRec.eta = 1.f;
            return Color3f(1.0f);
        } else {
            auto n = Vector3f(0.0f, 0.0f, 1.0f);
            auto factor = m_intIOR / m_extIOR;
            /* inside out */
            if (Frame::cosTheta(bRec.wi) < 0.f) {
                factor = m_extIOR / m_intIOR;
                n.z() = -1.0f;
            }
            bRec.wo = refract(-bRec.wi, n, factor);
            bRec.eta = m_intIOR / m_extIOR;
            return Color3f(1.0f);
        }
    }

    std::string toString() const {
        return fmt::format(
            "Dielectric[\n"
            "  intIOR = {},\n"
            "  extIOR = {}\n"
            "]",
            m_intIOR, m_extIOR);
    }
private:
    float m_intIOR, m_extIOR;
};

/**
 * \brief Mirror / Ideal mirror BRDF
 */
class Mirror : public BSDF {
public:
    Mirror(const PropertyList &) { }

    Color3f eval(const BSDFQueryRecord &) const {
        /* Discrete BRDFs always evaluate to zero in kazen */
        return Color3f(0.0f);
    }

    float pdf(const BSDFQueryRecord &) const {
        /* Discrete BRDFs always evaluate to zero in kazen */
        return 1.0f;
    }

    Color3f sample(BSDFQueryRecord &bRec, const Point2f &) const {
        if (Frame::cosTheta(bRec.wi) <= 0)
            return Color3f(0.0f);

        // Reflection in local coordinates
        bRec.wo = Vector3f(
            -bRec.wi.x(),
            -bRec.wi.y(),
             bRec.wi.z()
        );
        bRec.measure = EDiscrete;

        /* Relative index of refraction: no change */
        bRec.eta = 1.0f;

        return Color3f(1.0f);
    }

    std::string toString() const {
        return "Mirror[]";
    }
};

/**
 * \brief Diffuse / Lambertian BRDF model with texture as albedo
 */
class Lambertian : public BSDF {
public:
    Lambertian(const PropertyList &propList) { }

    ~Lambertian() {
        if(!m_albedo) delete m_albedo;
    }

    Color3f eval(const BSDFQueryRecord &bRec) const override {
        /* This is a smooth BRDF -- return zero if the measure
           is wrong, or when queried for illumination on the backside */
        if (bRec.measure != ESolidAngle
            || Frame::cosTheta(bRec.wi) <= 0
            || Frame::cosTheta(bRec.wo) <= 0)
            return Color3f(0.0f);

        /* The BRDF is simply the albedo / pi */
        return m_albedo->eval(bRec.uv) * INV_PI * Frame::cosTheta(bRec.wo);
    }

    float pdf(const BSDFQueryRecord &bRec) const override {
        /* This is a smooth BRDF -- return zero if the measure
           is wrong, or when queried for illumination on the backside */
        if (bRec.measure != ESolidAngle
            || Frame::cosTheta(bRec.wi) <= 0
            || Frame::cosTheta(bRec.wo) <= 0)
            return 0.0f;

        /* Importance sampling density wrt. solid angles:
           cos(theta) / pi.

           Note that the directions in 'bRec' are in local coordinates,
           so Frame::cosTheta() actually just returns the 'z' component.
        */
        return INV_PI * Frame::cosTheta(bRec.wo);
    }

    Color3f sample(BSDFQueryRecord &bRec, const Point2f &sample) const override {
        if (Frame::cosTheta(bRec.wi) <= 0)
            return Color3f(0.0f);

        bRec.measure = ESolidAngle;

        /* Warp a uniformly distributed sample on [0,1]^2
           to a direction on a cosine-weighted hemisphere */
        bRec.wo = Warp::squareToCosineHemisphere(sample);

        /* Relative index of refraction: no change */
        bRec.eta = 1.0f;

        /* eval() / pdf() * cos(theta) = albedo. There
           is no need to call these functions. */
        return m_albedo->eval(bRec.uv);
    }

    void addChild(Object *obj) override {
        switch (obj->getClassType()) {
            case ETexture:
                m_albedo = static_cast<Texture<Color3f>*>(obj);
                break;
            default:
                throw Exception("addChild is not supported other than albedi maps");
        }
    }

    EClassType getClassType() const { return EBSDF; }

    std::string toString() const {
        return fmt::format("Lambertian[]");
    }

private:
    Texture<Color3f>* m_albedo=nullptr;
};

/// normal map (normal switch)
class NormalMap : public BSDF {
public:
    NormalMap(const PropertyList &propList) { }

    ~NormalMap() {
        if(!m_normalMap) delete m_normalMap;
        if(!m_nested) delete m_nested;
    }

    Color3f eval(const BSDFQueryRecord &bRec) const override {
        const Intersection &its = bRec.its;
        Color3f rgb = m_normalMap->eval(its.uv);
        Vector3f n(2*rgb.r()-1, 2*rgb.g()-1, 2*rgb.b()-1);

		if (Frame::cosTheta(bRec.wi) > 0 && Frame::cosTheta(bRec.wo) > 0 && n.dot(bRec.wi) <= 0)
            return m_nested->eval(bRec);

        Intersection perturbed(its);
        perturbed.shFrame = getFrame(its, n.normalized(), bRec.wi);

        BSDFQueryRecord perturbedQuery(
            perturbed.toLocal(its.toWorld(bRec.wi)),
            perturbed.toLocal(its.toWorld(bRec.wo)), bRec.measure);

		if (Frame::cosTheta(bRec.wo) * Frame::cosTheta(perturbedQuery.wo) <= 0)
			return Color3f(0.0f);

        perturbedQuery.uv = bRec.uv;
        perturbedQuery.measure = bRec.measure;
        perturbedQuery.eta = bRec.eta;
        return m_nested->eval(perturbedQuery);
    }

    float pdf(const BSDFQueryRecord &bRec) const override {
        const Intersection &its = bRec.its;
        Color3f rgb = m_normalMap->eval(its.uv);
        Vector3f n(2*rgb.r()-1, 2*rgb.g()-1, 2*rgb.b()-1);

		if (Frame::cosTheta(bRec.wi) > 0 && Frame::cosTheta(bRec.wo) > 0 && n.dot(bRec.wi) <= 0)
            return m_nested->pdf(bRec);

        Intersection perturbed(its);
        perturbed.shFrame = getFrame(its, n.normalized(), bRec.wi);

        BSDFQueryRecord perturbedQuery(
            perturbed.toLocal(its.toWorld(bRec.wi)),
            perturbed.toLocal(its.toWorld(bRec.wo)), bRec.measure);

		if (Frame::cosTheta(bRec.wo) * Frame::cosTheta(perturbedQuery.wo) <= 0)
			return 0.0f;

        perturbedQuery.uv = bRec.uv;
        perturbedQuery.measure = bRec.measure;
        perturbedQuery.eta = bRec.eta;
        return m_nested->pdf(perturbedQuery);
    }

    Color3f sample(BSDFQueryRecord &bRec, const Point2f &sample) const override {
        const Intersection &its = bRec.its;
        Color3f rgb = m_normalMap->eval(its.uv);
        Vector3f n(2*rgb.r()-1, 2*rgb.g()-1, 2*rgb.b()-1);

		if (Frame::cosTheta(bRec.wi) > 0 && n.dot(bRec.wi) <= 0) {
			bRec.eta = 1.0f;
			return m_nested->sample(bRec, sample);
		}

		Intersection perturbed(its);
		perturbed.shFrame = getFrame(its, n.normalized(), bRec.wi);

        BSDFQueryRecord perturbedQuery(perturbed.toLocal(its.toWorld(bRec.wi)));
        perturbedQuery.uv = its.uv;
        perturbedQuery.measure = bRec.measure;
        perturbedQuery.eta = bRec.eta;
        Color3f result = m_nested->sample(perturbedQuery, sample);
        if (!result.isZero()) {
            bRec.wo = its.toLocal(perturbed.toWorld(perturbedQuery.wo));
			bRec.eta = perturbedQuery.eta;
			if (Frame::cosTheta(bRec.wo) * Frame::cosTheta(perturbedQuery.wo) <= 0)
				return Color3f(0.0f);
        }
        return result;
    }

    // https://arxiv.org/abs/1705.01263
    Frame getFrame(const Intersection &its, Vector3f n, Vector3f wi) const {

        // 1. Naive implementation
        Frame result;
        result.n = its.shFrame.toWorld(n).normalized();
        result.s = (its.dpdu - result.n * result.n.dot(its.dpdu)).normalized();
        result.t = result.n.cross(result.s).normalized();

        return result;

        // 2. Normalmap switch (Iray way)
        // Frame result;
		// Vector3f r = 2.0f * n.dot(wi) * n - wi;
		// if (Frame::cosTheta(r) <= 0) {
		// 	// pull up normal (see p. 46 https://arxiv.org/abs/1705.01263)
		// 	r = (r - r.dot(wi) * 1.01f * wi).normalized();
		// 	n = (wi + r).normalized();
		// }

		// Frame frame = its.shFrame;
		// result.n = frame.toWorld(n).normalized();
		// result.s = (its.dpdu - result.n * result.n.dot(its.dpdu)).normalized();
		// result.t = result.n.cross(result.s).normalized();

		// return result;
    }

    void addChild(Object *obj) override {
        switch (obj->getClassType()) {
            case ETexture:
                m_normalMap = static_cast<Texture<Color3f>*>(obj);
                break;
            case EBSDF:
                m_nested = static_cast<BSDF*>(obj);
                break;
            default:
                throw Exception("addChild is not supported other than normal maps and nested BSDF");
        }
    }

    EClassType getClassType() const { return EBSDF; }

    std::string toString() const {
        return fmt::format("NormalMap[]");
    }

private:
    Texture<Color3f>* m_normalMap=nullptr;
    BSDF* m_nested = nullptr;
};

// // Eric Heitz's 2017 - Microfacet-based Normal Mapping for Robust Monte Carlo Path Tracing
// class NormalMapMicrofacet : public BSDF {
// public:
//     NormalMapMicrofacet(const PropertyList &propList) {
//         m_albedo = propList.getColor("albedo", Color3f(0.5f));

//     }

//     ~NormalMapMicrofacet() {
//         if(!m_normalMap) delete m_normalMap;
//         if(!m_nested) delete m_nested;
//     }

//     static float pdot(Vector3f a, Vector3f b) {
//         return std::max(0.f, a.dot(b));
//     }

//     static Vector3f wt(Vector3f wp) {
//         return Vector3f(-wp.x(), -wp.y(), 0.f).normalized();
//     }

//     static float G1(Vector3f wp, Vector3f w) {
//         return std::min(1.f, std::max(0.f, Frame::cosTheta(w)) * std::max(0.f, Frame::cosTheta(wp))
//             / (pdot(w, wp) + pdot(w, wt(wp)) * Frame::sinTheta(wp))
//         );
//     }

//     static float lambda_p(Vector3f wp, Vector3f wi) {
//         float i_dot_p = pdot(wp, wi);
//         return i_dot_p / (i_dot_p + pdot(wt(wp), wi) * Frame::sinTheta(wp));
//     }

// 	Color3f eval(const BSDFQueryRecord &bRec) const {
// 		if (Frame::cosTheta(bRec.wi) <= 0 || Frame::cosTheta(bRec.wo) <= 0)
// 			return Color3f(0.0f);

// 		Vector3f wp;
//         Color3f rgb = m_normalMap->eval(bRec.its.uv);
//         wp = Vector3f(2*rgb.r()-1, 2*rgb.g()-1, 2*rgb.b()-1).normalized();

// 		if (Frame::cosTheta(wp) <= 0 || (std::abs(wp.x()) < 1e-6 && std::abs(wp.y()) < 1e-6))
// 			return m_nested->eval(bRec);

// 		Frame frame_wp(bRec.its.toWorld(wp));
// 		Intersection perturbed_wp(bRec.its);
// 		perturbed_wp.geoFrame = frame_wp;
// 		perturbed_wp.shFrame = frame_wp;
// 		perturbed_wp.wi = perturbed_wp.toLocal(bRec.its.toWorld(bRec.wi));

// 		Vector3f wo_wp = perturbed_wp.toLocal(bRec.its.toWorld(bRec.wo));
// 		Vector3f wt_ = bRec.its.toWorld(wt(wp));
// 		Vector3f wo = bRec.its.toWorld(bRec.wo);
// 		Vector3f wi = bRec.its.toWorld(bRec.wi);
// 		Vector3f wo_reflected = (wo - 2.0f * wo.dot(wt_) * wt_).normalized();
// 		float notShadowedWpMirror = 1.f - G1(wp, bRec.its.toLocal(wo_reflected));
// 		wo_reflected = perturbed_wp.toLocal(wo_reflected);
// 		float shadowing = G1(wp, bRec.wo);

// 		Color3f value(0.f);

// 		float lambda_p_ = lambda_p(wp, bRec.wi);

// 		// i -> p -> o
// 		BSDFQueryRecord evalSingleP(perturbed_wp, perturbed_wp.wi, wo_wp);
// 		value += m_nested->eval(evalSingleP) * (lambda_p_ * shadowing);

// 		// i -> p -> t -> o
// 		if (wo.dot(wt_) > 0) {
// 			BSDFQueryRecord evalDoubleP(perturbed_wp, perturbed_wp.wi, wo_reflected);
// 			value += m_nested->eval(evalDoubleP) * (lambda_p_ * notShadowedWpMirror * shadowing);
// 		}

// 		// i -> t -> p -> o
// 		if (wi.dot(wt_) > 0) {
// 			Vector3f wi_reflected = (wi - 2.0f * wi.dot(wt_) * wt_).normalized();
// 			BSDFQueryRecord evalDoubleT(perturbed_wp, perturbed_wp.toLocal(wi_reflected), wo_wp);
// 			value += m_nested->eval(evalDoubleT) * ((1.f - lambda_p_) * shadowing);
// 		}

// 		return value;
//     }

// 	float pdf(const BSDFQueryRecord &bRec) const {
// 		if (Frame::cosTheta(bRec.wi) <= 0 || Frame::cosTheta(bRec.wo) <= 0)
// 			return 0.0f;

// 		Vector3f wp;
//         Color3f rgb = m_normalMap->eval(bRec.its.uv);
//         wp = Vector3f(2*rgb.r()-1, 2*rgb.g()-1, 2*rgb.b()-1).normalized();

// 		if (Frame::cosTheta(wp) <= 0 || (std::abs(wp.x()) < 1e-6 && std::abs(wp.y()) < 1e-6)) {
// 			return m_nested->pdf(bRec);
// 		}

// 		float probability_wp = lambda_p(wp, bRec.wi);
// 		Frame frameWp(bRec.its.toWorld(wp));
// 		Intersection perturbed_wp(bRec.its);
// 		perturbed_wp.geoFrame = frameWp;
// 		perturbed_wp.shFrame = frameWp;
// 		Vector3f wi = bRec.its.toWorld(bRec.wi);
// 		Vector3f wo = bRec.its.toWorld(bRec.wo);
// 		Vector3f wt_ = bRec.its.toWorld(wt(wp));
// 		float pdf = 0.f;
// 		if (probability_wp > 0.f) {
// 			BSDFQueryRecord queryWp(perturbed_wp, perturbed_wp.toLocal(wi), perturbed_wp.toLocal(wo));
// 			pdf += probability_wp * m_nested->pdf(queryWp) * G1(wp, bRec.wo);

// 			if (wo.dot(wt_) > 1e-6) {
// 				Vector3f woReflected = (wo - 2.0f * wo.dot(wt_) * wt_).normalized();
// 				BSDFQueryRecord queryWpt(perturbed_wp, perturbed_wp.toLocal(wi), perturbed_wp.toLocal(woReflected));
// 				pdf += probability_wp * m_nested->pdf(queryWpt)
// 					* (1.f - G1(wp, bRec.its.toLocal(woReflected)));
// 			}
// 		}

// 		if (probability_wp < 1.f && wi.dot(wt_) > 1e-6) {
// 			Vector3f wiReflected = (wi - 2.0f * wi.dot(wt_) * wt_).normalized();
// 			BSDFQueryRecord queryWtp(perturbed_wp, perturbed_wp.toLocal(wiReflected), perturbed_wp.toLocal(wo));
// 			pdf += (1.f - probability_wp) * m_nested->pdf(queryWtp);
// 		}

// 		return pdf;
// 	}

// 	Color3f sample(BSDFQueryRecord &bRec, const Point2f &sample) const {
// 		if (Frame::cosTheta(bRec.wi) <= 0)
// 			return Color3f(0.0f);

// 		bRec.eta = 1.0f;

// 		Vector3f wp;
//         Color3f rgb = m_normalMap->eval(bRec.its.uv);
//         wp = Vector3f(2*rgb.r()-1, 2*rgb.g()-1, 2*rgb.b()-1).normalized();

// 		if (Frame::cosTheta(wp) <= 0 || (std::abs(wp.x()) < 1e-6 && std::abs(wp.y()) < 1e-6)) {
// 			return m_nested->sample(bRec, sample);
// 		}

// 		Frame frame_wp(bRec.its.toWorld(wp));
// 		Intersection perturbed_wp(bRec.its);
// 		perturbed_wp.geoFrame = frame_wp;
// 		perturbed_wp.shFrame = frame_wp;

// 		Vector3f wt_ = bRec.its.toWorld(wt(wp));

// 		Vector3f wr = -bRec.its.toWorld(bRec.wi);
// 		Color3f energy(1.f);
// 		if (sample.x() < lambda_p(wp, bRec.wi)) {
// 			// sample on wp
// 			perturbed_wp.wi = -perturbed_wp.toLocal(wr);
// 			BSDFQueryRecord query_wp(perturbed_wp, Vector3f(1.f));
// 			energy *= m_nested->sample(query_wp, sample);
// 			// did sampling fail?
// 			if (energy.isZero()) return energy;
// 			wr = perturbed_wp.toWorld(query_wp.wo);
// 			float G1_ = G1(wp, bRec.its.toLocal(wr));

// 			// is the sampled direction shadowed?
// 			if (sample.x() > G1_) {
// 				// reflect on wt
// 				wr = (wr + 2.0f * wt_.dot(-wr) * wt_).normalized();
// 				energy *= G1(wp, bRec.its.toLocal(wr));
// 			}
// 		} else {
// 			// do one reflection if we start at wt
// 			wr = (wr + 2.0f * wt_.dot(-wr) * wt_).normalized();
// 			// sample on wp
// 			perturbed_wp.wi = -perturbed_wp.toLocal(wr);
// 			BSDFQueryRecord query_wp(perturbed_wp, NULL);
// 			energy *= m_nested->sample(query_wp, sample);
// 			// did sampling fail?
// 			if (energy.isZero()) return energy;
// 			wr = perturbed_wp.toWorld(query_wp.wo);
// 			energy *= G1(wp, bRec.its.toLocal(wr));
// 		}
// 		bRec.wo = bRec.its.toLocal(wr);
// 		if (Frame::cosTheta(bRec.wo) <= 0.f) return Color3f(0.f);
// 		return energy;
// 	}

//     void addChild(Object *obj) override {
//         switch (obj->getClassType()) {
//             case ETexture:
//                 m_normalMap = static_cast<Texture<Color3f>*>(obj);
//                 break;
//             case EBSDF:
//                 m_nested = static_cast<BSDF*>(obj);
//                 // m_albedo = Color3f(0.f, 1.0f, 0.f); // this line is for debugging
//                 m_albedo = m_nested->getColor();
//                 break;
//             default:
//                 throw Exception("addChild is not supported other than normal maps and nested BSDF");
//         }
//     }

//     EClassType getClassType() const { return EBSDF; }

//     std::string toString() const {
//         return fmt::format("NormalMapMircofacet[]");
//     }

// private:
//     Color3f m_albedo;
//     Texture<Color3f>* m_normalMap = nullptr;
//     BSDF* m_nested = nullptr;

// };

class GGX : public BSDF {
public:
    GGX(const PropertyList &propList) {
        m_roughness = propList.getFloat("roughness", 0.5f);
        m_anisotropy = propList.getFloat("anisotropy", 0.f);
    }

    ~GGX() {
        if(!m_albedo) delete m_albedo;
    }

    Color3f eval(const BSDFQueryRecord &bRec) const {
        if (Frame::cosTheta(bRec.wi) <= 0 || Frame::cosTheta(bRec.wo) <= 0)
            return Color3f(0.0f);
        Color3f F;
        Color3f albedo = m_albedo->eval(bRec.uv);
        return evaluateGGXSmithBRDF(bRec.wi, bRec.wo, albedo, m_roughness, m_anisotropy, F) * Frame::cosTheta(bRec.wo);
    }

    float pdf(const BSDFQueryRecord &bRec) const {
        if (Frame::cosTheta(bRec.wi) <= 0 || Frame::cosTheta(bRec.wo) <= 0)
            return 0.0f;

        auto H = (bRec.wi + bRec.wo).normalized();
        auto alpha = roughnessToAlpha(m_roughness, m_anisotropy);
        auto denom = 4.0f * bRec.wi.dot(H);
        return computeGGXSmithPDF(bRec.wi, H, alpha) / denom;
    }

    Color3f sample(BSDFQueryRecord &bRec, const Point2f &sample) const {
        if (Frame::cosTheta(bRec.wi) <= 0)
            return Color3f(0.0f);
        Color3f albedo = m_albedo->eval(bRec.uv);
        Color3f color = sampleGGXSmithBRDF(bRec.wi, albedo, m_roughness, m_anisotropy, sample, bRec.wo, bRec.pdf);

        if (Frame::cosTheta(bRec.wo) <= 0)
            return Color3f(0.0f);

        return color * Frame::cosTheta(bRec.wo) / bRec.pdf;

    }

    void addChild(Object *obj) override {
        switch (obj->getClassType()) {
            case ETexture:
                m_albedo = static_cast<Texture<Color3f>*>(obj);
                break;
            default:
                throw Exception("addChild is not supported other than albedi maps");
        }
    }

    std::string toString() const {
        return "GGX[]";
    }

private:
    Texture<Color3f>* m_albedo=nullptr;
    float m_roughness;
    float m_anisotropy;
};

class RoughConductor : public BSDF {
public:
    RoughConductor(const PropertyList &propList) {
        /* RMS surface roughness */
        auto roughness = propList.getFloat("alpha", 0.1f);
        // Specify a minimum alpha value, as the GGX NDF does not support a zero alpha.
        float MIN_ALPHA = 0.001f;
        // Square the roughness value and combine with anisotropy to produce X and Y alpha values.
        m_alpha = std::max(MIN_ALPHA, sqr(roughness));

        /* Eta and K (default: gold) */
        std::string material = propList.getString("material","Au");
        if (material == "Au") {
            m_eta = Color3f(0.1431189557f, 0.3749570432f, 1.4424785571f);
            m_k = Color3f(3.9831604247f, 2.3857207478f, 1.6032152899f);
        } else if (material == "Cu") {
            m_eta = Color3f(0.2004376970f, 0.9240334304f, 1.1022119527f);
            m_k = Color3f(3.9129485033f, 2.4528477015f, 2.1421879552f);
        } else if (material == "Cr") {
            m_eta = Color3f(4.3696828663f, 2.9167024892f, 1.6547005413f);
            m_k = Color3f(5.2064337956f, 4.2313645277f, 3.7549467933f);
        }
    }

    /// Evaluate the fresnel for conductor
    Color3f fresnelCond(float cosThetaI, Color3f eta, Color3f k) const {
        Color3f tmp_f = pow(eta,2) + pow(k,2);
        Color3f tmp = tmp_f * pow(cosThetaI,2);
        Color3f Rparl2 = (tmp - (2.f * eta * cosThetaI) + 1) /
                    (tmp + (2.f * eta * cosThetaI) + 1);
        Color3f Rperp2 = (tmp_f - (2.f * eta * cosThetaI) + pow(cosThetaI,2)) /
                    (tmp_f + (2.f * eta * cosThetaI) + pow(cosThetaI,2));
        return (Rparl2 + Rperp2) / 2.0f;
    }

    /// Evaluate the microfacet normal distribution D
    float evalBeckmann(const Normal3f &m) const {
        float temp = Frame::tanTheta(m) / m_alpha,
              ct = Frame::cosTheta(m), ct2 = ct*ct;

        return std::exp(-temp*temp)
            / (M_PI * m_alpha * m_alpha * ct2 * ct2);
    }

    /// Evaluate Smith's shadowing-masking function G1
    float smithBeckmannG1(const Vector3f &v, const Normal3f &m) const {
        float tanTheta = Frame::tanTheta(v);

        /* Perpendicular incidence -- no shadowing/masking */
        if (tanTheta == 0.0f)
            return 1.0f;

        /* Can't see the back side from the front and vice versa */
        if (m.dot(v) * Frame::cosTheta(v) <= 0)
            return 0.0f;

        float a = 1.0f / (m_alpha * tanTheta);
        if (a >= 1.6f)
            return 1.0f;
        float a2 = a * a;

        /* Use a fast and accurate (<0.35% rel. error) rational
           approximation to the shadowing-masking function */
        return (3.535f * a + 2.181f * a2)
             / (1.0f + 2.276f * a + 2.577f * a2);
    }

    /// Evaluate the BRDF for the given pair of directions
    virtual Color3f eval(const BSDFQueryRecord &bRec) const override {
        if (Frame::cosTheta(bRec.wi) <= 0 || Frame::cosTheta(bRec.wo) <= 0)
            return Color3f(0.0f);
        Vector3f wh = (bRec.wi + bRec.wo).normalized();

        Color3f F = fresnelCond(wh.dot(bRec.wo), m_eta, m_k);
        float D = evalBeckmann(wh);
        float G = smithBeckmannG1(bRec.wi, wh) * smithBeckmannG1(bRec.wo,wh);

        return D * F * G / (4.f * Frame::cosTheta(bRec.wi));
    }

    /// Evaluate the sampling density of \ref sample() wrt. solid angles
    virtual float pdf(const BSDFQueryRecord &bRec) const override {
        if (Frame::cosTheta(bRec.wi) <= 0 || Frame::cosTheta(bRec.wo) <= 0)
            return 0.0f;
        Vector3f wh = (bRec.wi + bRec.wo).normalized();
        float D = evalBeckmann(wh);
        float Jh = 1.f / (4.f * wh.dot(bRec.wo));
        return D * Frame::cosTheta(wh) * Jh;
    }

    /// Sample the BRDF
    virtual Color3f sample(BSDFQueryRecord &bRec, const Point2f &_sample) const override {
        if (Frame::cosTheta(bRec.wi) <= 0)
            return Color3f(0.0f);

        Point2f sample = _sample;
        Vector3f wh = Warp::squareToBeckmann(sample, m_alpha);
        bRec.wo = reflect(bRec.wi, wh).normalized();
        if (Frame::cosTheta(bRec.wo) <= 0)
            return Color3f(0.0f);

        return eval(bRec) / pdf(bRec);
    }

    virtual std::string toString() const override {
        return fmt::format(
            "RoughConductor[\n"
            "  alpha = %f,\n"
            "  intIOR = %f,\n"
            "  extIOR = %f,\n"
            "]",
            m_alpha,
            m_eta,
            m_k
        );
    }
private:
    float m_alpha;
    Color3f m_eta, m_k;
};

class RoughPlastic : public BSDF {
public:
    RoughPlastic(const PropertyList &propList) {
        /* RMS surface roughness */
        auto roughness = propList.getFloat("alpha", 0.1f);
        // Specify a minimum alpha value, as the GGX NDF does not support a zero alpha.
        float MIN_ALPHA = 0.001f;
        // Square the roughness value and combine with anisotropy to produce X and Y alpha values.
        m_alpha = std::max(MIN_ALPHA, sqr(roughness));

        /* Interior IOR (default: BK7 borosilicate optical glass) */
        m_intIOR = propList.getFloat("intIOR", 1.5046f);

        /* Exterior IOR (default: air) */
        m_extIOR = propList.getFloat("extIOR", 1.000277f);

        /* Albedo of the diffuse base material (a.k.a "kd") */
        m_kd = propList.getColor("kd", Color3f(0.5f));

        /* To ensure energy conservation, we must scale the
           specular component by 1-kd.
           While that is not a particularly realistic model of what
           happens in reality, this will greatly simplify the
           implementation. Please see the course staff if you're
           interested in implementing a more realistic version
           of this BRDF. */
        m_ks = 1 - m_kd.maxCoeff();
    }

    /// Evaluate the microfacet normal distribution D
    float evalBeckmann(const Normal3f &m) const {
        float temp = Frame::tanTheta(m) / m_alpha,
              ct = Frame::cosTheta(m), ct2 = ct*ct;

        return std::exp(-temp*temp)
            / (M_PI * m_alpha * m_alpha * ct2 * ct2);
    }

    /// Evaluate Smith's shadowing-masking function G1
    float smithBeckmannG1(const Vector3f &v, const Normal3f &m) const {
        float tanTheta = Frame::tanTheta(v);

        /* Perpendicular inci		return 0.0f;
         * dence -- no shadowing/masking */
        if (tanTheta == 0.0f)
            return 1.0f;

        /* Can't see the back side from the front and vice versa */
        if (m.dot(v) * Frame::cosTheta(v) <= 0)
            return 0.0f;

        float a = 1.0f / (m_alpha * tanTheta);
        if (a >= 1.6f)
            return 1.0f;
        float a2 = a * a;

        /* Use a fast and accurate (<0.35% rel. error) rational
           approximation to the shadowing-masking function */
        return (3.535f * a + 2.181f * a2)
             / (1.0f + 2.276f * a + 2.577f * a2);
    }

    /// Evaluate the BRDF for the given pair of directions
    virtual Color3f eval(const BSDFQueryRecord &bRec) const override {
        if (Frame::cosTheta(bRec.wi) <= 0 || Frame::cosTheta(bRec.wo) <= 0)
            return Color3f(0.0f);

        Vector3f wh = (bRec.wi + bRec.wo).normalized();
		float D = evalBeckmann(wh);
		float F = fresnel((wh.dot(bRec.wo)), m_extIOR, m_intIOR);
		float G = (smithBeckmannG1(bRec.wo, wh) * smithBeckmannG1(bRec.wi, wh));

		return m_kd * INV_PI * Frame::cosTheta(bRec.wo) + m_ks * (D * F * G)
			/ (4.f * Frame::cosTheta(bRec.wi));

    }

    /// Evaluate the sampling density of \ref sample() wrt. solid angles
    virtual float pdf(const BSDFQueryRecord &bRec) const override {
	    if (Frame::cosTheta(bRec.wi) <= 0 || Frame::cosTheta(bRec.wo) <= 0)
            return 0.0f;

        Vector3f wh = (bRec.wi + bRec.wo).normalized();
		float D = evalBeckmann(wh);
		float Jh = 1.f / (4.f * abs(wh.dot(bRec.wo)));

		return m_ks * D * Frame::cosTheta(wh) * Jh + (1- m_ks) * Frame::cosTheta(bRec.wo) * INV_PI;
		// return m_ks * Warp::squareToBeckmannPdf(wh, m_alpha) * Jh + (1 - m_ks) * Warp::squareToCosineHemispherePdf(bRec.wo);
	}

    /// Sample the BRDF
    virtual Color3f sample(BSDFQueryRecord &bRec, const Point2f &_sample) const override {
		if (Frame::cosTheta(bRec.wi) <= 0)
			return Color3f(0.0f);

		if (_sample.x() < m_ks) {
			Point2f sample(_sample.x() / m_ks, _sample.y());
			Vector3f wh = Warp::squareToBeckmann(sample, m_alpha);
			bRec.wo = ((2.f * wh.dot(bRec.wi) * wh) - bRec.wi).normalized();
		} else {
			Point2f sample((_sample.x() - m_ks) / (1 - m_ks), _sample.y());
			bRec.wo = Warp::squareToCosineHemisphere(sample);
		}

		if (Frame::cosTheta(bRec.wo) <= 0)
			return Color3f(0.0f);

		// return eval(bRec) / pdf(bRec) * Frame::cosTheta(bRec.wo);
		return eval(bRec) / pdf(bRec);
	}

    virtual std::string toString() const override {
        return fmt::format(
            "RoughPlastic[\n"
            "  alpha = %f,\n"
            "  intIOR = %f,\n"
            "  extIOR = %f,\n"
            "  kd = %s,\n"
            "  ks = %f\n"
            "]",
            m_alpha,
            m_intIOR,
            m_extIOR,
            m_kd.toString(),
            m_ks
        );
    }
private:
    float m_alpha;
    float m_intIOR, m_extIOR;
    float m_ks;
    Color3f m_kd;
};

/// TODO: Rough Dielectric

/// kazen innercircle standard surface (kiss)
/* References:
* [1] [Physically Based Shading at Disney] https://media.disneyanimation.com/uploads/production/publication_asset/48/asset/s2012_pbs_disney_brdf_notes_v3.pdf
* [2] [Extending the Disney BRDF to a BSDF with Integrated Subsurface Scattering] https://blog.selfshadow.com/publications/s2015-shading-course/burley/s2015_pbs_disney_bsdf_notes.pdf
* [3] [Simon Kallweit's project report] http://simon-kallweit.me/rendercompo2015/report/
* [4] [Microfacet Models for Refraction through Rough Surfaces] https://www.cs.cornell.edu/~srm/publications/EGSR07-btdf.pdf
* [5] [Understanding the Masking-Shadowing Function in Microfacet-Based BRDFs] https://jcgt.org/published/0003/02/03/paper.pdf
* [6] [Sampling the GGX Distribution of Visible Normals] https://jcgt.org/published/0007/04/01/paper.pdf
*/
class KazenStandardSurface : public BSDF {
public:
    KazenStandardSurface(const PropertyList &proplist) {
        m_anisotropy = proplist.getFloat("anisotropy", 0.0f);
        m_specular = proplist.getFloat("specular", 0.5f);
        m_specularTint = proplist.getFloat("specularTint", 0.5f);
        m_clearcoat = proplist.getFloat("clearcoat", 0.0f);
        m_clearcoatRoughness = proplist.getFloat("clearcoatRoughness", 0.5f);
        m_sheen = proplist.getFloat("sheen", 0.0f);
        m_sheenTint = proplist.getFloat("sheenTint", 0.5);
    }

    ~KazenStandardSurface() {
        if(!m_baseColor) delete m_baseColor;
        if(!m_metallic) delete m_metallic;
        if(!m_roughness) delete m_roughness;
    }

    float schlickWeight(float x) const {
        x = math::clamp(1.f - x, 0.f, 1.f);
        auto x2 = x * x;
        return x2 * x2 * x;
    }

    Color3f lerp(const Color3f& c1, const Color3f& c2, float t) const {
        return (1.f - t) * c1 + t * c2;
    }

    float ior(float specular) const {
        return 2.f/(1.f - std::sqrt(0.08*specular)) - 1.f;
    };

    Color3f eval(const BSDFQueryRecord &bRec) const {
        /* This is a smooth BRDF -- return zero if the measure
           is wrong, or when queried for illumination on the backside */
        if (Frame::cosTheta(bRec.wi) <= 0 || Frame::cosTheta(bRec.wo) <= 0)
            return Color3f(0.0f);

        Vector3f V = bRec.wi;
        Vector3f L = bRec.wo;
        Vector3f H = (V + L).normalized();

        // color
        Color3f Cdlin = m_baseColor->eval(bRec.uv);
        auto metallic = m_metallic->eval(bRec.uv).r(); // TODO: single channel?
        auto roughness = m_roughness->eval(bRec.uv).r();
        float Cdlum = Cdlin.getLuminance();
        Color3f Ctint = Cdlum > 0.f ? Cdlin/Cdlum : Color3f(1.f);
		Color3f Ctintmix = 0.08f * m_specular * lerp(Color3f(1.f), Ctint, m_specularTint);
		Color3f Cspec0 = lerp(Ctintmix, Cdlin, metallic);

        // diffuse
        float FL = schlickWeight(L.z());
        float FV = schlickWeight(V.z());
        float FH = schlickWeight(L.dot(H));

        float cosThetaD = V.dot(H);
        float FD90 = 0.5f * 2 * roughness * cosThetaD * cosThetaD;

        float Lambert = (1.f - 0.5f*FL) * (1.f - 0.5f*FV);
        float RR = 2.f * roughness * cosThetaD * cosThetaD;
        float retro_reflection = RR * (FL + FV + FL * FV * (RR - 1.f));

        // sheen
        Color3f Csheen = lerp(Color3f(1.f), Ctint, m_sheenTint);
        Color3f Fsheen = FH * m_sheen * Csheen;

        // specular
        Color3f F;
        Color3f specTerm = evaluateGGXSmithBRDF(V, L, Cspec0, roughness, m_anisotropy, F);

        // clearcoat(ior = 1.5 -> F0 = 0.04)
        float clearcoatRoughness = math::lerp(m_clearcoatRoughness, .01f, .3f);
        Color3f clearcoatTerm = 0.25f * m_clearcoat * evaluateGGXSmithBRDF(V, L, 0.04f, clearcoatRoughness, m_anisotropy, F);

        return ((1.f-metallic)*(Cdlin * INV_PI * (Lambert + retro_reflection) +  Fsheen) +
                (specTerm + clearcoatTerm))* Frame::cosTheta(bRec.wo);

    }

    float pdf(const BSDFQueryRecord &bRec) const override {
        /* This is a smooth BRDF -- return zero if the measure
           is wrong, or when queried for illumination on the backside */
        if (Frame::cosTheta(bRec.wi) <= 0 || Frame::cosTheta(bRec.wo) <= 0)
            return 0.0f;

        // weight: reference - http://simon-kallweit.me/rendercompo2015/report/
        auto metallic = m_metallic->eval(bRec.uv).r();
        float diffuse = (1.f - metallic) * 0.5f;
        // float diffuse =  m_baseColor->eval(bRec.uv).maxCoeff();
        float GTR2 = 1.f / (1.f + m_clearcoat);

        auto H = (bRec.wi + bRec.wo).normalized();
        auto jacobian = 4.0f * bRec.wi.dot(H);

        // specular
        auto roughness = m_roughness->eval(bRec.uv).r();
        auto alpha = roughnessToAlpha(roughness, m_anisotropy);
        auto specPdf = computeGGXSmithPDF(bRec.wi, H, alpha) / jacobian;

        // clearcoat
        auto coatalpha = roughnessToAlpha(math::lerp(m_clearcoatRoughness, .01f, .3f), 0.f);
        float clearcoatPdf = computeGGXSmithPDF(bRec.wi, H, coatalpha) / jacobian;

        return diffuse * INV_PI * Frame::cosTheta(bRec.wo) +
                (1.f-diffuse) * (GTR2*specPdf + (1.f-GTR2)*clearcoatPdf);
    }

    Color3f sample(BSDFQueryRecord &bRec, const Point2f &_sample) const override {
        if (Frame::cosTheta(bRec.wi) <= 0)
            return Color3f(0.0f);

        bRec.measure = ESolidAngle;
        /* Relative index of refraction: no change */
        bRec.eta = 1.0f;

        auto metallic = m_metallic->eval(bRec.uv).r();
        float diffuse = (1.f - metallic) * 0.5f;
        // float diffuse = m_baseColor->eval(bRec.uv).maxCoeff();

		if (_sample.x() < diffuse) {
            auto sample = Point2f(_sample.x()/diffuse, _sample.y());
            bRec.wo = Warp::squareToCosineHemisphere(sample);
		} else {
			auto sample = Point2f((_sample.x()-diffuse) / (1.f-diffuse), _sample.y());
            float GTR2 = 1.f / (1.f + m_clearcoat);

            Vector3f H;
            Point2f sample1;
            bool flip = bRec.wi.z() <= 0.f;
            if (sample.x() < GTR2) {
                sample1 = Point2f(sample.x() / GTR2, sample.y());
                auto roughness = m_roughness->eval(bRec.uv).r();
                Vector2f alpha = roughnessToAlpha(roughness, m_anisotropy);
                H = sampleGGXSmithVNDF(flip ? -bRec.wi : bRec.wi, alpha, sample1);
            } else {
                sample1 = Point2f((sample.x()-GTR2) / (1.f-GTR2), sample.y());
                Vector2f alpha = roughnessToAlpha(math::lerp(m_clearcoatRoughness, 0.01f, .3f), 0.f);
                H = sampleGGXSmithVNDF(flip ? -bRec.wi : bRec.wi, alpha, sample1);
            }
            H = flip ? -H : H;
            // Reflect the view direction across the microfacet normal to get the sample direction.
            bRec.wo = reflect(bRec.wi, H).normalized();
		}

        auto invalid = [&]() {
            return std::isnan(bRec.wo.x()) || std::isnan(bRec.wo.y()) || std::isnan(bRec.wo.z());
        };

        if (Frame::cosTheta(bRec.wo) <= 0 || pdf(bRec) <= Epsilon || invalid())
            return Color3f(0.f);

		// return eval(bRec) / pdf(bRec) * Frame::cosTheta(bRec.wo);
        return eval(bRec) / pdf(bRec);
    }

    void addChild(Object *obj) override {
        switch (obj->getClassType()) {
            case ETexture:
                if( obj->getId() == "baseColor" ) {
                    if (m_baseColor)
                        throw Exception("There is already an baseColor defined!");
                    m_baseColor = static_cast<Texture<Color3f> *>(obj);
                }
                else if ( obj->getId() == "metallic" ) {
                    if (m_metallic)
                        throw Exception("There is already an metallic defined!");
                    m_metallic = static_cast<Texture<Color3f> *>(obj);
                }
                else if ( obj->getId() == "roughness" ) {
                    if (m_roughness)
                        throw Exception("There is already an roughness defined!");
                    m_roughness = static_cast<Texture<Color3f> *>(obj);
                }
                break;
            default:
                throw Exception("addChild is not supported other than baseColor maps");
        }
    }

    std::string toString() const {
        return fmt::format(
            "KazenStandardSurface"
        );
    }

private:
    Texture<Color3f>* m_baseColor=nullptr;
    Texture<Color3f>* m_roughness=nullptr;
    Texture<Color3f>* m_metallic=nullptr;
    float m_anisotropy;
    float m_specular;
    float m_specularTint;
    float m_sheen;
    float m_sheenTint;
    float m_clearcoat;
    float m_clearcoatRoughness;
};

KAZEN_REGISTER_CLASS(Diffuse, "diffuse");
KAZEN_REGISTER_CLASS(Dielectric, "dielectric");
KAZEN_REGISTER_CLASS(Mirror, "mirror");
KAZEN_REGISTER_CLASS(Lambertian, "lambertian");
KAZEN_REGISTER_CLASS(NormalMap, "normalmap");
// KAZEN_REGISTER_CLASS(NormalMapMicrofacet, "normalmap_mircofacet");
KAZEN_REGISTER_CLASS(GGX, "ggx");
KAZEN_REGISTER_CLASS(RoughConductor, "roughconductor");
KAZEN_REGISTER_CLASS(RoughPlastic, "roughplastic");
// KAZEN_REGISTER_CLASS(RoughDieletric, "roughdieletric");
KAZEN_REGISTER_CLASS(KazenStandardSurface, "kazenstandard");
NAMESPACE_END(kazen)