//
//  Copyright © 2015 Claus Christmann <hcc |ä| gatech.edu>.
//
//  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.
//

#include "vroniinterface.h"

/* VRONI header files
 *   These header files are part of the VRONI source code
 *   NOTE: The order of inlcusion does matter!
 */
#include <vroni.h>

#include <boost/geometry/algorithms/envelope.hpp>
#include <boost/geometry/algorithms/reverse.hpp>
#include <QtCore/QHash>
#include <QtCore/QDebug>

#include <unordered_map>

#include "Graphics/arrowitem.h"

VroniInterface::VroniInterface()
{
  // from VRONI docu:
  // call this routine once prior calling any other routine of VRONI
  VRONI::API_InitializeProgram();

  VRONI::quiet = true; // suppress printfs
}

VroniInterface::~VroniInterface()
{
  // from VRONI docu:
  // call this routine to release all memory allocated by VRONI
  VRONI::API_TerminateProgram();
}

void VroniInterface::resetAll(void )
{
  // call VRONI's API funtion
  VRONI::API_ResetAll();

  // mark the (now erased) VD/DT data as not current
  m_vdIsCurrent = false;
  m_isPointSite = true;
}

void VroniInterface::setVroniSiteData(const Geometry::Polygon_2D& complexPolygon)
{
  resetAll(); // reset VRONI's data...
  m_isPointSite = false;

  namespace bg = boost::geometry;

  // get the containing box of the polygon
  bg::model::box<Geometry::Point_2D> box;
  bg::envelope(complexPolygon,box);

  // set the scaling box from that
  auto x_min = box.min_corner().get<0>();
  auto y_min = box.min_corner().get<1>();
  auto x_max = box.max_corner().get<0>();
  auto y_max = box.max_corner().get<1>();

  m_scalingBox = ScalingBox(x_min,y_min,x_max,y_max); //NOTE: Warning, this could include an implicit conversion to double!

  // add the outer polygon
  addElementToSite( complexPolygon.outer());

  // add the inner holes, i.e. the obstacles
  auto obstacleList =  complexPolygon.inners();
  for(Geometry::Polygon_2D::ring_type const & ring : obstacleList)
  {
    addElementToSite(ring);
  }

  boundingBox();
}

void VroniInterface::setVroniSiteData(const Geometry::Polygon_2D& boundary, const std::unordered_map<ElementId, Obstacle >& obstacleData)
{
  resetAll(); // reset VRONI's data...
  m_isPointSite = false;

//   qDebug() << "ENTERING setVroniSiteData";
//   printVroniSiteData();

  namespace bg = boost::geometry;

  // get the containing box of the polygon
  bg::model::box<Geometry::Point_2D> box;
  bg::envelope(boundary,box);

  // set the scaling box from that
  auto x_min = box.min_corner().get<0>();
  auto y_min = box.min_corner().get<1>();
  auto x_max = box.max_corner().get<0>();
  auto y_max = box.max_corner().get<1>();

  m_scalingBox = ScalingBox(x_min,y_min,x_max,y_max); //NOTE: Warning, this could include an implicit conversion to double!

  // add the outer boundary to the site and add the shape identifier "0"
  int index = 0;

  Geometry::Point_2D centroid;
  boost::geometry::centroid(boundary.outer(),centroid);
  shapeCentroidMap.clear();
  shapeCentroidMap[index] = centroid;

  addElementToSite( boundary.outer(), index++);

  // add the obstacles to the site_file
  for( auto& pair : obstacleData  )
  {
    Obstacle const& obstacle = pair.second;

    //NOTE: we need to make a copy of the outer obstacle shape so that  we can
    // use bg::reverse. This is important so that the boundary and the obstacle
    // shapes have different orientations so that VRONI can correctly determined
    // "holes" and the "inside" of the arena.
    Geometry::Polygon_2D::ring_type tempRing;
    bg::assign(tempRing,obstacle.shape().outer());
    bg::reverse(tempRing);

    Geometry::Point_2D centroid;
    boost::geometry::centroid(tempRing,centroid);
    shapeCentroidMap[index] = centroid;

    addElementToSite( tempRing, index++ );
  }

  // update the bounding box
  boundingBox();

}

void VroniInterface::setVroniSiteData(const Geometry::MultiPoint_2D& mulitPoint)
{
  resetAll(); // reste VRONI's internal site data
  m_isPointSite = true;

  namespace bg = boost::geometry;

  // get the containing box of the point set
  boost::geometry::model::box<Geometry::Point_2D> box;
  boost::geometry::envelope(mulitPoint,box);

  // set the scaling box from that
  auto x_min = box.min_corner().get<0>();
  auto y_min = box.min_corner().get<1>();
  auto x_max = box.max_corner().get<0>();
  auto y_max = box.max_corner().get<1>();

  m_scalingBox = ScalingBox(x_min,y_min,x_max,y_max); //NOTE: Warning, this could include an implicit conversion to double!

  // add the points to the internal VRONI site data set
  addElementToSite(mulitPoint);

  boundingBox();

}

void VroniInterface::addElementToSite(const Geometry::Polygon_2D::ring_type& ring, VRONI::eas_type data)
{
  using namespace VRONI;

  m_isPointSite = false; //NOTE: this function adds non-points to the VRONI site

  // get the number of edges of the geometry
  std::size_t num_edges = boost::geometry::num_points(ring) - 1; // this assumes that the ring is closed!

  // preallocate a VRONI in_segs array to hold the edges' data
  in_segs* edges = new in_segs[num_edges];

  // populate the edges array
  int i=0;
  for( auto it_point=ring.begin() ; it_point !=--(ring.end()); /* no action */)
  {
    edges[i].x1 = boost::geometry::get<0>(*it_point); //NOTE: Warning: this could cause an implicit conversion
    edges[i].y1 = boost::geometry::get<1>(*it_point); //NOTE: Warning: this could cause an implicit conversion
    ++it_point;
    edges[i].x2 = boost::geometry::get<0>(*it_point); //NOTE: Warning: this could cause an implicit conversion
    edges[i].y2 = boost::geometry::get<1>(*it_point); //NOTE: Warning: this could cause an implicit conversion

    edges[i].ext_appl = data;
    ++i;
  }

  // store whether or not VRONI determined the API input to be new or not
  boolean new_input = false;

  // Call the VRONI API to add the edge segments representing the QPolygon
  API_ArrayInput(
    0,nullptr,   // not adding any points
    num_edges, edges,   // adding num_edges segments, stored in/at edges
    0,nullptr,     // not adding any arcs
    &new_input    // check if VRONI determines this is a new input
  );

  if( new_input )
  {
    // reset the Voronoi diagram tracker
    m_vdIsCurrent = false;
  }

  // free the allocated memory temporarily holding the edges.
  // VRONIs API_ArrayInput performs an internal copy-by-value, hence this is
  // safe
  delete[] edges;
}

void VroniInterface::addElementToSite(const Geometry::Point_2D& point)
{
  using namespace VRONI;

  // get/set the number of points to add
  int num_points = 1;

  // preallocate a VRONI in_pnts array to hold the points' data
  in_pnts * points = new in_pnts[num_points];

  // populate the points array
  int i = 0;
  {
    points[i].x1 =  boost::geometry::get<0>(point);
    points[i].y1 =  boost::geometry::get<1>(point);
  }

  // store whether or not VRONI determined the API input to be new or not
  boolean new_input = false;

  // Call the VRONI API to add the edge segments representing the QPolygon
  API_ArrayInput(
    num_points, points,   // adding num_points elements, stored in/at points
    0,nullptr,   // not adding any edges
    0,nullptr,     // not adding any arcs
    &new_input    // check if VRONI determines this is a new input
  );

  if( new_input )
  {
    // reset the Voronoi diagram tracker
    m_vdIsCurrent = false;
  }

  // free the allocated memory temporarily holding the edges.
  // VRONIs API_ArrayInput performs an internal copy-by-value, hence this is
  // safe
  delete[] points;

}

void VroniInterface::addElementToSite(const Geometry::MultiPoint_2D& mulitPoint)
{
  using namespace VRONI;

  // get/set the number of points to add
  int num_points = boost::geometry::num_geometries(mulitPoint);

  // preallocate a VRONI in_pnts array to hold the points' data
  in_pnts * points = new in_pnts[num_points];

  // populate the points array
  int i = 0;
  for( auto & point : mulitPoint )
  {
    points[i].x1 =  boost::geometry::get<0>(point);
    points[i].y1 =  boost::geometry::get<1>(point);
    ++i;
  }

  // store whether or not VRONI determined the API input to be new or not
  boolean new_input = false;

  // Call the VRONI API to add the edge segments representing the QPolygon
  API_ArrayInput(
    num_points, points,   // adding num_points elements, stored in/at points
    0,nullptr,   // not adding any edges
    0,nullptr,     // not adding any arcs
    &new_input    // check if VRONI determines this is a new input
  );

  if( new_input )
  {
    // reset the Voronoi diagram tracker
    m_vdIsCurrent = false;
  }

  // free the allocated memory temporarily holding the edges.
  // VRONIs API_ArrayInput performs an internal copy-by-value, hence this is
  // safe
  delete[] points;
}

#ifdef WITH_GRAPHICS

QGraphicsItemGroup* VroniInterface::getSiteGraphics()
{
  auto group = new QGraphicsItemGroup;

  QColor pntColor = Qt::red;    /// PNT site color
  QColor segColor = Qt::blue;   /// SEG site color
  QColor arcColor = Qt::green;  /// ARC site color

//   using LineGraphics = QGraphicsLineItem;
  using LineGraphics = Graphics_2D::Arrow;
  QPen segPen = QPen(segColor);

  using ArcGraphics = QGraphicsEllipseItem;
  QPen arcPen = QPen(arcColor);
  const bool approxArcsByLines = true;
  const bool drawArcCenter = true;

  using VertexGraphics = QGraphicsEllipseItem;
  QPen pntPen = QPen(pntColor);
  constexpr qreal radius = 4.0;

  // add segments
  for( int i = 0; i<VRONI::num_segs; ++i)
  {
    VRONI::seg* vr_seg  = VRONI::segs+i;

    Geometry::Point_2D tail;
    get_pnt_coords(tail,vr_seg->i1);

    Geometry::Point_2D head;
    get_pnt_coords(head,vr_seg->i2);

    auto line = new LineGraphics(tail.x(), tail.y(),
                                 head.x(), head.y());

    line->setPen(segPen);
    group->addToGroup(line);
  }

  // add arcs
  for(int i = 0; i<VRONI::num_arcs; ++i)
  {
    VRONI::arc* vr_arc = VRONI::arcs+i;

    if( approxArcsByLines )
    {
      Geometry::Point_2D tail;
      get_pnt_coords(tail,vr_arc->i1);

      Geometry::Point_2D head;
      get_pnt_coords(head,vr_arc->i2);

      if( boost::geometry::distance(tail,head) > ZERO )
      {
        auto line = new LineGraphics(tail.x(), tail.y(),
                                  head.x(), head.y());
        group->addToGroup(line);
      }
    }

    if( drawArcCenter )
    {
      Geometry::Point_2D center(vr_arc->c.x, vr_arc->c.y);
      if(VRONI::scale_data)
      { center = unScale(center); }

      auto point = new VertexGraphics(center.x()-radius/2, center.y()-radius/2,
                                      radius,radius);
    }

  }

  // add points
  for( int i = 0; i<VRONI::num_pnts; ++i )
  {
    Geometry::Point_2D point;
    get_pnt_coords(point, i);

    auto vertex = new VertexGraphics(point.x()-radius/2, point.y()-radius/2,
                                      radius,radius);
    vertex->setPen(pntPen);
    group->addToGroup(vertex);
  }

  printVroniSiteData();

  return group;
}

QGraphicsItemGroup* VroniInterface::getNodeGraphics()
{
  auto group = new QGraphicsItemGroup;

  QColor nodeColor = Qt::darkRed;

  int num_nodes = VRONI::GetNumberOfNodes();

  using VertexGraphics = QGraphicsEllipseItem;
  QPen nodePen = QPen(nodeColor);
  constexpr qreal radius = 4.0;

  // add nodes
  for( int i = 0; i<num_nodes; ++i )
  {
    Geometry::Point_2D node;
    get_node_coords(node,i);

    auto vertex = new VertexGraphics(node.x()-radius/2, node.y()-radius/2,
                                      radius,radius);
    vertex->setPen(nodePen);
    group->addToGroup(vertex);

  }

  return group;
}

QGraphicsItemGroup* VroniInterface::getEdgeGraphics()
{
  auto group = new QGraphicsItemGroup;

  QColor edgeColor = Qt::darkGreen;

  int num_edges = VRONI::GetNumberOfEdges();

  using LineGraphics = Graphics_2D::Arrow;
//   using LineGraphics = QGraphicsLineItem;
  QPen edgePen = QPen(edgeColor);
  edgePen.setWidthF(0);

  // add edges
  for( int i=0; i< num_edges; ++i)
  {
    VRONI::edge* vr_edge  = VRONI::edges+i;

    Geometry::Point_2D tail;
    get_node_coords(tail,vr_edge->n1);

    Geometry::Point_2D head;
    get_node_coords(head,vr_edge->n2);

    if( boost::geometry::distance(tail,head) > ZERO ) // NOTE: ZERO is a VRONI #define
    {
      auto line = new LineGraphics(tail.x(), tail.y(),
                                  head.x(), head.y());

      line->setPen(edgePen);
      group->addToGroup(line);
    }
  }

  return group;
}

#endif // WITH_GRAPHICS

Simple2dGraph::Graph VroniInterface::createSiteShapeAssociationGraph()
{
  using Graph = Simple2dGraph::Graph;
  Graph graph;

  if( !m_vdIsCurrent )
  {
    qDebug() << "Thr Voronoi diagram is not current. Can't create the site-to-shape association graph.";
    return graph;
  }

  auto vIndexMap  = boost::get(boost::vertex_index,graph); ///< Vertex_Index_Map
  auto vPosMap    = boost::get(boost::vertex_position_2D, graph); ///< Vertex_Position2d_Map

  QHash<int,Graph::vertex_descriptor> shapeVertexHash;

  // create the vertices representing the shapes (as they won't be itner connected)
  for( int i=0; i<shapeCentroidMap.size(); ++i)
  {
    Graph::vertex_descriptor shapeVertex = boost::add_vertex(graph);
        shapeVertexHash[i]=shapeVertex;

    vPosMap[shapeVertex] = shapeCentroidMap[i];
  }

  QHash<int,Graph::vertex_descriptor> segSiteHash;

  // create the vertices representing the SEG sites
  for( int i=0; i<VRONI::num_segs; ++i)
  {
    Graph::vertex_descriptor segVertex = boost::add_vertex(graph);
    segSiteHash[i]=segVertex;

    VRONI::seg* vr_seg = VRONI::segs+i;

    Geometry::Point_2D tail;
    get_pnt_coords(tail,vr_seg->i1);

    Geometry::Point_2D head;
    get_pnt_coords(head,vr_seg->i2);

    vPosMap[segVertex] = Geometry::midpoint(head,tail);

    boost::add_edge(segVertex,shapeVertexHash[vr_seg->ext_appl],graph);

  }

  // get the PNT-to-shape map
  QHash<int,int> pntShapeMap  = getPntToShapeMap();

  QHash<int,Graph::vertex_descriptor> pntSiteHash;

  // create the vertices representing the PNT sites
  for( int i=0; i<VRONI::num_pnts;++i )
  {
    Graph::vertex_descriptor pntVertex = boost::add_vertex(graph);
    pntSiteHash[i]=pntVertex;

    Geometry::Point_2D point;
    get_pnt_coords(point,i);

    vPosMap[pntVertex] = point;

    boost::add_edge(pntVertex,shapeVertexHash[pntShapeMap[i]],graph);
  }

  return graph;

}

Simple2dGraph::Graph VroniInterface::createEdgeSiteAssociationGraph()
{
  using Graph = Simple2dGraph::Graph;
  Graph graph;

  auto vIndexMap  = boost::get(boost::vertex_index,graph); ///< Vertex_Index_Map
  auto vPosMap    = boost::get(boost::vertex_position_2D, graph); ///< Vertex_Position2d_Map

  QHash<int,Graph::vertex_descriptor> pntSiteHash;
  QHash<int,Graph::vertex_descriptor> segSiteHash;
  QHash<int,Graph::vertex_descriptor> arcSiteHash;
  QHash<int,Graph::vertex_descriptor> vdEdgeHash;

  // first, create the vertices representing the sites (as they won't be connected)

  for( int i=0; i<VRONI::num_pnts;++i )
  {
    Graph::vertex_descriptor vertex = boost::add_vertex(graph);
    pntSiteHash[i]=vertex;

    Geometry::Point_2D point;
    get_pnt_coords(point,i);

    vPosMap[vertex] = point;
  }

  for( int i=0; i<VRONI::num_segs; ++i)
  {
    Graph::vertex_descriptor vertex = boost::add_vertex(graph);
    segSiteHash[i]=vertex;

    VRONI::seg* vr_seg = VRONI::segs+i;

    Geometry::Point_2D tail;
    get_pnt_coords(tail,vr_seg->i1);

    Geometry::Point_2D head;
    get_pnt_coords(head,vr_seg->i2);

    vPosMap[vertex] = Geometry::midpoint(head,tail);
  }

  for( int i=0; i<VRONI::num_arcs; ++i)
  {
    Graph::vertex_descriptor vertex = boost::add_vertex(graph);
    arcSiteHash[i]=vertex;

    VRONI::arc* vr_arc = VRONI::arcs+i;

    Geometry::Point_2D tail;
    get_pnt_coords(tail,vr_arc->i1);

    Geometry::Point_2D head;
    get_pnt_coords(head,vr_arc->i2);

    vPosMap[vertex] = Geometry::midpoint(head,tail);
  }

  auto inclusion_check = always_true;

  // second, create the vertices representing the edges and connect the resp.
  // vertices
  for( int i=0; i<VRONI::GetNumberOfEdges(); ++i)
  {
    VRONI::edge* vr_edge = VRONI::edges+i;

    if( inclusion_check(vr_edge))
    {
      Graph::vertex_descriptor vertex = boost::add_vertex(graph);
      vdEdgeHash[i] = vertex;

      Geometry::Point_2D tail;
      get_node_coords(tail,vr_edge->n1);

      Geometry::Point_2D head;
      get_node_coords(head,vr_edge->n2);

      vPosMap[vertex] = Geometry::midpoint(head,tail);

      // get left site vertex

      Graph::vertex_descriptor left;
      switch(vr_edge->ltype)
      {
        case VRONI::PNT : left = pntSiteHash[vr_edge->lft]; break;
        case VRONI::SEG : left = segSiteHash[vr_edge->lft]; break;
        case VRONI::ARC : left = arcSiteHash[vr_edge->lft]; break;
        default: ;  // this shouldn't happen
      }
      boost::add_edge(left,vertex,graph);

      Graph::vertex_descriptor right;
      switch(vr_edge->rtype)
      {
        case VRONI::PNT : right = pntSiteHash[vr_edge->rgt]; break;
        case VRONI::SEG : right = segSiteHash[vr_edge->rgt]; break;
        case VRONI::ARC : right = arcSiteHash[vr_edge->rgt]; break;
        default: ;  // this shouldn't happen
      }
      boost::add_edge(right,vertex,graph);
    }

  }

  return graph;

}

Simple2dGraph::Graph VroniInterface::createEdgeShapeAssociationGraph()
{
  using Graph = Simple2dGraph::Graph;
  Graph graph;

  if( !m_vdIsCurrent )
  {
    qDebug() << "Thr Voronoi diagram is not current. Can't create the edge-to-shape association graph.";
    return graph;
  }

  auto vIndexMap  = boost::get(boost::vertex_index,graph); ///< Vertex_Index_Map
  auto vPosMap    = boost::get(boost::vertex_position_2D, graph); ///< Vertex_Position2d_Map

  QHash<int,Graph::vertex_descriptor> shapeHash;
  QHash<int,Graph::vertex_descriptor> vdEdgeHash;

  // create the vertices representing the shapes (as they won't be connected)
  for( int i=0; i<shapeCentroidMap.size(); ++i)
  {
    Graph::vertex_descriptor shapeVertex = boost::add_vertex(graph);
    shapeHash[i]=shapeVertex;

    vPosMap[shapeVertex] = shapeCentroidMap[i];
  }

  // get the SITE-to-shape maps
  QHash<int,int> pntToShapeMap  = getPntToShapeMap();
  QHash<int,int> segToShapeMap  = getSegToShapeMap();

  printVroniSiteData();
  qDebug() << "PNT-to-Shape Map:\n" <<  pntToShapeMap;
  qDebug() << "SEG-to-Shape Map:\n" <<  segToShapeMap;

  auto inclusion_check = in_w_mat;

  // third, create the vertices representing the edges and connect the resp.
  // vertices
  for( int i_vr_edge=0; i_vr_edge<VRONI::GetNumberOfEdges(); ++i_vr_edge)
  {
    VRONI::edge* vr_edge = VRONI::edges+i_vr_edge;

    // check whether the current edge should be included in this extraction
    if( inclusion_check(vr_edge ) )
    {
//       QString edgeString = QString("Edge %1 :").arg(i_vr_edge,2);

      Graph::vertex_descriptor edgeVertex = boost::add_vertex(graph);
      vdEdgeHash[i_vr_edge] = edgeVertex;

      Geometry::Point_2D tail;
      get_node_coords(tail,vr_edge->n1);

      Geometry::Point_2D head;
      get_node_coords(head,vr_edge->n2);

      vPosMap[edgeVertex] = Geometry::midpoint(head,tail);

      // get left site index

      int leftShapeIndex = NIL;
      switch(vr_edge->ltype)
      {
        case VRONI::PNT :
          // NOTE: the .contains check is necessary as operator[] default
          // constructs an entry in case the key isn't present -- and that default
          // constucted int is not VRONI::NIL.
          if( pntToShapeMap.contains(vr_edge->lft) )
          { leftShapeIndex = pntToShapeMap[vr_edge->lft]; }
//           edgeString.append(QString("PNT %1").arg(vr_edge->lft,2));
          break;
        case VRONI::SEG :
          if( segToShapeMap.contains(vr_edge->lft) )
          { leftShapeIndex = segToShapeMap[vr_edge->lft]; }
//           edgeString.append(QString("SEG %1").arg(vr_edge->lft,2));
          break;
        default: ; // this shouldn' happen
      }

      if( leftShapeIndex != NIL )
      {
        Graph::vertex_descriptor leftShape = shapeHash[leftShapeIndex];
        if( leftShapeIndex!=0 ) // don't add edges to the boundary
        { boost::add_edge(leftShape,edgeVertex,graph); }
//         edgeString.append(QString(" (# %1)").arg(leftShapeIndex,2));
      }
//       else
//       { edgeString.append(QString("       ")); } // space for the alignment

      int rightShapeIndex = NIL;
      switch(vr_edge->rtype)
      {
        case VRONI::PNT :
          // NOTE: the .contains check is necessary as operator[] default
          // constructs an entry in case the key isn't present -- and that default
          // constucted int is not VRONI::NIL.
          if( pntToShapeMap.contains(vr_edge->rgt) )
          { rightShapeIndex = pntToShapeMap[vr_edge->rgt]; }
//           edgeString.append(QString("PNT %1").arg(vr_edge->rgt,2));
          break;
        case VRONI::SEG :
          if( segToShapeMap.contains(vr_edge->rgt) )
          { rightShapeIndex = segToShapeMap[vr_edge->rgt]; }
//           edgeString.append(QString("SEG %1").arg(vr_edge->rgt,2));
          break;
        default: ; // this shouldn' happen
      }

      if( rightShapeIndex != NIL )
      {
        Graph::vertex_descriptor rightShape = shapeHash[rightShapeIndex];
        if( rightShapeIndex != 0 ) // don't add edges to the boundary
        { boost::add_edge(rightShape,edgeVertex,graph); }
//         edgeString.append(QString(" (# %1)").arg(rightShapeIndex,2));
      }
//       else
//       { edgeString.append(QString("       ")); } // space for the alignment

//       qDebug() << edgeString;
    }
  }

  return graph;
}

Simple2dGraph::Graph VroniInterface::createEdgeShapeAssociationGraph(
  QHash<int,int> shapeToEdgeMap
)
{
  using Graph = Simple2dGraph::Graph;
  Graph graph;

//   auto vIndexMap  = boost::get(boost::vertex_index,graph); ///< Vertex_Index_Map
  auto vPosMap    = boost::get(boost::vertex_position_2D, graph); ///< Vertex_Position2d_Map

  QHash<int,Graph::vertex_descriptor> vdEdgeHash;
  auto inclusion_check = in_w_mat;

  // create the vertices representing the edges
  for( int i_vr_edge=0; i_vr_edge<VRONI::GetNumberOfEdges(); ++i_vr_edge)
  {
    VRONI::edge* vr_edge = VRONI::edges+i_vr_edge;

    // check whether the current edge should be included in this extraction
    if( inclusion_check(vr_edge ) )
    {
      Graph::vertex_descriptor edgeVertex = boost::add_vertex(graph);
      vdEdgeHash[i_vr_edge] = edgeVertex;

      Geometry::Point_2D tail;
      get_node_coords(tail,vr_edge->n1);

      Geometry::Point_2D head;
      get_node_coords(head,vr_edge->n2);

      vPosMap[edgeVertex] = Geometry::midpoint(head,tail);
    }
  }

  QHash<int,Graph::vertex_descriptor> shapeHash;

  // create the vertices representing the shapes and connect them to the edges
  for( int i=0; i<shapeCentroidMap.size(); ++i)
  {

    // create vertex

    Graph::vertex_descriptor shapeVertex = boost::add_vertex(graph);
    shapeHash[i]=shapeVertex;

    vPosMap[shapeVertex] = shapeCentroidMap[i];

    // connect to edge(s)

    auto connectedEdges = shapeToEdgeMap.values(i);

    for( auto edge : connectedEdges)
    {
      if( vdEdgeHash.contains(edge) )
      { boost::add_edge(shapeVertex,vdEdgeHash[edge],graph); }
    }

  }

  return graph;
}

void VroniInterface::computeVD(const VroniInterface::VDDTParameter& params)
{
  // VRONI is not const correct, hence some const casting is necessary
  auto output_file = const_cast<char*>( params.site_file.c_str() );
  auto vd_dt_file  = const_cast<char*>( params.points_VDDT_file.c_str() );

  // call the VRONI function computing the Voronoi diagram and the Delaunay
  // triangulation
  VRONI::API_ComputeVD(
    params.save_input, params.new_data, params.timing, params.bound,
    params.sample,params.approx, output_file, params.check_for_duplicates,
    params.recover_arcs, params.apx_absolute, params.apx_relative,
    params.auto_apx, params.points_only, params.save_point_output, vd_dt_file,
    params.clean_data
  );

  // mark the current VD/DT data as current
  m_vdIsCurrent = true;
}

/** \todo Maybe introduce an exception to indicate the out-of-dateness of the
 *    VD/DT data instead of implicitely calling computeVD()? This could
 *    improve the const-correctness of the overall interface.
 */
void VroniInterface::computeWMAT(double WMAT_distance, double WMAT_angle, bool auto_WMAT, bool left, bool right, bool timing)
{
  // check if the VD/DT data is current. If not, update the VD/DT data
  if( !m_vdIsCurrent )
  { computeVD(); }

  // call VRONI's API function
  // NOTE: technically the function prototype calls for VRONI:boolean as bool
  //   input types. But since those are just typdef'ed to bool this should work.
  VRONI::API_ComputeWMAT(auto_WMAT, WMAT_angle,WMAT_distance,timing,left,right);
}

void VroniInterface::computeWMAT(const VroniInterface::WMATParameter& params)
{
  // check if the VD/DT data is current. If not, update the VD/DT data
  if( !m_vdIsCurrent )
  { computeVD(); }

  // call VRONI's API function
  // NOTE: technically the function prototype calls for VRONI:boolean as bool
  //   input types. But since those are just typdef'ed to bool this should work.
  VRONI::API_ComputeWMAT(
    params.auto_WMAT, params.WMAT_angle, params.WMAT_distance, params.timing,
    params.left, params.right
  );
}

VroniGraph::Graph VroniInterface::extractVdGraph(std::function< bool (VRONI::edge*)> inclusion_check) const
{
  using Graph = VroniGraph::Graph;
  Graph graph; ///< a new boost graph to hold the VD graph.

  auto vIndexMap  = boost::get(boost::vertex_index,graph); ///< Vertex_Index_Map
  auto vPosMap    = boost::get(boost::vertex_position_2D, graph); ///< Vertex_Position2d_Map
  auto vVroniMap  = boost::get(boost::vertex_vroniIndex, graph); ///< Vertex_VroniIndex_Map
  auto vDataMap   = boost::get(boost::vertex_vroniNodeData, graph); ///<  Vertex_VroniNodeData_Map
  auto eWeightMap = boost::get(boost::edge_weight, graph); ///<  Edge_Weight_Map
  auto eVroniMap  = boost::get(boost::edge_vroniIndex, graph); ///<  Edge_VroniIndex_Map
  auto eDataMap   = boost::get(boost::edge_vroniEdgeData, graph); ///<  Edge_VroniEdgeData_Map

  // A hash to relate VRONI node indices to the corresponding boost vertices
  QHash<int,Graph::vertex_descriptor> vroniIndexVertexHash;
  // A hash to relate VRONI edge indices to the corresponding boost edges
  QHash<int,Graph::edge_descriptor>   vroniIndexEdgeHash;

  // A counter to assign consecutive indices to the vertices (which will
  // differ from the VRONI indices stored in vVroniMap!)
  unsigned int vertexCounter = 0;

  auto get_vertex_descriptor = [&](Graph::vertex_descriptor & vertex, int vroniNodeIndex)
  {
    if( !vroniIndexVertexHash.contains(vroniNodeIndex) )
    { //no. add a new vertex.
      vertex = boost::add_vertex(graph);
      vroniIndexVertexHash[vroniNodeIndex]=vertex;

      { // populate the vertex properties

        vIndexMap[vertex]=vertexCounter++;
        vVroniMap[vertex]=vroniNodeIndex;

        VRONI::node* vroniNode = VRONI::nodes+vroniNodeIndex;
        vDataMap[vertex]=*vroniNode;

        Geometry::Point_2D position(vroniNode->p.x,vroniNode->p.y);
        if( VRONI::scale_data)
        { position = unScale(position); }
        vPosMap[vertex]=position;

      }
    }
    else
    { //yes. recover it.
      vertex = vroniIndexVertexHash.value(vroniNodeIndex);
    }
  };

  //
  // process the VRONI data and copy them over to the boost graph
  // ============================================================
  //

  // The total number of edges in VRONI's internal data. This includes VD/DT and
  // WMAT edges, but no data from the input site.
  int number_of_edges = VRONI::GetNumberOfEdges();

  // the index for the currently processed VRONI edge
  for(  int i_vr_edge = 4; // NOTE: I have NO IDEA why it has to start on 4, but this is a straight copy from VRONI::AddWMATToBuffer().
        i_vr_edge < number_of_edges;
        ++i_vr_edge)
  { // loop over all edges in the VRONI data, i.e. WMAT and VD/DT:

    // get a pointer to the currently processed edge
    VRONI::edge* vr_edge = VRONI::edges+i_vr_edge;

    // check whether the current edge should be included in this extraction
    if( inclusion_check(vr_edge ) )
    {
      Graph::vertex_descriptor tail;
            get_vertex_descriptor( tail, vr_edge->n1 );

      Graph::vertex_descriptor head;
            get_vertex_descriptor( head, vr_edge->n2 );

      {/* connect the head and the tail in the boost graph and compute the edge
        * length
        */
        using namespace boost;

        bool success= false;
        Graph::edge_descriptor edge;
        boost::tie(edge,success) = boost::add_edge(tail, head, graph);

        if(success)
        {
          eVroniMap[edge]   = i_vr_edge;
          eDataMap[edge]    = *vr_edge;
          eWeightMap[edge]  = geometry::distance(vPosMap[head],vPosMap[tail]);
          vroniIndexEdgeHash[i_vr_edge] = edge;
        }
      }

    }
  }

  // Initialize the interior edge index
  auto eIndexMap  = boost::get(boost::edge_index, graph); ///<  Edge_Index_Map
  boost::graph_traits<Graph>::edges_size_type edge_count = 0;
  boost::graph_traits<Graph>::edge_iterator ei, ei_end;
  for(boost::tie(ei, ei_end) = boost::edges(graph); ei != ei_end; ++ei)
  { eIndexMap[*ei] = edge_count++; }

  // done
  return graph;
}

VroniGraph::Graph VroniInterface::extractDtGraph(void ) const
{
  using Graph = VroniGraph::Graph;

  if( !m_isPointSite )
  {
    qDebug() << "The VRONI site isn't a pure point site. Cannot extract Delaunay triangulation and/or graph.";
    return Graph();
  }

  Graph graph; ///< a new boost graph to hold the DT graph.

  auto vIndexMap  = boost::get(boost::vertex_index,graph); ///< Vertex_Index_Map
  auto vPosMap    = boost::get(boost::vertex_position_2D, graph); ///< Vertex_Position2d_Map
  auto vVroniMap  = boost::get(boost::vertex_vroniIndex, graph); ///< Vertex_VroniIndex_Map
  auto vDataMap   = boost::get(boost::vertex_vroniNodeData, graph); ///<  Vertex_VroniNodeData_Map
  auto eWeightMap = boost::get(boost::edge_weight, graph); ///<  Edge_Weight_Map

  //NOTE: There is no VRONI data associated with these edges, as VRONI doesn't
  // explicitely compute them.
  //   auto eVroniMap  = boost::get(boost::edge_vroniIndex, graph); ///<  Edge_VroniIndex_Map
  //   auto eDataMap   = boost::get(boost::edge_vroniEdgeData, graph); ///<  Edge_VroniEdgeData_Map

  // A hash to relate VRONI node indices to the corresponding boost vertices
  QHash<int,Graph::vertex_descriptor> vroniIndexHash;

  // A counter to assigne consecutive indices to the vertices (which will
  // differ from the VRONI indices stored in vVroniMap!)
  unsigned int vertexCounter = 0;

  // lambda to check if any of those VRONI nodes already is in the index hash
  auto get_site_vertex = [&](Graph::vertex_descriptor & vertex, int vroniNodeIndex)
  {
    if( !vroniIndexHash.contains(vroniNodeIndex) )
    { //no. add a new vertex.
      vertex = boost::add_vertex(graph);
      vroniIndexHash[vroniNodeIndex]=vertex;

      { // populate the vertex properties

        vIndexMap[vertex]=vertexCounter++;
        vVroniMap[vertex]=vroniNodeIndex;

        VRONI::node* vroniNode = VRONI::nodes+vroniNodeIndex;
        vDataMap[vertex]=*vroniNode;

        Geometry::Point_2D position(VRONI::pnts[vroniNodeIndex+2].p.x/*NOTE: this is different from vroniNode->p.x*/,
                                    VRONI::pnts[vroniNodeIndex+2].p.y/*NOTE: this is different from vroniNode->p.y*/);
        if( VRONI::scale_data)
        { position = unScale(position); }
        vPosMap[vertex]=position;

      }
    }
    else
    { //yes. recover it.
      vertex = vroniIndexHash.value(vroniNodeIndex);
    }
  };

  // get the Delaunay triangulation from VRONI
  std::vector<VRONI::d_triangle> dtTriangles = VRONI::GetDelaunayTriangles();

  for( VRONI::d_triangle& triangle : dtTriangles )
  {
    // get the involved nodes
    Graph::vertex_descriptor n1;
        get_site_vertex(n1,triangle.n1);
    Graph::vertex_descriptor n2;
        get_site_vertex(n2,triangle.n2);
    Graph::vertex_descriptor n3;
        get_site_vertex(n3,triangle.n3);

    // connect them in the boost graph and compute the edge lengths
    {

      bool success= false;
      Graph::edge_descriptor edge;

      boost::tie(edge,success) = boost::add_edge(n1, n2, graph);
      if(success)
      {

        eWeightMap[edge] = boost::geometry::distance(vPosMap[n1],vPosMap[n2]);
      }

      boost::tie(edge,success) = boost::add_edge(n2, n3, graph);
      if(success)
      {

        eWeightMap[edge] = boost::geometry::distance(vPosMap[n2],vPosMap[n3]);
      }

      boost::tie(edge,success) = boost::add_edge(n3, n1, graph);
      if(success)
      {

        eWeightMap[edge] = boost::geometry::distance(vPosMap[n3],vPosMap[n1]);
      }

    }

  }

//   VRONI::WriteVDDT("vddt.out");

  return graph;

}

VroniGraph::Graph VroniInterface::extractVdGraph(void ) const
{
  return extractVdGraph(always_true);
}
VroniGraph::Graph VroniInterface::extractWmatGraph(void ) const
{
  return extractVdGraph(in_w_mat);
}
VroniGraph::Graph VroniInterface::extractNonWmatGraph(void ) const
{
  return extractVdGraph(in_vddt);
}

QHash< int, int > VroniInterface::determineVoronoiCells()
{
  QHash<int,int> shapeToEdgeMap;

  if( !m_vdIsCurrent )
  {
    qDebug() << "Thr Voronoi diagram is not current. Can't determine the Voronoi cells.";
    return shapeToEdgeMap;
  }

  // get the SITE-to-shape maps
  QHash<int,int> pntToShapeMap  = getPntToShapeMap();
  QHash<int,int> segToShapeMap  = getSegToShapeMap();

  auto inclusion_check = in_w_mat;

  // third, create the vertices representing the edges and connect the resp.
  // vertices
  for( int i_vr_edge=0; i_vr_edge<VRONI::GetNumberOfEdges(); ++i_vr_edge)
  {
    VRONI::edge* vr_edge = VRONI::edges+i_vr_edge;

    // check whether the current edge should be included in this extraction
    if( inclusion_check(vr_edge ) )
    {

      int leftShapeIndex = NIL;
      switch(vr_edge->ltype)
      {
        case VRONI::PNT :
          // NOTE: the .contains check is necessary as operator[] default
          // constructs an entry in case the key isn't present -- and that default
          // constucted int is not VRONI::NIL.
          if( pntToShapeMap.contains(vr_edge->lft) )
          { leftShapeIndex = pntToShapeMap[vr_edge->lft]; }
          break;
        case VRONI::SEG :
          if( segToShapeMap.contains(vr_edge->lft) )
          { leftShapeIndex = segToShapeMap[vr_edge->lft]; }
          break;
        default: ; // this shouldn' happen
      }

      if( leftShapeIndex != NIL )
      { shapeToEdgeMap.insertMulti(leftShapeIndex,i_vr_edge); }

      int rightShapeIndex = NIL;
      switch(vr_edge->rtype)
      {
        case VRONI::PNT :
          // NOTE: the .contains check is necessary as operator[] default
          // constructs an entry in case the key isn't present -- and that default
          // constucted int is not VRONI::NIL.
          if( pntToShapeMap.contains(vr_edge->rgt) )
          { rightShapeIndex = pntToShapeMap[vr_edge->rgt]; }
          break;
        case VRONI::SEG :
          if( segToShapeMap.contains(vr_edge->rgt) )
          { rightShapeIndex = segToShapeMap[vr_edge->rgt]; }
          break;
        default: ; // this shouldn' happen
      }

      if( rightShapeIndex != NIL and rightShapeIndex != 0)
      { shapeToEdgeMap.insertMulti(rightShapeIndex,i_vr_edge); }

    }
  }

  return shapeToEdgeMap;
}

// QHash< int, Geometry::Polygon_2D > VroniInterface::extractVoronoiCellGeometries() const
// {
//   QHash<int, Geometry::Polygon_2D> geometries;
//
//   if( !m_vdIsCurrent )
//   {
//     qDebug() << "The VD is not current, cannot extract Voronoi cell geometries.";
//     return geometries;
//   }
//
//   QHash<int,int>  shapeToEdgeMap = determineVoronoiCells;
//   QList<int>      shapeIndices = shapeToEdgeMap.uniqueKeys;
//
//   for( auto shape_i : shapeIndices)
//   {
//     Geometry::MultiPoint_2D cellCorners;
//
//     // get a list of all edges associacted with that shape.
//     QList<int> edgeIndices = shapeToEdgeMap.values(shape_i);
//
//     for( auto edge_i : edgeIndices)
//     {
//       VRONI::edge* vr_edge = VRONI::edges+edge_i;
//
//       Geometry::Point_2D tail;
//       get_node_coords(tail,vr_edge->n1);
//       boost::geometry::append(cellCorners,tail);
//
//       Geometry::Point_2D head;
//       get_node_coords(tail,vr_edge->n2);
//       boost::geometry::append(cellCorners,head);
//     }
//
//     Geometry::Polygon_2D cellGeometry;
//     boost::geometry::convex_hull(cellCorners,cellGeometry);
//     geometries[shape_i] = cellGeometry;
//   }
//
//   return geometries;
// }

Geometry::Box_2D VroniInterface::boundingBox() const
{
  using namespace VRONI;

  if (!bbox_added) //NOTE: the bbox gets added in VRONI::AddDummyCorners(int bound)
    SetBoundingBox();

  if( data_scaled )
  {
    Geometry::Point_2D minCorner((bb_min.x/scale_factor)+shift.x,
                                 (bb_min.y/scale_factor)+shift.y);
    Geometry::Point_2D maxCorner((bb_max.x/scale_factor)+shift.x,
                                 (bb_max.y/scale_factor)+shift.y);

    return Geometry::Box_2D(minCorner,maxCorner);
  }
  else
  {
    Geometry::Point_2D minCorner((bb_min.x),
                                 (bb_min.y));
    Geometry::Point_2D maxCorner((bb_max.x),
                                 (bb_max.y));

    return Geometry::Box_2D(minCorner,maxCorner);

  }
}

Geometry::Point_2D VroniInterface::unScale(const Geometry::Point_2D& point) const
{
  if( VRONI::data_scaled )
    return Geometry::Point_2D(point.get<0>()/VRONI::scale_factor+VRONI::shift.x,
                              point.get<1>()/VRONI::scale_factor+VRONI::shift.y);
  else
    return point;
}

void VroniInterface::get_pnt_coords(Geometry::Point_2D& point, const int& pntIndex) const
{
  VRONI::pnt* vr_pnt = VRONI::pnts+pntIndex;
  point.set_x(vr_pnt->p.x);
  point.set_y(vr_pnt->p.y);
  if(VRONI::scale_data)
  { point = unScale(point); }
};

void VroniInterface::get_node_coords(Geometry::Point_2D& point, const int& nodeIndex) const
{
  VRONI::node* vr_node = VRONI::nodes+nodeIndex;
  point.set_x(vr_node->p.x);
  point.set_y(vr_node->p.y);
  if(VRONI::scale_data)
  { point = unScale(point); }
}

// void VroniInterface::unScale(Geometry::Point_2D& point) const
// {
//   point.get<0>() /= VRONI::scale_factor;
//   point.get<0>() += VRONI::shift.x;
//   point.get<1>() /= VRONI::scale_factor;
//   point.get<1>() += VRONI::shift.y;
// }

void VroniInterface::printVroniSiteData()
{
  qDebug() <<"VRONI Site Data:"
    <<"\nNumber of PNTs :" << VRONI::num_pnts
    <<"\nNumber of SEGs :" << VRONI::num_segs
    <<"\nNumber or ARCs :" << VRONI::num_arcs;

  qDebug() << "VRONI PNT Data :";
  for( int i=0; i<VRONI::num_pnts; ++i)
  {
    Geometry::Point_2D p;
    get_pnt_coords(p,i);
    qDebug() << QString("PNT %1:(%2,%3)").arg(i,2).arg(p.x()).arg(p.y());
  }

  qDebug() << "VRONI SEG Data :";
  for( int i=0; i<VRONI::num_segs; ++i)
  {

    Geometry::Point_2D tail,head;
    VRONI::seg* vr_seg = VRONI::segs+i;
    get_pnt_coords(tail,vr_seg->i1);
    get_pnt_coords(head,vr_seg->i2);

//     qDebug() << QString("SEG %1: PNT %2 -> PNT %3 : (%4,%5) -> (%6,%7)")
//     .arg(i,2)
//     .arg(vr_seg->i1,2)
//     .arg(vr_seg->i2,2)
//     .arg(tail.x(),8,'f',2).arg(tail.y(),8,'f',2)
//     .arg(head.x(),8,'f',2).arg(head.y(),8,'f',2) ;
    qDebug() << QString("SEG %1 (%2): PNT %3 -> PNT %4 ")
    .arg(i,2)
    .arg(vr_seg->ext_appl,2)
    .arg(vr_seg->i1,2)
    .arg(vr_seg->i2,2);
  }

}

QHash< int, int > VroniInterface::getSegToShapeMap() const
{
  QHash<int,int> segToShapeMap;

  for(int i=0; i<VRONI::num_segs; ++i)
  {
    VRONI::seg* vr_seg = VRONI::segs+i;
    segToShapeMap[i] = vr_seg->ext_appl;
  }

  return segToShapeMap;
}

QHash< int, int > VroniInterface::getPntToShapeMap() const
{
  QHash<int,int> pntShapeMap;

  // loop over all segments, grab the end points and the assoc. shape and
  // put it in the pntShapeMap
  for( int i=0; i< VRONI::num_segs; ++i)
  {
    VRONI::seg* vr_seg = VRONI::segs+i;

    pntShapeMap[vr_seg->i1] = vr_seg->ext_appl; // tail
    pntShapeMap[vr_seg->i2] = vr_seg->ext_appl; // head

  }

  return pntShapeMap;
}

bool VroniInterface::isVdCurrent() const
{
  return m_vdIsCurrent;
}


//#include <boost/serialization/export.hpp>
#include "TCell.h"

//BOOST_CLASS_EXPORT_IMPLEMENT(TCell)
#include "../../core/GlobalUtilities.h"
#include "../compartment/Tumor.h"

#include <boost/math/tools/roots.hpp>

namespace SP_QSP_IO{
namespace SP_QSP_NSCLC{

//#define IFNG_ID 0

using std::string;
using std::stringstream;

static int TCellSize = 1;

TCell::TCell()
	: _count_neighbor_cancer(0)
	, _count_neighbor_Treg(0)
	, _count_neighbor_all(0)
	, _max_neighbor_PDL1(0)
{
}

TCell::TCell(SpatialCompartment* c)
	:Cell_Tumor(c)
	, _divide_flag(false)
	, _divide_cd(0)
	, _divide_limit(params.getVal(PARAM_T_DIV_LIMIT))
	, _IL2_exposure(0)
	, _IL2_release_remain(params.getVal(PARAM_IL_2_RELEASE_TIME))
	, _IFN_release_remain(params.getVal(PARAM_IFN_RELEASE_TIME))
	, _source_IFNg(NULL)
	, _source_IL_2(NULL)
	, _count_neighbor_cancer(0)
	, _count_neighbor_Treg(0)
	, _count_neighbor_all(0)
	, _max_neighbor_PDL1(0)
{
	_state = AgentStateEnum::T_CELL_EFF;
	_life = TCell::getTCellLife();
	//cout << getID() << ", " << getType() << endl;
}

TCell::TCell( const TCell & c)
	:Cell_Tumor(c)
	, _divide_flag(c._divide_flag)
	, _divide_cd(c._divide_cd)
	, _divide_limit(c._divide_limit)
	, _IL2_exposure(c._IL2_exposure)
	, _IL2_release_remain(c._IL2_release_remain)
	, _IFN_release_remain(c._IFN_release_remain)
	, _source_IFNg(NULL)
	, _source_IL_2(NULL)
	, _count_neighbor_cancer(0)
	, _count_neighbor_Treg(0)
	, _count_neighbor_all(0)
	, _max_neighbor_PDL1(0)
{
	_state = c._state;
	//cout << getID() << ", " << getType() << endl;
	_life = TCell::getTCellLife();
	//std::cout << "Teff copy life: " << _life << std::endl;
	if (_state == AgentStateEnum::T_CELL_CYT)
	{
		setup_chem_source(_source_IFNg, CHEM_IFN, params.getVal(PARAM_IFN_G_RELEASE));
		setup_chem_source(_source_IL_2, CHEM_IL_2, params.getVal(PARAM_IL_2_RELEASE));
	}
}

TCell::~TCell()
{
}
string TCell::toString()const{
	stringstream ss;
	ss << CellAgent::toString();
	ss << "division flag: " << _divide_flag << ", division cool down: "
		<< _divide_cd  << std::endl;
	return ss.str();
}

bool TCell::agent_movement_step(double t, double dt, Coord& c){
	bool move = false;
	if (rng.get_unif_01() < params.getVal(PARAM_T_CELL_MOVE_PROB))
	{
		// move
		int idx;
		const auto shape = getCellShape();
		if (_compartment->getOneOpenVoxel(shape->getMoveDestinationVoxels(),
			shape->getMoveDirectionAnchor(), _coord, getType(), idx, rng))
		{
			move = true;
			c = getCellShape()->getMoveDirectionAnchor()[idx] + _coord;
		}
	}
	return move;

}

/*! Scan neighborhood and count neighbor cell type
	number of Teff neighbor for Cancer cell;
	number of Cancer cell neighbor for Teff;
	number of Treg neighbor for Teff;
*/
void TCell::agent_state_scan(void){

	const auto shape = getCellShape();
	if (_state == AgentStateEnum::T_CELL_CYT){
		/**/
		// scan cancer cells
		//std::cout << "T cell at: "<< _coord << std::endl;
		//int nr_cancer_neighbor = 0;
		_compartment->for_each_neighbor_ag(shape->getEnvironmentLocations(),
			_coord, [&](BaseAgent* ag){
			_count_neighbor_all++;
			auto pCell = dynamic_cast<Cell_Tumor*>(ag);
			update_neighbor_PDL1(pCell->get_PDL1());
			if (ag->getType() == AgentTypeEnum::CELL_TYPE_CANCER){
				auto pCancer = dynamic_cast<CancerCell*>(ag);
				inc_neighbor_cancer();
				pCancer->inc_neighbor_Teff();
				//nr_cancer_neighbor += 1;
			}
			else if (ag->getType() == AgentTypeEnum::CELL_TYPE_TREG){
				inc_neighbor_Treg();
			}
			return false;
		});
		//std::cout << "T cell neighbor PDL1: "<< _max_neighbor_PDL1 << std::endl;
		//std::cout << "T cell neighbor Cancer: "<< nr_cancer_neighbor << std::endl;
	}
	return;
}

bool TCell::agent_state_step(double t, double dt, Coord& c){
	bool divide = false;
	if (!isDead())
	{
		_life--;
		if (_life <= 0)
		{
			setDead();
			// remove source when cell die
			return divide;
		}
	}
	//std::cout << "Teff life: " << _life << std::endl;

	const auto shape = getCellShape();

	Cell_Tumor::agent_state_step(t, dt, c);

	auto tumor = dynamic_cast<Tumor*>(_compartment);

	double IL2 = get_tumor().get_chem(_coord, CHEM_IL_2);
	_IL2_exposure += params.getVal(PARAM_SEC_PER_TIME_SLICE) * IL2;

	// effector cells to proliferate on IL2 exposure
	if (_IL2_exposure > params.getVal(PARAM_IL_2_PROLIF_TH))
	{
		_divide_flag = true;
	}

	// Look for tumor Ag
	if (_state == AgentStateEnum::T_CELL_EFF)
	{
		bool cancer_found = _compartment->hasTypeStateInTarget(shape->getEnvironmentLocations(),
			_coord, AgentTypeEnum::CELL_TYPE_CANCER, AgentStateEnum::CANCER_PROGENITOR);
		if (cancer_found)
		{
			_state = AgentStateEnum::T_CELL_CYT;
			_divide_flag = true;
			setup_chem_source(_source_IFNg, CHEM_IFN, params.getVal(PARAM_IFN_G_RELEASE));
			setup_chem_source(_source_IL_2, CHEM_IL_2, params.getVal(PARAM_IL_2_RELEASE));
		}
	}

	if (_state == AgentStateEnum::T_CELL_CYT){
		/**/
		double nivo = tumor->get_Nivo();

		// kill one cancer cell
		// now handled from cancer cells

		// IL-2 release time limit
		if (_IL2_release_remain > 0)
		{
			_IL2_release_remain -= params.getVal(PARAM_SEC_PER_TIME_SLICE);
		}
		else{
			// set IL-2 source to 0
			update_chem_source(_source_IL_2, 0.0);
		}
		// IFNg release time limit
		if (_IFN_release_remain > 0)
		{
			_IFN_release_remain -= params.getVal(PARAM_SEC_PER_TIME_SLICE);
		}
		else{
			// set IL-2 source to 0
			update_chem_source(_source_IFNg, 0.0);
		}

		// exhaustion
		if (_count_neighbor_Treg > 0)// suppresion by Treg
		{
			double bond = get_PD1_PDL1(_max_neighbor_PDL1, nivo);
			double supp = get_PD1_supp(bond, params.getVal(PARAM_N_PD1_PDL1));
			double q = double(_count_neighbor_Treg) / _count_neighbor_all;
			double p_exhaust = get_exhaust_prob_Treg(supp, q);
			if (rng.get_unif_01() < p_exhaust)
			{
				/*
				std::cout << "RNG check: ID=" << getID() <<
				"(Teff exhaust by Treg): " << rng.get_unif_01() << std::endl;
				*/
				set_suppressed();
			}
		}
		else if (_count_neighbor_all > 0)// suppresion by PDL1
		{
			double bond = get_PD1_PDL1(_max_neighbor_PDL1, nivo);
			double supp = get_PD1_supp(bond, 1);
			double p_exhaust = get_exhaust_prob_PDL1(supp, 1.0);
			/*
			std::cout << "suppression prob: " << p_exhaust << std::endl;
			*/
			if (rng.get_unif_01() < p_exhaust)
			{
				/*
				std::cout << "RNG check: ID=" << getID() <<
				" (Teff exhaust by PDL1): " << rng.get_unif_01() << std::endl;
				*/
				set_suppressed();
			}
		}
	}

	if (_divide_cd > 0)
	{
		_divide_cd--;
	}

	if (_divide_limit > 0 && _divide_flag && _divide_cd == 0)
	{
		int idx;
		if (_compartment->getOneOpenVoxel(shape->getProlifDestinationVoxels(),
			shape->getProlifDestinationAnchor(), _coord, getType(), idx, rng))
		{
			divide = true;
			//cout << "idx: " << idx << ", " << getCellShape()->getProlif()[idx] << endl;
			c = getCellShape()->getProlifDestinationAnchor()[idx] + _coord;

			_divide_flag = true;
			_divide_limit -= 1;
			_divide_cd = params.getVal(PARAM_T_DIV_INTERVAL);
		}

	}

	_count_neighbor_cancer = 0;
	_count_neighbor_Treg = 0;
	_count_neighbor_all = 0;
	_max_neighbor_PDL1 = 0;
	return divide;
}

/*! set to suppressed state
*/
void TCell::set_suppressed(void){
	_divide_flag = false;
	_divide_limit = 0;
	_state = AgentStateEnum::T_CELL_SUPP;
	remove_source_sink(_source_IFNg);
	remove_source_sink(_source_IL_2);
	return;
}

//! move sources (IFN and IL2)
void TCell::move_all_source_sink(void)const{
	//std::cout << "moving sources: " << _coord << std::endl;
	move_source_sink(_source_IFNg);
	move_source_sink(_source_IL_2);
	return;
}

//! remove sources (IFN and IL2)
void TCell::remove_all_source_sink(void){
	remove_source_sink(_source_IFNg);
	remove_source_sink(_source_IL_2);
	return;
}

void TCell::update_neighbor_PDL1(double PDL1){
	if (PDL1 > _max_neighbor_PDL1)
	{
		_max_neighbor_PDL1 = PDL1;
	}
	return;
}

//! PD1_PDL1 bond in synapse, newton_raphson method
double TCell::get_PD1_PDL1(double PDL1, double Nivo){
	using namespace boost::math::tools;
	double guess = 0;
	double xmin = 0;
	double xmax = 1;
	int digits = static_cast<int>(std::numeric_limits<double>::digits);
	boost::uintmax_t maxit = 20;

	static double T1 = params.getVal(PARAM_PD1_SYN);
	double T2 = PDL1;
	static double k1 = params.getVal(PARAM_PDL1_K1);
	static double k2 = params.getVal(PARAM_PDL1_K2);
	static double k3 = params.getVal(PARAM_PDL1_K3);

	// 0 if no T2
	if (T2 == 0){
		return 0;
	}
	// f(x)
	double a, b, c, d;
	a = 1;
	b = -(T1 / T2 + 2 + 1 / T2 / k1 + Nivo*k2 / T2 / k1*(1 - 2 * k3 / k1));
	c = 2 * T1 / T2 + 1 + 1 / T2 / k1 + Nivo*k2 / T2 / k1;
	d = -T1 / T2;
	// f'(x)
	double a1, b1, c1;
	a1 = 3;
	b1 = -2 * (T1 / T2 + 2 + 1 / T2 / k1 + Nivo*k2 / T2 / k1*(1 - 2 * k3 / k1));
	c1 = 1 + 1 / k1 / T2 + Nivo*k2 / k1 / T2 + 2 * T1 / T2;

	double root = newton_raphson_iterate(
		// lambda function:
		[a, b, c, d, a1, b1, c1](const double g){ return std::make_pair(
		a * g * g * g + b * g * g + c * g + d,
		a1 * g * g + b1 * g + c1); },
		guess, xmin, xmax, digits, maxit);

	double bond = T2*root;
	return bond;
}

//! PD1_PDL1 bond in synapse
/*
double TCell::get_PD1_PDL1(double PDL1, double Nivo){

	static double T1 = params.getVal(PARAM_PD1_SYN);
	double T2 = PDL1;
	static double k1 = params.getVal(PARAM_PDL1_K1);
	static double k2 = params.getVal(PARAM_PDL1_K2);
	static double k3 = params.getVal(PARAM_PDL1_K3);

	double a = 1;
	double b = (Nivo*k2/k1*(2*k3/k1-1) - 2*T2 - T1 - 1/k1)/T2;
	double c = (Nivo*k2/k1 + 1/k1  +T2 + 2*T1 )/T2;
	double d = -T1/T2;

	//Newton_Raphson_root
	int max_iter = 20;
	double tol_rel = 1E-5;
	double root = 0;
	double res, root_new, f, f1;
	int i = 0;
	while (i < max_iter){
		f = a*std::pow(root, 3) + b*std::pow(root, 2)+ c*root + d;
		f1 = 3.0*a*std::pow(root, 2) + 2.0*b*root + c;
		root_new = root - f/f1;
		res = std::abs(root_new - root) / root_new;
		if (res > tol_rel){
			i++;
			root = root_new;
		}
		else{
			break;
		}
	}
	double bond = T2*root;
	return bond;
}*/

//! get suppression from PD1_PDL1 bond
double TCell::get_PD1_supp(double bond, double n) {
	double k50 = params.getVal(PARAM_PD1_PDL1_HALF);
	return get_Hill_equation(bond, k50, n);
}

int TCell::getTCellLife(){
	double lifeMean = params.getVal(PARAM_T_CELL_LIFE_MEAN_SLICE);
	double lifeSd = params.getVal(PARAM_T_CELL_LIFE_SD_SLICE);
	double tLifeD = lifeMean + rng.get_norm_std() * lifeSd;
	/*
	double tLifeD = rng.get_exponential(lifeMean);
	*/

	int tLife = int(tLifeD + 0.5);
	tLife = tLife > 0 ? tLife : 0;
	//std::cout << "random T cell life: " << tLife << std::endl;
	return tLife;
}

/*! probability of Cancer cell getting killed by Teff
	\param[in] supp: suppression
	p_kill = 1 - B_esc^((1-supp)*q),
	where B_esc = exp(-tstep * k_C_death_by_T)
*/
double TCell::get_kill_prob(double supp, double q){
	return 1 - std::pow(params.getVal(PARAM_ESCAPE_BASE), q*(1-supp));
}

/*! probability of turning exhausted from PDL1-PD1 interaction
	p_exhaust_PDL1 = 1 - B^(supp*q)
*/
double TCell::get_exhaust_prob_PDL1(double supp, double q){
	return 1 - std::pow(params.getVal(PARAM_EXHUAST_BASE_PDL1), q*supp);
}

/*! probability of turning exhausted from Treg
	p_exhaust_Treg = 1 - B^((1 + supp)*q)
*/
double TCell::get_exhaust_prob_Treg(double supp, double q){
	return 1 - std::pow(params.getVal(PARAM_EXHUAST_BASE_TREG), q*(1 + supp));
}

};
};


/***
第三节 完美转发
（1）完美转发的概念和步骤演绎
a)直接调用：  funcLast();
b)转发： 通过funcMiddle()间接调用funcLast。funcMiddle相当于一个跳板函数。如果有参数，那么参数也需要通过funcMiddle中转传递给funcLast()
c)完美转发：const,左值，右值。实参的属性完全不丢失，原原本本的通过funcMiddle转发给funcLast，这种转发就是完美转发。

万能引用：实参的所有信息都会传递到万能引用当中去从而让编译器推导出来函数模板最终的形参类型 (引用折叠)。
完美转发：就是让开发者可以书写接受任意实参的函数模板（funcMiddle_Temp），并将其转发到目标函数（funcLast2)，目标函数会接收到与
转发函数（funcMiddle_Temp）所接收的完全相同（当然包括类型相同比如保持参数的左值、右值特性）的参数。
要实现完美转发，就要用到std::forward了。
***/

#include <iostream>

//#include <boost/type_index.hpp>
using namespace std;
//#pragma warning(disable : 4996)

//函数模板
//template <typename T>
//void myfunc(T  tmprv)
//{
//	cout << "--------------------------------begin----------------" << endl;
//	using boost::typeindex::type_id_with_cvr;
//	cout << "T=" << type_id_with_cvr<T>().pretty_name() << endl; //显示T的类型
//	cout << "tmprv=" << type_id_with_cvr<decltype(tmprv)>().pretty_name() << endl; //显示tmprv的类型
//	cout << "--------------------------------end------------------" << endl;
//}

namespace _nmsp1
{
	//void funcLast(int v1, int v2)
	void funcLast(int v1, int& v2)
	{
		++v2; //改变v2的值，让其自增1
		cout << v1 + v2 << endl;
	}

	void funcLast2(int&& v1, int& v2)
	{
		cout << v1 << endl;
		cout << v2 << endl;
	}

	//函数模板（跳板函数）：把收到的参数以及这些参数相对应的类型不变的转发给其他函数（完美转发）
	template<typename F, typename T1,typename T2>
	//void funcMiddle_Temp(F f, T1 t1, T2 t2) //f:函数指针类型void(*)(int,int)，而funcLast是函数类型void(int,int)
	void funcMiddle_Temp(F f, T1&& t1, T2&& t2)
	{
		f(t1, t2);
	}
}

int main()
{
	/*
	int i = 50;
	_nmsp1::funcLast(41, i);                           //变量i自加1后 + 50 = 92
	*/

	/*
	int j = 70;
	_nmsp1::funcMiddle_Temp(_nmsp1::funcLast, 20, j);  //变量j自加1后 + 20 = 91
	*/

	/*
	int k = 50;
	_nmsp1::funcLast(41, k);                          //直接调用，92,执行完k=51
	*/

	/*
	int l = 70;
	_nmsp1::funcMiddle_Temp(_nmsp1::funcLast, 20, l); //91,执行完本函数，l = 71？70呢？ = 70
	                                                  //当前情况下l被funcMiddle_Temp推断成了int而不是int&
	                                                  //void funcMiddle_Temp(void(*f)(int,int &),int t1,int t2){...}
	*/

	/*
	int m = 70;
	_nmsp1::funcMiddle_Temp(_nmsp1::funcLast, 20, m);  //91,T1=int, t1 = int &&，      T2 = int & , t2 = int &
		                                               //m = 71;
	*/

	int n = 70;
	//_nmsp1::funcLast2(20, n); //20,70
	_nmsp1::funcMiddle_Temp(_nmsp1::funcLast2, 20, n);
	                                                 //20->t1(int &&)，但是t1本身是左值。

	/*int&& abc = 1;
	abc = 15;*/

	return 0;
}


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

#include "multisig_account.h"

#include "crypto/crypto.h"
#include "cryptonote_config.h"
#include "include_base_utils.h"
#include "multisig.h"
#include "multisig_kex_msg.h"
#include "ringct/rctOps.h"

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

#include <algorithm>
#include <cmath>
#include <cstdint>
#include <limits>
#include <memory>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>

#undef MONERO_DEFAULT_LOG_CATEGORY
#define MONERO_DEFAULT_LOG_CATEGORY "multisig"

namespace multisig
{
  //----------------------------------------------------------------------------------------------------------------------
  /**
  * INTERNAL
  *
  * brief: check_multisig_config - validate multisig configuration details
  * param: round - the round of the message that should be produced
  * param: threshold - threshold for multisig (M in M-of-N)
  * param: num_signers - number of participants in multisig (N)
  */
  //----------------------------------------------------------------------------------------------------------------------
  static void check_multisig_config(const std::uint32_t round,
    const std::uint32_t threshold,
    const std::uint32_t num_signers)
  {
    CHECK_AND_ASSERT_THROW_MES(num_signers > 1, "Must be at least one other multisig signer.");
    CHECK_AND_ASSERT_THROW_MES(num_signers <= config::MULTISIG_MAX_SIGNERS,
      "Too many multisig signers specified (limit = 16 to prevent dangerous combinatorial explosion during key exchange).");
    CHECK_AND_ASSERT_THROW_MES(num_signers >= threshold,
      "Multisig threshold may not be larger than number of signers.");
    CHECK_AND_ASSERT_THROW_MES(threshold > 0, "Multisig threshold must be > 0.");
    CHECK_AND_ASSERT_THROW_MES(round > 0, "Multisig kex round must be > 0.");
    CHECK_AND_ASSERT_THROW_MES(round <= multisig_kex_rounds_required(num_signers, threshold) + 1,
      "Trying to process multisig kex for an invalid round.");
  }
  //----------------------------------------------------------------------------------------------------------------------
  /**
  * INTERNAL
  *
  * brief: calculate_multisig_keypair_from_derivation - wrapper on calculate_multisig_keypair() for an input public key
  *    Converts an input public key into a crypto private key (type cast, does not change serialization),
  *    then passes it to get_multisig_blinded_secret_key().
  *
  *    Result:
  *      - privkey = H(derivation)
  *      - pubkey = privkey * G
  * param: derivation - a curve point
  * outparam: derived_pubkey_out - public key of the resulting privkey
  * return: multisig private key
  */
  //----------------------------------------------------------------------------------------------------------------------
  static crypto::secret_key calculate_multisig_keypair_from_derivation(const crypto::public_key_memsafe &derivation,
    crypto::public_key &derived_pubkey_out)
  {
    crypto::secret_key blinded_skey = get_multisig_blinded_secret_key(rct::rct2sk(rct::pk2rct(derivation)));
    CHECK_AND_ASSERT_THROW_MES(crypto::secret_key_to_public_key(blinded_skey, derived_pubkey_out), "Failed to derive public key");

    return blinded_skey;
  }
  //----------------------------------------------------------------------------------------------------------------------
  /**
  * INTERNAL
  *
  * brief: make_multisig_common_privkey - Create the 'common' multisig privkey, owned by all multisig participants.
  *    - common privkey = H(sorted base common privkeys)
  * param: participant_base_common_privkeys - Base common privkeys contributed by multisig participants.
  * outparam: common_privkey_out - result
  */
  //----------------------------------------------------------------------------------------------------------------------
  static void make_multisig_common_privkey(std::vector<crypto::secret_key> participant_base_common_privkeys,
    crypto::secret_key &common_privkey_out)
  {
    // sort the privkeys for consistency
    //TODO: need a constant-time operator< for sorting secret keys
    std::sort(participant_base_common_privkeys.begin(), participant_base_common_privkeys.end(),
        [](const crypto::secret_key &key1, const crypto::secret_key &key2) -> bool
        {
          return memcmp(&key1, &key2, sizeof(crypto::secret_key)) < 0;
        }
      );

    // privkey = H(sorted ancillary base privkeys)
    crypto::hash_to_scalar(participant_base_common_privkeys.data(),
      participant_base_common_privkeys.size()*sizeof(crypto::secret_key),
      common_privkey_out);

    CHECK_AND_ASSERT_THROW_MES(common_privkey_out != crypto::null_skey, "Unexpected null secret key (danger!).");
  }
  //----------------------------------------------------------------------------------------------------------------------
  /**
  * INTERNAL
  *
  * brief: compute_multisig_aggregation_coefficient - creates aggregation coefficient for a specific public key in a set
  *    of public keys
  *
  *    WARNING: The coefficient will only be deterministic if...
  *      1) input keys are pre-sorted
  *         - tested here
  *      2) input keys are in canonical form (compressed points in the prime-order subgroup of Ed25519)
  *         - untested here for performance
  * param: sorted_keys - set of component public keys that will be merged into a multisig public spend key
  * param: aggregation_key - one of the component public keys
  * return: aggregation coefficient
  */
  //----------------------------------------------------------------------------------------------------------------------
  static rct::key compute_multisig_aggregation_coefficient(const std::vector<crypto::public_key> &sorted_keys,
    const crypto::public_key &aggregation_key)
  {
    CHECK_AND_ASSERT_THROW_MES(std::is_sorted(sorted_keys.begin(), sorted_keys.end()),
      "Keys for aggregation coefficient aren't sorted.");

    // aggregation key must be in sorted_keys
    CHECK_AND_ASSERT_THROW_MES(std::find(sorted_keys.begin(), sorted_keys.end(), aggregation_key) != sorted_keys.end(),
      "Aggregation key expected to be in input keyset.");

    // aggregation coefficient salt
    rct::key salt = rct::zero();
    static_assert(sizeof(rct::key) >= sizeof(config::HASH_KEY_MULTISIG_KEY_AGGREGATION), "Hash domain separator is too big.");
    memcpy(salt.bytes, config::HASH_KEY_MULTISIG_KEY_AGGREGATION, sizeof(config::HASH_KEY_MULTISIG_KEY_AGGREGATION));

    // coeff = H(aggregation_key, sorted_keys, domain-sep)
    rct::keyV data;
    data.reserve(sorted_keys.size() + 2);
    data.push_back(rct::pk2rct(aggregation_key));
    for (const auto &key : sorted_keys)
      data.push_back(rct::pk2rct(key));
    data.push_back(salt);

    // note: coefficient is considered public knowledge, no need to memwipe data
    return rct::hash_to_scalar(data);
  }
  //----------------------------------------------------------------------------------------------------------------------
  /**
  * INTERNAL
  *
  * brief: generate_multisig_aggregate_key - generates a multisig public spend key via key aggregation
  *    Key aggregation via aggregation coefficients prevents key cancellation attacks.
  *    See: https://www.getmonero.org/resources/research-lab/pubs/MRL-0009.pdf
  * param: final_keys - address components (public keys) obtained from other participants (not shared with local)
  * param: privkeys_inout - private keys of address components known by local; each key will be multiplied by an aggregation coefficient (return by reference)
  * return: final multisig public spend key for the account
  */
  //----------------------------------------------------------------------------------------------------------------------
  static crypto::public_key generate_multisig_aggregate_key(std::vector<crypto::public_key> final_keys,
    std::vector<crypto::secret_key> &privkeys_inout)
  {
    // collect all public keys that will go into the spend key (these don't need to be memsafe)
    final_keys.reserve(final_keys.size() + privkeys_inout.size());

    // 1. convert local multisig private keys to pub keys
    // 2. insert to final keyset if not there yet
    // 3. save the corresponding index of input priv key set for later reference
    std::unordered_map<crypto::public_key, std::size_t> own_keys_mapping;

    for (std::size_t multisig_keys_index{0}; multisig_keys_index < privkeys_inout.size(); ++multisig_keys_index)
    {
      crypto::public_key pubkey;
      CHECK_AND_ASSERT_THROW_MES(crypto::secret_key_to_public_key(privkeys_inout[multisig_keys_index], pubkey), "Failed to derive public key");

      own_keys_mapping[pubkey] = multisig_keys_index;

      final_keys.push_back(pubkey);
    }

    // sort input final keys for computing aggregation coefficients (lowest to highest)
    // note: input should be sanitized (no duplicates)
    std::sort(final_keys.begin(), final_keys.end());
    CHECK_AND_ASSERT_THROW_MES(std::adjacent_find(final_keys.begin(), final_keys.end()) == final_keys.end(),
        "Unexpected duplicate found in input list.");

    // key aggregation
    rct::key aggregate_key = rct::identity();

    for (const crypto::public_key &key : final_keys)
    {
      // get aggregation coefficient
      rct::key coeff = compute_multisig_aggregation_coefficient(final_keys, key);

      // convert private key if possible
      // note: retain original priv key index in input list, in case order matters upstream
      auto found_key = own_keys_mapping.find(key);
      if (found_key != own_keys_mapping.end())
      {
        // k_agg = coeff*k_base
        sc_mul((unsigned char*)&(privkeys_inout[found_key->second]),
          coeff.bytes,
          (const unsigned char*)&(privkeys_inout[found_key->second]));

        CHECK_AND_ASSERT_THROW_MES(privkeys_inout[found_key->second] != crypto::null_skey,
          "Multisig privkey with aggregation coefficient unexpectedly null.");
      }

      // convert public key (pre-merge operation)
      // K_agg = coeff*K_base
      rct::key converted_pubkey = rct::scalarmultKey(rct::pk2rct(key), coeff);

      // build aggregate key (merge operation)
      rct::addKeys(aggregate_key, aggregate_key, converted_pubkey);
    }

    return rct::rct2pk(aggregate_key);
  }
  //----------------------------------------------------------------------------------------------------------------------
  /**
  * INTERNAL
  *
  * brief: multisig_kex_make_round_keys - Makes a kex round's keys.
  *    - Involves DH exchanges with pubkeys provided by other participants.
  *    - Conserves mapping [pubkey -> DH derivation] : [origin keys of participants that share this secret with you].
  * param: base_privkey - account's base private key, for performing DH exchanges and signing messages
  * param: pubkey_origins_map - map between pubkeys to produce DH derivations with and identity keys of
  *    participants who will share each derivation with you
  * outparam: derivation_origins_map_out - map between DH derivations (shared secrets) and identity keys
  */
  //----------------------------------------------------------------------------------------------------------------------
  static void multisig_kex_make_round_keys(const crypto::secret_key &base_privkey,
    multisig_keyset_map_memsafe_t pubkey_origins_map,
    multisig_keyset_map_memsafe_t &derivation_origins_map_out)
  {
    // make shared secrets with input pubkeys
    derivation_origins_map_out.clear();

    for (auto &pubkey_and_origins : pubkey_origins_map)
    {
      // D = 8 * k_base * K_pubkey
      // note: must be mul8 (cofactor), otherwise it is possible to leak to a malicious participant if the local
      //       base_privkey is a multiple of 8 or not
      // note2: avoid making temporaries that won't be memwiped
      rct::key derivation_rct;
      auto a_wiper = epee::misc_utils::create_scope_leave_handler([&]{
        memwipe(&derivation_rct, sizeof(rct::key));
      });

      rct::scalarmultKey(derivation_rct, rct::pk2rct(pubkey_and_origins.first), rct::sk2rct(base_privkey));
      rct::scalarmultKey(derivation_rct, derivation_rct, rct::EIGHT);

      // retain mapping between pubkey's origins and the DH derivation
      // note: if working on last kex round, then caller must know how to handle these derivations properly
      derivation_origins_map_out[rct::rct2pk(derivation_rct)] = std::move(pubkey_and_origins.second);
    }
  }
  //----------------------------------------------------------------------------------------------------------------------
  /**
  * INTERNAL
  *
  * brief: check_messages_round - Check that a set of messages have an expected round number.
  * param: expanded_msgs - set of multisig kex messages to process
  * param: expected_round - round number the kex messages should have
  */
  //----------------------------------------------------------------------------------------------------------------------
  static void check_messages_round(const std::vector<multisig_kex_msg> &expanded_msgs,
    const std::uint32_t expected_round)
  {
    CHECK_AND_ASSERT_THROW_MES(expanded_msgs.size() > 0, "At least one input message expected.");
    const std::uint32_t round{expanded_msgs[0].get_round()};
    CHECK_AND_ASSERT_THROW_MES(round == expected_round, "Messages don't have the expected kex round number.");

    for (const auto &expanded_msg : expanded_msgs)
      CHECK_AND_ASSERT_THROW_MES(expanded_msg.get_round() == round, "All messages must have the same kex round number.");
  }
  //----------------------------------------------------------------------------------------------------------------------
  /**
  * INTERNAL
  *
  * brief: multisig_kex_msgs_sanitize_pubkeys - Sanitize multisig kex messages.
  *    - Removes duplicates from msg pubkeys, ignores pubkeys equal to the local account's signing key,
  *      ignores messages signed by the local account, ignores keys found in input 'exclusion set',
  *      constructs map of pubkey:origins.
  *    - Requires that all input msgs have the same round number.
  *
  *    origins = all the signing pubkeys that recommended a given pubkey found in input msgs
  *
  *    - If the messages' round numbers are all '1', then only the message signing pubkey is considered
  *      'recommended'. Furthermore, the 'exclusion set' is ignored.
  * param: own_pubkey - local account's signing key (key used to sign multisig messages)
  * param: expanded_msgs - set of multisig kex messages to process
  * param: exclude_pubkeys - pubkeys to exclude from output set
  * outparam: sanitized_pubkeys_out - processed pubkeys obtained from msgs, mapped to their origins
  * return: round number shared by all input msgs
  */
  //----------------------------------------------------------------------------------------------------------------------
  static std::uint32_t multisig_kex_msgs_sanitize_pubkeys(const crypto::public_key &own_pubkey,
    const std::vector<multisig_kex_msg> &expanded_msgs,
    const std::vector<crypto::public_key> &exclude_pubkeys,
    multisig_keyset_map_memsafe_t &sanitized_pubkeys_out)
  {
    // all messages should have the same round (redundant sanity check)
    CHECK_AND_ASSERT_THROW_MES(expanded_msgs.size() > 0, "At least one input message expected.");
    const std::uint32_t round{expanded_msgs[0].get_round()};
    check_messages_round(expanded_msgs, round);

    sanitized_pubkeys_out.clear();

    // get all pubkeys from input messages, add them to pubkey:origins map
    // - origins = all the signing pubkeys that recommended a given msg pubkey
    for (const auto &expanded_msg : expanded_msgs)
    {
      // ignore messages from self
      if (expanded_msg.get_signing_pubkey() == own_pubkey)
        continue;

      // in round 1, only the signing pubkey is treated as a msg pubkey
      if (round == 1)
      {
        // note: ignores duplicates
        sanitized_pubkeys_out[expanded_msg.get_signing_pubkey()].insert(expanded_msg.get_signing_pubkey());
      }
      // in other rounds, only the msg pubkeys are treated as msg pubkeys
      else
      {
        // copy all pubkeys from message into list
        for (const auto &pubkey : expanded_msg.get_msg_pubkeys())
        {
          // ignore own pubkey
          if (pubkey == own_pubkey)
            continue;

          // ignore pubkeys in 'ignore' set
          if (std::find(exclude_pubkeys.begin(), exclude_pubkeys.end(), pubkey) != exclude_pubkeys.end())
            continue;

          // note: ignores duplicates
          sanitized_pubkeys_out[pubkey].insert(expanded_msg.get_signing_pubkey());
        }
      }
    }

    return round;
  }
  //----------------------------------------------------------------------------------------------------------------------
  /**
  * INTERNAL
  *
  * brief: evaluate_multisig_kex_round_msgs - Evaluate pubkeys from a kex round in order to prepare for the next round.
  *    - Sanitizes input msgs.
  *    - Require uniqueness in: 'exclude_pubkeys'.
  *    - Requires each input pubkey be recommended by 'num_recommendations = expected_round' msg signers.
  *      - For a final multisig key to be truly 'M-of-N', each of the the private key's components must be
  *        shared by (N - M + 1) signers.
  *    - Requires that msgs are signed by only keys in 'signers'.
  *    - Requires that each key in 'signers' recommends [num_signers - 2 CHOOSE (expected_round - 1)] pubkeys.
  *      - These should be derivations each signer recommends for round 'expected_round', excluding derivations shared
  *        with the local account.
  *    - Requires that 'exclude_pubkeys' has [num_signers - 1 CHOOSE (expected_round - 1)] pubkeys.
  *      - These should be derivations the local account has corresponding to round 'expected_round'.
  * param: base_pubkey - multisig account's base public key
  * param: expected_round - expected kex round of input messages
  * param: signers - expected participants in multisig kex
  * param: expanded_msgs - set of multisig kex messages to process
  * param: exclude_pubkeys - derivations held by the local account corresponding to round 'expected_round'
  * return: fully sanitized and validated pubkey:origins map for building the account's next kex round message
  */
  //----------------------------------------------------------------------------------------------------------------------
  static multisig_keyset_map_memsafe_t evaluate_multisig_kex_round_msgs(
    const crypto::public_key &base_pubkey,
    const std::uint32_t expected_round,
    const std::vector<crypto::public_key> &signers,
    const std::vector<multisig_kex_msg> &expanded_msgs,
    const std::vector<crypto::public_key> &exclude_pubkeys)
  {
    // exclude_pubkeys should all be unique
    for (auto it = exclude_pubkeys.begin(); it != exclude_pubkeys.end(); ++it)
    {
      CHECK_AND_ASSERT_THROW_MES(std::find(exclude_pubkeys.begin(), it, *it) == it,
        "Found duplicate pubkeys for exclusion unexpectedly.");
    }

    // sanitize input messages
    multisig_keyset_map_memsafe_t pubkey_origins_map;
    const std::uint32_t round = multisig_kex_msgs_sanitize_pubkeys(base_pubkey, expanded_msgs, exclude_pubkeys, pubkey_origins_map);
    CHECK_AND_ASSERT_THROW_MES(round == expected_round,
      "Kex messages were for round [" << round << "], but expected round is [" << expected_round << "]");

    // evaluate pubkeys collected
    std::unordered_map<crypto::public_key, std::unordered_set<crypto::public_key>> origin_pubkeys_map;

    // 1. each pubkey should be recommended by a precise number of signers
    for (const auto &pubkey_and_origins : pubkey_origins_map)
    {
      // expected amount = round_num
      // With each successive round, pubkeys are shared by incrementally larger groups,
      //  starting at 1 in round 1 (i.e. the local multisig key to start kex with).
      CHECK_AND_ASSERT_THROW_MES(pubkey_and_origins.second.size() == round,
        "A pubkey recommended by multisig kex messages had an unexpected number of recommendations.");

      // map (sanitized) pubkeys back to origins
      for (const auto &origin : pubkey_and_origins.second)
        origin_pubkeys_map[origin].insert(pubkey_and_origins.first);
    }

    // 2. the number of unique signers recommending pubkeys should equal the number of signers passed in (minus the local signer)
    CHECK_AND_ASSERT_THROW_MES(origin_pubkeys_map.size() == signers.size() - 1,
      "Number of unique other signers does not equal number of other signers that recommended pubkeys.");

    // 3. each origin should recommend a precise number of pubkeys

    // TODO: move to a 'math' library, with unit tests
    auto n_choose_k_f =
      [](const std::uint32_t n, const std::uint32_t k) -> std::uint32_t
      {
        static_assert(std::numeric_limits<std::int32_t>::digits <= std::numeric_limits<double>::digits,
          "n_choose_k requires no rounding issues when converting between int32 <-> double.");

        if (n < k)
          return 0;

        double fp_result = boost::math::binomial_coefficient<double>(n, k);

        if (fp_result < 0)
          return 0;

        if (fp_result > std::numeric_limits<std::int32_t>::max())  // note: std::round() returns std::int32_t
          return 0;

        return static_cast<std::uint32_t>(std::round(fp_result));
      };

    // other signers: (N - 2) choose (msg_round_num - 1)
      // - Each signer recommends keys they share with other signers.
      // - In each round, a signer shares a key with 'round num - 1' other signers.
      // - Since 'origins pubkey map' excludes keys shared with the local account,
      //   only keys shared with participants 'other than local and self' will be in the map (e.g. N - 2 signers).
      // - So other signers will recommend (N - 2) choose (msg_round_num - 1) pubkeys (after removing keys shared with local).
      // - Each origin should have a shared key with each group of size 'round - 1'.
      // Note: Keys shared with local are ignored to facilitate kex round boosting, where one or more signers may
      //       have boosted the local signer (implying they didn't have access to the local signer's previous round msg).
    const std::uint32_t expected_recommendations_others = n_choose_k_f(signers.size() - 2, round - 1);

    // local: (N - 1) choose (msg_round_num - 1)
    const std::uint32_t expected_recommendations_self = n_choose_k_f(signers.size() - 1, round - 1);

    // note: expected_recommendations_others would be 0 in the last round of 1-of-N, but we return early for that case
    CHECK_AND_ASSERT_THROW_MES(expected_recommendations_self > 0 && expected_recommendations_others > 0,
      "Bad num signers or round num (possibly numerical limits exceeded).");

    // check that local account recommends expected number of keys
    CHECK_AND_ASSERT_THROW_MES(exclude_pubkeys.size() == expected_recommendations_self,
      "Local account did not recommend expected number of multisig keys.");

    // check that other signers recommend expected number of keys
    for (const auto &origin_and_pubkeys : origin_pubkeys_map)
    {
      CHECK_AND_ASSERT_THROW_MES(origin_and_pubkeys.second.size() == expected_recommendations_others,
        "A pubkey recommended by multisig kex messages had an unexpected number of recommendations.");

      // 2 (continued). only expected signers should be recommending keys
      CHECK_AND_ASSERT_THROW_MES(std::find(signers.begin(), signers.end(), origin_and_pubkeys.first) != signers.end(),
        "Multisig kex message with unexpected signer encountered.");
    }

    // note: above tests implicitly detect if the total number of recommended keys is correct or not
    return pubkey_origins_map;
  }
  //----------------------------------------------------------------------------------------------------------------------
  /**
  * INTERNAL
  *
  * brief: evaluate_multisig_post_kex_round_msgs - Evaluate messages for the post-kex verification round.
  *    - Sanitizes input msgs.
  *    - Requires that only one pubkey is recommended.
  *    - Requires that all signers (other than self) recommend that one pubkey.
  * param: base_pubkey - multisig account's base public key
  * param: expected_round - expected kex round of input messages
  * param: signers - expected participants in multisig kex
  * param: expanded_msgs - set of multisig kex messages to process
  * return: sanitized and validated pubkey:origins map
  */
  //----------------------------------------------------------------------------------------------------------------------
  static multisig_keyset_map_memsafe_t evaluate_multisig_post_kex_round_msgs(
    const crypto::public_key &base_pubkey,
    const std::uint32_t expected_round,
    const std::vector<crypto::public_key> &signers,
    const std::vector<multisig_kex_msg> &expanded_msgs)
  {
    // sanitize input messages
    const std::vector<crypto::public_key> dummy;
    multisig_keyset_map_memsafe_t pubkey_origins_map;
    const std::uint32_t round = multisig_kex_msgs_sanitize_pubkeys(base_pubkey, expanded_msgs, dummy, pubkey_origins_map);
    CHECK_AND_ASSERT_THROW_MES(round == expected_round,
      "Kex messages were for round [" << round << "], but expected round is [" << expected_round << "]");

    // evaluate pubkeys collected

    // 1) there should only be two pubkeys
    CHECK_AND_ASSERT_THROW_MES(pubkey_origins_map.size() == 2,
      "Multisig post-kex round messages from other signers did not all contain two pubkeys.");

    // 2) both keys should be recommended by the same set of signers
    CHECK_AND_ASSERT_THROW_MES(pubkey_origins_map.begin()->second == (++(pubkey_origins_map.begin()))->second,
      "Multisig post-kex round messages from other signers did not all recommend the same pubkey pair.");

    // 3) all signers should be present in the recommendation list
    auto origins = pubkey_origins_map.begin()->second;
    origins.insert(base_pubkey);  //add self

    CHECK_AND_ASSERT_THROW_MES(origins.size() == signers.size(),
      "Multisig post-kex round message origins don't line up with multisig signer set.");

    for (const crypto::public_key &signer : signers)
    {
      CHECK_AND_ASSERT_THROW_MES(origins.find(signer) != origins.end(),
        "Could not find an expected signer in multisig post-kex round messages (all signers expected).");
    }

    return pubkey_origins_map;
  }
  //----------------------------------------------------------------------------------------------------------------------
  /**
  * INTERNAL
  *
  * brief: multisig_kex_process_round_msgs - Process kex messages for the active kex round.
  *    - A wrapper around evaluate_multisig_kex_round_msgs() -> multisig_kex_make_next_msg().
  *      - In other words, evaluate the input messages and try to make a message for the next round.
  *    - Note: Must be called on the final round's msgs to evaluate the final key components
  *            recommended by other participants.
  * param: base_privkey - multisig account's base private key
  * param: current_round - round of kex the input messages should be designed for
  * param: threshold - threshold for multisig (M in M-of-N)
  * param: signers - expected participants in multisig kex
  * param: expanded_msgs - set of multisig kex messages to process
  * param: exclude_pubkeys - keys held by the local account corresponding to round 'current_round'
  *    - If 'current_round' is the final round, these are the local account's shares of the final aggregate key.
  * outparam: keys_to_origins_map_out - map between round keys and identity keys
  *    - If in the final round, these are key shares recommended by other signers for the final aggregate key.
  *    - Otherwise, these are the local account's DH derivations for the next round.
  *      - See multisig_kex_make_next_msg() for an explanation.
  * return: multisig kex message for next round, or empty message if 'current_round' is the final round
  */
  //----------------------------------------------------------------------------------------------------------------------
  static void multisig_kex_process_round_msgs(const crypto::secret_key &base_privkey,
    const crypto::public_key &base_pubkey,
    const std::uint32_t current_round,
    const std::uint32_t threshold,
    const std::vector<crypto::public_key> &signers,
    const std::vector<multisig_kex_msg> &expanded_msgs,
    const std::vector<crypto::public_key> &exclude_pubkeys,
    multisig_keyset_map_memsafe_t &keys_to_origins_map_out)
  {
    check_multisig_config(current_round, threshold, signers.size());
    const std::uint32_t kex_rounds_required{multisig_kex_rounds_required(signers.size(), threshold)};

    // process messages into a [pubkey : {origins}] map
    multisig_keyset_map_memsafe_t evaluated_pubkeys;

    if (threshold == 1 && current_round == kex_rounds_required)
    {
      // in the last main kex round of 1-of-N, all signers share a key so the local signer doesn't care about evaluating
      // recommendations from other signers
    }
    else if (current_round <= kex_rounds_required)
    {
      // for normal kex rounds, fully evaluate kex round messages
      evaluated_pubkeys = evaluate_multisig_kex_round_msgs(base_pubkey,
        current_round,
        signers,
        expanded_msgs,
        exclude_pubkeys);
    }
    else //(current_round == kex_rounds_required + 1)
    {
      // for the post-kex verification round, validate the last kex round's messages
      evaluated_pubkeys = evaluate_multisig_post_kex_round_msgs(base_pubkey,
        current_round,
        signers,
        expanded_msgs);
    }

    // prepare keys-to-origins map for updating the multisig account
    if (current_round < kex_rounds_required)
    {
      // normal kex round: make new keys
      multisig_kex_make_round_keys(base_privkey, std::move(evaluated_pubkeys), keys_to_origins_map_out);
    }
    else if (current_round >= kex_rounds_required)
    {
      // last kex round: collect the key shares recommended by other signers for the final aggregate key
      // post-kex verification round: save the keys found in input messages
      keys_to_origins_map_out = std::move(evaluated_pubkeys);
    }
  }
  //----------------------------------------------------------------------------------------------------------------------
  // multisig_account: INTERNAL
  //----------------------------------------------------------------------------------------------------------------------
  void multisig_account::initialize_kex_update(const std::vector<multisig_kex_msg> &expanded_msgs,
    const std::uint32_t kex_rounds_required,
    std::vector<crypto::public_key> &exclude_pubkeys_out)
  {
    if (m_kex_rounds_complete == 0)
    {
      // the first round of kex msgs will contain each participant's base pubkeys and ancillary privkeys

      // collect participants' base common privkey shares
      // note: duplicate privkeys are acceptable, and duplicates due to duplicate signers
      //       will be blocked by duplicate-signer errors after this function is called
      std::vector<crypto::secret_key> participant_base_common_privkeys;
      participant_base_common_privkeys.reserve(expanded_msgs.size() + 1);

      // add local ancillary base privkey
      participant_base_common_privkeys.emplace_back(m_base_common_privkey);

      // add other signers' base common privkeys
      for (const auto &expanded_msg : expanded_msgs)
      {
        if (expanded_msg.get_signing_pubkey() != m_base_pubkey)
        {
          participant_base_common_privkeys.emplace_back(expanded_msg.get_msg_privkey());
        }
      }

      // make common privkey
      make_multisig_common_privkey(std::move(participant_base_common_privkeys), m_common_privkey);

      // set common pubkey
      CHECK_AND_ASSERT_THROW_MES(crypto::secret_key_to_public_key(m_common_privkey, m_common_pubkey),
        "Failed to derive public key");

      // if N-of-N, then the base privkey will be used directly to make the account's share of the final key
      if (kex_rounds_required == 1)
      {
        m_multisig_privkeys.clear();
        m_multisig_privkeys.emplace_back(m_base_privkey);
      }

      // exclude all keys the local account recommends
      // - in the first round, only the local pubkey is recommended by the local signer
      exclude_pubkeys_out.emplace_back(m_base_pubkey);
    }
    else
    {
      // in other rounds, kex msgs will contain participants' shared keys

      // ignore shared keys the account helped create for this round
      for (const auto &shared_key_with_origins : m_kex_keys_to_origins_map)
      {
        exclude_pubkeys_out.emplace_back(shared_key_with_origins.first);
      }
    }
  }
  //----------------------------------------------------------------------------------------------------------------------
  // multisig_account: INTERNAL
  //----------------------------------------------------------------------------------------------------------------------
  void multisig_account::finalize_kex_update(const std::uint32_t kex_rounds_required,
    multisig_keyset_map_memsafe_t result_keys_to_origins_map)
  {
    std::vector<crypto::public_key> next_msg_keys;

    // prepare for next round (or complete the multisig account fully)
    if (m_kex_rounds_complete == kex_rounds_required)
    {
      // post-kex verification round: check that the multisig pubkey and common pubkey were recommended by other signers
      CHECK_AND_ASSERT_THROW_MES(result_keys_to_origins_map.count(m_multisig_pubkey) > 0,
        "Multisig post-kex round: expected multisig pubkey wasn't found in other signers' messages.");
      CHECK_AND_ASSERT_THROW_MES(result_keys_to_origins_map.count(m_common_pubkey) > 0,
        "Multisig post-kex round: expected common pubkey wasn't found in other signers' messages.");

      // save keys that should be recommended to other signers
      // - for convenience, re-recommend the post-kex verification message once an account is complete
      next_msg_keys.reserve(2);
      next_msg_keys.push_back(m_multisig_pubkey);
      next_msg_keys.push_back(m_common_pubkey);
    }
    else if (m_kex_rounds_complete + 1 == kex_rounds_required)
    {
      // finished with main kex rounds (have set of msgs to complete address)

      // when 'completing the final round', result keys are other signers' shares of the final key
      std::vector<crypto::public_key> result_keys;
      result_keys.reserve(result_keys_to_origins_map.size());

      for (const auto &result_key_and_origins : result_keys_to_origins_map)
      {
        result_keys.emplace_back(result_key_and_origins.first);
      }

      // compute final aggregate key, update local multisig privkeys with aggregation coefficients applied
      m_multisig_pubkey = generate_multisig_aggregate_key(std::move(result_keys), m_multisig_privkeys);

      // no longer need the account's pubkeys saved for this round (they were only used to build exclude_pubkeys)
      // TODO: record [pre-aggregation pubkeys : origins] map for aggregation-style signing
      m_kex_keys_to_origins_map.clear();

      // save keys that should be recommended to other signers
      // - for post-kex verification, recommend the multisig pubkeys to notify other signers that the local signer is done
      next_msg_keys.reserve(2);
      next_msg_keys.push_back(m_multisig_pubkey);
      next_msg_keys.push_back(m_common_pubkey);
    }
    else if (m_kex_rounds_complete + 2 == kex_rounds_required)
    {
      // one more round (must send/receive one more set of kex msgs)
      // - at this point, have local signer's pre-aggregation private key shares of the final address

      // result keys are the local signer's DH derivations for the next round

      // derivations are shared secrets between each group of N - M + 1 signers of which the local account is a member
      // - convert them to private keys: multisig_key = H(derivation)
      // - note: shared key = multisig_key[i]*G is recorded in the kex msg for sending to other participants
      //   instead of the original 'derivation' value (which MUST be kept secret!)
      m_multisig_privkeys.clear();
      m_multisig_privkeys.reserve(result_keys_to_origins_map.size());

      m_kex_keys_to_origins_map.clear();
      next_msg_keys.reserve(result_keys_to_origins_map.size());

      for (const auto &derivation_and_origins : result_keys_to_origins_map)
      {
        // multisig_privkey = H(derivation)
        // derived pubkey = multisig_key * G
        crypto::public_key_memsafe derived_pubkey;
        m_multisig_privkeys.push_back(
          calculate_multisig_keypair_from_derivation(derivation_and_origins.first, derived_pubkey));

        // save the account's kex key mappings for this round [derived pubkey : other signers who will have the same key]
        m_kex_keys_to_origins_map[derived_pubkey] = std::move(derivation_and_origins.second);

        // save keys that should be recommended to other signers
        // - The keys multisig_key*G are sent to other participants in the message, so they can be used to produce the final
        //   multisig key via generate_multisig_spend_public_key().
        next_msg_keys.push_back(derived_pubkey);
      }
    }
    else  //(m_kex_rounds_complete + 3 <= kex_rounds_required)
    {
      // next round is an 'intermediate' key exchange round, so there is nothing special to do here

      // save keys that should be recommended to other signers
      // - Send this round's DH derivations to other participants, who will make more DH derivations for the following round.
      next_msg_keys.reserve(result_keys_to_origins_map.size());

      for (const auto &derivation_and_origins : result_keys_to_origins_map)
        next_msg_keys.push_back(derivation_and_origins.first);

      // save the account's kex keys for this round [DH derivation : other signers who should have the same derivation]
      m_kex_keys_to_origins_map = std::move(result_keys_to_origins_map);
    }

    // a full set of msgs has been collected and processed, so the 'round is complete'
    ++m_kex_rounds_complete;

    // make next round's message (or reproduce the post-kex verification round if kex is complete)
    m_next_round_kex_message = multisig_kex_msg{
      (m_kex_rounds_complete > kex_rounds_required ? kex_rounds_required : m_kex_rounds_complete) + 1,
      m_base_privkey,
      std::move(next_msg_keys)}.get_msg();
  }
  //----------------------------------------------------------------------------------------------------------------------
  // multisig_account: INTERNAL
  //----------------------------------------------------------------------------------------------------------------------
  void multisig_account::kex_update_impl(const std::vector<multisig_kex_msg> &expanded_msgs)
  {
    // check messages are for the expected kex round
    check_messages_round(expanded_msgs, m_kex_rounds_complete + 1);

    // check kex round count
    const std::uint32_t kex_rounds_required{multisig_kex_rounds_required(m_signers.size(), m_threshold)};

    CHECK_AND_ASSERT_THROW_MES(kex_rounds_required > 0, "Multisig kex rounds required unexpectedly 0.");
    CHECK_AND_ASSERT_THROW_MES(m_kex_rounds_complete < kex_rounds_required + 1,
      "Multisig kex has already completed all required rounds (including post-kex verification).");

    // initialize account update
    std::vector<crypto::public_key> exclude_pubkeys;
    initialize_kex_update(expanded_msgs, kex_rounds_required, exclude_pubkeys);

    // process messages into a [pubkey : {origins}] map
    multisig_keyset_map_memsafe_t result_keys_to_origins_map;
    multisig_kex_process_round_msgs(
      m_base_privkey,
      m_base_pubkey,
      m_kex_rounds_complete + 1,
      m_threshold,
      m_signers,
      expanded_msgs,
      exclude_pubkeys,
      result_keys_to_origins_map);

    // finish account update
    finalize_kex_update(kex_rounds_required, std::move(result_keys_to_origins_map));
  }
  //----------------------------------------------------------------------------------------------------------------------
} //namespace multisig