package IP::Geolocation::MMDB;

# SPDX-License-Identifier: Artistic-1.0-Perl OR GPL-1.0-or-later

use 5.016;
use warnings;
use utf8;

our $VERSION = 1.010;

use IP::Geolocation::MMDB::Metadata;
use Math::BigInt 1.999806;

require XSLoader;
XSLoader::load(__PACKAGE__, $VERSION);

sub getcc {
    my ($self, $ip_address) = @_;

    my $country_code;

    my $data = $self->record_for_address($ip_address);
    if (ref $data eq 'HASH') {
        if (exists $data->{country}) {
            my $country = $data->{country};
            if (exists $country->{iso_code}) {
                $country_code = $country->{iso_code};
            }
        }
    }

    return $country_code;
}

sub metadata {
    my ($self) = @_;

    return IP::Geolocation::MMDB::Metadata->new(%{$self->_metadata});
}

## no critic (Subroutines::ProhibitUnusedPrivateSubroutines)

sub _to_bigint {
    my ($self, $bytes) = @_;

    return Math::BigInt->from_bytes($bytes);
}

1;
__END__

=encoding UTF-8

=head1 NAME

IP::Geolocation::MMDB - Read MaxMind DB files

=head1 VERSION

version 1.010

=head1 SYNOPSIS

  use IP::Geolocation::MMDB;
  my $db = IP::Geolocation::MMDB->new(file => 'Country.mmdb');
  my $metadata = $db->metadata;
  my $data = $db->record_for_address('1.2.3.4');
  my $country_code = $db->getcc('2620:fe::9');

=head1 DESCRIPTION

A Perl module that reads MaxMind DB files and maps IP addresses to location
information such as country and city names.

=head1 SUBROUTINES/METHODS

=head2 new

  my $db = IP::Geolocation::MMDB->new(file => 'Country.mmdb');

Returns a new database object.  Dies if the specified file cannot be read.

=head2 getcc

  my $country_code = $db->getcc($ip_address);

Takes an IPv4 or IPv6 address as a string and returns a two-letter country
code or the undefined value.  Dies if the address is not a valid IP address.

=head2 record_for_address

  my $data = $db->record_for_address($ip_address);

Takes an IPv4 or IPv6 address as a string and returns the data associated with
the IP address or the undefined value.  Dies if the address is not a valid IP
address.

The returned data is usually a hash reference but could also be a an array
reference or a scalar for custom databases.  Here's an example from an IP to
city database:

  {
    city => {
      geoname_id => 2950159,
      names      => {
        en => "Berlin"
      }
    },
    country => {
      geoname_id => 2921044,
      iso_code   => "DE",
      names      => {
        en => "Germany",
        fr => "Allemagne"
      }
    },
    location => {
      latitude  => 52.524,
      longitude => 13.411
    }
  }

=head2 iterate_search_tree

  sub data_callback {
    my ($numeric_ip, $prefix_length, $data) = @_;
  }

  sub node_callback {
    my ($node_number, $left_node_number, $right_node_number) = @_;
  }

  $db->iterate_search_tree(\&data_callback, \&node_callback);

Iterates over the entire search tree.  Calls the provided callbacks for each
data record and node in the tree.  Both callbacks are optional.

The data callback is called with a numeric IP address as a L<Math::BigInt>
object, a network prefix length and the data associated with the network.

The node callback is called with the node's number in the tree and the
children's node numbers.

=head2 metadata

  my $metadata = $db->metadata;

Returns an L<IP::Geolocation::MMDB::Metadata> object for the database.

=head2 file

  my $file = $db->file;

Returns the file path passed to the constructor.

=head2 libmaxminddb_version

  my $version = IP::Geolocation::MMDB::libmaxminddb_version;

Returns the libmaxminddb version.

=head1 DIAGNOSTICS

=over

=item B<< The "file" parameter is mandatory >>

The constructor was called without a database filename.

=item B<< Error opening database file >>

The database file could not be read.

=item B<< The IP address you provided is not a valid IPv4 or IPv6 address >>

A parameter did not contain a valid IP address.

=item B<< Error looking up IP address >>

A database error occurred while looking up an IP address.

=item B<< Entry data error looking up >>

A database error occurred while reading the data associated with an IP
address.

=item B<< Error getting metadata >>

An error occurred while reading the database's metadata.

=item B<< Invalid record when reading node >>

Either an invalid node was looked up or the database is corrupt.

=item B<< Unknown record type >>

An unknown record type was found in the database.

=item B<< Invalid depth when reading node >>

An error occurred while traversing the search tree.

=back

=head1 CONFIGURATION AND ENVIRONMENT

None.

=head1 DEPENDENCIES

Requires L<Alien::libmaxminddb> from CPAN.  Requires L<Math::BigInt> version
1.999806, which is distributed with Perl 5.26 and newer.  Requires
libmaxminddb 1.2.0 or newer.

Requires an IP to country, city or ASN database in the MaxMind DB file format
from L<MaxMind|https://www.maxmind.com/> or L<DP-IP.com|https://db-ip.com/>.

=head1 INCOMPATIBILITIES

None.

=head1 BUGS AND LIMITATIONS

If your Perl interpreter does not support 64-bit integers,
MMDB_DATA_TYPE_UINT64 values are put into Math::BigInt objects;

MMDB_DATA_TYPE_UINT128 values are put into Math::BigInt objects;

IP::Geolocation::MMDB can replace MaxMind::DB::Reader in many cases with the
following differences:

=over

=item *

The classes aren't Moo classes.

=item *

There is no replacement for MaxMind::DB::Reader::Decoder.

=back

=head1 ACKNOWLEDGEMENTS

Thanks to all who have contributed patches and reported bugs:

=over

=item *

Yujuan Jiang

=back

=head1 AUTHOR

Andreas Vögele E<lt>voegelas@cpan.orgE<gt>

=head1 LICENSE AND COPYRIGHT

Copyright (C) 2022 Andreas Vögele

This module is free software; you can redistribute it and/or modify it under
the same terms as Perl itself.

=cut