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

#############################################################################
## This file contains methods for finite semigroups which do not depend on
## whether they are acting or not, i.e. they should work for all semigroups.
## It is organized as follows:
##
##   1.  Helper functions
##
##   2.  Generators
##
##   3.  Semigroup/Monoid/InverseSemigroup/InverseMonoidByGenerators
##
##   4.  RegularSemigroup
##
##   5.  ClosureSemigroup/Monoid
##
##   6.  ClosureInverseSemigroup/Monoid
##
##   7.  Subsemigroups
##
##   8.  Random semigroups and elements
##
##   9.  Changing representation: AsSemigroup, AsMonoid
##
##   10. Operators
##
#############################################################################

#############################################################################
## 1. Helper functions
#############################################################################

# Returns an isomorphism from the semigroup <S> to a semigroup in <filter> by
# composing an isomorphism from <S> to a transformation semigroup <T> with an
# isomorphism from <T> to a semigroup in <filter>, i.e.  <filter> might be
# IsMaxPlusMatrixSemigroup or similar.

SEMIGROUPS.DefaultIsomorphismSemigroup := function(filter, S)
  local iso1, inv1, iso2, inv2;

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

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

# Returns an isomorphism from the monoid (or IsMonoidAsSemigroup) <S> to a
# monoid in <filter> by composing an isomorphism from <S> to a transformation
# monoid <T> with an isomorphism from <T> to a monoid in <filter>, i.e.
# <filter> might be IsMaxPlusMatrixMonoid or similar.

SEMIGROUPS.DefaultIsomorphismMonoid := function(filter, S)
  local iso1, inv1, iso2, inv2;

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

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

#############################################################################
## 2. Generators
#############################################################################

InstallMethod(IsGeneratorsOfInverseSemigroup,
"for a semigroup with generators",
[IsSemigroup and HasGeneratorsOfSemigroup],
function(S)
  if IsInverseActingSemigroupRep(S) then
    # There is currently no way to enter this!
    return true;
  fi;
  return IsGeneratorsOfInverseSemigroup(GeneratorsOfSemigroup(S));
end);

# TODO(later) the next method should really be in the library
InstallMethod(IsGeneratorsOfInverseSemigroup, "for a list",
[IsList], ReturnFalse);

InstallMethod(Generators, "for a semigroup",
[IsSemigroup],
function(S)

  if HasGeneratorsOfMagmaIdeal(S) then
    return GeneratorsOfMagmaIdeal(S);
  elif HasGeneratorsOfGroup(S) then
    return GeneratorsOfGroup(S);
  elif HasGeneratorsOfInverseMonoid(S) then
    return GeneratorsOfInverseMonoid(S);
  elif HasGeneratorsOfInverseSemigroup(S) then
    return GeneratorsOfInverseSemigroup(S);
  elif HasGeneratorsOfMonoid(S) then
    return GeneratorsOfMonoid(S);
  fi;

  return GeneratorsOfSemigroup(S);
end);

#############################################################################
## 3. Semigroup/Monoid/InverseSemigroup/InverseMonoidByGenerators
#############################################################################

InstallImmediateMethod(IsTrivial, IsMonoid and HasGeneratorsOfMonoid, 0,
function(S)
  local gens;
  gens := GeneratorsOfMonoid(S);
  if CanEasilyCompareElements(gens) and Length(gens) = 1
      and gens[1] = One(gens[1]) then
    return true;
  fi;
  TryNextMethod();
end);

InstallImmediateMethod(IsTrivial, IsMonoid and HasGeneratorsOfSemigroup, 0,
function(S)
  local gens;
  gens := GeneratorsOfSemigroup(S);
  if CanEasilyCompareElements(gens) and Length(gens) = 1
      and gens[1] = One(gens[1]) then
    return true;
  fi;
  TryNextMethod();
end);

InstallMethod(MagmaByGenerators,
"for a finite associative element collection",
[IsAssociativeElementCollection and IsFinite], SemigroupByGenerators);

InstallMethod(SemigroupByGenerators,
"for a finite list or collection",
[IsListOrCollection and IsFinite],
{coll} -> SemigroupByGenerators(coll, SEMIGROUPS.DefaultOptionsRec));

InstallMethod(SemigroupByGenerators,
"for a finite list or collection and record",
[IsListOrCollection and IsFinite, IsRecord],
function(gens, opts)
  local filts, S;

  opts := SEMIGROUPS.ProcessOptionsRec(SEMIGROUPS.DefaultOptionsRec, opts);

  if CanEasilyCompareElements(gens) then
    # Check if the One of the generators is a generator
    if (IsMultiplicativeElementWithOneCollection(gens)
        or (IsFFECollCollColl(gens) and IsGeneratorsOfSemigroup(gens)))
        and Position(gens, One(gens)) <> fail then
      return MonoidByGenerators(gens, opts);
    fi;

    # Try to find a smaller generating set
    if opts.small and Length(gens) > 1 then
      opts := ShallowCopy(opts);
      opts.small := false;
      opts.regular := false;
      return ClosureSemigroup(Semigroup(gens[1], opts), gens, opts);
    fi;
  fi;

  filts := IsSemigroup and IsAttributeStoringRep;

  if opts.regular then  # This overrides everything!
    filts := filts and IsRegularActingSemigroupRep;
  elif opts.acting and IsGeneratorsOfActingSemigroup(gens) then
    filts := filts and IsActingSemigroup;
  fi;

  if IsMatrixObj(Representative(gens)) then
    if BaseDomain(Representative(gens)) = Integers then
      filts := filts and IsIntegerMatrixSemigroup;
    elif IsField(BaseDomain(Representative(gens)))
        and IsFinite(BaseDomain(Representative(gens))) then
      filts := filts and IsMatrixOverFiniteFieldSemigroup;
    fi;
  fi;

  S := Objectify(NewType(FamilyObj(gens), filts), rec(opts := opts));
  SetGeneratorsOfMagma(S, AsList(gens));

  return S;
end);

InstallMethod(MonoidByGenerators,
"for a finite list or collection",
[IsListOrCollection and IsFinite],
{gens} -> MonoidByGenerators(gens, SEMIGROUPS.DefaultOptionsRec));

InstallMethod(MonoidByGenerators,
"for a finite list or collection and record",
[IsListOrCollection and IsFinite, IsRecord],
function(gens, opts)
  local mgens, pos, filts, one, S;

  opts := SEMIGROUPS.ProcessOptionsRec(SEMIGROUPS.DefaultOptionsRec, opts);
  mgens := ShallowCopy(gens);

  if CanEasilyCompareElements(gens) then
    # Try to find a smaller generating set
    if opts.small and Length(gens) > 1 then
      opts         := ShallowCopy(opts);
      opts.small   := false;
      opts.regular := false;
      return ClosureMonoid(Monoid(One(gens), opts), gens, opts);
    elif (IsMultiplicativeElementWithOneCollection(gens)
        or IsFFECollCollColl(gens)) and Length(gens) > 1 then
      pos := Position(gens, One(gens));
      if pos <> fail and
          (not IsPartialPermCollection(gens) or One(gens) =
           One(gens{Concatenation([1 .. pos - 1],
                                  [pos + 1 .. Length(gens)])})) then
        Remove(mgens, pos);
      fi;
    fi;
  fi;

  filts := IsMonoid and IsAttributeStoringRep;

  if opts.regular then  # This overrides everything!
    filts := filts and IsRegularActingSemigroupRep;
  elif opts.acting and IsGeneratorsOfActingSemigroup(gens) then
    filts := filts and IsActingSemigroup;
  fi;
  if IsMatrixObj(Representative(gens)) then
    one := One(Representative(gens));
    if BaseDomain(Representative(gens)) = Integers then
      filts := filts and IsIntegerMatrixMonoid;
    elif IsField(BaseDomain(Representative(gens)))
        and IsFinite(BaseDomain(Representative(gens))) then
      filts := filts and IsMatrixOverFiniteFieldMonoid;
    fi;
  else
    one := One(gens);
  fi;

  S := Objectify(NewType(FamilyObj(gens), filts), rec(opts := opts));

  if not CanEasilyCompareElements(gens) or not one in gens then
    SetGeneratorsOfMagma(S, Concatenation([one], gens));
  else
    SetGeneratorsOfMagma(S, AsList(gens));
  fi;
  SetGeneratorsOfMagmaWithOne(S, AsList(mgens));

  return S;
end);

InstallMethod(InverseSemigroupByGenerators,
"for a finite collection",
[IsCollection and IsFinite],
{gens} -> InverseSemigroupByGenerators(gens, SEMIGROUPS.DefaultOptionsRec));

InstallMethod(InverseSemigroupByGenerators,
"for a finite multiplicative element collection and record",
[IsMultiplicativeElementCollection and IsFinite, IsRecord],
function(gens, opts)
  local filts, S;

  if not IsGeneratorsOfInverseSemigroup(gens) then
    ErrorNoReturn("the 1st argument (a finite mult. elt. coll.) must satisfy ",
                  "IsGeneratorsOfInverseSemigroup");
  fi;

  opts := SEMIGROUPS.ProcessOptionsRec(SEMIGROUPS.DefaultOptionsRec, opts);

  if CanEasilyCompareElements(gens) then
    # Check if the One of the generators is a generator
    if IsMultiplicativeElementWithOneCollection(gens)
        and Position(gens, One(gens)) <> fail then
      return InverseMonoidByGenerators(gens, opts);
    elif opts.small and Length(gens) > 1 then
      # Try to find a smaller generating set
      opts := ShallowCopy(opts);
      opts.small := false;
      return ClosureInverseSemigroup(InverseSemigroup(gens[1], opts),
                                     gens,
                                     opts);
    fi;
  fi;

  filts := IsMagma and IsInverseSemigroup and IsAttributeStoringRep;

  if opts.acting and IsGeneratorsOfActingSemigroup(gens) then
    filts := filts and IsInverseActingSemigroupRep;
  fi;

  S := Objectify(NewType(FamilyObj(gens), filts), rec(opts := opts));
  SetGeneratorsOfInverseSemigroup(S, AsList(gens));

  return S;
end);

InstallMethod(InverseMonoidByGenerators,
"for a finite collection",
[IsCollection and IsFinite],
{gens} -> InverseMonoidByGenerators(gens, SEMIGROUPS.DefaultOptionsRec));

InstallMethod(InverseMonoidByGenerators,
"for a finite multiplicative element collection and record",
[IsMultiplicativeElementCollection and IsMultiplicativeElementWithOneCollection
 and IsFinite, IsRecord],
function(gens, opts)
  local pos, filts, S;

  if not IsGeneratorsOfInverseSemigroup(gens) then
    ErrorNoReturn("the 1st argument (a finite mult. elt. coll.) must satisfy ",
                  "IsGeneratorsOfInverseSemigroup");
  fi;

  opts := SEMIGROUPS.ProcessOptionsRec(SEMIGROUPS.DefaultOptionsRec, opts);

  if CanEasilyCompareElements(gens) then
    if (not IsPartialPermCollection(gens) or not opts.small)
        and IsMultiplicativeElementWithOneCollection(gens)
        and Length(gens) > 1 then
      pos := Position(gens, One(gens));
      if pos <> fail and
          (not IsPartialPermCollection(gens) or One(gens) =
           One(gens{Concatenation([1 .. pos - 1],
                                  [pos + 1 .. Length(gens)])})) then
        gens := ShallowCopy(gens);
        Remove(gens, pos);
      fi;
    fi;
    if opts.small and Length(gens) > 1 then
      # Try to find a smaller generating set
      opts := ShallowCopy(opts);
      opts.small := false;
      return ClosureInverseMonoid(InverseMonoid(One(gens), opts),
                                  gens,
                                  opts);
    fi;
  fi;

  filts := IsMagmaWithOne and IsInverseMonoid and IsAttributeStoringRep;

  if opts.acting and IsGeneratorsOfActingSemigroup(gens) then
    filts := filts and IsInverseActingSemigroupRep;
  fi;

  S := Objectify(NewType(FamilyObj(gens), filts), rec(opts := opts));
  SetOne(S, One(gens));

  SetGeneratorsOfInverseMonoid(S, AsList(gens));
  if not CanEasilyCompareElements(gens) or not One(gens) in gens then
    SetGeneratorsOfInverseSemigroup(S, Concatenation(gens, [One(gens)]));
  else
    SetGeneratorsOfInverseSemigroup(S, AsList(gens));
  fi;
  return S;
end);

#############################################################################
## 4. RegularSemigroup
#############################################################################

InstallGlobalFunction(RegularSemigroup,
function(arg...)
  if not IsRecord(Last(arg)) then
    Add(arg, rec(regular := true));
  else
    Last(arg).regular := true;
  fi;
  return CallFuncList(Semigroup, arg);
end);

#############################################################################
## 5. ClosureSemigroup/Monoid
#############################################################################

InstallMethod(ClosureSemigroup,
"for a semigroup and finite list or collection",
[IsSemigroup, IsListOrCollection and IsFinite],
{S, coll} -> ClosureSemigroup(S, coll, SEMIGROUPS.OptionsRec(S)));

InstallMethod(ClosureMonoid,
"for a monoid and finite mult. element with one collection",
[IsMonoid, IsMultiplicativeElementWithOneCollection and IsFinite],
{S, coll} -> ClosureMonoid(S, coll, SEMIGROUPS.OptionsRec(S)));

InstallMethod(ClosureSemigroup, "for a semigroup and multiplicative element",
[IsSemigroup, IsMultiplicativeElement],
{S, x} -> ClosureSemigroup(S, [x], SEMIGROUPS.OptionsRec(S)));

InstallMethod(ClosureMonoid, "for a monoid and mult. element with one",
[IsMonoid, IsMultiplicativeElementWithOne],
{S, x} -> ClosureMonoid(S, [x], SEMIGROUPS.OptionsRec(S)));

InstallMethod(ClosureSemigroup,
"for a semigroup, multiplicative element, and record",
[IsSemigroup, IsMultiplicativeElement, IsRecord],
{S, x, opts} -> ClosureSemigroup(S, [x], opts));

InstallMethod(ClosureMonoid,
"for a monoid, mult. element with one, and record",
[IsMonoid, IsMultiplicativeElementWithOne, IsRecord],
{S, x, opts} -> ClosureMonoid(S, [x], opts));

InstallMethod(ClosureSemigroup,
"for a semigroup, finite list or collection, and record",
[IsSemigroup, IsListOrCollection and IsFinite, IsRecord],
function(S, coll, opts)

  # coll is copied here to avoid doing it repeatedly in
  # ClosureSemigroupOrMonoidNC

  if IsEmpty(coll) then
    return S;
  elif IsSemigroup(coll) then
    coll := GeneratorsOfSemigroup(coll);
  elif not IsList(coll) then
    coll := AsList(coll);
  fi;

  if not IsMutable(coll) then
    coll := ShallowCopy(coll);
  fi;

  # This error has to come after coll is turned into a list, otherwise it may
  # fail in Concatenation(GeneratorsOfSemigroup(S), coll).

  if ElementsFamily(FamilyObj(S)) <> FamilyObj(Representative(coll))
      or not IsGeneratorsOfSemigroup(Concatenation(GeneratorsOfSemigroup(S),
                                                   coll)) then
    ErrorNoReturn("the 1st argument (a semigroup) and the 2nd argument ",
                  "(a list or coll.) cannot be used to ",
                  "generate a semigroup");
  fi;

  # opts is copied and processed here to avoid doing it repeatedly in
  # ClosureSemigroupOrMonoidNC

  opts         := ShallowCopy(opts);
  opts.small   := false;
  opts.regular := false;
  opts         := SEMIGROUPS.ProcessOptionsRec(SEMIGROUPS.DefaultOptionsRec,
                                               opts);

  return ClosureSemigroupOrMonoidNC(Semigroup, S, coll, opts);
end);

InstallMethod(ClosureMonoid,
"for a monoid, finite multiplicative with one element collection, and record",
[IsMonoid, IsMultiplicativeElementWithOneCollection and IsFinite, IsRecord],
function(S, coll, opts)
  # coll is copied here to avoid doing it repeatedly in
  # ClosureSemigroupOrMonoidNC

  if IsSemigroup(coll) then
    coll := ShallowCopy(GeneratorsOfSemigroup(coll));
  elif not IsList(coll) then
    coll := AsList(coll);
  fi;

  if not IsMutable(coll) then
    coll := ShallowCopy(coll);
  fi;

  # This error has to come after coll is turned into a list, otherwise it may
  # fail in Concatenation(GeneratorsOfSemigroup(S), coll).

  if ElementsFamily(FamilyObj(S)) <> FamilyObj(Representative(coll))
      or not IsGeneratorsOfSemigroup(Concatenation(GeneratorsOfSemigroup(S),
                                                   coll)) then
    ErrorNoReturn("the 1st argument (a monoid) and the 2nd argument ",
                  "(a mult. elt. with one coll.) cannot be ",
                  "used to generate a monoid");
  fi;

  # opts is copied and processed here to avoid doing it repeatedly in
  # ClosureSemigroupOrMonoidNC

  opts         := ShallowCopy(opts);
  opts.small   := false;
  opts.regular := false;
  opts         := SEMIGROUPS.ProcessOptionsRec(SEMIGROUPS.DefaultOptionsRec,
                                               opts);

  return ClosureSemigroupOrMonoidNC(Monoid, S, coll, opts);
end);

InstallMethod(ClosureSemigroupOrMonoidNC,
"for a function, semigroup, finite list, and record",
[IsFunction, IsSemigroup, IsList and IsFinite, IsRecord],
function(Constructor, S, coll, opts)
  local n, T, x;

  # opts must be copied and processed before calling this function
  # coll must be copied before calling this function

  coll := Filtered(coll, x -> not x in S);
  if IsEmpty(coll) then
    return S;
  fi;

  coll := Shuffle(Set(coll));
  if IsGeneratorsOfActingSemigroup(coll) then
    n := ActionDegree(coll);
    Sort(coll, {x, y} -> ActionRank(x, n) > ActionRank(y, n));
  elif Length(coll) < 120 then
    Sort(coll, IsGreensDGreaterThanFunc(Semigroup(coll)));
  fi;

  T := Constructor(S, opts);

  for x in coll do
    if not x in T then
      T := Constructor(T, x, opts);
    fi;
  od;
  return T;
end);

#############################################################################
## 5. ClosureInverseSemigroup
#############################################################################

InstallMethod(ClosureInverseSemigroup,
"for an inverse semigroup with inverse op and finite mult. element coll",
[IsInverseSemigroup and IsGeneratorsOfInverseSemigroup,
 IsMultiplicativeElementCollection and IsFinite],
{S, coll} -> ClosureInverseSemigroup(S, coll, SEMIGROUPS.OptionsRec(S)));

InstallMethod(ClosureInverseMonoid,
"for an inverse monoid with inverse op and finite mult. element with one coll",
[IsInverseMonoid and IsGeneratorsOfInverseSemigroup,
 IsMultiplicativeElementWithOneCollection and IsFinite],
{S, coll} -> ClosureInverseMonoid(S, coll, SEMIGROUPS.OptionsRec(S)));

InstallMethod(ClosureInverseSemigroup,
"for an inverse semigroup with inverse op and a multiplicative element",
[IsInverseSemigroup and IsGeneratorsOfInverseSemigroup,
 IsMultiplicativeElement],
{S, x} -> ClosureInverseSemigroup(S, [x], SEMIGROUPS.OptionsRec(S)));

InstallMethod(ClosureInverseMonoid,
"for an inverse monoid with inverse op and a mult. element with one",
[IsInverseMonoid and IsGeneratorsOfInverseSemigroup,
 IsMultiplicativeElementWithOne],
{S, x} -> ClosureInverseMonoid(S, [x], SEMIGROUPS.OptionsRec(S)));

InstallMethod(ClosureInverseSemigroup,
"for inverse semigroup with inverse op, multiplicative element, record",
[IsInverseSemigroup and IsGeneratorsOfInverseSemigroup,
 IsMultiplicativeElement,
 IsRecord],
{S, x, opts} -> ClosureInverseSemigroup(S, [x], opts));

InstallMethod(ClosureInverseMonoid,
"for inverse monoid with inverse op, multiplicative element with one, record",
[IsInverseMonoid and IsGeneratorsOfInverseSemigroup,
 IsMultiplicativeElementWithOne,
 IsRecord],
{S, x, opts} -> ClosureInverseMonoid(S, [x], opts));

InstallMethod(ClosureInverseSemigroup,
"for an inverse semigroup with inverse op, finite mult elt coll, and record",
[IsInverseSemigroup and IsGeneratorsOfInverseSemigroup,
 IsMultiplicativeElementCollection and IsFinite,
 IsRecord],
function(S, coll, opts)

  # coll is copied here to avoid doing it repeatedly in
  # ClosureSemigroupOrMonoidNC

  if not IsGeneratorsOfInverseSemigroup(coll) then
    ErrorNoReturn("the 2nd argument (a finite mult. elt. coll.) must satisfy ",
                  "IsGeneratorsOfInverseSemigroup");
  elif IsSemigroup(coll) then
    coll := ShallowCopy(GeneratorsOfSemigroup(coll));
  elif not IsList(coll) then
    coll := AsList(coll);
  else
    coll := ShallowCopy(coll);
  fi;

  # This error has to come after coll is turned into a list, otherwise it may
  # fail in Concatenation(GeneratorsOfSemigroup(S), coll).

  if ElementsFamily(FamilyObj(S)) <> FamilyObj(Representative(coll))
      or not IsGeneratorsOfSemigroup(Concatenation(GeneratorsOfSemigroup(S),
                                                   coll)) then
    ErrorNoReturn("the 1st argument (a semigroup) and the 2nd argument ",
                  "(a mult. elt. coll.) cannot be used to ",
                  "generate an inverse semigroup");
  fi;

  # opts is copied and processed here to avoid doing it repeatedly in
  # ClosureInverseSemigroupOrMonoidNC

  opts         := ShallowCopy(opts);
  opts.small   := false;
  opts         := SEMIGROUPS.ProcessOptionsRec(SEMIGROUPS.DefaultOptionsRec,
                                               opts);

  return ClosureInverseSemigroupOrMonoidNC(InverseSemigroup, S, coll, opts);
end);

InstallMethod(ClosureInverseMonoid,
"for an inverse monoid, finite mult elt with one coll, and record",
[IsInverseMonoid and IsGeneratorsOfInverseSemigroup,
 IsMultiplicativeElementCollection and IsFinite,
 IsRecord],
function(S, coll, opts)

  # coll is copied here to avoid doing it repeatedly in
  # ClosureSemigroupOrMonoidNC

  if not IsGeneratorsOfInverseSemigroup(coll) then
    ErrorNoReturn("the 2nd argument (a finite mult. elt. coll.) must satisfy ",
                  "IsGeneratorsOfInverseSemigroup");
  elif IsSemigroup(coll) then
    coll := ShallowCopy(GeneratorsOfSemigroup(coll));
  elif not IsList(coll) then
    coll := AsList(coll);
  else
    coll := ShallowCopy(coll);
  fi;

  # This error has to come after coll is turned into a list, otherwise it may
  # fail in Concatenation(GeneratorsOfSemigroup(S), coll).

  if ElementsFamily(FamilyObj(S)) <> FamilyObj(Representative(coll))
      or not IsGeneratorsOfSemigroup(Concatenation(GeneratorsOfSemigroup(S),
                                                   coll)) then
    ErrorNoReturn("the 1st argument (a semigroup) and the 2nd argument ",
                  "(a mult. elt. coll.) cannot be used to ",
                  "generate an inverse monoid");
  fi;

  # opts is copied and processed here to avoid doing it repeatedly in
  # ClosureInverseSemigroupOrMonoidNC

  opts         := ShallowCopy(opts);
  opts.small   := false;
  opts         := SEMIGROUPS.ProcessOptionsRec(SEMIGROUPS.DefaultOptionsRec,
                                               opts);

  return ClosureInverseSemigroupOrMonoidNC(InverseMonoid, S, coll, opts);
end);

InstallMethod(ClosureInverseSemigroupOrMonoidNC,
"for a function, an inverse semigroup, finite list of mult. element, and rec",
[IsFunction,
 IsInverseSemigroup and IsGeneratorsOfInverseSemigroup,
 IsMultiplicativeElementCollection and IsFinite and IsList,
 IsRecord],
function(Constructor, S, coll, opts)
  local n, x, one, i;
  coll := Filtered(coll, x -> not x in S);
  if IsEmpty(coll) then
    return S;
  fi;

  # opts must be copied and processed before calling this function
  # coll must be copied before calling this function

  # Make coll closed under inverses
  coll := Set(coll);
  n    := Length(coll);
  for i in [1 .. n] do
    x := coll[i] ^ -1;
    if not x in coll then
      AddSet(coll, x);
    fi;
  od;

  if Constructor = InverseMonoid
      and IsMultiplicativeElementWithOneCollection(S)
      and IsMultiplicativeElementWithOneCollection(coll) then
    one := One(Concatenation(coll, GeneratorsOfSemigroup(S)));
    if not one in coll and not one in S then
      AddSet(coll, one);
    fi;
  fi;

  # Shuffle and sort by rank or the D-order
  coll := Shuffle(coll);
  if IsGeneratorsOfActingSemigroup(coll) then
    n := ActionDegree(coll);
    Sort(coll, {x, y} -> ActionRank(x, n) > ActionRank(y, n));
  elif Length(coll) < 120 then
    Sort(coll, IsGreensDGreaterThanFunc(InverseSemigroup(coll)));
  fi;

  return ClosureSemigroupOrMonoidNC(Constructor, S, coll, opts);
end);

# Both of these methods are required for ClosureInverseSemigroup(NC) and an
# empty list because ClosureInverseSemigroup might be called with an empty
# list, it might be that all of the elements in the collection passed to
# ClosureInverseSemigroup already belong to the semigroup, in which case we
# call ClosureInverseSemigroupOrMonoidNC with an empty list.

InstallMethod(ClosureInverseSemigroup,
"for an inverse semigroup, empty list or collection, and record",
[IsInverseSemigroup and IsGeneratorsOfInverseSemigroup,
 IsListOrCollection and IsEmpty,
 IsRecord],
ReturnFirst);

InstallMethod(ClosureInverseMonoid,
"for an inverse monoid, empty list or collection, and record",
[IsInverseMonoid and IsGeneratorsOfInverseSemigroup,
 IsListOrCollection and IsEmpty,
 IsRecord],
ReturnFirst);

InstallMethod(ClosureInverseSemigroupOrMonoidNC,
"for a function, inverse semigroup, empty list, and record",
[IsFunction,
 IsInverseSemigroup and IsGeneratorsOfInverseSemigroup,
 IsList and IsEmpty,
 IsRecord],
{Constructor, S, coll, opts} -> S);

#############################################################################
## 7. Subsemigroups
#############################################################################

InstallMethod(SubsemigroupByProperty,
"for a semigroup, function, and positive integer",
[IsSemigroup, IsFunction, IsPosInt],
function(S, func, limit)
  local iter, x, closure, T;

  iter := Iterator(S);

  repeat
    x := NextIterator(iter);
  until func(x) or IsDoneIterator(iter);

  if not func(x) then
    return fail;  # should really return the empty semigroup
  elif CanUseLibsemigroupsFroidurePin(S) then
    closure := {S, coll, opts} ->
               ClosureSemigroupOrMonoidNC(Semigroup, S, coll, opts);
  else
    closure := ClosureSemigroup;
  fi;
  T := Semigroup(x);

  while Size(T) < limit and not IsDoneIterator(iter) do
    x := NextIterator(iter);
    if func(x) and not x in T then
      T := closure(T, [x], SEMIGROUPS.OptionsRec(T));
    fi;
  od;
  SetParent(T, S);
  return T;
end);

InstallMethod(InverseSubsemigroupByProperty,
"for an inverse semigroup with inverse op, function, positive integer",
[IsInverseSemigroup and IsGeneratorsOfInverseSemigroup, IsFunction, IsPosInt],
function(S, func, limit)
  local iter, T, x;

  iter := Iterator(S);

  repeat
    x := NextIterator(iter);
  until func(x) or IsDoneIterator(iter);

  if not func(x) then
    return fail;  # should really return the empty semigroup
  fi;

  T := InverseSemigroup(x);

  while Size(T) < limit and not IsDoneIterator(iter) do
    x := NextIterator(iter);
    if func(x) then
      T := ClosureInverseSemigroup(T, x);
    fi;
  od;
  SetParent(T, S);
  return T;
end);

InstallMethod(SubsemigroupByProperty, "for a semigroup and function",
[IsSemigroup, IsFunction],
{S, func} -> SubsemigroupByProperty(S, func, Size(S)));

InstallMethod(InverseSubsemigroupByProperty,
"for inverse semigroup with inverse op and function",
[IsInverseSemigroup and IsGeneratorsOfInverseSemigroup, IsFunction],
{S, func} -> InverseSubsemigroupByProperty(S, func, Size(S)));

#############################################################################
## 8. Random semigroups and elements
#############################################################################

InstallMethod(Random,
"for a semigroup with AsList",
[IsSemigroup and HasAsList],
20,  # to beat other random methods
{S} -> AsList(S)[Random(1, Size(S))]);

InstallMethod(SEMIGROUPS_ProcessRandomArgsCons,
[IsSemigroup, 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) must be a pos int";
  fi;
  if Length(params) < 2 then  # degree / dimension
    params[2] := Random(1, 20);
  elif not IsPosInt(params[2]) then
    return "the 3rd argument (degree or dimension) must be a pos int";
  fi;
  if Length(params) > 2 then
    return "there must be at most 3 arguments";
  fi;
  return params;
end);

InstallMethod(SEMIGROUPS_ProcessRandomArgsCons,
[IsMonoid, IsList],
{filt, params} -> SEMIGROUPS_ProcessRandomArgsCons(IsSemigroup, params));

SEMIGROUPS.DefaultRandomInverseSemigroup := function(filt, params)
  if Length(params) = 2 then
    return AsSemigroup(filt,
                       RandomInverseSemigroup(IsPartialPermSemigroup,
                                              params[1],
                                              params[2]));
  elif Length(params) = 3 then
    return AsSemigroup(filt,
                       params[3],  # threshold
                       RandomInverseSemigroup(IsPartialPermSemigroup,
                                              params[1],
                                              params[2]));
  elif Length(params) = 4 then
    return AsSemigroup(filt,
                       params[3],  # threshold
                       params[4],  # period
                       RandomInverseSemigroup(IsPartialPermSemigroup,
                                              params[1],
                                              params[2]));
  fi;
  ErrorNoReturn("the 2nd argument must have length 2, 3, or 4");
end;

SEMIGROUPS.DefaultRandomInverseMonoid := function(filt, params)
  if Length(params) = 2 then
    return AsMonoid(filt,
                    RandomInverseMonoid(IsPartialPermMonoid,
                                        params[1],
                                        params[2]));
  elif Length(params) = 3 then
    return AsMonoid(filt,
                    params[3],  # threshold
                    RandomInverseMonoid(IsPartialPermMonoid,
                                        params[1],
                                        params[2]));
  elif Length(params) = 4 then
    return AsMonoid(filt,
                    params[3],  # threshold
                    params[4],  # period
                    RandomInverseMonoid(IsPartialPermMonoid,
                                        params[1],
                                        params[2]));
  fi;
  ErrorNoReturn("the 2nd argument must have length 2, 3, or 4");
end;

InstallGlobalFunction(RandomSemigroup,
function(arg...)
  local filt, params;

  # check for optional first arg
  if Length(arg) >= 1 and IsPosInt(arg[1]) then
    CopyListEntries(arg, 1, 1, arg, 2, 1, Length(arg));
    Unbind(arg[1]);
  fi;

  if Length(arg) >= 1 and IsBound(arg[1]) then
    filt := arg[1];
    if not IsFilter(filt) then
      ErrorNoReturn("the 1st argument must be a filter");
    fi;
  else
    filt := Random([IsReesMatrixSemigroup,
                    IsReesZeroMatrixSemigroup,
                    IsFpSemigroup,
                    IsPBRSemigroup,
                    IsBipartitionSemigroup,
                    IsBlockBijectionSemigroup,
                    IsTransformationSemigroup,
                    IsPartialPermSemigroup,
                    IsBooleanMatSemigroup,
                    IsMaxPlusMatrixSemigroup,
                    IsMinPlusMatrixSemigroup,
                    IsTropicalMaxPlusMatrixSemigroup,
                    IsTropicalMinPlusMatrixSemigroup,
                    IsProjectiveMaxPlusMatrixSemigroup,
                    IsNTPMatrixSemigroup,
                    IsIntegerMatrixSemigroup,
                    IsMatrixOverFiniteFieldSemigroup]);
  fi;

  params := SEMIGROUPS_ProcessRandomArgsCons(filt, arg{[2 .. Length(arg)]});
  if IsString(params) then
    ErrorNoReturn(params);
  fi;
  return RandomSemigroupCons(filt, params);
end);

InstallGlobalFunction(RandomMonoid,
function(arg...)
  local filt, params;

  # check for optional first arg
  if Length(arg) >= 1 and IsPosInt(arg[1]) then
    CopyListEntries(arg, 1, 1, arg, 2, 1, Length(arg));
    Unbind(arg[1]);
  fi;

  if Length(arg) >= 1 and IsBound(arg[1]) then
    filt := arg[1];
    if not IsFilter(filt) then
      ErrorNoReturn("the 1st argument must be a filter");
    fi;
  else
    filt := Random([IsFpMonoid,
                    IsPBRMonoid,
                    IsBipartitionMonoid,
                    IsTransformationMonoid,
                    IsPartialPermMonoid,
                    IsBooleanMatMonoid,
                    IsMaxPlusMatrixMonoid,
                    IsMinPlusMatrixMonoid,
                    IsTropicalMaxPlusMatrixMonoid,
                    IsTropicalMinPlusMatrixMonoid,
                    IsProjectiveMaxPlusMatrixMonoid,
                    IsNTPMatrixMonoid,
                    IsBlockBijectionMonoid,
                    IsIntegerMatrixMonoid,
                    IsMatrixOverFiniteFieldMonoid]);
  fi;

  params := SEMIGROUPS_ProcessRandomArgsCons(filt, arg{[2 .. Length(arg)]});
  if IsString(params) then
    ErrorNoReturn(params);
  fi;
  return RandomMonoidCons(filt, params);
end);

InstallGlobalFunction(RandomInverseSemigroup,
function(arg...)
  local filt, params;

  # check for optional first arg
  if Length(arg) >= 1 and IsPosInt(arg[1]) then
    CopyListEntries(arg, 1, 1, arg, 2, 1, Length(arg));
    Unbind(arg[1]);
  fi;

  if Length(arg) >= 1 and IsBound(arg[1]) then
    filt := arg[1];
    if not IsFilter(filt) then
      ErrorNoReturn("the 1st argument must be a filter");
    fi;
  else
    filt := Random([IsFpSemigroup,
                    IsPBRSemigroup,
                    IsBipartitionSemigroup,
                    IsTransformationSemigroup,
                    IsPartialPermSemigroup,
                    IsBooleanMatSemigroup,
                    IsMaxPlusMatrixSemigroup,
                    IsMinPlusMatrixSemigroup,
                    IsTropicalMaxPlusMatrixSemigroup,
                    IsTropicalMinPlusMatrixSemigroup,
                    IsProjectiveMaxPlusMatrixSemigroup,
                    IsNTPMatrixSemigroup,
                    IsBlockBijectionSemigroup,
                    IsIntegerMatrixSemigroup,
                    IsMatrixOverFiniteFieldSemigroup]);
  fi;

  params := SEMIGROUPS_ProcessRandomArgsCons(filt, arg{[2 .. Length(arg)]});
  if IsString(params) then
    ErrorNoReturn(params);
  fi;
  return RandomInverseSemigroupCons(filt, params);
end);

InstallGlobalFunction(RandomInverseMonoid,
function(arg...)
  local filt, params;

  # check for optional first arg
  if Length(arg) >= 1 and IsPosInt(arg[1]) then
    CopyListEntries(arg, 1, 1, arg, 2, 1, Length(arg));
    Unbind(arg[1]);
  fi;

  if Length(arg) >= 1 and IsBound(arg[1]) then
    filt := arg[1];
    if not IsFilter(filt) then
      ErrorNoReturn("the 1st argument must be a filter");
    fi;
  else
    filt := Random([IsFpMonoid,
                    IsPBRMonoid,
                    IsBipartitionMonoid,
                    IsTransformationMonoid,
                    IsPartialPermMonoid,
                    IsBooleanMatMonoid,
                    IsMaxPlusMatrixMonoid,
                    IsMinPlusMatrixMonoid,
                    IsTropicalMaxPlusMatrixMonoid,
                    IsTropicalMinPlusMatrixMonoid,
                    IsProjectiveMaxPlusMatrixMonoid,
                    IsNTPMatrixMonoid,
                    IsBlockBijectionMonoid,
                    IsIntegerMatrixMonoid,
                    IsMatrixOverFiniteFieldMonoid]);
  fi;

  params := SEMIGROUPS_ProcessRandomArgsCons(filt, arg{[2 .. Length(arg)]});
  if IsString(params) then
    ErrorNoReturn(params);
  fi;
  return RandomInverseMonoidCons(filt, params);
end);

#############################################################################
## 9. Changing representation: AsSemigroup, AsMonoid
#############################################################################

# IsomorphismSemigroup can be used to find an isomorphism from any semigroup to
# a semigroup of any other type (provided a method is installed!). This is done
# to avoid having to have an operation/attribute called IsomorphismXSemigroup
# for every single type of semigroup (X = Bipartition, MaxPlusMatrix, etc).
# This is simpler but has the disadvantage that the isomorphisms are not stored
# as attributes, and slightly more typing is required.
#
# The following IsomorphismXSemigroup functions remain:
#
# - IsomorphismTransformationSemigroup/Monoid
# - IsomorphismPartialPermSemigroup/Monoid
# - IsomorphismFpSemigroup/Monoid
# - IsomorphismRees(Zero)MatrixSemigroup
#
# since they are defined in the GAP library, and, in some sense, are
# fundamental and so it is desirable that they are stored as attributes. The
# method for IsomorphismSemigroup(IsTransformationSemigroup, S) delegates to
# IsomorphismTransformationSemigroup(S), and similarly for the other types
# listed above.
#
# If introducing a new type IsNewTypeOfSemigroup of semigroup, then the minimum
# requirement is to install a method for:
#
#     IsomorphismSemigroup(IsNewTypeOfSemigroup, IsTransformationSemigroup)
#
# and
#
#     InstallMethod(IsomorphismSemigroup,
#     "for IsNewTypeOfSemigroup and a semigroup",
#     [IsNewTypeOfSemigroup, IsSemigroup],
#     SEMIGROUPS.DefaultIsomorphismSemigroup);
#
# Since every other isomorphism can then be computed by composing with an
# isomorphism to a transformation semigroup. Of course, if a better method is
# known, then this can be installed instead. The default (right regular)
# isomorphism from a semigroup in IsNewTypeOfSemigroup to a transformation
# semigroup will be used, if no better method is installed.
#
# It is necessary that all of the methods for IsomorphismSemigroup in a given
# file have the same filter IsXSemigroup for the first argument.  (i.e.
# methods for IsomorphismSemgroup(IsXSemigroup, ...) must go in the
# corresponding file).  Also the methods for IsomorphismSemigroup must appear
# from lowest to highest rank due to the way that constructors are implemented.
# If they are not in lowest to highest rank order, then the wrong constructor
# method is selected (i.e.  the last applicable one is selected).
#
# IsomorphismMonoid is only really necessary to convert semigroups with
# MultiplicativeNeutralElement, which are not in IsMonoid, to monoids. For
# example,
#
#     gap> S := Monoid(Transformation([1, 4, 6, 2, 5, 3, 7, 8, 9, 9]),
#     >                Transformation([6, 3, 2, 7, 5, 1, 8, 8, 9, 9]));;
#     gap> AsSemigroup(IsBooleanMatSemigroup, S);
#     <monoid of 10x10 boolean matrices with 2 generators>
#     gap> AsMonoid(IsBooleanMatMonoid, S);
#     <monoid of 10x10 boolean matrices with 2 generators>
#     gap> S := Semigroup(Transformation([1, 4, 6, 2, 5, 3, 7, 8, 9, 9]),
#     >                   Transformation([6, 3, 2, 7, 5, 1, 8, 8, 9, 9]));;
#     gap> AsSemigroup(IsBooleanMatSemigroup, S);
#     <semigroup of 10x10 boolean matrices with 2 generators>
#     gap> AsMonoid(IsBooleanMatMonoid, S);
#     <monoid of 8x8 boolean matrices with 2 generators>
#
# The reason that AsSemigroup(IsBooleanMatSemigroup, S) returns a monoid, is
# that in IsomorphismSemigroup the GeneratorsOfSemigroup(S) are used to
# construct generators <gens> for the isomorphic boolean matrix semigroup. But
# GeneratorsOfSemigroup(S) contains the One(S) (since it is a monoid) and so
# when we call Semigroup(gens), Semigroup detects that one of the generators is
# the One of the others, and so returns a monoid.
#
# Note also that in the example of semigroups of pbrs, there is a good
# (semigroup) embedding of the partition monoid, but not a good monoid
# embedding. So, if you do AsSemigroup(IsPBRSemigroup, S) when S is a
# bipartition monoid, it returns a semigroup of pbrs with degree equal to the
# degree of S, whereas if you do AsMonoid(IsPBRMonoid, S), you get a monoid
# where the degree is equal to the size of S plus 1 (since this is computed by
# computing an isomorphic transformation monoid, and then this is embedded, as
# a monoid, into a monoid of pbrs).

InstallMethod(AsSemigroup, "for a filter and a semigroup",
[IsOperation, IsSemigroup],
function(filt, S)

  if Tester(filt)(S) and filt(S) then
    return S;
  elif filt = IsTransformationSemigroup then
    return Range(IsomorphismTransformationSemigroup(S));
  elif filt = IsPartialPermSemigroup then
    return Range(IsomorphismPartialPermSemigroup(S));
  elif filt = IsReesMatrixSemigroup then
    return Range(IsomorphismReesMatrixSemigroup(S));
  elif filt = IsReesZeroMatrixSemigroup then
    return Range(IsomorphismReesZeroMatrixSemigroup(S));
  elif filt = IsFpSemigroup then
    return Range(IsomorphismFpSemigroup(S));
  fi;

  return Range(IsomorphismSemigroup(filt, S));
end);

InstallMethod(AsSemigroup, "for a filter, pos int, a semigroup",
[IsOperation, IsPosInt, IsSemigroup],
{filt, threshold, S} -> Range(IsomorphismSemigroup(filt, threshold, S)));

InstallMethod(AsSemigroup,
"for a filter, pos int, pos int, a semigroup",
[IsOperation, IsPosInt, IsPosInt, IsSemigroup],
{filt, threshold, period, S}
-> Range(IsomorphismSemigroup(filt, threshold, period, S)));

InstallMethod(AsSemigroup, "for a filter, ring, and semigroup",
[IsOperation, IsRing, IsSemigroup],
{filt, R, S} -> Range(IsomorphismSemigroup(filt, R, S)));

InstallMethod(AsMonoid, "for a filter and a semigroup",
[IsOperation, IsSemigroup],
function(filt, S)

  if IsMonoid(S) and Tester(filt)(S) and filt(S) then
    return S;
  elif filt = IsTransformationMonoid then
    return Range(IsomorphismTransformationMonoid(S));
  elif filt = IsPartialPermMonoid then
    return Range(IsomorphismPartialPermMonoid(S));
  elif filt = IsFpMonoid then
    return Range(IsomorphismFpMonoid(S));
  fi;

  return Range(IsomorphismMonoid(filt, S));
end);

InstallMethod(AsMonoid, "for a filter, pos int, and a semigroup",
[IsOperation, IsPosInt, IsSemigroup],
{filt, threshold, S} -> Range(IsomorphismMonoid(filt, threshold, S)));

InstallMethod(AsMonoid,
"for a filter, pos int, pos int, and a semigroup",
[IsOperation, IsPosInt, IsPosInt, IsSemigroup],
{filt, threshold, period, S} ->
Range(IsomorphismMonoid(filt, threshold, period, S)));

InstallMethod(AsMonoid, "for a filter, ring, and semigroup",
[IsOperation, IsRing, IsSemigroup],
{filt, R, S} -> Range(IsomorphismMonoid(filt, R, S)));

#############################################################################
## 10. Operators
#############################################################################

InstallMethod(\<, "for semigroups in the same family", IsIdenticalObj,
[IsSemigroup, IsSemigroup],
function(S, T)
  local SS, TT, s, t;

  if not IsFinite(S) or not IsFinite(T) then
    TryNextMethod();
  fi;

  SS := IteratorSorted(S);
  TT := IteratorSorted(T);

  while not (IsDoneIterator(SS) or IsDoneIterator(TT)) do
    s := NextIterator(SS);
    t := NextIterator(TT);
    if s <> t then
      return s < t;
    fi;
  od;

  if IsDoneIterator(SS) and IsDoneIterator(TT) then
    return false;  # S = T
  fi;
  return IsDoneIterator(SS);
end);
