# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements.  See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License.  You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

use strict;
use warnings;

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

BEGIN { our @EXPORT_OK = qw( from_perl to_perl ); }

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

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

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

    if ( $type->is_object ) {
        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.
            return "($struct_sym*)XSBind_sv_to_cfish_obj($xs_var, "
                . "$vtable, alloca(cfish_ZCB_size()))";
        }
        else {
            return "($struct_sym*)XSBind_sv_to_cfish_obj($xs_var, "
                . "$vtable, NULL)";
        }
    }
    elsif ( $type->is_primitive ) {
        if ( my $sub = $primitives_from_perl{ $type->to_c } ) {
            return $sub->($xs_var);
        }
    }

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

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

    if ( $type->is_object ) {
        return "($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->($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 "newRV_inc((SV*)($cf_var))";
        }
    }

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

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

    my $class_typemap_start  = "";
    my $class_typemap_input  = "";
    my $class_typemap_output = "";

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

END_INPUT

        $class_typemap_output .= <<END_OUTPUT;
$label
    \$arg = (SV*)Cfish_Obj_To_Host((cfish_Obj*)\$var);
    CFISH_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 <<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 lucy_CharBuf*\tCONST_CHARBUF
$class_typemap_start

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 
    \$var = (sizeof(IV) == 8) ? (\$type)SvIV(\$arg) : (\$type)SvNV(\$arg);

CHY_BIG_UNSIGNED_INT 
    \$var = (sizeof(UV) == 8) ? (\$type)SvUV(\$arg) : (\$type)SvNV(\$arg);

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

$class_typemap_input

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
    if (sizeof(IV) == 8) { sv_setiv(\$arg, (IV)\$var); }
    else                 { sv_setnv(\$arg, (NV)\$var); }

CHY_BIG_UNSIGNED_INT
    if (sizeof(UV) == 8) { sv_setuv(\$arg, (UV)\$var); }
    else                 { sv_setnv(\$arg, (NV)\$var); }

$class_typemap_output

END_STUFF

    close $typemap_fh or die $!;
}

1;

__END__

__POD__

=head1 NAME

Clownfish::CFC::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 $expression = from_perl( $type, $xs_var );

Return an expression which converts from a Perl scalar to a variable of type
$type.

=over

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

=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, $cf_var );

Return an expression converts from a variable of type $type to a Perl scalar.

=over

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

=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::CFC::Binding::Perl::Typemap->write_xs_typemap(
        hierarchy => $hierarchy,
    );

=over

=item * B<hierarchy> - A L<Clownfish::CFC::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.

=cut

