#############################################################################
##
##  homomorph.gi
##  Copyright (C) 2022                               Artemis Konstantinidi
##                                                         Chinmaya Nagpal
##
##  Licensing information can be found in the README file of this package.
##
#############################################################################
##

# This file contains various methods for representing homomorphisms between
# semigroups

InstallMethod(SemigroupHomomorphismByImages,
"for two semigroups and two lists",
[IsSemigroup, IsSemigroup, IsList, IsList],
function(S, T, gens, imgs)
  local original_gens, U, map, R, rel;

  if not ForAll(gens, x -> x in S) then
    ErrorNoReturn("the 3rd argument (a list) must consist of elements ",
                  "of the 1st argument (a semigroup)");
  elif Semigroup(gens) <> S then
    ErrorNoReturn("the 1st argument (a semigroup) is not generated by ",
                  "the 3rd argument (a list)");
  elif not ForAll(imgs, x -> x in T) then
    ErrorNoReturn("the 4th argument (a list) must consist of elements ",
                  "of the 2nd argument (a semigroup)");
  elif Size(gens) <> Size(imgs) then
    ErrorNoReturn("the 3rd argument (a list) and the 4th argument ",
                  "(a list) are not the same size");
  fi;

  # in case of different generators, do:
  # gens -> original generators (as passed to Semigroup function)
  # imgs -> images of original generators
  original_gens := GeneratorsOfSemigroup(S);
  if original_gens <> gens then
    U := Semigroup(gens);
    # Use MinimalFactorization rather than Factorization because
    # MinimalFactorization is guaranteed to return a list of positive integers,
    # but Factorization is not (i.e. if S is an inverse acting semigroup.
    # Also since we require an IsomorphismFpSemigroup, there's no additional
    # cost to using MinimalFactorization instead of Factorization.
    imgs := List(original_gens,
                 x -> EvaluateWord(imgs, MinimalFactorization(U, x)));
    gens := original_gens;
  fi;

  # maps S to a finitely presented semigroup
  map := IsomorphismFpSemigroup(S);   # S (source) -> fp semigroup (range)
  # List of relations of the above finitely presented semigroup (hence of S)
  R   := RelationsOfFpSemigroup(Range(map));

  # check that each relation is satisfied by the elements imgs
  for rel in R do
    rel := List(rel, x -> SEMIGROUPS.ExtRepObjToWord(ExtRepOfObj(x)));
    if EvaluateWord(imgs, rel[1]) <> EvaluateWord(imgs, rel[2]) then
      return fail;
    fi;
  od;

  return SemigroupHomomorphismByImages_NC(S, T, gens, imgs);
end);

InstallMethod(SemigroupHomomorphismByImages,
"for two transformation semigroups and two transformation collections",
[IsTransformationSemigroup and IsActingSemigroup,
 IsTransformationSemigroup and IsActingSemigroup,
 IsTransformationCollection and IsList,
 IsTransformationCollection and IsList],
function(S, T, gens, imgs)
  local original_gens, U, S1, T1, SxT, embS, embT, K, i;

  if not ForAll(gens, x -> x in S) then
    ErrorNoReturn("the 3rd argument (a list) must consist of elements ",
                  "of the 1st argument (a semigroup)");
  elif Semigroup(gens) <> S then
    ErrorNoReturn("the 1st argument (a semigroup) is not generated by ",
                  "the 3rd argument (a list)");
  elif not ForAll(imgs, x -> x in T) then
    ErrorNoReturn("the 4th argument (a list) must consist of elements ",
                  "of the 2nd argument (a semigroup)");
  elif Size(gens) <> Size(imgs) then
    ErrorNoReturn("the 3rd argument (a list) and the 4th argument ",
                  "(a list) are not the same size");
  fi;

  # in case of different generators, do:
  # gens -> original generators (as passed to Semigroup function)
  # imgs -> images of original generators
  original_gens := GeneratorsOfSemigroup(S);
  if original_gens <> gens then
    U := Semigroup(gens, rec(acting := true));
    # Use Factorization not MinimalFactorization because it might be quicker,
    # and there's no danger of negative numbers since S and T are both
    # transformation semigroups.
    imgs := List(original_gens, x -> EvaluateWord(imgs, Factorization(U, x)));
    gens := original_gens;
  fi;

  if not IsMonoidAsSemigroup(S) then
    S1 := Monoid(S);
  else
    S1 := S;
  fi;

  if not IsMonoidAsSemigroup(T) then
    T1 := Monoid(T);
  else
    T1 := T;
  fi;

  SxT  := DirectProduct(S1, T1);
  embS := Embedding(SxT, 1);
  embT := Embedding(SxT, 2);
  K    := [];

  for i in [1 .. Size(gens)] do
    Add(K, gens[i] ^ embS * imgs[i] ^ embT);
  od;
  K := Semigroup(K, rec(acting := true));

  # TODO(later) stop this loop as soon as K exceeds S in size:
  if Size(K) <> Size(S) then
    return fail;
  fi;
  return SemigroupHomomorphismByImages_NC(S, T, gens, imgs);
end);

InstallMethod(SemigroupHomomorphismByImages, "for two semigroups and one list",
[IsSemigroup, IsSemigroup, IsList],
{S, T, imgs}
-> SemigroupHomomorphismByImages(S, T, GeneratorsOfSemigroup(S), imgs));

InstallMethod(SemigroupHomomorphismByImages, "for two semigroups",
[IsSemigroup, IsSemigroup],
function(S, T)
  return SemigroupHomomorphismByImages(S,
                                       T,
                                       GeneratorsOfSemigroup(S),
                                       GeneratorsOfSemigroup(T));
end);

InstallMethod(SemigroupHomomorphismByImages, "for a semigroup and two lists",
[IsSemigroup, IsList, IsList],
{S, gens, imgs}
-> SemigroupHomomorphismByImages(S, Semigroup(imgs), gens, imgs));

InstallMethod(SemigroupIsomorphismByImages, "for two semigroup and two lists",
[IsSemigroup, IsSemigroup, IsList, IsList],
function(S, T, gens, imgs)
  local hom;
  # TODO(Homomorph): we could check for other isomorphism invariants here, like
  # we require that gens, and imgs are duplicate free for example, and that S
  # and T have the same size etc
  hom := SemigroupHomomorphismByImages(S, T, gens, imgs);
  if hom <> fail and IsBijective(hom) then
    return hom;
  fi;
  return fail;
end);

InstallMethod(SemigroupIsomorphismByImagesNC, "for two semigroup and two lists",
[IsSemigroup, IsSemigroup, IsList, IsList],
function(S, T, gens, imgs)
  local iso;
  iso := Objectify(NewType(GeneralMappingsFamily(ElementsFamily(FamilyObj(S)),
                                               ElementsFamily(FamilyObj(T))),
                           IsSemigroupHomomorphismByImages and IsBijective),
                           rec());
  SetSource(iso, S);
  SetRange(iso, T);
  SetMappingGeneratorsImages(iso, [Immutable(gens), Immutable(imgs)]);
  return iso;
end);

InstallMethod(SemigroupIsomorphismByImages, "for two semigroups and one list",
[IsSemigroup, IsSemigroup, IsList],
{S, T, imgs}
-> SemigroupIsomorphismByImages(S, T, GeneratorsOfSemigroup(S), imgs));

InstallMethod(SemigroupIsomorphismByImages, "for two semigroups",
[IsSemigroup, IsSemigroup],
function(S, T)
  return SemigroupIsomorphismByImages(S,
                                      T,
                                      GeneratorsOfSemigroup(S),
                                      GeneratorsOfSemigroup(T));
end);

InstallMethod(SemigroupIsomorphismByImages, "for a semigroup and two lists",
[IsSemigroup, IsList, IsList],
{S, gens, imgs}
-> SemigroupIsomorphismByImages(S, Semigroup(imgs), gens, imgs));

InstallMethod(SemigroupHomomorphismByImages_NC,
"for two semigroups and two lists",
[IsSemigroup, IsSemigroup, IsList, IsList],
function(S, T, gens, imgs)
  local hom;

  hom := Objectify(NewType(GeneralMappingsFamily(ElementsFamily(FamilyObj(S)),
                                                 ElementsFamily(FamilyObj(T))),
                           IsSemigroupHomomorphismByImages), rec());
  SetSource(hom, S);
  SetRange(hom, T);
  SetMappingGeneratorsImages(hom, [Immutable(gens), Immutable(imgs)]);

  return hom;
end);

InstallMethod(SemigroupHomomorphismByFunctionNC,
"for semigroup, semigroup, and function",
[IsSemigroup, IsSemigroup, IsFunction],
function(S, T, f)
  local hom;
  hom := Objectify(NewType(GeneralMappingsFamily(ElementsFamily(FamilyObj(S)),
                                                 ElementsFamily(FamilyObj(T))),
                           IsSemigroupHomomorphismByFunction), rec(fun := f));
  SetSource(hom, S);
  SetRange(hom, T);
  return hom;
end);

InstallMethod(SemigroupHomomorphismByFunction,
"for two semigroups and a function",
[IsSemigroup, IsSemigroup, IsFunction],
function(S, T, f)
  local map;
  map := MappingByFunction(S, T, f);
  if not RespectsMultiplication(map) then
    return fail;
  fi;
  SetFilterObj(map, IsSemigroupHomomorphismByFunction);
  return map;
end);

InstallMethod(SemigroupIsomorphismByFunction,
"for two semigroups and two functions",
[IsSemigroup, IsSemigroup, IsFunction, IsFunction],
function(S, T, f, g)
  local map, inv;
  map := SemigroupHomomorphismByFunction(S, T, f);
  if map = fail or not IsBijective(map) then
    return fail;
  fi;
  inv := SemigroupHomomorphismByFunction(T, S, g);
  if inv = fail or not IsBijective(inv) then
    return fail;
  elif CompositionMapping(map, inv)
      <> SemigroupHomomorphismByFunctionNC(T, T, IdFunc) then
    return fail;
  fi;

  return SemigroupIsomorphismByFunctionNC(S, T, f, g);
end);

InstallMethod(SemigroupIsomorphismByFunctionNC,
"for two semigroups and two functions",
[IsSemigroup, IsSemigroup, IsFunction, IsFunction],
function(S, T, f, g)
  local iso;
  iso := Objectify(NewType(GeneralMappingsFamily(ElementsFamily(FamilyObj(S)),
                                                 ElementsFamily(FamilyObj(T))),
                           IsSemigroupIsomorphismByFunction),
                           rec(fun    := f,
                               invFun := g));
  SetSource(iso, S);
  SetRange(iso, T);
  return iso;
end);

InstallMethod(InverseGeneralMapping,
"for a semigroup isomorphism by function",
[IsSemigroupIsomorphismByFunction],
function(map)
  local inv;
  inv := SemigroupIsomorphismByFunctionNC(Range(map),
                                          Source(map),
                                          map!.invFun,
                                          map!.fun);
  TransferMappingPropertiesToInverse(map, inv);
  return inv;
end);

# The next method applies when we create a homomorphism using
# SemigroupHomomorphismByFunction and so invFun is not available.

InstallMethod(InverseGeneralMapping,
"for a bijective semigroup homomorphism by function",
[IsSemigroupHomomorphismByFunction and IsBijective],
function(map)
  local inv;
  inv := SemigroupIsomorphismByFunctionNC(Range(map),
                                          Source(map),
                                          x -> First(Source(map),
                                                     y -> y ^ map = x),
                                          map!.fun);
  TransferMappingPropertiesToInverse(map, inv);
  return inv;
end);

# methods for converting between SHBI and SHBF
InstallMethod(AsSemigroupHomomorphismByImages,
"for a semigroup homomorphism by function",
[IsSemigroupHomomorphismByFunction],
function(hom)
  local S, T, gens, imgs;
  S    := Source(hom);
  T    := Range(hom);
  gens := GeneratorsOfSemigroup(S);
  imgs := List(gens, x -> x ^ hom);
  return SemigroupHomomorphismByImages(S, T, gens, imgs);
end);

InstallMethod(AsSemigroupHomomorphismByFunction,
"for a semigroup homomorphism by images",
[IsSemigroupHomomorphismByImages],
hom -> SemigroupHomomorphismByFunctionNC(Source(hom),
                                         Range(hom),
                                         x -> ImageElm(hom, x)));

InstallMethod(AsSemigroupIsomorphismByFunction,
"for a semigroup homomorphism by images",
[IsSemigroupHomomorphismByImages],
hom -> SemigroupIsomorphismByFunctionNC(Source(hom),
                                        Range(hom),
                                        x -> ImageElm(hom, x),
                                        y -> PreImages(hom, y)));

# Methods for SHBI/SIBI/SHBF
InstallMethod(IsSurjective, "for a semigroup homomorphism",
[IsSemigroupHomomorphismByImagesOrFunction],
{hom} -> Size(ImagesSource(hom)) = Size(Range(hom)));

InstallMethod(IsInjective, "for a semigroup homomorphism",
[IsSemigroupHomomorphismByImagesOrFunction],
{hom} -> Size(Source(hom)) = Size(ImagesSource(hom)));

InstallMethod(ImagesSet, "for a semigroup homom. and list of elements",
[IsSemigroupHomomorphismByImagesOrFunction, IsList],
{hom, elms} -> List(elms, x -> ImageElm(hom, x)));

InstallMethod(ImageElm, "for a semigroup homom. by images and element",
[IsSemigroupHomomorphismByImages, IsMultiplicativeElement],
function(hom, x)
  if not x in Source(hom) then
    ErrorNoReturn("the 2nd argument (a mult. elt.) is not an element ",
                  "of the source of the 1st argument (semigroup homom. by ",
                  "images)");
  fi;
  # Use MinimalFactorization rather than Factorization because
  # MinimalFactorization is guaranteed to return a list of positive integers,
  # but Factorization is not (i.e. if S is an inverse acting semigroup.
  # Also since we require an IsomorphismFpSemigroup, there's no additional
  # cost to using MinimalFactorization instead of Factorization.
  return EvaluateWord(MappingGeneratorsImages(hom)[2],
                      MinimalFactorization(Source(hom), x));
end);

InstallMethod(ImagesSource, "for SHBI",
[IsSemigroupHomomorphismByImages],
hom -> Semigroup(MappingGeneratorsImages(hom)[2]));

InstallMethod(PreImagesRepresentative,
"for a semigroup homom. by images and an element in the range",
[IsSemigroupHomomorphismByImages, IsMultiplicativeElement],
function(hom, x)
  if not x in Range(hom) then
    ErrorNoReturn("the 2nd argument is not an element of the range of the ",
                  "1st argument (semigroup homom. by images)");
  elif not x in ImagesSource(hom) then
    return fail;
  fi;
  # Use MinimalFactorization rather than Factorization because
  # MinimalFactorization is guaranteed to return a list of positive integers,
  # but Factorization is not (i.e. if S is an inverse acting semigroup.
  # Also since we require an IsomorphismFpSemigroup, there's no additional
  # cost to using MinimalFactorization instead of Factorization.
  return EvaluateWord(MappingGeneratorsImages(hom)[1],
                      MinimalFactorization(ImagesSource(hom), x));
end);

InstallMethod(ImagesRepresentative,
"for a semigroup homom. by images and an element in the source",
[IsSemigroupHomomorphismByImages, IsMultiplicativeElement],
function(hom, x)
  if not x in Source(hom) then
    ErrorNoReturn("the 2nd argument is not an element of the source of the ",
                  "1st argument (semigroup homom. by images)");
  fi;
  # Use MinimalFactorization rather than Factorization because
  # MinimalFactorization is guaranteed to return a list of positive integers,
  # but Factorization is not (i.e. if S is an inverse acting semigroup.
  # Also since we require an IsomorphismFpSemigroup, there's no additional
  # cost to using MinimalFactorization instead of Factorization.
  return EvaluateWord(MappingGeneratorsImages(hom)[2],
                      MinimalFactorization(Source(hom), x));
end);

InstallMethod(ImagesElm, "for a semigroup homom. by images and an element",
[IsSemigroupHomomorphismByImages, IsMultiplicativeElement],
{hom, x} -> [ImageElm(hom, x)]);

InstallMethod(PreImagesElm,
"for a semigroup homom. by images and an element in the range",
[IsSemigroupHomomorphismByImages, IsMultiplicativeElement],
function(hom, x)
  local preim, y;
  if not x in Range(hom) then
    ErrorNoReturn("the 2nd argument is not an element of the range of the ",
                  "1st argument (semigroup homom. by images)");
  elif not x in ImagesSource(hom) then
    ErrorNoReturn("the 2nd argument is not mapped to by the 1st argument ",
                  "(semigroup homom. by images)");
  fi;
  preim := [];
  for y in Source(hom) do
    if ImageElm(hom, y) = x then
      Add(preim, y);
    fi;
  od;
  return preim;
end);

InstallMethod(KernelOfSemigroupHomomorphism, "for a semigroup homomorphism",
[IsSemigroupHomomorphismByImagesOrFunction],
function(hom)
  local S, cong, enum, x, y, pairs, i, j;

  if IsQuotientSemigroup(Range(hom)) then
    return QuotientSemigroupCongruence(Range(hom));
  fi;

  S := Source(hom);
  if IsBijective(hom) then
    return SemigroupCongruence(S, []);
  elif Size(ImagesSource(hom)) = 1 then
    return UniversalSemigroupCongruence(S);
  fi;

  cong := SemigroupCongruence(S, []);
  enum := EnumeratorCanonical(S);
  for i in [1 .. Size(S) - 1] do
    x := enum[i];
    for j in [i + 1 .. Size(S)] do
      y := enum[j];
      if x ^ hom = y ^ hom then
        if not [x, y] in cong then
          pairs := ShallowCopy(GeneratingPairsOfSemigroupCongruence(cong));
          Add(pairs, [x, y]);
          cong := SemigroupCongruence(S, pairs);
          if NrEquivalenceClasses(cong) = Size(ImagesSource(hom)) then
            return cong;
          fi;
        fi;
      fi;
    od;
  od;
  return cong;
end);

InstallMethod(String, "for a semigroup homom. by images",
[IsSemigroupHomomorphismByImages],
function(hom)
  local mapi;
  if UserPreference("semigroups", "ViewObj") <> "semigroups-pkg" then
    TryNextMethod();
  fi;
  mapi := MappingGeneratorsImages(hom);
  return Concatenation("SemigroupHomomorphismByImages( ",
                       String(Source(hom)),
                       ", ",
                       String(Range(hom)),
                       ", ",
                       String(mapi[1]),
                       ", ",
                       String(mapi[2]),
                       " )");
end);

InstallMethod(PrintObj, "for a semigroup homom. by images",
[IsSemigroupHomomorphismByImages],
function(hom)
  Print(String(hom));
  return;
end);

InstallMethod(String, "for a semigroup isom. by images",
[IsSemigroupHomomorphismByImages and IsBijective],
function(iso)
  local mapi, str;
  if UserPreference("semigroups", "ViewObj") <> "semigroups-pkg" then
    TryNextMethod();
  fi;
  mapi := MappingGeneratorsImages(iso);
  str := Concatenation("SemigroupIsomorphismByImages( ",
                       String(Source(iso)),
                       ", ",
                       String(Range(iso)),
                       ", ",
                       String(mapi[1]),
                       ", ",
                       String(mapi[2]),
                       " )");
  # print empty lists with two spaces for consistency
  # see https://github.com/gap-system/gap/pull/5418
  return ReplacedString(str, "[ ]", "[  ]");
end);

InstallMethod(\=, "compare homom. by images", IsIdenticalObj,
[IsSemigroupHomomorphismByImages, IsSemigroupHomomorphismByImages],
function(hom1, hom2)
  local i;
  if Source(hom1) <> Source(hom2)
      or Range(hom1) <> Range(hom2)
      or PreImagesRange(hom1) <> PreImagesRange(hom2)
      or ImagesSource(hom1) <> ImagesSource(hom2) then
    return false;
  fi;
  hom1 := MappingGeneratorsImages(hom1);
  return hom1[2] = List(hom1[1], i -> ImageElm(hom2, i));
end);

InstallMethod(ViewObj, "for SHBI/SHBF",
[IsSemigroupHomomorphismByImagesOrFunction],
2,  # to beat method for mapping by function with inverse
function(hom)
  if UserPreference("semigroups", "ViewObj") <> "semigroups-pkg" then
    TryNextMethod();
  fi;
  Print("\>");
  ViewObj(Source(hom));
  Print("\< \>->\< \>");
  ViewObj(Range(hom));
  Print("\<");
end);

InstallMethod(String, "for a semigroup homom. by function",
[IsSemigroupHomomorphismByFunction],
function(hom)
  if UserPreference("semigroups", "ViewObj") <> "semigroups-pkg" then
    TryNextMethod();
  fi;
  return Concatenation("SemigroupHomomorphismByFunction( ",
                       String(Source(hom)),
                       ", ",
                       String(Range(hom)),
                       ", ",
                       String(hom!.fun),
                       " )");
end);

InstallMethod(PrintObj, "for a semigroup homom. by function",
[IsSemigroupHomomorphismByFunction],
function(hom)
  if UserPreference("semigroups", "ViewObj") <> "semigroups-pkg" then
    TryNextMethod();
  fi;
  Print(String(hom));
  return;
end);

InstallMethod(String, "for a semigroup isom. by function",
[IsSemigroupIsomorphismByFunction],
function(iso)
  if UserPreference("semigroups", "ViewObj") <> "semigroups-pkg" then
    TryNextMethod();
  fi;
  return Concatenation("SemigroupIsomorphismByFunction( ",
                       String(Source(iso)),
                       ", ",
                       String(Range(iso)),
                       ", ",
                       String(iso!.fun),
                       ", ",
                       String(iso!.invFun),
                       " )");
end);

InstallMethod(PrintObj, "for a semigroup isom. by function",
[IsSemigroupIsomorphismByFunction],
function(iso)
  if UserPreference("semigroups", "ViewObj") <> "semigroups-pkg" then
    TryNextMethod();
  fi;
  Print(String(iso));
  return;
end);
