use base ("Understand::Codecheck");
use strict;
use constant ERR1 => "Switch Incorrect: Case detected after default";
use constant ERR2 => "Switch Incorrect: No default case";

sub register_tr_text {
  my $check = shift;
  $check->add_tr_text(ERR1);
  $check->add_tr_text(ERR2);
}


sub name {
  return "6-4-6 The final clause of a switch statement shall be the default-clause";
}

sub description {
  return "6-4-6(Required) The final clause of a switch statement shall be the default-clause.";
}

sub detailed_description {
  return <<"END_DESC"
<p><b>Rationale</b><br>
The requirement for a final default-clause is defensive programming. This clause shall either take 
appropriate action, or else contain a suitable comment as to why no action is taken.</p>
<p><b>Exception</b><br>
If the condition of a switch statement is of type enum, and all the enumerators are listed in case 
labels, then the default-clause is not required as the rules associated with enums are intended to 
ensure that the enum cannot be assigned values outside of its set of enumerators. Note that it may 
still be appropriate to include a default-clause for the purpose of defensive programming.
</p><b>Example</b><pre style="margin-top:0;padding-top:0;">
  switch ( int16 ) 
  { 
  case 0: 
     break;  
  case 1:  
  case 2: 
     break; 
                   // Non-compliant - default clause is required. 
   }
  enum Colours { RED, BLUE, GREEN } colour;
  switch ( colour ) 
  { 
     case RED: 
        break; 
     case GREEN: 
        break; 
                   // Non-compliant - default clause is required. 
  }  </pre>
END_DESC
}

sub test_language {
  my $language = shift;
  return $language =~ /C\+\+/;
}

sub test_entity {
  return 1;
}

sub test_global {
  return 0;
}

sub check {
  my $check = shift;
  my $file = shift;
  return unless $file->kind->check("file ~unresolved ~unknown");
  # create lexer once for file
  my $lexer = $file->lexer();
  return unless $lexer;
  # loop through functions defined in the file
  foreach my $ref ($file->filerefs("define","function",1)) {
      my $func = $ref->ent();
      my ($begin,$end) = getFunctionDefnLines($func);
      next if (!$begin);
      do_one_function($check,$file,$func,$lexer,$begin,$end);
  }  
}


# Check one function.
sub do_one_function {
    my ($check,$file,$func,$lexer,$begin,$end) = @_;

    setupLexemes($lexer,$begin,$end);
    while (my $lexeme = nextLexeme()) {
    handle_switch($check,$file,$func) if $lexeme->text() eq "switch";
    }
}


# Handle a switch statement. Begin with current lexeme at the 'switch'
# token and end with current lexeme at the closing '}' of the switch.
sub handle_switch {
    my ($check,$file,$func) = @_;

    # skip to first curly brace
    my $lexeme = nextLexeme() or return;
    while ($lexeme->text() ne "{") {
  $lexeme = nextLexeme() or return;
    }
    # process everything until closing brace
    my $seen_default=0;
    my $choice_okay=1;
    while ($lexeme = nextLexeme(1)) { # allow comment lexeme
  my $text = $lexeme->text();
  last if $text eq "}";
  if ($text eq "case") {
      if ($seen_default) {
        $check->violation($func,$file,$lexeme->line_end,$lexeme->column_end,ERR1);        
      }
      $choice_okay = 0;
      $lexeme = nextLexeme() or return 0 while $lexeme->text() ne ":";
  } elsif  ($text eq "default") {
      $seen_default = 1;
      $choice_okay = 0;
      $lexeme = nextLexeme() or return 0 while $lexeme->text() ne ":";
  } elsif ($lexeme->token() eq "Comment") {
      $choice_okay = 1;
  } else {
      $choice_okay = handle_statement($check,$file,$func);
  }
    }
    $lexeme = lastLexeme() if !$lexeme;
    if (!$seen_default) {
      $check->violation($func,$file,$lexeme->line_end,$lexeme->column_end,ERR2);
    }
}


# Handle a statement, within a switch context.
# Begin with current lexeme (passed in) at the first token of the statement
# and end with the current lexeme at the last token of the statement.
# Return 1 if a new choice statement is okay after this statement (ie, this
# is a break/return statement or an if/else series of statements where all
# paths end in break/return).
sub handle_statement {
    my ($check,$file,$func) = @_;
    my $lexeme = currentLexeme() or return 0;
    my $text = $lexeme->text();

    # handle 'switch' statement, always returns 0.
    if ($text eq "switch") {
  handle_switch($check,$file,$func);
  return 0;
    }

    # handle 'for' or 'while' statement, always returns 0.
    if ($text eq "for" || $text eq "while") {
  $lexeme = nextLexeme() or return 0 while $lexeme->text() ne "(";
  my $paren=1;
  while ($paren && ($lexeme = nextLexeme())) {
      $text = $lexeme->text();
      if ($text eq "(") {
    ++$paren;
      } elsif ($text eq ")") {
    --$paren;
      }
  }
  nextLexeme();
  handle_statement($check,$file,$func);
  return 0;
    }

    # handle 'do' statement, always returns 0.
    if ($text eq "do") {
  nextLexeme();
  handle_statement($check,$file,$func);
  return 0;
    }

    # handle 'if' statement.
    if ($text eq "if") {
  $lexeme = nextLexeme() or return 0 while $lexeme->text() ne "(";
  my $paren=1;
  while ($paren && ($lexeme = nextLexeme())) {
      $text = $lexeme->text();
      if ($text eq "(") {
    ++$paren;
      } elsif ($text eq ")") {
    --$paren;
      }
  }
  nextLexeme();
  my $okay = handle_statement($check,$file,$func);
  $lexeme = peekNextLexeme() or return 0;
  return 0 if $lexeme->text() ne "else";
  nextLexeme();
  nextLexeme();
  return handle_statement($check,$file,$func) && $okay;
    }

    # handle compound statement
    if ($text eq "{") {
  my $okay=0;
  while (($lexeme = nextLexeme()) && $lexeme->text() ne "}") {
      $okay = handle_statement($check,$file,$func);
  }
  return $okay;
    }

    my $okay = ($text eq "return" || $text eq "break");
    while ($text ne ";") {
  $lexeme = nextLexeme() or return $okay;
  $text = $lexeme->text();
    }

    return $okay;
}


# Pass a function entity. Return an array of:
#   the begin line
#   the end line
#   the defn file entity
# Return undef if this info cannot be provided.
sub getFunctionDefnLines {
    my $func = shift;
    my $begin_ref = $func->ref("definein");
    my $end_ref = $func->ref("end");
    return undef if (!$begin_ref || !$end_ref);
    return ($begin_ref->line(), $end_ref->line(), $begin_ref->file());
}


# Setup the global lexemes array once per function, to use the NextLexeme() sub.
my @lexemes=();
my $lexeme_pos=0;
sub setupLexemes {
    my $lexer = shift;
    my $begin = shift;
    my $end = shift;
    @lexemes = $lexer->lexemes($begin,$end);
    $lexeme_pos = 0;
}

# Return the current lexeme.
sub currentLexeme {
    return $lexemes[$lexeme_pos-1];
}

# Return the last lexeme.
sub lastLexeme {
    return $lexemes[$#lexemes-1]
}

# Peek and return the next lexeme, but do not update the current lexeme.
sub peekNextLexeme {
    my $pos = $lexeme_pos;
    my $next = nextLexeme();
    $lexeme_pos = $pos;
    return $next;
}

# Return the next interesting lexeme or undef when all lexemes are used.
sub nextLexeme {
    my $comment_okay = shift;
    while ($lexeme_pos < $#lexemes) {
  my $lexeme = $lexemes[$lexeme_pos++];
  my $token = $lexeme->token();
  if ($token eq "Preprocessor") {
      while ($lexeme_pos < $#lexemes && $lexemes[$lexeme_pos]->token() ne "Newline") {
    ++$lexeme_pos;
      }
      next;
  } elsif ($comment_okay) {
      next if ($lexeme->token() =~ m/Whitespace|Newline/);
  } else {
      next if ($lexeme->token() =~ m/Comment|Whitespace|Newline/);
  }
  return $lexeme;
    }
    return undef;
}

1;