#pragma once
#include "rendering_randomwalk_impl.hxx"
#include "renderingalgorithms_simplebase.hxx"

#include <unordered_map>
#include <boost/functional/hash.hpp>

#pragma GCC diagnostic push
#pragma GCC diagnostic warning "-Wunused-parameter"
#pragma GCC diagnostic warning "-Wunused-variable"

namespace RandomWalk
{
using namespace SimplePixelByPixelRenderingDetails;

// Ref: Veach's Thesis. Chpt. 10.
namespace BdptDetail
{

using NodeContainer = ToyVector<RW::PathNode>;
using PdfContainer  = ToyVector<Pdf>;
using SegmentContainer = ToyVector<RaySegment>;
using PathDensityContainer = ToyVector<double>;
using ConversionFactorContainer = ToyVector<double>;
using SpecularFlagContainer = ToyVector<bool>;
using WeightContainer = ToyVector<Spectral3>;

class SubpathHistory
{
public:
  SubpathHistory(RadianceEstimatorBase& _randomwalk_functions)
    : randomwalk_functions(_randomwalk_functions)
  {
  }

  void Reset()
  {
    // Note: Allocation only on first use. clear() keeps the capacity up. However, incurs dtor calls.
    fwd_conversion_factors.clear();
    bwd_conversion_factors.clear();
    pdfs.clear();
    pdfs_rev.clear();
    nodes.clear();
    betas.clear();
    segments.clear();
    is_parallel_beam = true;
  }

  void Start(const RW::PathNode &start, const Spectral3 &_weight, Pdf _start_pdf)
  {
    // Bah ... copying :-(
    nodes.push_back(start);
    betas.push_back(_weight);
    pdfs.push_back(_start_pdf);  // The probability density of the starting location.
    pdfs_rev.push_back(1.);
    segments.push_back(RaySegment{}); // For convenience
    fwd_conversion_factors.push_back(1.);
    bwd_conversion_factors.push_back(1.);
    assert(_start_pdf > 0 && std::isfinite(_start_pdf)); // Because it is the sample. So the probability to generate it must be positive!
  }

  void AddSegment(const RW::PathNode &end_node, const Spectral3 &_weight, Pdf pdf_prev_scatter, Pdf pdf_prev_rev_scatter, const VolumePdfCoefficients &volume_pdf_coeff, const RaySegment &segment)
  {
    nodes.push_back(end_node);
    const int idx = isize(nodes)-1;
    Spectral3  beta_at_node = betas.back()*_weight;
    betas.push_back(beta_at_node);
    pdfs.push_back(pdf_prev_scatter);
    pdfs_rev.push_back(pdf_prev_rev_scatter);
    segments.push_back(segment);
    is_parallel_beam &= pdf_prev_scatter.IsFromDelta();
    double geom_coeff = randomwalk_functions.PdfConversionFactor(nodes[idx-1], nodes[idx], segments[idx], FwdCoeffs(volume_pdf_coeff), is_parallel_beam);
    fwd_conversion_factors.push_back(geom_coeff);
    geom_coeff = randomwalk_functions.PdfConversionFactor(nodes[idx], nodes[idx-1], segments[idx].Reversed(), BwdCoeffs(volume_pdf_coeff), false);
    bwd_conversion_factors.push_back(geom_coeff);
    assert(pdf_prev_scatter > 0 && std::isfinite(pdf_prev_scatter)); // Because it is the sample. So the probability to generate it must be positive!
    assert(geom_coeff > 0);
  }

  void Finish()
  {
    fwd_conversion_factors.push_back(NaN); // Space for data of connection segment.
    pdfs.push_back(Pdf{});  // Dito.
    pdfs_rev.push_back(Pdf{});
    assert (fwd_conversion_factors.size() == nodes.size() + 1);
    assert (bwd_conversion_factors.size() == nodes.size());
    assert (pdfs.size() == nodes.size() + 1);
    assert (pdfs_rev.size() == pdfs.size());
  }

  void Pop()
  {
    assert (!nodes.empty());
    nodes.pop_back();
    pdfs.pop_back();
    pdfs_rev.pop_back();
    betas.pop_back();
    fwd_conversion_factors.pop_back();
    bwd_conversion_factors.pop_back();
    segments.pop_back();
  }

  int NumNodes() const
  {
    return isize(nodes);
  }

  const RW::PathNode& Node(int i) const
  {
    return nodes[i];
  }

  RW::PathNode& Node(int i)
  {
    return nodes[i];
  }

  const RW::PathNode& NodeFromBack(int i) const
  {
    return nodes[isize(nodes)-i-1];
  }

  const Spectral3& Beta(int i) const
  {
    return betas[i];
  }

  friend class BdptMis;
  friend class BackupAndReplace;
private:
  RadianceEstimatorBase& randomwalk_functions;
  ConversionFactorContainer fwd_conversion_factors;
  ConversionFactorContainer bwd_conversion_factors;
  ToyVector<RaySegment> segments; // TODO: Might be able to get rid of this.
  PdfContainer pdfs; // Initial node probability, then scatter pdf at *previous* node.
  PdfContainer pdfs_rev; // First 1, then scatter pdf with swapped arguments at *previous* node.
  NodeContainer nodes;
  WeightContainer betas;
  bool is_parallel_beam;
};

struct Connection
{
  Pdf eye_pdf;
  Pdf eye_pdf_rev;
  Pdf light_pdf;
  Pdf light_pdf_rev;
  VolumePdfCoefficients volume_pdf_coeff;
  int eye_index;
  int light_index;
  RaySegment segment;
};

using DebugBuffers = std::unordered_map<std::pair<int,int>, Spectral3ImageBuffer, util::pair_hash<int,int>>;

class BackupAndReplace
{
  SubpathHistory &h;
  int node_index;
  Pdf pdf;
  Pdf pdf_rev;
  double fwd_conversion_factor;

  void InitBothPathsNonEmpty(const RW::PathNode &other_end_node, Pdf pdf_node_scatter, Pdf pdf_node_scatter_rev, const RaySegment &segment, const VolumePdfCoefficients &vol_pdf_coeff)
  {
    assert (node_index >= 0 && other_end_node.node_type != RW::NodeType::ZERO_CONTRIBUTION_ABORT_WALK);
    // The pdf arrays contain the pdf at the previous node. So bump the index by one.
    pdf = h.pdfs[node_index+1];
    pdf_rev = h.pdfs_rev[node_index+1];
    // The conversion factor "pointing" to the next node.
    fwd_conversion_factor = h.fwd_conversion_factors[node_index+1];
    h.pdfs[node_index+1] = pdf_node_scatter;
    h.pdfs_rev[node_index+1] = pdf_node_scatter_rev;
    // Replace by conversion factor for going from solid angle at this node to position at the opposite node.
    h.fwd_conversion_factors[node_index+1] = h.randomwalk_functions.PdfConversionFactor(
      h.Node(node_index), other_end_node, segment, FwdCoeffs(vol_pdf_coeff), pdf_node_scatter.IsFromDelta());
  }

  void InitEitherSideEmpty(double one_or_start_position_pdf, double one_or_angular_emission_pdf)
  {
    // If this side has zero vertices, then it is a light (or not-implemented sensor), which was randomly hit,
    // and the pdf should have the node position density in the forward pdf and 1 in the reverse pdf.
    // On the other hand,
    // If the other side has zero vertices, then this is the path that did the random walk to hit the light.
    // It's forward pdf should be set to 1, and the reverse pdf to the angular emission density.
    //assert ((one_or_angular_emission_pdf==1.) ^ (one_or_start_position_pdf==1.));
    pdf = h.pdfs[node_index+1];
    pdf_rev = h.pdfs[node_index+1];
    fwd_conversion_factor = h.fwd_conversion_factors[node_index+1];
    h.pdfs[node_index+1] = one_or_start_position_pdf;
    h.pdfs_rev[node_index+1] = one_or_angular_emission_pdf;
    h.fwd_conversion_factors[node_index+1] = 1.;
  }

public:
  BackupAndReplace(const BackupAndReplace &) = delete;
  BackupAndReplace& operator=(const BackupAndReplace &) = delete;

  BackupAndReplace(SubpathHistory &_h, int _node_index, SubpathHistory &other_h, int other_node_index, Pdf pdf, Pdf pdf_rev, const RaySegment &segment, const VolumePdfCoefficients &vol_pdf_coeff)
    : h{_h}, node_index{_node_index}
  {
    if (_node_index >= 0 && other_node_index >= 0)
      InitBothPathsNonEmpty(other_h.Node(other_node_index), pdf, pdf_rev, segment, vol_pdf_coeff);
    else
      InitEitherSideEmpty(pdf, pdf_rev);
  }

  ~BackupAndReplace()
  {
    h.pdfs[node_index+1] = pdf;
    h.fwd_conversion_factors[node_index+1] = fwd_conversion_factor;
  }
};

class BdptMis
{
public:
  BdptMis(const Scene& scene)
  {
    one_over_number_of_splat_attempts_per_pixel = 1./(scene.GetCamera().xres*scene.GetCamera().yres);
  }

  double Compute(const SubpathHistory &eye, const SubpathHistory &light, const Connection &connection)
  {
    Reset(connection);
    ComputeSpecularFlags(eye, light, connection);
    ComputeForwardSubpathDensities(eye, light, connection.eye_index, connection.light_index, eye_densities);
    ComputeForwardSubpathDensities(light, eye, connection.light_index, connection.eye_index, light_densities);
    FactorInNumberWeight();
    double result = ComputePowerHeuristicWeight(connection);
    //double result = WeightByNumberOfAdmissibleTechniques(); // For debugging!
    return result;
  }
private:
  SpecularFlagContainer specular_flags;
  PathDensityContainer eye_densities;
  PathDensityContainer light_densities;
  int total_node_count;
  double one_over_number_of_splat_attempts_per_pixel;

  void Reset(const Connection &connection)
  {
    total_node_count = connection.eye_index+1+connection.light_index+1;
    specular_flags.clear();
    eye_densities.clear();
    light_densities.clear();
  }

  void ComputeSpecularFlags(const SubpathHistory &eye, const SubpathHistory &light, const Connection &connection)
  {
    for (int i=0; i<=connection.eye_index+1; ++i)
      specular_flags.push_back(eye.pdfs[i].IsFromDelta());
    for (int i=connection.light_index+1; i>=0; --i)
      specular_flags.push_back(light.pdfs[i].IsFromDelta());
    assert(specular_flags.size() == total_node_count + 2);
  }

  void ComputeForwardSubpathDensities(const SubpathHistory &fwd_path, const SubpathHistory &reverse_path,
                                      int idx_fwd, int idx_rev, PathDensityContainer &destination)
  {
    assert (fwd_path.fwd_conversion_factors[0] == 1.);
    destination.push_back(Pdf{1.}); // When there are zero nodes on this subpath.
    for (int i=0; i<=idx_fwd; ++i)
    {
      destination.push_back(destination.back()*
        fwd_path.fwd_conversion_factors[i] * fwd_path.pdfs[i]);
    }
    if (idx_rev >= 0) // Handling the connection segment.
    {
      destination.push_back(destination.back()*
        fwd_path.fwd_conversion_factors[idx_fwd+1] * fwd_path.pdfs[idx_fwd+1]);
    }
    for (int i=idx_rev; i>=1; --i)
    {
      // Multiplying the scatter density at the node i with the conversion factor
      // leading in the reverse direction from node i.
      destination.push_back(destination.back()*
        reverse_path.bwd_conversion_factors[i] * reverse_path.pdfs_rev[i+1]);
    }
    assert(destination.size() == total_node_count + 1);
  }

  double ComputePowerHeuristicWeight(const Connection &connection) const
  {
    /* Simple way to understand the number of admissible techniques in Veach's sense:
     * Imagine there is a very glossy vertex. Sampling via bsdf is low variance because the bsdf and the pdf cancel each other in the nominator/denominator.
     * Now, sampling something else and connecting to the vertex has high variance because only very few time we get a contribution, and if so, the bsdf has very large value because it is strongly peaked.
     * Now make the bsdf sharper, narrower, take limit to Dirac-Delta function.
     * Variance goes to infinity. We cannot do this. And in fact, my 'Evaluate' & 'Pdf' functions return 0 for specular components, no matter what coordinates.
     * What is left are the sampling techniques where only non-specular vertices are connected.
     * So I simply take the segments that have no adjacent specular vertices.
     */
    double nominator = eye_densities[connection.eye_index+1]*
                       light_densities[connection.light_index+1];
    nominator = Sqr(nominator);
    assert (std::isfinite(nominator));
    double denom = 0.;
    for (int s = 0; s <= total_node_count; ++s)
    {
      bool flag = !(specular_flags[s] || specular_flags[s+1]);
      double summand = flag ? (eye_densities[s]*light_densities[total_node_count-s]) : 0.;
      denom += Sqr(summand);
      assert (std::isfinite(denom));
    }
    assert(denom > 0.);
    return nominator/denom;
  }

  void FactorInNumberWeight()
  {
    for (int i=2; i<=total_node_count; ++i)
    {
      eye_densities[i] *= one_over_number_of_splat_attempts_per_pixel;
    }
  }

  double WeightByNumberOfAdmissibleTechniques() const
  {
    int num_tech = 0;
    for (int i = 0; i <= total_node_count; ++i)
    {
      num_tech += !(specular_flags[i] || specular_flags[i+1]);
    }
    assert (num_tech >= 1); // Actually there can be purely specular paths that cannot be sampled.
    return 1./(num_tech + Epsilon);
  }
};

} // namespace BdptDetail

class BdptWorker;

class BdptAlgo : public SimplePixelByPixelRenderingDetails::SimplePixelByPixelRenderingAlgo
{
  friend class BdptWorker;
  bool enable_debug_buffers = false;
  BdptDetail::DebugBuffers debug_buffers;
  BdptDetail::DebugBuffers debug_buffers_mis;
  tbb::spin_mutex debug_buffer_mutex;
public:
  BdptAlgo(const Scene &scene_,const RenderingParameters &render_params_)
    : SimplePixelByPixelRenderingAlgo{render_params_,scene_}
  {}
protected:
  std::unique_ptr<SimplePixelByPixelRenderingDetails::Worker> AllocateWorker(int i) override;
  void PassCompleted() override;
};

class BdptWorker : public RadianceEstimatorBase, public Worker
{
  LambdaSelectionStrategy lambda_selection_factory;
  ToyVector<SensorResponse> rgb_responses;

  struct Splat : public ROI::PointEmitterArray::Response
  {
    Splat(int unit_index, const Spectral3 &weight, int _path_length) :
      ROI::PointEmitterArray::Response{unit_index, weight}, path_length{_path_length} {}
    int path_length = {};
  };

  BdptDetail::SubpathHistory eye_history;
  BdptDetail::SubpathHistory direct_light_history; // The alternate history ;-) .Where a random light is sampled for s=*,t=1 paths, instead of the original one. Contains only a single node.
  BdptDetail::SubpathHistory light_history;
  BdptDetail::BdptMis bdptmis;
  ToyVector<MediumTracker> eye_medium_tracker_before_node;
  Spectral3 total_eye_measurement_contributions;
  ToyVector<Splat> splats;
  Index3 lambda_idx;
  int pixel_index;
  PathContext eye_context;
  PathContext light_context;
  BdptAlgo* master;

public:
  BdptWorker(const Scene &_scene, const AlgorithmParameters &algo_params, BdptAlgo* master)
  : RadianceEstimatorBase{_scene, algo_params},
    lambda_selection_factory{},
    eye_history{*this},
    direct_light_history{*this},
    light_history{*this},
    bdptmis{_scene},
    eye_medium_tracker_before_node{},
    master{master}
  {
    rgb_responses.reserve(1024);
    eye_medium_tracker_before_node.reserve(32);
  }

  RGB RenderPixel(int _pixel_index) override
  {
    eye_history.Reset();
    light_history.Reset();
    total_eye_measurement_contributions = Spectral3{0.};
    splats.clear();
    eye_medium_tracker_before_node.clear();

    pixel_index = _pixel_index;
    auto lambda_selection = lambda_selection_factory.WithWeights(sampler);
    this->lambda_idx = lambda_selection.indices;
    const auto &lambda_weights = lambda_selection.weights;

    eye_context = PathContext{lambda_selection, TransportType::RADIANCE};
    light_context = PathContext{lambda_selection, TransportType::IMPORTANCE};

    {
      Pdf eye_node_pdf;
      RW::PathNode eye_node = SampleSensorEnd(pixel_index, eye_context, eye_node_pdf);
      MediumTracker eye_medium_tracker{scene};
      InitializeMediumTracker(eye_node, eye_medium_tracker);
      eye_history.Start(eye_node, lambda_weights/eye_node_pdf, eye_node_pdf);
      BdptForwardTrace(
        eye_history, eye_medium_tracker,
        [&eye_medium_tracker, this](int ) { this->eye_medium_tracker_before_node.emplace_back(eye_medium_tracker); },
        eye_context);
      eye_history.Finish();
    }

    {
      Pdf light_node_pdf;
      RW::PathNode light_node = SampleEmissiveEnd(light_context, light_node_pdf);
      MediumTracker light_medium_tracker{scene};
      InitializeMediumTracker(light_node, light_medium_tracker);
      light_history.Start(light_node, Spectral3{1./light_node_pdf}, light_node_pdf);
      BdptForwardTrace(
        light_history, light_medium_tracker,
        [](int ) {},
        light_context);
      light_history.Finish();
    }

    assert (light_history.NumNodes() <= max_path_node_count);
    assert (eye_history.NumNodes() <= max_path_node_count);

    for (int eye_idx=0; eye_idx<eye_history.NumNodes(); ++eye_idx)
    {
      if (eye_idx==0 || eye_history.Node(eye_idx).IsScatterNode())
      {
        // Only camera and scatter nodes are allowed to connect to the light path. Sensor nodes only if they are the first node.
        // In particular we must not connect light sources on the eye path to the light path.
        // Surface or volume nodes default to being scatter nodes, so I don't have to consider these explicitly in the above conditional.
        if (eye_idx+1 < max_path_node_count)
          DirectLighting(eye_idx, eye_medium_tracker_before_node[eye_idx]);
        for (int light_idx=1; light_idx<std::min(light_history.NumNodes(), max_path_node_count-eye_idx-1); ++light_idx)
        {
          ConnectWithLightPath(eye_idx, light_idx,  eye_medium_tracker_before_node[eye_idx]);
        }
      }
      if (eye_idx>0) // Would cause error on starting node
        HandleLightHit(eye_idx);
    }

    for (const auto &splat : splats)
    {
      assert(splat.path_length>=2);
      rgb_responses.push_back(SensorResponse{
        splat.unit_index,
        Color::SpectralSelectionToRGB(splat.weight, lambda_idx)
      });
    }

    return Color::SpectralSelectionToRGB(total_eye_measurement_contributions, lambda_idx);
  }

  void DirectLighting(int eye_idx, const MediumTracker &eye_medium_tracker)
  {
    Pdf pdf_sample_light;
    RW::PathNode light_node = SampleEmissiveEnd(light_context, pdf_sample_light);
    direct_light_history.Reset();
    direct_light_history.Start(light_node, Spectral3{1./pdf_sample_light}, pdf_sample_light);
    direct_light_history.Finish();

    ConnectEyeWithOtherPath(eye_idx, 0, direct_light_history, eye_medium_tracker);
  }

  void ConnectWithLightPath(int eye_idx, int light_idx, const MediumTracker &eye_medium_tracker)
  {
    ConnectEyeWithOtherPath(eye_idx, light_idx, light_history, eye_medium_tracker);
  }

  void ConnectEyeWithOtherPath(int eye_idx, int other_idx, BdptDetail::SubpathHistory &other, const MediumTracker &eye_medium_tracker)
  {
    VolumePdfCoefficients volume_pdf_coeff{};
    double eye_pdf, light_pdf;
    ConnectionSegment to_light = CalculateConnection(
      eye_history.Node(eye_idx), eye_medium_tracker, other.Node(other_idx),
      eye_context, light_context, &eye_pdf, &light_pdf, &volume_pdf_coeff);

    if (to_light.weight.isZero())
      return;

    double eye_rev_pdf = ReverseScatterPdf(eye_history.Node(eye_idx), eye_pdf, to_light.segment.ray.dir, this->eye_context);
    double light_rev_pdf = ReverseScatterPdf(light_history.Node(other_idx), light_pdf, -to_light.segment.ray.dir, this->light_context);

    double mis_weight = MisWeight(eye_history, other,
      BdptDetail::Connection{
        Pdf{eye_pdf},
        Pdf{eye_rev_pdf},
        Pdf{light_pdf},
        Pdf{light_rev_pdf},
        volume_pdf_coeff,
        eye_idx,
        other_idx,
        to_light.segment
      }
    );

    Spectral3 path_weight = eye_history.Beta(eye_idx)*other.Beta(other_idx)*to_light.weight;
    AddPathContribution(eye_idx, other_idx, mis_weight, path_weight);
  }

  // Eye node lies on a light source. Compute path contribution.
  void HandleLightHit(int eye_idx)
  {
    assert(eye_history.NumNodes()>=2);
    auto &end_node = eye_history.Node(eye_idx);

    if (!end_node.TryEnableOperationsOnEmitter(scene))
      return;

    double reverse_scatter_pdf = 0.;
    Spectral3 end_weight = Evaluate(
      end_node, -end_node.incident_dir, light_context, &reverse_scatter_pdf);

    VolumePdfCoefficients volume_pdf_coeff{}; // Already covered by the random walk. The SubpathHistory has the coefficients for the last segment.

    double mis_weight = MisWeight(eye_history, light_history,
      BdptDetail::Connection{
        Pdf{1.},
        Pdf{reverse_scatter_pdf},
        GetPdfOfGeneratingSampleOnEmitter(end_node, light_context),
        Pdf{1.},
        volume_pdf_coeff,
        eye_idx,
        -1,
        RaySegment{}
      }
    );

    Spectral3 path_weight = end_weight*eye_history.Beta(eye_idx);
    AddPathContribution(eye_idx, -1, mis_weight, path_weight);

    end_node.EnableOperationOnScattererIfFeasible();
  }

  double MisWeight(BdptDetail::SubpathHistory &eye, BdptDetail::SubpathHistory &light, BdptDetail::Connection &&connection)
  {
    using B = BdptDetail::BackupAndReplace;
    B eye_backup{eye, connection.eye_index, light, connection.light_index, connection.eye_pdf, connection.eye_pdf_rev, connection.segment, connection.volume_pdf_coeff};
    std::swap(connection.volume_pdf_coeff.pdf_scatter_bwd, connection.volume_pdf_coeff.pdf_scatter_fwd);
    B light_backup{light, connection.light_index, eye, connection.eye_index, connection.light_pdf, connection.light_pdf_rev, connection.segment, connection.volume_pdf_coeff};
    return bdptmis.Compute(eye, light, connection);
  }

  void AddPathContribution(int s, int t, double mis_weight, const Spectral3 &path_weight)
  {
    assert(path_weight.allFinite() && std::isfinite(mis_weight));
    int path_length = s+t+2; // +2 Because s and t are 0-based indices.
    assert(path_length >= 2);
    if (s == 0) // Initial eye vertex.
    {
      if (sensor_connection_unit >= 0)
      {
        int num_attempted_paths_per_unit = scene.GetCamera().xres*scene.GetCamera().yres;
        Spectral3 new_weight = path_weight/num_attempted_paths_per_unit;
        splats.emplace_back(
          sensor_connection_unit,
          mis_weight*new_weight,
          path_length
        );
        AddToDebugBuffer(sensor_connection_unit, s, t, mis_weight, new_weight);
        sensor_connection_unit = -1;
      }
    }
    else
    {
      total_eye_measurement_contributions += mis_weight*path_weight;
      AddToDebugBuffer(pixel_index, s, t, mis_weight, path_weight);
    }
  }

  template<class StepCallback>
  void BdptForwardTrace(BdptDetail::SubpathHistory &path, MediumTracker &medium_tracker, StepCallback step_callback, const PathContext &context)
  {
    step_callback(0);

    if (this->max_path_node_count <= 1)
      return;

    int path_node_count = 1;
    while (true)
    {
      VolumePdfCoefficients volume_pdf_coeff;
      auto &current_node = path.NodeFromBack(0);

      StepResult step = TakeRandomWalkStep(
        current_node, medium_tracker, context, &volume_pdf_coeff);

      if (step.node.node_type == RW::NodeType::ZERO_CONTRIBUTION_ABORT_WALK) // Hit a light or particle escaped the scene or aborting.
        break;

      // If this is a light path we must not generate paths with light sources in between the ends.
      if (context.transport==IMPORTANCE && !step.node.IsScatterNode())
        break;

      Pdf reverse_pdf = ReverseScatterPdf(current_node, step.scatter_pdf, step.segment.ray.dir, context);

      // Determine survival of russian roulette early because it modifies beta_factor!
      bool survive = SurvivalAtNthScatterNode(step.beta_factor, path_node_count+1);

      path.AddSegment(step.node, step.beta_factor, step.scatter_pdf, reverse_pdf, volume_pdf_coeff, step.segment);

      step_callback(path_node_count);

      // The new node is used definitely. However now the walk can terminate.
      // It would make more sense maybe to do RR right after evaluating the
      // scatter function. On the other hand, it should not matter how and where
      // I compute the termination (or survival) probability.
      if (!survive)
        break;

      if (step.node.node_type == RW::NodeType::ZERO_CONTRIBUTION_ABORT_WALK ||
          step.node.node_type == RW::NodeType::ENV)
        break;

      ++path_node_count;
    }
  }

  Pdf ReverseScatterPdf(const PathNode &node, Pdf forward_pdf, const Double3 &reverse_incident_dir, const PathContext &context) const
  {
    // Only surface (BSDF) sampling pdf's are allowed to be unsymmetric. There is no need for phase functions to be asymmetric.
    // And emission directions of light sources and sensors don't have a reverse path at all.
    if (node.node_type != NodeType::SURFACE_SCATTER)
      return forward_pdf;
    // Components proportional to Dirac-Delta should be symmetrical because it importance samples the symmetrical bsdf!
    // And then there is really no other place to put the peak other than the symmetrical directions.
    // In that case, the "pdf" value here represents the probability to select the Dirac-Delta component of the material.
    if (forward_pdf.IsFromDelta())
      return forward_pdf;

    const SurfaceInteraction &intersection = node.interaction.surface;
    return GetShaderOf(intersection, scene).Pdf(reverse_incident_dir, intersection, -node.incident_dir, context);
  }

  ToyVector<SensorResponse>& GetSensorResponses() override
  {
    return rgb_responses;
  }

  void AddToDebugBuffer(int unit_index, int s, int t, double mis_weight, const Spectral3 &path_weight)
  {
    if (!master->enable_debug_buffers)
      return;

    tbb::spin_mutex::scoped_lock lock(master->debug_buffer_mutex);
    auto key = std::make_pair(s, t);
    auto it = master->debug_buffers.find(key);
    if (it == master->debug_buffers.end())
    {
      bool _;
      std::tie(it, _) = master->debug_buffers.insert(std::make_pair(
        key, Spectral3ImageBuffer(scene.GetCamera().xres, scene.GetCamera().yres)));

    }
    it->second.Insert(unit_index, Color::SpectralSelectionToRGB(path_weight, lambda_idx));

    if (!path_weight.isZero())
    {
      it = master->debug_buffers_mis.find(key);
      if (it == master->debug_buffers_mis.end())
      {
        bool _;
        std::tie(it, _) = master->debug_buffers_mis.insert(std::make_pair(
          key, Spectral3ImageBuffer(scene.GetCamera().xres, scene.GetCamera().yres)));

      }
      it->second.Insert(unit_index, RGB{Color::RGBScalar{mis_weight}});
    }
  }
};

class PathTracingWorker : public Worker, public RandomWalk::RadianceEstimatorBase
{
  LambdaSelectionStrategy lambda_selection_factory;
  bool do_sample_brdf;
  bool do_sample_lights;
  ToyVector<SensorResponse> rgb_responses;
public:
  PathTracingWorker(const Scene &_scene, const AlgorithmParameters &algo_params)
  : RadianceEstimatorBase{_scene, algo_params},
    lambda_selection_factory{},
    do_sample_brdf{true},
    do_sample_lights{true}
  {
    // Which style of sampling for the last vertex of the path.
    // Defaults to both light and brdf sampling.
    if (algo_params.pt_sample_mode == "bsdf")
    {
      do_sample_lights = false;
    }
    else if (algo_params.pt_sample_mode == "lights")
    {
      do_sample_brdf = false;
    }
    rgb_responses.reserve(1024);
  }

  RGB RenderPixel(int pixel_index) override
  {
    sensor_connection_unit = -1;

    auto lambda_selection = lambda_selection_factory.WithWeights(sampler);
    PathContext context{lambda_selection, TransportType::RADIANCE};
    context.pixel_index = pixel_index;
    PathContext light_context{context}; light_context.transport = IMPORTANCE;
    MediumTracker medium_tracker{scene};
    VolumePdfCoefficients volume_pdf_coeff;
    Spectral3 path_sample_values{0.};

    RW::PathNode prev_node;

    StepResult step;
    step.node = SampleSensorEnd(pixel_index, context, step.scatter_pdf); // Note: abuse of scatter_pdf. This is not about scattering but picking the initial location.

    Spectral3 beta{lambda_selection.weights/step.scatter_pdf};

    InitializeMediumTracker(step.node, medium_tracker);

    int path_node_count = 1;
    while (true)
    {
      if (this->do_sample_lights)
      {
        // Next vertex. Sample a point on a light source. Compute geometry term. Add path weight to total weights.
        Pdf pdf_light;
        RW::PathNode light_node = SampleEmissiveEnd(context, pdf_light);
        double pdf_scatter = NaN;
        volume_pdf_coeff = VolumePdfCoefficients{};
        ConnectionSegment to_light = CalculateConnection(step.node, medium_tracker, light_node, context, light_context, &pdf_scatter, nullptr, &volume_pdf_coeff);
        double pdf_of_light_due_to_scatter = pdf_scatter*PdfConversionFactor(step.node, light_node, to_light.segment, FwdCoeffs(volume_pdf_coeff), false);
        double mis_weight = MisWeight(
          pdf_light, // Maybe this needs multiplication with pmf to select the unit_index.
          pdf_of_light_due_to_scatter);
        Spectral3 path_weight = (mis_weight/pdf_light)*to_light.weight*beta;
        if (step.node.node_type == NodeType::CAMERA)
        {
          if (sensor_connection_unit >= 0)
          {
            int num_attempted_paths = scene.GetCamera().xres*scene.GetCamera().yres;
            path_weight /= num_attempted_paths;
            rgb_responses.push_back({
              sensor_connection_unit,
              Color::SpectralSelectionToRGB(path_weight, lambda_selection.indices)
            }); // This part works via the auto-generated initializer list ctor.
          }
        }
        else
        {
          path_sample_values += path_weight;
        }
      }

      prev_node = step.node;
      volume_pdf_coeff = VolumePdfCoefficients{};
      step = TakeRandomWalkStep(prev_node, medium_tracker, context, &volume_pdf_coeff);

      beta *= step.beta_factor;

      ++path_node_count;

      if (this->do_sample_brdf && step.node.TryEnableOperationsOnEmitter(scene))
      {
        Spectral3 end_weight = Evaluate(step.node, -step.segment.ray.dir, context, nullptr);
        double pdf_direct_sample = GetPdfOfGeneratingSampleOnEmitter(step.node, context);
        Pdf pdf_due_to_scatter = PdfConversionFactor(prev_node, step.node, step.segment, FwdCoeffs(volume_pdf_coeff), step.scatter_pdf.IsFromDelta())*step.scatter_pdf;
        double mis_weight = MisWeight(pdf_due_to_scatter, pdf_direct_sample);
        path_sample_values += mis_weight*end_weight*beta;
        step.node.EnableOperationOnScattererIfFeasible();
      }

      if (step.node.node_type == RW::NodeType::ZERO_CONTRIBUTION_ABORT_WALK ||
          step.node.node_type == RW::NodeType::ENV)
        break;

      bool survive = SurvivalAtNthScatterNode(beta, path_node_count);
      if (!survive)
        break;

      assert(beta.allFinite());
    }

    return Color::SpectralSelectionToRGB(path_sample_values, lambda_selection.indices);
  }

  ToyVector<SensorResponse>& GetSensorResponses() override
  {
    return rgb_responses;
  }

  double MisWeight(Pdf pdf_or_pmf_taken, double pdf_other) const
  {
    double mis_weight = 1.;
    if (!pdf_or_pmf_taken.IsFromDelta() && this->do_sample_brdf && this->do_sample_lights)
    {
      mis_weight = PowerHeuristic(pdf_or_pmf_taken, {pdf_other});
    }
    return mis_weight;
  }
};

} // namespace

using RadianceEstimatorBase = RandomWalk::RadianceEstimatorBase;

#pragma GCC diagnostic pop // Restore command line options


#ifndef STAN_MCMC_HMC_NUTS_BASE_XHMC_HPP
#define STAN_MCMC_HMC_NUTS_BASE_XHMC_HPP

#include <stan/callbacks/logger.hpp>
#include <boost/math/special_functions/fpclassify.hpp>
#include <stan/mcmc/hmc/base_hmc.hpp>
#include <stan/mcmc/hmc/hamiltonians/ps_point.hpp>
#include <algorithm>
#include <cmath>
#include <limits>
#include <string>
#include <vector>
#include <utility>

namespace stan {
namespace mcmc {

/**
 * Exhaustive Hamiltonian Monte Carlo (XHMC) with multinomial sampling.
 * See http://arxiv.org/abs/1601.00225.
 */
template <class Model, template <class, class> class Hamiltonian,
          template <class> class Integrator, class BaseRNG>
class base_xhmc : public base_hmc<Model, Hamiltonian, Integrator, BaseRNG> {
 public:
  base_xhmc(const Model& model, BaseRNG& rng)
      : base_hmc<Model, Hamiltonian, Integrator, BaseRNG>(model, rng),
        depth_(0),
        max_depth_(5),
        max_deltaH_(1000),
        x_delta_(0.1),
        n_leapfrog_(0),
        divergent_(0),
        energy_(0) {}

  ~base_xhmc() {}

  void set_max_depth(int d) {
    if (d > 0)
      max_depth_ = d;
  }

  void set_max_deltaH(double d) { max_deltaH_ = d; }

  void set_x_delta(double d) {
    if (d > 0)
      x_delta_ = d;
  }

  int get_max_depth() { return this->max_depth_; }
  double get_max_deltaH() { return this->max_deltaH_; }
  double get_x_delta() { return this->x_delta_; }

  sample transition(sample& init_sample, callbacks::logger& logger) {
    // Initialize the algorithm
    this->sample_stepsize();

    this->seed(init_sample.cont_params());

    this->hamiltonian_.sample_p(this->z_, this->rand_int_);
    this->hamiltonian_.init(this->z_, logger);

    ps_point z_plus(this->z_);
    ps_point z_minus(z_plus);

    ps_point z_sample(z_plus);
    ps_point z_propose(z_plus);

    double ave = this->hamiltonian_.dG_dt(this->z_, logger);
    double log_sum_weight = 0;  // log(exp(H0 - H0))

    double H0 = this->hamiltonian_.H(this->z_);
    int n_leapfrog = 0;
    double sum_metro_prob = 1;  // exp(H0 - H0)

    // Build a trajectory until the NUTS criterion is no longer satisfied
    this->depth_ = 0;
    this->divergent_ = 0;

    while (this->depth_ < this->max_depth_) {
      // Build a new subtree in a random direction
      bool valid_subtree = false;
      double ave_subtree = 0;
      double log_sum_weight_subtree = -std::numeric_limits<double>::infinity();

      if (this->rand_uniform_() > 0.5) {
        this->z_.ps_point::operator=(z_plus);
        valid_subtree = build_tree(this->depth_, z_propose, ave_subtree,
                                   log_sum_weight_subtree, H0, 1, n_leapfrog,
                                   sum_metro_prob, logger);
        z_plus.ps_point::operator=(this->z_);
      } else {
        this->z_.ps_point::operator=(z_minus);
        valid_subtree = build_tree(this->depth_, z_propose, ave_subtree,
                                   log_sum_weight_subtree, H0, -1, n_leapfrog,
                                   sum_metro_prob, logger);
        z_minus.ps_point::operator=(this->z_);
      }

      if (!valid_subtree)
        break;
      std::tie(ave, log_sum_weight) = stable_sum(
          ave, log_sum_weight, ave_subtree, log_sum_weight_subtree);

      // Sample from an accepted subtree
      ++(this->depth_);

      double accept_prob = std::exp(log_sum_weight_subtree - log_sum_weight);
      if (this->rand_uniform_() < accept_prob)
        z_sample = z_propose;

      // Break if exhaustion criterion is satisfied
      if (std::fabs(ave) < x_delta_)
        break;
    }

    this->n_leapfrog_ = n_leapfrog;

    // Compute average acceptance probabilty across entire trajectory,
    // even over subtrees that may have been rejected
    double accept_prob = sum_metro_prob / static_cast<double>(n_leapfrog + 1);

    this->z_.ps_point::operator=(z_sample);
    this->energy_ = this->hamiltonian_.H(this->z_);
    return sample(this->z_.q, -this->z_.V, accept_prob);
  }

  void get_sampler_param_names(std::vector<std::string>& names) {
    names.push_back("stepsize__");
    names.push_back("treedepth__");
    names.push_back("n_leapfrog__");
    names.push_back("divergent__");
    names.push_back("energy__");
  }

  void get_sampler_params(std::vector<double>& values) {
    values.push_back(this->epsilon_);
    values.push_back(this->depth_);
    values.push_back(this->n_leapfrog_);
    values.push_back(this->divergent_);
    values.push_back(this->energy_);
  }

  /**
   * Recursively build a new subtree to completion or until
   * the subtree becomes invalid.  Returns validity of the
   * resulting subtree.
   *
   * @param depth Depth of the desired subtree
   * @param z_propose State proposed from subtree
   * @param ave Weighted average of dG/dt across trajectory
   * @param log_sum_weight Log of summed weights across trajectory
   * @param H0 Hamiltonian of initial state
   * @param sign Direction in time to built subtree
   * @param n_leapfrog Summed number of leapfrog evaluations
   * @param sum_metro_prob Summed Metropolis probabilities across trajectory
   * @param logger Logger for messages
   * @return whether built tree is valid
   */
  bool build_tree(int depth, ps_point& z_propose, double& ave,
                  double& log_sum_weight, double H0, double sign,
                  int& n_leapfrog, double& sum_metro_prob,
                  callbacks::logger& logger) {
    // Base case
    if (depth == 0) {
      this->integrator_.evolve(this->z_, this->hamiltonian_,
                               sign * this->epsilon_, logger);
      ++n_leapfrog;

      double h = this->hamiltonian_.H(this->z_);
      if (boost::math::isnan(h))
        h = std::numeric_limits<double>::infinity();

      if ((h - H0) > this->max_deltaH_)
        this->divergent_ = true;

      double dG_dt = this->hamiltonian_.dG_dt(this->z_, logger);

      std::tie(ave, log_sum_weight)
          = stable_sum(ave, log_sum_weight, dG_dt, H0 - h);

      if (H0 - h > 0)
        sum_metro_prob += 1;
      else
        sum_metro_prob += std::exp(H0 - h);

      z_propose = this->z_;

      return !this->divergent_;
    }
    // General recursion

    // Build the left subtree
    double ave_left = 0;
    double log_sum_weight_left = -std::numeric_limits<double>::infinity();

    bool valid_left
        = build_tree(depth - 1, z_propose, ave_left, log_sum_weight_left, H0,
                     sign, n_leapfrog, sum_metro_prob, logger);

    if (!valid_left)
      return false;
    std::tie(ave, log_sum_weight)
        = stable_sum(ave, log_sum_weight, ave_left, log_sum_weight_left);

    // Build the right subtree
    ps_point z_propose_right(this->z_);
    double ave_right = 0;
    double log_sum_weight_right = -std::numeric_limits<double>::infinity();

    bool valid_right = build_tree(depth - 1, z_propose_right, ave_right,
                                  log_sum_weight_right, H0, sign, n_leapfrog,
                                  sum_metro_prob, logger);

    if (!valid_right)
      return false;
    std::tie(ave, log_sum_weight)
        = stable_sum(ave, log_sum_weight, ave_right, log_sum_weight_right);

    // Multinomial sample from right subtree
    double ave_subtree;
    double log_sum_weight_subtree;
    std::tie(ave_subtree, log_sum_weight_subtree) = stable_sum(
        ave_left, log_sum_weight_left, ave_right, log_sum_weight_right);

    double accept_prob
        = std::exp(log_sum_weight_right - log_sum_weight_subtree);
    if (this->rand_uniform_() < accept_prob)
      z_propose = z_propose_right;

    return std::abs(ave_subtree) >= x_delta_;
  }

  /**
   * a1 and a2 are running averages of the form
   *   \f$ a1 =   ( \sum_{n \in N1} w_{n} f_{n} )
   *            / ( \sum_{n \in N1}  w_{n} ) \f$
   *   \f$ a2 =   ( \sum_{n \in N2} w_{n} f_{n} )
   *            / ( \sum_{n \in N2}  w_{n} ) \f$
   * and the weights are the respective normalizing constants
   *   \f$ w1 = \sum_{n \in N1} w_{n} \f$
   *   \f$ w2 = \sum_{n \in N2} w_{n}. \f$
   *
   * This function returns the pooled average
   *   \f$ sum_a =   ( \sum_{n \in N1 \cup N2} w_{n} f_{n} )
   *               / ( \sum_{n \in N1 \cup N2}  w_{n} ) \f$
   * and the pooled weights
   *   \f$ log_sum_w = log(w1 + w2). \f$
   *
   * @param[in] a1 First running average, f1 / w1
   * @param[in] log_w1 Log of first summed weight
   * @param[in] a2 Second running average
   * @param[in] log_w2 Log of second summed weight
   * @return Pair of average of input running averages and log of summed input
   * weights
   */
  static std::pair<double, double> stable_sum(double a1, double log_w1,
                                              double a2, double log_w2) {
    if (log_w2 > log_w1) {
      const double e = std::exp(log_w1 - log_w2);
      return std::make_pair((e * a1 + a2) / (1 + e), log_w2 + std::log1p(e));
    } else {
      const double e = std::exp(log_w2 - log_w1);
      return std::make_pair((a1 + e * a2) / (1 + e), log_w1 + std::log1p(e));
    }
  }

  int depth_;
  int max_depth_;
  double max_deltaH_;
  double x_delta_;

  int n_leapfrog_;
  bool divergent_;
  double energy_;
};

}  // namespace mcmc
}  // namespace stan
#endif


#include "framework/builder/framework_builder.hpp"

#include <deal.II/base/conditional_ostream.h>
#include <deal.II/base/mpi.h>
#include <sstream>
#include <fstream>
#include <quadrature/calculators/angular_flux_integrator.hpp>
#include <quadrature/calculators/quadrature_calculators_factories.hpp>
#include <calculator/drift_diffusion/drift_diffusion_vector_calculator.hpp>
#include <calculator/drift_diffusion/factory.hpp>
#include <formulation/scalar/scalar_formulation_factory.hpp>
#include <formulation/updater/formulation_updater_factories.hpp>
#include <iteration/subroutine/get_scalar_flux_from_framework.hpp>

// Builders & factories
#include "solver/builder/solver_builder.hpp"

// Convergence classes
#include "convergence/iteration_completion_checker.hpp"
#include "convergence/moments/convergence_checker_l1_norm.hpp"
#include "convergence/moments/convergence_checker_l_infinity_norm.hpp"
#include "convergence/moments/multi_moment_checker_max.hpp"
#include "convergence/parameters/single_parameter_checker.hpp"

// Domain classes
#include "domain/domain.hpp"
#include "domain/finite_element/finite_element_gaussian.hpp"
#include "domain/mesh/mesh_cartesian.hpp"

// Formulation classes
#include "formulation/angular/self_adjoint_angular_flux.h"
#include "formulation/scalar/diffusion.hpp"
#include "formulation/scalar/drift_diffusion.hpp"
#include "formulation/stamper.hpp"
#include "formulation/updater/saaf_updater.h"
#include "formulation/updater/diffusion_updater.hpp"
#include "formulation/updater/drift_diffusion_updater.hpp"

// Framework class
#include "framework/framework.hpp"

// KEffective Updater Classes
#include "calculator/cell/total_aggregated_fission_source.hpp"
#include "calculator/cell/integrated_fission_source.hpp"
#include "eigenvalue/k_eigenvalue/calculator_via_fission_source.hpp"
#include "eigenvalue/k_eigenvalue/calculator_via_rayleigh_quotient.hpp"

// Material classes
#include "data/material/material_protobuf.hpp"

// Iteration classes
#include "iteration/initializer/initialize_fixed_terms_once.h"
#include "iteration/initializer/initialize_fixed_terms_reset_moments.hpp"
#include "iteration/group/group_solve_iteration.hpp"
#include "iteration/group/group_source_iteration.hpp"
#include "iteration/outer/outer_power_iteration.hpp"
#include "iteration/outer/outer_fixed_source_iteration.hpp"

// Quadrature classes & factories
#include "quadrature/quadrature_generator_i.h"
#include "quadrature/factory/quadrature_factories.h"
#include "quadrature/utility/quadrature_utilities.h"

// Results class
#include "results/output_dealii_vtu.h"

// System classes
#include "system/system.hpp"
#include "system/solution/mpi_group_angular_solution.h"
#include "system/solution/solution_types.h"

// Instrumentation
#include "instrumentation/builder/instrument_builder.hpp"

namespace bart::framework::builder {

namespace  {

using InstrumentBuilder = instrumentation::builder::InstrumentBuilder;
using InstrumentName = instrumentation::builder::InstrumentName;
using StringColorPair = std::pair<std::string, utility::Color>;

} // namespace

template<int dim>
FrameworkBuilder<dim>::FrameworkBuilder(std::unique_ptr<Validator> validator_ptr)
    : validator_ptr_(std::move(validator_ptr)) {}

// =============================================================================

template<int dim>
auto FrameworkBuilder<dim>::BuildAngularFluxIntegrator(const std::shared_ptr<QuadratureSet> quadrature_set_ptr)
-> std::unique_ptr<AngularFluxIntegrator> {
  return quadrature::calculators::AngularFluxIntegrator<dim>::Factory::get()
      .GetConstructor(quadrature::calculators::AngularFluxIntegratorName::kDefaultImplementation)(quadrature_set_ptr);
}

template<int dim>
auto FrameworkBuilder<dim>::BuildDiffusionFormulation(const std::shared_ptr<FiniteElement>& finite_element_ptr,
                                                      const std::shared_ptr<data::cross_sections::CrossSectionsI>& cross_sections_ptr,
                                                      const DiffusionFormulationImpl implementation)
-> std::unique_ptr<DiffusionFormulation> {
  ReportBuildingComponant("Diffusion formulation");
  std::unique_ptr<DiffusionFormulation> return_ptr = nullptr;

  if (implementation == DiffusionFormulationImpl::kDefault) {
    using ReturnType = formulation::scalar::Diffusion<dim>;
    return_ptr = std::move(std::make_unique<ReturnType>(finite_element_ptr, cross_sections_ptr));
  }
  ReportBuildSuccess(return_ptr->description());

  return return_ptr;
}

template<int dim>
auto FrameworkBuilder<dim>::BuildDriftDiffusionFormulation(
    const std::shared_ptr<AngularFluxIntegrator>& angular_flux_integrator_ptr,
    const std::shared_ptr<FiniteElement>& finite_element_ptr,
    const std::shared_ptr<data::cross_sections::CrossSectionsI>& cross_sections_ptr) -> std::unique_ptr<DriftDiffusionFormulation> {
  auto drift_diffusion_vector_calculator_ptr = Shared(calculator::drift_diffusion::DriftDiffusionVectorCalculatorIFactory<dim>::get()
      .GetConstructor(calculator::drift_diffusion::DriftDiffusionVectorCalculatorName::kDefaultImplementation)());
  return formulation::scalar::DriftDiffusion<dim>::Factory::get()
      .GetConstructor(formulation::scalar::DriftDiffusionFormulationName::kDefaultImplementation)(
          finite_element_ptr, cross_sections_ptr, drift_diffusion_vector_calculator_ptr, angular_flux_integrator_ptr);
}

template<int dim>
auto FrameworkBuilder<dim>::BuildDomain(FrameworkParameters::DomainSize domain_size,
                                        FrameworkParameters::NumberOfCells number_of_cells,
                                        const std::shared_ptr<FiniteElement>& finite_element_ptr,
                                        std::string material_mapping) -> std::unique_ptr<Domain> {
  std::unique_ptr<Domain> return_ptr = nullptr;
  try {
    ReportBuildingComponant("Mesh");
    auto mesh_ptr = std::make_unique<domain::mesh::MeshCartesian<dim>>(
        domain_size.get(), number_of_cells.get(), material_mapping);
    ReportBuildSuccess(mesh_ptr->description());

    ReportBuildingComponant("Domain");
    return_ptr = std::move(std::make_unique<domain::Domain<dim>>(std::move(mesh_ptr), finite_element_ptr));
    ReportBuildSuccess(return_ptr->description());
  } catch (...) {
    ReportBuildError();
    throw;
  }
  return return_ptr;
}

template<int dim>
auto FrameworkBuilder<dim>::BuildFiniteElement(problem::CellFiniteElementType finite_element_type,
                                               problem::DiscretizationType discretization_type,
                                               FrameworkParameters::PolynomialDegree polynomial_degree)
-> std::unique_ptr<FiniteElement> {
  ReportBuildingComponant("Cell finite element basis");
  std::unique_ptr<FiniteElement> return_ptr{ nullptr };

  try {
    AssertThrow(polynomial_degree.get() > 0, dealii::ExcMessage("Bad polynomial degree"))
    switch (finite_element_type) {
      case problem::CellFiniteElementType::kGaussian: {
        using ReturnType = domain::finite_element::FiniteElementGaussian<dim>;
        return_ptr = std::move(std::make_unique<ReturnType>(discretization_type, polynomial_degree.get()));
      }
    }
  } catch (...) {
    ReportBuildError();
    throw;
  }
  ReportBuildSuccess(return_ptr->description());
  return return_ptr;
}

template<int dim>
auto FrameworkBuilder<dim>::BuildUpdaterPointers(
    std::unique_ptr<DiffusionFormulation> diffusion_formulation_ptr,
    std::unique_ptr<DriftDiffusionFormulation> drift_diffusion_formulation_ptr,
    std::shared_ptr<Stamper> stamper_ptr,
    std::shared_ptr<AngularFluxIntegrator> angular_flux_integrator_ptr,
    std::shared_ptr<SphericalHarmonicMoments> higher_order_moments_ptr,
    AngularFluxStorage& angular_flux_storage,
    const std::map<problem::Boundary, bool>& reflective_boundaries) -> UpdaterPointers {
  ReportBuildingComponant("Building Drift-Diffusion Formulation updater");
  UpdaterPointers return_struct;

  std::unordered_set<problem::Boundary> reflective_boundary_set;

  for (const auto boundary_pair : reflective_boundaries) {
    if (boundary_pair.second)
      reflective_boundary_set.insert(boundary_pair.first);
  }

  using ReturnType = formulation::updater::DriftDiffusionUpdater<dim>;
  auto implementation_name{ formulation::updater::DriftDiffusionUpdaterName::kDefaultImplementation };
  auto drift_diffusion_updater_ptr = Shared(ReturnType::Factory::get().GetConstructor(implementation_name)(
      std::move(diffusion_formulation_ptr),
      std::move(drift_diffusion_formulation_ptr),
      stamper_ptr,
      angular_flux_integrator_ptr,
      higher_order_moments_ptr,
      angular_flux_storage,
      reflective_boundary_set));

  return_struct.fixed_updater_ptr = drift_diffusion_updater_ptr;
  return_struct.fission_source_updater_ptr = drift_diffusion_updater_ptr;
  return_struct.scattering_source_updater_ptr = drift_diffusion_updater_ptr;

  return return_struct;
}

template<int dim>
auto FrameworkBuilder<dim>::BuildUpdaterPointers(
    std::unique_ptr<DiffusionFormulation> formulation_ptr,
    std::unique_ptr<Stamper> stamper_ptr,
    const std::map<problem::Boundary, bool>& reflective_boundaries) -> UpdaterPointers {
  ReportBuildingComponant("Building Diffusion Formulation updater");
  UpdaterPointers return_struct;

  std::unordered_set<problem::Boundary> reflective_boundary_set;

  for (const auto boundary_pair : reflective_boundaries) {
    if (boundary_pair.second)
      reflective_boundary_set.insert(boundary_pair.first);
  }

  using ReturnType = formulation::updater::DiffusionUpdater<dim>;

  auto diffusion_updater_ptr = std::make_shared<ReturnType>(std::move(formulation_ptr),
                                                            std::move(stamper_ptr),
                                                            reflective_boundary_set);
  ReportBuildSuccess(diffusion_updater_ptr->description());
  return_struct.fixed_updater_ptr = diffusion_updater_ptr;
  return_struct.scattering_source_updater_ptr = diffusion_updater_ptr;
  return_struct.fission_source_updater_ptr = diffusion_updater_ptr;

  return return_struct;
}

template<int dim>
auto FrameworkBuilder<dim>::BuildUpdaterPointers(
    std::unique_ptr<SAAFFormulation> formulation_ptr,
    std::unique_ptr<Stamper> stamper_ptr,
    const std::shared_ptr<QuadratureSet>& quadrature_set_ptr) -> UpdaterPointers {
  ReportBuildingComponant("Building SAAF Formulation updater");
  UpdaterPointers return_struct;

  using ReturnType = formulation::updater::SAAFUpdater<dim>;
  auto saaf_updater_ptr = std::make_shared<ReturnType>(std::move(formulation_ptr),
                                                       std::move(stamper_ptr),
                                                       quadrature_set_ptr);
  ReportBuildSuccess(saaf_updater_ptr->description());
  return_struct.fixed_updater_ptr = saaf_updater_ptr;
  return_struct.scattering_source_updater_ptr = saaf_updater_ptr;
  return_struct.fission_source_updater_ptr = saaf_updater_ptr;

  return return_struct;
}

template<int dim>
auto FrameworkBuilder<dim>::BuildUpdaterPointers(
    std::unique_ptr<SAAFFormulation> formulation_ptr,
    std::unique_ptr<Stamper> stamper_ptr,
    const std::shared_ptr<QuadratureSet>& quadrature_set_ptr,
    const std::map<problem::Boundary, bool>& reflective_boundaries,
    const AngularFluxStorage& angular_flux_storage) -> UpdaterPointers {
  ReportBuildingComponant("Building SAAF Formulation updater (with boundary conditions update)");
  UpdaterPointers return_struct;

  // Transform map into set

  std::unordered_set<problem::Boundary> reflective_boundary_set;

  for (const auto boundary_pair : reflective_boundaries) {
    if (boundary_pair.second)
      reflective_boundary_set.insert(boundary_pair.first);
  }

  using ReturnType = formulation::updater::SAAFUpdater<dim>;
  auto saaf_updater_ptr = std::make_shared<ReturnType>(std::move(formulation_ptr),
                                                       std::move(stamper_ptr),
                                                       quadrature_set_ptr,
                                                       angular_flux_storage,
                                                       reflective_boundary_set);
  ReportBuildSuccess(saaf_updater_ptr->description());

  return_struct.fixed_updater_ptr = saaf_updater_ptr;
  return_struct.scattering_source_updater_ptr = saaf_updater_ptr;
  return_struct.fission_source_updater_ptr = saaf_updater_ptr;
  return_struct.boundary_conditions_updater_ptr = saaf_updater_ptr;

  return return_struct;
}

template <int dim>
auto FrameworkBuilder<dim>::BuildGroupSolveIteration(
    std::unique_ptr<SingleGroupSolver> single_group_solver_ptr,
    std::unique_ptr<MomentConvergenceChecker> moment_convergence_checker_ptr,
    std::unique_ptr<MomentCalculator> moment_calculator_ptr,
    const std::shared_ptr<GroupSolution>& group_solution_ptr,
    const UpdaterPointers& updater_ptrs,
    std::unique_ptr<MomentMapConvergenceChecker> moment_map_convergence_checker_ptr)
    -> std::unique_ptr<GroupSolveIteration> {
  std::unique_ptr<GroupSolveIteration> return_ptr = nullptr;

  ReportBuildingComponant("Iterative group solver");

  if (updater_ptrs.boundary_conditions_updater_ptr == nullptr) {
    return_ptr = std::move(
        std::make_unique<iteration::group::GroupSourceIteration<dim>>(
            std::move(single_group_solver_ptr),
            std::move(moment_convergence_checker_ptr),
            std::move(moment_calculator_ptr),
            group_solution_ptr,
            updater_ptrs.scattering_source_updater_ptr,
            std::move(moment_map_convergence_checker_ptr))
    );
  } else {
    return_ptr = std::move(
        std::make_unique<iteration::group::GroupSourceIteration<dim>>(
            std::move(single_group_solver_ptr),
            std::move(moment_convergence_checker_ptr),
            std::move(moment_calculator_ptr),
            group_solution_ptr,
            updater_ptrs.scattering_source_updater_ptr,
            updater_ptrs.boundary_conditions_updater_ptr,
            std::move(moment_map_convergence_checker_ptr))
    );
  }

  using ConvergenceStatusPort = iteration::group::data_ports::ConvergenceStatusPort;
  using StatusPort = iteration::group::data_ports::StatusPort;

  instrumentation::GetPort<ConvergenceStatusPort>(*return_ptr)
      .AddInstrument(convergence_status_instrument_ptr_);
  instrumentation::GetPort<StatusPort>(*return_ptr)
      .AddInstrument(status_instrument_ptr_);

  validator_ptr_->AddPart(FrameworkPart::ScatteringSourceUpdate);
  ReportBuildSuccess(return_ptr->description());
  return return_ptr;
}

template<int dim>
auto FrameworkBuilder<dim>::BuildGroupSolution(const int n_angles) -> std::unique_ptr<GroupSolution> {
  std::unique_ptr<GroupSolution> return_ptr = nullptr;
  ReportBuildingComponant("Group solution");

  return_ptr = std::move(std::make_unique<system::solution::MPIGroupAngularSolution>(n_angles));
  ReportBuildSuccess(return_ptr->description());
  return return_ptr;
}

template<int dim>
auto FrameworkBuilder<dim>::BuildInitializer(const std::shared_ptr<FixedTermUpdater>& updater_ptr,
                                             const int total_groups,
                                             const int total_angles,
                                             const InitializerName implementation) -> std::unique_ptr<Initializer> {
  ReportBuildingComponant("Initializer");

  std::unique_ptr<Initializer> return_ptr = nullptr;

  if (implementation == InitializerName::kInitializeFixedTermsOnce) {
    using InitializeOnceType = iteration::initializer::InitializeFixedTermsOnce;
    return_ptr = std::move(std::make_unique<InitializeOnceType>(updater_ptr, total_groups, total_angles));
  } else if (implementation == InitializerName::kInitializeFixedTermsAndResetMoments) {
    using InitializeAndResetMoments = iteration::initializer::InitializeFixedTermsResetMoments;
    return_ptr = std::move(std::make_unique<InitializeAndResetMoments>(updater_ptr, total_groups, total_angles));
  }

  ReportBuildSuccess(return_ptr->description());
  return return_ptr;
}

template<int dim>
auto FrameworkBuilder<dim>::BuildInitializer(const std::shared_ptr<FixedTermUpdater>& updater_ptr,
                                             const int total_groups,
                                             const int total_angles) -> std::unique_ptr<Initializer> {
  return BuildInitializer(updater_ptr, total_groups, total_angles, InitializerName::kInitializeFixedTermsOnce);
}

template<int dim>
auto FrameworkBuilder<dim>::BuildKEffectiveUpdater() -> std::unique_ptr<KEffectiveUpdater> {
  using ReturnType = eigenvalue::k_eigenvalue::CalculatorViaRayleighQuotient;
  ReportBuildingComponant("K_Effective updater");
  std::unique_ptr<KEffectiveUpdater> return_ptr{ nullptr };
  return_ptr = std::move(std::make_unique<ReturnType>());
  ReportBuildSuccess(return_ptr->description());
  return return_ptr;
}

template<int dim>
auto FrameworkBuilder<dim>::BuildKEffectiveUpdater(
    const std::shared_ptr<FiniteElement>& finite_element_ptr,
    const std::shared_ptr<CrossSections>& cross_sections_ptr,
    const std::shared_ptr<Domain>& domain_ptr)
-> std::unique_ptr<KEffectiveUpdater> {
  using AggregatedFissionSource = calculator::cell::TotalAggregatedFissionSource<dim>;
  using IntegratedFissionSource = calculator::cell::IntegratedFissionSource<dim>;
  using ReturnType = eigenvalue::k_eigenvalue::CalculatorViaFissionSource;

  ReportBuildingComponant("K_Effective updater");
  std::unique_ptr<KEffectiveUpdater> return_ptr = nullptr;

  return_ptr = std::move(
      std::make_unique<ReturnType>(
          std::make_unique<AggregatedFissionSource>(
              std::make_unique<IntegratedFissionSource>(finite_element_ptr,
                                                        cross_sections_ptr),
              domain_ptr),
          2.0,
          10));

  ReportBuildSuccess(return_ptr->description());
  return return_ptr;
}

template<int dim>
auto FrameworkBuilder<dim>::BuildMomentCalculator(MomentCalculatorImpl implementation)
-> std::unique_ptr<MomentCalculator> {
  return BuildMomentCalculator(nullptr, implementation);
}

template<int dim>
auto FrameworkBuilder<dim>::BuildMomentCalculator(std::shared_ptr<QuadratureSet> quadrature_set_ptr,
                                                  FrameworkBuilder::MomentCalculatorImpl implementation)
-> std::unique_ptr<MomentCalculator> {
  ReportBuildingComponant("Moment calculator");
  std::unique_ptr<MomentCalculator> return_ptr = nullptr;

  try {
    return_ptr = std::move(quadrature::factory::MakeMomentCalculator<dim>(implementation, quadrature_set_ptr));

    if (implementation == MomentCalculatorImpl::kScalarMoment) {
      ReportBuildSuccess("(default) calculator for scalar solve");
    } else if (implementation == MomentCalculatorImpl::kZerothMomentOnly) {
      ReportBuildSuccess("(default) calculator for 0th moment only");
    } else {
      AssertThrow(false,
                  dealii::ExcMessage("Unsupported implementation of moment calculator specified in call to "
                                     "BuildMomentCalculator"))
    }
  } catch (...) {
    ReportBuildError();
    throw;
  }

  return return_ptr;
}

template<int dim>
auto FrameworkBuilder<dim>::BuildMomentConvergenceChecker(
    double max_delta,
    int max_iterations) -> std::unique_ptr<MomentConvergenceChecker>{
  //TODO(Josh): Add option for using other than L1Norm
  ReportBuildingComponant("Moment convergence checker");

  using CheckerType = convergence::moments::ConvergenceCheckerLInfinityNorm;
  using FinalCheckerType = convergence::IterationCompletionChecker<system::moments::MomentVector>;

  auto return_ptr = std::make_unique<FinalCheckerType>(std::make_unique<CheckerType>(max_delta));
  return_ptr->SetMaxIterations(max_iterations);
  return return_ptr;
}

template <int dim>
auto FrameworkBuilder<dim>::BuildMomentMapConvergenceChecker(
    double max_delta, int max_iterations)
-> std::unique_ptr<MomentMapConvergenceChecker> {
  ReportBuildingComponant("Moment map convergence checker");

  using SingleCheckerType = convergence::moments::ConvergenceCheckerLInfinityNorm;
  using CheckerType = convergence::moments::MultiMomentCheckerMax;
  using FinalCheckerType = convergence::IterationCompletionChecker<system::moments::MomentsMap>;

  auto return_ptr = std::make_unique<FinalCheckerType>(
      std::make_unique<CheckerType>(std::make_unique<SingleCheckerType>(max_delta)));
  return_ptr->SetMaxIterations(max_iterations);
  return return_ptr;
}
template <int dim>
auto FrameworkBuilder<dim>::BuildOuterIteration(
    std::unique_ptr<GroupSolveIteration> group_iteration_ptr,
    std::unique_ptr<ParameterConvergenceChecker> convergence_checker_ptr,
    const std::string&)
    -> std::unique_ptr<OuterIteration> {
  ReportBuildingComponant("Outer iteration");
  std::unique_ptr<OuterIteration> return_ptr = nullptr;
  using ReturnType = iteration::outer::OuterFixedSourceIteration;

  return_ptr = std::move(std::make_unique<ReturnType>(
      std::move(group_iteration_ptr),
      std::move(convergence_checker_ptr)));

  using ConvergenceDataPort = iteration::outer::data_names::ConvergenceStatusPort;
  using StatusPort =  iteration::outer::data_names::StatusPort;

  instrumentation::GetPort<ConvergenceDataPort>(*return_ptr)
      .AddInstrument(convergence_status_instrument_ptr_);

  instrumentation::GetPort<StatusPort>(*return_ptr)
      .AddInstrument(status_instrument_ptr_);

  ReportBuildSuccess(return_ptr->description());

  return return_ptr;
}

template<int dim>
auto FrameworkBuilder<dim>::BuildOuterIteration(
    std::unique_ptr<GroupSolveIteration> group_solve_iteration_ptr,
    std::unique_ptr<ParameterConvergenceChecker> parameter_convergence_checker_ptr,
    std::unique_ptr<KEffectiveUpdater> k_effective_updater_ptr,
    const std::shared_ptr<FissionSourceUpdater>& fission_source_updater_ptr,
    const std::string& output_filename_base)
-> std::unique_ptr<OuterIteration> {
  std::unique_ptr<OuterIteration> return_ptr = nullptr;
  ReportBuildingComponant("Outer Iteration");

  using DefaultOuterPowerIteration = iteration::outer::OuterPowerIteration;

  return_ptr = std::move(
      std::make_unique<DefaultOuterPowerIteration>(
          std::move(group_solve_iteration_ptr),
          std::move(parameter_convergence_checker_ptr),
          std::move(k_effective_updater_ptr),
          fission_source_updater_ptr));

  using ConvergenceDataPort = iteration::outer::data_names::ConvergenceStatusPort;
  using StatusPort =  iteration::outer::data_names::StatusPort;
  using IterationErrorPort = iteration::outer::data_names::IterationErrorPort;

  using InstrumentBuilder = instrumentation::builder::InstrumentBuilder;
  instrumentation::GetPort<ConvergenceDataPort>(*return_ptr)
      .AddInstrument(convergence_status_instrument_ptr_);
  instrumentation::GetPort<StatusPort>(*return_ptr)
      .AddInstrument(status_instrument_ptr_);
  instrumentation::GetPort<IterationErrorPort>(*return_ptr)
      .AddInstrument(Shared(InstrumentBuilder::BuildInstrument<std::pair<int,double>>(
          instrumentation::builder::InstrumentName::kIntDoublePairToFile,
          output_filename_base + "_iteration_error.csv")));

  validator_ptr_->AddPart(FrameworkPart::FissionSourceUpdate);
  ReportBuildSuccess(return_ptr->description());
  return return_ptr;
}

template<int dim>
auto FrameworkBuilder<dim>::BuildParameterConvergenceChecker(
    double max_delta, int max_iterations)
-> std::unique_ptr<ParameterConvergenceChecker>{
  ReportBuildingComponant("Parameter (double) convergence checker");
  using CheckerType = convergence::parameters::SingleParameterChecker;
  using FinalCheckerType = convergence::IterationCompletionChecker<double>;

  auto return_ptr = std::make_unique<FinalCheckerType>(std::make_unique<CheckerType>(max_delta));
  return_ptr->SetMaxIterations(max_iterations);

  return return_ptr;
}

template <int dim>
auto FrameworkBuilder<dim>::BuildQuadratureSet(
    const problem::AngularQuadType quadrature_type,
    const FrameworkParameters::AngularQuadratureOrder order) -> std::shared_ptr<QuadratureSet> {
  ReportBuildingComponant("quadrature set");
  using QuadratureGenerator = quadrature::QuadratureGeneratorI<dim>;

  std::shared_ptr<QuadratureSet> return_ptr{ nullptr };
  std::shared_ptr<QuadratureGenerator> quadrature_generator_ptr{ nullptr };

  try {

    switch (quadrature_type) {
      case problem::AngularQuadType::kLevelSymmetricGaussian: {
        AssertThrow(dim == 3, dealii::ExcMessage("Error in BuildQuadratureSet LSGC only available for 3D"))
        quadrature_generator_ptr = quadrature::factory::MakeAngularQuadratureGeneratorPtr<dim>(
            order, quadrature::AngularQuadratureSetType::kLevelSymmetricGaussian);
        break;
      }
      case problem::AngularQuadType::kGaussLegendre: {
        AssertThrow(dim == 1, dealii::ExcMessage("Error in BuildQuadratureSet GaussLegendre only available for 1D"))
        quadrature_generator_ptr =
            quadrature::factory::MakeAngularQuadratureGeneratorPtr<dim>(
                order, quadrature::AngularQuadratureSetType::kGaussLegendre);
        break;
      }
      default: {
        AssertThrow(false, dealii::ExcMessage("No supported quadratures for this dimension and transport model"))
        break;
      }
    }
    ReportBuildSuccess(quadrature_generator_ptr->description());

    return_ptr = quadrature::factory::MakeQuadratureSetPtr<dim>();

    auto quadrature_points = quadrature::utility::GenerateAllPositiveX<dim>(quadrature_generator_ptr->GenerateSet());

    quadrature::factory::FillQuadratureSet<dim>(return_ptr.get(), quadrature_points);

    return return_ptr;
  } catch (...) {
    ReportBuildError();
    throw;
  }
}

template <int dim>
auto FrameworkBuilder<dim>::BuildSAAFFormulation(
    const std::shared_ptr<FiniteElement>& finite_element_ptr,
    const std::shared_ptr<data::cross_sections::CrossSectionsI>& cross_sections_ptr,
    const std::shared_ptr<QuadratureSet>& quadrature_set_ptr,
    const formulation::SAAFFormulationImpl implementation) -> std::unique_ptr<SAAFFormulation> {
  ReportBuildingComponant("Building SAAF Formulation");
  std::unique_ptr<SAAFFormulation> return_ptr;

  if (implementation == formulation::SAAFFormulationImpl::kDefault) {
    using ReturnType = formulation::angular::SelfAdjointAngularFlux<dim>;
    return_ptr = std::move(std::make_unique<ReturnType>(finite_element_ptr, cross_sections_ptr, quadrature_set_ptr));
  }

  return return_ptr;
}

template<int dim>
auto FrameworkBuilder<dim>::BuildSingleGroupSolver(const int max_iterations, const double convergence_tolerance)
-> std::unique_ptr<SingleGroupSolver> {
  using SolverName = solver::builder::SolverName;
  using SolverBuilder = solver::builder::SolverBuilder;

  ReportBuildingComponant("Single group solver");
  std::unique_ptr<SingleGroupSolver> return_ptr = nullptr;

  return_ptr = std::move(SolverBuilder::BuildSolver(SolverName::kDefaultGMRESGroupSolver, max_iterations,
                                                    convergence_tolerance));

  ReportBuildSuccess("Default implementation with GMRES");

  return return_ptr;
}

template<int dim>
auto FrameworkBuilder<dim>::BuildSystem(
    const int total_groups,
    const int total_angles,
    const Domain& domain,
    const std::size_t solution_size,
    bool is_eigenvalue_problem,
    bool need_rhs_boundary_condition) -> std::unique_ptr<System> {
  std::unique_ptr<System> return_ptr;

  ReportBuildingComponant("system");
  try {
    return_ptr = std::move(std::make_unique<System>());
    system_helper_.InitializeSystem(*return_ptr, total_groups, total_angles,
                             is_eigenvalue_problem, need_rhs_boundary_condition);
    system_helper_.SetUpSystemTerms(*return_ptr, domain);
    system_helper_.SetUpSystemMoments(*return_ptr, solution_size);
    ReportBuildSuccess("system");
  } catch (...) {
    ReportBuildError("system initialization error.");
    throw;
  }

  return return_ptr;
}

template<int dim>
auto FrameworkBuilder<dim>::BuildStamper(const std::shared_ptr<Domain>& domain_ptr) -> std::unique_ptr<Stamper> {
  ReportBuildingComponant("Stamper");
  std::unique_ptr<Stamper> return_ptr = nullptr;

  return_ptr = std::move(std::make_unique<formulation::Stamper<dim>>(domain_ptr));
  ReportBuildSuccess(return_ptr->description());
  return return_ptr;
}

template<int dim>
auto FrameworkBuilder<dim>::BuildSubroutine(std::unique_ptr<FrameworkI> framework_ptr,
                                            const SubroutineName subroutine_name) -> std::unique_ptr<Subroutine> {
  std::unique_ptr<Subroutine> return_ptr{ nullptr };
  if (subroutine_name == SubroutineName::kGetScalarFluxFromFramework) {
    using ReturnType = iteration::subroutine::GetScalarFluxFromFramework;
    return_ptr = std::move(std::make_unique<ReturnType>(std::move(framework_ptr)));
  }
  return return_ptr;
}

template<int dim>
void FrameworkBuilder<dim>::Validate() const {
  validator_ptr_->ReportValidation();
}

template class FrameworkBuilder<1>;
template class FrameworkBuilder<2>;
template class FrameworkBuilder<3>;

} // namespace bart::framework::builder


#ifndef BOOST_GEOMETRY_PROJECTIONS_STERE_HPP
#define BOOST_GEOMETRY_PROJECTIONS_STERE_HPP

// Boost.Geometry - extensions-gis-projections (based on PROJ4)
// This file is automatically generated. DO NOT EDIT.

// Copyright (c) 2008-2015 Barend Gehrels, Amsterdam, the Netherlands.

// This file was modified by Oracle on 2017, 2018.
// Modifications copyright (c) 2017-2018, Oracle and/or its affiliates.
// Contributed and/or modified by Adam Wulkiewicz, on behalf of Oracle.

// Use, modification and distribution is subject to the Boost Software License,
// Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at
// http://www.boost.org/LICENSE_1_0.txt)

// This file is converted from PROJ4, http://trac.osgeo.org/proj
// PROJ4 is originally written by Gerald Evenden (then of the USGS)
// PROJ4 is maintained by Frank Warmerdam
// PROJ4 is converted to Boost.Geometry by Barend Gehrels

// Last updated version of proj: 4.9.1

// Original copyright notice:

// 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 <boost/config.hpp>
#include <boost/geometry/util/math.hpp>
#include <boost/math/special_functions/hypot.hpp>

#include <boost/geometry/srs/projections/impl/base_static.hpp>
#include <boost/geometry/srs/projections/impl/base_dynamic.hpp>
#include <boost/geometry/srs/projections/impl/projects.hpp>
#include <boost/geometry/srs/projections/impl/factory_entry.hpp>
#include <boost/geometry/srs/projections/impl/pj_tsfn.hpp>

namespace boost { namespace geometry
{

namespace srs { namespace par4
{
    struct stere {};
    struct ups {};

}} //namespace srs::par4

namespace projections
{
    #ifndef DOXYGEN_NO_DETAIL
    namespace detail { namespace stere
    {
            static const double EPS10 = 1.e-10;
            static const double TOL = 1.e-8;
            static const int NITER = 8;
            static const double CONV = 1.e-10;
            static const int S_POLE = 0;
            static const int N_POLE = 1;
            static const int OBLIQ = 2;
            static const int EQUIT = 3;

            template <typename T>
            struct par_stere
            {
                T   phits;
                T   sinX1;
                T   cosX1;
                T   akm1;
                int mode;
            };

            template <typename T>
            inline T ssfn_(T const& phit, T sinphi, T const& eccen)
            {
                sinphi *= eccen;
                return (tan (.5 * (geometry::math::half_pi<T>() + phit)) *
                   pow((1. - sinphi) / (1. + sinphi), .5 * eccen));
            }

            // template class, using CRTP to implement forward/inverse
            template <typename CalculationType, typename Parameters>
            struct base_stere_ellipsoid : public base_t_fi<base_stere_ellipsoid<CalculationType, Parameters>,
                     CalculationType, Parameters>
            {

                typedef CalculationType geographic_type;
                typedef CalculationType cartesian_type;

                par_stere<CalculationType> m_proj_parm;

                inline base_stere_ellipsoid(const Parameters& par)
                    : base_t_fi<base_stere_ellipsoid<CalculationType, Parameters>,
                     CalculationType, Parameters>(*this, par) {}

                // FORWARD(e_forward)  ellipsoid
                // Project coordinates from geographic (lon, lat) to cartesian (x, y)
                inline void fwd(geographic_type& lp_lon, geographic_type& lp_lat, cartesian_type& xy_x, cartesian_type& xy_y) const
                {
                    static const CalculationType HALFPI = detail::HALFPI<CalculationType>();

                    CalculationType coslam, sinlam, sinX=0.0, cosX=0.0, X, A, sinphi;

                    coslam = cos(lp_lon);
                    sinlam = sin(lp_lon);
                    sinphi = sin(lp_lat);
                    if (this->m_proj_parm.mode == OBLIQ || this->m_proj_parm.mode == EQUIT) {
                        sinX = sin(X = 2. * atan(ssfn_(lp_lat, sinphi, this->m_par.e)) - HALFPI);
                        cosX = cos(X);
                    }
                    switch (this->m_proj_parm.mode) {
                    case OBLIQ:
                        A = this->m_proj_parm.akm1 / (this->m_proj_parm.cosX1 * (1. + this->m_proj_parm.sinX1 * sinX +
                           this->m_proj_parm.cosX1 * cosX * coslam));
                        xy_y = A * (this->m_proj_parm.cosX1 * sinX - this->m_proj_parm.sinX1 * cosX * coslam);
                        goto xmul;
                    case EQUIT:
                        A = this->m_proj_parm.akm1 / (1. + cosX * coslam);
                        xy_y = A * sinX;
                xmul:
                        xy_x = A * cosX;
                        break;
                    case S_POLE:
                        lp_lat = -lp_lat;
                        coslam = - coslam;
                        sinphi = -sinphi;
                        BOOST_FALLTHROUGH;
                    case N_POLE:
                        xy_x = this->m_proj_parm.akm1 * pj_tsfn(lp_lat, sinphi, this->m_par.e);
                        xy_y = - xy_x * coslam;
                        break;
                    }
                    xy_x = xy_x * sinlam;
                }

                // INVERSE(e_inverse)  ellipsoid
                // Project coordinates from cartesian (x, y) to geographic (lon, lat)
                inline void inv(cartesian_type& xy_x, cartesian_type& xy_y, geographic_type& lp_lon, geographic_type& lp_lat) const
                {
                    static const CalculationType HALFPI = detail::HALFPI<CalculationType>();

                    CalculationType cosphi, sinphi, tp=0.0, phi_l=0.0, rho, halfe=0.0, halfpi=0.0;
                    int i;

                    rho = boost::math::hypot(xy_x, xy_y);
                    switch (this->m_proj_parm.mode) {
                    case OBLIQ:
                    case EQUIT:
                        cosphi = cos( tp = 2. * atan2(rho * this->m_proj_parm.cosX1 , this->m_proj_parm.akm1) );
                        sinphi = sin(tp);
                                if( rho == 0.0 )
                            phi_l = asin(cosphi * this->m_proj_parm.sinX1);
                                else
                            phi_l = asin(cosphi * this->m_proj_parm.sinX1 + (xy_y * sinphi * this->m_proj_parm.cosX1 / rho));

                        tp = tan(.5 * (HALFPI + phi_l));
                        xy_x *= sinphi;
                        xy_y = rho * this->m_proj_parm.cosX1 * cosphi - xy_y * this->m_proj_parm.sinX1* sinphi;
                        halfpi = HALFPI;
                        halfe = .5 * this->m_par.e;
                        break;
                    case N_POLE:
                        xy_y = -xy_y;
                        BOOST_FALLTHROUGH;
                    case S_POLE:
                        phi_l = HALFPI - 2. * atan(tp = - rho / this->m_proj_parm.akm1);
                        halfpi = -HALFPI;
                        halfe = -.5 * this->m_par.e;
                        break;
                    }
                    for (i = NITER; i--; phi_l = lp_lat) {
                        sinphi = this->m_par.e * sin(phi_l);
                        lp_lat = 2. * atan(tp * pow((1.+sinphi)/(1.-sinphi), halfe)) - halfpi;
                        if (fabs(phi_l - lp_lat) < CONV) {
                            if (this->m_proj_parm.mode == S_POLE)
                                lp_lat = -lp_lat;
                            lp_lon = (xy_x == 0. && xy_y == 0.) ? 0. : atan2(xy_x, xy_y);
                            return;
                        }
                    }
                    BOOST_THROW_EXCEPTION( projection_exception(-20) );
                }

                static inline std::string get_name()
                {
                    return "stere_ellipsoid";
                }

            };

            // template class, using CRTP to implement forward/inverse
            template <typename CalculationType, typename Parameters>
            struct base_stere_spheroid : public base_t_fi<base_stere_spheroid<CalculationType, Parameters>,
                     CalculationType, Parameters>
            {

                typedef CalculationType geographic_type;
                typedef CalculationType cartesian_type;

                par_stere<CalculationType> m_proj_parm;

                inline base_stere_spheroid(const Parameters& par)
                    : base_t_fi<base_stere_spheroid<CalculationType, Parameters>,
                     CalculationType, Parameters>(*this, par) {}

                // FORWARD(s_forward)  spheroid
                // Project coordinates from geographic (lon, lat) to cartesian (x, y)
                inline void fwd(geographic_type& lp_lon, geographic_type& lp_lat, cartesian_type& xy_x, cartesian_type& xy_y) const
                {
                    static const CalculationType FORTPI = detail::FORTPI<CalculationType>();
                    static const CalculationType HALFPI = detail::HALFPI<CalculationType>();

                    CalculationType  sinphi, cosphi, coslam, sinlam;

                    sinphi = sin(lp_lat);
                    cosphi = cos(lp_lat);
                    coslam = cos(lp_lon);
                    sinlam = sin(lp_lon);
                    switch (this->m_proj_parm.mode) {
                    case EQUIT:
                        xy_y = 1. + cosphi * coslam;
                        goto oblcon;
                    case OBLIQ:
                        xy_y = 1. + this->m_proj_parm.sinX1 * sinphi + this->m_proj_parm.cosX1 * cosphi * coslam;
                oblcon:
                        if (xy_y <= EPS10)
                            BOOST_THROW_EXCEPTION( projection_exception(-20) );
                        xy_x = (xy_y = this->m_proj_parm.akm1 / xy_y) * cosphi * sinlam;
                        xy_y *= (this->m_proj_parm.mode == EQUIT) ? sinphi :
                           this->m_proj_parm.cosX1 * sinphi - this->m_proj_parm.sinX1 * cosphi * coslam;
                        break;
                    case N_POLE:
                        coslam = - coslam;
                        lp_lat = - lp_lat;
                        BOOST_FALLTHROUGH;
                    case S_POLE:
                        if (fabs(lp_lat - HALFPI) < TOL)
                            BOOST_THROW_EXCEPTION( projection_exception(-20) );
                        xy_x = sinlam * ( xy_y = this->m_proj_parm.akm1 * tan(FORTPI + .5 * lp_lat) );
                        xy_y *= coslam;
                        break;
                    }
                }

                // INVERSE(s_inverse)  spheroid
                // Project coordinates from cartesian (x, y) to geographic (lon, lat)
                inline void inv(cartesian_type& xy_x, cartesian_type& xy_y, geographic_type& lp_lon, geographic_type& lp_lat) const
                {
                    CalculationType  c, rh, sinc, cosc;

                    sinc = sin(c = 2. * atan((rh = boost::math::hypot(xy_x, xy_y)) / this->m_proj_parm.akm1));
                    cosc = cos(c);
                    lp_lon = 0.;
                    switch (this->m_proj_parm.mode) {
                    case EQUIT:
                        if (fabs(rh) <= EPS10)
                            lp_lat = 0.;
                        else
                            lp_lat = asin(xy_y * sinc / rh);
                        if (cosc != 0. || xy_x != 0.)
                            lp_lon = atan2(xy_x * sinc, cosc * rh);
                        break;
                    case OBLIQ:
                        if (fabs(rh) <= EPS10)
                            lp_lat = this->m_par.phi0;
                        else
                            lp_lat = asin(cosc * this->m_proj_parm.sinX1 + xy_y * sinc * this->m_proj_parm.cosX1 / rh);
                        if ((c = cosc - this->m_proj_parm.sinX1 * sin(lp_lat)) != 0. || xy_x != 0.)
                            lp_lon = atan2(xy_x * sinc * this->m_proj_parm.cosX1, c * rh);
                        break;
                    case N_POLE:
                        xy_y = -xy_y;
                        BOOST_FALLTHROUGH;
                    case S_POLE:
                        if (fabs(rh) <= EPS10)
                            lp_lat = this->m_par.phi0;
                        else
                            lp_lat = asin(this->m_proj_parm.mode == S_POLE ? - cosc : cosc);
                        lp_lon = (xy_x == 0. && xy_y == 0.) ? 0. : atan2(xy_x, xy_y);
                        break;
                    }
                }

                static inline std::string get_name()
                {
                    return "stere_spheroid";
                }

            };

            template <typename Parameters, typename T>
            inline void setup(Parameters& par, par_stere<T>& proj_parm)  /* general initialization */
            {
                static const T FORTPI = detail::FORTPI<T>();
                static const T HALFPI = detail::HALFPI<T>();

                T t;

                if (fabs((t = fabs(par.phi0)) - HALFPI) < EPS10)
                    proj_parm.mode = par.phi0 < 0. ? S_POLE : N_POLE;
                else
                    proj_parm.mode = t > EPS10 ? OBLIQ : EQUIT;
                proj_parm.phits = fabs(proj_parm.phits);
                if (par.es != 0.0) {
                    T X;

                    switch (proj_parm.mode) {
                    case N_POLE:
                    case S_POLE:
                        if (fabs(proj_parm.phits - HALFPI) < EPS10)
                            proj_parm.akm1 = 2. * par.k0 /
                               sqrt(pow(1+par.e,1+par.e)*pow(1-par.e,1-par.e));
                        else {
                            proj_parm.akm1 = cos(proj_parm.phits) /
                               pj_tsfn(proj_parm.phits, t = sin(proj_parm.phits), par.e);
                            t *= par.e;
                            proj_parm.akm1 /= sqrt(1. - t * t);
                        }
                        break;
                    case EQUIT:
                        //proj_parm.akm1 = 2. * par.k0;
                        //break;
                    case OBLIQ:
                        t = sin(par.phi0);
                        X = 2. * atan(ssfn_(par.phi0, t, par.e)) - HALFPI;
                        t *= par.e;
                        proj_parm.akm1 = 2. * par.k0 * cos(par.phi0) / sqrt(1. - t * t);
                        proj_parm.sinX1 = sin(X);
                        proj_parm.cosX1 = cos(X);
                        break;
                    }
                } else {
                    switch (proj_parm.mode) {
                    case OBLIQ:
                        proj_parm.sinX1 = sin(par.phi0);
                        proj_parm.cosX1 = cos(par.phi0);
                        BOOST_FALLTHROUGH;
                    case EQUIT:
                        proj_parm.akm1 = 2. * par.k0;
                        break;
                    case S_POLE:
                    case N_POLE:
                        proj_parm.akm1 = fabs(proj_parm.phits - HALFPI) >= EPS10 ?
                           cos(proj_parm.phits) / tan(FORTPI - .5 * proj_parm.phits) :
                           2. * par.k0 ;
                        break;
                    }
                }
            }

            // Stereographic
            template <typename Parameters, typename T>
            inline void setup_stere(Parameters& par, par_stere<T>& proj_parm)
            {
                static const T HALFPI = detail::HALFPI<T>();

                proj_parm.phits = pj_param(par.params, "tlat_ts").i ?
                        pj_param(par.params, "rlat_ts").f : HALFPI;
                setup(par, proj_parm);
            }

            // Universal Polar Stereographic
            template <typename Parameters, typename T>
            inline void setup_ups(Parameters& par, par_stere<T>& proj_parm)
            {
                static const T HALFPI = detail::HALFPI<T>();

                /* International Ellipsoid */
                par.phi0 = pj_param(par.params, "bsouth").i ? -HALFPI: HALFPI;
                if (!par.es)
                    BOOST_THROW_EXCEPTION( projection_exception(-34) );
                par.k0 = .994;
                par.x0 = 2000000.;
                par.y0 = 2000000.;
                proj_parm.phits = HALFPI;
                par.lam0 = 0.;
                setup(par, proj_parm);
            }

    }} // namespace detail::stere
    #endif // doxygen

    /*!
        \brief Stereographic projection
        \ingroup projections
        \tparam Geographic latlong point type
        \tparam Cartesian xy point type
        \tparam Parameters parameter type
        \par Projection characteristics
         - Azimuthal
         - Spheroid
         - Ellipsoid
        \par Projection parameters
         - lat_ts: Latitude of true scale (degrees)
        \par Example
        \image html ex_stere.gif
    */
    template <typename CalculationType, typename Parameters>
    struct stere_ellipsoid : public detail::stere::base_stere_ellipsoid<CalculationType, Parameters>
    {
        inline stere_ellipsoid(const Parameters& par) : detail::stere::base_stere_ellipsoid<CalculationType, Parameters>(par)
        {
            detail::stere::setup_stere(this->m_par, this->m_proj_parm);
        }
    };

    /*!
        \brief Stereographic projection
        \ingroup projections
        \tparam Geographic latlong point type
        \tparam Cartesian xy point type
        \tparam Parameters parameter type
        \par Projection characteristics
         - Azimuthal
         - Spheroid
         - Ellipsoid
        \par Projection parameters
         - lat_ts: Latitude of true scale (degrees)
        \par Example
        \image html ex_stere.gif
    */
    template <typename CalculationType, typename Parameters>
    struct stere_spheroid : public detail::stere::base_stere_spheroid<CalculationType, Parameters>
    {
        inline stere_spheroid(const Parameters& par) : detail::stere::base_stere_spheroid<CalculationType, Parameters>(par)
        {
            detail::stere::setup_stere(this->m_par, this->m_proj_parm);
        }
    };

    /*!
        \brief Universal Polar Stereographic projection
        \ingroup projections
        \tparam Geographic latlong point type
        \tparam Cartesian xy point type
        \tparam Parameters parameter type
        \par Projection characteristics
         - Azimuthal
         - Spheroid
         - Ellipsoid
        \par Projection parameters
         - south: Denotes southern hemisphere UTM zone (boolean)
        \par Example
        \image html ex_ups.gif
    */
    template <typename CalculationType, typename Parameters>
    struct ups_ellipsoid : public detail::stere::base_stere_ellipsoid<CalculationType, Parameters>
    {
        inline ups_ellipsoid(const Parameters& par) : detail::stere::base_stere_ellipsoid<CalculationType, Parameters>(par)
        {
            detail::stere::setup_ups(this->m_par, this->m_proj_parm);
        }
    };

    /*!
        \brief Universal Polar Stereographic projection
        \ingroup projections
        \tparam Geographic latlong point type
        \tparam Cartesian xy point type
        \tparam Parameters parameter type
        \par Projection characteristics
         - Azimuthal
         - Spheroid
         - Ellipsoid
        \par Projection parameters
         - south: Denotes southern hemisphere UTM zone (boolean)
        \par Example
        \image html ex_ups.gif
    */
    template <typename CalculationType, typename Parameters>
    struct ups_spheroid : public detail::stere::base_stere_spheroid<CalculationType, Parameters>
    {
        inline ups_spheroid(const Parameters& par) : detail::stere::base_stere_spheroid<CalculationType, Parameters>(par)
        {
            detail::stere::setup_ups(this->m_par, this->m_proj_parm);
        }
    };

    #ifndef DOXYGEN_NO_DETAIL
    namespace detail
    {

        // Static projection
        BOOST_GEOMETRY_PROJECTIONS_DETAIL_STATIC_PROJECTION(srs::par4::stere, stere_spheroid, stere_ellipsoid)
        BOOST_GEOMETRY_PROJECTIONS_DETAIL_STATIC_PROJECTION(srs::par4::ups, ups_spheroid, ups_ellipsoid)

        // Factory entry(s)
        template <typename CalculationType, typename Parameters>
        class stere_entry : public detail::factory_entry<CalculationType, Parameters>
        {
            public :
                virtual base_v<CalculationType, Parameters>* create_new(const Parameters& par) const
                {
                    if (par.es)
                        return new base_v_fi<stere_ellipsoid<CalculationType, Parameters>, CalculationType, Parameters>(par);
                    else
                        return new base_v_fi<stere_spheroid<CalculationType, Parameters>, CalculationType, Parameters>(par);
                }
        };

        template <typename CalculationType, typename Parameters>
        class ups_entry : public detail::factory_entry<CalculationType, Parameters>
        {
            public :
                virtual base_v<CalculationType, Parameters>* create_new(const Parameters& par) const
                {
                    if (par.es)
                        return new base_v_fi<ups_ellipsoid<CalculationType, Parameters>, CalculationType, Parameters>(par);
                    else
                        return new base_v_fi<ups_spheroid<CalculationType, Parameters>, CalculationType, Parameters>(par);
                }
        };

        template <typename CalculationType, typename Parameters>
        inline void stere_init(detail::base_factory<CalculationType, Parameters>& factory)
        {
            factory.add_to_factory("stere", new stere_entry<CalculationType, Parameters>);
            factory.add_to_factory("ups", new ups_entry<CalculationType, Parameters>);
        }

    } // namespace detail
    #endif // doxygen

} // namespace projections

}} // namespace boost::geometry

#endif // BOOST_GEOMETRY_PROJECTIONS_STERE_HPP