package Inline::Struct;

use strict;
use Carp;
require Inline;
require Inline::Struct::grammar;
use Data::Dumper;

use vars qw($VERSION);
$VERSION = '0.06';

#=============================================================================
# Inline::Struct is NOT an ILSM: no register() function
#=============================================================================

#=============================================================================
# parse -- gets all C/C++ struct definitions and binds them to Perl
#=============================================================================
sub parse {
    require Parse::RecDescent;
    my $o = shift;
    return if $o->{STRUCT}{'.parser'};
    return unless $o->{STRUCT}{'.any'};

    # Figure out whether to grab all structs
    my $nstructs = scalar grep { $_ =~ /^[_a-z][_0-9a-z]*$/i } 
      keys %{$o->{STRUCT}};
    $o->{STRUCT}{'.all'} = 1 
      if $nstructs == 0;

    # Load currently-defined types (stored in $o->{ILSM}{typeconv})
    $o->get_maps;
    $o->get_types;

    # Parse structs
    $::RD_HINT++;
    my $grammar = Inline::Struct::grammar::grammar()
      or croak "Can't find Struct grammar!\n";
    my $parser = $o->{STRUCT}{'.parser'} = Parse::RecDescent->new($grammar);
    $parser->{data}{typeconv} = $o->{ILSM}{typeconv};
    $parser->code($o->{ILSM}{code});
    $o->{ILSM}{typeconv} = $parser->{data}{typeconv};

    $o->{STRUCT}{'.xs'} = "";
    $o->{STRUCT}{'.macros'} = <<END;
#define NEW_INLINE_STRUCT(targ,type) INLINE_STRUCT_NEW_##type(targ)
#define INLINE_STRUCT_FIELDS(type) INLINE_STRUCT_FIELDS_##type
#define INLINE_STRUCT_INIT_LIST(targ,type) INLINE_STRUCT_INITL_##type(targ)
#define INLINE_STRUCT_INIT_AREF(src,targ,type) INLINE_STRUCT_INITA_##type(src,targ)
#define INLINE_STRUCT_INIT_HREF(src,targ,type) INLINE_STRUCT_INITH_##type(src,targ)
#define INLINE_STRUCT_ARRAY(src,targ,type) INLINE_STRUCT_ARRAY_##type(src,targ)
#define INLINE_STRUCT_VALUES(src,targ,type) INLINE_STRUCT_ARRAY_##type(src,targ)
#define INLINE_STRUCT_HASH(src,targ,type) INLINE_STRUCT_HASH_##type(src,targ)
#define INLINE_STRUCT_KEYS(src,targ,type) INLINE_STRUCT_KEYS_##type(src,targ)
END

    my @struct_list;
    if ($o->{STRUCT}{'.all'}) { 
	@struct_list = @{$parser->{data}{structs}} 
    }
    else { 
	@struct_list = grep { $_ =~ /^[_a-z][_a-z0-9]*$/i } 
	  keys %{$o->{STRUCT}} 
    }
    for my $struct (@struct_list) {
	unless (defined $parser->{data}{struct}{$struct}) {
	    warn "Struct $struct requested but not found" if $^W;
	    next;
	}
	$o->{STRUCT}{'.bound'}{$struct}++;
	my $cname = $parser->{data}{struct}{$struct}{cname};
	my ($NEW, $FIELDS, $INITL, $INITA, $INITH, $HASH, $ARRAY, $KEYS);

	# Set up the initial part of the macros
	$NEW = <<END;
#define INLINE_STRUCT_NEW_${struct}(targ) { \\
	HV *hv = get_hv("Inline::Struct::${struct}::_map_", 1); \\
	HV *entry = newHV(); \\
	SV *entrv = (SV*)newRV((SV*)entry); \\
	SV *lookup; \\
	char *key; \\
	STRLEN klen; \\
	Newz(1564,targ,1,$cname); \\
	lookup = newSViv((IV)targ); \\
	key = SvPV(lookup, klen); \\
	hv_store(entry, "REFCNT", 6, newSViv(0), 0); \\
	hv_store(entry, "FREE", 4, newSViv(1), 0); \\
	hv_store(hv, key, klen, entrv, 0); \\
}
END
	$FIELDS = "#define INLINE_STRUCT_FIELDS_$struct " .
	  (scalar @{$parser->{data}{struct}{$struct}{fields}}) . "\n";
	$INITL = "#define INLINE_STRUCT_INITL_$struct(targ) {\\\n";
	$INITA = <<END;
#define INLINE_STRUCT_INITA_$struct(src,targ) { \\
AV *av = (AV*)SvRV(src); \\
int l = av_len(av) + 1; \\
int i; \\
for (i=0; i<l; i++) { \\
SV *val = *av_fetch(av, i, 0); \\
END
	$INITH = <<END;
#define INLINE_STRUCT_INITH_$struct(src,targ) { \\
HV *hv = (HV*)SvRV(src); \\
int l = hv_iterinit(hv); \\
int i; \\
for (i=0; i<l; i++) { \\
I32 retlen; \\
HE *he = hv_iternext(hv); \\
char *k = hv_iterkey(he,&retlen); \\
SV *val = hv_iterval(hv,he); \\
END
        $HASH = <<END;
#define INLINE_STRUCT_HASH_$struct(src,targ) \\
hv_clear(targ); \\
END
        $ARRAY = <<END;
#define INLINE_STRUCT_ARRAY_$struct(src,targ) \\
av_clear(targ); \\
END

	$KEYS = <<END;
#define INLINE_STRUCT_KEYS_$struct(src,targ) \\
av_clear(targ); \\
END

        my $maxi = scalar @{$parser->{data}{struct}{$struct}{fields}};
	next unless $maxi > 0;

	$o->{STRUCT}{'.xs'} .= <<END;
MODULE = $o->{API}{module}		PACKAGE = Inline::Struct::$struct

PROTOTYPES: DISABLE

$cname *
new(klass, ...)
        char *klass
    PREINIT:
        int _items = items - 1;
    CODE:
	NEW_INLINE_STRUCT(RETVAL,$struct);
	if (_items == 0) { }
	else if (SvROK(ST(1)) && SvTYPE(SvRV(ST(1)))==SVt_PVAV) {
	    INLINE_STRUCT_INIT_AREF(ST(1),RETVAL,$struct);
	}
	else if (SvROK(ST(1)) && SvTYPE(SvRV(ST(1)))==SVt_PVHV) {
	    INLINE_STRUCT_INIT_HREF(ST(1),RETVAL,$struct);
	}
	else {
            INLINE_STRUCT_INIT_LIST(RETVAL,$struct);
	}
    OUTPUT:
        RETVAL

void
DESTROY(object)
        $cname *object
    PREINIT:
        HV *map = get_hv("Inline::Struct::${struct}::_map_", 1);
        SV *lookup;
        STRLEN klen;
        char *key;
    CODE:
        lookup = newSViv((IV)object);
        key = SvPV(lookup, klen);
        if (hv_exists(map, key, klen)) {
            HV *info = (HV*)SvRV(*hv_fetch(map, key, klen, 0));
            SV *refcnt = *hv_fetch(info, "REFCNT", 6, 0);
            int tofree = SvIV(*hv_fetch(info, "FREE", 4, 0));
            if (tofree && SvIV(refcnt) == 1) {
              Safefree(object);
              hv_delete(map, key, klen, 0);
            }
            else
              sv_dec(refcnt);
        }

HV *
_HASH(object)
        $cname *object
    CODE:
        RETVAL = newHV();
	INLINE_STRUCT_HASH(object, RETVAL, $struct);
    OUTPUT:
        RETVAL

AV *
_VALUES(object)
        $cname *object
    CODE:
        RETVAL = newAV();
	INLINE_STRUCT_VALUES(object, RETVAL, $struct);
    OUTPUT:
        RETVAL

AV *
_ARRAY(object)
        $cname *object
    CODE:
        RETVAL = newAV();
	INLINE_STRUCT_ARRAY(object, RETVAL, $struct);
    OUTPUT:
        RETVAL

AV *
_KEYS(object)
        $cname *object
    CODE:
        RETVAL = newAV();
        INLINE_STRUCT_KEYS(object, RETVAL, $struct);
    OUTPUT:
        RETVAL

END

        my $i=1;
	for my $field (@{$parser->{data}{struct}{$struct}{fields}}) {
	    my $flen = length $field;
	    my $type = $parser->{data}{struct}{$struct}{field}{$field};
	    my $q = ($i == 1 ? 'if' : 'else if');
	    my $t =
	      typeconv($o, "targ->$field",
			   "val",
			   $type,
			   "input_expr",
			   1,
			  );
	    my $s =
	      typeconv($o, "src->$field",
			   "tmp",
			   $type,
			   "output_expr",
			   1,
			  );
	    $INITL .=
	      (typeconv($o, "targ->$field",
			    "ST($i)",
			    $type,
			    "input_expr",
			    1
			   ) .
	       "; \\\n");
	    $INITA .= qq{$q(i == ${\($i-1)}) \\\n$t; \\\n};
	    $INITH .= qq{$q(strEQ(k, "$field")) \\\n$t;\\\n};
	    $HASH .= (qq{{\\\n\tSV*tmp=newSViv(0);\\\n$s \\
\thv_store(targ, "$field", $flen, tmp, 0); \\\n}} .
		      ($i == $maxi ? "" : "\\") .
		      "\n"
		     );
	    $ARRAY .= (qq{{\\\n\tSV*tmp=newSViv(0);\\\n$s \\
\tav_push(targ, tmp); \\\n}} .
		       ($i == $maxi ? "" : "\\") .
		       "\n"
		      );
	    $KEYS .= (qq{av_push(targ, newSVpv("$field", 0));} .
		      ($i == $maxi ? "" : "\\") .
		      "\n"
		     );
	    $o->{STRUCT}{'.xs'} .=
	       ("void\n" .
	        $field . "(object, ...)\n\t" .
		$cname . " *object\n" .
		"    PREINIT:\n" .
		"\tSV *retval = newSViv(0);\n" .
		"    PPCODE:\n" .
		"\tif (items != 1) {\n" .
		typeconv($o, "object->$field",
		             "ST(1)",
			     $type,
			     "input_expr",
			    ) . ";\n" .
		typeconv($o, "object", "retval", "$cname *", "output_expr")."\n" .
		"\t}\n" .
		"\telse {\n" .
		typeconv($o, "object->$field", "retval", $type, "output_expr") .
		"\n\t}\n\t" .
		"XPUSHs(retval);\n\n"
	       );
            $i++;
	}
	$INITA .= "}}\n";
	$INITL .= "}\n";
	$INITH .=
	  qq|else \\\n\tcroak("No such field '%s' in $cname\\n", k);}}\n|;

	$o->{STRUCT}{'.macros'} .= <<END;
$NEW
$FIELDS
$INITA
$INITL
$INITH
$HASH
$ARRAY
$KEYS
END

    }

    # Write a typemap file containing typemaps for each thingy
    write_typemap($o);
}

sub write_typemap {
    my $o = shift;
    my $data = $o->{STRUCT}{'.parser'}{data};

    my ($TYPEMAP, $INPUT, $OUTPUT);
    for my $struct (@{$data->{structs}}) {
	my $type = "O_OBJECT_$struct";
	my @ctypes = grep { $data->{typeconv}{type_kind}{$_} eq $type } 
	   keys %{$data->{typeconv}{type_kind}};
	$TYPEMAP .= join "\n", map { "$_\t\t$type" } @ctypes;
	$INPUT .= $type."\n".$data->{typeconv}{input_expr}{$type};
	$OUTPUT .= $type."\n".$data->{typeconv}{output_expr}{$type};
    }

    $o->mkpath($o->{API}{build_dir})
      unless -d $o->{API}{build_dir};
    my $fh;
    my $fname = $o->{API}{build_dir}.'/Struct.map'; 
    open $fh, ">$fname"
      or die $!;
    print $fh <<END;
TYPEMAP
$TYPEMAP

INPUT
$INPUT

OUTPUT
$OUTPUT
END

    close $fh;
    $o->validate( TYPEMAPS => $fname );
}

sub typeconv {
    my $o = shift;
    my $var = shift;
    my $arg = shift;
    my $type = shift;
    my $dir = shift;
    my $preproc = shift;
    my $tkind = $o->{ILSM}{typeconv}{type_kind}{$type};
    my $ret =
      eval qq{qq{$o->{ILSM}{typeconv}{$dir}{$tkind}}};
    chomp $ret;
    $ret =~ s/\n/\\\n/g if $preproc;
    return $ret;
}

#=============================================================================
# Return a little info about the structs we bound to.
#=============================================================================
sub info {
    my $o = shift;
    my $info = "";
    parse($o) unless defined $o->{STRUCT}{'.parser'};
    my $data = $o->{STRUCT}{'.parser'}{data};
    if (defined $o->{STRUCT}{'.bound'}) {
	$info .= "The following structs have been bound to Perl:\n";
	for my $struct (keys %{$o->{STRUCT}{'.bound'}}) {
	    $info .= "\tstruct $struct {\n";
	    for my $field (@{$data->{struct}{$struct}{fields}}) {
		my $type = $data->{struct}{$struct}{field}{$field};
		$info .= "\t\t$type $field;\n";
	    }
	    $info .= "\t};\n";
	}
    }
    else {
	$info .= "No structs were bound to Perl.\n";
    }
    return $info;
}

1;

__END__
