/* Copyright (c) 2019, NVIDIA CORPORATION. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *  * Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *  * Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *  * Neither the name of NVIDIA CORPORATION 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 ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include <boost/multiprecision/cpp_int.hpp>

#include "numeric.hpp"

using namespace boost::multiprecision;

//------------------------------------
//           Miscellaneous
//------------------------------------

// Returns the smallest factor of an integer and the quotient after
// division with the smallest factor.
void SmallestFactor(uint64_t n, uint64_t& factor, uint64_t& residue)
{
  for (uint64_t i = 2; i < n; i++)
  {
    if (n % i == 0)
    {
      factor = i;
      residue = n / i;
      return;
    }
  }
  factor = n;
  residue = 1;
}

// Helper function to get close-to-square layouts of arrays
// containing a given number of nodes.
void GetTiling(uint64_t num_elems, uint64_t& height, uint64_t& width)
{
  std::vector<uint64_t> factors;
  uint64_t residue = num_elems;
  uint64_t cur_factor;
  while (residue > 1)
  {
    SmallestFactor(residue, cur_factor, residue);
    factors.push_back(cur_factor);
  }

  height = 1;
  width = 1;
  for (uint64_t i = 0; i < factors.size(); i++)
  {
    if (i % 2 == 0)
      height *= factors[i];
    else
      width *= factors[i];
  }

  if (height > width)
  {
    uint64_t temp = height;
    height = width;
    width = temp;
  }
}

double LinearInterpolate(double x,
                         double x0, double x1,
                         double q0, double q1)
{
  double slope = (x0 == x1) ? 0 : (q1 - q0) / double(x1 - x0);
  return q0 + slope * (x - x0);
}

double BilinearInterpolate(double x, double y,
                           double x0, double x1,
                           double y0, double y1,
                           double q00, double q01, double q10, double q11)
{
  // Linear interpolate along x dimension.
  double qx0 = LinearInterpolate(x, x0, x1, q00, q10);
  double qx1 = LinearInterpolate(x, x0, x1, q01, q11);

  // Linear interpolate along y dimension.
  return LinearInterpolate(y, y0, y1, qx0, qx1);
}


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

#ifndef BOOST_MATH_INTERPOLATORS_CARDINAL_QUINTIC_B_SPLINE_HPP
#define BOOST_MATH_INTERPOLATORS_CARDINAL_QUINTIC_B_SPLINE_HPP
#include <memory>
#include <limits>
#include <boost/math/interpolators/detail/cardinal_quintic_b_spline_detail.hpp>

namespace boost{ namespace math{ namespace interpolators {

template <class Real>
class cardinal_quintic_b_spline
{
public:
    // If you don't know the value of the derivative at the endpoints, leave them as nans and the routine will estimate them.
    // y[0] = y(a), y[n - 1] = y(b), step_size = (b - a)/(n -1).
    cardinal_quintic_b_spline(const Real* const y,
                                size_t n,
                                Real t0 /* initial time, left endpoint */,
                                Real h  /*spacing, stepsize*/,
                                std::pair<Real, Real> left_endpoint_derivatives = {std::numeric_limits<Real>::quiet_NaN(), std::numeric_limits<Real>::quiet_NaN()},
                                std::pair<Real, Real> right_endpoint_derivatives = {std::numeric_limits<Real>::quiet_NaN(), std::numeric_limits<Real>::quiet_NaN()})
     : impl_(std::make_shared<detail::cardinal_quintic_b_spline_detail<Real>>(y, n, t0, h, left_endpoint_derivatives, right_endpoint_derivatives))
    {}

    // Oh the bizarre error messages if we template this on a RandomAccessContainer:
    cardinal_quintic_b_spline(std::vector<Real> const & y,
                                Real t0 /* initial time, left endpoint */,
                                Real h  /*spacing, stepsize*/,
                                std::pair<Real, Real> left_endpoint_derivatives = {std::numeric_limits<Real>::quiet_NaN(), std::numeric_limits<Real>::quiet_NaN()},
                                std::pair<Real, Real> right_endpoint_derivatives = {std::numeric_limits<Real>::quiet_NaN(), std::numeric_limits<Real>::quiet_NaN()})
     : impl_(std::make_shared<detail::cardinal_quintic_b_spline_detail<Real>>(y.data(), y.size(), t0, h, left_endpoint_derivatives, right_endpoint_derivatives))
    {}

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

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

    Real double_prime(Real t) const {
        return impl_->double_prime(t);
    }

    Real t_max() const {
        return impl_->t_max();
    }

private:
    std::shared_ptr<detail::cardinal_quintic_b_spline_detail<Real>> impl_;
};

}}}
#endif


#include "modules/neuralNetwork/layer/activation/activation.hpp"
#include "modules/neuralNetwork/neuralNetwork.hpp"

#ifdef _KORALI_USE_CUDNN
  #include "auxiliar/cudaUtils.hpp"
#endif

#ifdef _KORALI_USE_ONEDNN
  #include "auxiliar/dnnUtils.hpp"
using namespace dnnl;
#endif

#include <Eigen/Dense>
using namespace Eigen;

namespace korali
{
namespace neuralNetwork
{
namespace layer
{
;

void Activation::initialize()
{
  // The node count for this layer should be the same as the previous layer
  _outputChannels = _prevLayer->_outputChannels;

  // Checking Layer size
  if (_outputChannels == 0) KORALI_LOG_ERROR("Node count for layer (%lu) should be larger than zero.\n", _index);

  // Checking position
  if (_index == 0) KORALI_LOG_ERROR("Activation layers cannot be the starting layer of the NN\n");
  if (_index == _nn->_layers.size() - 1) KORALI_LOG_ERROR("Activation layers cannot be the last layer of the NN\n");
}

void Activation::createForwardPipeline()
{
  // Calling base layer function
  Layer::createForwardPipeline();

#ifdef _KORALI_USE_ONEDNN
  if (_nn->_engine == "OneDNN")
  {
    // If it is an element-wise operation, create an element-wise primitive
    if (_function.rfind("Elementwise", 0) == 0)
    {
      if (_function == "Elementwise/Clip") _activationAlgorithm = algorithm::eltwise_clip;
      if (_function == "Elementwise/Linear") _activationAlgorithm = algorithm::eltwise_linear;
      if (_function == "Elementwise/Log") _activationAlgorithm = algorithm::eltwise_log;
      if (_function == "Elementwise/Logistic") _activationAlgorithm = algorithm::eltwise_logistic;
      if (_function == "Elementwise/ReLU") _activationAlgorithm = algorithm::eltwise_relu;
      if (_function == "Elementwise/SoftReLU") _activationAlgorithm = algorithm::eltwise_soft_relu;
      if (_function == "Elementwise/SoftSign") KORALI_LOG_ERROR("ONEDNN does not support activation functions of type 'Elementwise/SoftSign'.");
      if (_function == "Elementwise/Tanh") _activationAlgorithm = algorithm::eltwise_tanh;

      // Creating descriptor
      auto activationDesc = eltwise_forward::desc(
        _propKind,
        _activationAlgorithm,
        _prevLayer->_outputMem[0].get_desc(),
        _alpha,
        _beta);

      // Create primitive descriptor.
      _forwardEltwiseActivationPrimitiveDesc = eltwise_forward::primitive_desc(activationDesc, _nn->_dnnlEngine);

      // Create the primitive.
      _forwardActivationPrimitive = eltwise_forward(_forwardEltwiseActivationPrimitiveDesc);
    }

    // Check other possible types of activation functions
    if (_function == "Softmax")
    {
      // Creating descriptor
      const int axis = 1;
      auto activationDesc = softmax_forward::desc(_propKind, _prevLayer->_outputMem[0].get_desc(), axis);

      // Create primitive descriptor.
      _forwardSoftmaxActivationPrimitiveDesc = softmax_forward::primitive_desc(activationDesc, _nn->_dnnlEngine);

      // Create the primitive.
      _forwardActivationPrimitive = softmax_forward(_forwardSoftmaxActivationPrimitiveDesc);
    }
  }
#endif

#ifdef _KORALI_USE_CUDNN
  if (_nn->_engine == "CuDNN")
  {
    cudnnErrCheck(cudnnCreateActivationDescriptor(&_activationDesc));
    cudnnActivationMode_t activationMode;

    if (_function == "Elementwise/Clip") activationMode = CUDNN_ACTIVATION_CLIPPED_RELU;
    if (_function == "Elementwise/Linear") activationMode = CUDNN_ACTIVATION_IDENTITY;
    if (_function == "Elementwise/Log") KORALI_LOG_ERROR("Activation function not supported: %s.\n", _function.c_str());
    if (_function == "Elementwise/Logistic") activationMode = CUDNN_ACTIVATION_SIGMOID;
    if (_function == "Elementwise/ReLU") activationMode = CUDNN_ACTIVATION_RELU;
    if (_function == "Elementwise/SoftReLU") KORALI_LOG_ERROR("CUDNN does not support activation functions of type 'Elementwise/SoftSign'.");
    if (_function == "Elementwise/SoftSign") KORALI_LOG_ERROR("CUDNN does not support activation functions of type 'Elementwise/SoftSign'.");
    if (_function == "Elementwise/Tanh") activationMode = CUDNN_ACTIVATION_TANH;
    if (_function == "Softmax") activationMode = CUDNN_ACTIVATION_IDENTITY;

    if (cudnnSetActivationDescriptor(_activationDesc, activationMode, CUDNN_PROPAGATE_NAN, _alpha) != CUDNN_STATUS_SUCCESS) KORALI_LOG_ERROR("Error creating activation algorithm\n");
  }
#endif
}

void Activation::createBackwardPipeline()
{
  // Calling base layer function
  Layer::createBackwardPipeline();

// Creating backward propagation primitives for activation functions
#ifdef _KORALI_USE_ONEDNN
  if (_nn->_engine == "OneDNN")
  {
    // If it is an element-wise operation, create an element-wise backward primitive
    if (_function.rfind("Elementwise", 0) == 0)
    {
      // Creating descriptor
      auto activationDesc = eltwise_backward::desc(_activationAlgorithm, _prevLayer->_outputMem[0].get_desc(), _outputMem[0].get_desc(), _alpha, _beta);

      // Create primitive descriptor.
      auto backwardActivationPrimitiveDesc = eltwise_backward::primitive_desc(activationDesc, _nn->_dnnlEngine, _forwardEltwiseActivationPrimitiveDesc);

      // Create the primitive.
      _backwardActivationPrimitive = eltwise_backward(backwardActivationPrimitiveDesc);
    }

    // Check other possible types of activation functions
    if (_function == "Softmax")
    {
      // Creating descriptor
      const int axis = 1;
      auto activationDesc = softmax_backward::desc(_prevLayer->_outputMem[0].get_desc(), _outputMem[0].get_desc(), axis);

      // Create primitive descriptor.
      auto backwardActivationPrimitiveDesc = softmax_backward::primitive_desc(activationDesc, _nn->_dnnlEngine, _forwardSoftmaxActivationPrimitiveDesc);

      // Create the primitive.
      _backwardActivationPrimitive = softmax_backward(backwardActivationPrimitiveDesc);
    }
  }
#endif
}

void Activation::forwardData(const size_t t)
{
  size_t N = _batchSize;
  size_t OC = _outputChannels;

  if (_nn->_engine == "Korali")
  {
    if (_function == "Elementwise/Clip")
    {
      for (size_t i = 0; i < N * OC; i++)
      {
        if (_prevLayer->_outputValues[i] < _alpha)
          _outputValues[i] = _alpha;
        else if (_prevLayer->_outputValues[i] > _beta)
          _outputValues[i] = _beta;
        else
          _outputValues[i] = _prevLayer->_outputValues[i];
      }
    }
    if (_function == "Elementwise/Linear")
    {
      for (size_t i = 0; i < N * OC; i++)
        _outputValues[i] = _prevLayer->_outputValues[i] * _alpha + _beta;
    }
    if (_function == "Elementwise/Log")
    {
      for (size_t i = 0; i < N * OC; i++)
        _outputValues[i] = std::log(_prevLayer->_outputValues[i]);
    }
    if (_function == "Elementwise/ReLU")
    {
      for (size_t i = 0; i < N * OC; i++)
        if (_prevLayer->_outputValues[i] > 0.0f)
          _outputValues[i] = _prevLayer->_outputValues[i];
        else
          _outputValues[i] = _prevLayer->_outputValues[i] * _alpha;
    }
    if (_function == "Elementwise/SoftReLU")
    {
      for (size_t i = 0; i < N * OC; i++)
        _outputValues[i] = std::log(1.0f + std::exp(_prevLayer->_outputValues[i]));
    }

    if (_function == "Elementwise/Tanh")
    {
      for (size_t i = 0; i < N * OC; i++)
        _outputValues[i] = std::tanh(_prevLayer->_outputValues[i]);
    }
    if (_function == "Elementwise/Logistic")
    {
      for (size_t i = 0; i < N * OC; i++)
        _outputValues[i] = 1.0f / (1.0f + std::exp(-_prevLayer->_outputValues[i]));
    }
    if (_function == "Elementwise/SoftSign")
    {
      for (size_t i = 0; i < N * OC; i++)
        _outputValues[i] = _prevLayer->_outputValues[i] / (1.0f + std::abs(_prevLayer->_outputValues[i]));
    }
    if (_function == "Softmax")
    {
      for (size_t i = 0; i < N; i++)
      {
        float LSE = logSumExp(&_prevLayer->_outputValues[i * OC], OC);
        for (size_t j = 0; j < OC; j++)
          _outputValues[i * OC + j] = std::exp(_prevLayer->_outputValues[i * OC + j] - LSE);
      }
    }
  }

#ifdef _KORALI_USE_ONEDNN
  if (_nn->_engine == "OneDNN")
  {
    // Primitive arguments.
    _forwardActivationArgs[DNNL_ARG_SRC] = _prevLayer->_outputMem[t];
    _forwardActivationArgs[DNNL_ARG_DST] = _outputMem[t];

    _forwardActivationPrimitive.execute(_nn->_dnnlStream, _forwardActivationArgs);
  }
#endif

#ifdef _KORALI_USE_CUDNN
  if (_nn->_engine == "CuDNN")
  {
    if (_function == "Elementwise/Linear")
    {
      cudaErrCheck(cudaMemcpy(
        _outputTensor[t],
        _prevLayer->_outputTensor[t],
        N * OC * sizeof(float),
        cudaMemcpyDeviceToDevice));
    }
    else if (_function == "Softmax")
    {
      cudnnErrCheck(cudnnSoftmaxForward(
        _nn->_cuDNNHandle,
        CUDNN_SOFTMAX_LOG,
        CUDNN_SOFTMAX_MODE_CHANNEL,
        &_alpha,
        _prevLayer->_outputTensorDesc,
        _prevLayer->_outputTensor[t],
        &_beta,
        _outputTensorDesc,
        _outputTensor[t]));
    }
    else
    {
      cudnnErrCheck(cudnnActivationForward(
        _nn->_cuDNNHandle,
        _activationDesc,
        &_alpha,
        _prevLayer->_outputTensorDesc,
        _prevLayer->_outputTensor[t],
        &_beta,
        _outputTensorDesc,
        _outputTensor[t]));
    }
  }
#endif
}

void Activation::backwardData(const size_t t)
{
  size_t N = _batchSize;
  size_t OC = _outputChannels;

  if (_nn->_mode == "Inference")
    KORALI_LOG_ERROR("Requesting Layer backward data propagation but NN was configured for inference only.\n");

  if (_nn->_engine == "Korali")
  {
    if (_function == "Elementwise/Linear")
      for (size_t i = 0; i < N * OC; i++)
        _prevLayer->_outputGradient[i] = _outputGradient[i] * _alpha;

    if (_function == "Elementwise/Log")
      for (size_t i = 0; i < N * OC; i++)
        _prevLayer->_outputGradient[i] = _outputGradient[i] / _prevLayer->_outputValues[i];

    if (_function == "Elementwise/ReLU")
    {
      for (size_t i = 0; i < N * OC; i++)
        if (_prevLayer->_outputValues[i] > 0.0f)
        {
          _prevLayer->_outputGradient[i] = _outputGradient[i];
        }
        else
        {
          _prevLayer->_outputGradient[i] = _outputGradient[i] * _alpha;
        }
    }
    if (_function == "Elementwise/SoftReLU")
      for (size_t i = 0; i < N * OC; i++)
      {
        const float expOutVal = std::exp(_outputValues[i]);
        _prevLayer->_outputGradient[i] = _outputGradient[i] * (expOutVal - 1.0f) / expOutVal;
      }

    if (_function == "Elementwise/Tanh")
      for (size_t i = 0; i < N * OC; i++)
        _prevLayer->_outputGradient[i] = _outputGradient[i] * (1.0f - _outputValues[i] * _outputValues[i]);

    if (_function == "Elementwise/Logistic")
      for (size_t i = 0; i < N * OC; i++)
        _prevLayer->_outputGradient[i] = _outputGradient[i] * _outputValues[i] * (1.0f - _outputValues[i]);

    if (_function == "Elementwise/SoftSign")
      for (size_t i = 0; i < N * OC; i++)
        _prevLayer->_outputGradient[i] = _outputGradient[i] / ((1.0f + std::abs(_outputValues[i])) * (1.0f + std::abs(_outputValues[i])));

    if (_function == "Softmax")
      for (size_t i = 0; i < N * OC; i++)
        _prevLayer->_outputGradient[i] = _outputGradient[i] * _outputValues[i] * (1.0f - _outputValues[i]);
  }

#ifdef _KORALI_USE_ONEDNN
  if (_nn->_engine == "OneDNN")
  {
    // Primitive arguments.
    _backwardActivationArgs[DNNL_ARG_DIFF_DST] = _outputGradientMem[t];             // Input
    _backwardActivationArgs[DNNL_ARG_SRC] = _prevLayer->_outputMem[t];              // Input
    _backwardActivationArgs[DNNL_ARG_DIFF_SRC] = _prevLayer->_outputGradientMem[t]; // Output
    if (_function == "Softmax") _backwardActivationArgs[DNNL_ARG_DST] = _outputMem[t];

    _backwardActivationPrimitive.execute(_nn->_dnnlStream, _backwardActivationArgs);
  }
#endif

#ifdef _KORALI_USE_CUDNN
  if (_nn->_engine == "CuDNN")
  {
    if (_function == "Elementwise/Linear")
    {
      cudaErrCheck(cudaMemcpy(
        _prevLayer->_outputGradientTensor[t],
        _outputGradientTensor[t],
        N * OC * sizeof(float),
        cudaMemcpyDeviceToDevice));
    }
    else if (_function == "Softmax")
    {
      cudnnErrCheck(cudnnSoftmaxBackward(
        _nn->_cuDNNHandle,
        CUDNN_SOFTMAX_LOG,
        CUDNN_SOFTMAX_MODE_CHANNEL,
        &_alpha,
        _outputTensorDesc,
        _outputTensor[t],
        _outputTensorDesc,
        _outputGradientTensor[t],
        &_beta,
        _prevLayer->_outputTensorDesc,
        _prevLayer->_outputGradientTensor[t]));
    }
    else
    {
      cudnnErrCheck(cudnnActivationBackward(
        _nn->_cuDNNHandle,
        _activationDesc,
        &_alpha,
        _outputTensorDesc,
        _outputTensor[t],
        _outputTensorDesc,
        _outputGradientTensor[t],
        _prevLayer->_outputTensorDesc,
        _prevLayer->_outputTensor[t],
        &_beta,
        _prevLayer->_outputTensorDesc,
        _prevLayer->_outputGradientTensor[t]));
    }
  }
#endif
}

void Activation::setConfiguration(knlohmann::json& js)
{
 if (isDefined(js, "Results"))  eraseValue(js, "Results");

 if (isDefined(js, "Function"))
 {
 try { _function = js["Function"].get<std::string>();
} catch (const std::exception& e)
 { KORALI_LOG_ERROR(" + Object: [ activation ] \n + Key:    ['Function']\n%s", e.what()); }
{
 bool validOption = false;
 if (_function == "Elementwise/Clip") validOption = true;
 if (_function == "Elementwise/Linear") validOption = true;
 if (_function == "Elementwise/Log") validOption = true;
 if (_function == "Elementwise/Logistic") validOption = true;
 if (_function == "Elementwise/ReLU") validOption = true;
 if (_function == "Elementwise/SoftReLU") validOption = true;
 if (_function == "Elementwise/SoftSign") validOption = true;
 if (_function == "Elementwise/Tanh") validOption = true;
 if (_function == "Softmax") validOption = true;
 if (validOption == false) KORALI_LOG_ERROR(" + Unrecognized value (%s) provided for mandatory setting: ['Function'] required by activation.\n", _function.c_str());
}
   eraseValue(js, "Function");
 }
  else   KORALI_LOG_ERROR(" + No value provided for mandatory setting: ['Function'] required by activation.\n");

 if (isDefined(js, "Alpha"))
 {
 try { _alpha = js["Alpha"].get<float>();
} catch (const std::exception& e)
 { KORALI_LOG_ERROR(" + Object: [ activation ] \n + Key:    ['Alpha']\n%s", e.what()); }
   eraseValue(js, "Alpha");
 }
  else   KORALI_LOG_ERROR(" + No value provided for mandatory setting: ['Alpha'] required by activation.\n");

 if (isDefined(js, "Beta"))
 {
 try { _beta = js["Beta"].get<float>();
} catch (const std::exception& e)
 { KORALI_LOG_ERROR(" + Object: [ activation ] \n + Key:    ['Beta']\n%s", e.what()); }
   eraseValue(js, "Beta");
 }
  else   KORALI_LOG_ERROR(" + No value provided for mandatory setting: ['Beta'] required by activation.\n");

 Layer::setConfiguration(js);
 _type = "layer/activation";
 if(isDefined(js, "Type")) eraseValue(js, "Type");
 if(isEmpty(js) == false) KORALI_LOG_ERROR(" + Unrecognized settings for Korali module: activation: \n%s\n", js.dump(2).c_str());
}

void Activation::getConfiguration(knlohmann::json& js)
{

 js["Type"] = _type;
   js["Function"] = _function;
   js["Alpha"] = _alpha;
   js["Beta"] = _beta;
 Layer::getConfiguration(js);
}

void Activation::applyModuleDefaults(knlohmann::json& js)
{

 std::string defaultString = "{\"Alpha\": 1.0, \"Beta\": 0.0}";
 knlohmann::json defaultJs = knlohmann::json::parse(defaultString);
 mergeJson(js, defaultJs);
 Layer::applyModuleDefaults(js);
}

void Activation::applyVariableDefaults()
{

 Layer::applyVariableDefaults();
}

;

} //layer
} //neuralNetwork
} //korali
;


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

#ifndef CRYPTO3_DETAIL_ENDIAN_SHIFT_HPP
#define CRYPTO3_DETAIL_ENDIAN_SHIFT_HPP

#include <boost/assert.hpp>

#include <nil/crypto3/detail/stream_endian.hpp>
#include <nil/crypto3/detail/basic_functions.hpp>
#include <nil/crypto3/detail/unbounded_shift.hpp>

namespace nil {
    namespace crypto3 {
        namespace detail {

            template<typename Endianness, std::size_t WordBits>
            struct endian_shift;

            template<int UnitBits, std::size_t WordBits>
            struct endian_shift<stream_endian::big_unit_big_bit<UnitBits>, WordBits>
                : public basic_functions<WordBits> {

                constexpr static const std::size_t word_bits = basic_functions<WordBits>::word_bits;
                typedef typename basic_functions<WordBits>::word_type word_type;

                static word_type &to_msb(word_type &w, std::size_t shift) {
                    // shift to most significant bits according to endianness
                    w = unbounded_shl(w, shift);
                    return w;
                }
            };

            template<int UnitBits, std::size_t WordBits>
            struct endian_shift<stream_endian::little_unit_big_bit<UnitBits>, WordBits>
                : public basic_functions<WordBits> {

                constexpr static const std::size_t word_bits = basic_functions<WordBits>::word_bits;
                typedef typename basic_functions<WordBits>::word_type word_type;

                static word_type &to_msb(word_type &w, std::size_t shift) {
                    // shift to most significant bits according to endianness
                    std::size_t shift_rem = shift % UnitBits;
                    std::size_t shift_unit_bits = shift - shift_rem;

                    std::size_t sz[2] = {UnitBits - shift_rem, shift_rem};
                    word_type masks[2];
                    masks[0] = unbounded_shl(low_bits<word_bits>(~word_type(), sz[0]), shift_unit_bits);
                    masks[1] =
                        unbounded_shl(low_bits<word_bits>(~word_type(), sz[1]), shift_unit_bits + UnitBits + sz[0]);
                    std::size_t bits_left = word_bits - shift;

                    word_type w_combined = 0;
                    int ind = 0;

                    while (bits_left) {
                        w_combined |= (!ind ? unbounded_shl(w & masks[0], shift_rem) :
                                              unbounded_shr(w & masks[1], UnitBits + sz[0]));
                        bits_left -= sz[ind];
                        masks[ind] = unbounded_shl(masks[ind], UnitBits);
                        ind = 1 - ind;
                    }

                    w = unbounded_shr(w_combined, shift_unit_bits);
                    return w;
                }
            };

            template<int UnitBits, std::size_t WordBits>
            struct endian_shift<stream_endian::big_unit_little_bit<UnitBits>, WordBits>
                : public basic_functions<WordBits> {

                constexpr static const std::size_t word_bits = basic_functions<WordBits>::word_bits;
                typedef typename basic_functions<WordBits>::word_type word_type;

                static word_type &to_msb(word_type &w, std::size_t shift) {
                    // shift to most significant bits according to endianness
                    std::size_t shift_rem = shift % UnitBits;
                    std::size_t shift_unit_bits = shift - shift_rem;

                    std::size_t sz[2] = {UnitBits - shift_rem, shift_rem};
                    word_type masks[2] = {
                        unbounded_shr(high_bits<word_bits>(~word_type(), sz[0]), shift_unit_bits),
                        unbounded_shr(high_bits<word_bits>(~word_type(), sz[1]), shift_unit_bits + UnitBits + sz[0])};

                    std::size_t bits_left = word_bits - shift;
                    word_type w_combined = 0;
                    int ind = 0;

                    while (bits_left) {
                        w_combined |= (!ind ? unbounded_shr(w & masks[0], shift_rem) :
                                              unbounded_shl(w & masks[1], UnitBits + sz[0]));
                        bits_left -= sz[ind];
                        masks[ind] = unbounded_shr(masks[ind], UnitBits);
                        ind = 1 - ind;
                    }

                    w = unbounded_shl(w_combined, shift_unit_bits);
                    return w;
                }
            };

            template<int UnitBits, std::size_t WordBits>
            struct endian_shift<stream_endian::little_unit_little_bit<UnitBits>, WordBits>
                : public basic_functions<WordBits> {

                constexpr static const std::size_t word_bits = basic_functions<WordBits>::word_bits;
                typedef typename basic_functions<WordBits>::word_type word_type;

                static word_type &to_msb(word_type &w, std::size_t shift) {

                    // shift to most significant bits according to endianness
                    w = unbounded_shr(w, shift);
                    return w;
                }
            };
        }    // namespace detail
    }        // namespace crypto3
}    // namespace nil

#endif    // CRYPTO3_DETAIL_ENDIAN_SHIFT_HPP


/*
 * Copyright 2014-2017 Jouni Roivas
 */

#include "blieng/point.h"
#include "blieng.h"

#include <boost/format.hpp>
#include <math.h>

#include <string>

using blieng::Point;

Point::~Point()
{
    x = 0;
    y = 0;
}

std::string Point::toString() const
{
    return (boost::format("%f,%f") % x % y).str();
}

bool Point::operator==(const Point &other) const
{
    return (x == other.x && y == other.y);
}

bool Point::operator!=(const Point &other) const
{
    return (x != other.x || y != other.y);
}

Point Point::operator+(const Point &other)
{
    Point res(x + other.x, y + other.y);
    return res;
}

Point Point::operator-(const Point &other)
{
    Point res(x - other.x, y - other.y);
    return res;
}

Point &Point::operator+=(const Point &other)
{
    x += other.x;
    y += other.y;
    return *this;
}

Point &Point::operator-=(const Point &other)
{
    x -= other.x;
    y -= other.y;
    return *this;
}

void Point::update(Point another)
{
    x = another.x;
    y = another.y;
}

Point Point::semiPoint(Point target, double pos) const
{
    if (pos < 0) return *this;
    if (pos >= 1.0) return target;

    double nposx = target.x - x;
    double nposy = target.y - y;

    nposx *= pos;
    nposy *= pos;

    nposx += x;
    nposy += y;

    return Point(nposx, nposy);
}

Point Point::traverse(Point target, double now, double time) const
{
    // Travel to another point, in specific time, specify now as current time
    if (now <= 0) return *this;
    if (now > time) return target;

    double nposx, nposy;
    nposx = target.x - x;
    nposy = target.y - y;
    nposx = (nposx * now / time) + x;
    nposy = (nposy * now / time) + y;

    return Point(nposx, nposy);
}

double Point::length(Point another)
{
    double dx = x - another.x;
    double dy = y - another.y;
    if (dx < 0) dx *= -1;
    if (dy < 0) dy *= -1;

    // return sqrt(dx*dx + dy*dy);
    return dx+dy;
}

double Point::lengthGeo(Point another)
{
    double dlat = (x - another.x) * blieng::PI / 180.0;
    double dlon = (y - another.y) * blieng::PI / 180.0;

    double a = sin(dlat / 2) * sin(dlat / 2) +
        cos(another.x * blieng::PI / 180.0) * cos(x * blieng::PI / 180.0) *
        sin(dlon / 2) * sin(dlon / 2);
    double c = 2 * atan2(sqrt(a), sqrt(1 - a));
    double d = blieng::RADIUS * c;

    // Meters
    return d;
}

Point Point::geoToMeters()
{
    double latF = 2 * blieng::RADIUS * blieng::PI / 360.0;
    double lngF = latF * cos(x * blieng::PI / 180.0);

    return Point(x * latF, y * lngF);
}

blieng::Complex Point::toComplex() const
{
    return Complex(x, y);
}


//---------------------------------------------------------------------------//
// Copyright (c) 2018-2020 Mikhail Komarov <nemo@nil.foundation>
//
// Distributed under the Boost Software License, Version 1.0
// See accompanying file LICENSE_1_0.txt or copy at
// http://www.boost.org/LICENSE_1_0.txt
//---------------------------------------------------------------------------//

#ifndef CRYPTO3_BLOCK_BASIC_SHACAL_HPP
#define CRYPTO3_BLOCK_BASIC_SHACAL_HPP

#include <boost/crypto3/block/detail/shacal/shacal_policy.hpp>
#include <boost/crypto3/block/detail/shacal/shacal1_policy.hpp>

#include <boost/static_assert.hpp>

#ifdef CRYPTO3_BLOCK_SHOW_PROGRESS
#include <cstdio>
#endif

namespace boost {
    namespace crypto3 {
        namespace block {
            /*!
             * @ingroup block
             *
             * Encrypt implemented directly from the SHA standard as found at
             * http://csrc.nist.gov/publications/fips/fips180-2/fips180-2.pdf
             *
             * Decrypt is a straight-forward inverse
             *
             * In SHA terminology:
             * - plaintext = H^(i-1)
             * - ciphertext = H^(i)
             * - key = M^(i)
             * - schedule = W
             *
             * The algorithms for SHA(-0) and SHA-1 are identical apart from the
             * key scheduling, so encapsulate that as a class that takes an
             * already-prepared schedule.  (Constructor is protected to help keep
             * people from accidentally giving it just a key in a schedule.)
             */
            class basic_shacal {
            protected:
                typedef detail::shacal_policy policy_type;

            public:
                constexpr static const std::size_t word_bits = policy_type::word_bits;
                typedef policy_type::word_type word_type;

                constexpr static const std::size_t key_bits = policy_type::key_bits;
                constexpr static const std::size_t key_words = policy_type::key_words;
                typedef policy_type::key_type key_type;

                constexpr static const std::size_t block_bits = policy_type::block_bits;
                constexpr static const std::size_t block_words = policy_type::block_words;
                typedef policy_type::block_type block_type;

                constexpr static const std::size_t rounds = policy_type::rounds;
                typedef policy_type::schedule_type schedule_type;

            protected:
                basic_shacal(const schedule_type &s) : schedule(s) {
                }

                virtual ~basic_shacal() {
                    schedule.fill(0);
                }

            public:
                inline block_type encrypt(const block_type &plaintext) const {
                    return encrypt_block(schedule, plaintext);
                }

                inline block_type decrypt(const block_type &ciphertext) const {
                    return decrypt_block(schedule, ciphertext);
                }

            private:
                schedule_type schedule;

                inline static block_type encrypt_block(const schedule_type &schedule, const block_type &plaintext) {

#ifdef CRYPTO3_BLOCK_SHOW_PROGRESS
                    for (unsigned t = 0; t < block_words; ++t) {
                        std::printf(word_bits == 32 ? "H[%d] = %.8x\n" : "H[%d] = %.16lx\n", t, plaintext[t]);
                    }
#endif

                    // Initialize working variables with block
                    word_type a = plaintext[0], b = plaintext[1], c = plaintext[2], d = plaintext[3], e = plaintext[4];

                    // Encipher block
#ifdef CRYPTO3_BLOCK_NO_OPTIMIZATION

                    for (unsigned t = 0; t < rounds; ++t) {
                        word_type T = policy_type::rotl<5>(a) + policy_type::f(t, b, c, d) + e +
                                      policy_type::constants[t] + round_constants_words[t];

                        e = d;
                        d = c;
                        c = policy_type::rotl<30>(b);
                        b = a;
                        a = T;

#ifdef CRYPTO3_BLOCK_SHOW_PROGRESS
                        printf(word_bits == 32 ? "t = %2d: %.8x %.8x %.8x %.8x %.8x\n" :
                                                 "t = %2d: %.16lx %.16lx %.16lx %.16lx %.16lx\n",
                               t, a, b, c, d, e);
#endif
                    }

#else    // CRYPTO3_BLOCK_NO_OPTIMIZATION

#ifdef CRYPTO3_BLOCK_SHOW_PROGRESS
#define CRYPTO3_BLOCK_SHACAL1_TRANSFORM_PROGRESS                                                                      \
    printf(word_bits == 32 ? "t = %2d: %.8x %.8x %.8x %.8x %.8x\n" : "t = %2d: %.16lx %.16lx %.16lx %.16lx %.16lx\n", \
           t, a, b, c, d, e);
#else
#define CRYPTO3_BLOCK_SHACAL1_TRANSFORM_PROGRESS
#endif

#define CRYPTO3_BLOCK_SHACAL1_TRANSFORM                                                                               \
    word_type T = policy_type::rotl<5>(a) + policy_type::f(t, b, c, d) + e + policy_type::constants[t] + schedule[t]; \
    e = d;                                                                                \
    d = c;                                                                                \
    c = policy_type::rotl<30>(b);                                                                                \
    b = a;                                                                                \
    a = T;                                                                                \
    CRYPTO3_BLOCK_SHACAL1_TRANSFORM_PROGRESS

                    BOOST_STATIC_ASSERT(rounds == 80);
                    BOOST_STATIC_ASSERT(rounds % block_words == 0);
                    for (unsigned t = 0; t < 20;) {
                        for (int n = block_words; n--; ++t) {
                            CRYPTO3_BLOCK_SHACAL1_TRANSFORM
                        }
                    }
                    for (unsigned t = 20; t < 40;) {
                        for (int n = block_words; n--; ++t) {
                            CRYPTO3_BLOCK_SHACAL1_TRANSFORM
                        }
                    }
                    for (unsigned t = 40; t < 60;) {
                        for (int n = block_words; n--; ++t) {
                            CRYPTO3_BLOCK_SHACAL1_TRANSFORM
                        }
                    }
                    for (unsigned t = 60; t < 80;) {
                        for (int n = block_words; n--; ++t) {
                            CRYPTO3_BLOCK_SHACAL1_TRANSFORM
                        }
                    }

#endif

                    return {{a, b, c, d, e}};
                }

                inline static block_type decrypt_block(const schedule_type &schedule, const block_type &ciphertext) {

#ifdef CRYPTO3_BLOCK_SHOW_PROGRESS
                    for (unsigned t = 0; t < block_words; ++t) {
                        std::printf(word_bits == 32 ? "H[%d] = %.8x\n" : "H[%d] = %.16lx\n", t, ciphertext[t]);
                    }
#endif

                    // Initialize working variables with block
                    word_type a = ciphertext[0], b = ciphertext[1], c = ciphertext[2], d = ciphertext[3],
                              e = ciphertext[4];

                    // Decipher block
                    for (unsigned t = rounds; t--;) {
                        word_type T = a;

                        a = b;
                        b = policy_type::rotr<30>(c);
                        c = d;
                        d = e;
                        e = T - policy_type::rotl<5>(a) - policy_type::f(t, b, c, d) - policy_type::constants[t] -
                            schedule[t];

#ifdef CRYPTO3_BLOCK_SHOW_PROGRESS
                        std::printf(word_bits == 32 ? "t = %2d: %.8x %.8x %.8x %.8x %.8x\n" :
                                                      "t = %2d: %.16lx %.16lx %.16lx %.16lx %.16lx\n",
                                    t, a, b, c, d, e);
#endif
                    }

                    return {{a, b, c, d, e}};
                }
            };
        }    // namespace block
    }        // namespace crypto3
}    // namespace boost

#endif    // CRYPTO3_BLOCK_CIPHERS_BASIC_SHACAL_HPP


/* Copyright (c) 2020 C. Pattison
 * 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.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#pragma once
#include <cassert>
#include <algorithm>
#include <utility>
#include <complex>
#include <random>
#include <exception>
#include <cmath>

#include <Eigen/Dense>
#include "syk_types.hpp"
#include "pauli.hpp"
#include "util.hpp"

/** Generation for SYK Hamiltonians
 */

namespace syk {
using namespace std::complex_literals;

/** Utility class for construction Majorana fermion representations
 */
struct FermionRep : PauliRep {
protected:
    int num_fermions_;

public:

    FermionRep(int num_fermions) : PauliRep(num_fermions/2), num_fermions_(num_fermions) {
        if(!(num_fermions > 0 && num_fermions % 2 == 0)) { throw std::runtime_error("Number of fermions must be positive and even"); }
    }

    Pauli get_pauli_factor(int a, int qubit_index) {
        assert(0 <= a && a < num_fermions_);
        assert(0 <= qubit_index && qubit_index < num_qubits_);
        if (a/2 == qubit_index) { return (a%2 == 1) ? PauliY() : PauliX(); }
        if (a/2 < qubit_index)  { return PauliI(); }
        if (a/2 > qubit_index)  { return PauliZ(); }

        assert(false);
        return PauliI();
    }

    /** Four body interaction terms
     */
    PauliString four_fermion(int a, int b, int c, int d) {
        PauliString pstring;
        for(int qubit_index = 0; qubit_index < num_qubits_; ++qubit_index) {
            pstring[qubit_index] =
                  get_pauli_factor(a, qubit_index)
                * get_pauli_factor(b, qubit_index)
                * get_pauli_factor(c, qubit_index)
                * get_pauli_factor(d, qubit_index);
        }
        return pstring;
    }

    /** Returns the parity operator (gamma_*) for the Clifford algebra repp
     */
    PauliString gamma_star() const {
        PauliString pstring;
        pstring.fill(PauliZ());
        return pstring;
    }
};

/** Return all numbers of even hamming weight below size
 */
std::vector<std::uint64_t> hamming_weight(std::uint64_t size, bool odd = false) {

    std::vector<std::uint64_t> idx;
    idx.reserve(size/2);
    for (std::uint64_t i = 0; i < size; ++i) {
        if (static_cast<bool>(__builtin_popcount(i) % 2) == odd) {
            idx.push_back(i);
        }
    }

    return idx;
}

/** Generate an SYK Hamiltonian
 * Returns only a single parity sector
 */
template<typename rng_type>
__attribute__((optimize("fast-math")))
MatrixType syk_hamiltonian(rng_type* rng, int N, double J) {
    auto hilbert_space_size = (1 << (N+1)/2);
    auto repp = FermionRep(N);

    auto distr = std::normal_distribution<double>(0, J/std::pow(static_cast<double>(N), 1.5));

    std::vector<FermionRep::Term> interactions;
    interactions.reserve(N*N*N);
    for(int i = 0; i < N; ++i) {
        for(int j = 0; j < i; ++j) {
            for(int k = 0; k < j; ++k) {
                for(int l = 0; l < k; ++l) {
                    interactions.push_back(FermionRep::Term { repp.four_fermion(i, j, k, l), distr(*rng)} );
                }
            }
        }
    }

    // The parity projection (1 + gamma_*) removes all rows/columns with odd hamming weight
    auto even_weight = hamming_weight(hilbert_space_size);
    auto hamiltonian = repp.get_hamiltonian(interactions, even_weight);

    return hamiltonian;
}

/** Returns a lambda for computing the spectral form factor at some time
 */
auto spectral_form_factor(const std::vector<double>& eigenvals) {
    return [=](std::complex<double> beta) -> double {
        auto z_part = util::transform_reduce(eigenvals.cbegin(), eigenvals.cend(), 0.0i,
            std::plus<>(), [=](auto a) { return std::exp(a * beta); });
        return std::real(z_part * std::conj(z_part));
    };
}

/** Vector of doubles to Eigen::VectorXd
 */
auto to_eigen_vector(const std::vector<double>& vals) {
    Eigen::Matrix<MatrixType::Scalar, Eigen::Dynamic, 1> vec(vals.size());
    std::copy(vals.begin(), vals.end(), vec.data());
    return vec;
}
}


// Copyright 2015-2020 The ALMA Project Developers
//
// 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.

/// @file
///
/// Definitions of geometry_2d.hpp

#include <geometry_2d.hpp>
#include <io_utils.hpp>
#include <boost/property_tree/xml_parser.hpp>
#include <utilities.hpp>

namespace alma {

Eigen::MatrixXd geometry_2d::bounding_box(polygon& p) {
    Eigen::Matrix2d limits;
    Eigen::MatrixXd points;
    std::size_t cols = 0;
    boost::geometry::for_each_point(p, [&cols](point& i) { cols++; });
    points.resize(2, cols);
    cols = 0;
    boost::geometry::for_each_point(p, [&cols, &points](point& i) {
        points(0, cols) = boost::geometry::get<0>(i);
        points(1, cols) = boost::geometry::get<1>(i);
        cols++;
    });
    // Xmin
    limits(0, 0) = points.row(0).minCoeff();
    // Xmax
    limits(1, 0) = points.row(0).maxCoeff();
    // Ymin
    limits(0, 1) = points.row(1).minCoeff();
    // Ymax
    limits(1, 1) = points.row(1).maxCoeff();

    return limits;
}

void geometry_2d::get_sides(Eigen::MatrixXd& vertices_) {
    if (vertices_.rows() != 2) {
        throw alma::input_error("Error in provided vertices,"
                                " bad dimension\n");
    }
    for (auto i = 0; i < vertices_.cols(); i++) {
        Eigen::Vector2d p1 = vertices_.col(i);
        for (auto j = i; j < vertices_.cols(); j++) {
            Eigen::Vector2d p2 = vertices_.col(j);
            /// If same point pass
            if ((p1 - p2).norm() < 1.0e-6)
                continue;
            segment s({p1(0), p1(1)}, {p2(0), p2(1)});
            point sc;
            /// Looking for midpoint
            boost::geometry::centroid(s, sc);
            /// To prevent numerical issues about boder
            /// we slightly displace the point
            /// away from hull center
            Eigen::Vector2d ncsc;
            ncsc << sc.get<0>() - this->center(0),
                sc.get<1>() - this->center(1);

            if (ncsc.norm() == 0.) {
                continue;
            }

            double nc = 1. / ncsc.norm();
            /// Normalize and multiply by the eps
            ncsc(0) *= 1.0e-4 * nc;
            ncsc(1) *= 1.0e-4 * nc;
            point fp(ncsc(0) + sc.get<0>(), ncsc(1) + sc.get<1>());

            bool valid_segment = boost::geometry::covered_by(fp, this->hull);
            /// If valid segment
            if (!valid_segment) {
                geom2d_border b;
                b.p1 = p1;
                b.p2 = p2;
                b.nd = (p2 - p1);
                // Build it to point outside
                Eigen::Vector2d np_t;
                np_t << b.nd(1), -b.nd(0);
                if (np_t.dot(ncsc) < 0.) {
                    np_t *= -1;
                }
                b.np = np_t / np_t.norm();

                auto pa = (p1 - 1.0e-6 * b.nd).eval();
                auto pb = (p2 + 1.0e-6 * b.nd).eval();

                segment sf({pa(0), pa(1)}, {pb(0), pb(1)});

                b.sb = s;
                b.sbl = sf;

                if (std::find_if(borders.begin(),
                                 borders.end(),
                                 [&b](const geom2d_border& a) -> bool {
                                     if ((a.np - b.np).norm() < 1.0e-6)
                                         return true;
                                     return false;
                                 }) == borders.end()) {
                    borders.push_back(b);
                }

                if (borders.size() >
                    static_cast<std::size_t>(vertices_.cols())) {
                    throw alma::geometry_error("To much borders");
                }
            }
        }
    }
}

geometry_2d::geometry_2d(Eigen::MatrixXd& vertices_) {
    //     if (vertices_.cols()!=3) {
    //         std::cerr << "Error in vertices, we are only accepting
    //         triangles\n"; exit(EXIT_FAILURE);
    //     }

    polygon poly;

    for (auto i = 0; i < vertices_.cols(); i++) {
        double x = vertices_(0, i);
        double y = vertices_(1, i);
        boost::geometry::append(poly, boost::make_tuple(x, y));
    }
    /// Creating convex hull
    boost::geometry::convex_hull(poly, this->hull);
    /// Calculating centroid
    point c;
    boost::geometry::centroid(this->hull, c);
    this->center << c.get<0>(), c.get<1>();
    /// Calculating area
    this->area = boost::geometry::area(this->hull);
    /// Geting borders
    get_sides(vertices_);
    /// Getting bounding_box
    this->bbox = bounding_box(this->hull);
}

bool geometry_2d::inside(Eigen::Vector2d& point2check) const {
    point p(point2check(0), point2check(1));
    return boost::geometry::covered_by(p, this->hull);
}

std::tuple<double, Eigen::Vector2d, std::vector<int>>
geometry_2d::get_inter_side(Eigen::Vector2d& r0,
                            Eigen::Vector2d& v,
                            double dt) {
    point p0(r0(0), r0(1));
    point pf(r0(0) + dt * v(0), r0(1) + dt * v(1));
    segment s(p0, pf);

    double time = 1.0e+7;
    Eigen::Vector2d rf;
    int border_id = 0;
    std::vector<int> bids;
    bool already_there = true;

    for (auto& b : borders) {
        /// To rule out already there stuff
        if (boost::geometry::intersects(s, b.sbl) and
            !boost::geometry::covered_by(p0, b.sbl)) {
            if (b.np.dot(v) > 0.) {
                std::vector<point> rinter;
                boost::geometry::intersection(s, b.sbl, rinter);

                double newtime =
                    std::max({(std::abs(v(0)) < 1.0e-6)
                                  ? -1
                                  : (rinter[0].get<0>() - r0(0)) / v(0),
                              (std::abs(v(1)) < 1.0e-6)
                                  ? -1
                                  : (rinter[0].get<1>() - r0(1)) / v(1)});

                /// Corner case
                if (newtime == time) {
                    bids.push_back(border_id);
                }

                if (newtime < time) {
                    bids.clear();
                    rf(0) = rinter[0].get<0>();
                    rf(1) = rinter[0].get<1>();
                    time = newtime;
                    already_there = false;
                    bids.push_back(border_id);
                }
            }
        }

        border_id++;
    }

    /// In the case already there and going outside
    /// of box
    if (already_there) {
        point p0p(r0(0) - 1.0e-6 * v(0), r0(1) - 1.0e-6 * v(1));
        segment sp(p0p, pf);

        border_id = 0;

        for (auto& b : borders) {
            /// To rule out already there stuff
            if (boost::geometry::intersects(sp, b.sbl)) {
                if (b.np.dot(v) > 0.) {
                    std::vector<point> rinter;
                    boost::geometry::intersection(sp, b.sbl, rinter);

                    double newtime =
                        std::max({(std::abs(v(0)) < 1.0e-6)
                                      ? -1.0e+8
                                      : (rinter[0].get<0>() - r0(0)) / v(0),
                                  (std::abs(v(1)) < 1.0e-6)
                                      ? -1.0e+8
                                      : (rinter[0].get<1>() - r0(1)) / v(1)});

                    /// Corner case
                    if (newtime == time) {
                        bids.push_back(border_id);
                    }

                    if (std::abs(newtime) < time) {
                        bids.clear();
                        rf(0) = rinter[0].get<0>();
                        rf(1) = rinter[0].get<1>();
                        time = newtime;
                        bids.push_back(border_id);
                    }
                }
            }

            border_id++;
        }

        if (time > 1.0e+6) {
            //             std::cout << "Error in get_inter_side\n";
            //             std::cout << time << '\t' << dt << std::endl;
            //             std::cout << "v\n" << v << std::endl;
            //             std::cout << "r0\n" << r0 << std::endl;
            //             for (auto &b : borders) {
            //                 std::cout << "nb\n"<< b.np << std::endl;
            //                 std::cout << std::boolalpha <<
            //                 boost::geometry::intersects(sp, b.sbl) << '\t'
            //                 << boost::geometry::intersects(s, b.sbl) << '\t'
            //                 << boost::geometry::covered_by(pf,b.sb) << '\t'
            //                 << boost::geometry::covered_by(p0,b.sb) <<
            //                 std::endl;
            //             }
            //             std::cout << "box_id: " << this->get_id() <<
            //             std::endl;
            // 	    Eigen::Vector2d rf_ = r0+dt*v;
            //             std::cout << std::boolalpha <<
            //                 this->inside(r0) << '\t'  << std::endl;
            //             std::cout << "rf:\n" << rf_ << std::endl <<
            //             std::boolalpha <<
            //                 this->inside(rf_) << '\t' << std::endl;
            throw alma::geometry_error("Error to huge time");
        }
    }

    /// This is to clean out some numerical noise
    /// from geometric library
    if (alma::almost_equal(time, 0.))
        time = 0.;

    if (time < 0.) {
        std::cout << "Error in get_inter_side\n";
        std::cout << time << std::endl;
        std::cout << "v\n" << v << std::endl;
        std::cout << "r0\n" << r0 << std::endl;
        std::cout << "rf\n" << rf << std::endl;
        throw alma::geometry_error("Bad time");
    }

    return std::make_tuple(time, rf, bids);
}

void geometry_2d::calculate_contacts(std::vector<geometry_2d>& system) {
    /// First get real contacts
    for (auto& element : system) {
        if (element.id == this->id)
            continue;
        if (boost::geometry::intersects(this->hull, element.hull)) {
            std::vector<point> pinter;
            /// This gives the 2 points of the segment
            boost::geometry::intersection(this->hull, element.hull, pinter);

            /// Border object
            geom2d_border cb;

            /// If point contact
            if (pinter.size() == 1) {
                cb.p1(0) = pinter[0].get<0>();
                cb.p1(1) = pinter[0].get<1>();
                cb.p2(0) = pinter[0].get<0>();
                cb.p2(1) = pinter[0].get<1>();

                cb.nd = Eigen::Vector2d::Zero();
                cb.sb = segment(pinter[0], pinter[0]);
                cb.np = cb.p1 - this->get_center();
                cb.np /= cb.np.norm();

                contacts.emplace(
                    std::make_pair(element.id, std::vector<geom2d_border>{cb}));
                continue;
            }

            cb.p1(0) = pinter[0].get<0>();
            cb.p1(1) = pinter[0].get<1>();
            cb.p2(0) = pinter[1].get<0>();
            cb.p2(1) = pinter[1].get<1>();

            if ((cb.p2 - cb.p1).norm() < 1.0e-6) {
                cb.p1(0) = pinter[0].get<0>();
                cb.p1(1) = pinter[0].get<1>();
                cb.p2(0) = pinter[1].get<0>();
                cb.p2(1) = pinter[1].get<1>();

                cb.nd = Eigen::Vector2d::Zero();
                cb.sb = segment(pinter[0], pinter[1]);
                cb.np = cb.p1 - this->get_center();
                cb.np /= cb.np.norm();

                contacts.emplace(
                    std::make_pair(element.id, std::vector<geom2d_border>{}));
                continue;
            }

            /// Get director vector of segment
            cb.nd = (cb.p2 - cb.p1); //
                                     //(cb.p2 - cb.p1).norm();
            /// Get perpendicular vector pointing outside
            segment s(pinter[0], pinter[1]);
            point sc;
            /// Looking for midpoint
            boost::geometry::centroid(s, sc);
            /// To prevent numerical issues about boder
            /// we slightly displace the point
            /// away from hull center
            Eigen::Vector2d ncsc;
            ncsc << sc.get<0>() - this->center(0),
                sc.get<1>() - this->center(1);
            Eigen::Vector2d np_t;
            np_t << cb.nd(1), -cb.nd(0);
            if (np_t.dot(ncsc) < 0.) {
                np_t *= -1;
            }
            cb.np = np_t / np_t.norm();
            cb.sb = s;

            auto pa = (cb.p1 - 1.0e-6 * cb.nd / cb.nd.norm()).eval();
            auto pb = (cb.p2 + 1.0e-6 * cb.nd / cb.nd.norm()).eval();
            segment sf({pa(0), pa(1)}, {pb(0), pb(1)});
            cb.sbl = sf;

            contacts.emplace(
                std::make_pair(element.id, std::vector<geom2d_border>{cb}));
        }
    }

    if (contacts.size() == 0 and system.size() > 1) {
        throw alma::geometry_error("There are unconnected boxes");
    }
}

/// Helper function definitions
///@param[in] r point to check if tri-or-higher
///         intersection
///@param[in] gs vector containing the geometry
std::pair<bool, std::vector<std::size_t>> in_corner3(
    Eigen::Vector2d& r,
    std::vector<geometry_2d>& gs) {
    std::size_t ic = 0;
    std::vector<std::size_t> ids;
    ids.reserve(gs.size());
    boost::tuple<double, double> p(r(0), r(1));
    for (auto& g : gs) {
        if (boost::geometry::covered_by(p, g.get_poly())) {
            ids.push_back(g.get_id());
            ic++;
        }
    }

    if (ic > 2) {
        return std::make_pair(true, ids);
    }
    return std::make_pair(false, ids);
}

std::vector<alma::geometry_2d> read_geometry_XML(std::string xmlfname) {
    // Vector containing geometry
    std::vector<alma::geometry_2d> geometries;

    // Create empty property tree object
    boost::property_tree::ptree tree;

    // Parse XML input file into the tree
    boost::property_tree::read_xml(xmlfname, tree);

    for (const auto& v : tree.get_child("Geometry")) {
        if (v.first == "number_of_boxes") {
            std::size_t ngeometries =
                alma::parseXMLfield<std::size_t>(v, "Ngeom");
            geometries.reserve(ngeometries);
        }
        /// Iterate throught boxes
        if (v.first == "Box") {
            auto box_tree = v.second;
            std::string matname;
            Eigen::MatrixXd vertices;
            double Teq = -1.;
            bool periodic = false;
            std::size_t box2translate;
            Eigen::Vector2d translation;
            bool reservoir = false;
            double theta = 0.;
            double Treal = -1.;

            for (auto it = box_tree.begin(); it != box_tree.end(); it++) {
                /// Parsing name
                if (it->first == "MaterialID")
                    matname = alma::parseXMLfield<std::string>(*it, "name");
                // Parsing vertices:
                if (it->first == "Vertices") {
                    int rows, cols;
                    rows = alma::parseXMLfield<int>(*it, "dim");
                    cols = alma::parseXMLfield<int>(*it, "npoints");
                    vertices.resize(rows, cols);

                    std::stringstream datass(
                        (box_tree).get<std::string>("Vertices"));

                    for (auto i = 0; i < rows * cols; i++) {
                        datass >> vertices.data()[i];
                    }
                }
                /// Equilibrium temperature
                if (it->first == "initCnd") {
                    Teq = alma::parseXMLfield<double>(*it, "Teq");
                    if (alma::probeXMLfield<double>(*it, "Tinit")) {
                        Treal = alma::parseXMLfield<double>(*it, "Tinit");
                    }
                    else {
                        Treal = Teq;
                    }
                }
                /// Parsing periodic:
                if (it->first == "Translate_to") {
                    box2translate = alma::parseXMLfield<std::size_t>(*it, "id");

                    periodic = true;

                    std::stringstream datass(
                        (box_tree).get<std::string>("Translate_to"));

                    datass >> translation(0) >> translation(1);
                }
                /// Reservoir
                if (it->first == "Reservoir")
                    reservoir = true;
                /// Parsing angle:
                if (it->first == "theta")
                    theta = alma::parseXMLfield<double>(*it, "angle");
            }

            /// Set properties
            alma::geometry_2d g(vertices);
            if (periodic) {
                g.periodic = true;
                g.translation = translation;
                g.box2translate = box2translate;
            }
            if (reservoir)
                g.reservoir = true;

            g.Teq = Teq;
            g.Treal = Treal;
            g.material = matname;
            g.theta = theta;
            g.rotmat.setZero();
            /// Obtain rotation matrix
            if (theta == 0.) {
                g.rotmat = Eigen::Matrix2d::Identity();
            }
            else {
                g.rotmat(0, 0) = std::cos(theta);
                g.rotmat(1, 1) = std::cos(theta);
                g.rotmat(0, 1) = -std::sin(theta);
                g.rotmat(1, 0) = std::sin(theta);
            }
            geometries.push_back(g);
        }
    }

    /// Assing ids
    assign_geom_ids(geometries);
    /// Calculate contacts
    for (auto& g : geometries)
        g.calculate_contacts(geometries);

    return geometries;
}

Eigen::MatrixXd calculate_gradientT(std::vector<alma::geometry_2d>& sys) {
    Eigen::MatrixXd gradients(3, sys.size());
    gradients.setZero();

    for (auto& s : sys) {
        if (s.reservoir or s.periodic)
            continue;
        /// Get data for gradient
        /// calculation
        double T0 = s.Teq;
        auto id0 = s.get_id();
        Eigen::Vector2d c0 = s.get_center();

        auto& contacts = s.get_contacts();

        std::size_t csize = 0;
        for (auto& [c, b] : contacts) {
            if (!sys[c].reservoir and !sys[c].periodic)
                csize++;
        }

        /// We want to solve:
        /// f(r0+hi) - f(r0) = h_i * gradf
        /// dfi = hi*gradf
        /// gradf = MPinv(hi)*dfi
        /// where MPinv is the Moore–Penrose inverse
        Eigen::VectorXd dfi(csize);
        Eigen::MatrixXd hi(csize, 2);

        int ci = 0;
        for (auto& [c, b] : contacts) {
            if (sys[c].reservoir or sys[c].periodic)
                continue;
            dfi(ci) = sys[c].Teq - T0;
            Eigen::Vector2d hi_ = sys[c].get_center() - c0;
            hi.row(ci) = hi_;
            ci++;
        }

        /// The actual MPinv is not used
        Eigen::CompleteOrthogonalDecomposition<Eigen::MatrixXd> cqr(hi);

        /// But we solve the minimum-norm solution gradT
        /// to a least squares problem
        Eigen::Vector2d gradT = cqr.solve(dfi);

        gradients(0, id0) = gradT(0);
        gradients(1, id0) = gradT(1);
    }
    return gradients;
}

}; // namespace alma


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

#ifndef CRYPTO3_PUBKEY_BLS_HPP
#define CRYPTO3_PUBKEY_BLS_HPP

#include <map>
#include <vector>
#include <iterator>
#include <type_traits>
#include <utility>
#include <functional>

#include <boost/assert.hpp>
#include <boost/concept_check.hpp>

#include <boost/range/concepts.hpp>

#include <boost/mpl/vector.hpp>

#include <nil/crypto3/detail/stream_endian.hpp>

#include <nil/crypto3/algebra/curves/bls12.hpp>
#include <nil/crypto3/algebra/pairing/bls12.hpp>

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

#include <nil/crypto3/pubkey/detail/bls/bls_basic_policy.hpp>
#include <nil/crypto3/pubkey/detail/bls/bls_basic_functions.hpp>
#include <nil/crypto3/pubkey/keys/private_key.hpp>
#include <nil/crypto3/pubkey/operations/aggregate_op.hpp>
#include <nil/crypto3/pubkey/operations/aggregate_verify_op.hpp>
#include <nil/crypto3/pubkey/operations/aggregate_verify_single_msg_op.hpp>

namespace nil {
    namespace crypto3 {
        namespace pubkey {
            /*!
             * @brief Basic BLS Scheme
             * @tparam SignatureVersion
             * @tparam BlsParams
             * @see https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-bls-signature-04#section-3.1
             */
            template<typename SignatureVersion>
            struct bls_basic_scheme {
                typedef SignatureVersion signature_version;
                typedef typename signature_version::basic_functions basic_functions;

                typedef typename basic_functions::private_key_type private_key_type;
                typedef typename basic_functions::public_key_type public_key_type;
                typedef typename basic_functions::signature_type signature_type;

                typedef typename basic_functions::internal_accumulator_type internal_accumulator_type;
                typedef typename basic_functions::internal_aggregation_accumulator_type
                    internal_aggregation_accumulator_type;

                static inline public_key_type generate_public_key(const private_key_type &privkey) {
                    return basic_functions::privkey_to_pubkey(privkey);
                }

                static inline void init_accumulator(internal_accumulator_type &acc, const private_key_type &privkey) {
                }

                static inline void init_accumulator(internal_accumulator_type &acc, const public_key_type &pubkey) {
                }

                template<typename InputRange>
                static inline void update(internal_accumulator_type &acc, const InputRange &range) {
                    basic_functions::update(acc, range);
                }

                template<typename InputIterator>
                static inline void update(internal_accumulator_type &acc, InputIterator first, InputIterator last) {
                    basic_functions::update(acc, first, last);
                }

                static inline signature_type sign(internal_accumulator_type &acc, const private_key_type &privkey) {
                    return basic_functions::sign(acc, privkey);
                }

                static inline bool verify(internal_accumulator_type &acc, const public_key_type &pubkey,
                                          const signature_type &sig) {
                    return basic_functions::verify(acc, pubkey, sig);
                }

                template<typename SignatureRange>
                static inline void update_aggregate(signature_type &acc, const SignatureRange &signatures) {
                    basic_functions::aggregate(acc, signatures);
                }

                template<typename SignatureIterator>
                static inline void update_aggregate(signature_type &acc, SignatureIterator sig_first,
                                                    SignatureIterator sig_last) {
                    basic_functions::aggregate(acc, sig_first, sig_last);
                }

                static inline bool aggregate_verify(internal_aggregation_accumulator_type &acc,
                                                    const signature_type &signature) {
                    // TODO: add check - If any two input messages are equal, return INVALID.
                    return basic_functions::aggregate_verify(acc, signature);
                }
            };

            /*!
             * @brief
             * @tparam SignatureVersion
             * @tparam BlsParams
             * @see https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-bls-signature-04#section-3.2
             */
            template<typename SignatureVersion>
            struct bls_aug_scheme {
                typedef SignatureVersion signature_version;
                typedef typename signature_version::basic_functions basic_functions;

                typedef typename basic_functions::private_key_type private_key_type;
                typedef typename basic_functions::public_key_type public_key_type;
                typedef typename basic_functions::signature_type signature_type;

                typedef typename basic_functions::internal_accumulator_type internal_accumulator_type;
                typedef typename basic_functions::internal_aggregation_accumulator_type
                    internal_aggregation_accumulator_type;

                static inline public_key_type generate_public_key(const private_key_type &privkey) {
                    return basic_functions::privkey_to_pubkey(privkey);
                }

                static inline void init_accumulator(internal_accumulator_type &acc, const private_key_type &privkey) {
                    init_accumulator(acc, generate_public_key(privkey));
                }

                static inline void init_accumulator(internal_accumulator_type &acc, const public_key_type &pubkey) {
                    basic_functions::update(acc, basic_functions::point_to_pubkey(pubkey));
                }

                template<typename InputRange>
                static inline void update(internal_accumulator_type &acc, const InputRange &range) {
                    basic_functions::update(acc, range);
                }

                template<typename InputIterator>
                static inline void update(internal_accumulator_type &acc, InputIterator first, InputIterator last) {
                    basic_functions::update(acc, first, last);
                }

                static inline signature_type sign(internal_accumulator_type &acc, const private_key_type &privkey) {
                    return basic_functions::sign(acc, privkey);
                }

                static inline bool verify(internal_accumulator_type &acc, const public_key_type &pubkey,
                                          const signature_type &sig) {
                    return basic_functions::verify(acc, pubkey, sig);
                }

                template<typename SignatureRange>
                static inline void update_aggregate(signature_type &acc, const SignatureRange &signatures) {
                    basic_functions::aggregate(acc, signatures);
                }

                template<typename SignatureIterator>
                static inline void update_aggregate(signature_type &acc, SignatureIterator sig_first,
                                                    SignatureIterator sig_last) {
                    basic_functions::aggregate(acc, sig_first, sig_last);
                }

                static inline bool aggregate_verify(internal_aggregation_accumulator_type &acc,
                                                    const signature_type &signature) {
                    return basic_functions::aggregate_verify(acc, signature);
                }
            };

            /*!
             * @brief Proof of possession BLS Scheme
             * @tparam SignatureVersion
             * @tparam BlsParams
             * @see https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-bls-signature-04#section-3.3
             */
            template<typename SignatureVersion>
            struct bls_pop_scheme {
                typedef SignatureVersion signature_version;
                typedef typename signature_version::basic_functions basic_functions;

                typedef typename basic_functions::private_key_type private_key_type;
                typedef typename basic_functions::public_key_type public_key_type;
                typedef typename basic_functions::signature_type signature_type;

                typedef typename basic_functions::internal_accumulator_type internal_accumulator_type;
                typedef typename basic_functions::internal_aggregation_accumulator_type
                    internal_aggregation_accumulator_type;
                typedef typename basic_functions::internal_fast_aggregation_accumulator_type
                    internal_fast_aggregation_accumulator_type;

                static inline public_key_type generate_public_key(const private_key_type &privkey) {
                    return basic_functions::privkey_to_pubkey(privkey);
                }

                static inline void init_accumulator(internal_accumulator_type &acc, const private_key_type &privkey) {
                }

                static inline void init_accumulator(internal_accumulator_type &acc, const public_key_type &pubkey) {
                }

                template<typename InputRange>
                static inline void update(internal_accumulator_type &acc, const InputRange &range) {
                    basic_functions::update(acc, range);
                }

                template<typename InputIterator>
                static inline void update(internal_accumulator_type &acc, InputIterator first, InputIterator last) {
                    basic_functions::update(acc, first, last);
                }

                static inline signature_type sign(internal_accumulator_type &acc, const private_key_type &privkey) {
                    return basic_functions::sign(acc, privkey);
                }

                static inline bool verify(internal_accumulator_type &acc, const public_key_type &pubkey,
                                          const signature_type &sig) {
                    return basic_functions::verify(acc, pubkey, sig);
                }

                template<typename SignatureRange>
                static inline void update_aggregate(signature_type &acc, const SignatureRange &signatures) {
                    basic_functions::aggregate(acc, signatures);
                }

                template<typename SignatureIterator>
                static inline void update_aggregate(signature_type &acc, SignatureIterator sig_first,
                                                    SignatureIterator sig_last) {
                    basic_functions::aggregate(acc, sig_first, sig_last);
                }

                static inline bool aggregate_verify(internal_aggregation_accumulator_type &acc,
                                                    const signature_type &signature) {
                    return basic_functions::aggregate_verify(acc, signature);
                }

                static inline bool aggregate_verify(internal_fast_aggregation_accumulator_type &acc,
                                                    const signature_type &signature) {
                    return basic_functions::aggregate_verify(acc, signature);
                }

                static inline signature_type pop_prove(const private_key_type &privkey) {
                    return basic_functions::pop_prove(privkey);
                }

                static inline bool pop_verify(const public_key_type &pubkey, const signature_type &proof) {
                    return basic_functions::pop_verify(pubkey, proof);
                }
            };

            //
            // Minimal-signature-size
            // Random oracle version of hash-to-point
            //
            template<typename PublicParams, typename CurveType = algebra::curves::bls12_381>
            struct bls_mss_ro_version {
                typedef detail::bls_mss_ro_policy<PublicParams, CurveType> policy_type;
                typedef detail::bls_basic_functions<policy_type> basic_functions;
            };

            //
            // Minimal-pubkey-size
            // Random oracle version of hash-to-point
            //
            template<typename PublicParams, typename CurveType = algebra::curves::bls12_381>
            struct bls_mps_ro_version {
                typedef detail::bls_mps_ro_policy<PublicParams, CurveType> policy_type;
                typedef detail::bls_basic_functions<policy_type> basic_functions;
            };

            template<hashes::UniformityCount _uniformity_count = hashes::UniformityCount::uniform_count,
                     hashes::ExpandMsgVariant _expand_msg_variant = hashes::ExpandMsgVariant::rfc_xmd>
            struct bls_default_public_params {
                constexpr static hashes::UniformityCount uniformity_count = _uniformity_count;
                constexpr static hashes::ExpandMsgVariant expand_msg_variant = _expand_msg_variant;

                // "BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_"
                typedef std::array<std::uint8_t, 43> dst_type;
                static constexpr dst_type dst = {0x42, 0x4c, 0x53, 0x5f, 0x53, 0x49, 0x47, 0x5f, 0x42, 0x4c, 0x53,
                                                 0x31, 0x32, 0x33, 0x38, 0x31, 0x47, 0x32, 0x5f, 0x58, 0x4d, 0x44,
                                                 0x3a, 0x53, 0x48, 0x41, 0x2d, 0x32, 0x35, 0x36, 0x5f, 0x53, 0x53,
                                                 0x57, 0x55, 0x5f, 0x52, 0x4f, 0x5f, 0x4e, 0x55, 0x4c, 0x5f};
            };

            template<hashes::UniformityCount _uniformity_count = hashes::UniformityCount::uniform_count,
                     hashes::ExpandMsgVariant _expand_msg_variant = hashes::ExpandMsgVariant::rfc_xmd>
            struct bls_pop_prove_default_public_params {
                constexpr static hashes::UniformityCount uniformity_count = _uniformity_count;
                constexpr static hashes::ExpandMsgVariant expand_msg_variant = _expand_msg_variant;

                typedef std::vector<std::uint8_t> dst_type;
                static inline dst_type dst = []() {
                    const std::string _dst_str = "BLS_POP_BLS12381G1_XMD:SHA-256_SSWU_RO_POP_";
                    const std::vector<std::uint8_t> _dst(_dst_str.begin(), _dst_str.end());
                    return _dst;
                }();
            };

            template<hashes::UniformityCount _uniformity_count = hashes::UniformityCount::uniform_count,
                     hashes::ExpandMsgVariant _expand_msg_variant = hashes::ExpandMsgVariant::rfc_xmd>
            struct bls_pop_sign_default_public_params {
                constexpr static hashes::UniformityCount uniformity_count = _uniformity_count;
                constexpr static hashes::ExpandMsgVariant expand_msg_variant = _expand_msg_variant;

                typedef std::vector<std::uint8_t> dst_type;
                static inline dst_type dst = []() {
                    const std::string _dst_str = "BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_POP_";
                    const std::vector<std::uint8_t> _dst(_dst_str.begin(), _dst_str.end());
                    return _dst;
                }();
            };

            template<typename PublicParams = bls_default_public_params<>,
                     template<typename, typename> class BlsVersion = bls_mss_ro_version,
                     template<typename> class BlsScheme = bls_basic_scheme,
                     typename CurveType = algebra::curves::bls12_381>
            struct bls {
                typedef bls<PublicParams, BlsVersion, BlsScheme, CurveType> self_type;
                typedef BlsVersion<PublicParams, CurveType> bls_version_type;
                typedef BlsScheme<bls_version_type> bls_scheme_type;

                typedef public_key<self_type> public_key_type;
                typedef private_key<self_type> private_key_type;
                typedef aggregate_op<self_type> aggregate_op_policy;
                typedef aggregate_verify_op<self_type> aggregate_verify_op_policy;
            };

            template<typename PublicParams, template<typename, typename> class BlsVersion,
                     template<typename> class BlsScheme, typename CurveType>
            struct public_key<bls<PublicParams, BlsVersion, BlsScheme, CurveType>> {
                typedef bls<PublicParams, BlsVersion, BlsScheme, CurveType> scheme_type;
                typedef typename scheme_type::bls_scheme_type bls_scheme_type;

                typedef typename bls_scheme_type::private_key_type private_key_type;
                typedef typename bls_scheme_type::public_key_type public_key_type;
                typedef typename bls_scheme_type::signature_type signature_type;

                typedef typename public_key_type::group_type public_key_group_type;
                typedef typename signature_type::group_type signature_group_type;

                typedef typename bls_scheme_type::internal_accumulator_type internal_accumulator_type;

                typedef public_key_type key_type;

                public_key() = delete;
                public_key(const key_type &pubkey) : pubkey(pubkey) {
                }

                inline void init_accumulator(internal_accumulator_type &acc) const {
                    bls_scheme_type::init_accumulator(acc, pubkey);
                }

                template<typename InputRange>
                static inline void update(internal_accumulator_type &acc, const InputRange &range) {
                    bls_scheme_type::update(acc, range);
                }

                template<typename InputIterator>
                static inline void update(internal_accumulator_type &acc, InputIterator first, InputIterator last) {
                    bls_scheme_type::update(acc, first, last);
                }

                inline bool verify(internal_accumulator_type &acc, const signature_type &sig) const {
                    return bls_scheme_type::verify(acc, pubkey, sig);
                }

                inline public_key_type public_key_data() const {
                    return pubkey;
                }

                // TODO: refactor pop
                template<typename FakeAccumulator>
                inline bool pop_verify(FakeAccumulator, const signature_type &proof) const {
                    return bls_scheme_type::pop_verify(pubkey, proof);
                }

                // FIXME: copy pubkey between equivalent public keys is a bottleneck
                // TODO: support using of the same pubkey even if scheme policy differs in public params and scheme type
                template<typename ToPublicParams, template<typename> class ToBlsScheme>
                operator public_key<bls<ToPublicParams, BlsVersion, ToBlsScheme, CurveType>>() const {
                    return public_key<bls<ToPublicParams, BlsVersion, BlsScheme, CurveType>>(pubkey);
                }

            protected:
                public_key_type pubkey;
            };

            template<typename PublicParams, template<typename, typename> class BlsVersion,
                     template<typename> class BlsScheme, typename CurveType>
            struct private_key<bls<PublicParams, BlsVersion, BlsScheme, CurveType>>
                : public public_key<bls<PublicParams, BlsVersion, BlsScheme, CurveType>> {
                typedef bls<PublicParams, BlsVersion, BlsScheme, CurveType> scheme_type;
                typedef typename scheme_type::bls_scheme_type bls_scheme_type;
                typedef public_key<scheme_type> base_type;

                typedef typename base_type::private_key_type private_key_type;
                typedef typename base_type::public_key_type public_key_type;
                typedef typename base_type::signature_type signature_type;

                typedef typename bls_scheme_type::internal_accumulator_type internal_accumulator_type;

                typedef private_key_type key_type;

                private_key() = delete;
                private_key(const key_type &privkey) :
                    privkey(privkey), base_type(bls_scheme_type::generate_public_key(privkey)) {
                }

                inline void init_accumulator(internal_accumulator_type &acc) const {
                    bls_scheme_type::init_accumulator(acc, privkey);
                }

                template<typename InputRange>
                static inline void update(internal_accumulator_type &acc, const InputRange &range) {
                    bls_scheme_type::update(acc, range);
                }

                template<typename InputIterator>
                static inline void update(internal_accumulator_type &acc, InputIterator first, InputIterator last) {
                    bls_scheme_type::update(acc, first, last);
                }

                inline signature_type sign(internal_accumulator_type &acc) const {
                    return bls_scheme_type::sign(acc, privkey);
                }

                inline signature_type pop_prove() const {
                    return bls_scheme_type::pop_prove(privkey);
                }

                // FIXME: copy privkey between equivalent private keys is a bottleneck
                // TODO: support using of the same privkey even if scheme policy differs in public params and scheme
                //  type
                template<typename ToPublicParams, template<typename> class ToBlsScheme>
                operator private_key<bls<ToPublicParams, BlsVersion, ToBlsScheme, CurveType>>() const {
                    return private_key<bls<ToPublicParams, BlsVersion, BlsScheme, CurveType>>(privkey);
                }

            protected:
                private_key_type privkey;
            };

            template<typename PublicParams, template<typename, typename> class BlsVersion,
                     template<typename> class BlsScheme, typename CurveType>
            struct aggregate_op<bls<PublicParams, BlsVersion, BlsScheme, CurveType>> {
                typedef bls<PublicParams, BlsVersion, BlsScheme, CurveType> scheme_type;
                typedef typename scheme_type::bls_scheme_type bls_scheme_type;

                typedef typename bls_scheme_type::private_key_type private_key_type;
                typedef typename bls_scheme_type::public_key_type public_key_type;
                typedef typename bls_scheme_type::signature_type signature_type;

                typedef signature_type internal_accumulator_type;
                typedef signature_type result_type;

                static inline void init_accumulator(internal_accumulator_type &acc) {
                    acc = signature_type::zero();
                }

                template<typename InputRange>
                static inline void update(internal_accumulator_type &acc, const InputRange &range) {
                    bls_scheme_type::update_aggregate(acc, range);
                }

                template<typename InputIterator>
                static inline void update(internal_accumulator_type &acc, InputIterator first, InputIterator last) {
                    bls_scheme_type::update_aggregate(acc, first, last);
                }

                static inline result_type process(internal_accumulator_type &acc) {
                    return acc;
                }
            };

            template<typename PublicParams, template<typename, typename> class BlsVersion,
                     template<typename> class BlsScheme, typename CurveType>
            struct aggregate_verify_op<bls<PublicParams, BlsVersion, BlsScheme, CurveType>> {
                typedef bls<PublicParams, BlsVersion, BlsScheme, CurveType> scheme_type;
                typedef typename scheme_type::bls_scheme_type bls_scheme_type;
                typedef public_key<scheme_type> scheme_public_key_type;

                typedef typename bls_scheme_type::private_key_type private_key_type;
                typedef typename bls_scheme_type::public_key_type public_key_type;
                typedef typename bls_scheme_type::signature_type signature_type;

                typedef typename bls_scheme_type::internal_accumulator_type _internal_accumulator_type;
                typedef typename bls_scheme_type::internal_aggregation_accumulator_type
                    _internal_aggregation_accumulator_type;
                typedef _internal_aggregation_accumulator_type internal_accumulator_type;
                typedef bool result_type;

                static inline void init_accumulator(internal_accumulator_type &acc) {
                }

                template<typename InputIterator>
                static inline void update(internal_accumulator_type &acc, const scheme_public_key_type &scheme_pubkey,
                                          InputIterator first, InputIterator last) {
                    auto index = get_public_key_index(acc, scheme_pubkey);
                    bls_scheme_type::update(acc.second[index], first, last);
                }

                template<typename InputRange>
                static inline void update(internal_accumulator_type &acc, const scheme_public_key_type &scheme_pubkey,
                                          const InputRange &range) {
                    auto index = get_public_key_index(acc, scheme_pubkey);
                    bls_scheme_type::update(acc.second[index], range);
                }

                static inline result_type process(internal_accumulator_type &acc, const signature_type &sig) {
                    return bls_scheme_type::aggregate_verify(acc, sig);
                }

            private:
                static inline std::size_t get_public_key_index(internal_accumulator_type &acc,
                                                               const scheme_public_key_type &scheme_pubkey) {
                    assert(std::distance(std::cbegin(acc.first), std::cend(acc.first)) ==
                           std::distance(std::cbegin(acc.second), std::cend(acc.second)));

                    auto found_pos_it =
                        std::find(std::cbegin(acc.first), std::cend(acc.first), scheme_pubkey.public_key_data());

                    if (std::cend(acc.first) == found_pos_it) {
                        acc.first.push_back(scheme_pubkey.public_key_data());
                        acc.second.push_back(_internal_accumulator_type());
                        bls_scheme_type::init_accumulator(acc.second.back(), acc.first.back());
                        return std::size(acc.first) - 1;
                    }

                    return std::distance(std::cbegin(acc.first), found_pos_it);
                }
            };

            template<typename PublicParams, template<typename, typename> class BlsVersion, typename CurveType>
            struct aggregate_verify_single_msg_op<bls<PublicParams, BlsVersion, bls_pop_scheme, CurveType>> {
                typedef bls<PublicParams, BlsVersion, bls_pop_scheme, CurveType> scheme_type;
                typedef typename scheme_type::bls_scheme_type bls_scheme_type;
                typedef public_key<scheme_type> scheme_public_key_type;

                typedef typename bls_scheme_type::private_key_type private_key_type;
                typedef typename bls_scheme_type::public_key_type public_key_type;
                typedef typename bls_scheme_type::signature_type signature_type;

                typedef typename bls_scheme_type::internal_accumulator_type _internal_accumulator_type;
                typedef typename bls_scheme_type::internal_fast_aggregation_accumulator_type
                    _internal_fast_aggregation_accumulator_type;
                typedef _internal_fast_aggregation_accumulator_type internal_accumulator_type;
                typedef bool result_type;

                static inline void init_accumulator(internal_accumulator_type &acc) {
                }

                template<typename InputIterator>
                static inline typename std::enable_if<!std::is_convertible<
                    typename std::iterator_traits<InputIterator>::value_type, scheme_public_key_type>::value>::type
                    update(internal_accumulator_type &acc, InputIterator first, InputIterator last) {
                    bls_scheme_type::update(acc.second, first, last);
                }

                template<typename InputRange>
                static inline typename std::enable_if<
                    !std::is_convertible<typename std::iterator_traits<typename InputRange::iterator>::value_type,
                                         scheme_public_key_type>::value>::type
                    update(internal_accumulator_type &acc, const InputRange &range) {
                    bls_scheme_type::update(acc.second, range);
                }

                template<typename InputIterator>
                static inline typename std::enable_if<std::is_convertible<
                    typename std::iterator_traits<InputIterator>::value_type, scheme_public_key_type>::value>::type
                    update(internal_accumulator_type &acc, InputIterator first, InputIterator last) {
                    for (auto iter = first; iter != last; ++iter) {
                        update(acc, *iter);
                    }
                }

                template<typename InputRange>
                static inline typename std::enable_if<
                    std::is_convertible<typename std::iterator_traits<typename InputRange::iterator>::value_type,
                                        scheme_public_key_type>::value>::type
                    update(internal_accumulator_type &acc, const InputRange &range) {
                    for (const auto &scheme_pubkey : range) {
                        update(acc, scheme_pubkey);
                    }
                }

                static inline void update(internal_accumulator_type &acc, const scheme_public_key_type &scheme_pubkey) {
                    auto found_pos_it =
                        std::find(std::cbegin(acc.first), std::cend(acc.first), scheme_pubkey.public_key_data());
                    if (std::cend(acc.first) == found_pos_it) {
                        acc.first.push_back(scheme_pubkey.public_key_data());
                    }
                }

                static inline result_type process(internal_accumulator_type &acc, const signature_type &sig) {
                    return bls_scheme_type::aggregate_verify(acc, sig);
                }
            };
        }    // namespace pubkey
    }        // namespace crypto3
}    // namespace nil

#endif    // CRYPTO3_PUBKEY_BLS_HPP