#! /usr/bin/perl -w

# perl code to turn an SGF file into TeX diagrams
#   Copyright (C) 1997-2005 Reid Augustin reid@hellosix.com
#                      1000 San Mateo Dr.
#                      Menlo Park, CA 94025 USA
#
#   This program is free software; you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation; either version 2 of the License, or
#   (at your option) any later version.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with this program; if not, write to the Free Software
#   Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111, USA

# todo: 

=head1 NAME

sgf2dg - convert Smart Go Format (SGF) files to diagrams similar to
those seen in Go books and magazines.

=head1 SYNOPSIS

sgf2dg [ option ... ] file[.sgf|.mgt]

=head1 DESCRIPTION

B<sgf2dg> takes a Smart Go Format (SGF) file I<filename> or
I<filename>.sgf or I<filename>.mgt and produces a diagram file
I<filename>.suffix where suffix is determined by the B<converter>
(see below).

The default B<converter> is Dg2TeX which converts the Diagram to TeX
source code (sgf2dg is a superset replacement for the sgf2tex script
and package).  If you have the GOOE fonts (provided in the same
package as sgf2dg) correctly installed on your system you will be
able to TeX I<filename>.tex to produce a .dvi file.  You can of
course embed all or parts of I<filename>.tex into other TeX
documents.

=cut

use strict;
require 5.001;
use IO::File;
use Games::Go::Diagram; # the go diagram module

BEGIN {
    our $VERSION = sprintf "4.%03d", '$Revision: 144 $' =~ /(\d+)/;
}

use constant a_MINUS_1 => ord('a') - 1; # base of SGF coordinates is 'a'

my $version = sprintf "version 4.%03d", '$Revision: 144 $' =~ /(\d+)/;

my $myName = $0;             # full pathname of this file
$myName =~ s".*/"";          # delete any preceding path

$version = "(sgf2dg) $version" if($myName ne 'sgf2dg');

my $commandLine = "$0 " . join(' ', @ARGV);

my $help = <<HELP_END;
$myName [options] [file.sgf]

    -h | -help                print this message and exit
    -v | -version             print version number and exit
    -i | -in                  input file name (STDIN for standard input)
    -o | -out                 output file name (STDOUT for standard output)
    -t | -top                 top line in diagram
    -b | -bottom              bottom line in diagram
    -l | -left                leftmost line in diagram
    -r | -right               rightmost line in diagram
    -break | -breakList       a list of first move in each diagram
    -m | -movesPerDiagram     number of moves per diagram
    -d | -doubleDigits        label stones modulo 100
    -n | -newNumbers          begin each diagram with number 1
    -rv | -relativeVarNums    start each variation from 1
    -av | -absoluteVarNums    move numbers in variation == main line numbers
    -cv | -correlativeVarNums start main variations from 1 
    -rl | -repeatLast         repeat last move as first in next diagram
    -il | -ignoreLetters      ignore SGF letters
    -im | -ignoreMarks        ignore SGF marks
    -iv | -ignoreVariations   ignore SGF variations
    -ip | -ignorePass         ignore SGF pass moves
    -ia | -ignoreAll          ignore SGF letters, marks, variations, and passes
    -firstDiagram             first diagram to print
    -lastDiagram              last diagram to print
    -longComments             allow page breaks in comments
    -simple                   use a very simple TeX format
    -coords                   print coordinates
    -twoColumn                use two-column format
    -bigFonts                 use fonts magnified 1.2 times
    -texComments              \\, { and } in comments not modified
    -gap                      gap in points between diagrams (default: 12)
    -converter | -convert     name of a Diagram converter (see below)
    -verbose                  print diagnostic information about move

    The -i and -o options are not needed with normal usage:
             $myName [options] name
 is equivalent to:
             $myName [options] -i name -o name.tex 
 or          $myName [options] -i name.sgf -o name.tex
 and         $myName [options] name.

 for those of you who are fans of tab completion.

    The breakList consists of a comma-separated list of numbers (NO
 spaces). Each number will be the last move in one diagram.
 -movesPerDiagram sets an upper limit on the number of moves per
 diagram. The default movesPerDiagram is 50 unless a breakList
 (without -movesPerDiagram) is set, in which case movesPerDiagram is
 set to a very large number. -breakList and -movesPerDiagram may be
 combined.

    -doubleDigits and -newNumbers are alternative schemes for avoiding
 large numerals.  -doubleDigits limits stone numbers to be between 1
 and 100.  Stone number 101 prints as 1.  -newNumbers causes each
 diagram to start with number 1.

    If you use -doubleDigits and -repeatLast together, you'll get
 warnings because there is no font character for stones numbered 0.
 The diagrams with 100, 200, 300, etc. as the first move will
 complain, and those stones will show with their real numbers.

    By default, variation diagrams start with stone number 1
 (-relativeVarNums).  Alternatively, variation numbers can be the
 same as the numbers in the main diagram (-absoluteVarNums) or they
 can start from 1 at the beginning of each variation tree
 (-correlativeVarNums).

    -longComments implements more elaborate TeX to allow page breaks
 in the comments. Use this if comments are very extensive. It cannot
 be used with -simple or -twoColumn.

    -simple uses a very simple TeX format. This option may be useful
 if you intend to edit the resulting TeX file by hand. -longComments
 and -simple should not be used together.

    -twoColumn uses a two-column format with small fonts. This cannot 
 be used with -coords or -longComments.

    -coords prints a coordinate grid. It cannot be used with
 -twoColumn.

    -texComments is appropriate if your sgf comments contain TeX.
 If this option is NOT used, \\ { and } are replaced by /, [ and ]
 since these characters are not available in TeX roman fonts. If
 this -texComments is used, this change is not made so you can put
 code to {\\bf change fonts} in your comments.

    -converter changes the output converter.  The default converter
 is Games::Go::Dg2Tex.  All converters get 'Games::Go::Dg2'
 prepended, so you should enter only the part after Dg2.  The
 default is thus equivilent to '-converter TeX'.  Converters
 supplied with this release are (case sensitive):

    TeX       - Donald Knuth's typesetting language
    Mp        - MetaPost embedded in TeX (Encapsulated PostScript)
    ASCII     - ASCII art
    PDF       - Portable Document Format
    Ps        - PostScript
    Tk        - perl/Tk NoteBook window
    TkPs      - PostScript from the Tk NoteBook

    Example:    \$ $myName ... -converter ASCII ...

    See the perldoc or man pages for details on converter-specific
 options (eg: 'perldoc Games::Go::Dg2TeX' or 'man Games::Go::Dg2PDF').

    More details on $myName can be found in the perldoc or man
 pages: 'perldoc $myName' or 'man $myName'.

HELP_END

# some global variables (we'll need to localize some of them)
our ($diagram,          # current Diagram
     $variationDepth,   # how many levels deep in variations (0 == main line)
     $removedCount,     # number of stones removed at start of diagram
     $moveNum,          # move number
     $currentLetter,    # for adding a bunch of letters in sequence
     @lastMove1,        # copy of the last move in case repeatLast is true
     @lastMove2,        # copy of the last move in case repeatLast is true
     );

$variationDepth = 0;
$moveNum = 0;

my $rootDiagram;
my $diagramId = 0;
my $diagramNum = 1;     # current diagram number
my $variationNum = 0;   # current variation number
my @parentDiagram;      # list of refs to parent diagrams (for variations)
# other globals
my %option;             # command line options
my @nodePlays;          # save plays in each node to check
                        #     for captures after moves are finalized

my @ungotten;           # this array will hold 'ungotten' chars from $fileID
my $gotten = '';        # this is helpful when debugging to see how much of the file has been read

# code to handle sgf file formats

sub getC {
    my ($fileID) = @_;
    my ($chr);

    if (scalar(@ungotten)) {
        $chr = shift(@ungotten);
    } else {
        $chr = getc($fileID);
        if (defined($chr)) {
            $gotten .= $chr;
        } else {
            $chr = '';
        }
    }
    return($chr);
}

sub ungetC {
    my ($fileID, $chr) = @_;

    unshift(@ungotten, $chr);
}

sub skipToToken {
    my ($fileID) = @_;
    my ($chr);

    for(;;) {
        $chr = getC($fileID);
        if ((not(defined($chr)) or 
             ($chr eq '')) and
            eof($fileID)) {
            return '';
        }
        if ($chr =~ m/\S/) {
            return($chr);       # non-blank
        }
    }
}

sub printVerbose {
    my (@msg) = @_;

    printIndent(@msg) if ($option{verbose});
}

sub printIndent {
    my (@msg) = @_;

    print(STDERR ' ' x $variationDepth);
    print(STDERR @msg);
}

sub SGF_ReadFile {
    my ($fileID) = @_;
    my ($chr, $start);

    my $prevChr = "\n";
    for(;;) {
        $chr = getC($fileID);                   # collect chars til we see '(' as first char on a line
                                                # this allows emails and news postings to be read - the
                                                # pre-amble will get skipped here.
        if (($prevChr eq "\n") and
            ($chr eq '(')) {
            SGF_ProcessVariation ($fileID);     # got the first line of the SGF part
            return;
        } elsif (($chr eq '') and eof($fileID)) {
            die("Couldn't find the start of the SGF part (no \"(\" as first character on a line).\n");
        }
        $prevChr = $chr;
    }
}

# SGF nesting: (m1 (m2 (m3)(v3 of m3 in m2)(v4 of m3 in m2))(v1m of m2 in m1 (v2m)(v1.1 of v2m in v1m))(v2 of m2 in m1))
# So:  'non-)(' means spawn variation group off main line here (but main line continues)
#      ')('     means variation starting here
#      '))'     means end of variation group here

sub SGF_ProcessVariation {
    my ($fileID) = @_;

# printIndent("SGF_ProcessVariation depth is $variationDepth\n");
    my $prevChr = '(';          # we start off inside the first '('
    my $nestCount = 1;          # we're already inside first '('
    for ( ; ; ) {
        my $chr = skipToToken($fileID);
        if ($chr eq '') {
            die("SGF_ProcessVariation: end of file without closing \")\".\n");
        } elsif ($chr eq '(') {
            if ($prevChr eq ')') {     # new variation
                return if ($option{ignoreVariations});           # done with main line
                $variationDepth++;
                local $diagram = $parentDiagram[0]->next;       # each variation in this group starts from this parent
                local $removedCount = $removedCount;
                local $moveNum = $parentDiagram[0]->var_on_move;
                local @lastMove2;
                local @lastMove1;
                printIndent("Parsing Variation on move $moveNum\n");
                # link back to parent which is 2 levels up:
                my $parent = $parentDiagram[0]->parent;
                # add to parent's variation list.  Note: variation
                # will be on next move (unless stones are removed)
                push(@{$parent->user->{variations}{$moveNum+1}}, $diagram);
                $diagram->var_on_move($moveNum + 1);
                $diagram->parent($parent),
                $diagram->user({id => $diagramId++});
                if (($option{varNumbersFlag} eq 'relative') or          # each variation starts at 1
                    ($option{varNumbersFlag} eq 'correlative' and       # each root variation starts at 1
                    ($variationDepth <= 1))) {
                    $moveNum = 0;
                }
                SGF_ProcessVariation($fileID);
                $variationDepth--;
                $chr = ')';                     # done with variation
                # and now continue with the main line
            } elsif (not $option{ignoreVariations}) {
                $nestCount++;
                # create a new diagram from starting from the
                # current position.  this new diagram is really a
                # place holder since we have to spawn another level
                # of diagrams when we start parsing the variations
                unshift (@parentDiagram, $diagram->next);       # put a fresh diagram on the parent stack
                $parentDiagram[0]->parent($diagram);            # point back to parent
                $parentDiagram[0]->var_on_move($moveNum);       # and remember move number
                                                                #    where variation was spawned
                $parentDiagram[0]->user({id => $diagramId++});
                printIndent("Creating Variation(s) at move $moveNum\n");
            }
        } elsif ($chr eq ')') {                 # end of a variation
            $nestCount--;
            if ($prevChr eq ')') {
                shift(@parentDiagram);                  # done with this group of variations
            }
            if ($nestCount <= 0) {
                printVerbose('SGF_ProcessVariation done: ');
                return;
            }
        } else {                                # a node
            ungetC($fileID, $chr) if ($chr ne ';');     # hmm, missing ';'?
            SGF_ProcessNode($fileID);
            $diagram->node if (defined($diagram));
            while (@nodePlays) {
                my $coords = shift @nodePlays;
                CheckForDeadGroups(SGF2Coords($coords));    # check for captures
            }
        }
        if ((@{$option{breakList}} > 0) and
            ($diagram->last_number >= $option{breakList}[0])) {
            printVerbose('BreakList: ');
            shift(@{$option{breakList}});
            @lastMove2 = @lastMove1;        # use last 1 for repeatLast
            finishDiagram();
        }
        if (($diagram->last_number - $diagram->first_number + 1) >= $option{movesPerDiagram} + $option{repeatLast}) {
            printVerbose('movesPerDiagram: ');
            @lastMove2 = @lastMove1;        # use last 1 for repeatLast
            finishDiagram();
        }
        if ($option{doubleDigits} and
            ($option{newNumbers} ?
                    ($diagram->last_number - $diagram->first_number >= 100) :
                    (($diagram->first_number != $diagram->last_number) and
                     ($diagram->last_number % 100 == 0)))) {
            printVerbose('doubleDigits: ');
            @lastMove2 = @lastMove1;        # use last 1 for repeatLast
            finishDiagram();
        }
        $prevChr = $chr;
    } 
}

sub SGF_ProcessNode {
    my ($fileID) = shift;
    my ($propVal, @propList, $prop, $p);

    local $currentLetter = 'a';         # start each node with a new set of letters

    # sadly, the properties can be in any order.  for example, you might have
    #    a mark before the stone that is to be marked.  so we'll accumulate
    #    all the properties and put them into some sane order.
    my %props;
    for(;;) {
        my $shortPropID = SGF_GetShortPropID(SGF_GetPropID($fileID));
        last if ($shortPropID eq '');
        while((my $propVal = SGF_GetPropVal($fileID)) ne '') {
            push(@{$props{$shortPropID}}, $propVal);
        }

    }
    foreach my $p (sort { &PropOrder } keys(%props)) {
        SGF_ProcessProperty($p, $props{$p});
    }
}

# specify order in which to process properties:
my %propOrder = ( N  => 0,      # first because it always flushes
                  AE => 15,     # might flush
                  AB => 20,     # might flush
                  AW => 20,     # might flush
                  B  => 25,     # changes $moveNum
                  W  => 25,     # changes $moveNum
                                # marks, letter, comments, etc all after
                  );

sub PropOrder {
    my $aa = $propOrder{$a} || 100;
    my $bb = $propOrder{$b} || 100;
    return ($aa <=> $bb);
}

# called every time stones are added or played
sub SGF_ProcessProperty {
    my ($shortPropID, $propVals) = @_;

    foreach my $propVal (@{$propVals}) {
        if (($shortPropID eq 'W') or
            ($shortPropID eq 'B')) {            # White and Black (coordList)
            $moveNum++;
            if ($propVal eq 'tt') {
                $diagram->property($moveNum, $shortPropID, $propVal) unless($option{ignorePass});
            } else {
                printVerbose("Playing $propVal, color=$shortPropID, move=$moveNum\n");
                my $int = $diagram->get($propVal);
                $diagram->put($propVal, $shortPropID, $moveNum);
                @lastMove2 = @lastMove1;
                @lastMove1 = ($propVal, $shortPropID, $moveNum);
                push(@nodePlays, $propVal);
            }
        } elsif (($shortPropID eq 'AW') or
                 ($shortPropID eq 'AB')) {      # AddWhite and AddBlack (coordList)
            printVerbose("Adding stone at move $moveNum: $propVal\n");
            if ($diagram->last_number) {
                finishDiagram();                 # need a new diagram
            }
            my $color = ($shortPropID eq 'AB') ? 'B' : 'W';
            printVerbose("Adding $color stone to $propVal at move $moveNum\n");
            $diagram->put($propVal, $color);
        } elsif ($shortPropID eq 'C') {         # Comment
            if ($propVal =~ m/\S/m) {           # if nothing but whitespace, delete
                printVerbose("Adding comment at move $moveNum:\n$propVal\n");
                $diagram->property($moveNum, $shortPropID, $propVal);
            }
        } elsif ($shortPropID eq 'N') {         # Name (node name)
            if(defined($propVal) and
               ($propVal ne '')) {              # no name? return
                printVerbose("Adding name at move $moveNum: $propVal\n");
                if ($diagram->last_number) {
                    @lastMove2 = @lastMove1;        # use last 1 for repeatLast
                    finishDiagram();                # node names should be at the start of the diagram
                }
                $diagram->property($moveNum, $shortPropID, $propVal);
            }
        } elsif ($shortPropID eq 'SZ') {
            $option{boardSize} = $propVal;
        } elsif ($shortPropID eq 'AE') {        # AddEmpty
            printVerbose("Adding empty at move $moveNum: $propVal\n");
            if ($diagram->last_number) {
                finishDiagram();                 # need a new diagram
            } else {# if no stones played yet
                # keep track of number of stones removed at the
                # start of the diagram so we can figure out which
                # move in the parent diagram spawned the variation
                $removedCount++;
                $diagram->user->{removedCount} = $removedCount;
            }
            $diagram->remove($propVal);
        } elsif ($shortPropID eq 'HA' and
                 $propVal) {                    # non-zero HAndicap
            foreach(@{hoshi($propVal)}) {
                $diagram->put($_, 'B');
            }
        } elsif ($shortPropID eq 'L') {         # letter
            unless ($option{ignoreLetters}) {
                $diagram->label($propVal, $currentLetter++);
                printVerbose("Adding letter \"$currentLetter\" at $propVal move $moveNum\n");
            }
        } elsif (($shortPropID eq 'M') or ($shortPropID eq 'MA')) {     # mark
            unless ($option{ignoreMarks}) {
                $diagram->mark($propVal);
                printVerbose("Adding mark at $propVal move $moveNum\n");
            }
        } elsif ($shortPropID eq 'LB') {        # label
            unless ($option{ignoreMarks}) {
                my ($item, $coord, $label);
                foreach $item (split(/\s+/, $propVal)) {
                    ($coord, $label) = split(/:/, $item);
                    if (length($coord) < 2) {   # didn't work? hmmm, let's try the other way around
                        ($label, $coord) = split(/:/, $item);
                    }
                    $label =~ s/(.).*/$1/m;     # stupid: labels can now be long!
                    $diagram->label($coord, $label);
                    printVerbose("Adding label \"$label\" at $coord move $moveNum\n");
                }
            }
        } elsif (($shortPropID eq 'VW') or      # VieW
                 ($shortPropID eq 'FF') or      # FileFormat
                 ($shortPropID eq 'GM') or      # GaMe
                 ($shortPropID eq 'KO') or      # KO illegal move??
                 ($shortPropID eq 'PL') or      # PLayer
                 ($shortPropID eq 'CR') or      # CiRcle (duh!)
                 ($shortPropID eq 'WL') or      # WhiteLeft (time)
                 ($shortPropID eq 'BL') or      # BlackLeft (time)
                 ($shortPropID eq 'WS') or      # WhiteSpecies
                 ($shortPropID eq 'BS')) {      # BlackSpecies
            # ignore
        } else {
            $diagram->property($moveNum, $shortPropID, $propVal);       # shrug
        }
    }
}

# a property is an ID followed by value(s)
# the propertyID consists of text
sub SGF_GetPropID {
    my ($fileID) = @_;
    my ($chr, $pos);
    my ($propID) = '';

    $chr = skipToToken($fileID);
    while ($chr ne '') {
        if ($chr =~ m/\w/) {
            $propID .= $chr;
        } else {
            ungetC($fileID, $chr);
            last;
        }
        $chr = getC($fileID);
    }
    return($propID);
}

# a property is an ID followed by value(s)
# proertyVals consists of stuff inside brackets []
sub SGF_GetPropVal {
    my ($fileID) = @_;
    my ($chr, $pos);
    my ($propVal) = '';

    $chr = skipToToken($fileID);
    if ($chr ne '[') {
        ungetC($fileID, $chr);
        return('');              # no more proerty values
    }
    for(;;) {
        $chr = getC($fileID);
        if ($chr eq "\\") {
            $propVal .= getC($fileID);
        } elsif ($chr eq ']') {
            return($propVal);
        } elsif (($chr eq '') and eof($fileID)) {
            print(STDERR "Unterminated property: $propVal\n");
            return($propVal);
        } else {
            $propVal .= $chr;
        }
    }
}

sub SGF_GetShortPropID {
    my ($propID) = @_;

    return('') unless (defined($propID));
    $propID =~ s/[a-z]//g;      # just get rid of all lower case letters
    return($propID);
}

sub SGF2Coords {
    my ($coords) = @_;
    my ($x, $y) = unpack('C2', $coords);

    return($x - a_MINUS_1, $y - a_MINUS_1);
}

sub Coords2SGF {
    my ($x, $y) = @_;

    $x = chr($x + a_MINUS_1);
    $y = chr($y + a_MINUS_1);
    return("$x$y");
}

# returns a ref to list of SGF coords for hoshi (or handicap) points
sub hoshi {
    my ($num) = @_;

    my %hoshiTable = (21 => [4, 11, 18],
                      19 => [4, 10, 16],
                      17 => [4, 9,  14],
                      15 => [4, 8,  12],
                      13 => [4, 7,  10],
                      11 => [4, 5,  8 ],
                       9 => [3, 5,  7 ],
                       7 => [2, 4,  6 ],
                       5 => [2, 3,  4 ],
                       );

    unless(defined($num)) {
        # 11x11 and smaller get 5 hoshi points
        $num = ($option{boardSize} > 11) ? 9 : 5;
    }
    my ($a, $b, $c);
    if (exists($hoshiTable{$option{boardSize}})) {
        ($a, $b, $c) = @{$hoshiTable{$option{boardSize}}};
    } else {
        print(STDERR "I don't know about hoshi/handicaps for boardSize $option{boardSize}\n");
        return;
    }

    my @hoshi;
    push(@hoshi, Coords2SGF($a,$c), Coords2SGF($c,$a)) if ($num >= 2);
    push(@hoshi, Coords2SGF($c,$c))          if ($num >= 3);
    push(@hoshi, Coords2SGF($a,$a))          if ($num >= 4);
    push(@hoshi, Coords2SGF($b,$b))          if (($num == 5) or
                                       ($num == 7) or
                                       ($num == 9));
    push(@hoshi, Coords2SGF($a,$b), Coords2SGF($c,$b)) if ($num >= 6);
    push(@hoshi, Coords2SGF($b,$a), Coords2SGF($b,$c)) if ($num >= 8);
    if (($num > 9) or ($num < 2)) {
        print(STDERR "Handicap is $num - I can only handle 2 through 9.\n");
    }
    return \@hoshi;
}

sub CheckForDeadGroups {
    my ($x, $y) = @_;

    my $color = $diagram->game_stone(Coords2SGF($x, $y));
    return unless(defined($color));
    my $otherColor = ($color eq 'black') ? 'white' : 'black';
    CheckIfDead($x + 1, $y, $otherColor); # first check the four neighboring stones of the other color
    CheckIfDead($x - 1, $y, $otherColor);
    CheckIfDead($x, $y + 1, $otherColor);
    CheckIfDead($x, $y - 1, $otherColor);
    CheckIfDead($x, $y,     $color);      # and finally we need to check the stone just placed
}

sub CheckIfDead {
    my ($x, $y, $color) = @_;

    my $stone = $diagram->game_stone(Coords2SGF($x, $y));
    return unless(defined($stone) and   # no stone/group here to check
                  ($stone eq $color));  # color doesn't match
    unless (HasLibs($x, $y, $color, {}, 0)) {
        RemoveGroup($x, $y, $color);    # no liberties? - it's dead!
    }
}

sub HasLibs {
    my ($x, $y, $color, $been_here, $depth) = @_;

    if ($depth > 1000) {
        die("Oops, recursion > 1000 while checking for liberties at move $moveNum coords ($x,$y)\n" .
            "This isn't supposed to be possible!  Aborting...\n");
    }
    if (($x < 1) or ($x > ($option{boardSize})) or ($y < 1) or ($y > ($option{boardSize}))) {
        return(0);              # oops! off the board.
    }
    return 0 if (exists($been_here->{"$x,$y"})); # we've been here before
    $been_here->{"$x,$y"} = 1;     # mark that we've been here
    my $thisStone = $diagram->game_stone(Coords2SGF($x, $y));
    return 1 unless(defined($thisStone));       # empty, the group has liberties
    return(0) if ($thisStone ne $color);        # this is an opponents stone - no liberties here!
    # this is a connected stone of the same color
    $depth++;
    if (HasLibs($x + 1, $y, $color, $been_here, $depth) or
        HasLibs($x - 1, $y, $color, $been_here, $depth) or
        HasLibs($x, $y + 1, $color, $been_here, $depth) or
        HasLibs($x, $y - 1, $color, $been_here, $depth)) {
        return(1);              # yes! we're alive!
    }
    return(0);                  # uh-oh! no liberties yet...
}

sub RemoveGroup {
    my ($x, $y, $color) = @_;

    my $thisStone = $diagram->game_stone(Coords2SGF($x, $y));
    if (defined($thisStone) and
        ($thisStone eq $color)) {
        $diagram->capture(Coords2SGF($x, $y));
        RemoveGroup($x + 1, $y, $color);  # remove any connected stones of the same color
        RemoveGroup($x - 1, $y, $color);
        RemoveGroup($x, $y + 1, $color);
        RemoveGroup($x, $y - 1, $color);
    }
}

sub finishDiagram {
    my ($d, $done) = @_;

    my $prevDiagram = $diagram;
    $diagram = $diagram->next;                  # start a fresh diagram
    $diagram->user({id => $diagramId++});       # init user hash
    if (exists($prevDiagram->user->{mainId})) {
        my $mainId = $prevDiagram->user->{mainId} + 1;
        $diagram->user->{mainId} = $mainId;
        printIndent("Parsing Diagram $mainId at move $moveNum\n");
    } else {
        printIndent("Parsing Variation continuation at move $moveNum\n");
    }
    $prevDiagram->user->{next} = $diagram;      # link from previous to new diagram
    if ($option{repeatLast} and
        defined($lastMove2[0])) {
        $diagram->renumber($lastMove2[0], $lastMove2[1], undef, $lastMove2[2]);
    }
}

sub CompareVariation {

    my @aa = split(/\./, $a);
    my @bb = split(/\./, $b);

    my ($ii, $max, $return);
#print "CompareVariation($a, $b)\n";
    if (@aa > @bb) {
        $max = @aa;
    } else {
        $max = @bb;
    }
    for ($ii = 0; $ii < $max; $ii++) {
        $aa[$ii] = -1 unless(defined($aa[$ii]));
        $bb[$ii] = -1 unless(defined($bb[$ii]));
        $return = ($aa[$ii] <=> $bb[$ii]);
#print("ii=$ii, aa=$aa[$ii], bb=$bb[$ii], returns $return\n");
        last unless ($return == 0);
    }
    return($return);
}

sub nameDiagram {
    my ($diagram, $sequence, $level) = @_;

    my ($name, $type, $num);
    if ($level) {
        if ($sequence) {
            $name = "Variation $variationNum.$sequence";
        } else {
            $variationNum++;
            $name = "Variation $variationNum";
        }
    } else {
        $name = "Diagram $diagramNum";
        $diagramNum++;
    }
    if (0) {
        my $id = $diagram->user->{id} || '?';   # helpful for debugging
        $diagram->name("$id: $name");
    } else {
        $diagram->name($name);
    }
    if ($level) {
        if ($sequence) {
            $diagram->name(" (continued)")
        } else {
            my $rc = $diagram->user->{removedCount} || 0;
            # adjust "variation on move" for removed stone count
            $diagram->var_on_move($diagram->var_on_move - $rc);
        }
    }
    return ($diagramNum - 1, $name);
}

sub convertDiagram {
    my ($dg2, $diagram, $level) = @_;

    my $sequence = 0;
    while (defined($diagram)) {
        my ($dNum, $dName) = nameDiagram($diagram, $sequence++, $level);
        if(($dNum >= $option{firstDiagram}) and
           ($dNum <= $option{lastDiagram})) {
            $diagram->hoshi(@{hoshi()});    # add hoshi points to board
            $diagram->offset($diagram->first_number - 1) if ($option{newNumbers} and
                                                             $diagram->first_number);
            printIndent("Converting $dName\n");
            $dg2->convertDiagram($diagram);
            my $vars = $diagram->user->{variations};
            if (defined($vars) and
                not $option{ignoreVariations}) {
                foreach my $moveNum (sort(keys(%{$vars}))) {
                    foreach my $varD (@{$vars->{$moveNum}}) {
                        convertDiagram($dg2, $varD, $level + 1);
                    }
                }
            }
        }
        $diagram = $diagram->user->{next};
    }
}

#
# OK, now we've got everything defined, we can start executing stuff

#
# set the defaults
#
$option{topLine} = $option{leftLine} = 1;
$option{bottomLine} = $option{rightLine} = 19;
$option{varNumbersFlag} = 'relative';
$option{doubleDigits} = 0;
$option{repeatLast} = 0;
$option{newNumbers} = 0;
$option{firstDiagram} = 1;
$option{lastDiagram} = 10000;
$option{boardSize} = 19;
$option{breakList} = [];
$option{coords} = 0;
$option{verbose} = 0;

$option{converter} = 'TeX';

if ($myName =~ m/sgf2(.*)/) {
    if (($1 ne 'tex') and
        ($1 ne 'diagram') and
        ($1 ne 'dg')) {
        $option{converter} = $1;
    }
}

=head1 OPTIONS

=over 4

=item B<-h > | B<-help>

Print a help message and quit.

=item B<-i> | B<-in>  <filename> | <filename>.sgf | <filename>.mgt

Specifies the input filename. (STDIN or none for standard input.)
This option is not needed in ordinary use.

=item B<-o> | B<-out> <filename>

Specifies the output file. ('STDOUT' for standard output.) If the
input file is <filename>, <filename>.sgf or <filename>.mgt, then
<filename>.I<converter> is the default (see the B<converter> option).
This option is not needed in ordinary usage.

=item B<-t> | B<-top> <top line number>

Specifies the top line to print. Default is 1.

=item B<-b> | B<-bottom> <bottom line number>

Specifies the bottom line to print. Default is 19.

=item B<-l> | B<-left> <left line number>

Specifies the leftmost line to print. Default is 1.

=item B<-r> | B<-right> <right line number> 

Specifies the rightmost line to print. Default is 19.

=item B<-break> | B<-breakList> <break list>

'break list' is a list of moves, separated by comma, with no spaces. These 
are breakpoints: each will be the last move in one diagram. 

=item B<-m> | B<-movesPerDiagram> <moves per diagram>

'moves per diagram' is a positive integer, specifying the maximal number of
moves per diagram. Default is 50 unless B<-break> or B<-breakList> is set, in
which case the default is set to a very large number (10,000). The two options
B<-breakList> and B<-movesPerDiagram> may be used together.

=item B<-n> | B<-newNumbers>

Begin each diagram with the number 1. The actual move numbers are still used
in the label.

B<-newNumbers> and B<-doubleDigits> are alternative schemes for
avoiding three-digit numbers in the diagrams. They should probably not be used
together.

=item B<-d> | B<-doubleDigits>

If the first move of a diagram exceeds 100, the move number is reduced modulo
100. The actual move numbers are still used in the label.  B<-newNumbers> and
B<-doubleDigits> are alternative schemes for avoiding three-digit numbers in
the diagrams. They should probably not be used together.

=item B<-rl> | B<-repeatLast>

The last move in each diagram is the first move in the next. This
emulates a common style for annotating Go games.

=item B<-il> | B<-ignoreLetters>

Letters embedded in the SGF with the L or LB property are ignored.

=item B<-im> | B<-ignoreMarks>

Marks embedded in the SGF with the M or MA property are ignored.

=item B<-ip> | B<-ignorePass>

Passes are ignored. In sgf, a pass is a move at the fictitious point tt.
Without this option, sgf2dg indicates passes in the diagram comments.

=item B<-ia> | B<-ignore all>

Ignore SGF letters, marks, variations and passes.

=item B<-firstDiagram> <diagram number>

Specifies the first diagram to print. Default is 1.

=item B<-lastDiagram> <diagram number>

Specifies the last diagram to print. Default is to print all
diagrams until the end.

=item B<-coords>

Generates a coordinate grid. This option may not be used with B<-twoColumn>.

=item B<-verbose>

Print diagnostic messages as the conversion proceeds.  Most SGF
properties produce some kind of message.

=item B<-converter> | B<-convert>

Selects different output converter plugins.  Converters available
with the current distribution package are:

=over 4

=item   L<Games::Go::Dg2TeX>       TeX source (default)

=item   L<Games::Go::Dg2Mp>        MetaPost embedded in TeX

=item   L<Games::Go::Dg2ASCII>     simple ASCII diagrams

=item   L<Games::Go::Dg2PDF>       Portable Document Format (PDF)

=item   L<Games::Go::Dg2Ps>        PostScript

=item   L<Games::Go::Dg2Tk>        Perl/Tk NoteBook/Canvas

=item   L<Games::Go::Dg2TkPs>      PostScript via Dg2Tk (Dg2Ps is prefered)

=back

B<converter>s are quite easy to write - should take just a few hours if
you are already conversant with the conversion target.  If you would
like to create a B<converter> plugin module, the easiest way is
probably to grab a copy of Dg2Ps.pm (for example) and modify it.
Once it's working, please be sure to send us a copy so we can add it
to the distribution.

Converters are always prepended with 'Games::Go::Dg2', so to select
the ASCII converter instead of the default TeX converter, use:

    -converter ASCII

Converter names are case sensitive.

The default output filename suffix is determined by the converter:
the converter name is lower-cased to become the suffix, so the ASCII
converter produces <filename>.ascii from <filename>.sgf.

You can also select different B<converter>s by changing the name of
the sgf2dg script (or better, make symbolic links, or copies if
your system can't handle links).  The B<converter> name is extracted
from the name with this regular expression:

    m/sgf2(.*)/

Anything after 'sgf2' is assumed to be the name of a B<converter>
module.  For example, let's create a link to the script:

    $ cd /usr/local/bin
    $ ln -s sgf2dg sgf2Xyz

Executing:

    $ sgf2Xyz foo.sgf [ options ]

attempts to use Games::Go::Dg2Xyz as the B<converter>.  The
B<converter> name extracted from the script name is case sensitive.

Note that three extracted names are treated specially:

=over 4

=item   tex

=item   diagram

=item   dg

=back

These three names (when extracted from the script name) always
attempt to use Games::Go::Dg2TeX as the B<converter>.

=back

=head1 CONVERTER OPTIONS

Converters may be added dynamically as plugins, so this list only
includes converter plugin modules that are included with the Sgf2Dg
distribution.

Converter options are prepended with the converter name so that
option xyz for converter Games::Go::Dg2Abc is written on the command
line as:

    $ sgf2dg ... -Abc-xyz ...
    
Converter options that take arguments must be quoted so that the
shell passes the option and any arguments as a single ARGV.  For
example, if the xyz option for converter Dg2Abc takes 'foo' and
'bar' as additional arguments, the command line would be:

    $ sgf2dg ... "-Abc-xyz foo bar" ...

or a more realistic example of changing the background color:

    $ sgf2dg genan-shuwa -converter Tk "-Tk-bg #d2f1b4bc8c8b"

Since Sgf2Dg is a super-set replacement for the Sgf2TeX package,
TeX holds the default position for converters.  Because of this
historically priviledged position, the Dg2TeX options below do
not need to be prepended with 'TeX-'.  All of the following
options apply to the Dg2TeX converter.

Other plugins available at the time of release are Dg2Mp, Dg2ASCII,
Dg2PDF, Dg2Ps, Dg2Tk and Dg2TkPs.  Dg2ASCII and Dg2TkPs take no
additional options.  Dg2Tk doesn't explicitly accept options, but it
attempts to pass unrecognized options to the Tk::Canvas widgets at
creation time (which is why the example above works).

For more information about converter-specific options, please refer
to the perldoc or manual pages:

    $ perldoc Games::Go::Dg2PDF

or

    $ man Games::Go::Dg2Ps

=head2 Dg2TeX options

=over 4

=item B<-longComments>

(Dg2TeX) In its default usage, the comments to each diagram
comprise an unbreakable vbox---they must all appear on one page.
This can cause problems if the comments are very extensive. This
option generates more complicated TeX macros which allow the
comments to be broken across pages. This option may not be used
with B<-simple> or B<-longComments>.

=item B<-simple>

(Dg2TeX) This generates very simple TeX which may not look so
good on the page, but is convenient if you intend to edit the
TeX. This option should not be used with B<-longComments>.

=item B<-twoColumn>

(Dg2TeX) This generates a two-column format using smaller fonts.
This option may not be used with B<-longComments> or B<-coords>.

=item B<-bigFonts>

(Dg2TeX) Use fonts magnified 1.2 times.

=item B<-texComments>

(Dg2TeX) If this option is NOT used then the characters {, } and
\ found in comments are replaced by [, ] and /, since TeX roman
fonts do not have these characters. If this option is used, these
substitutions are not made, so you can embed TeX source (like
{\bf change fonts}) directly inside the comments.

=item B<-gap>

(Dg2TeX) The vertical gap (in points) between diagrams.  Default
is 12 points.

=back

=cut

#
# parse the command line arguments:
#
my ($inHandle, $outHandle,  $inFileName, $outFileName);
my ($arg, @unknownOpt);
while (scalar(@ARGV)) {
    $arg = shift(@ARGV);
    if (($arg eq '-d') or ($arg eq '-doubleDigits')) {
        $option{doubleDigits} = 1 
    } elsif (($arg eq '-i') or ($arg eq '-in')) {
        $inFileName = shift(@ARGV);
    } elsif (($arg eq '-o') or ($arg eq '-out')) {
        $outFileName = shift(@ARGV);
    } elsif (($arg eq '-m') or ($arg eq '-movesPerDiagram')) {
        $option{movesPerDiagram} = shift(@ARGV);
    } elsif (($arg eq '-n') or ($arg eq '-newNumbers')) {
        $option{newNumbers} = 1;
    } elsif (($arg eq '-av') or ($arg eq '-absoluteVarNums')) {
        $option{varNumbersFlag} = 'absolute';
    } elsif (($arg eq '-rv') or ($arg eq '-relativeVarNums')) {
        $option{varNumbersFlag} = 'relative';
    } elsif (($arg eq '-cv') or ($arg eq '-correlativeVarNums')) {
        $option{varNumbersFlag} = 'correlative';
    } elsif (($arg eq '-im') or ($arg eq '-ignoreMarks')) {
        $option{ignoreMarks} = 1;
    } elsif (($arg eq '-il') or ($arg eq '-ignoreLetters')) {
        $option{ignoreLetters} = 1;
    } elsif (($arg eq '-ip') or ($arg eq '-ignorePass')) {
        $option{ignorePass} = 1;
    } elsif (($arg eq '-iv') or ($arg eq '-ignoreVariations')) {
        $option{ignoreVariations} = 1;
    } elsif (($arg eq '-ia') or ($arg eq '-ignoreAll')) {
        $option{ignoreVariations} = 1;
        $option{ignoreLetters} = 1;
        $option{ignoreMarks} = 1;
        $option{ignorePass} = 1;
    } elsif (($arg eq '-rl') or ($arg eq '-repeatLast')) {
        $option{repeatLast} = 1;
    } elsif (($arg eq '-break') or ($arg eq '-breakList')) {
        @{$option{breakList}} = sort {$::a <=> $::b} (split(/,/, shift(@ARGV)));
    } elsif (($arg eq '-t') or ($arg eq '-top')) {
        $option{topLine} = shift(@ARGV);
    } elsif (($arg eq '-b') or ($arg eq '-bottom')) {
        $option{bottomLine} = shift(@ARGV);
    } elsif (($arg eq '-l') or ($arg eq '-left')) {
        $option{leftLine} = shift(@ARGV);
    } elsif (($arg eq '-r') or ($arg eq '-right')) {
        $option{rightLine} = shift(@ARGV);
    } elsif ($arg eq "-coords") {
        $option{coords} = 1;
    } elsif ($arg eq '-firstDiagram') {
        $option{firstDiagram} = shift(@ARGV);
    } elsif ($arg eq '-lastDiagram') {
        $option{lastDiagram} = shift(@ARGV);
    } elsif (($arg eq '-h') or ($arg eq '-help')) {
        print($help);
        exit(0);
    } elsif (($arg eq '-v')or($arg eq '-version')) {
        print("$myName $version\n");
        exit(0);
    } elsif ($arg eq '-verbose') {
        $option{verbose} = 1;
    } elsif (($arg eq '-converter') or
             ($arg eq '-convert')) {
        $option{converter} = shift(@ARGV);
        $option{converter} =~ s/.*Games::Go::Dg2//;
    } elsif ($arg eq '-texComments') {
        $option{converterOption}{texComments} = 1;
    } elsif ($arg eq '-bigFonts') {
        $option{converterOption}{bigFonts} = 1;
    } elsif ($arg eq '-longComments') {
        $option{converterOption}{longComments} = 1;
    } elsif ($arg eq '-simple') {
        $option{converterOption}{simple} = 1;
    } elsif ($arg eq '-twoColumn') {
        $option{converterOption}{twoColumn} = 1;
    } elsif ($arg eq '-gap') {
        $option{converterOption}{gap} = shift(@ARGV);
    } elsif (substr($arg, 0, 1) eq '-') {
        push(@unknownOpt, $arg);        # worry about it later...
    } else {
        $inFileName = $arg;
    }
}

foreach (@unknownOpt) {
    if (m/^-$option{converter}-(.*)/) {
        my $cnvOpt = $1;
        if ($cnvOpt =~ m/(\S+)\s+(.*)/) {
            $option{converterOption}{$1} = $2;
        } else {
            $option{converterOption}{$cnvOpt} = 1;
        }
    } else {
        print("\nUnknown option: $_\n");
        print($help);
        exit(1);
    }
}

unless(exists($option{movesPerDiagram})) {
    $option{movesPerDiagram} = scalar(@{$option{breakList}}) ? 10000 : 50
}

# open the input file handle
if (not defined($inFileName) or ($inFileName eq '-')) {
    $inFileName = 'STDIN';
    $inHandle = \*STDIN;
} else {
    if (!-e $inFileName) {
        if (-e "$inFileName.sgf") {
            $inFileName = "$inFileName.sgf";
        } elsif (-e "${inFileName}sgf") {
            $inFileName = "${inFileName}sgf";
        } else {
            if (-e "$inFileName.mgt") {
                $inFileName = "$inFileName.mgt";
            } else {
                die("Can't find $inFileName, $inFileName.sgf or $inFileName.mgt\n");
            }
        }
    }
    $inHandle = IO::File->new("<$inFileName") or
        die("Can't open $inFileName for reading: $!\n");
}

# convert the input filename into an output filename
unless (defined ($outFileName)) {
    if ($inFileName eq  'STDIN') {
        $outFileName = 'STDOUT';
    } else {
        $outFileName = $inFileName;
        unless ($outFileName =~ s/.sgf$//i) {
            $outFileName =~ s/.mgt$//i;
        }
    }
    $outFileName =~ s/.*\///;   # output into current directory
}
# create the root diagram object
$rootDiagram = $diagram = Games::Go::Diagram->new(callback => \&finishDiagram);
$diagram->user({first  => 'first',      # init user hash
                mainId => 1,
                id     => $diagramId++});   # id isn't really used for anything,
                                            #   but it helps during debug

# create the converter object (early so we get errors right away)
my $fullConvName = "Games::Go::Dg2$option{converter}";
eval "require $fullConvName;";
die "Couldn't require $fullConvName: $@" if $@;

my $converter;
eval "\$converter = $fullConvName->new(
        doubleDigits => \$option{doubleDigits},
        coords       => \$option{coords},
        \%{\$option{converterOption}});";

die "Couldn't create new $fullConvName converter: $@" if $@;

# parse the SGF into the diagrams
printIndent("Parsing Diagram 1 at move $moveNum\n");
SGF_ReadFile($inHandle);
close($inHandle);                       # don't need this anymore

$converter->configure(
        boardSize    => $option{boardSize},
        topLine      => $option{topLine},
        bottomLine   => $option{bottomLine},
        leftLine     => $option{leftLine},
        rightLine    => $option{rightLine},
        %{$option{converterOption}}
        );
# handle the output file
if (($outFileName eq 'STDOUT') or ($outFileName eq '-')) {
    $converter->configure(file => \*STDOUT);
    $converter->configure(filename => 'STDOUT');
} else {
    my $outSuffix = lc $option{converter};
    unless ($outFileName =~ m/.$outSuffix$/i) {
        $outFileName .= ".$outSuffix";  # tack on the converter extension
    }
    $converter->configure(file => ">$outFileName");
}

# add an attribution comment
$converter->comment(
"%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

 This file was created by $myName $version with the following command line:

 $commandLine

 $myName was created by Reid Augustin.  The go fonts, TeX
 macros and TeX programming were designed by Daniel Bump.

 More information about the $myName package can be found at:

               http://match.stanford.edu/bump/go.html

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

");

# now print the diagrams
convertDiagram($converter, $rootDiagram, 0);

$converter->close;

exit(0);

__END__

=head1 SEE ALSO

=over 0

=item o sgfsplit(1)   - splits a .sgf file into its component variations

=back

=head1 AUTHOR

sgf2dg was written by Reid Augustin, E<lt>reid@hellosix.comE<gt>

The GOOE fonts and TeX macros were designed by Daniel Bump
(bump@math.stanford.edu).  Daniel hosts the GOOE and sgf2dg home page at:

    L<http://match.stanford.edu/bump/go.html>

=head1 COPYRIGHT AND LICENSE

Copyright (C) 1997-2005 by Reid Augustin

This program is free software; you can redistribute it and/or modify it under
the terms of the GNU General Public License as published by the Free Software
Foundation; either version 2 of the License, or (at your option) any later
version.

This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
details.

You should have received a copy of the GNU General Public License along with
this program; if not, write to the Free Software Foundation, Inc., 59 Temple
Place - Suite 330, Boston, MA 02111, USA

=cut
