#############################################################################
##
#W  access.gi            GAP 4 package `atlasrep'               Thomas Breuer
##
#H  @(#)$Id: access.gi,v 1.76 2004/03/19 16:38:45 gap Exp $
##
#Y  Copyright (C)  2001,  Lehrstuhl D fuer Mathematik,  RWTH Aachen,  Germany
##
##  This file contains functions for accessing data from the ATLAS of Group
##  Representations.
##
Revision.( "atlasrep/gap/access_gi" ) :=
    "@(#)$Id: access.gi,v 1.76 2004/03/19 16:38:45 gap Exp $";


#############################################################################
##
#F  FilenameAtlas( <dirname>, <groupname>, <filename> )
##
InstallGlobalFunction( FilenameAtlas,
    function( dirname, groupname, filename )
    local datadirs,
          private,
          privdir,
          len,
          datafile,
          triple,
          script,
          infile,
          outfile,
          perl,
          wget,
          gzip,
          info,
          server,
          path,
          login,
          password,
          result,
          result2;

    # Get the absolute path name.
    if dirname in [ "datagens", "dataword" ] then
      datadirs:= DirectoriesPackageLibrary( "atlasrep", dirname );
    else
      private:= AtlasOfGroupRepresentationsInfo.private;
      privdir:= 0;
      for info in private do
        if dirname = info[2] then
          privdir:= info[1];
          break;
        fi;
      od;
      if privdir = 0 then
        Error( "no private directory with identifier `", dirname, "'" );
      fi;
      datadirs:= [ Directory( privdir ) ];
    fi;

    # Check whether the file is already stored.
    datafile:= Filename( datadirs, filename );
    if datafile = fail then

      # The file is not stored.
      # If it is not private then try to transfer it.
      if     AtlasOfGroupRepresentationsInfo.remote = true
         and dirname in [ "datagens", "dataword" ] then

        # Get the group name info.
        triple:= First( AtlasOfGroupRepresentationsInfo.groupnames,
                        x -> x[3] = groupname );
        if triple = fail then
          Error( "illegal value of <groupname>" );
        fi;

        # Get the necessary files (either `wget' or `perl' and `perlftp.pl').
        if IsBound( AtlasOfGroupRepresentationsInfo.wget ) and
           AtlasOfGroupRepresentationsInfo.wget = true then
          wget:= Filename( DirectoriesSystemPrograms(), "wget" );
          if wget = fail then
            Info( InfoAtlasRep, 1, "no `wget' executable found" );
            return fail;
          fi;
        else
          script:= Filename( datadirs[1], "../etc/perlftp.pl" );
          if IsExecutableFile( script ) <> true then
            Info( InfoAtlasRep, 1, "no script `etc/perlftp.pl' found" );
            return fail;
          fi;
          script:= "../etc/perlftp.pl";
          perl:= Filename( DirectoriesSystemPrograms(), "perl" );
          if perl = fail then
            Info( InfoAtlasRep, 1, "no `perl' executable found" );
            return fail;
          fi;
        fi;

        # Try to fetch the remote file.
        for info in AtlasOfGroupRepresentationsInfo.servers do

          server   := info[1];
          path     := info[2];
          login    := info[3];
          password := info[4];

          # Compose the name of the directory on the server.
          if dirname = "datagens" then
            if   filename[ Length( filename ) ] = 'g' then
              dirname:= Concatenation( path, triple[1],"/",triple[2],"/gap0/" );
            else
              dirname:= Concatenation( path, triple[1],"/",triple[2],"/mtx/" );
            fi;
          elif dirname = "dataword" then
            dirname:= Concatenation( path, triple[1],"/",triple[2],"/words/" );
          else
            Error( "illegal value of <dirname>" );
          fi;

          # Fetch the file if possible.
          if IsBound( wget ) then
            Info( InfoAtlasRep, 2,
                  "calling `wget' to get `", filename, "'" );
            result:= Process( datadirs[1], wget,
                InputTextNone(), OutputTextNone(),
                [ "-q", Concatenation( "ftp://", login, ":", password, "@",
                                       server, "/", dirname, filename ) ] );
          else
            Info( InfoAtlasRep, 2,
                  "calling `etc/perlftp.pl' to get `", filename, "'" );
            result:= Process( datadirs[1], perl,
                InputTextNone(), OutputTextNone(),
                [ script, server, dirname, login, password, "get",
                  filename ] );
          fi;

          if result = fail then
            Info( InfoAtlasRep, 2,
                  "no connection to AtlasRep server ", server );
          else
            result:= Filename( datadirs, filename );
          fi;
          if result <> fail then
            datafile:= result;
            break;
          fi;

        od;

        if datafile = fail then
          Info( InfoAtlasRep, 1,
                "no file `", filename, "' found on the servers" );
          return fail;
        fi;

        # If wanted then compress the new file (only for MeatAxe format).
        if     AtlasOfGroupRepresentationsInfo.compress = true
           and dirname[ Length( dirname ) - 1 ] = 'x' then
          gzip:= Filename( DirectoriesSystemPrograms(), "gzip" );
          if gzip = fail then
            Info( InfoAtlasRep, 1, "no `gzip' executable found" );
            return fail;
          fi;
          result:= Process( DirectoryCurrent(), gzip,
             InputTextNone(), OutputTextNone(), [ datafile ] );
          if result = fail then
            Info( InfoAtlasRep, 2,
                  "impossible to compress file `", datafile, "'" );
          fi;
        fi;

      else

        # The file cannot be made available.
        Info( InfoAtlasRep, 1,
              "no file `", filename, "' found in the local directories" );
        return fail;

      fi;

    fi;

    # Return the filename.
    return datafile;
end );


#############################################################################
##
#F  DirectoryTree( <string> )
##
InstallGlobalFunction( DirectoryTree, function( string )
    local lines, tree, i, len, line, lline, dir, file;

    # Split the string into lines.
    lines:= SplitString( string, "", "\n" );

    # Omit lines of the form `total <n>' that occur for `ls -l' output.
    lines:= Filtered( lines,
                l -> Length( l ) < 6 or l{ [ 1 .. 6 ] } <> "total " );

    # Initialize the result.
    tree:= rec();
    i:= 1;
    len:= Length( lines );

    # The files in the top directory are listed before the first leading dot.
    while i <= len do
      line:= lines[i];
      if line[ Length( line ) ] = ':' and line <> ".:" then
        break;
      fi;
      line:= SplitString( line, "", " " );
      tree.( line[ Length( line ) ] ):= rec();
      i:= i+1;
    od;

    # Scan blocks separated by lines of the form `./<path>:' or `<path>:',
    # which describe the directory in which the following files reside.
    while i <= len do

      # If we deal with `ls -l' output then cut split the line at blanks.
      line:= SplitString( lines[i], "", " " );

      # Step down the directory hierarchy of the path.
      lline:= SplitString( line[ Length( line ) ], ".:", "/" );
      dir:= tree;
      for file in lline do
        if not IsEmpty( file ) then
          dir:= dir.( file );
        fi;
      od;
      i:= i+1;

      # Store filenames (and `ls -l' info if applicable) in the directory.
      while i <= len do
        line:= lines[i];
        if line[ Length( line ) ] = ':' then
          break;
        fi;
        line:= SplitString( line, "", " " );
        dir.( line[ Length( line ) ] ):=
            rec( data := line{ [ 1 .. Length( line ) - 1 ] } );
        i:= i+1;
      od;

    od;

    # Return the result.
    return tree;
end );


#############################################################################
##
#F  AtlasFilesFromDirectoryTree( <tree> )
##
InstallGlobalFunction( AtlasFilesFromDirectoryTree, function( tree )
    local filenames,
          dir,
          sub,
          simpname,
          dirname,
          filename,
          info;

    # Check whether the notified directories are there.
    if not ForAll( AtlasOfGroupRepresentationsInfo.dirnames,
                   name -> IsBound( tree.( name ) ) ) then
      Info( InfoAtlasRep, 1,
            "some necessary top level directory is missing" );
      return fail;
    fi;

    # Walk through the tree.
    filenames:= [];
    for dir in AtlasOfGroupRepresentationsInfo.dirnames do

      sub:= tree.( dir );
      for simpname in RecNames( sub ) do
        if sub.( simpname ) <> rec() then

          # Add the files in the subdirectories `gap0', `mtx', and `words'.
          for dirname in [ "gap0", "mtx", "words" ] do
            if IsBound( sub.( simpname ).( dirname ) ) then
              for filename in RecNames( sub.( simpname ).( dirname ) ) do
                if filename <> "data" then
                  info:= [ dir, simpname, ShallowCopy( filename ) ];
                  if IsBound( sub.( simpname ).( dirname ).( filename
                         ).data ) then
                    Add( info, sub.( simpname ).( dirname ).( filename
                                   ).data );
                  fi;
                  Add( filenames, info );
                fi;
              od;
            fi;
          od;

        fi;
      od;

    od;

    # Return the list.
    return filenames;
end );


#############################################################################
##
#F  AtlasFilesFromPrivateDirectoryTree( <tree> )
##
InstallGlobalFunction( AtlasFilesFromPrivateDirectoryTree, function( tree )
    local evaltree, name;

    # Recursive evaluation.
    evaltree:= function( tree )
      local recnames, result, recname;
      recnames:= Difference( RecNames( tree ), [ "data" ] );
      if IsEmpty( recnames ) then
        return [ "" ];
      fi;
      result:= [];
      for recname in recnames do
        Append( result, List( evaltree( tree.( recname ) ),
                              x -> Concatenation( recname, "/", x ) ) );
      od;
      return result;
    end;

    # Fetch the information for the relevant subdirectories.
    tree:= evaltree( tree );

    # Remove the trailing slashes.
    for name in tree do
      Unbind( name[ Length( name ) ] );
    od;

    # Return the result.
    return tree;
end );


#############################################################################
##
#F  AGRGNAN( <gapname>, <atlasname> )
##
InstallGlobalFunction( AGRGNAN, function( gapname, atlasname )
    if   ForAny( AtlasOfGroupRepresentationsInfo.GAPnames,
                 pair -> gapname = pair[1] ) then
      Error( "cannot notify `", gapname, "' more than once" );
    elif ForAny( AtlasOfGroupRepresentationsInfo.GAPnames,
                 pair -> atlasname = pair[2] ) then
      Error( "ambiguous GAP names for ATLAS name `", atlasname, "'" );
    fi;
    AddSet( AtlasOfGroupRepresentationsInfo.GAPnames,
            [ gapname, atlasname ] );
end );


#############################################################################
##
#F  AGRParseFilenameFormat( <string>, <format> )
##
InstallGlobalFunction( "AGRParseFilenameFormat", function( string, format )
    local result, i, res;

    string:= SplitString( string, "-" );
    result:= [];
    for i in [ 1 .. Length( string ) ] do

      # Loop over the '-' separated components.
      res:= format[2][i]( string[i], format[1][i] );
      if res = fail then
        return fail;
      fi;
      Append( result, res );

    od;
    return result;
end );


#############################################################################
##
#F  GroupNamesOfAtlasOfGroupRepresentations( <list> )
##
InstallGlobalFunction( GroupNamesOfAtlasOfGroupRepresentations,
    function( list )
    local types, result, i, triple, name, type, parsed, new;

    types:= AGRDataTypes( "rep", "slp" );

    # Initialize the result list.
    result:= [];

    # Loop over the triples.
    for i in [ 1 .. Length( list ) ] do

      # Extract the group name from the filename.
      triple:= list[i];
      name:= triple[3];
      for type in types do
        parsed:= AGRParseFilenameFormat( name, type[2].FilenameFormat );
        if parsed <> fail then
          new:= [ triple[1], triple[2], parsed[1] ];
          if not new in result then
            if ForAny( result, tr -> tr[3] = parsed[1] ) then
              Info( InfoAtlasRep, 1,
                    "group name `", new[3], "' used in different places\n",
                    "#I  (ignore parse result ", new, ")" );
              Unbind( list[i] );
            else
              AddSet( result, new );
            fi;
          fi;
          break;
        fi;
      od;

    od;

    # Sort the result list.
    Sort( result );

    # Return the result list.
    return result;
end );


#############################################################################
##
#F  AGRFLD( <filename>, <descr> )
##
InstallGlobalFunction( AGRFLD, function( filename, descr )
    local triple;
    if descr[1] = 'H' then
      triple:= [ filename, descr, descr ];
    else
      triple:= [ filename, descr, EvalString( descr ) ];
    fi;
    if triple in AtlasOfGroupRepresentationsInfo.fieldinfo then
      Info( InfoAtlasRep, 1,
            "triple `", triple, "' cannot be notified more than once" );
    else
      Add( AtlasOfGroupRepresentationsInfo.fieldinfo, triple );
    fi;
end );


#############################################################################
##
#F  AtlasDataCharacteristicZeroFile( <filename> )
##
InstallGlobalFunction( AtlasDataCharacteristicZeroFile,
    function( filename )
    local record;
    InfoRead1( "#I  reading `", filename, "' started\n" );
    record:= ReadAsFunction( filename );
    InfoRead1( "#I  reading `", filename, "' done\n" );
    if record = fail then
      Info( InfoAtlasRep, 1,
            "problem reading `", filename, "' as function\n" );
    else
      record:= record();
    fi;
    return record;
end );


#############################################################################
##
#F  AtlasStringOfFieldOfMatrixEntries( <mats> )
#F  AtlasStringOfFieldOfMatrixEntries( <filename> )
##
InstallGlobalFunction( AtlasStringOfFieldOfMatrixEntries, function( mats )
    local datafile, F, n, str;

    if IsString( mats ) then
      mats:= AtlasDataCharacteristicZeroFile( mats ).generators;
    fi;

    if   IsCyclotomicCollCollColl( mats ) then
      F:= Field( Rationals, Flat( mats ) );
    elif ForAll( mats, IsQuaternionCollColl ) then
      F:= Field( Flat( List( Flat( mats ), ExtRepOfObj ) ) );
    else
      Error( "<mats> must be a matrix list of cyclotomics or quaternions" );
    fi;

    n:= Conductor( F );

    if DegreeOverPrimeField( F ) = 2 then

      # The field is quadratic,
      # so it is generated by `Sqrt(n)' if $`n' \equiv 1 \pmod{4}$,
      # by `Sqrt(-n)' if $`n' \equiv 3 \pmod{4}$,
      # and by one of `Sqrt(n/4)', `Sqrt(-n/4)' otherwise.
      if   n mod 4 = 1 then
        str:= Concatenation( "[Sqrt(", String( n ), ")]" );
      elif n mod 4 = 3 then
        str:= Concatenation( "[Sqrt(-", String( n ), ")]" );
      elif Sqrt( -n/4 ) in F then
        str:= Concatenation( "[Sqrt(-", String( n/4 ), ")]" );
      else
        str:= Concatenation( "[Sqrt(", String( n/4 ), ")]" );
      fi;

    elif IsCyclotomicField( F ) then

      # The field is not quadratic but cyclotomic.
      str:= Concatenation( "[E(", String( n ), ")]" );

    else
      str:= String( F );
    fi;

    if IsCyclotomicCollCollColl( mats ) then
      str:= Concatenation( "Field(", str, ")" );
    else
      if F = Rationals then
        str:= "H";
      else
        str:= Concatenation( "H(", str, ")" );
      fi;
    fi;

    return [ F, str ];
end );


#############################################################################
##
#F  AtlasOfGroupRepresentationsScanFilename( <name>, <result> )
##
BindGlobal( "AtlasOfGroupRepresentationsScanFilename",
    function( name, result )
    local type, format, groupname, pos;

    for type in AGRDataTypes( "rep", "slp" ) do
      format:= AGRParseFilenameFormat( name, type[2].FilenameFormat );
      if format <> fail then
        groupname:= format[1];
        pos:= Position( groupname, '/' );
        if pos <> fail then
          groupname:= groupname{ [ 1 .. pos-1 ] };
        fi;
        if not IsBound( result.( groupname ) ) then
          result.( groupname ):= rec();
        fi;
        if not IsBound( result.( groupname ).( type[1] ) ) then
          result.( groupname ).( type[1] ):= [];
        fi;
        return type[2].AddFileInfo( result.( groupname ).( type[1] ),
                                    format, name );
      fi;
    od;
    return false;
end );


#############################################################################
##
#F  AtlasOfGroupRepresentationsServerListing( <long> )
##
BindGlobal( "AtlasOfGroupRepresentationsServerListing", function( long )
    local perl, script, server, str, out, options;

    if IsBound( AtlasOfGroupRepresentationsInfo.wget ) and
       AtlasOfGroupRepresentationsInfo.wget = true then
      Info( InfoAtlasRep, 1,
            "no server listing is possible if `wget' is chosen" );
      return fail;
    fi;

    perl:= Filename( DirectoriesSystemPrograms(), "perl" );
    if perl = fail then
      Info( InfoAtlasRep, 1, "no `perl' executable found" );
      return fail;
    fi;
    script:= Filename( DirectoriesPackageLibrary( "atlasrep", "etc" ),
                       "perlftp.pl" );
    if script = fail then
      Info( InfoAtlasRep, 1, "no script `etc/perlftp.pl' found" );
      return fail;
    fi;
    for server in AtlasOfGroupRepresentationsInfo.servers do
      str:= "";
      out:= OutputTextString( str, true );
      if long then
        options:= "-lR";
      else
        options:= "-R";
      fi;
      Info( InfoAtlasRep, 2,
            "calling `etc/perlftp.pl' to list all server files" );
      Process( DirectoryCurrent(), perl, InputTextNone(), out,
               [ script, server[1], server[2], server[3], server[4],
                 "ls", options ] );
      CloseStream( out );
      if not IsEmpty( str ) then
        break;
      fi;
    od;

    return str;
    end );


#############################################################################
##
#F  AtlasTableOfContents( <dirname>[, <long>] )
##
##  If `true' is given as the optional second argument <long>
##  then the `TableOfContents' component of the result record has the
##  additional component `data',
##  which is a list describing the last modification dates of the files
##  in the directory in question, respectively.
##  (This option is used in the test performed by
##  `AtlasOfGroupRepresentationsTestTableOfContentsRemoteUpdates'.)
##
InstallGlobalFunction( AtlasTableOfContents, function( arg )
    local string,
          long,
          groupnames,
          prefix,
          name,
          toc,
          str,
          out,
          options,
          data,
          len,
          path,
          dir,
          result,
          groupname,
          gens,
          record,
          listtosort,
          type;

    toc:= AtlasOfGroupRepresentationsInfo.TableOfContents;

    # Get and check the arguments.
    if   Length( arg ) = 1 and IsString( arg[1] ) then
      string := arg[1];
      long   := false;
    elif Length( arg ) = 2 and IsString( arg[1] )
                           and IsBool( arg[2] ) then
      string := arg[1];
      long   := arg[2];
    else
      Error( "usage: AtlasTableOfContents( <dirname>[, <long>] )" );
    fi;

    # Take the stored version if it is already available.
    groupnames:= AtlasOfGroupRepresentationsInfo.groupnames;
    if IsBound( toc.( string ) ) then
      return rec( groupnames      := groupnames,
                  TableOfContents := toc.( string ) );
    fi;

    prefix:= "";
    if string = "remote" then

      # List the contents of the remote data directories.
      str:= AtlasOfGroupRepresentationsServerListing( long );
      if str = fail then
        return fail;
      fi;

      # Turn the information about directories and files into the
      # appropriate format,
      # and extract the group name info.
      str:= AtlasFilesFromDirectoryTree( DirectoryTree( str ) );
      if str = fail then
        # An error message has been printed already by
        # `AtlasFilesFromDirectoryTree'.
        return fail;
      fi;
      UniteSet( groupnames, GroupNamesOfAtlasOfGroupRepresentations( str ) );

      # Save the `ls -l' info if applicable.
      if long then
        data:= List( str, x -> x[4] );
      else
        data:= ListWithIdenticalEntries( Length( str ), [] );
      fi;
      str:= List( str, x -> x[3] );

    else

      # List the contents of the local or private data directories.
      str:= "";
      out:= OutputTextString( str, true );
      name:= Filename( DirectoriesSystemPrograms(), "ls" );
      if name = fail or not AtlasOfGroupRepresentationsInfo.ls then
        Info( InfoAtlasRep, 2,
              "problem in `AtlasTableOfContents':  no `ls' available" );
        return fail;
      elif long then
        options:= [ "-l", "-R" ];
      else
        options:= [ "-R" ];
      fi;
      if string = "local" then

        # List the information available in the local installation.
        for dir in DirectoriesPackageLibrary( "atlasrep", "datagens" ) do
          Info( InfoAtlasRep, 2,
                "calling `ls' for the local `datagens' directory" );
          Process( dir, name, InputTextNone(), out, options );
        od;
        for dir in DirectoriesPackageLibrary( "atlasrep", "dataword" ) do
          Info( InfoAtlasRep, 2,
                "calling `ls' for the local `dataword' directory" );
          Process( dir, name, InputTextNone(), out, options );
        od;
        CloseStream( out );

        # Split the string into lines.
        str:= Filtered( SplitString( str, "", "\n" ),
                  x -> Length( x ) < 6 or x{ [ 1 .. 6 ] } <> "total " );

        # If applicable then split the lines into data and filenames.
        str:= List( str, l -> SplitString( l, "", " " ) );
        data:= List( str, l -> l{ [ 1 .. Length( l ) - 1 ] } );
        str:= List( str, l -> l[ Length( l ) ] );

      else

        # List the information available in the private directory.
        prefix:= string;
        if ForAll( AtlasOfGroupRepresentationsInfo.private,
               pair -> string <> pair[2] ) then
          Error( "no private directory with identifier `", string, "'" );
        fi;
        Info( InfoAtlasRep, 2,
              "calling `ls' for the private directory `", string, "'" );
        Process( Directory( First( AtlasOfGroupRepresentationsInfo.private,
                                   pair -> pair[2] = string )[1] ),
                 name, InputTextNone(), out, options );
        CloseStream( out );

        str:= AtlasFilesFromPrivateDirectoryTree( DirectoryTree( str ) );

      fi;

    fi;

    # Initialize the result record.
    result:= rec( otherfiles:= [] );

    # Loop over the filenames.
    for name in str do

      # Deal with the case of `gzip'ped files.
      len:= Length( name );
      if 3 <= len and name{ [ len-2 .. len ] } = ".gz" then
        name:= name{ [ 1 .. len-3 ] };
      fi;

      if AtlasOfGroupRepresentationsScanFilename( name, result ) = false then
        if not ( name in [ "dummy", ".:", "CVS", "CVS:", "./CVS:",
                           "Entries", "Repository", "Root", "toc.g" ] or
                 name[ Length( name ) ] = '%' or
                 ( 3 <= Length( name )
                   and name{ Length( name ) + [ - 2 .. 0 ] } = "BAK" ) ) then
          Info( InfoAtlasRep, 3,
                "t.o.c. construction: ignoring name `", name, "'" );
          AddSet( result.otherfiles, name );
        fi;
      fi;

    od;

    # Postprocessing,
    # and *sort* the representations as given in the type definition.
    for groupname in List( groupnames, x -> x[3] ) do
      if IsBound( result.( groupname ) ) then

        record:= result.( groupname );
        for type in AGRDataTypes( "rep", "slp" ) do
          if IsBound( record.( type[1] ) ) then

            type[2].PostprocessFileInfo( result, record );

            # Sort the data of the given type as defined.
            if IsBound( type[2].SortTOCEntries ) then
              listtosort:= List( record.( type[1] ), type[2].SortTOCEntries );
              SortParallel( listtosort, record.( type[1] ) );
            fi;

          fi;
        od;

      fi;
    od;

    # Store the newly computed table of contents.
    toc.( string ):= result;
    UniteSet( AtlasOfGroupRepresentationsInfo.groupnames, groupnames );

    if long then
      result.data:= [ str, data ];
    fi;

    # Store the current date in Coordinated Universal Time
    # (Greenwich Mean Time).
    result.lastupdated:= CurrentDateTimeString();

    # Return the result record.
    return rec( groupnames      := groupnames,
                TableOfContents := result );
end );


#############################################################################
##
#F  ReloadAtlasTableOfContents( \"local\"[, <long>] )
#F  ReloadAtlasTableOfContents( \"remote\"[, <long>] )
##
##  If `true' is given as the optional second argument <long>
##  then also the `data' component of the table of contents is computed.
##  (This option is used by `AtlasTableOfContents' and
##  `AtlasOfGroupRepresentationsTestTableOfContentsRemoteUpdates'.)
##
InstallGlobalFunction( ReloadAtlasTableOfContents, function( arg )
    local string, long, toc, old, new;

    if Length( arg ) = 1 then
      string := arg[1];
      long   := false;
    elif Length( arg ) = 2 and IsBool( arg[2] ) then
      string := arg[1];
      long   := arg[2];
    else
      Error( "usage: ReloadAtlasTableOfContents( <string> )" );
    fi;
    if not string in [ "local", "remote" ] then
      Error( "usage: <string> must be \"local\" or \"remote\"" );
    fi;

    toc:= AtlasOfGroupRepresentationsInfo.TableOfContents;
    if IsBound( toc.( string ) ) then
      old:= toc.( string );
      Unbind( toc.( string ) );
    fi;
    new:= AtlasTableOfContents( string, long );
    if new = fail then
      if IsBound( old ) then
        toc.( string ):= old;
      fi;
      Info( InfoAtlasRep, 1,
            "could not reload ", string, " table of contents" );
      return fail;
    fi;
    toc.( string ):= new.TableOfContents;
    if string = "remote" then
      AtlasOfGroupRepresentationsInfo.groupnames:= new.groupnames;
    fi;
    return true;
end );


#############################################################################
##
#F  StoreAtlasTableOfContents( <filename> )
##
InstallGlobalFunction( StoreAtlasTableOfContents, function( filename )
    if IsExistingFile( filename ) and not IsWritableFile( filename ) then
      Error( "<filename> must be writable if exists" );
    elif not IsBound( AtlasOfGroupRepresentationsInfo.TableOfContents.(
                          "remote" ) ) then
      Error( "no remote t.o.c. exists" );
    fi;

    PrintTo( filename, StringOfAtlasTableOfContents( "remote" ) );
end );


#############################################################################
##
#F  ReplaceAtlasTableOfContents( <filename> )
##
InstallGlobalFunction( ReplaceAtlasTableOfContents, function( filename )
    if not IsReadableFile( filename ) then
      Error( "<filename> must be readable" );
    fi;

    AtlasOfGroupRepresentationsInfo.TableOfContents.( "remote" ):= rec();
    AtlasOfGroupRepresentationsInfo.groupnames:= [];
    Read( filename );
end );


#############################################################################
##
#F  StringOfAtlasTableOfContents( \"remote\" )
##
InstallGlobalFunction( StringOfAtlasTableOfContents, function( string )
    local toc, str, triple, groupname, reps, type, entry, i;

    toc:= AtlasTableOfContents( string ).TableOfContents;
    str:= "";

    for triple in AtlasOfGroupRepresentationsInfo.groupnames do

      # Start with the notification of the group.
      Append( str, "# " );
      Append( str, triple[3] );
      Append( str, "\n" );
      Append( str, "AGRGRP(\"" );
      Append( str, triple[1] );
      Append( str, "\",\"" );
      Append( str, triple[2] );
      Append( str, "\",\"" );
      Append( str, triple[3] );
      Append( str, "\");\n" );

      # Append the notifications of data entries for the group.
      groupname:= triple[3];
      if IsBound( toc.( groupname ) ) then
        reps:= toc.( groupname );
        for type in AGRDataTypes( "rep", "slp" ) do
          if IsBound( reps.( type[1] ) ) then
            for entry in reps.( type[1] ) do
              if IsBound( type[2].TOCEntryString ) then
                Append( str, type[2].TOCEntryString( type[1], entry ) );
              else
                Info( InfoAtlasRep, 1,
                      "no component `TOCEntryString' for data type `",
                      type[1], "'" );
              fi;
            od;
          fi;
        od;
      fi;

      Append( str, "\n" );

    od;

    # Append the current date and time.
    Append( str, "\n" );
    Append( str, "AtlasOfGroupRepresentationsInfo.TableOfContents.( \"" );
    Append( str, string );
    Append( str, "\" ).lastupdated:=\n  \"" );
    Append( str, toc.lastupdated );
    Append( str, "\";\n\n" );

    return str;
end );


#############################################################################
##
#F  AGRGRP( <dirname>, <simpname>, <groupname> )
##
InstallGlobalFunction( AGRGRP,
    function( dirname, simpname, groupname )
    local entry;

    if   not dirname in AtlasOfGroupRepresentationsInfo.dirnames then

      # The directory name is not known.
      # (Note that the automatic creation of a table of contents
      # assumes that all relevant directories are stored.)
      Error( "`", dirname, "' is not known" );

    elif ForAll( AtlasOfGroupRepresentationsInfo.GAPnames,
                 pair -> pair[2] <> groupname ) then

      # There is no corresponding {\GAP} name.
      AddSet( AtlasOfGroupRepresentationsInfo.GAPnames,
              [ groupname, groupname ] );
      Info( InfoAtlasRep, 1,
            "no GAP name known for `", groupname, "'" );

    fi;

    entry:= First( AtlasOfGroupRepresentationsInfo.groupnames,
                   l -> l[3] = groupname );
    if entry <> fail then

      # Check whether the group is already notified.
      if entry[1] = dirname then
        Info( InfoAtlasRep, 1,
              "group with Atlas name `", groupname, "' already notified" );
      else
        Error( "group with Atlas name `", groupname,
               "' notified for different directories!" );
      fi;
      if   entry[2] <> simpname then
        Error( "group with Atlas name `", groupname,
               "' notified for different simple groups!" );
      fi;

    else

      # Notify the group.
      Add( AtlasOfGroupRepresentationsInfo.groupnames,
           [ dirname, simpname, groupname ] );

    fi;
end );


#############################################################################
##
#F  AGRTOC( <arg> )
##
InstallGlobalFunction( AGRTOC, function( arg )
    local type, string, format, record, entry, groupname, added, j;

    # Get the arguments.
    type:= arg[1];
    string:= arg[2];
    if Length( arg ) = 3 then
      string:= Concatenation( string, "1" );
    fi;

    # Parse the filename with the given format info.
    type:= First( AGRDataTypes( "rep", "slp" ), x -> x[1] = type );
    format:= type[2].FilenameFormat;
    record:= AtlasTableOfContents( "remote" ).TableOfContents;
    entry:= AGRParseFilenameFormat( string, format );
    if entry = fail then
      Info( InfoAtlasRep, 1, "`", arg, "' is not a valid t.o.c. entry" );
      return;
    fi;

    # Get the list for the data in the record for the group name.
    groupname:= entry[1];
    if ForAll( AtlasOfGroupRepresentationsInfo.groupnames,
               x -> x[3] <> groupname ) then
      Info( InfoAtlasRep, 1, "`", groupname, "' is not a valid group name" );
      return;
    fi;
    if not IsBound( record.( groupname ) ) then
      record.( groupname ):= rec();
    fi;
    record:= record.( groupname );
    if not IsBound( record.( type[1] ) ) then
      record.( type[1] ):= [];
    fi;

    # Add the first filename.
    added:= type[2].AddFileInfo( record.( type[1] ), entry, string );

    # Add the other filenames if necessary.
    if added and Length( arg ) = 3 then
      for j in [ 2 .. arg[3] ] do
        entry[ Length( entry ) ]:= j;
        string:= Concatenation( arg[2], String( j ) );
        added:= type[2].AddFileInfo( record.( type[1] ), entry, string )
                and added;
      od;
    fi;

    if not added then
      Info( InfoAtlasRep, 1, "`", arg, "' is not a valid t.o.c. entry" );
    fi;
end );


#############################################################################
##
#F  AtlasOfGroupRepresentationsNotifyPrivateDirectory( <dirname>[, <dirid>] )
##
InstallGlobalFunction( AtlasOfGroupRepresentationsNotifyPrivateDirectory,
    function( arg )
    local dirname,
          dirid,
          dir,
          prm,
          toc,
          allfilenames,
          RemovedDirectories,
          groupname,
          record,
          type,
          entry,
          name,
          tocs,
          ok,
          oldtoc,
          unknown;

    # Get and check the arguments.
    if   Length( arg ) = 1 and IsString( arg[1] ) then
      dirname:= ShallowCopy( arg[1] );
      dirid:= dirname;
    elif Length( arg ) = 2 and IsString( arg[1] ) and IsString( arg[2] ) then
      dirname:= ShallowCopy( arg[1] );
      dirid:= ShallowCopy( arg[2] );
    else
      Error( "usage: AtlasOfGroupRepresentationsNotifyPrivateDirectory(\n",
             "<dirname>[,<dirid>])" );
    fi;

    # Read the primary file (for the group declarations).
    dir:= Directory( dirname );
    prm:= Filename( dir, "toc.g" );
    if IsExistingFile( prm ) = true then
      Read( prm );
    fi;

    # Add the directory name.
    if IsEmpty( dirname ) then
      Error( "<dirname> must not be empty" );
    elif dirname[ Length( dirname ) ] <> '/' then
      Add( dirname, '/' );
    fi;
    if ForAny( AtlasOfGroupRepresentationsInfo.private,
               pair -> pair[2] = dirid and pair[1] <> dirname ) then
      Error( dirid,
             " is already the identifier of another private directory" );
    fi;
    AddSet( AtlasOfGroupRepresentationsInfo.private, [ dirname, dirid ] );

    # Set up a table of contents for the private directory.
    toc:= AtlasTableOfContents( dirid ).TableOfContents;
    Unbind( toc.otherfiles );

    # Check that no filename of this table of contents exists already in
    # other tables of contents.
    # 1. Compute the list of all filenames.
    allfilenames:= [];

    RemovedDirectories:= function( string )
      string:= SplitString( string, "/" );
      return string[ Length( string ) ];
    end;

    for groupname in RecNames( toc ) do
      record:= toc.( groupname );
      if IsRecord( record ) then
        for type in RecNames( record ) do
          for entry in record.( type ) do
            name:= entry[ Length( entry ) ];
            if IsString( name ) then
              Add( allfilenames, RemovedDirectories( name ) );
            else
              Append( allfilenames, List( name, RemovedDirectories ) );
            fi;
          od;
        od;
      fi;
    od;

    # 2. Check whether the list is disjoint to the other tables of contents.
    if AtlasOfGroupRepresentationsInfo.remote = true then
      tocs:= [ AtlasTableOfContents( "remote" ).TableOfContents ];
    else
      tocs:= [ AtlasTableOfContents( "local" ).TableOfContents ];
    fi;
    Append( tocs, List( Filtered( AtlasOfGroupRepresentationsInfo.private,
                                  pair -> pair[2] <> dirid ),
        x -> AtlasOfGroupRepresentationsInfo.TableOfContents.( x[2] ) ) );
    ok:= true;
    for oldtoc in tocs do
      for groupname in RecNames( toc ) do
        if IsBound( oldtoc.( groupname ) ) then
          record:= oldtoc.( groupname );
          if IsRecord( record ) then
            for type in RecNames( record ) do
              for entry in List( record.( type ), x -> x[ Length( x ) ] ) do
                if   ( IsString( entry ) and entry in allfilenames )
                  or ForAny( entry,
                         x -> IsString( x ) and x in allfilenames ) then
                  ok:= false;
                  Info( InfoAtlasRep, 1,
                        "file `", entry, "' was already in another t.o.c." );
                fi;
              od;
            od;
          fi;
        fi;
      od;
    od;

    # Add group names that were not notified.
    unknown:= Filtered( RecNames( toc ),
                  x -> ForAll( AtlasOfGroupRepresentationsInfo.GAPnames,
                           pair -> x <> pair[2] ) );
    RemoveSet( unknown, "lastupdated" );
    if not IsEmpty( unknown ) then
      ok:= false;
      Info( InfoAtlasRep, 1,
            "no GAP names defined for ", unknown );
      UniteSet( AtlasOfGroupRepresentationsInfo.GAPnames,
                List( unknown, x -> [ x, x ] ) );
    fi;

    # Return the flag.
    return ok;
    end );


#############################################################################
##
#F  AtlasOfGroupRepresentationsForgetPrivateDirectory( <dirid> )
##
InstallGlobalFunction( AtlasOfGroupRepresentationsForgetPrivateDirectory,
    function( dirid )
    local private, i, dirname, dir, prm, str, file, GAPnames, pos, pos2,
          gapname, j;

    Unbind( AtlasOfGroupRepresentationsInfo.TableOfContents.( dirid ) );

    private:= AtlasOfGroupRepresentationsInfo.private;
    for i in [ 1 .. Length( private ) ] do
      if private[i][2] = dirid then

        # Remove the group declarations.
        dirname:= private[i][1];
        dir:= Directory( dirname );
        prm:= Filename( dir, "toc.g" );
        if IsExistingFile( prm ) = true then
          str:= StringFile( prm );
          GAPnames:= AtlasOfGroupRepresentationsInfo.GAPnames;
          pos:= PositionSublist( str, "AGRGNAN" );
          while pos <> fail do
            while pos <= Length( str ) and str[ pos ] <> '"' do
              pos:= pos + 1;
              pos2:= Position( str, '"', pos );
              if pos2 <> fail then
                gapname:= str{ [ pos+1 .. pos2-1 ] };
                for j in [ 1 .. Length( GAPnames ) ] do
                  if IsBound( GAPnames[j] ) and GAPnames[j][1] = gapname then
                    Unbind( GAPnames[j] );
                    break;
                  fi;
                od;
              fi;
            od;
            pos:= PositionSublist( str, "AGRGNAN", pos2 );
          od;
          AtlasOfGroupRepresentationsInfo.GAPnames:= Compacted( GAPnames );
        fi;

        # Remove the information concerning the private directory.
        Unbind( private[i] );
        AtlasOfGroupRepresentationsInfo.private:= Compacted( private );
        break;

      fi;
    od;
    end );


#############################################################################
##
#E

