#############################################################################
##
#W cong.gi                 The Congruence package                   Ann Dooms
#W                                                               Eric Jespers
#W                                                         Olexandr Konovalov
##
##
#############################################################################


#############################################################################
##
## Constructors of congruence subgroups

InstallMethod( PrincipalCongruenceSubgroup, "for positive integer",
    [ IsPosInt ],
    function(n)
    local type, G;
    type := NewType( FamilyObj([[[1,0],[0,1]]]),
                     IsGroup and 
                     IsAttributeStoringRep and 
                     IsFinitelyGeneratedGroup and 
                     IsMatrixGroup and
                     IsCongruenceSubgroup);
    G := rec();                   
    ObjectifyWithAttributes( G, type, 
      DimensionOfMatrixGroup, 2,
      OneImmutable, [[1,0],[0,1]],
      IsIntegerMatrixGroup, true,
      IsFinite, false,
      LevelOfCongruenceSubgroup, n,
	  IsPrincipalCongruenceSubgroup, true,
      IsIntersectionOfCongruenceSubgroups, false,
      IsCongruenceSubgroupGamma0, false, IsCongruenceSubgroupGammaUpper0, false,
      IsCongruenceSubgroupGamma1, false, IsCongruenceSubgroupGammaUpper1, false,
      IsCongruenceSubgroupGammaMN, false );
    return G;
	end);


InstallMethod( CongruenceSubgroupGamma0, "for positive integer",
    [ IsPosInt ],
    function(n)
    local type, G;
    type := NewType( FamilyObj([[[1,0],[0,1]]]),
                     IsGroup and 
                     IsAttributeStoringRep and 
                     IsFinitelyGeneratedGroup and 
                     IsMatrixGroup and
                     IsCongruenceSubgroup);
    G := rec();                   
    ObjectifyWithAttributes( G, type, 
      DimensionOfMatrixGroup, 2,
      OneImmutable, [[1,0],[0,1]],
      IsIntegerMatrixGroup, true,
      IsFinite, false,
      LevelOfCongruenceSubgroup, n,
	  IsPrincipalCongruenceSubgroup, false,
	  IsIntersectionOfCongruenceSubgroups, false,
      IsCongruenceSubgroupGamma0, true,  IsCongruenceSubgroupGammaUpper0, false,
      IsCongruenceSubgroupGamma1, false, IsCongruenceSubgroupGammaUpper1, false,
      IsCongruenceSubgroupGammaMN, false );
    return G;
	end);
	
InstallMethod( CongruenceSubgroupGammaUpper0, "for positive integer",
    [ IsPosInt ],
    function(n)
    local type, G;
    type := NewType( FamilyObj([[[1,0],[0,1]]]),
                     IsGroup and 
                     IsAttributeStoringRep and 
                     IsFinitelyGeneratedGroup and 
                     IsMatrixGroup and
                     IsCongruenceSubgroup);
    G := rec();                   
    ObjectifyWithAttributes( G, type, 
      DimensionOfMatrixGroup, 2,
      OneImmutable, [[1,0],[0,1]],
      IsIntegerMatrixGroup, true,
      IsFinite, false,
      LevelOfCongruenceSubgroup, n,
	  IsPrincipalCongruenceSubgroup, false,
      IsIntersectionOfCongruenceSubgroups, false,
      IsCongruenceSubgroupGamma0, false, IsCongruenceSubgroupGammaUpper0, true,
      IsCongruenceSubgroupGamma1, false, IsCongruenceSubgroupGammaUpper1, false,
      IsCongruenceSubgroupGammaMN, false );
    return G;
	end);	
	

InstallMethod( CongruenceSubgroupGamma1, "for positive integer",
    [ IsPosInt ],
    function(n)
    local type, G;
    type := NewType( FamilyObj([[[1,0],[0,1]]]),
                     IsGroup and 
                     IsAttributeStoringRep and 
                     IsFinitelyGeneratedGroup and 
                     IsMatrixGroup and
                     IsCongruenceSubgroup);
    G := rec();                   
    ObjectifyWithAttributes( G, type, 
      DimensionOfMatrixGroup, 2,
      OneImmutable, [[1,0],[0,1]],
      IsIntegerMatrixGroup, true,
      IsFinite, false,
      LevelOfCongruenceSubgroup, n,
	  IsPrincipalCongruenceSubgroup, false,
	  IsIntersectionOfCongruenceSubgroups, false,
      IsCongruenceSubgroupGamma0, false, IsCongruenceSubgroupGammaUpper0, false,
      IsCongruenceSubgroupGamma1, true, IsCongruenceSubgroupGammaUpper1, false,
      IsCongruenceSubgroupGammaMN, false );
    return G;
	end);
	

InstallMethod( CongruenceSubgroupGammaUpper1, "for positive integer",
    [ IsPosInt ],
    function(n)
    local type, G;
    type := NewType( FamilyObj([[[1,0],[0,1]]]),
                     IsGroup and 
                     IsAttributeStoringRep and 
                     IsFinitelyGeneratedGroup and 
                     IsMatrixGroup and
                     IsCongruenceSubgroup);
    G := rec();                   
    ObjectifyWithAttributes( G, type, 
      DimensionOfMatrixGroup, 2,
      OneImmutable, [[1,0],[0,1]],
      IsIntegerMatrixGroup, true,
      IsFinite, false,
      LevelOfCongruenceSubgroup, n,
	  IsPrincipalCongruenceSubgroup, false,
	  IsIntersectionOfCongruenceSubgroups, false,
      IsCongruenceSubgroupGamma0, false, IsCongruenceSubgroupGammaUpper0, false,
      IsCongruenceSubgroupGamma1, false, IsCongruenceSubgroupGammaUpper1, true,
      IsCongruenceSubgroupGammaMN, false );
    return G;
	end);
	

InstallMethod( CongruenceSubgroupGammaMN, "for two positive integers",
    [ IsPosInt, IsPosInt ],
    function(m,n)
    local type, G;
    type := NewType( FamilyObj([[[1,0],[0,1]]]),
                     IsGroup and 
                     IsAttributeStoringRep and 
                     IsFinitelyGeneratedGroup and 
                     IsMatrixGroup and
                     IsCongruenceSubgroup);
    G := rec();                   
    ObjectifyWithAttributes( G, type, 
      DimensionOfMatrixGroup, 2,
      OneImmutable, [[1,0],[0,1]],
      IsIntegerMatrixGroup, true,
      IsFinite, false,
      LevelOfCongruenceSubgroup, m*n,
      LevelOfCongruenceSubgroupGammaMN, [m,n],
	  IsPrincipalCongruenceSubgroup, false,
	  IsIntersectionOfCongruenceSubgroups, false,
      IsCongruenceSubgroupGamma0, false, IsCongruenceSubgroupGammaUpper0, false,
      IsCongruenceSubgroupGamma1, false, IsCongruenceSubgroupGammaUpper1, false,
      IsCongruenceSubgroupGammaMN, true );
    return G;
	end);
		

InstallGlobalFunction( IntersectionOfCongruenceSubgroups,
    function( arg )
    local type, G, H, K, T, arglist, n, i, pos;
    type := NewType( FamilyObj([[[1,0],[0,1]]]),
                     IsGroup and 
                     IsAttributeStoringRep and 
                     IsFinitelyGeneratedGroup and 
                     IsMatrixGroup and
                     IsCongruenceSubgroup);
    if not ForAll( arg, IsCongruenceSubgroup ) then
      Error("Usage : IntersectionOfCongruenceSubgroups( G1, G2, ... GN ) \n");
    fi;
    # First we create a list arglist to eliminate evident repetitions of subgroups.
    # Then we eliminate evident inclusions of one subgroup into another:
    # - since intersection is associative, if we can intersect the group T which
    #   is to be added with another subgroup K already contained in alglist, and
    #   the result is one of the canonical congruence subgroups, we replace K by
    #   the result of intersection of K and T 
    # - we do not add a subgroup T to the list of defining subgroups, if alglist 
    #   already contains another subgroup K such that K is in T. 
    # - if we add to alglist a subgroup T and alglist already contains one or more
    #   subgroups K such that T is in K, we add T and remove all these K.
    arglist := [];
    for H in arg do
      if IsIntersectionOfCongruenceSubgroups(H) then
        for T in DefiningCongruenceSubgroups( H ) do
          pos:=PositionProperty( arglist, K -> 
                 CanReduceIntersectionOfCongruenceSubgroups( K, T ) );
          if pos<>fail then
            arglist[pos]:=Intersection( arglist[pos], T );
          else
            if ForAll( arglist, K -> not CanEasilyCompareCongruenceSubgroups( K, T ) ) and
               ForAll( arglist, K -> not IsSubgroup( T, K ) ) then
              for i in [ 1 .. Length(arglist) ] do
                if IsSubgroup( arglist[i], T ) then
                  Unbind( arglist[i] );
                fi;
              od;
              arglist := Compacted( arglist );    
              Add( arglist, T );
            fi;    
          fi;
        od;
      else
        pos:=PositionProperty( arglist, K -> 
               CanReduceIntersectionOfCongruenceSubgroups( K, H ) );
        if pos<>fail then
          arglist[pos]:=Intersection( arglist[pos], H );
        else     
          if ForAll( arglist, K -> not CanEasilyCompareCongruenceSubgroups( K, H ) ) and
             ForAll( arglist, K -> not IsSubgroup( H, K ) ) then
           for i in [ 1 .. Length(arglist) ] do
              if IsSubgroup( arglist[i], H ) then
                Unbind( arglist[i] );
              fi;
            od;
            arglist := Compacted( arglist );    
            Add( arglist, H );
          fi;
        fi;  
      fi;
    od;
    # if the list of defining subgroups was reduced 
    # to a single subgroup, we return this subgroup
    if Length( arglist ) = 1 then
      return arglist[1];
    fi; 
    # otherwise we sort the list of defining subgroups:
    # types of subgroups are sorted in the following way:
    # - IsCongruenceSubgroupGamma0
    # - IsCongruenceSubgroupGammaUpper0
    # - IsCongruenceSubgroupGamma1
    # - IsCongruenceSubgroupGammaUpper1
    # - IsPrincipalCongruenceSubgroup
    # and subgroups of the same type are sorted by ascending level
    Sort( arglist, 
          function(X,Y) 
          local f, t;
          f:=[IsCongruenceSubgroupGamma0,IsCongruenceSubgroupGammaUpper0,IsCongruenceSubgroupGamma1,IsCongruenceSubgroupGammaUpper1,IsPrincipalCongruenceSubgroup];
          return PositionProperty(f, t -> t(X)) < PositionProperty(f, t -> t(Y)) or
               ( PositionProperty(f, t -> t(X)) = PositionProperty(f, t -> t(Y)) and
                 LevelOfCongruenceSubgroup(X) < LevelOfCongruenceSubgroup(Y) ); 
          end );
    n := Lcm( List( arglist, H -> LevelOfCongruenceSubgroup(H) ) );                     
    G := rec();                   
    ObjectifyWithAttributes( G, type, 
      DimensionOfMatrixGroup, 2,
      OneImmutable, [[1,0],[0,1]],	
      IsIntegerMatrixGroup, true,
      IsFinite, false,
      LevelOfCongruenceSubgroup, n,
	  IsPrincipalCongruenceSubgroup, false,
	  IsIntersectionOfCongruenceSubgroups, true,
      IsCongruenceSubgroupGamma0, false, IsCongruenceSubgroupGammaUpper0, false,
      IsCongruenceSubgroupGamma1, false, IsCongruenceSubgroupGammaUpper1, false,
      DefiningCongruenceSubgroups, arglist );
    return G;
	end);
	
InstallMethod( DefiningCongruenceSubgroups, "for congruence subgroups",
    [ IsCongruenceSubgroup ],
    function(G)
    if not IsIntersectionOfCongruenceSubgroups(G) then
      return [G];
    fi;
	end);
  		
#############################################################################
##
## Methods for PrintObj and ViewObj for congruence subgroups		
	
InstallMethod( ViewObj,
    "for principal congruence subgroup",
    [ IsPrincipalCongruenceSubgroup ],
    0,
    function( G )
      Print( "<principal congruence subgroup of level ", 
             LevelOfCongruenceSubgroup(G), " in SL_2(Z)>" );    
    end );
    
    
InstallMethod( PrintObj,
    "for principal congruence subgroup",
    [ IsPrincipalCongruenceSubgroup ],
    0,
    function( G )
      Print( "PrincipalCongruenceSubgroup(", 
             LevelOfCongruenceSubgroup(G), ")" );    
    end ); 
    

InstallMethod( ViewObj,
    "for CongruenceSubgroupGamma0 congruence subgroup",
    [ IsCongruenceSubgroupGamma0 ],
    0,
    function( G )
      Print( "<congruence subgroup CongruenceSubgroupGamma_0(", 
             LevelOfCongruenceSubgroup(G), ") in SL_2(Z)>" );    
    end );
    
    
InstallMethod( PrintObj,
    "for CongruenceSubgroupGamma0 congruence subgroup",
    [ IsCongruenceSubgroupGamma0 ],
    0,
    function( G )
      Print( "CongruenceSubgroupGamma0(", 
             LevelOfCongruenceSubgroup(G), ")" );    
    end );     
    

InstallMethod( ViewObj,
    "for CongruenceSubgroupGammaUpper0 congruence subgroup",
    [ IsCongruenceSubgroupGammaUpper0 ],
    0,
    function( G )
      Print( "<congruence subgroup CongruenceSubgroupGamma^0(", 
             LevelOfCongruenceSubgroup(G), ") in SL_2(Z)>" );    
    end );
    
    
InstallMethod( PrintObj,
    "for CongruenceSubgroupGammaUpper0 congruence subgroup",
    [ IsCongruenceSubgroupGammaUpper0 ],
    0,
    function( G )
      Print( "CongruenceSubgroupGammaUpper0(", 
             LevelOfCongruenceSubgroup(G), ")" );    
    end );    
    

InstallMethod( ViewObj,
    "for CongruenceSubgroupGamma1 congruence subgroup",
    [ IsCongruenceSubgroupGamma1 ],
    0,
    function( G )
      Print( "<congruence subgroup CongruenceSubgroupGamma_1(", 
             LevelOfCongruenceSubgroup(G), ") in SL_2(Z)>" );    
    end );
    
    
InstallMethod( PrintObj,
    "for CongruenceSubgroupGamma1 congruence subgroup",
    [ IsCongruenceSubgroupGamma1 ],
    0,
    function( G )
      Print( "CongruenceSubgroupGamma1(", 
             LevelOfCongruenceSubgroup(G), ")" );    
    end );     


InstallMethod( ViewObj,
    "for CongruenceSubgroupGammaUpper1 congruence subgroup",
    [ IsCongruenceSubgroupGammaUpper1 ],
    0,
    function( G )
      Print( "<congruence subgroup CongruenceSubgroupGamma^1(", 
             LevelOfCongruenceSubgroup(G), ") in SL_2(Z)>" );    
    end );
    
    
InstallMethod( PrintObj,
    "for CongruenceSubgroupGammaUpper1 congruence subgroup",
    [ IsCongruenceSubgroupGammaUpper1 ],
    0,
    function( G )
      Print( "CongruenceSubgroupGammaUpper1(", 
             LevelOfCongruenceSubgroup(G), ")" );    
    end );  


InstallMethod( ViewObj,
    "for CongruenceSubgroupGammaMN congruence subgroup",
    [ IsCongruenceSubgroupGammaMN ],
    0,
    function( G )
      Print( "<congruence subgroup CongruenceSubgroupGammaMN(", 
             LevelOfCongruenceSubgroupGammaMN(G)[1], ",", 
             LevelOfCongruenceSubgroupGammaMN(G)[2], ") in SL_2(Z)>" );    
    end );
    
    
InstallMethod( PrintObj,
    "for CongruenceSubgroupGammaMN congruence subgroup",
    [ IsCongruenceSubgroupGammaMN ],
    0,
    function( G )
      Print( "CongruenceSubgroupGammaMN(", 
              LevelOfCongruenceSubgroupGammaMN(G)[1], ",", 
             LevelOfCongruenceSubgroupGammaMN(G)[2], ")" );    
    end ); 
    

InstallMethod( ViewObj,
    "for intersection of congruence subgroups",
    [ IsIntersectionOfCongruenceSubgroups ],
    0,
    function( G )
      Print( "<intersection of congruence subgroups of resulting level ", 
             LevelOfCongruenceSubgroup(G), " in SL_2(Z)>" );    
    end );
    
    
InstallMethod( PrintObj,
    "for intersection of congruence subgroups",
    [ IsIntersectionOfCongruenceSubgroups ],
    0,
    function( G )
      local i, k;
      k := Length(DefiningCongruenceSubgroups(G)); 
      Print( "IntersectionOfCongruenceSubgroups( \n" );
      for i in [ 1 .. k-1 ] do
        Print( "  ", DefiningCongruenceSubgroups(G)[i], ", \n" );
      od;  
      Print( "  ", DefiningCongruenceSubgroups(G)[k], " )" );    
    end );
    

#############################################################################
##
## Membership tests for congruence subgroups

InstallMethod( \in,
    "for a 2x2 matrix and a principal congruence subgroup",
    [ IsMatrix, IsPrincipalCongruenceSubgroup],
    0,
    function( m, G )
    local n;
    if not DimensionsMat( m ) = [2,2] then
      return false;
    elif DeterminantMat(m)<>1 then
      return false;  
    else
      n := LevelOfCongruenceSubgroup(G);
      return IsInt( (m[1][1]-1)/n ) and 
             IsInt(m[1][2]/n) and 
             IsInt(m[2][1]/n) and 
             IsInt( (m[2][2]-1)/n );
    fi;  
    end);         


InstallMethod( \in,
    "for a 2x2 matrix and a congruence subgroup CongruenceSubgroupGamma0",
    [ IsMatrix, IsCongruenceSubgroupGamma0 ],
    0,
    function( m, G )
    local n;
    if not DimensionsMat( m ) = [2,2] then
      return false;
    elif DeterminantMat(m)<>1 then
      return false;  
    else
      n := LevelOfCongruenceSubgroup(G);
      return IsInt(m[2][1]/n);
    fi;  
    end); 
    

InstallMethod( \in,
    "for a 2x2 matrix and a congruence subgroup CongruenceSubgroupGammaUpper0",
    [ IsMatrix, IsCongruenceSubgroupGammaUpper0 ],
    0,
    function( m, G )
    local n;
    if not DimensionsMat( m ) = [2,2] then
      return false;
    elif DeterminantMat(m)<>1 then
      return false;  
    else
      n := LevelOfCongruenceSubgroup(G);
      return IsInt(m[1][2]/n);
    fi;  
    end);
    

InstallMethod( \in,
    "for a 2x2 matrix and a congruence subgroup CongruenceSubgroupGamma1",
    [ IsMatrix, IsCongruenceSubgroupGamma1 ],
    0,
    function( m, G )
    local n;
    if not DimensionsMat( m ) = [2,2] then
      return false;
    elif DeterminantMat(m)<>1 then
      return false;  
    else
      n := LevelOfCongruenceSubgroup(G);
      return IsInt( (m[1][1]-1)/n ) and 
             IsInt( m[2][1]/n ) and 
             IsInt( (m[2][2]-1)/n );
    fi;  
    end);
  

InstallMethod( \in,
    "for a 2x2 matrix and a congruence subgroup CongruenceSubgroupGammaUpper1",
    [ IsMatrix, IsCongruenceSubgroupGammaUpper1 ],
    0,
    function( m, G )
    local n;
    if not DimensionsMat( m ) = [2,2] then
      return false;
    elif DeterminantMat(m)<>1 then
      return false;  
    else
      n := LevelOfCongruenceSubgroup(G);
      return IsInt( (m[1][1]-1)/n ) and 
             IsInt( m[1][2]/n ) and 
             IsInt( (m[2][2]-1)/n );
    fi;  
    end);
    

InstallMethod( \in,
    "for a 2x2 matrix and a congruence subgroup CongruenceSubgroupGammaMN",
    [ IsMatrix, IsCongruenceSubgroupGammaMN ],
    0,
    function( mat, G )
    local m, n;
    if not DimensionsMat( mat ) = [2,2] then
      return false;
    elif DeterminantMat(mat)<>1 then
      return false;  
    else
      m := LevelOfCongruenceSubgroupGammaMN(G)[1];
      n := LevelOfCongruenceSubgroupGammaMN(G)[2];
      return IsInt( (mat[1][1]-1)/m ) and 
             IsInt(mat[1][2]/m) and 
             IsInt(mat[2][1]/n) and 
             IsInt( (mat[2][2]-1)/n );
    fi;  
    end);    
    
    
InstallMethod( \in,
    "for an intersection of congruence subgroups",
    [ IsMatrix, IsIntersectionOfCongruenceSubgroups ],
    0,
    function( m, G )
    local H;
    if not DimensionsMat( m ) = [2,2] then
      return false;
    elif DeterminantMat(m)<>1 then
      return false;  
    else
      return ForAll( DefiningCongruenceSubgroups(G), H -> m in H );
    fi;  
    end);
    
    
#############################################################################
##
## Installing special methods for congruence subgroups 
## for some general methods installed in GAP for matrix groups
    
InstallMethod( DimensionOfMatrixGroup,
    "for congruence subgroup",
    [ IsCongruenceSubgroup ],
    0,
    G -> 2 );  
 
InstallMethod( \=,
    "for a pair of congruence subgroups",
    [ IsCongruenceSubgroup, IsCongruenceSubgroup ],
    0,
    function( G, H )
    if CanEasilyCompareCongruenceSubgroups( G, H ) then
      return true;
    else
      TryNextMethod();
    fi;  
    end);


#############################################################################
##
## IsSubset
##
#############################################################################

InstallMethod( IsSubset,
    "for a natural SL_2(Z) and a congruence subgroup",
    [ IsNaturalSL, IsCongruenceSubgroup ],
    0,
    function( G, H )
    return MultiplicativeNeutralElement(G)=[ [ 1, 0 ], [ 0, 1 ] ];
    end);

InstallMethod( IsSubset,
    "for a congruence subgroup and a principal congruence subgroup",
    [ IsCongruenceSubgroup, IsPrincipalCongruenceSubgroup ],
    0,
    function( G, H )
    local T;
    if IsIntersectionOfCongruenceSubgroups(G) then
      return ForAll( DefiningCongruenceSubgroups(G), T -> IsSubset(T,H) );
    elif IsPrincipalCongruenceSubgroup(G) or 
         IsCongruenceSubgroupGamma1(G) or IsCongruenceSubgroupGammaUpper1(G) or
         IsCongruenceSubgroupGamma0(G) or IsCongruenceSubgroupGammaUpper0(G) then
      return IsInt( LevelOfCongruenceSubgroup(H) / LevelOfCongruenceSubgroup(G) ); 
    else
      # for a case of another type of congruence subgroup  
      TryNextMethod();
    fi;  
    end); 
    
InstallMethod( IsSubset,
    "for a congruence subgroup and CongruenceSubgroupGamma1",
    [ IsCongruenceSubgroup, IsCongruenceSubgroupGamma1 ],
    0,
    function( G, H )
    local T;
    if IsIntersectionOfCongruenceSubgroups(G) then
      return ForAll( DefiningCongruenceSubgroups(G), T -> IsSubset(T,H) );
    elif IsPrincipalCongruenceSubgroup(G) or 
         IsCongruenceSubgroupGammaUpper1(G) or IsCongruenceSubgroupGammaUpper0(G) then
      return false;
    elif IsCongruenceSubgroupGamma1(G) or IsCongruenceSubgroupGamma0(G) then
      return IsInt( LevelOfCongruenceSubgroup(H) / LevelOfCongruenceSubgroup(G) ); 
    else
      # for a case of another type of congruence subgroup  
      TryNextMethod();
    fi;  
    end); 
    
InstallMethod( IsSubset,
    "for a congruence subgroup and CongruenceSubgroupGammaUpper1",
    [ IsCongruenceSubgroup, IsCongruenceSubgroupGammaUpper1 ],
    0,
    function( G, H )
    local T;
    if IsIntersectionOfCongruenceSubgroups(G) then
      return ForAll( DefiningCongruenceSubgroups(G), T -> IsSubset(T,H) );
    elif IsPrincipalCongruenceSubgroup(G) or 
         IsCongruenceSubgroupGamma1(G) or IsCongruenceSubgroupGamma0(G) then
      return false;
    elif IsCongruenceSubgroupGammaUpper1(G) or IsCongruenceSubgroupGammaUpper0(G) then
      return IsInt( LevelOfCongruenceSubgroup(H) / LevelOfCongruenceSubgroup(G) ); 
    else
      # for a case of another type of congruence subgroup  
      TryNextMethod();
    fi;  
    end); 
    
InstallMethod( IsSubset,
    "for a congruence subgroup and CongruenceSubgroupGamma0",
    [ IsCongruenceSubgroup, IsCongruenceSubgroupGamma0 ],
    0,
    function( G, H )
    local T;
    if IsIntersectionOfCongruenceSubgroups(G) then
      return ForAll( DefiningCongruenceSubgroups(G), T -> IsSubset(T,H) );
    elif IsPrincipalCongruenceSubgroup(G) or 
         IsCongruenceSubgroupGamma1(G) or IsCongruenceSubgroupGammaUpper1(G) or IsCongruenceSubgroupGammaUpper0(G) then
      return false;
    elif IsCongruenceSubgroupGamma0(G) then
      return IsInt( LevelOfCongruenceSubgroup(H) / LevelOfCongruenceSubgroup(G) ); 
    else
      # for a case of another type of congruence subgroup  
      TryNextMethod();
    fi;  
    end);     
    
InstallMethod( IsSubset,
    "for a congruence subgroup and CongruenceSubgroupGammaUpper0",
    [ IsCongruenceSubgroup, IsCongruenceSubgroupGammaUpper0 ],
    0,
    function( G, H )
    local T;
    if IsIntersectionOfCongruenceSubgroups(G) then
      return ForAll( DefiningCongruenceSubgroups(G), T -> IsSubset(T,H) );
    elif IsPrincipalCongruenceSubgroup(G) or 
         IsCongruenceSubgroupGamma1(G) or IsCongruenceSubgroupGammaUpper1(G) or IsCongruenceSubgroupGamma0(G) then
      return false;
    elif IsCongruenceSubgroupGammaUpper0(G) then
      return IsInt( LevelOfCongruenceSubgroup(H) / LevelOfCongruenceSubgroup(G) ); 
    else
      # for a case of another type of congruence subgroup  
      TryNextMethod();
    fi;  
    end);
    
InstallMethod( IsSubset,
    "for a congruence subgroup and intersection of congruence subgroups",
    [ IsCongruenceSubgroup, IsIntersectionOfCongruenceSubgroups ],
    0,
    function( G, H )
    local DG, DH;
    # here we can check only sufficient conditions, and they are not 
    # satisfied, then we call the next method
    if IsIntersectionOfCongruenceSubgroups(G) then
      if ForAll( DefiningCongruenceSubgroups(H), DH -> 
                 ForAll( DefiningCongruenceSubgroups(G), DG -> 
                         IsSubset(G,DH) ) ) then
        return true;
      else
        TryNextMethod();
      fi;
    elif IsPrincipalCongruenceSubgroup(G) or 
         IsCongruenceSubgroupGamma1(G) or IsCongruenceSubgroupGammaUpper1(G) or 
         IsCongruenceSubgroupGamma0(G) or IsCongruenceSubgroupGammaUpper0(G) then
      if ForAll( DefiningCongruenceSubgroups(H), DH -> IsSubset(G,DH) ) then
        return true;
      else
        TryNextMethod();
      fi;  
    else
      # for a case of another type of congruence subgroup  
      TryNextMethod();
    fi;  
    end);    
    
    
#############################################################################
##
## Intersection2
##
#############################################################################


InstallMethod( Intersection2,
    "for a pair of congruence subgroups",
    [ IsCongruenceSubgroup, IsCongruenceSubgroup ],
    0,
    function( G, H )
    #
    # Case 1 - at least one subgroup is an intersection of congruence subgroups
    #
    if IsIntersectionOfCongruenceSubgroups(G) or
       IsIntersectionOfCongruenceSubgroups(H) then
      return IntersectionOfCongruenceSubgroups(G,H);
    #
    # Case 2 - the diagonal (both subgroups has the same type)
    # 
    elif IsPrincipalCongruenceSubgroup(G) and IsPrincipalCongruenceSubgroup(H) then
      return PrincipalCongruenceSubgroup( Lcm( LevelOfCongruenceSubgroup(G),
                                               LevelOfCongruenceSubgroup(H) ) );
    elif IsCongruenceSubgroupGamma1(G) and IsCongruenceSubgroupGamma1(H) then
      return CongruenceSubgroupGamma1( Lcm( LevelOfCongruenceSubgroup(G),
                          LevelOfCongruenceSubgroup(H) ) );
    elif IsCongruenceSubgroupGammaUpper1(G) and IsCongruenceSubgroupGammaUpper1(H) then
      return CongruenceSubgroupGammaUpper1( Lcm( LevelOfCongruenceSubgroup(G),
                          LevelOfCongruenceSubgroup(H) ) );
    elif IsCongruenceSubgroupGamma0(G) and IsCongruenceSubgroupGamma0(H) then
      return CongruenceSubgroupGamma0( Lcm( LevelOfCongruenceSubgroup(G),
                          LevelOfCongruenceSubgroup(H) ) );
    elif IsCongruenceSubgroupGammaUpper0(G) and IsCongruenceSubgroupGammaUpper0(H) then
      return CongruenceSubgroupGammaUpper0( Lcm( LevelOfCongruenceSubgroup(G),
                          LevelOfCongruenceSubgroup(H) ) );
    #
    # Case 3 - Subgroups has different level
    #
    elif LevelOfCongruenceSubgroup(G) <> LevelOfCongruenceSubgroup(H) then
      return IntersectionOfCongruenceSubgroups(G,H);
    #
    # Now subgroups have the same level
    #
    elif IsCongruenceSubgroupGamma1(G) and IsCongruenceSubgroupGamma0(H) then
      return G; # so all properties and attributes of G will be preserved
    elif IsCongruenceSubgroupGamma0(G) and IsCongruenceSubgroupGamma1(H) then
      return H; 
    elif IsCongruenceSubgroupGammaUpper1(G) and IsCongruenceSubgroupGammaUpper0(H) then
      return G;
    elif IsCongruenceSubgroupGammaUpper0(G) and IsCongruenceSubgroupGammaUpper1(H) then
      return H;
    elif IsCongruenceSubgroupGamma0(G) and IsCongruenceSubgroupGammaUpper0(H) or IsCongruenceSubgroupGammaUpper0(G) and IsCongruenceSubgroupGamma0(H) then
      return IntersectionOfCongruenceSubgroups(G,H);
    else
      return PrincipalCongruenceSubgroup(LevelOfCongruenceSubgroup(G));
    fi;                                             
    end);
    

#############################################################################
##
## Indices of congruence subgroups
##
#############################################################################

    
InstallMethod( Index,
    "for a natural SL_2(Z) and a congruence subgroup",
    [ IsNaturalSL, IsCongruenceSubgroup ],
    0,
    function( G, H )
    local n, prdiv, r, p;
    n := LevelOfCongruenceSubgroup(H);     
    if HasIsPrincipalCongruenceSubgroup( H ) and 
       IsPrincipalCongruenceSubgroup( H ) then
      if n=1 then
        Assert( 1, IndexInPSL2ZByFareySymbol( FareySymbol ( H ) ) = 1 );
        return 1;
      elif n=2 then
        Assert( 1, IndexInPSL2ZByFareySymbol( FareySymbol ( H ) ) = 6 );
        return 12; # not 6, since we are in SL, not in PSL
      else
        prdiv := Set( Factors( n ) );
        r := n^3; # not (n^3)/2 since we are in SL, not in PSL
        for p in prdiv do
          r := r*(1-1/p^2);
        od;
        Assert( 1, IndexInPSL2ZByFareySymbol( FareySymbol ( H ) ) = r/2 );
        return r;
      fi;
    elif ( HasIsCongruenceSubgroupGamma0( H ) and IsCongruenceSubgroupGamma0( H ) ) or 
         ( HasIsCongruenceSubgroupGammaUpper0( H ) and IsCongruenceSubgroupGammaUpper0( H ) ) then
      # for CongruenceSubgroupGamma0 we use the formula
      # [ SL_2(Z) : CongruenceSubgroupGamma0(n) ] = n * "Product over prime p | n" ( 1 + 1/p )
      prdiv := Set( Factors( n ) );
      r := n; 
      for p in prdiv do
        r := r*(1+1/p);
      od;
      Assert( 1, IndexInPSL2ZByFareySymbol( FareySymbol ( H ) ) = r );
      return r;
    elif ( HasIsCongruenceSubgroupGamma1( H ) and IsCongruenceSubgroupGamma1( H ) ) or 
         ( HasIsCongruenceSubgroupGammaUpper1( H ) and IsCongruenceSubgroupGammaUpper1( H ) ) then 
      # for CongruenceSubgroupGamma1 we use the formula
      # [ CongruenceSubgroupGamma0(n) : CongruenceSubgroupGamma1(n) ] = n * "Product over prime p | n" ( 1 - 1/p )
      # Combining with the previous case, we get that
      # [ SL_2(Z) : CongruenceSubgroupGamma1(n) ] = n^2 * "Product over prime p | n" ( 1 - 1/p^2 )
      prdiv := Set( Factors( n ) );
      r := n^2;
      for p in prdiv do
        r := r*(1-1/p^2);
      od;
      Assert( 1, IndexInPSL2ZByFareySymbol( FareySymbol ( H ) ) = r/2 );
      return r;
    else
      # if H is not in any of the cases above, for example is an intersection
      # of some congruence subgroups, we derive the index from its Farey symbol
      if [[-1,0],[0,-1]] in H then
        return IndexInPSL2ZByFareySymbol( FareySymbol ( H ) ) ;
      else
        return IndexInPSL2ZByFareySymbol( FareySymbol ( H ) ) * 2;
      fi;  
    fi;  
    end);    
    

InstallMethod( IndexInSL2Z,
    "for a congruence subgroup",
    [ IsCongruenceSubgroup ],
    0,
    G -> Index( SL(2,Integers), G ) );
    

InstallMethod( Index,
    "for a pair of congruence subgroups",
    [ IsCongruenceSubgroup, IsCongruenceSubgroup ],
    0,
    function( G, H )
    if IsSubgroup( G, H ) then
      return IndexInSL2Z(H)/IndexInSL2Z(G);
    fi;  
    end);

    
#############################################################################
##
## Generators of confruence subgroups from Farey symbols
##
#############################################################################


InstallMethod( GeneratorsOfGroup,
	"for a congruence subgroup",
	[ IsCongruenceSubgroup ],
	0,
	function(G)
	local gens, i;
	Info( InfoCongruence, 1, "Using the Congruence package for GeneratorsOfGroup ...");
	gens := GeneratorsByFareySymbol( FareySymbol( G ) );
	for i in [ 1 .. Length(gens) ] do
	  if not gens[i] in G then
	    gens[i] := -gens[i];
	    Assert( 1, gens[i] in G );
	  fi;
	od;
	return gens;
	end );
	
	
#############################################################################
##
#E
##
