#  You may distribute under the terms of either the GNU General Public License
#  or the Artistic License (the same terms as Perl itself)
#
#  (C) Paul Evans, 2013-2014 -- leonerd@leonerd.org.uk

package Tangence::Type::Primitive;

use strict;
use warnings;
use feature qw( switch );
no if $] >= 5.017011, warnings => 'experimental::smartmatch';

use base qw( Tangence::Type );

package
   Tangence::Type::Primitive::bool;
use base qw( Tangence::Type::Primitive );
use Carp;
use Tangence::Constants;

sub default_value { "" }

sub pack_value
{
   my $self = shift;
   my ( $message, $value ) = @_;

   $message->_pack_leader( DATA_NUMBER, $value ? DATANUM_BOOLTRUE : DATANUM_BOOLFALSE );
}

sub unpack_value
{
   my $self = shift;
   my ( $message ) = @_;

   my ( $type, $num ) = $message->_unpack_leader();

   $type == DATA_NUMBER or croak "Expected to unpack a number(bool) but did not find one";
   $num == DATANUM_BOOLFALSE and return 0;
   $num == DATANUM_BOOLTRUE  and return 1;
   croak "Expected to find a DATANUM_BOOL subtype but got $num";
}

package
   Tangence::Type::Primitive::_integral;
use base qw( Tangence::Type::Primitive );
use Carp;
use Tangence::Constants;

use constant SUBTYPE => undef;

sub default_value { 0 }

{
   my %format = (
      DATANUM_UINT8,  [ "C",  1 ],
      DATANUM_SINT8,  [ "c",  1 ],
      DATANUM_UINT16, [ "S>", 2 ],
      DATANUM_SINT16, [ "s>", 2 ],
      DATANUM_UINT32, [ "L>", 4 ],
      DATANUM_SINT32, [ "l>", 4 ],
      DATANUM_UINT64, [ "Q>", 8 ],
      DATANUM_SINT64, [ "q>", 8 ],
   );

   sub _best_int_type_for
   {
      my ( $n ) = @_;

      if( $n < 0 ) {
         return DATANUM_SINT8  if $n >= -0x80;
         return DATANUM_SINT16 if $n >= -0x8000;
         return DATANUM_SINT32 if $n >= -0x80000000;
         return DATANUM_SINT64;
      }

      return DATANUM_UINT8  if $n <= 0xff;
      return DATANUM_UINT16 if $n <= 0xffff;
      return DATANUM_UINT32 if $n <= 0xffffffff;
      return DATANUM_UINT64;
   }

   sub pack_value
   {
      my $self = shift;
      my ( $message, $value ) = @_;

      defined $value or croak "cannot pack_int(undef)";
      ref $value and croak "$value is not a number";

      my $subtype = $self->SUBTYPE || _best_int_type_for( $value );
      $message->_pack_leader( DATA_NUMBER, $subtype );

      $message->_pack( pack( $format{$subtype}[0], $value ) );
   }

   sub unpack_value
   {
      my $self = shift;
      my ( $message ) = @_;

      my ( $type, $num ) = $message->_unpack_leader();

      $type == DATA_NUMBER or croak "Expected to unpack a number but did not find one";
      exists $format{$num} or croak "Expected an integer subtype but got $num";

      if( my $subtype = $self->SUBTYPE ) {
         $subtype == $num or croak "Expected integer subtype $subtype, got $num";
      }

      my ( $n ) = unpack( $format{$num}[0], $message->_unpack( $format{$num}[1] ) );

      return $n;
   }
}

package
   Tangence::Type::Primitive::u8;
use base qw( Tangence::Type::Primitive::_integral );
use constant SUBTYPE => Tangence::Constants::DATANUM_UINT8;

package
   Tangence::Type::Primitive::s8;
use base qw( Tangence::Type::Primitive::_integral );
use constant SUBTYPE => Tangence::Constants::DATANUM_SINT8;

package
   Tangence::Type::Primitive::u16;
use base qw( Tangence::Type::Primitive::_integral );
use constant SUBTYPE => Tangence::Constants::DATANUM_UINT16;

package
   Tangence::Type::Primitive::s16;
use base qw( Tangence::Type::Primitive::_integral );
use constant SUBTYPE => Tangence::Constants::DATANUM_SINT16;

package
   Tangence::Type::Primitive::u32;
use base qw( Tangence::Type::Primitive::_integral );
use constant SUBTYPE => Tangence::Constants::DATANUM_UINT32;

package
   Tangence::Type::Primitive::s32;
use base qw( Tangence::Type::Primitive::_integral );
use constant SUBTYPE => Tangence::Constants::DATANUM_SINT32;

package
   Tangence::Type::Primitive::u64;
use base qw( Tangence::Type::Primitive::_integral );
use constant SUBTYPE => Tangence::Constants::DATANUM_UINT64;

package
   Tangence::Type::Primitive::s64;
use base qw( Tangence::Type::Primitive::_integral );
use constant SUBTYPE => Tangence::Constants::DATANUM_SINT64;

package
   Tangence::Type::Primitive::int;
use base qw( Tangence::Type::Primitive::_integral );

package
   Tangence::Type::Primitive::float;
use base qw( Tangence::Type::Primitive );
use Carp;
use Tangence::Constants;

use constant SUBTYPE => undef;

sub default_value { 0.0 }

{
   my %format = (
      DATANUM_FLOAT32, [ "f>", 4 ],
      DATANUM_FLOAT64, [ "d>", 8 ],
   );

   sub _best_type_for
   {
      my ( $value ) = @_;

      # Unpack as 64bit float and see if it's within limits
      my $float64BIN = pack "d>", $value;

      # float64 == 1 / 11 / 52
      my $exp64 = ( unpack "Q>", $float64BIN & "\x7f\xf0\x00\x00\x00\x00\x00\x00" ) >> 52;

      # Zero is smallest
      return DATANUM_FLOAT16 if $exp64 == 0;

      # De-bias
      $exp64 -= 1023;

      # Smaller types are OK if the exponent will fit and there's no loss of
      # mantissa precision

      return DATANUM_FLOAT16 if abs($exp64) < 15  &&
         ($float64BIN & "\x00\x00\x03\xff\xff\xff\xff\xff") eq "\x00"x8;

      return DATANUM_FLOAT32 if abs($exp64) < 127 &&
         ($float64BIN & "\x00\x00\x00\x00\x1f\xff\xff\xff") eq "\x00"x8;

      return DATANUM_FLOAT64;
   }

   sub pack_value
   {
      my $self = shift;
      my ( $message, $value ) = @_;

      defined $value or croak "cannot pack undef as float";
      ref $value and croak "$value is not a number";

      my $subtype = $self->SUBTYPE || _best_type_for( $value );

      return Tangence::Type::Primitive::float16->pack_value( $message, $value ) if $subtype == DATANUM_FLOAT16;

      $message->_pack_leader( DATA_NUMBER, $subtype );
      $message->_pack( pack( $format{$subtype}[0], $value ) );
   }

   sub unpack_value
   {
      my $self = shift;
      my ( $message ) = @_;

      my ( $type, $num ) = $message->_unpack_leader( "peek" );

      $type == DATA_NUMBER or croak "Expected to unpack a number but did not find one";
      exists $format{$num} or $num == DATANUM_FLOAT16 or
         croak "Expected a float subtype but got $num";

      if( my $subtype = $self->SUBTYPE ) {
         $subtype == $num or croak "Expected float subtype $subtype, got $num";
      }

      return Tangence::Type::Primitive::float16->unpack_value( $message ) if $num == DATANUM_FLOAT16;

      $message->_unpack_leader; # no-peek

      my ( $n ) = unpack( $format{$num}[0], $message->_unpack( $format{$num}[1] ) );

      return $n;
   }
}

package
   Tangence::Type::Primitive::float16;
use base qw( Tangence::Type::Primitive::float );
use Carp;
use Tangence::Constants;

use constant SUBTYPE => DATANUM_FLOAT16;

# TODO: This code doesn't correctly cope with Inf, -Inf or NaN

sub pack_value
{
   my $self = shift;
   my ( $message, $value ) = @_;

   defined $value or croak "cannot pack undef as float";
   ref $value and croak "$value is not a number";

   my $float32 = unpack( "N", pack "f>", $value );

   # float32 == 1 / 8 / 23
   my $sign   = ( $float32 & 0x80000000 ) >> 31;
   my $exp32  = ( $float32 & 0x7f800000 ) >> 23;
   my $mant32 = ( $float32 & 0x007fffff );

   # float16 == 1 / 5 / 10
   # TODO: if $exp > 7: become (-)Inf
   my $exp16 = $exp32 ? $exp32 - 127 + 15 : 0; # Preserve zero
   my $mant16 = $mant32 >> 13;

   my $float16 = $sign   << 15 |
                 $exp16  << 10 |
                 $mant16;

   $message->_pack_leader( DATA_NUMBER, DATANUM_FLOAT16 );
   $message->_pack( pack "n", $float16 );
}

sub unpack_value
{
   my $self = shift;
   my ( $message ) = @_;

   my ( $type, $num ) = $message->_unpack_leader;

   $type == DATA_NUMBER or croak "Expected to unpack a number but did not find one";
   $num == DATANUM_FLOAT16 or croak "Expected to unpack a float16 but found $num";

   my $float16 = unpack "n", $message->_unpack( 2 );

   # float16 == 1 / 5 / 10
   my $sign   = ( $float16 & 0x8000 ) >> 15;
   my $exp16  = ( $float16 & 0x7c00 ) >> 10;
   my $mant16 = ( $float16 & 0x03ff );

   # float32 == 1 / 8 / 23
   my $exp32 = $exp16 ? $exp16 - 15 + 127 : 0; # Preserve zero
   my $mant32 = $mant16 << 13;

   my $float32 = $sign   << 31 |
                 $exp32  << 23 |
                 $mant32;

   return unpack( "f>", pack "N", $float32 );
}

package
   Tangence::Type::Primitive::float32;
use base qw( Tangence::Type::Primitive::float );
use Tangence::Constants;

use constant SUBTYPE => DATANUM_FLOAT32;

package
   Tangence::Type::Primitive::float64;
use base qw( Tangence::Type::Primitive::float );
use Tangence::Constants;

use constant SUBTYPE => DATANUM_FLOAT64;

package
   Tangence::Type::Primitive::str;
use base qw( Tangence::Type::Primitive );
use Carp;
use Encode qw( encode_utf8 decode_utf8 );
use Tangence::Constants;

sub default_value { "" }

sub pack_value
{
   my $self = shift;
   my ( $message, $value ) = @_;

   defined $value or croak "cannot pack_str(undef)";
   ref $value and croak "$value is not a string";
   my $octets = encode_utf8( $value );
   $message->_pack_leader( DATA_STRING, length($octets) );
   $message->_pack( $octets );
}

sub unpack_value
{
   my $self = shift;
   my ( $message ) = @_;

   my ( $type, $num ) = $message->_unpack_leader();

   $type == DATA_STRING or croak "Expected to unpack a string but did not find one";
   my $octets = $message->_unpack( $num );
   return decode_utf8( $octets );
}

package
   Tangence::Type::Primitive::obj;
use base qw( Tangence::Type::Primitive );
use Carp;
use Scalar::Util qw( blessed );
use Tangence::Constants;

sub default_value { undef }

sub pack_value
{
   my $self = shift;
   my ( $message, $value ) = @_;

   my $stream = $message->stream;

   if( !defined $value ) {
      $message->_pack_leader( DATA_OBJECT, 0 );
   }
   elsif( blessed $value and $value->isa( "Tangence::Object" ) ) {
      my $id = $value->id;
      my $preamble = "";

      $value->{destroyed} and croak "Cannot pack destroyed object $value";

      $message->packmeta_construct( $value ) unless $stream->peer_hasobj->{$id};

      $message->_pack_leader( DATA_OBJECT, 4 );
      $message->_pack( pack( "N", $id ) );
   }
   elsif( blessed $value and $value->isa( "Tangence::ObjectProxy" ) ) {
      $message->_pack_leader( DATA_OBJECT, 4 );
      $message->_pack( pack( "N", $value->id ) );
   }
   else {
      croak "Do not know how to pack a " . ref($value);
   }
}

sub unpack_value
{
   my $self = shift;
   my ( $message ) = @_;

   my ( $type, $num ) = $message->_unpack_leader();

   my $stream = $message->stream;

   $type == DATA_OBJECT or croak "Expected to unpack an object but did not find one";
   return undef unless $num;
   if( $num == 4 ) {
      my ( $id ) = unpack( "N", $message->_unpack( 4 ) );
      return $stream->get_by_id( $id );
   }
   else {
      croak "Unexpected number of bits to encode an OBJECT";
   }
}

package
   Tangence::Type::Primitive::any;
use base qw( Tangence::Type::Primitive );
use Carp;
use Scalar::Util qw( blessed );
use Tangence::Constants;

# We can't use Tangence::Types here without a dependency cycle
# However, it's OK to create even TYPE_ANY right here, because the 'any' class
# now exists.
use constant TYPE_BOOL  => Tangence::Type->new( 'bool' );
use constant TYPE_INT   => Tangence::Type->new( 'int' );
use constant TYPE_FLOAT => Tangence::Type->new( 'float' );
use constant TYPE_STR   => Tangence::Type->new( 'str' );
use constant TYPE_OBJ   => Tangence::Type->new( 'obj' );
use constant TYPE_ANY   => Tangence::Type->new( 'any' );

use constant TYPE_LIST_ANY => Tangence::Type->new( list => TYPE_ANY );
use constant TYPE_DICT_ANY => Tangence::Type->new( dict => TYPE_ANY );

sub default_value { undef }

sub pack_value
{
   my $self = shift;
   my ( $message, $value ) = @_;

   if( !defined $value ) {
      TYPE_OBJ->pack_value( $message, undef );
   }
   elsif( !ref $value ) {
      no warnings 'numeric';
      if( int($value) eq $value ) {
         TYPE_INT->pack_value( $message, $value );
      }
      elsif( $message->stream->_ver_can_num_float and $value+0 eq $value ) {
         TYPE_FLOAT->pack_value( $message, $value );
      }
      else {
         TYPE_STR->pack_value( $message, $value );
      }
   }
   elsif( blessed $value and $value->isa( "Tangence::Object" ) || $value->isa( "Tangence::ObjectProxy" ) ) {
      TYPE_OBJ->pack_value( $message, $value );
   }
   elsif( my $struct = eval { Tangence::Struct->for_perlname( ref $value ) } ) {
      $message->pack_record( $value, $struct );
   }
   elsif( ref $value eq "ARRAY" ) {
      TYPE_LIST_ANY->pack_value( $message, $value );
   }
   elsif( ref $value eq "HASH" ) {
      TYPE_DICT_ANY->pack_value( $message, $value );
   }
   else {
      croak "Do not know how to pack a " . ref($value);
   }
}

sub unpack_value
{
   my $self = shift;
   my ( $message ) = @_;

   my $type = $message->_peek_leader_type();

   if( $type == DATA_NUMBER ) {
      my ( undef, $num ) = $message->_unpack_leader( "peek" );
      if( $num >= DATANUM_BOOLFALSE and $num <= DATANUM_BOOLTRUE ) {
         return TYPE_BOOL->unpack_value( $message );
      }
      elsif( $num >= DATANUM_UINT8 and $num <= DATANUM_SINT64 ) {
         return TYPE_INT->unpack_value( $message );
      }
      elsif( $num >= DATANUM_FLOAT16 and $num <= DATANUM_FLOAT64 ) {
         return TYPE_FLOAT->unpack_value( $message );
      }
      else {
         croak "Do not know how to unpack DATA_NUMBER subtype $num";
      }
   }
   if( $type == DATA_STRING ) {
      return TYPE_STR->unpack_value( $message );
   }
   elsif( $type == DATA_OBJECT ) {
      return TYPE_OBJ->unpack_value( $message );
   }
   elsif( $type == DATA_LIST ) {
      return TYPE_LIST_ANY->unpack_value( $message );
   }
   elsif( $type == DATA_DICT ) {
      return TYPE_DICT_ANY->unpack_value( $message );
   }
   elsif( $type == DATA_RECORD ) {
      return $message->unpack_record( undef );
   }
   else {
      croak "Do not know how to unpack record of type $type";
   }
}

=head1 AUTHOR

Paul Evans <leonerd@leonerd.org.uk>

=cut

0x55AA;
