package PatchReader::Raw;

#
# USAGE:
# use PatchReader::Raw;
# my $parser = new PatchReader::Raw();
# $parser->sends_data_to($my_target);
# $parser->start_lines();
# open FILE, "mypatch.patch";
# while (<FILE>) {
#   $parser->next_line($_);
# }
# $parser->end_lines();
#

use strict;

use PatchReader::Base;

@PatchReader::Raw::ISA = qw(PatchReader::Base);

sub new {
  my $class = shift;
  $class = ref($class) || $class;
  my $this  = $class->SUPER::new();
  bless $this, $class;

  return $this;
}

# We send these notifications:
# start_patch({ patchname })
# start_file({ filename, rcs_filename, old_revision, old_date_str, new_revision, new_date_str, is_add, is_remove })
# next_section({ old_start, new_start, old_lines, new_lines, @lines })
# end_patch
# end_file
sub next_line {
  my $this = shift;
  my ($line) = @_;

  return if $line =~ /^\?/;

  # patch header parsing
  if ($line =~ /^---\s*(\S+)\s*\t([^\t\r\n]*)\s*(\S*)/) {
    if ($1 eq "/dev/null") {
      $this->{FILE_STATE}{is_add} = 1;
    } else {
      $this->{FILE_STATE}{filename} = $1;
    }
    $this->{FILE_STATE}{old_date_str} = $2;
    $this->{FILE_STATE}{old_revision} = $3 if $3;

    $this->{IN_HEADER} = 1;

  } elsif ($line =~ /^\+\+\+\s*(\S+)\s*\t([^\t\r\n]*)(\S*)/) {
    if ($1 eq "/dev/null") {
      $this->{FILE_STATE}{is_remove} = 1;
    }
    $this->{FILE_STATE}{new_date_str} = $2;
    $this->{FILE_STATE}{new_revision} = $3 if $3;

    $this->{IN_HEADER} = 1;

  } elsif ($line =~ /^RCS file: (.+)/) {
    $this->{FILE_STATE}{rcs_filename} = $1;

    $this->{IN_HEADER} = 1;

  } elsif ($line =~ /^retrieving revision (\S+)/) {
    $this->{FILE_STATE}{old_revision} = $1;

    $this->{IN_HEADER} = 1;

  } elsif ($line =~ /^Index:\s*(.+)/) {
    $this->_maybe_end_file();

    $this->{FILE_STATE}{filename} = $1;

    $this->{IN_HEADER} = 1;

  } elsif ($line =~ /^diff\s*(-\S+\s*)*(\S+)\s*(\S*)/ && $3) {
    # Simple diff <dir> <dir>
    $this->_maybe_end_file();
    $this->{FILE_STATE}{filename} = $2;

    $this->{IN_HEADER} = 1;

  # section parsing
  } elsif ($line =~ /^@@\s*-(\d+),?(\d*)\s*\+(\d+),?(\d*)\s*(?:@@\s*(.*))?/) {
    $this->{IN_HEADER} = 0;

    $this->_maybe_start_file();
    $this->_maybe_end_section();
    $2 = 0 if !defined($2);
    $4 = 0 if !defined($4);
    $this->{SECTION_STATE} = { old_start => $1, old_lines => $2,
                               new_start => $3, new_lines => $4,
                               func_info => $5,
                               minus_lines => 0, plus_lines => 0,
                             };

  } elsif ($line =~ /^(\d+),?(\d*)([acd])(\d+),?(\d*)/) {
    # Non-universal diff.  Calculate as though it were universal.
    $this->{IN_HEADER} = 0;

    $this->_maybe_start_file();
    $this->_maybe_end_section();

    my $old_start;
    my $old_lines;
    my $new_start;
    my $new_lines;
    if ($3 eq 'a') {
      # 'a' has the old number one off from diff -u ("insert after this line"
      # vs. "insert at this line")
      $old_start = $1 + 1;
      $old_lines = 0;
    } else {
      $old_start = $1;
      $old_lines = $2 ? ($2 - $1 + 1) : 1;
    }
    if ($3 eq 'd') {
      # 'd' has the new number one off from diff -u ("delete after this line"
      # vs. "delete at this line")
      $new_start = $4 + 1;
      $new_lines = 0;
    } else {
      $new_start = $4;
      $new_lines = $5 ? ($5 - $4 + 1) : 1;
    }

    $this->{SECTION_STATE} = { old_start => $old_start, old_lines => $old_lines,
                               new_start => $new_start, new_lines => $new_lines,
                               minus_lines => 0, plus_lines => 0
                             };

  # line parsing
  } elsif ($line =~ /^ /) {
    push @{$this->{SECTION_STATE}{lines}}, $line;
  } elsif ($line =~ /^-($|[^-])/) {
    $this->{SECTION_STATE}{minus_lines}++;
    push @{$this->{SECTION_STATE}{lines}}, $line;
  } elsif ($line =~ /^\+($|[^+])/) {
    $this->{SECTION_STATE}{plus_lines}++;
    push @{$this->{SECTION_STATE}{lines}}, $line;
  } elsif ($line =~ /^< /) {
    $this->{SECTION_STATE}{minus_lines}++;
    push @{$this->{SECTION_STATE}{lines}}, "-" . substr($line, 2);
  } elsif ($line =~ /^> /) {
    $this->{SECTION_STATE}{plus_lines}++;
    push @{$this->{SECTION_STATE}{lines}}, "+" . substr($line, 2);
  }
}

sub start_lines {
  my $this = shift;
  die "No target specified: call sends_data_to!" if !$this->{TARGET};
  delete $this->{FILE_STARTED};
  delete $this->{FILE_STATE};
  delete $this->{SECTION_STATE};
  $this->{FILE_NEVER_STARTED} = 1;

  $this->{TARGET}->start_patch(@_);
}

sub end_lines {
  my $this = shift;
  $this->_maybe_end_file();
  $this->{TARGET}->end_patch(@_);
}

sub _maybe_start_file {
  my $this = shift;
  if (exists($this->{FILE_STATE}) && !$this->{FILE_STARTED} ||
      $this->{FILE_NEVER_STARTED}) {
    $this->_start_file();
  }
}

sub _maybe_end_file {
  my $this = shift;
  return if $this->{IN_HEADER};

  $this->_maybe_end_section();
  if (exists($this->{FILE_STATE})) {
    # Handle empty patch sections (if the file has not been started and we're
    # already trying to end it, start it first!)
    if (!$this->{FILE_STARTED}) {
      $this->_start_file();
    }
    
    # Send end notification and set state
    $this->{TARGET}->end_file($this->{FILE_STATE});
    delete $this->{FILE_STATE};
    delete $this->{FILE_STARTED};
  }
}

sub _start_file {
  my $this = shift;

  # Send start notification and set state
  if (!$this->{FILE_STATE}) {
    $this->{FILE_STATE} = { filename => "file_not_specified_in_diff" };
  }

  # Send start notification and set state
  $this->{TARGET}->start_file($this->{FILE_STATE});
  $this->{FILE_STARTED} = 1;
  delete $this->{FILE_NEVER_STARTED};
}

sub _maybe_end_section {
  my $this = shift;
  if (exists($this->{SECTION_STATE})) {
    $this->{TARGET}->next_section($this->{SECTION_STATE});
    delete $this->{SECTION_STATE};
  }
}

sub iterate_file {
  my $this = shift;
  my ($filename) = @_;

  open FILE, $filename or die "Could not open $filename: $!";
  $this->start_lines($filename);
  while (<FILE>) {
    $this->next_line($_);
  }
  $this->end_lines($filename);
  close FILE;
}

sub iterate_fh {
  my $this = shift;
  my ($fh, $filename) = @_;

  $this->start_lines($filename);
  while (<$fh>) {
    $this->next_line($_);
  }
  $this->end_lines($filename);
}

sub iterate_string {
  my $this = shift;
  my ($id, $data) = @_;

  $this->start_lines($id);
  while ($data =~ /([^\n]*(\n|$))/g) {
    $this->next_line($1);
  }
  $this->end_lines($id);
}

1
