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

# Many of the methods in this file should probably have the filter
# HasRelationsOfFpMonoid/Semigroup added to their requirements, but for some
# reason fp semigroups and monoids don't known their relations at creation.

# All methods for fp semigroups and monoids go via the underlying congruence on
# the free semigroup and free monoid.

# TODO methods for submonoids of fp monoids and subsemigroups of fp semigroups
InstallImmediateMethod(CanUseGapFroidurePin,
IsSubsemigroupOfFpMonoid and HasGeneratorsOfSemigroup, 0,
function(T)
  local x;
  x := Representative(GeneratorsOfSemigroup(T));
  return CanUseFroidurePin(FpMonoidOfElementOfFpMonoid(x));
end);

InstallMethod(UnderlyingCongruence, "for an fp semigroup",
[IsFpSemigroup],
function(S)
  local F, R;
  F := FreeSemigroupOfFpSemigroup(S);
  R := RelationsOfFpSemigroup(S);
  return SemigroupCongruenceByGeneratingPairs(F, R);
end);

InstallMethod(UnderlyingCongruence, "for an fp monoid",
[IsFpMonoid],
function(S)
  local F, R;
  F := FreeMonoidOfFpMonoid(S);
  R := RelationsOfFpMonoid(S);
  return SemigroupCongruenceByGeneratingPairs(F, R);
end);

InstallMethod(ElementOfFpSemigroup,
"for an fp semigroup and an associative word",
[IsFpSemigroup, IsAssocWord],
{S, w} -> ElementOfFpSemigroup(FamilyObj(Representative(S)), w));

InstallMethod(ElementOfFpMonoid,
"for an fp monoid and an associative word",
[IsFpMonoid, IsAssocWord],
{M, w} -> ElementOfFpMonoid(FamilyObj(Representative(M)), w));

InstallMethod(Size, "for an fp semigroup", [IsFpSemigroup],
S -> NrEquivalenceClasses(UnderlyingCongruence(S)));

# TODO(later) more of these

InstallMethod(Size, "for an fp semigroup with nice monomorphism",
[IsFpSemigroup and HasNiceMonomorphism],
S -> Size(Range(NiceMonomorphism(S))));

InstallMethod(AsList, "for an fp semigroup with nice monomorphism",
[IsFpSemigroup and HasNiceMonomorphism],
function(S)
  local map;
  map := InverseGeneralMapping(NiceMonomorphism(S));
  return List(Enumerator(Source(map)), x -> x ^ map);
end);

InstallMethod(Enumerator, "for an fp semigroup with nice monomorphism",
[IsFpSemigroup and HasNiceMonomorphism], 100,
function(S)
  local enum;
  enum := rec();

  enum.map := NiceMonomorphism(S);

  enum.NumberElement := {enum, x} ->
    PositionCanonical(Range(enum!.map), x ^ enum!.map);

  enum.ElementNumber := function(enum, nr)
    return EnumeratorCanonical(Range(enum!.map))[nr]
      ^ InverseGeneralMapping(enum!.map);
  end;

  enum.Length := enum -> Size(S);

  enum.Membership := {x, enum} -> x ^ enum!.map in Range(enum!.map);

  enum.IsBound\[\] := {enum, nr} -> nr <= Size(S);

  return EnumeratorByFunctions(S, enum);
end);

# TODO(later) EnumeratorSorted

InstallMethod(AsSSortedList, "for an fp semigroup with nice monomorphism",
[IsFpSemigroup and HasNiceMonomorphism],
22,
function(S)
  local map;
  map := InverseGeneralMapping(NiceMonomorphism(S));
  # EnumeratorCanonical returns the elements of the Range(NiceMonomorphism(S))
  # in short-lex order, which is sorted.
  return List(EnumeratorCanonical(Source(map)), x -> x ^ map);
end);

InstallMethod(Size, "for an fp monoid", [IsFpMonoid],
S -> NrEquivalenceClasses(UnderlyingCongruence(S)));

InstallMethod(\=, "for elements of an f.p. semigroup",
IsIdenticalObj, [IsElementOfFpSemigroup, IsElementOfFpSemigroup],
function(x, y)
  local C;
  C := UnderlyingCongruence(FpSemigroupOfElementOfFpSemigroup(x));
  return CongruenceTestMembershipNC(C,
                                    UnderlyingElement(x),
                                    UnderlyingElement(y));
end);

InstallMethod(\=, "for two elements of an f.p. monoid",
IsIdenticalObj, [IsElementOfFpMonoid, IsElementOfFpMonoid],
function(x, y)
  local C;
  C := UnderlyingCongruence(FpMonoidOfElementOfFpMonoid(x));
  return CongruenceTestMembershipNC(C,
                                    UnderlyingElement(x),
                                    UnderlyingElement(y));
end);

InstallMethod(\<, "for elements of an f.p. semigroup",
IsIdenticalObj, [IsElementOfFpSemigroup, IsElementOfFpSemigroup],
function(x, y)
  local S, map, C;
  S := FpSemigroupOfElementOfFpSemigroup(x);
  if HasNiceMonomorphism(S) then
    map := NiceMonomorphism(S);
    return PositionCanonical(Range(map), x ^ map)
      < PositionCanonical(Range(map), y ^ map);
  fi;
  C := UnderlyingCongruence(S);
  return CongruenceLessNC(C, UnderlyingElement(x), UnderlyingElement(y));
end);

InstallMethod(\<, "for two elements of an f.p. monoid",
IsIdenticalObj, [IsElementOfFpMonoid, IsElementOfFpMonoid],
function(x, y)
  local C;
  C := UnderlyingCongruence(FpMonoidOfElementOfFpMonoid(x));
  return CongruenceLessNC(C, UnderlyingElement(x), UnderlyingElement(y));
end);

#############################################################################
# Methods not using the underlying congruence directly
#############################################################################

InstallMethod(ExtRepOfObj, "for an element of an fp semigroup",
[IsElementOfFpSemigroup], x -> ExtRepOfObj(UnderlyingElement(x)));

InstallMethod(ExtRepOfObj, "for an element of an fp monoid",
[IsElementOfFpMonoid], x -> ExtRepOfObj(UnderlyingElement(x)));

# TODO(later) AsSSortedList, RightCayleyDigraph, any more?
# - for both IsFpSemigroup/Monoid and IsFpSemigroup/Monoid +
# HasNiceMonomorphism

#############################################################################
# Viewing and printing - elements
#############################################################################

InstallMethod(ViewString, "for an f.p. semigroup element",
[IsElementOfFpSemigroup], String);

InstallMethod(ViewString, "for an f.p. monoid element",
[IsElementOfFpMonoid], String);

#############################################################################
# Viewing and printing - free monoids and semigroups
#############################################################################

InstallMethod(String, "for a free monoid with known generators",
[IsFreeMonoid and HasGeneratorsOfMonoid],
function(M)

  if UserPreference("semigroups", "ViewObj") <> "semigroups-pkg" then
    TryNextMethod();
  fi;

  return StringFormatted("FreeMonoidAndAssignGeneratorVars({})",
                         List(GeneratorsOfMonoid(M), String));
end);

InstallMethod(PrintString, "for a free monoid with known generators",
[IsFreeMonoid and HasGeneratorsOfMonoid], String);

BindGlobal("SEMIGROUPS_FreeSemigroupMonoidPrintObj",
function(M)

  if UserPreference("semigroups", "ViewObj") <> "semigroups-pkg" then
    TryNextMethod();
  fi;

  Print(PrintString(M));
  return;
end);

BindGlobal("SEMIGROUPS_FreeSemigroupMonoidString",
function(M)
  if UserPreference("semigroups", "ViewObj") <> "semigroups-pkg" then
    TryNextMethod();
  fi;
  return String(M);
end);

InstallMethod(PrintObj, "for a free monoid with known generators",
[IsFreeMonoid and HasGeneratorsOfMonoid],
SEMIGROUPS_FreeSemigroupMonoidPrintObj);

InstallMethod(String, "for a free semigroup with known generators",
[IsFreeSemigroup and HasGeneratorsOfSemigroup],
function(M)

  if UserPreference("semigroups", "ViewObj") <> "semigroups-pkg" then
    TryNextMethod();
  fi;

  return StringFormatted("FreeSemigroupAndAssignGeneratorVars({})",
                         List(GeneratorsOfSemigroup(M), String));
end);

InstallMethod(PrintString, "for a free semigroup with known generators",
[IsFreeSemigroup and HasGeneratorsOfSemigroup], String);

InstallMethod(PrintObj, "for a free semigroup with known generators",
[IsFreeSemigroup and HasGeneratorsOfSemigroup],
SEMIGROUPS_FreeSemigroupMonoidPrintObj);

#############################################################################
# Viewing and printing - free monoids and semigroups
#############################################################################

InstallMethod(String, "for an f.p. monoid with known generators",
[IsFpMonoid and HasGeneratorsOfMonoid],
function(M)
  local result, fam, id;

  if UserPreference("semigroups", "ViewObj") <> "semigroups-pkg" then
    TryNextMethod();
  fi;

  result := PRINT_STRINGIFY(FreeMonoidOfFpMonoid(M),
                            " / ",
                            RelationsOfFpMonoid(M));

  fam := ElementsFamily(FamilyObj(FreeMonoidOfFpMonoid(M)));

  if IsEmpty(fam!.names) then
    return result;
  fi;
  id := StringFormatted("One({})", fam!.names[1]);
  return ReplacedString(result, "<identity ...>", id);
end);

InstallMethod(PrintString, "for an f.p. monoid with known generators",
[IsFpMonoid and HasGeneratorsOfMonoid],
SEMIGROUPS_FreeSemigroupMonoidString);

InstallMethod(PrintObj, "for an f.p. monoid with known generators",
[IsFpMonoid and HasGeneratorsOfMonoid],
4,  # to beat the library method
SEMIGROUPS_FreeSemigroupMonoidPrintObj);

InstallMethod(ViewString, "for an f.p. monoid with known generators",
[IsFpMonoid and HasGeneratorsOfMonoid],
function(M)

  if UserPreference("semigroups", "ViewObj") <> "semigroups-pkg" then
    TryNextMethod();
  fi;

  return PRINT_STRINGIFY(
    StringFormatted("\>\><fp monoid with {} and {} of length {}>\<\<",
                    Pluralize(Length(GeneratorsOfMonoid(M)), "generator"),
                    Pluralize(Length(RelationsOfFpMonoid(M)), "relation"),
                    Length(M)));
end);

InstallMethod(ViewObj, "for an f.p. monoid with known generators",
[IsFpMonoid and HasGeneratorsOfMonoid],
4,  # to beat the library method
function(M)
  if UserPreference("semigroups", "ViewObj") <> "semigroups-pkg" then
    TryNextMethod();
  fi;
  Print(ViewString(M));
end);

InstallMethod(String, "for an f.p. semigroup with known generators",
[IsFpSemigroup and HasGeneratorsOfSemigroup],
function(M)

  if UserPreference("semigroups", "ViewObj") <> "semigroups-pkg" then
    TryNextMethod();
  fi;

  return PRINT_STRINGIFY(FreeSemigroupOfFpSemigroup(M),
                        " / ",
                         RelationsOfFpSemigroup(M));
end);

InstallMethod(PrintString, "for an f.p. semigroup with known generators",
[IsFpSemigroup and HasGeneratorsOfSemigroup],
SEMIGROUPS_FreeSemigroupMonoidString);

InstallMethod(PrintObj, "for an f.p. semigroup with known generators",
[IsFpSemigroup and HasGeneratorsOfSemigroup],
4,  # to beat the library method
SEMIGROUPS_FreeSemigroupMonoidPrintObj);

InstallMethod(ViewString, "for an f.p. semigroup with known generators",
[IsFpSemigroup and HasGeneratorsOfSemigroup],
function(S)
  if UserPreference("semigroups", "ViewObj") <> "semigroups-pkg" then
    TryNextMethod();
  fi;

  return PRINT_STRINGIFY(
    StringFormatted("\>\><fp semigroup with {} and {} of length {}>\<\<",
                    Pluralize(Length(GeneratorsOfSemigroup(S)), "generator"),
                    Pluralize(Length(RelationsOfFpSemigroup(S)), "relation"),
                    Length(S)));
end);

InstallMethod(ViewObj,
"for an f.p. semigroup with known generators",
[IsFpSemigroup and HasGeneratorsOfSemigroup],
4,  # to beat the library method
function(S)
  if UserPreference("semigroups", "ViewObj") <> "semigroups-pkg" then
    TryNextMethod();
  fi;
  Print(ViewString(S));
end);

InstallGlobalFunction(FreeMonoidAndAssignGeneratorVars,
function(arg...)
  local F;
  F := CallFuncList(FreeMonoid, arg);
  AssignGeneratorVariables(F);
  return F;
end);

InstallGlobalFunction(FreeSemigroupAndAssignGeneratorVars,
function(arg...)
  local F;
  F := CallFuncList(FreeSemigroup, arg);
  AssignGeneratorVariables(F);
  return F;
end);

########################################################################
# Random
########################################################################

InstallMethod(SEMIGROUPS_ProcessRandomArgsCons,
[IsFpSemigroup, IsList],
function(_, params)
  if Length(params) < 1 then  # nr gens
    params[1] := Random(1, 20);
    params[2] := Random(1, 8);
  elif Length(params) < 2 then  # degree
    params[2] := Random(1, 8);
  fi;
  if not ForAll(params, IsPosInt) then
    ErrorNoReturn("the arguments must be positive integers");
  fi;
  return params;
end);

InstallMethod(SEMIGROUPS_ProcessRandomArgsCons,
[IsFpMonoid, IsList],
{filt, params} -> SEMIGROUPS_ProcessRandomArgsCons(IsFpSemigroup, params));

# this doesn't work very well

InstallMethod(RandomSemigroupCons, "for IsFpSemigroup and a list",
[IsFpSemigroup, IsList],
function(filt, params)
  return AsSemigroup(filt,
                     CallFuncList(RandomSemigroup,
                                  Concatenation([IsTransformationSemigroup],
                                                 params)));
end);

# this doesn't work very well

InstallMethod(RandomMonoidCons, "for IsFpMonoid and a list",
[IsFpMonoid, IsList],
function(filt, params)
  return AsMonoid(filt,
                  CallFuncList(RandomMonoid,
                               Concatenation([IsTransformationMonoid],
                                              params)));
end);

# this doesn't work very well

InstallMethod(RandomInverseSemigroupCons, "for IsFpSemigroup and a list",
[IsFpSemigroup, IsList],
function(filt, params)
  return AsSemigroup(filt,
                     CallFuncList(RandomInverseSemigroup,
                                  Concatenation([IsPartialPermSemigroup],
                                                params)));
end);

# this doesn't work very well

InstallMethod(RandomInverseMonoidCons, "for IsFpMonoid and a list",
[IsFpMonoid, IsList],
function(filt, params)
  return AsMonoid(filt,
                  CallFuncList(RandomInverseMonoid,
                               Concatenation([IsPartialPermMonoid],
                                              params)));
end);

InstallMethod(IsomorphismSemigroup, "for IsFpSemigroup and a semigroup",
[IsFpSemigroup, IsSemigroup],
{filt, S} -> IsomorphismFpSemigroup(S));

InstallMethod(AsMonoid, "for an fp semigroup",
[IsFpSemigroup],
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(IsFpMonoid, S));
end);

InstallMethod(IsomorphismMonoid, "for IsFpMonoid and a semigroup",
[IsFpMonoid, IsSemigroup],
{filt, S} -> IsomorphismFpMonoid(S));

# same method for ideals

InstallMethod(IsomorphismFpSemigroup,
"for a semigroup with CanUseFroidurePin",
[CanUseFroidurePin],
function(S)
  local rules, F, A, rels, Q, B, map, inv, result;

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

  rules := RulesOfSemigroup(S);

  F := FreeSemigroup(Length(GeneratorsOfSemigroup(S)));
  A := GeneratorsOfSemigroup(F);
  rels := List(rules, x -> [EvaluateWord(A, x[1]), EvaluateWord(A, x[2])]);

  Q := F / rels;
  B := GeneratorsOfSemigroup(Q);

  map := x -> EvaluateWord(B, Factorization(S, x));
  inv := x -> MappedWord(UnderlyingElement(x), A, GeneratorsOfSemigroup(S));
  result := SemigroupIsomorphismByFunctionNC(S, Q, map, inv);
  if IsTransformationSemigroup(S) or IsPartialPermSemigroup(S)
      or IsBipartitionSemigroup(S) then
    SetNiceMonomorphism(Q, InverseGeneralMapping(result));
  fi;
  if IsTransformationSemigroup(S) then
    SetIsomorphismTransformationSemigroup(Q, InverseGeneralMapping(result));
  elif IsPartialPermSemigroup(S) then
    SetIsomorphismPartialPermSemigroup(Q, InverseGeneralMapping(result));
  fi;
  return result;
end);

# same method for ideals

InstallMethod(IsomorphismFpMonoid, "for a semigroup with CanUseFroidurePin",
[CanUseFroidurePin],
function(S)
  local sgens, mgens, F, A, start, lookup, spos, mpos, pos, rules, rels,
  convert, word, is_redundant, Q, map, inv, i, rule;

  if not IsMonoidAsSemigroup(S) then
    ErrorNoReturn("the 1st argument (a semigroup) must ",
                  "satisfy `IsMonoidAsSemigroup`");
  elif not IsFinite(S) then
    TryNextMethod();
  fi;

  sgens := GeneratorsOfSemigroup(S);
  mgens := Filtered(sgens,
                    x -> x <> MultiplicativeNeutralElement(S));

  F := FreeMonoid(Length(mgens));
  A := GeneratorsOfMonoid(F);
  start := [1 .. Length(sgens)] * 0;
  lookup := [];
  # make sure we map duplicate generators to the correct values
  for i in [1 .. Length(sgens)] do
    spos := Position(sgens, sgens[i]);
    mpos := Position(mgens, sgens[i], start[spos]);
    lookup[i] := mpos;
    if mpos <> fail then
      start[spos] := mpos;
    fi;
  od;

  pos := Position(lookup, fail);

  rules := RulesOfSemigroup(S);
  rels := [];

  # convert a word in GeneratorsOfSemigroup to a word in GeneratorsOfMonoid
  convert := function(word)
    local out, i;
    out := One(F);
    for i in word do
      if lookup[i] <> fail then
        out := out * A[lookup[i]];
      fi;
    od;
    return out;
  end;

  if mgens = sgens then
    # the identity is not a generator, so to avoid adjoining an additional
    # identity in the output, we must add a relation equating the identity with
    # a word in the generators.
    word := Factorization(S, MultiplicativeNeutralElement(S));
    Add(rels, [convert(word), One(F)]);
    # Note that the previously line depends on Factorization always giving a
    # factorization in the GeneratorsOfSemigroup(S), and not in
    # GeneratorsOfMonoid(S) if S happens to be a monoid.
  fi;

  # check if a rule is a consequence of the relation (word = one)
  is_redundant := function(rule)
    local prefix, suffix, i;
    if not IsBound(word) or Length(rule[1]) < Length(word) then
      return false;
    fi;

    # check if <word> is a prefix
    prefix := true;
    for i in [1 .. Length(word)] do
      if word[i] <> rule[1][i] then
        prefix := false;
        break;
      fi;
    od;
    if prefix then
      return rule[1]{[Length(word) + 1 .. Length(rule[1])]} = rule[2];
    fi;

    # check if <word> is a suffix
    suffix := true;
    for i in [1 .. Length(word)] do
      if word[i] <> rule[1][i] then
        suffix := false;
        break;
      fi;
    od;
    if suffix then
      return rule[1]{[1 .. Length(rule[1]) - Length(word)]} = rule[2];
    fi;
    return false;
  end;

  for rule in rules do
    # only include non-redundant rules
    if (Length(rule[1]) <> 2
        or (rule[1][1] <> pos and rule[1][Length(rule[1])] <> pos))
        and (not is_redundant(rule)) then
      Add(rels, [convert(rule[1]), convert(rule[2])]);
    fi;
  od;

  Q := F / rels;

  if sgens = mgens then
    map := x -> EvaluateWord(GeneratorsOfMonoid(Q),
                             Factorization(S, x));
  else
    map := x -> EvaluateWord(GeneratorsOfSemigroup(Q),
                             Factorization(S, x));
  fi;

  inv := function(x)
    if not IsOne(UnderlyingElement(x)) then
      return MappedWord(UnderlyingElement(x), A, mgens);
    fi;
    return MultiplicativeNeutralElement(S);
  end;

  return SemigroupIsomorphismByFunctionNC(S, Q, map, inv);
end);

InstallMethod(AssignGeneratorVariables, "for a free semigroup",
[IsFreeSemigroup],
function(S)
  DoAssignGenVars(GeneratorsOfSemigroup(S));
end);

InstallMethod(AssignGeneratorVariables, "for an free monoid",
[IsFreeMonoid],
function(S)
  DoAssignGenVars(GeneratorsOfMonoid(S));
end);

InstallMethod(AssignGeneratorVariables, "for an fp semigroup",
[IsFpSemigroup],
function(S)
  DoAssignGenVars(GeneratorsOfSemigroup(S));
end);

InstallMethod(AssignGeneratorVariables, "for an fp monoid",
[IsFpMonoid],
function(S)
  DoAssignGenVars(GeneratorsOfMonoid(S));
end);

InstallMethod(IsomorphismFpSemigroup, "for a group",
[IsGroup],
function(G)
  local iso1, inv1, iso2, inv2;
  # The next clause shouldn't be required, but for some reason in GAP 4.10 the
  # rank of the method for IsomorphismFpMonoid for IsFpGroup is lower than this
  # methods rank.
  if IsFpGroup(G) then
    TryNextMethod();
  fi;

  iso1 := IsomorphismFpGroup(G);
  inv1 := InverseGeneralMapping(iso1);
  # TODO(later) the method for IsomorphismFpSemigroup uses the generators of G
  # and their inverses, since we know that G is finite this could be avoided.
  iso2 := IsomorphismFpSemigroup(Range(iso1));
  inv2 := InverseGeneralMapping(iso2);

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

InstallMethod(IsomorphismFpSemigroup, "for a free semigroup",
[IsFreeSemigroup],
function(S)
  local T;
  T := S / [];
  return SemigroupIsomorphismByImagesNC(S,
                                        T,
                                        GeneratorsOfSemigroup(S),
                                        GeneratorsOfSemigroup(T));
end);

InstallMethod(IsomorphismFpMonoid, "for a free monoid",
[IsFreeMonoid],
function(S)
  local T;
  T := S / [];
  return SemigroupIsomorphismByImagesNC(S,
                                        T,
                                        GeneratorsOfMonoid(S),
                                        GeneratorsOfMonoid(T));
end);

InstallMethod(IsomorphismFpSemigroup, "for a free monoid",
[IsFreeMonoid],
function(S)
  local map1, map2;
  map1 := IsomorphismFpMonoid(S);
  map2 := IsomorphismFpSemigroup(Range(map1));
  return CompositionMapping(map2, map1);
end);

InstallMethod(IsomorphismFpSemigroup, "for an fp semigroup",
[IsFpSemigroup],
function(S)
  return SemigroupIsomorphismByImagesNC(S,
                                        S,
                                        GeneratorsOfSemigroup(S),
                                        GeneratorsOfSemigroup(S));
end);

InstallMethod(IsomorphismFpMonoid, "for an fp monoid",
[IsFpMonoid],
function(M)
  return SemigroupIsomorphismByImagesNC(M,
                                        M,
                                        GeneratorsOfSemigroup(M),
                                        GeneratorsOfSemigroup(M));
end);

# The next method is copied directly from the GAP library the only change is
# the return value which uses SemigroupIsomorphismByFunctionNC here but
# MagmaIsomorphismByFunctionsNC in the GAP library; comments are also removed
# and other superficial changes to adhere to Semigroups package conventions.

InstallMethod(IsomorphismFpSemigroup, "for an fp group", [IsFpGroup],
function(G)
  local freegp, gensfreegp, freesmg, gensfreesmg, idgen, newrels, rels, smgrel,
  semi, gens, isomfun, id, invfun, i, rel;

  freegp := FreeGroupOfFpGroup(G);

  gensfreegp := List(GeneratorsOfSemigroup(freegp), String);
  freesmg := FreeSemigroup(gensfreegp{[1 .. Length(gensfreegp)]});

  gensfreesmg := GeneratorsOfSemigroup(freesmg);
  idgen := gensfreesmg[1];

  newrels := [[idgen * idgen, idgen]];
  for i in [2 .. Length(gensfreesmg)] do
    Add(newrels, [idgen * gensfreesmg[i], gensfreesmg[i]]);
    Add(newrels, [gensfreesmg[i] * idgen, gensfreesmg[i]]);
  od;

  # then relations gens * gens ^ -1 = idgen (and the other way around)
  for i in [2 .. Length(gensfreesmg)] do
    if IsOddInt(i) then
      Add(newrels, [gensfreesmg[i] * gensfreesmg[i - 1], idgen]);
    else
      Add(newrels, [gensfreesmg[i] * gensfreesmg[i + 1], idgen]);
    fi;
  od;

  rels := RelatorsOfFpGroup(G);
  for rel in rels do
     smgrel := [Gpword2MSword(idgen, rel, 1), idgen];
     Add(newrels, smgrel);
  od;

  # finally create the fp semigroup
  semi := FactorFreeSemigroupByRelations(freesmg, newrels);
  gens := GeneratorsOfSemigroup(semi);

  isomfun := x -> ElementOfFpSemigroup(FamilyObj(gens[1]),
                  Gpword2MSword(idgen, UnderlyingElement(x), 1));

  id := One(freegp);
  invfun := x -> ElementOfFpGroup(FamilyObj(One(G)),
              MSword2gpword(id, UnderlyingElement(x), 1));

  return SemigroupIsomorphismByFunctionNC(G, semi, isomfun, invfun);
end);

# The next method is copied directly from the GAP library the only change is
# the return value which uses SemigroupIsomorphismByFunctionNC here but
# MagmaIsomorphismByFunctionsNC in the GAP library; comments are also removed
# and other superficial changes to adhere to Semigroups package conventions.

InstallMethod(IsomorphismFpMonoid, "for an fp group", [IsFpGroup],
function(G)
  local freegp, gens, mongens, s, t, p, freemon, gensmon, id, newrels, rels, w,
  monrel, mon, monfam, isomfun, idg, invfun, hom, i, j, rel;

  freegp := FreeGroupOfFpGroup(G);
  gens := GeneratorsOfGroup(G);

  mongens := [];
  for i in gens do
    s := String(i);
    Add(mongens, s);
    if ForAll(s, x -> x in CHARS_UALPHA or x in CHARS_LALPHA) then
      # inverse: change casification
      t := "";
      for j in [1 .. Length(s)] do
        p := Position(CHARS_LALPHA, s[j]);
        if p <> fail then
          Add(t, CHARS_UALPHA[p]);
        else
          p := Position(CHARS_UALPHA, s[j]);
          Add(t, CHARS_LALPHA[p]);
        fi;
      od;
      s := t;
    else
      s := Concatenation(s, "^-1");
    fi;
    Add(mongens, s);
  od;

  freemon := FreeMonoid(mongens);
  gensmon := GeneratorsOfMonoid(freemon);
  id := Identity(freemon);
  newrels := [];
  # inverse relators
  for i in [1 .. Length(gens)] do
    Add(newrels, [gensmon[2 * i - 1] * gensmon[2 * i], id]);
    Add(newrels, [gensmon[2 * i] * gensmon[2 * i - 1], id]);
  od;

  rels := ValueOption("relations");
  if rels = fail then
    rels := RelatorsOfFpGroup(G);
    for rel in rels do
      w := rel;
      w := GroupwordToMonword(id, w);
      monrel := [w, id];
      Add(newrels, monrel);
    od;
  else
    if not ForAll(Flat(rels), x -> x in FreeGroupOfFpGroup(G)) then
      Info(InfoFpGroup, 1, "Converting relation words into free group");
      rels := List(rels, i -> List(i, UnderlyingElement));
    fi;
    for rel in rels do
      Add(newrels, List(rel, x -> GroupwordToMonword(id, x)));
    od;
  fi;

  mon := FactorFreeMonoidByRelations(freemon, newrels);
  gens := GeneratorsOfMonoid(mon);
  monfam := FamilyObj(Representative(mon));

  isomfun := x -> ElementOfFpMonoid(monfam,
                  GroupwordToMonword(id, UnderlyingElement(x)));

  idg := One(freegp);
  invfun := x -> ElementOfFpGroup(FamilyObj(One(G)),
     MonwordToGroupword(idg, UnderlyingElement(x)));
  hom := SemigroupIsomorphismByFunctionNC(G, mon, isomfun, invfun);
  hom!.type := 1;
  if not HasIsomorphismFpMonoid(G) then
    SetIsomorphismFpMonoid(G, hom);
  fi;
  return hom;
end);

# The next method is copied directly from the GAP library the only change is
# the return value which uses SemigroupIsomorphismByFunctionNC here but
# MagmaIsomorphismByFunctionsNC in the GAP library; comments are also removed
# and other superficial changes to adhere to Semigroups package conventions.

InstallMethod(IsomorphismFpSemigroup, "for an fp monoid", [IsFpMonoid],
function(M)
  local AddToExtRep, FMtoFS, FStoFM, FM, FS, id, rels, next, S, map, inv, x,
  rel;

  AddToExtRep := function(w, id, val)
    local wlist, i;

    wlist := ExtRepOfObj(w);
    wlist := ShallowCopy(wlist);
    for i in [1 .. 1 / 2 * (Length(wlist))] do
      wlist[2 * i - 1] := wlist[2 * i - 1] + val;
    od;

    return ObjByExtRep(FamilyObj(id), wlist);
  end;

  FMtoFS := function(id, w)
    if IsOne(w) then
      return id;
    fi;
    return AddToExtRep(w, id, 1);
  end;

  FStoFM := function(id, w)
    if ExtRepOfObj(w) = [1, 1] then
      return id;
    fi;
    return AddToExtRep(w, id, -1);
  end;

  FM := FreeMonoidOfFpMonoid(M);
  FS := FreeSemigroup(List(GeneratorsOfSemigroup(FM), String));

  id := FS.(Position(GeneratorsOfSemigroup(FM), One(FM)));

  rels := [[id * id, id]];
  for x in GeneratorsOfSemigroup(FS) do
    if x <> id then
      Add(rels, [id * x, x]);
      Add(rels, [x * id, x]);
    fi;
  od;

  for rel in RelationsOfFpMonoid(M) do
    next := [FMtoFS(id, rel[1]), FMtoFS(id, rel[2])];
    Add(rels, next);
  od;

  S := FS / rels;

  map := x -> ElementOfFpSemigroup(FamilyObj(S.1),
                                   FMtoFS(id, UnderlyingElement(x)));

  inv := x -> Image(NaturalHomomorphismByGenerators(FM, M),
                    FStoFM(One(FM), UnderlyingElement(x)));

  return SemigroupIsomorphismByFunctionNC(M, S, map, inv);
end);

InstallMethod(IsomorphismFpMonoid, "for a group", [IsGroup],
function(G)
  local iso1, inv1, iso2, inv2;
  # The next clause shouldn't be required, but for some reason in GAP 4.10 the
  # rank of the method for IsomorphismFpMonoid for IsFpGroup is lower than this
  # methods rank.
  if IsFpGroup(G) then
    TryNextMethod();
  fi;
  iso1 := IsomorphismFpGroup(G);
  inv1 := InverseGeneralMapping(iso1);
  # TODO(later) the method for IsomorphismFpMonoid uses the generators of G and
  # their inverses, since we know that G is finite this could be avoided.
  iso2 := IsomorphismFpMonoid(Range(iso1));
  inv2 := InverseGeneralMapping(iso2);

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

SEMIGROUPS.ExtRepObjToWord := function(ext_rep_obj)
  local n, word, val, pow, i;
  n    := Length(ext_rep_obj);
  word := [];
  for i in [1, 3 .. n - 1] do
    val := ext_rep_obj[i];
    pow := ext_rep_obj[i + 1];
    while pow > 0 do
      Add(word, val);
      pow := pow - 1;
    od;
  od;
  return word;
end;

SEMIGROUPS.WordToExtRepObj := function(word)
  local ext_rep_obj, i, j;
  ext_rep_obj := [];
  i           := 1;
  j           := 1;

  while i <= Length(word) do
    Add(ext_rep_obj, word[i]);
    Add(ext_rep_obj, 1);
    i := i + 1;
    while i <= Length(word) and word[i] = ext_rep_obj[j] do
      ext_rep_obj[j + 1] := ext_rep_obj[j + 1] + 1;
      i := i + 1;
    od;
    j := j + 2;
  od;
  return ext_rep_obj;
end;

SEMIGROUPS.ExtRepObjToString := function(ext_rep_obj)
  local alphabet, out, i;
  alphabet := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
  out := "";
  for i in [1, 3 .. Length(ext_rep_obj) - 1] do
    if ext_rep_obj[i] > Length(alphabet) then
      ErrorNoReturn("the maximum value in an odd position of the ",
                    "argument must be at most ", Length(alphabet));
    fi;
    Add(out, alphabet[ext_rep_obj[i]]);
    if ext_rep_obj[i + 1] > 1 then
      Append(out, " ^ ");
      Append(out, String(ext_rep_obj[i + 1]));
    fi;
  od;
  return out;
end;

SEMIGROUPS.WordToString := function(word)
  local alphabet, out, letter;
  alphabet := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
  out := "";
  for letter in word do
    if letter > Length(alphabet) then
      ErrorNoReturn("the argument be at most ", Length(alphabet));
    fi;
    Add(out, alphabet[letter]);
  od;
  return out;
end;

# This method is based on the following paper
# Presentations of Factorizable Inverse Monoids
# David Easdown, James East, and D. G. FitzGerald
# July 19, 2004
InstallMethod(IsomorphismFpSemigroup,
"for an inverse partial perm semigroup",
[IsPartialPermSemigroup and IsInverseActingSemigroupRep],
function(M)
  local add_to_odd_positions, S, SS, G, GG, s, g, F, rels, fam, alpha,
  beta, lhs, rhs, map, H, o, comp, U, rhs_list, conj, MF, T, inv, rel, x, y, m,
  i;

  if not IsFactorisableInverseMonoid(M) then
    TryNextMethod();
  fi;

  add_to_odd_positions := function(list, s)
    list{[1, 3 .. Length(list) - 1]} := list{[1, 3 .. Length(list) - 1]} + s;
    return list;
  end;

  S := Semigroup(IdempotentGeneratedSubsemigroup(M));
  SS := GeneratorsOfSemigroup(S);
  G := GroupOfUnits(M);
  GG := GeneratorsOfSemigroup(G);
  s := Length(GeneratorsOfSemigroup(S));
  g := Length(GeneratorsOfSemigroup(G));

  F := FreeSemigroup(s + g);
  rels := [];
  fam := ElementsFamily(FamilyObj(F));

  # R_S - semigroup relations for the idempotent generated subsemigroup
  alpha := IsomorphismFpSemigroup(S);
  for rel in RelationsOfFpSemigroup(Image(alpha)) do
    Add(rels, [ObjByExtRep(fam, ExtRepOfObj(rel[1])),
               ObjByExtRep(fam, ExtRepOfObj(rel[2]))]);
  od;

  # R_G - semigroup relations for the group of units
  beta  := IsomorphismFpSemigroup(G);
  for rel in RelationsOfFpSemigroup(Image(beta)) do
    lhs := add_to_odd_positions(ShallowCopy(ExtRepOfObj(rel[1])), s);
    rhs := add_to_odd_positions(ShallowCopy(ExtRepOfObj(rel[2])), s);
    Add(rels, [ObjByExtRep(fam, lhs), ObjByExtRep(fam, rhs)]);
  od;

  # R_product - see page 4 of the paper
  for x in [1 .. s] do
    for y in [1 .. g] do
      rhs := Factorization(S, SS[x] ^ (GG[y] ^ -1));
      Add(rels, [F.(s + y) * F.(x),
                 EvaluateWord(GeneratorsOfSemigroup(F), rhs) * F.(s + y)]);
    od;
  od;

  map := InverseGeneralMapping(IsomorphismPermGroup(G));
  H   := Source(map);
  o   := Enumerate(LambdaOrb(M));
  # R_tilde - see page 4 of the paper
  for m in [2 .. Length(OrbSCC(o))] do
    comp := OrbSCC(o)[m];
    U := SmallGeneratingSet(Stabilizer(H,
                                       PartialPerm(o[comp[1]], o[comp[1]]),
                                       OnRight));

    for i in comp do
      rhs_list := Factorization(S, PartialPerm(o[i], o[i]));
      rhs := EvaluateWord(GeneratorsOfSemigroup(F), rhs_list);
      conj := MappingPermListList(o[comp[1]], o[i]);
      for x in List(U, x -> x ^ conj) do
        lhs := ShallowCopy(rhs_list);
        Append(lhs, Factorization(G, x ^ map) + s);
        Add(rels, [EvaluateWord(GeneratorsOfSemigroup(F), lhs), rhs]);
      od;
    od;
  od;

  # Relation to identify One(G) and One(S)
  Add(rels, [EvaluateWord(GeneratorsOfSemigroup(F),
                          Factorization(G, One(G)) + s),
             EvaluateWord(GeneratorsOfSemigroup(F),
                          Factorization(S, One(S)))]);

  MF := F / rels;  # FpSemigroup which is isomorphic to M, with different gens.
  fam := ElementsFamily(FamilyObj(MF));
  T := Semigroup(Concatenation(SS, GG));  # M with isomorphic generators to MF
  map := x -> ElementOfFpSemigroup(fam, EvaluateWord(GeneratorsOfSemigroup(F),
                                                     Factorization(T, x)));
  inv := x -> EvaluateWord(GeneratorsOfSemigroup(T),
                           SEMIGROUPS.ExtRepObjToWord(ExtRepOfObj(x)));

  return SemigroupIsomorphismByFunctionNC(M, MF, map, inv);
end);

InstallMethod(ParseRelations,
"for a list of free generators and a string",
[IsDenseList, IsString],
function(gens, inputstring)
    local newinputstring, g, chartoel, RemoveBrackets, ParseRelation, output,
    chars;

    for g in gens do
      g := String(g);
      if Size(g) <> 1 or not IsAlphaChar(g[1]) then
        ErrorNoReturn(
        "expected the 1st argument to be a list of a free semigroup or monoid ",
        "generators represented by a single alphabet letter but found ",
        String(g));
      fi;
    od;

    newinputstring := Filtered(inputstring, x -> x <> ' ');
    chars := List(gens, x -> String(x)[1]);
    if PositionSublist(newinputstring, "=1") <> fail then
      Add(chars, '1');
    fi;

    for g in chars do
      newinputstring := ReplacedString(newinputstring,
                                       [g, '^'],
                                       ['(', g, ')', '^']);
      newinputstring := ReplacedString(newinputstring,
                                       Concatenation(['(', g, ')'], "^1="),
                                       ['(', g, ')', '=']);
      newinputstring := ReplacedString(newinputstring,
                                       Concatenation(['(', g, ')'], "^1)"),
                                       ['(', g, ')', ')']);
      newinputstring := ReplacedString(newinputstring,
                                       Concatenation(['(', g, ')'], "^1("),
                                       ['(', g, ')', '(']);

    od;

    RemoveBrackets := function(word)
        local i, product, lbracket, rbracket, nestcount, index, p, chartoel;
        if word = "" then
            ErrorNoReturn("expected the 2nd argument to be",
                          " a string listing the relations of a semigroup",
                          " but found an = symbol which isn't pairing two",
                          " words");
        fi;

        # if the number of left brackets is different from the number of right
        # brackets they can't possibly pair up
        if Number(word, x -> x = '(') <> Number(word, x -> x = ')') then
            ErrorNoReturn("expected the number of open brackets",
                          " to match the number of closed brackets");
        fi;

        # if the ^ is at the end of the string there is no exponent.
        # if the ^ is at the start of the string there is no base.
        if word[1] = '^' then
            ErrorNoReturn("expected ^ to be preceded by a ) or",
                          " a generator but found beginning of string");
        elif word[Size(word)] = '^' then
            ErrorNoReturn("expected ^ to be followed by a ",
                          "positive integer but found end of string");
        fi;
        # checks that all ^s have an exponent.
        for index in [1 .. Size(word)] do
            if word[index] = '^' then
                if not word[index + 1] in "0123456789" then
                  ErrorNoReturn("expected ^ to be followed by",
                  " a positive integer but found ", [word[index + 1]]);
                fi;
                if word[index - 1] in "0123456789^(" then
                  ErrorNoReturn(
                  "expected ^ to be preceded by a ) or a generator",
                  " but found ", [word[index - 1]]);
                fi;
            fi;
        od;

        # converts a character to the element it represents
        chartoel := function(char)
            local i;
            for i in [1 .. Size(gens)] do
                if char = String(gens[i])[1] then
                    return gens[i];
                fi;
            od;
            if char = '1' and IsAssocWordWithOne(gens[1]) then
              return One(gens);
            fi;
            ErrorNoReturn("expected a free semigroup generator",
                          " but found ", [char]);
        end;

        # i acts as a pointer to positions in the string.
        product := "";
        i := 1;
        while i <= Size(word) do
            # if there are no brackets the character is left as it is.
            if word[i] <> '(' then
                if product = "" then
                  product := chartoel(word[i]);
                else
                  product := product * chartoel(word[i]);
                fi;
            else
                lbracket := i;
                rbracket := -1;
                # tracks how 'deep' the position of i is in terms of nested
                # brackets
                nestcount := 0;
                i := i + 1;
                while i <= Size(word) do
                    if word[i] = '(' then
                        nestcount := nestcount + 1;
                    elif word[i] = ')' then
                        if nestcount = 0 then
                            rbracket := i;
                            break;
                        else
                            nestcount := nestcount - 1;
                        fi;
                    fi;
                    i := i + 1;
                od;
                # if rbracket is not followed by ^ then the value inside the
                # bracket is appended (recursion is used to remove any brackets
                # in this value)
                if rbracket = Size(word) or (not word[rbracket + 1] = '^') then
                    if product = "" then
                      product := RemoveBrackets(
                                 word{[lbracket + 1 .. rbracket - 1]});
                    else
                      product := product *
                                 RemoveBrackets(
                                 word{[lbracket + 1 .. rbracket - 1]});
                    fi;
                # if rbracket is followed by ^ then the value inside the
                # bracket is appended the given number of time
                else
                    i := i + 2;
                    while i <= Size(word) do
                        if word[i] in "0123456789" then
                            i := i + 1;
                        else
                            break;
                        fi;
                    od;

                    p := Int(word{[rbracket + 2 .. i - 1]});
                    if p = 0 then
                      ErrorNoReturn("expected ^ to be followed",
                                    " by a positive integer but found 0");
                    fi;
                    if product = "" then
                       product := RemoveBrackets(word{[lbracket + 1 ..
                                                       rbracket - 1]}) ^ p;
                    else
                       product := product *
                                  RemoveBrackets(word{[lbracket + 1 ..
                                                       rbracket - 1]}) ^ p;
                    fi;
                    i := i - 1;
                fi;
            fi;
            i := i + 1;
        od;
        return product;
    end;

    ParseRelation := x -> List(SplitString(x, "="), RemoveBrackets);
    output := List(SplitString(newinputstring, ","), ParseRelation);
    if ForAny(output, x -> Size(x) = 1) then
      ErrorNoReturn("expected the 2nd argument to be",
                    " a string listing the relations of a semigroup",
                    " but found an = symbol which isn't pairing two",
                    " words");
    fi;
    output := Filtered(output, x -> Size(x) >= 2);
    output := List(output,
                   x -> List([1 .. Size(x) - 1], y -> [x[y], x[y + 1]]));
    return Concatenation(output);
end);

InstallMethod(Factorization, "for an fp semigroup and element",
IsCollsElms, [IsFpSemigroup, IsElementOfFpSemigroup],
{S, x} -> SEMIGROUPS.ExtRepObjToWord(ExtRepOfObj(x)));

# Returns a factorization of the semigroup generators of S, not the monoid
# generators !!!
InstallMethod(Factorization, "for an fp monoid and element",
IsCollsElms, [IsFpMonoid, IsElementOfFpMonoid],
function(_, x)
  local y;
  y := ExtRepOfObj(x);
  if IsEmpty(y) then
    return [1];
  else
    return SEMIGROUPS.ExtRepObjToWord(y) + 1;
  fi;
end);

InstallMethod(Factorization, "for a free semigroup and word",
[IsFreeSemigroup, IsWord],
{S, x} -> SEMIGROUPS.ExtRepObjToWord(ExtRepOfObj(x)));

InstallMethod(MinimalFactorization, "for a free semigroup and word",
[IsFreeSemigroup, IsWord], Factorization);

# Returns a factorization of the semigroup generators of S, not the monoid
# generators !!!
InstallMethod(Factorization, "for a free monoid and word",
[IsFreeMonoid, IsWord],
function(_, x)
  if IsOne(x) then
    return [1];
  else
    return SEMIGROUPS.ExtRepObjToWord(ExtRepOfObj(x)) + 1;
  fi;
end);

InstallMethod(MinimalFactorization, "for a free monoid and word",
[IsFreeMonoid, IsWord], Factorization);

InstallMethod(Length, "for an fp semigroup", [IsFpSemigroup],
function(S)
  return Length(GeneratorsOfSemigroup(S))
    + Sum(RelationsOfFpSemigroup(S), x -> Length(x[1]) + Length(x[2]));
end);

InstallMethod(Length, "for an fp monoid", [IsFpMonoid],
function(S)
  return Length(GeneratorsOfMonoid(S))
    + Sum(RelationsOfFpMonoid(S), x -> Length(x[1]) + Length(x[2]));
end);

InstallMethod(ReversedOp, "for an element of an fp semigroup",
[IsElementOfFpSemigroup],
function(word)
  local rev;
  rev := Reversed(UnderlyingElement(word));
  return ElementOfFpSemigroup(FamilyObj(word), rev);
end);

InstallMethod(ReversedOp, "for an element of an fp monoid",
[IsElementOfFpMonoid],
function(word)
  local rev;
  rev := Reversed(UnderlyingElement(word));
  return ElementOfFpMonoid(FamilyObj(word), rev);
end);

InstallMethod(EmbeddingFpMonoid, "for an fp semigroup",
[IsFpSemigroup],
function(S)
  local F, R, map, T, hom, rel;

  if HasIsMonoidAsSemigroup(S) and IsMonoidAsSemigroup(S) then
    return IsomorphismFpMonoid(S);
  fi;

  F := FreeSemigroupOfFpSemigroup(S);
  F := FreeMonoid(List(GeneratorsOfSemigroup(F), String));
  R := [];

  map := x -> ObjByExtRep(ElementsFamily(FamilyObj(F)), ExtRepOfObj(x));

  for rel in RelationsOfFpSemigroup(S) do
    Add(R, [map(rel[1]), map(rel[2])]);
  od;

  T := F / R;
  hom := SemigroupHomomorphismByImages_NC(S,
                                          T,
                                          GeneratorsOfSemigroup(S),
                                          GeneratorsOfMonoid(T));
  SetIsInjective(hom, true);
  return hom;
end);
