// Copyright (c) 2017-2020, The Monero Project
//
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification, are
// permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of
//    conditions and the following disclaimer.
//
// 2. 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.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be
//    used to endorse or promote products derived from this software without specific
//    prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
// THE COPYRIGHT HOLDER 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.

// Implements the Bulletproofs+ prover and verifier algorithms
//
// Preprint: https://eprint.iacr.org/2020/735, version 17 Jun 2020
//
// NOTE ON NOTATION:
//  In the signature constructions used in Monero, commitments to zero are treated as
//      public keys against the curve group generator `G`. This means that amount
//      commitments must use another generator `H` for values in order to show balance.
//  The result is that the roles of `g` and `h` in the preprint are effectively swapped
//      in this code, taking on the roles of `H` and `G`, respectively. Read carefully!

#include <stdlib.h>
#include <boost/thread/mutex.hpp>
#include <boost/thread/lock_guard.hpp>
#include "misc_log_ex.h"
#include "span.h"
#include "cryptonote_config.h"
extern "C"
{
#include "crypto/crypto-ops.h"
}
#include "rctOps.h"
#include "multiexp.h"
#include "bulletproofs_plus.h"

#undef MONERO_DEFAULT_LOG_CATEGORY
#define MONERO_DEFAULT_LOG_CATEGORY "bulletproof_plus"

#define STRAUS_SIZE_LIMIT 232
#define PIPPENGER_SIZE_LIMIT 0

namespace rct
{
    // Vector functions
    static rct::key vector_exponent(const rct::keyV &a, const rct::keyV &b);
    static rct::keyV vector_of_scalar_powers(const rct::key &x, size_t n);

    // Proof bounds
    static constexpr size_t maxN = 64; // maximum number of bits in range
    static constexpr size_t maxM = BULLETPROOF_PLUS_MAX_OUTPUTS; // maximum number of outputs to aggregate into a single proof

    // Cached public generators
    static ge_p3 Hi_p3[maxN*maxM], Gi_p3[maxN*maxM];
    static std::shared_ptr<straus_cached_data> straus_HiGi_cache;
    static std::shared_ptr<pippenger_cached_data> pippenger_HiGi_cache;

    // Useful scalar constants
    static const constexpr rct::key ZERO = { {0x00, 0x00, 0x00,0x00 , 0x00, 0x00, 0x00,0x00 , 0x00, 0x00, 0x00,0x00 , 0x00, 0x00, 0x00,0x00 , 0x00, 0x00, 0x00,0x00 , 0x00, 0x00, 0x00,0x00 , 0x00, 0x00, 0x00,0x00 , 0x00, 0x00, 0x00,0x00  } }; // 0
    static const constexpr rct::key ONE = { {0x01, 0x00, 0x00,0x00 , 0x00, 0x00, 0x00,0x00 , 0x00, 0x00, 0x00,0x00 , 0x00, 0x00, 0x00,0x00 , 0x00, 0x00, 0x00,0x00 , 0x00, 0x00, 0x00,0x00 , 0x00, 0x00, 0x00,0x00 , 0x00, 0x00, 0x00,0x00  } }; // 1
    static const constexpr rct::key TWO = { {0x02, 0x00, 0x00,0x00 , 0x00, 0x00, 0x00,0x00 , 0x00, 0x00, 0x00,0x00 , 0x00, 0x00, 0x00,0x00 , 0x00, 0x00, 0x00,0x00 , 0x00, 0x00, 0x00,0x00 , 0x00, 0x00, 0x00,0x00 , 0x00, 0x00, 0x00,0x00  } }; // 2
    static const constexpr rct::key MINUS_ONE = { { 0xec, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58, 0xd6, 0x9c, 0xf7, 0xa2, 0xde, 0xf9, 0xde, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10 } }; // -1
    static const constexpr rct::key MINUS_INV_EIGHT = { { 0x74, 0xa4, 0x19, 0x7a, 0xf0, 0x7d, 0x0b, 0xf7, 0x05, 0xc2, 0xda, 0x25, 0x2b, 0x5c, 0x0b, 0x0d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a } }; // -(8**(-1))
    static rct::key TWO_SIXTY_FOUR_MINUS_ONE; // 2**64 - 1

    // Initial transcript hash
    static rct::key initial_transcript;

    static boost::mutex init_mutex;

    // Use the generator caches to compute a multiscalar multiplication
    static inline rct::key multiexp(const std::vector<MultiexpData> &data, size_t HiGi_size)
    {
        if (HiGi_size > 0)
        {
            static_assert(232 <= STRAUS_SIZE_LIMIT, "Straus in precalc mode can only be calculated till STRAUS_SIZE_LIMIT");
            return HiGi_size <= 232 && data.size() == HiGi_size ? straus(data, straus_HiGi_cache, 0) : pippenger(data, pippenger_HiGi_cache, HiGi_size, get_pippenger_c(data.size()));
        }
        else
        {
            return data.size() <= 95 ? straus(data, NULL, 0) : pippenger(data, NULL, 0, get_pippenger_c(data.size()));
        }
    }

    // Confirm that a scalar is properly reduced
    static inline bool is_reduced(const rct::key &scalar)
    {
        return sc_check(scalar.bytes) == 0;
    }

    // Use hashed values to produce indexed public generators
    static ge_p3 get_exponent(const rct::key &base, size_t idx)
    {
        std::string hashed = std::string((const char*)base.bytes, sizeof(base)) + config::HASH_KEY_BULLETPROOF_PLUS_EXPONENT + tools::get_varint_data(idx);
        rct::key generator;
        ge_p3 generator_p3;
        rct::hash_to_p3(generator_p3, rct::hash2rct(crypto::cn_fast_hash(hashed.data(), hashed.size())));
        ge_p3_tobytes(generator.bytes, &generator_p3);
        CHECK_AND_ASSERT_THROW_MES(!(generator == rct::identity()), "Exponent is point at infinity");
        return generator_p3;
    }

    // Construct public generators
    static void init_exponents()
    {
        boost::lock_guard<boost::mutex> lock(init_mutex);

        // Only needs to be done once
        static bool init_done = false;
        if (init_done)
            return;

        std::vector<MultiexpData> data;
        data.reserve(maxN*maxM*2);
        for (size_t i = 0; i < maxN*maxM; ++i)
        {
            Hi_p3[i] = get_exponent(rct::H, i * 2);
            Gi_p3[i] = get_exponent(rct::H, i * 2 + 1);

            data.push_back({rct::zero(), Gi_p3[i]});
            data.push_back({rct::zero(), Hi_p3[i]});
        }

        straus_HiGi_cache = straus_init_cache(data, STRAUS_SIZE_LIMIT);
        pippenger_HiGi_cache = pippenger_init_cache(data, 0, PIPPENGER_SIZE_LIMIT);

        // Compute 2**64 - 1 for later use in simplifying verification
        TWO_SIXTY_FOUR_MINUS_ONE = TWO;
        for (size_t i = 0; i < 6; i++)
        {
            sc_mul(TWO_SIXTY_FOUR_MINUS_ONE.bytes, TWO_SIXTY_FOUR_MINUS_ONE.bytes, TWO_SIXTY_FOUR_MINUS_ONE.bytes);
        }
        sc_sub(TWO_SIXTY_FOUR_MINUS_ONE.bytes, TWO_SIXTY_FOUR_MINUS_ONE.bytes, ONE.bytes);

        // Generate the initial Fiat-Shamir transcript hash, which is constant across all proofs
        const std::string domain_separator(config::HASH_KEY_BULLETPROOF_PLUS_TRANSCRIPT);
        ge_p3 initial_transcript_p3;
        rct::hash_to_p3(initial_transcript_p3, rct::hash2rct(crypto::cn_fast_hash(domain_separator.data(), domain_separator.size())));
        ge_p3_tobytes(initial_transcript.bytes, &initial_transcript_p3);

        init_done = true;
    }

    // Given two scalar arrays, construct a vector pre-commitment:
    //
    // a = (a_0, ..., a_{n-1})
    // b = (b_0, ..., b_{n-1})
    //
    // Outputs a_0*Gi_0 + ... + a_{n-1}*Gi_{n-1} +
    //         b_0*Hi_0 + ... + b_{n-1}*Hi_{n-1}
    static rct::key vector_exponent(const rct::keyV &a, const rct::keyV &b)
    {
        CHECK_AND_ASSERT_THROW_MES(a.size() == b.size(), "Incompatible sizes of a and b");
        CHECK_AND_ASSERT_THROW_MES(a.size() <= maxN*maxM, "Incompatible sizes of a and maxN");

        std::vector<MultiexpData> multiexp_data;
        multiexp_data.reserve(a.size()*2);
        for (size_t i = 0; i < a.size(); ++i)
        {
            multiexp_data.emplace_back(a[i], Gi_p3[i]);
            multiexp_data.emplace_back(b[i], Hi_p3[i]);
        }
        return multiexp(multiexp_data, 2 * a.size());
    }

    // Helper function used to compute the L and R terms used in the inner-product round function
    static rct::key compute_LR(size_t size, const rct::key &y, const std::vector<ge_p3> &G, size_t G0, const std::vector<ge_p3> &H, size_t H0, const rct::keyV &a, size_t a0, const rct::keyV &b, size_t b0, const rct::key &c, const rct::key &d)
    {
        CHECK_AND_ASSERT_THROW_MES(size + G0 <= G.size(), "Incompatible size for G");
        CHECK_AND_ASSERT_THROW_MES(size + H0 <= H.size(), "Incompatible size for H");
        CHECK_AND_ASSERT_THROW_MES(size + a0 <= a.size(), "Incompatible size for a");
        CHECK_AND_ASSERT_THROW_MES(size + b0 <= b.size(), "Incompatible size for b");
        CHECK_AND_ASSERT_THROW_MES(size <= maxN*maxM, "size is too large");

        std::vector<MultiexpData> multiexp_data;
        multiexp_data.resize(size*2 + 2);
        rct::key temp;
        for (size_t i = 0; i < size; ++i)
        {
            sc_mul(temp.bytes, a[a0+i].bytes, y.bytes);
            sc_mul(multiexp_data[i*2].scalar.bytes, temp.bytes, INV_EIGHT.bytes);
            multiexp_data[i*2].point = G[G0+i];

            sc_mul(multiexp_data[i*2+1].scalar.bytes, b[b0+i].bytes, INV_EIGHT.bytes);
            multiexp_data[i*2+1].point = H[H0+i];
        }

        sc_mul(multiexp_data[2*size].scalar.bytes, c.bytes, INV_EIGHT.bytes);
        ge_p3 H_p3;
        ge_frombytes_vartime(&H_p3, rct::H.bytes);
        multiexp_data[2*size].point = H_p3;

        sc_mul(multiexp_data[2*size+1].scalar.bytes, d.bytes, INV_EIGHT.bytes);
        ge_p3 G_p3;
        ge_frombytes_vartime(&G_p3, rct::G.bytes);
        multiexp_data[2*size+1].point = G_p3;

        return multiexp(multiexp_data, 0);
    }

    // Given a scalar, construct a vector of its powers:
    //
    // Output (1,x,x**2,...,x**{n-1})
    static rct::keyV vector_of_scalar_powers(const rct::key &x, size_t n)
    {
        CHECK_AND_ASSERT_THROW_MES(n != 0, "Need n > 0");

        rct::keyV res(n);
        res[0] = rct::identity();
        if (n == 1)
            return res;
        res[1] = x;
        for (size_t i = 2; i < n; ++i)
        {
            sc_mul(res[i].bytes, res[i-1].bytes, x.bytes);
        }
        return res;
    }

    // Given a scalar, construct the sum of its powers from 2 to n (where n is a power of 2):
    //
    // Output x**2 + x**4 + x**6 + ... + x**n
    static rct::key sum_of_even_powers(const rct::key &x, size_t n)
    {
        CHECK_AND_ASSERT_THROW_MES((n & (n - 1)) == 0, "Need n to be a power of 2");
        CHECK_AND_ASSERT_THROW_MES(n != 0, "Need n > 0");

        rct::key x1 = copy(x);
        sc_mul(x1.bytes, x1.bytes, x1.bytes);

        rct::key res = copy(x1);
        while (n > 2)
        {
            sc_muladd(res.bytes, x1.bytes, res.bytes, res.bytes);
            sc_mul(x1.bytes, x1.bytes, x1.bytes);
            n /= 2;
        }

        return res;
    }

    // Given a scalar, return the sum of its powers from 1 to n
    //
    // Output x**1 + x**2 + x**3 + ... + x**n
    static rct::key sum_of_scalar_powers(const rct::key &x, size_t n)
    {
        CHECK_AND_ASSERT_THROW_MES(n != 0, "Need n > 0");

        rct::key res = ONE;
        if (n == 1)
            return res;

        n += 1;
        rct::key x1 = copy(x);

        const bool is_power_of_2 = (n & (n - 1)) == 0;
        if (is_power_of_2)
        {
            sc_add(res.bytes, res.bytes, x1.bytes);
            while (n > 2)
            {
                sc_mul(x1.bytes, x1.bytes, x1.bytes);
                sc_muladd(res.bytes, x1.bytes, res.bytes, res.bytes);
                n /= 2;
            }
        }
        else
        {
            rct::key prev = x1;
            for (size_t i = 1; i < n; ++i)
            {
                if (i > 1)
                    sc_mul(prev.bytes, prev.bytes, x1.bytes);
                sc_add(res.bytes, res.bytes, prev.bytes);
            }
        }
        sc_sub(res.bytes, res.bytes, ONE.bytes);

        return res;
    }

    // Given two scalar arrays, construct the weighted inner product against another scalar
    //
    // Output a_0*b_0*y**1 + a_1*b_1*y**2 + ... + a_{n-1}*b_{n-1}*y**n
    static rct::key weighted_inner_product(const epee::span<const rct::key> &a, const epee::span<const rct::key> &b, const rct::key &y)
    {
        CHECK_AND_ASSERT_THROW_MES(a.size() == b.size(), "Incompatible sizes of a and b");
        rct::key res = rct::zero();
        rct::key y_power = ONE;
        rct::key temp;
        for (size_t i = 0; i < a.size(); ++i)
        {
            sc_mul(temp.bytes, a[i].bytes, b[i].bytes);
            sc_mul(y_power.bytes, y_power.bytes, y.bytes);
            sc_muladd(res.bytes, temp.bytes, y_power.bytes, res.bytes);
        }
        return res;
    }

    static rct::key weighted_inner_product(const rct::keyV &a, const epee::span<const rct::key> &b, const rct::key &y)
    {
        CHECK_AND_ASSERT_THROW_MES(a.size() == b.size(), "Incompatible sizes of a and b");
        rct::key res = rct::zero();
        rct::key y_power = ONE;
        rct::key temp;
        for (size_t i = 0; i < a.size(); ++i)
        {
            sc_mul(temp.bytes, a[i].bytes, b[i].bytes);
            sc_mul(y_power.bytes, y_power.bytes, y.bytes);
            sc_muladd(res.bytes, temp.bytes, y_power.bytes, res.bytes);
        }
        return res;
    }

    // Fold inner-product point vectors
    static void hadamard_fold(std::vector<ge_p3> &v, const rct::key &a, const rct::key &b)
    {
        CHECK_AND_ASSERT_THROW_MES((v.size() & 1) == 0, "Vector size should be even");
        const size_t sz = v.size() / 2;
        for (size_t n = 0; n < sz; ++n)
        {
            ge_dsmp c[2];
            ge_dsm_precomp(c[0], &v[n]);
            ge_dsm_precomp(c[1], &v[sz + n]);
            ge_double_scalarmult_precomp_vartime2_p3(&v[n], a.bytes, c[0], b.bytes, c[1]);
        }
        v.resize(sz);
    }

    // Add vectors componentwise
    static rct::keyV vector_add(const rct::keyV &a, const rct::keyV &b)
    {
        CHECK_AND_ASSERT_THROW_MES(a.size() == b.size(), "Incompatible sizes of a and b");
        rct::keyV res(a.size());
        for (size_t i = 0; i < a.size(); ++i)
        {
            sc_add(res[i].bytes, a[i].bytes, b[i].bytes);
        }
        return res;
    }

    // Add a scalar to all elements of a vector
    static rct::keyV vector_add(const rct::keyV &a, const rct::key &b)
    {
        rct::keyV res(a.size());
        for (size_t i = 0; i < a.size(); ++i)
        {
            sc_add(res[i].bytes, a[i].bytes, b.bytes);
        }
        return res;
    }

    // Subtract a scalar from all elements of a vector
    static rct::keyV vector_subtract(const rct::keyV &a, const rct::key &b)
    {
        rct::keyV res(a.size());
        for (size_t i = 0; i < a.size(); ++i)
        {
            sc_sub(res[i].bytes, a[i].bytes, b.bytes);
        }
        return res;
    }

    // Multiply a scalar by all elements of a vector
    static rct::keyV vector_scalar(const epee::span<const rct::key> &a, const rct::key &x)
    {
        rct::keyV res(a.size());
        for (size_t i = 0; i < a.size(); ++i)
        {
            sc_mul(res[i].bytes, a[i].bytes, x.bytes);
        }
        return res;
    }

    // Inversion helper function
    static rct::key sm(rct::key y, int n, const rct::key &x)
    {
        while (n--)
            sc_mul(y.bytes, y.bytes, y.bytes);
        sc_mul(y.bytes, y.bytes, x.bytes);
        return y;
    }

    // Compute the inverse of a nonzero
    static rct::key invert(const rct::key &x)
    {
        CHECK_AND_ASSERT_THROW_MES(!(x == ZERO), "Cannot invert zero!");
        rct::key _1, _10, _100, _11, _101, _111, _1001, _1011, _1111;

        _1 = x;
        sc_mul(_10.bytes, _1.bytes, _1.bytes);
        sc_mul(_100.bytes, _10.bytes, _10.bytes);
        sc_mul(_11.bytes, _10.bytes, _1.bytes);
        sc_mul(_101.bytes, _10.bytes, _11.bytes);
        sc_mul(_111.bytes, _10.bytes, _101.bytes);
        sc_mul(_1001.bytes, _10.bytes, _111.bytes);
        sc_mul(_1011.bytes, _10.bytes, _1001.bytes);
        sc_mul(_1111.bytes, _100.bytes, _1011.bytes);

        rct::key inv;
        sc_mul(inv.bytes, _1111.bytes, _1.bytes);

        inv = sm(inv, 123 + 3, _101);
        inv = sm(inv, 2 + 2, _11);
        inv = sm(inv, 1 + 4, _1111);
        inv = sm(inv, 1 + 4, _1111);
        inv = sm(inv, 4, _1001);
        inv = sm(inv, 2, _11);
        inv = sm(inv, 1 + 4, _1111);
        inv = sm(inv, 1 + 3, _101);
        inv = sm(inv, 3 + 3, _101);
        inv = sm(inv, 3, _111);
        inv = sm(inv, 1 + 4, _1111);
        inv = sm(inv, 2 + 3, _111);
        inv = sm(inv, 2 + 2, _11);
        inv = sm(inv, 1 + 4, _1011);
        inv = sm(inv, 2 + 4, _1011);
        inv = sm(inv, 6 + 4, _1001);
        inv = sm(inv, 2 + 2, _11);
        inv = sm(inv, 3 + 2, _11);
        inv = sm(inv, 3 + 2, _11);
        inv = sm(inv, 1 + 4, _1001);
        inv = sm(inv, 1 + 3, _111);
        inv = sm(inv, 2 + 4, _1111);
        inv = sm(inv, 1 + 4, _1011);
        inv = sm(inv, 3, _101);
        inv = sm(inv, 2 + 4, _1111);
        inv = sm(inv, 3, _101);
        inv = sm(inv, 1 + 2, _11);

        return inv;
    }

    // Invert a batch of scalars, all of which _must_ be nonzero
    static rct::keyV invert(rct::keyV x)
    {
        rct::keyV scratch;
        scratch.reserve(x.size());

        rct::key acc = rct::identity();
        for (size_t n = 0; n < x.size(); ++n)
        {
            CHECK_AND_ASSERT_THROW_MES(!(x[n] == ZERO), "Cannot invert zero!");
            scratch.push_back(acc);
            if (n == 0)
                acc = x[0];
            else
                sc_mul(acc.bytes, acc.bytes, x[n].bytes);
        }

        acc = invert(acc);

        rct::key tmp;
        for (int i = x.size(); i-- > 0; )
        {
            sc_mul(tmp.bytes, acc.bytes, x[i].bytes);
            sc_mul(x[i].bytes, acc.bytes, scratch[i].bytes);
            acc = tmp;
        }

        return x;
    }

    // Compute the slice of a vector
    static epee::span<const rct::key> slice(const rct::keyV &a, size_t start, size_t stop)
    {
        CHECK_AND_ASSERT_THROW_MES(start < a.size(), "Invalid start index");
        CHECK_AND_ASSERT_THROW_MES(stop <= a.size(), "Invalid stop index");
        CHECK_AND_ASSERT_THROW_MES(start < stop, "Invalid start/stop indices");
        return epee::span<const rct::key>(&a[start], stop - start);
    }

    // Update the transcript
    static rct::key transcript_update(rct::key &transcript, const rct::key &update_0)
    {
        rct::key data[2];
        data[0] = transcript;
        data[1] = update_0;
        rct::hash_to_scalar(transcript, data, sizeof(data));
        return transcript;
    }

    static rct::key transcript_update(rct::key &transcript, const rct::key &update_0, const rct::key &update_1)
    {
        rct::key data[3];
        data[0] = transcript;
        data[1] = update_0;
        data[2] = update_1;
        rct::hash_to_scalar(transcript, data, sizeof(data));
        return transcript;
    }

    // Given a value v [0..2**N) and a mask gamma, construct a range proof
    BulletproofPlus bulletproof_plus_PROVE(const rct::key &sv, const rct::key &gamma)
    {
        return bulletproof_plus_PROVE(rct::keyV(1, sv), rct::keyV(1, gamma));
    }

    BulletproofPlus bulletproof_plus_PROVE(uint64_t v, const rct::key &gamma)
    {
        return bulletproof_plus_PROVE(std::vector<uint64_t>(1, v), rct::keyV(1, gamma));
    }

    // Given a set of values v [0..2**N) and masks gamma, construct a range proof
    BulletproofPlus bulletproof_plus_PROVE(const rct::keyV &sv, const rct::keyV &gamma)
    {
        // Sanity check on inputs
        CHECK_AND_ASSERT_THROW_MES(sv.size() == gamma.size(), "Incompatible sizes of sv and gamma");
        CHECK_AND_ASSERT_THROW_MES(!sv.empty(), "sv is empty");
        for (const rct::key &sve: sv)
            CHECK_AND_ASSERT_THROW_MES(is_reduced(sve), "Invalid sv input");
        for (const rct::key &g: gamma)
            CHECK_AND_ASSERT_THROW_MES(is_reduced(g), "Invalid gamma input");

        init_exponents();

        // Useful proof bounds
        //
        // N: number of bits in each range (here, 64)
        // logN: base-2 logarithm
        // M: first power of 2 greater than or equal to the number of range proofs to aggregate
        // logM: base-2 logarithm
        constexpr size_t logN = 6; // log2(64)
        constexpr size_t N = 1<<logN;
        size_t M, logM;
        for (logM = 0; (M = 1<<logM) <= maxM && M < sv.size(); ++logM);
        CHECK_AND_ASSERT_THROW_MES(M <= maxM, "sv/gamma are too large");
        const size_t logMN = logM + logN;
        const size_t MN = M * N;

        rct::keyV V(sv.size());
        rct::keyV aL(MN), aR(MN);
        rct::keyV aL8(MN), aR8(MN);
        rct::key temp;
        rct::key temp2;

        // Prepare output commitments and offset by a factor of 8**(-1)
        //
        // This offset is applied to other group elements as well;
        //  it allows us to apply a multiply-by-8 operation in the verifier efficiently
        //  to ensure that the resulting group elements are in the prime-order point subgroup
        //  and avoid much more constly multiply-by-group-order operations.
        for (size_t i = 0; i < sv.size(); ++i)
        {
            rct::key gamma8, sv8;
            sc_mul(gamma8.bytes, gamma[i].bytes, INV_EIGHT.bytes);
            sc_mul(sv8.bytes, sv[i].bytes, INV_EIGHT.bytes);
            rct::addKeys2(V[i], gamma8, sv8, rct::H);
        }

        // Decompose values
        //
        // Note that this effectively pads the set to a power of 2, which is required for the inner-product argument later.
        for (size_t j = 0; j < M; ++j)
        {
            for (size_t i = N; i-- > 0; )
            {
                if (j < sv.size() && (sv[j][i/8] & (((uint64_t)1)<<(i%8))))
                {
                    aL[j*N+i] = rct::identity();
                    aL8[j*N+i] = INV_EIGHT;
                    aR[j*N+i] = aR8[j*N+i] = rct::zero();
                }
                else
                {
                    aL[j*N+i] = aL8[j*N+i] = rct::zero();
                    aR[j*N+i] = MINUS_ONE;
                    aR8[j*N+i] = MINUS_INV_EIGHT;
                }
            }
        }

try_again:
        // This is a Fiat-Shamir transcript
        rct::key transcript = copy(initial_transcript);
        transcript = transcript_update(transcript, rct::hash_to_scalar(V));

        // A
        rct::key alpha = rct::skGen();
        rct::key pre_A = vector_exponent(aL8, aR8);
        rct::key A;
        sc_mul(temp.bytes, alpha.bytes, INV_EIGHT.bytes);
        rct::addKeys(A, pre_A, rct::scalarmultBase(temp));

        // Challenges
        rct::key y = transcript_update(transcript, A);
        if (y == rct::zero())
        {
            MINFO("y is 0, trying again");
            goto try_again;
        }
        rct::key z = transcript = rct::hash_to_scalar(y);
        if (z == rct::zero())
        {
            MINFO("z is 0, trying again");
            goto try_again;
        }
        rct::key z_squared;
        sc_mul(z_squared.bytes, z.bytes, z.bytes);

        // Windowed vector
        // d[j*N+i] = z**(2*(j+1)) * 2**i
        //
        // We compute this iteratively in order to reduce scalar operations.
        rct::keyV d(MN, rct::zero());
        d[0] = z_squared;
        for (size_t i = 1; i < N; i++)
        {
            sc_mul(d[i].bytes, d[i-1].bytes, TWO.bytes);
        }

        for (size_t j = 1; j < M; j++)
        {
            for (size_t i = 0; i < N; i++)
            {
                sc_mul(d[j*N+i].bytes, d[(j-1)*N+i].bytes, z_squared.bytes);
            }
        }

        rct::keyV y_powers = vector_of_scalar_powers(y, MN+2);

        // Prepare inner product terms
        rct::keyV aL1 = vector_subtract(aL, z);

        rct::keyV aR1 = vector_add(aR, z);
        rct::keyV d_y(MN);
        for (size_t i = 0; i < MN; i++)
        {
            sc_mul(d_y[i].bytes, d[i].bytes, y_powers[MN-i].bytes);
        }
        aR1 = vector_add(aR1, d_y);

        rct::key alpha1 = alpha;
        temp = ONE;
        for (size_t j = 0; j < sv.size(); j++)
        {
            sc_mul(temp.bytes, temp.bytes, z_squared.bytes);
            sc_mul(temp2.bytes, y_powers[MN+1].bytes, temp.bytes);
            sc_mul(temp2.bytes, temp2.bytes, gamma[j].bytes);
            sc_add(alpha1.bytes, alpha1.bytes, temp2.bytes);
        }

        // These are used in the inner product rounds
        size_t nprime = MN;
        std::vector<ge_p3> Gprime(MN);
        std::vector<ge_p3> Hprime(MN);
        rct::keyV aprime(MN);
        rct::keyV bprime(MN);

        const rct::key yinv = invert(y);
        rct::keyV yinvpow(MN);
        yinvpow[0] = ONE;
        for (size_t i = 0; i < MN; ++i)
        {
            Gprime[i] = Gi_p3[i];
            Hprime[i] = Hi_p3[i];
            if (i > 0)
            {
                sc_mul(yinvpow[i].bytes, yinvpow[i-1].bytes, yinv.bytes);
            }
            aprime[i] = aL1[i];
            bprime[i] = aR1[i];
        }
        rct::keyV L(logMN);
        rct::keyV R(logMN);
        int round = 0;

        // Inner-product rounds
        while (nprime > 1)
        {
            nprime /= 2;

            rct::key cL = weighted_inner_product(slice(aprime, 0, nprime), slice(bprime, nprime, bprime.size()), y);
            rct::key cR = weighted_inner_product(vector_scalar(slice(aprime, nprime, aprime.size()), y_powers[nprime]), slice(bprime, 0, nprime), y);

            rct::key dL = rct::skGen();
            rct::key dR = rct::skGen();

            L[round] = compute_LR(nprime, yinvpow[nprime], Gprime, nprime, Hprime, 0, aprime, 0, bprime, nprime, cL, dL);
            R[round] = compute_LR(nprime, y_powers[nprime], Gprime, 0, Hprime, nprime, aprime, nprime, bprime, 0, cR, dR);

            const rct::key challenge = transcript_update(transcript, L[round], R[round]);
            if (challenge == rct::zero())
            {
                MINFO("challenge is 0, trying again");
                goto try_again;
            }

            const rct::key challenge_inv = invert(challenge);

            sc_mul(temp.bytes, yinvpow[nprime].bytes, challenge.bytes);
            hadamard_fold(Gprime, challenge_inv, temp);
            hadamard_fold(Hprime, challenge, challenge_inv);

            sc_mul(temp.bytes, challenge_inv.bytes, y_powers[nprime].bytes);
            aprime = vector_add(vector_scalar(slice(aprime, 0, nprime), challenge), vector_scalar(slice(aprime, nprime, aprime.size()), temp));
            bprime = vector_add(vector_scalar(slice(bprime, 0, nprime), challenge_inv), vector_scalar(slice(bprime, nprime, bprime.size()), challenge));

            rct::key challenge_squared;
            sc_mul(challenge_squared.bytes, challenge.bytes, challenge.bytes);
            rct::key challenge_squared_inv = invert(challenge_squared);
            sc_muladd(alpha1.bytes, dL.bytes, challenge_squared.bytes, alpha1.bytes);
            sc_muladd(alpha1.bytes, dR.bytes, challenge_squared_inv.bytes, alpha1.bytes);

            ++round;
        }

        // Final round computations
        rct::key r = rct::skGen();
        rct::key s = rct::skGen();
        rct::key d_ = rct::skGen();
        rct::key eta = rct::skGen();

        std::vector<MultiexpData> A1_data;
        A1_data.reserve(4);
        A1_data.resize(4);

        sc_mul(A1_data[0].scalar.bytes, r.bytes, INV_EIGHT.bytes);
        A1_data[0].point = Gprime[0];

        sc_mul(A1_data[1].scalar.bytes, s.bytes, INV_EIGHT.bytes);
        A1_data[1].point = Hprime[0];

        sc_mul(A1_data[2].scalar.bytes, d_.bytes, INV_EIGHT.bytes);
        ge_p3 G_p3;
        ge_frombytes_vartime(&G_p3, rct::G.bytes);
        A1_data[2].point = G_p3;

        sc_mul(temp.bytes, r.bytes, y.bytes);
        sc_mul(temp.bytes, temp.bytes, bprime[0].bytes);
        sc_mul(temp2.bytes, s.bytes, y.bytes);
        sc_mul(temp2.bytes, temp2.bytes, aprime[0].bytes);
        sc_add(temp.bytes, temp.bytes, temp2.bytes);
        sc_mul(A1_data[3].scalar.bytes, temp.bytes, INV_EIGHT.bytes);
        ge_p3 H_p3;
        ge_frombytes_vartime(&H_p3, rct::H.bytes);
        A1_data[3].point = H_p3;

        rct::key A1 = multiexp(A1_data, 0);

        sc_mul(temp.bytes, r.bytes, y.bytes);
        sc_mul(temp.bytes, temp.bytes, s.bytes);
        sc_mul(temp.bytes, temp.bytes, INV_EIGHT.bytes);
        sc_mul(temp2.bytes, eta.bytes, INV_EIGHT.bytes);
        rct::key B;
        rct::addKeys2(B, temp2, temp, rct::H);

        rct::key e = transcript_update(transcript, A1, B);
        if (e == rct::zero())
        {
            MINFO("e is 0, trying again");
            goto try_again;
        }
        rct::key e_squared;
        sc_mul(e_squared.bytes, e.bytes, e.bytes);

        rct::key r1;
        sc_muladd(r1.bytes, aprime[0].bytes, e.bytes, r.bytes);

        rct::key s1;
        sc_muladd(s1.bytes, bprime[0].bytes, e.bytes, s.bytes);

        rct::key d1;
        sc_muladd(d1.bytes, d_.bytes, e.bytes, eta.bytes);
        sc_muladd(d1.bytes, alpha1.bytes, e_squared.bytes, d1.bytes);

        return BulletproofPlus(std::move(V), A, A1, B, r1, s1, d1, std::move(L), std::move(R));
    }

    BulletproofPlus bulletproof_plus_PROVE(const std::vector<uint64_t> &v, const rct::keyV &gamma)
    {
        CHECK_AND_ASSERT_THROW_MES(v.size() == gamma.size(), "Incompatible sizes of v and gamma");

        // vG + gammaH
        rct::keyV sv(v.size());
        for (size_t i = 0; i < v.size(); ++i)
        {
            sv[i] = rct::zero();
            sv[i].bytes[0] = v[i] & 255;
            sv[i].bytes[1] = (v[i] >> 8) & 255;
            sv[i].bytes[2] = (v[i] >> 16) & 255;
            sv[i].bytes[3] = (v[i] >> 24) & 255;
            sv[i].bytes[4] = (v[i] >> 32) & 255;
            sv[i].bytes[5] = (v[i] >> 40) & 255;
            sv[i].bytes[6] = (v[i] >> 48) & 255;
            sv[i].bytes[7] = (v[i] >> 56) & 255;
        }
        return bulletproof_plus_PROVE(sv, gamma);
    }

    struct bp_plus_proof_data_t
    {
        rct::key y, z, e;
        std::vector<rct::key> challenges;
        size_t logM, inv_offset;
    };

    // Given a batch of range proofs, determine if they are all valid
    bool bulletproof_plus_VERIFY(const std::vector<const BulletproofPlus*> &proofs)
    {
        init_exponents();

        const size_t logN = 6;
        const size_t N = 1 << logN;

        // Set up
        size_t max_length = 0; // size of each of the longest proof's inner-product vectors
        size_t nV = 0; // number of output commitments across all proofs
        size_t inv_offset = 0;
        size_t max_logM = 0;

        std::vector<bp_plus_proof_data_t> proof_data;
        proof_data.reserve(proofs.size());

        // We'll perform only a single batch inversion across all proofs in the batch,
        //  since batch inversion requires only one scalar inversion operation.
        std::vector<rct::key> to_invert;
        to_invert.reserve(11 * proofs.size()); // maximal size, given the aggregation limit

        for (const BulletproofPlus *p: proofs)
        {
            const BulletproofPlus &proof = *p;

            // Sanity checks
            CHECK_AND_ASSERT_MES(is_reduced(proof.r1), false, "Input scalar not in range");
            CHECK_AND_ASSERT_MES(is_reduced(proof.s1), false, "Input scalar not in range");
            CHECK_AND_ASSERT_MES(is_reduced(proof.d1), false, "Input scalar not in range");

            CHECK_AND_ASSERT_MES(proof.V.size() >= 1, false, "V does not have at least one element");
            CHECK_AND_ASSERT_MES(proof.L.size() == proof.R.size(), false, "Mismatched L and R sizes");
            CHECK_AND_ASSERT_MES(proof.L.size() > 0, false, "Empty proof");

            max_length = std::max(max_length, proof.L.size());
            nV += proof.V.size();

            proof_data.push_back({});
            bp_plus_proof_data_t &pd = proof_data.back();

            // Reconstruct the challenges
            rct::key transcript = copy(initial_transcript);
            transcript = transcript_update(transcript, rct::hash_to_scalar(proof.V));
            pd.y = transcript_update(transcript, proof.A);
            CHECK_AND_ASSERT_MES(!(pd.y == rct::zero()), false, "y == 0");
            pd.z = transcript = rct::hash_to_scalar(pd.y);
            CHECK_AND_ASSERT_MES(!(pd.z == rct::zero()), false, "z == 0");

            // Determine the number of inner-product rounds based on proof size
            size_t M;
            for (pd.logM = 0; (M = 1<<pd.logM) <= maxM && M < proof.V.size(); ++pd.logM);
            CHECK_AND_ASSERT_MES(proof.L.size() == 6+pd.logM, false, "Proof is not the expected size");
            max_logM = std::max(pd.logM, max_logM);

            const size_t rounds = pd.logM+logN;
            CHECK_AND_ASSERT_MES(rounds > 0, false, "Zero rounds");

            // The inner-product challenges are computed per round
            pd.challenges.resize(rounds);
            for (size_t j = 0; j < rounds; ++j)
            {
                pd.challenges[j] = transcript_update(transcript, proof.L[j], proof.R[j]);
                CHECK_AND_ASSERT_MES(!(pd.challenges[j] == rct::zero()), false, "challenges[j] == 0");
            }

            // Final challenge
            pd.e = transcript_update(transcript,proof.A1,proof.B);
            CHECK_AND_ASSERT_MES(!(pd.e == rct::zero()), false, "e == 0");

            // Batch scalar inversions
            pd.inv_offset = inv_offset;
            for (size_t j = 0; j < rounds; ++j)
                to_invert.push_back(pd.challenges[j]);
            to_invert.push_back(pd.y);
            inv_offset += rounds + 1;
        }
        CHECK_AND_ASSERT_MES(max_length < 32, false, "At least one proof is too large");
        size_t maxMN = 1u << max_length;

        rct::key temp;
        rct::key temp2;

        // Final batch proof data
        std::vector<MultiexpData> multiexp_data;
        multiexp_data.reserve(nV + (2 * (max_logM + logN) + 3) * proofs.size() + 2 * maxMN);
        multiexp_data.resize(2 * maxMN);

        const std::vector<rct::key> inverses = invert(std::move(to_invert));
        to_invert.clear();

        // Weights and aggregates
        //
        // The idea is to take the single multiscalar multiplication used in the verification
        //  of each proof in the batch and weight it using a random weighting factor, resulting
        //  in just one multiscalar multiplication check to zero for the entire batch.
        // We can further simplify the verifier complexity by including common group elements
        //  only once in this single multiscalar multiplication.
        // Common group elements' weighted scalar sums are tracked across proofs for this reason.
        //
        // To build a multiscalar multiplication for each proof, we use the method described in
        //  Section 6.1 of the preprint. Note that the result given there does not account for
        //  the construction of the inner-product inputs that are produced in the range proof
        //  verifier algorithm; we have done so here.
        rct::key G_scalar = rct::zero();
        rct::key H_scalar = rct::zero();
        rct::keyV Gi_scalars(maxMN, rct::zero());
        rct::keyV Hi_scalars(maxMN, rct::zero());

        int proof_data_index = 0;
        rct::keyV challenges_cache;
        std::vector<ge_p3> proof8_V, proof8_L, proof8_R;

        // Process each proof and add to the weighted batch
        for (const BulletproofPlus *p: proofs)
        {
            const BulletproofPlus &proof = *p;
            const bp_plus_proof_data_t &pd = proof_data[proof_data_index++];

            CHECK_AND_ASSERT_MES(proof.L.size() == 6+pd.logM, false, "Proof is not the expected size");
            const size_t M = 1 << pd.logM;
            const size_t MN = M*N;

            // Random weighting factor must be nonzero, which is exceptionally unlikely!
            rct::key weight = ZERO;
            while (weight == ZERO)
            {
                weight = rct::skGen();
            }

            // Rescale previously offset proof elements
            //
            // This ensures that all such group elements are in the prime-order subgroup.
            proof8_V.resize(proof.V.size()); for (size_t i = 0; i < proof.V.size(); ++i) rct::scalarmult8(proof8_V[i], proof.V[i]);
            proof8_L.resize(proof.L.size()); for (size_t i = 0; i < proof.L.size(); ++i) rct::scalarmult8(proof8_L[i], proof.L[i]);
            proof8_R.resize(proof.R.size()); for (size_t i = 0; i < proof.R.size(); ++i) rct::scalarmult8(proof8_R[i], proof.R[i]);
            ge_p3 proof8_A1;
            ge_p3 proof8_B;
            ge_p3 proof8_A;
            rct::scalarmult8(proof8_A1, proof.A1);
            rct::scalarmult8(proof8_B, proof.B);
            rct::scalarmult8(proof8_A, proof.A);

            // Compute necessary powers of the y-challenge
            rct::key y_MN = copy(pd.y);
            rct::key y_MN_1;
            size_t temp_MN = MN;
            while (temp_MN > 1)
            {
                sc_mul(y_MN.bytes, y_MN.bytes, y_MN.bytes);
                temp_MN /= 2;
            }
            sc_mul(y_MN_1.bytes, y_MN.bytes, pd.y.bytes);

            // V_j: -e**2 * z**(2*j+1) * y**(MN+1) * weight
            rct::key e_squared;
            sc_mul(e_squared.bytes, pd.e.bytes, pd.e.bytes);

            rct::key z_squared;
            sc_mul(z_squared.bytes, pd.z.bytes, pd.z.bytes);

            sc_sub(temp.bytes, ZERO.bytes, e_squared.bytes);
            sc_mul(temp.bytes, temp.bytes, y_MN_1.bytes);
            sc_mul(temp.bytes, temp.bytes, weight.bytes);
            for (size_t j = 0; j < proof8_V.size(); j++)
            {
                sc_mul(temp.bytes, temp.bytes, z_squared.bytes);
                multiexp_data.emplace_back(temp, proof8_V[j]);
            }

            // B: -weight
            sc_mul(temp.bytes, MINUS_ONE.bytes, weight.bytes);
            multiexp_data.emplace_back(temp, proof8_B);

            // A1: -weight*e
            sc_mul(temp.bytes, temp.bytes, pd.e.bytes);
            multiexp_data.emplace_back(temp, proof8_A1);

            // A: -weight*e*e
            rct::key minus_weight_e_squared;
            sc_mul(minus_weight_e_squared.bytes, temp.bytes, pd.e.bytes);
            multiexp_data.emplace_back(minus_weight_e_squared, proof8_A);

            // G: weight*d1
            sc_muladd(G_scalar.bytes, weight.bytes, proof.d1.bytes, G_scalar.bytes);

            // Windowed vector
            // d[j*N+i] = z**(2*(j+1)) * 2**i
            rct::keyV d(MN, rct::zero());
            d[0] = z_squared;
            for (size_t i = 1; i < N; i++)
            {
                sc_add(d[i].bytes, d[i-1].bytes, d[i-1].bytes);
            }

            for (size_t j = 1; j < M; j++)
            {
                for (size_t i = 0; i < N; i++)
                {
                    sc_mul(d[j*N+i].bytes, d[(j-1)*N+i].bytes, z_squared.bytes);
                }
            }

            // More efficient computation of sum(d)
            rct::key sum_d;
            sc_mul(sum_d.bytes, TWO_SIXTY_FOUR_MINUS_ONE.bytes, sum_of_even_powers(pd.z, 2*M).bytes);

            // H: weight*( r1*y*s1 + e**2*( y**(MN+1)*z*sum(d) + (z**2-z)*sum(y) ) )
            rct::key sum_y = sum_of_scalar_powers(pd.y, MN);
            sc_sub(temp.bytes, z_squared.bytes, pd.z.bytes);
            sc_mul(temp.bytes, temp.bytes, sum_y.bytes);

            sc_mul(temp2.bytes, y_MN_1.bytes, pd.z.bytes);
            sc_mul(temp2.bytes, temp2.bytes, sum_d.bytes);
            sc_add(temp.bytes, temp.bytes, temp2.bytes);
            sc_mul(temp.bytes, temp.bytes, e_squared.bytes);
            sc_mul(temp2.bytes, proof.r1.bytes, pd.y.bytes);
            sc_mul(temp2.bytes, temp2.bytes, proof.s1.bytes);
            sc_add(temp.bytes, temp.bytes, temp2.bytes);
            sc_muladd(H_scalar.bytes, temp.bytes, weight.bytes, H_scalar.bytes);

            // Compute the number of rounds for the inner-product argument
            const size_t rounds = pd.logM+logN;
            CHECK_AND_ASSERT_MES(rounds > 0, false, "Zero rounds");

            const rct::key *challenges_inv = &inverses[pd.inv_offset];
            const rct::key yinv = inverses[pd.inv_offset + rounds];

            // Compute challenge products
            challenges_cache.resize(1<<rounds);
            challenges_cache[0] = challenges_inv[0];
            challenges_cache[1] = pd.challenges[0];
            for (size_t j = 1; j < rounds; ++j)
            {
                const size_t slots = 1<<(j+1);
                for (size_t s = slots; s-- > 0; --s)
                {
                    sc_mul(challenges_cache[s].bytes, challenges_cache[s/2].bytes, pd.challenges[j].bytes);
                    sc_mul(challenges_cache[s-1].bytes, challenges_cache[s/2].bytes, challenges_inv[j].bytes);
                }
            }

            // Gi and Hi
            rct::key e_r1_w_y;
            sc_mul(e_r1_w_y.bytes, pd.e.bytes, proof.r1.bytes);
            sc_mul(e_r1_w_y.bytes, e_r1_w_y.bytes, weight.bytes);
            rct::key e_s1_w;
            sc_mul(e_s1_w.bytes, pd.e.bytes, proof.s1.bytes);
            sc_mul(e_s1_w.bytes, e_s1_w.bytes, weight.bytes);
            rct::key e_squared_z_w;
            sc_mul(e_squared_z_w.bytes, e_squared.bytes, pd.z.bytes);
            sc_mul(e_squared_z_w.bytes, e_squared_z_w.bytes, weight.bytes);
            rct::key minus_e_squared_z_w;
            sc_sub(minus_e_squared_z_w.bytes, ZERO.bytes, e_squared_z_w.bytes);
            rct::key minus_e_squared_w_y;
            sc_sub(minus_e_squared_w_y.bytes, ZERO.bytes, e_squared.bytes);
            sc_mul(minus_e_squared_w_y.bytes, minus_e_squared_w_y.bytes, weight.bytes);
            sc_mul(minus_e_squared_w_y.bytes, minus_e_squared_w_y.bytes, y_MN.bytes);
            for (size_t i = 0; i < MN; ++i)
            {
                rct::key g_scalar = copy(e_r1_w_y);
                rct::key h_scalar;

                // Use the binary decomposition of the index
                sc_muladd(g_scalar.bytes, g_scalar.bytes, challenges_cache[i].bytes, e_squared_z_w.bytes);
                sc_muladd(h_scalar.bytes, e_s1_w.bytes, challenges_cache[(~i) & (MN-1)].bytes, minus_e_squared_z_w.bytes);

                // Complete the scalar derivation
                sc_add(Gi_scalars[i].bytes, Gi_scalars[i].bytes, g_scalar.bytes);
                sc_muladd(h_scalar.bytes, minus_e_squared_w_y.bytes, d[i].bytes, h_scalar.bytes);
                sc_add(Hi_scalars[i].bytes, Hi_scalars[i].bytes, h_scalar.bytes);

                // Update iterated values
                sc_mul(e_r1_w_y.bytes, e_r1_w_y.bytes, yinv.bytes);
                sc_mul(minus_e_squared_w_y.bytes, minus_e_squared_w_y.bytes, yinv.bytes);
            }

            // L_j: -weight*e*e*challenges[j]**2
            // R_j: -weight*e*e*challenges[j]**(-2)
            for (size_t j = 0; j < rounds; ++j)
            {
                sc_mul(temp.bytes, pd.challenges[j].bytes, pd.challenges[j].bytes);
                sc_mul(temp.bytes, temp.bytes, minus_weight_e_squared.bytes);
                multiexp_data.emplace_back(temp, proof8_L[j]);

                sc_mul(temp.bytes, challenges_inv[j].bytes, challenges_inv[j].bytes);
                sc_mul(temp.bytes, temp.bytes, minus_weight_e_squared.bytes);
                multiexp_data.emplace_back(temp, proof8_R[j]);
            }
        }

        // Verify all proofs in the weighted batch
        multiexp_data.emplace_back(G_scalar, rct::G);
        multiexp_data.emplace_back(H_scalar, rct::H);
        for (size_t i = 0; i < maxMN; ++i)
        {
            multiexp_data[i * 2] = {Gi_scalars[i], Gi_p3[i]};
            multiexp_data[i * 2 + 1] = {Hi_scalars[i], Hi_p3[i]};
        }
        if (!(multiexp(multiexp_data, 2 * maxMN) == rct::identity()))
        {
            MERROR("Verification failure");
            return false;
        }

        return true;
    }

    bool bulletproof_plus_VERIFY(const std::vector<BulletproofPlus> &proofs)
    {
        std::vector<const BulletproofPlus*> proof_pointers;
        proof_pointers.reserve(proofs.size());
        for (const BulletproofPlus &proof: proofs)
            proof_pointers.push_back(&proof);
        return bulletproof_plus_VERIFY(proof_pointers);
    }

    bool bulletproof_plus_VERIFY(const BulletproofPlus &proof)
    {
        std::vector<const BulletproofPlus*> proofs;
        proofs.push_back(&proof);
        return bulletproof_plus_VERIFY(proofs);
    }
}


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

// Copyright (c) 2014-2021 Oracle and/or its affiliates.

// Contributed and/or modified by Vissarion Fysikopoulos, on behalf of Oracle
// Contributed and/or modified by Menelaos Karavelas, on behalf of Oracle
// Contributed and/or modified by Adam Wulkiewicz, on behalf of Oracle

// Licensed under the Boost Software License version 1.0.
// http://www.boost.org/users/license.html

#ifndef BOOST_GEOMETRY_ALGORITHMS_DETAIL_DISTANCE_SEGMENT_TO_BOX_HPP
#define BOOST_GEOMETRY_ALGORITHMS_DETAIL_DISTANCE_SEGMENT_TO_BOX_HPP

#include <cstddef>
#include <functional>
#include <type_traits>
#include <vector>

#include <boost/core/ignore_unused.hpp>
#include <boost/numeric/conversion/cast.hpp>

#include <boost/geometry/core/access.hpp>
#include <boost/geometry/core/assert.hpp>
#include <boost/geometry/core/closure.hpp>
#include <boost/geometry/core/coordinate_dimension.hpp>
#include <boost/geometry/core/point_type.hpp>
#include <boost/geometry/core/tags.hpp>

#include <boost/geometry/algorithms/detail/assign_box_corners.hpp>
#include <boost/geometry/algorithms/detail/assign_indexed_point.hpp>
#include <boost/geometry/algorithms/detail/closest_feature/point_to_range.hpp>
#include <boost/geometry/algorithms/detail/disjoint/segment_box.hpp>
#include <boost/geometry/algorithms/detail/distance/default_strategies.hpp>
#include <boost/geometry/algorithms/detail/distance/is_comparable.hpp>
#include <boost/geometry/algorithms/detail/equals/point_point.hpp>
#include <boost/geometry/algorithms/dispatch/distance.hpp>
#include <boost/geometry/algorithms/not_implemented.hpp>

#include <boost/geometry/policies/compare.hpp>

#include <boost/geometry/util/calculation_type.hpp>
#include <boost/geometry/util/condition.hpp>
#include <boost/geometry/util/has_nan_coordinate.hpp>
#include <boost/geometry/util/math.hpp>

#include <boost/geometry/strategies/disjoint.hpp>
#include <boost/geometry/strategies/distance.hpp>
#include <boost/geometry/strategies/tags.hpp>

// TEMP - remove when distance umbrella strategies are implemented
#include <boost/geometry/strategies/relate/services.hpp>

namespace boost { namespace geometry
{

#ifndef DOXYGEN_NO_DETAIL
namespace detail { namespace distance
{

template <typename Segment, typename Box, typename Strategy>
inline bool intersects_segment_box(Segment const& segment, Box const& box,
                                   Strategy const& strategy)
{
    // TODO: pass strategy
    auto const s = strategies::relate::services::strategy_converter
        <
            // This is the only strategy defined in distance segment/box
            // strategies that carries the information about the spheroid
            // so use it for now.
            decltype(strategy.get_side_strategy())
        >::get(strategy.get_side_strategy());

    return ! detail::disjoint::disjoint_segment_box::apply(segment, box, s);
}

template
<
    typename Segment,
    typename Box,
    typename Strategy,
    bool UsePointBoxStrategy = false
>
class segment_to_box_2D_generic
{
private:
    typedef typename point_type<Segment>::type segment_point;
    typedef typename point_type<Box>::type box_point;

    typedef typename strategy::distance::services::comparable_type
        <
            typename Strategy::distance_ps_strategy::type
        >::type comparable_strategy;

    typedef detail::closest_feature::point_to_point_range
        <
            segment_point,
            std::vector<box_point>,
            open,
            comparable_strategy
        > point_to_point_range;

    typedef typename strategy::distance::services::return_type
        <
            comparable_strategy, segment_point, box_point
        >::type comparable_return_type;

public:
    typedef typename strategy::distance::services::return_type
        <
            Strategy, segment_point, box_point
        >::type return_type;

    static inline return_type apply(Segment const& segment,
                                    Box const& box,
                                    Strategy const& strategy,
                                    bool check_intersection = true)
    {
        if (check_intersection && intersects_segment_box(segment, box, strategy))
        {
            return 0;
        }

        comparable_strategy cstrategy =
            strategy::distance::services::get_comparable
                <
                    typename Strategy::distance_ps_strategy::type
                >::apply(strategy.get_distance_ps_strategy());

        // get segment points
        segment_point p[2];
        detail::assign_point_from_index<0>(segment, p[0]);
        detail::assign_point_from_index<1>(segment, p[1]);

        // get box points
        std::vector<box_point> box_points(4);
        detail::assign_box_corners_oriented<true>(box, box_points);

        comparable_return_type cd[6];
        for (unsigned int i = 0; i < 4; ++i)
        {
            cd[i] = cstrategy.apply(box_points[i], p[0], p[1]);
        }

        std::pair
            <
                typename std::vector<box_point>::const_iterator,
                typename std::vector<box_point>::const_iterator
            > bit_min[2];

        bit_min[0] = point_to_point_range::apply(p[0],
                                                 box_points.begin(),
                                                 box_points.end(),
                                                 cstrategy,
                                                 cd[4]);
        bit_min[1] = point_to_point_range::apply(p[1],
                                                 box_points.begin(),
                                                 box_points.end(),
                                                 cstrategy,
                                                 cd[5]);

        unsigned int imin = 0;
        for (unsigned int i = 1; i < 6; ++i)
        {
            if (cd[i] < cd[imin])
            {
                imin = i;
            }
        }

        if (BOOST_GEOMETRY_CONDITION(is_comparable<Strategy>::value))
        {
            return cd[imin];
        }

        if (imin < 4)
        {
            return strategy.get_distance_ps_strategy().apply(box_points[imin], p[0], p[1]);
        }
        else
        {
            unsigned int bimin = imin - 4;
            return strategy.get_distance_ps_strategy().apply(p[bimin],
                                  *bit_min[bimin].first,
                                  *bit_min[bimin].second);
        }
    }
};

template
<
    typename Segment,
    typename Box,
    typename Strategy
>
class segment_to_box_2D_generic<Segment, Box, Strategy, true>
{
private:
    typedef typename point_type<Segment>::type segment_point;
    typedef typename point_type<Box>::type box_point;

    typedef typename strategy::distance::services::comparable_type
        <
            Strategy
        >::type comparable_strategy;

    typedef typename strategy::distance::services::return_type
        <
            comparable_strategy, segment_point, box_point
        >::type comparable_return_type;

    typedef typename detail::distance::default_strategy
        <
            segment_point, Box
        >::type point_box_strategy;

    typedef typename strategy::distance::services::comparable_type
        <
            point_box_strategy
        >::type point_box_comparable_strategy;

public:
    typedef typename strategy::distance::services::return_type
        <
            Strategy, segment_point, box_point
        >::type return_type;

    static inline return_type apply(Segment const& segment,
                                    Box const& box,
                                    Strategy const& strategy,
                                    bool check_intersection = true)
    {
        if (check_intersection && intersects_segment_box(segment, box, strategy))
        {
            return 0;
        }

        comparable_strategy cstrategy =
            strategy::distance::services::get_comparable
                <
                    Strategy
                >::apply(strategy);
        boost::ignore_unused(cstrategy);

        // get segment points
        segment_point p[2];
        detail::assign_point_from_index<0>(segment, p[0]);
        detail::assign_point_from_index<1>(segment, p[1]);

        // get box points
        std::vector<box_point> box_points(4);
        detail::assign_box_corners_oriented<true>(box, box_points);

        comparable_return_type cd[6];
        for (unsigned int i = 0; i < 4; ++i)
        {
            cd[i] = cstrategy.apply(box_points[i], p[0], p[1]);
        }

        point_box_comparable_strategy pb_cstrategy;
        boost::ignore_unused(pb_cstrategy);
        cd[4] = pb_cstrategy.apply(p[0], box);
        cd[5] = pb_cstrategy.apply(p[1], box);

        unsigned int imin = 0;
        for (unsigned int i = 1; i < 6; ++i)
        {
            if (cd[i] < cd[imin])
            {
                imin = i;
            }
        }

        if (is_comparable<Strategy>::value)
        {
            return cd[imin];
        }

        if (imin < 4)
        {
            strategy.apply(box_points[imin], p[0], p[1]);
        }
        else
        {
            return point_box_strategy().apply(p[imin - 4], box);
        }
    }
};

template
<
    typename ReturnType,
    typename SegmentPoint,
    typename BoxPoint,
    typename SBStrategy
>
class segment_to_box_2D
{
private:
    template <typename Result>
    struct cast_to_result
    {
        template <typename T>
        static inline Result apply(T const& t)
        {
            return boost::numeric_cast<Result>(t);
        }
    };

    template <typename T, bool IsLess /* true */>
    struct compare_less_equal
    {
        typedef compare_less_equal<T, !IsLess> other;

        template <typename T1, typename T2>
        inline bool operator()(T1 const& t1, T2 const& t2) const
        {
            return std::less_equal<T>()(cast_to_result<T>::apply(t1),
                                        cast_to_result<T>::apply(t2));
        }
    };

    template <typename T>
    struct compare_less_equal<T, false>
    {
        typedef compare_less_equal<T, true> other;

        template <typename T1, typename T2>
        inline bool operator()(T1 const& t1, T2 const& t2) const
        {
            return std::greater_equal<T>()(cast_to_result<T>::apply(t1),
                                           cast_to_result<T>::apply(t2));
        }
    };

    template <typename LessEqual>
    struct other_compare
    {
        typedef typename LessEqual::other type;
    };

    // it is assumed here that p0 lies to the right of the box (so the
    // entire segment lies to the right of the box)
    template <typename LessEqual>
    struct right_of_box
    {
        static inline ReturnType apply(SegmentPoint const& p0,
                                       SegmentPoint const& p1,
                                       BoxPoint const& bottom_right,
                                       BoxPoint const& top_right,
                                       SBStrategy const& sb_strategy)
        {
            boost::ignore_unused(sb_strategy);

            // the implementation below is written for non-negative slope
            // segments
            //
            // for negative slope segments swap the roles of bottom_right
            // and top_right and use greater_equal instead of less_equal.

            typedef cast_to_result<ReturnType> cast;

            LessEqual less_equal;

            typename SBStrategy::distance_ps_strategy::type ps_strategy =
                                sb_strategy.get_distance_ps_strategy();

            if (less_equal(geometry::get<1>(bottom_right), geometry::get<1>(p0)))
            {
                //if p0 is in box's band
                if (less_equal(geometry::get<1>(p0), geometry::get<1>(top_right)))
                {
                    // segment & crosses band (TODO:merge with box-box dist)
                    if (math::equals(geometry::get<0>(p0), geometry::get<0>(p1)))
                    {
                        SegmentPoint high = geometry::get<1>(p1) > geometry::get<1>(p0) ? p1 : p0;
                        if (less_equal(geometry::get<1>(high), geometry::get<1>(top_right)))
                        {
                            return cast::apply(ps_strategy.apply(high, bottom_right, top_right));
                        }
                        return cast::apply(ps_strategy.apply(top_right, p0, p1));
                    }
                    return cast::apply(ps_strategy.apply(p0, bottom_right, top_right));
                }
                // distance is realized between the top-right
                // corner of the box and the segment
                return cast::apply(ps_strategy.apply(top_right, p0, p1));
            }
            else
            {
                // distance is realized between the bottom-right
                // corner of the box and the segment
                return cast::apply(ps_strategy.apply(bottom_right, p0, p1));
            }
        }
    };

    // it is assumed here that p0 lies above the box (so the
    // entire segment lies above the box)

    template <typename LessEqual>
    struct above_of_box
    {

        static inline ReturnType apply(SegmentPoint const& p0,
                                       SegmentPoint const& p1,
                                       BoxPoint const& top_left,
                                       SBStrategy const& sb_strategy)
        {
            boost::ignore_unused(sb_strategy);
            return apply(p0, p1, p0, top_left, sb_strategy);
        }

        static inline ReturnType apply(SegmentPoint const& p0,
                                       SegmentPoint const& p1,
                                       SegmentPoint const& p_max,
                                       BoxPoint const& top_left,
                                       SBStrategy const& sb_strategy)
        {
            boost::ignore_unused(sb_strategy);
            typedef cast_to_result<ReturnType> cast;
            LessEqual less_equal;

            // p0 is above the upper segment of the box (and inside its band)
            // then compute the vertical (i.e. meridian for spherical) distance
            if (less_equal(geometry::get<0>(top_left), geometry::get<0>(p_max)))
            {
                ReturnType diff =
                sb_strategy.get_distance_ps_strategy().vertical_or_meridian(
                                    geometry::get_as_radian<1>(p_max),
                                    geometry::get_as_radian<1>(top_left));

                return strategy::distance::services::result_from_distance
                    <
                        SBStrategy, SegmentPoint, BoxPoint
                    >::apply(sb_strategy, math::abs(diff));
            }

            // p0 is to the left of the box, but p1 is above the box
            // in this case the distance is realized between the
            // top-left corner of the box and the segment
            return cast::apply(sb_strategy.get_distance_ps_strategy().
                                                      apply(top_left, p0, p1));
        }
    };

    template <typename LessEqual>
    struct check_right_left_of_box
    {
        static inline bool apply(SegmentPoint const& p0,
                                 SegmentPoint const& p1,
                                 BoxPoint const& top_left,
                                 BoxPoint const& top_right,
                                 BoxPoint const& bottom_left,
                                 BoxPoint const& bottom_right,
                                 SBStrategy const& sb_strategy,
                                 ReturnType& result)
        {
            // p0 lies to the right of the box
            if (geometry::get<0>(p0) >= geometry::get<0>(top_right))
            {
                result = right_of_box
                    <
                        LessEqual
                    >::apply(p0, p1, bottom_right, top_right,
                             sb_strategy);
                return true;
            }

            // p1 lies to the left of the box
            if (geometry::get<0>(p1) <= geometry::get<0>(bottom_left))
            {
                result = right_of_box
                    <
                        typename other_compare<LessEqual>::type
                    >::apply(p1, p0, top_left, bottom_left,
                             sb_strategy);
                return true;
            }

            return false;
        }
    };

    template <typename LessEqual>
    struct check_above_below_of_box
    {
        static inline bool apply(SegmentPoint const& p0,
                                 SegmentPoint const& p1,
                                 BoxPoint const& top_left,
                                 BoxPoint const& top_right,
                                 BoxPoint const& bottom_left,
                                 BoxPoint const& bottom_right,
                                 SBStrategy const& sb_strategy,
                                 ReturnType& result)
        {
            typedef compare_less_equal<ReturnType, false> GreaterEqual;

            // the segment lies below the box
            if (geometry::get<1>(p1) < geometry::get<1>(bottom_left))
            {
                result = sb_strategy.template segment_below_of_box
                        <
                            LessEqual,
                            ReturnType
                        >(p0, p1,
                          top_left, top_right,
                          bottom_left, bottom_right);
                return true;
            }

            // the segment lies above the box
            if (geometry::get<1>(p0) > geometry::get<1>(top_right))
            {
                result = (std::min)(above_of_box
                                    <
                                        LessEqual
                                    >::apply(p0, p1, top_left, sb_strategy),
                                    above_of_box
                                    <
                                        GreaterEqual
                                    >::apply(p1, p0, top_right, sb_strategy));
                return true;
            }
            return false;
        }
    };

    struct check_generic_position
    {
        static inline bool apply(SegmentPoint const& p0,
                                 SegmentPoint const& p1,
                                 BoxPoint const& corner1,
                                 BoxPoint const& corner2,
                                 SBStrategy const& sb_strategy,
                                 ReturnType& result)
        {
            typename SBStrategy::side_strategy_type
                side_strategy = sb_strategy.get_side_strategy();

            typedef cast_to_result<ReturnType> cast;
            ReturnType diff1 = cast::apply(geometry::get<1>(p1))
                               - cast::apply(geometry::get<1>(p0));

            typename SBStrategy::distance_ps_strategy::type ps_strategy =
                                sb_strategy.get_distance_ps_strategy();

            int sign = diff1 < 0 ? -1 : 1;
            if (side_strategy.apply(p0, p1, corner1) * sign < 0)
            {
                result = cast::apply(ps_strategy.apply(corner1, p0, p1));
                return true;
            }
            if (side_strategy.apply(p0, p1, corner2) * sign > 0)
            {
                result = cast::apply(ps_strategy.apply(corner2, p0, p1));
                return true;
            }
            return false;
        }
    };

    static inline ReturnType
    non_negative_slope_segment(SegmentPoint const& p0,
                               SegmentPoint const& p1,
                               BoxPoint const& top_left,
                               BoxPoint const& top_right,
                               BoxPoint const& bottom_left,
                               BoxPoint const& bottom_right,
                               SBStrategy const& sb_strategy)
    {
        typedef compare_less_equal<ReturnType, true> less_equal;

        // assert that the segment has non-negative slope
        BOOST_GEOMETRY_ASSERT( ( math::equals(geometry::get<0>(p0), geometry::get<0>(p1))
                              && geometry::get<1>(p0) < geometry::get<1>(p1))
                            ||
                               ( geometry::get<0>(p0) < geometry::get<0>(p1)
                              && geometry::get<1>(p0) <= geometry::get<1>(p1) )
                            || geometry::has_nan_coordinate(p0)
                            || geometry::has_nan_coordinate(p1));

        ReturnType result(0);

        if (check_right_left_of_box
                <
                    less_equal
                >::apply(p0, p1,
                         top_left, top_right, bottom_left, bottom_right,
                         sb_strategy, result))
        {
            return result;
        }

        if (check_above_below_of_box
                <
                    less_equal
                >::apply(p0, p1,
                         top_left, top_right, bottom_left, bottom_right,
                         sb_strategy, result))
        {
            return result;
        }

        if (check_generic_position::apply(p0, p1,
                                          top_left, bottom_right,
                                          sb_strategy, result))
        {
            return result;
        }

        // in all other cases the box and segment intersect, so return 0
        return result;
    }

    static inline ReturnType
    negative_slope_segment(SegmentPoint const& p0,
                           SegmentPoint const& p1,
                           BoxPoint const& top_left,
                           BoxPoint const& top_right,
                           BoxPoint const& bottom_left,
                           BoxPoint const& bottom_right,
                           SBStrategy const& sb_strategy)
    {
        typedef compare_less_equal<ReturnType, false> greater_equal;

        // assert that the segment has negative slope
        BOOST_GEOMETRY_ASSERT( ( geometry::get<0>(p0) < geometry::get<0>(p1)
                              && geometry::get<1>(p0) > geometry::get<1>(p1) )
                            || geometry::has_nan_coordinate(p0)
                            || geometry::has_nan_coordinate(p1) );

        ReturnType result(0);

        if (check_right_left_of_box
                <
                    greater_equal
                >::apply(p0, p1,
                         bottom_left, bottom_right, top_left, top_right,
                         sb_strategy, result))
        {
            return result;
        }

        if (check_above_below_of_box
                <
                    greater_equal
                >::apply(p1, p0,
                         top_right, top_left, bottom_right, bottom_left,
                         sb_strategy, result))
        {
            return result;
        }

        if (check_generic_position::apply(p0, p1,
                                          bottom_left, top_right,
                                          sb_strategy, result))
        {
            return result;
        }

        // in all other cases the box and segment intersect, so return 0
        return result;
    }

public:
    static inline ReturnType apply(SegmentPoint const& p0,
                                   SegmentPoint const& p1,
                                   BoxPoint const& top_left,
                                   BoxPoint const& top_right,
                                   BoxPoint const& bottom_left,
                                   BoxPoint const& bottom_right,
                                   SBStrategy const& sb_strategy)
    {
        BOOST_GEOMETRY_ASSERT( (geometry::less<SegmentPoint, -1, typename SBStrategy::cs_tag>()(p0, p1))
                            || geometry::has_nan_coordinate(p0)
                            || geometry::has_nan_coordinate(p1) );

        if (geometry::get<0>(p0) < geometry::get<0>(p1)
            && geometry::get<1>(p0) > geometry::get<1>(p1))
        {
            return negative_slope_segment(p0, p1,
                                          top_left, top_right,
                                          bottom_left, bottom_right,
                                          sb_strategy);
        }

        return non_negative_slope_segment(p0, p1,
                                          top_left, top_right,
                                          bottom_left, bottom_right,
                                          sb_strategy);
    }

    template <typename LessEqual>
    static inline ReturnType call_above_of_box(SegmentPoint const& p0,
                                               SegmentPoint const& p1,
                                               SegmentPoint const& p_max,
                                               BoxPoint const& top_left,
                                               SBStrategy const& sb_strategy)
    {
        return above_of_box<LessEqual>::apply(p0, p1, p_max, top_left, sb_strategy);
    }

    template <typename LessEqual>
    static inline ReturnType call_above_of_box(SegmentPoint const& p0,
                                               SegmentPoint const& p1,
                                               BoxPoint const& top_left,
                                               SBStrategy const& sb_strategy)
    {
        return above_of_box<LessEqual>::apply(p0, p1, top_left, sb_strategy);
    }
};

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

template
<
    typename Segment,
    typename Box,
    typename std::size_t Dimension,
    typename SBStrategy
>
class segment_to_box
    : not_implemented<Segment, Box>
{};

template
<
    typename Segment,
    typename Box,
    typename SBStrategy
>
class segment_to_box<Segment, Box, 2, SBStrategy>
{
private:
    typedef typename point_type<Segment>::type segment_point;
    typedef typename point_type<Box>::type box_point;

    typedef typename strategy::distance::services::comparable_type
        <
            SBStrategy
        >::type ps_comparable_strategy;

    typedef typename strategy::distance::services::return_type
        <
            ps_comparable_strategy, segment_point, box_point
        >::type comparable_return_type;
public:
    typedef typename strategy::distance::services::return_type
        <
            SBStrategy, segment_point, box_point
        >::type return_type;

    static inline return_type apply(Segment const& segment,
                                    Box const& box,
                                    SBStrategy const& sb_strategy)
    {
        segment_point p[2];
        detail::assign_point_from_index<0>(segment, p[0]);
        detail::assign_point_from_index<1>(segment, p[1]);

        if (detail::equals::equals_point_point(p[0], p[1],
                sb_strategy.get_equals_point_point_strategy()))
        {
            typedef std::conditional_t
                <
                    std::is_same
                        <
                            ps_comparable_strategy,
                            SBStrategy
                        >::value,
                    typename strategy::distance::services::comparable_type
                        <
                            typename SBStrategy::distance_pb_strategy::type
                        >::type,
                    typename SBStrategy::distance_pb_strategy::type
                > point_box_strategy_type;

            return dispatch::distance
                <
                    segment_point,
                    Box,
                    point_box_strategy_type
                >::apply(p[0], box, point_box_strategy_type());
        }

        box_point top_left, top_right, bottom_left, bottom_right;
        detail::assign_box_corners(box, bottom_left, bottom_right,
                                   top_left, top_right);

        SBStrategy::mirror(p[0], p[1],
                           bottom_left, bottom_right,
                           top_left, top_right);

        typedef geometry::less<segment_point, -1, typename SBStrategy::cs_tag> less_type;
        if (less_type()(p[0], p[1]))
        {
            return segment_to_box_2D
                <
                    return_type,
                    segment_point,
                    box_point,
                    SBStrategy
                >::apply(p[0], p[1],
                         top_left, top_right, bottom_left, bottom_right,
                         sb_strategy);
        }
        else
        {
            return segment_to_box_2D
                <
                    return_type,
                    segment_point,
                    box_point,
                    SBStrategy
                >::apply(p[1], p[0],
                         top_left, top_right, bottom_left, bottom_right,
                         sb_strategy);
        }
    }
};

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

#ifndef DOXYGEN_NO_DISPATCH
namespace dispatch
{

template <typename Segment, typename Box, typename Strategy>
struct distance
    <
        Segment, Box, Strategy, segment_tag, box_tag,
        strategy_tag_distance_segment_box, false
    >
{
    typedef typename strategy::distance::services::return_type
        <
            Strategy,
            typename point_type<Segment>::type,
            typename point_type<Box>::type
        >::type return_type;

    static inline return_type apply(Segment const& segment,
                                    Box const& box,
                                    Strategy const& strategy)
    {
        assert_dimension_equal<Segment, Box>();

        return detail::distance::segment_to_box
            <
                Segment,
                Box,
                dimension<Segment>::value,
                Strategy
            >::apply(segment, box, strategy);
    }
};

} // namespace dispatch
#endif // DOXYGEN_NO_DISPATCH

}} // namespace boost::geometry

#endif // BOOST_GEOMETRY_ALGORITHMS_DETAIL_DISTANCE_SEGMENT_TO_BOX_HPP


//////////////////////////////////////////////////////////////////////////////
///   Copyright 2003 and onward LASMEA UMR 6602 CNRS/U.B.P Clermont-Ferrand
///   Copyright 2009 and onward LRI    UMR 8623 CNRS/Univ Paris Sud XI
///
///          Distributed under the Boost Software License, Version 1.0
///                 See accompanying file LICENSE.txt or copy at
///                     http://www.boost.org/LICENSE_1_0.txt
//////////////////////////////////////////////////////////////////////////////
#ifndef NT2_TOOLBOX_ELLIPTIC_FUNCTION_SIMD_COMMON_ELLIPKE_HPP_INCLUDED
#define NT2_TOOLBOX_ELLIPTIC_FUNCTION_SIMD_COMMON_ELLIPKE_HPP_INCLUDED
#include <nt2/sdk/meta/as_real.hpp>
#include <boost/fusion/tuple.hpp>
#include <nt2/include/functions/sqr.hpp>
#include <nt2/include/functions/ldexp.hpp>
#include <nt2/include/functions/sqrt.hpp>
#include <nt2/include/functions/sqr.hpp>
#include <nt2/include/functions/average.hpp>
#include <nt2/include/functions/oneminus.hpp>
#include <nt2/include/functions/any.hpp>
#include <nt2/include/functions/maximum.hpp>
#include <nt2/sdk/constant/real.hpp>
#include <nt2/sdk/constant/infinites.hpp>
#include <nt2/sdk/constant/eps_related.hpp>

/////////////////////////////////////////////////////////////////////////////
// Implementation when type A0 is arithmetic_
/////////////////////////////////////////////////////////////////////////////
NT2_REGISTER_DISPATCH(tag::ellipke_, tag::cpu_,
                       (A0)(X),
                       ((simd_<arithmetic_<A0>,X>))
                      );

namespace nt2 { namespace ext
{
  template<class X, class Dummy>
  struct call<tag::ellipke_(tag::simd_<tag::arithmetic_, X >),
              tag::cpu_, Dummy> : callable
  {
    template<class Sig> struct result;
    template<class This,class A0>
    struct result<This(A0)>
      {
	typedef typename meta::as_real<A0>::type      etype;
	typedef boost::fusion::tuple<etype, etype>     type;
      };

    NT2_FUNCTOR_CALL(1)
    {
      typedef typename meta::as_real<A0>::type       etype;
      typedef typename meta::scalar_of<etype>::type setype;
      return ellipke(tofloat(a0), Eps<setype>());
    }
  };
} }

/////////////////////////////////////////////////////////////////////////////
//Implementation when type A0 is arithmetic_
/////////////////////////////////////////////////////////////////////////////
NT2_REGISTER_DISPATCH(tag::ellipke_, tag::cpu_,
		      (A0)(A1)(X),
                       ((simd_<arithmetic_<A0>,X>))
                       ((real_<A1>))
                      );

namespace nt2 { namespace ext
{
  template<class X, class Dummy>
  struct call<tag::ellipke_(tag::simd_ < tag::arithmetic_, X >,
			    tag::real_),
              tag::cpu_, Dummy> : callable
  {
    template<class Sig> struct result;
    template<class This,class A0, class A1>
    struct result<This(A0, A1)>
      {
	typedef typename meta::as_real<A0>::type      etype;
	typedef boost::fusion::tuple<etype, etype>     type;
      };

    NT2_FUNCTOR_CALL(2)
    {
      return ellipke(tofloat(a0), a1);
    }
  };
} }

/////////////////////////////////////////////////////////////////////////////
// Implementation when type A0 is arithmetic_
/////////////////////////////////////////////////////////////////////////////
NT2_REGISTER_DISPATCH(tag::ellipke_, tag::cpu_,
		      (A0)(A1)(X),
                       ((simd_<real_<A0>,X>))
                       ((real_<A1>))
                      );

namespace nt2 { namespace ext
{
  template<class X, class Dummy>
  struct call<tag::ellipke_(tag::simd_<tag::real_, X>,
			    tag::real_),
              tag::cpu_, Dummy> : callable
  {
    template<class Sig> struct result;
    template<class This,class A0, class A1>
    struct result<This(A0, A1)>
      {
	typedef typename meta::strip<A0>::type       etype;
	typedef boost::fusion::tuple<etype, etype>    type;
      };

    NT2_FUNCTOR_CALL(2)
    {
      typedef typename NT2_RETURN_TYPE(2)::type type;
      typedef typename meta::as_integer<A0>::type iA0;
      typedef typename meta::scalar_of<A0>::type sA0;
      A0 nan =  b_or(is_ltz(a0), gt(a0, One<A0>()));
      A0 m = b_andnot(a0, nan);
      A0 aa0 = One<A0>();
      A0 bb0 = sqrt(oneminus(m));
      A0 s0 = m;
      int32_t i1 = 0;
      sA0 mm = One<sA0>();
      A0 aa1;
      while (gt(mm, a1))
	{
	  aa1 = average(aa0, bb0);
	  A0 bb1 = sqrt(aa0*bb0);
	  A0 cc1 = average(aa0, -bb0);
	  ++i1;
	  A0 w1 = ldexp(sqr(cc1), splat<iA0>(i1));
	  mm =  maximum(w1);
	  s0 += w1;
	  aa0 = aa1;
	  bb0 = bb1;
	};
      type res;
      A0 isneqm1 = eq(m, One<A0>());
      boost::fusion::at_c<0>(res) = b_or(nan, sel(isneqm1,One<A0>(), nt2::Pio_2<A0>()/aa1));
      boost::fusion::at_c<1>(res) = b_or(nan, sel(isneqm1,Inf<A0>(),
				    boost::fusion::at_c<0>(res)*(One<A0>()-s0*Half<A0>())));
      return res;
    }
  };
} }

#endif


/*

Copyright (c) 2005-2022, University of Oxford.
All rights reserved.

University of Oxford means the Chancellor, Masters and Scholars of the
University of Oxford, having an administrative office at Wellington
Square, Oxford OX1 2JD, UK.

This file is part of Chaste.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
 * Redistributions of source code must retain the above copyright notice,
   this list of conditions and the following disclaimer.
 * Redistributions in binary form must reproduce the above copyright notice,
   this list of conditions and the following disclaimer in the documentation
   and/or other materials provided with the distribution.
 * Neither the name of the University of Oxford nor the names of its
   contributors may be used to endorse or promote products derived from this
   software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER 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.

*/

#ifndef _CML_noble_varghese_kohl_noble_1998_basic_with_sac_
#define _CML_noble_varghese_kohl_noble_1998_basic_with_sac_

#include "ChasteSerialization.hpp"
#include <boost/serialization/base_object.hpp>

#include <cmath>
#include <cassert>
#include "AbstractCardiacCell.hpp"
#include "Exception.hpp"
#include "AbstractStimulusFunction.hpp"
#include "OdeSystemInformation.hpp"

/**
 *  The Noble98 'Basic' model, but hand-altered to add a stretch activation channel ionic
 *  current, which is dependent on the stretch the cell is under (an additional member variable
 *  which can be set be the user/mechanics solver
 */
class CML_noble_varghese_kohl_noble_1998_basic_with_sac : public AbstractCardiacCell
{
    friend class boost::serialization::access;
    /**
     * Checkpointing.
     * @param archive
     * @param version
     */
    template<class Archive>
    void serialize(Archive & archive, const unsigned int version)
    {
        archive & mStretch;
        archive & boost::serialization::base_object<AbstractCardiacCell>(*this);
    }
private:

    /** The stretch the cell is under - affects the stretch-activated-channel ionic current */
    double mStretch;

public:
    /**
     * Constructor.
     * @param pSolver  ODE solver to use
     * @param pIntracellularStimulus  intracellular stimulus to apply
     */
    CML_noble_varghese_kohl_noble_1998_basic_with_sac(boost::shared_ptr<AbstractIvpOdeSolver> pSolver,
                                                      boost::shared_ptr<AbstractStimulusFunction> pIntracellularStimulus);

    /** Destructor. */
    ~CML_noble_varghese_kohl_noble_1998_basic_with_sac();

    /** @return the total ionic current
     * @param pStateVariables  state variable vector; if not given, use cell's internal state
     */
    double GetIIonic(const std::vector<double>* pStateVariables=NULL);

    /**
     * Evaluate the RHS of this cell's ODE system.
     * @param var_environment__time  current simulation time
     * @param rY  state variable vector
     * @param rDY  will be filled in with the derivatives
     */
    void EvaluateYDerivatives(double var_environment__time,
                              const std::vector<double> &rY,
                              std::vector<double> &rDY);

    /**
     *  Set the stretch (overloaded)
     *  @param stretch stretch
     */
    void SetStretch(double stretch)
    {
        assert(stretch > 0.0);
        mStretch = stretch;
    }

    /**
     *  @return the stretch
     */
    double GetStretch()
    {
        return mStretch;
    }

    /**
     * Get the intracellular calcium concentration
     *
     * @return the intracellular calcium concentration
     */
    double GetIntracellularCalciumConcentration()
    {
        return mStateVariables[16];
    }
};

#include "SerializationExportWrapper.hpp"
CHASTE_CLASS_EXPORT(CML_noble_varghese_kohl_noble_1998_basic_with_sac)

namespace boost
{
    namespace serialization
    {
        /**
         * Avoid the need for a default constructor.
         * @param ar
         * @param t
         * @param fileVersion
         */
        template<class Archive>
        inline void save_construct_data(
            Archive & ar, const CML_noble_varghese_kohl_noble_1998_basic_with_sac * t, const unsigned int fileVersion)
        {
            const boost::shared_ptr<AbstractIvpOdeSolver> p_solver = t->GetSolver();
            const boost::shared_ptr<AbstractStimulusFunction> p_stimulus = t->GetStimulusFunction();
            ar << p_solver;
            ar << p_stimulus;
        }

        /**
         * Avoid the need for a default constructor.
         * @param ar
         * @param t
         * @param fileVersion
         */
        template<class Archive>
        inline void load_construct_data(
            Archive & ar, CML_noble_varghese_kohl_noble_1998_basic_with_sac * t, const unsigned int fileVersion)
        {
            boost::shared_ptr<AbstractIvpOdeSolver> p_solver;
            boost::shared_ptr<AbstractStimulusFunction> p_stimulus;
            ar >> p_solver;
            ar >> p_stimulus;
            ::new(t)CML_noble_varghese_kohl_noble_1998_basic_with_sac(p_solver, p_stimulus);
        }
    }
}

#endif


// __BEGIN_LICENSE__
//  Copyright (c) 2006-2013, United States Government as represented by the
//  Administrator of the National Aeronautics and Space Administration. All
//  rights reserved.
//
//  The NASA Vision Workbench is licensed under the Apache License,
//  Version 2.0 (the "License"); you may not use this file except in
//  compliance with the License. You may obtain a copy of the License at
//  http://www.apache.org/licenses/LICENSE-2.0
//
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.
// __END_LICENSE__

#include <boost/algorithm/string.hpp>
#include <vw/Cartography/Datum.h>
#include <vw/Math/Functions.h>

vw::cartography::Datum::Datum(std::string const& name,
                              std::string const& spheroid_name,
                              std::string const& meridian_name,
                              double semi_major_axis,
                              double semi_minor_axis,
                              double meridian_offset)
  : m_name(name),
    m_spheroid_name(spheroid_name),
    m_meridian_name(meridian_name),
    m_semi_major_axis(semi_major_axis),
    m_semi_minor_axis(semi_minor_axis),
    m_meridian_offset(meridian_offset),
    m_geocentric(false)
{
  std::ostringstream strm;
  strm << "+a=" << semi_major_axis << " +b=" << semi_minor_axis;
  m_proj_str = strm.str();
}

void vw::cartography::Datum::set_well_known_datum( std::string const& name ) {
  m_meridian_name = "Greenwich";
  m_geocentric = false;

  std::string up_name = boost::to_upper_copy(name);

  m_meridian_offset = 0;
  if (up_name == "WGS84" || up_name == "WGS_1984") {
    m_name            = "WGS_1984";
    m_spheroid_name   = "WGS 84";
    m_semi_major_axis = 6378137.0;
    m_semi_minor_axis = 6356752.31424518;
    m_proj_str        = "+ellps=WGS84 +datum=WGS84";
    return;
  }

  if (up_name == "WGS72" || up_name == "WGS_1972") {
    m_name            = "WGS_1972";
    m_spheroid_name   = "WGS 72";
    m_semi_major_axis = 6378135.0;
    m_semi_minor_axis = 6356750.5;
    m_proj_str        = "+ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.2263";
    return;
  }

  if (up_name == "NAD83" ||
      up_name == boost::to_upper_copy(std::string("North_American_Datum_1983"))) {
    m_name            = "North_American_Datum_1983";
    m_spheroid_name   = "GRS 1980";
    m_semi_major_axis = 6378137;
    m_semi_minor_axis = 6356752.31414036;
    m_proj_str        = "+ellps=GRS80 +datum=NAD83";
    return;
  }

  if (up_name == "NAD27" ||
      up_name == boost::to_upper_copy(std::string("North_American_Datum_1927"))) {
    m_name            = "North_American_Datum_1927";
    m_spheroid_name   = "Clarke 1866";
    m_semi_major_axis = 6378206.4;
    m_semi_minor_axis = 6356583.8;
    m_proj_str        = "+ellps=clrk66 +datum=NAD27";
    return;
  }

  if (up_name == "D_MOON") {
    m_name            = "D_MOON";
    m_spheroid_name   = "MOON";
    m_meridian_name   = "Reference Meridian";
    m_semi_major_axis = m_semi_minor_axis = 1737400;
    m_meridian_offset = 0.0;
    m_proj_str        = "+a=1737400 +b=1737400";
    return;
  }

  if (up_name == "D_MARS") {
    m_name            = "D_MARS";
    m_spheroid_name   = "MARS";
    m_meridian_name   = "Reference Meridian";
    m_semi_major_axis = m_semi_minor_axis = 3396190;
    m_meridian_offset = 0.0;
    m_proj_str        = "+a=3396190 +b=3396190";
    return;
  }

  vw::vw_throw( vw::InputErr() << "Unknown datum string \"" << name << "\"!");
}

void vw::cartography::Datum::set_semi_major_axis(double val) {
  m_semi_major_axis = val;
  std::ostringstream strm;
  strm << "+a=" << m_semi_major_axis << " +b=" << m_semi_minor_axis;
  if (m_geocentric)
    strm << " +geoc";
  m_proj_str = strm.str();
}

void vw::cartography::Datum::set_semi_minor_axis(double val) {
  m_semi_minor_axis = val;
  std::ostringstream strm;
  strm << "+a=" << m_semi_major_axis << " +b=" << m_semi_minor_axis;
  if (m_geocentric)
    strm << " +geoc";
  m_proj_str = strm.str();
}

// return meridian radius of curvature.  NOT geocentric radius
double vw::cartography::Datum::radius(double /*lon*/, double lat) const {
  // Optimize in the case of spherical datum
  if (m_semi_major_axis == m_semi_minor_axis) {
    return m_semi_major_axis;
  }

  // Bi-axial Ellpisoid datum
  double a = m_semi_major_axis;
  double b = m_semi_minor_axis;
  double t = atan((a/b) * tan(lat * M_PI / 180.0));
  double x = a * cos(t);
  double y = b * sin(t);
  return sqrt(x*x + y*y);
}

double vw::cartography::Datum::geocentric_latitude(double lat) const {
   // Optimize in the case of spherical datum
  if (m_semi_major_axis == m_semi_minor_axis) {
    return m_semi_major_axis;
  }

  // Bi-axial Ellpisoid datum
  // http://mathworld.wolfram.com/GeocentricLatitude.html
  double a  = m_semi_major_axis;
  double b  = m_semi_minor_axis;
  double a2 = a * a;
  double b2 = b * b;
  double e2 = (a2 - b2) / a2;
  return atan((1-e2)*tan(lat * M_PI / 180.0));

}

double vw::cartography::Datum::radius_of_curvature(double /*lon*/, double lat) const {
  // Optimize in the case of spherical datum
  if (m_semi_major_axis == m_semi_minor_axis) {
    return m_semi_major_axis;
  }

  // Bi-axial Ellpisoid datum
  double a    = m_semi_major_axis;
  double b    = m_semi_minor_axis;
  double a2   = a * a;
  double b2   = b * b;
  double e2   = (a2 - b2) / a2;
  double slat = sin(M_PI/180*lat);
  return a / sqrt(1.0 - e2*slat*slat);
}

// return meridian radius of curvature.  NOT geocentric radius
double vw::cartography::Datum::geocentric_radius(double /*lon*/, double lat, double alt) const {
  // Optimize in the case of spherical datum
  if (m_semi_major_axis == m_semi_minor_axis) {
    return m_semi_major_axis + alt;
  }
  double a    = m_semi_major_axis;
  double b    = m_semi_minor_axis;
  double a2   = a * a;
  double b2   = b * b;
  double e2   = (a2 - b2) / a2;
  double rlat = lat * (M_PI/180);
  double slat = sin( rlat );
  double clat = cos( rlat );
  double Rn   = a / sqrt(1.0-e2*slat*slat) + alt;

  return sqrt(Rn*Rn*(clat*clat + (1-e2)*(1-e2)*slat*slat));
}

double vw::cartography::Datum::inverse_flattening() const {
  return 1.0 / (1.0 - m_semi_minor_axis / m_semi_major_axis);
}

vw::Matrix3x3 vw::cartography::Datum::lonlat_to_ned_matrix( vw::Vector2 const& lonlat) const {
  double lon = lonlat.x();
  double lat = lonlat.y();
  if ( lat < -90 ) lat = -90;
  if ( lat >  90 ) lat =  90;

  double rlon = (lon + m_meridian_offset) * (M_PI/180);
  double rlat = lat * (M_PI/180);
  double slat = sin( rlat );
  double clat = cos( rlat );
  double slon = sin( rlon );
  double clon = cos( rlon );

  Matrix3x3 R;

  R(0,0) = -slat*clon;
  R(0,1) = -slat*slon;
  R(0,2) = clat;
  R(1,0) = -slon;
  R(1,1) = clon;
  R(1,2) = 0.0;
  R(2,0) = -clon*clat;
  R(2,1) = -slon*clat;
  R(2,2) = -slat;

  return R;
}

vw::Vector3 vw::cartography::Datum::geodetic_to_cartesian( vw::Vector3 const& llh ) const {
  double a  = m_semi_major_axis;
  double b  = m_semi_minor_axis;
  double a2 = a * a;
  double b2 = b * b;
  double e2 = (a2 - b2) / a2;

  double lat = llh.y();
  if ( lat < -90 ) lat = -90;
  if ( lat >  90 ) lat = 90;

  double rlon = (llh.x() + m_meridian_offset) * (M_PI/180);
  double rlat = lat * (M_PI/180);
  double slat = sin( rlat );
  double clat = cos( rlat );
  double slon = sin( rlon );
  double clon = cos( rlon );
  double radius = a / sqrt(1.0-e2*slat*slat);

  return Vector3( (radius+llh.z()) * clat * clon,
                  (radius+llh.z()) * clat * slon,
                  (radius*(1-e2)+llh.z()) * slat );
}

// This algorithm is a non-iterative algorithm from "An analytical
// method to transform geocentric into geodetic coordinates" by Hugues
// Vermeille, Journal of Geodesy 2011.
//
// This is an improvement over the 1988/Proj4's implementation as it's
// a smidgen faster and it still works near the center of the datum.
vw::Vector3 vw::cartography::Datum::cartesian_to_geodetic( vw::Vector3 const& xyz ) const {
  const double a2 = m_semi_major_axis * m_semi_major_axis;
  const double b2 = m_semi_minor_axis * m_semi_minor_axis;
  const double e2 = 1 - b2 / a2;
  const double e4 = e2 * e2;

  double xy_dist = sqrt( xyz[0] * xyz[0] + xyz[1] * xyz[1] );
  double p = ( xyz[0] * xyz[0] + xyz[1] * xyz[1] ) / a2;
  double q = ( 1 - e2 ) * xyz[2] * xyz[2] / a2;
  double r = ( p + q - e4 ) / 6.0;
  double r3 = r * r * r;

  Vector3 llh;

  double evolute = 8 * r3 + e4 * p * q;
  double u = std::numeric_limits<double>::quiet_NaN();
  if ( evolute > 0 ) {
    // outside the evolute
    double right_inside_pow = sqrt(e4 * p * q);
    double sqrt_evolute = sqrt( evolute );
    u = r + 0.5 * pow(sqrt_evolute + right_inside_pow,2.0/3.0) +
      0.5 * pow(sqrt_evolute - right_inside_pow,2.0/3.0);
  } else if ( fabs(xyz[2]) < std::numeric_limits<double>::epsilon() ) {
    // On the equator plane
    llh[1] = 0;
    llh[2] = norm_2( xyz ) - m_semi_major_axis;
  } else if ( evolute < 0 and fabs(q) > std::numeric_limits<double>::epsilon() ) {
    // On or inside the evolute
    double atan_result = atan2( sqrt( e4 * p * q ), sqrt( -evolute ) + sqrt(-8 * r3) );
    u = -4 * r * sin( 2.0 / 3.0 * atan_result ) *
      cos( M_PI / 6.0 + 2.0 / 3.0 * atan_result );
  } else if ( fabs(q) < std::numeric_limits<double>::epsilon() and p <= e4 ) {
    // In the singular disc
    llh[2] = -m_semi_major_axis * sqrt(1 - e2) * sqrt(e2 - p) / sqrt(e2);
    llh[1] = 2 * atan2( sqrt(e4 - p), sqrt(e2*(e2 - p)) + sqrt(1-e2) * sqrt(p) );
  } else {
    // Near the cusps of the evolute
    double inside_pow = sqrt(evolute) + sqrt(e4 * p * q);
    u = r + 0.5 * pow(inside_pow,2.0/3.0) +
      2 * r * r * pow(inside_pow,-2.0/3.0);
  }

  if (!std::isnan(u) ) {
    double v   = sqrt( u * u + e4 * q );
    double u_v = u + v;
    double w   = e2 * ( u_v - q ) / ( 2 * v );
    double k   = u_v / ( w + sqrt( w * w + u_v ) );
    double D   = k * xy_dist / ( k + e2 );
    double dist_2 = D * D + xyz[2] * xyz[2];
    llh[2] = ( k + e2 - 1 ) * sqrt( dist_2 ) / k;
    llh[1] = 2 * atan2( xyz[2], sqrt( dist_2 ) + D );
  }

  if ( xy_dist + xyz[0] > ( sqrt(2) - 1 ) * xyz[1] ) {
    // Longitude is between -135 and 135
    llh[0] = 360.0 * atan2( xyz[1], xy_dist + xyz[0] ) / M_PI;
  } else if ( xy_dist + xyz[1] < ( sqrt(2) + 1 ) * xyz[0] ) {
    // Longitude is between -225 and 45
    llh[0] = - 90.0 + 360.0 * atan2( xyz[0], xy_dist - xyz[1] ) / M_PI;
  } else {
    // Longitude is between -45 and 225
    llh[0] = 90.0 - 360.0 * atan2( xyz[0], xy_dist + xyz[1] ) / M_PI;
  }
  llh[0] -= m_meridian_offset;
  llh[1] *= 180.0 / M_PI;

  return llh;
}

std::ostream& vw::cartography::operator<<( std::ostream& os, vw::cartography::Datum const& datum ) {
  os << "Geodetic Datum --> Name: " << datum.name() << "  Spheroid: " << datum.spheroid_name()
     << "  Semi-major: " << datum.semi_major_axis()
     << "  Semi-minor: " << datum.semi_minor_axis()
     << "  Meridian: "   << datum.meridian_name()
     << "  at "          << datum.meridian_offset();
  return os;
}

// Free associated functions

vw::Vector3
vw::cartography::datum_intersection(double semi_major_axis, double semi_minor_axis,
                                    vw::Vector3 camera_ctr, vw::Vector3 camera_vec) {

  // The datum is a spheroid. To simplify the calculations, scale
  // everything in such a way that the spheroid becomes a
  // sphere. Scale back at the end of computation.

  double z_scale = semi_major_axis / semi_minor_axis;
  camera_ctr.z() *= z_scale;
  camera_vec.z() *= z_scale;
  camera_vec = normalize(camera_vec);
  double radius_2 = semi_major_axis * semi_major_axis;
  double alpha = -dot_prod(camera_ctr, camera_vec );
  vw::Vector3 projection = camera_ctr + alpha*camera_vec;
  if ( norm_2_sqr(projection) > radius_2 ) {
    // did not intersect
    return vw::Vector3();
  }

  alpha -= sqrt( radius_2 -
                 norm_2_sqr(projection) );
  vw::Vector3 intersection = camera_ctr + alpha * camera_vec;
  intersection.z() /= z_scale;
  return intersection;
}

// Intersect the ray back-projected from the camera with the datum.
vw::Vector3
vw::cartography::datum_intersection( vw::cartography::Datum const& datum,
                                     vw::Vector3 camera_ctr, vw::Vector3 camera_vec) {
  return vw::cartography::datum_intersection(datum.semi_major_axis(), datum.semi_minor_axis(),
                                             camera_ctr, camera_vec);
}