use strict;
use warnings;

package Clownfish::Binding::Perl::TypeMap;
use base qw( Exporter );
use Scalar::Util qw( blessed );
use Carp;
use Fcntl;
use Config;

our @EXPORT_OK = qw( from_perl to_perl );

# Convert from a Perl scalar to a primitive type.
my %primitives_from_perl = (
    double => sub {"$_[0] = SvNV( $_[1] );"},
    float  => sub {"$_[0] = (float)SvNV( $_[1] );"},
    int    => sub {"$_[0] = (int)SvIV( $_[1] );"},
    short  => sub {"$_[0] = (short)SvIV( $_[1] );"},
    long   => sub {
        $Config{longsize} <= $Config{ivsize}
            ? "$_[0] = (long)SvIV( $_[1] );"
            : "$_[0] = (long)SvNV( $_[1] );";
    },
    size_t     => sub {"$_[0] = (size_t)SvIV( $_[1] );"},
    uint64_t   => sub {"$_[0] = (uint64_t)SvNV( $_[1] );"},
    uint32_t   => sub {"$_[0] = (uint32_t)SvUV( $_[1] );"},
    uint16_t   => sub {"$_[0] = (uint16_t)SvUV( $_[1] );"},
    uint8_t    => sub {"$_[0] = (uint8_t)SvUV( $_[1] );"},
    int64_t    => sub {"$_[0] = (int64_t)SvNV( $_[1] );"},
    int32_t    => sub {"$_[0] = (int32_t)SvIV( $_[1] );"},
    int16_t    => sub {"$_[0] = (int16_t)SvIV( $_[1] );"},
    int8_t     => sub {"$_[0] = (int8_t)SvIV( $_[1] );"},
    chy_bool_t => sub {"$_[0] = SvTRUE( $_[1] ) ? 1 : 0;"},
);

# Convert from a primitive type to a Perl scalar.
my %primitives_to_perl = (
    double => sub {"$_[0] = newSVnv( $_[1] );"},
    float  => sub {"$_[0] = newSVnv( $_[1] );"},
    int    => sub {"$_[0] = newSViv( $_[1] );"},
    short  => sub {"$_[0] = newSViv( $_[1] );"},
    long   => sub {
        $Config{longsize} <= $Config{ivsize}
            ? "$_[0] = newSViv( $_[1] );"
            : "$_[0] = newSVnv( (NV)$_[1] );";
    },
    size_t   => sub {"$_[0] = newSViv( $_[1] );"},
    uint64_t => sub {
        $Config{uvsize} == 8
            ? "$_[0] = newSVuv( $_[1] );"
            : "$_[0] = newSVnv( (NV)$_[1] );";
    },
    uint32_t => sub {"$_[0] = newSVuv( $_[1] );"},
    uint16_t => sub {"$_[0] = newSVuv( $_[1] );"},
    uint8_t  => sub {"$_[0] = newSVuv( $_[1] );"},
    int64_t  => sub {
        $Config{ivsize} == 8
            ? "$_[0] = newSViv( $_[1] );"
            : "$_[0] = newSVnv( (NV)$_[1] );";
    },
    int32_t    => sub {"$_[0] = newSViv( $_[1] );"},
    int16_t    => sub {"$_[0] = newSViv( $_[1] );"},
    int8_t     => sub {"$_[0] = newSViv( $_[1] );"},
    chy_bool_t => sub {"$_[0] = newSViv( $_[1] );"},
);

# Extract a Clownfish object from a Perl SV.
sub _sv_to_cf_obj {
    my ( $type, $cf_var, $xs_var ) = @_;
    my $struct_sym = $type->get_specifier;
    my $vtable     = uc($struct_sym);
    if ( $struct_sym =~ /^[a-z_]*(Obj|CharBuf)$/ ) {
        # Share buffers rather than copy between Perl scalars and Clownfish
        # string types.  Assume that the appropriate ZombieCharBuf has been
        # declared on the stack.
        return "$cf_var = ($struct_sym*)XSBind_sv_to_cfish_obj($xs_var, "
            . "$vtable, alloca(cfish_ZCB_size()));";
    }
    else {
        return "$cf_var = ($struct_sym*)XSBind_sv_to_cfish_obj($xs_var, "
            . "$vtable, NULL);";
    }
}

sub _void_star_to_clownfish {
    my ( $type, $cf_var, $xs_var ) = @_;
    # Assume that void* is a reference SV -- either a hashref or an arrayref.
    return qq|if (SvROK($xs_var)) {
            $cf_var = SvRV($xs_var);
        }
        else {
            $cf_var = NULL; /* avoid uninitialized compiler warning */
            CFISH_THROW(CFISH_ERR, "$cf_var is not a reference");
        }\n|;
}

sub from_perl {
    my ( $type, $cf_var, $xs_var ) = @_;
    confess("Not a Clownfish::Type")
        unless blessed($type) && $type->isa('Clownfish::Type');

    if ( $type->is_object ) {
        return _sv_to_cf_obj( $type, $cf_var, $xs_var );
    }
    elsif ( $type->is_primitive ) {
        if ( my $sub = $primitives_from_perl{ $type->to_c } ) {
            return $sub->( $cf_var, $xs_var );
        }
    }
    elsif ( $type->is_composite ) {
        if ( $type->to_c eq 'void*' ) {
            return _void_star_to_clownfish( $type, $cf_var, $xs_var );
        }
    }

    confess( "Missing typemap for " . $type->to_c );
}

sub to_perl {
    my ( $type, $xs_var, $cf_var ) = @_;
    confess("Not a Clownfish::Type")
        unless ref($type) && $type->isa('Clownfish::Type');
    my $type_str = $type->to_c;

    if ( $type->is_object ) {
        return "$xs_var = $cf_var == NULL ? newSV(0) : "
            . "XSBind_cfish_to_perl((cfish_Obj*)$cf_var);";
    }
    elsif ( $type->is_primitive ) {
        if ( my $sub = $primitives_to_perl{$type_str} ) {
            return $sub->( $xs_var, $cf_var );
        }
    }
    elsif ( $type->is_composite ) {
        if ( $type_str eq 'void*' ) {
            # Assume that void* is a reference SV -- either a hashref or an
            # arrayref.
            return "$xs_var = newRV_inc( (SV*)($cf_var) );";
        }
    }

    confess("Missing typemap for '$type_str'");
}

sub write_xs_typemap {
    my ( undef, %args ) = @_;
    my $hierarchy = $args{hierarchy};

    my $typemap_start  = _typemap_start();
    my $typemap_input  = _typemap_input_start();
    my $typemap_output = _typemap_output_start();

    for my $class ( $hierarchy->ordered_classes ) {
        my $full_struct_sym = $class->full_struct_sym;
        my $vtable          = $class->full_vtable_var;
        my $label           = $vtable . "_";
        $typemap_start .= "$full_struct_sym*\t$label\n";
        $typemap_input .= <<END_INPUT;
$label
    \$var = ($full_struct_sym*)XSBind_sv_to_cfish_obj(\$arg, $vtable, NULL);

END_INPUT

        $typemap_output .= <<END_OUTPUT;
$label
    \$arg = (SV*)Cfish_Obj_To_Host((cfish_Obj*)\$var);
    KINO_DECREF(\$var);

END_OUTPUT
    }

    # Blast it out.
    sysopen( my $typemap_fh, 'typemap', O_CREAT | O_WRONLY | O_EXCL )
        or die "Couldn't open 'typemap' for writing: $!";
    print $typemap_fh "$typemap_start $typemap_input $typemap_output"
        or die "Print to 'typemap' failed: $!";
}

sub _typemap_start {
    my $content = <<END_STUFF;
# Auto-generated file.

TYPEMAP
chy_bool_t\tCHY_BOOL
int8_t\tCHY_SIGNED_INT
int16_t\tCHY_SIGNED_INT
int32_t\tCHY_SIGNED_INT
int64_t\tCHY_BIG_SIGNED_INT
uint8_t\tCHY_UNSIGNED_INT
uint16_t\tCHY_UNSIGNED_INT
uint32_t\tCHY_UNSIGNED_INT
uint64_t\tCHY_BIG_UNSIGNED_INT

const kino_CharBuf*\tCONST_CHARBUF
END_STUFF

    return $content;
}

sub _typemap_input_start {
    my ( $big_signed_convert, $big_unsigned_convert );
    if ( $Config{ivsize} == 8 ) {
        $big_signed_convert   = '$var = ($type)SvIV($arg);';
        $big_unsigned_convert = '$var = ($type)SvUV($arg);';
    }
    else {
        $big_signed_convert   = '$var   = ($type)SvNV($arg);';
        $big_unsigned_convert = '$var = ($type)SvNV($arg);';
    }
    return <<END_STUFF;
    
INPUT

CHY_BOOL
    \$var = (\$type)SvTRUE(\$arg);

CHY_SIGNED_INT 
    \$var = (\$type)SvIV(\$arg);

CHY_UNSIGNED_INT
    \$var = (\$type)SvUV(\$arg);

CHY_BIG_SIGNED_INT 
    $big_signed_convert

CHY_BIG_UNSIGNED_INT 
    $big_unsigned_convert

CONST_CHARBUF
    \$var = (const cfish_CharBuf*)CFISH_ZCB_WRAP_STR(SvPVutf8_nolen(\$arg), SvCUR(\$arg));

END_STUFF
}

sub _typemap_output_start {
    my ( $big_signed_convert, $big_unsigned_convert );
    if ( $Config{ivsize} == 8 ) {
        $big_signed_convert   = 'sv_setiv($arg, (IV)$var);';
        $big_unsigned_convert = 'sv_setuv($arg, (UV)$var);';
    }
    else {
        $big_signed_convert   = 'sv_setnv($arg, (NV)$var);';
        $big_unsigned_convert = 'sv_setnv($arg, (NV)$var);';
    }
    return <<END_STUFF;

OUTPUT

CHY_BOOL
    sv_setiv(\$arg, (IV)\$var);

CHY_SIGNED_INT
    sv_setiv(\$arg, (IV)\$var);

CHY_UNSIGNED_INT
    sv_setuv(\$arg, (UV)\$var);

CHY_BIG_SIGNED_INT
    $big_signed_convert

CHY_BIG_UNSIGNED_INT
    $big_unsigned_convert

END_STUFF
}

1;

__END__

__POD__

=head1 NAME

Clownfish::Binding::Perl::TypeMap - Convert between Clownfish and Perl via XS.

=head1 DESCRIPTION

TypeMap serves up C code fragments for translating between Perl data
structures and Clownfish data structures.  The functions to_perl() and
from_perl() achieve this for individual types; write_xs_typemap() exports all
types using the XS "typemap" format documented in C<perlxs>.

=head1 FUNCTIONS

=head2 from_perl

    my $c_code = from_perl( $type, $cf_var, $xs_var );

Return C code which converts from a Perl scalar to a variable of type $type.

Variable declarations must precede the returned code, as from_perl() won't
make any declarations itself.

=over

=item * B<type> - A Clownfish::Type, which will be used to select the
mapping code.

=item * B<cf_var> - The name of the variable being assigned to.

=item * B<xs_var> - The C name of the Perl scalar from which we are extracting
a value.

=back

=head2 to_perl

    my $c_code = to_perl( $type, $xs_var, $cf_var );

Return C code which converts from a variable of type $type to a Perl scalar.

Variable declarations must precede the returned code, as to_perl() won't make
any declarations itself.

=over

=item * B<type> - A Clownfish::Type, which will be used to select the
mapping code.

=item * B<xs_var> - The C name of the Perl scalar being assigned to.

=item * B<cf_var> - The name of the variable from which we are extracting a
value.

=back

=head1 CLASS METHODS

=head2 write_xs_typemap 

    Clownfish::Binding::Perl::Typemap->write_xs_typemap(
        hierarchy => $hierarchy,
    );

=over

=item * B<hierarchy> - A L<Clownfish::Hierarchy>.

=back 

Auto-generate a "typemap" file that adheres to the conventions documented in
L<perlxs>.  

We generate this file on the fly rather than maintain a static copy because we
want an entry for each Clownfish type so that we can differentiate between
them when checking arguments.  Keeping the entries up-to-date manually as
classes come and go would be a pain.

=head1 COPYRIGHT AND LICENSE

Copyright 2008-2011 Marvin Humphrey

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

=cut
