#include "neal8_algorithm.h"

#include <Eigen/Dense>
#include <memory>
#include <stan/math/prim/fun.hpp>

#include "algorithm_id.pb.h"
#include "algorithm_state.pb.h"
#include "hierarchy_id.pb.h"
#include "mixing_id.pb.h"
#include "neal2_algorithm.h"
#include "src/algorithms/marginal_algorithm.h"
#include "src/hierarchies/base_hierarchy.h"
#include "src/utils/distributions.h"

void Neal8Algorithm::read_params_from_proto(
    const bayesmix::AlgorithmParams &params) {
  // Set number of auxiliary blocks in addition to regular parameters
  BaseAlgorithm::read_params_from_proto(params);
  n_aux = params.neal8_n_aux();
}

void Neal8Algorithm::initialize() {
  MarginalAlgorithm::initialize();
  // Create correct amount of auxiliary blocks
  aux_unique_values.clear();
  for (size_t i = 0; i < n_aux; i++) {
    aux_unique_values.push_back(unique_values[0]->clone());
  }
}

void Neal8Algorithm::print_startup_message() const {
  std::string msg = "Running Neal8 algorithm (m=" + std::to_string(n_aux) +
                    " aux. blocks) with " +
                    bayesmix::HierarchyId_Name(unique_values[0]->get_id()) +
                    " hierarchies, " +
                    bayesmix::MixingId_Name(mixing->get_id()) + " mixing...";
  std::cout << msg << std::endl;
}

void Neal8Algorithm::sample_allocations() {
  // Initialize relevant values
  unsigned int n_data = data.rows();
  auto &rng = bayesmix::Rng::Instance().get();

  // Loop over data points
  for (size_t i = 0; i < n_data; i++) {
    bool singleton = (unique_values[allocations[i]]->get_card() <= 1);
    unsigned int c_old = allocations[i];

    if (singleton) {
      // Save unique value in the first auxiliary block
      bayesmix::AlgorithmState::ClusterState curr_val;
      unique_values[allocations[i]]->write_state_to_proto(&curr_val);
      aux_unique_values[0]->set_state_from_proto(curr_val);
      // Remove datum from cluster
      remove_singleton(c_old);
    } else {
      unique_values[c_old]->remove_datum(
          i, data.row(i), update_hierarchy_params(), hier_covariates.row(i));
    }

    unsigned int n_clust = unique_values.size();
    // Draw the unique values in the auxiliary blocks from their prior
    for (size_t j = singleton; j < n_aux; j++) {
      aux_unique_values[j]->sample_prior();
    }
    // Compute probabilities of clusters in log-space
    Eigen::VectorXd logprobas =
        get_cluster_prior_mass(i) + get_cluster_lpdf(i);
    // Draw a NEW value for datum allocation
    unsigned int c_new =
        bayesmix::categorical_rng(stan::math::softmax(logprobas), rng, 0);

    if (c_new >= n_clust) {
      // datum moves to a new cluster
      // Copy one of the auxiliary block as the new cluster
      std::shared_ptr<AbstractHierarchy> hier_new =
          aux_unique_values[c_new - n_clust]->clone();
      unique_values.push_back(hier_new);
      allocations[i] = n_clust;
      unique_values[n_clust]->add_datum(
          i, data.row(i), update_hierarchy_params(), hier_covariates.row(i));
    } else {
      allocations[i] = c_new;
      unique_values[c_new]->add_datum(
          i, data.row(i), update_hierarchy_params(), hier_covariates.row(i));
    }
  }
}

Eigen::VectorXd Neal8Algorithm::lpdf_marginal_component(
    std::shared_ptr<AbstractHierarchy> hier, const Eigen::MatrixXd &grid,
    const Eigen::RowVectorXd &covariate) const {
  unsigned int n_grid = grid.rows();
  Eigen::VectorXd lpdf_(n_grid);
  Eigen::MatrixXd lpdf_temp(n_grid, n_aux);
  for (size_t i = 0; i < n_aux; i++) {
    hier->sample_prior();
    lpdf_temp.col(i) = hier->like_lpdf_grid(grid, covariate);
  }
  for (size_t i = 0; i < n_grid; i++) {
    lpdf_(i) = stan::math::log_sum_exp(lpdf_temp.row(i));
  }
  return lpdf_.array() - log(n_aux);
}

Eigen::VectorXd Neal8Algorithm::get_cluster_prior_mass(
    const unsigned int data_idx) const {
  unsigned int n_data = data.rows();
  unsigned int n_clust = unique_values.size();
  Eigen::VectorXd logprior(n_clust + n_aux);
  for (size_t j = 0; j < n_clust; j++) {
    // Probability of being assigned to an already existing cluster
    logprior(j) = mixing->get_mass_existing_cluster(
        n_data - 1, n_clust, true, true, unique_values[j],
        mix_covariates.row(data_idx));
  }
  // Further update with marginal components
  for (size_t j = 0; j < n_aux; j++) {
    logprior(n_clust + j) = mixing->get_mass_new_cluster(
        n_data - 1, n_clust, true, true, mix_covariates.row(data_idx));
  }
  return logprior;
}

Eigen::VectorXd Neal8Algorithm::get_cluster_lpdf(
    const unsigned int data_idx) const {
  unsigned int n_data = data.rows();
  unsigned int n_clust = unique_values.size();
  Eigen::VectorXd loglpdf(n_clust + n_aux);
  for (size_t j = 0; j < n_clust; j++) {
    // Probability of being assigned to an already existing cluster
    loglpdf(j) = unique_values[j]->get_like_lpdf(
        data.row(data_idx), hier_covariates.row(data_idx));
  }
  for (size_t j = 0; j < n_aux; j++) {
    // Probability of being assigned to a newly created cluster
    loglpdf(n_clust + j) =
        aux_unique_values[j]->get_like_lpdf(data.row(data_idx),
                                            hier_covariates.row(data_idx)) -
        log(n_aux);
  }
  return loglpdf;
}


#include <ThermalAnalysis/LinearCombination.hpp>
#include <boost/program_options.hpp>
#include <boost/lexical_cast.hpp>
#include <boost/filesystem.hpp>
#include <boost/algorithm/string.hpp>
#include <libField/HDF5.hpp>
#include <gputils/io.hpp>
#include <libArrhenius/Arrhenius.hpp>

#include <iostream>
#include <fstream>
#include <vector>
#include <algorithm>

namespace po = boost::program_options;

Field<double,1> read_profile(const std::string& filename)
{
  Field<double, 1> F;
  GP2DData data;
  ReadGPASCII2DDataFile(filename, data);

  F.reset(data.x.size());
  for(int i = 0; i < F.size(); ++i)
  {
    F.getAxis(0)[i] = data.x[i];
    F(i) = data.f[i];
  }

  return F;
}

int main(int argc, const char** argv)
{
  // define command line options
  po::options_description po_opts("Options");
  po_opts.add_options()("help,h", "print help message.")(
      "verbose,v", po::value<int>()->implicit_value(0), "verbose level.")
    ("Ea", po::value<double>()->default_value(6.28e5), "Activation energy [J/mol].")
    ("A",  po::value<double>()->default_value(3.1e99), "Frequency factor [1/s].")
    ("temperature-bias",  po::value<double>()->default_value(0.0), "A temperature offset that will be added to the temperature history to convert it to absolute temperature.")
    ("exposure",  po::value<double>()->default_value(1.0), "The exposure that gave the temperature history. I.e., if the temperature history corresponds to a 2.5 W exposure, then computed damage threshold scaling factors will be multiplied by 2.5 and the threshold values will correspond to power.")
    ("taus",  po::value<string>(), "Directly specify the exposure durations to run (as a comma separated list) instead of computing them [s].")
    ("tau-min",  po::value<double>()->default_value(10e-6), "The minimum exposure duration to compute the damage threshold for [s].")
    ("tau-max",  po::value<double>(), "The maximum exposure duration to compute the damage threshold for. The default is to use the entire exposure, but this may not be accurate if the cool down period is important [s].")
    ("tau-reduction-factor",  po::value<double>()->default_value(2), "The reduction factor between consecutive exposure duration. I.e., a reduction factor of 2 would cut the exposure duration in half each time.")
    ("dt",  po::value<double>()->default_value(10e-6), "Time resolution to use for generated temperature histories.")
    ("write-profiles", "Write damage threshold profiles.")
    ;

  // now define our arguments.
  po::options_description po_args("Arguments");
  po_args.add_options()
    ("thermal-profiles"  , po::value<std::vector<std::string>>()->composing(), "Text files containing thermal profiles. Temperature should be expressed in Kelvin.")
    ;

  // combine the options and arguments into one option list.
  // this is what we will use to parse the command line, but
  // when we output a description of the options, we will just use
  // po_opts
  po::options_description all_options("Options and Arguments");
  all_options.add(po_opts).add(po_args);

  // tell boost how to translate positional options to named options
  po::positional_options_description args;
  args.add("thermal-profiles", -1);

  po::variables_map vm;
  po::store(po::command_line_parser(argc, argv)
      .options(all_options)
      .positional(args)
      .run(),
      vm);
  po::notify(vm);

  if (argc == 1 || vm.count("help")) {
    std::cout << R"EOL(
    A small utility to quickly compute the damage threshold vs. exposure duration from a single temperature history computed from a long exposure.

    For the linear heat equation, the temperature rise resulting from a short exposure can be computed from the temperature rise resulting from a long exposure by
    linear combination. Even for the Penne's bioheat equation, the non-linear blood perfusion term has a small effect on the temeprature rise during an exposure, and
    linearity can be assumed.

    To run the tool, simply pass a file containing the temperature history (plain text file with time in the first column and temperature in the second column).
    By default, the Welch-Polhamus Arrhenius coefficients will be used, but these can be set wit the --Ea and --A options. For example, so use the Henruques
    coefficients (used fo skin), run

    > ./thermal-damage-trender --A 3.1e98 --Ea 6.27e5 ./Tvst.txt

    This will start printing out the damage threshold scaling factor as a function of exposure duration.

)EOL";

    std::cout << po_opts << std::endl;
    return 0;
  }

  if(vm["tau-reduction-factor"].as<double>() <= 1)
  {
    std::cerr << "Error: The tau reduction factor must be greater than 1." << std::endl;
    return 1;
  }

  for( auto file : vm["thermal-profiles"].as<std::vector<std::string>>() )
  {
    if( !boost::filesystem::exists(file) )
    {
      std::cerr << file << " does not appear to exists. Skipping" << std::endl;
      continue;
    }

    Field<double,1> Tvst = read_profile(file);
    if(Tvst.size() < 10)
    {
      std::cerr << "Thermal profile has less than 10 data points. Cannot make any reasonable predictions about threshold trends... Skipping." << std::endl;
      continue;
    }

    Tvst += vm["temperature-bias"].as<double>();
    auto T0 = Tvst[0];
    auto tmin = Tvst.getCoord(0);
    auto tmax = Tvst.getCoord(Tvst.size()-1);

    Tvst -= T0;

    // calculate how many
    std::vector<double> taus;

    if( vm.count("taus") > 0 )
    {
      std::vector<std::string> toks;
      boost::split(toks, vm["taus"].as<std::string>(),boost::is_any_of(","),boost::token_compress_on);
      for( auto& tok : toks )
      {
        taus.push_back( boost::lexical_cast<double>(tok));
      }

    }
    else
    {
      taus.push_back(tmax-tmin);
      if( vm.count("tau-max") > 0 )
      {
        taus[0] = vm["tau-max"].as<double>();
      }
      while( taus[taus.size()-1] > vm["tau-min"].as<double>() )
      {
        taus.push_back(taus[taus.size()-1]/vm["tau-reduction-factor"].as<double>());
      }
    }

    double dt = 10e-6;
    int N = tmax/dt;
    libArrhenius::ThresholdCalculator< libArrhenius::ArrheniusIntegral<double> > calc(vm["A"].as<double>(),vm["Ea"].as<double>());
    for( auto tau : taus)
    {
      Field<double,1> Tvst2(N);
      Tvst2.setCoordinateSystem( Uniform(tmin,tmax) );
      LinearCombination<_1D::MonotonicInterpolator<double>> tempBuilder;

      tempBuilder.add(Tvst, 0, 1.);
      tempBuilder.add(Tvst, tau, -1.);
      tempBuilder.build(Tvst2);

      Tvst2 += T0;

      double threshold = calc(Tvst2.size(), Tvst2.getAxis(0).data(),Tvst2.data() );
      std::cout << tau << " " << threshold * vm["exposure"].as<double>() << std::endl;

      if(vm.count("write-profiles"))
      {
        std::string ofile = file+"-"+boost::lexical_cast<std::string>(tau)+"-threshold";
        std::ofstream out(ofile.c_str());
        Tvst2 -= T0;
        Tvst2 *= threshold;
        Tvst2 += T0;
        out << Tvst2;
      }

    }

  }

  return 0;
}


/*
  Copyright 2010 Larry Gritz and the other authors and contributors.
  All Rights Reserved.

  Redistribution and use in source and binary forms, with or without
  modification, are permitted provided that the following conditions are
  met:
  * Redistributions of source code must retain the above copyright
    notice, this list of conditions and the following disclaimer.
  * Redistributions in binary form must reproduce the above copyright
    notice, this list of conditions and the following disclaimer in the
    documentation and/or other materials provided with the distribution.
  * Neither the name of the software's owners nor the names of its
    contributors may be used to endorse or promote products derived from
    this software without specific prior written permission.
  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

  (This is the Modified BSD License)
*/

#include <cmath>
#include <vector>
#include <string>

#include <boost/algorithm/string.hpp>
using boost::algorithm::iequals;

#include "colortransfer.h"

OIIO_NAMESPACE_ENTER
{

bool
ColorTransfer::set (std::string name_, float param)
{
    return false;
}

bool
ColorTransfer::get (std::string name_, float &param)
{
    return false;
}

namespace {  // anonymous

/// Null (pass-thru) transfer function
///
class ColorTransfer_null : public ColorTransfer {
public:
    ColorTransfer_null () : ColorTransfer("null") { };
    ~ColorTransfer_null (void) { };
    float operator() (float x) { return x; }
};

/// Gamma transfer function
///
class ColorTransfer_gamma : public ColorTransfer {
public:
    ColorTransfer_gamma (float gamma=2.2f, float gain=1.0f)
        : ColorTransfer("gamma"), m_gamma(gamma), m_gain(gain)
    {
        add_paramater ("gamma");
        add_paramater ("gain");
    };

    ~ColorTransfer_gamma (void) { };

    bool set (std::string name, float param) {
        if (iequals (name, "gamma"))
            m_gamma = param;
        else if (iequals (name, "gain"))
            m_gain = param;
        else return false;
        return true;
    };

    bool get (std::string name, float &param) {
        if (iequals (name, "gamma"))
            param = m_gamma;
        else if (iequals (name, "gain"))
            param = m_gain;
        else return false;
        return true;
    };

    float operator() (float x) {
        if (x < 0.0f)
            return m_gain * x;
        return std::pow (m_gain * x, m_gamma);
    };

private:
    float m_gamma;
    float m_gain;
};

/// sRGB transfer function which is a widely used computer monitor standard
///    http://en.wikipedia.org/wiki/SRGB
class ColorTransfer_linear_to_sRGB : public ColorTransfer {
public:
    ColorTransfer_linear_to_sRGB () : ColorTransfer("linear_to_sRGB") { };
    ~ColorTransfer_linear_to_sRGB (void) { };

    float operator() (float x) {
        if (x < 0.0f)
            return 0.0f;
        return (x <= 0.0031308f) ? (12.92f * x)
                                 : (1.055f * std::pow (x, 1.f/2.4f) - 0.055f);
    }
};

/// sRGB transfer function which is a widely used computer monitor standard
///    http://en.wikipedia.org/wiki/SRGB
class ColorTransfer_sRGB_to_linear : public ColorTransfer {
public:
    ColorTransfer_sRGB_to_linear () : ColorTransfer("sRGB_to_linear") { };
    ~ColorTransfer_sRGB_to_linear (void) { };

    float operator() (float x) {
        return (x <= 0.04045f) ? (x / 12.92f)
                               : std::pow ((x + 0.055f) / 1.055f, 2.4f);
    }
};

/// AdobeRGB transfer function
///    http://en.wikipedia.org/wiki/Adobe_RGB
///    http://www.adobe.com/digitalimag/pdfs/AdobeRGB1998.pdf
class ColorTransfer_AdobeRGB_to_linear : public ColorTransfer {

public:
    ColorTransfer_AdobeRGB_to_linear () : ColorTransfer("AdobeRGB_to_linear") {}
    ~ColorTransfer_AdobeRGB_to_linear (void) { };

    float operator() (float x) {
        if (x < 0.f)
            return 0.f;
        return std::pow (x, 2.f + (51.f / 256.f));
    }
};

class ColorTransfer_linear_to_AdobeRGB : public ColorTransfer {

public:
    ColorTransfer_linear_to_AdobeRGB () : ColorTransfer("linear_to_AdobeRGB") {}
    ~ColorTransfer_linear_to_AdobeRGB (void) { }

    float operator() (float x) {
        if (x < 0.f)
            return 0.f;
        return std::pow (x, 1.f / (2.f + (51.f / 256.f)));
    }
};

/// Rec709 transfer function which is a HDTV standard
///    http://en.wikipedia.org/wiki/Rec._709
class ColorTransfer_Rec709_to_linear : public ColorTransfer {
public:
    ColorTransfer_Rec709_to_linear () : ColorTransfer ("Rec709_to_linear") { };
    ~ColorTransfer_Rec709_to_linear (void) { };

    float operator() (float x) {
        if (x < 0.f)
            return 0.f;
        return (x <= 0.081f ? x / 4.5f : std::pow ((x + 0.099f) / 1.099f, 1.0f / 0.45f));
    };
};

class ColorTransfer_linear_to_Rec709 : public ColorTransfer {
public:
    ColorTransfer_linear_to_Rec709 () : ColorTransfer ("linear_to_Rec709") { };
    ~ColorTransfer_linear_to_Rec709 (void) { };

    float operator() (float x) {
        if (x < 0.f)
            return 0.f;
        return (x <= 0.018f) ? x * 4.5f : (std::pow (x, 0.45f) * 1.099f) - 0.099f;
    }
};

static void
compute_kodak_coeff (float refBlack, float refWhite,
                     float dispGamma, float negGamma,
                     float &black, float &white, float &gamma, float &gain,
                     float &offset)
{
    // reference black code values below 0 are not allowed
    black = refBlack;
    if (black < 0.f) black = 0.f;

    // reference white code values above 1023 are not allowed, and must be
    // equal or above reference black
    white = refWhite;
    if (white > 1023.f)
        white = 1023.f;
    else if (white < black)
        white = black;

    // compute coefficients
    gamma = 0.002f / negGamma * dispGamma / 1.7f;
    gain = 1.f / (1.f - std::pow(10.f, (black - white) * gamma));
    offset = gain - 1.f;

    // convert 10bit code values to float
    black /= 1023.f;
    white /= 1023.f;
    gamma *= 1023.f;
}

/// Kodak Log transfer function which is used on Kodak 10bit log data
///
/// @todo cache results into a look up table with enough detail for
/// 10bits as this function will be slightly slow at the moment.
///
/// @note the soft clip is not implemented as it makes the transfer
/// function non-reversible. This parameter was designed to help display
/// high code values on a CRT monitor. Having a non-reversible transfer
/// function isn't so good for an image processing pipeline.
class ColorTransfer_KodakLog_to_linear : public ColorTransfer {
public:
    ColorTransfer_KodakLog_to_linear ()
        : ColorTransfer("_KodakLog_to_linear"), m_refBlack(95.f),
          m_refWhite(685.f), m_dispGamma(1.7f), m_negGamma(0.6f)
    {
        add_paramater ("refBlack");
        add_paramater ("refWhite");
        add_paramater ("dispGamma");
        add_paramater ("negGamma");
        compute_kodak_coeff (m_refBlack, m_refWhite, m_dispGamma, m_negGamma,
                                     m_black, m_white, m_gamma, m_gain, m_offset);
    };

    ~ColorTransfer_KodakLog_to_linear (void) { };

    bool set (std::string name, float param) {
        if (iequals (name, "refBlack"))  m_refBlack = param;
        else if (iequals (name, "refWhite"))  m_refWhite = param;
        else if (iequals (name, "dispGamma")) m_dispGamma = param;
        else if (iequals (name, "negGamma"))  m_negGamma = param;
        else return false;
        // recompute coefficients
        compute_kodak_coeff (m_refBlack, m_refWhite, m_dispGamma, m_negGamma,
                             m_black, m_white, m_gamma, m_gain, m_offset);
        return true;
    }

    bool get (std::string name, float &param) {
        if (iequals (name, "refBlack"))  param = m_refBlack;
        else if (iequals (name, "refWhite"))  param = m_refWhite;
        else if (iequals (name, "dispGamma")) param = m_dispGamma;
        else if (iequals (name, "negGamma"))  param = m_negGamma;
        else return false;
        return true;
    }

    float operator() (float x) {
        return (x < (m_black + 1e-06)) ? 0.f :
            std::pow (10.f, ((x - m_white) * m_gamma)) * m_gain - m_offset;
    }

private:
    // transfer paramaters
    float m_refBlack;
    float m_refWhite;
    float m_dispGamma;
    float m_negGamma;

    // coefficents
    float m_black;
    float m_white;
    float m_gamma;
    float m_gain;
    float m_offset;
};

class ColorTransfer_linear_to_KodakLog : public ColorTransfer {
public:
    ColorTransfer_linear_to_KodakLog ()
        : ColorTransfer("linear_to_KodakLog"), m_refBlack(95.f),
          m_refWhite(685.f), m_dispGamma(1.7f), m_negGamma(0.6f)
    {
        add_paramater ("refBlack");
        add_paramater ("refWhite");
        add_paramater ("dispGamma");
        add_paramater ("negGamma");
        compute_kodak_coeff (m_refBlack, m_refWhite, m_dispGamma, m_negGamma,
                                     m_black, m_white, m_gamma, m_gain, m_offset);
    }

    ~ColorTransfer_linear_to_KodakLog (void) { };

    bool set (std::string name, float param) {
        if (iequals (name, "refBlack"))  m_refBlack = param;
        else if (iequals (name, "refWhite"))  m_refWhite = param;
        else if (iequals (name, "dispGamma")) m_dispGamma = param;
        else if (iequals (name, "negGamma"))  m_negGamma = param;
        else return false;
        // recompute coefficients
        compute_kodak_coeff (m_refBlack, m_refWhite, m_dispGamma, m_negGamma,
                             m_black, m_white, m_gamma, m_gain, m_offset);
        return true;
    }

    bool get (std::string name, float &param) {
        if (iequals (name, "refBlack"))  param = m_refBlack;
        else if (iequals (name, "refWhite"))  param = m_refWhite;
        else if (iequals (name, "dispGamma")) param = m_dispGamma;
        else if (iequals (name, "negGamma"))  param = m_negGamma;
        else return false;
        return true;
    }

    float operator() (float x) {
        if (x < 1e-10)
            x = 1e-10;
        x = std::log10 ((x + m_offset) / m_gain) / m_gamma + m_white;
        return (x < m_black) ? 0.f : x;
    }

private:
    // transfer paramaters
    float m_refBlack;
    float m_refWhite;
    float m_dispGamma;
    float m_negGamma;

    // coefficents
    float m_black;
    float m_white;
    float m_gamma;
    float m_gain;
    float m_offset;
};

};  // anonymous namespace

// ColorTransfer::create is a static method that, given a transfer function
// name, returns an allocated and instantiated transfer function of the correct
// implementation.
// If the name is not recongnoized, return NULL.
ColorTransfer *
ColorTransfer::create (const std::string &name)
{
    if (iequals (name, "linear_to_linear") || iequals (name, "null"))
        return new ColorTransfer_null ();
    if (iequals (name, "Gamma"))
        return new ColorTransfer_gamma ();
    if (iequals (name, "linear_to_sRGB"))
        return new ColorTransfer_linear_to_sRGB ();
    if (iequals (name, "sRGB_to_linear"))
        return new ColorTransfer_sRGB_to_linear ();
    if (iequals (name, "linear_to_AdobeRGB"))
        return new ColorTransfer_linear_to_AdobeRGB ();
    if (iequals (name, "AdobeRGB_to_linear"))
        return new ColorTransfer_AdobeRGB_to_linear ();
    if (iequals (name, "linear_to_Rec709"))
        return new ColorTransfer_linear_to_Rec709 ();
    if (iequals (name, "Rec709_to_linear"))
        return new ColorTransfer_Rec709_to_linear ();
    if (iequals (name, "linear_to_KodakLog"))
        return new ColorTransfer_linear_to_KodakLog ();
    if (iequals (name, "KodakLog_to_linear"))
        return new ColorTransfer_KodakLog_to_linear ();
    return NULL;
}

}
OIIO_NAMESPACE_EXIT


// Boost.Geometry (aka GGL, Generic Geometry Library)

// Copyright (c) 2007-2012 Barend Gehrels, Amsterdam, the Netherlands.
// Copyright (c) 2008-2012 Bruno Lalande, Paris, France.
// Copyright (c) 2009-2012 Mateusz Loskot, London, UK.

// Parts of Boost.Geometry are redesigned from Geodan's Geographic Library
// (geolib/GGL), copyright (c) 1995-2010 Geodan, Amsterdam, the Netherlands.

// 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)

#ifndef BOOST_GEOMETRY_ALGORITHMS_CONVEX_HULL_HPP
#define BOOST_GEOMETRY_ALGORITHMS_CONVEX_HULL_HPP

#include <boost/array.hpp>

#include <boost/geometry/core/cs.hpp>
#include <boost/geometry/core/point_order.hpp>
#include <boost/geometry/core/exterior_ring.hpp>

#include <boost/geometry/geometries/concepts/check.hpp>

#include <boost/geometry/strategies/convex_hull.hpp>
#include <boost/geometry/strategies/concepts/convex_hull_concept.hpp>

#include <boost/geometry/views/detail/range_type.hpp>

#include <boost/geometry/algorithms/num_points.hpp>
#include <boost/geometry/algorithms/detail/as_range.hpp>
#include <boost/geometry/algorithms/detail/assign_box_corners.hpp>

namespace boost { namespace geometry
{

#ifndef DOXYGEN_NO_DETAIL
namespace detail { namespace convex_hull
{

template <order_selector Order>
struct hull_insert
{

    // Member template function (to avoid inconvenient declaration
    // of output-iterator-type, from hull_to_geometry)
    template <typename Geometry, typename OutputIterator, typename Strategy>
    static inline OutputIterator apply(Geometry const& geometry,
            OutputIterator out, Strategy const& strategy)
    {
        typename Strategy::state_type state;

        strategy.apply(geometry, state);
        strategy.result(state, out, Order == clockwise);
        return out;
    }
};

struct hull_to_geometry
{
    template <typename Geometry, typename OutputGeometry, typename Strategy>
    static inline void apply(Geometry const& geometry, OutputGeometry& out,
            Strategy const& strategy)
    {
        hull_insert
            <
                geometry::point_order<OutputGeometry>::value
            >::apply(geometry,
                std::back_inserter(
                    // Handle linestring, ring and polygon the same:
                    detail::as_range
                        <
                            typename range_type<OutputGeometry>::type
                        >(out)), strategy);
    }
};

// Helper metafunction for default strategy retrieval
template <typename Geometry>
struct default_strategy
    : strategy_convex_hull
          <
              Geometry,
              typename point_type<Geometry>::type
          >
{};

}} // namespace detail::convex_hull
#endif // DOXYGEN_NO_DETAIL

#ifndef DOXYGEN_NO_DISPATCH
namespace dispatch
{

template
<
    typename Geometry,
    typename Tag = typename tag<Geometry>::type
>
struct convex_hull
    : detail::convex_hull::hull_to_geometry
{};

template <typename Box>
struct convex_hull<Box, box_tag>
{
    template <typename OutputGeometry, typename Strategy>
    static inline void apply(Box const& box, OutputGeometry& out,
            Strategy const& )
    {
        static bool const Close
            = geometry::closure<OutputGeometry>::value == closed;
        static bool const Reverse
            = geometry::point_order<OutputGeometry>::value == counterclockwise;

        // A hull for boxes is trivial. Any strategy is (currently) skipped.
        boost::array<typename point_type<Box>::type, 4> range;
        geometry::detail::assign_box_corners_oriented<Reverse>(box, range);
        geometry::append(out, range);
        if (Close)
        {
            geometry::append(out, *boost::begin(range));
        }
    }
};

template <order_selector Order>
struct convex_hull_insert
    : detail::convex_hull::hull_insert<Order>
{};

} // namespace dispatch
#endif // DOXYGEN_NO_DISPATCH

template<typename Geometry, typename OutputGeometry, typename Strategy>
inline void convex_hull(Geometry const& geometry,
            OutputGeometry& out, Strategy const& strategy)
{
    concept::check_concepts_and_equal_dimensions
        <
            const Geometry,
            OutputGeometry
        >();

    BOOST_CONCEPT_ASSERT( (geometry::concept::ConvexHullStrategy<Strategy>) );

    if (geometry::num_points(geometry) == 0)
    {
        // Leave output empty
        return;
    }

    dispatch::convex_hull<Geometry>::apply(geometry, out, strategy);
}

/*!
\brief \brief_calc{convex hull}
\ingroup convex_hull
\details \details_calc{convex_hull,convex hull}.
\tparam Geometry1 \tparam_geometry
\tparam Geometry2 \tparam_geometry
\param geometry \param_geometry,  input geometry
\param hull \param_geometry \param_set{convex hull}

\qbk{[include reference/algorithms/convex_hull.qbk]}
 */
template<typename Geometry, typename OutputGeometry>
inline void convex_hull(Geometry const& geometry,
            OutputGeometry& hull)
{
    concept::check_concepts_and_equal_dimensions
        <
            const Geometry,
            OutputGeometry
        >();

    typedef typename detail::convex_hull::default_strategy<Geometry>::type strategy_type;

    convex_hull(geometry, hull, strategy_type());
}

#ifndef DOXYGEN_NO_DETAIL
namespace detail { namespace convex_hull
{

template<typename Geometry, typename OutputIterator, typename Strategy>
inline OutputIterator convex_hull_insert(Geometry const& geometry,
            OutputIterator out, Strategy const& strategy)
{
    // Concept: output point type = point type of input geometry
    concept::check<Geometry const>();
    concept::check<typename point_type<Geometry>::type>();

    BOOST_CONCEPT_ASSERT( (geometry::concept::ConvexHullStrategy<Strategy>) );

    return dispatch::convex_hull_insert
        <
            geometry::point_order<Geometry>::value
        >::apply(geometry, out, strategy);
}

/*!
\brief Calculate the convex hull of a geometry, output-iterator version
\ingroup convex_hull
\tparam Geometry the input geometry type
\tparam OutputIterator: an output-iterator
\param geometry the geometry to calculate convex hull from
\param out an output iterator outputing points of the convex hull
\note This overloaded version outputs to an output iterator.
In this case, nothing is known about its point-type or
    about its clockwise order. Therefore, the input point-type
    and order are copied

 */
template<typename Geometry, typename OutputIterator>
inline OutputIterator convex_hull_insert(Geometry const& geometry,
            OutputIterator out)
{
    // Concept: output point type = point type of input geometry
    concept::check<Geometry const>();
    concept::check<typename point_type<Geometry>::type>();

    typedef typename detail::convex_hull::default_strategy<Geometry>::type strategy_type;

    return convex_hull_insert(geometry, out, strategy_type());
}

}} // namespace detail::convex_hull
#endif // DOXYGEN_NO_DETAIL

}} // namespace boost::geometry

#endif // BOOST_GEOMETRY_ALGORITHMS_CONVEX_HULL_HPP


/**
 * Copyright Soramitsu Co., Ltd. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

#include "consensus/babe/impl/threshold_util.hpp"

#include <boost/range/adaptors.hpp>
#include <boost/range/numeric.hpp>

namespace kagome::consensus {

  Threshold calculateThreshold(const std::pair<uint64_t, uint64_t> &c_pair,
                               const primitives::AuthorityList &authorities,
                               primitives::AuthorityIndex authority_index) {
    double c = double(c_pair.first) / c_pair.second;

    using boost::adaptors::transformed;
    double theta =
        double(authorities[authority_index].weight)
        / boost::accumulate(authorities | transformed([](auto &authority) {
                              return authority.weight;
                            }),
                            0.);

    using namespace boost::multiprecision;  // NOLINT
    cpp_rational p_rat(1. - pow(1. - c, theta));
    static const auto a = (uint256_t{1} << 128);
    return Threshold{a * numerator(p_rat) / denominator(p_rat)};
  }

}  // namespace kagome::consensus


// Distributed under the MIT License.
// See LICENSE.txt for details.

#pragma once

#include <array>
#include <boost/functional/hash.hpp>  // IWYU pragma: keep
#include <cmath>
#include <cstddef>
#include <limits>
#include <ostream>
#include <pup.h>  // IWYU pragma: keep
#include <type_traits>
#include <unordered_map>
#include <utility>  // for pair

#include "DataStructures/DataVector.hpp"  // IWYU pragma: keep
#include "DataStructures/Index.hpp"
#include "DataStructures/ModalVector.hpp"  // IWYU pragma: keep
#include "DataStructures/Tags.hpp"
#include "DataStructures/Tensor/Metafunctions.hpp"  // IWYU pragma: keep
#include "DataStructures/Tensor/Tensor.hpp"         // IWYU pragma: keep
#include "DataStructures/Variables.hpp"             // IWYU pragma: keep
#include "Domain/Structure/Direction.hpp"
#include "Domain/Structure/Element.hpp"         // IWYU pragma: keep
#include "Domain/Structure/ElementId.hpp"       // IWYU pragma: keep
#include "Domain/Structure/OrientationMap.hpp"  // IWYU pragma: keep
#include "Domain/Structure/OrientationMapHelpers.hpp"
#include "Domain/Tags.hpp"  // IWYU pragma: keep
#include "NumericalAlgorithms/LinearOperators/CoefficientTransforms.hpp"
#include "NumericalAlgorithms/Spectral/Mesh.hpp"
#include "NumericalAlgorithms/Spectral/Spectral.hpp"
#include "Options/Options.hpp"
#include "Utilities/Algorithm.hpp"
#include "Utilities/ErrorHandling/Error.hpp"
#include "Utilities/Gsl.hpp"
#include "Utilities/MakeArray.hpp"
#include "Utilities/Math.hpp"
#include "Utilities/TMPL.hpp"

// IWYU pragma: no_include <algorithm>
// IWYU pragma: no_forward_declare Variables

/// \cond
namespace Limiters {
template <size_t VolumeDim, typename TagsToLimit>
class Krivodonova;
}  // namespace Limiters
/// \endcond

namespace Limiters {
/*!
 * \ingroup LimitersGroup
 * \brief An implementation of the Krivodonova limiter.
 *
 * The limiter is described in \cite Krivodonova2007. The Krivodonova limiter
 * works by limiting the highest derivatives/modal coefficients using an
 * aggressive minmod approach, decreasing in derivative/modal coefficient order
 * until no more limiting is necessary. In 3d, the function being limited is
 * expanded as:
 *
 * \f{align}{
 * u^{l,m,n}=\sum_{i,j,k=0,0,0}^{N_i,N_j,N_k}c^{l,m,n}_{i,j,k}
 *  P_{i}(\xi)P_{j}(\eta)P_{k}(\zeta)
 * \f}
 *
 * where \f$\left\{\xi, \eta, \zeta\right\}\f$ are the logical coordinates,
 * \f$P_{i}\f$ are the Legendre polynomials, the superscript \f$\{l,m,n\}\f$
 * represents the element indexed by \f$l,m,n\f$, and \f$N_i,N_j\f$ and
 * \f$N_k\f$ are the number of collocation points minus one in the
 * \f$\xi,\eta,\f$ and \f$\zeta\f$ direction, respectively. The coefficients are
 * limited according to:
 *
 * \f{align}{
 * \tilde{c}^{l,m,n}_{i,j,k}=\mathrm{minmod}
 *   &\left(c_{i,j,k}^{l,m,n},
 *          \alpha_i\left(c^{l+1,m,n}_{i-1,j,k}-c^{l,m,n}_{i-1,j,k}\right),
 *          \alpha_i\left(c^{l,m,n}_{i-1,j,k}-c^{l-1,m,n}_{i-1,j,k}\right),
 *     \right.\notag \\
 *   &\;\;\;\;
 *          \alpha_j\left(c^{l,m+1,n}_{i,j-1,k}-c^{l,m,n}_{i,j-1,k}\right),
 *          \alpha_j\left(c^{l,m,n}_{i,j-1,k}-c^{l,m-1,n}_{i,j-1,k}\right),
 *     \notag \\
 *   &\;\;\;\;\left.
 *          \alpha_k\left(c^{l,m,n+1}_{i,j,k-1}-c^{l,m,n}_{i,j,k-1}\right),
 *          \alpha_k\left(c^{l,m,n}_{i,j,k-1}-c^{l,m,n-1}_{i,j,k-1}\right)
 *     \right),
 * \label{eq:krivodonova 3d minmod}
 * \f}
 *
 * where \f$\mathrm{minmod}\f$ is the minmod function defined as
 *
 * \f{align}{
 *  \mathrm{minmod}(a,b,c,\ldots)=
 *  \left\{
 *  \begin{array}{ll}
 *    \mathrm{sgn}(a)\min(\lvert a\rvert, \lvert b\rvert,
 *    \lvert c\rvert, \ldots) & \mathrm{if} \;
 *    \mathrm{sgn}(a)=\mathrm{sgn}(b)=\mathrm{sgn}(c)=\mathrm{sgn}(\ldots) \\
 *    0 & \mathrm{otherwise}
 *  \end{array}\right.
 * \f}
 *
 * Krivodonova \cite Krivodonova2007 requires \f$\alpha_i\f$ to be in the range
 *
 * \f{align*}{
 * \frac{1}{2(2i-1)}\le \alpha_i \le 1
 * \f}
 *
 * where the lower bound comes from finite differencing the coefficients between
 * neighbor elements when using Legendre polynomials (see \cite Krivodonova2007
 * for details). Note that we normalize our Legendre polynomials by \f$P_i(1) =
 * 1\f$; this is the normalization \cite Krivodonova2007 uses in 1D, (but not in
 * 2D), which is why our bounds on \f$\alpha_i\f$ match Eq. 14 of
 * \cite Krivodonova2007 (but not Eq. 23). We relax the lower bound:
 *
 * \f{align*}{
 * 0 \le \alpha_i \le 1
 * \f}
 *
 * to allow different basis functions (e.g. Chebyshev polynomials) and to allow
 * the limiter to be more dissipative if necessary. The same \f$\alpha_i\f$s are
 * used in all dimensions.
 *
 * \note The only place where the specific choice of 1d basis
 * comes in is the lower bound for the \f$\alpha_i\f$s, and so in general the
 * limiter can be applied to any 1d or tensor product of 1d basis functions.
 *
 * The limiting procedure must be applied from the highest derivatives to the
 * lowest, i.e. the highest coefficients to the lowest. Let us consider a 3d
 * element with \f$N+1\f$ coefficients in each dimension and denote the
 * coefficients as \f$c_{i,j,k}\f$. Then the limiting procedure starts at
 * \f$c_{N,N,N}\f$, followed by \f$c_{N,N,N-1}\f$, \f$c_{N,N-1,N}\f$, and
 * \f$c_{N-1,N,N}\f$. A detailed example is given below. Limiting is stopped if
 * all symmetric pairs of coefficients are left unchanged, i.e.
 * \f$c_{i,j,k}=\tilde{c}_{i,j,k}\f$. By all symmetric coefficients we mean
 * that, for example, \f$c_{N-i,N-j,N-k}\f$, \f$c_{N-j,N-i,N-k}\f$,
 * \f$c_{N-k,N-j,N-i}\f$, \f$c_{N-j,N-k,N-i}\f$, \f$c_{N-i,N-k,N-j}\f$, and
 * \f$c_{N-k,N-i,N-j}\f$ are not limited. As a concrete example, consider a 3d
 * element with 3 collocation points per dimension. Each limited coefficient is
 * defined as (though only computed if needed):
 *
 * \f{align*}{
 * \tilde{c}^{l,m,n}_{2,2,2}=\mathrm{minmod}
 *   &\left(c_{2,2,2}^{l,m,n},
 *          \alpha_2\left(c^{l+1,m,n}_{1,2,2}-c^{l,m,n}_{1,2,2}\right),
 *          \alpha_2\left(c^{l,m,n}_{1,2,2}-c^{l-1,m,n}_{1,2,2}\right),
 *     \right.\\
 *   &\;\;\;\;
 *          \alpha_2\left(c^{l,m+1,n}_{2,1,2}-c^{l,m,n}_{2,1,2}\right),
 *          \alpha_2\left(c^{l,m,n}_{2,1,2}-c^{l,m-1,n}_{2,1,2}\right),\\
 *   &\;\;\;\;\left.
 *          \alpha_2\left(c^{l,m,n+1}_{2,2,1}-c^{l,m,n}_{2,2,1}\right),
 *          \alpha_2\left(c^{l,m,n}_{2,2,1}-c^{l,m,n-1}_{2,2,1}\right)
 *     \right),\\
 * \tilde{c}^{l,m,n}_{2,2,1}=\mathrm{minmod}
 *   &\left(c_{2,2,1}^{l,m,n},
 *          \alpha_2\left(c^{l+1,m,n}_{1,2,1}-c^{l,m,n}_{1,2,1}\right),
 *          \alpha_2\left(c^{l,m,n}_{1,2,1}-c^{l-1,m,n}_{1,2,1}\right),
 *     \right.\\
 *   &\;\;\;\;
 *          \alpha_2\left(c^{l,m+1,n}_{2,1,1}-c^{l,m,n}_{2,1,1}\right),
 *          \alpha_2\left(c^{l,m,n}_{2,1,1}-c^{l,m-1,n}_{2,1,1}\right),\\
 *   &\;\;\;\;\left.
 *          \alpha_1\left(c^{l,m,n+1}_{2,2,0}-c^{l,m,n}_{2,2,0}\right),
 *          \alpha_1\left(c^{l,m,n}_{2,2,0}-c^{l,m,n-1}_{2,2,0}\right)
 *     \right),\\
 * \tilde{c}^{l,m,n}_{2,1,2}=\mathrm{minmod}
 *   &\left(c_{2,1,2}^{l,m,n},
 *          \alpha_2\left(c^{l+1,m,n}_{1,1,2}-c^{l,m,n}_{1,1,2}\right),
 *          \alpha_2\left(c^{l,m,n}_{1,1,2}-c^{l-1,m,n}_{1,1,2}\right),
 *     \right.\\
 *   &\;\;\;\;
 *          \alpha_1\left(c^{l,m+1,n}_{2,0,2}-c^{l,m,n}_{2,0,2}\right),
 *          \alpha_1\left(c^{l,m,n}_{2,0,2}-c^{l,m-1,n}_{2,0,2}\right),\\
 *   &\;\;\;\;\left.
 *          \alpha_2\left(c^{l,m,n+1}_{2,1,1}-c^{l,m,n}_{2,1,1}\right),
 *          \alpha_2\left(c^{l,m,n}_{2,1,1}-c^{l,m,n-1}_{2,1,1}\right)
 *     \right),\\
 * \tilde{c}^{l,m,n}_{1,2,2}=\mathrm{minmod}
 *   &\left(c_{1,2,2}^{l,m,n},
 *          \alpha_1\left(c^{l+1,m,n}_{0,2,2}-c^{l,m,n}_{0,2,2}\right),
 *          \alpha_1\left(c^{l,m,n}_{0,2,2}-c^{l-1,m,n}_{0,2,2}\right),
 *     \right.\\
 *   &\;\;\;\;
 *          \alpha_2\left(c^{l,m+1,n}_{1,1,2}-c^{l,m,n}_{1,1,2}\right),
 *          \alpha_2\left(c^{l,m,n}_{1,1,2}-c^{l,m-1,n}_{1,1,2}\right),\\
 *   &\;\;\;\;\left.
 *          \alpha_2\left(c^{l,m,n+1}_{1,2,1}-c^{l,m,n}_{1,2,1}\right),
 *          \alpha_2\left(c^{l,m,n}_{1,2,1}-c^{l,m,n-1}_{1,2,1}\right)
 *     \right),\\
 * \tilde{c}^{l,m,n}_{2,2,0}=\mathrm{minmod}
 *   &\left(c_{2,2,0}^{l,m,n},
 *          \alpha_2\left(c^{l+1,m,n}_{1,2,0}-c^{l,m,n}_{1,2,0}\right),
 *          \alpha_2\left(c^{l,m,n}_{1,2,0}-c^{l-1,m,n}_{1,2,0}\right),
 *     \right.\\
 *   &\;\;\;\;\left.
 *          \alpha_2\left(c^{l,m+1,n}_{2,1,0}-c^{l,m,n}_{2,1,0}\right),
 *          \alpha_2\left(c^{l,m,n}_{2,1,0}-c^{l,m-1,n}_{2,1,0}\right)
 *     \right),\\
 * \tilde{c}^{l,m,n}_{2,0,2}=\mathrm{minmod}
 *   &\left(c_{2,0,2}^{l,m,n},
 *          \alpha_2\left(c^{l+1,m,n}_{1,0,2}-c^{l,m,n}_{1,0,2}\right),
 *          \alpha_2\left(c^{l,m,n}_{1,0,2}-c^{l-1,m,n}_{1,0,2}\right),
 *     \right.\\
 *   &\;\;\;\;\left.
 *          \alpha_2\left(c^{l,m,n+1}_{2,0,1}-c^{l,m,n}_{2,0,1}\right),
 *          \alpha_2\left(c^{l,m,n}_{2,0,1}-c^{l,m,n-1}_{2,0,1}\right)
 *     \right),\\
 * \tilde{c}^{l,m,n}_{0,2,2}=\mathrm{minmod}
 *   &\left(c_{0,2,2}^{l,m,n},
 *          \alpha_2\left(c^{l,m+1,n}_{0,1,2}-c^{l,m,n}_{0,1,2}\right),
 *          \alpha_2\left(c^{l,m,n}_{0,1,2}-c^{l,m-1,n}_{0,1,2}\right),
 *     \right.\\
 *   &\;\;\;\;\left.
 *          \alpha_2\left(c^{l,m,n+1}_{0,2,1}-c^{l,m,n}_{0,2,1}\right),
 *          \alpha_2\left(c^{l,m,n}_{0,2,1}-c^{l,m,n-1}_{0,2,1}\right)
 *     \right),\\
 * \tilde{c}^{l,m,n}_{2,1,1}=\mathrm{minmod}
 *   &\left(c_{2,1,1}^{l,m,n},
 *          \alpha_2\left(c^{l+1,m,n}_{1,1,1}-c^{l,m,n}_{1,1,1}\right),
 *          \alpha_2\left(c^{l,m,n}_{1,1,1}-c^{l-1,m,n}_{1,1,1}\right),
 *     \right.\\
 *   &\;\;\;\;
 *          \alpha_1\left(c^{l,m+1,n}_{2,0,1}-c^{l,m,n}_{2,0,1}\right),
 *          \alpha_1\left(c^{l,m,n}_{2,0,1}-c^{l,m-1,n}_{2,0,1}\right),\\
 *   &\;\;\;\;\left.
 *          \alpha_1\left(c^{l,m,n+1}_{2,1,0}-c^{l,m,n}_{2,1,0}\right),
 *          \alpha_1\left(c^{l,m,n}_{2,1,0}-c^{l,m,n-1}_{2,1,0}\right)
 *     \right),\\
 * \tilde{c}^{l,m,n}_{1,2,1}=\mathrm{minmod}
 *   &\left(c_{1,2,1}^{l,m,n},
 *          \alpha_1\left(c^{l+1,m,n}_{0,2,1}-c^{l,m,n}_{0,2,1}\right),
 *          \alpha_1\left(c^{l,m,n}_{0,2,1}-c^{l-1,m,n}_{0,2,1}\right),
 *     \right.\\
 *   &\;\;\;\;
 *          \alpha_2\left(c^{l,m+1,n}_{1,1,1}-c^{l,m,n}_{1,1,1}\right),
 *          \alpha_2\left(c^{l,m,n}_{1,1,1}-c^{l,m-1,n}_{1,1,1}\right),\\
 *   &\;\;\;\;\left.
 *          \alpha_1\left(c^{l,m,n+1}_{1,2,0}-c^{l,m,n}_{1,2,0}\right),
 *          \alpha_1\left(c^{l,m,n}_{1,2,0}-c^{l,m,n-1}_{1,2,0}\right)
 *     \right),\\
 * \tilde{c}^{l,m,n}_{1,1,2}=\mathrm{minmod}
 *   &\left(c_{1,1,2}^{l,m,n},
 *          \alpha_1\left(c^{l+1,m,n}_{0,1,2}-c^{l,m,n}_{0,1,2}\right),
 *          \alpha_1\left(c^{l,m,n}_{0,1,2}-c^{l-1,m,n}_{0,1,2}\right),
 *     \right.\\
 *   &\;\;\;\;
 *          \alpha_1\left(c^{l,m+1,n}_{1,0,2}-c^{l,m,n}_{1,0,2}\right),
 *          \alpha_1\left(c^{l,m,n}_{1,0,2}-c^{l,m-1,n}_{1,0,2}\right),\\
 *   &\;\;\;\;\left.
 *          \alpha_2\left(c^{l,m,n+1}_{1,1,1}-c^{l,m,n}_{1,1,1}\right),
 *          \alpha_2\left(c^{l,m,n}_{1,1,1}-c^{l,m,n-1}_{1,1,1}\right)
 *     \right),
 * \f}
 * \f{align*}{
 * \tilde{c}^{l,m,n}_{2,1,0}=\mathrm{minmod}
 *   &\left(c_{2,1,0}^{l,m,n},
 *          \alpha_2\left(c^{l+1,m,n}_{1,1,0}-c^{l,m,n}_{1,1,0}\right),
 *          \alpha_2\left(c^{l,m,n}_{1,1,0}-c^{l-1,m,n}_{1,1,0}\right),
 *     \right.\\
 *   &\;\;\;\;\left.
 *          \alpha_1\left(c^{l,m+1,n}_{2,0,0}-c^{l,m,n}_{2,0,0}\right),
 *          \alpha_1\left(c^{l,m,n}_{2,0,0}-c^{l,m-1,n}_{2,0,0}\right)
 *     \right),\\
 * \tilde{c}^{l,m,n}_{2,0,1}=\mathrm{minmod}
 *   &\left(c_{2,0,1}^{l,m,n},
 *          \alpha_2\left(c^{l+1,m,n}_{1,0,1}-c^{l,m,n}_{1,0,1}\right),
 *          \alpha_2\left(c^{l,m,n}_{1,0,1}-c^{l-1,m,n}_{1,0,1}\right),
 *     \right.\\
 *   &\;\;\;\;\left.
 *          \alpha_1\left(c^{l,m,n+1}_{2,0,0}-c^{l,m,n}_{2,0,0}\right),
 *          \alpha_1\left(c^{l,m,n}_{2,0,0}-c^{l,m,n-1}_{2,0,0}\right)
 *     \right),\\
 * \tilde{c}^{l,m,n}_{1,2,0}=\mathrm{minmod}
 *   &\left(c_{1,2,0}^{l,m,n},
 *          \alpha_1\left(c^{l+1,m,n}_{0,2,0}-c^{l,m,n}_{0,2,0}\right),
 *          \alpha_1\left(c^{l,m,n}_{0,2,0}-c^{l-1,m,n}_{0,2,0}\right),
 *     \right.\\
 *   &\;\;\;\;\left.
 *          \alpha_2\left(c^{l,m+1,n}_{1,1,0}-c^{l,m,n}_{1,1,0}\right),
 *          \alpha_2\left(c^{l,m,n}_{1,1,0}-c^{l,m-1,n}_{1,1,0}\right)
 *     \right),\\
 * \tilde{c}^{l,m,n}_{1,0,2}=\mathrm{minmod}
 *   &\left(c_{1,0,2}^{l,m,n},
 *          \alpha_1\left(c^{l+1,m,n}_{0,0,2}-c^{l,m,n}_{0,0,2}\right),
 *          \alpha_1\left(c^{l,m,n}_{0,0,2}-c^{l-1,m,n}_{0,0,2}\right),
 *     \right.\\
 *   &\;\;\;\;\left.
 *          \alpha_2\left(c^{l,m,n+1}_{1,0,1}-c^{l,m,n}_{1,0,1}\right),
 *          \alpha_2\left(c^{l,m,n}_{1,0,1}-c^{l,m,n-1}_{1,0,1}\right)
 *     \right),\\
 * \tilde{c}^{l,m,n}_{0,1,2}=\mathrm{minmod}
 *   &\left(c_{0,1,2}^{l,m,n},
 *          \alpha_1\left(c^{l,m+1,n}_{0,0,2}-c^{l,m,n}_{0,0,2}\right),
 *          \alpha_1\left(c^{l,m,n}_{0,0,2}-c^{l,m-1,n}_{0,0,2}\right),
 *   \right. \\
 *   &\;\;\;\;\left.
 *          \alpha_2\left(c^{l,m,n+1}_{0,1,1}-c^{l,m,n}_{0,1,1}\right),
 *          \alpha_2\left(c^{l,m,n}_{0,1,1}-c^{l,m,n-1}_{0,1,1}\right)
 *     \right),\\
 * \tilde{c}^{l,m,n}_{0,2,1}=\mathrm{minmod}
 *   &\left(c_{0,2,1}^{l,m,n},
 *          \alpha_2\left(c^{l,m+1,n}_{0,1,1}-c^{l,m,n}_{0,1,1}\right),
 *          \alpha_2\left(c^{l,m,n}_{0,1,1}-c^{l,m-1,n}_{0,1,1}\right),
 *   \right.\\
 *   &\;\;\;\;\left.
 *          \alpha_1\left(c^{l,m,n+1}_{0,2,0}-c^{l,m,n}_{0,2,0}\right),
 *          \alpha_1\left(c^{l,m,n}_{0,2,0}-c^{l,m,n-1}_{0,2,0}\right)
 *     \right),
 * \f}
 * \f{align*}{
 * \tilde{c}^{l,m,n}_{2,0,0}=\mathrm{minmod}
 *   &\left(c_{2,0,0}^{l,m,n},
 *          \alpha_2\left(c^{l+1,m,n}_{1,0,0}-c^{l,m,n}_{1,0,0}\right),
 *          \alpha_2\left(c^{l,m,n}_{1,0,0}-c^{l-1,m,n}_{1,0,0}\right)
 *     \right),\\
 * \tilde{c}^{l,m,n}_{0,2,0}=\mathrm{minmod}
 *   &\left(c_{0,2,0}^{l,m,n},
 *          \alpha_2\left(c^{l,m+1,n}_{0,1,0}-c^{l,m,n}_{0,1,0}\right),
 *          \alpha_2\left(c^{l,m,n}_{0,1,0}-c^{l,m-1,n}_{0,1,0}\right)
 *     \right),\\
 * \tilde{c}^{l,m,n}_{0,0,2}=\mathrm{minmod}
 *   &\left(c_{0,0,2}^{l,m,n},
 *          \alpha_2\left(c^{l,m,n+1}_{0,0,1}-c^{l,m,n}_{0,0,1}\right),
 *          \alpha_2\left(c^{l,m,n}_{0,0,1}-c^{l,m,n-1}_{0,0,1}\right)
 *     \right),\\
 * \tilde{c}^{l,m,n}_{1,1,1}=\mathrm{minmod}
 *   &\left(c_{1,1,1}^{l,m,n},
 *          \alpha_1\left(c^{l+1,m,n}_{0,1,1}-c^{l,m,n}_{0,1,1}\right),
 *          \alpha_1\left(c^{l,m,n}_{0,1,1}-c^{l-1,m,n}_{0,1,1}\right),
 *     \right.\\
 *   &\;\;\;\;
 *          \alpha_1\left(c^{l,m+1,n}_{1,0,1}-c^{l,m,n}_{1,0,1}\right),
 *          \alpha_1\left(c^{l,m,n}_{1,0,1}-c^{l,m-1,n}_{1,0,1}\right),\\
 *   &\;\;\;\;\left.
 *          \alpha_1\left(c^{l,m,n+1}_{1,1,0}-c^{l,m,n}_{1,1,0}\right),
 *          \alpha_1\left(c^{l,m,n}_{1,1,0}-c^{l,m,n-1}_{1,1,0}\right)
 *     \right),\\
 * \tilde{c}^{l,m,n}_{1,1,0}=\mathrm{minmod}
 *   &\left(c_{1,1,0}^{l,m,n},
 *          \alpha_1\left(c^{l+1,m,n}_{0,1,0}-c^{l,m,n}_{0,1,0}\right),
 *          \alpha_1\left(c^{l,m,n}_{0,1,0}-c^{l-1,m,n}_{0,1,0}\right),
 *     \right.\\
 *   &\;\;\;\;\left.
 *          \alpha_1\left(c^{l,m+1,n}_{1,0,0}-c^{l,m,n}_{1,0,0}\right),
 *          \alpha_1\left(c^{l,m,n}_{1,0,0}-c^{l,m-1,n}_{1,0,0}\right),
 *     \right),\\
 * \tilde{c}^{l,m,n}_{1,0,1}=\mathrm{minmod}
 *   &\left(c_{1,0,1}^{l,m,n},
 *          \alpha_1\left(c^{l+1,m,n}_{0,0,1}-c^{l,m,n}_{0,0,1}\right),
 *          \alpha_1\left(c^{l,m,n}_{0,0,1}-c^{l-1,m,n}_{0,0,1}\right),
 *     \right.\\
 *   &\;\;\;\;\left.
 *          \alpha_1\left(c^{l,m,n+1}_{1,0,0}-c^{l,m,n}_{1,0,0}\right),
 *          \alpha_1\left(c^{l,m,n}_{1,0,0}-c^{l,m,n-1}_{1,0,0}\right)
 *     \right),\\
 * \tilde{c}^{l,m,n}_{0,1,1}=\mathrm{minmod}
 *   &\left(c_{0,1,1}^{l,m,n},
 *          \alpha_1\left(c^{l,m+1,n}_{0,0,1}-c^{l,m,n}_{0,0,1}\right),
 *          \alpha_1\left(c^{l,m,n}_{0,0,1}-c^{l,m-1,n}_{0,0,1}\right),
 *   \right.\\
 *   &\;\;\;\;\left.
 *          \alpha_1\left(c^{l,m,n+1}_{0,1,0}-c^{l,m,n}_{0,1,0}\right),
 *          \alpha_1\left(c^{l,m,n}_{0,1,0}-c^{l,m,n-1}_{0,1,0}\right)
 *     \right),\\
 * \tilde{c}^{l,m,n}_{1,0,0}=\mathrm{minmod}
 *   &\left(c_{1,0,0}^{l,m,n},
 *          \alpha_1\left(c^{l+1,m,n}_{0,0,0}-c^{l,m,n}_{0,0,0}\right),
 *          \alpha_1\left(c^{l,m,n}_{0,0,0}-c^{l-1,m,n}_{0,0,0}\right)
 *     \right),\\
 * \tilde{c}^{l,m,n}_{0,1,0}=\mathrm{minmod}
 *   &\left(c_{0,1,0}^{l,m,n},
 *          \alpha_1\left(c^{l,m+1,n}_{0,0,0}-c^{l,m,n}_{0,0,0}\right),
 *          \alpha_1\left(c^{l,m,n}_{0,0,0}-c^{l,m-1,n}_{0,0,0}\right)
 *     \right),\\
 * \tilde{c}^{l,m,n}_{0,0,1}=\mathrm{minmod}
 *   &\left(c_{0,0,1}^{l,m,n},
 *          \alpha_1\left(c^{l,m,n+1}_{0,0,0}-c^{l,m,n}_{0,0,0}\right),
 *          \alpha_1\left(c^{l,m,n}_{0,0,0}-c^{l,m,n-1}_{0,0,0}\right)
 *     \right),
 * \f}
 *
 *
 * The algorithm to perform the limiting is as follows:
 *
 * - limit \f$c_{2,2,2}\f$ (i.e. \f$c_{2,2,2}\leftarrow\tilde{c}_{2,2,2}\f$), if
 *   not changed, stop
 * - limit \f$c_{2,2,1}\f$, \f$c_{2,1,2}\f$, and \f$c_{1,2,2}\f$, if all not
 * changed, stop
 * - limit \f$c_{2,2,0}\f$, \f$c_{2,0,2}\f$, and \f$c_{0,2,2}\f$, if all not
 * changed, stop
 * - limit \f$c_{2,1,1}\f$, \f$c_{1,2,1}\f$, and \f$c_{1,1,2}\f$, if all not
 * changed, stop
 * - limit \f$c_{2,1,0}\f$, \f$c_{2,0,1}\f$, \f$c_{1,2,0}\f$, \f$c_{1,0,2}\f$,
 * \f$c_{0,1,2}\f$, and \f$c_{0,2,1}\f$, if all not changed, stop
 * - limit \f$c_{2,0,0}\f$, \f$c_{0,2,0}\f$, and \f$c_{0,0,2}\f$, if all not
 * changed, stop
 * - limit \f$c_{1,1,1}\f$, if not changed, stop
 * - limit \f$c_{1,1,0}\f$, \f$c_{1,0,1}\f$, and \f$c_{0,1,1}\f$, if all not
 * changed, stop
 * - limit \f$c_{1,0,0}\f$, \f$c_{0,1,0}\f$, and \f$c_{0,0,1}\f$, if all not
 * changed, stop
 *
 * The 1d and 2d implementations are straightforward restrictions of the
 * described algorithm.
 *
 * #### Limitations:
 *
 * - We currently recompute the spectral coefficients for local use, after
 *   having computed these same coefficients for sending to the neighbors. We
 *   should be able to avoid this either by storing the coefficients in the
 *   DataBox or by allowing the limiters' `packaged_data` function to return an
 *   object to be passed as an additional argument to the `operator()` (still
 *   would need to be stored in the DataBox).
 * - We cannot handle the case where neighbors have more/fewer
 *   coefficients. In the case that the neighbor has more coefficients we could
 *   just ignore the higher coefficients. In the case that the neighbor has
 *   fewer coefficients we have a few choices.
 * - Having a different number of collocation points in different directions is
 *   not supported. However, it is straightforward to handle this case. The
 *   outermost loop should be in the direction with the most collocation points,
 *   while the inner most loop should be over the direction with the fewest
 *   collocation points. The highest to lowest coefficients can then be limited
 *   appropriately again.
 * - h-refinement is not supported, but there is one reasonably straightforward
 *   implementation that may work. In this case we would ignore refinement that
 *   is not in the direction of the neighbor, treating the element as
 *   simply having multiple neighbors in that direction. The only change would
 *   be accounting for different refinement in the direction of the neighor,
 *   which should be easy to add since the differences in coefficients in
 *   Eq.\f$\ref{eq:krivodonova 3d minmod}\f$ will just be multiplied by
 *   non-unity factors.
 */
template <size_t VolumeDim, typename... Tags>
class Krivodonova<VolumeDim, tmpl::list<Tags...>> {
 public:
  /*!
   * \brief The \f$\alpha_i\f$ values in the Krivodonova algorithm.
   */
  struct Alphas {
    using type = std::array<
        double, Spectral::maximum_number_of_points<Spectral::Basis::Legendre>>;
    static constexpr Options::String help = {
        "The alpha parameters of the Krivodonova limiter"};
  };
  /*!
   * \brief Turn the limiter off
   *
   * This option exists to temporarily disable the limiter for debugging
   * purposes. For problems where limiting is not needed, the preferred
   * approach is to not compile the limiter into the executable.
   */
  struct DisableForDebugging {
    using type = bool;
    static type suggested_value() noexcept { return false; }
    static constexpr Options::String help = {"Disable the limiter"};
  };

  using options = tmpl::list<Alphas, DisableForDebugging>;
  static constexpr Options::String help = {
      "The hierarchical limiter of Krivodonova.\n\n"
      "This limiter works by limiting the highest modal "
      "coefficients/derivatives using an aggressive minmod approach, "
      "decreasing in modal coefficient order until no more limiting is "
      "necessary."};

  explicit Krivodonova(
      std::array<double,
                 Spectral::maximum_number_of_points<Spectral::Basis::Legendre>>
          alphas,
      bool disable_for_debugging = false, const Options::Context& context = {});

  Krivodonova() = default;
  Krivodonova(const Krivodonova&) = delete;
  Krivodonova& operator=(const Krivodonova&) = delete;
  Krivodonova(Krivodonova&&) = default;
  Krivodonova& operator=(Krivodonova&&) = default;
  ~Krivodonova() = default;

  // NOLINTNEXTLINE(google-runtime-references)
  void pup(PUP::er& p) noexcept;

  bool operator==(const Krivodonova& rhs) const noexcept;

  struct PackagedData {
    Variables<tmpl::list<::Tags::Modal<Tags>...>> modal_volume_data;
    Mesh<VolumeDim> mesh;

    // clang-tidy: google-runtime-references
    void pup(PUP::er& p) noexcept {  // NOLINT
      p | modal_volume_data;
      p | mesh;
    }
  };

  using package_argument_tags =
      tmpl::list<Tags..., domain::Tags::Mesh<VolumeDim>>;

  /// \brief Package data for sending to neighbor elements.
  void package_data(gsl::not_null<PackagedData*> packaged_data,
                    const typename Tags::type&... tensors,
                    const Mesh<VolumeDim>& mesh,
                    const OrientationMap<VolumeDim>& orientation_map) const
      noexcept;

  using limit_tags = tmpl::list<Tags...>;
  using limit_argument_tags = tmpl::list<domain::Tags::Element<VolumeDim>,
                                         domain::Tags::Mesh<VolumeDim>>;

  bool operator()(
      const gsl::not_null<std::add_pointer_t<typename Tags::type>>... tensors,
      const Element<VolumeDim>& element, const Mesh<VolumeDim>& mesh,
      const std::unordered_map<
          std::pair<Direction<VolumeDim>, ElementId<VolumeDim>>, PackagedData,
          boost::hash<std::pair<Direction<VolumeDim>, ElementId<VolumeDim>>>>&
          neighbor_data) const noexcept;

 private:
  template <typename Tag>
  char limit_one_tensor(
      gsl::not_null<Variables<tmpl::list<::Tags::Modal<Tags>...>>*> coeffs_self,
      gsl::not_null<bool*> limited_any_component, const Mesh<1>& mesh,
      const std::unordered_map<
          std::pair<Direction<1>, ElementId<1>>, PackagedData,
          boost::hash<std::pair<Direction<1>, ElementId<1>>>>& neighbor_data)
      const noexcept;
  template <typename Tag>
  char limit_one_tensor(
      gsl::not_null<Variables<tmpl::list<::Tags::Modal<Tags>...>>*> coeffs_self,
      gsl::not_null<bool*> limited_any_component, const Mesh<2>& mesh,
      const std::unordered_map<
          std::pair<Direction<2>, ElementId<2>>, PackagedData,
          boost::hash<std::pair<Direction<2>, ElementId<2>>>>& neighbor_data)
      const noexcept;
  template <typename Tag>
  char limit_one_tensor(
      gsl::not_null<Variables<tmpl::list<::Tags::Modal<Tags>...>>*> coeffs_self,
      gsl::not_null<bool*> limited_any_component, const Mesh<3>& mesh,
      const std::unordered_map<
          std::pair<Direction<3>, ElementId<3>>, PackagedData,
          boost::hash<std::pair<Direction<3>, ElementId<3>>>>& neighbor_data)
      const noexcept;

  template <typename Tag, size_t Dim>
  char fill_variables_tag_with_spectral_coeffs(
      gsl::not_null<Variables<tmpl::list<::Tags::Modal<Tags>...>>*>
          modal_coeffs,
      const typename Tag::type& nodal_tensor, const Mesh<Dim>& mesh) const
      noexcept;

  std::array<double,
             Spectral::maximum_number_of_points<Spectral::Basis::Legendre>>
      alphas_ = make_array<
          Spectral::maximum_number_of_points<Spectral::Basis::Legendre>>(
          std::numeric_limits<double>::signaling_NaN());
  bool disable_for_debugging_{false};
};

template <size_t VolumeDim, typename... Tags>
Krivodonova<VolumeDim, tmpl::list<Tags...>>::Krivodonova(
    std::array<double,
               Spectral::maximum_number_of_points<Spectral::Basis::Legendre>>
        alphas,
    bool disable_for_debugging, const Options::Context& context)
    : alphas_(alphas), disable_for_debugging_(disable_for_debugging) {
  // See the main documentation for an explanation of why these bounds are
  // different from those of Krivodonova 2007
  if (alg::any_of(alphas_, [](const double t) noexcept {
        return t > 1.0 or t <= 0.0;
      })) {
    PARSE_ERROR(context,
                "The alphas in the Krivodonova limiter must be in the range "
                "(0,1].");
  }
}

template <size_t VolumeDim, typename... Tags>
void Krivodonova<VolumeDim, tmpl::list<Tags...>>::package_data(
    const gsl::not_null<PackagedData*> packaged_data,
    const typename Tags::type&... tensors, const Mesh<VolumeDim>& mesh,
    const OrientationMap<VolumeDim>& orientation_map) const noexcept {
  if (UNLIKELY(disable_for_debugging_)) {
    // Do not initialize packaged_data
    return;
  }

  packaged_data->modal_volume_data.initialize(mesh.number_of_grid_points());
  // perform nodal coefficients to modal coefficients transformation on each
  // tensor component
  expand_pack(fill_variables_tag_with_spectral_coeffs<Tags>(
      &(packaged_data->modal_volume_data), tensors, mesh)...);

  packaged_data->modal_volume_data = orient_variables(
      packaged_data->modal_volume_data, mesh.extents(), orientation_map);

  packaged_data->mesh = orientation_map(mesh);
}

template <size_t VolumeDim, typename... Tags>
bool Krivodonova<VolumeDim, tmpl::list<Tags...>>::operator()(
    const gsl::not_null<std::add_pointer_t<typename Tags::type>>... tensors,
    const Element<VolumeDim>& element, const Mesh<VolumeDim>& mesh,
    const std::unordered_map<
        std::pair<Direction<VolumeDim>, ElementId<VolumeDim>>, PackagedData,
        boost::hash<std::pair<Direction<VolumeDim>, ElementId<VolumeDim>>>>&
        neighbor_data) const noexcept {
  if (UNLIKELY(disable_for_debugging_)) {
    // Do not modify input tensors
    return false;
  }
  if (UNLIKELY(mesh != Mesh<VolumeDim>(mesh.extents()[0], mesh.basis()[0],
                                       mesh.quadrature()[0]))) {
    ERROR(
        "The Krivodonova limiter does not yet support non-uniform number of "
        "collocation points, bases, and quadrature in each direction. The "
        "mesh is: "
        << mesh);
  }
  if (UNLIKELY(alg::any_of(element.neighbors(),
                           [](const auto& direction_neighbors) noexcept {
                             return direction_neighbors.second.size() != 1;
                           }))) {
    ERROR("The Krivodonova limiter does not yet support h-refinement");
  }
  alg::for_each(neighbor_data, [&mesh](const auto& id_packaged_data) noexcept {
    if (UNLIKELY(id_packaged_data.second.mesh != mesh)) {
      ERROR(
          "The Krivodonova limiter does not yet support differing meshes "
          "between neighbors. Self mesh is: "
          << mesh << " neighbor mesh is: " << id_packaged_data.second.mesh);
    }
  });

  // Compute local modal coefficients
  Variables<tmpl::list<::Tags::Modal<Tags>...>> coeffs_self(
      mesh.number_of_grid_points(), 0.0);
  expand_pack(fill_variables_tag_with_spectral_coeffs<Tags>(&coeffs_self,
                                                            *tensors, mesh)...);

  // Perform the limiting on the modal coefficients
  bool limited_any_component = false;
  expand_pack(limit_one_tensor<::Tags::Modal<Tags>>(
      make_not_null(&coeffs_self), make_not_null(&limited_any_component), mesh,
      neighbor_data)...);

  // transform back to nodal coefficients
  const auto wrap_copy_nodal_coeffs =
      [&mesh, &coeffs_self](auto tag, const auto tensor) noexcept {
        auto& coeffs_tensor = get<decltype(tag)>(coeffs_self);
        auto tensor_it = tensor->begin();
        for (auto coeffs_it = coeffs_tensor.begin();
             coeffs_it != coeffs_tensor.end();
             (void)++coeffs_it, (void)++tensor_it) {
          to_nodal_coefficients(make_not_null(&*tensor_it), *coeffs_it, mesh);
        }
        return '0';
      };
  expand_pack(wrap_copy_nodal_coeffs(::Tags::Modal<Tags>{}, tensors)...);

  return limited_any_component;
}

template <size_t VolumeDim, typename... Tags>
template <typename Tag>
char Krivodonova<VolumeDim, tmpl::list<Tags...>>::limit_one_tensor(
    const gsl::not_null<Variables<tmpl::list<::Tags::Modal<Tags>...>>*>
        coeffs_self,
    const gsl::not_null<bool*> limited_any_component, const Mesh<1>& mesh,
    const std::unordered_map<
        std::pair<Direction<1>, ElementId<1>>, PackagedData,
        boost::hash<std::pair<Direction<1>, ElementId<1>>>>& neighbor_data)
    const noexcept {
  using tensor_type = typename Tag::type;
  for (size_t storage_index = 0; storage_index < tensor_type::size();
       ++storage_index) {
    // for each coefficient...
    for (size_t i = mesh.extents()[0] - 1; i > 0; --i) {
      auto& self_coeffs = get<Tag>(*coeffs_self)[storage_index];
      double min_abs_coeff = std::abs(self_coeffs[i]);
      const double sgn_of_coeff = sgn(self_coeffs[i]);
      bool sgns_all_equal = true;
      for (const auto& kv : neighbor_data) {
        const auto& neighbor_coeffs =
            get<Tag>(kv.second.modal_volume_data)[storage_index];
        const double tmp = kv.first.first.sign() * gsl::at(alphas_, i) *
                           (neighbor_coeffs[i - 1] - self_coeffs[i - 1]);

        min_abs_coeff = std::min(min_abs_coeff, std::abs(tmp));
        sgns_all_equal &= sgn(tmp) == sgn_of_coeff;
        if (not sgns_all_equal) {
          self_coeffs[i] = 0.0;
          break;
        }
      }
      if (sgns_all_equal) {
        const double tmp = sgn_of_coeff * min_abs_coeff;
        if (tmp == self_coeffs[i]) {
          break;
        }
        *limited_any_component |= true;
        self_coeffs[i] = tmp;
      }
    }
  }
  return '0';
}

template <size_t VolumeDim, typename... Tags>
template <typename Tag>
char Krivodonova<VolumeDim, tmpl::list<Tags...>>::limit_one_tensor(
    const gsl::not_null<Variables<tmpl::list<::Tags::Modal<Tags>...>>*>
        coeffs_self,
    const gsl::not_null<bool*> limited_any_component, const Mesh<2>& mesh,
    const std::unordered_map<
        std::pair<Direction<2>, ElementId<2>>, PackagedData,
        boost::hash<std::pair<Direction<2>, ElementId<2>>>>& neighbor_data)
    const noexcept {
  using tensor_type = typename Tag::type;
  const auto minmod = [&coeffs_self, &mesh, &neighbor_data, this](
                          const size_t local_i, const size_t local_j,
                          const size_t local_tensor_storage_index) noexcept {
    const auto& self_coeffs =
        get<Tag>(*coeffs_self)[local_tensor_storage_index];
    double min_abs_coeff = std::abs(
        self_coeffs[mesh.storage_index(Index<VolumeDim>{local_i, local_j})]);
    const double sgn_of_coeff = sgn(
        self_coeffs[mesh.storage_index(Index<VolumeDim>{local_i, local_j})]);
    for (const auto& kv : neighbor_data) {
      const Direction<2>& dir = kv.first.first;
      const auto& neighbor_coeffs =
          get<Tag>(kv.second.modal_volume_data)[local_tensor_storage_index];
      const size_t index_i =
          dir.axis() == Direction<VolumeDim>::Axis::Xi ? local_i - 1 : local_i;
      const size_t index_j =
          dir.axis() == Direction<VolumeDim>::Axis::Eta ? local_j - 1 : local_j;
      // skip neighbors where we cannot compute a finite difference in that
      // direction because we are already at the lowest coefficient.
      if (index_i == std::numeric_limits<size_t>::max() or
          index_j == std::numeric_limits<size_t>::max()) {
        continue;
      }
      const size_t alpha_index =
          dir.axis() == Direction<VolumeDim>::Axis::Xi ? local_i : local_j;
      const double tmp =
          dir.sign() * gsl::at(alphas_, alpha_index) *
          (neighbor_coeffs[mesh.storage_index(
               Index<VolumeDim>{index_i, index_j})] -
           self_coeffs[mesh.storage_index(Index<VolumeDim>{index_i, index_j})]);

      min_abs_coeff = std::min(min_abs_coeff, std::abs(tmp));
      if (sgn(tmp) != sgn_of_coeff) {
        return 0.0;
      }
    }
    return sgn_of_coeff * min_abs_coeff;
  };

  for (size_t tensor_storage_index = 0;
       tensor_storage_index < tensor_type::size(); ++tensor_storage_index) {
    // for each coefficient...
    for (size_t i = mesh.extents()[0] - 1; i > 0; --i) {
      for (size_t j = i; j < mesh.extents()[1]; --j) {
        // Check if we are done limiting, and if so we move on to the next
        // tensor component.
        auto& self_coeffs = get<Tag>(*coeffs_self)[tensor_storage_index];
        // We treat the different cases separately to reduce the number of
        // times we call minmod, not because it is required for correctness.
        if (UNLIKELY(i == j)) {
          const double tmp = minmod(i, j, tensor_storage_index);
          if (tmp == self_coeffs[mesh.storage_index(Index<VolumeDim>{i, j})]) {
            goto next_tensor_index;
          }
          *limited_any_component |= true;
          self_coeffs[mesh.storage_index(Index<VolumeDim>{i, j})] = tmp;
        } else {
          const double tmp_ij = minmod(i, j, tensor_storage_index);
          const double tmp_ji = minmod(j, i, tensor_storage_index);
          if (tmp_ij ==
                  self_coeffs[mesh.storage_index(Index<VolumeDim>{i, j})] and
              tmp_ji ==
                  self_coeffs[mesh.storage_index(Index<VolumeDim>{j, i})]) {
            goto next_tensor_index;
          }
          *limited_any_component |= true;
          self_coeffs[mesh.storage_index(Index<VolumeDim>{i, j})] = tmp_ij;
          self_coeffs[mesh.storage_index(Index<VolumeDim>{j, i})] = tmp_ji;
        }
      }
    }
  next_tensor_index:
    continue;
  }
  return '0';
}

template <size_t VolumeDim, typename... Tags>
template <typename Tag>
char Krivodonova<VolumeDim, tmpl::list<Tags...>>::limit_one_tensor(
    const gsl::not_null<Variables<tmpl::list<::Tags::Modal<Tags>...>>*>
        coeffs_self,
    const gsl::not_null<bool*> limited_any_component, const Mesh<3>& mesh,
    const std::unordered_map<
        std::pair<Direction<3>, ElementId<3>>, PackagedData,
        boost::hash<std::pair<Direction<3>, ElementId<3>>>>& neighbor_data)
    const noexcept {
  using tensor_type = typename Tag::type;
  const auto minmod = [&coeffs_self, &mesh, &neighbor_data, this](
                          const size_t local_i, const size_t local_j,
                          const size_t local_k,
                          const size_t local_tensor_storage_index) noexcept {
    const auto& self_coeffs =
        get<Tag>(*coeffs_self)[local_tensor_storage_index];
    double min_abs_coeff = std::abs(self_coeffs[mesh.storage_index(
        Index<VolumeDim>{local_i, local_j, local_k})]);
    const double sgn_of_coeff = sgn(self_coeffs[mesh.storage_index(
        Index<VolumeDim>{local_i, local_j, local_k})]);
    for (const auto& kv : neighbor_data) {
      const Direction<3>& dir = kv.first.first;
      const auto& neighbor_coeffs =
          get<Tag>(kv.second.modal_volume_data)[local_tensor_storage_index];
      const size_t index_i =
          dir.axis() == Direction<VolumeDim>::Axis::Xi ? local_i - 1 : local_i;
      const size_t index_j =
          dir.axis() == Direction<VolumeDim>::Axis::Eta ? local_j - 1 : local_j;
      const size_t index_k = dir.axis() == Direction<VolumeDim>::Axis::Zeta
                                 ? local_k - 1
                                 : local_k;
      // skip neighbors where we cannot compute a finite difference in that
      // direction because we are already at the lowest coefficient.
      if (index_i == std::numeric_limits<size_t>::max() or
          index_j == std::numeric_limits<size_t>::max() or
          index_k == std::numeric_limits<size_t>::max()) {
        continue;
      }
      const size_t alpha_index =
          dir.axis() == Direction<VolumeDim>::Axis::Xi
              ? local_i
              : dir.axis() == Direction<VolumeDim>::Axis::Eta ? local_j
                                                              : local_k;
      const double tmp = dir.sign() * gsl::at(alphas_, alpha_index) *
                         (neighbor_coeffs[mesh.storage_index(
                              Index<VolumeDim>{index_i, index_j, index_k})] -
                          self_coeffs[mesh.storage_index(
                              Index<VolumeDim>{index_i, index_j, index_k})]);

      min_abs_coeff = std::min(min_abs_coeff, std::abs(tmp));
      if (sgn(tmp) != sgn_of_coeff) {
        return 0.0;
      }
    }
    return sgn_of_coeff * min_abs_coeff;
  };

  for (size_t tensor_storage_index = 0;
       tensor_storage_index < tensor_type::size(); ++tensor_storage_index) {
    // for each coefficient...
    for (size_t i = mesh.extents()[0] - 1; i > 0; --i) {
      for (size_t j = i; j < mesh.extents()[1]; --j) {
        for (size_t k = j; k < mesh.extents()[2]; --k) {
          // Check if we are done limiting, and if so we move on to the next
          // tensor component.
          auto& self_coeffs = get<Tag>(*coeffs_self)[tensor_storage_index];
          // We treat the different cases separately to reduce the number of
          // times we call minmod, not because it is required for correctness.
          //
          // Note that the case `i == k and i != j` cannot be encountered since
          // the loop bounds are `i >= j >= k`.
          if (UNLIKELY(i == j and i == k)) {
            const double tmp = minmod(i, j, k, tensor_storage_index);
            if (tmp ==
                self_coeffs[mesh.storage_index(Index<VolumeDim>{i, j, k})]) {
              goto next_tensor_index;
            }
            *limited_any_component |= true;
            self_coeffs[mesh.storage_index(Index<VolumeDim>{i, j, k})] = tmp;
          } else if (i == j and i != k) {
            const double tmp_ijk = minmod(i, j, k, tensor_storage_index);
            const double tmp_ikj = minmod(i, k, j, tensor_storage_index);
            const double tmp_kij = minmod(k, i, j, tensor_storage_index);
            if (tmp_ijk == self_coeffs[mesh.storage_index(
                               Index<VolumeDim>{i, j, k})] and
                tmp_ikj == self_coeffs[mesh.storage_index(
                               Index<VolumeDim>{i, k, j})] and
                tmp_kij == self_coeffs[mesh.storage_index(
                               Index<VolumeDim>{k, i, j})]) {
              goto next_tensor_index;
            }
            *limited_any_component |= true;
            self_coeffs[mesh.storage_index(Index<VolumeDim>{i, j, k})] =
                tmp_ijk;
            self_coeffs[mesh.storage_index(Index<VolumeDim>{i, k, j})] =
                tmp_ikj;
            self_coeffs[mesh.storage_index(Index<VolumeDim>{k, i, j})] =
                tmp_kij;
          } else if (i != j and j == k) {
            const double tmp_ijk = minmod(i, j, k, tensor_storage_index);
            const double tmp_kij = minmod(k, i, j, tensor_storage_index);
            const double tmp_kji = minmod(k, j, i, tensor_storage_index);
            if (tmp_ijk == self_coeffs[mesh.storage_index(
                               Index<VolumeDim>{i, j, k})] and
                tmp_kij == self_coeffs[mesh.storage_index(
                               Index<VolumeDim>{k, i, j})] and
                tmp_kji == self_coeffs[mesh.storage_index(
                               Index<VolumeDim>{k, j, i})]) {
              goto next_tensor_index;
            }
            *limited_any_component |= true;
            self_coeffs[mesh.storage_index(Index<VolumeDim>{i, j, k})] =
                tmp_ijk;
            self_coeffs[mesh.storage_index(Index<VolumeDim>{k, i, j})] =
                tmp_kij;
            self_coeffs[mesh.storage_index(Index<VolumeDim>{k, j, i})] =
                tmp_kji;
          } else {
            const double tmp_ijk = minmod(i, j, k, tensor_storage_index);
            const double tmp_jik = minmod(j, i, k, tensor_storage_index);
            const double tmp_ikj = minmod(i, k, j, tensor_storage_index);
            const double tmp_jki = minmod(j, k, i, tensor_storage_index);
            const double tmp_kij = minmod(k, i, j, tensor_storage_index);
            const double tmp_kji = minmod(k, j, i, tensor_storage_index);
            if (tmp_ijk == self_coeffs[mesh.storage_index(
                               Index<VolumeDim>{i, j, k})] and
                tmp_jik == self_coeffs[mesh.storage_index(
                               Index<VolumeDim>{j, i, k})] and
                tmp_ikj == self_coeffs[mesh.storage_index(
                               Index<VolumeDim>{i, k, j})] and
                tmp_jki == self_coeffs[mesh.storage_index(
                               Index<VolumeDim>{j, k, i})] and
                tmp_kij == self_coeffs[mesh.storage_index(
                               Index<VolumeDim>{k, i, j})] and
                tmp_kji == self_coeffs[mesh.storage_index(
                               Index<VolumeDim>{k, j, i})]) {
              goto next_tensor_index;
            }
            *limited_any_component |= true;
            self_coeffs[mesh.storage_index(Index<VolumeDim>{i, j, k})] =
                tmp_ijk;
            self_coeffs[mesh.storage_index(Index<VolumeDim>{j, i, k})] =
                tmp_jik;
            self_coeffs[mesh.storage_index(Index<VolumeDim>{i, k, j})] =
                tmp_ikj;
            self_coeffs[mesh.storage_index(Index<VolumeDim>{j, k, i})] =
                tmp_jki;
            self_coeffs[mesh.storage_index(Index<VolumeDim>{k, i, j})] =
                tmp_kij;
            self_coeffs[mesh.storage_index(Index<VolumeDim>{k, j, i})] =
                tmp_kji;
          }
        }
      }
    }
  next_tensor_index:
    continue;
  }
  return '0';
}

template <size_t VolumeDim, typename... Tags>
template <typename Tag, size_t Dim>
char Krivodonova<VolumeDim, tmpl::list<Tags...>>::
    fill_variables_tag_with_spectral_coeffs(
        const gsl::not_null<Variables<tmpl::list<::Tags::Modal<Tags>...>>*>
            modal_coeffs,
        const typename Tag::type& nodal_tensor, const Mesh<Dim>& mesh) const
    noexcept {
  auto& coeffs_tensor = get<::Tags::Modal<Tag>>(*modal_coeffs);
  auto tensor_it = nodal_tensor.begin();
  for (auto coeffs_it = coeffs_tensor.begin(); coeffs_it != coeffs_tensor.end();
       (void)++coeffs_it, (void)++tensor_it) {
    to_modal_coefficients(make_not_null(&*coeffs_it), *tensor_it, mesh);
  }
  return '0';
}

template <size_t VolumeDim, typename... Tags>
void Krivodonova<VolumeDim, tmpl::list<Tags...>>::pup(PUP::er& p) noexcept {
  p | alphas_;
  p | disable_for_debugging_;
}

template <size_t VolumeDim, typename... Tags>
bool Krivodonova<VolumeDim, tmpl::list<Tags...>>::operator==(
    const Krivodonova<VolumeDim, tmpl::list<Tags...>>& rhs) const noexcept {
  return alphas_ == rhs.alphas_ and
         disable_for_debugging_ == rhs.disable_for_debugging_;
}

template <size_t VolumeDim, typename... Tags>
bool operator!=(
    const Krivodonova<VolumeDim, tmpl::list<Tags...>>& lhs,
    const Krivodonova<VolumeDim, tmpl::list<Tags...>>& rhs) noexcept {
  return not(lhs == rhs);
}
}  // namespace Limiters


#pragma once

#include <fstream>
#include <sstream>
#include <map>
#include <vector>
#include <algorithm>
#include <assert.h>
#include <iostream>
#include <cmath>
#include <stdexcept>
#include <list>
#include <float.h>
#include <random>
#include <chrono>
#include <unordered_map>
#include <unordered_set>
#include <stdexcept>
#include <time.h>
#include <stdio.h>
#include <boost/math/distributions/binomial.hpp>
#include <boost/math/distributions/poisson.hpp>
#include <boost/random.hpp>
#include <boost/math/special_functions/gamma.hpp>

//#define REUSE
#define STACK
//#define SAMPLE

#define MISS_BAR 1024 * 2

namespace sdd {

/* to calculate C(a, k) / C(b, k) */
inline double combinationRatio(int b, int a, int k)
{
	if (b < a || !a || !b || !k)
		throw std::exception();

	/* This is for the condition that wouldn't occur. */
	if (k > a) return 0.0;

	int loops = b - a;
	double result = 1.0;
	while (loops--)
		result *= (double)(b - k) / (b--);

	if (result <= DBL_MIN)
		throw std::exception();

	return result;
}

/* calculate log2(s) + 1 */
template<class T>
inline T log2p1(T s)
{
	T result = 0;
	while (s) {
		s >>= 1;
		++result;
	}

	return result;
}

/* for recording distribution into a Histogram,
   Accur is the accuracy of transforming calculation */
template <class B = int64_t>
class Histogram
{
	B * bins;
	B samples;
	int size;

public:
	Histogram() : bins(nullptr), samples(0), size(0) {};

	~Histogram()
	{
    	delete [] bins;
	}

	void allocateBins(int _size)
	{
    	size = _size;
    	bins = new B[size];
    	/* init bins to 0 */
    	for (int i = 0; i < size; ++i)
        	bins[i] = 0;

    	std::cout << "size of bins " << size << std::endl;
	}

	void clear()
	{
    	delete [] bins;
    	samples = 0;
    	/* when clear bins, the size keeps unchanged */
    	allocateBins(size);
	}

	void sample(B x);

	void print(std::ofstream & file)
	{
    	//file.write((char *)bins, sizeof(B) * size);
    	for (int i = 0; i < size; ++i) {
    		file << bins[i] << " ";
    	}
    	file << "\n";
	}
};

template <class Accur>
class AvlNode
{
	//int holes;
	friend class AvlTreeStack;
	/* this is the num of holes of this tree,
	including the holes of subtrees and the self interval. */
	int holes;
	/* this is the num of holes of entire right subtree. */
	int rHoles;
	std::pair<Accur, Accur> interval;
	int height;
	AvlNode<Accur> * left;
	AvlNode<Accur> * right;

public:
	AvlNode(Accur & a);

	AvlNode(AvlNode<Accur> & n);

	~AvlNode();

	static int getHeight(AvlNode<Accur> * & node)
	{
		return node ? node->height : -1;
	}

	void updateHeight();

	void updateHoles();
};

/* for calculating stack distance distribution via AVL Tree, with no sampling*/
class AvlTreeStack
{
	std::map <uint64_t, long> addrMap;
	AvlNode<long> * root;
	/* the index of refs in memory trace */
	long index;
	/* holes between current ref and last ref with same address */
	int curHoles;

public:
	AvlTreeStack(long & v);

	AvlTreeStack();

	~AvlTreeStack() { destroy(root); }

	void destroy(AvlNode<long> * & tree);

	void clear();

	void insert(AvlNode<long> * & tree, long & v);

	void insert(long & a);

	/*AvlNode<long> * & find(AvlNode<long> * & tree, int & v)
	{
	if (!tree)
	return nullptr;

	if (v < tree->holes)
	find(tree->left, v);
	else if (v > tree->holes)
	find(tree->right, v);
	else
	return tree;
	}*/

	/* find the minimal interval node */
	AvlNode<long> * & findMin(AvlNode<long> * & tree);

	/* find the maximal interval node */
	AvlNode<long> * & findMax(AvlNode<long> * & tree);

	void remove(AvlNode<long> * & tree, std::pair<long, long> & inter);

	void rotate(AvlNode<long> * & tree);

	void doubleRotate(AvlNode<long> * & tree);

	void balance(AvlNode<long> * & tree);

	void calStackDist(uint64_t addr, Histogram<> & hist);
};

/* do reuse distance statistics */
class ReuseDist
{
	std::map<uint64_t, long> addrMap;
	long index;

public:
	ReuseDist() {};
	~ReuseDist() {};

	void calReuseDist(uint64_t addr, Histogram<> & hist);
};

/* stack distance staticstics with sampling */
class SampleStack
{
	long index;

	typedef std::unordered_set<uint64_t> AddrSet;
	/* each watchpoint has a set to keep the unique mem refs */
	std::unordered_map<uint64_t, AddrSet> addrTable;
	int randNum;
	int sampleCounter;
	int expectSamples;
	int hibernInter;
	int sampleInter;
	long statusCounter;
	bool isSampling;

public:
	SampleStack();

	/* to generate a random number */
	int genRandom();

	void calStackDist(uint64_t addr, Histogram<> & hist);
};

}; // namespace sdd


#include <fstream>
#include <iostream>
#include <string>
#include <cmath>
#include <vector>
#include <cfloat>
#include <boost/filesystem.hpp>
#include "canvas.h"
#include "vector.h"
#include "matrix.h"
#include "shapes.h"
#include "model.h"
#include "light.h"

// Local function prototypes
void swap(vec3&, vec3&);
void sort_desc(std::vector<vec3>&);

////////////

// Constructor
Canvas::Canvas(int w, int h) : width(w), height(h)
{
    canvas.resize(w, std::vector<color>(h));
    depth_buffer.resize(w, std::vector<float>(h));
    for (int i = 0; i < w; ++i)
    {
        std::fill(depth_buffer[i].begin(),
                  depth_buffer[i].end(),
                  -std::numeric_limits<float>::max()
        );
    }
}

vec3 Canvas::convert_ndc_to_canvas(const vec3 &p)
{
    vec3 canvas_coords(
        (int)(0.5f * (width * p.x() + width)),
        (int)(0.5f * (height * p.y() + height)),
              0
    );

    return canvas_coords;
}

vec3 Canvas::convert_canvas_to_ndc(const vec3 &p)
{
    vec3 ndc_coords(
        (2 * p.x() - width) / width,
        (2 * p.y() - height) / height,
         0
    );

    return ndc_coords;
}

void Canvas::draw_shapes()
{
    // Draw all the shapes that are stored in the shapes_list
    for (std::vector<shape*>::const_iterator it = shapes_list.begin();
           it != shapes_list.end(); ++it)
    {
        // TODO refactor this to be just one function
        //(*it)->draw(canvas);
    }
}

void Canvas::apply_transform(const mat4& m_transform)
{
    // Apply transformation matrix on all shapes
    // and their corresponding vertices
    for (std::vector<shape *>::const_iterator it = shapes_list.begin();
           it != shapes_list.end(); ++it)
    {
        (*it)->apply_transform(m_transform);
    }
}

void Canvas::reset_canvas(const color& _color)
{
    for (int h = 0; h < height; ++h)
    {
        for (int w = 0; w < width; ++w)
        {
            canvas[w][h] = _color;

            // Set depth_buffer values to -FLT_MAX
            // which serves as the furthest point from the camera
            depth_buffer[w][h] = -FLT_MAX;
        }
    }
}

void Canvas::print_canvas()
{
    // Output canvas into ppm file
    std::cout << "P3\n" << width << " " << height << "\n255\n";
    for (int y = height-1; y >= 0; --y)
    {
        for (int x = 0; x < width; ++x)
        {
            int r = (int)(canvas[x][y].r() * 255.99f);
            int g = (int)(canvas[x][y].g() * 255.99f);
            int b = (int)(canvas[x][y].b() * 255.99f);

            std::cout << r << " "
                      << g << " "
                      << b << std::endl;
        }
    }
}

void Canvas::print_canvas(std::string _title)
{
    // Create an output directory to store renders
    // using the boost filesystem lib
    std::string output_dir = "../renders/";
    if (!boost::filesystem::exists(output_dir))
    {
        std::cout << "Creating output directory for renders at: " << output_dir << std::endl;
        boost::filesystem::create_directory(output_dir);
    }

    // Output canvas into ppm file
    std::ofstream image_file;
    image_file.open (output_dir + _title.c_str());
    image_file << "P3\n" << width << " " << height << "\n255\n";
    for (int y = height-1; y >= 0; --y)
    {
        for (int x = 0; x < width; ++x)
        {
            int r = (int)(canvas[x][y].r() * 255.99f);
            int g = (int)(canvas[x][y].g() * 255.99f);
            int b = (int)(canvas[x][y].b() * 255.99f);

            image_file << r << " "
                       << g << " "
                       << b << "\n";
        }
    }
    image_file.close();

    std::cout << "Printing " << _title << " to " << output_dir << std::endl;
}

void Canvas::put_pixel(int x, int y, const color& _color)
{
    // Only color the pixel if it is within the bounds of the canvas
    if (x > canvas.size() ||
        y > canvas[0].size() ||
        x < 0 || y < 0)
    {
        return;
    }

    // TODO
    // This is done due to the issues with convert_ndc_to_canvas
    // the 0.5f multiplication factor causes a 1.0 ndc value to be out of range
    // using 0.49999f can work but can be inaccurate based on the resolution
    if (x == canvas.size())
    {
        x--;
    }
    if (y == canvas[0].size())
    {
        y--;
    }

    canvas[x][y] = _color;
}

void swap(vec3 &p0, vec3 &p1)
{
    vec3 temp(p0);
    p0 = p1;
    p1 = temp;
}

// ==========================================
// This function orders the list of vertices
// by descending Y.
// So that v[0].y >= v[1].y >= .. >= v[n-1].y
// ==========================================
void sort_desc(std::vector<vec3> &verts)
{
    for (int i = 0; i < verts.size(); ++i)
    {
        for (int j = i + 1; j < verts.size(); ++j)
        {
            if (verts[i].y() < verts[j].y())
            {
                swap(verts[i], verts[j]);
            }
        }
    }
}

// ==========================================
// Draws a model from a *.obj format
// This is done by processing three vertices
// within the face
// ==========================================
void Canvas::draw_model(Model model, const color& _color, bool is_wire)
{
    vec3 new_color = _color;
    int nfaces = model.num_faces();
    for (int i = 0; i < nfaces; ++i)
    {
        bool is_back_face = false;
        // Check backface by using normal with scene camera

        // Get the index of the vertices that comprise the face
        std::vector<int> vert_indices = model.face(i);

        vec3 p0 = model.vert(vert_indices[0]);
        vec3 p1 = model.vert(vert_indices[1]);
        vec3 p2 = model.vert(vert_indices[2]);

        // TODO rethink how to handle drawing to NDC
        // Need to rethink how to tie with camera functions
        // Compute the direction of the normal and compare it with the light
        vec3 normal = cross((p1-p0), (p2-p0));
        normal.make_unit_vector();

        // TODO check backface based on camera position?
        vec3 cameraDir = camera.look_from - camera.look_at;
        cameraDir.make_unit_vector();
        is_back_face = dot(cameraDir, normal) < 0.0f;

        if (!is_wire && !lights.empty() && !is_back_face)
        {
            // TODO calculate direction off each fragment instead of a single vertex for more accurate results
            // TODO base light dir off of the center of the triangle
            vec3 midPoint = (p2 - ((p0 - p1)*0.5f)) * 0.5f;

            vec3 diffuse(0.0f);
            for (auto light : lights)
            {
                vec3 light_dir = (light->pos) - midPoint;
                light_dir.make_unit_vector();

                diffuse += std::max(0.0f, dot(normal, light_dir)) * light->color;
            }

            float ambient = 0.2f;
            new_color = (ambient + diffuse) * _color;
        }

        // TODO add depth buffer

        // Back-face culling - don't draw if it's a back-face
        if (!is_back_face)
        {
            // Draw the triangles based on the position of the three vertices
            draw_triangle(convert_ndc_to_canvas(p0),
                    convert_ndc_to_canvas(p1),
                    convert_ndc_to_canvas(p2),
                    new_color,
                    is_wire);
        }
    }
}

void Canvas::draw_line(vec3 p0, vec3 p1, const color& _color)
{
    int dx = p1.x() - p0.x();
    int dy = p1.y() - p0.y();

    // Check if p0 == p1, then just paint just that point
    if (dx == 0 && dy == 0)
    {
        put_pixel(p0.x(), p0.y(), _color);
        return;
    }

    // Draw line using y = f(x)
    if (abs(dx) > abs(dy))
    {
        if (p0.x() > p1.x())
        {
            swap(p0, p1);
        }

        int x_end = p1.x();
        float y = p0.y();
        float m = (p1.y() - p0.y())/(p1.x() - p0.x());  // slope of line

        for (int x = p0.x(); x < x_end; ++x)
        {
            put_pixel(x, (int)y, _color);
            y += m;
        }
    }
    // Draw line using x = f(y)
    else
    {
        if (p0.y() > p1.y())
        {
            swap(p0, p1);
        }

        int y_end = p1.y();
        float x = p0.x();
        float m = (p1.x() - p0.x())/(p1.y() - p0.y());  // slope of line

        for (int y = p0.y(); y < y_end; ++y)
        {
            put_pixel((int)x, y, _color);
            x += m;
        }
    }
}

// ==========================================
// Fills triangle by drawing lines from
// bottom-most point p1/p2 to the top-most
// point p0.
//
//     p0
//    /  \
//   p1--p2
//
// precondition: p0.y > p1.y == p2.y
// ==========================================
void Canvas::fill_flat_bottom_triangle(vec3 p0, vec3 p1, vec3 p2, const color &_color)
{
    int dy = p0.y() - p1.y();
    float slope_p1_p0 = (p0.x() - p1.x())/(p0.y() - p1.y());
    float slope_p2_p0 = (p0.x() - p2.x())/(p0.y() - p2.y());

    for (int i = 0; i < dy; ++i)
    {
        p1.e[0] += slope_p1_p0;
        p1.e[1] ++;
        p2.e[0] += slope_p2_p0;
        p2.e[1] ++;

        draw_line(p1, p2, _color);
    }
}

// =====================================
// Fills triangle by drawing lines from
// bottom most point p2 to the
// top-most points p0 and p1.
//
//   p0--p1
//    \  /
//     p2
//
// precondition: p0.y == p1.y > p2.y
// =====================================
void Canvas::fill_flat_top_triangle(vec3 p0, vec3 p1, vec3 p2, const color &_color)
{
    int dy = p0.y() - p2.y();
    float slope_p2_p0 = (p0.x() - p2.x())/(p0.y() - p2.y());
    float slope_p2_p1 = (p1.x() - p2.x())/(p1.y() - p2.y());

    vec3 pa = p2;
    vec3 pb = p2;

    for (int i = 0; i < dy; ++i)
    {
        pa.e[0] += slope_p2_p0;
        pa.e[1] ++;
        pb.e[0] += slope_p2_p1;
        pb.e[1] ++;

        draw_line(pa, pb, _color);
    }
}

//==========================================
// Calculates the triangles barycentric coordinate
//=========================================
vec3 barycentric(vec3 p0, vec3 p1, vec3 p2)
{
    vec3 bp;
}

//==========================================
// Draws a filled triangle, by splitting
// a triangle into a flat bottom part and
// flat top part. Then fills each line
// horizontally.
// =========================================
void Canvas::draw_triangle_filled(vec3 p0, vec3 p1, vec3 p2, const color &_color)
{
    // sort vertices on descending y
    std::vector<vec3> verts = {p0, p1, p2};
    sort_desc(verts);

    p0 = verts[0];
    p1 = verts[1];
    p2 = verts[2];

    if ((int)p1.y() == (int)p2.y())
    {
        fill_flat_bottom_triangle(p0, p1, p2, _color);
    }
    else if ((int)p0.y() == (int)p1.y())
    {
        fill_flat_top_triangle(p0, p1, p2, _color);
    }
    else
    {
        // Split the triangle into 2 triangles
        // one with a flat top and one with a flat bottom part
        //
        // precondition: p0.y > p1.y > p2.y
        //

        // slope from p0 to p2
        float mx = (p2.x() - p0.x())/(p2.y() - p0.y());
        // Get pa using pa = p0 + t(p2 - p0)
        vec3 pa(p0.x() + (p1.y() - p0.y())*mx, p1.y(), 0.0f);

        // Current use requires that p0.y > p1.y == pa.y
        //                           p1.y == pa.y > p2.y
        // due to the fill triangle method signature
        fill_flat_bottom_triangle(p0, p1, pa, _color);
        fill_flat_top_triangle(p1, pa, p2, _color);
    }
}

void Canvas::draw_triangle_wireframe(vec3 p0, vec3 p1, vec3 p2, const color &_color)
{
    draw_line(p0, p1, _color);
    draw_line(p1, p2, _color);
    draw_line(p2, p0, _color);
}

void Canvas::draw_triangle(vec3 p0, vec3 p1, vec3 p2, const color &_color, bool is_wire)
{
    if (is_wire)
    {
        draw_triangle_wireframe(p0, p1, p2, _color);
    }
    else
    {
        draw_triangle_filled(p0, p1, p2, _color);
    }
}