#############################################################################
##
#W  quasigroups.gi  Representing, creating and displaying quasigroups [loops]
##
#H  @(#)$Id: creation.gi, v 3.2.0 2015/11/22 gap Exp $
##
#Y  Copyright (C)  2004,  G. P. Nagy (University of Szeged, Hungary),
#Y                        P. Vojtechovsky (University of Denver, USA)
##

#############################################################################
##  TESTING MULTIPLICATION TABLES
##  -------------------------------------------------------------------------

#############################################################################
##
#O  IsLeftQuasigroupTable( ls )
##
##  Returns true if <ls> is an n by n matrix with n distinct
##  integral entries, each occurring exactly once in each row

InstallMethod( IsLeftQuasigroupTable, "for matrix",
    [ IsMatrix ],
function( ls )
    local first_row;
    # checking rows
    first_row := Set( ls[ 1 ] );
    if not Length( first_row ) = Length( ls[ 1 ] ) then return false; fi;
    if ForAll( ls, row -> Set( row ) = first_row ) = false then return false; fi;
    return true;
end );

#############################################################################
##
#O  IsRightQuasigroupTable( ls )
##
##  Returns true if <ls> is an n by n matrix with n distinct
##  integral entries, each occurring exactly once in each column

InstallMethod( IsRightQuasigroupTable, "for matrix",
    [ IsMatrix ],
    mat -> IsLeftQuasigroupTable(TransposedMat(mat))
);

#############################################################################
##
#O  IsLoopTable( ls )
##
##  Returns true if <ls> is a normalized latin square. An n by n latin square
##  is normalized if the first row and first column read the same, and the
##  entries in the first row are ordered.

InstallMethod( IsLoopTable, "for matrix",
    [ IsMatrix ],
function( ls )
    if not IsQuasigroupTable( ls ) then return false; fi;
    return Set( ls[ 1 ] ) = ls[ 1 ]
        and ls[ 1 ] = List( [1..Length(ls)], i -> ls[ i ][ 1 ] );
end );

#############################################################################
##
#O CanonicalCayleyTableOfLeftQuasigroupTable( ls )
##
## Returns a Cayley table isomorphic to <ls>, which must already be known
## to be a left quasigroup table, in which the entries of ls
## have been replaced by numerical values 1, ..., n in the following way:
## Let e_1 < ... < e_n be all distinct entries of ls. Then e_i is renamed
## to i. In particular, when {e_1,...e_n} = {1,...,n}, the operation
## does nothing.

InstallGlobalFunction( CanonicalCayleyTableOfLeftQuasigroupTable,
function( ls )
    local n, entries, i, j, T;
    n := Length( ls );
    # finding all distinct entries in the table
    entries := [];
    for i in ls[1] do 
        AddSet( entries, i );
    od;
    if entries = [1..n] then return List(ls, l -> ShallowCopy(l));
    fi;
    # renaming the entries and making a mutable copy, too
    T := List( [1..n], i -> [1..n] );
    for i in [1..n] do for j in [1..n] do
       T[ i ][ j ] := Position( entries, ls[ i ][ j ] );
    od; od;
    return T;
end );

#############################################################################
##
#O CanonicalCayleyTable( ls )
##
## Returns a Cayley table isomorphic to <ls>, in which the entries of ls
## have been replaced by numerical values 1, ..., n in the following way:
## Let e_1 < ... < e_n be all distinct entries of ls. Then e_i is renamed
## to i. In particular, when {e_1,...e_n} = {1,...,n}, the operation
## does nothing.

InstallMethod( CanonicalCayleyTable, "for matrix",
    [ IsMatrix ],
function( ls )
    local n, entries, i, j, T;
    n := Length( ls );
    # finding all distinct entries in the table
    entries := [];
    for i in [1..n] do for j in [1..n] do
        AddSet( entries, ls[ i ][ j ] );
    od; od;
    if entries = [1..n] then return List(ls, l -> ShallowCopy(l));
    fi;
    # renaming the entries and making a mutable copy, too
    T := List( [1..n], i -> [1..n] );
    for i in [1..n] do for j in [1..n] do
       T[ i ][ j ] := Position( entries, ls[ i ][ j ] );
    od; od;
    return T;
end);

#############################################################################
##
#O  NormalizedQuasigroupTable( ls )
##
##  Given a latin square <ls>, returns the corresponding normalized
##  latin square with entries 1, ..., n.

InstallMethod( NormalizedQuasigroupTable, "for matrix",
    [ IsMatrix ],
function( ls )
    local T, perm;
    if not IsQuasigroupTable( ls ) then
        Error( "LOOPS: <1> must be a latin square." );
    fi;
    # renaming the entries to be 1, ..., n
    T := CanonicalCayleyTableOfLeftQuasigroupTable( ls );
    # permuting the columns so that the first row reads 1, ..., n
    perm := PermList( T[ 1 ] );
    T := List( T, row -> Permuted( row, perm ) );
    # permuting the rows so that the first column reads 1, ..., n
    return Set( T );
end );

#############################################################################
##  CREATING QUASIGROUPS AND LOOPS MANUALLY
##  -------------------------------------------------------------------------

#############################################################################
##
#A  CayleyTable( Q )
##
##  Returns the Cayley table of the magma <Q>. This is just like
##  its multiplication table, except in case Q is a submagma of P, in which
##  case the entries used are the indices in P rather than in Q.

InstallMethod( CayleyTable, "for magma",
    [ IsMagma ],
function( Q )
    local elms, parent_elms;
    elms := Elements( Q );
    parent_elms := Elements( Parent( Q ) );
    return List( elms, x-> List( elms, y -> Position( parent_elms, x * y ) ) );
end );

#############################################################################
##
#O  QuasigroupByCayleyTable( ct )
##
##  Returns quasigroup with multiplication table <ct>.

InstallMethod( QuasigroupByCayleyTable, "for matrix",
    [ IsMatrix ],
function( ct )

    local F, Q, elms, n;
    if not IsQuasigroupTable( ct ) then
        Error( "LOOPS: <1> must be a latin square." );
    fi;
    # Making sure that entries are 1, ..., n
    ct := CanonicalCayleyTableOfLeftQuasigroupTable( ct );
    # constructing the family
    F := NewFamily( "QuasigroupByCayleyTableFam", IsQuasigroupElement );
    # installing data for the family
    n := Length ( ct );
    F!.size := n;
    elms := Immutable( List( [1..n], i -> Objectify(
        NewType( F, IsQuasigroupElement and IsQuasigroupElmRep), [ i ] ) ) );
    F!.set := elms;
    F!.cayleyTable := ct;
    F!.elmNamePrefix := "q";
    # creating the quasigroup
    Q := Objectify( NewType( FamilyObj( elms ),
        IsQuasigroup and IsAttributeStoringRep ), rec() );
    # setting attributes for the quasigroup
    SetSize( Q, n );
    SetAsSSortedList( Q, elms );
    SetParent( Q, Q );
    SetCayleyTable( Q, ct );
    SetConstructorFromTable(Q, QuasigroupByCayleyTable);
    return Q;
end );

#############################################################################
##
#O  LoopByCayleyTable( ct )
##
##  Returns loop with multiplication table <ct>.

InstallMethod( LoopByCayleyTable, "for matrix",
    [ IsMatrix ],
function( ct )
    local F, L, elms, n;
    if not IsLoopTable( ct ) then
        Error( "LOOPS: <1> must be a normalized latin square." );
    fi;
    # Making sure that the entries are 1, ..., n.
    # The table will remain normalized.
    ct := CanonicalCayleyTableOfLeftQuasigroupTable( ct );
    # constructing the family
    F := NewFamily( "LoopByCayleyTableFam", IsLoopElement );
    # installing the data for the family
    n := Length ( ct );
    F!.size := n;
    elms := Immutable( List( [1..n], i -> Objectify(
        NewType( F, IsLoopElement and IsLoopElmRep), [ i ] ) ) );
    F!.set := elms;
    F!.cayleyTable := ct;
    F!.elmNamePrefix := "l";
    # creating the loop
    L := Objectify( NewType( FamilyObj( elms ),
        IsLoop and IsAttributeStoringRep ), rec() );
    # setting attributes for the loop
    SetSize( L, n );
    SetAsSSortedList( L, elms );
    SetParent( L, L );
    SetCayleyTable( L, ct );
    SetOne( L, elms[ 1 ] );
    SetConstructorFromTable(L, LoopByCayleyTable);
    return L;
end );

#############################################################################
##
#O  SpecifyElmNamePrefix( C, name )
##
##  Sets the elmNamePrefix property on the family of a Representative of 
##  collection <C>. For quasigroups, loops, and possibly related structures
##  this changes the prefix with which the elements of that family are printed.

InstallMethod( SpecifyElmNamePrefix, "for collection and string",
    [ IsCollection, IsString ],
function( Q, name )
    local F;
    F := FamilyObj( Representative( Q ) );
    F!.elmNamePrefix := name;
end);
#############################################################################
##
#O  BindElmNames( M )
##
##  For each element e of the magma <M>, binds the identifier named String(e)
##  to e. 

InstallMethod( BindElmNames, "for a magma",
    [ IsMagma ],
    function( M )
        local e, nm;
        for e in Elements(M) do
            nm := String(e);
            BindGlobal(nm, e);
            MakeReadWriteGlobal(nm);
        od;
        return;
end);

#############################################################################
##
#O ConstructorFromTable( M )
##
## Given a magma <M>, returns a function which will create a domain of the
## same structure as M from from an operation table. This implementation of
## the method is to backfill the constructors for library domains that do
## not set the attribute directly at construction time. New domains that wish
## to use facilities like CanonicalCopy or Opposite should call
## SetConstructorFromTable at creation time.

InstallMethod( ConstructorFromTable, "for other magmas",
    [ IsMagma ],
    function ( M ) 
      # Go in reverse order of refinement of structure
      if IsGroup(M) then
        return GroupByMultiplicationTable;
      elif IsMagmaWithInverses(M) then
        return MagmaWithInversesByMultiplicationTable;
      elif IsMonoid(M) then
        return MonoidByMultiplicationTable;
      elif IsMagmaWithOne(M) then
        return MagmaWithOneByMultiplicationTable;
      elif IsSemigroup(M) then
        return SemigroupByMultiplicationTable;
      fi;
      return MagmaByMultiplicationTable;
end);

            
#############################################################################
##
#O  CanonicalCopy( Q )
##
##  Returns a canonical copy of <Q>, that is, an isomorphic object <O> with
##  canonical multiplication table and no parent set. Note that this is
##  guaranteed to be a new object, not satisfying IsIdenticalObj with any
##  previously existing structure. THEREFORE:
##  (PROG) Properties and attributes are lost!

InstallMethod( CanonicalCopy, "for magma",
    [ IsMagma ],
    M -> ConstructorFromTable(M)(MultiplicationTable(M))
);


#############################################################################
##  CREATING QUASIGROUPS AND LOOPS FROM A FILE
##  -------------------------------------------------------------------------

#############################################################################
##
#F  LOOPS_ReadCayleyTableFromFile( filename, replace_by_spaces )
##
##  Auxiliary function. Reads the content of <filename> and tries to
##  interpret the data as a multiplication table, according to the rules
##  summarized below. If successful, it returns the multiplication table as
##  an n by n array.
##  ALGORITHM:
##  1) the content of the file is read into one string
##  2) all end-of-lines and all characters in the string <replace_by_spaces>
##     are replaced by spaces
##  3) the string is split into chunks, where chunk = maximal substring
##     without spaces
##  4) the number n of distinct chunks is found. If the number of chunks
##     is not n^2, error is announced and function terminates
##  5) a numerical value 1 .. n is assigned to each chunk, depending on
##     its position among the distinct chunks
##  6) multiplication table is constructed and returned

InstallGlobalFunction( LOOPS_ReadCayleyTableFromFile,
function( filename, replace_by_spaces)

    local input, s, i, chunks, started, starting_pos, z, j, distinct_chunks, c, n, T;

    if not ( IsString( filename) and IsString( replace_by_spaces ) ) then
        Error( "LOOPS: <1> must be a file name, and <2> must be a string." );
    fi;

    input := InputTextFile( filename );
    if input = fail then Error( "LOOPS: <1> is not a valid file name." ); fi;

    s := ReadAll( input );

    # removing end-of-lines, etc.
    for i in [1..Length( s )] do
        if (s[ i ] = '\n') or (s[ i ] in replace_by_spaces) then
             s[ i ] := ' ';
        fi;
    od;
    s[ Length( s ) + 1 ] := ' '; #to make sure that string ends with space

    #parsing string into chunks separated by spaces
    chunks := [];
    started := false; starting_pos := 0;
    for i in [1..Length( s )] do
        if not started then
            if not s[ i ] = ' ' then
                started := true;
                starting_pos := i;
            fi;
        else
            if s[ i ] = ' ' then #end of chunk
                z := "";
                for j in [ starting_pos..i-1 ] do
                    z[ j - starting_pos + 1 ] := s[ j ];
                od;
                Add( chunks, z );
                started := false;
            fi;
        fi;
    od;

    distinct_chunks := [];
    for c in chunks do if not c in distinct_chunks then
        Add( distinct_chunks, c );
    fi; od;

    n := Length( distinct_chunks );
    if not Length( chunks ) = n^2 then
        Error( "LOOPS: The data in the file cannot be arranged into a square table." );
    fi;

    T := List( [1..n], i -> 0*[1..n] );
    for i in [1..n] do for j in [1..n] do
        T[ i ][ j ] := Position( distinct_chunks, chunks[ (i-1)*n + j ] );
    od; od;

    return T;

end);

#############################################################################
##
#O  QuasigroupFromFile( filename, replace] )
##
##  Calls LOOPS_ReadCayleyTableFromFile( filename, replace ) in order to return
##  the quasigroup with multiplication table in file <filename>.

InstallMethod( QuasigroupFromFile, "for string and string",
    [ IsString, IsString ],
function( filename, replace )
    return QuasigroupByCayleyTable( LOOPS_ReadCayleyTableFromFile( filename, replace ) );
end );


#############################################################################
##
#O  LoopFromFile( filename , replace] )
##
##  Calls LOOPS_ReadCayleyTableFromFile( filename, replace ) in order to return
##  the loop with multiplication table in file <filename>.

InstallMethod( LoopFromFile, "for string and string",
    [ IsString, IsString ],
function( filename, replace )
    return LoopByCayleyTable(  LOOPS_ReadCayleyTableFromFile( filename, replace ) );
end );

#############################################################################
##  CREATING QUASIGROUPS AND LOOPS BY SECTIONS AND FOLDERS
##  -------------------------------------------------------------------------

#############################################################################
##
#O  CayleyTableByPerms( perms, [X] )
##
##  Given a set <perms> of n permutations of an n-element set <X> of natural
##  numbers, returns an n by n Cayley table ct such that 
##    ct[i][j] = X[j]^perms[i]. 
##
##  Note that the argument <X> is optional, and if omitted, the function will
##  assume that at most one permutation of <perms> is the identity
##  permutation, and that all other permutations of <perms> 
##  move all points of <X>.

InstallGlobalFunction( CayleyTableByPerms,
function( perms, rest... )
    local n, pts, max;
    n := Length( perms );
    if n=1 then
        return [ [ 1 ] ];
    fi;
    
    # one of perms[ 1 ], perms[ 2 ] must move all points
    if Length(rest) > 0 then
      pts := rest[1];
    else  
      pts := MovedPoints( perms[ 2 ] );
      if pts = [] then
        pts := MovedPoints( perms[ 1 ] );
      fi;
    fi;
    if Length(pts) <> n then
      Error("perms for cayley table of size ", n, " cannot act on ", 
            Length(pts), " points.");
    fi;
    max := Maximum( pts );
    if max = n then
      # Common case, we are permuting [1..n]
      return List( perms, x -> ListPerm(x, n));
    fi;
    # Otherwise we permute the whole interval [1..max] and then keep only those coordinates corresponding to pts
    return List( perms, p -> Permuted( [1..max], p^(-1) ){ pts } );
end);

#############################################################################
##
#O  QuasigroupByLeftSection( sect )
##
##  Returns the quasigroup whose left section is the list of permutations
##  <sect>.

InstallMethod( QuasigroupByLeftSection,
    "for a set of left translation maps",
    [ IsPermCollection ],
function( sect )
    return QuasigroupByCayleyTable( CayleyTableByPerms( sect ) );
end);

#############################################################################
##
#O  LoopByLeftSection( sect )
##
##  Returns the loop whose left section is the list of permutations <sect>.
##  Since the order of translations in <sect> is determined by their
##  image of the neutral element 1, we disregard the order.

InstallMethod( LoopByLeftSection,
    "for a set of left translation maps",
    [ IsPermCollection ],
function( sect )
    return LoopByCayleyTable( Set ( CayleyTableByPerms( sect ) ) );
end);

#############################################################################
##
#O  QuasigroupByRightSection( sect )
##
##  Returns the quasigroup whose right section is the list of permutations
##  <sect>.

InstallMethod( QuasigroupByRightSection,
    "for a set of left translation maps",
    [ IsPermCollection ],
function( sect )
    return QuasigroupByCayleyTable( TransposedMat ( CayleyTableByPerms( sect ) ) );
end);

#############################################################################
##
#O  LoopByRightSection( sect )
##
##  Returns the loop whose right section is the list of permutations <sect>.
##  Since the order of translations in <sect> is determined by their
##  image of the neutral element 1, we disregard the order.

InstallMethod( LoopByRightSection,
    "for a set of left translation maps",
    [ IsPermCollection ],
function( sect )
    return LoopByCayleyTable( TransposedMat ( Set ( CayleyTableByPerms( sect ) ) ) );
end);

#############################################################################
##
#O  LOOPS_CayleyTableByRightFolder( G, H, T )
##
##  Auxiliary operation.
##
##  A right folder is a triple (G,H,T) such that G is a group, H is 
##  a subgroup of G, and T is a right transversal to H in G.
##
##  Returns the multiplication table on {Hx: x in G} by Ht*Hs = H(ts).
##
##  The multiplication table is a quasigroup if and only if
##  T is a right transversal to every conjugate H^g in G.

InstallGlobalFunction( LOOPS_CayleyTableByRightFolder, 
function( G, H, T )
    local act, nT, actT, i, p, ct;
    # act = action of G on right cosest G/H
    act := ActionHomomorphism( G, RightCosets( G, H ), OnRight );
    nT := Length( T );
    # actT = permutations on G/H induced by elements of T
    actT := [1..nT];
    for i in [1..nT] do
        actT[ i ] := T[ i ]^act;
    od;
    # the order of right cosets determined by T might not agree with the default order of right cosets ...
    p := PermList( List( [1..nT], i -> 1^actT[ i ] ) );
    ct := List( [1..nT], i -> ListPerm( p * actT[ i ] * p^(-1) ) );
    for i in [1..nT] do
        if ct[ i ] = [] then # this can happen since ListPerm( () ) = []
            ct[ i ] := [1..nT];
        fi;
    od;
    return TransposedMat( ct );
end);

#############################################################################
##
#O  QuasigroupByRightFolder( G, H, T )
##
##  See CayleyTableByRightFolder. We do not check if the right folder
##  is a quasigroup right folder.

InstallMethod( QuasigroupByRightFolder,
    "for a group, a subgroup and right transversal",
    [ IsGroup, IsGroup, IsMultiplicativeElementCollection ],
function( G, H, T )
    return QuasigroupByCayleyTable( LOOPS_CayleyTableByRightFolder( G, H, T ) );
end);

#############################################################################
##
#O  LoopByRightFolder( G, H, T )
##
##  See CayleyTableByRigthFolder. We do not check if the right folder
##  is a loop right folder.

InstallOtherMethod( LoopByRightFolder,
    "for a group, a subgroup and right transversal",
    [ IsGroup, IsGroup, IsMultiplicativeElementCollection ],
function( G, H, T )
    return LoopByCayleyTable( LOOPS_CayleyTableByRightFolder( G, H, T ) );
end);

#############################################################################
##  CONVERSIONS
##  -------------------------------------------------------------------------

#############################################################################
##
#O  IntoQuasigroup( M )
##
##  Given a magma, returns the corresponding quasigroup, if possible.

InstallMethod( IntoQuasigroup, "for magma",
    [ IsMagma ],
function( M )
    local ct;
    if IsQuasigroup( M ) then # leave quasigroups and loops intact
        return M;
    fi;
    # magma, not necessarily a quasigroup
    ct := MultiplicationTable( Elements( M ) );
    if IsQuasigroupTable( ct ) then 
        return QuasigroupByCayleyTable( ct );
    fi;
    return fail;
end);

#############################################################################
##
#O  PrincipalLoopIsotope( Q, f, g )
##
##  Let Q be a quasigroup and f, g elements of Q.
##  Define new operation on Q by x+y = R^{-1}(g)(x) * L^{-1}(f)(y).
##  Then (Q,+) is a loop with neutral element f*g.
##  We return isomorphic copy of (Q,+) via the isomorphism (1,f*g).

InstallMethod( PrincipalLoopIsotope,
    "for quasigroup and two quasigroup elements",
    [ IsQuasigroup, IsQuasigroupElement, IsQuasigroupElement ],
function( Q, f, g )
    local n, L, R, i, j, ct, p;
    if not (f in Q and g in Q) then
        Error("LOOPS: <2> and <3> must be elements of quasigroup <1>.");
    fi;
    # constructing new multiplication
    n := Size( Q );
    ct := List( [1..n], i -> [1..n] );
    L := Inverse( LeftTranslation( Q, f ) );    # inverse of left translation by f
    R := Inverse( RightTranslation( Q, g ) );   # inverse or right translation by g
    for i in [1..n] do for j in [1..n] do
        ct[ i ][ j ] := CayleyTable( Q )[ i^R ][ j^L ];
    od; od;
    # the neutral element of ct is now f*g. We apply isomorphism (1, f*g).
    p := Position(Q, f*g);
    if p>1 then
        p := (1, p); # note that p is its own inverse
        ct := List([1..n], i-> List([1..n], j -> ( ct[ i^p ][ j^p ] )^p ) );
    fi;
    return LoopByCayleyTable( ct );
end);

#############################################################################
##
#O  IntoLoop( M )
##
##  Given a magma, returns the corresponding loop, if possible.

InstallMethod( IntoLoop, "for magma",
    [ IsMagma ],
function( M )
    local e, p, ct;
    if IsLoop( M ) then # loops are left intact
        return M;
    fi; 
    # magma, not necessarily a loop
    M := IntoQuasigroup( M );
    if M = fail then
        return fail;
    fi;
    # quasigroup, not necesarily a loop
    e := MultiplicativeNeutralElement( M );
    if e = fail then # no neutral element, use principal isotope
        return PrincipalLoopIsotope( M, Elements( M )[ 1 ], Elements( M )[ 1 ] );
    fi;
    # quasigroup with neutral element, i.e., a loop
    p := Position( M, e );
    if p>1 then
        p := (1,p); # note that p is its own inverse
        ct := List([1..Size(M)], i-> List([1..Size(M)], j ->
            ( CayleyTable( M )[ i^p ][ j^p ] )^p
        ) );
    else
        ct := CayleyTable( M );
    fi;
    return LoopByCayleyTable( ct );
end );

#############################################################################
##
#O  IntoGroup( M )
##
##  Given a magma <M>, returns the corresponding group, if possible.

InstallOtherMethod( IntoGroup, "for magma",
    [ IsMagma ],
function( M )
    if IsGroup( M ) then # groups are left intact
        return M;
    fi;
    # magma, not necessarily a group
    M := IntoLoop( M );
    if M=fail or (not IsAssociative( M ) ) then
        return fail;
    fi;
    # group
    return RightMultiplicationGroup( M );
end); 

#############################################################################
##  PRODUCTS OF QUASIGROUPS AND LOOPS
##  -------------------------------------------------------------------------

#############################################################################
##
#F  DirectProduct( Q1, Q2, ..., Qn )
##
##  Returns the direct product of quasigroups <Q1>, <Q2>, ... , <Qn>.
##  The quasigroups can be declared as quasigroups, loops or groups.

# The following is necessary due to implementation of DirectProduct for
# groups in GAP. The idea is as follows:
# We want to calculate direct product of quasigroups, loops and groups.
# If only groups are on the list, standard GAP DirectProduct will take care
# of it. If there are also some quasigroups or loops on the list (but nothing
# that is not a quasigroup), we must take care of it.
# However, we do not know if such a list will be processed with
# DirectProductOp( <IsList>, <IsGroup> ), or
# DirectProductOp( <IsList>, <IsQuasigroup> ),
# since this depends on which algebra is listed first.
# Call the item in the second argument of DirectProductOp the "distinguished"
# item. To produce the correct result whichever of the two above cases for
# DirectProductOp ends up being called, we add a method in the first case
# which repeats the product call with the first non-group it encounters (if
# any) as the distinguished item. Further, the method for the
# latter case must itself repeat the call with a similar reordering if it
# finds anyitem that is not a quasigroup.

InstallMethod( DirectProductOp, "for DirectProduct( <IsList>, <IsGroup> )",
    [ IsList, IsGroup],
function( list, first )
    local L, p;

    # Check the arguments.
    if IsEmpty( list ) then
        Error( "LOOPS: <1> must be nonempty." );
    elif Length(list) = 1 then
        return list[1];
    fi;
    for p in [1..Length(list)] do
        if not IsGroup(list[p]) then
            return DirectProductOp(Permuted(list, (1,p)), list[p]);
        fi;
    od;
    # OK, everything is a group, so let the rest of GAP do the work.
    TryNextMethod();
end);

InstallGlobalFunction(ProductTableOfCanonicalCayleyTables,
function(tablist)
    local i, nL, nM, TL, TM, T, j, k, s;
    TL := tablist[1];
    for s in [2..Length(tablist)] do
        TM := tablist[ s ];
        nL := Length( TL);  nM := Length( TM );
        T := List( [1..nL*nM], j->[] );
        # not efficient, but it does the job
        for i in [1..nM] do for j in [1..nM] do for k in [1..nL] do
            Append( T[ (i-1)*nL + k ], TL[ k ] + nL*(TM[i][j]-1) );
        od; od; od;
        TL := T;
    od;  
    return TL;
end);

InstallOtherMethod( DirectProductOp, "for DirectProduct( <IsList>, <IsQuasigroup> )",
    [ IsList, IsQuasigroup ],
function( list, dummy )

    local group_list, quasigroup_list, group_product, are_all_loops, n, i, T;

    # check the arguments
    if IsEmpty( list ) then
        Error( "LOOPS: <1> must be nonempty." );
    elif Length(list) = 1 then
        return list(1);
    fi;
    group_list := [];
    quasigroup_list := list{[1]};
    are_all_loops := true;
    for i in [2..Length(list)] do
        if IsGroup(list[i]) then 
            Add(group_list, list[i]);
        elif IsQuasigroup(list[i]) then
            Add(quasigroup_list, list[i]);
            if are_all_loops and not IsLoop(list[i]) then
                are_all_loops := false;
            fi;
        else # Oops, something that's neither a group or quasigroup
            return DirectProductOp(Permuted(list,(1.i)), list[i]);
        fi;
    od;

    # only groups, quasigroups and loops are on the list, with at least one
    # non-group; moreover, we have partitioned the list into groups and
    # non-groups and checked whether all quasigroups are really loops.
    if not IsEmpty( group_list ) then   # some groups are on the list
        group_product := DirectProductOp( group_list, group_list[ 1 ] );
        Add( quasigroup_list, IntoLoop( group_product ) );
    fi;

    # now only quasigroups and loops are on the list, with at least 2 of them
    n := Length( quasigroup_list );
    # We will not use recursion; start by making all Cayley tables canonical
    Apply(quasigroup_list, 
          Q -> CanonicalCayleyTableOfLeftQuasigroupTable( CayleyTable( Q ) ) );
    T := ProductTableOfCanonicalCayleyTables( quasigroup_list );
        
    if are_all_loops then
        return LoopByCayleyTable( T );
    fi;
    return QuasigroupByCayleyTable( T );
 end );

#############################################################################
## OPPOSITE QUASIGROUPS AND LOOPS
## --------------------------------------------------------------------------

#############################################################################
##
#O  OppositeQuasigroup( Q )
##
##  Identical to Opposite, except forces its return to be a quasigroup if
##  possible 

InstallGlobalFunction( OppositeQuasigroup,
                       Q -> IntoQuasigroup( Opposite( Q ) ) );

#############################################################################
##
#O  OppositeLoop( Q )
##
##  Identical to Opposite, except forces its return to be a loop if possible

InstallGlobalFunction( OppositeLoop, L -> IntoLoop( Opposite( L ) ) );

#############################################################################
##
#A  Opposite( M )
##
##  Returns the magma opposite to the magma <M>, with as much structure
##  as can be preserved.

InstallMethod( Opposite, "for magma",
    [ IsMagma and HasMultiplicationTable],
    M -> ConstructorFromTable(M)( TransposedMat( MultiplicationTable( M ) ) )
);

#############################################################################
##  DISPLAYING QUASIGROUPS AND LOOPS
##  -------------------------------------------------------------------------

InstallMethod( ViewObj, "for quasigroup",
    [ IsQuasigroup ],
function( Q )
    Print( "<quasigroup of order ", Size( Q ), ">" );
end );

## dangerous for large quasigroups
InstallMethod( PrintObj, "for quasigroup",
    [ IsQuasigroup ],
function( Q )
    if HasCayleyTable( Q ) then
        Print( "<quasigroup with multiplication table\n" );
        PrintArray( CayleyTable( Q ) );
    else
        Print( "<quasigroup with elements\n" );
        Print( Elements( Q ) );
    fi;
    Print( ">\n" );
end );

InstallMethod( ViewObj, "for loop",
    [ IsLoop ],
function( L )
    local PrintMe;
    
    PrintMe := function( name, L )
        Print( "<", name, " loop of order ", Size( L ), ">");
    
    end;
    if HasIsAssociative( L ) and IsAssociative( L ) then PrintMe( "associative", L ); 
    elif HasIsExtraLoop( L ) and IsExtraLoop( L ) then PrintMe( "extra", L ); 
    elif HasIsMoufangLoop( L ) and IsMoufangLoop( L ) then PrintMe( "Moufang", L ); 
    elif HasIsCLoop( L ) and IsCLoop( L ) then PrintMe( "C", L ); 
    elif HasIsLeftBruckLoop( L ) and IsLeftBruckLoop( L ) then PrintMe( "left Bruck", L );
    elif HasIsRightBruckLoop( L ) and IsRightBruckLoop( L ) then PrintMe( "right Bruck", L );
    elif HasIsLeftBolLoop( L ) and IsLeftBolLoop( L ) then PrintMe( "left Bol", L );
    elif HasIsRightBolLoop( L ) and IsRightBolLoop( L ) then PrintMe( "right Bol", L );
    elif HasIsAutomorphicLoop( L ) and IsAutomorphicLoop( L ) then PrintMe( "automorphic", L );
    elif HasIsLeftAutomorphicLoop( L ) and IsLeftAutomorphicLoop( L ) then PrintMe( "left automorphic", L );
    elif HasIsRightAutomorphicLoop( L ) and IsRightAutomorphicLoop( L ) then PrintMe( "right automorphic", L );
    elif HasIsLCLoop( L ) and IsLCLoop( L ) then PrintMe( "LC", L );
    elif HasIsRCLoop( L ) and IsRCLoop( L ) then PrintMe( "RC", L );
    elif HasIsLeftAlternative( L ) and IsLeftAlternative( L ) then
        if HasIsRightAlternative( L ) and IsRightAlternative( L ) then
            PrintMe("alternative", L );
        else 
            PrintMe("left alternative", L );
        fi;
    elif HasIsRightAlternative( L ) and IsRightAlternative( L ) then PrintMe( "right alternative", L );
    elif HasIsCommutative( L ) and IsCommutative( L ) then PrintMe( "commutative", L );
    elif HasIsFlexible( L ) and IsFlexible( L ) then PrintMe( "flexible", L);
    else
        # MORE ??
        Print( "<loop of order ", Size( L ), ">" );
    fi;
end );

## dangerous for large loops
InstallMethod( PrintObj, "for loop",
    [ IsLoop ],
function( L )
    if HasCayleyTable( L ) then
        Print( "<loop with multiplication table\n" );
        PrintArray( CayleyTable( L ) );
    else
        Print( "<loop with elements\n" );
        Print( Elements( L ) );
    fi;
    Print( ">\n" );
end );
