package Brackup::PositionedChunk;

use strict;
use warnings;
use Carp qw(croak);
use Digest::SHA1 qw(sha1_hex);

use fields (
            'file',     # the Brackup::File object
            'offset',   # offset within said file
            'length',   # length of data
            '_raw_digest',
            '_raw_chunkref',
            );

sub new {
    my ($class, %opts) = @_;
    my $self = ref $class ? $class : fields::new($class);

    $self->{file}   = delete $opts{'file'};    # Brackup::File object
    $self->{offset} = delete $opts{'offset'};
    $self->{length} = delete $opts{'length'};

    croak("Unknown options: " . join(', ', keys %opts)) if %opts;
    croak("offset not numeric") unless $self->{offset} =~ /^\d+$/;
    croak("length not numeric") unless $self->{length} =~ /^\d+$/;
    return $self;
}

sub as_string {
    my $self = shift;
    return $self->{file}->as_string . "{off=$self->{offset},len=$self->{length}}";
}

# the original length, pre-encryption
sub length {
    my $self = shift;
    return $self->{length};
}

sub offset {
    my $self = shift;
    return $self->{offset};
}

sub file {
    my $self = shift;
    return $self->{file};
}

sub root {
    my $self = shift;
    return $self->file->root;
}

sub raw_digest {
    my $self = shift;
    return $self->{_raw_digest} ||= $self->_calc_raw_digest;
}

sub _calc_raw_digest {
    my $self = shift;

    my $n_chunks = $self->{file}->chunks
        or die "zero chunks?";
    if ($n_chunks == 1) {
        # don't calculate this chunk's digest.. it's the same as our
        # file's digest, since this chunk spans the entire file.
        die "ASSERT" unless $self->length == $self->{file}->size;
        return $self->{file}->full_digest;
    }

    my $cache = $self->root->digest_cache;
    my $key   = $self->cachekey;
    my $dig;

    if ($dig = $cache->get($key)) {
        return $self->{_raw_digest} = $dig;
    }

    my $rchunk = $self->raw_chunkref;
    $dig = "sha1:" . sha1_hex($$rchunk);

    $cache->set($key => $dig);

    return $self->{_raw_digest} = $dig;
}

sub raw_chunkref {
    my $self = shift;
    return $self->{_raw_chunkref} if $self->{_raw_chunkref};

    my $data;
    my $fullpath = $self->{file}->fullpath;
    open(my $fh, $fullpath) or die "Failed to open $fullpath: $!\n";
    binmode($fh);
    seek($fh, $self->{offset}, 0) or die "Couldn't seek: $!\n";
    my $rv = read($fh, $data, $self->{length})
        or die "Failed to read: $!\n";
    unless ($rv == $self->{length}) {
        Carp::confess("Read $rv bytes, not $self->{length}");
    }

    return $self->{_raw_chunkref} = \$data;
}

# useful string for targets to key on.  of one of the forms:
#    "<digest>;to=<enc_to>"
#    "<digest>;raw"
#    "<digest>;gz"   (future)
sub inventory_key {
    my $self = shift;
    my $key = $self->raw_digest;
    if (my $rcpt = $self->root->gpg_rcpt) {
        $key .= ";to=$rcpt";
    } else {
        $key .= ";raw";
    }
    return $key;
}

sub forget_chunkref {
    my $self = shift;
    delete $self->{_raw_chunkref};
}

sub cachekey {
    my $self = shift;
    return $self->{file}->cachekey . ";o=$self->{offset};l=$self->{length}";
}

sub is_entire_file {
    my $self = shift;
    return $self->{file}->chunks == 1;
}

1;
