package Net::GPSD3;
use strict;
use warnings;
use base qw{Net::GPSD3::Base};
use JSON::XS qw{};
use IO::Socket::INET6 qw{};
use Net::GPSD3::Return::Unknown;
use Net::GPSD3::Cache;
use DateTime;

our $VERSION='0.19';
our $PACKAGE=__PACKAGE__;

=head1 NAME

Net::GPSD3 - Interface to the gpsd server daemon protocol versions 3 (JSON).

=head1 SYNOPSIS

=head2 Watch Interface

  use Net::GPSD3;
  my $gpsd=Net::GPSD3->new;
  $gpsd->watch;

One Liner

  perl -MNet::GPSD3 -e 'Net::GPSD3->new->watch'

=head2 Poll Interface

  use Net::GPSD3;
  use Data::Dumper qw{Dumper};
  my $gpsd=Net::GPSD3->new;
  my $poll=$gpsd->poll;
  print Dumper($poll);

One Liner

  perl -MNet::GPSD3 -e 'printf "Protocol: %s\n", Net::GPSD3->new->poll->parent->cache->VERSION->protocol;'

  Protocol: 3.4

=head2 POE Interface

See L<Net::GPSD3::POE>

=head1 DESCRIPTION

Net::GPSD3 provides an object client interface to the gpsd server daemon utilizing the version 3 protocol. gpsd is an open source GPS daemon from http://www.catb.org/gpsd/  Support for Version 3 of the protocol (JSON) was added to the daemon in version 2.90.  If your daemon is before 2.90 (protocol 2.X), please use the L<Net::GPSD> package.

=head1 CONSTRUCTOR

=head2 new

Returns a new Net::GPSD3 object.

  my $gpsd=Net::GPSD3->new;
  my $gpsd=Net::GPSD3->new(host=>"127.0.0.1", port=>2947); #defaults

=head1 METHODS

=head2 host

Sets or returns the current gpsd host.

 my $host=$obj->host;

=cut

sub host {
  my $self=shift;
  if (@_) {
    $self->{'host'}=shift;
    undef($self->{'socket'});
  }
  $self->{'host'}="127.0.0.1" unless defined $self->{'host'};
  return $self->{'host'};
}

=head2 port

Sets or returns the current gpsd TCP port.

 my $port=$obj->port;

=cut

sub port {
  my $self=shift;
  if (@_) {
    $self->{'port'}=shift;
    undef($self->{'socket'});
  }
  $self->{'port'}='2947' unless defined $self->{'port'};
  return $self->{'port'};
}

=head2 poll

Sends a Poll request to the gpsd server and returns a L<Net::GPSD3::Return::POLL> object. The method also populates the cache object with the L<Net::GPSD3::Return::VERISON> and L<Net::GPSD3::Return::DEVICES> objects.

  my $poll=$gpsd->poll; #isa Net::GPSD3::Return::POLL object

Note: In order to use the poll method consistently you should run the GPSD daemon as a service.  You may also need to run the daemon with the "-n" option.

=cut

sub poll {
  my $self=shift;
  $self->socket->send(qq(?DEVICES;\n)) unless $self->cache->DEVICES;
  $self->socket->send(qq(?POLL;\n));
  my $object;
  do { #Reads and caches VERSION and DEVICES
    local $/="\r\n";
    my $line=$self->socket->getline;
    chomp $line;
    $object=$self->constructor($self->decode($line), string=>$line);
    $self->cache->add($object) unless $object->class eq "POLL";
  } until $object->class eq "POLL"; #this needs more logic
  return $object;
}

=head2 watch

Calls all handlers that are registered in the handler method.

  $gpsd->watch;  #will not return unless something goes wrong.

=cut

sub watch {
  my $self=shift;
  my @handler=$self->handlers;
  push @handler, \&default_handler unless scalar(@handler);
  #$self->socket->send(qq(?DEVICES;\n)); #appears this is now done in the daemon
  $self->socket->send($self->_watch_string_on. "\n");
  my $object;
  #man 8 gpsd - Each request returns a line of response text ended by a CR/LF.
  local $/="\r\n";
  my $line;
  while (defined($line=$self->socket->getline)) { #Reads VERSION and DEVICES object too.
    #print "$line\n";
    chomp $line;
    my $object=$self->constructor($self->decode($line), string=>$line);
    $_->($object) foreach @handler;
    $self->cache($object); #cache after handler so that the last point is available to the handler.
  }
  return $self;
}

sub _watch_string_on {
  return q(?WATCH={"enable":true,"json":true};);
}

sub _watch_string_off {
  return q(?WATCH={"enable":false,"json":true};);
}

=head2 addHandler

Adds handlers to the handler list.

  $gpsd->addHandler(\&myHandler);
  $gpsd->addHandler(\&myHandler1, \&myHandler2);

A handler is a sub reference where the first argument is a Net::GPSD3::Return::* object.

=cut

sub addHandler {
  my $self=shift;
  my $array=$self->handlers;
  push @$array, @_ if @_;
  return $self;
}

=head2 handlers

List of handlers that are called in order to process objects from the gpsd wathcer stream.  

  my @handler=$gpsd->handlers; #()
  my $handler=$gpsd->handlers; #[]

=cut

sub handlers {
  my $self=shift;
  $self->{'handler'}=[] unless ref($self->{'handler'});
  return wantarray ? @{$self->{'handler'}} : $self->{'handler'};
}

=head2 cache

Returns the L<Net::GPSD3::Cache> caching object.

=cut

sub cache {
  my $self=shift;
  $self->{"cache"}=Net::GPSD3::Cache->new(parent=>$self)
    unless defined $self->{"cache"};
  return $self->{"cache"};
}

=head1 METHODS Internal

=head2 default_handler

=cut

sub default_handler {
  my $object=shift;
  #use Data::Dumper qw{Dumper};
  #print Dumper($object);
  if ($object->class eq "TPV") {
    printf "%s: %s, Time: %s, Lat: %s, Lon: %s, Speed: %s, Heading: %s\n",
             DateTime->now,
             $object->class,
             $object->timestamp,
             $object->lat,
             $object->lon,
             $object->speed,
             $object->track;
  } elsif ($object->class eq "SKY") {
    printf "%s: %s, Satellites: %s, Used: %s, PRNs: %s\n",
             DateTime->now,
             $object->class,
             $object->reported,
             $object->used,
             join(",", map {$_->prn} grep {$_->used} $object->Satellites),
  } elsif ($object->class eq "SUBFRAME") {
    printf qq{%s: %s, Device: %s\n},
             DateTime->now,
             $object->class,
             $object->device;
  } elsif ($object->class eq "VERSION") {
    printf "%s: %s, GPSD: %s (%s), %s: %s\n",
             DateTime->now,
             $object->class,
             $object->release,
             $object->revision,
             ref($object->parent),
             $object->parent->VERSION;
  } elsif ($object->class eq "WATCH") {
    printf "%s: %s, Enabled: %s\n",
             DateTime->now,
             $object->class,
             $object->enabled;
  } elsif ($object->class eq "DEVICES") {
    my @device=$object->Devices;
    foreach my $device (@device) {
      if ($device->activated) {
        $device=sprintf("%s (%s bps %s-%s)", $device->path, $device->bps, $device->driver, $device->subtype);
      } else {
        $device=$device->path;
      }
    }
    printf "%s: %s, Devices: %s\n",
             DateTime->now,
             $object->class,
             join(", ", @device);
  } elsif ($object->class eq "DEVICE") {
    printf qq{%s: %s, Device: %s (%s bps %s-%s)\n},
             DateTime->now,
             $object->class,
             $object->path,
             $object->bps,
             $object->driver,
             $object->subtype;
  } elsif ($object->class eq "ERROR") {
    printf qq{%s: %s, Message: "%s"\n},
             DateTime->now,
             $object->class,
             $object->message;
  } else {
    warn(sprintf(qq{Warning: Unknown class "%s" for object "%s".}, $object->class, ref($object)));
    #print Dumper($object);
  }
  #print Dumper($object);
}

=head2 socket

Returns the cached L<IO::Socket::INET6> object

  my $socket=$gpsd->socket;  #try to reconnect on failure

=cut

sub socket {
  my $self=shift;
  unless (defined($self->{'socket'}) and
            defined($self->{'socket'}->connected)) { 
    $self->{"socket"}=IO::Socket::INET6->new(
                        PeerAddr => $self->host,
                        PeerPort => $self->port,
                      );
    die(sprintf("Error: Cannot connect to gpsd://%s:%s/.\n",
      $self->host, $self->port)) unless defined($self->{"socket"});
  }
  return $self->{'socket'};
}

=head2 json

Returns the cached L<JSON::XS> object

=cut

sub json {
  my $self=shift;
  #Do I need to support JSON::PP?
  $self->{"json"}=JSON::XS->new unless ref($self->{"json"}) eq "JSON::XS";
  return $self->{"json"};
}

=head2 decode

Returns a perl data structure given a JSON formated string.

  my %data=$gpsd->decode($string); #()
  my $data=$gpsd->decode($string); #{}

=cut

sub decode {
  my $self=shift;
  my $string=shift;
  my $data=eval {$self->json->decode($string)};
  if ($@) {
    $data={class=>"ERROR", message=>"Invalid JSON"};
  }
  return wantarray ? %$data : $data;
}

=head2 encode

Returns a JSON string from a perl data structure

=cut

sub encode {
  my $self=shift;
  my $data=shift;
  my $string=$self->json->encode($data);
  return $string;
}

=head2 constructor

Constructs a class object by lazy loading the classes.

  my $obj=$gpsd->constructor(%$data);
  my $obj=$gpsd->constructor(class=>"DEVICE",
                             string=>'{...}',
                             ...);

Returns and object in the Net::GPSD3::Return::* namespace.

=cut

sub constructor {
  my $self=shift;
  my %data=@_;
  $data{"class"}||="undef";
  my $class=join("::", $PACKAGE, "Return", $data{"class"});
  my $object;
  eval("use $class");
  if ($@) { #Failed to load class
    $object=Net::GPSD3::Return::Unknown->new(parent=>$self, %data);
  } else {
    $object=$class->new(parent=>$self, %data);
  }
  return $object;
}

=head1 BUGS

Log on RT and Send to gpsd-dev email list

There are no two GPS devices that are alike.  Each GPS device has a different GPSD signature as well. If your GPS device does not work out of the box with this package, please send me a log of your devices JSON sentences.

  echo '?POLL;' | nc 127.0.0.1 2947

  echo '?WATCH={"enable":true,"json":true};' | socat -t10 stdin stdout | nc 127.0.0.1 2947

=head1 SUPPORT

DavisNetworks.com supports all Perl applications including this package.

Try gpsd-dev email list

=head1 AUTHOR

  Michael R. Davis
  CPAN ID: MRDVT
  STOP, LLC
  domain=>michaelrdavis,tld=>com,account=>perl
  http://www.stopllc.com/

=head1 COPYRIGHT

This program is free software licensed under the...

  The BSD License

The full text of the license can be found in the LICENSE file included with this module.

=head1 SEE ALSO

L<Net::GPSD>, L<Net::GPSD3::POE>, L<GPS::Point>, L<JSON::XS>, L<IO::Socket::INET6>, L<DateTime>

=cut

1;
