package GDPR::IAB::TCFv2::RangeSection;
use strict;
use warnings;
use integer;
use bytes;

use GDPR::IAB::TCFv2::BitUtils qw<is_set get_uint12 get_uint16>;
use Carp                       qw<croak>;

sub Parse {
    my ( $klass, %args ) = @_;

    croak "missing 'data'"      unless defined $args{data};
    croak "missing 'data_size'" unless defined $args{data_size};
    croak "missing 'offset'"    unless defined $args{offset};
    croak "missing 'max_id'"
      unless defined $args{max_id};

    croak "missing 'options'"      unless defined $args{options};
    croak "missing 'options.json'" unless defined $args{options}->{json};

    my $data      = $args{data};
    my $data_size = $args{data_size};
    my $offset    = $args{offset};
    my $max_id    = $args{max_id};
    my $options   = $args{options};

    Carp::confess
      "a BitField for vendor consent strings using RangeSections require at least 31 bytes. Got $data_size"
      if $data_size < 31;

    my ( $num_entries, $next_offset ) = get_uint12( $data, $offset );

    my @ranges;

    foreach my $i ( 1 .. $num_entries ) {
        my $range;
        ( $range, $next_offset ) = _parse_range(
            $data,
            $data_size,
            $next_offset,
            $max_id,
            $options,
        );

        push @ranges, $range;
    }

    my $self = {
        ranges  => \@ranges,
        max_id  => $max_id,
        options => $options,
    };

    bless $self, $klass;

    return ( $self, $next_offset );
}

sub _parse_range {
    my ( $data, $data_size, $offset, $max_id, $options ) = @_;

    croak
      "bit $offset was suppose to start a new range entry, but the consent string was only $data_size bytes long"
      if $data_size <= $offset / 8;

    # If the first bit is set, it's a Range of IDs
    my ( $is_range, $next_offset ) = is_set $data, $offset;
    if ($is_range) {
        my ( $start, $end );

        ( $start, $next_offset ) = get_uint16( $data, $next_offset );
        ( $end,   $next_offset ) = get_uint16( $data, $next_offset );

        croak
          "bit $offset range entry exclusion starts at $start, but the min vendor ID is 1"
          if 1 > $start;

        croak
          "bit $offset range entry exclusion ends at $end, but the max vendor ID is $max_id"
          if $end > $max_id;

        croak "start $start can't be bigger than end $end" if $start > $end;

        return [ $start, $end ],
          $next_offset;
    }

    my $vendor_id;

    ( $vendor_id, $next_offset ) = get_uint16( $data, $next_offset );

    croak
      "bit $offset range entry exclusion vendor $vendor_id, but only vendors [1, $max_id] are valid"
      if 1 > $vendor_id || $vendor_id > $max_id;

    return [ $vendor_id, $vendor_id ], $next_offset;
}

sub contains {
    my ( $self, $id ) = @_;

    croak "invalid vendor id $id: must be positive integer bigger than 0"
      if $id < 1;

    return if $id > $self->{max_id};

    foreach my $range ( @{ $self->{ranges} } ) {
        return 1 if $range->[0] <= $id && $id <= $range->[1];
    }

    return 0;
}

sub all {
    my $self = shift;

    my @vendors;
    foreach my $range ( @{ $self->{ranges} } ) {
        push @vendors, $range->[0] .. $range->[1];
    }

    return \@vendors;
}

sub TO_JSON {
    my $self = shift;

    if ( !!$self->{options}->{json}->{compact} ) {
        my @vendors;

        foreach my $range ( @{ $self->{ranges} } ) {
            push @vendors, $range->[0] .. $range->[1];
        }

        return \@vendors;
    }

    my ( $false, $true ) = @{ $self->{options}->{json}->{boolean_values} };

    my %map;
    if ( !!$self->{options}->{json}->{verbose} ) {
        %map = map { $_ => $false } 1 .. $self->{max_id};
    }

    foreach my $range ( @{ $self->{ranges} } ) {
        %map = ( %map, map { $_ => $true } $range->[0] .. $range->[1] );
    }

    return \%map;
}

1;
__END__

=head1 NAME

GDPR::IAB::TCFv2::RangeSection - Transparency & Consent String version 2 range section parser

=head1 SYNOPSIS

    my $data = unpack "B*", decode_base64url('tcf v2 consent string base64 encoded');
    
    my $max_id_consent = << get 16 bits from $data offset 213 >>

    my ($range_section, $next_offset) = GDPR::IAB::TCFv2::RangeSection->Parse(
        data          => $data,
        offset     => 230,                      # offset for vendor ranges
        max_id => $max_id_consent,
    );

    if $range_section->contains(284) { ... }

=head1 CONSTRUCTOR

Constructor C<Parse> receives 3 parameters: data (as sequence of bits), start bit offset and vendor bits required (max vendor id).

Will die if any parameter is missing.

Will die if data does not contain all bits required.

Will die if the range sections are malformed.

Will return an array of two elements: the object itself and the next offset.

=head1 METHODS

=head2 contains

Return the vendor id bit status (if enable or not) from one of the range sections.

Will return false if id is bigger than max vendor id.

    my $ok = $range_section->contains(284);

=head2 max_id

Returns the max vendor id.

=head2 all

Returns an arrayref of all vendors mapped with the bit enabled.
