#############################################################################
##
#W  ctbldisp.g            GAP 4 package `browse'                Thomas Breuer
##
#Y  Copyright (C)  2006,  Lehrstuhl D fuer Mathematik,  RWTH Aachen,  Germany
##


#############################################################################
##
#F  BrowseData.CompareCharacterValues( <a>, <b> )
##
##  This function is used to sort strings <a>, <b> representing the
##  display values of character table entries.
##  Strings involving the substring `" = "' (used in categories)
##  are replaced by the expression to the right of this substring,
##  strings representing integers are sorted by their values,
##  `"."' is regarded as zero,
##  other strings (denoting irrationalities) are larger than integers
##  and sorted lexicographically.
##
BrowseData.ReplacedCharacterValueString:= function( val )
    local pos;

    pos:= PositionSublist( val, " = " );
    if   pos <> fail then
      val:= val{ [ pos+3 .. Length( val ) ] };
    fi;
    if val = "." then
      return 0; 
    else
      return Int( val );
    fi;
end;

BrowseData.CompareCharacterValues:= function( a, b )
    local v1, v2;

    v1:= BrowseData.ReplacedCharacterValueString( a );
    v2:= BrowseData.ReplacedCharacterValueString( b );
    if v1 <> fail then 
      return v2 = fail or v1 < v2;
    elif v2 <> fail then
      return false;
    else
      return a < b;
    fi;
end;


#############################################################################
##
#M  Browse( <tbl>[, <options>] )
##
##  <#GAPDoc Label="Ctbl_section">
##  <Section Label="sec:ctbldisp">
##  <Heading>Character Table Display</Heading>
##
##  The &GAP; library provides a <Ref Oper="Display" BookName="ref"/> method
##  for character tables
##  that breaks the table into columns fitting on the screen.
##  &Browse; provides an alternative,
##  using the standard facilities of the function
##  <Ref Func="NCurses.BrowseGeneric"/>, i.&nbsp;e.,
##  one can scroll in the matrix of character values,
##  searching and sorting are provided etc.
##  <P/>
##  The <C>Browse</C> method for character tables can be called instead
##  of <C>Display</C>.
##  For convenience, one can additionally make this function the default
##  <C>Display</C> method for character tables,
##  by assigning it to the <C>Display</C> component in the global record
##  <C>CharacterTableDisplayDefaults.User</C>,
##  see&nbsp;<Ref Sect="Printing Character Tables" BookName="ref"/>;
##  for example, one can do this in one's <F>.gaprc</F> file,
##  see&nbsp;<Ref Sect="The .gaprc file" BookName="ref"/>.
##  (This can be undone by unbinding the component
##  <C>CharacterTableDisplayDefaults.User.Display</C>.)
##
##  <ManSection>
##  <Meth Name="Browse" Arg="tbl[, options]" Label="for character tables"/>
##
##  <Description>
##  This method displays the character table <A>tbl</A> in a window.
##  The optional record <A>options</A> describes what shall be displayed,
##  the supported components and the default values are described
##  in&nbsp;<Ref Sect="Printing Character Tables" BookName="ref"/>.
##  <P/>
##  The full functionality of the function
##  <Ref Func="NCurses.BrowseGeneric"/> is available.
##  <P/>
##  <Example><![CDATA[
##  gap> BrowseData.SetReplay( Concatenation(
##  >         # scroll in the table
##  >         "DRULdddddrrrrrlluu",
##  >         # select an entry and move it around
##  >         "seddrruuuddlll",
##  >         # search for the pattern 135 (six times)
##  >         "/135", [ NCurses.keys.ENTER ], "nnnnn",
##  >         # deselect the entry, select the first column
##  >         "qLsc",
##  >         # sort and categorize by this column
##  >         "sc",
##  >         # select the first row, move down the selection
##  >         "srdddd",
##  >         # expand the selected category, scroll the selection down
##  >         "xd",
##  >         # and quit the application
##  >         "Q" ) );
##  gap> Browse( CharacterTable( "HN" ) );
##  gap> BrowseData.SetReplay( false );
##  ]]></Example>
##  <P/>
##  <E>Implementation remarks</E>:
##  The first part of the code in the <C>Browse</C> method for character
##  tables is almost identical with the code for extracting the data to be
##  displayed from the input data in the &GAP; library function
##  <C>CharacterTableDisplayDefault</C>.
##  The second part of the code transforms these data into a browse table.
##  Character names and (if applicable) indicator values are used as row
##  labels,
##  and centralizer orders, power maps, and class names are used as column
##  labels.
##  The identifier of the table is used as the static header.
##  When an irrational entry is selected, a description of this entry is
##  shown in the dynamic footer.
##  <P/>
##  Due to an extension of the standard modes in <Ref Var="BrowseData"/>
##  by two new actions,
##  a pager can be opened that gives an overview of all all irrationalities
##  in the table, and an overview of all those irrationalities that have been
##  shown on the screen in the current call, respectively.
##  The corresponding user inputs are the <B>I</B> and the <B>i</B> key.
##  (The names assigned to the irrationalities are generated column-wise.
##  If one just scrolls through the table, without jumping, then these names
##  coincide with the names generated by the defaults <C>Display</C> method
##  for character tables;
##  this is in general <E>not</E> the case for example when a row-wise search
##  in the table is performed.)
##  <P/>
##  In order to keep the required space small also for large character
##  tables, caching of formatted matrix entries is disabled,
##  and the strings to be displayed are computed on demand with a
##  <C>Main</C> function in the <C>work</C> component of the browse table.
##  For the same reason, the constant height one for all table rows is set
##  in advance, so one need not inspect a whole character if only a few
##  values of it shall be shown.
##  <P/>
##  Special functions are provided for
##  sorting (concerning the comparison of character values,
##  which can be integers or irrationalities) and
##  categorizing the table by a column (the value in the category row
##  involves the class name of the column in question).
##  <P/>
##  The code can be found in the file <F>lib/ctbldisp.g</F> of the package.
##  </Description>
##  </ManSection>
##  </Section>
##  <#/GAPDoc>
##
if not IsBound( CambridgeMaps ) then
  CambridgeMaps:= "dummy";
fi;

InstallMethod( Browse,
    "for a character table",
    [ IsNearlyCharacterTable ],
    function( tbl )
    if   HasDisplayOptions( tbl ) then
      Browse( tbl, DisplayOptions( tbl ) );
    elif IsBound( CharacterTableDisplayDefaults.User ) then
      Browse( tbl, CharacterTableDisplayDefaults.User );
    else
      Browse( tbl, CharacterTableDisplayDefaults.Global );
    fi;
    end );


InstallMethod( Browse,
    "for a character table, and a record",
    [ IsNearlyCharacterTable, IsRecord ],
    function( tbl, options )
    local record, stringEntry, stringEntryData, legend,
          cletter, centralizers, chars_from_irr, cnr, chars, charnames,
          classes, tbl_powermap, powermap, indicator, nam, indic, i, heights,
          modes, table, ind, j, list, p, mode, pos;

    # Prepare a list of the available options records.
    options:= [ options ];
    if HasDisplayOptions( tbl ) and
       not IsIdenticalObj( options[1], DisplayOptions( tbl ) ) then
      Add( options, DisplayOptions( tbl ) );
    fi;
    if IsBound( CharacterTableDisplayDefaults.User ) and
       not IsIdenticalObj( options[1],
               CharacterTableDisplayDefaults.User ) then
      Add( options, CharacterTableDisplayDefaults.User );
    fi;
    if not IsIdenticalObj( options[1],
                CharacterTableDisplayDefaults.Global ) then
      Add( options, CharacterTableDisplayDefaults.Global );
    fi;

    # Get the options that are in at least one record.
    for record in options do
      if IsBound( record.StringEntry ) then
        stringEntry:= record.StringEntry;
        break;
      fi;
    od;
    for record in options do
      if IsBound( record.StringEntryData ) then
        stringEntryData:= record.StringEntryData( tbl );
        break;
      fi;
    od;
    for record in options do
      if IsBound( record.Legend ) then
        legend:= record.Legend;
        break;
      fi;
    od;
    for record in options do
      if IsBound( record.letter ) and Length( record.letter ) = 1 then
        cletter:= record.letter;
        break;
      fi;
    od;
    for record in options do
      if IsBound( record.centralizers ) then
        centralizers:= record.centralizers;
        break;
      fi;
    od;

    # Get the options that have no global default.
    # choice of characters and character names
    chars_from_irr:= true;
    for record in options do
      if IsBound( record.chars ) then
        if IsList( record.chars ) and ForAll( record.chars, IsPosInt ) then
          cnr:= record.chars;
          chars:= List( Irr( tbl ){ cnr }, ValuesOfClassFunction );
        elif IsInt( record.chars ) then
          cnr:= [ record.chars ];
          chars:= List( Irr( tbl ){ cnr }, ValuesOfClassFunction );
        elif IsHomogeneousList( record.chars ) then
          chars:= record.chars;
          cnr:= [ 1 .. Length( chars ) ];
          chars_from_irr:= false;
          cletter:= "Y";
        else
          cnr:= [];
          chars:= [];
        fi;
        break;
      fi;
    od;
    if not IsBound( chars ) then
      # Perhaps the irreducibles have to be computed here,
      # so we do not use this before evaluating the options.
      chars:= List( Irr( tbl ), ValuesOfClassFunction );
      cnr:= [ 1 .. Length( chars ) ];
      if HasCharacterNames( tbl ) then
        charnames:= CharacterNames( tbl );
      else
      fi;
    fi;
    if not IsBound( charnames ) then
      charnames:= List( cnr,
          i -> Concatenation( cletter, ".", String( i ) ) );
    fi;

    # choice of classes
    classes:= [ 1 .. NrConjugacyClasses( tbl ) ];
    for record in options do
      if IsBound( record.classes ) then
        if IsInt( record.classes ) then
          classes:= [ record.classes ];
        else
          classes:= record.classes;
        fi;
        break;
      fi;
    od;

    # choice of power maps
    tbl_powermap:= ComputedPowerMaps( tbl );
    powermap:= Filtered( [ 2 .. Length( tbl_powermap ) ],
                         x -> IsBound( tbl_powermap[x] ) );
    for record in options do
      if IsBound( record.powermap ) then
        if IsInt( record.powermap ) then
          IntersectSet( powermap, [ record.powermap ] );
        elif record.powermap = "ATLAS" and IsBound( CambridgeMaps ) then
          powermap:= "ATLAS";
          powermap:= CambridgeMaps( tbl );
        elif IsList( record.powermap ) then
          IntersectSet( powermap, record.powermap );
        elif record.powermap = false then
          powermap:= [];
        fi;
        break;
      fi;
    od;

    # print Frobenius-Schur indicators?
    indicator:= [];
    for record in options do
      if IsBound( record.indicator ) then
        if record.indicator = true then
          indicator:= [ 2 ];
        elif IsList( record.indicator ) then
          indicator:= Set( Filtered( record.indicator, IsPosInt ) );
        fi;
        break;
      fi;
    od;

    # (end of options handling)

    # prepare classnames
    if powermap = "ATLAS" then
      nam:= ClassNames( tbl, "ATLAS" );
    else
      nam:= ClassNames( tbl );
    fi;

    # prepare indicator
    if indicator <> [] then
      indic:= [];
      for i in indicator do
        if chars_from_irr and IsBound( ComputedIndicators( tbl )[i] ) then
          indic[i]:= ComputedIndicators( tbl )[i]{ cnr };
        else
          indic[i]:= Indicator( tbl, Irr( tbl ){ cnr }, i );
        fi;
      od;
    fi;

    heights:= ListWithIdenticalEntries( 2 * Length( chars ) + 1, 0 );
    for i in [ 2, 4 .. 2 * Length( chars ) ] do
      heights[i]:= 1;
    od;

    # Construct the modes.
    # Create a shallow copy of each default mode for `Browse',
    # and add a new action to all available modes (except the help mode):
    # Show a pager with all irrationalities in the table.
    modes:= List( BrowseData.defaults.work.availableModes,
                  BrowseData.ShallowCopyMode );
    for mode in modes do
      if mode.name <> "help" then
        BrowseData.SetActions( mode, [
          [ [ "i" ], rec( helplines := [
               "show an overview of those irrationalities in the table",
               "that have been displayed in the current call" ],
            action := function( t )
              local str;

              str:= legend( t.work.legend );
              if IsEmpty( str ) then
                str:= "(no irrationalities yet in this table)";
              fi;
              if BrowseData.IsDoneReplay( t.dynamic.replay ) then
                NCurses.Pager( str );
                NCurses.update_panels();
                NCurses.doupdate();
                NCurses.curs_set( 0 );
              fi;
              return t.dynamic.changed;
            end ) ],
          [ [ "I" ], rec( helplines := [
               "show an overview of all irrationalities in the table" ],
            action := function( t )
              local j, i, str;

              for j in [ 1 .. t.work.n ] do
                for i in [ 1 .. t.work.m ] do
                  stringEntry( chars[i][j], stringEntryData );
                od;
              od;
              str:= legend( t.work.legend );
              if IsEmpty( str ) then
                str:= "(no irrationalities yet in this table)";
              fi;
              if BrowseData.IsDoneReplay( t.dynamic.replay ) then
                NCurses.Pager( str );
                NCurses.update_panels();
                NCurses.doupdate();
                NCurses.curs_set( 0 );
              fi;
              return t.dynamic.changed;
            end ) ],
        ] );
      fi;
    od;

    # Construct the browse table.
    table:= rec(
      work:= rec(
        align:= "ct",
        header:= [ Identifier( tbl ) ],
        footer:= rec(
          # Show a description of a selected irrational value.
          select_entry:= function( t )
            local entry, result, pos, q;

            if t.dynamic.selectedEntry = [ 0, 0 ] then
              result:= [ "", First( t.dynamic.categories[2],
                               x -> x{ [ 1, 2 ] }
                                    = t.dynamic.selectedCategory )[3], "" ];
              entry:= First( t.dynamic.categories[2],
                x -> x{ [ 1, 2 ] } = t.dynamic.selectedCategory )[3];
              pos:= PositionSublist( entry, " = " );
              entry:= entry{ [ pos+3 .. Length( entry ) ] };
            else
              entry:= t.work.Main( t,
                t.dynamic.indexRow[ t.dynamic.selectedEntry[1] ] / 2,
                t.dynamic.indexCol[ t.dynamic.selectedEntry[2] ] / 2 );
            fi;
            result:= [ "", "", "" ];
            if Int( entry ) = fail then
              while 0 < Length( entry ) and entry[1] in "-/*" do
                entry:= entry{ [ 2 .. Length( entry ) ] };
              od;
              pos:= Position( t.work.legend.irrnames, entry );
              if pos <> fail then
                result[2]:= Concatenation( t.work.legend.irrnames[ pos ],
                    " = ", String( t.work.legend.irrstack[ pos ] ) );
                q:= Quadratic( t.work.legend.irrstack[ pos ] );
                if q = fail then
                  result[3]:= "";
                else
                  result[3]:= Concatenation( RepeatedString( " ",
                        Length( t.work.legend.irrnames[ pos ] ) ), " = ",
                      q.display, " = ", q.ATLAS );
                fi;
              fi;
            fi;
            return result;
          end ),
        sortFunctionForColumnsDefault:= function( t, positions )
          return BrowseData.CompareCharacterValues;
          end,
        sortFunctionForRowsDefault:= ~.work.sortFunctionForColumnsDefault,
        CategoryValues:= function( t, i, j )
          return [ Concatenation( nam[ j/2 ], " = ",
                       t.work.Main( t, i/2, j/2 ) ) ];
          end,
        availableModes:= modes,

        # Avoid computing strings for all character values in advance.
        main:= [],
        Main:= function( t, i, j )
          return stringEntry( chars[i][j], stringEntryData );
        end,
        m:= Length( chars ),
        n:= Length( classes ),

        # The labels lists will be filled below.
        labelsRow:= [ List( charnames,
                        x -> rec( rows:= [ String( x ) ], align:= "l" ) ) ],
        labelsCol:= [],
        sepLabelsRow:= [ "" ],
        sepLabelsCol:= [],
        sepCol:= [ " " ],
        corner:= [],

        # Avoid computing all entries in a row for determining the heights.
        heightRow:= heights,

        legend:= stringEntryData,
      ),
      dynamic:= rec(
        activeModes:= [ First( modes, x -> x.name = "browse" ) ],
      ),
    );

    # Provide indicator columns.
    ind:= ListWithIdenticalEntries( Maximum( 1, Length( indicator ) ), "" );
    for i in indicator do
      Add( table.work.sepLabelsRow, " " );
      if i = 2 then
        list:= [];
        for j in cnr do
          if   indic[i][j] = 0 then
            Add( list, "o" );
          elif indic[i][j] = 1 then
            Add( list, "+" );
          else
            Add( list, "-" );
          fi;
        od;
        Add( table.work.labelsRow, list );
      else
        Add( table.work.labelsRow, List( indic[i]{ cnr }, String ) );
      fi;
    od;
    table.work.labelsRow:= TransposedMat( table.work.labelsRow );
    if indicator = [] then
      Append( table.work.sepLabelsRow, [ "", "" ] );
    else
      Append( table.work.sepLabelsRow, [ "" ] );
    fi;

    if centralizers = "ATLAS" then
      # Add centralizers themselves as column labels.
      Add( table.work.sepLabelsCol, " " );
      Add( table.work.corner, [ "" ] );
      Add( table.work.labelsCol,
           List( SizesCentralizers( tbl ){ cnr }, String ) );
#T admit splitting into two rows!
    elif centralizers = true then
      # Add factored centralizer orders to the column labels.
      Add( table.work.sepLabelsCol, " " );
      for p in Set( FactorsInt( Size( tbl ) ) ) do
        Add( table.work.labelsCol, List( SizesCentralizers( tbl ){ cnr },
               x -> stringEntry( Number( Factors( x ), y -> y = p ),
                                 stringEntryData ) ) );
        Add( table.work.corner,
             Concatenation( ind, [ stringEntry( p, stringEntryData ) ] ) );
        Add( table.work.sepLabelsCol, "" );
      od;
      Unbind( table.work.sepLabelsCol[ Length( table.work.sepLabelsCol ) ] );
    fi;

    # class names and power maps
    if IsRecord( powermap ) then

      # three lines: power maps, p' part, and class names
      Append( table.work.sepLabelsCol, [ " ", "", "", " " ] );
      Append( table.work.labelsCol, [ powermap.power,
                                      powermap.prime,
                                      powermap.names ] );
      Append( table.work.corner, [ Concatenation( ind, [ "p " ] ),
                                   Concatenation( ind, [ "p'" ] ) ] );
      if   indicator = [] then
        Add( table.work.corner, [ "", "" ] );
      elif indicator = [ 2 ] then
        Add( table.work.corner, [ "", "ind" ] );
      else
        Add( table.work.corner,
             Concatenation( [ "ind" ], List( indicator, String ) ) );
      fi;

    else

      # first class names, then a row for each power map
      Add( table.work.sepLabelsCol, " " );
      Add( table.work.labelsCol, List( nam, String ) );
      Add( table.work.corner, Concatenation( ind, [ "" ] ) );

      for p in Set( FactorsInt( Size( tbl ) ) ) do
        list:= CompositionMaps( nam, tbl_powermap[p] );
        for i in [ 1 .. Length( list ) ] do
          if not IsString( list[i] ) then
            list[i]:= "?";
          fi;
        od;
        Add( table.work.labelsCol, list );
        Add( table.work.corner,
             Concatenation( ind, [ Concatenation( String( p ), "P" ) ] ) );
        Add( table.work.sepLabelsCol, "" );
      od;
      Add( table.work.sepLabelsCol, " " );

      if indicator <> [] then
        # If indicators are shown then replace the empty separator row below
        # the power maps by a contents row showing the indicator headers.
        table.work.sepLabelsCol[ Length( table.work.sepLabelsCol ) ]:= "";
        Add( table.work.sepLabelsCol, "" );
        Add( table.work.corner,
             Concatenation( [ "" ], List( indicator, String ) ) );
      fi;

    fi;

    if indicator <> [] then
      # Replace the separator before the last indicator by empty
      # if its column is only one character wide.
      pos:= Length( indicator ) + 1;
      if indicator[ Length( indicator ) ] < 10 and
         ForAll( table.work.labelsRow, x -> Length( x[ pos ] ) <= 1 ) then
        table.work.sepLabelsRow[ pos ]:= "";
      fi;
    fi;

    # Check whether at least one character and at least the degree column
    # fit into the window (the footer needs at most three lines).
    table.work.minyx:= [ t -> BrowseData.MinimalWindowHeight( t ) + 3,
                         BrowseData.MinimalWindowWidth ];

    # Show the table.
    NCurses.BrowseGeneric( table );
end );

if CambridgeMaps = "dummy" then
  Unbind( CambridgeMaps );
fi;


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

