/* Copyright 2017 The sfcpp Authors. All Rights Reserved.

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.
==============================================================================*/

#pragma once

#include <Eigen/Dense>

#include <latex/tikz/TikzElement.hpp>
#include <latex/tikz/TikzElementConfiguration.hpp>
#include <sfc/CurveInformation.hpp>
#include <sfc/CurveSpecification.hpp>

#include <memory>
#include <string>

namespace sfcpp {
namespace sfc {

/**
 * Helper class that realizes a node of a tree.
 */
struct SpacetreeNode {
  size_t state;
  size_t level;
  size_t index;
  bool isLeaf;
  Eigen::MatrixXd pointMatrix;
  std::vector<std::shared_ptr<SpacetreeNode>> children;

  SpacetreeNode(size_t state, size_t level, size_t index, bool isLeaf,
                Eigen::MatrixXd const &pointMatrix)
      : state(state),
        level(level),
        index(index),
        isLeaf(isLeaf),
        pointMatrix(pointMatrix) {}

  SpacetreeNode()
      : state(0), level(0), index(0), isLeaf(true), pointMatrix(), children() {}

  Eigen::VectorXd getBarycenter() const {
    return (1.0 / pointMatrix.cols()) * pointMatrix *
           Eigen::VectorXd::Ones(pointMatrix.cols());
  }
};

/**
 * Helper class for rendering faces.
 */
struct SpacetreeFace {
  size_t dim;
  size_t faceNumber;
  SpacetreeNode node;
  std::vector<Eigen::VectorXd> vertices;

  SpacetreeFace(size_t dim, size_t faceNumber, SpacetreeNode node,
                std::vector<Eigen::VectorXd> vertices)
      : dim(dim), faceNumber(faceNumber), node(node), vertices(vertices) {}
};

/**
 * This class can be used to generate Latex/TikZ code for CurveSpecification
 * objects.
 */
class CurveRenderer {
 public:
  typedef std::function<void(SpacetreeNode const &)> RenderFunction;
  typedef std::function<void(SpacetreeFace const &)> FaceRenderFunction;

 private:
  std::shared_ptr<CurveSpecification> spec;
  std::shared_ptr<CurveInformation> info;

  std::string structureString;
  std::shared_ptr<SpacetreeNode> root;
  size_t numLeaves;

  std::string getStructureString(size_t numLevels);
  size_t refineRecursively(std::shared_ptr<SpacetreeNode> const &node,
                           std::string &structure, size_t index);
  void renderNodesRecursively(std::shared_ptr<SpacetreeNode> const &node,
                              RenderFunction const &func,
                              bool notOnlyLeaves) const;

 public:
  CurveRenderer(std::shared_ptr<CurveSpecification> spec,
                std::shared_ptr<CurveInformation> info);

  /**
   * Sets the structure of the tree to be rendered.
   *
   * @param structure a string that corresponds to the output of a preorder
   * traversal where each inner node outputs a "1" and each leaf outputs a "0".
   */
  void setTreeStructure(std::string const &structure);

  /**
   * Sets the structure of the tree to a regular grid.
   *
   * @param level level of the leaves in the tree.
   */
  void setTreeStructure(size_t level);

  void renderNodes(RenderFunction const &func) const;
  void renderLeaves(RenderFunction const &func) const;
  void renderFaces(FaceRenderFunction const &func) const;

  size_t getNumLeaves() const;

  /**
   * Renders all 1D faces of all leaves.
   */
  std::shared_ptr<latex::tikz::TikzElement> renderBoundaries(
      latex::tikz::TikzElementConfiguration const &elementConfig) const;

  /**
   * Returns a list of the barycenters of all nodes in the correct order
   */
  std::vector<Eigen::VectorXd> getCurveMidpoints() const;

  /**
   * Connects all barycenters using a linear color transition
   */
  std::shared_ptr<latex::tikz::TikzElement> renderCurve(
      img::HSVColor startColor, img::HSVColor endColor,
      latex::tikz::TikzElementConfiguration config =
          latex::tikz::TikzElementConfiguration(),
      size_t startIndex = 0,
      size_t endIndex = std::numeric_limits<size_t>::max()) const;

  /**
   * @return Returns an appropriate 3D representation of the point
   */
  Eigen::Vector3d point(Eigen::VectorXd coordinates) const;
};

} /* namespace sfc */
} /* namespace sfcpp */


#include <NTL/lzz_pEX.h>
#include <NTL/vec_vec_lzz_p.h>

#include <NTL/new.h>

NTL_START_IMPL

const zz_pEX& zz_pEX::zero()
{
   static zz_pEX z;
   return z;
}

istream& operator>>(istream& s, zz_pEX& x)
{
   s >> x.rep;
   x.normalize();
   return s;
}

ostream& operator<<(ostream& s, const zz_pEX& a)
{
   return s << a.rep;
}

void zz_pEX::normalize()
{
   long n;
   const zz_pE* p;

   n = rep.length();
   if (n == 0) return;
   p = rep.elts() + n;
   while (n > 0 && IsZero(*--p)) {
      n--;
   }
   rep.SetLength(n);
}

long IsZero(const zz_pEX& a)
{
   return a.rep.length() == 0;
}

long IsOne(const zz_pEX& a)
{
    return a.rep.length() == 1 && IsOne(a.rep[0]);
}

long operator==(const zz_pEX& a, long b)
{
   if (b == 0)
      return IsZero(a);

   if (b == 1)
      return IsOne(a);

   long da = deg(a);

   if (da > 0) return 0;

   NTL_zz_pRegister(bb);
   bb = b;

   if (da < 0)
      return IsZero(bb);

   return a.rep[0] == bb;
}

long operator==(const zz_pEX& a, const zz_p& b)
{
   if (IsZero(b))
      return IsZero(a);

   long da = deg(a);

   if (da != 0)
      return 0;

   return a.rep[0] == b;
}

long operator==(const zz_pEX& a, const zz_pE& b)
{
   if (IsZero(b))
      return IsZero(a);

   long da = deg(a);

   if (da != 0)
      return 0;

   return a.rep[0] == b;
}

void SetCoeff(zz_pEX& x, long i, const zz_pE& a)
{
   long j, m;

   if (i < 0)
      Error("SetCoeff: negative index");

   if (NTL_OVERFLOW(i, 1, 0))
      Error("overflow in SetCoeff");

   m = deg(x);

   if (i > m && IsZero(a)) return;

   if (i > m) {
      /* careful: a may alias a coefficient of x */

      long alloc = x.rep.allocated();

      if (alloc > 0 && i >= alloc) {
         zz_pE aa = a;
         x.rep.SetLength(i+1);
         x.rep[i] = aa;
      }
      else {
         x.rep.SetLength(i+1);
         x.rep[i] = a;
      }

      for (j = m+1; j < i; j++)
         clear(x.rep[j]);
   }
   else
      x.rep[i] = a;

   x.normalize();
}

void SetCoeff(zz_pEX& x, long i, const zz_p& aa)
{
   long j, m;

   if (i < 0)
      Error("SetCoeff: negative index");

   if (NTL_OVERFLOW(i, 1, 0))
      Error("overflow in SetCoeff");

   NTL_zz_pRegister(a);  // watch out for aliases!
   a = aa;

   m = deg(x);

   if (i > m && IsZero(a)) return;

   if (i > m) {
      x.rep.SetLength(i+1);
      for (j = m+1; j < i; j++)
         clear(x.rep[j]);
   }
   x.rep[i] = a;
   x.normalize();
}

void SetCoeff(zz_pEX& x, long i, long a)
{
   if (a == 1)
      SetCoeff(x, i);
   else {
      NTL_zz_pRegister(T);
      T = a;
      SetCoeff(x, i, T);
   }
}

void SetCoeff(zz_pEX& x, long i)
{
   long j, m;

   if (i < 0)
      Error("coefficient index out of range");

   if (NTL_OVERFLOW(i, 1, 0))
      Error("overflow in SetCoeff");

   m = deg(x);

   if (i > m) {
      x.rep.SetLength(i+1);
      for (j = m+1; j < i; j++)
         clear(x.rep[j]);
   }
   set(x.rep[i]);
   x.normalize();
}

void SetX(zz_pEX& x)
{
   clear(x);
   SetCoeff(x, 1);
}

long IsX(const zz_pEX& a)
{
   return deg(a) == 1 && IsOne(LeadCoeff(a)) && IsZero(ConstTerm(a));
}

const zz_pE& coeff(const zz_pEX& a, long i)
{
   if (i < 0 || i > deg(a))
      return zz_pE::zero();
   else
      return a.rep[i];
}

const zz_pE& LeadCoeff(const zz_pEX& a)
{
   if (IsZero(a))
      return zz_pE::zero();
   else
      return a.rep[deg(a)];
}

const zz_pE& ConstTerm(const zz_pEX& a)
{
   if (IsZero(a))
      return zz_pE::zero();
   else
      return a.rep[0];
}

void conv(zz_pEX& x, const zz_pE& a)
{
   if (IsZero(a))
      x.rep.SetLength(0);
   else {
      x.rep.SetLength(1);
      x.rep[0] = a;
   }
}

void conv(zz_pEX& x, long a)
{
   if (a == 0)
      clear(x);
   else if (a == 1)
      set(x);
   else {
      NTL_zz_pRegister(T);
      T = a;
      conv(x, T);
   }
}

void conv(zz_pEX& x, const ZZ& a)
{
   NTL_zz_pRegister(T);
   conv(T, a);
   conv(x, T);
}

void conv(zz_pEX& x, const zz_p& a)
{
   if (IsZero(a))
      clear(x);
   else if (IsOne(a))
      set(x);
   else {
      x.rep.SetLength(1);
      conv(x.rep[0], a);
      x.normalize();
   }
}

void conv(zz_pEX& x, const zz_pX& aa)
{
   zz_pX a = aa; // in case a aliases the rep of a coefficient of x

   long n = deg(a)+1;
   long i;

   x.rep.SetLength(n);
   for (i = 0; i < n; i++)
      conv(x.rep[i], coeff(a, i));
}

void conv(zz_pEX& x, const vec_zz_pE& a)
{
   x.rep = a;
   x.normalize();
}

void add(zz_pEX& x, const zz_pEX& a, const zz_pEX& b)
{
   long da = deg(a);
   long db = deg(b);
   long minab = min(da, db);
   long maxab = max(da, db);
   x.rep.SetLength(maxab+1);

   long i;
   const zz_pE *ap, *bp;
   zz_pE* xp;

   for (i = minab+1, ap = a.rep.elts(), bp = b.rep.elts(), xp = x.rep.elts();
        i; i--, ap++, bp++, xp++)
      add(*xp, (*ap), (*bp));

   if (da > minab && &x != &a)
      for (i = da-minab; i; i--, xp++, ap++)
         *xp = *ap;
   else if (db > minab && &x != &b)
      for (i = db-minab; i; i--, xp++, bp++)
         *xp = *bp;
   else
      x.normalize();
}

void add(zz_pEX& x, const zz_pEX& a, const zz_pE& b)
{
   long n = a.rep.length();
   if (n == 0) {
      conv(x, b);
   }
   else if (&x == &a) {
      add(x.rep[0], a.rep[0], b);
      x.normalize();
   }
   else if (x.rep.MaxLength() == 0) {
      x = a;
      add(x.rep[0], a.rep[0], b);
      x.normalize();
   }
   else {
      // ugly...b could alias a coeff of x

      zz_pE *xp = x.rep.elts();
      add(xp[0], a.rep[0], b);
      x.rep.SetLength(n);
      xp = x.rep.elts();
      const zz_pE *ap = a.rep.elts();
      long i;
      for (i = 1; i < n; i++)
         xp[i] = ap[i];
      x.normalize();
   }
}

void add(zz_pEX& x, const zz_pEX& a, const zz_p& b)
{
   long n = a.rep.length();
   if (n == 0) {
      conv(x, b);
   }
   else if (&x == &a) {
      add(x.rep[0], a.rep[0], b);
      x.normalize();
   }
   else if (x.rep.MaxLength() == 0) {
      x = a;
      add(x.rep[0], a.rep[0], b);
      x.normalize();
   }
   else {
      // ugly...b could alias a coeff of x

      zz_pE *xp = x.rep.elts();
      add(xp[0], a.rep[0], b);
      x.rep.SetLength(n);
      xp = x.rep.elts();
      const zz_pE *ap = a.rep.elts();
      long i;
      for (i = 1; i < n; i++)
         xp[i] = ap[i];
      x.normalize();
   }
}

void add(zz_pEX& x, const zz_pEX& a, long b)
{
   if (a.rep.length() == 0) {
      conv(x, b);
   }
   else {
      if (&x != &a) x = a;
      add(x.rep[0], x.rep[0], b);
      x.normalize();
   }
}

void sub(zz_pEX& x, const zz_pEX& a, const zz_pEX& b)
{
   long da = deg(a);
   long db = deg(b);
   long minab = min(da, db);
   long maxab = max(da, db);
   x.rep.SetLength(maxab+1);

   long i;
   const zz_pE *ap, *bp;
   zz_pE* xp;

   for (i = minab+1, ap = a.rep.elts(), bp = b.rep.elts(), xp = x.rep.elts();
        i; i--, ap++, bp++, xp++)
      sub(*xp, (*ap), (*bp));

   if (da > minab && &x != &a)
      for (i = da-minab; i; i--, xp++, ap++)
         *xp = *ap;
   else if (db > minab)
      for (i = db-minab; i; i--, xp++, bp++)
         negate(*xp, *bp);
   else
      x.normalize();
}

void sub(zz_pEX& x, const zz_pEX& a, const zz_pE& b)
{
   long n = a.rep.length();
   if (n == 0) {
      conv(x, b);
      negate(x, x);
   }
   else if (&x == &a) {
      sub(x.rep[0], a.rep[0], b);
      x.normalize();
   }
   else if (x.rep.MaxLength() == 0) {
      x = a;
      sub(x.rep[0], a.rep[0], b);
      x.normalize();
   }
   else {
      // ugly...b could alias a coeff of x

      zz_pE *xp = x.rep.elts();
      sub(xp[0], a.rep[0], b);
      x.rep.SetLength(n);
      xp = x.rep.elts();
      const zz_pE *ap = a.rep.elts();
      long i;
      for (i = 1; i < n; i++)
         xp[i] = ap[i];
      x.normalize();
   }
}

void sub(zz_pEX& x, const zz_pEX& a, const zz_p& b)
{
   long n = a.rep.length();
   if (n == 0) {
      conv(x, b);
      negate(x, x);
   }
   else if (&x == &a) {
      sub(x.rep[0], a.rep[0], b);
      x.normalize();
   }
   else if (x.rep.MaxLength() == 0) {
      x = a;
      sub(x.rep[0], a.rep[0], b);
      x.normalize();
   }
   else {
      // ugly...b could alias a coeff of x

      zz_pE *xp = x.rep.elts();
      sub(xp[0], a.rep[0], b);
      x.rep.SetLength(n);
      xp = x.rep.elts();
      const zz_pE *ap = a.rep.elts();
      long i;
      for (i = 1; i < n; i++)
         xp[i] = ap[i];
      x.normalize();
   }
}

void sub(zz_pEX& x, const zz_pEX& a, long b)
{
   if (a.rep.length() == 0) {
      conv(x, b);
      negate(x, x);
   }
   else {
      if (&x != &a) x = a;
      sub(x.rep[0], x.rep[0], b);
      x.normalize();
   }
}

void sub(zz_pEX& x, const zz_pE& b, const zz_pEX& a)
{
   long n = a.rep.length();
   if (n == 0) {
      conv(x, b);
   }
   else if (x.rep.MaxLength() == 0) {
      negate(x, a);
      add(x.rep[0], x.rep[0], b);
      x.normalize();
   }
   else {
      // ugly...b could alias a coeff of x

      zz_pE *xp = x.rep.elts();
      sub(xp[0], b, a.rep[0]);
      x.rep.SetLength(n);
      xp = x.rep.elts();
      const zz_pE *ap = a.rep.elts();
      long i;
      for (i = 1; i < n; i++)
         negate(xp[i], ap[i]);
      x.normalize();
   }
}

void sub(zz_pEX& x, const zz_p& a, const zz_pEX& b)
{
   NTL_zz_pRegister(T);   // avoids aliasing problems
   T = a;
   negate(x, b);
   add(x, x, T);
}

void sub(zz_pEX& x, long a, const zz_pEX& b)
{
   NTL_zz_pRegister(T);
   T = a;
   negate(x, b);
   add(x, x, T);
}

void mul(zz_pEX& c, const zz_pEX& a, const zz_pEX& b)
{
   if (&a == &b) {
      sqr(c, a);
      return;
   }

   if (IsZero(a) || IsZero(b)) {
      clear(c);
      return;
   }

   if (deg(a) == 0) {
      mul(c, b, ConstTerm(a));
      return;
   }

   if (deg(b) == 0) {
      mul(c, a, ConstTerm(b));
      return;
   }

   // general case...Kronecker subst

   zz_pX A, B, C;

   long da = deg(a);
   long db = deg(b);

   long n = zz_pE::degree();
   long n2 = 2*n-1;

   if (NTL_OVERFLOW(da+db+1, n2, 0))
      Error("overflow in zz_pEX mul");

   long i, j;

   A.rep.SetLength((da+1)*n2);

   for (i = 0; i <= da; i++) {
      const zz_pX& coeff = rep(a.rep[i]);
      long dcoeff = deg(coeff);
      for (j = 0; j <= dcoeff; j++)
         A.rep[n2*i + j] = coeff.rep[j];
   }

   A.normalize();

   B.rep.SetLength((db+1)*n2);

   for (i = 0; i <= db; i++) {
      const zz_pX& coeff = rep(b.rep[i]);
      long dcoeff = deg(coeff);
      for (j = 0; j <= dcoeff; j++)
         B.rep[n2*i + j] = coeff.rep[j];
   }

   B.normalize();

   mul(C, A, B);

   long Clen = C.rep.length();
   long lc = (Clen + n2 - 1)/n2;
   long dc = lc - 1;

   c.rep.SetLength(dc+1);

   zz_pX tmp;

   for (i = 0; i <= dc; i++) {
      tmp.rep.SetLength(n2);
      for (j = 0; j < n2 && n2*i + j < Clen; j++)
         tmp.rep[j] = C.rep[n2*i + j];
      for (; j < n2; j++)
         clear(tmp.rep[j]);
      tmp.normalize();
      conv(c.rep[i], tmp);
   }

   c.normalize();
}

void mul(zz_pEX& x, const zz_pEX& a, const zz_pE& b)
{
   if (IsZero(b)) {
      clear(x);
      return;
   }

   zz_pE t;
   t = b;

   long i, da;

   const zz_pE *ap;
   zz_pE* xp;

   da = deg(a);
   x.rep.SetLength(da+1);
   ap = a.rep.elts();
   xp = x.rep.elts();

   for (i = 0; i <= da; i++)
      mul(xp[i], ap[i], t);

   x.normalize();
}

void mul(zz_pEX& x, const zz_pEX& a, const zz_p& b)
{
   if (IsZero(b)) {
      clear(x);
      return;
   }

   NTL_zz_pRegister(t);
   t = b;

   long i, da;

   const zz_pE *ap;
   zz_pE* xp;

   da = deg(a);
   x.rep.SetLength(da+1);
   ap = a.rep.elts();
   xp = x.rep.elts();

   for (i = 0; i <= da; i++)
      mul(xp[i], ap[i], t);

   x.normalize();
}

void mul(zz_pEX& x, const zz_pEX& a, long b)
{
   NTL_zz_pRegister(t);
   t = b;
   mul(x, a, t);
}

void sqr(zz_pEX& c, const zz_pEX& a)
{
   if (IsZero(a)) {
      clear(c);
      return;
   }

   if (deg(a) == 0) {
      zz_pE res;
      sqr(res, ConstTerm(a));
      conv(c, res);
      return;
   }

   // general case...Kronecker subst

   zz_pX A, C;

   long da = deg(a);

   long n = zz_pE::degree();
   long n2 = 2*n-1;

   if (NTL_OVERFLOW(2*da+1, n2, 0))
      Error("overflow in zz_pEX sqr");

   long i, j;

   A.rep.SetLength((da+1)*n2);

   for (i = 0; i <= da; i++) {
      const zz_pX& coeff = rep(a.rep[i]);
      long dcoeff = deg(coeff);
      for (j = 0; j <= dcoeff; j++)
         A.rep[n2*i + j] = coeff.rep[j];
   }

   A.normalize();

   sqr(C, A);

   long Clen = C.rep.length();
   long lc = (Clen + n2 - 1)/n2;
   long dc = lc - 1;

   c.rep.SetLength(dc+1);

   zz_pX tmp;

   for (i = 0; i <= dc; i++) {
      tmp.rep.SetLength(n2);
      for (j = 0; j < n2 && n2*i + j < Clen; j++)
         tmp.rep[j] = C.rep[n2*i + j];
      for (; j < n2; j++)
         clear(tmp.rep[j]);
      tmp.normalize();
      conv(c.rep[i], tmp);
   }

   c.normalize();
}

void MulTrunc(zz_pEX& x, const zz_pEX& a, const zz_pEX& b, long n)
{
   if (n < 0) Error("MulTrunc: bad args");

   zz_pEX t;
   mul(t, a, b);
   trunc(x, t, n);
}

void SqrTrunc(zz_pEX& x, const zz_pEX& a, long n)
{
   if (n < 0) Error("SqrTrunc: bad args");

   zz_pEX t;
   sqr(t, a);
   trunc(x, t, n);
}

void CopyReverse(zz_pEX& x, const zz_pEX& a, long hi)

   // x[0..hi] = reverse(a[0..hi]), with zero fill
   // input may not alias output

{
   long i, j, n, m;

   n = hi+1;
   m = a.rep.length();

   x.rep.SetLength(n);

   const zz_pE* ap = a.rep.elts();
   zz_pE* xp = x.rep.elts();

   for (i = 0; i < n; i++) {
      j = hi-i;
      if (j < 0 || j >= m)
         clear(xp[i]);
      else
         xp[i] = ap[j];
   }

   x.normalize();
}

void trunc(zz_pEX& x, const zz_pEX& a, long m)

// x = a % X^m, output may alias input

{
   if (m < 0) Error("trunc: bad args");

   if (&x == &a) {
      if (x.rep.length() > m) {
         x.rep.SetLength(m);
         x.normalize();
      }
   }
   else {
      long n;
      long i;
      zz_pE* xp;
      const zz_pE* ap;

      n = min(a.rep.length(), m);
      x.rep.SetLength(n);

      xp = x.rep.elts();
      ap = a.rep.elts();

      for (i = 0; i < n; i++) xp[i] = ap[i];

      x.normalize();
   }
}

void random(zz_pEX& x, long n)
{
   long i;

   x.rep.SetLength(n);

   for (i = 0; i < n; i++)
      random(x.rep[i]);

   x.normalize();
}

void negate(zz_pEX& x, const zz_pEX& a)
{
   long n = a.rep.length();
   x.rep.SetLength(n);

   const zz_pE* ap = a.rep.elts();
   zz_pE* xp = x.rep.elts();
   long i;

   for (i = n; i; i--, ap++, xp++)
      negate((*xp), (*ap));
}

static
void MulByXModAux(zz_pEX& h, const zz_pEX& a, const zz_pEX& f)
{
   long i, n, m;
   zz_pE* hh;
   const zz_pE *aa, *ff;

   zz_pE t, z;

   n = deg(f);
   m = deg(a);

   if (m >= n || n == 0) Error("MulByXMod: bad args");

   if (m < 0) {
      clear(h);
      return;
   }

   if (m < n-1) {
      h.rep.SetLength(m+2);
      hh = h.rep.elts();
      aa = a.rep.elts();
      for (i = m+1; i >= 1; i--)
         hh[i] = aa[i-1];
      clear(hh[0]);
   }
   else {
      h.rep.SetLength(n);
      hh = h.rep.elts();
      aa = a.rep.elts();
      ff = f.rep.elts();
      negate(z, aa[n-1]);
      if (!IsOne(ff[n]))
         div(z, z, ff[n]);
      for (i = n-1; i >= 1; i--) {
         mul(t, z, ff[i]);
         add(hh[i], aa[i-1], t);
      }
      mul(hh[0], z, ff[0]);
      h.normalize();
   }
}

void MulByXMod(zz_pEX& h, const zz_pEX& a, const zz_pEX& f)
{
   if (&h == &f) {
      zz_pEX hh;
      MulByXModAux(hh, a, f);
      h = hh;
   }
   else
      MulByXModAux(h, a, f);
}

void PlainMul(zz_pEX& x, const zz_pEX& a, const zz_pEX& b)
{
   long da = deg(a);
   long db = deg(b);

   if (da < 0 || db < 0) {
      clear(x);
      return;
   }

   long d = da+db;

   const zz_pE *ap, *bp;
   zz_pE *xp;

   zz_pEX la, lb;

   if (&x == &a) {
      la = a;
      ap = la.rep.elts();
   }
   else
      ap = a.rep.elts();

   if (&x == &b) {
      lb = b;
      bp = lb.rep.elts();
   }
   else
      bp = b.rep.elts();

   x.rep.SetLength(d+1);

   xp = x.rep.elts();

   long i, j, jmin, jmax;
   static zz_pX t, accum;

   for (i = 0; i <= d; i++) {
      jmin = max(0, i-db);
      jmax = min(da, i);
      clear(accum);
      for (j = jmin; j <= jmax; j++) {
	 mul(t, rep(ap[j]), rep(bp[i-j]));
	 add(accum, accum, t);
      }
      conv(xp[i], accum);
   }
   x.normalize();
}

void SetSize(vec_zz_pX& x, long n, long m)
{
   x.SetLength(n);
   long i;
   for (i = 0; i < n; i++)
      x[i].rep.SetMaxLength(m);
}

void PlainDivRem(zz_pEX& q, zz_pEX& r, const zz_pEX& a, const zz_pEX& b)
{
   long da, db, dq, i, j, LCIsOne;
   const zz_pE *bp;
   zz_pE *qp;
   zz_pX *xp;

   zz_pE LCInv, t;
   zz_pX s;

   da = deg(a);
   db = deg(b);

   if (db < 0) Error("zz_pEX: division by zero");

   if (da < db) {
      r = a;
      clear(q);
      return;
   }

   zz_pEX lb;

   if (&q == &b) {
      lb = b;
      bp = lb.rep.elts();
   }
   else
      bp = b.rep.elts();

   if (IsOne(bp[db]))
      LCIsOne = 1;
   else {
      LCIsOne = 0;
      inv(LCInv, bp[db]);
   }

   vec_zz_pX x;

   SetSize(x, da+1, 2*zz_pE::degree());

   for (i = 0; i <= da; i++)
      x[i] = rep(a.rep[i]);

   xp = x.elts();

   dq = da - db;
   q.rep.SetLength(dq+1);
   qp = q.rep.elts();

   for (i = dq; i >= 0; i--) {
      conv(t, xp[i+db]);
      if (!LCIsOne)
	 mul(t, t, LCInv);
      qp[i] = t;
      negate(t, t);

      for (j = db-1; j >= 0; j--) {
	 mul(s, rep(t), rep(bp[j]));
	 add(xp[i+j], xp[i+j], s);
      }
   }

   r.rep.SetLength(db);
   for (i = 0; i < db; i++)
      conv(r.rep[i], xp[i]);
   r.normalize();
}

void PlainRem(zz_pEX& r, const zz_pEX& a, const zz_pEX& b, vec_zz_pX& x)
{
   long da, db, dq, i, j, LCIsOne;
   const zz_pE *bp;
   zz_pX *xp;

   zz_pE LCInv, t;
   zz_pX s;

   da = deg(a);
   db = deg(b);

   if (db < 0) Error("zz_pEX: division by zero");

   if (da < db) {
      r = a;
      return;
   }

   bp = b.rep.elts();

   if (IsOne(bp[db]))
      LCIsOne = 1;
   else {
      LCIsOne = 0;
      inv(LCInv, bp[db]);
   }

   for (i = 0; i <= da; i++)
      x[i] = rep(a.rep[i]);

   xp = x.elts();

   dq = da - db;

   for (i = dq; i >= 0; i--) {
      conv(t, xp[i+db]);
      if (!LCIsOne)
	 mul(t, t, LCInv);
      negate(t, t);

      for (j = db-1; j >= 0; j--) {
	 mul(s, rep(t), rep(bp[j]));
	 add(xp[i+j], xp[i+j], s);
      }
   }

   r.rep.SetLength(db);
   for (i = 0; i < db; i++)
      conv(r.rep[i], xp[i]);
   r.normalize();
}

void PlainDivRem(zz_pEX& q, zz_pEX& r, const zz_pEX& a, const zz_pEX& b,
     vec_zz_pX& x)
{
   long da, db, dq, i, j, LCIsOne;
   const zz_pE *bp;
   zz_pE *qp;
   zz_pX *xp;

   zz_pE LCInv, t;
   zz_pX s;

   da = deg(a);
   db = deg(b);

   if (db < 0) Error("zz_pEX: division by zero");

   if (da < db) {
      r = a;
      clear(q);
      return;
   }

   zz_pEX lb;

   if (&q == &b) {
      lb = b;
      bp = lb.rep.elts();
   }
   else
      bp = b.rep.elts();

   if (IsOne(bp[db]))
      LCIsOne = 1;
   else {
      LCIsOne = 0;
      inv(LCInv, bp[db]);
   }

   for (i = 0; i <= da; i++)
      x[i] = rep(a.rep[i]);

   xp = x.elts();

   dq = da - db;
   q.rep.SetLength(dq+1);
   qp = q.rep.elts();

   for (i = dq; i >= 0; i--) {
      conv(t, xp[i+db]);
      if (!LCIsOne)
	 mul(t, t, LCInv);
      qp[i] = t;
      negate(t, t);

      for (j = db-1; j >= 0; j--) {
	 mul(s, rep(t), rep(bp[j]));
	 add(xp[i+j], xp[i+j], s);
      }
   }

   r.rep.SetLength(db);
   for (i = 0; i < db; i++)
      conv(r.rep[i], xp[i]);
   r.normalize();
}

void PlainDiv(zz_pEX& q, const zz_pEX& a, const zz_pEX& b)
{
   long da, db, dq, i, j, LCIsOne;
   const zz_pE *bp;
   zz_pE *qp;
   zz_pX *xp;

   zz_pE LCInv, t;
   zz_pX s;

   da = deg(a);
   db = deg(b);

   if (db < 0) Error("zz_pEX: division by zero");

   if (da < db) {
      clear(q);
      return;
   }

   zz_pEX lb;

   if (&q == &b) {
      lb = b;
      bp = lb.rep.elts();
   }
   else
      bp = b.rep.elts();

   if (IsOne(bp[db]))
      LCIsOne = 1;
   else {
      LCIsOne = 0;
      inv(LCInv, bp[db]);
   }

   vec_zz_pX x;
   SetSize(x, da+1-db, 2*zz_pE::degree());

   for (i = db; i <= da; i++)
      x[i-db] = rep(a.rep[i]);

   xp = x.elts();

   dq = da - db;
   q.rep.SetLength(dq+1);
   qp = q.rep.elts();

   for (i = dq; i >= 0; i--) {
      conv(t, xp[i]);
      if (!LCIsOne)
	 mul(t, t, LCInv);
      qp[i] = t;
      negate(t, t);

      long lastj = max(0, db-i);

      for (j = db-1; j >= lastj; j--) {
	 mul(s, rep(t), rep(bp[j]));
	 add(xp[i+j-db], xp[i+j-db], s);
      }
   }
}

void PlainRem(zz_pEX& r, const zz_pEX& a, const zz_pEX& b)
{
   long da, db, dq, i, j, LCIsOne;
   const zz_pE *bp;
   zz_pX *xp;

   zz_pE LCInv, t;
   zz_pX s;

   da = deg(a);
   db = deg(b);

   if (db < 0) Error("zz_pEX: division by zero");

   if (da < db) {
      r = a;
      return;
   }

   bp = b.rep.elts();

   if (IsOne(bp[db]))
      LCIsOne = 1;
   else {
      LCIsOne = 0;
      inv(LCInv, bp[db]);
   }

   vec_zz_pX x;
   SetSize(x, da + 1, 2*zz_pE::degree());

   for (i = 0; i <= da; i++)
      x[i] = rep(a.rep[i]);

   xp = x.elts();

   dq = da - db;

   for (i = dq; i >= 0; i--) {
      conv(t, xp[i+db]);
      if (!LCIsOne)
	 mul(t, t, LCInv);
      negate(t, t);

      for (j = db-1; j >= 0; j--) {
	 mul(s, rep(t), rep(bp[j]));
	 add(xp[i+j], xp[i+j], s);
      }
   }

   r.rep.SetLength(db);
   for (i = 0; i < db; i++)
      conv(r.rep[i], xp[i]);
   r.normalize();
}

void RightShift(zz_pEX& x, const zz_pEX& a, long n)
{
   if (IsZero(a)) {
      clear(x);
      return;
   }

   if (n < 0) {
      if (n < -NTL_MAX_LONG) Error("overflow in RightShift");
      LeftShift(x, a, -n);
      return;
   }

   long da = deg(a);
   long i;

   if (da < n) {
      clear(x);
      return;
   }

   if (&x != &a)
      x.rep.SetLength(da-n+1);

   for (i = 0; i <= da-n; i++)
      x.rep[i] = a.rep[i+n];

   if (&x == &a)
      x.rep.SetLength(da-n+1);

   x.normalize();
}

void LeftShift(zz_pEX& x, const zz_pEX& a, long n)
{
   if (IsZero(a)) {
      clear(x);
      return;
   }

   if (n < 0) {
      if (n < -NTL_MAX_LONG)
         clear(x);
      else
         RightShift(x, a, -n);
      return;
   }

   if (NTL_OVERFLOW(n, 1, 0))
      Error("overflow in LeftShift");

   long m = a.rep.length();

   x.rep.SetLength(m+n);

   long i;
   for (i = m-1; i >= 0; i--)
      x.rep[i+n] = a.rep[i];

   for (i = 0; i < n; i++)
      clear(x.rep[i]);
}

void NewtonInv(zz_pEX& c, const zz_pEX& a, long e)
{
   zz_pE x;

   inv(x, ConstTerm(a));

   if (e == 1) {
      conv(c, x);
      return;
   }

   static vec_long E;
   E.SetLength(0);
   append(E, e);
   while (e > 1) {
      e = (e+1)/2;
      append(E, e);
   }

   long L = E.length();

   zz_pEX g, g0, g1, g2;

   g.rep.SetMaxLength(E[0]);
   g0.rep.SetMaxLength(E[0]);
   g1.rep.SetMaxLength((3*E[0]+1)/2);
   g2.rep.SetMaxLength(E[0]);

   conv(g, x);

   long i;

   for (i = L-1; i > 0; i--) {
      // lift from E[i] to E[i-1]

      long k = E[i];
      long l = E[i-1]-E[i];

      trunc(g0, a, k+l);

      mul(g1, g0, g);
      RightShift(g1, g1, k);
      trunc(g1, g1, l);

      mul(g2, g1, g);
      trunc(g2, g2, l);
      LeftShift(g2, g2, k);

      sub(g, g, g2);
   }

   c = g;
}

void InvTrunc(zz_pEX& c, const zz_pEX& a, long e)
{
   if (e < 0) Error("InvTrunc: bad args");
   if (e == 0) {
      clear(c);
      return;
   }

   if (NTL_OVERFLOW(e, 1, 0))
      Error("overflow in InvTrunc");

   NewtonInv(c, a, e);
}

const long zz_pEX_MOD_PLAIN = 0;
const long zz_pEX_MOD_MUL = 1;

void build(zz_pEXModulus& F, const zz_pEX& f)
{
   long n = deg(f);

   if (n <= 0) Error("build(zz_pEXModulus,zz_pEX): deg(f) <= 0");

   if (NTL_OVERFLOW(n, zz_pE::degree(), 0))
      Error("build(zz_pEXModulus,zz_pEX): overflow");

   F.tracevec.SetLength(0);

   F.f = f;
   F.n = n;

   if (F.n < zz_pE::ModCross()) {
      F.method = zz_pEX_MOD_PLAIN;
   }
   else {
      F.method = zz_pEX_MOD_MUL;
      zz_pEX P1;
      zz_pEX P2;

      CopyReverse(P1, f, n);
      InvTrunc(P2, P1, n-1);
      CopyReverse(P1, P2, n-2);
      trunc(F.h0, P1, n-2);
      trunc(F.f0, f, n);
      F.hlc = ConstTerm(P2);
   }
}

zz_pEXModulus::zz_pEXModulus()
{
   n = -1;
   method = zz_pEX_MOD_PLAIN;
}

zz_pEXModulus::~zz_pEXModulus()
{
}

zz_pEXModulus::zz_pEXModulus(const zz_pEX& ff)
{
   n = -1;
   method = zz_pEX_MOD_PLAIN;

   build(*this, ff);
}

void UseMulRem21(zz_pEX& r, const zz_pEX& a, const zz_pEXModulus& F)
{
   zz_pEX P1;
   zz_pEX P2;

   RightShift(P1, a, F.n);
   mul(P2, P1, F.h0);
   RightShift(P2, P2, F.n-2);
   if (!IsOne(F.hlc)) mul(P1, P1, F.hlc);
   add(P2, P2, P1);
   mul(P1, P2, F.f0);
   trunc(P1, P1, F.n);
   trunc(r, a, F.n);
   sub(r, r, P1);
}

void UseMulDivRem21(zz_pEX& q, zz_pEX& r, const zz_pEX& a, const zz_pEXModulus& F)
{
   zz_pEX P1;
   zz_pEX P2;

   RightShift(P1, a, F.n);
   mul(P2, P1, F.h0);
   RightShift(P2, P2, F.n-2);
   if (!IsOne(F.hlc)) mul(P1, P1, F.hlc);
   add(P2, P2, P1);
   mul(P1, P2, F.f0);
   trunc(P1, P1, F.n);
   trunc(r, a, F.n);
   sub(r, r, P1);
   q = P2;
}

void UseMulDiv21(zz_pEX& q, const zz_pEX& a, const zz_pEXModulus& F)
{
   zz_pEX P1;
   zz_pEX P2;

   RightShift(P1, a, F.n);
   mul(P2, P1, F.h0);
   RightShift(P2, P2, F.n-2);
   if (!IsOne(F.hlc)) mul(P1, P1, F.hlc);
   add(P2, P2, P1);
   q = P2;

}

void rem(zz_pEX& x, const zz_pEX& a, const zz_pEXModulus& F)
{
   if (F.method == zz_pEX_MOD_PLAIN) {
      PlainRem(x, a, F.f);
      return;
   }

   long da = deg(a);
   long n = F.n;

   if (da <= 2*n-2) {
      UseMulRem21(x, a, F);
      return;
   }

   zz_pEX buf(INIT_SIZE, 2*n-1);

   long a_len = da+1;

   while (a_len > 0) {
      long old_buf_len = buf.rep.length();
      long amt = min(2*n-1-old_buf_len, a_len);

      buf.rep.SetLength(old_buf_len+amt);

      long i;

      for (i = old_buf_len+amt-1; i >= amt; i--)
         buf.rep[i] = buf.rep[i-amt];

      for (i = amt-1; i >= 0; i--)
         buf.rep[i] = a.rep[a_len-amt+i];

      buf.normalize();

      UseMulRem21(buf, buf, F);

      a_len -= amt;
   }

   x = buf;
}

void DivRem(zz_pEX& q, zz_pEX& r, const zz_pEX& a, const zz_pEXModulus& F)
{
   if (F.method == zz_pEX_MOD_PLAIN) {
      PlainDivRem(q, r, a, F.f);
      return;
   }

   long da = deg(a);
   long n = F.n;

   if (da <= 2*n-2) {
      UseMulDivRem21(q, r, a, F);
      return;
   }

   zz_pEX buf(INIT_SIZE, 2*n-1);
   zz_pEX qbuf(INIT_SIZE, n-1);

   zz_pEX qq;
   qq.rep.SetLength(da-n+1);

   long a_len = da+1;
   long q_hi = da-n+1;

   while (a_len > 0) {
      long old_buf_len = buf.rep.length();
      long amt = min(2*n-1-old_buf_len, a_len);

      buf.rep.SetLength(old_buf_len+amt);

      long i;

      for (i = old_buf_len+amt-1; i >= amt; i--)
         buf.rep[i] = buf.rep[i-amt];

      for (i = amt-1; i >= 0; i--)
         buf.rep[i] = a.rep[a_len-amt+i];

      buf.normalize();

      UseMulDivRem21(qbuf, buf, buf, F);
      long dl = qbuf.rep.length();
      a_len = a_len - amt;
      for(i = 0; i < dl; i++)
         qq.rep[a_len+i] = qbuf.rep[i];
      for(i = dl+a_len; i < q_hi; i++)
         clear(qq.rep[i]);
      q_hi = a_len;
   }

   r = buf;

   qq.normalize();
   q = qq;
}

void div(zz_pEX& q, const zz_pEX& a, const zz_pEXModulus& F)
{
   if (F.method == zz_pEX_MOD_PLAIN) {
      PlainDiv(q, a, F.f);
      return;
   }

   long da = deg(a);
   long n = F.n;

   if (da <= 2*n-2) {
      UseMulDiv21(q, a, F);
      return;
   }

   zz_pEX buf(INIT_SIZE, 2*n-1);
   zz_pEX qbuf(INIT_SIZE, n-1);

   zz_pEX qq;
   qq.rep.SetLength(da-n+1);

   long a_len = da+1;
   long q_hi = da-n+1;

   while (a_len > 0) {
      long old_buf_len = buf.rep.length();
      long amt = min(2*n-1-old_buf_len, a_len);

      buf.rep.SetLength(old_buf_len+amt);

      long i;

      for (i = old_buf_len+amt-1; i >= amt; i--)
         buf.rep[i] = buf.rep[i-amt];

      for (i = amt-1; i >= 0; i--)
         buf.rep[i] = a.rep[a_len-amt+i];

      buf.normalize();

      a_len = a_len - amt;
      if (a_len > 0)
         UseMulDivRem21(qbuf, buf, buf, F);
      else
         UseMulDiv21(qbuf, buf, F);

      long dl = qbuf.rep.length();
      for(i = 0; i < dl; i++)
         qq.rep[a_len+i] = qbuf.rep[i];
      for(i = dl+a_len; i < q_hi; i++)
         clear(qq.rep[i]);
      q_hi = a_len;
   }

   qq.normalize();
   q = qq;
}

void MulMod(zz_pEX& c, const zz_pEX& a, const zz_pEX& b, const zz_pEXModulus& F)
{
   if (deg(a) >= F.n || deg(b) >= F.n) Error("MulMod: bad args");

   zz_pEX t;
   mul(t, a, b);
   rem(c, t, F);
}

void SqrMod(zz_pEX& c, const zz_pEX& a, const zz_pEXModulus& F)
{
   if (deg(a) >= F.n) Error("MulMod: bad args");

   zz_pEX t;
   sqr(t, a);
   rem(c, t, F);
}

void UseMulRem(zz_pEX& r, const zz_pEX& a, const zz_pEX& b)
{
   zz_pEX P1;
   zz_pEX P2;

   long da = deg(a);
   long db = deg(b);

   CopyReverse(P1, b, db);
   InvTrunc(P2, P1, da-db+1);
   CopyReverse(P1, P2, da-db);

   RightShift(P2, a, db);
   mul(P2, P1, P2);
   RightShift(P2, P2, da-db);
   mul(P1, P2, b);
   sub(P1, a, P1);

   r = P1;
}

void UseMulDivRem(zz_pEX& q, zz_pEX& r, const zz_pEX& a, const zz_pEX& b)
{
   zz_pEX P1;
   zz_pEX P2;

   long da = deg(a);
   long db = deg(b);

   CopyReverse(P1, b, db);
   InvTrunc(P2, P1, da-db+1);
   CopyReverse(P1, P2, da-db);

   RightShift(P2, a, db);
   mul(P2, P1, P2);
   RightShift(P2, P2, da-db);
   mul(P1, P2, b);
   sub(P1, a, P1);

   r = P1;
   q = P2;
}

void UseMulDiv(zz_pEX& q, const zz_pEX& a, const zz_pEX& b)
{
   zz_pEX P1;
   zz_pEX P2;

   long da = deg(a);
   long db = deg(b);

   CopyReverse(P1, b, db);
   InvTrunc(P2, P1, da-db+1);
   CopyReverse(P1, P2, da-db);

   RightShift(P2, a, db);
   mul(P2, P1, P2);
   RightShift(P2, P2, da-db);

   q = P2;
}

void DivRem(zz_pEX& q, zz_pEX& r, const zz_pEX& a, const zz_pEX& b)
{
   long sa = a.rep.length();
   long sb = b.rep.length();

   if (sb < zz_pE::DivCross() || sa-sb < zz_pE::DivCross())
      PlainDivRem(q, r, a, b);
   else if (sa < 4*sb)
      UseMulDivRem(q, r, a, b);
   else {
      zz_pEXModulus B;
      build(B, b);
      DivRem(q, r, a, B);
   }
}

void div(zz_pEX& q, const zz_pEX& a, const zz_pEX& b)
{
   long sa = a.rep.length();
   long sb = b.rep.length();

   if (sb < zz_pE::DivCross() || sa-sb < zz_pE::DivCross())
      PlainDiv(q, a, b);
   else if (sa < 4*sb)
      UseMulDiv(q, a, b);
   else {
      zz_pEXModulus B;
      build(B, b);
      div(q, a, B);
   }
}

void div(zz_pEX& q, const zz_pEX& a, const zz_pE& b)
{
   zz_pE T;
   inv(T, b);
   mul(q, a, T);
}

void div(zz_pEX& q, const zz_pEX& a, const zz_p& b)
{
   NTL_zz_pRegister(T);
   inv(T, b);
   mul(q, a, T);
}

void div(zz_pEX& q, const zz_pEX& a, long b)
{
   NTL_zz_pRegister(T);
   T = b;
   inv(T, T);
   mul(q, a, T);
}

void rem(zz_pEX& r, const zz_pEX& a, const zz_pEX& b)
{
   long sa = a.rep.length();
   long sb = b.rep.length();

   if (sb < zz_pE::DivCross() || sa-sb < zz_pE::DivCross())
      PlainRem(r, a, b);
   else if (sa < 4*sb)
      UseMulRem(r, a, b);
   else {
      zz_pEXModulus B;
      build(B, b);
      rem(r, a, B);
   }
}

void GCD(zz_pEX& x, const zz_pEX& a, const zz_pEX& b)
{
   zz_pE t;

   if (IsZero(b))
      x = a;
   else if (IsZero(a))
      x = b;
   else {
      long n = max(deg(a),deg(b)) + 1;
      zz_pEX u(INIT_SIZE, n), v(INIT_SIZE, n);

      vec_zz_pX tmp;
      SetSize(tmp, n, 2*zz_pE::degree());

      u = a;
      v = b;
      do {
         PlainRem(u, u, v, tmp);
         swap(u, v);
      } while (!IsZero(v));

      x = u;
   }

   if (IsZero(x)) return;
   if (IsOne(LeadCoeff(x))) return;

   /* make gcd monic */

   inv(t, LeadCoeff(x));
   mul(x, x, t);
}

void XGCD(zz_pEX& d, zz_pEX& s, zz_pEX& t, const zz_pEX& a, const zz_pEX& b)
{
   zz_pE z;

   if (IsZero(b)) {
      set(s);
      clear(t);
      d = a;
   }
   else if (IsZero(a)) {
      clear(s);
      set(t);
      d = b;
   }
   else {
      long e = max(deg(a), deg(b)) + 1;

      zz_pEX temp(INIT_SIZE, e), u(INIT_SIZE, e), v(INIT_SIZE, e),
            u0(INIT_SIZE, e), v0(INIT_SIZE, e),
            u1(INIT_SIZE, e), v1(INIT_SIZE, e),
            u2(INIT_SIZE, e), v2(INIT_SIZE, e), q(INIT_SIZE, e);

      set(u1); clear(v1);
      clear(u2); set(v2);
      u = a; v = b;

      do {
         DivRem(q, u, u, v);
         swap(u, v);
         u0 = u2;
         v0 = v2;
         mul(temp, q, u2);
         sub(u2, u1, temp);
         mul(temp, q, v2);
         sub(v2, v1, temp);
         u1 = u0;
         v1 = v0;
      } while (!IsZero(v));

      d = u;
      s = u1;
      t = v1;
   }

   if (IsZero(d)) return;
   if (IsOne(LeadCoeff(d))) return;

   /* make gcd monic */

   inv(z, LeadCoeff(d));
   mul(d, d, z);
   mul(s, s, z);
   mul(t, t, z);
}

NTL_vector_impl(zz_pEX,vec_zz_pEX)

NTL_eq_vector_impl(zz_pEX,vec_zz_pEX)

NTL_io_vector_impl(zz_pEX,vec_zz_pEX)

void IterBuild(zz_pE* a, long n)
{
   long i, k;
   zz_pE b, t;

   if (n <= 0) return;

   negate(a[0], a[0]);

   for (k = 1; k <= n-1; k++) {
      negate(b, a[k]);
      add(a[k], b, a[k-1]);
      for (i = k-1; i >= 1; i--) {
         mul(t, a[i], b);
         add(a[i], t, a[i-1]);
      }
      mul(a[0], a[0], b);
   }
}

void BuildFromRoots(zz_pEX& x, const vec_zz_pE& a)
{
   long n = a.length();

   if (n == 0) {
      set(x);
      return;
   }

   x.rep.SetMaxLength(n+1);
   x.rep = a;
   IterBuild(&x.rep[0], n);
   x.rep.SetLength(n+1);
   SetCoeff(x, n);
}

void eval(zz_pE& b, const zz_pEX& f, const zz_pE& a)
// does a Horner evaluation
{
   zz_pE acc;
   long i;

   clear(acc);
   for (i = deg(f); i >= 0; i--) {
      mul(acc, acc, a);
      add(acc, acc, f.rep[i]);
   }

   b = acc;
}

void eval(vec_zz_pE& b, const zz_pEX& f, const vec_zz_pE& a)
// naive algorithm:  repeats Horner
{
   if (&b == &f.rep) {
      vec_zz_pE bb;
      eval(bb, f, a);
      b = bb;
      return;
   }

   long m = a.length();
   b.SetLength(m);
   long i;
   for (i = 0; i < m; i++)
      eval(b[i], f, a[i]);
}

void interpolate(zz_pEX& f, const vec_zz_pE& a, const vec_zz_pE& b)
{
   long m = a.length();
   if (b.length() != m) Error("interpolate: vector length mismatch");

   if (m == 0) {
      clear(f);
      return;
   }

   vec_zz_pE prod;
   prod = a;

   zz_pE t1, t2;

   long k, i;

   vec_zz_pE res;
   res.SetLength(m);

   for (k = 0; k < m; k++) {

      const zz_pE& aa = a[k];

      set(t1);
      for (i = k-1; i >= 0; i--) {
         mul(t1, t1, aa);
         add(t1, t1, prod[i]);
      }

      clear(t2);
      for (i = k-1; i >= 0; i--) {
         mul(t2, t2, aa);
         add(t2, t2, res[i]);
      }

      inv(t1, t1);
      sub(t2, b[k], t2);
      mul(t1, t1, t2);

      for (i = 0; i < k; i++) {
         mul(t2, prod[i], t1);
         add(res[i], res[i], t2);
      }

      res[k] = t1;

      if (k < m-1) {
         if (k == 0)
            negate(prod[0], prod[0]);
         else {
            negate(t1, a[k]);
            add(prod[k], t1, prod[k-1]);
            for (i = k-1; i >= 1; i--) {
               mul(t2, prod[i], t1);
               add(prod[i], t2, prod[i-1]);
            }
            mul(prod[0], prod[0], t1);
         }
      }
   }

   while (m > 0 && IsZero(res[m-1])) m--;
   res.SetLength(m);
   f.rep = res;
}

void InnerProduct(zz_pEX& x, const vec_zz_pE& v, long low, long high,
                   const vec_zz_pEX& H, long n, vec_zz_pX& t)
{
   zz_pX s;
   long i, j;

   for (j = 0; j < n; j++)
      clear(t[j]);

   high = min(high, v.length()-1);
   for (i = low; i <= high; i++) {
      const vec_zz_pE& h = H[i-low].rep;
      long m = h.length();
      const zz_pX& w = rep(v[i]);

      for (j = 0; j < m; j++) {
         mul(s, w, rep(h[j]));
         add(t[j], t[j], s);
      }
   }

   x.rep.SetLength(n);
   for (j = 0; j < n; j++)
      conv(x.rep[j], t[j]);
   x.normalize();
}

void CompMod(zz_pEX& x, const zz_pEX& g, const zz_pEXArgument& A,
             const zz_pEXModulus& F)
{
   if (deg(g) <= 0) {
      x = g;
      return;
   }

   zz_pEX s, t;
   vec_zz_pX scratch;
   SetSize(scratch, deg(F), 2*zz_pE::degree());

   long m = A.H.length() - 1;
   long l = ((g.rep.length()+m-1)/m) - 1;

   const zz_pEX& M = A.H[m];

   InnerProduct(t, g.rep, l*m, l*m + m - 1, A.H, F.n, scratch);
   for (long i = l-1; i >= 0; i--) {
      InnerProduct(s, g.rep, i*m, i*m + m - 1, A.H, F.n, scratch);
      MulMod(t, t, M, F);
      add(t, t, s);
   }

   x = t;
}

void build(zz_pEXArgument& A, const zz_pEX& h, const zz_pEXModulus& F, long m)
{
   long i;

   if (m <= 0 || deg(h) >= F.n)
      Error("build: bad args");

   if (m > F.n) m = F.n;

   if (zz_pEXArgBound > 0) {
      double sz = zz_p::storage();
      sz = sz*zz_pE::degree();
      sz = sz + NTL_VECTOR_HEADER_SIZE + sizeof(vec_zz_p);
      sz = sz*F.n;
      sz = sz + NTL_VECTOR_HEADER_SIZE + sizeof(vec_zz_pE);
      sz = sz/1024;
      m = min(m, long(zz_pEXArgBound/sz));
      m = max(m, 1);
   }

   A.H.SetLength(m+1);

   set(A.H[0]);
   A.H[1] = h;
   for (i = 2; i <= m; i++)
      MulMod(A.H[i], A.H[i-1], h, F);
}

long zz_pEXArgBound = 0;

void CompMod(zz_pEX& x, const zz_pEX& g, const zz_pEX& h, const zz_pEXModulus& F)
   // x = g(h) mod f
{
   long m = SqrRoot(g.rep.length());

   if (m == 0) {
      clear(x);
      return;
   }

   zz_pEXArgument A;

   build(A, h, F, m);

   CompMod(x, g, A, F);
}

void Comp2Mod(zz_pEX& x1, zz_pEX& x2, const zz_pEX& g1, const zz_pEX& g2,
              const zz_pEX& h, const zz_pEXModulus& F)

{
   long m = SqrRoot(g1.rep.length() + g2.rep.length());

   if (m == 0) {
      clear(x1);
      clear(x2);
      return;
   }

   zz_pEXArgument A;

   build(A, h, F, m);

   zz_pEX xx1, xx2;

   CompMod(xx1, g1, A, F);
   CompMod(xx2, g2, A, F);

   x1 = xx1;
   x2 = xx2;
}

void Comp3Mod(zz_pEX& x1, zz_pEX& x2, zz_pEX& x3,
              const zz_pEX& g1, const zz_pEX& g2, const zz_pEX& g3,
              const zz_pEX& h, const zz_pEXModulus& F)

{
   long m = SqrRoot(g1.rep.length() + g2.rep.length() + g3.rep.length());

   if (m == 0) {
      clear(x1);
      clear(x2);
      clear(x3);
      return;
   }

   zz_pEXArgument A;

   build(A, h, F, m);

   zz_pEX xx1, xx2, xx3;

   CompMod(xx1, g1, A, F);
   CompMod(xx2, g2, A, F);
   CompMod(xx3, g3, A, F);

   x1 = xx1;
   x2 = xx2;
   x3 = xx3;
}

void build(zz_pEXTransMultiplier& B, const zz_pEX& b, const zz_pEXModulus& F)
{
   long db = deg(b);

   if (db >= F.n) Error("build TransMultiplier: bad args");

   zz_pEX t;

   LeftShift(t, b, F.n-1);
   div(t, t, F);

   // we optimize for low degree b

   long d;

   d = deg(t);
   if (d < 0)
      B.shamt_fbi = 0;
   else
      B.shamt_fbi = F.n-2 - d;

   CopyReverse(B.fbi, t, d);

   // The following code optimizes the case when
   // f = X^n + low degree poly

   trunc(t, F.f, F.n);
   d = deg(t);
   if (d < 0)
      B.shamt = 0;
   else
      B.shamt = d;

   CopyReverse(B.f0, t, d);

   if (db < 0)
      B.shamt_b = 0;
   else
      B.shamt_b = db;

   CopyReverse(B.b, b, db);
}

void TransMulMod(zz_pEX& x, const zz_pEX& a, const zz_pEXTransMultiplier& B,
               const zz_pEXModulus& F)
{
   if (deg(a) >= F.n) Error("TransMulMod: bad args");

   zz_pEX t1, t2;

   mul(t1, a, B.b);
   RightShift(t1, t1, B.shamt_b);

   mul(t2, a, B.f0);
   RightShift(t2, t2, B.shamt);
   trunc(t2, t2, F.n-1);

   mul(t2, t2, B.fbi);
   if (B.shamt_fbi > 0) LeftShift(t2, t2, B.shamt_fbi);
   trunc(t2, t2, F.n-1);
   LeftShift(t2, t2, 1);

   sub(x, t1, t2);
}

void ShiftSub(zz_pEX& U, const zz_pEX& V, long n)
// assumes input does not alias output
{
   if (IsZero(V))
      return;

   long du = deg(U);
   long dv = deg(V);

   long d = max(du, n+dv);

   U.rep.SetLength(d+1);
   long i;

   for (i = du+1; i <= d; i++)
      clear(U.rep[i]);

   for (i = 0; i <= dv; i++)
      sub(U.rep[i+n], U.rep[i+n], V.rep[i]);

   U.normalize();
}

void UpdateMap(vec_zz_pE& x, const vec_zz_pE& a,
         const zz_pEXTransMultiplier& B, const zz_pEXModulus& F)
{
   zz_pEX xx;
   TransMulMod(xx, to_zz_pEX(a), B, F);
   x = xx.rep;
}

static
void ProjectPowers(vec_zz_pE& x, const zz_pEX& a, long k,
                   const zz_pEXArgument& H, const zz_pEXModulus& F)
{
   if (k < 0 || NTL_OVERFLOW(k, 1, 0) || deg(a) >= F.n)
      Error("ProjectPowers: bad args");

   long m = H.H.length()-1;
   long l = (k+m-1)/m - 1;

   zz_pEXTransMultiplier M;
   build(M, H.H[m], F);

   zz_pEX s;
   s = a;

   x.SetLength(k);

   long i;

   for (i = 0; i <= l; i++) {
      long m1 = min(m, k-i*m);
      for (long j = 0; j < m1; j++)
         InnerProduct(x[i*m+j], H.H[j].rep, s.rep);
      if (i < l)
         TransMulMod(s, s, M, F);
   }
}

static
void ProjectPowers(vec_zz_pE& x, const zz_pEX& a, long k, const zz_pEX& h,
                   const zz_pEXModulus& F)
{
   if (k < 0 || deg(a) >= F.n || deg(h) >= F.n)
      Error("ProjectPowers: bad args");

   if (k == 0) {
      x.SetLength(0);;
      return;
   }

   long m = SqrRoot(k);

   zz_pEXArgument H;
   build(H, h, F, m);

   ProjectPowers(x, a, k, H, F);
}

void ProjectPowers(vec_zz_pE& x, const vec_zz_pE& a, long k,
                   const zz_pEXArgument& H, const zz_pEXModulus& F)
{
   ProjectPowers(x, to_zz_pEX(a), k, H, F);
}

void ProjectPowers(vec_zz_pE& x, const vec_zz_pE& a, long k,
                   const zz_pEX& h, const zz_pEXModulus& F)
{
   ProjectPowers(x, to_zz_pEX(a), k, h, F);
}

void BerlekampMassey(zz_pEX& h, const vec_zz_pE& a, long m)
{
   zz_pEX Lambda, Sigma, Temp;
   long L;
   zz_pE Delta, Delta1, t1;
   long shamt;

   // cerr << "*** " << m << "\n";

   Lambda.SetMaxLength(m+1);
   Sigma.SetMaxLength(m+1);
   Temp.SetMaxLength(m+1);

   L = 0;
   set(Lambda);
   clear(Sigma);
   set(Delta);
   shamt = 0;

   long i, r, dl;

   for (r = 1; r <= 2*m; r++) {
      // cerr << r << "--";
      clear(Delta1);
      dl = deg(Lambda);
      for (i = 0; i <= dl; i++) {
         mul(t1, Lambda.rep[i], a[r-i-1]);
         add(Delta1, Delta1, t1);
      }

      if (IsZero(Delta1)) {
         shamt++;
         // cerr << "case 1: " << deg(Lambda) << " " << deg(Sigma) << " " << shamt << "\n";
      }
      else if (2*L < r) {
         div(t1, Delta1, Delta);
         mul(Temp, Sigma, t1);
         Sigma = Lambda;
         ShiftSub(Lambda, Temp, shamt+1);
         shamt = 0;
         L = r-L;
         Delta = Delta1;
         // cerr << "case 2: " << deg(Lambda) << " " << deg(Sigma) << " " << shamt << "\n";
      }
      else {
         shamt++;
         div(t1, Delta1, Delta);
         mul(Temp, Sigma, t1);
         ShiftSub(Lambda, Temp, shamt);
         // cerr << "case 3: " << deg(Lambda) << " " << deg(Sigma) << " " << shamt << "\n";
      }
   }

   // cerr << "finished: " << L << " " << deg(Lambda) << "\n";

   dl = deg(Lambda);
   h.rep.SetLength(L + 1);

   for (i = 0; i < L - dl; i++)
      clear(h.rep[i]);

   for (i = L - dl; i <= L; i++)
      h.rep[i] = Lambda.rep[L - i];
}

void MinPolySeq(zz_pEX& h, const vec_zz_pE& a, long m)
{
   if (m < 0 || NTL_OVERFLOW(m, 1, 0)) Error("MinPoly: bad args");
   if (a.length() < 2*m) Error("MinPoly: sequence too short");

   BerlekampMassey(h, a, m);
}

void DoMinPolyMod(zz_pEX& h, const zz_pEX& g, const zz_pEXModulus& F, long m,
               const zz_pEX& R)
{
   vec_zz_pE x;

   ProjectPowers(x, R, 2*m, g, F);
   MinPolySeq(h, x, m);
}

void ProbMinPolyMod(zz_pEX& h, const zz_pEX& g, const zz_pEXModulus& F, long m)
{
   long n = F.n;
   if (m < 1 || m > n) Error("ProbMinPoly: bad args");

   zz_pEX R;
   random(R, n);

   DoMinPolyMod(h, g, F, m, R);
}

void ProbMinPolyMod(zz_pEX& h, const zz_pEX& g, const zz_pEXModulus& F)
{
   ProbMinPolyMod(h, g, F, F.n);
}

void MinPolyMod(zz_pEX& hh, const zz_pEX& g, const zz_pEXModulus& F, long m)
{
   zz_pEX h, h1;
   long n = F.n;
   if (m < 1 || m > n) Error("MinPoly: bad args");

   /* probabilistically compute min-poly */

   ProbMinPolyMod(h, g, F, m);
   if (deg(h) == m) { hh = h; return; }
   CompMod(h1, h, g, F);
   if (IsZero(h1)) { hh = h; return; }

   /* not completely successful...must iterate */

   zz_pEX h2, h3;
   zz_pEX R;
   zz_pEXTransMultiplier H1;

   for (;;) {
      random(R, n);
      build(H1, h1, F);
      TransMulMod(R, R, H1, F);
      DoMinPolyMod(h2, g, F, m-deg(h), R);

      mul(h, h, h2);
      if (deg(h) == m) { hh = h; return; }
      CompMod(h3, h2, g, F);
      MulMod(h1, h3, h1, F);
      if (IsZero(h1)) { hh = h; return; }
   }
}

void IrredPolyMod(zz_pEX& h, const zz_pEX& g, const zz_pEXModulus& F, long m)
{
   if (m < 1 || m > F.n) Error("IrredPoly: bad args");

   zz_pEX R;
   set(R);

   DoMinPolyMod(h, g, F, m, R);
}

void IrredPolyMod(zz_pEX& h, const zz_pEX& g, const zz_pEXModulus& F)
{
   IrredPolyMod(h, g, F, F.n);
}

void MinPolyMod(zz_pEX& hh, const zz_pEX& g, const zz_pEXModulus& F)
{
   MinPolyMod(hh, g, F, F.n);
}

void diff(zz_pEX& x, const zz_pEX& a)
{
   long n = deg(a);
   long i;

   if (n <= 0) {
      clear(x);
      return;
   }

   if (&x != &a)
      x.rep.SetLength(n);

   for (i = 0; i <= n-1; i++) {
      mul(x.rep[i], a.rep[i+1], i+1);
   }

   if (&x == &a)
      x.rep.SetLength(n);

   x.normalize();
}

void MakeMonic(zz_pEX& x)
{
   if (IsZero(x))
      return;

   if (IsOne(LeadCoeff(x)))
      return;

   zz_pE t;

   inv(t, LeadCoeff(x));
   mul(x, x, t);
}

long divide(zz_pEX& q, const zz_pEX& a, const zz_pEX& b)
{
   if (IsZero(b)) {
      if (IsZero(a)) {
         clear(q);
         return 1;
      }
      else
         return 0;
   }

   zz_pEX lq, r;
   DivRem(lq, r, a, b);
   if (!IsZero(r)) return 0;
   q = lq;
   return 1;
}

long divide(const zz_pEX& a, const zz_pEX& b)
{
   if (IsZero(b)) return IsZero(a);
   zz_pEX lq, r;
   DivRem(lq, r, a, b);
   if (!IsZero(r)) return 0;
   return 1;
}

static
long OptWinSize(long n)
// finds k that minimizes n/(k+1) + 2^{k-1}

{
   long k;
   double v, v_new;

   v = n/2.0 + 1.0;
   k = 1;

   for (;;) {
      v_new = n/(double(k+2)) + double(1L << k);
      if (v_new >= v) break;
      v = v_new;
      k++;
   }

   return k;
}

void PowerMod(zz_pEX& h, const zz_pEX& g, const ZZ& e, const zz_pEXModulus& F)
// h = g^e mod f using "sliding window" algorithm
{
   if (deg(g) >= F.n) Error("PowerMod: bad args");

   if (e == 0) {
      set(h);
      return;
   }

   if (e == 1) {
      h = g;
      return;
   }

   if (e == -1) {
      InvMod(h, g, F);
      return;
   }

   if (e == 2) {
      SqrMod(h, g, F);
      return;
   }

   if (e == -2) {
      SqrMod(h, g, F);
      InvMod(h, h, F);
      return;
   }

   long n = NumBits(e);

   zz_pEX res;
   res.SetMaxLength(F.n);
   set(res);

   long i;

   if (n < 16) {
      // plain square-and-multiply algorithm

      for (i = n - 1; i >= 0; i--) {
         SqrMod(res, res, F);
         if (bit(e, i))
            MulMod(res, res, g, F);
      }

      if (e < 0) InvMod(res, res, F);

      h = res;
      return;
   }

   long k = OptWinSize(n);
   k = min(k, 3);

   vec_zz_pEX v;

   v.SetLength(1L << (k-1));

   v[0] = g;

   if (k > 1) {
      zz_pEX t;
      SqrMod(t, g, F);

      for (i = 1; i < (1L << (k-1)); i++)
         MulMod(v[i], v[i-1], t, F);
   }

   long val;
   long cnt;
   long m;

   val = 0;
   for (i = n-1; i >= 0; i--) {
      val = (val << 1) | bit(e, i);
      if (val == 0)
         SqrMod(res, res, F);
      else if (val >= (1L << (k-1)) || i == 0) {
         cnt = 0;
         while ((val & 1) == 0) {
            val = val >> 1;
            cnt++;
         }

         m = val;
         while (m > 0) {
            SqrMod(res, res, F);
            m = m >> 1;
         }

         MulMod(res, res, v[val >> 1], F);

         while (cnt > 0) {
            SqrMod(res, res, F);
            cnt--;
         }

         val = 0;
      }
   }

   if (e < 0) InvMod(res, res, F);

   h = res;
}

void InvMod(zz_pEX& x, const zz_pEX& a, const zz_pEX& f)
{
   if (deg(a) >= deg(f) || deg(f) == 0) Error("InvMod: bad args");

   zz_pEX d, t;

   XGCD(d, x, t, a, f);
   if (!IsOne(d))
      Error("zz_pEX InvMod: can't compute multiplicative inverse");
}

long InvModStatus(zz_pEX& x, const zz_pEX& a, const zz_pEX& f)
{
   if (deg(a) >= deg(f) || deg(f) == 0) Error("InvModStatus: bad args");
   zz_pEX d, t;

   XGCD(d, x, t, a, f);
   if (!IsOne(d)) {
      x = d;
      return 1;
   }
   else
      return 0;
}

void MulMod(zz_pEX& x, const zz_pEX& a, const zz_pEX& b, const zz_pEX& f)
{
   if (deg(a) >= deg(f) || deg(b) >= deg(f) || deg(f) == 0)
      Error("MulMod: bad args");

   zz_pEX t;

   mul(t, a, b);
   rem(x, t, f);
}

void SqrMod(zz_pEX& x, const zz_pEX& a, const zz_pEX& f)
{
   if (deg(a) >= deg(f) || deg(f) == 0) Error("SqrMod: bad args");

   zz_pEX t;

   sqr(t, a);
   rem(x, t, f);
}

void PowerXMod(zz_pEX& hh, const ZZ& e, const zz_pEXModulus& F)
{
   if (F.n < 0) Error("PowerXMod: uninitialized modulus");

   if (IsZero(e)) {
      set(hh);
      return;
   }

   long n = NumBits(e);
   long i;

   zz_pEX h;

   h.SetMaxLength(F.n);
   set(h);

   for (i = n - 1; i >= 0; i--) {
      SqrMod(h, h, F);
      if (bit(e, i))
         MulByXMod(h, h, F.f);
   }

   if (e < 0) InvMod(h, h, F);

   hh = h;
}

void reverse(zz_pEX& x, const zz_pEX& a, long hi)
{
   if (hi < 0) { clear(x); return; }
   if (NTL_OVERFLOW(hi, 1, 0))
      Error("overflow in reverse");

   if (&x == &a) {
      zz_pEX tmp;
      CopyReverse(tmp, a, hi);
      x = tmp;
   }
   else
      CopyReverse(x, a, hi);
}

void power(zz_pEX& x, const zz_pEX& a, long e)
{
   if (e < 0) {
      Error("power: negative exponent");
   }

   if (e == 0) {
      x = 1;
      return;
   }

   if (a == 0 || a == 1) {
      x = a;
      return;
   }

   long da = deg(a);

   if (da == 0) {
      x = power(ConstTerm(a), e);
      return;
   }

   if (da > (NTL_MAX_LONG-1)/e)
      Error("overflow in power");

   zz_pEX res;
   res.SetMaxLength(da*e + 1);
   res = 1;

   long k = NumBits(e);
   long i;

   for (i = k - 1; i >= 0; i--) {
      sqr(res, res);
      if (bit(e, i))
         mul(res, res, a);
   }

   x = res;
}

static
void FastTraceVec(vec_zz_pE& S, const zz_pEXModulus& f)
{
   long n = deg(f);

   zz_pEX x = reverse(-LeftShift(reverse(diff(reverse(f)), n-1), n-1)/f, n-1);

   S.SetLength(n);
   S[0] = n;

   long i;
   for (i = 1; i < n; i++)
      S[i] = coeff(x, i);
}

void PlainTraceVec(vec_zz_pE& S, const zz_pEX& ff)
{
   if (deg(ff) <= 0)
      Error("TraceVec: bad args");

   zz_pEX f;
   f = ff;

   MakeMonic(f);

   long n = deg(f);

   S.SetLength(n);

   if (n == 0)
      return;

   long k, i;
   zz_pX acc, t;
   zz_pE t1;

   S[0] = n;

   for (k = 1; k < n; k++) {
      mul(acc, rep(f.rep[n-k]), k);

      for (i = 1; i < k; i++) {
         mul(t, rep(f.rep[n-i]), rep(S[k-i]));
         add(acc, acc, t);
      }

      conv(t1, acc);
      negate(S[k], t1);
   }
}

void TraceVec(vec_zz_pE& S, const zz_pEX& f)
{
   if (deg(f) < zz_pE::DivCross())
      PlainTraceVec(S, f);
   else
      FastTraceVec(S, f);
}

static
void ComputeTraceVec(const zz_pEXModulus& F)
{
   vec_zz_pE& S = *((vec_zz_pE *) &F.tracevec);

   if (S.length() > 0)
      return;

   if (F.method == zz_pEX_MOD_PLAIN) {
      PlainTraceVec(S, F.f);
   }
   else {
      FastTraceVec(S, F);
   }
}

void TraceMod(zz_pE& x, const zz_pEX& a, const zz_pEXModulus& F)
{
   long n = F.n;

   if (deg(a) >= n)
      Error("trace: bad args");

   if (F.tracevec.length() == 0)
      ComputeTraceVec(F);

   InnerProduct(x, a.rep, F.tracevec);
}

void TraceMod(zz_pE& x, const zz_pEX& a, const zz_pEX& f)
{
   if (deg(a) >= deg(f) || deg(f) <= 0)
      Error("trace: bad args");

   project(x, TraceVec(f), a);
}

void PlainResultant(zz_pE& rres, const zz_pEX& a, const zz_pEX& b)
{
   zz_pE res;

   if (IsZero(a) || IsZero(b))
      clear(res);
   else if (deg(a) == 0 && deg(b) == 0)
      set(res);
   else {
      long d0, d1, d2;
      zz_pE lc;
      set(res);

      long n = max(deg(a),deg(b)) + 1;
      zz_pEX u(INIT_SIZE, n), v(INIT_SIZE, n);
      vec_zz_pX tmp;
      SetSize(tmp, n, 2*zz_pE::degree());

      u = a;
      v = b;

      for (;;) {
         d0 = deg(u);
         d1 = deg(v);
         lc = LeadCoeff(v);

         PlainRem(u, u, v, tmp);
         swap(u, v);

         d2 = deg(v);
         if (d2 >= 0) {
            power(lc, lc, d0-d2);
            mul(res, res, lc);
            if (d0 & d1 & 1) negate(res, res);
         }
         else {
            if (d1 == 0) {
               power(lc, lc, d0);
               mul(res, res, lc);
            }
            else
               clear(res);

            break;
         }
      }

      rres = res;
   }
}

void resultant(zz_pE& rres, const zz_pEX& a, const zz_pEX& b)
{
   PlainResultant(rres, a, b);
}

void NormMod(zz_pE& x, const zz_pEX& a, const zz_pEX& f)
{
   if (deg(f) <= 0 || deg(a) >= deg(f))
      Error("norm: bad args");

   if (IsZero(a)) {
      clear(x);
      return;
   }

   zz_pE t;
   resultant(t, f, a);
   if (!IsOne(LeadCoeff(f))) {
      zz_pE t1;
      power(t1, LeadCoeff(f), deg(a));
      inv(t1, t1);
      mul(t, t, t1);
   }

   x = t;
}

// tower stuff...

void InnerProduct(zz_pEX& x, const vec_zz_p& v, long low, long high,
                   const vec_zz_pEX& H, long n, vec_zz_pE& t)
{
   zz_pE s;
   long i, j;

   for (j = 0; j < n; j++)
      clear(t[j]);

   high = min(high, v.length()-1);
   for (i = low; i <= high; i++) {
      const vec_zz_pE& h = H[i-low].rep;
      long m = h.length();
      const zz_p& w = v[i];

      for (j = 0; j < m; j++) {
         mul(s, h[j], w);
         add(t[j], t[j], s);
      }
   }

   x.rep.SetLength(n);
   for (j = 0; j < n; j++)
      x.rep[j] = t[j];

   x.normalize();
}

void CompTower(zz_pEX& x, const zz_pX& g, const zz_pEXArgument& A,
             const zz_pEXModulus& F)
{
   if (deg(g) <= 0) {
      conv(x, g);
      return;
   }

   zz_pEX s, t;
   vec_zz_pE scratch;
   scratch.SetLength(deg(F));

   long m = A.H.length() - 1;
   long l = ((g.rep.length()+m-1)/m) - 1;

   const zz_pEX& M = A.H[m];

   InnerProduct(t, g.rep, l*m, l*m + m - 1, A.H, F.n, scratch);
   for (long i = l-1; i >= 0; i--) {
      InnerProduct(s, g.rep, i*m, i*m + m - 1, A.H, F.n, scratch);
      MulMod(t, t, M, F);
      add(t, t, s);
   }
   x = t;
}

void CompTower(zz_pEX& x, const zz_pX& g, const zz_pEX& h,
             const zz_pEXModulus& F)
   // x = g(h) mod f
{
   long m = SqrRoot(g.rep.length());

   if (m == 0) {
      clear(x);
      return;
   }

   zz_pEXArgument A;

   build(A, h, F, m);

   CompTower(x, g, A, F);
}

void PrepareProjection(vec_vec_zz_p& tt, const vec_zz_pE& s,
                       const vec_zz_p& proj)
{
   long l = s.length();
   tt.SetLength(l);

   zz_pXMultiplier M;
   long i;

   for (i = 0; i < l; i++) {
      build(M, rep(s[i]), zz_pE::modulus());
      UpdateMap(tt[i], proj, M, zz_pE::modulus());
   }
}

void ProjectedInnerProduct(zz_p& x, const vec_zz_pE& a,
                           const vec_vec_zz_p& b)
{
   long n = min(a.length(), b.length());

   zz_p t, res;

   res = 0;

   long i;
   for (i = 0; i < n; i++) {
      project(t, b[i], rep(a[i]));
      res += t;
   }

   x = res;
}

void PrecomputeProj(vec_zz_p& proj, const zz_pX& f)
{
   long n = deg(f);

   if (n <= 0) Error("PrecomputeProj: bad args");

   if (ConstTerm(f) != 0) {
      proj.SetLength(1);
      proj[0] = 1;
   }
   else {
      proj.SetLength(n);
      clear(proj);
      proj[n-1] = 1;
   }
}

void ProjectPowersTower(vec_zz_p& x, const vec_zz_pE& a, long k,
                   const zz_pEXArgument& H, const zz_pEXModulus& F,
                   const vec_zz_p& proj)

{
   long n = F.n;

   if (a.length() > n || k < 0 || NTL_OVERFLOW(k, 1, 0))
      Error("ProjectPowers: bad args");

   long m = H.H.length()-1;
   long l = (k+m-1)/m - 1;

   zz_pEXTransMultiplier M;
   build(M, H.H[m], F);

   vec_zz_pE s(INIT_SIZE, n);
   s = a;

   x.SetLength(k);

   vec_vec_zz_p tt;

   for (long i = 0; i <= l; i++) {
      long m1 = min(m, k-i*m);
      zz_p* w = &x[i*m];

      PrepareProjection(tt, s, proj);

      for (long j = 0; j < m1; j++)
         ProjectedInnerProduct(w[j], H.H[j].rep, tt);
      if (i < l)
         UpdateMap(s, s, M, F);
   }
}

void ProjectPowersTower(vec_zz_p& x, const vec_zz_pE& a, long k,
                   const zz_pEX& h, const zz_pEXModulus& F,
                   const vec_zz_p& proj)

{
   if (a.length() > F.n || k < 0) Error("ProjectPowers: bad args");

   if (k == 0) {
      x.SetLength(0);
      return;
   }

   long m = SqrRoot(k);

   zz_pEXArgument H;

   build(H, h, F, m);
   ProjectPowersTower(x, a, k, H, F, proj);
}

void DoMinPolyTower(zz_pX& h, const zz_pEX& g, const zz_pEXModulus& F, long m,
               const vec_zz_pE& R, const vec_zz_p& proj)
{
   vec_zz_p x;

   ProjectPowersTower(x, R, 2*m, g, F, proj);

   MinPolySeq(h, x, m);
}

void ProbMinPolyTower(zz_pX& h, const zz_pEX& g, const zz_pEXModulus& F,
                      long m)
{
   long n = F.n;
   if (m < 1 || m > n*zz_pE::degree()) Error("ProbMinPoly: bad args");

   vec_zz_pE R;
   R.SetLength(n);
   long i;
   for (i = 0; i < n; i++)
      random(R[i]);

   vec_zz_p proj;
   PrecomputeProj(proj, zz_pE::modulus());

   DoMinPolyTower(h, g, F, m, R, proj);
}

void ProbMinPolyTower(zz_pX& h, const zz_pEX& g, const zz_pEXModulus& F,
                      long m, const vec_zz_p& proj)
{
   long n = F.n;
   if (m < 1 || m > n*zz_pE::degree()) Error("ProbMinPoly: bad args");

   vec_zz_pE R;
   R.SetLength(n);
   long i;
   for (i = 0; i < n; i++)
      random(R[i]);

   DoMinPolyTower(h, g, F, m, R, proj);
}

void MinPolyTower(zz_pX& hh, const zz_pEX& g, const zz_pEXModulus& F, long m)
{
   zz_pX h;
   zz_pEX h1;
   long n = F.n;
   if (m < 1 || m > n*zz_pE::degree()) {
      Error("MinPoly: bad args");
   }

   vec_zz_p proj;
   PrecomputeProj(proj, zz_pE::modulus());

   /* probabilistically compute min-poly */

   ProbMinPolyTower(h, g, F, m, proj);
   if (deg(h) == m) { hh = h; return; }
   CompTower(h1, h, g, F);
   if (IsZero(h1)) { hh = h; return; }

   /* not completely successful...must iterate */

   long i;

   zz_pX h2;
   zz_pEX h3;
   vec_zz_pE R;
   zz_pEXTransMultiplier H1;

   for (;;) {
      R.SetLength(n);
      for (i = 0; i < n; i++) random(R[i]);
      build(H1, h1, F);
      UpdateMap(R, R, H1, F);
      DoMinPolyTower(h2, g, F, m-deg(h), R, proj);

      mul(h, h, h2);
      if (deg(h) == m) { hh = h; return; }
      CompTower(h3, h2, g, F);
      MulMod(h1, h3, h1, F);
      if (IsZero(h1)) { hh = h; return; }
   }
}

void IrredPolyTower(zz_pX& h, const zz_pEX& g, const zz_pEXModulus& F, long m)
{
   if (m < 1 || m > deg(F)*zz_pE::degree()) Error("IrredPoly: bad args");

   vec_zz_pE R;
   R.SetLength(1);
   R[0] = 1;

   vec_zz_p proj;
   proj.SetLength(1);
   proj[0] = 1;

   DoMinPolyTower(h, g, F, m, R, proj);
}

NTL_END_IMPL


/*
 * Copyright 2009-2021 The VOTCA Development Team (http://www.votca.org)
 *
 * 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.
 *
 */

// Standard includes
#include <iostream>

// Third party includes
#include <boost/program_options.hpp>

// VOTCA includes
#include <votca/tools/akimaspline.h>
#include <votca/tools/cubicspline.h>
#include <votca/tools/linspline.h>
#include <votca/tools/spline.h>
#include <votca/tools/table.h>
#include <votca/tools/tokenizer.h>

// Local VOTCA includes
#include "votca/csg/version.h"

using namespace std;
namespace po = boost::program_options;
using namespace votca::csg;
using namespace votca::tools;

void help_text() {
  votca::csg::HelpTextHeader("csg_resample");
  cout << "Change grid and interval of any sort of table files.\n"
          "Mainly called internally by inverse script, can also be\n"
          "used to manually prepare input files for coarse-grained\n"
          "simulations.\n\n";
}

void check_option(po::options_description &desc, po::variables_map &vm,
                  const string &option) {
  if (!vm.count(option)) {
    cout << "csg_resample \n\n";
    cout << desc << endl << "parameter " << option << " is not specified\n";
    exit(1);
  }
}

int main(int argc, char **argv) {

  string in_file, out_file, grid, fitgrid, comment, type, boundaries;

  Table in, out, der;
  // program options
  po::options_description desc("Allowed options");

  desc.add_options()("help", "produce this help message")(
      "in", po::value<string>(&in_file), "table to read")(
      "out", po::value<string>(&out_file), "table to write")(
      "derivative", po::value<string>(), "table to write")(
      "grid", po::value<string>(&grid),
      "new grid spacing (min:step:max). If 'grid' is specified only, "
      "interpolation is performed.")(
      "type", po::value<string>(&type)->default_value("akima"),
      "[cubic|akima|linear]. If option is not specified, the default type "
      "'akima' is assumed.")("fitgrid", po::value<string>(&fitgrid),
                             "specify fit grid (min:step:max). If 'grid' and "
                             "'fitgrid' are specified, a fit is performed.")(
      "nocut",
      "Option for fitgrid: Normally, values out of fitgrid boundaries are cut "
      "off. If they shouldn't, choose --nocut.")(
      "comment", po::value<string>(&comment),
      "store a comment in the output table")(
      "boundaries", po::value<string>(&boundaries),
      "(natural|periodic|derivativezero) sets boundary conditions");

  po::variables_map vm;
  try {
    po::store(po::parse_command_line(argc, argv, desc), vm);
    po::notify(vm);
  } catch (po::error &err) {
    cout << "error parsing command line: " << err.what() << endl;
    return -1;
  }

  try {
    // does the user want help?
    if (vm.count("help")) {
      help_text();
      cout << desc << endl;
      return 0;
    }

    check_option(desc, vm, "in");
    check_option(desc, vm, "out");

    if (!(vm.count("grid") || vm.count("fitgrid"))) {
      cout << "Need grid for interpolation or fitgrid for fit.\n";
      return 1;
    }

    if ((!vm.count("grid")) && vm.count("fitgrid")) {
      cout << "Need a grid for fitting as well.\n";
      return 1;
    }

    double min, max, step;
    {
      vector<string> toks = Tokenizer(grid, ":").ToVector();
      if (toks.size() != 3) {
        cout << "wrong range format, use min:step:max\n";
        return 1;
      }
      min = stod(toks[0]);
      step = stod(toks[1]);
      max = stod(toks[2]);
    }

    in.Load(in_file);
    std::unique_ptr<Spline> spline;
    if (vm.count("type")) {
      if (type == "cubic") {
        spline = std::make_unique<CubicSpline>(CubicSpline());
      } else if (type == "akima") {
        spline = std::make_unique<AkimaSpline>(AkimaSpline());
      } else if (type == "linear") {
        spline = std::make_unique<LinSpline>(LinSpline());
      } else {
        throw std::runtime_error("unknown spline type:" + type);
      }
    }
    spline->setBC(Spline::splineNormal);

    if (vm.count("boundaries")) {
      if (boundaries == "periodic") {
        spline->setBC(Spline::splinePeriodic);
      }
      if (boundaries == "derivativezero") {
        spline->setBC(Spline::splineDerivativeZero);
      }
      // default: normal
    }

    // in case fit is specified
    if (vm.count("fitgrid")) {
      vector<string> toks = Tokenizer(fitgrid, ":").ToVector();
      if (toks.size() != 3) {
        cout << "wrong range format in fitgrid, use min:step:max\n";
        return 1;
      }
      double sp_min, sp_max, sp_step;
      sp_min = stod(toks[0]);
      sp_step = stod(toks[1]);
      sp_max = stod(toks[2]);
      cout << "doing " << type << " fit " << sp_min << ":" << sp_step << ":"
           << sp_max << endl;

      // cut off any values out of fitgrid boundaries (exception: do nothing in
      // case of --nocut)
      Eigen::VectorXd x_copy;
      Eigen::VectorXd y_copy;
      if (!vm.count("nocut")) {
        // determine vector size
        votca::Index minindex = -1, maxindex = -1;
        for (votca::Index i = 0; i < in.x().size(); i++) {
          if (in.x(i) < sp_min) {
            minindex = i;
          }
          if (in.x(i) <= sp_max) {
            maxindex = i;
          }
        }
        // copy data values in [sp_min,sp_max] into new vectors
        minindex++;
        x_copy = Eigen::VectorXd::Zero(maxindex - minindex + 1);
        y_copy = Eigen::VectorXd::Zero(maxindex - minindex + 1);
        for (votca::Index i = minindex; i <= maxindex; i++) {
          x_copy(i - minindex) = in.x(i);
          y_copy(i - minindex) = in.y(i);
        }
      }

      // fitting
      spline->GenerateGrid(sp_min, sp_max, sp_step);
      try {
        if (vm.count("nocut")) {
          spline->Fit(in.x(), in.y());
        } else {
          spline->Fit(x_copy, y_copy);
        }
      } catch (const char *message) {
        if (strcmp("qrsolve_zero_column_in_matrix", message)) {
          throw std::runtime_error(
              "error in Linalg::linalg_qrsolve : Not enough data for fit, "
              "please adjust grid (zero row in fit matrix)");
        } else if (strcmp("constrained_qrsolve_zero_column_in_matrix",
                          message)) {
          throw std::runtime_error(
              "error in Linalg::linalg_constrained_qrsolve : Not enough data "
              "for fit, please adjust grid (zero row in fit matrix)");
        } else {
          throw std::runtime_error(
              "Unknown error in csg_resample while fitting.");
        }
      }
    } else {
      // otherwise do interpolation (default = cubic)
      try {
        spline->Interpolate(in.x(), in.y());
      } catch (const char *message) {
        if (strcmp("qrsolve_zero_column_in_matrix", message)) {
          throw std::runtime_error(
              "error in Linalg::linalg_qrsolve : Not enough data, please "
              "adjust grid (zero row in fit matrix)");
        } else if (strcmp("constrained_qrsolve_zero_column_in_matrix",
                          message)) {
          throw std::runtime_error(
              "error in Linalg::linalg_constrained_qrsolve : Not enough data, "
              "please adjust grid (zero row in fit matrix)");
        } else {
          throw std::runtime_error(
              "Unknown error in csg_resample while interpolating.");
        }
      }
    }

    out.GenerateGridSpacing(min, max, step);
    out.y() = spline->Calculate(out.x());

    // store a comment line
    if (vm.count("comment")) {
      out.set_comment(comment);
    }
    out.flags() = std::vector<char>(out.flags().size(), 'o');

    der.GenerateGridSpacing(min, max, step);
    der.flags() = std::vector<char>(der.flags().size(), 'o');

    votca::Index i = 0;
    for (i = 0; out.x(i) < in.x(0) && i < out.size(); ++i) {
      ;
    }

    votca::Index j = 0;
    for (; i < out.size(); ++i) {
      for (; j < in.size(); ++j) {
        if (in.x(j) >= out.x(i) ||
            std::abs(in.x(j) - out.x(i)) < 1e-12) {  // fix for precision errors
          break;
        }
      }
      if (in.size() == j) {
        break;
      }
      out.flags(i) = in.flags(j);
      der.flags(i) = in.flags(j);
    }

    out.Save(out_file);

    if (vm.count("derivative")) {
      der.y() = spline->CalculateDerivative(der.x());

      der.Save(vm["derivative"].as<string>());
    }

  } catch (std::exception &error) {
    cerr << "an error occurred:\n" << error.what() << endl;
    return -1;
  }
  return 0;
}


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

#ifndef SMOOTH__SPLINE__FIT_HPP_
#define SMOOTH__SPLINE__FIT_HPP_

/**
 * @file
 * @brief Fit Spline and Bspline from data.
 */

#include <Eigen/Sparse>
#include <Eigen/SparseCholesky>
#include <Eigen/SparseLU>

#include <cassert>
#include <ranges>

#include "smooth/manifold_vector.hpp"
#include "smooth/optim.hpp"

#include "bspline.hpp"
#include "spline.hpp"

namespace smooth {

/**
 * @brief Spline specification.
 */
template<typename T>
concept SplineSpec = requires(T t)
{
  // clang-format off
  { T::Degree } -> std::convertible_to<int>;
  { T::OptDeg } -> std::convertible_to<int>;
  { T::InnCnt } -> std::convertible_to<int>;
  { t.LeftDeg };
  { t.RghtDeg };
  // clang-format on
};

namespace spline_specs
{

  /**
   * @brief SplineSpec without boundary constraints
   *
   * @tparam K spline degree (must be 0 or 1)
   */
  template<LieGroup G, std::size_t K>
  struct NoConstraints
  {
    /// @brief Polynomial degree
    static constexpr int Degree = K;
    /// @brief Optimization degree (absolute integral of derivative OptDeg is minimized)
    static constexpr int OptDeg = -1;
    /// @brief Number of derivatives to enforce continuity for
    static constexpr int InnCnt = int(K) - 1;

    /// @brief Degrees of left-side boundary constraints (no constraints)
    static constexpr std::array<int, 0> LeftDeg{};
    /// @brief Values of left-side boundary constraints
    std::array<Tangent<G>, 0> left_values{};

    /// @brief Degrees of right-side boundary constraints (no constraints)
    static constexpr std::array<int, 0> RghtDeg{};
    /// @brief Values of right-side boundary constraints
    std::array<Tangent<G>, 0> rght_values{};
  };

  /// @brief SplineSpec for a piecewise constant function
  template<LieGroup G>
  using PiecewiseConstant = NoConstraints<G, 0>;

  /// @brief SplineSpec for a piecewise linear function
  template<LieGroup G>
  using PiecewiseLinear = NoConstraints<G, 1>;

  /**
   * @brief SplineSpec for a cubic spline with two boundary conditions.
   *
   * @tparam P1 order of left boundary contraint (must be 1 or 2).
   * @tparam P2 order of right boundary contraint (must be 1 or 2).
   */
  template<LieGroup G, std::size_t P1 = 2, std::size_t P2 = P1>
  struct FixedDerCubic
  {
    /// @brief Polynomial degree
    static constexpr int Degree = 3;
    /// @brief Optimization degree (absolute integral of derivative OptDeg is minimized)
    static constexpr int OptDeg = -1;
    /// @brief Number of derivatives to enforce continuity for
    static constexpr int InnCnt = 2;

    /// @brief Degrees of left-side boundary constraints: P1
    static constexpr std::array<int, 1> LeftDeg{P1};
    /// @brief Values of left-side boundary constraints
    std::array<Tangent<G>, 1> left_values{Tangent<G>::Zero()};

    /// @brief Degrees of right-side boundary constraints: P2
    static constexpr std::array<int, 1> RghtDeg{P2};
    /// @brief Values of right-side boundary constraints
    std::array<Tangent<G>, 1> rght_values{Tangent<G>::Zero()};
  };

  /**
   * @brief SplineSpec for optimized spline.
   *
   * @tparam K spline degree
   * @tparam O order to optimize
   * @tparam P continuity order
   */
  template<LieGroup G, std::size_t K = 6, std::size_t O = 3, std::size_t P = 3>
  struct MinDerivative
  {
    /// @brief Polynomial degree
    static constexpr int Degree = K;
    /// @brief Optimization degree (absolute integral of derivative OptDeg is minimized)
    static constexpr int OptDeg = O;
    /// @brief Number of derivatives to enforce continuity for
    static constexpr int InnCnt = P;

    /// @brief Degrees of left-side boundary constraints: 1, 2, ..., P-1
    static constexpr std::array<int, P - 1> LeftDeg = []() {
      std::array<int, P - 1> ret;
      for (auto i = 0u; i < P - 1; ++i) { ret[i] = i + 1; }
      return ret;
    }();

    /// @brief Values of left-side boundary constraints
    std::array<Tangent<G>, P - 1> left_values = []() {
      std::array<Tangent<G>, P - 1> ret;
      ret.fill(Tangent<G>::Zero());
      return ret;
    }();

    /// @brief Degrees of left-side boundary constraints: 1, 2, ..., P-1
    static constexpr std::array<int, P - 1> RghtDeg = LeftDeg;
    /// @brief Values of right-side boundary constraints
    std::array<Tangent<G>, P - 1> rght_values = left_values;
  };

}  // namespace spline_specs

// \cond
namespace detail {

template<SplineSpec SS>
constexpr int splinespec_max_deriv()
{
  int ret = std::max<int>(0, SS::InnCnt);

  for (const auto & x : SS::LeftDeg) { ret = std::max(ret, x); }
  for (const auto & x : SS::RghtDeg) { ret = std::max(ret, x); }

  return ret;
}

template<SplineSpec T>
struct splinespec_extract;

template<template<LieGroup, std::size_t...> typename T, LieGroup G, std::size_t... Is>
struct splinespec_extract<T<G, Is...>>
{
  using group = G;
};

template<SplineSpec T, LieGroup Gnew>
struct splinespec_rebind;

template<
  template<LieGroup, std::size_t...>
  typename T,
  LieGroup Gold,
  LieGroup Gnew,
  std::size_t... Is>
struct splinespec_rebind<T<Gold, Is...>, Gnew>
{
  using type = T<Gnew, Is...>;
};

template<SplineSpec SS>
auto splinespec_project(const SS & ss, std::size_t k)
{
  using Scalar = Scalar<typename splinespec_extract<SS>::group>;

  typename splinespec_rebind<SS, Scalar>::type ret;
  for (auto i = 0u; i < ss.LeftDeg.size(); ++i) {
    ret.left_values[i] = ss.left_values[i].template segment<1>(k);
  }
  for (auto i = 0u; i < ss.RghtDeg.size(); ++i) {
    ret.rght_values[i] = ss.rght_values[i].template segment<1>(k);
  }
  return ret;
}

}  // namespace detail
// \endcond

/**
 * @brief Find N degree K Bernstein polynomials p_i(t) for i = 0, ..., N s.t that satisfies
 * constraints and s.t.
 * \f[
 *   p_i(0) = 0 \\
 *   p_i(\delta t) = \delta x
 * \f]
 *
 * @param dt_r range of parameter differences \f$ \delta_t \f$
 * @param dx_r range of value differences \f$ \delta_x \f$
 * @param ss spline specification
 * @return vector \f$ \alpha \f$ of size (K + 1) * N s.t. \f$ \beta = \alpha_{i (K + 1): (i + 1) (K
 * + 1) } \f$ defines polynomial \f$ p_i \f$ as \f[ p_i(t) = \sum_{\nu = 0}^K \beta_\nu b_{\nu, k}
 * \left( \frac{t}{\delta t} \right), \f] where \f$ \delta t \f$ is the i:th member of \p dt_r.
 *
 * @note Allocates heap memory.
 */
Eigen::VectorXd fit_spline_1d(
  std::ranges::sized_range auto && dt_r,
  std::ranges::sized_range auto && dx_r,
  const SplineSpec auto & ss)
{
  using namespace std::views;

  using SS = std::decay_t<decltype(ss)>;

  const std::size_t N = std::min(std::ranges::size(dt_r), std::ranges::size(dx_r));

  // coefficient layout is
  //   [ x0 x1   ...   Xn ]
  // where p_i(t) = \sum_k x_i[k] * b_{i,k}(t) defines p on [tvec(i), tvec(i+1)]

  static constexpr auto K = SS::Degree;
  static constexpr auto D = detail::splinespec_max_deriv<SS>();

  static_assert(K >= D, "K >= D");

  // compile-time matrix algebra
  static constexpr auto B_s    = polynomial_basis<PolynomialBasis::Bernstein, K>();
  static constexpr auto U0_s   = monomial_derivatives<K, D>(0.);
  static constexpr auto U1_s   = monomial_derivatives<K, D>(1.);
  static constexpr auto U0tB_s = U0_s * B_s;
  static constexpr auto U1tB_s = U1_s * B_s;

  Eigen::Map<const Eigen::Matrix<double, D + 1, K + 1, Eigen::RowMajor>> U0tB(U0tB_s[0].data());
  Eigen::Map<const Eigen::Matrix<double, D + 1, K + 1, Eigen::RowMajor>> U1tB(U1tB_s[0].data());

  // d:th derivative of basis polynomial at 0 (resp. 1) is now U0tB.row(d) * x (resp. U1tB.row(d) *
  // x), where x are the coefficients.

  const std::size_t N_coef = (K + 1) * N;
  const std::size_t N_eq   = ss.LeftDeg.size()                            // left endpoint
                         + N                                              // value left-segment
                         + (SS::InnCnt >= 0 ? N : 0)                      // value rght-segment
                         + (SS::InnCnt > 0 ? (N - 1) * (SS::InnCnt) : 0)  // derivative continuity
                         + ss.RghtDeg.size();                             // right endpiont

  assert(N_coef >= N_eq);

  // CONSTRAINT MATRICES A, b

  Eigen::SparseMatrix<double, Eigen::ColMajor> A(N_eq, N_coef);
  Eigen::VectorXi A_pattern(N_coef);
  A_pattern.head(K + 1).setConstant(1 + ss.LeftDeg.size() + (SS::InnCnt >= 0 ? 1 + SS::InnCnt : 0));
  if (N >= 2) {
    A_pattern.segment(K + 1, (N - 2) * (K + 1))
      .setConstant(1 + (SS::InnCnt >= 0 ? 1 + 2 * SS::InnCnt : 0));
  }
  A_pattern.tail(K + 1).setConstant(1 + ss.RghtDeg.size() + (SS::InnCnt >= 0 ? 1 + SS::InnCnt : 0));
  A.reserve(A_pattern);

  Eigen::VectorXd b = Eigen::VectorXd::Zero(N_eq);

  // current inequality counter
  std::size_t M = 0;

  // curve beg derivative constraints
  for (auto i = 0u; i < ss.LeftDeg.size(); ++i) {
    for (auto j = 0u; j < K + 1; ++j) { A.insert(M, j) = U0tB(ss.LeftDeg[i], j); }
    b(M++) = ss.left_values[i].x();
  }

  // interval beg + end value constraint
  for (const auto & [i, dx] : utils::zip(iota(0u), dx_r)) {
    for (auto j = 0; j < K + 1; ++j) { A.insert(M, i * (K + 1) + j) = U0tB(0, j); }
    b(M++) = 0;
    if (SS::InnCnt >= 0) {
      for (auto j = 0; j < K + 1; ++j) { A.insert(M, i * (K + 1) + j) = U1tB(0, j); }
      b(M++) = dx;
    }
  }

  // inner derivative continuity constraint
  for (const auto & [k, dt, dt_next] : utils::zip(iota(0u, N - 1), dt_r, dt_r | drop(1))) {
    for (auto d = 1; d <= SS::InnCnt; ++d) {
      const double fac1 = 1. / std::pow(dt, d);
      const double fac2 = 1. / std::pow(dt_next, d);
      for (auto j = 0; j < K + 1; ++j) {
        A.insert(M, k * (K + 1) + j)       = U1tB(d, j) * fac1;
        A.insert(M, (k + 1) * (K + 1) + j) = -U0tB(d, j) * fac2;
      }
      b(M++) = 0;
    }
  }

  // curve end derivative constraints
  for (auto i = 0u; i < ss.RghtDeg.size(); ++i) {
    for (auto j = 0u; j < K + 1; ++j) {
      A.insert(M, (K + 1) * (N - 1) + j) = U1tB(ss.RghtDeg[i], j);
    }
    b(M++) = ss.rght_values[i].x();
  }

  A.prune(1e-9);  // there are typically a lot of zeros (depends on basis)..
  A.makeCompressed();

  if constexpr (SS::OptDeg < 0) {
    // No optimization, solve directly
    assert(N_eq == N_coef);
    Eigen::SparseLU<decltype(A)> lu(A);
    return lu.solve(b);
  } else {
    static_assert(K >= SS::OptDeg, "K >= OptDeg");

    // COST MATRIX P

    // cost function is ∫ | p^{(D)} (t) |^2 dt,  t : 0 -> T,
    // or (1 / T)^{2D - 1} ∫ | p^{(D)} (u) |^2 du,  u : 0 -> 1
    //
    // p(u) = u^{(D)}^T B x, so p^{(D)} (u)^2 = x' B' u^{(D)}' u^{(D)} B x
    //
    // Let M = \int_{0}^1 u^{(D)} u^{(D)}' du   u : 0 -> 1, then the cost matrix P
    // is (1 / T)^{2D - 1} * B' * M * B

    static constexpr auto Mmat = monomial_integral<K, SS::OptDeg, double>();
    static constexpr StaticMatrix<double, K + 1, K + 1> P_s = B_s.transpose() * Mmat * B_s;

    Eigen::Map<const Eigen::Matrix<double, K + 1, K + 1, Eigen::RowMajor>> P(P_s[0].data());

    // SOLVE QP

    // We solve
    //   min_{x : Ax = b}  (1/2) x' Q x
    // by solving the KKT equations
    //   [Q A'; A 0] [x; l] =   [0; b]
    // via LDLt factorization

    Eigen::SparseMatrix<double> H(N_coef + N_eq, N_coef + N_eq);

    Eigen::Matrix<int, -1, 1> H_pattern(N_coef + N_eq);
    for (auto i = 0u; i != N_coef; ++i) {
      H_pattern(i) = (K + 1) + A.outerIndexPtr()[i + 1] - A.outerIndexPtr()[i];
    }
    H_pattern.tail(N_eq).setZero();

    H.reserve(H_pattern);

    for (const auto & [i, dt] : utils::zip(iota(0u), dt_r | take(int64_t(N)))) {
      const double fac = std::pow(dt, 1 - 2 * int(D));
      for (auto ki = 0u; ki != K + 1; ++ki) {
        for (auto kj = 0u; kj != K + 1; ++kj) {
          H.insert(i * (K + 1) + ki, i * (K + 1) + kj) = (ki == kj ? 1e-6 : 0.) + fac * P(ki, kj);
        }
      }
    }

    for (auto col = 0u; col != N_coef; ++col) {
      for (typename decltype(A)::InnerIterator it(A, col); it; ++it) {
        H.insert(N_coef + it.index(), col) = it.value();
      }
    }

    H.makeCompressed();

    Eigen::VectorXd rhs(N_coef + N_eq);
    rhs.head(N_coef).setZero();
    rhs.tail(N_eq) = b;

    const Eigen::SimplicialLDLT<decltype(H), Eigen::Lower> ldlt(H);
    return ldlt.solve(rhs).head(N_coef);
  }
}

/**
 * @brief Fit a Spline to given points.
 *
 * @tparam G LieGroup
 * @tparam K Spline degree
 * @param ts range of times
 * @param gs range of values
 * @param ss spline specification
 * @return Spline c s.t. \f$ c(t_i) = g_i \f$ for \f$(t_i, g_i) \in zip(ts, gs) \f$
 *
 * @note Allocates heap memory.
 */
auto fit_spline(
  std::ranges::random_access_range auto && ts,
  std::ranges::random_access_range auto && gs,
  const SplineSpec auto & ss)
{
  using namespace std::views;

  using SS = std::decay_t<decltype(ss)>;
  using G  = PlainObject<std::ranges::range_value_t<std::decay_t<decltype(gs)>>>;

  assert(std::ranges::adjacent_find(ts, std::ranges::greater_equal()) == ts.end());

  static constexpr auto K = SS::Degree;
  const auto N            = std::min(std::ranges::size(ts), std::ranges::size(gs));

  assert(N >= 2);

  static constexpr auto sub     = [](const auto & x1, const auto & x2) { return x2 - x1; };
  static constexpr auto sub_lie = [](const auto & x1, const auto & x2) { return rminus(x2, x1); };

  auto dts = ts | utils::views::pairwise_transform(sub);
  auto dgs = gs | utils::views::pairwise_transform(sub_lie);

  Eigen::Matrix<double, Dof<G>, -1> V(Dof<G>, (N - 1) * (K + 1));

  for (auto k = 0u; k < Dof<G>; ++k) {
    const auto ss_proj = detail::splinespec_project(ss, k);
    V.row(k) = fit_spline_1d(dts, dgs | transform([k](const auto & v) { return v(k); }), ss_proj);
  }

  Spline<K, G> ret;
  ret.reserve(N);

  for (const auto & [i, dt, g, g_next] : utils::zip(iota(0u), dts, gs, gs | drop(1))) {
    // spline is in cumulative form, need to get cumulative coefficients
    Eigen::Matrix<double, Dof<G>, K> cum_coefs =
      V.template block<Dof<G>, K>(0, i * (K + 1) + 1) - V.template block<Dof<G>, K>(0, i * (K + 1));

    if constexpr (K > 2) {
      // modify segment to ensure it is interpolating
      // want exp(v1) * ... * exp(vK) = inv(g) * gnext
      auto mid = K / 2;

      G midval = composition<G>(::smooth::inverse<G>(g), g_next);
      for (auto k = 0; k < mid; ++k) {
        midval = composition<G>(::smooth::exp<G>(-cum_coefs.col(k)), midval);
      }
      for (auto k = K - 1; k > mid; --k) {
        midval = composition<G>(midval, ::smooth::exp<G>(-cum_coefs.col(k)));
      }
      cum_coefs.col(mid) = ::smooth::log<G>(midval);
    }

    ret.concat_global(Spline<K, G>(dt, std::move(cum_coefs), g));
  }

  ret.concat_global(gs[N - 1]);

  return ret;
}

/**
 * @brief Fit a cubic Spline with natural boundary conditions
 *
 * @param ts range of times
 * @param gs range of values
 * @return Spline c s.t. \f$ c(t_i) = g_i \f$ for \f$(t_i, g_i) \in zip(ts, gs) \f$
 *
 * @note Allocates heap memory.
 */
auto fit_spline_cubic(std::ranges::range auto && ts, std::ranges::range auto && gs)
{
  using G = std::ranges::range_value_t<std::decay_t<decltype(gs)>>;
  return fit_spline(
    std::forward<decltype(ts)>(ts),
    std::forward<decltype(gs)>(gs),
    spline_specs::FixedDerCubic<G, 2, 2>{});
}

/**
 * @brief Objective struct for Bspline fitting with analytic jacobian.
 */
template<std::size_t K, std::ranges::range Rs, std::ranges::range Rg>
struct fit_bspline_objective
{
  using G = std::ranges::range_value_t<Rg>;

  Rs ts;
  Rg gs;

  double t0, t1, dt;

  std::size_t NumData, NumPts;

  static constexpr auto M_s = polynomial_cumulative_basis<PolynomialBasis::Bspline, K>();
  inline static const Eigen::Map<const Eigen::Matrix<double, K + 1, K + 1, Eigen::RowMajor>> M =
    Eigen::Map<const Eigen::Matrix<double, K + 1, K + 1, Eigen::RowMajor>>(M_s[0].data());

  fit_bspline_objective(
    std::ranges::range auto && tsin, std::ranges::range auto && gsin, double dtin)
      : ts(std::forward<decltype(tsin)>(tsin)), gs(std::forward<decltype(gsin)>(gsin)), dt(dtin)
  {
    const auto [rt0, rt1] = std::ranges::minmax(ts);

    t0 = rt0;
    t1 = rt1;

    NumData = std::min(std::ranges::size(ts), std::ranges::size(gs));
    NumPts  = K + static_cast<std::size_t>((t1 - t0 + dt) / dt);
  }

  Eigen::VectorXd operator()(const ManifoldVector<G> & var) const
  {
    using namespace std::views;

    Eigen::VectorXd ret(Dof<G> * NumData);

    for (const auto & [i, t, g] : utils::zip(iota(0u), ts, gs)) {
      const int64_t istar = static_cast<int64_t>((t - t0) / dt);
      const double u      = (t - t0 - istar * dt) / dt;

      // gcc 11.1 bug can't handle uint64_t
      const auto g_spline = cspline_eval<K>(var | drop(istar) | take(int64_t(K + 1)), M, u);

      ret.segment<Dof<G>>(i * Dof<G>) = rminus(g_spline, g);
    }

    return ret;
  }

  Eigen::SparseMatrix<double> jacobian(const ManifoldVector<G> & var) const
  {
    using namespace std::views;

    Eigen::SparseMatrix<double, Eigen::RowMajor> Jac;
    Jac.resize(Dof<G> * NumData, Dof<G> * NumPts);
    Jac.reserve(Eigen::Matrix<int, -1, 1>::Constant(Dof<G> * NumData, Dof<G> * (K + 1)));

    for (const auto & [i, t, g] : utils::zip(iota(0u), ts, gs)) {
      const int64_t istar = static_cast<int64_t>((t - t0) / dt);
      const double u      = (t - t0 - istar * dt) / dt;

      Eigen::Matrix<double, Dof<G>, (K + 1) * Dof<G>> d_vali_pts;
      // gcc 11.1 bug can't handle uint64_t
      auto g_spline =
        cspline_eval<K>(var | drop(istar) | take(int64_t(K + 1)), M, u, {}, {}, d_vali_pts);

      const Tangent<G> resi = rminus(g_spline, g);

      const Eigen::Matrix<double, Dof<G>, Dof<G>> d_resi_vali          = dr_expinv<G>(resi);
      const Eigen::Matrix<double, Dof<G>, (K + 1) * Dof<G>> d_resi_pts = d_resi_vali * d_vali_pts;

      for (auto r = 0u; r != Dof<G>; ++r) {
        for (auto c = 0u; c != Dof<G> * (K + 1); ++c) {
          Jac.insert(i * Dof<G> + r, istar * Dof<G> + c) = d_resi_pts(r, c);
        }
      }
    }

    Jac.makeCompressed();

    return Jac;
  }
};

/**
 * @brief Fit a bpsline to data points \f$(t_i, g_i)\f$
 *        by solving the optimization problem
 *
 * \f[
 *   \min_{p}  \left\| p(t_i) - g_i \right\|^2
 * \f]
 *
 * @tparam K bspline degree
 * @param ts time values t_i (doubles, strictly increasing)
 * @param gs data values t_i
 * @param dt distance between spline control points
 *
 * @note Allocates heap memory.
 */
template<std::size_t K>
auto fit_bspline(std::ranges::range auto && ts, std::ranges::range auto && gs, const double dt)
{
  using namespace std::views;
  using G = PlainObject<std::ranges::range_value_t<std::decay_t<decltype(gs)>>>;

  assert(std::ranges::adjacent_find(ts, std::ranges::greater_equal()) == ts.end());

  using obj_t = fit_bspline_objective<K, decltype(ts), decltype(gs)>;

  obj_t obj(std::forward<decltype(ts)>(ts), std::forward<decltype(gs)>(gs), dt);

  // create optimization variable
  ManifoldVector<G> ctrl_pts(obj.NumPts);

  // create initial guess
  auto t_iter = std::ranges::begin(ts);
  auto g_iter = std::ranges::begin(gs);
  for (auto i = 0u; i != obj.NumPts; ++i) {
    const double t_target = obj.t0 + (i - static_cast<double>(K - 1) / 2) * dt;
    while (t_iter + 1 < std::ranges::end(ts)
           && std::abs(t_target - *(t_iter + 1)) < std::abs(t_target - *t_iter)) {
      ++t_iter;
      ++g_iter;
    }
    ctrl_pts[i] = *g_iter;
  }

  // fit to data with loose convergence criteria
  const MinimizeOptions opts{
    .ptol     = 1e-3,
    .ftol     = 1e-3,
    .max_iter = 10,
    .verbose  = false,
  };
  minimize<diff::Type::Analytic>(obj, smooth::wrt(ctrl_pts), opts);

  return BSpline<K, G>(obj.t0, dt, std::move(ctrl_pts));
}

}  // namespace smooth

#endif  // SMOOTH__SPLINE__FIT_HPP_


#include <Eigen/Dense>
#include "beta_dist.h"
#include "configure.h"
#include "dabaghi_der_kiureghian.h"
#include "li_diao_v_h.h"
#include "li_diao_mp.h"
#include "factory.h"
#include "filter.h"
#include "function_dispatcher.h"
#include "inv_gauss_dist.h"
#include "lognormal_dist.h"
#include "numeric_utils.h"
#include "normal_dist.h"
#include "normal_multivar.h"
#include "students_t_dist.h"
#include "uniform_dist.h"
#include "vlachos_et_al.h"
#include "wind_profile.h"
#include "window.h"
#include "wittig_sinha.h"

void config::initialize() {
  // RANDOM VARIABLE GENERATION
  // Register multivariate normal distribution random number generator
  static Register<numeric_utils::RandomGenerator, numeric_utils::NormalMultiVar>
      normal_multivar_default("MultivariateNormal");
  static Register<numeric_utils::RandomGenerator, numeric_utils::NormalMultiVar,
                  int>
      normal_multivar("MultivariateNormal");

  // DISTRIBUTION TYPES
  // Register normal distribution
  static Register<stochastic::Distribution, stochastic::NormalDistribution,
                  double, double>
      normal_dist("NormalDist");
  // Register lognormal distribution
  static Register<stochastic::Distribution, stochastic::LognormalDistribution,
                  double, double>
      lognormal_dist("LognormalDist");
  // Register inverse Gaussian distribution
  static Register<stochastic::Distribution,
                  stochastic::InverseGaussianDistribution, double, double>
      inv_gauss_dist("InverseGaussianDist");
  // Register beta distribution
  static Register<stochastic::Distribution, stochastic::BetaDistribution,
                  double, double>
      beta_dist("BetaDist");
  // Register Student's t distribution
  static Register<stochastic::Distribution, stochastic::StudentstDistribution,
                  double, double, double>
      student_t_dist("StudentstDist");
  // Register uniform distribution
  static Register<stochastic::Distribution, stochastic::UniformDistribution,
                  double, double>
      uniform_dist("UniformDist");

  // STOCHASTIC MODELS
  // Earthquake
  static Register<stochastic::StochasticModel, stochastic::VlachosEtAl, double,
                  double, double, double, unsigned int, unsigned int>
      vlachos_et_al("VlachosSiteSpecificEQ");
  static Register<stochastic::StochasticModel, stochastic::VlachosEtAl, double,
                  double, double, double, unsigned int, unsigned int, int>
      vlachos_et_al_seed("VlachosSiteSpecificEQ");
  static Register<stochastic::StochasticModel, stochastic::DabaghiDerKiureghian,
                  stochastic::FaultType, stochastic::SimulationType, double,
                  double, double, double, double, double, unsigned int,
                  unsigned int, bool>
      dabaghi_der_kiureghian("DabaghiDerKiureghianNFGM");
  static Register<stochastic::StochasticModel, stochastic::DabaghiDerKiureghian,
                  stochastic::FaultType, stochastic::SimulationType, double,
                  double, double, double, double, double, unsigned int,
                  unsigned int, bool, int>
      dabaghi_der_kiureghian_seed("DabaghiDerKiureghianNFGM");
  static Register<stochastic::StochasticModel, stochastic::LiningDiaozemin,
                  stochastic::FaultType, stochastic::SimulationType, double,
                  double, double, double, double, unsigned int,
                  unsigned int, bool, int>
      li_diao_v_h("LiningDiaozemin_VH");
  static Register<stochastic::StochasticModel, stochastic::LiningDiaozemin_MP,
                  stochastic::FaultType, stochastic::SimulationType, double,
                  double, double, double, double, unsigned int,
                  unsigned int, bool, int, int, stochastic::CohType>
      li_diao_mp("LiningDiaozemin_MP");

  // Wind
  static Register<stochastic::StochasticModel, stochastic::WittigSinha,
                  std::string, double, double, unsigned int, double>
      wittig_sinha_equal_floors("WittigSinhaDiscreteFreqWind");
  static Register<stochastic::StochasticModel, stochastic::WittigSinha,
                  std::string, double, double, unsigned int, double, int>
      wittig_sinha_equal_floors_seed("WittigSinhaDiscreteFreqWind");
  static Register<stochastic::StochasticModel, stochastic::WittigSinha,
                  std::string, double, const std::vector<double>&,
                  const std::vector<double>&, const std::vector<double>&,
                  double>
      wittig_sinha_unequal_floors("WittigSinhaDiscreteFreqWind");
  static Register<stochastic::StochasticModel, stochastic::WittigSinha,
                  std::string, double, const std::vector<double>&,
                  const std::vector<double>&, const std::vector<double>&,
                  double, int>
      wittig_sinha_unequal_floors_seed("WittigSinhaDiscreteFreqWind");

  // WINDOW FUNCTIONS
  // Register Hann window
  static DispatchRegister<Eigen::VectorXd, unsigned int> hann_window_function(
      "HannWindow", signal_processing::hann_window);

  // FILTER FUNCTIONS
  // Register highpass Butterworth filter
  static DispatchRegister<std::vector<std::vector<double>>, int, double>
      hp_butterworth_function("HighPassButter",
                              signal_processing::hp_butterworth());

  // Register filter impulse response
  static DispatchRegister<std::vector<double>, std::vector<double>,
                          std::vector<double>, int, int>
      filter_impulse_response("ImpulseResponse",
                              signal_processing::impulse_response());

  // Register acausal highpass Butterwork filter
  static DispatchRegister<std::vector<double>, double, double, unsigned int,
                          unsigned int>
      acausal_highpass_filter("AcausalHighpassButterworth",
                              signal_processing::acausal_highpass_filter());

  // WIND VELOCITY PROFILES
  // Exposure category-based velocity profile using power law
  static DispatchRegister<double, const std::string&,
                          const std::vector<double>&, double, double,
                          std::vector<double>&>
      exposure_category_vel("ExposureCategoryVel",
                            wind::exposure_category_velocity());
}