#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


// 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_


//
//  LinSysSolver.hpp
//  OptCuts
//
//  Created by Minchen Li on 6/30/18.
//

#ifndef LinSysSolver_hpp
#define LinSysSolver_hpp

#include "Types.hpp"

#include <Eigen/Eigen>
#include <Eigen/Sparse>

#include <set>
#include <map>
#include <iostream>

namespace OptCuts {

    template <typename vectorTypeI, typename vectorTypeS>
    class LinSysSolver
    {
    protected:
        int numRows;
        Eigen::VectorXi ia, ja;
        std::vector<std::map<int, int>> IJ2aI;
        Eigen::VectorXd a;

    public:
        virtual ~LinSysSolver(void) {};

    public:
        virtual void set_type(int threadAmt, int _mtype, bool is_upper_half = false) = 0;

        virtual void set_pattern(const std::vector<std::set<int>>& vNeighbor,
                                 const std::set<int>& fixedVert)
        {
            numRows = static_cast<int>(vNeighbor.size()) * DIM;
            ia.resize(vNeighbor.size() * DIM + 1);
            ia[0] = 1; // 1 + nnz above row i
            ja.resize(0); // colI of each element
            IJ2aI.resize(0); // map from matrix index to ja index
            IJ2aI.resize(vNeighbor.size() * DIM);
            for(int rowI = 0; rowI < vNeighbor.size(); rowI++) {
                if(fixedVert.find(rowI) == fixedVert.end()) {
                    int oldSize_ja = static_cast<int>(ja.size());
                    IJ2aI[rowI * DIM][rowI * DIM] = oldSize_ja;
                    IJ2aI[rowI * DIM][rowI * DIM + 1] = oldSize_ja + 1;
                    if(DIM == 3) {
                        IJ2aI[rowI * DIM][rowI * DIM + 2] = oldSize_ja + 2;
                    }
                    ja.conservativeResize(oldSize_ja + DIM);
                    ja[oldSize_ja] = rowI * DIM + 1;
                    ja[oldSize_ja + 1] = rowI * DIM + 2;
                    if(DIM == 3) {
                        ja[oldSize_ja + 2] = rowI * DIM + 3;
                    }

                    int nnz_rowI = 1;
                    for(const auto& colI : vNeighbor[rowI]) {
                        if(fixedVert.find(colI) == fixedVert.end()) {
                            if(colI > rowI) {
                                // only the lower-left part
                                // colI > rowI means upper-right, but we are preparing CSR here
                                // in a row-major manner and CHOLMOD is actually column-major
                                int oldSize_ja_temp = static_cast<int>(ja.size());
                                IJ2aI[rowI * DIM][colI * DIM] = oldSize_ja_temp;
                                IJ2aI[rowI * DIM][colI * DIM + 1] = oldSize_ja_temp + 1;
                                if(DIM == 3) {
                                    IJ2aI[rowI * DIM][colI * DIM + 2] = oldSize_ja_temp + 2;
                                }
                                ja.conservativeResize(oldSize_ja_temp + DIM);
                                ja[oldSize_ja_temp] = colI * DIM + 1;
                                ja[oldSize_ja_temp + 1] = colI * DIM + 2;
                                if(DIM == 3) {
                                    ja[oldSize_ja_temp + 2] = colI * DIM + 3;
                                }
                                nnz_rowI++;
                            }
                        }
                    }

                    // another row for y,
                    // excluding the left-bottom entry on the diagonal band
                    IJ2aI[rowI * DIM + 1] = IJ2aI[rowI * DIM];
                    for(auto& IJ2aI_newRow : IJ2aI[rowI * DIM + 1]) {
                        IJ2aI_newRow.second += nnz_rowI * DIM - 1;
                    }
                    ja.conservativeResize(ja.size() + nnz_rowI * DIM - 1);
                    ja.bottomRows(nnz_rowI * DIM - 1) = ja.block(oldSize_ja + 1, 0, nnz_rowI * DIM - 1, 1);

                    if(DIM == 3) {
                        // third row for z
                        IJ2aI[rowI * DIM + 2] = IJ2aI[rowI * DIM + 1];
                        for(auto& IJ2aI_newRow : IJ2aI[rowI * DIM + 2]) {
                            IJ2aI_newRow.second += nnz_rowI * DIM - 2;
                        }
                        ja.conservativeResize(ja.size() + nnz_rowI * DIM - 2);
                        ja.bottomRows(nnz_rowI * DIM - 2) = ja.block(oldSize_ja + 2, 0, nnz_rowI * DIM - 2, 1);

                        IJ2aI[rowI * DIM + 2].erase(rowI * DIM);
                        IJ2aI[rowI * DIM + 2].erase(rowI * DIM + 1);
                    }
                    IJ2aI[rowI * DIM + 1].erase(rowI * DIM);

                    ia[rowI * DIM + 1] = ia[rowI * DIM] + nnz_rowI * DIM;
                    ia[rowI * DIM + 2] = ia[rowI * DIM + 1] + nnz_rowI * DIM - 1;
                    if(DIM == 3) {
                        ia[rowI * DIM + 3] = ia[rowI * DIM + 2] + nnz_rowI * DIM - 2;
                    }
                }
                else {
                    int oldSize_ja = static_cast<int>(ja.size());
                    IJ2aI[rowI * DIM][rowI * DIM] = oldSize_ja;
                    IJ2aI[rowI * DIM + 1][rowI * DIM + 1] = oldSize_ja + 1;
                    if(DIM == 3) {
                        IJ2aI[rowI * DIM + 2][rowI * DIM + 2] = oldSize_ja + 2;
                    }
                    ja.conservativeResize(oldSize_ja + DIM);
                    ja[oldSize_ja] = rowI * DIM + 1;
                    ja[oldSize_ja + 1] = rowI * DIM + 2;
                    if(DIM == 3) {
                        ja[oldSize_ja + 2] = rowI * DIM + 3;
                    }
                    ia[rowI * DIM + 1] = ia[rowI * DIM] + 1;
                    ia[rowI * DIM + 2] = ia[rowI * DIM + 1] + 1;
                    if(DIM == 3) {
                        ia[rowI * DIM + 3] = ia[rowI * DIM + 2] + 1;
                    }
                }
            }
            a.resize(ja.size());
        }
        virtual void set_pattern(const Eigen::SparseMatrix<double>& mtr) = 0; //NOTE: mtr must be SPD

        virtual void update_a(const vectorTypeI &II,
                              const vectorTypeI &JJ,
                              const vectorTypeS &SS)
        {
            //TODO: faster O(1) indices!!

            assert(II.size() == JJ.size());
            assert(II.size() == SS.size());

            a.setZero(ja.size());
            for(int tripletI = 0; tripletI < II.size(); tripletI++) {
                int i = II[tripletI], j = JJ[tripletI];
                if(i <= j) {
                    //        if((i <= j) && (i != 2) && (j != 2)) {
                    assert(i < IJ2aI.size());
                    const auto finder = IJ2aI[i].find(j);
                    assert(finder != IJ2aI[i].end());
                    a[finder->second] += SS[tripletI];
                }
            }
            //    a[IJ2aI[2].find(2)->second] = 1.0;
        }
        virtual void update_a(const Eigen::SparseMatrix<double>& mtr)
        {
            assert(0 && "please implement in subclass!");
        }

        virtual void analyze_pattern(void) = 0;

        virtual bool factorize(void) = 0;

        virtual void solve(Eigen::VectorXd &rhs,
                           Eigen::VectorXd &result) = 0;

        virtual void multiply(const Eigen::VectorXd& x,
                              Eigen::VectorXd& Ax)
        {
            assert(x.size() == numRows);
            assert(IJ2aI.size() == numRows);

            Ax.setZero(numRows);
            for(int rowI = 0; rowI < numRows; ++rowI) {
                for(const auto& colI : IJ2aI[rowI]) {
                    Ax[rowI] += colI.second * x[colI.first];
                    if(rowI != colI.first) {
                        Ax[colI.first] += colI.second * x[rowI];
                    }
                }
            }
        }

    public:
        virtual double coeffMtr(int rowI, int colI) const {
            if(rowI > colI) {
                // return only upper right part for symmetric matrix
                int temp = rowI;
                rowI = colI;
                colI = temp;
            }
            assert(rowI < IJ2aI.size());
            const auto finder = IJ2aI[rowI].find(colI);
            if(finder != IJ2aI[rowI].end()) {
                return a[finder->second];
            }
            else {
                return 0.0;
            }
        }
        virtual void getCoeffMtr(Eigen::SparseMatrix<double>& mtr) const {
            mtr.resize(numRows, numRows);
            mtr.setZero();
            mtr.reserve(a.size() * 2 - numRows);
            for(int rowI = 0; rowI < numRows; rowI++) {
                for(const auto& colIter : IJ2aI[rowI]) {
                    mtr.insert(rowI, colIter.first) = a[colIter.second];
                    if(rowI != colIter.first) {
                        mtr.insert(colIter.first, rowI) = a[colIter.second];
                    }
                }
            }
        }
        virtual void setCoeff(int rowI, int colI, double val) {
            //TODO: faster O(1) indices!!

            if(rowI <= colI) {
                assert(rowI < IJ2aI.size());
                const auto finder = IJ2aI[rowI].find(colI);
                assert(finder != IJ2aI[rowI].end());
                a[finder->second] = val;
            }
        }
        virtual void setZero(void) {
            a.setZero();
        }
        virtual void addCoeff(int rowI, int colI, double val) {
            //TODO: faster O(1) indices!!

            if(rowI <= colI) {
                assert(rowI < IJ2aI.size());
                const auto finder = IJ2aI[rowI].find(colI);
                assert(finder != IJ2aI[rowI].end());
                a[finder->second] += val;
            }
        }

        virtual int getNumRows(void) const {
            return numRows;
        }
        virtual int getNumNonzeros(void) const {
            return a.size();
        }
        virtual const std::vector<std::map<int, int>>& getIJ2aI(void) const {
            return IJ2aI;
        }
        virtual Eigen::VectorXi& get_ia(void) { return ia; }
        virtual Eigen::VectorXi& get_ja(void) { return ja; }
        virtual Eigen::VectorXd& get_a(void) { return a; }
        virtual const Eigen::VectorXd& get_a(void) const { return a; }
    };

}

#endif /* LinSysSolver_hpp */


// Copyright (c) 2017 University of Minnesota
//
// MCLSCENE Uses the BSD 2-Clause License (http://www.opensource.org/licenses/BSD-2-Clause)
// Redistribution and use in source and binary forms, with or without modification, are
// permitted provided that the following conditions are met:
// 1. Redistributions of source code must retain the above copyright notice, this list of
//    conditions and the following disclaimer.
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
//    of conditions and the following disclaimer in the documentation and/or other materials
//    provided with the distribution.
// THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR  A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE UNIVERSITY OF MINNESOTA, DULUTH OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
// OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
// IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
// OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
// By Matt Overby (http://www.mattoverby.net)

#ifndef MCL_VEC3_H
#define MCL_VEC3_H 1

// Helper to hush api warnings
#define MCL_UNUSED(x) (void)(x)

#include <Eigen/Geometry>

namespace mcl {

	// Common types that I don't feel like typing out all the time:
	template <typename T> using Vec4 = Eigen::Matrix<T,4,1>;
	template <typename T> using Vec3 = Eigen::Matrix<T,3,1>;
	template <typename T> using Vec2 = Eigen::Matrix<T,2,1>;
	typedef Vec4<float> Vec4f;
	typedef Vec3<float> Vec3f;
	typedef Vec2<float> Vec2f;
	typedef Vec4<double> Vec4d;
	typedef Vec3<double> Vec3d;
	typedef Vec2<double> Vec2d;
	typedef Vec4<int> Vec4i;
	typedef Vec3<int> Vec3i;
	typedef Vec2<int> Vec2i;

namespace vec {

	template <typename T> // Returns a normalized vector
	static inline Vec3<T> normalized(const Vec3<T> &v){ Vec3<T> t=v; t.normalize(); return t; }

	template <typename T, size_t D> // Vec output as transpose with spaces instead of tabs
	static inline std::string to_str(const Eigen::Matrix<T,D,1> &v){
		std::stringstream ss; ss << v[0];
		for( size_t i=1; i<D; ++i ){ ss << ' ' << v[i]; }
		return ss.str();
	}

	template <typename T> // Compute barycentric coords for a point on a triangle
	static inline Vec3<T> barycoords(const Vec3<T> &p, const Vec3<T> &p0, const Vec3<T> &p1, const Vec3<T> &p2){
		Vec3<T> v0 = p1 - p0, v1 = p2 - p0, v2 = p - p0;
		T d00 = v0.dot(v0);
		T d01 = v0.dot(v1);
		T d11 = v1.dot(v1);
		T d20 = v2.dot(v0);
		T d21 = v2.dot(v1);
		T invDenom = 1.0 / (d00 * d11 - d01 * d01);
		Vec3<T> r;
		r[1] = (d11 * d20 - d01 * d21) * invDenom;
		r[2] = (d00 * d21 - d01 * d20) * invDenom;
		r[0] = 1.0 - r[1] - r[2];
		return r;
	}

	template <typename T> // scalar triple product
	static inline T scalar_triple_product( const Vec3<T> &u, const Vec3<T> &v, const Vec3<T> &w ){ return u.dot(v.cross(w)); }

	template <typename T> // Compute barycentric coords for a point in a tet
	static inline Vec4<T> barycoords(const Vec3<T> &p, const Vec3<T> &a, const Vec3<T> &b, const Vec3<T> &c, const Vec3<T> &d){
		Vec3<T> vap = p - a;
		Vec3<T> vbp = p - b;
		Vec3<T> vab = b - a;
		Vec3<T> vac = c - a;
		Vec3<T> vad = d - a;
		Vec3<T> vbc = c - b;
		Vec3<T> vbd = d - b;
		T va6 = scalar_triple_product(vbp, vbd, vbc);
		T vb6 = scalar_triple_product(vap, vac, vad);
		T vc6 = scalar_triple_product(vap, vad, vab);
		T vd6 = scalar_triple_product(vap, vab, vac);
		T v6 = 1.0 / scalar_triple_product(vab, vac, vad);
		return Vec4<T>(va6*v6, vb6*v6, vc6*v6, vd6*v6);
	}

	template <typename T> // Spherical coords to cartesian
	static inline Vec3<T> spherical_to_cartesian(T theta, T phi){
		T sin_t = std::sin(theta); T cos_t = std::cos(theta);
		T sin_p = std::sin(phi); T cos_p = std::cos(phi);
		return Vec3<T>( sin_t * sin_p, sin_t * cos_p, cos_t );
	}

	template <typename T>  // Cartesian coords to spherical
	static inline Vec2<T> cartesian_to_spherical(const Vec3<T> &v){
		Vec2<T> r( std::acos(v[2]), std::atan2(v[1], v[0]) );
		if(r[1] < 0){ r[1] += 2*M_PI; }
		return r;
	}

} // end namespace vec

// Randoms (u1, u2, etc...): 0 to 1
// Putting it here until I find a better spot
namespace sample {

	template<typename T> // Uniformly samples a cone (e.g. spotlight)
	static inline Vec3<T> uniform_cone( T u1, T u2, T max_theta ){
		T cos_theta = (1 - u1) + u1 * std::cos(max_theta);
		T sin_theta = std::sqrt(1 - cos_theta*cos_theta);
		T phi = u2 * 2 * M_PI;
		return Vec3<T>( std::cos(phi)*sin_theta, std::sin(phi)*sin_theta, cos_theta );
	}

	template<typename T> // Cosine weighted hemisphere sampling (e.g. diffuse reflection)
	static inline Vec3<T> cosine_hemisphere( T u1, T u2 ){
		T r = std::sqrt( u1 );
		T theta = 2 * M_PI * u2;
		return Vec3<T>( r * std::cos(theta), r * std::sin(theta), std::sqrt( std::max(T(0), T(1.f-u1)) ) );
	}

}; // end namespace sample

} // end namespace mcl

/*
//
//	trimesh and mcl::Vec xforms:
//
namespace trimesh {

	template <typename T, typename U>
	static inline mcl::Vec3<T> operator*(const trimesh::XForm<U> &m, const mcl::Vec3<T> &v){
		mcl::Vec3<T> r;
		r[0] = m[0]*v[0]+m[4]*v[1]+m[8]*v[2]+m[12];
		r[1] = m[1]*v[0]+m[5]*v[1]+m[9]*v[2]+m[13];
		r[2] = m[2]*v[0]+m[6]*v[1]+m[10]*v[2]+m[14];
		return r;
	}

	template <typename T, typename U>
	static inline mcl::Vec4<T> operator*(const trimesh::XForm<U> &m, const mcl::Vec4<T> &v){
		mcl::Vec4<T> r;
		r[0] = m[0]*v[0]+m[4]*v[1]+m[8]*v[2]+m[12]*v[3];
		r[1] = m[1]*v[0]+m[5]*v[1]+m[9]*v[2]+m[13]*v[3];
		r[2] = m[2]*v[0]+m[6]*v[1]+m[10]*v[2]+m[14]*v[3];
		r[3] = m[3]*v[0]+m[7]*v[1]+m[11]*v[2]+m[15]*v[3];
		return r;
	}

} // end namespace trimesh
*/

#endif