#############################################################################
##
##  semigroups/semiffmat.gi
##  Copyright (C) 2015-2022                              James D. Mitchell
##                                                         Markus Pfeiffer
##
##  Licensing information can be found in the README file of this package.
##
#############################################################################
##

# Contents:
# 1. Isomorphisms for semigroups
# 2. Isomorphisms for monoids
# 3. Printing and viewing
# 4. Random
# 5. Methods for acting semigroup setup
# 6. Properties of matrix over finite field semigroups

#############################################################################
# 1. Isomorphisms for semigroups
#############################################################################

# fallback method: via a transformation semigroup

InstallMethod(IsomorphismSemigroup,
"for IsMatrixOverFiniteFieldSemigroup and a semigroup",
[IsMatrixOverFiniteFieldSemigroup, IsSemigroup],
SEMIGROUPS.DefaultIsomorphismSemigroup);

InstallMethod(IsomorphismSemigroup,
"for IsMatrixOverFiniteFieldSemigroup, a ring, and a semigroup",
[IsMatrixOverFiniteFieldSemigroup, IsRing, IsSemigroup],
function(filt, R, S)
  local iso1, inv1, iso2, inv2;

  if not (IsField(R) and IsFinite(R)) then
    ErrorNoReturn("the 2nd argument (a ring) must be a finite field");
  fi;

  iso1 := IsomorphismTransformationSemigroup(S);
  inv1 := InverseGeneralMapping(iso1);
  iso2 := IsomorphismSemigroup(filt, R, Range(iso1));
  inv2 := InverseGeneralMapping(iso2);

  return SemigroupIsomorphismByFunctionNC(S,
                                          Range(iso2),
                                          x -> (x ^ iso1) ^ iso2,
                                          x -> (x ^ inv2) ^ inv1);
end);

InstallMethod(IsomorphismSemigroup,
"for IsMatrixOverFiniteFieldSemigroup and a finite field matrix semigroup",
[IsMatrixOverFiniteFieldSemigroup, IsMatrixOverFiniteFieldSemigroup],
{filt, S} -> SemigroupIsomorphismByFunctionNC(S, S, IdFunc, IdFunc));

InstallMethod(IsomorphismSemigroup,
"for IsMatrixOverFiniteFieldSemigroup, ring, and matrix over ff semigroup",
[IsMatrixOverFiniteFieldSemigroup,
 IsRing,
 IsMatrixOverFiniteFieldSemigroup],
function(_, R, S)
  local D, map, inv, T;
  D := BaseDomain(Representative(S));
  if D = R then
    return SemigroupIsomorphismByFunctionNC(S, S, IdFunc, IdFunc);
  elif Size(D) <= Size(R) and IsIdenticalObj(FamilyObj(D), FamilyObj(R))
      and DegreeOverPrimeField(R) mod DegreeOverPrimeField(D) = 0 then
    map := x -> Matrix(R, x);
    inv := x -> Matrix(D, x);
    T   := Semigroup(List(GeneratorsOfSemigroup(S), map));
    return SemigroupIsomorphismByFunctionNC(S, T, map, inv);
  fi;
  TryNextMethod();  # take an isomorphism to a transformation semigroup
end);

# This is for converting semigroups of GAP library matrices over finite fields
# to IsMatrixOverFiniteFieldSemigroup
InstallMethod(IsomorphismSemigroup,
"for IsMatrixOverFiniteFieldSemigroup and a semigroup of matrices over a ff",
[IsMatrixOverFiniteFieldSemigroup,
 IsSemigroup and HasGeneratorsOfSemigroup and IsFFECollCollColl],
function(_, S)
  local R, map, T;
  # The following line is required because of the weirdness in constructor
  # method selection, if the method for IsMatrixOverFiniteFieldSemigroup was
  # after this method, then the next 3 lines wouldn't be required, but at the
  # same time that'd be less robust.
  if IsMatrixOverFiniteFieldSemigroup(S) then
    TryNextMethod();
  fi;
  R := DefaultFieldOfMatrix(Representative(S));
  map := x -> Matrix(R, x);
  T := Semigroup(List(GeneratorsOfSemigroup(S), map));
  return SemigroupIsomorphismByFunctionNC(S, T, map, AsList);
end);

InstallMethod(IsomorphismSemigroup,
"for IsMatrixOverFiniteFieldSemigroup, a ring, and a ff mat. semigroup",
[IsMatrixOverFiniteFieldSemigroup,
 IsRing,
 IsSemigroup and HasGeneratorsOfSemigroup and IsFFECollCollColl],
function(_, R, S)
  local D, map, T;
  # The following line is required because of the weirdness in constructor
  # method selection, if the method for IsMatrixOverFiniteFieldSemigroup was
  # after this method, then the next 3 lines wouldn't be required, but at the
  # same time that'd be less robust.
  if IsMatrixOverFiniteFieldSemigroup(S) then
    TryNextMethod();
  fi;
  D := BaseDomain(Representative(S));
  if Size(D) <= Size(R) and IsIdenticalObj(FamilyObj(D), FamilyObj(R))
      and DegreeOverPrimeField(R) mod DegreeOverPrimeField(D) = 0 then
    map := x -> Matrix(R, x);
    T := Semigroup(List(GeneratorsOfSemigroup(S), map));
    return SemigroupIsomorphismByFunctionNC(S, T, map, AsList);
  fi;
  TryNextMethod();
end);

InstallMethod(IsomorphismSemigroup,
"for IsMatrixOverFiniteFieldSemigroup and transformation semigroup with gens",
[IsMatrixOverFiniteFieldSemigroup,
 IsTransformationSemigroup and HasGeneratorsOfSemigroup],
 {filt, S} -> IsomorphismSemigroup(IsMatrixOverFiniteFieldSemigroup, GF(2), S));

InstallMethod(IsomorphismSemigroup,
"for IsMatrixOverFiniteFieldSemigroup, a ring, and a transformation semigroup",
[IsMatrixOverFiniteFieldSemigroup,
 IsRing,
 IsTransformationSemigroup and HasGeneratorsOfSemigroup],
function(_, R, S)
  local n, basis, map, iso, inv, gens;

  if not (IsField(R) and IsFinite(R)) then
    ErrorNoReturn("the 2nd argument (a ring) must be a finite field");
  fi;

  n := DegreeOfTransformationSemigroup(S);
  # TODO(later) when GAP allows 0-dimensional matrices and Semigroups requires
  # that version of GAP, then the following if-statement can be removed.
  if n = 0 then
    n := 1;
  fi;
  basis := IdentityMatrix(R, n);
  map := x -> Unpack(basis){ImageListOfTransformation(x, n)};
  iso := x -> Matrix(R, map(x));
  inv := x -> Transformation([1 .. n], i -> PositionNonZero(x[i]));
  gens := List(GeneratorsOfSemigroup(S), iso);

  return SemigroupIsomorphismByFunctionNC(S, Semigroup(gens), iso, inv);
end);

#############################################################################
# 2. Isomorphisms for monoids
#############################################################################

InstallMethod(AsMonoid, "for a matrix over finite field semigroup",
[IsMatrixOverFiniteFieldSemigroup],
function(S)
  if MultiplicativeNeutralElement(S) = fail then
    return fail;  # so that we do the same as the GAP/ref manual says
  fi;
  return Range(IsomorphismMonoid(IsMatrixOverFiniteFieldMonoid, S));
end);

InstallMethod(IsomorphismMonoid,
"for IsMatrixOverFiniteFieldMonoid and a semigroup",
[IsMatrixOverFiniteFieldMonoid, IsSemigroup],
SEMIGROUPS.DefaultIsomorphismMonoid);

InstallMethod(IsomorphismMonoid,
"for IsMatrixOverFiniteFieldMonoid, a ring, and a semigroup",
[IsMatrixOverFiniteFieldMonoid, IsRing, IsSemigroup],
function(filt, R, S)
  local iso1, inv1, iso2, inv2;

  iso1 := IsomorphismTransformationMonoid(S);
  inv1 := InverseGeneralMapping(iso1);
  iso2 := IsomorphismMonoid(filt, R, Range(iso1));
  inv2 := InverseGeneralMapping(iso2);

  return SemigroupIsomorphismByFunctionNC(S,
                                          Range(iso2),
                                          x -> (x ^ iso1) ^ iso2,
                                          x -> (x ^ inv2) ^ inv1);
end);

InstallMethod(IsomorphismMonoid,
"for IsMatrixOverFiniteFieldMonoid and a monoid",
[IsMatrixOverFiniteFieldMonoid, IsMonoid],
{filt, S} -> IsomorphismSemigroup(IsMatrixOverFiniteFieldSemigroup, S));

InstallMethod(IsomorphismMonoid,
"for IsMatrixOverFiniteFieldMonoid, a ring, and a monoid",
[IsMatrixOverFiniteFieldMonoid, IsRing, IsMonoid],
{filt, R, S} -> IsomorphismSemigroup(IsMatrixOverFiniteFieldSemigroup, R, S));

InstallMethod(IsomorphismMonoid,
"for IsMatrixOverFiniteFieldMonoid and a matrix over finite field monoid",
[IsMatrixOverFiniteFieldMonoid, IsMatrixOverFiniteFieldMonoid],
{filt, S} -> SemigroupIsomorphismByFunctionNC(S, S, IdFunc, IdFunc));

InstallMethod(IsomorphismMonoid,
"for IsMatrixOverFiniteFieldMonoid, a ring, and a matrix over ff monoid",
[IsMatrixOverFiniteFieldMonoid, IsRing, IsMatrixOverFiniteFieldMonoid],
{filt, R, S} -> IsomorphismSemigroup(IsMatrixOverFiniteFieldSemigroup, R, S));

#############################################################################
# 3. Viewing and printing
#############################################################################

InstallMethod(SemigroupViewStringSuffix,
"for a matrix over finite field semigroup",
[IsMatrixOverFiniteFieldSemigroup],
function(S)
  local n;
  if UserPreference("semigroups", "ViewObj") <> "semigroups-pkg" then
    TryNextMethod();
  fi;
  n := ViewString(NrRows(Representative(S)));
  return Concatenation("\>\>", n, "x", n, "\< \>matrices\< \>over\< \>",
                       ViewString(BaseDomain(S)), "\<\< ");
end);

InstallMethod(ViewObj, "for a general linear monoid",
[IsGeneralLinearMonoid],
Maximum(RankFilter(IsMonoid and HasGeneratorsOfMonoid),
        RankFilter(IsMatrixOverFiniteFieldSemigroup
                   and HasGeneratorsOfSemigroup))
- RankFilter(IsGeneralLinearMonoid) + 1,
function(S)
  PrintFormatted("<general linear monoid {1}x{1} over {2}>",
                 NrRows(Representative(S)),
                 BaseDomain(S));
end);

InstallMethod(PrintString, "for general linear monoid",
[IsGeneralLinearMonoid],
7,  # to beat the method for monoids with generators
function(M)
  local rep, str;

  rep := Representative(M);
  str := Concatenation("GLM(",
                       String(NrRows(rep)),
                       ", ",
                       String(Characteristic(BaseDomain(M))));
  if Characteristic(BaseDomain(M)) <> 1 then
    Append(str, " ^ ");
    Append(str, String(Log(Size(BaseDomain(M)),
                           Characteristic(BaseDomain(M)))));
  fi;
  Append(str, ")");
  return str;
end);

InstallMethod(PrintObj, "for general linear monoid",
[IsGeneralLinearMonoid],
7,  # to beat the method for monoids with generators
function(M)
  Print(PrintString(M));
end);

#############################################################################
# 4. Random
#############################################################################

InstallMethod(SEMIGROUPS_ProcessRandomArgsCons,
[IsMatrixOverFiniteFieldSemigroup, IsList],
function(_, params)

  if Length(params) < 1 then  # nr gens
    params[1] := Random(1, 20);
  elif not IsPosInt(params[1]) then
    return "the 2nd argument (number of generators) is not a pos int";
  fi;
  if Length(params) < 2 then  # dimension
    params[2] := Random(1, 20);
  elif not IsPosInt(params[2]) then
    return "the 3rd argument (matrix dimension) is not a pos int";
  fi;
  if Length(params) < 3 then  # field
    params[3] := GF(Random(Primes), Random(1, 9));
  elif not IsField(params[3]) or not IsFinite(params[3]) then
    return "the 4th argument is not a finite field";
  fi;
  if Length(params) < 4 then  # ranks
    params[4] := [1 .. params[2]];
  elif not IsList(params[4])
      or not ForAll(params[4], x -> IsPosInt(x) and x <= params[2]) then
    return "the 5th argument (matrix ranks) is not a list of pos ints";
  fi;

  if Length(params) > 4 then
    return "there must be at most 5 arguments";
  fi;

  return params;
end);

InstallMethod(SEMIGROUPS_ProcessRandomArgsCons,
[IsMatrixOverFiniteFieldMonoid, IsList],
{filt, params} ->
SEMIGROUPS_ProcessRandomArgsCons(IsMatrixOverFiniteFieldSemigroup, params));

InstallMethod(RandomSemigroupCons,
"for IsMatrixOverFiniteFieldSemigroup and list",
[IsMatrixOverFiniteFieldSemigroup, IsList],
function(_, params)  # params = [nrgens, dim, field, ranks]
  return Semigroup(List([1 .. params[1]], i -> RandomMatrix(params[3],
                                                            params[2],
                                                            params[4])));
end);

InstallMethod(RandomMonoidCons,
"for IsMatrixOverFiniteFieldMonoid and list",
[IsMatrixOverFiniteFieldMonoid, IsList],
function(_, params)  # params = [nrgens, dim, field, ranks]
  return Monoid(List([1 .. params[1]], i -> RandomMatrix(params[3],
                                                         params[2],
                                                         params[4])));
end);

InstallMethod(RandomInverseSemigroupCons,
"for IsMatrixOverFiniteFieldSemigroup and list",
[IsMatrixOverFiniteFieldSemigroup, IsList],
function(filt, params)
  return AsSemigroup(filt,
                     params[3],
                     RandomInverseSemigroup(IsPartialPermSemigroup,
                                            params[1],
                                            params[2]));
end);

InstallMethod(RandomInverseMonoidCons,
"for IsMatrixOverFiniteFieldMonoid and list",
[IsMatrixOverFiniteFieldMonoid, IsList],
function(filt, params)
    return AsMonoid(filt,
                    params[3],
                    RandomInverseMonoid(IsPartialPermMonoid,
                                        params[1],
                                        params[2]));
end);

InstallMethod(BaseDomain, "for a matrix over finite field semigroup",
[IsMatrixOverFiniteFieldSemigroup], S -> BaseDomain(Representative(S)));

InstallMethod(IsGeneratorsOfInverseSemigroup,
"for an ffe coll coll coll ",
[IsFFECollCollColl],
coll -> IsGeneratorsOfSemigroup(coll) and ForAll(coll, x -> x ^ -1 <> fail));

#############################################################################
# 5. Methods for acting semigroups setup
#############################################################################

InstallGlobalFunction(MatrixOverFiniteFieldRowSpaceRightAction,
function(_, vsp, m)
  local nvsp, deg, i;

  Assert(1, IsRowBasisOverFiniteField(vsp));
  Assert(1, IsMatrixObjOverFiniteField(m));
  Assert(1, Rank(vsp) > 0);

  # This takes care of the token element
  if Rank(vsp) > NrRows(m) then
    return RowSpaceBasis(m);
  else
    nvsp := Unpack(vsp!.rows * m);
  fi;
  TriangulizeMat(nvsp);

  deg := Length(nvsp);
  for i in [deg, deg - 1 .. 1] do
    if IsZero(nvsp[i]) then
      Remove(nvsp, i);
    fi;
  od;

  return NewRowBasisOverFiniteField(IsPlistRowBasisOverFiniteFieldRep,
                                    BaseDomain(vsp),
                                    nvsp);
end);

InstallGlobalFunction(MatrixOverFiniteFieldLocalRightInverse,
function(S, V, mat)
  local n, k, W, se, zv, u, j, i;

  Assert(1, IsMatrixOverFiniteFieldSemigroup(S));
  Assert(1, IsRowBasisOverFiniteField(V));
  Assert(1, IsMatrixObjOverFiniteField(mat));
  Assert(1, NrRows(mat) > 0);
  Assert(1, Rank(V) > 0);

  n := NrRows(mat);
  k := Rank(V);

  W := Unpack(V!.rows * mat);

  for i in [1 .. k] do
    Append(W[i], V!.rows[i]);
  od;
  se := SemiEchelonMat(W);
  # If the matrix does not act injectively on V, then there is no right inverse
  # TODO(later) I think we can now simplify things below
  if Number(se.heads{[1 .. n]}, IsZero) > n - k then
    return fail;
  fi;

  for i in [1 .. Length(se.vectors)] do
    W[i] := ShallowCopy(se.vectors[i]);
  od;

  zv := [1 .. 2 * n] * Zero(BaseDomain(mat));
  for i in [1 .. n - Length(W)] do
    Add(W, ShallowCopy(zv));
  od;

  # add missing heads
  u := One(BaseDomain(mat));
  j := k + 1;
  for i in [1 .. n] do
    if se.heads[i] = 0 then
      W[j][i] := u;
      W[j][n + i] := u;
      j := j + 1;
    fi;
  od;
  TriangulizeMat(W);

  return Matrix(W{[1 .. n]}{[n + 1 .. 2 * n]}, mat);
end);

# Returns an invertible matrix.
# TODO(later): make pretty and efficient (in that order).  In particular the
# setup for the matrix should be much more efficient.
InstallGlobalFunction(MatrixOverFiniteFieldSchutzGrpElement,
function(_, x, y)
  local deg, n, eqs, idx, col, row, res;

  Assert(1, IsMatrixObjOverFiniteField(x));
  Assert(1, IsMatrixObjOverFiniteField(y));

  n := Rank(x);
  deg := NrRows(x);

  eqs := TransposedMatMutable(Concatenation(TransposedMat(x),
                                            TransposedMat(y)));
  TriangulizeMat(eqs);

  idx := [];
  col := 1;
  row := 1;

  while col <= deg do
    while IsZero(eqs[row, col]) and col <= deg do
      col := col + 1;
    od;
    if col <= deg then
      Add(idx, col);
      row := row + 1;
      col := col + 1;
    fi;
  od;
  res := Matrix(BaseDomain(x), eqs{[1 .. n]}{idx + deg});
  Assert(1, res ^ (-1) <> fail);
  return res;
end);

# StabilizerAction
InstallGlobalFunction(MatrixOverFiniteFieldStabilizerAction,
function(_, x, m)
  local n, k, rsp, zv, i;
  Assert(1, IsMatrixObjOverFiniteField(x));
  Assert(1, IsFFECollColl(m));
  Assert(1, not IsZero(x));
  Assert(1, not IsZero(m));

  n := NrRows(m);
  k := Rank(x);
  rsp := ShallowCopy(m * RowSpaceBasis(x)!.rows);

  zv := [1 .. n] * Zero(BaseDomain(x));
  for i in [1 .. n - k] do
    Add(rsp, ShallowCopy(zv));
  od;

  return Matrix(RowSpaceTransformationInv(x) * rsp, x);
end);

# This should be doable in a much more efficient way
InstallGlobalFunction(MatrixOverFiniteFieldLambdaConjugator,
function(_, x, y)
  local xse, h, p, yse, q;
  Assert(1, IsMatrixObjOverFiniteField(x));
  Assert(1, IsMatrixObjOverFiniteField(y));
  xse := SemiEchelonMat(Unpack(x));
  h := Filtered(xse.heads, x -> x <> 0);
  p := One(BaseDomain(x))
       * PermutationMat(SortingPerm(h), Length(h), BaseDomain(x));
  yse := SemiEchelonMat(Unpack(y));
  h := Filtered(yse.heads, x -> x <> 0);
  q := One(BaseDomain(y))
       * PermutationMat(SortingPerm(h), Length(h), BaseDomain(y));
  return p * q ^ (-1);
end);

# Is there a complete direct way of testing whether this idempotent exists
# (without constructing it)?  The method below is already pretty efficient.

InstallGlobalFunction(MatrixOverFiniteFieldIdempotentTester,
function(_, x, y)
  Assert(1, IsPlistRowBasisOverFiniteFieldRep(x));
  Assert(1, IsPlistRowBasisOverFiniteFieldRep(y));
  return MatrixOverFiniteFieldIdempotentCreator(_, x, y) <> fail;
end);

# Attempt to construct an idempotent m with RowSpace(m) = x
# ColumnSpace(m) = y

InstallGlobalFunction(MatrixOverFiniteFieldIdempotentCreator,
function(S, x, y)
  local m, inv;

  Assert(1, IsMatrixOverFiniteFieldSemigroup(S));
  Assert(1, IsRowBasisOverFiniteField(x));
  Assert(1, Rank(x) <> 0);
  Assert(1, IsRowBasisOverFiniteField(y));
  Assert(1, Rank(y) <> 0);
  Assert(1, Length(x!.rows[1]) = Length(y!.rows[1]));

  m := Matrix(BaseDomain(S), TransposedMat(y!.rows) * x!.rows);
  inv := MatrixOverFiniteFieldLocalRightInverse(S, x, m);
  if inv = fail then
    return fail;
  fi;
  return m * inv;
end);

InstallMethod(IsGeneratorsOfSemigroup, "for an ffe coll coll coll",
[IsFFECollCollColl],
function(coll)
  local rep;
  Assert(1, not IsEmpty(coll));
  rep := Representative(coll);
  if IsMatrixObjOverFiniteField(rep) then
    return ForAll(coll, x -> NrRows(x) = NrRows(coll[1])
                             and BaseDomain(x) = BaseDomain(coll[1])
                             and NrRows(x) = NrCols(x));
  fi;
  TryNextMethod();
end);

#############################################################################
# 6. Properties of matrix over finite field semigroups
#############################################################################

# FIXME(later) this method is not correct (although it works as documented)
# This should check whether <S> = GLM of the right dimensions/field

InstallMethod(IsFullMatrixMonoid, "for a semigroup",
[IsSemigroup], ReturnFalse);
