// Copyright Nick Thompson, 2020
// Use, modification and distribution are 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_MATH_INTERPOLATORS_CUBIC_HERMITE_HPP
#define BOOST_MATH_INTERPOLATORS_CUBIC_HERMITE_HPP
#include <memory>
#include <boost/math/interpolators/detail/cubic_hermite_detail.hpp>

namespace boost::math::interpolators {

template<class RandomAccessContainer>
class cubic_hermite {
public:
    using Real = typename RandomAccessContainer::value_type;

    cubic_hermite(RandomAccessContainer && x, RandomAccessContainer && y, RandomAccessContainer && dydx)
    : impl_(std::make_shared<detail::cubic_hermite_detail<RandomAccessContainer>>(std::move(x), std::move(y), std::move(dydx)))
    {}

    Real operator()(Real x) const {
        return impl_->operator()(x);
    }

    Real prime(Real x) const {
        return impl_->prime(x);
    }

    friend std::ostream& operator<<(std::ostream & os, const cubic_hermite & m)
    {
        os << *m.impl_;
        return os;
    }

    void push_back(Real x, Real y, Real dydx) {
        impl_->push_back(x, y, dydx);
    }

private:
    std::shared_ptr<detail::cubic_hermite_detail<RandomAccessContainer>> impl_;
};

}
#endif


//  Boost next_prior.hpp header file  ---------------------------------------//

//  (C) Copyright Dave Abrahams and Daniel Walker 1999-2003. Distributed under 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)

//  See http://www.boost.org/libs/utility for documentation.

//  Revision History
//  13 Dec 2003  Added next(x, n) and prior(x, n) (Daniel Walker)

#ifndef BOOST_NEXT_PRIOR_HPP_INCLUDED
#define BOOST_NEXT_PRIOR_HPP_INCLUDED

#include <iterator>
#if defined(_MSC_VER) && _MSC_VER <= 1310
#include <boost/mpl/and.hpp>
#include <boost/type_traits/is_integral.hpp>
#endif
#include <boost/type_traits/is_unsigned.hpp>
#include <boost/type_traits/integral_promotion.hpp>
#include <boost/type_traits/make_signed.hpp>
#include <boost/type_traits/has_plus.hpp>
#include <boost/type_traits/has_plus_assign.hpp>
#include <boost/type_traits/has_minus.hpp>
#include <boost/type_traits/has_minus_assign.hpp>

namespace geofeatures_boost {} namespace boost = geofeatures_boost; namespace geofeatures_boost {

//  Helper functions for classes like bidirectional iterators not supporting
//  operator+ and operator-
//
//  Usage:
//    const std::list<T>::iterator p = get_some_iterator();
//    const std::list<T>::iterator prev = geofeatures_boost::prior(p);
//    const std::list<T>::iterator next = geofeatures_boost::next(prev, 2);

//  Contributed by Dave Abrahams

namespace next_prior_detail {

template< typename T, typename Distance, bool HasPlus = has_plus< T, Distance >::value >
struct next_impl2
{
    static T call(T x, Distance n)
    {
        std::advance(x, n);
        return x;
    }
};

template< typename T, typename Distance >
struct next_impl2< T, Distance, true >
{
    static T call(T x, Distance n)
    {
        return x + n;
    }
};

template< typename T, typename Distance, bool HasPlusAssign = has_plus_assign< T, Distance >::value >
struct next_impl1 :
    public next_impl2< T, Distance >
{
};

template< typename T, typename Distance >
struct next_impl1< T, Distance, true >
{
    static T call(T x, Distance n)
    {
        x += n;
        return x;
    }
};

template<
    typename T,
    typename Distance,
    typename PromotedDistance = typename integral_promotion< Distance >::type,
#if !defined(_MSC_VER) || _MSC_VER > 1310
    bool IsUInt = is_unsigned< PromotedDistance >::value
#else
    // MSVC 7.1 has problems with applying is_unsigned to non-integral types
    bool IsUInt = mpl::and_< is_integral< PromotedDistance >, is_unsigned< PromotedDistance > >::value
#endif
>
struct prior_impl3
{
    static T call(T x, Distance n)
    {
        std::advance(x, -n);
        return x;
    }
};

template< typename T, typename Distance, typename PromotedDistance >
struct prior_impl3< T, Distance, PromotedDistance, true >
{
    static T call(T x, Distance n)
    {
        typedef typename make_signed< PromotedDistance >::type signed_distance;
        std::advance(x, -static_cast< signed_distance >(static_cast< PromotedDistance >(n)));
        return x;
    }
};

template< typename T, typename Distance, bool HasMinus = has_minus< T, Distance >::value >
struct prior_impl2 :
    public prior_impl3< T, Distance >
{
};

template< typename T, typename Distance >
struct prior_impl2< T, Distance, true >
{
    static T call(T x, Distance n)
    {
        return x - n;
    }
};

template< typename T, typename Distance, bool HasMinusAssign = has_minus_assign< T, Distance >::value >
struct prior_impl1 :
    public prior_impl2< T, Distance >
{
};

template< typename T, typename Distance >
struct prior_impl1< T, Distance, true >
{
    static T call(T x, Distance n)
    {
        x -= n;
        return x;
    }
};

} // namespace next_prior_detail

template <class T>
inline T next(T x) { return ++x; }

template <class T, class Distance>
inline T next(T x, Distance n)
{
    return next_prior_detail::next_impl1< T, Distance >::call(x, n);
}

template <class T>
inline T prior(T x) { return --x; }

template <class T, class Distance>
inline T prior(T x, Distance n)
{
    return next_prior_detail::prior_impl1< T, Distance >::call(x, n);
}

} // namespace geofeatures_boost

#endif  // BOOST_NEXT_PRIOR_HPP_INCLUDED


// Std
#include <algorithm>
#include <deque>
#include <unordered_map>
// Boost
#include <boost/graph/graphviz.hpp>
#include <boost/graph/depth_first_search.hpp>
#include <boost/graph/visitors.hpp>
// Our stuff
#include "chemgraph.hpp"
#include "ctab.hpp"
#include "log.hpp"
#include "gauss.hpp"

using namespace std;
using namespace boost;

ChemGraph toGraph(CTab& tab)
{
    ChemGraph graph;
    //add_vertex
    for_each(tab.atoms.begin(), tab.atoms.end(), [&graph](const AtomEntry& a)
    {
        add_vertex(AtomVertex(a.code), graph);
    });
    auto vrange = vertices(graph);
    //add edges
    for_each(tab.bounds.begin(), tab.bounds.end(), [&graph, &vrange](const BoundEntry& b)
    {
        add_edge(vrange.first[b.a1] - 1, vrange.first[b.a2] - 1, Bound(b.type), graph);
    });
    return graph;
}

int getValence(ChemGraph& graph, ChemGraph::vertex_descriptor vertex)
{
    auto edges = out_edges(vertex, graph);
    int valence = 0;
    for (auto p = edges.first; p != edges.second; p++)
    {
        valence += graph[*p].type;
    }
    return valence;
}

ChemGraph& addHydrogen(ChemGraph& graph)
{
    std::unordered_map<int, int> valences;
    valences.insert(make_pair(C.code(), 4));
    valences.insert(make_pair(N.code(), 3));
    valences.insert(make_pair(O.code(), 2));
    auto vtx = vertices(graph);
    for(auto p = vtx.first; p != vtx.second; p++)
    {
        //Note: no extra hydrogens for negative ions
        if(valences.find(graph[*p].code.code()) != valences.end()
            && graph[*p].code.charge() >= 0)
        {
            int normal_valence = valences[graph[*p].code.code()];
            // FIXME: counts 1.5 as 4 but that is "works for me"
            int cur_val = getValence(graph, *p);
            // fit with hydrogens if not enough valence
            for(int i=cur_val; i<normal_valence; i++)
            {
                size_t v = add_vertex(AtomVertex(H), graph);
                add_edge(*p, v, Bound(1), graph);
            }
        }
    }
    return graph;
}

class CodeWriter {
public:
    CodeWriter(ChemGraph& graph):g(graph){}
    template <class VertexOrEdge>
    void operator()(ostream& out, const VertexOrEdge& v) const {
      out << "[label=\"" << g[v].code.symbol() << " [" << v << "]" << "\"]";
    }
private:
    ChemGraph& g;
};

void dumpGraph(ChemGraph& graph, ostream& out)
{
    write_graphviz(out, graph, CodeWriter(graph));
}

ostream& operator<<(ostream& stream, const Cycle& cycle)
{
    for (auto & e : cycle.edges)
    {
        stream << e.first << "--" << e.second << '\n';
    }
    return stream;
}

inline set<pair<size_t, size_t>> chainToEdgeSet(const vector<size_t>& v){
    assert(v.size() > 1);
    set<pair<size_t,size_t>> out;
    for(size_t i=1; i<v.size(); i++){
        if(v[i-1] < v[i])
            out.insert(make_pair(v[i-1], v[i]));
        else
            out.insert(make_pair(v[i], v[i-1]));
    }
    if(v.front() < v.back())
        out.insert(make_pair(v.front(), v.back()));
    else
        out.insert(make_pair(v.back(), v.front()));
    return out;
}

void logCycle(vector<pair<vd,vd>>& c)
{
    for (auto & e : c)
    {
        LOG(DEBUG) << e.first << "--" << e.second << '\n';
    }
    LOG(DEBUG) << endline;
}

Cycle::Cycle(vector<pair<vd, vd>> edges_):edges(edges_)
{
    chain = cycleToChain(edges, [](const pair<vd,vd>&p){ return p; });
}

Cycle::Cycle(vector<vd> chain_):chain(chain_)
{
    auto eset = chainToEdgeSet(chain_);
    edges.resize(eset.size());
    copy(eset.begin(), eset.end(), edges.begin());
}

bool Cycle::intersects(const Cycle& that)const
{
   return intersection(that).size() > 0; // can be optimized futher
}

vector<pair<vd,vd>> Cycle::intersection(const Cycle& c)const
{
    vector<pair<vd,vd>> ret;
    auto& c1 = edges;
    auto& c2 = c.edges;
    set_intersection(c1.begin(), c1.end(), c2.begin(), c2.end(),
        back_inserter(ret));
    return ret;
}

Cycle& Cycle::markAromatic(ChemGraph& g)
{
    LOG(TRACE) << "CHAIN: ";
    for (int k : chain)
        LOG(TRACE) << k << " - ";
    LOG(TRACE) << endline;
    int piEl = 0;
    for_each(chain.begin(), chain.end(), [&g, &piEl](int n){
        LOG(TRACE) << "Atom # " << n << " " << g[n].code.symbol() << " pi E = " << g[n].piE << endline;
        if (g[n].piE > 0)
            piEl += g[n].piE;
        //TODO: add debug trace for < 0
    });
    aromatic_ = ((piEl - 2) % 4 == 0); // Hukkel rule 4n + 2
    LOG(TRACE) << "PI E " << piEl << " aromatic? : " << aromatic_ << endline;
    //assign cyclic-only DC
    if (aromatic_)
    {
        for (auto n : chain)
        {
            g[n].inAromaCycle = true;
        }
        for (auto e : edges)
        {
            auto ed = edge(e.first, e.second, g);
            g[ed.first].type = AROMATIC;
        }
    }
    return *this;
}

struct BfsNoop{
    void onVertex(size_t v){}
};

// assuming constant edge weight - no priority queue required
template<class Policy=BfsNoop>
struct ShortestPaths : Policy{
public:
    using G = ChemGraph;

    ShortestPaths(G& graph, size_t start, const vector<bool>& m=vector<bool>()):
    g(graph), visited(num_vertices(graph)), edgeTo(num_vertices(graph)), s(start), mask(m){
        queue.push_back(start);
        visited[start] = true;
        edgeTo[start] = (size_t)1<<(sizeof(size_t)*8-1);
        bfs();
    }
    // apply functor to each node along the shortest path from v to starting point
    // except the starting point itself
    template<class Fn>
    void apply(size_t v, Fn&& fn, bool includeStart=false){
        if(!visited[v]) // cut apply for unexplored vertices
            return;
        size_t w = v;
        while(w != s){
            fn(w);
            w = edgeTo[w];
        }
        if(includeStart)
            fn(s);
    }
    // get shortest path to s
    vector<size_t> path(size_t v, bool includeStart=false){
        vector<size_t> vec;
        apply(v, [&vec](size_t w){ vec.push_back(w); }, includeStart);
        return vec;
    }

    // get prior vertex on path to this one
    size_t prev(size_t p){
        return edgeTo[p];
    }
private:
    void bfs(){
        while(!queue.empty()){
            size_t v = queue.front();
            queue.pop_front();
            // push all not visited
            auto adj = adjacent_vertices(v, g);
            for(auto p = adj.first; p != adj.second; p++ ){
                auto w = *p;
                if(!visited[w] && (mask.empty() || mask[w])){
                    edgeTo[w] = v;
                    visited[w] = true;
                    queue.push_back(w);
                }
            }
        }
    }
    G& g;
    vector<bool> visited;
    vector<size_t> edgeTo;
    deque<size_t> queue;
    size_t s;
    const vector<bool>& mask;

};

ShortestPaths<> shortestPaths(ChemGraph& g, size_t start, const vector<bool>& mask){
    return ShortestPaths<>(g, start, mask);
}

// depth-first search to detect all cycles
// ploicy-based design, final processing is deffered to the inherited policy
template<class Policy>
struct CycleDetect : Policy{
public:
    using G = ChemGraph;
    CycleDetect(G& graph, size_t start):Policy(), g(graph), visited(num_vertices(graph)){
        dfs(start);
    }

private:
    void dfs(size_t v){
        visited[v] = true;
        auto prev = path.size() ? path.back() : -1;
        path.push_back(v);
        auto adj = adjacent_vertices(v, g);
        for(auto p = adj.first; p != adj.second; p++ ){
            auto w = *p;
            auto e = edge(v, w, g);
            // TODO: turn to predicate or just use Boost DFS
            if(g[e.first].type >= STEREO)
                continue;
            if(!visited[w]){
                dfs(w); //pushes w on path
                path.pop_back();
            }
            else if(w != prev){ // visited and not previous one
                auto i = find(path.begin(), path.end(), w);
                if(i != path.end()) //cycle
                    Policy::onCycle(i, path.end());
            }
        }
    }
    G& g;
    vector<bool> visited;
    vector<int> path;
};

struct FetchCycles{
    vector<vector<size_t>> cycles;
    template<class I>
    void onCycle(I beg, I end){
        cycles.emplace_back(beg, end);
    }
};

//some cycle basis not even Horton's cycle basis
vector<vector<size_t>> cycleBasis(ChemGraph& g, size_t start=0){
    return CycleDetect<FetchCycles>(g, start).cycles;
}

vector<Cycle> minimalCycleBasis(ChemGraph& g){
    vector<vector<size_t>> isolatedCycles; //
    vector<vector<size_t>> basisCandidates; // that are not isolated
    auto const V = num_vertices(g);
    vector<vd> inCycle(V); // >0 if v is on some cycle
    vector<bool> inCycleSystem(V); // if v is in some cycle system
    LOG(DEBUG)<<"IN CYCLE: "<<inCycle<<endline;
    // find isolated cycles and enumerate vertices belonging to some cycle
    {
        vector<vector<size_t>> someCycles = cycleBasis(g);
        LOG(DEBUG) << "G SIZE:" << V << endline << "CYCLES ARE:" << endline;
        for(auto &c : someCycles)
            LOG(DEBUG) << c << endline;
        // count number of times a cycles passes through a vertex
        for(auto& c : someCycles){
            for(size_t v : c){
                inCycle[v]++;
                inCycleSystem[v] = 1; // consider every as non-isoalted
            }
        }

        // test each cycle
        for(auto& c : someCycles){
            int total = accumulate(c.begin(), c.end(), 0,
                [&inCycle](int sum, size_t v){
                return sum + inCycle[v];
            });
            // no other cycles passing though isolated cycle's vertices
            // therefore sum == length of cycle
            if(total == (int)c.size()){
                isolatedCycles.push_back(c);
                for(auto v : c) // filter out isolated cycles from cycle systems
                    inCycleSystem[v] = 0;
            }

        }
    }
    LOG(DEBUG)<<"IN CYCLE: "<<inCycle<<endline;

    // from now on consider only vertices from some cycles
    // find connection points - vertices with > 2 adjacent
    vector<size_t> connPts;
    for(size_t v=0; v<num_vertices(g); v++){
        if(inCycle[v] <= 1)
            continue; //vertex in an isolated cycle
        auto adj = adjacent_vertices(v, g);
        size_t neib = count_if(adj.first, adj.second,
            [&inCycle](vd a){
                return inCycle[a] > 0;
        });
        if(neib > 2){
            connPts.push_back(v);
        }
    }
    LOG(DEBUG) << "Conn pts.:" << connPts <<endline;
    // Modified Horton's algorithm (1987)
    // Each cycle in minimal cycle base
    // has form : Puw + Pvw + {u, v} (where Pxy is shortest path from x -> y)
    // Puw & Pvw = {w} (the paths have no common edges)
    // Find candidates using these principles

    // Combined with knowledge of connection points, all points being
    // on some cycle & no isolated cycles we can examine only connection points
    // for shortest-path trees
    {
        for(size_t con : connPts){
            auto spf = shortestPaths(g, con, inCycleSystem);
            auto eds = edges(g);
            for(auto ep = eds.first ; ep != eds.second; ep++){
                auto a = target(*ep, g);
                auto b = source(*ep, g);
                if(!inCycleSystem[a] || !inCycleSystem[b])
                    continue;
                // drop these along the paths
                if(spf.prev(a) == b || spf.prev(b) == a)
                    continue;
                auto pa = spf.path(a);
                auto pb = spf.path(b);
                LOG(DEBUG) << "Two shortest paths from " << con << ": A "<< a
                    << "  B " << b << endline;
                LOG(DEBUG) << pa << endline;
                LOG(DEBUG) << pb << endline;
                // both paths w/o starting point 'con'
                size_t missingLink = con;
                if(pa.empty() || pb.empty())
                    continue;
                // if have common suffix - drop
                // they must pass through some connection point
                // that we are going to process anyway
                if(pa.back() == pb.back())
                    continue;
                // add missing link
                pa.push_back(missingLink);
                // and follow 2nd path
                for_each(pb.rbegin(), pb.rend(), [&pa](size_t v){
                    pa.push_back(v);
                });
                basisCandidates.push_back(pa);
            }
        }
    }
    // now perform Gaussian ellimination of candidate cycles
    sort(basisCandidates.begin(), basisCandidates.end(),
        [](const vector<size_t>& v1, const vector<size_t>& v2){
            return v1.size() < v2.size();
    });
    vector<set<pair<size_t,size_t>>> basis; // basis accumulated as sets of edges
    vector<size_t> basisIdx; // indices of these candidates that are in final basis
    for(size_t i=0; i<basisCandidates.size(); i++){
        auto s = chainToEdgeSet(basisCandidates[i]);
        if(!elimination(s, basis)){
            basis.push_back(s);
            basisIdx.push_back(i);
        }
    }
    LOG(DEBUG) << "Isolated cycles:\n";
    for(auto & c : isolatedCycles){
        LOG(DEBUG) << c << endline;
    }
    LOG(DEBUG)<<"Cycle base candidates:\n";
    for(auto & c : basisCandidates){
        LOG(DEBUG) << c << endline;
    }
    using std::move;
    vector<Cycle> results;
    copy(isolatedCycles.begin(), isolatedCycles.end(), back_inserter(results));
    for(size_t i : basisIdx){
        results.push_back(Cycle{basisCandidates[i]});
    }
    LOG(DEBUG)<<"Minimal cycle base :\n";
    for(auto& v : results){
        LOG(DEBUG) << v << endline;
    }
    return results;
}


#ifndef STAN_MATH_TORSTEN_ONECPT_RK45_HPP
#define STAN_MATH_TORSTEN_ONECPT_RK45_HPP

#include <Eigen/Dense>
#include <stan/math/torsten/to_array_2d.hpp>
#include <stan/math/torsten/ev_manager.hpp>
#include <stan/math/torsten/pmx_ode_model.hpp>
#include <stan/math/torsten/pmx_coupled_model.hpp>
#include <stan/math/torsten/pmx_onecpt_model.hpp>
#include <stan/math/torsten/dsolve/pmx_odeint_integrator.hpp>
#include <vector>
#include <string>
#include <stan/math/torsten/ev_solver.hpp>

namespace torsten {

/**
 * Compute the predicted amounts in each compartment at each event
 * of an ODEs model. The model contains a base 1 Compartment PK
 * component which gets solved analytically, while the other ODEs
 * are solved numerically using stan::math::integrate_ode_rk45. This
 * amounts to using the mixed solver method.
 *
 * <b>Warning:</b> This prototype does not handle steady state events.
 *
 * @tparam T0 type of scalar for time of events.
 * @tparam T1 type of scalar for amount at each event.
 * @tparam T2 type of scalar for rate at each event.
 * @tparam T3 type of scalar for inter-dose inteveral at each event.
 * @tparam T4 type of scalars for the model parameters.
 * @tparam T5 type of scalars for the bio-variability parameters.
 * @tparam T6 type of scalars for the model tlag parameters.
 * @tparam F type of ODE system function.
 * @param[in] f functor for base ordinary differential equation
 *            which gets solved numerically.
 * @param[in] nOde number of ODEs we solve numerically.
 * @param[in] time times of events
 * @param[in] amt amount at each event
 * @param[in] rate rate at each event
 * @param[in] ii inter-dose interval at each event
 * @param[in] evid event identity:
 *                    (0) observation
 *                    (1) dosing
 *                    (2) other
 *                    (3) reset
 *                    (4) reset AND dosing
 * @param[in] cmt compartment number at each event
 * @param[in] addl additional dosing at each event
 * @param[in] ss steady state approximation at each event (0: no, 1: yes)
 * @param[in] theta vector of ODE parameters
 * @param[in] biovar bio-availability in each compartment
 * @param[in] tlag lag time in each compartment
 * @param[in] rel_tol relative tolerance for the Boost ode solver
 * @param[in] abs_tol absolute tolerance for the Boost ode solver
 * @param[in] max_num_steps maximal number of steps to take within
 *            the Boost ode solver
 * @return a matrix with predicted amount in each compartment
 *         at each event.
 *
 * FIX ME: msg should be passed on to functor (allows use of
 * print statement inside ODE system).
 */
template <typename T0, typename T1, typename T2, typename T3, typename T4,
          typename T5, typename T6, typename F>
Eigen::Matrix <typename stan::return_type_t<T0, T1, T2, T3, T4, T5, T6>,
               Eigen::Dynamic, Eigen::Dynamic>
pmx_solve_onecpt_rk45(const F& f,
                      const int nOde,
                      const std::vector<T0>& time,
                      const std::vector<T1>& amt,
                      const std::vector<T2>& rate,
                      const std::vector<T3>& ii,
                      const std::vector<int>& evid,
                      const std::vector<int>& cmt,
                      const std::vector<int>& addl,
                      const std::vector<int>& ss,
                      const std::vector<std::vector<T4> >& theta,
                      const std::vector<std::vector<T5> >& biovar,
                      const std::vector<std::vector<T6> >& tlag,
                     double rel_tol,
                     double abs_tol,
                     long int max_num_steps,
                     double as_rel_tol,
                     double as_abs_tol,
                     long int as_max_num_steps,
                     std::ostream* msgs = 0) {
  using std::vector;
  using Eigen::Dynamic;
  using Eigen::Matrix;
  using boost::math::tools::promote_args;
  using torsten::dsolve::odeint_scheme_rk45;

  // check arguments
  static const char* function("pmx_solve_onecpt_rk45");
  torsten::pmx_check(time, amt, rate, ii, evid, cmt, addl, ss, theta, function);

  // Construct dummy array of matrix for last argument of pred
  Matrix<T4, Dynamic, Dynamic> dummy_system;
  vector<Matrix<T4, Dynamic, Dynamic> > dummy_systems(1, dummy_system);

  const int &nPK = torsten::PMXOneCptModel<double>::Ncmt;

  dsolve::PMXOdeIntegrator<dsolve::PMXVariadicOdeSystem, dsolve::PMXOdeintIntegrator<odeint_scheme_rk45>>
    integrator(rel_tol, abs_tol, max_num_steps, as_rel_tol, as_abs_tol, as_max_num_steps, msgs);
  const int nCmt = nPK + nOde;

  using ER = NONMENEventsRecord<T0, T1, T2, T3>;
  using EM = EventsManager<ER, NonEventParameters<T0, T4, std::vector, std::tuple<T5, T6> >>;
  const ER events_rec(nCmt, time, amt, rate, ii, evid, cmt, addl, ss);

  Matrix<typename EM::T_scalar, Dynamic, Dynamic> pred =
    Matrix<typename EM::T_scalar, Dynamic, Dynamic>::Zero(events_rec.num_event_times(), EM::nCmt(events_rec));

  using model_type = torsten::PkOneCptOdeModel<typename EM::T_par, F>;
  EventSolver<model_type, EM> pr;
  pr.pred(0, events_rec, pred, integrator, theta, biovar, tlag, nOde, f);
  return pred;
}

  /*
   * overload with default ode & algebra solver controls
   */
template <typename T0, typename T1, typename T2, typename T3, typename T4,
          typename T5, typename T6, typename F>
Eigen::Matrix <typename stan::return_type_t<T0, T1, T2, T3, T4, T5, T6>,
               Eigen::Dynamic, Eigen::Dynamic>
pmx_solve_onecpt_rk45(const F& f,
                      const int nOde,
                      const std::vector<T0>& time,
                      const std::vector<T1>& amt,
                      const std::vector<T2>& rate,
                      const std::vector<T3>& ii,
                      const std::vector<int>& evid,
                      const std::vector<int>& cmt,
                      const std::vector<int>& addl,
                      const std::vector<int>& ss,
                      const std::vector<std::vector<T4> >& theta,
                      const std::vector<std::vector<T5> >& biovar,
                      const std::vector<std::vector<T6> >& tlag,
                      std::ostream* msgs = 0) {
  return pmx_solve_onecpt_rk45(f, nOde,
                               time, amt, rate, ii, evid, cmt, addl, ss,
                               theta, biovar, tlag,
                               1.e-6, 1.e-6, 1e6,
                               1.e-6, 1.e-6, 1e2,
                               msgs);
}

  /*
   * overload with default algebra solver controls
   */
template <typename T0, typename T1, typename T2, typename T3, typename T4,
          typename T5, typename T6, typename F>
Eigen::Matrix <typename stan::return_type_t<T0, T1, T2, T3, T4, T5, T6>,
               Eigen::Dynamic, Eigen::Dynamic>
pmx_solve_onecpt_rk45(const F& f,
                      const int nOde,
                      const std::vector<T0>& time,
                      const std::vector<T1>& amt,
                      const std::vector<T2>& rate,
                      const std::vector<T3>& ii,
                      const std::vector<int>& evid,
                      const std::vector<int>& cmt,
                      const std::vector<int>& addl,
                      const std::vector<int>& ss,
                      const std::vector<std::vector<T4> >& theta,
                      const std::vector<std::vector<T5> >& biovar,
                      const std::vector<std::vector<T6> >& tlag,
                     double rel_tol,
                     double abs_tol,
                     long int max_num_steps,
                     std::ostream* msgs = 0) {
  return pmx_solve_onecpt_rk45(f, nOde,
                               time, amt, rate, ii, evid, cmt, addl, ss,
                               theta, biovar, tlag,
                               rel_tol, abs_tol, max_num_steps,
                               1.e-6, 1.e-6, 1e2,
                               msgs);
}

  /**
   * Overload function to allow user to pass an std::vector for
   * pMatrix/bioavailability/tlag
   */
  template <typename T0, typename T1, typename T2, typename T3,
            typename T_par, typename T_biovar, typename T_tlag,
            typename F,
            typename = require_any_not_std_vector_t<T_par, T_biovar, T_tlag> >
  stan::matrix_return_t<T0, T1, T2, T3, T_par, T_biovar, T_tlag>
  pmx_solve_onecpt_rk45(const F& f,
                        const int nOde,
                        const std::vector<T0>& time,
                        const std::vector<T1>& amt,
                        const std::vector<T2>& rate,
                        const std::vector<T3>& ii,
                        const std::vector<int>& evid,
                        const std::vector<int>& cmt,
                        const std::vector<int>& addl,
                        const std::vector<int>& ss,
                        const std::vector<T_par>& pMatrix,
                        const std::vector<T_biovar>& biovar,
                        const std::vector<T_tlag>& tlag,
                       double rel_tol,
                       double abs_tol,
                       long int max_num_steps,
                       double as_rel_tol,
                       double as_abs_tol,
                       long int as_max_num_steps,
                       std::ostream* msgs = 0) {
    auto param_ = torsten::to_array_2d(pMatrix);
    auto biovar_ = torsten::to_array_2d(biovar);
    auto tlag_ = torsten::to_array_2d(tlag);

    return pmx_solve_onecpt_rk45(f, nOde,
                                 time, amt, rate, ii, evid, cmt, addl, ss,
                                 param_, biovar_, tlag_,
                                 rel_tol, abs_tol, max_num_steps,
                                 as_rel_tol, as_abs_tol, as_max_num_steps,
                                 msgs);
  }

  /**
   * Overload function to allow user to pass an std::vector for
   * pMatrix/bioavailability/tlag, with default ode &
   * algebra solver controls
   */
  template <typename T0, typename T1, typename T2, typename T3,
            typename T_par, typename T_biovar, typename T_tlag,
            typename F,
            typename = require_any_not_std_vector_t<T_par, T_biovar, T_tlag> >
  stan::matrix_return_t<T0, T1, T2, T3, T_par, T_biovar, T_tlag>
  pmx_solve_onecpt_rk45(const F& f,
                        const int nOde,
                        const std::vector<T0>& time,
                        const std::vector<T1>& amt,
                        const std::vector<T2>& rate,
                        const std::vector<T3>& ii,
                        const std::vector<int>& evid,
                        const std::vector<int>& cmt,
                        const std::vector<int>& addl,
                        const std::vector<int>& ss,
                        const std::vector<T_par>& pMatrix,
                        const std::vector<T_biovar>& biovar,
                        const std::vector<T_tlag>& tlag,
                        std::ostream* msgs = 0) {
    return pmx_solve_onecpt_rk45(f, nOde,
                                 time, amt, rate, ii, evid, cmt, addl, ss,
                                 pMatrix, biovar, tlag,
                                 1.e-6, 1.e-6, 1e6,
                                 1.e-6, 1.e-6, 1e2,
                                 msgs);
  }

  /**
   * Overload function to allow user to pass an std::vector for
   * pMatrix/bioavailability/tlag, with default
   * algebra solver controls
   */
  template <typename T0, typename T1, typename T2, typename T3,
            typename T_par, typename T_biovar, typename T_tlag,
            typename F,
            typename = require_any_not_std_vector_t<T_par, T_biovar, T_tlag> >
  stan::matrix_return_t<T0, T1, T2, T3, T_par, T_biovar, T_tlag>
  pmx_solve_onecpt_rk45(const F& f,
                        const int nOde,
                        const std::vector<T0>& time,
                        const std::vector<T1>& amt,
                        const std::vector<T2>& rate,
                        const std::vector<T3>& ii,
                        const std::vector<int>& evid,
                        const std::vector<int>& cmt,
                        const std::vector<int>& addl,
                        const std::vector<int>& ss,
                        const std::vector<T_par>& pMatrix,
                        const std::vector<T_biovar>& biovar,
                        const std::vector<T_tlag>& tlag,
                        double rel_tol,
                        double abs_tol,
                        long int max_num_steps,
                        std::ostream* msgs = 0) {
    return pmx_solve_onecpt_rk45(f, nOde,
                                 time, amt, rate, ii, evid, cmt, addl, ss,
                                 pMatrix, biovar, tlag,
                                 rel_tol, abs_tol, max_num_steps,
                                 1.e-6, 1.e-6, 1e2,
                                 msgs);
  }

  // old version
template <typename T0, typename T1, typename T2, typename T3, typename T4,
          typename T5, typename T6, typename F>
stan::matrix_return_t<T0, T1, T2, T3, T4, T5, T6>
mixOde1CptModel_rk45(const F& f,
                     const int nOde,
                     const std::vector<T0>& time,
                     const std::vector<T1>& amt,
                     const std::vector<T2>& rate,
                     const std::vector<T3>& ii,
                     const std::vector<int>& evid,
                     const std::vector<int>& cmt,
                     const std::vector<int>& addl,
                     const std::vector<int>& ss,
                     const std::vector<std::vector<T4> >& theta,
                     const std::vector<std::vector<T5> >& biovar,
                     const std::vector<std::vector<T6> >& tlag,
                     double rel_tol = 1e-6,
                     double abs_tol = 1e-6,
                     long int max_num_steps = 1e6,
                     std::ostream* msgs = 0) {
  auto x = pmx_solve_onecpt_rk45(f, nOde,
                                 time, amt, rate, ii, evid, cmt, addl, ss,
                                 theta, biovar, tlag,
                                 rel_tol, abs_tol, max_num_steps,
                                 msgs);
  return x.transpose();
}

  template <typename T0, typename T1, typename T2, typename T3,
            typename T_par, typename T_biovar, typename T_tlag,
            typename F,
            typename = require_any_not_std_vector_t<T_par, T_biovar, T_tlag> >
  stan::matrix_return_t<T0, T1, T2, T3, T_par, T_biovar, T_tlag>
  mixOde1CptModel_rk45(const F& f,
                        const int nOde,
                        const std::vector<T0>& time,
                        const std::vector<T1>& amt,
                        const std::vector<T2>& rate,
                        const std::vector<T3>& ii,
                        const std::vector<int>& evid,
                        const std::vector<int>& cmt,
                        const std::vector<int>& addl,
                        const std::vector<int>& ss,
                        const std::vector<T_par>& pMatrix,
                        const std::vector<T_biovar>& biovar,
                        const std::vector<T_tlag>& tlag,
                        double rel_tol = 1e-6,
                        double abs_tol = 1e-6,
                       long int max_num_steps = 1e6,
                       std::ostream* msgs = 0) {
    auto x = pmx_solve_onecpt_rk45(f, nOde,
                                   time, amt, rate, ii, evid, cmt, addl, ss,
                                   pMatrix, biovar, tlag,
                                   rel_tol, abs_tol, max_num_steps,
                                   msgs);
    return x.transpose();
  }

}
#endif


//////////////////////////////////////////////////////////////////////////
//
//  Copyright (c) 2021, Murray Stevenson. 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 John Haddon nor the names of
//        any other contributors to this software 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.
//
//////////////////////////////////////////////////////////////////////////

#include "GafferScene/Cryptomatte.h"

#include "GafferImage/ImageAlgo.h"

#include "Gaffer/Context.h"

#include "IECore/MessageHandler.h"

#include <boost/filesystem.hpp>
#include <boost/iostreams/stream.hpp>
#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/json_parser.hpp>
#include <boost/regex.hpp>

#include <unordered_map>
#include <unordered_set>

using namespace std;
using namespace IECore;
using namespace Gaffer;

namespace
{

//-----------------------------------------------------------------------------
// MurmurHash3 was written by Austin Appleby, and is placed in the public
// domain. The author hereby disclaims copyright to this source code.

inline uint32_t rotl32( uint32_t x, int8_t r )
{
	return (x << r) | (x >> (32 - r));
}

inline uint32_t fmix( uint32_t h )
{
	h ^= h >> 16;
	h *= 0x85ebca6b;
	h ^= h >> 13;
	h *= 0xc2b2ae35;
	h ^= h >> 16;

	return h;
}

uint32_t MurmurHash3_x86_32( const void *key, size_t len, uint32_t seed )
{
	const uint8_t *data = (const uint8_t *)key;
	const size_t nblocks = len / 4;

	uint32_t h1 = seed;

	const uint32_t c1 = 0xcc9e2d51;
	const uint32_t c2 = 0x1b873593;

	/* body */

	const uint32_t *blocks = (const uint32_t *)(data + nblocks * 4);

	for( size_t i = -nblocks; i; i++ )
	{
		uint32_t k1 = blocks[i];

		k1 *= c1;
		k1 = rotl32( k1, 15 );
		k1 *= c2;

		h1 ^= k1;
		h1 = rotl32( h1, 13 );
		h1 = h1 * 5 + 0xe6546b64;
	}

	/* tail */

	const uint8_t *tail = (const uint8_t *)(data + nblocks * 4);

	uint32_t k1 = 0;

	switch( len & 3 )
	{
	case 3:
		k1 ^= tail[2] << 16;
		[[fallthrough]];
	case 2:
		k1 ^= tail[1] << 8;
		[[fallthrough]];
	case 1:
		k1 ^= tail[0];
		k1 *= c1;
		k1 = rotl32( k1, 15 );
		k1 *= c2;
		h1 ^= k1;
	}

	/* finalization */

	h1 ^= len;

	h1 = fmix( h1 );

	return h1;
}

//-----------------------------------------------------------------------------

inline float matteNameToValue( const std::string &matteName )
{
	uint32_t hash = MurmurHash3_x86_32( matteName.c_str(), matteName.length(), 0);

	// Taken from the Cryptomatte specification - https://github.com/Psyop/Cryptomatte/blob/master/specification/cryptomatte_specification.pdf
	// if all exponent bits are 0 (subnormals, +zero, -zero) set exponent to 1
	// if all exponent bits are 1 (NaNs, +inf, -inf) set exponent to 254
	const uint32_t exponent = hash >> 23 & 255;
	// extract exponent (8 bits)
	if( exponent == 0 || exponent == 255 )
	{
		hash ^= 1 << 23; // toggle bit
	}
	float result;
	std::memcpy( &result, &hash, sizeof( uint32_t ) );

	return result;
}

inline std::string hashLayerName( const std::string &layerName )
{
	// hash the layer name to find manifest keys from image metadata
	return boost::str( boost::format( "%08x" ) % MurmurHash3_x86_32( layerName.c_str(), layerName.length(), 0 ) );
}

IECore::CompoundDataPtr propertyTreeToCompoundData( const boost::property_tree::ptree &pt )
{
	boost::regex instanceDataRegex( "^instance:[0-9a-f]+$" );

	IECore::CompoundDataPtr resultData = new IECore::CompoundData();
	CompoundDataMap &result = resultData->writable();

	uint32_t hash;

	for( auto &it : pt )
	{
		// NOTE: exclude locations starting with "instance:" under root
		if( boost::regex_match( it.first, instanceDataRegex ) )
		{
			continue;
		}

		std::sscanf( it.second.data().c_str(), "%x", &hash );

		result[hash] = new StringData( it.first );
	}

	return resultData;
}

IECore::CompoundDataPtr parseManifestFromMetadata( const std::string &metadataKey, ConstCompoundDataPtr metadata )
{
	if( metadata->readable().find( metadataKey ) == metadata->readable().end() )
	{
		throw IECore::Exception( boost::str( boost::format( "Image metadata entry not found: %s" ) % metadataKey ) );
	}

	const StringData *manifest = metadata->member<StringData>( metadataKey );
	boost::iostreams::stream<boost::iostreams::array_source> stream( manifest->readable().c_str(), manifest->readable().size() );
	boost::property_tree::ptree pt;

	try
	{
		boost::property_tree::read_json( stream, pt );
	}
	catch( const boost::property_tree::json_parser::json_parser_error &e )
	{
		throw IECore::Exception( boost::str( boost::format( "Error parsing manifest metadata: %s" ) % e.what() ) );
	}

	return propertyTreeToCompoundData( pt );
}

IECore::CompoundDataPtr parseManifestFromSidecarFile( const std::string &manifestFile )
{
	if( manifestFile == "" )
	{
		throw IECore::Exception( "No manifest file provided." );
	}
	else if( !boost::filesystem::is_regular_file( manifestFile ) )
	{
		throw IECore::Exception( boost::str( boost::format( "Manifest file not found: %s" ) % manifestFile ) );
	}

	boost::property_tree::ptree pt;

	try
	{
		boost::property_tree::read_json( manifestFile, pt );
	}
	catch( const boost::property_tree::json_parser::json_parser_error &e )
	{
		throw IECore::Exception( boost::str( boost::format( "Error parsing manifest file: %s" ) % e.what() ) );
	}

	return propertyTreeToCompoundData( pt );
}

IECore::CompoundDataPtr parseManifestFromMetadataAndSidecar( const std::string &metadataKey, ConstCompoundDataPtr metadata, const std::string &manifestDirectory )
{
	if( metadata->readable().find( metadataKey ) == metadata->readable().end() )
	{
		throw IECore::Exception( boost::str( boost::format( "Image metadata entry not found: %s" ) % metadataKey ) );
	}

	if( manifestDirectory == "" )
	{
		throw IECore::Exception( "No manifest directory provided. A directory is required to locate the manifest." );
	}
	else if( !boost::filesystem::is_directory( manifestDirectory ) )
	{
		throw IECore::Exception( boost::str( boost::format( "Manifest directory not found: %s") % manifestDirectory ) );
	}

	const StringData *manifestFile = metadata->member<StringData>( metadataKey );
	boost::filesystem::path p( manifestDirectory );
	// append manifest file to directory path
	p /= manifestFile->readable();

	return parseManifestFromSidecarFile( p.string() );
}

IECore::CompoundDataPtr parseManifestFromFirstMetadataEntry( const std::string &cryptomatteLayer, ConstCompoundDataPtr metadata, const std::string &manifestDirectory )
{
	// The Cryptomatte specification suggests metadata entries stored for each layer based on a key generated from the first 7 characters of the hashed layer name.
	const std::string layerPrefix = boost::str( boost::format( "cryptomatte/%.7s" ) % hashLayerName( cryptomatteLayer ) );

	// A "conversion" metadata entry is required, specifying the conversion method used to convert hash values to pixel values.
	// As per the Cryptomatte specification, "uint32_to_float32" is the only currently supported conversion type.
	const std::string manifestConversion = layerPrefix + "/conversion";

	if( metadata->readable().find( manifestConversion ) == metadata->readable().end() )
	{
		throw IECore::Exception( boost::str( boost::format( "Image metadata entry not found: %s" ) % manifestConversion ) );
	}
	else if( metadata->member<StringData>( manifestConversion )->readable() != "uint32_to_float32" )
	{
		throw IECore::Exception( "Unsupported manifest conversion. Only \"uint32_to_float32\" is supported." );
	}

	// A "hash" metadata entry is required, specifying the type of hash used to generate the manifest.
	// As per the Cryptomatte specification, "MurmurHash3_32" is the only currently supported hash type.
	const std::string manifestHash = layerPrefix + "/hash";

	if( metadata->readable().find( manifestHash ) == metadata->readable().end() )
	{
		throw IECore::Exception( boost::str( boost::format( "Image metadata entry not found: %s" ) % manifestHash ) );
	}
	else if( metadata->member<StringData>( manifestHash )->readable() != "MurmurHash3_32" )
	{
		throw IECore::Exception( "Unsupported manifest hash. Only \"MurmurHash3_32\" is supported." );
	}

	// The manifest is defined by the existence of one of two metadata entries representing either the manifest data, or the filename of a sidecar manifest.
	const std::string manifestKey = layerPrefix + "/manifest";
	const std::string manifestFileKey = layerPrefix + "/manif_file";

	if( metadata->readable().find( manifestKey ) != metadata->readable().end() )
	{
		// This "manifest" entry contains the manifest as JSON data.
		return parseManifestFromMetadata( manifestKey, metadata );
	}
	else if( metadata->readable().find( manifestFileKey ) != metadata->readable().end() )
	{
		// This "manif_file" entry contains a filename of a sidecar JSON manifest file. We also require a directory to locate the file within.
		return parseManifestFromMetadataAndSidecar( manifestFileKey, metadata, manifestDirectory );
	}
	else
	{
		throw IECore::Exception( boost::str( boost::format( "Image metadata entry not found. One of the following entries expected: %s %s" ) % manifestKey % manifestFileKey ) );
	}
}

} // namespace

namespace GafferScene
{
GAFFER_NODE_DEFINE_TYPE( Cryptomatte );

size_t Cryptomatte::g_firstPlugIndex = 0;

// first channel contains id, second contains alpha contribution
using ChannelMap = std::unordered_map<std::string, std::string>;
static const ChannelMap g_channelMap = {
	{"R", "G"},
	{"B", "A"},
};

const std::string g_firstDataChannelSuffix = "00.R";
const std::string g_cryptomatteChannelPattern = "^%s[0-9]+\\.[RGBA]";

Cryptomatte::Cryptomatte( const std::string &name )
	: GafferImage::FlatImageProcessor( name )
{
	storeIndexOfNextChild( g_firstPlugIndex );
	addChild( new StringPlug( "layer", Gaffer::Plug::In, "" ) );
	addChild( new IntPlug( "manifestSource", Gaffer::Plug::In, (int)ManifestSource::Metadata, /* min */ (int)ManifestSource::None, /* max */ (int)ManifestSource::Sidecar ) );
	addChild( new StringPlug( "manifestDirectory", Gaffer::Plug::In, "") );
	addChild( new StringPlug( "sidecarFile", Gaffer::Plug::In, "") );
	addChild( new StringPlug( "outputChannel", Gaffer::Plug::In, "A") );
	addChild( new StringVectorDataPlug( "matteNames", Gaffer::Plug::In, new StringVectorData() ) );
	addChild( new FloatVectorDataPlug( "__matteValues", Gaffer::Plug::Out, new FloatVectorData() ) );
	addChild( new AtomicCompoundDataPlug( "__manifest", Gaffer::Plug::Out, new CompoundData() ) );
	addChild( new PathMatcherDataPlug( "__manifestPaths", Gaffer::Plug::Out, new PathMatcherData ) );
	addChild( new ScenePlug( "__manifestScene", Gaffer::Plug::Out ) );
	addChild( new FloatVectorDataPlug( "__matteChannelData", Gaffer::Plug::Out, GafferImage::ImagePlug::blackTile() ) );

	outPlug()->formatPlug()->setInput( inPlug()->formatPlug() );
	outPlug()->metadataPlug()->setInput( inPlug()->metadataPlug() );
	outPlug()->dataWindowPlug()->setInput( inPlug()->dataWindowPlug() );
}

Cryptomatte::~Cryptomatte()
{
}

Gaffer::StringPlug *Cryptomatte::layerPlug()
{
	return getChild<StringPlug>( g_firstPlugIndex );
}

const Gaffer::StringPlug *Cryptomatte::layerPlug() const
{
	return getChild<StringPlug>( g_firstPlugIndex );
}

Gaffer::IntPlug *Cryptomatte::manifestSourcePlug()
{
	return getChild<IntPlug>( g_firstPlugIndex + 1 );
}

const Gaffer::IntPlug *Cryptomatte::manifestSourcePlug() const
{
	return getChild<IntPlug>( g_firstPlugIndex + 1 );
}

Gaffer::StringPlug *Cryptomatte::manifestDirectoryPlug()
{
	return getChild<StringPlug>( g_firstPlugIndex + 2 );
}

const Gaffer::StringPlug *Cryptomatte::manifestDirectoryPlug() const
{
	return getChild<StringPlug>( g_firstPlugIndex + 2 );
}

Gaffer::StringPlug *Cryptomatte::sidecarFilePlug()
{
	return getChild<StringPlug>( g_firstPlugIndex + 3 );
}

const Gaffer::StringPlug *Cryptomatte::sidecarFilePlug() const
{
	return getChild<StringPlug>( g_firstPlugIndex + 3 );
}

Gaffer::StringPlug *Cryptomatte::outputChannelPlug()
{
	return getChild<StringPlug>( g_firstPlugIndex + 4 );
}

const Gaffer::StringPlug *Cryptomatte::outputChannelPlug() const
{
	return getChild<StringPlug>( g_firstPlugIndex + 4 );
}

Gaffer::StringVectorDataPlug *Cryptomatte::matteNamesPlug()
{
	return getChild<StringVectorDataPlug>( g_firstPlugIndex + 5 );
}

const Gaffer::StringVectorDataPlug *Cryptomatte::matteNamesPlug() const
{
	return getChild<StringVectorDataPlug>( g_firstPlugIndex + 5 );
}

Gaffer::FloatVectorDataPlug *Cryptomatte::matteValuesPlug()
{
	return getChild<FloatVectorDataPlug>( g_firstPlugIndex + 6 );
}

const Gaffer::FloatVectorDataPlug *Cryptomatte::matteValuesPlug() const
{
	return getChild<FloatVectorDataPlug>( g_firstPlugIndex + 6 );
}

Gaffer::AtomicCompoundDataPlug *Cryptomatte::manifestPlug()
{
	return getChild<AtomicCompoundDataPlug>( g_firstPlugIndex + 7 );
}

const Gaffer::AtomicCompoundDataPlug *Cryptomatte::manifestPlug() const
{
	return getChild<AtomicCompoundDataPlug>( g_firstPlugIndex + 7 );
}

Gaffer::PathMatcherDataPlug *Cryptomatte::manifestPathDataPlug()
{
	return getChild<PathMatcherDataPlug>( g_firstPlugIndex + 8 );
}

const Gaffer::PathMatcherDataPlug *Cryptomatte::manifestPathDataPlug() const
{
	return getChild<PathMatcherDataPlug>( g_firstPlugIndex + 8 );
}

GafferScene::ScenePlug *Cryptomatte::manifestScenePlug()
{
	return getChild<GafferScene::ScenePlug>( g_firstPlugIndex + 9 );
}

const GafferScene::ScenePlug *Cryptomatte::manifestScenePlug() const
{
	return getChild<GafferScene::ScenePlug>( g_firstPlugIndex + 9 );
}

Gaffer::FloatVectorDataPlug *Cryptomatte::matteChannelDataPlug()
{
	return getChild<FloatVectorDataPlug>( g_firstPlugIndex + 10 );
}

const Gaffer::FloatVectorDataPlug *Cryptomatte::matteChannelDataPlug() const
{
	return getChild<FloatVectorDataPlug>( g_firstPlugIndex + 10 );
}

void Cryptomatte::affects(const Gaffer::Plug *input, AffectedPlugsContainer &outputs) const
{
	FlatImageProcessor::affects(input, outputs);

	if( input == inPlug()->channelDataPlug() ||
		input == layerPlug() ||
		input == outputChannelPlug() ||
		input == matteValuesPlug() )
	{
		outputs.push_back( matteChannelDataPlug() );
	}

	if( input == matteChannelDataPlug() )
	{
		outputs.push_back( outPlug()->channelDataPlug() );
	}

	if( input == inPlug()->channelNamesPlug() ||
		input == outputChannelPlug() )
	{
		outputs.push_back( outPlug()->channelNamesPlug() );
	}

	if( input == layerPlug() ||
		input == manifestDirectoryPlug() ||
		input == sidecarFilePlug() ||
		input == manifestSourcePlug() ||
		input == inPlug()->metadataPlug() )
	{
		outputs.push_back( manifestPlug() );
	}

	if( input == matteNamesPlug() ||
		input == manifestPlug() )
	{
		outputs.push_back( matteValuesPlug() );
	}

	if( input == manifestPlug() )
	{
		outputs.push_back( manifestPathDataPlug() );
	}

	if( input == manifestPathDataPlug() )
	{
		outputs.push_back( manifestScenePlug()->childNamesPlug() );
	}
}

void Cryptomatte::hash( const Gaffer::ValuePlug *output, const Gaffer::Context *context, IECore::MurmurHash &h ) const
{
	FlatImageProcessor::hash( output, context, h );

	if( output == manifestPlug() )
	{
		manifestSourcePlug()->hash( h );
		switch( (ManifestSource)manifestSourcePlug()->getValue() )
		{
			case ManifestSource::Metadata :
			{
				layerPlug()->hash( h );
				if( !layerPlug()->isSetToDefault() )
				{
					inPlug()->metadataPlug()->hash( h );
					manifestDirectoryPlug()->hash( h );
				}
				break;
			}
			case ManifestSource::Sidecar :
			{
				sidecarFilePlug()->hash( h );
				break;
			}
			case ManifestSource::None :
			{
				break;
			}
		}
	}
	else if( output == matteValuesPlug() )
	{
		manifestPlug()->hash( h );
		matteNamesPlug()->hash( h );
	}
	else if( output == manifestPathDataPlug() )
	{
		manifestPlug()->hash( h );
	}
	else if( output == manifestScenePlug()->childNamesPlug() )
	{
		const GafferScene::ScenePlug::ScenePath &scenePath = context->get<GafferScene::ScenePlug::ScenePath>( GafferScene::ScenePlug::scenePathContextName );
		h.append( &scenePath.front(), scenePath.size() );
		ScenePlug::GlobalScope globalScope( context );
		manifestPathDataPlug()->hash(h);
	}
	else if( output == matteChannelDataPlug() )
	{
		inPlug()->channelDataPlug()->hash( h );

		std::string cryptomatteLayer;
		ConstStringVectorDataPtr channelNamesData;
		{
			GafferImage::ImagePlug::GlobalScope globalScope( context );
			cryptomatteLayer = layerPlug()->getValue();
			channelNamesData = inPlug()->channelNamesPlug()->getValue();

			matteValuesPlug()->hash( h );
		}

		boost::regex channelNameRegex( boost::str( boost::format( g_cryptomatteChannelPattern ) % cryptomatteLayer ) );
		GafferImage::ImagePlug::ChannelDataScope channelDataScope( context );
		for( const auto &c : channelNamesData->readable() )
		{
			if( boost::regex_match( c, channelNameRegex ) )
			{
				channelDataScope.setChannelName( &c );
				inPlug()->channelDataPlug()->hash( h );
			}
		}
	}
}

void Cryptomatte::compute( Gaffer::ValuePlug *output, const Gaffer::Context *context ) const
{
	FlatImageProcessor::compute( output, context );

	if( output == manifestPlug() )
	{
		IECore::CompoundDataPtr resultData = nullptr;

		switch( (ManifestSource)manifestSourcePlug()->getValue() )
		{
			case ManifestSource::Metadata :
			{
				const std::string cryptomatteLayer = layerPlug()->getValue();
				if( cryptomatteLayer != "" )
				{
					ConstCompoundDataPtr metadata = inPlug()->metadataPlug()->getValue();
					const std::string manifestDirectory = manifestDirectoryPlug()->getValue();
					resultData = parseManifestFromFirstMetadataEntry( cryptomatteLayer, metadata, manifestDirectory );
				}

				break;
			}
			case ManifestSource::Sidecar :
			{
				const std::string sidecarFile = sidecarFilePlug()->getValue();
				resultData = parseManifestFromSidecarFile( sidecarFile );
				break;
			}
			case ManifestSource::None :
			{
				break;
			}
		}

		if( resultData )
		{
			static_cast<AtomicCompoundDataPlug *>( output )->setValue( resultData );
		}
		else
		{
			static_cast<AtomicCompoundDataPlug *>( output )->setToDefault();
		}
	}
	else if( output == matteValuesPlug() )
	{
		FloatVectorDataPtr resultData = new IECore::FloatVectorData();
		std::vector<float> &result = resultData->writable();
		std::unordered_set<float> matteValues;

		ConstStringVectorDataPtr matteNames = matteNamesPlug()->getValue();
		ConstCompoundDataPtr manifest = manifestPlug()->getValue();

		IECore::PathMatcher pathMatcher;
		for( const auto &name : matteNames->readable() )
		{
			if( name.size() > 0 && name.front() == '<' && name.back() == '>' )
			{
				try
				{
					matteValues.insert( std::stof( name.substr(1, name.size() - 2) ) );
				}
				catch( const std::exception &e )
				{
					IECore::msg( IECore::Msg::Error, "Cryptomatte::matteValues", boost::format( "Error converting value: %s" ) % name );
					continue;
				}
			}
			else
			{
				if( manifest )
				{
					pathMatcher.addPath( name );
				}

				if( !StringAlgo::hasWildcards( name ) || name.find( "..." ) == string::npos )
				{
					// Hash names without wildcards directly. This allows them to still be matched if no manifest exists or has been truncated by the renderer
					matteValues.insert( matteNameToValue( name ) );
				}
			}
		}

		if( manifest )
		{
			for( const auto &manifestEntry : manifest->readable() )
			{
				const std::string &matteName = static_cast<IECore::StringData *>( manifestEntry.second.get() )->readable();
				if( pathMatcher.match( matteName ) & ( IECore::PathMatcher::ExactMatch | IECore::PathMatcher::AncestorMatch ) )
				{
					matteValues.insert( matteNameToValue( matteName ) );
				}
			}
		}

		result.insert( result.end(), matteValues.begin(), matteValues.end() );
		// NOTE: pre-sort values as they're later used in a binary_search
		std::sort( result.begin(), result.end() );

		static_cast<FloatVectorDataPlug *>( output )->setValue( resultData );
	}
	else if( output == manifestPathDataPlug() )
	{
		PathMatcherDataPtr resultData = new PathMatcherData;
		PathMatcher &pathMatcher = resultData->writable();

		ConstCompoundDataPtr manifest = manifestPlug()->getValue();

		if( manifest )
		{
			for( const auto &manifestEntry : manifest->readable() )
			{
				pathMatcher.addPath( static_cast<IECore::StringData *>( manifestEntry.second.get() )->readable() );
			}
		}

		static_cast<PathMatcherDataPlug *>( output )->setValue( resultData );
	}
	else if( output == manifestScenePlug()->childNamesPlug() )
	{
		InternedStringVectorDataPtr resultData = new InternedStringVectorData;
		std::vector<InternedString> &result = resultData->writable();

		ConstPathMatcherDataPtr manifestPathData;
		{
			ScenePlug::GlobalScope globalScope( context );
			manifestPathData = manifestPathDataPlug()->getValue();
		}

		const PathMatcher &manifestPaths = manifestPathData->readable();

		const GafferScene::ScenePlug::ScenePath &scenePath = context->get<GafferScene::ScenePlug::ScenePath>( GafferScene::ScenePlug::scenePathContextName );

		auto match = manifestPaths.subTree( scenePath );
		PathMatcher::RawIterator it = match.begin();

		while( it != match.end() )
		{
			if( it->size() == 1 )
			{
				result.push_back( it->back() );
				it.prune();
			}

			++it;
		}

		std::sort(
			result.begin(), result.end(),
			[] ( const InternedString a, const InternedString b ) {
				return a.string() < b.string();
			}
		);

		static_cast<InternedStringVectorDataPlug *>( output )->setValue( resultData );
	}
	else if( output->parent() == manifestScenePlug() )
	{
		output->setToDefault();
	}
	else if( output == matteChannelDataPlug() )
	{
		FloatVectorDataPtr resultData = new IECore::FloatVectorData();
		std::vector<float> &result = resultData->writable();
		result.resize( GafferImage::ImagePlug::tilePixels(), 0.0f );

		ConstStringVectorDataPtr channelNamesData;
		std::string cryptomatteLayer;
		ConstFloatVectorDataPtr matteValuesData;
		{
			GafferImage::ImagePlug::GlobalScope c( context );
			channelNamesData = inPlug()->channelNamesPlug()->getValue();
			cryptomatteLayer = layerPlug()->getValue();
			matteValuesData = matteValuesPlug()->getValue();
		}

		const std::vector<std::string> &channelNames = channelNamesData->readable();
		const std::vector<float> &matteValues = matteValuesData->readable();

		boost::regex channelNameRegex( boost::str( boost::format( g_cryptomatteChannelPattern ) % cryptomatteLayer ) );
		GafferImage::ImagePlug::ChannelDataScope channelDataScope( context );
		for( const auto &c : channelNames )
		{
			if( boost::regex_match( c, channelNameRegex ) )
			{
				ChannelMap::const_iterator cIt = g_channelMap.find( GafferImage::ImageAlgo::baseName( c ) );
				if( cIt == g_channelMap.end() )
				{
					continue;
				}

				const std::string &alphaChannel = GafferImage::ImageAlgo::channelName( GafferImage::ImageAlgo::layerName( c ), cIt->second );
				if( !GafferImage::ImageAlgo::channelExists( channelNames, alphaChannel ) )
				{
					continue;
				}

				channelDataScope.setChannelName( &c );
				ConstFloatVectorDataPtr valueData = inPlug()->channelDataPlug()->getValue();
				const std::vector<float> &value = valueData->readable();

				channelDataScope.setChannelName( &alphaChannel );
				ConstFloatVectorDataPtr alphaData = inPlug()->channelDataPlug()->getValue();
				const std::vector<float> &alpha = alphaData->readable();

				std::vector<float>::const_iterator vIt = value.begin();
				std::vector<float>::const_iterator aIt = alpha.begin();
				for( std::vector<float>::iterator it = result.begin(), eIt = result.end(); it != eIt; ++it, ++vIt, ++aIt )
				{
					if( std::binary_search( matteValues.begin(), matteValues.end(), *vIt ) )
					{
						*it += *aIt;
					}
				}
			}
		}

		static_cast<FloatVectorDataPlug *>( output )->setValue( resultData );
	}
}

Gaffer::ValuePlug::CachePolicy Cryptomatte::computeCachePolicy( const Gaffer::ValuePlug *output ) const
{
	if( output == matteValuesPlug() ||
		output == manifestPlug() ||
		output == manifestPathDataPlug() )
	{
		// Request blocking compute to avoid concurrent threads computing the manifest redundantly.
		return ValuePlug::CachePolicy::Standard;
	}

	return ImageNode::computeCachePolicy( output );
}

void Cryptomatte::hashChannelNames( const GafferImage::ImagePlug *parent, const Gaffer::Context *context, IECore::MurmurHash &h ) const
{
	FlatImageProcessor::hashChannelNames( parent, context, h );
	inPlug()->channelNamesPlug()->hash( h );
	outputChannelPlug()->hash( h );
}

IECore::ConstStringVectorDataPtr Cryptomatte::computeChannelNames( const Gaffer::Context *context, const GafferImage::ImagePlug *parent ) const
{
	StringVectorDataPtr resultData = inPlug()->channelNamesPlug()->getValue()->copy();
	std::vector<std::string> &result = resultData->writable();
	const std::string alphaChannel = outputChannelPlug()->getValue();

	for( const auto &channelName : { "R", "G", "B" } )
	{
		if( find( result.begin(), result.end(), channelName ) == result.end() )
		{
			result.push_back( channelName );
		}
	}

	if( alphaChannel != "" && find( result.begin(), result.end(), alphaChannel ) == result.end() )
	{
		result.push_back( alphaChannel );
	}

	return resultData;
}

void Cryptomatte::hashChannelData( const GafferImage::ImagePlug *output, const Gaffer::Context *context, IECore::MurmurHash &h ) const
{
	std::string alphaChannel;
	{
		GafferImage::ImagePlug::GlobalScope globalScope( context );
		alphaChannel = outputChannelPlug()->getValue();
	}

	const std::string channelName = context->get<std::string>( GafferImage::ImagePlug::channelNameContextName );
	if( channelName != "R" && channelName != "G" && channelName != "B" && channelName != alphaChannel )
	{
		h = inPlug()->channelDataPlug()->hash();
		return;
	}

	std::string cryptomatteLayer;
	ConstStringVectorDataPtr channelNamesData;
	{
		GafferImage::ImagePlug::GlobalScope globalScope( context );
		channelNamesData = inPlug()->channelNamesPlug()->getValue();
		cryptomatteLayer = layerPlug()->getValue();
	}

	if( cryptomatteLayer == "" )
	{
		if( GafferImage::ImageAlgo::channelExists( channelNamesData->readable(), channelName ) )
		{
			h = inPlug()->channelDataPlug()->hash();
			return;
		}
		else
		{
			h = GafferImage::ImagePlug::blackTile()->Object::hash();
			return;
		}
	}

	const std::string &firstDataChannel = cryptomatteLayer + g_firstDataChannelSuffix;
	if( !GafferImage::ImageAlgo::channelExists( channelNamesData->readable(), firstDataChannel ) )
	{
		h = GafferImage::ImagePlug::blackTile()->Object::hash();
		return;
	}

	h.append( channelName );

	{
		GafferImage::ImagePlug::GlobalScope globalScope( context );
		layerPlug()->hash( h );
		matteValuesPlug()->hash( h );
		inPlug()->metadataPlug()->hash( h );

		if( channelName == alphaChannel )
		{
			outputChannelPlug()->hash( h );
		}
	}

	if( channelName != "R" )
	{
		matteChannelDataPlug()->hash( h );
	}

	GafferImage::ImagePlug::ChannelDataScope channelDataScope( context );
	channelDataScope.setChannelName( &firstDataChannel );
	inPlug()->channelDataPlug()->hash( h );
}

IECore::ConstFloatVectorDataPtr Cryptomatte::computeChannelData( const std::string &channelName, const Imath::V2i &tileOrigin, const Gaffer::Context *context, const GafferImage::ImagePlug *parent ) const
{
	std::string cryptomatteLayer;
	std::string alphaChannel;
	ConstStringVectorDataPtr channelNamesData;
	{
		GafferImage::ImagePlug::GlobalScope globalScope( context );
		channelNamesData = inPlug()->channelNamesPlug()->getValue();
		cryptomatteLayer = layerPlug()->getValue();
		alphaChannel = outputChannelPlug()->getValue();
	}

	if( channelName != "R" && channelName != "G" && channelName != "B" && channelName != alphaChannel )
	{
		return inPlug()->channelDataPlug()->getValue();
	}

	const std::vector<std::string> &channelNames = channelNamesData->readable();

	if( cryptomatteLayer == "" )
	{
		if( GafferImage::ImageAlgo::channelExists( channelNames, channelName ) )
		{
			return inPlug()->channelDataPlug()->getValue();
		}
		else
		{
			return GafferImage::ImagePlug::blackTile();
		}
	}

	if( channelName == "R" || channelName == "G" || channelName == "B" )
	{
		const std::string &firstDataChannel = cryptomatteLayer + g_firstDataChannelSuffix;
		if( !GafferImage::ImageAlgo::channelExists( channelNames, firstDataChannel ) )
		{
			return GafferImage::ImagePlug::blackTile();
		}

		GafferImage::ImagePlug::ChannelDataScope channelDataScope( context );
		channelDataScope.setChannelName( &firstDataChannel );

		if( channelName == "R" )
		{
			return inPlug()->channelDataPlug()->getValue();
		}
		else if( channelName == "G" || channelName == "B" )
		{
			FloatVectorDataPtr resultData = new FloatVectorData;
			std::vector<float> &result = resultData->writable();
			result.resize( GafferImage::ImagePlug::tilePixels(), 0.0f );

			ConstFloatVectorDataPtr valueData = inPlug()->channelDataPlug()->getValue();
			const std::vector<float> &values = valueData->readable();

			ConstFloatVectorDataPtr alphaData = matteChannelDataPlug()->getValue();
			const std::vector<float> &alphas = alphaData->readable();

			const size_t shift = channelName == "G" ? 8 : 16;
			const float mult = channelName == "G" ? 0.25f : 0.75f;
			uint32_t h;

			std::vector<float>::const_iterator vIt = values.begin();
			std::vector<float>::const_iterator aIt = alphas.begin();
			for( std::vector<float>::iterator it = result.begin(), eIt = result.end(); it != eIt; ++it, ++vIt, ++aIt )
			{
				// Adapted from the Cryptomatte specification
				std::memcpy( &h, &(*vIt), sizeof( uint32_t ) );
				*it = (float)(h << shift) / (float)UINT32_MAX * 0.3f + *aIt * mult;
			}

			return resultData;
		}
	}
	else if( channelName == alphaChannel )
	{
		return matteChannelDataPlug()->getValue();
	}

	return GafferImage::ImagePlug::blackTile();
}

} // namespace GafferScene


#include "distribution/distribute_tree.hpp"
#include "messages/sphere_tracts.pb.h"
#include "utility/gaussian.hpp"
#include "utility/options.hpp"
#include "utility/volume_image.hpp"
#include <boost/filesystem.hpp>
#include <boost/program_options.hpp>
#include <fmt/ostream.h>
#include <random>
#include <unordered_map>

namespace fs = boost::filesystem;
namespace po = boost::program_options;
using namespace jhmi;

auto compare_distributions(tract_tree const& tree, double straight_ratio,
    std::mt19937& gen, int num_comparisons,
    m min90_0, m max90_0, m max0, m min90_1, m max90_1, m max1) {
  auto mv = distribute_tree<distribute_vessel>{tree, straight_ratio};
  auto nd0 = truncated_gaussian{gaussian_90_sampler{gen, min90_0, max90_0}, 0_mm, max0};
  auto nd1 = truncated_gaussian{gaussian_90_sampler{gen, min90_1, max90_1}, 0_mm, max1};

  auto seeds  = ranges::view::generate_n([&] { return gen(); }, num_comparisons) | ranges::to_vector;
  auto radii0 = ranges::view::generate_n([&] { return nd0(); }, num_comparisons) | ranges::to_vector;
  auto radii1 = ranges::view::generate_n([&] { return nd1(); }, num_comparisons) | ranges::to_vector;

  std::atomic<int> idx{0};
  return tbb::parallel_reduce(tbb::blocked_range<size_t>(0, num_comparisons), m3{},
    [&](tbb::blocked_range<size_t> const& prange, m3 diffs) {
      for(auto i = prange.begin(); i != prange.end(); ++i) {
        std::mt19937 tgen{seeds[i]};
        auto r0 = radii0[i], r1 = radii1[i];

        auto urd = std::uniform_real_distribution<>{};
        auto branch_rand = std::bind(urd, std::ref(tgen));

        boost::optional<m3> pt0, pt1;
        m3 pt;
        mv.traverse_individual([&](distribute_vessel& dv, flow_vessel const& fv,
              distribute_vessel const* left, distribute_vessel const* right) {
          pt = fv.start;
          if (!pt0 && (!left || fv.radius < r0))
            pt0 = fv.start;
          if (!pt1 && (!left || fv.radius < r1))
            pt1 = fv.start;

          if (right && branch_rand() > left->p)
            return distribute_tree<distribute_vessel>::traversal::right;
          else if (left)
            return distribute_tree<distribute_vessel>::traversal::left;
          assert(pt0 && pt1);
          return distribute_tree<distribute_vessel>::traversal::abort;
        });
        assert(pt0 && pt1);
        diffs.x += distance(pt - *pt0);
        diffs.y += distance(pt - *pt1);
        diffs.z += distance(*pt0 - *pt1);
        auto curridx = ++idx;
        if (curridx % 10000 == 0)
          fmt::print("{} %\n", (100. * curridx) / num_comparisons);
      }
      return diffs;
    }, std::plus<>{}) / double(num_comparisons);
}

int main(int argc, char* argv[]) {
  try {
    GOOGLE_PROTOBUF_VERIFY_VERSION;

    auto opts = options<seed_option, treefile_option>{};
    int comparisons;
    m min90_0, max90_0, max_0, min90_1, max90_1, max_1;
    double straight_ratio;
    opts.description().add_options()
      ("comparisons,c", po::value(&comparisons)->default_value(10'000'000),
         "number of particle distributions to compare.")
      ("percentage", po::value(&straight_ratio)->default_value(.6),
         "Percentage of microspheres entering the straight vessel given equal flow")
      ("min90_0", unit_value(&min90_0, 1e-6)->required(),
         "diameter in um of 5th percentile of a normal sphere distribution")
      ("max90_0", unit_value(&max90_0, 1e-6)->required(),
         "diameter in um of 95th percentile of a normal sphere distribution")
      ("max_0", unit_value(&max_0, 1e-6)->required(),
         "maximum diameter in um of a normal sphere distribution")
      ("min90_1", unit_value(&min90_1, 1e-6)->required(),
         "diameter in um of 5th percentile of a normal sphere distribution")
      ("max90_1", unit_value(&max90_1, 1e-6)->required(),
         "diameter in um of 95th percentile of a normal sphere distribution")
      ("max_1", unit_value(&max_1, 1e-6)->required(),
         "maximum diameter in um of a normal sphere distribution");

    if (!opts.parse(argc, argv))
      return 1;

    auto tree = tract_tree{opts.treefile().string()};
    auto gen = std::mt19937{opts.seed()};
    fmt::print("Beginning distribution\n");
    auto start = std::chrono::high_resolution_clock::now();
    auto means = compare_distributions(tree, straight_ratio, gen, comparisons,
      min90_0/2., max90_0/2., max_0/2., min90_1/2., max90_1/2., max_1/2.);
    auto end = std::chrono::high_resolution_clock::now();
    fmt::print("Done in {} s!\n", std::chrono::duration<float>(end - start).count());

    fmt::print("\nAverage distances between:\n");
    fmt::print("  {}-{} um to end: {} mm\n",
      1e6*min90_0.value(), 1e6*max90_0.value(), 1e3*means.x.value());
    fmt::print("  {}-{} um to end: {} mm\n",
      1e6*min90_1.value(), 1e6*max90_1.value(), 1e3*means.y.value());
    fmt::print("  {}-{} um to {}-{} um: {} mm\n",
      1e6*min90_0.value(), 1e6*max90_0.value(),
      1e6*min90_1.value(), 1e6*max90_1.value(), 1e3*means.z.value());
  }
  catch(std::exception const& e) {
    fmt::print("Error running program: {}\n", e.what());
    return 1;
  }

  google::protobuf::ShutdownProtobufLibrary();
  return 0;
}