#
# Copyright (c) 2003 Lev A. Serebryakov <lev@serebryakov.spb.ru>
#
#    This module is free software; you can redistribute it and/or modify it
#    under the same terms as Perl itself.
#
# This package contains object to store one `Delta' from RCS file.
# `Deltatext' can be attached to `Delta'.
#
# `Delta' can read itself from given stream.
#
# $Id: Delta.pm 795 2004-01-31 10:38:49Z lev $
#
package Cvs::Repository::Delta;

use strict;
no warnings 'recursion';

use vars qw($VERSION);
$VERSION  = join('.',0,76,('$LastChangedRevision: 795 $' =~ /^\$\s*LastChangedRevision:\s+(\d+)\s*\$$/),'cvs2svn');

use Time::Local;

#
# We need these sub-modules
use Cvs::Repository::Exception qw(:INTERNAL);
use Cvs::Repository::Revision;
use Cvs::Repository::Reader;
use Cvs::Repository::DeltaCache;

sub New
{
  my $proto = shift;
  my $class = ref($proto) || $proto;
  my $self = bless({}, $class);
  
  # We support
  $self->{'rev'}        = undef;
  $self->{'date'}       = '';
  $self->{'author'}     = '';
  $self->{'state'}      = 'Exp';
  $self->{'branches'}   = [];
  $self->{'next'}       = undef;
  $self->{'newphrases'} = {};
  $self->{'deltatext'}  = undef;
  $self->{'_dtb'}       = 0;
  $self->{'_dts'}       = 0;
  $self->{'_tn'}        = undef;
  $self->{'_tp'}        = undef;
  $self->{'_tb'}        = [];
  
  return $self;
}

sub Read
{
  my $self  = shift;
  my $reader = shift;
  my $s;
  # First of all, read revision
  $s = $reader->readWord();
  throw "Could not find revision to start delta" unless defined $s;

  # Check: is it 'desc'?
  if($s eq 'desc') {
    $reader->ungetLine($s);
    return 0;
  }

  eval {
    $self->{'rev'} = Cvs::Repository::Revision->New($s);
  };
  rethrow "Invalid revision '$s' to start delta";

  # Ok, date now
  $s = $reader->readWord();
  throw "Could not find 'date'" unless defined $s;
  throw "Invalid word '$s', 'date' expected" if $s ne 'date';
  $s = $reader->readUpToSemicolon();
  throw "Could not find 'date' data" unless defined $s && $s;
  throw "Unparsable date: '$s'" if $s !~ /^(\d{2,4})\.(\d{2})\.(\d{2})\.(\d{2})\.(\d{2})\.(\d{2})$/;
  my ($year,$month,$day,$hour,$min,$sec) = ($1,$2,$3,$4,$5,$6);
  $year += 0; $month += 0; $day += 0; $hour += 0; $min += 0; $sec += 0;
  $year -= 1900 if $year > 99;
  $month--;
  $self->{'date'} = Time::Local::timegm($sec,$min,$hour,$day,$month,$year);
  throw "Invalid date: '$s' ($year/$month/$day $hour:$min:$sec)" unless defined $self->{'date'};

  # Ok, author now
  $s = $reader->readWord();
  throw "Could not find 'author'" unless defined $s;
  throw "Invalid word '$s', 'author' expected" if $s ne 'author';
  $s = $reader->readUpToSemicolon();
  throw "Could not find 'author' data" unless defined $s && $s;
  $self->{'author'} = $s;

  # Ok, state now
  $s = $reader->readWord();
  throw "Could not find 'state'" unless defined $s;
  throw "Invalid word '$s', 'state' expected" if $s ne 'state';
  $s = $reader->readUpToSemicolon();
  throw "Could not find 'state' data" unless defined $s;
  $self->{'state'} = $s;

  # Ok, branches now
  $s = $reader->readWord();
  throw "Could not find 'branches'" unless defined $s;
  throw "Invalid word '$s', 'branches' expected" if $s ne 'branches';
  $s = $reader->readUpToSemicolon();
  throw "Could not find 'branches' data" unless defined $s;
  foreach my $b (split(/\s+/,$s)) {
    my $rev;
    eval {
      $rev = Cvs::Repository::Revision->New($b);
    };
    rethrow "Invalid revision '$b' in 'branches' field";
    push @{$self->{'branches'}},$rev;
  }

  # Ok, next now
  $s = $reader->readWord();
  throw "Could not find 'next'" unless defined $s;
  throw "Invalid word '$s', 'next' expected" if $s ne 'next';
  $s = $reader->readUpToSemicolon();
  throw "Could not find 'next' data" unless defined $s;
  if($s) {
    eval {
      $self->{'next'} = Cvs::Repository::Revision->New($s);
    };
    rethrow "Invalid revision '$s' in 'next' field";
  }

  $s = $reader->readWord();
  throw "Could not find newphrases, 'desc' or other delta" unless defined $s;
  # And read all newphrases
  while($s !~ /^[0-9.]+$/ && $s ne 'desc') {
    my $str = $reader->readUpToSemicolon();
    throw "Could not find newphrase '$s' data" unless defined $str;
    $self->{'newphrases'}->{$s} = $str;
    $s = $reader->readWord();
    throw "Could not find newphrases, 'desc' or other delta" unless defined $s;
  }
  #
  # put word back
  $reader->ungetLine($s);

  return 1;
}

sub readDeltaText
{
  my ($self,$reader,$readtext) = @_;
  my $s;
  $readtext = 0 unless defined $readtext;
  # Revision already read.
  # We need rea 'log'
  $s = $reader->readWord();
  throw "Could not find 'log'" unless defined $s;
  throw "Invalid word '$s', 'log' expected" if $s ne 'log';
  ($s,undef,undef) = $reader->readString();
  throw "Could not find 'log' string" unless defined $s;
  $self->{'log'} = $s;

  $s = $reader->readWord();
  throw "Could not find newphrases, or 'text' in deltatext" unless defined $s;
  # And read all newphrases
  while($s ne 'text') {
    my $str = $reader->readUpToSemicolon();
    throw "Could not find newphrase '$s' data" unless defined $str;
    $self->{'newdeltaphrases'}->{$s} = $str;
    $s = $reader->readWord();
    throw "Could not find newphrases, or 'text' in deltatext" unless defined $s;
  }
  
  # Text is here
  my ($st,$sz);
  if($readtext) {
    ($s,$st,$sz) = $reader->readString();
    throw "Could not find 'text' string" unless defined $s;
    $self->{'deltatext'} = $s;
  } else {
    ($st,$sz) = $reader->skipString();
    throw "Could not find 'text' string" unless defined $st;
    $self->{'deltatext'} = undef;
  }
  $self->{'_dtb'} = $st;
  $self->{'_dts'} = $sz;
  return;
}

sub buildTree
{
  my ($self,$pool) = @_;
  my $next = undef;

  # Use loop, instead of recursion.
  while(defined $self) {
    if($self->{'rev'}->type() == Cvs::Repository::Revision::TYPE_TRUNK) {
      # 'next' field is link BACK
      if(defined $self->{'next'}) {
        my $n = $self->{'next'}->getString();
        throw "Could not find 'next' for '$self->{'rev'}' in pool" unless exists $pool->{$n};
        # Ok, and check that 'next' is really prev
        throw "Invalid 'next' field '$n' for '$self->{'rev'}' (must be less)" if $self->{'rev'} <= $self->{'next'};
        # Link
        $self->{'_tp'} = $pool->{$n};
        $pool->{$n}->{'_tn'} = $self;
        # Go back
        $next = $self->{'_tp'};
      }
    } else {
      # 'next' field is link FORWARD
      if(defined $self->{'next'}) {
        my $n = $self->{'next'}->getString();
        throw "Could not find 'next' for '$self->{'rev'}' in pool" unless exists $pool->{$n};
        # Ok, and check that 'next' is really next
        throw "Invalid 'next' field '$n' for '$self->{'rev'}' (must be greater)" if $self->{'rev'} >= $self->{'next'};
        # Link
        $self->{'_tn'} = $pool->{$n};
        $pool->{$n}->{'_tp'} = $self;
        # go forward
        $next = $self->{'_tn'};
      }
    }
    
    # Ok, Now build all branches
    foreach my $b (@{$self->{'branches'}}) {
      throw "There are no delta '$b' which is branch in '$self->{'rev'}'" unless exists $pool->{$b};
      # Previous is me :)
      $pool->{$b}->{'_tp'} = $self;
      push @{$self->{'_tb'}},$pool->{$b};
      # And build tree for branch
      $pool->{$b}->buildTree($pool);
    }
    $self = $next;
    $next = undef;
  }  
  return;
}

sub reReadDeltaText
{
  my ($self,$reader) = @_;
  # Check, was deltatext found or not?
  throw "reReadDeltaText() was called before readDeltaText" unless $self->{'_dtb'};
  # Don't read it again
  return if defined $self->{'deltatext'};
  $self->{'deltatext'} = $reader->readStringFrom($self->{'_dtb'},$self->{'_dts'});
}

sub discardDeltaText
{
  $_[0]->{'deltatext'} = undef unless defined $_[0]->{'deltatext'};
}

sub applyDeltaText
{
  my ($self,$cache,$text,$reader) = @_;
  my $chunks;
  
  # May be deltatext is in cache?
  if(defined $cache && defined ($chunks = $cache->getDiff($self))) {
    # Nothing to do
  } else {
    my $size;
    ($chunks,$size) = $self->_prepareChunks($reader);
    # And add to cache
    $cache->addDiff($self,$chunks,$size) if defined $cache && $cache->diffsEnabled();
  }
  
  # And we should apply these chunks in reverse order
  for(my $c = $#{$chunks}; $c >= 0; --$c) {
    if($chunks->[$c]->{'o'} eq 'a') {
      # Add lines
      splice(@{$text},$chunks->[$c]->{'f'},0,@{$chunks->[$c]->{'l'}});
    } else {
      # Delete lines
      splice(@{$text},$chunks->[$c]->{'f'}-1,$chunks->[$c]->{'c'});
    }
  }
  # Return new text as reference to array of lines
  return $text;
}

sub rev
{
  return $_[0]->{'rev'};
}

sub date
{
  return $_[0]->{'date'};
}

sub author
{
  return $_[0]->{'author'};
}

sub state
{
  return $_[0]->{'state'};
}

sub branches
{
  return @{$_[0]->{'branches'}};
}

sub next
{
  return $_[0]->{'next'};
}

sub newphrase
{
  return %{$_[0]->{'newphrases'}} unless defined $_[1] && $_[1];
  return undef unless exists $_[0]->{'newphrases'}->{$_[1]};
  return $_[0]->{'newphrases'}->{$_[1]};
}

sub log
{
  return $_[0]->{'log'};
}

sub deltatext
{
  return $_[0]->{'deltatext'};
}

sub newdeltaphrase
{
  return %{$_[0]->{'newdeltaphrases'}} unless defined $_[1] && $_[1];
  return undef unless exists $_[0]->{'newdeltaphrases'}->{$_[1]};
  return $_[0]->{'newdeltaphrases'}->{$_[1]};
}

sub treeNext
{
  return $_[0]->{'_tn'};
}

sub treePrev
{
  return $_[0]->{'_tp'};
}

sub treeBranches
{
  return () unless defined $_[0]->{'_tb'};
  return @{$_[0]->{'_tb'}};
}

sub treeStepToHead
{
  if($_[0]->{'rev'}->type() == Cvs::Repository::Revision::TYPE_TRUNK) {
    return $_[0]->{'_tn'};
  } else {
    return $_[0]->{'_tp'};
  }
}

sub _prepareChunks
{
  my ($self,$reader) = @_;
  my $chunks = [];
  my $size = 0;
  # Read our deltatext
  $self->reReadDeltaText($reader);
  $size = length($self->{'deltatext'});
  # Split it into chunks
  my @d = split(/(?<=\n)/,$self->{'deltatext'});
  # And discard it, if needed
  $self->discardDeltaText();
  # And make chunks array
  my $lineno = 0;
  while(@d) {
    my $cmd = shift @d;
    # There MUST BE command: (a|d)\d+ \d+
    throw "Invalid deltatext line $lineno in revision '$self->{'rev'}'" unless $cmd =~ /^(a|d)(\d+)\s+(\d+)\s*$/;
    my $chunk = {'o' => $1, 'f' => $2, 'c' => $3};
    # If it is 'add' chunk, add lines
    if($chunk->{'o'} eq 'a') {
      $chunk->{'l'} = [ splice(@d,0,$chunk->{'c'}) ];
      throw "Not enough lines for '$cmd' (line $lineno) in revision '$self->{'rev'}'" unless @{$chunk->{'l'}} == $chunk->{'c'};
      $lineno += $chunk->{'c'};
    }
    push @${chunks}, $chunk;
    # AVG 16 byttes per hash
    $size += 16;
  }
  return ($chunks, $size);
}

sub breakAllLinks
{
  $_[0]->{'_tn'} = undef;
  $_[0]->{'_tp'} = undef;
  for(my $i = 0; defined $_[0]->{'_tb'} && $i < @{$_[0]->{'_tb'}}; ++$i) {
    $_[0]->{'_tb'}->[$i] = undef;
  }
  $_[0]->{'_tb'} = undef;
}

sub DESTROY
{
  $_[0]->breakAllLinks();
}

1;
