# This file is part of reLyX
# Copyright (c) 1998-9 Amir Karger karger@post.harvard.edu
# You are free to use and modify this code under the terms of
# the GNU General Public Licence version 2 or later.

package ReadCommands;
# Read a file containing LaTeX commands and their syntax
# Also, read a file containing LyX layouts and their LaTeX equivalents

use strict; 

# Variables needed by other modules
# ToLayout is a ref to a hash which contains LyX layouts & LaTeX equivalents
# regular_env is a list of environments that have "reLyXable" LaTeX in them
# math_trans is a hash of math commands and what they're translated to
use vars qw($ToLayout @regular_env %math_trans);

#    CommandHash is the hash of LaTeX commands and their syntaxes which
# this package builds.
my %CommandHash;

# Name of the environment containing names of regular environments :)
my $regenv_name = "reLyXre";
# Name of environment containing translations of math commands
my $math_trans_name = "reLyXmt";
# Variables set when in the above environments
my ($in_regular_env, $in_math_trans);
my @Environments = qw($regenv_name $math_trans_name);

# This word in a command's argument (in the syntax file) means that reLyX
# should translate that argument as regular LaTeX
my $Translate_Word = "translate";

#########################  READ COMMAND SYNTAX  ################################
sub read_syntax_files {
# This subroutine calls the TeX parser & translator to read LaTeX syntax file(s)
#    It sets the list of "regular" environments, environments which aren't known
# by reLyX, but which contain text that reLyX can translate.
#    It also reads math commands which should be translated (e.g., \sp -> ^)
#
# @_ contains the syntax file(s) to read

    my @syntaxfiles = @_;

# Before anything else, set the package-wide variables based on the
#    user-given flags
    # opt_d is set to 1 if '-d' option given, else (probably) undefined
    my $debug_on = (defined($main::opt_d) && $main::opt_d);

    # opt_r is a group of environments to copy like regular text
    # If opt_r wasn't given, then there are no such special environments
    @regular_env = (defined $main::opt_r ? (split(/,/, $main::opt_r)) : () );

    # The only important commands to pass are \begin and \end, so that the
    # parser realizes they start/end environments, as opposed to being
    # regular old tokens.
    my %MyTokens = ( '{' => $Text::TeX::Tokens{'{'},
                     '}' => $Text::TeX::Tokens{'}'},
                     '\begin' => $Text::TeX::Tokens{'\begin'},
                     '\end' => $Text::TeX::Tokens{'\end'},
    );
    
    my $InFileName;
    foreach $InFileName (@syntaxfiles) {
	die "could not find syntax file $InFileName" unless -e $InFileName;
	my $zzz=$debug_on ? "from $InFileName " :"";
	warn "Reading LaTeX command syntax $zzz\n";

	# Open the file to turn into LyX.
	my $infile = new Text::TeX::OpenFile $InFileName,
	    'defaultact' => \&read_commands,
	    'tokens' => \%MyTokens;

	# When we start (each file), we're not reading regular environments yet
	$in_regular_env = 0;

	# Process the file
	$infile->process;
    }

    if ($debug_on) {
	print "Regular environments: @regular_env\n";
	my @mathkeys = keys(%math_trans);
	print "     Math command     |  translation\n" if @mathkeys;
	foreach (@mathkeys) { printf("%20s       %s\n",$_,$math_trans{$_}) }
    }

    #warn "Done reading commands\n";
    return;
} # end subroutine call_parser

sub read_commands {
# This subroutine is called by Text::TeX::process
# Arg0 is the token we just ate
# Arg1 is the file object we're reading from
#
#    We create a hash, where each command is a key. The value is just a string
# of zero or more 'o' and 'r' characters. Each 'o' stands for an optional
# argument, 'r' stands for a required argument. 'R' stands for a required
# argument whose text will be regular LaTeX, e.g., the argument to \mbox{}
#    In addition, the $regenv_name environment contains
# regular environments, like those input with the -r option.
#    Note that if a command is found more than once, then it wil be overwritten.
# This is a feature. This way, a user-defined syntax file can overwrite the
# argument list found in the default syntax file.
    my ($token,$fileobject) = (shift,shift);

    my $type = ref($token);
    $type =~ s/^Text::TeX::// or die "unknown token type $type from Text::TeX";
    #print $token->exact_print, unless $type eq "Paragraph";
    #print $token->comment if $token->comment;

    # Because there's no token list, ALL tokens will be
    #    Paragraph, Text, or Token
    SWITCH: for ($type) {
       # Handle blank lines.
        if (/Paragraph/) {
	    # don't do anything
	    last SWITCH;

        } elsif (/^Token/) {
	    # Comment in its own paragraph... skip
	    last SWITCH unless defined($token->print);

	    if ($in_math_trans) { # read translations of math commands
	        my $key = $token->print;
		# Translation is whatever's in the argument to the token
		# (There might be multiple tokens in there)
		my @vals = $fileobject->eatBalanced->contents;
		my $val = join ("", map {$_->exact_print} @vals);
		$math_trans{$key} = $val;
	    
	    } else { # regular portion of syntax file
		my ($dum2);
		my $args = "";
		# read while there are arguments
		while (($dum2 = $fileobject->lookAheadToken) &&
		       ($dum2 =~ /^[[{]$/)) {
		    if ($dum2 eq '[') { #eat optional argument - assumed simple
			$fileobject->eatOptionalArgument;
			$args .= "o";
		    } else {
			my $tok = $fileobject->eatBalanced or warn "bad group";
			if ($tok->exact_print eq $Translate_Word) {
			    $args .= "R";
			} else {
			    $args .= "r";
			}
		    } # end if $dummy = [{
		} # done reading command
		$CommandHash{$token->print} = $args;
	    } # in math trans env or regular token?

	    last SWITCH;

        } elsif (/^Begin::Group::Args/) {
	    my $env = $token->environment;
	    CASE: {
		$in_regular_env = 1, last CASE if $env eq $regenv_name;
		$in_math_trans = 1,  last CASE if $env eq $math_trans_name;
		warn "Unknown environment $env in syntax file";
	    }

        } elsif (/^End::Group::Args/) {
	    my $env = $token->environment;
	    CASE: {
		$in_regular_env = 0, last CASE if $env eq $regenv_name;
		$in_math_trans = 0,  last CASE if $env eq $math_trans_name;
		warn "Unknown environment $env in syntax file";
	    }

        } elsif (/^Text/) {
	    # don't do anything unless we're reading environments
	    if ($in_regular_env) {
		my @new_envs = (split(/\s+/, $token->print));
		@new_envs = grep ($_, @new_envs); # remove empty elements
		push @regular_env,@new_envs;
	    }
	    last SWITCH;
        } else {
	    die "unexpected token type $type";
	}

    } # end SWITCH

} # end sub read_commands

sub Merge {
#    This sub creates a token list (which could be used to call a Text::TeX
# parser) from %CommandHash, and merges it with the input token list
#    If a command takes any required arguments, it will be a report_args,
# but if it just takes an optional argument, it can stay a regular old token.
# In either case, we insert a new field, "relyx_args", into the token list,
# which is the expected order of arguments for that command. Even if there
# are no args, we insert an empty relyx_args, so that we can differentiate
# between a truly unknown token and a known token which takes no args.
#
# We don't allow a new command to override a command that already exists in
# OldHash, i.e., one that was defined explicitly in the calling sub.
#
# Arg0 is a (reference to an) existing token list
    my $OldHashRef = shift;

    foreach (keys %CommandHash) {
	my $val = $CommandHash{$_};
	my ($entry, $count, @foo);
        if (!exists $OldHashRef->{$_}) {
	    if ($count = scalar(@foo = ($val =~ /r/gi))) {
		# TeX.pm will make this a TT::BegArgsToken and $count-1
		#    TT::ArgTokens, followed by a TT::EndArgsToken
		$OldHashRef->{$_} = {"Type" => 'report_args',
		                     "count" => $count,
				     "relyx_args" => $val};
	    } else { # only non-required args
		# Make it a regular TT::Token, but write relyx_args
		#    (even if $val is "")
		$OldHashRef->{$_} = {"relyx_args" => $val};
	    }
	}
    } # end foreach

} # end sub Merge

############################  READ LAYOUTS  ####################################
sub read_layout_files {
# This subroutine reads a textclass-specific layout file and all files
# included in that file.
#    It sets up the layout hash table. For each environment, it describes which
# layout that environment refers to. It does the same for macros which
# begin LyX layouts (e.g., \section)
#    If we read a command that's not already in CommandHash, it means that this
# layout has some commands that aren't in syntax.default. If so, we ASSUME
# that the command takes just one required argument, and put it in
# CommandHash, so that &Merge will eventually put these commands into the
# token lists.
#
# TODO: we actually need to allow more sophisticated stuff. E.g. \foilhead
# is converted to Foilhead or ShortFoilHead (foils.layout) depending on whether
# the command has "r" or "or" arguments. Reading LatexParam (if it exists)
# can help us with this.
# Default is "r". Just unshift other args as you read them, since latexparam
# is put in between macro & the argument
# TODO: We need to store ToLayout s.t. we can have > 1 layout per command.
# Maybe by default just have one layout, but if (ref(layout)) then read
# more args & thereby figure out which layout?
# 
# Arg0 is the name of the documentclass
    use FileHandle;
    use File::Basename;
    my $doc_class = shift;
    my @filestack;
    my $fh;
    my $line;
    # ToLayout{latexname} stores layout; so ReversHash{layout} = latexname
    my %ReverseHash;
    my $debug_on = (defined($main::opt_d) && $main::opt_d);

    # look for layout file in $HOME/.lyx first, then system layouts directory
    my $searchname = "$doc_class.layout";
    my @searchdirs = ();
    my $personal_layout = "$main::dot_lyxdir/layouts";
    push(@searchdirs,$personal_layout) if -e $personal_layout;
    my $system_layout = "$main::lyxdir/layouts";
    # I guess this won't exist if running reLyX without installing...
    # Of course, in that case, this will probably break
    push(@searchdirs,$system_layout) if -e $system_layout;
    my @foundfiles = grep(-e "$_/$searchname", @searchdirs) or
          die "Cannot find layout file $searchname in dir(s) @searchdirs";
    my $LayoutFileName = "$foundfiles[0]/$searchname"; # take first one we found

    $fh = new FileHandle;
    $fh->open ("<$LayoutFileName");
    my $zzz=$debug_on ? "$LayoutFileName" :"";
    warn "Reading layout file $zzz\n";
    push @filestack, $fh;

    # Read the layout file!
    my ($lyxname, $latexname, $latextype, $latexparam, $keepempty);
    my $fname;
    while() {
	# Read a line. If eof, pop the filestack to return to the file
	#    that included this file *or* finish if the stack's empty
        unless (defined ($line = <$fh>)) {
	    $fh->close;
	    pop @filestack;
	    last unless ($#filestack+1); # finish when stack is empty
	    $fh = $filestack[-1];
	    next; # read another line from the "calling" file
	}

	# Skip blank lines
	next if $line =~ /^\s*$/;

	# Split the line. Use limit 2 since there may be whitespace in 2nd term
	my ($field_name, $field_stuff) = split(' ', $line, 2);
	$field_name = lc($field_name); # LyX is case insensitive for fields
	if (defined($field_stuff)) {
	    $field_stuff =~ s/^\"(.*)\"/$1/;
	    chomp ($field_stuff);
	    # Since split is limited to 2 fields, there may be extra whitespace
	    # at end. LyX breaks on a "\layout Abstract " command!
	    $field_stuff =~ s/\s*$//;
	}

	# This set of ifs deals with lines outside a style definition
	if ($field_name eq "style") { # start a style definition
	    $lyxname = $field_stuff;
	    # Styles in LyX have spaces, but _ in layout files
	    $lyxname =~ s/_/ /g;
	    $latexname  = ""; # make sure these variables are unset
	    $latextype  = "";
	    $latexparam = "";
	    $keepempty = 0;
	} elsif ($field_name eq "input") { #include a file
	    $searchname = $field_stuff;
	    @foundfiles = grep(-e "$_/$searchname", @searchdirs) or
	      die "Cannot find layout file $searchname in dir(s) @searchdirs";
	    $fname = "$foundfiles[0]/$searchname"; # take first one we found
	    $fh = new FileHandle;
	    push @filestack, $fh;
	    $fh->open("<$fname");
	    print "Reading included layout file $fname\n" if $debug_on;
	}

	next unless $lyxname; # not w/in style definition

	# This set of ifs deals with lines within a Style definition
	if ($field_name eq "latexname") {
	    $latexname = $field_stuff;
	    next;
	} elsif ($field_name eq "latexparam") {
	    #$dum = $field_stuff;
	    $latexparam = $field_stuff;
	    next;
	} elsif ($field_name eq "latextype") {
	    $latextype = $field_stuff;
	    next;
	} elsif ($field_name eq "keepempty") {
	    $keepempty = $field_stuff;
	    next;
	} elsif ($field_name eq "copystyle") { # copy an existing style
	    # "if" is necessary in case someone tries "CopyStyle Standard"
	    if (exists $ReverseHash{$field_stuff}) {
		my $layref = $ReverseHash{$field_stuff};
		$latexname  = $layref->{"name"};
		$latextype  = $layref->{"type"};
		$latexparam = $layref->{"param"};
		$keepempty = $layref->{"keepempty"};
	    }

	# When you get to the end of a definition, create hash table entries
	#    (if you've found the right information)
	} elsif ($field_name eq "end") {

	    if ($latextype and $latexname) {
		# Create reverse hash entry (needed for CopyStyle)
		# Do it before making modifications to $latexname, e.g.
		$ReverseHash{$lyxname} = {"name"  => $latexname,
		                          "type"  => $latextype,
					  "param" => $latexparam,
					  "keepempty" => $keepempty,
					  };

		my ($nest, $skip) = (0,0);
		for ($latextype) { # make $_=$latextype
		    if (/^Command/) {
			# Macros need a '\' before them. Environments don't
			$latexname = '\\' . $latexname;

			# Create the command if it wasn't in syntax.default
			unless (exists $CommandHash{$latexname}) {
			    $CommandHash{$latexname} = "r";
			}

		    } elsif (/^Environment/) {
			$nest = 1;
		    } elsif (/Item_Environment/i || /List_Environment/) {
			$nest = 1;

		    # layout Standard has LatexType Paragraph. It shouldn't
		    #    have any hash entry at all
		    } elsif (/^Paragraph$/) {
		        $skip = 1;
		    } else {
			warn "unknown LatexType $latextype" . 
			     "for $latexname (layout $lyxname)!\n";
		    }
	    # Handle latexparam, if any
#	    if ($dum =~ s/^"(.*)"/$1/) { # newer layout file syntax
#		while ($dum =~ /^[[{]/) {
#		    $dum =~ s/\[.*?\]// && ($latexargs = "o$latexargs") or
#		    $dum =~ s/\{.*?\}// && ($latexargs = "r$latexargs");
#		}
#		warn "leftover LatexParam stuff $dum" if $dum;
#	    } else { # 0.12.0
#		if ($latextype eq "Command") {optarg}
#		else {req. arg}
#	    }
		} #end for

		# Create the hash entry
		unless ($skip) {
		    $ToLayout->{$latexname} = {"layout" => $lyxname,
						"nestable" => $nest,
						"keepempty" => $keepempty};
		}

		# Now that we've finished the style, unset $lyxname so that
		#     we'll skip lines until the next style definition
		$lyxname = "";
	    } # end if ($latextype and $latexname)
	} # end if on $line
	    
    } #end while

## Print every known layout
#    print "     LatexName            Layout        Keepempty?\n";
#    foreach (sort keys %$ToLayout) {
#        printf "%20s%15s     %1d\n",$_,$ToLayout->{$_}{'layout'},
#    	      $ToLayout->{$_}{'keepempty'};
#    };
    #warn "Done reading layout files\n";
    return;
} # end sub read_layout_files



1; # return TRUE to calling routine