#############################################################################
##
##  This file is part of GAP, a system for computational discrete algebra.
##  This file's authors include Thomas Breuer.
##
##  Copyright of GAP belongs to its developers, whose names are too numerous
##  to list here. Please refer to the COPYRIGHT file for details.
##
##  SPDX-License-Identifier: GPL-2.0-or-later
##
##  This file contains methods for Molien series.
##


#############################################################################
##
#F  StringOfUnivariateRationalPolynomialByCoefficients( <coeffs>, <nam> )
##
#T maybe we need more flexible ways to influence how an object is printed or
#T how its string looks like;
#T in this case, I want to influence how the indeterminate is printed.
##
BindGlobal( "StringOfUnivariateRationalPolynomialByCoefficients",
    function( coeffs, nam )
    local string, i;

    string:= "";
    for i in [ 1 .. Length( coeffs ) ] do
      if coeffs[i] <> 0 then
        if   coeffs[i] > 0 then
          if not IsEmpty( string ) then
            Append( string, "+" );
          fi;
          if coeffs[i] <> 1 then
            Append( string, String( coeffs[i] ) );
            if i <> 1 then
              Append( string, "*" );
            fi;
          elif i = 1 then
            Append( string, "1" );
          fi;
        elif coeffs[i] < 0 then
          if coeffs[i] <> -1 then
            Append( string, String( coeffs[i] ) );
            if i <> 1 then
              Append( string, "*" );
            fi;
          elif i = 1 then
            Append( string, "-1" );
          else
            Append( string, "-" );
          fi;
        fi;
        if i <> 1 then
          Append( string, nam );
        fi;
        if i > 2 then
          Append( string, "^" );
          Append( string, String( i-1 ) );
        fi;

      fi;

    od;

    if IsEmpty( string ) then
      string:= "0";
    fi;
    ConvertToStringRep( string );

    # Return the string.
    return string;
end );


#############################################################################
##
#F  CoefficientTaylorSeries( <numer>, <r>, <k>, <i> )
##
##  We have
##  $$
##     \frac{1}{( 1 - x )^k} =
##        \frac{1}{(k-1)!} \frac{d^{k-1}}{dx^{k-1}} \frac{1}{1-x}
##     \mbox{\rm\ where\ }
##     \frac{1}{1 - x} = \sum_{j=0}^{\infty} x^j .
##  $$
##  Thus we get
##  $$
##     \frac{c_i z^i}{( 1 - z^r )^k} =
##     \sum_{j=0}^{\infty} c_i \frac{(j+k-1)!}{(k-1)! j!} z^{r j + i}.
##  $$
##
##  For $p(z) = \sum_{i=0}^m c_i z^i$ where $m = u r + n$ with $0\leq n \< r$
##  we have
##  $$
##     \frac{p(z)}{( 1 - z^r )^k} =
##        \frac{1}{(k-1)!}\sum_{i=0}^m\sum_{j=0}^{\infty}
##            c_i \frac{(j+k-1)!}{(k-1)! j!} z^{r j + i} .
##  $$
##
##  The coefficient of $z^l$ with $l = g r + v$, $0\leq v \< r$ is
##  $$
##     \sum_{j=0}^{\min\{g,u\}} c_{j r + v}
##                 \prod_{\mu=1}^{k-1} \frac{g-j+\mu}{\mu} .
##  $$
##
InstallGlobalFunction( CoefficientTaylorSeries, function( numer, r, k, l )
    local i, m, u, v, g, coeff, lower, summand, mu;

    m:= Length( numer ) - 1;
    u:= Int( m / r );
    v:= l mod r;
    g:= Int( l / r );

    coeff:= 0;

    # lower bound for the summation
    if g < u then
      lower:= u-g;
    else
      lower:= 0;
    fi;

    for i in [ lower .. u ] do

      if (u-i)*r + v <= m then

        summand:= numer[ (u-i)*r + v + 1 ];
        for mu in [ 1 .. k-1 ] do
          summand:= summand * ( i - u + g + mu ) / mu;
        od;
        coeff:= coeff + summand;

      fi;

    od;

    return coeff;
end );


#############################################################################
##
#F  SummandMolienSeries( <tbl>, <psi>, <chi>, <i> )
##
InstallGlobalFunction( SummandMolienSeries, function( tbl, psi, chi, i )
    local x,          # indeterminate
          numer,      # numerator in summands corresp. to `i'-th class
          a,          # multiplicities of cycl. pol. in the denominator
          ev,         # eigenvalues of `psi' at class `i'
          n,          # element order of class `i'
          e,          # `E(n)'
          div,        # divisors of `n'
          d,          # loop over `div'
          roots,      # exponents of `d'-th prim. roots
          r;          # loop over `roots'

    x:= Indeterminate( Cyclotomics );

    if chi[i] = 0 then
      numer := Zero(x);
      a     := [ 1, 1 ];
    else

      ev := EigenvaluesChar( tbl, psi, i );
      n  := Length( ev );
      e  := E(n);

      # numerator of summands corresponding to `i'-th class
      numer:= chi[i] * e ^ Sum( [ 1 .. n ], j -> j * ev[j] ) * One( x );

      div:= ShallowCopy( DivisorsInt( n ) );
      RemoveSet( div, 1 );
      a:= List( [ 1 .. n ], x -> 0 );
      a[1]:= ev[n];

      for d in div do

        # compute $a_d$, that is, the maximal multiplicity of `ev[k]'
        # for all `k' with $\gcd(n,k) = n / d$.
        roots:= ( n / d ) * PrimeResidues( d );
        a[d]:= Maximum( ev{ roots } );
        for r in roots do
          if a[d] <> ev[r] then
            numer:= numer * ( x - e ^ r ) ^ ( a[d] - ev[r] );
          fi;
        od;

      od;

    fi;

    return rec( numer := numer,
                a     := a );
end );


#############################################################################
##
#F  MolienSeries( <psi> )
#F  MolienSeries( <psi>, <chi> )
#F  MolienSeries( <tbl>, <psi> )
#F  MolienSeries( <tbl>, <psi>, <chi> )
##
InstallGlobalFunction( MolienSeries, function( arg )
    local tbl,          # character table, first argument
          psi,          # character of `tbl', second argument
          chi,          # character of `tbl', optional third argument
          numers,       # list of numerators   of sum of polynomial quotients
          denoms,       # list of denominators of sum of polynomial quotients
          R,
          x,            # indeterminate
          tblclasses,   # class lengths of `tbl'
          orders,       # representative orders of `tbl'
          classes,      # list of classes of `tbl' that are not yet used
          sub,          # classes that belong to one cyclic subgroup
          i,            # represenative of `sub'
          n,            # element order of class `i'
          summand,      #
          numer,        # numerator in summands corresp. to `i'-th class
          div,          # divisors of `n'
          a,            # multiplicities of cycl. pol. in the denominator
          d,            # loop over `div'
          r,            # loop over `roots'
          f,            # `CF( n )'
          special,      # parameters of special factor in the denominator
          dd,           # loop over divisors of `d'
          p,            #
          q,            #
          j,            #
          F,            #
          pol,          #
          qr,           #
          num,          #
          pos,          #
          denpos,       #
          repr,         #
          series,       # Molien series, result
          denom,        # smallest common denominator for the summands
          denomstring,  # string of `denom', in factored form
          c,            # coefficients & valuation
          numerstring,  # string of `numer'
          denominfo,    # list of pairs `[ r, k ]' in the denominator
          rkpairs,      # list of pairs of the form `[ r, k ]'
          rr,           # `r' value of the current summand
          kk,           # `k' value of the current summand
          sumnumer,     # numerator of the current summand
          pair,         # loop over `rkpairs'
          min;          # minimum of `kk' and `k' value of the current pair

    # Check and get the arguments.
    if   Length( arg ) = 1 and IsClassFunction( arg[1] ) then
      tbl:= UnderlyingCharacterTable( arg[1] );
      psi:= ValuesOfClassFunction( arg[1] );
      chi:= List( psi, x -> 1 );
    elif Length( arg ) = 2 and IsClassFunction( arg[1] )
                           and IsClassFunction( arg[2] ) then
      tbl:= UnderlyingCharacterTable( arg[1] );
      psi:= ValuesOfClassFunction( arg[1] );
      chi:= ValuesOfClassFunction( arg[2] );
    elif Length( arg ) = 2 and IsOrdinaryTable( arg[1] )
                           and IsHomogeneousList( arg[2] ) then
      tbl:= arg[1];
      psi:= arg[2];
      chi:= List( psi, x -> 1 );
    elif Length( arg ) = 3 and IsOrdinaryTable( arg[1] )
                           and IsList( arg[2] )
                           and IsList( arg[3] ) then
      tbl:= arg[1];
      psi:= arg[2];
      chi:= arg[3];
    else
      Error( "usage: MolienSeries( [<tbl>, ]<psi>[, <chi>] )" );
    fi;

    # Initialize lists of numerators and denominators
    # of summands of the form $p_j(z) / (z^r-1)^k$.
    # In `numers[ <j> ]' the coefficients list of $p_j(z)$ is stored,
    # in `denoms[ <j> ]' the pair `[ r, k ]'.
    # `pol' is an additive polynomial.
    numers:= [];
    denoms:= [];
    R:= UnivariatePolynomialRing(Rationals);
    x:= R.1;
    pol:= Zero( x );

    tblclasses:= SizesConjugacyClasses( tbl );
    classes:= [ 1 .. Length( tblclasses ) ];
    orders:= OrdersClassRepresentatives( tbl );

    # Take the cyclic subgroups of `tbl'.
    while not IsEmpty( classes ) do

      # Compute the next cyclic subgroup,
      # remove the classes of the cyclic subgroup,
      # take a representative.
      sub:= ClassOrbit( tbl, classes[1] );
      SubtractSet( classes, sub );
      i:= sub[1];

      # Compute $v(g) = \frac{\chi(g) \det(D(g))}{\det(z I - D(g))}$
      # for $g$ in class `i'.

      # This is encoded as record with components `numer' and `a'
      # where `a[r]' means the multiplicity of the `r'-th cyclotomic
      # polynomial in the denominator.
      summand:= SummandMolienSeries( tbl, psi, chi, i );

      # Omit summands with zero numerator.
      if not IsZero( summand.numer ) then

        numer:= CoefficientsOfLaurentPolynomial( summand.numer );
        a:= summand.a;

        # Compute the sum over class representatives of the cyclic
        # subgroup containing $g$, i.e., the relative trace of $v(g)$.
        n:= orders[i];
        f:= CF( n );
        numer:= List( ShiftedCoeffs( numer[1], numer[2] ),
                      y -> Trace( f, y ) )
                * ( Length( sub ) / Phi(n) );
        numer:= UnivariatePolynomial( Rationals, numer, 1 );

        # Try to reduce the number of factors in the denominator
        # by forming one factor of the form $(z^r - 1)^k$.
        # But we still want to guarantee that the factors are pairwise
        # coprime, that is, the exponents of all involved cyclotomic
        # polynomials must be equal.

        special:= false;

        if a[1] > 0 then

          # There is such a ``special\'\' factor.

          div:= DivisorsInt( n );
          for d in Reversed( div ) do

            if a[1] <> 0 and ForAll( DivisorsInt(d), y -> a[y] = a[1] ) then

              # The special factor is $( z^d - 1 ) ^ a[d]$.
              special:= [ d, a[d] ];
              for dd in DivisorsInt( d ) do
                a[dd]:= 0;
              od;

            fi;

          od;

        fi;

        # Compute the product of the remaining factors in the denominator.
        F:= One( x );
        for j in [ 1 .. n ] do
          if a[j] <> 0 then
            F:= F * CyclotomicPolynomial( Rationals, j ) ^ a[j];
          fi;
        od;

        if special <> false then

          # Split the summand into two summands, with denominators
          # the special factor `f' resp. the remaining factors `F'.
          f:= ( x ^ special[1] - 1 ) ^ special[2];
          repr:= GcdRepresentation( R, F, f );

          # Reduce the numerators if possible.
          num:= numer * repr[1];
          if special[1] * special[2]
             < DegreeOfLaurentPolynomial( num ) then
            qr:= QuotientRemainder( num, f );
            pol:= pol + tblclasses[i] * qr[1];
            num:= qr[2];
          fi;

          # Store the summand.
          denpos:= Position( denoms, special, 0 );
          if denpos = fail then
            Add( denoms, special );
            Add( numers, tblclasses[i] * num );
          else
            numers[ denpos ]:= numers[ denpos ] + tblclasses[i] * num;
          fi;

          # The remaining term is `numer \* repr[2] / F'.
          numer:= numer * repr[2];

        fi;

        # Split the quotient into a sum of quotients
        # whose denominators are cyclotomic polynomials.

        # We have $1 / \prod_{i=1}^k f_i = \sum_{i=1}^k p_i / f_i$
        # if the $f_i$ are pairwise coprime,
        # where the polynomials $p_i$ are computed by
        # $r_i \prod_{j>i} f_j + q_i f_i = 1$ for $1 \leq i \leq k-1$,
        # $r_k = 1$, and $p_i = r_i \prod_{j=1}^{i-1} q_j$.

        # In the end we have a sum of quotients with denominator of the
        # form $(z^r-1)^k$.  We store the pair $[ r, k ]$ in the list
        # `denoms', and $(-1)^k$ times the numerator in the list `numers'.

        pos:= 1;
        q:= 1;

        while pos <= n do

          if a[ pos ] <> 0 then

            # $f_i$ is the next factor encoded in `a'.
            f:= CyclotomicPolynomial( Rationals, pos ) ^ a[ pos ];
            F:= F / f;

            # $\prod_{j>i} f_j$ is stored in `F', and $f_i$ is in `f'.

            # at first position $r_i$, at second position $q_i$
            repr:= GcdRepresentation( R, F, f );

            # The numerator $p_i$.
            p:= q * repr[1];
            q:= q * repr[2];

            # We blow up the denominator $f_i$, and encode the summands.
            dd:= ShallowCopy( DivisorsInt( pos ) );
            RemoveSet( dd, pos );
            for r in dd do
              p:= p * CyclotomicPolynomial( Rationals, r ) ^ a[ pos ];
            od;

            # Reduce the numerators if possible.
            num:= numer * p;
            if DegreeOfLaurentPolynomial( num )
               > pos * a[ pos ] then
              qr:= QuotientRemainder( num, (x^pos - 1)^a[pos] );
              pol:= pol + tblclasses[i] * qr[1];
              num:= qr[2];
            fi;

            # Store the summand.
            denpos:= Position( denoms, [ pos, a[ pos ] ], 0 );
            if denpos = fail then
              Add( denoms, [ pos, a[ pos ] ] );
              Add( numers, tblclasses[i] * num );
            else
              numers[ denpos ]:= numers[ denpos ] + tblclasses[i] * num;
            fi;

          fi;

          pos:= pos + 1;

        od;

      fi;

    od;

    # Now compute the Taylor series for each summand.
    for i in [ 1 .. Length( numers ) ] do
      num:= CoefficientsOfLaurentPolynomial( numers[i] );
      num:= ShiftedCoeffs( num[1], num[2] );
      if IsEmpty( num ) then
        Unbind( numers[i] );
      else
        numers[i]:= rec( numer := num,
                         r     := denoms[i][1],
                         k     := denoms[i][2] );

        # Replace denominators $(z^r - 1)^k$ by $(1 - z^r)^k$.
        if numers[i].k mod 2 = 1 then
          numers[i].numer:= AdditiveInverse( numers[i].numer );
        fi;
      fi;
    od;

    numers:= Compacted( numers );

    # Sort the summands according to descending `r' component,
    # and for the same `r', according to descending `k'.
    Sort( numers, function( x, y )
                    return x.r > y.r or ( x.r = y.r and x.k > y.k );
                  end );

    pol:= CoefficientsOfLaurentPolynomial( pol );
    pol:= ShiftedCoeffs( pol[1], pol[2] );

    # Compute the display string.
    # First translate the sum of fractions into a single fraction.
    numer:= Zero( x );
    denom:= One( x );
    denomstring:= "";
    denominfo:= [];
    rkpairs:= [];

    for summand in numers do

      rr:= summand.r;
      kk:= summand.k;
      sumnumer:= UnivariatePolynomial( Rationals, summand.numer )
                 * denom;
      for pair in rkpairs do
        if kk <> 0 and pair[1] mod rr = 0 then
          min:= Minimum( kk, pair[2] );
          sumnumer:= sumnumer / ( 1 - x^rr )^min;
          kk:= kk - min;
        fi;
      od;
      if kk <> 0 then
        # Blow up the common denominator.
        numer:= numer * ( 1 - x^rr )^kk;
        denom:= denom * ( 1 - x^rr )^kk;
        Add( rkpairs, [ rr, kk ] );
        Append( denomstring, "(1-z" );
        if 1 < rr then
          Add( denomstring, '^' );
          Append( denomstring, String(rr) );
        fi;
        Add( denomstring, ')' );
        if 1 < kk then
          Add( denomstring, '^' );
          Append( denomstring, String(kk) );
        fi;
        Add( denomstring, '*' );
        Append( denominfo, [ rr, kk ] );
      fi;
      numer:= numer + sumnumer;
    od;
    if not IsEmpty( pol ) then
      numer:= numer + denom * UnivariatePolynomial( Rationals, pol );
    fi;
    numer:= numer / Size( tbl );
    if psi[1] mod 2 = 1 then
      numer:= - numer;
    fi;
    denomstring:= denomstring{ [ 1 .. Length(denomstring)-1] };
    ConvertToStringRep( denomstring );

    c:= CoefficientsOfLaurentPolynomial( numer );
    numerstring:= StringOfUnivariateRationalPolynomialByCoefficients(
        Concatenation( ListWithIdenticalEntries( c[2], 0 ), c[1] ), "z" );

    # Compute the series.
    series:= numer / denom;
#T avoid forming this quotient!
    SetIsUnivariateRationalFunction( series, true );

    # Set the info record.
    SetMolienSeriesInfo( series,
                         rec( summands    := numers,
                              size        := Size( tbl ),
                              degree      := psi[1],
                              pol         := pol,
                              numer       := numer,
                              denom       := denom,
                              denominfo   := denominfo,
                              numerstring := numerstring,
                              denomstring := denomstring,
                              ratfun      := series
                             ) );

    # Return the series.
    return series;
end );


#############################################################################
##
#F  MolienSeriesWithGivenDenominator( <molser>, <list> )
##
InstallGlobalFunction( MolienSeriesWithGivenDenominator,
    function( molser, list )
    local info,
          x,
          one,
          denom,
          pair,
          numer,
          c,
          numerstring,
          denomstring,
          rr, kk,
          coeffs,
          series;

    if not HasMolienSeriesInfo( molser ) then
      Error( "MolienSeriesInfo must be known for <molser>" );
    fi;
    info:= MolienSeriesInfo( molser );

    # Compute the numerator that belongs to the desired denominator.
    list:= Collected( list );
    x:= Indeterminate( Rationals );
    one:= One( x );
    denom:= one;
    for pair in list do
      denom:= denom * ( one - x^pair[1] )^pair[2];
    od;
    numer:= denom * info.numer / info.denom;
    if not IsUnivariatePolynomial( numer ) then
      return fail;
    fi;

    # Create the strings for numerator and denominator.
    c:= CoefficientsOfLaurentPolynomial( numer );
    numerstring:= StringOfUnivariateRationalPolynomialByCoefficients(
        Concatenation( ListWithIdenticalEntries( c[2], 0 ), c[1] ), "z" );

    denomstring:= "";
    for pair in Reversed( list ) do
      rr:= pair[1];
      kk:= pair[2];
      Append( denomstring, "(1-z" );
      if 1 < rr then
        Add( denomstring, '^' );
        Append( denomstring, String(rr) );
      fi;
      Add( denomstring, ')' );
      if 1 < kk then
        Add( denomstring, '^' );
        Append( denomstring, String(kk) );
      fi;
      Add( denomstring, '*' );
    od;
    denomstring:= denomstring{ [ 1 .. Length(denomstring)-1] };
    ConvertToStringRep( denomstring );

    # Create the Molien series object (create the rat. function
    # from the given one, without division).
    coeffs:= CoefficientsOfUnivariateRationalFunction( info.ratfun );
    series:= UnivariateRationalFunctionByExtRepNC( FamilyObj( info.ratfun ),
        coeffs[1], coeffs[2], coeffs[3],
        IndeterminateNumberOfUnivariateRationalFunction( info.ratfun ) );
    SetIsUnivariateRationalFunction( series, true );
#T why is this not automatically maintained?
    SetMolienSeriesInfo( series,
                         rec(
                              # We need not adjust these components
                              summands:= info.summands,
                              size:= info.size,
                              degree:= info.degree,
                              pol:= info.pol,

                              # These components are new.
                              ratfun:= series,
                              numer:= numer,
                              denom:= denom,
                              denominfo := Immutable( list ),
                              numerstring := numerstring,
                              denomstring := denomstring ) );

    # Return the new series.
    return series;
end );


#############################################################################
##
#M  ViewObj( <molser> ) . . . . . . . . . . . . . . . . . for a Molien series
#M  PrintObj( <molser> )  . . . . . . . . . . . . . . . . for a Molien series
##
BindGlobal( "ViewMolienSeries", function( molser )
    molser:= MolienSeriesInfo( molser );
    Print( "( ", molser.numerstring, " ) / ( ", molser.denomstring, " )" );
end );

InstallMethod( ViewObj,
    "for a Molien series",
    [ IsRationalFunction and IsUnivariateRationalFunction
      and HasMolienSeriesInfo ],
    ViewMolienSeries );

InstallMethod( PrintObj,
    "for a Molien series",
    [ IsRationalFunction and IsUnivariateRationalFunction
      and HasMolienSeriesInfo ],
    ViewMolienSeries );


#############################################################################
##
#F  ValueMolienSeries( series, i )
##
InstallGlobalFunction( ValueMolienSeries, function( series, i )
    local value;

    series:= MolienSeriesInfo( series );
    value:= Sum( series.summands,
                 s -> CoefficientTaylorSeries( s.numer, s.r, s.k, i ), 0 );
    if i+1 <= Length( series.pol ) then
      value:=value + series.pol[i+1];
    fi;

    # There is a factor $\frac{(-1)^{\psi(1)}}{\|G\|}$.
    if series.degree mod 2 = 1 then
      value:= AdditiveInverse( value );
    fi;

    return value / series.size;
end );
