############################################################################
##
##  semigroups/semicons.gi
##  Copyright (C) 2015-2022                              James D. Mitchell
##                                                          Wilf A. Wilson
##
##  Licensing information can be found in the README file of this package.
##
############################################################################
##

# Trivial semigroup: main method

InstallGlobalFunction(TrivialSemigroup,
function(arg...)
  local S;

  if IsEmpty(arg) then
    S := TrivialSemigroupCons(IsTransformationSemigroup, 0);
  elif Length(arg) = 1 and IsInt(arg[1]) and arg[1] >= 0 then
    S := TrivialSemigroupCons(IsTransformationSemigroup, arg[1]);
  elif Length(arg) = 1 and IsOperation(arg[1]) then
    S := TrivialSemigroupCons(arg[1], 0);
  elif Length(arg) = 2 and IsOperation(arg[1]) and IsInt(arg[2])
      and arg[2] >= 0 then
    S := TrivialSemigroupCons(arg[1], arg[2]);
  else
    ErrorNoReturn("the arguments must be a non-negative integer or ",
                  "a filter and a non-negative integer");
  fi;
  SetIsTrivial(S, true);
  return S;
end);

# Trivial semigroup: constructors

InstallMethod(TrivialSemigroupCons,
"for IsTransformationSemigroup and an integer",
[IsTransformationSemigroup, IsInt],
function(_, deg)
  if deg = 0 then
    return Semigroup(IdentityTransformation);
  fi;
  return Semigroup(ConstantTransformation(deg, 1));
end);

InstallMethod(TrivialSemigroupCons,
"for IsPartialPermSemigroup and an integer",
[IsPartialPermSemigroup, IsInt],
{filt, n} -> Semigroup(PartialPerm([1 .. n])));

InstallMethod(TrivialSemigroupCons,
"for IsBipartitionSemigroup and an integer",
[IsBipartitionSemigroup, IsInt],
function(_, deg)
  local n;
  n := Maximum(deg, 1);
  return Semigroup(Bipartition([Concatenation(List([1 .. n], x -> [-x, x]))]));
end);

InstallMethod(TrivialSemigroupCons,
"for IsBlockBijectionSemigroup and an integer",
[IsBlockBijectionSemigroup, IsInt],
function(_, deg)
  local n;
  n := Maximum(deg, 1);
  return TrivialSemigroupCons(IsBipartitionSemigroup, n);
end);

InstallMethod(TrivialSemigroupCons,
"for IsPBRSemigroup and an integer",
[IsPBRSemigroup, IsInt],
function(_, deg)
  local n;
  n := Maximum(deg, 1);
  return Semigroup(IdentityPBR(n));
end);

InstallMethod(TrivialSemigroupCons,
"for IsBooleanMatSemigroup and an integer",
[IsBooleanMatSemigroup, IsInt],
function(_, deg)
  local n;
  n := Maximum(deg, 1);
  return Semigroup(BooleanMat(List([1 .. n], x -> BlistList([1 .. n], [x]))));
end);

# Trivial semigroup: other constructors

for _IsXSemigroup in ["IsFpSemigroup",
                      "IsFpMonoid",
                      "IsNTPMatrixSemigroup",
                      "IsMaxPlusMatrixSemigroup",
                      "IsMinPlusMatrixSemigroup",
                      "IsTropicalMaxPlusMatrixSemigroup",
                      "IsTropicalMinPlusMatrixSemigroup",
                      "IsProjectiveMaxPlusMatrixSemigroup",
                      "IsIntegerMatrixSemigroup",
                      "IsReesMatrixSemigroup",
                      "IsReesZeroMatrixSemigroup"] do
  InstallMethod(TrivialSemigroupCons,
  Concatenation("for ", _IsXSemigroup, " and an integer"),
  [ValueGlobal(_IsXSemigroup), IsInt],
  function(filter, deg)
    return AsSemigroup(filter,
                       TrivialSemigroupCons(IsTransformationSemigroup, deg));
  end);
od;

Unbind(_IsXSemigroup);

# Monogenic semigroup: main method

InstallGlobalFunction(MonogenicSemigroup,
function(arg...)
  local filter, m, r, S;

  if Length(arg) = 2  then
    filter := IsTransformationSemigroup;
    m := arg[1];
    r := arg[2];
  elif Length(arg) = 3 then
    filter := arg[1];
    m := arg[2];
    r := arg[3];
  fi;

  if not IsBound(m) or not IsPosInt(m) or not IsPosInt(r)
      or not IsOperation(filter) then
    ErrorNoReturn("the arguments must be 2 positive integers or a filter ",
                  "and a 2 positive integers");
  fi;

  S := MonogenicSemigroupCons(filter, m, r);

  SetSize(S, m + r - 1);
  SetIsMonogenicSemigroup(S, true);
  if m = 1 then
    SetIsGroupAsSemigroup(S, true);
  else
    SetIsGroupAsSemigroup(S, false);
    SetIsRegularSemigroup(S, false);
  fi;

  SetIsZeroSemigroup(S, r = 1 and m < 3);
  SetMinimalSemigroupGeneratingSet(S, GeneratorsOfSemigroup(S));

  return S;
end);

# Monogenic semigroup: constructors

InstallMethod(MonogenicSemigroupCons,
"for a IsTransformationSemigroup and two positive integers",
[IsTransformationSemigroup, IsPosInt, IsPosInt],
function(_, m, r)
  local t;

  t := [1 .. r] + 1;
  t[r] := 1;

  if m <> 1 then  # m = 1 specifies a cyclic group
    Append(t, [1 .. m] + r - 1);
  fi;

  return Semigroup(Transformation(t));
end);

InstallMethod(MonogenicSemigroupCons,
"for a IsPartialPermSemigroup and two positive integers",
[IsPartialPermSemigroup, IsPosInt, IsPosInt],
function(_, m, r)
  local cyclic_group, nilpotent_offset, nilpotent, im;

  if m = 1 and r = 1 then
    return Semigroup(PartialPerm([], []));
  elif r = 1 then
    cyclic_group := [];
    nilpotent_offset := 0;
  else
    cyclic_group := [1 .. r] + 1;
    cyclic_group[r] := 1;
    nilpotent_offset := r;
  fi;
  nilpotent := [1 .. m - 1] + nilpotent_offset;
  im := Concatenation(cyclic_group, [0], nilpotent);

  return Semigroup(PartialPerm(im));
end);

InstallMethod(MonogenicSemigroupCons,
"for a IsBipartitionSemigroup and two positive integers",
[IsBipartitionSemigroup, IsPosInt, IsPosInt],
{filter, m, r} -> MonogenicSemigroupCons(IsBlockBijectionSemigroup, m, r));

InstallMethod(MonogenicSemigroupCons,
"for IsBlockBijectionSemigroup and two positive integers",
[IsBlockBijectionSemigroup, IsPosInt, IsPosInt],
function(_, m, r)
  local out, offset, i;

  if m = 1 and r = 1 then
    return Semigroup(Bipartition([[1, -1]]));
  fi;

  out := [];
  if r = 1 then
    offset := 1;
  else
    for i in [1 .. r - 1] do
      Add(out, [i, -i - 1]);
    od;
    Add(out, [r, -1]);
    offset := r + 1;
  fi;

  if m <> 1 then
    Add(out, [offset, -offset, offset + 1, -(offset + m)]);
    for i in [offset + 2 .. offset + m] do
      Add(out, [i, -i + 1]);
    od;
  fi;

  return Semigroup(Bipartition(out));
end);

# Monogenic semigroup: other constructors

for _IsXSemigroup in ["IsPBRSemigroup",
                      "IsBooleanMatSemigroup",
                      "IsNTPMatrixSemigroup",
                      "IsMaxPlusMatrixSemigroup",
                      "IsMinPlusMatrixSemigroup",
                      "IsTropicalMaxPlusMatrixSemigroup",
                      "IsTropicalMinPlusMatrixSemigroup",
                      "IsProjectiveMaxPlusMatrixSemigroup",
                      "IsIntegerMatrixSemigroup",
                      "IsReesMatrixSemigroup",
                      "IsReesZeroMatrixSemigroup"] do
  InstallMethod(MonogenicSemigroupCons,
  Concatenation("for ", _IsXSemigroup, " and two positive integers"),
  [ValueGlobal(_IsXSemigroup), IsPosInt, IsPosInt],
  function(filter, m, r)
    return AsSemigroup(filter,
                       MonogenicSemigroupCons(IsTransformationSemigroup, m, r));
  end);
od;
Unbind(_IsXSemigroup);

# Rectangular band: main method

InstallGlobalFunction(RectangularBand,
function(arg...)
  local filter, m, n, S;

  if Length(arg) = 2  then
    filter := IsTransformationSemigroup;
    m := arg[1];
    n := arg[2];
  elif Length(arg) = 3 then
    filter := arg[1];
    m := arg[2];
    n := arg[3];
  fi;

  if not IsBound(m) or not IsPosInt(m) or not IsPosInt(n)
      or not IsOperation(filter) then
    ErrorNoReturn("the arguments must be 2 positive integers or a filter ",
                  "and a 2 positive integers");
  fi;

  S := RectangularBandCons(filter, m, n);

  SetSize(S, m * n);
  SetIsRectangularBand(S, true);
  SetNrRClasses(S, m);
  SetNrLClasses(S, n);
  if m <> 1 or n <> 1 then
    SetIsGroupAsSemigroup(S, false);
    SetIsZeroSemigroup(S, false);
    SetIsTrivial(S, false);
  fi;
  SetIsRightZeroSemigroup(S, m = 1);
  SetIsLeftZeroSemigroup(S, n = 1);

  return S;
end);

# Rectangular band: constructors

InstallMethod(RectangularBandCons,
"for a filter and a positive integer and positive integer",
[IsTransformationSemigroup, IsPosInt, IsPosInt],
function(filter, m, n)
  local L, R, div, gen, gens, min, out, i;

  if m = 1 then
    return RightZeroSemigroup(filter, n);
  elif n = 1 then
    return LeftZeroSemigroup(filter, m);
  fi;

  # don't do:
  # DirectProduct(LeftZeroSemigroup(m), RightZeroSemigroup(n));
  # because we know a generating set.

  L := LeftZeroSemigroup(filter, m);
  R := RightZeroSemigroup(filter, n);
  div := DegreeOfTransformationSemigroup(L);

  gen := function(l, r)
    return Transformation(Concatenation(ListTransformation(L.(l), div),
                                        ListTransformation(R.(r)) + div));
  end;

  gens := [];
  min := Minimum(m, n);

  for i in [1 .. min] do  # 'diagonal' generators
    Add(gens, gen(i, i));
  od;

  for i in [min + 1 .. n] do  # additional generators when n > m
    Add(gens, gen(1, i));
  od;

  for i in [min + 1 .. m] do  # additional generators when n < m
    Add(gens, gen(i, 1));
  od;

  out := Semigroup(gens);
  SetMinimalSemigroupGeneratingSet(out, GeneratorsOfSemigroup(out));
  return out;
end);

InstallMethod(RectangularBandCons,
"for a filter and two positive integers",
[IsBipartitionSemigroup, IsPosInt, IsPosInt],
function(_, m, n)
  local max, min, out, nrpoints, partitions, neg, i;

  max := Maximum(m, n);
  min := Minimum(m, n);
  out := EmptyPlist(max);

  # Find a small degree of partition monoid in which to embed rectangular band
  nrpoints := 1;
  while NrPartitionsSet([1 .. nrpoints]) < max do
    nrpoints := nrpoints + 1;
  od;

  partitions := PartitionsSet([1 .. nrpoints]);

  for i in [1 .. min] do
    Add(out, Bipartition(Concatenation(partitions[i], -1 * partitions[i])));
  od;

  neg := -1 * partitions[1];
  for i in [min + 1 .. m] do
    Add(out, Bipartition(Concatenation(partitions[i], neg)));
  od;

  for i in [min + 1 .. n] do
    Add(out, Bipartition(Concatenation(partitions[1], -1 * partitions[i])));
  od;

  return Semigroup(out);
end);

InstallMethod(RectangularBandCons,
"for a filter and a positive integer and positive integer",
[IsReesMatrixSemigroup, IsPosInt, IsPosInt],
function(_, m, n)
  local id, mat;

  id := ();
  mat := List([1 .. n], x -> List([1 .. m], y -> id));
  return ReesMatrixSemigroup(Group(id), mat);
end);

InstallMethod(RectangularBandCons,
"for IsPBRSemigroup and a pos int, and pos int",
[IsPBRSemigroup, IsPosInt, IsPosInt],
function(filter, m, n)
  return AsSemigroup(filter,
                     RectangularBandCons(IsBipartitionSemigroup, m, n));
end);

# Rectangular band: other constructors

for _IsXSemigroup in ["IsBooleanMatSemigroup",
                      "IsNTPMatrixSemigroup",
                      "IsMaxPlusMatrixSemigroup",
                      "IsMinPlusMatrixSemigroup",
                      "IsTropicalMaxPlusMatrixSemigroup",
                      "IsTropicalMinPlusMatrixSemigroup",
                      "IsProjectiveMaxPlusMatrixSemigroup",
                      "IsIntegerMatrixSemigroup"] do
  InstallMethod(RectangularBandCons,
  Concatenation("for ", _IsXSemigroup, ", pos int, and pos int"),
  [ValueGlobal(_IsXSemigroup), IsPosInt, IsPosInt],
  function(filter, m, n)
    return AsSemigroup(filter,
                       RectangularBandCons(IsTransformationSemigroup, m, n));
  end);
od;
Unbind(_IsXSemigroup);

# Free semilattice: main method

InstallGlobalFunction(FreeSemilattice,
function(arg...)
  local filter, n, S;

  if Length(arg) = 1  then
    filter := IsTransformationSemigroup;
    n := arg[1];
  elif Length(arg) = 2 then
    filter := arg[1];
    n := arg[2];
  else
    ErrorNoReturn("expected 2 arguments found ", Length(arg));
  fi;

  if not IsPosInt(n) or not IsOperation(filter) then
    ErrorNoReturn("the arguments must be a positive integer or a filter ",
                  "and a positive integer");
  fi;

  S := FreeSemilatticeCons(filter, n);

  if "IsMagmaWithOne" in NamesFilter(filter) then
    SetSize(S, 2 ^ n);
  else
    SetSize(S, 2 ^ n - 1);
  fi;

  SetIsSemilattice(S, true);

  return S;
end);

# Free semilattice: constructors

InstallMethod(FreeSemilatticeCons,
"for IsFpSemigroup and a pos int",
[IsFpSemigroup, IsPosInt],
function(_, n)
    local F, gen, l, i, j, commR, idemR;
    F := FreeSemigroup(n);
    gen := GeneratorsOfSemigroup(F);
    l := Length(gen);

    commR := [];
    for i in [1 .. l - 1] do
        for j in [i + 1 .. l] do
            Add(commR, [gen[i] * gen[j], gen[j] * gen[i]]);
        od;
    od;

    idemR := List(gen, x -> [x * x, x]);
    return F / Concatenation(commR, idemR);
end);

InstallMethod(FreeSemilatticeCons,
"for IsFpSemigroup and a pos int",
[IsFpMonoid, IsPosInt],
function(_, n)
    local F, gen, l, i, j, commR, idemR;
    F := FreeMonoid(n);
    gen := GeneratorsOfSemigroup(F);
    l := Length(gen);

    commR := [];
    for i in [1 .. l - 1] do
        for j in [i + 1 .. l] do
            Add(commR, [gen[i] * gen[j], gen[j] * gen[i]]);
        od;
    od;

    idemR := List(gen, x -> [x * x, x]);
    return F / Concatenation(commR, idemR);
end);

InstallMethod(FreeSemilatticeCons,
"for IsTransformationSemigroup and a pos int",
[IsTransformationSemigroup, IsPosInt],
function(_, n)
    local gen, i, L;
    gen := [];
    for i in [1 .. n] do
        L := [1 .. n + 1];
        L[i] := n + 1;
        Add(gen, Transformation(L));
    od;
    return Semigroup(gen);
end);

InstallMethod(FreeSemilatticeCons,
"for IsTransformationSemigroup and a pos int",
[IsTransformationMonoid, IsPosInt],
function(_, n)
    local gen, i, L;
    gen := [];
    for i in [1 .. n] do
        L := [1 .. n + 1];
        L[i] := n + 1;
        Add(gen, Transformation(L));
    od;
    return Monoid(gen);
end);

InstallMethod(FreeSemilatticeCons,
"for IsPartialPermSemigroup and a pos int",
[IsPartialPermSemigroup, IsPosInt],
function(_, n)
    local gen, i, L;
    gen := [];
    for i in [1 .. n] do
        L := [1 .. n];
        Remove(L, i);
        Add(gen, PartialPerm(L, L));
    od;
    return Semigroup(gen);
end);

InstallMethod(FreeSemilatticeCons,
"for IsPartialPermSemigroup and a pos int",
[IsPartialPermMonoid, IsPosInt],
function(_, n)
    local gen, i, L;
    gen := [];
    for i in [1 .. n] do
        L := [1 .. n];
        Remove(L, i);
        Add(gen, PartialPerm(L, L));
    od;
    return Monoid(gen);
end);

# Free semilattice: other constructors

for _IsXSemigroup in ["IsBipartitionSemigroup",
                      "IsPBRSemigroup",
                      "IsBooleanMatSemigroup",
                      "IsNTPMatrixSemigroup",
                      "IsMaxPlusMatrixSemigroup",
                      "IsMinPlusMatrixSemigroup",
                      "IsTropicalMaxPlusMatrixSemigroup",
                      "IsTropicalMinPlusMatrixSemigroup",
                      "IsProjectiveMaxPlusMatrixSemigroup",
                      "IsIntegerMatrixSemigroup"] do
  InstallMethod(FreeSemilatticeCons,
  Concatenation("for ", _IsXSemigroup, ", and pos int"),
  [ValueGlobal(_IsXSemigroup), IsPosInt],
  function(filter, n)
    return AsSemigroup(filter,
                       FreeSemilatticeCons(IsTransformationSemigroup, n));
  end);
od;

for _IsXMonoid in ["IsBipartitionMonoid",
                    "IsPBRMonoid",
                    "IsBooleanMatMonoid",
                    "IsNTPMatrixMonoid",
                    "IsMaxPlusMatrixMonoid",
                    "IsMinPlusMatrixMonoid",
                    "IsTropicalMaxPlusMatrixMonoid",
                    "IsTropicalMinPlusMatrixMonoid",
                    "IsProjectiveMaxPlusMatrixMonoid",
                    "IsIntegerMatrixMonoid"] do
  InstallMethod(FreeSemilatticeCons,
  Concatenation("for ", _IsXMonoid, ", and pos int"),
  [ValueGlobal(_IsXMonoid), IsPosInt],
  function(filter, n)
    return AsMonoid(filter,
                    FreeSemilatticeCons(IsTransformationMonoid, n));
  end);
od;

Unbind(_IsXSemigroup);
Unbind(_IsXMonoid);

# Zero semigroup: main method

InstallGlobalFunction(ZeroSemigroup,
function(arg...)
  local filter, n, S;

  if Length(arg) = 1  then
    filter := IsTransformationSemigroup;
    n := arg[1];
  elif Length(arg) = 2 then
    filter := arg[1];
    n := arg[2];
  fi;

  if not IsBound(n) or not IsPosInt(n) or not IsOperation(filter) then
    ErrorNoReturn("the arguments must be a positive integer or a filter ",
                  "and a positive integer");
  fi;

  S := ZeroSemigroupCons(filter, n);

  SetSize(S, n);
  SetIsZeroSemigroup(S, true);
  SetMultiplicativeZero(S, S.1 ^ 2);
  SetIsGroupAsSemigroup(S, IsTrivial(S));
  SetIsRegularSemigroup(S, IsTrivial(S));
  SetIsMonogenicSemigroup(S, n <= 2);
  SetMinimalSemigroupGeneratingSet(S, GeneratorsOfSemigroup(S));
  return S;
end);

# Zero semigroup: constructors

InstallMethod(ZeroSemigroupCons,
"for IsTransformationSemigroup and a positive integer",
[IsTransformationSemigroup, IsPosInt],
function(_, n)
  local out, max, deg, N, R, gens, im, iter, r, i;

  if n = 1 then
    out := Semigroup(Transformation([1]));
    SetMultiplicativeZero(out, out.1);
    return out;
  fi;

  # calculate the minimal possible degree
  max := 0;
  deg := 0;
  while max < n do
    deg := deg + 1;
    for r in [1 .. deg - 1] do
      N := r ^ (deg - r);
      if N > max then
        max := N;
        R := r;
      fi;
    od;
  od;

  gens := [];
  im   := [1 .. R] * 0 + 1;
  iter := IteratorOfTuples([1 .. R], deg - R);
  NextIterator(iter);  # skip the zero

  for i in [1 .. n - 1] do
    Add(gens, Transformation(Concatenation(im, NextIterator(iter))));
  od;

  out := Semigroup(gens, rec(acting := false));
  SetMultiplicativeZero(out, ConstantTransformation(deg, 1));

  return out;
end);

InstallMethod(ZeroSemigroupCons,
"for IsPartialPermSemigroup and a positive integer",
[IsPartialPermSemigroup, IsPosInt],
function(_, n)
  local zero, gens, out, i;

  zero := PartialPerm([], []);
  if n = 1 then
    gens := [zero];
  else
    gens := EmptyPlist(n - 1);
    for i in [1 .. n - 1] do
      gens[i] := PartialPerm([2 * i - 1], [2 * i]);
    od;
  fi;
  out := Semigroup(gens);
  SetMultiplicativeZero(out, zero);
  return out;
end);

InstallMethod(ZeroSemigroupCons,
"for a filter and a positive integer",
[IsBlockBijectionSemigroup, IsPosInt],
function(_, n)
  local zero, gens, points, pair, out, i;

  if n = 1 then
    zero := Bipartition([[1, -1]]);
    gens := [zero];
  elif n = 2 then
    points := Concatenation([1 .. 3], [-3 .. -1]);
    zero := Bipartition([points]);
    gens := [Bipartition([[1, -2], [-1, 2, 3, -3]])];
  else
    points := Concatenation([1 .. 2 * (n - 1)], -[1 .. 2 * (n - 1)]);
    zero := Bipartition([points]);
    gens := EmptyPlist(n - 1);
    for i in [1 .. n - 1] do
      pair := [2 * i - 1, -(2 * i)];
      gens[i] := Bipartition([pair, Difference(points, pair)]);
    od;
  fi;
  out := Semigroup(gens);
  SetMultiplicativeZero(out, zero);
  return out;
end);

InstallMethod(ZeroSemigroupCons,
"for a filter and a positive integer",
[IsBipartitionSemigroup, IsPosInt],
function(_, n)
  local zero, out;

  if n = 2 then
    zero := Bipartition([[1], [2], [-1], [-2]]);
    out := Semigroup(Bipartition([[1, -2], [2], [-1]]));
    SetMultiplicativeZero(out, zero);
    return out;
  fi;

  return AsSemigroup(IsBipartitionSemigroup,
                     ZeroSemigroupCons(IsTransformationSemigroup, n));
end);

InstallMethod(ZeroSemigroupCons,
"for a IsReesZeroMatrixSemigroup and a positive integer",
[IsReesZeroMatrixSemigroup, IsPosInt],
function(_, n)
  local mat;

  if n = 1 then
    ErrorNoReturn("there is no Rees 0-matrix semigroup of order 1");
  fi;
  mat := [[1 .. n - 1] * 0];
  return ReesZeroMatrixSemigroup(Group(()), mat);
end);

# Zero semigroup: other constructors

for _IsXSemigroup in ["IsPBRSemigroup",
                      "IsBooleanMatSemigroup",
                      "IsNTPMatrixSemigroup",
                      "IsMaxPlusMatrixSemigroup",
                      "IsMinPlusMatrixSemigroup",
                      "IsTropicalMaxPlusMatrixSemigroup",
                      "IsTropicalMinPlusMatrixSemigroup",
                      "IsProjectiveMaxPlusMatrixSemigroup",
                      "IsIntegerMatrixSemigroup"] do
  InstallMethod(ZeroSemigroupCons,
  Concatenation("for ", _IsXSemigroup, " and a positive integer"),
  [ValueGlobal(_IsXSemigroup), IsPosInt],
  function(filter, n)
    return AsSemigroup(filter,
                       ZeroSemigroupCons(IsTransformationSemigroup, n));
  end);
od;
Unbind(_IsXSemigroup);

# Left zero semigroup: main method

InstallGlobalFunction(LeftZeroSemigroup,
function(arg...)
  local filt, n, S, max, deg, N, R, gens, im, iter, r, i;

  if Length(arg) = 1 then
    filt := IsTransformationSemigroup;
    n    := arg[1];
  elif Length(arg) = 2 then
    filt := arg[1];
    n    := arg[2];
  fi;

  if not IsBound(filt) or not IsFilter(filt) or not IsPosInt(n) then
    ErrorNoReturn("the arguments must be a positive integer or ",
                  "a filter and a positive integer");
  elif n = 1 then
    return TrivialSemigroup(filt);
  elif filt <> IsTransformationSemigroup then
    S := RectangularBand(filt, n, 1);
    SetIsLeftZeroSemigroup(S, true);
    return S;
  fi;

  # calculate the minimal possible degree
  max := 0;
  deg := 0;
  while max < n do
    deg := deg + 1;
    for r in [1 .. deg - 1] do
      N := r ^ (deg - r);
      if N > max then
        max := N;
        R := r;
      fi;
    od;
  od;

  gens := [];
  im   := [1 .. R];
  iter := IteratorOfTuples([1 .. R], deg - R);

  for i in [1 .. n] do
    Add(gens, Transformation(Concatenation(im, NextIterator(iter))));
  od;

  S := Semigroup(gens, rec(acting := false));
  SetIsLeftZeroSemigroup(S, true);
  return S;
end);

# Right zero semigroup: main method

InstallGlobalFunction(RightZeroSemigroup,
function(arg...)
  local filt, n, S, max, deg, ker, add, iter, gens, i;

  if Length(arg) = 1 then
    filt := IsTransformationSemigroup;
    n    := arg[1];
  elif Length(arg) = 2 then
    filt := arg[1];
    n    := arg[2];
  fi;

  if not IsBound(filt) or not IsFilter(filt) or not IsPosInt(n) then
    ErrorNoReturn("the arguments must be a positive integer or ",
                  "a filter and a positive integer");
  elif n = 1 then
    return TrivialSemigroup(filt);
  elif filt <> IsTransformationSemigroup then
    S := RectangularBand(filt, 1, n);
    SetIsRightZeroSemigroup(S, true);
    return S;
  fi;

  # calculate the minimal possible degree
  max := 0;
  deg := 0;
  while max < n do
    deg := deg + 1;
    if (deg mod 3) = 0 then
      max := 3 ^ (deg / 3);
    elif (deg mod 3) = 1 then
      max := 4 * 3 ^ ((deg - 4) / 3);
    else
      max := 2 * 3 ^ ((deg - 2) / 3);
    fi;
  od;

  # make the first class of the kernel
  if (deg mod 3) = 0 then
    ker := [[1 .. 3]];
    add := 3;
  elif (deg mod 3) = 1 then
    ker := [[1 .. 4]];
    add := 4;
  else
    ker := [[1 .. 2]];
    add := 2;
  fi;

  # add remaining classes in kernel (all of size 3)
  while add < deg do
    Add(ker, [1 .. 3] + add);
    add := add + 3;
  od;

  iter := IteratorOfCartesianProduct(ker);

  gens := [];
  for i in [1 .. n] do
    Add(gens, TransformationByImageAndKernel(NextIterator(iter), ker));
  od;

  S := Semigroup(gens, rec(acting := false));
  SetIsRightZeroSemigroup(S, true);
  return S;
end);

InstallGlobalFunction(BrandtSemigroup,
function(arg...)
  local S;

  if Length(arg) = 1 and IsPosInt(arg[1]) then
    S := BrandtSemigroupCons(IsPartialPermSemigroup, Group(()), arg[1]);
  elif Length(arg) = 2 and IsOperation(arg[1]) and IsPosInt(arg[2]) then
    S := BrandtSemigroupCons(arg[1], Group(()), arg[2]);
  elif Length(arg) = 2 and IsGroup(arg[1]) and IsPosInt(arg[2]) then
    S := BrandtSemigroupCons(IsPartialPermSemigroup, arg[1], arg[2]);
  elif Length(arg) = 3 and IsOperation(arg[1]) and IsGroup(arg[2])
      and IsPosInt(arg[3]) then
    S := BrandtSemigroupCons(arg[1], arg[2], arg[3]);
  else
    ErrorNoReturn("the arguments must be a positive integer or a filter and",
                   " a positive integer, or  a perm group and positive ",
                   "integer, or a filter, perm group, and positive ",
                   "integer");
  fi;
  SetIsZeroSimpleSemigroup(S, true);
  SetIsBrandtSemigroup(S, true);
  return S;
end);

InstallMethod(BrandtSemigroupCons,
"for IsPartialPermSemigroup, a perm group, and a positive integer",
[IsPartialPermSemigroup, IsPermGroup, IsPosInt],
function(_, G, n)
  local gens, one, m, i, x;

  gens := [];

  if IsTrivial(G) then
    if n = 1 then
      Add(gens, PartialPerm([1], [1]));
      Add(gens, EmptyPartialPerm());
    else
      for i in [2 .. n] do
        Add(gens, PartialPerm([1], [i]));
      od;
    fi;
  else
    one := PartialPerm(MovedPoints(G), MovedPoints(G));
    for x in GeneratorsOfGroup(G) do
      Add(gens, one * x);
    od;
    m := LargestMovedPoint(G) - SmallestMovedPoint(G) + 1;
    for i in [1 .. n - 1] do
      Add(gens, PartialPerm(MovedPoints(G), MovedPoints(G) + m * i));
    od;
    if n = 1 then
      Add(gens, EmptyPartialPerm());
    fi;
  fi;

  return InverseSemigroup(gens);
end);

InstallMethod(BrandtSemigroupCons,
"for IsReesZeroMatrixSemigroup, a finite group, and a positive integer",
[IsReesZeroMatrixSemigroup, IsGroup and IsFinite, IsPosInt],
function(_, G, n)
  local mat, i;
  mat := [];
  for i in [1 .. n] do
    Add(mat, ListWithIdenticalEntries(n, 0));
    mat[i][i] := One(G);
  od;

  return ReesZeroMatrixSemigroup(G, mat);
end);

for _IsXSemigroup in ["IsTransformationSemigroup",
                      "IsBipartitionSemigroup",
                      "IsPBRSemigroup",
                      "IsBooleanMatSemigroup",
                      "IsNTPMatrixSemigroup",
                      "IsMaxPlusMatrixSemigroup",
                      "IsMinPlusMatrixSemigroup",
                      "IsTropicalMaxPlusMatrixSemigroup",
                      "IsTropicalMinPlusMatrixSemigroup",
                      "IsProjectiveMaxPlusMatrixSemigroup",
                      "IsIntegerMatrixSemigroup"] do
  InstallMethod(BrandtSemigroupCons,
  Concatenation("for ", _IsXSemigroup, " and a positive integer"),
  [ValueGlobal(_IsXSemigroup), IsPermGroup, IsPosInt],
  function(filter, G, n)
    return AsSemigroup(filter,
                       BrandtSemigroupCons(IsPartialPermSemigroup, G, n));
  end);
od;
Unbind(_IsXSemigroup);

InstallMethod(StrongSemilatticeOfSemigroups,
"for a digraph, a list, and a list",
[IsDigraph, IsList, IsList],
function(D, semigroups, homomorphisms)
  local out, pos, err, efam, etype, type, maps, n, rtclosure, paths, path, len,
   tobecomposed, firsthom, gens, i, j;

  if not IsMeetSemilatticeDigraph(DigraphReflexiveTransitiveClosure(D)) then
    ErrorNoReturn("the reflexive transitive closure of the 1st argument ",
                  "(a digraph) must be a meet semilattice");
  fi;
  pos := PositionProperty(semigroups, x -> not IsSemigroup(x));
  if pos <> fail then
    ErrorNoReturn("the 2nd argument (a list) must consist of semigroups, ",
                  "but found ", TNAM_OBJ(semigroups[pos]), " in position ",
                  pos);
  elif DigraphNrVertices(D) <> Length(semigroups) then
    err := Concatenation(
             "the 2nd argument (a list) must have length {}, ",
             "the number of vertices of the 1st argument (a digraph)",
             ", but found length {}");
    ErrorNoReturn(StringFormatted(err,
                                  DigraphNrVertices(D),
                                  Length(semigroups)));
  fi;
  pos := PositionProperty(homomorphisms, x -> not IsList(x));
  if pos <> fail then
    ErrorNoReturn("the 3rd argument (a list) must consist of lists, ",
                  "but found ",
                  TNAM_OBJ(homomorphisms[pos]),
                  " in position ",
                  pos);
  elif DigraphNrVertices(D) <> Length(homomorphisms) then
    err := Concatenation(
             "the 3rd argument (a list) must have length {}, ",
             "the number of vertices of the 1st argument (a digraph)",
             ", but found length {}");
    ErrorNoReturn(StringFormatted(err,
                  DigraphNrVertices(D),
                  Length(homomorphisms)));
  fi;

  out := OutNeighbours(D);
  for i in [1 .. DigraphNrVertices(D)] do
    if Length(homomorphisms[i]) <> Length(out[i]) then
      err := Concatenation(
               "the 3rd argument (a list) must have the same shape ",
               "as the out-neighbours of the 1st argument (a digraph), ",
               "expected shape {} but found {}");
      ErrorNoReturn(StringFormatted(err,
                                    List(out, Length),
                                    List(homomorphisms, Length)));
    fi;
    pos := PositionProperty(homomorphisms[i],
                            x -> not IsSemigroupHomomorphism(x));
    if pos <> fail then
        ErrorNoReturn("the 3rd argument (a list) must consist ",
                      "of lists of homomorphisms, but position ",
                      StringFormatted("[{}, {}]", i, pos),
                      " is not a homomorphism");
    fi;

    for j in [1 .. Length(homomorphisms[i])] do
      if Source(homomorphisms[i][j]) <> semigroups[out[i][j]] then
        err := Concatenation("expected the homomorphism in position {} of the ",
                             "3rd argument to have source equal to position {}",
                             " in the 2nd argument");
        ErrorNoReturn(StringFormatted(err, [i, j], out[i][j]));
      elif Range(homomorphisms[i][j]) <> semigroups[i] then
        err := Concatenation("expected the homomorphism in position {} of the ",
                             "3rd argument to have range equal to position {} ",
                             "in the 2nd argument");
        ErrorNoReturn(StringFormatted(err, [i, j], i));
      fi;
    od;
  od;

  efam := NewFamily("StrongSemilatticeOfSemigroupsElementsFamily", IsSSSE);
  etype := NewType(efam, IsSSSERep);

  type := NewType(CollectionsFamily(efam),
                  IsStrongSemilatticeOfSemigroups
                    and IsComponentObjectRep
                    and IsAttributeStoringRep);

  # the next section converts the list of homomorphisms into a matrix,
  # composing when necessary.
  maps := [];
  n := Length(semigroups);
  rtclosure := DigraphReflexiveTransitiveClosure(D);
  for i in [1 .. n] do
    Add(maps, []);
    for j in [1 .. n] do
      if i = j then
        # First check that if a homomorphism from i to i was defined, it
        # is the identity
        if IsDigraphEdge(D, [i, i]) then
          if homomorphisms[i][Position(OutNeighboursOfVertex(D, i), i)]
              <> IdentityMapping(semigroups[i]) then
             ErrorNoReturn("Expected homomorphism from ",
                           i, " to ", i,
                           " to be the identity");
           fi;
        fi;
        Add(maps[i], IdentityMapping(semigroups[i]));
      elif IsDigraphEdge(rtclosure, [i, j]) then
        paths        := IteratorOfPaths(D, i, j);
        path         := NextIterator(paths);
        len          := Length(path[2]);
        tobecomposed := List([1 .. len],
                              x -> homomorphisms[path[1][x]][path[2][x]]);
        firsthom     := CompositionMapping(tobecomposed);

        # now check the first composition is the same as the composition along
        # all other paths from i to j
        while not IsDoneIterator(paths) do
          path         := NextIterator(paths);
          len          := Length(path[2]);
          tobecomposed := List([1 .. len],
                                x -> homomorphisms[path[1][x]][path[2][x]]);
          if CompositionMapping(tobecomposed) <> firsthom then
            ErrorNoReturn("Composing homomorphisms along different paths from ",
                          i,
                          " to ",
                          j,
                          " does not produce the same result. The ",
                          "homomorphisms must commute");
          fi;
        od;
        # If no errors so far, then all paths commute and we can add the comp.
        Add(maps[i], firsthom);
        # TODO(later) for larger digraphs, the current method will compute some
        # compositions of homomorphisms several times.  For example, take
        # Digraph([[], [1], [1], [2, 3], [4]]). It defines a meet-semilattice
        # and the SSS initialisation will check that composing the
        # homomorphisms 1->3->4 and 1->2->4 give the same result. Later on in
        # the initialisation, it will also check equivalence of the paths
        # 1->2->4->5 and 1->3->4->5, but will not be reusing previously

        # computed information on what the composition 1->3->4 equals, say.
        # Saving the homomorphsisms already computed using some sort of dynamic
        # programming approach may improve the speed (at the cost of memory).
      else
        Add(maps[i], fail);
      fi;
    od;
  od;

  out := rec();

  gens := [];
  for i in [1 .. Length(semigroups)] do
    Append(gens, List(GeneratorsOfSemigroup(semigroups[i]),
                      x -> Objectify(etype, [out, i, x])));
  od;

  ObjectifyWithAttributes(out,
                          type,
                          SemilatticeOfStrongSemilatticeOfSemigroups,
                          rtclosure,
                          SemigroupsOfStrongSemilatticeOfSemigroups,
                          semigroups,
                          HomomorphismsOfStrongSemilatticeOfSemigroups,
                          maps,
                          ElementTypeOfStrongSemilatticeOfSemigroups,
                          etype,
                          GeneratorsOfMagma,
                          gens);
  return out;
end);

InstallMethod(Size, "for a strong semilattice of semigroups",
[IsStrongSemilatticeOfSemigroups],
{S} -> Sum(SemigroupsOfStrongSemilatticeOfSemigroups(S), Size));

InstallMethod(ViewString, "for a strong semilattice of semigroups",
[IsStrongSemilatticeOfSemigroups],
function(S)
  local size;
  size := Size(SemigroupsOfStrongSemilatticeOfSemigroups(S));
  return Concatenation("<strong semilattice of ",
                       String(size),
                       " semigroups>");
end);

InstallMethod(SSSE,
"for a strong semilattice of semigroups, a pos int, and an associative elt",
[IsStrongSemilatticeOfSemigroups, IsPosInt, IsAssociativeElement],
function(S, n, x)
  if n > Size(SemigroupsOfStrongSemilatticeOfSemigroups(S)) then
    ErrorNoReturn("expected 2nd argument to be an integer between 1 and ",
                  "the size of the semilattice, i.e. ",
                  Size(SemigroupsOfStrongSemilatticeOfSemigroups(S)));
  elif not x in SemigroupsOfStrongSemilatticeOfSemigroups(S)[n] then
    ErrorNoReturn("where S, n and x are the 1st, 2nd and 3rd arguments ",
                  "respectively, expected x to be an element of ",
                  "SemigroupsOfStrongSemilatticeOfSemigroups(S)[n]");
  fi;
  return Objectify(ElementTypeOfStrongSemilatticeOfSemigroups(S), [S, n, x]);
end);

InstallMethod(\=, "for SSSEs", IsIdenticalObj,
[IsSSSERep, IsSSSERep],
{x, y} -> x![1] = y![1] and x![2] = y![2] and x![3] = y![3]);

InstallMethod(\<, "for SSSEs", IsIdenticalObj, [IsSSSERep, IsSSSERep],
{x, y} -> (x![2] < y![2]) or (x![2] = y![2] and x![3] < y![3]));

InstallMethod(\*, "for SSSEs", IsIdenticalObj,
[IsSSSERep, IsSSSERep],
function(x, y)
  local D, meet, maps;
  D    := SemilatticeOfStrongSemilatticeOfSemigroups(x![1]);
  meet := PartialOrderDigraphMeetOfVertices(D, x![2], y![2]);
  maps := HomomorphismsOfStrongSemilatticeOfSemigroups(x![1]);
  return SSSE(x![1],
              meet,
              (x![3] ^ (maps[meet][x![2]])) * (y![3] ^ (maps[meet][y![2]])));
end);

InstallMethod(ViewString, "for a SSSE", [IsSSSERep],
x -> Concatenation("SSSE(", ViewString(x![2]), ", ", ViewString(x![3]), ")"));

InstallMethod(UnderlyingSemilatticeOfSemigroups, "for a SSSE",
[IsSSSERep], x -> x![1]);
