package Geo::GDAL::FFI::Band;
use v5.10;
use strict;
use warnings;
use Carp;
use FFI::Platypus::Buffer;
use base 'Geo::GDAL::FFI::Object';

our $VERSION = '0.16';

sub DESTROY {
    my $self = shift;
    Geo::GDAL::FFI::_deregister_parent_ref ($$self);
}

sub GetDataType {
    my $self = shift;
    return $Geo::GDAL::FFI::data_types_reverse{Geo::GDAL::FFI::GDALGetRasterDataType($$self)};
}

sub GetWidth {
    my $self = shift;
    Geo::GDAL::FFI::GDALGetRasterBandXSize($$self);
}

sub GetHeight {
    my $self = shift;
    Geo::GDAL::FFI::GDALGetRasterBandYSize($$self);
}

sub GetSize {
    my $self = shift;
    return (
        Geo::GDAL::FFI::GDALGetRasterBandXSize($$self),
        Geo::GDAL::FFI::GDALGetRasterBandYSize($$self)
        );
}

sub GetCategoryNames {
    my $self = shift;
    my $csl = Geo::GDAL::FFI::GDALGetRasterCategoryNames($$self);
    my @names;
    for my $i (0..Geo::GDAL::FFI::CSLCount($csl)-1) {
        push @names, Geo::GDAL::FFI::CSLGetField($csl, $i);
    }
    return @names;
}

sub SetCategoryNames {
    my ($self, @names) = @_;
    my $csl = 0;
    for my $n (@names) {
        $csl = Geo::GDAL::FFI::CSLAddString($csl, $n);
    }
    Geo::GDAL::FFI::GDALSetRasterCategoryNames($$self, $csl);
    Geo::GDAL::FFI::CSLDestroy($csl);
}

sub GetNoDataValue {
    my $self = shift;
    my $b = 0;
    my $v = Geo::GDAL::FFI::GDALGetRasterNoDataValue($$self, \$b);
    return unless $b;
    return $v;
}

sub SetNoDataValue {
    my $self = shift;
    unless (@_) {
        Geo::GDAL::FFI::GDALDeleteRasterNoDataValue($$self);
        return;
    }
    my $v = shift;
    my $e = Geo::GDAL::FFI::GDALSetRasterNoDataValue($$self, $v);
    return unless $e;
    confess Geo::GDAL::FFI::error_msg() // "SetNoDataValue not supported by the driver.";
}

sub GetBlockSize {
    my $self = shift;
    my ($w, $h);
    Geo::GDAL::FFI::GDALGetBlockSize($$self, \$w, \$h);
    return ($w, $h);
}

sub pack_char {
    my $t = shift;
    my $is_big_endian = unpack("h*", pack("s", 1)) =~ /01/; # from Programming Perl
    return ('C', 1) if $t == 1;
    return ($is_big_endian ? ('n', 2) : ('v', 2)) if $t == 2;
    return ('s', 2) if $t == 3;
    return ($is_big_endian ? ('N', 4) : ('V', 4)) if $t == 4;
    return ('l', 4) if $t == 5;
    return ('f', 4) if $t == 6;
    return ('d', 8) if $t == 7;
    # CInt16 => 8,
    # CInt32 => 9,
    # CFloat32 => 10,
    # CFloat64 => 11
}

sub Read {
    my ($self, $xoff, $yoff, $xsize, $ysize, $bufxsize, $bufysize) = @_;
    $xoff //= 0;
    $yoff //= 0;
    my $t = Geo::GDAL::FFI::GDALGetRasterDataType($$self);
    my ($pc, $bytes_per_cell) = pack_char($t);
    my $w;
    $xsize //= Geo::GDAL::FFI::GDALGetRasterBandXSize($$self);
    $ysize //= Geo::GDAL::FFI::GDALGetRasterBandYSize($$self);
    $bufxsize //= $xsize;
    $bufysize //= $ysize;
    $w = $bufxsize * $bytes_per_cell;
    my $buf = ' ' x ($bufysize * $w);
    my ($pointer, $size) = scalar_to_buffer $buf;
    Geo::GDAL::FFI::GDALRasterIO($$self, $Geo::GDAL::FFI::Read, $xoff, $yoff, $xsize, $ysize, $pointer, $bufxsize, $bufysize, $t, 0, 0);
    my $offset = 0;
    my @data;
    for my $y (0..$bufysize-1) {
        my @d = unpack($pc."[$bufxsize]", substr($buf, $offset, $w));
        push @data, \@d;
        $offset += $w;
    }
    return \@data;
}

sub ReadBlock {
    my ($self, $xoff, $yoff, $xsize, $ysize, $t) = @_;
    $xoff //= 0;
    $yoff //= 0;
    Geo::GDAL::FFI::GDALGetBlockSize($$self, \$xsize, \$ysize) unless defined $xsize;
    $t = Geo::GDAL::FFI::GDALGetRasterDataType($$self) unless defined $t;
    my $buf;
    my ($pc, $bytes_per_cell) = pack_char($t);
    my $w = $xsize * $bytes_per_cell;
    $buf = ' ' x ($ysize * $w);
    my ($pointer, $size) = scalar_to_buffer $buf;
    Geo::GDAL::FFI::GDALReadBlock($$self, $xoff, $yoff, $pointer);
    my $offset = 0;
    my @data;
    for my $y (0..$ysize-1) {
        my @d = unpack($pc."[$xsize]", substr($buf, $offset, $w));
        push @data, \@d;
        $offset += $w;
    }
    return \@data;
}

sub Write {
    my ($self, $data, $xoff, $yoff, $xsize, $ysize) = @_;
    $xoff //= 0;
    $yoff //= 0;
    my $bufxsize = @{$data->[0]};
    my $bufysize = @$data;
    $xsize //= $bufxsize;
    $ysize //= $bufysize;
    my $t = Geo::GDAL::FFI::GDALGetRasterDataType($$self);
    my ($pc, $bytes_per_cell) = pack_char($t);
    my $buf = '';
    for my $i (0..$bufysize-1) {
        $buf .= pack($pc."[$bufxsize]", @{$data->[$i]});
    }
    my ($pointer, $size) = scalar_to_buffer $buf;
    Geo::GDAL::FFI::GDALRasterIO($$self, $Geo::GDAL::FFI::Write, $xoff, $yoff, $xsize, $ysize, $pointer, $bufxsize, $bufysize, $t, 0, 0);
}

sub WriteBlock {
    my ($self, $data, $xoff, $yoff) = @_;
    my ($xsize, $ysize);
    Geo::GDAL::FFI::GDALGetBlockSize($$self, \$xsize, \$ysize);
    my $t = Geo::GDAL::FFI::GDALGetRasterDataType($$self);
    my ($pc, $bytes_per_cell) = pack_char($t);
    my $buf = '';
    for my $i (0..$ysize-1) {
        $buf .= pack($pc."[$xsize]", @{$data->[$i]});
    }
    my ($pointer, $size) = scalar_to_buffer $buf;
    Geo::GDAL::FFI::GDALWriteBlock($$self, $xoff, $yoff, $pointer);
}

sub GetColorInterpretation {
    my $self = shift;
    return $Geo::GDAL::FFI::color_interpretations_reverse{
        Geo::GDAL::FFI::GDALGetRasterColorInterpretation($$self)
    };
}

sub SetColorInterpretation {
    my ($self, $i) = @_;
    my $tmp = $Geo::GDAL::FFI::color_interpretations{$i};
    confess "Unknown color interpretation: $i." unless defined $tmp;
    $i = $tmp;
    Geo::GDAL::FFI::GDALSetRasterColorInterpretation($$self, $i);
}

sub GetColorTable {
    my $self = shift;
    my $ct = Geo::GDAL::FFI::GDALGetRasterColorTable($$self);
    return unless $ct;
    # color table is a table of [c1...c4]
    # the interpretation of colors is from next method
    my @table;
    for my $i (0..Geo::GDAL::FFI::GDALGetColorEntryCount($ct)-1) {
        my $c = Geo::GDAL::FFI::GDALGetColorEntry($ct, $i);
        push @table, $c;
    }
    return wantarray ? @table : \@table;
}

sub SetColorTable {
    my ($self, $table) = @_;
    my $ct = Geo::GDAL::FFI::GDALCreateColorTable();
    for my $i (0..$#$table) {
        Geo::GDAL::FFI::GDALSetColorEntry($ct, $i, $table->[$i]);
    }
    Geo::GDAL::FFI::GDALSetRasterColorTable($$self, $ct);
    Geo::GDAL::FFI::GDALDestroyColorTable($ct);
}

sub GetPiddle {
    require PDL::Lite;  #  minimal load
    my ($self, $xoff, $yoff, $xsize, $ysize, $xdim, $ydim, $alg) = @_;
    $xoff //= 0;
    $yoff //= 0;
    my ($w, $h) = $self->GetSize;
    $xsize //= $w - $xoff;
    $ysize //= $h - $yoff;
    my $t = Geo::GDAL::FFI::GDALGetRasterDataType($$self);
    my $pdl_t = $Geo::GDAL::FFI::data_type2pdl_data_type{$Geo::GDAL::FFI::data_types_reverse{$t}};
    confess "The Piddle data_type is unsuitable." unless defined $pdl_t;
    $xdim //= $xsize;
    $ydim //= $ysize;
    $alg //= 'NearestNeighbour';
    my $tmp = $Geo::GDAL::FFI::resampling{$alg};
    confess "Unknown resampling scheme: $alg." unless defined $tmp;
    $alg = $tmp;
    my $bufxsize = $xsize;
    my $bufysize = $ysize;
    my ($pc, $bytes_per_cell) = pack_char($t);
    my $buf = ' ' x ($bufysize * $bufxsize * $bytes_per_cell);
    my ($pointer, $size) = scalar_to_buffer $buf;
    Geo::GDAL::FFI::GDALRasterIO($$self, $Geo::GDAL::FFI::Read, $xoff, $yoff, $xsize, $ysize, $pointer, $bufxsize, $bufysize, $t, 0, 0);
    my $pdl;
    if ($PDL::VERSION >= 2.091) {
        $pdl = PDL->new_around_datasv(0+\$buf);
        $pdl->set_datatype($pdl_t);
        $pdl->setdims([$xdim, $ydim]);
    }
    else {
        $pdl = PDL->new;
        $pdl->set_datatype($pdl_t);
        $pdl->setdims([ $xdim, $ydim ]);
        my $data = $pdl->get_dataref();
        # FIXME: see http://pdl.perl.org/PDLdocs/API.html how to wrap $buf into a piddle
        $$data = $buf;
        $pdl->upd_data;
    }
    # FIXME: we want approximate equality since no data value can be very large floating point value
    my $bad = GetNoDataValue($self);
    return $pdl->setbadif($pdl == $bad) if defined $bad;
    return $pdl;
}

sub SetPiddle {
    my ($self, $pdl, $xoff, $yoff, $xsize, $ysize) = @_;
    $xoff //= 0;
    $yoff //= 0;
    my ($w, $h) = $self->GetSize;
    my $t = $Geo::GDAL::FFI::pdl_data_type2data_type{$pdl->get_datatype};
    confess "The Piddle data_type '".$pdl->get_datatype."' is unsuitable." unless defined $t;
    $t = $Geo::GDAL::FFI::data_types{$t};
    my ($xdim, $ydim) = $pdl->dims();
    $xsize //= $xdim;
    $ysize //= $ydim;
    if ($xdim > $w - $xoff) {
        warn "Piddle too wide ($xdim) for this raster band (width = $w, offset = $xoff).";
        $xdim = $w - $xoff;
    }
    if ($ydim > $h - $yoff) {
        $ydim = $h - $yoff;
        warn "Piddle too tall ($ydim) for this raster band (height = $h, offset = $yoff).";
    }
    my $data = $pdl->get_dataref();
    my ($pointer, $size) = scalar_to_buffer $$data;
    Geo::GDAL::FFI::GDALRasterIO($$self, $Geo::GDAL::FFI::Write, $xoff, $yoff, $xsize, $ysize, $pointer, $xdim, $ydim, $t, 0, 0);
}

1;

=pod

=encoding UTF-8

=head1 NAME

Geo::GDAL::FFI::Band - A GDAL raster band

=head1 SYNOPSIS

=head1 DESCRIPTION

A band (channel) in a raster dataset. Use the Band method of a dataset
object to obtain a band object.

=head1 METHODS

=head2 GetDataType

 my $datatype = $band->GetDataType;

=head2 GetSize

 my @size = $band->GetSize;

=head2 GetBlockSize

 my @size = $band->GetBlockSize;

=head2 GetNoDataValue

 my $nodata = $band->GetNoDataValue;

=head2 SetNoDataValue

 $band->SetNoDataValue($value);

Calling the method without arguments deletes the nodata value.

 $band->SetNoDataValue;

=head2 Read

 my $data = $band->Read($xoff, $yoff, $xsize, $ysize, $bufxsize, $bufysize);

All arguments are optional. If no arguments are given, reads the whole
raster band into a 2D Perl array. The returned array is an array of
references to arrays of row values.

=head2 ReadBlock

 my $data = $band->ReadBlock($xoff, $yoff, @blocksize, $datatype);

Reads a block of data from the band and returns it as a Perl 2D
array. C<@blocksize> and C<$datatype> (an integer) are optional and
obtained from the GDAL raster object if not given.

=head2 Write

 $band->Write($data, $xoff, $yoff, $xsize, $ysize);

=head2 WriteBlock

 $band->WriteBlock($data, $xoff, $yoff);

=head2 SetPiddle

 $band->SetPiddle($pdl, $xoff, $yoff, $xsize, $ysize);

Read data from a piddle into this Band.

=head2 GetPiddle

 $band->GetPiddle($xoff, $yoff, $xsize, $ysize, $xdim, $ydim);

Read data from this Band into a piddle.

=head2 GetColorInterpretation

 my $ci = $band->GetColorInterpretation;

=head2 SetColorInterpretation

 $band->SetColorInterpretation($ci);

=head2 GetColorTable

 my $color_table = $band->GetColorTable;

Returns the color table as an array of arrays. The inner tables are
colors [c1...c4].

=head2 SetColorTable

 $band->SetColorTable($color_table);

=head1 LICENSE

This software is released under the Artistic License. See
L<perlartistic>.

=head1 AUTHOR

Ari Jolma - Ari.Jolma at gmail.com

=head1 SEE ALSO

L<Geo::GDAL::FFI>

L<Alien::gdal>, L<FFI::Platypus>, L<http://www.gdal.org>

=cut

__END__;
