#!/usr/bin/perl -c

# Package to handle Cyrus Skiplist files

package Cyrus::Skiplist;

use strict;
use warnings;

use IO::File;
use IO::File::fcntl;
use IO::Handle;
use File::Temp;
use Data::Dumper;

our $HL = qq{\241\002\213\015skiplist file\0\0\0};
our @HF = qw(major_version minor_version maxlevel curlevel listsize logstart lastrecovery);

our %TYPES = (
  1 => 'INORDER',
  2 => 'ADD',
  4 => 'DELETE',
  255 => 'COMMIT',
  257 => 'DUMMY',
);

# PUBLIC API

sub new {
  my $class = shift;
  my $handle = shift;

  my $buf;
  seek($handle, 0, 0);
  my $bytes = read($handle, $buf, 28);
  unless ($bytes == 28) {
    die "Failed to read initial version information\n";
  }
  my $major = unpack('N', substr($buf, 20, 4));
  my $minor = unpack('N', substr($buf, 24, 4));

  # XXX - version support
  if ($major == 1 and $minor == 2) {
    my $rest = read($handle, $buf, 20, 28);
    unless ($rest == 20) {
      die "Failed to read rest of header\n";
    }
  }

  my $Self = bless {
    headerbuf => $buf,
    handle => $handle,
    filesize => -s $handle,
    offset => 48,
  }, ref($class) || $class;

  $Self->{header} = $Self->parse_header($buf);

  return $Self;
}

sub new_file {
  my $class = shift;
  my $file = shift;
  my $lockopts = shift; 

  my $fh;
  if ($lockopts) {
    $lockopts = ['lock_ex'] unless ref($lockopts) eq 'ARRAY';
    $fh = IO::File::fcntl->new($file, '+<', @$lockopts)
          || die "Can't open $file for locked read: $!";
  } else {
    $fh = IO::File->new("< $file")
          || die "Can't open $file for read: $!";
  }

  return $class->new($fh);
}

sub header {
  my $Self = shift;
  my $Field = shift;

  if ($Field) {
    return $Self->{header}{$Field};
  }

  return $Self->{header};
}

sub write_header {
  my $Self = shift;
  my $fh = shift;
  my $header = shift;

  $fh->print($Self->make_header($header));
}

sub make_header {
  my $Self = shift;
  my $fields = shift;

  my @items;
  foreach my $n (0..$#HF) {
    $items[$n] = $fields->{$HF[$n]};
  }

  return $HL . pack('N' x @HF, @items);
}

sub parse_header {
  my $Self = shift;
  my $buf = shift;
  my %ret;

  $ret{magic} = substr($buf, 0, 20);
  my @items = unpack('N' x @HF, substr($buf, 20));
  foreach my $n (0..$#HF) {
    $ret{$HF[$n]} = $items[$n];
  }

  return \%ret;
}

sub next_record {
  my $Self = shift;
  my $offset = shift;

  # allow random reads
  if ($offset and $offset != $Self->{offset}) {
    seek($Self->{handle}, $offset, 0);
    $Self->{offset} = $offset;
  }

  return if $Self->{offset} == $Self->{filesize};
  
  my %record;
  $record{offset} = $Self->{offset};

  my $type = $Self->one_num();
  $record{typenum} = $type;
  $record{type} = $TYPES{$type};

  if ($type == 1) {
    $Self->get_vals(\%record);
  }
  elsif ($type == 2) {
    $Self->get_vals(\%record);
  }
  elsif ($type == 4) {
    $record{ptr} = $Self->one_num();
  }
  elsif ($type == 255) {
    # commit is a blank record
  }
  elsif ($type == 257) {
    $Self->get_vals(\%record);
  } else {
    die "ODD TYPE $type at $Self->{offset}\n";    
  }

  return \%record;
}

sub get_vals {
  my $Self = shift;
  my $record = shift;

  $record->{key} = $Self->one_str();
  $record->{value} = $Self->one_str();
  $record->{ptrs} = [];
  for (0..$Self->{header}{maxlevel}) {
    my $val = $Self->one_num();
    return if $val == 0xFFFFFFFF; # -1
    push @{$record->{ptrs}}, $val;
  }
  die "Went over 20 pointers with no final record!\n";
}

sub one_num {
  my $Self = shift;
  my $buf;
  my $bytes = read($Self->{handle}, $buf, 4);
  die "Failed to read a num at $Self->{offset}\n" unless $bytes == 4;
  $Self->{offset} += 4;

  my $num = unpack('N', $buf);
  return $num;
}

sub one_str {
  my $Self = shift;

  my $size = $Self->one_num();
  my $bytes = _roundup($size);

  my $buf = '';
  my $offset = 0;
  while ($bytes > $offset) {
    my $n = read($Self->{handle}, $buf, $bytes - $offset, $offset);
    $offset += $n;
  }
  $Self->{offset} += $offset;

  return substr($buf, 0, $size); # strip trailing \0's
}

sub dump {
  my $Self = shift;
  use Data::Dumper;
  print Dumper($Self->{header});
  while (my $record = $Self->next_record()) {
    print Dumper($record);
  }
}


sub _roundup {
  my $n = shift;
  if (my $val = $n % 4) {
    return $n + 4 - $val;
  }
  return $n;
}

1;
