#############################################################################
##
#W  brdbattr.gi           GAP 4 package `browse'                Thomas Breuer
##
##  This file contains the implementations for tools for database handling.
##


#############################################################################
##
#F  DatabaseIdEnumerator( <arec> )
##
BindGlobal( "DatabaseIdEnumerator", function( arec )
    local comps, entry;

    arec:= ShallowCopy( arec );

    # Check for the presence of the mandatory components.
    comps:= [ [ "identifiers", "list", IsList ],
              [ "entry", "function", IsFunction ],
            ];
    for entry in comps do
      if not IsBound( arec.( entry[1] ) )
         or not entry[3]( arec.( entry[1] ) ) then
        Error( "<arec>.", entry[1], " must be bound to a ", entry[2] );
      fi;
    od;

    # Set default values for the optional components.
    comps:= [ [ "attributes", "record", IsRecord, rec() ],
              [ "isUpToDate", "function", IsFunction, ReturnTrue ],
              [ "version", "object", IsObject, "" ],
              [ "update", "function", IsFunction, ReturnTrue ],
              [ "viewLabel", "table cell data object",
                BrowseData.IsBrowseTableCellData, "name" ],
              [ "viewValue", "function", IsFunction, String ],
              [ "viewSort", "function", IsFunction, \< ],
              [ "sortParameters", "list", IsList, [] ],
              [ "widthCol", "positive integer", IsPosInt ],
              [ "align", "string", IsString, "r" ],
              [ "categoryValue", "function", IsFunction ],
              [ "isSorted", "boolean", IsBool, false ],
            ];
    for entry in comps do
      if IsBound( arec.( entry[1] ) ) then
        if not entry[3]( arec.( entry[1] ) ) then
          Error( "<arec>.", entry[1], ", if bound, must be a ", entry[2] );
        fi;
      elif IsBound( entry[4] ) then
        arec.( entry[1] ):= entry[4];
      fi;
    od;
    if not IsBound( arec.categoryValue ) then
      arec.categoryValue:= arec.viewValue;
    fi;

    # Set the "self" attribute.
    DatabaseAttributeAdd( arec, rec(
        identifier:= "self",
        description:= "the identifiers themselves",
        type:= "values",
        data:= arec.identifiers,
        version:= arec.version,
        update:= function( a )
            a.data:= a.idenumerator.identifiers;
            return true;
          end,
        viewLabel:= arec.viewLabel,
        viewValue:= arec.viewValue,
        viewSort:= arec.viewSort,
        sortParameters:= arec.sortParameters,
        align:= arec.align,
        categoryValue:= arec.categoryValue,
      ) );
    if IsBound( arec.widthCol ) then
      arec.attributes.self.widthCol:= arec.widthCol;
    fi;

    return arec;
end );


#############################################################################
##
#F  DatabaseAttributeAdd( <dbidenum>, <arec> )
##
BindGlobal( "DatabaseAttributeAdd", function( dbidenum, arec )
    local comps, entry;

    # Check `dbidenum'.
    if not IsRecord( dbidenum ) or not IsBound( dbidenum.attributes )
                                or not IsRecord( dbidenum.attributes ) then
      Error( "<dbidenum> must be a database id enumerator" );
    elif IsBound( arec.identifier ) and
         IsBound( dbidenum.attributes.( arec.identifier ) ) then
      Error( "an attribute with identifier `", arec.identifier,
             "' is already bound in <dbidenum>" );
    fi;
    arec:= ShallowCopy( arec );
    arec.idenumerator:= dbidenum;

    # Check for the presence of the mandatory components.
    comps:= [ [ "identifier", "string", IsString ],
              [ "type", "string", IsString ],
            ];
    for entry in comps do
      if not IsBound( arec.( entry[1] ) )
         or not entry[3]( arec.( entry[1] ) ) then
        Error( "<arec>.", entry[1], " must be bound to a ", entry[2] );
      fi;
    od;

    # Do more tests.
    if not arec.type in [ "values", "pairs" ] then
      Error( "<arec>.type must be one of `\"values\"', `\"pairs\"'" );
    fi;

    # Set default values for the optional components.
    comps:= [ 
              [ "description", "string", IsString, "" ],
              [ "name", "string", IsString ],
              [ "datafile", "string", IsString ],
              [ "attributeValue", "function", IsFunction,
                DatabaseAttributeValueDefault ],
              [ "dataDefault", "object", IsObject, "" ],
              [ "eval", "function", IsFunction ],
              [ "neededAttributes", "list", IsList, [] ],
              [ "prepareAttributeComputation", "function", IsFunction,
                ReturnTrue ],
              [ "cleanupAfterAttributeComputation", "function", IsFunction,
                ReturnTrue ],
              [ "create", "function", IsFunction ],
              [ "string", "function", IsFunction,
     #          function( id, val ) return String( val ); end ],
                String ],
              [ "check", "function", IsFunction, ReturnTrue ],
              [ "viewLabel", "table cell data object",
                BrowseData.IsBrowseTableCellData ],
              [ "viewValue", "function", IsFunction, String ],
              [ "viewSort", "function", IsFunction, \< ],
              [ "sortParameters", "list", IsList, [] ],
              [ "widthCol", "positive integer", IsPosInt ],
              [ "align", "string", IsString, "r" ],
              [ "categoryValue", "function", IsFunction ],
            ];
    for entry in comps do
      if IsBound( arec.( entry[1] ) ) then
        if not entry[3]( arec.( entry[1] ) ) then
          Error( "<arec>.", entry[1], ", if bound, must be a ", entry[2] );
        fi;
      elif IsBound( entry[4] ) then
        arec.( entry[1] ):= entry[4];
      fi;
    od;

    # Do more tests.
    if IsBound( arec.data ) then
      if   arec.type = "values" and not IsList( arec.data ) then
        Error( "<arec>.type is \"values\", so <arec>.data must be a list" );
      elif arec.type = "pairs" and
        not ( IsRecord( arec.data ) and IsBound( arec.data.automatic ) and
                 IsBound( arec.data.nonautomatic ) ) then
        Error( "<arec>.type is \"pairs\", so <arec>.data must be a record ",
               "with the components `automatic' and `nonautomatic'" );
      fi;
    fi;
    if IsBound( arec.name ) then
      if not IsBoundGlobal( arec.name ) or
         not IsFunction( ValueGlobal( arec.name ) ) then
        Error( "<arec>.name must be the identifier of a global function" );
      fi;
    fi;
    if IsBound( arec.isSorted ) then
      if arec.type = "values" then
        Error( "<arec>.isSorted is valid only for <arec>.type = \"pairs\"" );
      elif not arec.isSorted in [ true, false ] then
        Error( "<arec>.isSorted must be `true' or `false'" );
      fi;
    elif arec.type = "pairs" then
      arec.isSorted:= false;
    fi;

    # Set default values for the optional components.
    if not IsBound( arec.create ) and IsBound( arec.name ) then
      arec.create:= function( attr, id )
        return ValueGlobal( arec.name )(
                 attr.idenumerator.entry( attr.idenumerator, id ) );
      end;
    fi;
    if not IsBound( arec.viewLabel ) then
      if IsBound( arec.name ) then
        arec.viewLabel:= arec.name;
      else
        arec.viewLabel:= arec.identifier;
      fi;
    fi;
    if not IsBound( arec.categoryValue ) then
      arec.categoryValue:= arec.viewValue;
    fi;
    if not IsBound( arec.version ) and not IsBound( arec.datafile ) then
      arec.version:= dbidenum.version;
    fi;

    dbidenum.attributes.( arec.identifier ):= arec;
#T update component for attributes!
#T (when can I set a default value?)
#T where do I just have to replace known values?
end );


#############################################################################
##
#F  DatabaseAttributeValueDefault( <attr>, <id> )
##
BindGlobal( "DatabaseAttributeValueDefault", function( attr, id )
    local pos, comp, result;

    # If the `data' component is not bound then initialize it.
    if not IsBound( attr.data ) then
      if IsBound( attr.datafile ) and IsReadableFile( attr.datafile ) then
        DatabaseAttributeLoadData( attr );
      else
        DatabaseAttributeCompute( attr.idenumerator, attr.identifier );
      fi;
    fi;
    if not IsBound( attr.data ) then
      Error( "<attr>.data is still not bound" );
    fi;

    if attr.type = "values" then
      if attr.idenumerator.isSorted then
        pos:= PositionSet( attr.idenumerator.identifiers, id );
      else
        pos:= Position( attr.idenumerator.identifiers, id );
      fi;
      if pos <> fail then
        if IsBound( attr.data[ pos ] ) then
          result:= attr.data[ pos ];
        elif IsBound( attr.name ) then
          result:= attr.create( attr, id );
          attr.data[ pos ]:= result;
        else
          result:= attr.dataDefault;
        fi;
      fi;
    elif attr.isSorted then
      for comp in [ attr.data.automatic, attr.data.nonautomatic ] do
        pos:= PositionSorted( comp, [ id ] );
        if pos <= Length( comp ) and comp[ pos ][1] = id then
          result:= comp[ pos ][2];
          break;
        fi;
      od;
    else
      for comp in [ attr.data.automatic, attr.data.nonautomatic ] do
        pos:= First( [ 1 .. Length( comp ) ], i -> comp[i][1] = id );
        if  pos <> fail then
          result:= comp[ pos ][2];
          break;
        fi;
      od;
    fi;

    if not IsBound( result ) then
      if IsBound( attr.dataDefault ) then
        result:= attr.dataDefault;
      else
        Error( "no `dataDefault' entry" );
      fi;
    fi;

    if IsBound( attr.eval ) then
      result:= attr.eval( attr, result );
    fi;
    return result;
end );


#############################################################################
##
#F  DatabaseAttributeLoadData( <attr> )
##
BindGlobal( "DatabaseAttributeLoadData", function( attr )
    local filename, data;

    if IsBound( attr.datafile ) and IsReadableFile( attr.datafile ) then
      filename:= attr.datafile;
      if EndsWith( filename, ".json" ) then
        # evaluate the JSON text;
        # note that Browse does not force that a JSON parser is available
        if IsBound( AGR ) and IsBound( AGR.GapObjectOfJsonText ) then
          data:= ValueGlobal( "AGR" ).GapObjectOfJsonText(
                                          StringFile( filename ) );
          if data.status = false then
            Error( "the file '", filename,
                   "' does not contain a valid JSON text" );
          fi;
          data:= data.value;
        elif IsBound( JsonStringToGap ) then
          data:= ValueGlobal( "JsonStringToGap" )( StringFile( filename ) );
        else
          Error( "cannot evaluate the JSON format file '", filename, "'" );
        fi;
        # consistency check
        if EvalString( data.idenum ) <> attr.idenumerator then
          Error( "file '", filename,
                 "' contains data for the id enumerator '", data.idenum,
                 "', not for the one of the attribute <attr>" );
        elif data.attrid <> attr.identifier then
          Error( "file '", filename,
                 "' contains data for the attribute '", data.attrid,
                 "' not '", attr.identifier, "'" );
        fi;
        if data.version <> attr.idenumerator.version then
          Info( InfoWarning, 1,
                "versions of attribute '", data.attrid,
                "' and of id enumerator '", data.idenum,
                "' are not compatible" );
        fi;
        # set the data
        DatabaseAttributeSetData( attr.idenumerator, attr.identifier,
            data.version,
            rec( automatic:= data.automatic,
                 nonautomatic:= data.nonautomatic ) );
      else
        # just read the file
        Read( attr.datafile );
      fi;
    fi;
end );


#############################################################################
##
#F  DatabaseAttributeSetData( <dbidenum>, <attridentifier>, <version>,
#F                            <data> )
##
BindGlobal( "DatabaseAttributeSetData",
    function( dbidenum, attridentifier, version, data )
    local attr;

    if   not IsRecord( dbidenum ) or not IsBound( dbidenum.attributes )
                                or not IsRecord( dbidenum.attributes ) then
      Error( "usage: DatabaseAttributeSetData( <dbidenum>, ",
             "<attridentifier>,\n <version>, <data> )" );
    elif not IsBound( dbidenum.attributes.( attridentifier ) ) then
      Error( "<dbidenum> has no attribute `", attridentifier, "'" );
    fi;
    attr:= dbidenum.attributes.( attridentifier );
    if not ( ( attr.type = "values" and IsList( data ) ) or
             ( attr.type = "pairs" and IsRecord( data ) ) ) then
      Error( "<data> does not fit to the type of <attr>" );
    fi;
    if attr.type = "pairs" then
      if attr.isSorted = true and
         ( not IsSSortedList( data.automatic ) or
           not IsSSortedList( data.nonautomatic ) ) then
        Error( "the data lists are not strictly sorted" );
      fi;
      if not IsEmpty( Intersection( List( data.automatic, x -> x[1] ),
                          List( data.nonautomatic, x -> x[1] ) ) ) then
#T provide an NC variant that skips the tests?
        Error( "automatic and nonautomatic data are not disjoint" );
      fi;
      attr.data:= data;
    else
      if Length( dbidenum.identifiers ) < Length( data ) then
        Error( "automatic and nonautomatic data are not disjoint" );
      fi;
      attr.data:= data;
    fi;
    attr.version:= version;
#T What shall happen if the version does not fit to dbidenum?
end );


#############################################################################
##
#F  DatabaseIdEnumeratorUpdate( <dbidenum> )
##
BindGlobal( "DatabaseIdEnumeratorUpdate", function( dbidenum )
    local name, attr;

    if dbidenum.update( dbidenum ) <> true then
      Info( InfoDatabaseAttribute, 1,
            "DatabaseIdEnumeratorUpdate: <dbidenum>.update returned ",
            "'false'" );
      return false;
    fi;

#T do this in the order prescribed by neededAttributes!
    for name in RecNames( dbidenum.attributes ) do
      attr:= dbidenum.attributes.( name );
      if not IsBound( attr.version ) then
        DatabaseAttributeLoadData( attr );
        if not IsBound( attr.version ) then
          Error( "<attr>.version still not bound" );
        fi;
      fi;
      if attr.version <> dbidenum.version then
        if IsBound( attr.update ) and attr.update( attr ) = true then
          attr.version:= dbidenum.version;
        else
          Info( InfoDatabaseAttribute, 1,
                "DatabaseIdEnumeratorUpdate: <attr>.update for attribute '",
                name, "' returned 'false'" );
          return false;
        fi;
      fi;
    od;

    return true;
end );


#############################################################################
##
#F  DatabaseAttributeCompute( <dbidenum>, <attridentifier>[, <what>] )
##
BindGlobal( "DatabaseAttributeCompute", function( arg )
    local idenum, attridentifier, what, attr, attrid, attr2, i, new,
          oldnonautomatic, oldautomatic, automatic, newautomatic, id;

    idenum:= arg[1];
    attridentifier:= arg[2];
    what:= "automatic";
    if Length( arg ) = 3 and arg[3] in [ "all", "automatic", "new" ] then
      what:= arg[3];
    fi;

    if not IsRecord( idenum ) or
       not IsBound( idenum.attributes ) or
       not IsRecord( idenum.attributes ) or
       not IsString( attridentifier ) or
       not IsBound( idenum.attributes.( attridentifier ) ) then
      Info( InfoDatabaseAttribute, 1,
            "<idenum> has no component <attridentifier>" );
      return false;
    fi;
    attr:= idenum.attributes.( attridentifier );

    if not IsBound( attr.create ) then
      Info( InfoDatabaseAttribute, 1,
            "<attr> has no component <create>" );
      return false;
    fi;

    # Update the needed attributes if necessary.
    for attrid in attr.neededAttributes do
      attr2:= idenum.attributes.( attrid );
      if not IsBound( attr2.version ) and not IsBound( attr2.data ) and
         IsBound( attr2.datafile ) and IsReadableFile( attr2.datafile ) then
        DatabaseAttributeLoadData( attr2 );
      fi;
      if not IsBound( attr2.version ) or attr2.version <> idenum.version then
        Info( InfoDatabaseAttribute, 1,
              "DatabaseAttributeCompute for attribute ", attridentifier,
              ":\n#I  compute needed attribute ", attrid );
        DatabaseAttributeCompute( idenum, attrid, what );
      fi;
    od;

    attr.prepareAttributeComputation( attr );

    if attr.type = "values" then
      if what = "automatic" then
        what:= "all";
      fi;
      if not IsBound( attr.data ) then
        # Fetch the known values; if necessary then initialize.
        if IsBound( attr.datafile ) and IsReadableFile( attr.datafile ) then
          DatabaseAttributeLoadData( attr );
        else
          attr.data:= [];
        fi;
      fi;

      Info( InfoDatabaseAttribute, 1,
            "DatabaseAttributeCompute: start for attribute ",
            attridentifier );

      for i in [ 1 .. Length( idenum.identifiers ) ] do
        if ( not IsBound( attr.data[i] ) ) or ( what <> "new" ) then
          new:= attr.create( attr, idenum.identifiers[i] );
          if IsBound( attr.data[i] ) then
            if IsBound( attr.dataDefault ) and new = attr.dataDefault then
              Info( InfoDatabaseAttribute, 2,
                    "difference in recompute for ", idenum.identifiers[i],
                     ":\n#E  deleting entry\n", attr.data[i] );
              Unbind( attr.data[i] );
            elif new <> attr.data[i] then
              Info( InfoDatabaseAttribute, 2,
                    "difference in recompute for ", idenum.identifiers[i],
                     ":\n#E  replacing entry\n#E  ", attr.data[i],
                     "\n#E  by\n#E  ", new );
              attr.data[i]:= new;
            fi;
          elif not IsBound( attr.dataDefault ) or new <> attr.dataDefault then
            Info( InfoDatabaseAttribute, 2,
                  "recompute: new entry for ", idenum.identifiers[i],
                  ":\n#I  ", new );
            attr.data[i]:= new;
          fi;
        fi;
      od;

      Info( InfoDatabaseAttribute, 1,
            "DatabaseAttributeCompute: done for attribute ",
            attridentifier );

      attr.version:= idenum.version;
    else
      if not IsBound( attr.data ) then
        # Fetch the known values; if necessary then initialize.
        if IsBound( attr.datafile ) and IsReadableFile( attr.datafile ) then
          DatabaseAttributeLoadData( attr );
        else
          attr.data:= rec( automatic:= [], nonautomatic:= [] );
        fi;
      fi;
      oldnonautomatic:= List( attr.data.nonautomatic, x -> x[1] );
      oldautomatic:= List( attr.data.automatic, x -> x[1] );
      automatic:= [];
      newautomatic:= [];

      Info( InfoDatabaseAttribute, 1,
            "DatabaseAttributeCompute: start for attribute ",
            attridentifier );

      for id in idenum.identifiers do
        if not ( ( what in [ "automatic", "new" ] and id in oldnonautomatic ) or
           ( what = "new" and id in oldautomatic ) ) then
          new:= attr.create( attr, id );
          if new <> attr.dataDefault then
            Add( automatic, [ id, new ] );

            # Handle the case that a nonautomatic value becomes automatic.
            if what = "all" and id in oldnonautomatic then
              Info( InfoDatabaseAttribute, 2,
                    "recompute: formerly nonautomatic value for ", id,
                    "#I  is now automatic" );
              Add( newautomatic, id );
            fi;
          fi;
        fi;
      od;
      attr.data.automatic:= automatic;
      if newautomatic <> [] then
        attr.data.nonautomatic:= Filtered( attr.data.nonautomatic,
            pair -> not pair[1] in newautomatic );
      fi;

      Info( InfoDatabaseAttribute, 1,
            "DatabaseAttributeCompute: done for attribute ",
            attridentifier );

      attr.version:= idenum.version;
    fi;

    attr.cleanupAfterAttributeComputation( attr );

    return true;
end );


#############################################################################
##
#F  DatabaseAttributeString( <idenum>, <idenumname>, <attridentifier>
#F                           [, <format>] )
##
BindGlobal( "DatabaseAttributeString",
    function( idenum, idenumname, attridentifier, format... )
    local attr, str, strfun, txt, comp, entry;

    if not IsBound( idenum.attributes.( attridentifier ) ) then
      Error( "<idenum> has no component <attridentifier>" );
    elif Length( format ) = 0 then
      format:= "GAP";
    elif format[1] in [ "GAP", "JSON" ] then
      format:= format[1];
    else
      Error( "<format>, if given, must be \"GAP\" or \"JSON\"" );
    fi;

    attr:= idenum.attributes.( attridentifier );
    if not IsBound( attr.data ) then
      if IsBound( attr.datafile ) and IsReadableFile( attr.datafile ) then
        DatabaseAttributeLoadData( attr );
      else
        DatabaseAttributeCompute( idenum, attridentifier );
      fi;
    fi;

    if attr.type = "values" then
      Error( "the attribute <attr> must have the type \"pairs\"" );
    elif IsBound( attr.string ) then
      strfun:= attr.string;
    else
      strfun:= String;
    fi;

    if format = "GAP" then
      str:= Concatenation( "DatabaseAttributeSetData( ", idenumname, ", \"",
                attridentifier, "\",\n" );
      if IsString( attr.version ) then
        Append( str, Concatenation( "\"", attr.version, "\"," ) );
      else
        Append( str, Concatenation( String( attr.version ), "," ) );
      fi;
      Append( str, "rec(\nautomatic:=[\n" );
      txt:= "],\nnonautomatic:=[\n";
      for comp in [ attr.data.automatic, attr.data.nonautomatic ] do
        for entry in comp do
          if entry[2] <> attr.dataDefault then
            Append( str, strfun( entry ) );
          fi;
        od;
        Append( str, txt );
        txt:= "]));\n";
      od;
    else
      # Json format:
      # - version
      str:= "{\n\"version\": ";
      if IsString( attr.version ) then
        Append( str, Concatenation( "\"", attr.version, "\",\n" ) );
      else
        Append( str, Concatenation( String( attr.version ), ",\n" ) );
      fi;

      # - ID enumerator:
      Append( str, Concatenation( "\"idenum\": \"", idenumname, "\",\n" ) );

      # - attribute identifier:
      Append( str, Concatenation( "\"attrid\": \"", attridentifier, "\",\n" ) );

      # - automatically computed data:
      Append( str, "\"automatic\": [\n" );
      for entry in attr.data.automatic do
        if entry[2] <> attr.dataDefault then
          Append( str, strfun( entry ) );
        fi;
      od;
      if not IsEmpty( attr.data.automatic ) then
        Unbind( str[ Length( str ) ] );  # no newline
        Unbind( str[ Length( str ) ] );  # no final comma allowed
        Append( str, "\n" );
      fi;

      # - other data:
      Append( str, "],\n\"nonautomatic\": [\n" );
      for entry in attr.data.nonautomatic do
        if entry[2] <> attr.dataDefault then
          Append( str, strfun( entry ) );
        fi;
      od;
      if not IsEmpty( attr.data.nonautomatic ) then
        Unbind( str[ Length( str ) ] );  # no newline
        Unbind( str[ Length( str ) ] );  # no final comma allowed
        Append( str, "\n" );
      fi;
      Append( str, "]\n}\n" );
    fi;

    return str;
end );


#############################################################################
##
#F  BrowseTableFromDatabaseIdEnumerator( <dbidenum>, <labelids>, <columnids>
#F      [, <header>[, <footer>[, <choice>]]] )
##
BindGlobal( "BrowseTableFromDatabaseIdEnumerator",
    function( arg )
    local dbidenum, labelids, columnids, header, footer, choice, aligned,
          identifiers, labels, columns, id, widthLabelsRow, widthCol,
          sortFunctions, categoryValues, formattedValue, i, table;

    # Get and check the arguments.
    if not Length( arg ) in [ 3 .. 6 ] then
      Error( "usage: BrowseTableFromDatabaseIdEnumerator( <dbidenum>,\n",
             "   <labelids>, <columnids>[, <header>[, <footer>",
             "[, <choice>]]] )" );
    fi;
    dbidenum:= arg[1];
    if not ( IsRecord( dbidenum ) and IsBound( dbidenum.attributes ) ) then
      Error( "<dbidenum> must be a database id enumerator" );
    fi;
    labelids:= arg[2];
    if not ( IsList( labelids ) and
             ForAll( labelids,
                     x -> IsBound( dbidenum.attributes.( x ) ) ) ) then
      Error( "<labelids> must be a list of attribute identifiers for ",
             "<dbidenum>" );
    fi;
    columnids:= arg[3];
    if not ( IsList( columnids ) and not IsEmpty( columnids ) and
             ForAll( columnids,
                     x -> IsBound( dbidenum.attributes.( x ) ) ) ) then
      Error( "<columnids> must be a nonempty list of attribute identifiers ",
             "for <dbidenum>" );
    fi;
    header:= [];
    footer:= [];
    choice:= dbidenum.identifiers;
    if   Length( arg ) = 4 then
      header:= arg[4];
    elif Length( arg ) = 5 then
      header:= arg[4];
      footer:= arg[5];
    elif Length( arg ) = 6 then
      header:= arg[4];
      footer:= arg[5];
      choice:= arg[6];
    fi;

    aligned:= function( list, align )
      local max;

      if IsEmpty( list ) then
        return list;
      fi;
      max:= Maximum( List( list, Length ) );
      if align = "left" then
        max:= - max;
      fi;
      return List( list, x -> String( x, max ) );
    end;

    labels:= List( labelids, id -> dbidenum.attributes.( id ) );
    columns:= List( columnids, id -> dbidenum.attributes.( id ) );

    widthLabelsRow:= [];
    for i in [ 1 .. Length( labels ) ] do
      if IsBound( labels[i].widthCol ) then
        widthLabelsRow[ 2*i ]:= labels[i].widthCol;
      fi;
    od;

    # Set fixed width where the attribute forces this.
    # Set sort functions.
    # Set alignments.
    # Set the functions for computing category values.
    widthCol:= [];
    sortFunctions:= List( columns, x -> x.viewSort );
    categoryValues:= List( columns, x -> x.categoryValue );
    for i in [ 1 .. Length( columns ) ] do
      if IsBound( columns[i].widthCol ) then
        widthCol[ 2*i ]:= columns[i].widthCol;
      fi;
    od;

    # Compute an attribute value, and the table cell data object.
    formattedValue:= function( attr, j, id, width )
      local val, align;

      # Compute the attribute value, and the table cell data object.
      val:= attr.viewValue( attr.attributeValue( attr, id ) );

      # Format the entry.
      if not IsRecord( val ) then
        if IsBound( width[ 2*j ] ) then
          if   'l' in attr.align then
            align:= "left";
          else
            align:= "right";
          fi;
          if IsString( val ) then
            # It is documented that non-strings need no additional
            # formatting if the column width is prescribed.
            val:= rec( rows:= aligned( SplitString(
                         BrowseData.ReallyFormatParagraph( val,
                                   width[ 2*j ], align ), "\n" ),
                           align ),
                       align:= attr.align );
          fi;
        else
          val:= rec( rows:= [ val ], align:= attr.align );
        fi;
      fi;
      return val;
    end;

    # Construct the browse table.
    table:= rec(
      work:= rec(
        align:= "tl",
        header:= header,
        main:= [],
        Main:= function( t, i, j )
          return formattedValue( columns[j], j, choice[i], widthCol );
        end,
        m:= Length( choice ),
        n:= Length( columns ),
        corner:= [ List( labels, x -> rec( rows:= [ x.viewLabel ],
                                           align:= x.align ) ) ],
        labelsRow:= TransposedMat( List( [ 1 .. Length( labels ) ],
                           j -> List( choice,
               y -> formattedValue( labels[j], j, y, widthLabelsRow ) ) ) ),
        labelsCol:= [ List( columns,
                        x -> rec( rows:= [ x.viewLabel ],
                                  align:= x.align ) ) ],
        sepLabelsRow:= [],
        sepLabelsCol:= "=",
        sepRow:= "-",
        sepCol:= Concatenation( [ "| " ],
                                List( [ 2 .. Length( columns ) ], x -> " | " ),
                                [ " |" ] ),
        widthLabelsRow:= ShallowCopy( widthLabelsRow ),
        widthCol:= ShallowCopy( widthCol ),

        SpecialGrid:= BrowseData.SpecialGridLineDraw,
        CategoryValues:= function( t, i, j )
          local val;

          i:= i / 2;
          j:= j / 2;
          val:= columns[j].attributeValue( columns[j], choice[i] );
          val:= categoryValues[j]( val );
          return BrowseData.CategoryValuesFromEntry( t, val, j );
        end,
      ),
      dynamic:= rec(
         sortFunctionsForColumns:= sortFunctions,
      ),
    );

    if not IsEmpty( labels ) then
      table.work.sepLabelsRow:= Concatenation( [ "| " ],
                                List( [ 2 .. Length( labels ) ], x -> " | " ),
                                [ " |" ] );
    fi;

    # Customize the sort parameters for the columns.
    for i in [ 1 .. Length( columns ) ] do
      if not IsEmpty( columns[i].sortParameters ) then
        BrowseData.SetSortParameters( table, "column", i,
            columns[i].sortParameters );
      fi;
    od;

    # Return the browse table.
    return table;
end );


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

