#! /local/bin/perl

# jinx version 2.0 -- Copyright (c) 1990, Henk P. Penning.
# You may distribute under the terms of the GNU General Public License
# as specified in the README file that comes with the Jinx 2.0 kit.

sub QUIT
  { if ( $#_ >= 0 )
      { &move(0,0) ;
        &addstrC(@_) ;
        &frefresh ;
      }
    &quit() ;
  }

sub quit
  { if ( $#_ >= 0 )
      { &showStatus(@_) ; }
    &move($LINES-1,0) ;
    &clrtoeol ;
    &frefresh ;
    &addlog("jinx closed.") ;
    &endwin ;
    &finishCterm ;
    close(STDOUT) || warn "can't close STDOUT ($!)\n" ;
    exit ;
  }

sub showMenu
  { local(*menu) = @_ ;
    return if $oldMenu eq *menu ;
    $oldMenu = *menu ;
    local($i) ;
    &clrreg($firstMenuLine,$lastMenuLine) ;
    for ($i=0 ; $i <= $#menu ; $i++)
      { &mvaddstr($firstMenuLine+$i,int(($COLS-length($menu[$i]))/2),
                  $menu[$i]) ;
      }
    &frefresh ;
  }

sub showHelp
  { local(*menu) = @_ ;
    &showCommand('help on (RET to quit):',' ') ;
    for ( $res = &getchintR
	; $res ne 'KEY_RET' && $res ne ' '
	; $res = &getchintR
	)
      { $res = defined($menu{$res})
               ? ( defined($menu{"help_$res"})
                 ? $menu{"help_$res"} : 'no help'
                 )
               : "not on the menu ($res)" ;
        &showStatus($res) ;
        &move($commandLine,$curposCommand) ;
      }
  }

sub showChoice
  { local(*menu,$default,$command) = @_ ;
    local($res) ;
    $command = "now what:" if $command eq '' ;
    &showMenu(*menu) ;
    &showCommand($command,$menu{$default}) ;
    while ( 1 )
      { $res = &getchintR ;
        &showStatus('') ;
        $res = $default if $res eq 'KEY_RET' ;
        $res = 'KEY_LEFT'  if $res eq 'h' ;
        $res = 'KEY_DOWN'  if $res eq 'j' ;
        $res = 'KEY_UP'    if $res eq 'k' ;
        $res = 'KEY_RIGHT' if $res eq 'l' ;
        last if defined $menu{$res} ;
        if ( $res eq '?' )
          { &showHelp(*menu) ;
            &showCommand($command,$menu{$default}) ;
            next ;
          }
        &showStatusBeep("not on the menu ($res)") ;
        &move($commandLine,$curposCommand) ;
        &frefresh ;
      }
    $res = $menu{'KEY_TAB'} if $res eq 'KEY_TAB' ;
    &addCommand($menu{$res}) if $res ne $default ;
    return $res ;
  }

sub showStatusList
  { local($_) ;
    &showStatus(shift) if $#_ >= 0 ;
    for ( @_ )
      { &showCommand('hit any key to continue: ') ;
        &getchintR ;
        &showStatus($_) ;
      }
    &frefresh ;
  }

sub showStatus
  { local($_) = join('',@_) ;
    return if $oldStatus eq $_ ;
    if ( $_[0] eq '' )
      { &mvaddstr($statusLine,0,'>') ;
        &clrtoeol ;
      }
    else
      { &show($_,0,$statusLine,0,$COLS-2,'> > ') ; }
    $oldStatus = $_ ;
    &frefresh ;
  }

sub showStatusBeep
  { &showStatus ;
    &beep ;
  }

sub showJinxD
  { local($marked) = @_ ;
    local($dbName,$mod) ;
    $dbName = ($db ne '') ? $db : (($#name < 0) ? 'none' : 'no name') ;
    $mod = ($#name < 0) ? '' : ($dirty ? ' [modified]' : ' [not modified]') ;
    $marked = ( $marked > 0 ) ? " marked $marked" : '' ;
    local($str) = "database: " . $dbName . $mod . $marked ;
    if ( $str ne $oldJinxD )
      { &show($str,0,$jinxLineD,0,$COLS-2,'> > ') ;
        $oldJinxD = $str ;
      }
  }

sub showJinxR
  { local($numRecs,$markInfo) = @_ ;
    local($str) ;
    if ( $numRecs >= 0 )
      { $str = sprintf("record %d of %d",$curr+1,$numRecs+1) ;
        $str .= " $markInfo" if $markInfo ne '' ;
      }
    else
      { $str = 'no data' ; }
    if ( $str ne $showJinxR )
      { &show($str,0,$jinxLineR,0,$COLS-2,'> > ') ;
        $showJinxR = $str ;
      }
  }

sub showCommand
  { &mvaddstr($commandLine,0,join(' ',@_)) ;
    &clrtoeol ;
    $curposCommand = length($_[0])+1  ;
    &move($commandLine,$curposCommand) if $#_ > 0 ;
    &frefresh ;
  }

sub addCommand
  { &mvaddstr($commandLine,$curposCommand,$_[0]) ;
    &clrtoeol ;
    &frefresh ;
  }

sub checkDirty
  { return 1 if $dirty == 0 ;
    local($dbname) = substr($db,rindex($db,'/')+1,1000) ;
    local($times) = ($dirty == 1) ? 'once' : "$dirty times" ;
    &showStatusBeep( (($db eq '') ? 'db' : $dbname) . " modified $times") ;
    if ( &showChoice(*dirtyMenu,'n',"$command anyway?") eq 'y' )
      { return 1 ; }
    else
      { &showStatus('no change') ;
        $checkDirty = 1 ;
        return 0 ;
      }
  }

sub doDescrMenu
  { local(*descr) = @_ ;
    local(@name,@pat,@record) ;
    local($res,$res1,$newPat,$next) = 'x' ;

    &splitDescr(*descr,*name,*pat) ;
    @record = @pat ;

    while ( 1 )
      { $res = &showChoice(*doDescrMenu,$res) ;
        if ( $res eq 'x' )
          { return 0 ; }
        elsif ( $res eq 'q' )
          { &mkDescr(*descr,*name,*pat) ;
            return 1 ;
          }
        elsif ( $res eq 'KEY_DOWN' && $curF < $#name )
          { $curF++ ; }
        elsif ( $res eq 'KEY_UP' && $curF > 0 )
          { $curF-- ; }
        elsif ( $res eq 'p' )
          { $res1 = &doUpdate(*updReMenu,'user',1,
                              *testRe,*quitUpd,*retPat) ;
            if ( $res1 eq 'a' )
              { @pat = @record ; }
            else
              { &showStatus('no change') ;
                @record = @pat ;
              }
          }
        elsif ( $res eq 'n' )
          { @record = @name ;
            &unshowValues ;
            &showRecord(*name,*record,$#data) ;
            $res1 = &doUpdate(*updReMenu,'user',1,
                              *testNames,*quitUpd,*retNames) ;
            if ( $res1 eq 'a' )
              { @name = @record ; }
            else
              { &showStatus('no change') ; }
            @record = @pat ;
            &unshowRecord ;
          }
        else
          { &showStatusBeep("can't do it") ; }
        &showRecord(*name,*record,$#data) ;
      }
  }

sub getNames
  { local(*name) = @_ ;
    local($res,$cnt,@record,@res,$_,$i) = ('q',1) ;

    @record = &emptyRecord($#name) ;
    $Xr = 0 ;
    $curF = 0 ;
    &unshowValues ;
    &showRecord(*name,*record,$#data) ;

    while ( 1 )
      { $res = &showChoice(*getNamesMenu,$res) ;
        if ( $res eq 'x' )
          { return 0 ; }
        elsif ( $res eq 'q' )
          { @res = () ;
            $i = 0 ;
            for ( @record )
              { $res[$_-1] = $i if $_ > 0 ;
                $i++ ;
              }
            return 1, @res ;
          }
        elsif ( $res eq 'KEY_DOWN' && $curF < $#name )
          { $curF++ ; }
        elsif ( $res eq 'KEY_UP' && $curF > 0 )
          { $curF-- ; }
        elsif ( $res eq 'KEY_RIGHT' && $record[$curF] == 0 )
          { $record[$curF] = $cnt++ ;
            &showValue($record[$curF],$curF) ;
          }
        elsif ( $res eq 'KEY_LEFT' && $record[$curF] != 0 )
          { $cnt-- ;
            if ( $record[$curF] == $cnt )
              { &showValue('',$curF) ; }
            else
              { for ( @record )
                 { $_-- if $_ != 0 && $_ > $record[$curF] ; }
                &unshowValues ;
              }
            $record[$curF] = '' ;
          }
        elsif ( $res eq 'd' && $record[$curF] > 1 )
          { $i = 0 ;
            for ( @record )
              { if ( $_ == $record[$curF] - 1 )
                  { $_++ ; &showValue($_,$i) ; }
                $i++ ;
              }
            &showValue(--$record[$curF],$curF) ;
          }
        elsif ( $res eq 'i' && $record[$curF] < $cnt-1 && $record[$curF] > 0 )
          { $i = 0 ;
            for ( @record )
              { if ( $_ == $record[$curF] + 1 )
                  { $_-- ; &showValue($_,$i) ; }
                $i++ ;
              }
            &showValue(++$record[$curF],$curF) ;
          }
        elsif ( $res eq 's' && $cnt > 1 )
          { $i = 0 ;
            @res = &emptyRecord($#name) ;
            for ( @record )
              { if ( $_ > 0 )
                  { $res[$_-1] = $name[$i] ; }
                $i++ ;
              }
            &unshowValues ;
            &showRecord(*name,*res,$#data) ;
            &showCommand('hit any key to continue: ') ;
            &getchintR ;
            &unshowValues ;
          }
        else
          { &showStatusBeep("can't do it") ; }
        &showRecord(*name,*record,$#data) ;
      }
  }

sub getFieldDescr
  { local($fName,$fPat,$fVal,$res) ;

    ($res,$fName) = &getStrFrom("fieldname:",*fName,$#fName) ;
    if ( $res == 0 || $fName eq '' )
      { return '', "no name ; no change" ; }
    elsif ( $fName !~ /$namePat/ )
      { return '', "name '$fName' not alfa-numeric ; no change" ; }
    elsif ( grep(/^$fName$/,@name) != 0 )
      { return '', "name '$fName' already used ; no change" ; }

    ($res,$fPat) = &getStrFrom("pattern:",*sPat,$#sPat) ;
    if ( $res == 0 )
      { return '', 'no change' ; }
    elsif ( ! &testPat($fPat) )
      { return '', "$@ ; no change" ; }

    ($res,$fVal) = &getStrFrom("default value:",*fVal,$#fVal) ;
    if ( $res == 0 )
      { return '', 'no change' ; }
    elsif ( $fPat && $fVal !~ /$fPat/ )
      { return '', "default '$fVal' doesn't match '$fPat' ; no change" ; }

    return $fName, $fPat, $fVal ;
  }

sub searchNext
  { local(*row,$from,$sPat) = @_ ;
    local($i) ;

    &showStatus("searching ($sPat) ...") ;
    for ($i=$from+1 ; $i<=$#row && $row[$i] !~ /$sPat/ ; $i++) { ; } ;
    return($i,'found')           if $i <= $#row ;
    for ($i=0 ; $i<=$from && $row[$i] !~ /$sPat/ ; $i++) { ; } ;
    return($i,'found only here') if $i == $from ;
    return($i,'found')           if $i <  $from ;
    return($from,'not found') ;
  }

sub searchNextRecord
  { local(*row,$fromR,*record,$fromF,$sPat) = @_ ;
    local($i,$j,@rec1) ;

    &showStatus("searching ($sPat) ...") ;
    for ($j=$fromF+1 ; $j<=$#record && $record[$j] !~ /$sPat/ ; $j++) { ; } ;
    return($fromR,$j,'found in this record') if $j <= $#record ;
    for ($i=$fromR+1 ; $i<=$#row && $row[$i] !~ /$sPat/ ; $i++) { ; } ;
    if ( $i <= $#row )
      { @rec1 = split($;,$row[$i],$#record+1) ;
	for ($j=0 ; $j<=$#rec1 && $rec1[$j] !~ /$sPat/ ; $j++) { ; } ;
	return($i,$j,'found') if $j <= $#rec1 ;
	return($i,$fromF,"error: record ($i) but no field in tail") ;
      }
    for ($i=0 ; $i<=$fromR && $row[$i] !~ /$sPat/ ; $i++) { ; } ;
    return($fromR,$fromF,'not found') if $i > $fromR ;
    @rec1 = split($;,$row[$i],$#record+1) ;
    for ($j=0 ; $j<=$#rec1 && $rec1[$j] !~ /$sPat/ ; $j++) { ; } ;
    return($i,$fromF,"error: record ($i) but no field in head") if $j > $#rec1 ;
    return($i,$j,'found')                     if $i != $fromR ;
    return($i,$j,'found only in this record') if $j != $fromF ;
    return($i,$j,'found only here')           if $j == $fromF ;
  }

sub searchNextIndRecord
  { local(*ind,*row,$fromR,*record,$fromF,$sPat) = @_ ;
    local($i,$j,@rec1) ;

    &showStatus("searching ($sPat) ...") ;
    for ($j=$fromF+1 ; $j<=$#record && $record[$j] !~ /$sPat/ ; $j++) { ; } ;
    return($fromR,$j,'found in this record') if $j <= $#record ;
    for ($i=$fromR+1 ; $i<=$#ind && $row[$ind[$i]] !~ /$sPat/ ; $i++) { ; } ;
    if ( $i <= $#ind )
      { @rec1 = split($;,$row[$ind[$i]],$#record+1) ;
	for ($j=0 ; $j<=$#rec1 && $rec1[$j] !~ /$sPat/ ; $j++) { ; } ;
	return($i,$j,'found') if $j <= $#rec1 ;
	return($i,$fromF,"error: record ($i) but no field in tail") ;
      }
    for ($i=0 ; $i<=$fromR && $row[$ind[$i]] !~ /$sPat/ ; $i++) { ; } ;
    return($fromR,$fromF,'not found') if $i > $fromR ;
    @rec1 = split($;,$row[$ind[$i]],$#record+1) ;
    for ($j=0 ; $j<=$#rec1 && $rec1[$j] !~ /$sPat/ ; $j++) { ; } ;
    return($i,$fromF,"error: record ($i) but no field in head") if $j > $#rec1 ;
    return($i,$j,'found')                     if $i != $fromR ;
    return($i,$j,'found only in this record') if $j != $fromF ;
    return($i,$j,'found only here')           if $j == $fromF ;
  }

sub setDataYXd
  { if ( $curr < $numFieldLines )
      { $Yd = 0 ; }
    elsif ( $curr > $#data - $numFieldLines )
      { $Yd = $#data - $numFieldLines + 1 ; }
    else
      { $Yd = $curr - int(($numFieldLines-1)/2) ; }
    $maxStrlenD = &maxStrlen(*data,$Yd,$Yd+$numFieldLines-1) ;
    $maxXd = $maxStrlenD - $numInfoCols + 2 ;
    $Xd = &max(0,&min($maxXd,$Xd)) ;
  }

sub unshowData
  { $oldYd = undef ; }

sub showData
  { local(*data) = @_ ;
    local($i) ;
    &showJinxR($#data) ;
    if ( ! defined($oldYd) || $Yd > $curr || $curr >= $Yd + $numFieldLines )
      { &setDataYXd ; }
    if ( ! defined($oldYd) )
      { &setDataScreen(*data) ;
        &showDataScreen(*data,$Yd,$Xd,$curr) ;
      }
    elsif ( $Yd != $oldYd || $Xd != $oldXd )
      { &showDataScreen(*data,$Yd,$Xd,$curr) ; }
    elsif ( $#data >= 0 )
      { &moveCursorScreen($curr,$Yd) ; }
    $oldYr = undef ;
    $oldYd = $Yd ;
    $oldXd = $Xd ;
    &frefresh ;
  }

sub setYr
  { if ( $curF < $numFieldLines )
      { $Yr = 0 ; }
    elsif ( $curF > $#record - $numFieldLines )
      { $Yr = $#record - $numFieldLines + 1 ; }
    else
      { $Yr = $curF - int(($numFieldLines-1)/2) ; }
  }

sub setXr
  { @strlenR = @record ;
    $maxStrlenR = &max(grep($_=length,@strlenR)) ;
    $maxXr = $maxStrlenR - $numValueCols + 2 ;
    $Xr = &max(0,&min($Xr,$maxXr)) ;
  }

sub unshowRecord
  { $oldYr = undef ; }

sub unshowValues
  { $oldXr = undef ; }

sub showValue
  { local($value,$curF) = @_ ;
    &showValueScreen($value,$curF,$Yr) ;
  }

sub editValue
  { local($newField,$curF) = @_ ;
    local(@res) ;
    @res = &editValueScreen($newField,$curF,$Yr) ;
    return @res ;
  }

sub showRecord
  { local(*name,*record,$numRecs,$markInfo) = @_ ;
    &showJinxR($numRecs,$markInfo) ;
    if ( ! defined($oldYr) || $Yr > $curF || $curF >= $Yr + $numFieldLines )
      { &setYr ; }
    if ( ! defined($oldYr) )
      { &setKeyScreen(*name) ;
        &showKeysScreen(*name,$Yr,0,$curF) ;
        &setXr ;
        &showValuesScreen(*record,$Yr,$Xr) ;
      }
    elsif ( $Yr != $oldYr )
      { &showKeysScreen(*name,$Yr,0,$curF) ;
        &setXr ;
        &showValuesScreen(*record,$Yr,$Xr) ;
      }
    else
      { if ( ! defined($oldXr) || $Xr != $oldXr )
          { &setXr ;
            &showValuesScreen(*record,$Yr,$Xr) ;
          }
        if ( $curF != $oldCurF )
          { &moveCursorScreen($curF,$Yr) ; }
      }
    $oldYd = undef ;
    $oldYr = $Yr ;
    $oldXr = $Xr ;
    $oldCurF = $curF ;
    &frefresh ;
  }

sub showTest
  { local(*name,*pat,*record) = @_ ;
    local($_,@res,$i) ;
    @res = &doTest(*record,*pat) ;
    do { &showStatus("ok") ; return ; } if $#res < 0 ;
    for ( @res )
      { $curF = $_ ;
        &showRecord(*name,*record,$#data) ;
        &showStatusBeep("field doesn't match $pat[$_]") ;
        last if $i++ == $#res ;
        &showCommand('hit any key to continue: ') ;
        &getchintR ;
      }
  }

sub showTestAll
  { local(*name,*pat,*data) = @_ ;
    local($c,$tmpCurr,@record,@res) = 'n' ;
    $tmpCurr = $curr ;
showTestAll:
    for $curr ( $tmpCurr..$#data,0..$tmpCurr-1 )
      {
showTest:
        while ( 1 )
          { @record = split(/$;/,$data[$curr],$#name+1) ;
            @res = &doTest(*record,*pat) ;
            next showTestAll if $#res < 0 ;

            &unshowValues ;
showField:  for ( @res )
              { $curF = $_ ;
                &showRecord(*name,*record,$#data) ;
                &showStatusBeep("field doesn't match $pat[$_]") ;
                $c = &showChoice(*testMenu,$c) ;
                last showField if $c =~ /[quN]/ ;
              }

            if ( $c eq 'q')
              { &showStatus('quit Test All') ;
                return $curr ;
              }
            elsif ( $c eq 'u' )
              { $c = &doUpdate(*updMatchMenu,'user',1,
                               *testUpd,*quitUpd,*retUpd) ;
                if ( $c eq 'a' )
                  { $data[$curr] = $record ;
                    $dirty++ ;
                    &showJinxD() ;
                  }
                $c = 'n' ;
                redo showTest ;
              }
            else
              { last showTest ; }
          }
        &showStatus('Testing All ...') ;
      }
    &showStatus('tested all') ;
    return $curr ;
  }

sub getStrFrom
  { local($command,*alts,$default) = @_ ;
    local($x,$str,$ret) ;

    $default = $#alts if $#_ < 2 ;
    push(@alts,'') ;
    $default = &max(0,&min($default,$#alts)) ;

    &showMenu(*searchMenu) ;
    $command .= ' ' ;
    $x = length($command) ;
    &showCommand($command) ;

    while ( 1 )
      { $str = $alts[$default] ;
        ($str,$res) = &edit($str,0,$commandLine,$x,$COLS-$x-2,'|<>|') ;
        $res = &ch2str($res) ;
        if ( $res eq 'KEY_TAB' )
          { $ret = 0 ; $str = '' ; last ; }
        elsif ( $res eq 'KEY_RET' )
          { $ret = 1 ; last ; }
        elsif ( $res eq 'KEY_UP' && $default > 0 )
          { $default-- ; }
        elsif ( $res eq 'KEY_DOWN' && $default < $#alts )
          { $default++ ; }
        else
          { &showStatusBeep("can't do $res") ; }
      }
    pop(@alts) while $#alts >= 0 && ! $alts[$#alts] ;
    push(@alts,$str) if $str && ( $#alts < 0 || $str ne $alts[$#alts] ) ;
    return $ret, $str ;
  }

sub getGoto
  { local($curr) = @_ ;
    local($res,$goto) = &getStrFrom('Where:',*gotos,$#gotos) ;
    return '' unless $res ;
    $goto .= '' ;
    return $goto-1  if $goto =~ /^[0-9]+$/ ;
    return $curr+$1 if $goto =~ /^\+([0-9]+)$/ ;
    return $curr-$1 if $goto =~ /^\-([0-9]+)$/ ;
    return '' ;
  }

sub doPeek
  { local(*name,$db,*name1,*data1) = @_ ;

    local($_,@vals,@common,@common1,@new,@empty) ;

    &mkInvert(*name) ;
    &mkInvert(*name1) ;
    for ( @name1 )
      { if ( defined $name{$_} )
	  { push(@common,$name{$_}) ;
	    push(@common1,$name1{$_}) ;
          }
	else
	  { $_ = '__' . $_ ; }
      }
    
    return(0,"no field in common ; no change") if $#common < 0 ;
    $_ = ( $#common ? 'fields' : 'field' ) ;
    &showStatus("relevant $_ : " . join(',',@name1[@common1]) ) ;

    @vals = 0..$#data1 ;
    $db = "Peeking in " . substr($db,rindex($db,'/')+1,1000) ;
    ($res,@vals) = &selectFrom($db,*name1,*data1,@vals) ;
    if ( $res eq 'q' || $#vals < 0 )
      { return(0,'no change') ; }
    else
      { @empty = &emptyRecord($#name) ;
	for ( @vals )
	  { @new = @empty ;
	    @new[@common] = (split(/$;/,$data1[$_],$#name1+1))[@common1] ;
	    push(@data,join($;,@new)) ;
	  }
	$_ = $#vals+1 ;
	$_ = ( $_ == 1 ) ? 'one record' : "$_ records" ;
	return(1,"added $_ (at the end)") ;
      }
  }

sub doRead
  { local(*name,$db,*name1,*data1) = @_ ;

    local($_,$i,@vals,@common,@common1,@new,@empty,%key,%key1,$key1) ;

    &mkInvert(*name) ;
    &mkInvert(*name1) ;
    for ( @name1 )
      { if ( defined $name{$_} )
	  { push(@common,$name{$_}) ;
	    push(@common1,$name1{$_}) ;
          }
	else
	  { $_ = '__' . $_ ; }
      }
    
    return(0,"no field in common ; no change") if $#common < 0 ;

    for ( @data )
      { $key{join($;,(split(/$;/,$_))[@common])} = 1 ; }

    $i = 0 ;
    for ( @data1 )
      { $key1 = join($;,(split(/$;/))[@common1]) ;
	push(@vals,$i) unless defined($key{$key1}) || defined($key1{$key1}) ;
        $key1{$key1}++ ;
        $i++ ;
      }

    return(0,"no new records to Read ; no change") if $#vals < 0 ;

    $_ = ( $#common ? 'fields' : 'field' ) ;
    &showStatus("relevant $_ : " . join(',',@name1[@common1]) ) ;

    $db = "Reading in " . substr($db,rindex($db,'/')+1,1000) ;
    ($res,@vals) = &selectFrom($db,*name1,*data1,@vals) ;
    if ( $res eq 'q' || $#vals < 0 )
      { return(0,'no change') ; }
    else
      { @empty = &emptyRecord($#name) ;
	for ( @vals )
	  { @new = @empty ;
	    @new[@common] = (split(/$;/,$data1[$_],$#name1+1))[@common1] ;
	    push(@data,join($;,@new)) ;
	  }
	$_ = $#vals+1 ;
	$_ = ( $_ == 1 ) ? 'one record' : "$_ records" ;
	return(1,"added $_ (at the end)") ;
      }
  }

# doKeyTest returns 0 if error or quit
# doKeyTest returns 1 if all uniq
# doKeyTest returns x if x+1 doubles appeared
sub doKeyTest
  { local(*data,*name,*fields) = @_ ;

    local($_,$i,$res,@dvals,@vals,$vals,@delete,%key,@prefix) ;
    local(@names) = @name ;
    local($ret) = 1 ;

    if ( $#fields >= 0 )
      { for ( 0..$#names ) { $prefix[0+$_] = '__' ; }
        for ( @fields )    { $prefix[0+$_] = '' ; }
	$i = 0 ;
        for ( @names )     { $_ = $prefix[$i++] . $_ ; }
	$i = 0 ;
        for ( @data )
          { $key{join($;,(split(/$;/,$_))[@fields])} .= ( $i++ . ',' ) ; }
      }
    else
      { for ( @data ) { $key{$_} .= ( $i++ . ',' ) ; }
	@fields = 0..$#names ;
      }

    local($db) = "SELECT TO DELETE" ;
keyTest:
    for ( sort keys %key )
      { $vals = $key{$_} ;
	chop $vals ;
	next keyTest unless $vals =~ /,/ ; 
	$ret++ ;
	next keyTest if $res eq 'q' ; 
	@vals = split(/,/,$vals) ;
keyTestMenu:
	while ( 1 )
	  { &showStatus($#vals+1, " records have key '", &intext($_), "'") ;
	    $res = &showChoice(*keyTestMenu,'n') ;
	    if ( $res eq 's' )
	      { ($res,@dvals) = &selectFrom($db,*names,*data,@vals) ;
                if ( $res eq 'q' )
                  { next keyTestMenu ; }
                elsif ( $res eq 'a' )
                  { push(@delete,@dvals) ;
		    next keyTest ;
		  }
                else
                  { &showStatus('Huh ?') ; }
	      }
            elsif ( $res eq 'n' )
              { next keyTest ; }
            elsif ( $res eq 'q' )
              { next keyTest ; }
            elsif ( $res eq 'x' )
              { @delete = () ;
		$ret = 0 ; ;
		last keyTest ;
	      }
            else
              { &showStatus('Huh ?') ; }
	  }
      }
    &unshowRecord ;
    return $ret, @delete ;
  }

sub selectFrom
{ local($db,*name2,*data2,@vals) = @_ ;
  local($c,$curr,$Yr,$Xr,$maxStrlenR,$curF,$dirty,$i) ;
  local($marked,@sub,@unmarked,%marked,$markInfo) ;
  local($res,$res1,$valscurr,$sPat) ;

  $curr = 0 ;
  $c = 'n' ;
  $Xr = 0 ;
  &unshowRecord ;

  while ( 1 )
    { $valscurr = $vals[$curr] ;
      @record = split(/$;/,$data2[$valscurr],$#name2+1) ;
      $markInfo = ( defined $marked{$valscurr} ) ? '** MARKED **' : '' ;
      &showJinxD($dirty) ;
      &showRecord(*name2,*record,$#vals,$markInfo) ;
      $c = &showChoice(*selectFromMenu,$c) ;

      if ( $c eq "q" )
        { return('q') ; }
      elsif ( $c eq "s" )
        { return('a',$valscurr) ; }
      elsif ( $c eq "S" )
        { return('a', keys %marked ) ; }
      elsif ( $c eq "t" )
        { if ( defined $marked{$valscurr} )
            { delete $marked{$valscurr} ; $dirty-- ; }
          else
            { $marked{$valscurr} = 1 ; $dirty++ ; }
        }
      elsif ( $c eq "T" )
        { for $i ( @vals )
	    { if ( defined $marked{$i} )
                { delete $marked{$i} ; $dirty-- ; }
              else
                { $marked{$i} = 1 ; $dirty++ ; }
            }
	}
      elsif ( $c eq "C" )
        { %marked = () ;
	  $dirty = 0 ;
	}
      elsif ( $c eq 'M' && $dirty > $#data2 )
	{ &showStatusBeep('every record is Marked') ; }
      elsif ( $c eq 'M' )
        { @record = &emptyRecord($#name2) ;
          $Xr = 0 ;
          $curF = 0 ;
          &unshowValues ;
          &showRecord(*name2,*record,$#data2) ;
          if ( &doUpdate(*updReMenu,'user',1,
                         *testRe,*quitUpdAdd,*retRe) eq 'a'
             )
            { &showStatus('Marking ...') ;
              @unmarked = grep( ! defined $marked{$_}, @vals) ;
              ($res,$res1) = &doAddMark(*data2,*record,*marked,*unmarked) ;
              if ( $res )
                { &showStatus("marked ",$res1+0) ;
                  $dirty += $res1 ;
                }
              &showStatus($res1) if ! $res ;
            }
          else
            { &showStatus('no change') ; }
          &unshowValues ;
        }
      elsif ( $c eq 'U' && $dirty == 0 )
	{ &showStatusBeep('every record is Unmarked') ; }
      elsif ( $c eq 'U' )
        { @record = &emptyRecord($#name2) ;
          $Xr = 0 ;
          $curF = 0 ;
          &unshowValues ;
          &showRecord(*name2,*record,$#data2) ;
          if ( &doUpdate(*updReMenu,'user',1,
                         *testRe,*quitUpdAdd,*retRe) eq 'a'
             )
            { &showStatus('unMarking ...') ;
              @sub = keys %marked ;
              ($res,$res1) = &doDelMark(*data2,*record,*marked,*sub) ;
              if ( $res )
                { &showStatus("unmarked ",0+$res1) ;
                  $dirty -= $res1 ;
                }
              &showStatus($res1) if ! $res ;
            }
          else
            { &showStatus('no change') ; }
          &unshowValues ;
        }
      elsif ( $c eq "n" && $curr < $#vals )
        { $curr++ ;
          &unshowValues ;
        }
      elsif ( $c eq "p" && $curr > 0 )
        { $curr-- ;
          &unshowValues ;
        }
      elsif ( $c eq "/" )
        { ($res,$sPat) = &getStrFrom('pattern:',*sPat,$#sPat) ;
          if ( $res == 0 || $sPat eq '' )
	    { &showStatus('no change') ; }
          elsif ( ! &testPat($sPat) )
	    { &showStatus($@) ; }
	  else
	    { ($curr,$curF,$res) =
		&searchNextIndRecord(*vals,*data2,$curr,*record,$curF,$sPat) ;
              &showStatus($res) ;
              &unshowValues if $res eq 'found' ;
	    }
        }
      elsif ( $c eq ";" && $#sPat >= 0 )
        { $sPat = $sPat[$#sPat] ;
          if ( ! &testPat($sPat) )
	    { &showStatus($@) ; }
	  else
	    { ($curr,$curF,$res) =
		&searchNextIndRecord(*vals,*data2,$curr,*record,$curF,$sPat) ;
              &showStatus($res) ;
              &unshowValues if $res eq 'found' ;
	    }
        }
      elsif ( $c eq ">" && $dirty > ( defined $marked{$valscurr} ) )
        { for ( $i=$curr+1 ;
                $i <= $#vals && ! defined $marked{$vals[$i]} ;
                $i++ ) { ; }
          if ( $i > $#vals )
            { &showStatusBeep('not found') ; }
          else
            { &showStatus('found') ;
              $curr = $i ;
              &unshowValues ;
            }
        }
      elsif ( $c eq "<" && $dirty > ( defined $marked{$valscurr} ) )
        { for ( $i=$curr-1 ; 
                $i >= 0 && ! defined $marked{$vals[$i]} ; 
                $i-- ) { ; }
          if ( $i < 0 )
            { &showStatusBeep('not found') ; }
          else
            { &showStatus('found') ;
              $curr = $i ;
              &unshowValues ;
            }
        }
      elsif ( $c eq ")" && ($#vals - $dirty >= (!defined($marked{$valscurr}))) )
        { for ( $i=$curr+1 ;
                $i <= $#vals && defined $marked{$vals[$i]} ;
                $i++ ) { ; }
          if ( $i > $#vals )
            { &showStatusBeep('not found') ; }
          else
            { &showStatus('found') ;
              $curr = $i ;
              &unshowValues ;
            }
        }
      elsif ( $c eq "(" && ($#vals - $dirty >= (!defined($marked{$valscurr}))) )
        { for ( $i=$curr-1 ; 
                $i >= 0 && defined $marked{$vals[$i]} ; 
                $i-- ) { ; }
          if ( $i < 0 )
            { &showStatusBeep('not found') ; }
          else
            { &showStatus('found') ;
              $curr = $i ;
              &unshowValues ;
            }
        }
      elsif ( $c eq "g" && ($res = &getGoto($curr)) ne '' &&
              0 <= $res && $res <= $#data2 )
        { do { $curr = $res ; &unshowValues ; } if $res != $curr ;
          $Xr = 0 ;
        }
      elsif ( $c eq 'KEY_UP' && $curF > 0 )
        { $curF-- ; }
      elsif ( $c eq 'KEY_DOWN' && $curF < $#name2 )
        { $curF++ ; }
      elsif ( $c eq 'KEY_LEFT' && $Xr > 0 )
        { $Xr = &max($Xr-1-int(($numValueCols-2)/3),0) ; }
      elsif ( $c eq 'KEY_RIGHT' && $Xr < $maxXr )
        { $Xr = &min($Xr+1+int(($numValueCols-2)/3),$maxXr) ; }
      else
        { &showStatusBeep("can't do it") ; }
   }
}

sub doUpdate
  { local(*menu,$modeDef,$acceptDef,*testUp,*quitUp,*retUp) = @_ ;
    local($newField,$res,$c) ;
    local($mode) = $modeDef ;
    local(@accept) = @record ;
    for (@accept) { $_ = $acceptDef ; }

    while (1)
      { $newField = $record[$curF] if $res ne 'edit' ;
        if ( $mode eq 'user' || $res eq 'edit' )
          { &showMenu(*updateMenu) ;
            &showCommand("now what:","edit, rule: $pat[$curF]") ;
            ($newField,$res) = &editValue($newField,$curF) ;
            $res = &ch2str($res) ;
            &showStatus('') ;
          }
        else
          { $res = 'KEY_TAB' ; }

        if ( $record[$curF] eq $newField && $accept[$curF] )
          { ; }
        elsif ( &testUp($newField,$curF,$res) )
          { $record[$curF] = $newField ;
            $accept[$curF] = $acceptDef ;
          }
        else
          { &showStatusBeep($testUp) ;
            $c = &showChoice(*menu,'e') ;
            if ( $c eq "e" )
              { $res = 'edit' ; }
            elsif ( $c eq "a" )
              { $record[$curF] = $newField ;
                $accept[$curF] = $acceptDef ;
              }
            elsif ( $c eq "f" )
              { &showValue($record[$curF],$curF) ;
                if ( $accept[$curF] || &testUp($record[$curF],$curF) )
                  { &showStatus('') ; }
                else
                  { $newField = $record[$curF] ;
                    $res = 'edit' ;
                    &showStatusBeep("this isn't ok either") ;
                  }
              }
            elsif ( $c eq "q" )
              { $res = 'exit' ;
                &unshowValues ;
              }
          }

        if ( $res eq 'exit' )
          { return('f') ; }
        elsif ( $res eq 'edit' )
          { ; }
        elsif ( &quitUp($res,$curF) )
          { $record = join($;,@record) ;
            $res = &retUp ;
            return $res if $res ne 'e' ;
            $res = 'edit' ;
          }
        elsif ( $res eq 'KEY_UP' && $curF > 0 )
          { $curF-- ; }
        elsif ( ( $res eq 'KEY_TAB' || $res eq 'KEY_RET' || $res eq 'KEY_DOWN' )
                && $curF < $#name )
          { $curF++ ; }
        else
          { &beep ; }
      }
    continue
      { &showRecord(*name,*record,$#data) ; }
  }

sub testRe
  { local($value,$curF,$next) = @_ ;
    return 1 if &testPat($value) ;
    $testRe = $@ ;
    return 0 ;
  }

sub testEx
  { local($value,$curF,$next) = @_ ;
    if ( $value =~ /^\s+$/ )
      { $testEx = 'empty expression' ;
        return 0 ;
      }
    elsif ( ! &testExpr($value) )
      { $testEx = $@ ;
        return 0 ;
      }
    return 1 ;
  }

sub testNames
  { local($value,$curF,$next) = @_ ;
    if ( $value !~ /$namePat/ )
      { $testNames = "name '$value' not alfa-numeric" ;
        return 0 ;
      }
    local($res) ; $res = grep(/^$value$/,@record) ;
    if ( $res == 1 && $value ne $record[$curF] )
      { $testNames = "name '$value' is used" ;
        return 0 ;
      }
    return 1 ;
  }

sub testUpd
  { local($value,$curF,$next) = @_ ;
    return 1 if $pat[$curF] eq '' || $value =~ /$pat[$curF]/ ;
    $testUpd = "$name[$curF] doesn\'t match $pat[$curF]" ;
    return 0 ;
  }

sub quitUpd
  { local($next,$curF) = @_ ;
    return $next eq 'KEY_TAB' ;
  }

sub quitUpdAdd
  { local($next,$curF) = @_ ;
    return 1 if $next eq 'KEY_TAB' && $curF == $#name ;
    $mode = ($next eq 'KEY_TAB') ? 'system' : 'user' ;
    return 0 ;
  }

sub retRe
  { local($res) =
      ( grep(/./,@record) == 0 ) ? 'a' : &showChoice(*updateXMenu,'a') ;
    $mode = 'user' if $res eq 'e' ;
    return $res ;
  }

sub retEx
  { local($res) =
      ( grep(/./,@record) == 0 ) ? 'f' : &showChoice(*updateXMenu,'a') ;
    $mode = 'user' if $res eq 'e' ;
    return $res ;
  }

sub retNames
  { local($res) = ( $record eq join("$;",@name) )
                  ? 'f' : &showChoice(*updateXMenu,'a') ;
    return $res ;
  }

sub retPat
  { local($res) ;
    if ( $record eq join("$;",@pat) )
      { $res = 'f' ; }
    else
      { $res = &showChoice(*updateXMenu,'a') ;
        &unshowValues if $res eq 'f' ;
      }
    return $res ;
  }

sub retUpd
  { if ( $data[$curr] eq $record )
      { return 'f' ; }
    else
      { local($res) = &showChoice(*updateXMenu,'a') ;
        &unshowValues if $res eq 'f' ;
        return $res ;
      }
  }

sub retUpdAdd
  { local($res) = &showChoice(*updateXMenu,'a') ;
    &unshowValues if $res eq 'f' ;
    $mode = 'user' if $res eq 'e' ;
    return $res ;
  }

sub doMetaMenu
{ local($c,$command,$Yd,$Xd,$Yr,$Xr,$curr,$maxStrlenD,$str,$res,$found) ;
  $c = ($#name < 0) ? 'o' : 'i' ;
  $command = $metaMenu{$c} ;

  &unshowData ;

  while( 1 )
    { $found = 1 ;

      &showJinxD() ;
      &showData(*data) ;
      $c = &showChoice(*metaMenu,$c) ;
      $command = $metaMenu{$c} ;

      if ( $c eq '' || $c eq "Q" )
        { &quit('no update') if $dirty ;
          &quit('normal quit') ;
        }
      elsif ( $c eq "q" && &checkDirty )
        { &quit('jinx says: bye bye') ; }
      elsif ( $c eq "p" && $curr > 0 )
        { $curr = &max(0,$curr-$numFieldLines) ; }
      elsif ( $c eq "n" && $curr < $#data )
        { $curr = &min($#data, $curr + $numFieldLines) ; }
      elsif ( $c eq 'KEY_UP' && $curr > 0 )
        { $curr-- ; }
      elsif ( $c eq 'KEY_DOWN' && $curr < $#data )
        { $curr++ ; }
      elsif ( $c eq 'KEY_LEFT' && $Xd > 0 )
        { $Xd = &max($Xd-1-int(($numInfoCols-2)/3),0) ; }
      elsif ( $c eq 'KEY_RIGHT' && $Xd < $maxXd )
        { $Xd = &min($Xd+1+int(($numInfoCols-2)/3),$maxXd) ; }
      elsif ( $c eq "g" && $#name >= 0 && $#data >= 0 &&
              ( $res = &getGoto($curr) ) ne '' &&
              0 <= $res && $res <= $#data )
        { $curr = $res ;
          $Xd = 0 ;
        }
      elsif ( $c eq "/" && $#name >= 0 && $#data >= 0)
        { ($res,$sPat) = &getStrFrom('pattern:',*sPat,$#sPat) ;
          if ( $res == 0 || $sPat eq '' )
	    { &showStatus('no change') ; }
          elsif ( ! &testPat($sPat) )
	    { &showStatus($@) ; }
	  else
	    { ($curr,$res) = &searchNext(*data,$curr,$sPat) ;
              &showStatus($res) ;
	    }
        }
      elsif ( $c eq ";" && $#name >= 0 && $#data >= 0 && $#sPat >= 0 )
        { $sPat = $sPat[$#sPat] ;
          if ( ! &testPat($sPat) )
	    { &showStatus($@) ; }
	  else
	    { ($curr,$res) = &searchNext(*data,$curr,$sPat) ;
              &showStatus($res) ;
	    }
        }
      elsif ( $c eq "i" && $#name >= 0 )
        { $curr = &doMainMenu(*name,*pat,*data,*ddata,$curr,*mainUpdate) ;
          if ( $curr eq 'Q' )
            { if ( $dirty ) 
                { &quit('quit without update') ; }
              else
                { &quit("normal quit ; database wasn't modified") ; }
            }
          $curr = &min($#data,$curr) ;
          $Xd = 0 ;
        }
      elsif ( $c eq "o" && &checkDirty )
        { ($res,$str) = &getStrFrom('open:',*openAs,$currDb) ;
          if ( $res == 0 )
            { &showStatus('no change') ; }
          else
            { $currDb++ ;
              &showStatus('opening ...') ;
              ($res,@res) = &openCurrDb($str) ;
              &showStatusList(@res) ;
              if ( $res )
                { push(@saveAs,$str)
		    if $#saveAs < 0 || $saveAs[$#saveAs] ne $str ;
		  $curr = &min($#data,0) ;
                  $Xd = 0 ;
                  $dirty = 0 ;
                  @ddata = () ;
                  &unshowData ;
                }
            }
        }
      elsif ( $c eq "C" && $#name >= 0 && &checkDirty )
        { &showStatus("database wasn't modified") if ! $dirty ;
          $dirty = 0 ;
          $db = '' ;
          @data = () ;
          @ddata = () ;
          @descr = () ;
          @name = () ;
          @pat = () ;
          $curr = 0 ;
          &unshowData ;
        }
      else { $found = 0 ; }

      if ( $found )
        { ; }
      elsif ( $c eq "s" && $#name >= 0)
        { if ( $db eq '' )
            { &showStatus('no name ; Save as ...') ; }
          elsif ( ! $dirty)
            { &showStatus('no modification since last update') ; }
          else
            { &showStatus('saving ...') ;
              ($res,$resStr) = &putInfo(*data,$db,'dat') ;
              &showStatus($resStr) ;
              $dirty = 0 if $res ;
            }
        }
      elsif ( $c eq "S" && $#name >= 0)
        { push(@saveAs,'') ;
          ($res,$str) = &getStrFrom('Save as:',*saveAs) ;
          if ( $res == 0 || $str eq '')
            { &showStatus('not saved') ; }
          else
            { if ( ! -e "$str.des" ||
                   do { &showStatusBeep("database $str exists") ;
                        &showChoice(*dirtyMenu,'n','write anyway ?') eq 'y' ;
                      }
                 )
               { &showStatus('Saving ...') ;
                 ($resDescr,$resStr1) = &putInfo(*descr,$str,'des') ;
                 if ( ! $resDescr )
                   { &showStatus($resStr1) ; }
		 else
		   { ($resData, $resStr2) = &putInfo(*data,$str,'dat') ;
                     &showStatus("$resStr1; $resStr2") ;
                     if ( $db eq '' && $resData )
                       { $db = $str ;
                         $dirty = 0 ;
                       }
		   }
               }
             else
               { &showStatus("no change") ; }
            }
        }
      elsif ( $c eq "O" && $#name >= 0 && $#data >= 0 )
        { &showStatus('select sort keys (or quit to sort whole records)') ;
	  ($res,@record) = &getNames(*name) ;
          if ( $res )
            { &showStatus('sorting ...') ;
              ($res,@errors) = &doSort(*data,*record) ;
              &showStatusList(@errors) ;
              do { $curr = 0 ; $dirty++ ; } if $res ;
            }
          else
            { &showStatus('no change') ; }
        }
      elsif ( $c eq "P" && $#name >= 0 && &checkDirty )
        { &showStatus('select columns to project') ;
	  ($res,@record) = &getNames(*name) ;
          if ( $res )
            { &showStatus('Projecting ...') ;
              ($res,@errors) = &doProject(*descr,*name,*pat,*data,*record) ;
              &showStatusList(@errors) ;
                if ( $res )
                  { &doProjectData(*ddata,*record) ;
		    $dirty = 1 ;
                    $curr = &min($#data,0) ;
                    $Xd = 0 ;
                    $db = '' ;
                  }
            }
          else
            { &showStatus('no change') ; }
        }
      elsif ( $c eq "D" && $#name >= 0 )
        { @record = @pat ;
          $Xr = 0 ;
          $curF = 0 ;
          &showRecord(*name,*record,$#data) ;
          @descr1 = @descr ;
          if ( &doDescrMenu(*descr1) &&
               join("$;",sort @descr) ne join("$;",sort @descr1) )
            { @descr = @descr1 ;
              &splitDescr(*descr,*name,*pat) ;
              $db = '' ;
            }
          else
            { &showStatus('no change') ; }
        }
      elsif ( $c eq "J" && $#name >= 0 && &checkDirty )
        { ($res,$str) = &getStrFrom('with what?',*joinList,$#joinList) ;
          if ( $res == 0 )
            { &showStatus('no change') ; }
          else
            { &showStatus('opening ...') ; 
              ($res,@errors) = &openDb($str,*descr2,*data2,*name2,*pat2) ;
              &showStatusList(@errors) ;
              if ( $res == 0 )
                { if ( $Xd )
                    { $Xd = 0 ;
                      &unshowData ;
                    }
                  &showStatus('Joining ...') ;
                  ($res,@errors) = &doJoin(*descr,*data,*descr2,*data2,'','') ;
                  &showStatusList(@errors) ;
                  if ( $res )
                    { &splitDescr(*descr,*name,*pat) ;
                      $dirty = 1 ;
                      @ddata = () ;
                      $curr = &min($#data,0) ;
                      $db = '' ;
                      &unshowData ;
                    }
                }
            }
        }
      elsif ( $c eq "G" && &checkDirty )
        { if ( do { ($res,$str) =
                       &getStrFrom('Guess what?',*guessList,$#guessList) ;
                    $res == 0 || $str eq '' ;
                  } 
             )
            { &showStatus('no change') ; }
          elsif ( ! -r $str )
            { &showStatus("can't read $str") ; }
          elsif ( do { ($res,$pat) =
                          &getStrFrom('separator?',*sepList,$#sepList) ;
                        $res == 0 ;
                     } 
                )
            { &showStatus('no change') ; }
          else
            { &showStatus('Guessing ...') ;
              ($res,$errors) = &doGuessData($str,$pat,*data1) ;
              &showStatus($errors) ;
              if ( $res != 0 )
                { @data = @data1 ;
                  &doGuessDescr($res,*descr) ;
                  &splitDescr(*descr,*name,*pat) ;
                  $dirty = 1 ;
                  @ddata = () ;
                  $curr = &min($#data,0) ;
                  $Xd = 0 ;
                  $db = '' ;
                  &unshowData ;
                }
            }
        }
      elsif ( $c eq "A" && &checkDirty )
        { ($fName,$fPat,$fVal) = &getFieldDescr ;
          if ( $fName ne '' )
            { &showStatus('Adding ...') ;
              &addFieldNP(*descr,*name,$fName,*pat,$fPat) ;
              for $data ( @data )  { $data .= $; . $fVal ; }
              for $data ( @ddata ) { $data .= $; . $fVal ; }
              $db = '' ;
              $dirty++ ;
              &unshowData ;
            }
          else
            { &showStatusBeep($fPat) ; }
        }
      elsif ( $c eq "T" && $#data >= 0 )
        { &showStatus('Testing All ...') ;
          $curr = &showTestAll(*name,*pat,*data) ;
        }
      elsif ( $checkDirty )
        { $checkDirty = 0 ; }
      else
        { &showStatusBeep("can't do it") ; }
    }
}

sub mainUpdate
  { local($c) = @_ ;
    if ( $c eq 'u' )
      { &doUpdate(*updMatchMenu,'user',1,*testUpd,*quitUpd,*retUpd) ; }
    else
      { &doUpdate(*updMatchMenu,'user',0,*testUpd,*quitUpdAdd,*retUpdAdd) ; }
  }

sub doMainMenu
{ local(*name,*pat,*data,*ddata,$curr,*onUpdate) = @_ ;

  local($c,$Yr,$Xr,$maxStrlenR,$curF,@ndata,$found) ;

  $curr = -1 if $#data < 0 ;
  $c = ( $#data < 0 ) ? 'a' : 'n' ;
  $Xr = 0 ;
  &unshowRecord ;

  while ( 1 )
    { $found = 1 ;
      if ($curr >= 0)
        { @record = split(/$;/,$data[$curr],$#name+1) ; }
      else
        { @record = &emptyRecord($#name) ; }
      &showJinxD() ;
      &showRecord(*name,*record,$#data) ;
      $c = ( $#data == -1 ) ? 'a' : 'n' if $c eq 'd' ;
      $c = &showChoice(*mainMenu,$c)  ;

      if ( $c eq '' || $c eq "Q" )
        { return('Q') ; }
      elsif ( $c eq "q" )
        { return($curr) ; }
      elsif ( $c eq "g" && $#data >= 0 &&
              ($res = &getGoto($curr)) ne '' &&
              0 <= $res && $res <= $#data )
        { do { $curr = $res ; &unshowValues ; } if $res != $curr ;
          $Xr = 0 ;
        }
      elsif ( $c eq 'KEY_UP' && $curF > 0 )
        { $curF-- ; }
      elsif ( $c eq 'KEY_DOWN' && $curF < $#name )
        { $curF++ ; }
      elsif ( $c eq 'KEY_LEFT' && $Xr > 0 )
        { $Xr = &max($Xr-1-int(($numValueCols-2)/3),0) ; }
      elsif ( $c eq 'KEY_RIGHT' && $Xr < $maxXr )
        { $Xr = &min($Xr+1+int(($numValueCols-2)/3),$maxXr) ; }
      elsif ( $c eq "n" && $curr < $#data )
        { $curr++ ;
          &unshowValues ;
        }
      elsif ( $c eq "p" && $curr > 0)
        { $curr-- ;
          &unshowValues ;
        }
      elsif ( $c eq "/" && $curr >= 0 )
        { ($res,$sPat) = &getStrFrom('pattern:',*sPat,$#sPat) ;
          if ( $res == 0 || $sPat eq '' )
	    { &showStatus('no change') ; }
          elsif ( ! &testPat($sPat) )
	    { &showStatus($@) ; }
	  else
	    { ($curr,$curF,$res) =
		&searchNextRecord(*data,$curr,*record,$curF,$sPat) ;
              &showStatus($res) ;
              &unshowValues if $res eq 'found' ;
	    }
        }
      elsif ( $c eq ";" && $curr >= 0 && $#sPat >= 0 )
        { $sPat = $sPat[$#sPat] ;
          if ( ! &testPat($sPat) )
	    { &showStatus($@) ; }
	  else
	    { ($curr,$curF,$res) =
		&searchNextRecord(*data,$curr,*record,$curF,$sPat) ;
              &showStatus($res) ;
              &unshowValues if $res eq 'found' ;
	    }
        }
      else
        { $found = 0 ; }
      
      if ( $found )
        { ; }
      elsif ( $c eq "u" && $curr >= 0 )
        { $Xr = 0 ;
          &showRecord(*name,*record,$#data) ;
          if ( &onUpdate($c) eq 'a' )
            { $data[$curr] = $record ;
              $dirty++ ;
            }
          else
            { &showStatus('no change') ; }
        }
      elsif ( $c eq "a" || ( $c eq "c" && $curr >= 0 ) )
        { if ( $c eq "a" )
            { @record = &emptyRecord($#name) ;
              &unshowValues ;
            }
          $Xr = 0 ;
          $oldF = $curF ;
          $curF = 0 ;
          $curr++ ;
          &showRecord(*name,*record,$#data) ;
          if ( &onUpdate($c) eq 'a' )
            { splice(@data,$curr,0,join($;,@record)) ;
              $dirty++ ;
            }
          else
            { &showStatus('no change') ;
              $curr-- ;
            }
          $curF = $oldF ;
        }
      elsif ( $c eq "d" && $curr >= 0 )
        { push(@ddata,splice(@data,$curr,1)) ;
          &showStatus('deleted') ;
          $curr = &min($curr,$#data) ;
          $dirty++ ;
          &unshowValues ;
        }
      elsif ( $c eq "t" && $curr >= 0 )
        { &showTest(*name,*pat,*record) ; }
      elsif ( $c eq "T" && $curr >= 0 )
        { &showStatus('Testing All ...') ;
          $curr = &showTestAll(*name,*pat,*data) ;
          &unshowValues ;
        }
      elsif ( $c eq "O" && $#data >= 0 )
        { &showStatus('select sort keys (or quit to sort whole records)') ;
          ($res,@record) = &getNames(*name) ;
          if ( $res )
            { &showStatus('sorting ...') ;
              &unshowValues ;
              ($res,@errors) = &doSort(*data,*record) ;
              &showStatusList(@errors) ;
              do { $curr = 0 ; $dirty++ ; } if $res ;
            }
          else
            { &showStatus('no change') ; }
          &unshowValues ;
        }
      elsif ( $c eq "U" && $#ddata >= 0 )
        { @vals = 0..$#ddata ;
          ($res,@vals) = &selectFrom('deleted records',*name,*ddata,@vals) ;
          if ( $res eq 'q' )
            { &showStatus("no change") ; }
          elsif ( $res eq 'a' )
            { if ( $#vals >= 0 )
                { $dirty++ ;
                  for $vals ( reverse sort byNum @vals )
                    { push(@data,splice(@ddata,$vals,1)) ; } 
                  &showStatus('undeleted ', $#vals+1) ;
                }
              else
                { &showStatus("no change") ; }
            }
          &unshowValues ;
        }
      elsif ( $c eq "E" && $#data >= 0 )
        { @vals = 0..$#data ;
          ($res,@vals) = &selectFrom('records to extract',*name,*data,@vals) ;
          if ( $res eq 'q' )
            { &showStatus("no change") ; }
          elsif ( $res eq 'a' )
            { if ( $#vals >= 0 )
                { $dirty++ ;
                  $db = '' ;
                  @ndata = () ;
                  for $vals ( reverse sort byNum @vals )
                    { push(@ndata,splice(@data,$vals,1)) ; } 
                  $curr = &min(0,$#data) ;
                  @ddata = (@ddata,@data) ;
                  @data = @ndata ;
                  &showStatus('extracted ', $#vals+1) ;
                }
              else
                { &showStatus("no change") ; }
            }
          &unshowValues ;
        }
      elsif ( $c eq "D" && $#data >= 0 )
        { @vals = 0..$#data ;
          ($res,@vals) = &selectFrom('records to delete',*name,*data,@vals) ;
          if ( $res eq 'q' )
            { &showStatus("no change") ; }
          elsif ( $res eq 'a' )
            { if ( $#vals >= 0 )
                { $dirty++ ;
                  $curr = &min(0,$#data) ;
                  for $vals ( reverse sort byNum @vals )
                    { push(@ddata,splice(@data,$vals,1)) ; } 
                  &showStatus('deleted ', $#vals+1) ;
                }
              else
                { &showStatus("no change") ; }
            }
          &unshowValues ;
        }
      elsif ( $c eq "P" )
        { ($res,$str) = &getStrFrom('at ?',*joinList,$#joinList) ;
          if ( $res == 0 )
            { &showStatus('no change') ; }
          else
            { &showStatus('opening ...') ; 
              ($res,@errors) = &openDb($str,*descr1,*data1,*name1,*pat1) ;
              &showStatusList(@errors) ;
              if ( $res == 0 )
                { &showStatus('Peeking ...') ;
                  ($res,@errors) = &doPeek(*name,$str,*name1,*data1) ;
                  &showStatusList(@errors) ;
                  $dirty++ if $res ;
                  &unshowRecord ;
                }
            }
        }
      elsif ( $c eq "R" )
        { ($res,$str) = &getStrFrom('what ?',*joinList,$#joinList) ;
          if ( $res == 0 )
            { &showStatus('no change') ; }
          else
            { &showStatus('opening ...') ; 
              ($res,@errors) = &openDb($str,*descr1,*data1,*name1,*pat1) ;
              &showStatusList(@errors) ;
              if ( $res == 0 )
                { &showStatus('Reading ...') ;
                  ($res,@errors) = &doRead(*name,$str,*name1,*data1) ;
                  &showStatusList(@errors) ;
                  $dirty++ if $res ;
                  &unshowRecord ;
                }
            }
        }
      elsif ( $c eq "K" && $#data >= 0 )
        { &showStatus('select key fields (or quit to uniq records)') ;
          ($res,@record) = &getNames(*name) ;
          if ( $res )
            { $res1 = ( $#record < 0 )
		? 'record' : 'key (' . join(',',@name[@record]) . ')' ;
	      &showStatus("Checking if every $res1 is uniq ...") ;
              ($res,@vals) = &doKeyTest(*data,*name,*record) ;
	      if ( $res && $#vals >= 0 )
                { $dirty++ ;
                  $curr = &min(0,$#data) ;
                  for $vals ( reverse sort byNum @vals )
                    { push(@ddata,splice(@data,$vals,1)) ; } 
                  &showStatus('deleted: ',$#vals+1,' record',$#vals?'s':'') ;
                }
	      elsif ( $res == 1 )
                { &showStatus("every $res1 is unique") ; }
	      elsif ( $res == 2 )
                { &showStatus("one $res1 was not uniq") ; }
	      elsif ( $res )
                { &showStatus("$res1 was not uniq in ",$res-1,' cases') ; }
              else
                { &showStatus("no change") ; }
            }
          else
            { &unshowValues ;
	      &showStatus("no change") ;
	    }
        }
      elsif ( $c eq "C" && $#data >= 0 )
        { @record = () ;
	  for ( @name ) { push(@record,$fieldExpr{$_}) ; }
	  $Xr = 0 ;
	  $curF = 0 ;
          &unshowValues ;
          &showRecord(*name,*record,$#data) ;
          $res = &doUpdate(*updReMenu,'user',1,*testEx,*quitUpd,*retEx) ;
          if ( $res eq 'a' )
            { $i = 0 ;
	      for ( @name ) { $fieldExpr{$_} = $record[$i] if $record[$i] ; }
	      continue { $i++ ; }
	      &showStatus("Computing ...") ;
	      $res = &doCompute(*name,*record,*data,*ddata) ;
              $dirty++ ;
	      &showStatus($res) ;
	    }
          else
            { &showStatus('no change') ; }
	  &unshowValues ;
	}
      else
        { &showStatusBeep("can't do it") ; }
    }
}

unshift(@INC,pop(@INC)) ;
do 'jinx.pl'   || die "can't include jinx.pl\n" ;
do 'curses.pl' || die "can't include curses.pl\n" ;
do 'cterm.pl'  || die "can't include cterm.pl\n" ;
do 'screen.pl' || die "can't include screen.pl\n" ;
do 'menus.pl'  || die "can't include menus.pl\n" ;

$inJinx = 1 ;

if ( $#ARGV == 0 && $ARGV[0] eq '-D' )
  { close(STDIN) ;
    close(STDOUT) ;
    close(STDERR) ;
    $dumped = 1 ;
    dump startHere ;
  }

startHere: ;

if ( $dumped )
  { open(TTY,'>/dev/tty') ;
    open(STDIN, '<&0') || do { print TTY "can't reopen STDIN"  ; exit ; } ;
    open(STDOUT,'>&1') || do { print TTY "can't reopen STDOUT" ; exit ; } ;
    open(STDERR,'>&2') || do { print TTY "can't reopen STDERR" ; exit ; } ;
    close(TTY) ;
  }

@ctermArgs = () ; $logLevel = -1 ;
while ( $#ARGV >= 0 && $ARGV[0] =~ /^-/ )
  { $OPT = shift ;
    $OPT =~ s/^.// ;
    if ( $OPT =~ /^L(\d+)?$/ )
      { $logLevel = $1 ;
        $logFile = shift ;
        push(@ctermArgs,$logFile,$logLevel) if $logFile ;
      }
    elsif ( $OPT eq 'D' )
      { ; }
    elsif ( $OPT eq 'v' )
      { print STDERR "Jinx version 2.0, " ;
        print STDERR "copyright (c) 1990, Henk P. Penning.\n" ;
	print STDERR "Jinx may be copied only under the terms " ;
	print STDERR "of the GNU General Public License,\na copy " ;
	print STDERR "of which comes with the Jinx distribution kit.\n" ;
	exit(0) ;
      }
    else
      { push(@errors,"$0: unknown option '$OPT'") ; }
  }

for ( @errors )
  { print STDERR "$_\n" ; }
die "Usage: $0 [-L[level] log-file] [ db ... ]\n" if $#errors >= 0 ;

($res,@errors) = &testCurses ;

if ( ! $res )
  { print STDERR join("\n",@errors), "\n" ;
    exit ;
  }

&startCterm('-X',@ctermArgs) ;

&initCurses ;
&initEdit($curcon{'KEY_UP'}, $curcon{'KEY_DOWN'},
          $curcon{'KEY_RET'}, $curcon{'KEY_TAB'}) ;
$flushOn = 1 ;

&addlog("open jinx:") ;

@ddata = () ;
@descr1 = () ;
@data1 = () ;
@descr2 = () ;
@data2 = () ;
@name1 = () ;
@name2 = () ;
@pat = () ;
@pat1 = () ;
@pat2 = () ;
%fieldExpr = () ;

&initMenus ;
&initParamsScreen ;

if ( $#ARGV < 0 )
  { @openAs = () ;
    @descr = () ;
    @name = () ;
    @pat = () ;
  }
else
  { @openAs = (@ARGV,'') ;
    $str = $ARGV[$currDb++] ;
    ($res,@res) = &openCurrDb($str) ;
    &showStatusList(@res) ;
  }

&addlog("logfile: " . ( $logFile ? "$logFile, level $logLevel" : 'none') ) ;

&QUIT('Get a wider terminal.') if $numInfoCols < 6 ;
&QUIT('Get a larger terminal.') if $numFieldLines < 1 ;

&doMetaMenu ;

&quit() ;
