# /=====================================================================\ #
# |  LaTeXML::Core::Definition::Conditional                             | #
# | Representation of definitions of Control Sequences                  | #
# |=====================================================================| #
# | Part of LaTeXML:                                                    | #
# |  Public domain software, produced as part of work done by the       | #
# |  United States Government & not subject to copyright in the US.     | #
# |---------------------------------------------------------------------| #
# | Bruce Miller <bruce.miller@nist.gov>                        #_#     | #
# | http://dlmf.nist.gov/LaTeXML/                              (o o)    | #
# \=========================================================ooo==U==ooo=/ #
package LaTeXML::Core::Definition::Conditional;
use strict;
use warnings;
use LaTeXML::Global;
use LaTeXML::Common::Object;
use LaTeXML::Common::Error;
use LaTeXML::Core::Token;
use LaTeXML::Core::Tokens;
use base qw(LaTeXML::Core::Definition::Expandable);

# Conditional control sequences; Expandable
#   Expand enough to determine true/false, then maybe skip
#   record a flag somewhere so that \else or \fi is recognized
#   (otherwise, they should signal an error)

sub new {
  my ($class, $cs, $parameters, $test, %traits) = @_;
  my $source = $STATE->getStomach->getGullet->getMouth;
  return bless { cs => $cs, parameters => $parameters, test => $test,
    locator      => $source->getLocator,
    isExpandable => 1,
    %traits }, $class; }

sub getTest {
  my ($self) = @_;
  return $$self{test}; }

sub invoke {
  my ($self, $gullet) = @_;
  # A real conditional must have condition_type set
  if (my $cond_type = $$self{conditional_type}) {
    if (($cond_type eq 'if') || ($cond_type eq 'unless')) {
      return $self->invoke_conditional($gullet); }
    elsif ($cond_type eq 'else') {
      return $self->invoke_else($gullet); }
    elsif ($cond_type eq 'or') {
      return $self->invoke_else($gullet); }
    elsif ($cond_type eq 'fi') {
      return $self->invoke_fi($gullet); } }
  Error('unexpected', $$self{cs}, $gullet,
    "Unknown conditional control sequence " . Stringify($LaTeXML::CURRENT_TOKEN));
  return; }

sub invoke_conditional {
  my ($self, $gullet) = @_;
  # Keep a stack of the conditionals we are processing.
  my $ifid = $STATE->lookupValue('if_count') || 0;
  $STATE->assignValue(if_count => ++$ifid, 'global');
  if ($LaTeXML::IF_LIMIT and $ifid > $LaTeXML::IF_LIMIT) {
    Fatal('timeout', 'if_limit', $self,
      "Conditional limit of $LaTeXML::IF_LIMIT exceeded, infinite loop?"); }
  local $LaTeXML::IFFRAME = { token => $LaTeXML::CURRENT_TOKEN, start => $gullet->getLocator,
    parsing => 1, elses => 0, ifid => $ifid };
  $STATE->unshiftValue(if_stack => $LaTeXML::IFFRAME);
  my $parms = $$self{parameters};
  my @args  = ($parms ? $parms->readArguments($gullet) : ());
  $$LaTeXML::IFFRAME{parsing} = 0;    # Now, we're done parsing the Test clause.
  my $tracing = $STATE->lookupValue('TRACINGCOMMANDS') || $LaTeXML::DEBUG{tracing};
  if ($tracing) {
    Debug('{' . $self->tracingCSName . "} [#$ifid]");
    Debug($self->tracingArgs(@args)) if @args; }
  if (my $test = $self->getTest) {
    my $result = &$test($gullet, @args);
    if ($result) {
      Debug("{true}") if $tracing; }
    else {
      my $to = skipConditionalBody($gullet, -1);
      Debug("{false} [skipped to " . ToString($to) . "]") if $tracing; } }
  # If there's no test, it must be the Special Case, \ifcase
  else {
    my $num = $args[0]->valueOf;
    if ($num > 0) {
      my $to = skipConditionalBody($gullet, $num);
      Debug("{$num} [skipped to " . ToString($to) . "]") if $tracing; } }
  return; }

#======================================================================
# Support for conditionals:

# Skipping for conditionals
#   0 : skip to \fi
#  -1 : skip to \else, if any, or \fi
#   n : skip to n-th \or, if any, or \else, if any, or \fi.

# NOTE that there are 2 kinds of "nested" ifs.
#  \if's inside the body of either the true or false branch
# are easily skipped by tracking a level of if nesting and skipping over the
# same number of \fi as you find \if.
#  \if's that get expanded while evaluating the test clause itself
# are considerably trickier. There's a frame on the if-stack for this \if
# that's above the one we're currently processing; typically the \else & \fi
# may still remain, but we need to either evaluate them a normal
# if we're continuing to follow the true branch, or skip oever them if
# we're trying to find the \else for the false branch.
# The danger is mistaking the \else that's associated with the test clause's \if
# and taking it for the \else that we're skipping to!
# Canonical example:
#   \if\ifx AA XY junk \else blah \fi True \else False \fi
# The inner \ifx should expand to "XY junk", since A==A
# Return the token we've skipped to, and the frame that this applies to.
sub skipConditionalBody {
  my ($gullet, $nskips) = @_;
  my $level = 1;
  my $n_ors = 0;
  my $start = $gullet->getLocator;
  # NOTE: Open-coded manipulation of if_stack!, Gullet and Token's
  # [we're only reading tokens & looking up, so State shouldn't change behind our backs]
  my $stack = $STATE->lookupValue('if_stack');
  while (1) {
    my ($t, $cond_type);
    while ($t = shift(@{ $$gullet{pushback} }) || $$gullet{mouth}->readToken()) {
      $t = $$t[2] if $$t[1] == CC_SMUGGLE_THE;
      if ($LaTeXML::Core::State::CATCODE_ACTIVE_OR_CS[$$t[1]]
        && ($cond_type = $STATE->lookupConditional($t))) {
        last; } }
    last unless $cond_type;
    if ($cond_type eq 'if') {    #  Found a \ifxx of some sort
      $level++; }
    elsif ($cond_type eq 'fi') {    #  Found a \fi
      if ($$stack[0] ne $LaTeXML::IFFRAME) {
        # But is it for a condition nested in the test clause?
        shift(@$stack); }           # then DO pop that conditional's frame; it's DONE!
      elsif (!--$level) {           # If no more nesting, we're done.
        shift(@$stack);             # Done with this frame
        return $t; } }              # AND Return the finishing token.
    elsif ($level > 1) { }                                    # Ignore \else,\or nested in the body.
    elsif (($cond_type eq 'or') && (++$n_ors == $nskips)) {
      return $t; }
    elsif (($cond_type eq 'else') && $nskips
      # Found \else and we're looking for one?
      # Make sure this \else is NOT for a nested \if that is part of the test clause!
      && ($$stack[0] eq $LaTeXML::IFFRAME)) {
      # No need to actually call elseHandler, but note that we've seen an \else!
      $$stack[0]{elses} = 1;
      return $t; } }    # } #}
  Error('expected', '\fi', $gullet, "Missing \\fi or \\else, conditional fell off end",
    "Conditional started at " . ToString($start));
  return; }

sub invoke_else {
  my ($self, $gullet) = @_;
  my $stack = $STATE->lookupValue('if_stack');
  if (!($stack && $$stack[0])) {    # No if stack entry ?
    Error('unexpected', $LaTeXML::CURRENT_TOKEN, $gullet,
      "Didn't expect a " . Stringify($LaTeXML::CURRENT_TOKEN)
        . " since we seem not to be in a conditional");
    return; }
  elsif ($$stack[0]{parsing}) {     # Defer expanding the \else if we're still parsing the test
    return Tokens(T_CS('\relax'), $LaTeXML::CURRENT_TOKEN); }
  elsif ($$stack[0]{elses}) {       # Already seen an \else's at this level?
    Error('unexpected', $LaTeXML::CURRENT_TOKEN, $gullet,
      "Extra " . Stringify($LaTeXML::CURRENT_TOKEN),
"already saw \\else for " . Stringify($$stack[0]{token}) . " [" . $$stack[0]{ifid} . "] at " . ToString($$stack[0]{start}));
    return; }
  else {
    local $LaTeXML::IFFRAME = $$stack[0];
    my $t = skipConditionalBody($gullet, 0);
    Debug('{' . ToString($LaTeXML::CURRENT_TOKEN) . '}'
        . " [for " . ToString($$LaTeXML::IFFRAME{token}) . " #" . $$LaTeXML::IFFRAME{ifid}
        . " skipping to " . ToString($t) . "]")
      if $STATE->lookupValue('TRACINGCOMMANDS') || $LaTeXML::DEBUG{tracing};
    return; } }

sub invoke_fi {
  my ($self, $gullet) = @_;
  my $stack = $STATE->lookupValue('if_stack');
  if (!($stack && $$stack[0])) {    # No if stack entry ?
    Error('unexpected', $LaTeXML::CURRENT_TOKEN, $gullet,
      "Didn't expect a " . Stringify($LaTeXML::CURRENT_TOKEN)
        . " since we seem not to be in a conditional");
    return; }
  elsif ($$stack[0]{parsing}) {     # Defer expanding the \else if we're still parsing the test
    return Tokens(T_CS('\relax'), $LaTeXML::CURRENT_TOKEN); }
  else {                            # "expand" by removing the stack entry for this level
    local $LaTeXML::IFFRAME = $$stack[0];
    $STATE->shiftValue('if_stack');    # Done with this frame
    Debug('{' . ToString($LaTeXML::CURRENT_TOKEN) . '}'
        . " [for " . Stringify($$LaTeXML::IFFRAME{token}) . " #" . $$LaTeXML::IFFRAME{ifid} . "]")
      if $STATE->lookupValue('TRACINGCOMMANDS') || $LaTeXML::DEBUG{tracing};
    return; } }

sub equals {
  my ($self, $other) = @_;
  return (defined $other && (ref $self) eq (ref $other))
    && Equals($self->getParameters, $other->getParameters)
    && Equals($self->getTest,       $other->getTest); }

#===============================================================================
1;

__END__

=pod

=head1 NAME

C<LaTeXML::Core::Definition::Conditional>  - Conditionals Control sequence definitions.

=head1 DESCRIPTION

These represent the control sequences for conditionals, as well as
C<\else>, C<\or> and C<\fi>.
See L<LaTeXML::Package> for the most convenient means to create them.

It extends L<LaTeXML::Core::Definition::Expandable>.

=head1 AUTHOR

Bruce Miller <bruce.miller@nist.gov>

=head1 COPYRIGHT

Public domain software, produced as part of work done by the
United States Government & not subject to copyright in the US.

=cut
