//---------------------------------------------------------------------------//
//  MIT License
//
//  Copyright (c) 2020-2021 Mikhail Komarov <nemo@nil.foundation>
//  Copyright (c) 2020-2021 Nikita Kaskov <nemo@nil.foundation>

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

#ifndef FILECOIN_STORAGE_PROOFS_CORE_MERKLE_PROOF_HPP
#define FILECOIN_STORAGE_PROOFS_CORE_MERKLE_PROOF_HPP

#include <algorithm>
#include <vector>

#include <boost/variant.hpp>

#include <nil/crypto3/hash/algorithm/hash.hpp>

#include <nil/filecoin/storage/proofs/core/merkle/merkle.hpp>
#include <nil/filecoin/storage/proofs/core/proof/proof.hpp>
#include <nil/filecoin/storage/proofs/core/crypto/feistel.hpp>
#include <nil/filecoin/storage/proofs/core/path_element.hpp>

namespace nil {
    namespace filecoin {
        namespace merkletree {
            template<std::size_t A, std::size_t B, std::size_t C>
            std::size_t base_path_length(std::size_t leaves) {
                std::size_t l;
                if (C > 0) {
                    l = leaves / C / B;
                } else if (B > 0) {
                    l = leaves / B;
                } else {
                    l = leaves;
                }

                return graph_height<A>(l) - 1;
            }

            template<std::size_t A, std::size_t B, std::size_t C>
            std::size_t compound_path_length(std::size_t leaves) {
                std::size_t len = base_path_length<A, B, C>(leaves);
                if (B > 0) {
                    len += 1;
                }

                if (C > 0) {
                    len += 1;
                }

                return len;
            }

            template<std::size_t A, std::size_t B, std::size_t C>
            std::size_t compound_tree_height(std::size_t leaves) {
                // base layer
                std::size_t a = graph_height<A>(leaves) - 1;

                // sub tree layer
                std::size_t b;
                if (B > 0) {
                    b = B - 1;
                } else {
                    b = 0;
                }

                // top tree layer
                std::size_t c;
                if (C > 0) {
                    c = C - 1;
                } else {
                    c = 0;
                }

                return a + b + c;
            }

            template<typename Hash>
            struct Proof_basic_policy {
                typedef std::array<uint8_t, Hash::digest_size> hash_result_type;
                constexpr static const std::size_t hash_digest_size = Hash::digest_size;
            };

            template<typename Hash, std::size_t BaseTreeArity = 2>
            struct Proof {

                typedef typename MerkleTree_basic_policy<Hash>::hash_result_type element;
                constexpr static const std::size_t element_size = MerkleTree_basic_policy<Hash>::hash_digest_size;

                // Optional proofs at immediate lower level from current.  Should
                // be None at the base layer.
                std::shared_ptr<Proof<element, BaseTreeArity>> sub_tree_proof;
                std::size_t top_layer_nodes;         // arity of top layer
                std::size_t sub_tree_layer_nodes;    // arity of sub-tree
                std::vector<element> lemma; // layer
                std::vector<std::size_t> path; // branch index

                /// Creates new MT inclusion proof
                template<std::size_t TopLayerArity, std::size_t SubTreeArity>
                Proof(std::shared_ptr<Proof<element, BaseTreeArity>> sub_tree_proof, const std::vector<element> &lemma, const
                      std::vector<std::size_t> &path) : sub_tree_proof(sub_tree_proof), top_layer_nodes(TopLayerArity),
                    sub_tree_layer_nodes(SubTreeArity), lemma(lemma), path(path){
                    if (TopLayerArity == 0 && SubTreeArity == 0) {
                        BOOST_ASSERT_MSG(lemma.size() > 2, "Invalid lemma length (short)");
                        BOOST_ASSERT_MSG(lemma.size() == utilities::get_merkle_proof_lemma_len(path.size() + 1, BaseTreeArity),
                            "Invalid lemma length");
                    }
                }

                /// Return proof target leaf
                element item() {
                    return *lemma.begin();
                }

                /// Return sub tree root
                element sub_tree_root() {
                    assert(sub_tree_layer_nodes > 0 && sub_tree_proof.is_some());
                    return sub_tree_proof.root();
                }

                /// Return tree root
                element root() {
                    return *(lemma.end() - 1);
                }

                /// Validates sub-tree proofs with the specified arity.
                bool validate_sub_tree_proof(std::size_t arity) {
                    // Ensure that the sub_tree validates to the root of that
                    // sub_tree.
                    bool valid = sub_tree_proof.unwrap().validate::<Algorithm<element>>();
                    if (!valid) {
                            return valid;
                        }

                    // Validate top-most/current layer
                    //
                    // Check that the remaining proof matches the tree root (note
                    // that Proof::validate at the base layer cannot handle a
                    // proof this small, so this is a version specific for what we
                    // know we have in this case).
                    auto a = Algorithm<T>::default();
                    a.reset();
                    const auto node_count = arity;
                    const auto h = {
                        std::vector<T> nodes;
                        nodes.reserve(node_count);

                        auto cur_index = 0;
                        for (const auto j = 0; j < node_count; ++j) {
                            if j == self.path()[0] {
                                nodes.push(self.sub_tree_root().clone());
                            } else {
                                nodes.push(self.lemma()[cur_index].clone());
                                cur_index += 1;
                            }
                        }

                        if cur_index != node_count - 1 {
                            return false;
                        }

                        a.multi_node(&nodes, 0)
                    };

                    return h == root();
                }

                /// Verifies MT inclusion proof
                bool validate() {
                    if (top_layer_nodes > 0) {
                        // Special Top layer handling here.
                        BOOST_ASSERT_MSG(sub_tree_proof,
                                "Sub tree proof must be present for validation");

                        return validate_sub_tree_proof<Algorithm>(top_layer_nodes);
                    }

                    if (sub_tree_layer_nodes > 0) {
                        // Sub-tree layer handling here.
                        BOOST_ASSERT_MSG(sub_tree_proof,
                                "Sub tree proof must be present for validation");

                        return validate_sub_tree_proof<Algorithm>(sub_tree_layer_nodes);
                    }

                    // Base layer handling here.
                    BOOST_ASSERT_MSG(sub_tree_layer_nodes == 0, "Base layer proof must have 0 as sub-tree layer node count");
                    BOOST_ASSERT_MSG(top_layer_nodes == 0, "Base layer proof must have 0 as top layer node count");
                    BOOST_ASSERT_MSG(!sub_tree_proof, "Sub tree proof must be None");

                    std::size_t size = lemma.size();
                    if (size < 2) {
                        return false;
                    }

                    std::size_t branches = BaseTreeArity;
                    auto a = Algorithm<element>::default();
                    auto h = this->item();
                    auto path_index = 1;

                    for (size_t i = 1; i < size - 1; i += branches - 1) {
                        a.reset();
                        h = {
                            std::vector<element> nodes;
                            nodes.reserve(branches);
                            auto cur_index = 0;
                            for (j = 0; j < branches; ++j) {
                                if j == self.path[path_index - 1] {
                                    nodes.push(h.clone());
                                } else {
                                    nodes.push(self.lemma[i + cur_index].clone());
                                    cur_index += 1;
                                }
                            }

                            if cur_index != branches - 1 {
                                return false;
                            }

                            path_index += 1;
                            a.multi_node(&nodes, i - 1)
                        };
                    }

                    return h == root();
                }

                    /// Verifies MT inclusion proof and that leaf_data is the original leaf data for which proof was generated.
                template<template<typename> class Algorithm>
                bool validate_with_data(leaf_data: &dyn Hashable<A>) {
                    auto a = Algorithm<T>::default();
                    leaf_data.hash(&a);
                    const auto item = a.hash();
                    a.reset();
                    const auto leaf_hash = a.leaf(item);

                    if (leaf_hash == item()) {
                        return validate<Algorithm>();
                    } else {
                        return false;
                    }
                }
            };

//            /// Interface to abstract over the concept of Merkle Proof.
//            template<typename Hash, std::size_t BaseArity, std::size_t SubTreeArity, std::size_t TopTreeArity,
//                     typename FieldType = typename crypto3::algebra::curves::bls12<381>::scalar_field_type>
//            struct BasicMerkleProof {
//                typedef Hash hash_type;
//                typedef FieldType field_type;
//                typedef typename field_type::value_type fr_value_type;
//
//                constexpr static const std::size_t base_arity = BaseArity;
//                constexpr static const std::size_t sub_tree_arity = SubTreeArity;
//                constexpr static const std::size_t top_tree_arity = TopTreeArity;
//
//                /// Try to convert a merkletree proof into this structure.
//                static BasicMerkleProof<Hash, BaseArity, SubTreeArity, TopTreeArity>
//                    try_from_proof(const Proof<typename Hash::digest_type, BaseArity> &p) {
//                }
//
//                std::vector<std::pair<std::vector<fr_value_type>, std::size_t>> as_options() {
//                    return path()
//                        .iter()
//                        .map(| v | {(v .0.iter().copied().map(Into::into).map(Some).collect(), Some(v .1), )})
//                        .collect::<Vec<_>>();
//                }
//
//                std::pair<fr_value_type, std::vector<std::pair<std::vector<fr_value_type>, std::size_t>>>
//                    into_options_with_leaf() {
//                    const auto leaf = leaf();
//                    const auto path = path();
//                    (Some(leaf.into()),
//                     path.into_iter()
//                         .map(| (a, b) | {(a.iter().copied().map(Into::into).map(Some).collect(), Some(b), )})
//                         .collect::<Vec<_>>(), )
//                }
//
//                std::vector<std::pair<std::vector<fr_value_type>, std::size_t>> as_pairs() {
//                    for (int i = 0; i < path().size(); i++) {
//
//                    }
//                        .iter()
//                        .map(| v | (v .0.iter().copied().map(Into::into).collect(), v .1))
//                        .collect::<Vec<_>>();
//                }
//
//                virtual bool verify() const = 0;
//
//                /// Validates the MerkleProof and that it corresponds to the supplied node.
//                ///
//                /// TODO: audit performance and usage in case verification is
//                /// unnecessary based on how it's used.
//                virtual bool validate(std::size_t node) {
//                    if (!verify()) {
//                        return false;
//                    }
//
//                    return node == path_index();
//                }
//
//                virtual bool validate_data(const typename Hash::digest_type &data) {
//                    if (!verify()) {
//                        return false;
//                    }
//
//                    return leaf() == data;
//                }
//
//                virtual typename Hash::digest_type leaf() = 0;
//                virtual typename Hash::digest_type root() = 0;
//                virtual std::size_t size() = 0;
//                virtual std::vector<std::pair<std::vector<typename Hash::digest_type>, std::size_t>> path() = 0;
//
//                std::size_t path_index() {
//                    return std::accumulate(
//                        path().begin(), path().end(), 0,
//                        [&](std::size_t acc, typename std::vector<std::pair<std::vector<typename Hash::digest_type>,
//                                                                            std::size_t>>::value_type &val) -> std::size_t {
//                            return (acc + BaseArity) + val.second;
//                        });
//                }
//
//                bool proves_challenge(std::size_t challenge) {
//                    path_index() == challenge;
//                }
//
//                /// Calcluates the exected length of the full path, given the number of leaves in the base layer.
//                std::size_t expected_len(std::size_t leaves) {
//                    return compound_path_length<BaseArity, SubTreeArity, TopTreeArity>(leaves);
//                }
//            };
//
//            template<typename Hash, std::size_t BaseArity>
//            struct InclusionPath {
//                /// Calculate the root of this path, given the leaf as input.
//                typename Hash::digest_type root(const typename Hash::digest_type &leaf) {
//                    using namespace nil::crypto3;
//                    accumulator_set<Hash> acc;
//                    std::accumulate(path.begin(), path.end(), leaf,
//                                    [&](typename Hash::digest_type acc,
//                                        typename std::vector<PathElement<Hash, BaseArity>>::value_type &v) {
//
//                                    });
//                    auto a = H::Function::default();
//                    (0..self.path.len())
//                        .fold(
//                            leaf, | h, height | {
//                                a.reset();
//
//                                const auto index = self.path[height].index;
//                                auto nodes = self.path[height].hashes.clone();
//                                nodes.insert(index, h);
//
//                                a.multi_node(&nodes, height)
//                            })
//                }
//
//                std::size_t size() {
//                    return path.size();
//                }
//
//                bool empty() {
//                    return path.empty();
//                }
//
//                std::size_t path_index() {
//                    return std::accumulate(
//                        path.begin(), path.end(), 0,
//                        [&](std::size_t acc, typename std::vector<PathElement<Hash, BaseArity>>::value_type &v) {
//                            return (acc * BaseArity) + v.index;
//                        });
//                }
//
//                std::vector<PathElement<Hash, BaseArity>> path;
//            };
//
//            template<typename Hash, std::size_t BaseArity>
//            struct SingleProof {
//                template<template<typename, std::size_t> class Proof>
//                static SingleProof<Hash, BaseArity> try_from_proof(const Proof<typename Hash::digest_type, BaseArity> &p) {
//                    return proof_to_single(p, 1);
//                }
//
//                bool verify() {
//                    return root == path.root(leaf);
//                }
//
//                std::size_t size() {
//                    return path.size() * (BaseArity - 1) + 2;
//                }
//
//                std::vector<std::pair<std::vector<typename Hash::digest_type>, std::size_t>> path() {
//                    return path.iter().map(| x | (x.hashes.clone(), x.index)).collect::<Vec<_>>();
//                }
//
//                std::size_t path_index() {
//                    return path.path_index();
//                }
//
//                /// Root of the merkle tree.
//                typename Hash::digest_type root;
//                /// The original leaf data for this prof.
//                typename Hash::digest_type leaf;
//                /// The path from leaf to root.
//                InclusionPath<Hash, BaseArity> path;
//            };
//
//            template<typename Hash, std::size_t BaseArity, std::size_t SubTreeArity>
//            struct SubProof {
//                static SubProof<Hash, BaseArity, SubTreeArity>
//                    try_from_proof(const Proof<typename Hash::digest_type, BaseArity> &p) {
//                    BOOST_ASSERT_MSG(p.sub_layer_nodes() == SubTreeArity, "sub arity mismatch");
//                    BOOST_ASSERT_MSG(p.sub_tree_proof, "Cannot generate sub proof without a base-proof");
//                    std::shared_ptr<Proof<typename Hash::digest_type, BaseArity>> base_p = p.sub_tree_proof;
//
//                    // Generate SubProof
//                    typename Hash::digest_type root = p.root();
//                    typename Hash::digest_type leaf = base_p.item();
//                    InclusionPath<Hash, BaseArity> base_proof =
//                        extract_path<typename Hash::digest_type, BaseArity>(base_p.lemma(), base_p.path(), 1);
//                    InclusionPath<Hash, SubTreeArity> sub_proof =
//                        extract_path<typename Hash::digest_type, SubTreeArity>(p.lemma(), p.path(), 0);
//
//                    return {base_proof, sub_proof, root, leaf};
//                }
//
//                bool verify() {
//                    root == sub_proof.root(base_proof.root(leaf));
//                }
//
//                std::size_t size() {
//                    return SubTreeArity;
//                }
//
//                std::vector<std::pair<std::vector<typename Hash::digest_type>, std::size_t>> path() {
//                    return base_proof.iter()
//                        .map(| x | (x.hashes.clone(), x.index))
//                        .chain(self.sub_proof.iter().map(| x | (x.hashes.clone(), x.index)))
//                        .collect();
//                }
//
//                std::size_t path_index() {
//                    std::size_t base_proof_leaves = 1;
//                    for (int i = 0; i < base_proof.size(); i++) {
//                        base_proof_leaves *= BaseArity;
//                    }
//
//                    std::size_t sub_proof_index = sub_proof.path_index();
//
//                    return (sub_proof_index * base_proof_leaves) + base_proof.path_index();
//                }
//
//                InclusionPath<Hash, BaseArity> base_proof;
//
//                InclusionPath<Hash, SubTreeArity> sub_proof;
//
//                typename Hash::digest_type root;
//                /// The original leaf data for this prof.
//
//                typename Hash::digest_type leaf;
//            };
//
//            template<typename Hash, std::size_t BaseArity, std::size_t SubTreeArity, std::size_t TopTreeArity>
//            struct TopProof {
//                TopProof<Hash, BaseArity, SubTreeArity, TopTreeArity>
//                    try_from_proof(const Proof<typename Hash::digest_type, BaseArity> &p) {
//                    BOOST_ASSERT_MSG(p.top_layer_nodes() == TopTreeArity, "top arity mismatch");
//                    BOOST_ASSERT_MSG(p.sub_layer_nodes() == SubTreeArity, "sub arity mismatch");
//
//                    BOOST_ASSERT_MSG(p.sub_tree_proof, "Cannot generate top proof without a sub-proof");
//                    const auto sub_p = p.sub_tree_proof;
//
//                    BOOST_ASSERT_MSG(sub_p.sub_tree_proof, "Cannot generate top proof without a base-proof");
//                    const auto base_p = sub_p.sub_tree_proof;
//
//                    const auto root = p.root();
//                    const auto leaf = base_p.item();
//
//                    return {extract_path<Hash, BaseArity>(base_p.lemma(), base_p.path(), 1), extract_path<Hash, SubTreeArity>(sub_p.lemma(), sub_p.path(), 0), extract_path<Hash, TopTreeArity>(p.lemma(), p.path(), 0), root, leaf};
//                }
//
//                bool verify() {
//                    root == top_proof.root(sub_proof.root(base_proof.root(leaf)));
//                }
//
//                std::size_t size() {
//                    return TopTreeArity;
//                }
//
//                std::vector<std::pair<std::vector<typename Hash::digest_type>, std::size_t>> path() {
//                    return base_proof.iter()
//                        .map(| x | (x.hashes.clone(), x.index))
//                        .chain(self.sub_proof.iter().map(| x | (x.hashes.clone(), x.index)))
//                        .chain(self.top_proof.iter().map(| x | (x.hashes.clone(), x.index)))
//                        .collect();
//                }
//
//                std::size_t path_index() {
//                    std::size_t base_proof_leaves = 1;
//                    for (int i = 0; i < base_proof.size(); i++) {
//                        base_proof_leaves *= BaseArity;
//                    }
//
//                    return (sub_proof.path_index() * base_proof_leaves) +
//                           (top_proof.path_index() * base_proof_leaves * SubTreeArity) + base_proof.path_index();
//                }
//
//                InclusionPath<Hash, BaseArity> base_proof;
//
//                InclusionPath<Hash, SubTreeArity> sub_proof;
//
//                InclusionPath<Hash, TopTreeArity> top_proof;
//                /// Root of the merkle tree.
//
//                typename Hash::digest_type root;
//                /// The original leaf data for this prof.
//                typename Hash::digest_type leaf;
//            };
//
//            template<typename Hash, std::size_t BaseArity, std::size_t SubTreeArity, std::size_t TopTreeArity>
//            using ProofData = boost::variant<SingleProof<Hash, BaseArity>, SubProof<Hash, BaseArity, SubTreeArity>,
//                                             TopProof<Hash, BaseArity, SubTreeArity, TopTreeArity>>;
//
//            template<typename Hash, std::size_t BaseArity, std::size_t SubTreeArity, std::size_t TopTreeArity>
//            struct MerkleProof : public BasicMerkleProof<Hash, BaseArity, SubTreeArity, TopTreeArity> {
//                typedef typename Hash::digest_type digest_type;
//                MerkleProof(std::size_t n) :
//                    data(SingleProof<Hash, BaseArity>(std::vector<PathElement<Hash, BaseArity>>(n), root, leaf)) {
//                }
//
//                virtual bool verify() const override {
//                    return false;
//                }
//                virtual bool validate(std::size_t node) override {
//                }
//                virtual bool validate_data(const digest_type &data) override {
//                }
//                virtual digest_type leaf() override {
//                    return nullptr;
//                }
//                virtual digest_type root() override {
//                    return nullptr;
//                }
//                virtual std::vector<std::pair<std::vector<typename Hash::digest_type>, std::size_t>> path() override {
//                    return std::vector<std::pair<std::vector<typename Hash::digest_type>, std::size_t>>();
//                }
//
//                ProofData<Hash, BaseArity, SubTreeArity, TopTreeArity> data;
//            };
//
//            /// 'lemma_start_index' is required because sub/top proofs start at
//            /// index 0 and base proofs start at index 1 (skipping the leaf at the
//            /// front)
//            template<typename Hash, std::size_t BaseArity>
//            InclusionPath<Hash, BaseArity> extract_path(const std::vector<typename Hash::digest_type> &lemma,
//                                                        const std::vector<std::size_t> &path,
//                                                        std::size_t lemma_start_index) {
//                std::vector<PathElement<Hash, BaseArity>> res;
//
//                for (int i = 0; i < path.size(); i++) {
//                    res.emplace_back(
//                        std::vector<typename Hash::digest_type>(lemma.begin() + lemma_start_index + BaseArity * i,
//                                                                lemma.begin() + lemma_start_index + BaseArity * (i + 1)),
//                        index);
//                }
//
//                return InclusionPath<Hash, BaseArity>(res);
//            }
//
//            /// Converts a merkle_light proof to a SingleProof
//            template<typename Hash, std::size_t BaseArity, std::size_t TargetArity,
//                     template<typename, std::size_t> class Proof>
//            SingleProof<Hash, TargetArity>
//                proof_to_single(const Proof<Hash, BaseArity> &proof, std::size_t lemma_start_index,
//                                typename Hash::digest_type &sub_root = typename Hash::digest_type()) {
//                typename Hash::digest_type root = proof.root();
//                typename Hash::digest_type leaf = sub_root.emplty() ? sub_root : proof.item();
//
//                InclusionPath<Hash, TargetArity> path =
//                    extract_path<Hash, TargetArity>(proof.lemma(), proof.path(), lemma_start_index);
//
//                return {path, root, leaf};
//            }
        }    // namespace merkletree
    }    // namespace filecoin
}    // namespace nil

#endif


// Boost.Geometry - gis-projections (based on PROJ4)

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

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

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

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

// Last updated version of proj: 5.0.0

// Original copyright notice:

// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:

// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.

// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.

#ifndef BOOST_GEOMETRY_PROJECTIONS_BONNE_HPP
#define BOOST_GEOMETRY_PROJECTIONS_BONNE_HPP

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

#include <boost/geometry/util/math.hpp>

#include <boost/math/special_functions/hypot.hpp>

namespace boost { namespace geometry
{

namespace projections
{
    #ifndef DOXYGEN_NO_DETAIL
    namespace detail { namespace bonne
    {

            static const double epsilon10 = 1e-10;

            template <typename T>
            struct par_bonne
            {
                T phi1;
                T cphi1;
                T am1;
                T m1;
                detail::en<T> en;
            };

            // template class, using CRTP to implement forward/inverse
            template <typename T, typename Parameters>
            struct base_bonne_ellipsoid
                : public base_t_fi<base_bonne_ellipsoid<T, Parameters>, T, Parameters>
            {
                par_bonne<T> m_proj_parm;

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

                // FORWARD(e_forward)  ellipsoid
                // Project coordinates from geographic (lon, lat) to cartesian (x, y)
                inline void fwd(T const& lp_lon, T const& lp_lat, T& xy_x, T& xy_y) const
                {
                    T rh, E, c;

                    rh = this->m_proj_parm.am1 + this->m_proj_parm.m1 - pj_mlfn(lp_lat, E = sin(lp_lat), c = cos(lp_lat), this->m_proj_parm.en);
                    E = c * lp_lon / (rh * sqrt(1. - this->m_par.es * E * E));
                    xy_x = rh * sin(E);
                    xy_y = this->m_proj_parm.am1 - rh * cos(E);
                }

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

                    T s, rh;

                    rh = boost::math::hypot(xy_x, xy_y = this->m_proj_parm.am1 - xy_y);
                    lp_lat = pj_inv_mlfn(this->m_proj_parm.am1 + this->m_proj_parm.m1 - rh, this->m_par.es, this->m_proj_parm.en);
                    if ((s = fabs(lp_lat)) < half_pi) {
                        s = sin(lp_lat);
                        lp_lon = rh * atan2(xy_x, xy_y) *
                           sqrt(1. - this->m_par.es * s * s) / cos(lp_lat);
                    } else if (fabs(s - half_pi) <= epsilon10)
                        lp_lon = 0.;
                    else
                        BOOST_THROW_EXCEPTION( projection_exception(error_tolerance_condition) );
                }

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

            };

            // template class, using CRTP to implement forward/inverse
            template <typename T, typename Parameters>
            struct base_bonne_spheroid
                : public base_t_fi<base_bonne_spheroid<T, Parameters>, T, Parameters>
            {
                par_bonne<T> m_proj_parm;

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

                // FORWARD(s_forward)  spheroid
                // Project coordinates from geographic (lon, lat) to cartesian (x, y)
                inline void fwd(T const& lp_lon, T const& lp_lat, T& xy_x, T& xy_y) const
                {
                    T E, rh;

                    rh = this->m_proj_parm.cphi1 + this->m_proj_parm.phi1 - lp_lat;
                    if (fabs(rh) > epsilon10) {
                        xy_x = rh * sin(E = lp_lon * cos(lp_lat) / rh);
                        xy_y = this->m_proj_parm.cphi1 - rh * cos(E);
                    } else
                        xy_x = xy_y = 0.;
                }

                // INVERSE(s_inverse)  spheroid
                // Project coordinates from cartesian (x, y) to geographic (lon, lat)
                inline void inv(T const& xy_x, T xy_y, T& lp_lon, T& lp_lat) const
                {
                    static const T half_pi = detail::half_pi<T>();

                    T rh;

                    rh = boost::math::hypot(xy_x, xy_y = this->m_proj_parm.cphi1 - xy_y);
                    lp_lat = this->m_proj_parm.cphi1 + this->m_proj_parm.phi1 - rh;
                    if (fabs(lp_lat) > half_pi) {
                        BOOST_THROW_EXCEPTION( projection_exception(error_tolerance_condition) );
                    }
                    if (fabs(fabs(lp_lat) - half_pi) <= epsilon10)
                        lp_lon = 0.;
                    else
                        lp_lon = rh * atan2(xy_x, xy_y) / cos(lp_lat);
                }

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

            };

            // Bonne (Werner lat_1=90)
            template <typename Params, typename Parameters, typename T>
            inline void setup_bonne(Params const& params, Parameters& par, par_bonne<T>& proj_parm)
            {
                static const T half_pi = detail::half_pi<T>();

                T c;

                proj_parm.phi1 = pj_get_param_r<T, srs::spar::lat_1>(params, "lat_1", srs::dpar::lat_1);
                if (fabs(proj_parm.phi1) < epsilon10)
                    BOOST_THROW_EXCEPTION( projection_exception(error_lat1_is_zero) );

                if (par.es != 0.0) {
                    proj_parm.en = pj_enfn<T>(par.es);
                    proj_parm.m1 = pj_mlfn(proj_parm.phi1, proj_parm.am1 = sin(proj_parm.phi1),
                        c = cos(proj_parm.phi1), proj_parm.en);
                    proj_parm.am1 = c / (sqrt(1. - par.es * proj_parm.am1 * proj_parm.am1) * proj_parm.am1);
                } else {
                    if (fabs(proj_parm.phi1) + epsilon10 >= half_pi)
                        proj_parm.cphi1 = 0.;
                    else
                        proj_parm.cphi1 = 1. / tan(proj_parm.phi1);
                }
            }

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

    /*!
        \brief Bonne (Werner lat_1=90) projection
        \ingroup projections
        \tparam Geographic latlong point type
        \tparam Cartesian xy point type
        \tparam Parameters parameter type
        \par Projection characteristics
         - Conic
         - Spheroid
         - Ellipsoid
        \par Projection parameters
         - lat_1: Latitude of first standard parallel (degrees)
        \par Example
        \image html ex_bonne.gif
    */
    template <typename T, typename Parameters>
    struct bonne_ellipsoid : public detail::bonne::base_bonne_ellipsoid<T, Parameters>
    {
        template <typename Params>
        inline bonne_ellipsoid(Params const& params, Parameters const& par)
            : detail::bonne::base_bonne_ellipsoid<T, Parameters>(par)
        {
            detail::bonne::setup_bonne(params, this->m_par, this->m_proj_parm);
        }
    };

    /*!
        \brief Bonne (Werner lat_1=90) projection
        \ingroup projections
        \tparam Geographic latlong point type
        \tparam Cartesian xy point type
        \tparam Parameters parameter type
        \par Projection characteristics
         - Conic
         - Spheroid
         - Ellipsoid
        \par Projection parameters
         - lat_1: Latitude of first standard parallel (degrees)
        \par Example
        \image html ex_bonne.gif
    */
    template <typename T, typename Parameters>
    struct bonne_spheroid : public detail::bonne::base_bonne_spheroid<T, Parameters>
    {
        template <typename Params>
        inline bonne_spheroid(Params const& params, Parameters const& par)
            : detail::bonne::base_bonne_spheroid<T, Parameters>(par)
        {
            detail::bonne::setup_bonne(params, this->m_par, this->m_proj_parm);
        }
    };

    #ifndef DOXYGEN_NO_DETAIL
    namespace detail
    {

        // Static projection
        BOOST_GEOMETRY_PROJECTIONS_DETAIL_STATIC_PROJECTION(srs::spar::proj_bonne, bonne_spheroid, bonne_ellipsoid)

        // Factory entry(s)
        BOOST_GEOMETRY_PROJECTIONS_DETAIL_FACTORY_ENTRY_FI2(bonne_entry, bonne_spheroid, bonne_ellipsoid)

        BOOST_GEOMETRY_PROJECTIONS_DETAIL_FACTORY_INIT_BEGIN(bonne_init)
        {
            BOOST_GEOMETRY_PROJECTIONS_DETAIL_FACTORY_INIT_ENTRY(bonne, bonne_entry);
        }

    } // namespace detail
    #endif // doxygen

} // namespace projections

}} // namespace boost::geometry

#endif // BOOST_GEOMETRY_PROJECTIONS_BONNE_HPP


/****************************************************************************
 * Copyright (c) 2021 by the Picasso authors                                *
 * All rights reserved.                                                     *
 *                                                                          *
 * This file is part of the Picasso library. Picasso is distributed under a *
 * BSD 3-clause license. For the licensing terms see the LICENSE file in    *
 * the top-level directory.                                                 *
 *                                                                          *
 * SPDX-License-Identifier: BSD-3-Clause                                    *
 ****************************************************************************/

#ifndef PICASSO_FACETGEOMETRY_HPP
#define PICASSO_FACETGEOMETRY_HPP

#include <Picasso_BatchedLinearAlgebra.hpp>

#include <Kokkos_Core.hpp>
#include <Kokkos_Random.hpp>

#include <boost/property_tree/ptree.hpp>

#include <cfloat>
#include <cmath>
#include <fstream>
#include <unordered_map>
#include <vector>

namespace Picasso
{
//---------------------------------------------------------------------------//
template <class MemorySpace>
struct FacetGeometryData
{
    // Get the number of volumes.
    KOKKOS_FUNCTION
    int numVolume() const { return volume_offsets.extent( 0 ); }

    // Given a local volume id get the facets associated with the volume.
    KOKKOS_FUNCTION
    Kokkos::View<float* [4][3], MemorySpace>
    volumeFacets( const int volume_id ) const
    {
        Kokkos::pair<int, int> facet_bounds(
            ( 0 == volume_id ) ? 0 : volume_offsets( volume_id - 1 ),
            volume_offsets( volume_id ) );
        return Kokkos::subview( volume_facets, facet_bounds, Kokkos::ALL(),
                                Kokkos::ALL() );
    }

    // Get the number of surfaces.
    KOKKOS_FUNCTION
    int numSurface() const { return surface_offsets.extent( 0 ); }

    // Given a local surface id get the facets associated with the surface.
    KOKKOS_FUNCTION
    Kokkos::View<float* [4][3], MemorySpace>
    surfaceFacets( const int surface_id ) const
    {
        Kokkos::pair<int, int> facet_bounds(
            ( 0 == surface_id ) ? 0 : surface_offsets( surface_id - 1 ),
            surface_offsets( surface_id ) );
        return Kokkos::subview( surface_facets, facet_bounds, Kokkos::ALL(),
                                Kokkos::ALL() );
    }

    // Volume facets. Ordered as (facet,vector,dim) where vector=0,1,2 are the
    // vertices and vector=3 is the unit normal facing outward from the
    // volume.
    Kokkos::View<float* [4][3], MemorySpace> volume_facets;

    // Volume face offsets. Inclusive scan of volume counts giving the offset
    // into the facet array for each volume.
    Kokkos::View<int*, MemorySpace> volume_offsets;

    // Surface facets. Ordered as (facet,vector,dim) where vector=0,1,2 are the
    // vertices and vector=3 is the unit normal facing outward from the
    // surface.
    Kokkos::View<float* [4][3], MemorySpace> surface_facets;

    // Surface face offsets. Inclusive scan of surface counts giving the offset
    // into the facet array for each surface.
    Kokkos::View<int*, MemorySpace> surface_offsets;

    // Local id of the volume the representes the global axis-aligned bounding
    // box.
    int global_bounding_volume_id;

    // Axis aligned bounding box for all volumes.
    Kokkos::View<float* [6], MemorySpace> volume_bounding_boxes;
};

//---------------------------------------------------------------------------//
template <class MemorySpace>
class FacetGeometry
{
  public:
    // Compute the axis-aligned bounding box of a volume.
    struct BoundingBoxReduce
    {
        typedef float value_type[];
        typedef Kokkos::View<float* [4][3], MemorySpace> facet_view_type;
        typedef Kokkos::View<int*, MemorySpace> offset_view_type;
        typedef typename facet_view_type::size_type size_type;
        size_type value_count;

        facet_view_type facets;
        offset_view_type offsets;
        int volume_id;

        BoundingBoxReduce( const facet_view_type& f, const offset_view_type& o,
                           int v )
            : value_count( 6 )
            , facets( f )
            , offsets( o )
            , volume_id( v )
        {
        }

        KOKKOS_FUNCTION
        void operator()( const size_type facet_id, value_type result ) const
        {
            auto f_start = ( volume_id == 0 ) ? 0 : offsets( volume_id - 1 );
            auto f = f_start + facet_id;
            result[0] =
                fmin( result[0],
                      fmin( facets( f, 0, 0 ),
                            fmin( facets( f, 1, 0 ), facets( f, 2, 0 ) ) ) );
            result[1] =
                fmin( result[1],
                      fmin( facets( f, 0, 1 ),
                            fmin( facets( f, 1, 1 ), facets( f, 2, 1 ) ) ) );
            result[2] =
                fmin( result[2],
                      fmin( facets( f, 0, 2 ),
                            fmin( facets( f, 1, 2 ), facets( f, 2, 2 ) ) ) );
            result[3] =
                fmax( result[3],
                      fmax( facets( f, 0, 0 ),
                            fmax( facets( f, 1, 0 ), facets( f, 2, 0 ) ) ) );
            result[4] =
                fmax( result[4],
                      fmax( facets( f, 0, 1 ),
                            fmax( facets( f, 1, 1 ), facets( f, 2, 1 ) ) ) );
            result[5] =
                fmax( result[5],
                      fmax( facets( f, 0, 2 ),
                            fmax( facets( f, 1, 2 ), facets( f, 2, 2 ) ) ) );
        }

        KOKKOS_FUNCTION
        void join( volatile value_type dst,
                   const volatile value_type src ) const
        {
            dst[0] = fmin( dst[0], src[0] );
            dst[1] = fmin( dst[1], src[1] );
            dst[2] = fmin( dst[2], src[2] );
            dst[3] = fmax( dst[3], src[3] );
            dst[4] = fmax( dst[4], src[4] );
            dst[5] = fmax( dst[5], src[5] );
        }

        KOKKOS_FUNCTION
        void init( value_type v ) const
        {
            v[0] = FLT_MAX;
            v[1] = FLT_MAX;
            v[2] = FLT_MAX;
            v[3] = -FLT_MAX;
            v[4] = -FLT_MAX;
            v[5] = -FLT_MAX;
        }
    };

  public:
    using memory_space = MemorySpace;

    // Default constructor.
    FacetGeometry() = default;

    // Create the geometry from an ASCII STL file.
    template <class ExecutionSpace>
    FacetGeometry( const boost::property_tree::ptree& ptree,
                   const ExecutionSpace& exec_space )
    {
        // Get the geometry parameters.
        const auto& params = ptree.get_child( "geometry" );

        // Read the stl file and create the facet geometry.
        auto stl_ascii_filename = params.get<std::string>( "stl_file" );

        // Containers.
        std::vector<int> volume_ids;
        std::vector<int> surface_ids;
        std::vector<float> volume_facets;
        std::vector<float> surface_facets;

        // Load the file.
        std::ifstream file( stl_ascii_filename );
        if ( !file.is_open() )
            throw std::runtime_error( "Unable to open STL ASCII file" );

        // Read the file.
        std::string buffer;
        std::vector<std::string> tokens;
        tokens.reserve( 10 );
        bool read_volume = false;
        bool read_surface = false;
        while ( !file.eof() )
        {
            // Get the current line.
            std::getline( file, buffer );

            // Break the line up into tokens.
            std::istringstream line( buffer );
            tokens.resize( 0 );
            while ( !line.eof() )
            {
                tokens.push_back( std::string() );
                line >> tokens.back();
            }

            // Parse the line.
            if ( tokens.size() > 0 )
            {
                // New solid.
                if ( tokens[0].compare( "solid" ) == 0 )
                {
                    if ( tokens.size() != 3 )
                        throw std::runtime_error(
                            "STL READER: Expected 3 solid line entries" );

                    // New volume.
                    if ( tokens[1].compare( "Volume" ) == 0 ||
                         tokens[1].compare( "Body" ) == 0 )
                    {
                        volume_ids.push_back( std::atoi( tokens[2].c_str() ) );
                        _volume_facet_count.push_back( 0 );
                        read_volume = true;
                    }

                    // New surface.
                    else if ( tokens[1].compare( "Surface" ) == 0 )
                    {
                        surface_ids.push_back( std::atoi( tokens[2].c_str() ) );
                        _surface_facet_count.push_back( 0 );
                        read_surface = true;
                    }

                    else
                    {
                        throw std::runtime_error(
                            "STL READER: Solids execpted to be Volume/Body or "
                            "Surface" );
                    }
                }

                // Read facet.
                else if ( tokens[0].compare( "facet" ) == 0 )
                {
                    if ( tokens.size() != 5 )
                        throw std::runtime_error(
                            "STL READER: Expected 5 facet line entries" );

                    // Volume facet.
                    if ( read_volume )
                    {
                        ++_volume_facet_count.back();
                    }

                    // Surface facet.
                    else if ( read_surface )
                    {
                        ++_surface_facet_count.back();
                    }
                }

                // Read vertex.
                else if ( tokens[0].compare( "vertex" ) == 0 )
                {
                    if ( tokens.size() != 4 )
                        throw std::runtime_error(
                            "STL READER: Expected 4 vertex line entries" );

                    // Volume vertex coordinates.
                    if ( read_volume )
                    {
                        volume_facets.push_back(
                            std::atof( tokens[1].c_str() ) );
                        volume_facets.push_back(
                            std::atof( tokens[2].c_str() ) );
                        volume_facets.push_back(
                            std::atof( tokens[3].c_str() ) );
                    }

                    // Surface vertex coordinates.
                    if ( read_surface )
                    {
                        surface_facets.push_back(
                            std::atof( tokens[1].c_str() ) );
                        surface_facets.push_back(
                            std::atof( tokens[2].c_str() ) );
                        surface_facets.push_back(
                            std::atof( tokens[3].c_str() ) );
                    }
                }

                // Finish reading a solid.
                else if ( tokens[0].compare( "endsolid" ) == 0 )
                {
                    read_volume = false;
                    read_surface = false;
                }
            }
        }

        // Close the file.
        file.close();

        // Put volume data on device.
        putFileDataOnDevice( volume_ids, _volume_facet_count, volume_facets,
                             _volume_ids, _data.volume_facets,
                             _data.volume_offsets );

        // Put surface data on device.
        putFileDataOnDevice( surface_ids, _surface_facet_count, surface_facets,
                             _surface_ids, _data.surface_facets,
                             _data.surface_offsets );

        // Get the volume id of the global bounding box. The user is required
        // to make an axis-aligned bounding box of their geometry that defines
        // the global bounds of the problem. The user input is the global id
        // of this volume.
        _data.global_bounding_volume_id =
            localVolumeId( params.get<int>( "global_bounding_volume_id" ) );

        // Compute the bounding boxes of all the volumes.
        _data.volume_bounding_boxes = Kokkos::View<float* [6], MemorySpace>(
            Kokkos::ViewAllocateWithoutInitializing( "volume_bounding_boxes" ),
            volume_ids.size() );
        auto host_boxes = Kokkos::create_mirror_view(
            Kokkos::HostSpace(), _data.volume_bounding_boxes );
        for ( int v = 0; v < _data.numVolume(); ++v )
        {
            BoundingBoxReduce reducer( _data.volume_facets,
                                       _data.volume_offsets, v );
            float box[6];
            Kokkos::parallel_reduce(
                "Picasso::FacetGeometry::VolumeBoundingBox",
                Kokkos::RangePolicy<ExecutionSpace>( exec_space, 0,
                                                     _volume_facet_count[v] ),
                reducer, box );
            for ( int i = 0; i < 6; ++i )
                host_boxes( v, i ) = box[i];
        }
        Kokkos::deep_copy( _data.volume_bounding_boxes, host_boxes );

        // Extract the global bounding box to the host.
        auto global_box = Kokkos::subview(
            host_boxes, _data.global_bounding_volume_id, Kokkos::ALL() );
        for ( int i = 0; i < 6; ++i )
            _global_bounding_box[i] = global_box( i );
    }

    // Given a global volume id get the local volume id.
    int localVolumeId( const int global_id ) const
    {
        return _volume_ids.find( global_id )->second;
    }

    // Given a global surface id get the local surface id.
    int localSurfaceId( const int global_id ) const
    {
        return _surface_ids.find( global_id )->second;
    }

    // Given a local volume id get the number of facets that compose the
    // volume.
    int numVolumeFacet( const int local_id ) const
    {
        return _volume_facet_count[local_id];
    }

    // Given a local surface id get the number of facets that compose the
    // surface.
    int numSurfaceFacet( const int local_id ) const
    {
        return _surface_facet_count[local_id];
    }

    // Get the global bounding box.
    const Kokkos::Array<double, 6>& globalBoundingBox() const
    {
        return _global_bounding_box;
    }

    // Get the geometry data.
    const FacetGeometryData<MemorySpace>& data() const { return _data; }

  private:
    // Put file data on device.
    void putFileDataOnDevice(
        const std::vector<int>& solid_ids,
        const std::vector<int>& solid_facet_counts,
        const std::vector<float>& solid_facets,
        std::unordered_map<int, int>& id_map,
        Kokkos::View<float* [4][3], MemorySpace>& device_facets,
        Kokkos::View<int*, MemorySpace>& device_offsets )
    {
        // Allocate solid data.
        int num_solid = solid_ids.size();
        device_offsets = Kokkos::View<int*, MemorySpace>(
            Kokkos::ViewAllocateWithoutInitializing( "offsets" ), num_solid );

        int num_solid_facet = solid_facets.size() / 9;
        device_facets = Kokkos::View<float* [4][3], MemorySpace>(
            Kokkos::ViewAllocateWithoutInitializing( "facets" ),
            num_solid_facet );

        // Compute the offsets for the facet-to-solid mapping.
        auto host_offsets =
            Kokkos::create_mirror_view( Kokkos::HostSpace(), device_offsets );
        for ( int i = 0; i < num_solid; ++i )
        {
            // Map global solid ids to local ids.
            id_map.emplace( solid_ids[i], i );

            // Compute the offset via inclusive scan.
            host_offsets( i ) =
                ( 0 == i ) ? solid_facet_counts[i]
                           : solid_facet_counts[i] + host_offsets( i - 1 );
        }

        // Build the facets.
        auto host_facets =
            Kokkos::create_mirror_view( Kokkos::HostSpace(), device_facets );
        for ( int f = 0; f < num_solid_facet; ++f )
        {
            // Extract the vertices.
            for ( int v = 0; v < 3; ++v )
                for ( int d = 0; d < 3; ++d )
                    host_facets( f, v, d ) = solid_facets[9 * f + 3 * v + d];

            // Compute the normals.
            float v10[3];
            float v20[3];
            for ( int d = 0; d < 3; ++d )
            {
                v10[d] = host_facets( f, 1, d ) - host_facets( f, 0, d );
                v20[d] = host_facets( f, 2, d ) - host_facets( f, 0, d );
            }
            host_facets( f, 3, 0 ) = v10[1] * v20[2] - v10[2] * v20[1];
            host_facets( f, 3, 1 ) = v10[2] * v20[0] - v10[0] * v20[2];
            host_facets( f, 3, 2 ) = v10[0] * v20[1] - v10[1] * v20[0];

            // Scale them to make it a unit normal.
            float nmag =
                std::sqrt( host_facets( f, 3, 0 ) * host_facets( f, 3, 0 ) +
                           host_facets( f, 3, 1 ) * host_facets( f, 3, 1 ) +
                           host_facets( f, 3, 2 ) * host_facets( f, 3, 2 ) );
            for ( int d = 0; d < 3; ++d )
                host_facets( f, 3, d ) /= nmag;
        }

        // Copy to device.
        Kokkos::deep_copy( device_facets, host_facets );
        Kokkos::deep_copy( device_offsets, host_offsets );
    }

  public:
    // Volume ids - global-to-local mapping.
    std::unordered_map<int, int> _volume_ids;

    // Surface ids - global-to-local mapping.
    std::unordered_map<int, int> _surface_ids;

    // Volume facet counts.
    std::vector<int> _volume_facet_count;

    // Surface facet counts.
    std::vector<int> _surface_facet_count;

    // Global bounding box.
    Kokkos::Array<double, 6> _global_bounding_box;

    // Data.
    FacetGeometryData<MemorySpace> _data;
};

//---------------------------------------------------------------------------//
// Facet Geometry operations.
//---------------------------------------------------------------------------//
namespace FacetGeometryOps
{
//---------------------------------------------------------------------------//
// Project a point x along the given direction, r, and determine if it
// projects to the facet. If returns true, the projection solution is
// valid.
//
// y[0] = projection barycentric coordinate 1
// y[1] = projection barycentric coordinate 2
// y[2] = distance to triangle
template <class FacetView>
KOKKOS_FUNCTION bool pointFacetProjection( const float x[3], const float r[3],
                                           const FacetView& facets, const int f,
                                           float y[3] )
{
    // Build the system of equations to solve for intersection. Fire the ray
    // in the Y direction - this choice is arbitary.
    Mat3<float> A;
    Vec3<float> b;
    for ( int i = 0; i < 3; ++i )
    {
        A( i, 0 ) = facets( f, 1, i ) - facets( f, 0, i );
        A( i, 1 ) = facets( f, 2, i ) - facets( f, 0, i );
        A( i, 2 ) = -r[i];
        b( i ) = x[i] - facets( f, 0, i );
    }

    // Check the determinant of the matrix. If zero then the ray is parallel
    // so no intersection.
    auto det_A = !A;
    if ( 0.0 == det_A )
        return false;

    // Solve the system.
    VecView3<float> y_view( y, 1 );
    y_view = A ^ b;

    // Check the solution for inclusion in the triangle.
    return ( y[0] >= 0.0 && y[1] >= 0.0 && y[0] + y[1] <= 1.0 );
}

//---------------------------------------------------------------------------//
// Fire a ray from point x along the given direction, r, and determine if it
// intersects the facet.
template <class FacetView>
KOKKOS_FUNCTION bool rayFacetIntersect( const float x[3], const float r[3],
                                        const FacetView& facets, const int f )
{
    // Project the point and check the distance.
    float y[3];
    auto projects = pointFacetProjection( x, r, facets, f, y );
    return projects && ( y[2] > 0.0 );
}

//---------------------------------------------------------------------------//
// Compute the signed distance from a point to the plane defined by a facet.
template <class FacetView>
KOKKOS_FUNCTION float
distanceToFacetPlane( const float x[3], const FacetView& facets, const int f )
{
    return ( x[0] - facets( f, 0, 0 ) ) * facets( f, 3, 0 ) +
           ( x[1] - facets( f, 0, 1 ) ) * facets( f, 3, 1 ) +
           ( x[2] - facets( f, 0, 2 ) ) * facets( f, 3, 2 );
}

//---------------------------------------------------------------------------//
// Determine if a point is in a volume represented by a view of facets.
template <class FacetView>
KOKKOS_FUNCTION bool pointInVolume( const float x[3],
                                    const FacetView& volume_facets )
{
    // The choice of ray direction is arbitrary so generate a random one. This
    // could potentially help with robustness as floating point noise may
    // avoid edge and vertex intersections which could lead to multiple
    // positive intersections and therefore an incorrect point-in-volume
    // determination. The facet geometry has no connectivity so it is
    // difficult to resolve multiple intersections without accumulating the
    // intersection points and checking for duplicates within some tolerance.
    //
    // Note: Duan has indicated that doing tests with 3 different random rays
    // has been enough to be robust as one is likely to pass out of the 3 if
    // is a true intersection.
    using rand_type =
        Kokkos::Random_XorShift64<typename FacetView::device_type>;
    rand_type rng( 0 );
    float r[3] = { Kokkos::rand<rand_type, float>::draw( rng ),
                   Kokkos::rand<rand_type, float>::draw( rng ),
                   Kokkos::rand<rand_type, float>::draw( rng ) };
    float r_mag_inv = 1.0 / sqrt( r[0] * r[0] + r[1] * r[1] + r[2] * r[2] );
    for ( int d = 0; d < 3; ++d )
        r[d] *= r_mag_inv;

    // Fire rays through each facet and count intersections. If an
    // odd number of intersections, the point is in the volume. This works for
    // convex and non-convex volumes.
    int count = 0;
    for ( std::size_t f = 0; f < volume_facets.extent( 0 ); ++f )
        if ( rayFacetIntersect( x, r, volume_facets, f ) )
            ++count;
    return ( 1 == count % 2 );
}

//---------------------------------------------------------------------------//
// Given a point determine the volume in the given facet geometry in which it
// is located.If it is in the implicit complement, return -1. If it is outside
// of the entire domain, return -2;
template <class MemorySpace>
KOKKOS_FUNCTION int locatePoint( const float x[3],
                                 const FacetGeometryData<MemorySpace>& geom )
{
    // Get the global bounding volume id.
    int gbv = geom.global_bounding_volume_id;

    // Start by checking that the point is in the global bounding volume.
    if ( geom.volume_bounding_boxes( gbv, 0 ) <= x[0] &&
         geom.volume_bounding_boxes( gbv, 1 ) <= x[1] &&
         geom.volume_bounding_boxes( gbv, 2 ) <= x[2] &&
         geom.volume_bounding_boxes( gbv, 3 ) >= x[0] &&
         geom.volume_bounding_boxes( gbv, 4 ) >= x[1] &&
         geom.volume_bounding_boxes( gbv, 5 ) >= x[2] )
    {
        // Check each volume except the global bounding volume for point
        // inclusion.
        for ( int v = 0; v < geom.numVolume(); ++v )
        {
            if ( v != gbv )
            {
                // First check if the point is in the axis-aligned
                // bounding box of the volume.
                if ( geom.volume_bounding_boxes( v, 0 ) <= x[0] &&
                     geom.volume_bounding_boxes( v, 1 ) <= x[1] &&
                     geom.volume_bounding_boxes( v, 2 ) <= x[2] &&
                     geom.volume_bounding_boxes( v, 3 ) >= x[0] &&
                     geom.volume_bounding_boxes( v, 4 ) >= x[1] &&
                     geom.volume_bounding_boxes( v, 5 ) >= x[2] )
                {
                    // If in the bounding box, check against each volume
                    // facet for point inclusion.
                    if ( pointInVolume( x, geom.volumeFacets( v ) ) )
                    {
                        return v;
                    }
                }
            }
        }

        // If the point was not in any volume it is in the implicit
        // complement so return -1.
        return -1;
    }

    // Otherwise point is outside of the global domain including the
    // implicit complement so return -2.
    return -2;
}

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

} // end namespace FacetGeometryOps

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

} // end namespace Picasso

#endif // end PICASSO_FACETGEOMETRY_HPP