# 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::Type;
use Clownfish;
use Clownfish::Parcel;
use Clownfish::Util qw( verify_args a_isa_b );
use Scalar::Util qw( blessed );
use Carp;

our %new_PARAMS = (
    const       => undef,
    specifier   => undef,
    indirection => undef,
    parcel      => undef,
    c_string    => undef,
    void        => undef,
    object      => undef,
    primitive   => undef,
    integer     => undef,
    floating    => undef,
    string_type => undef,
    va_list     => undef,
    arbitrary   => undef,
    composite   => undef,
);

sub new {
    my ( $either, %args ) = @_;
    my $package = ref($either) || $either;
    verify_args( \%new_PARAMS, %args ) or confess $@;

    my $flags = 0;
    $flags |= CONST       if $args{const};
    $flags |= NULLABLE    if $args{nullable};
    $flags |= VOID        if $args{void};
    $flags |= OBJECT      if $args{object};
    $flags |= PRIMITIVE   if $args{primitive};
    $flags |= INTEGER     if $args{integer};
    $flags |= FLOATING    if $args{floating};
    $flags |= STRING_TYPE if $args{string_type};
    $flags |= VA_LIST     if $args{va_list};
    $flags |= ARBITRARY   if $args{arbitrary};
    $flags |= COMPOSITE   if $args{composite};

    my $parcel
        = $args{parcel}
        ? Clownfish::Parcel->acquire( $args{parcel} )
        : $args{parcel};

    my $indirection = $args{indirection} || 0;
    my $specifier   = $args{specifier}   || '';
    my $c_string    = $args{c_string}    || '';

    return $package->_new( $flags, $parcel, $specifier, $indirection,
        $c_string );
}

our %new_integer_PARAMS = (
    const     => undef,
    specifier => undef,
);

sub new_integer {
    my ( $either, %args ) = @_;
    verify_args( \%new_integer_PARAMS, %args ) or confess $@;
    my $flags = 0;
    $flags |= CONST if $args{const};
    my $package = ref($either) || $either;
    return $package->_new_integer( $flags, $args{specifier} );
}

our %new_float_PARAMS = (
    const     => undef,
    specifier => undef,
);

sub new_float {
    my ( $either, %args ) = @_;
    verify_args( \%new_float_PARAMS, %args ) or confess $@;
    my $flags = 0;
    $flags |= CONST if $args{const};
    my $package = ref($either) || $either;
    return $package->_new_float( $flags, $args{specifier} );
}

our %new_object_PARAMS = (
    const       => undef,
    specifier   => undef,
    indirection => 1,
    parcel      => undef,
    incremented => 0,
    decremented => 0,
    nullable    => 0,
);

sub new_object {
    my ( $either, %args ) = @_;
    verify_args( \%new_object_PARAMS, %args ) or confess $@;
    my $flags = 0;
    $flags |= INCREMENTED if $args{incremented};
    $flags |= DECREMENTED if $args{decremented};
    $flags |= NULLABLE    if $args{nullable};
    $flags |= CONST       if $args{const};
    $args{indirection} = 1 unless defined $args{indirection};
    my $parcel = Clownfish::Parcel->acquire( $args{parcel} );
    my $package = ref($either) || $either;
    confess("Missing required param 'specifier'")
        unless defined $args{specifier};
    return $package->_new_object( $flags, $parcel, $args{specifier},
        $args{indirection} );
}

our %new_composite_PARAMS = (
    child       => undef,
    indirection => undef,
    array       => undef,
    nullable    => undef,
);

sub new_composite {
    my ( $either, %args ) = @_;
    verify_args( \%new_composite_PARAMS, %args ) or confess $@;
    my $flags = 0;
    $flags |= NULLABLE if $args{nullable};
    my $indirection = $args{indirection} || 0;
    my $array = defined $args{array} ? $args{array} : "";
    my $package = ref($either) || $either;
    return $package->_new_composite( $flags, $args{child}, $indirection,
        $array );
}

our %new_void_PARAMS = ( const => undef, );

sub new_void {
    my ( $either, %args ) = @_;
    verify_args( \%new_void_PARAMS, %args ) or confess $@;
    my $package = ref($either) || $either;
    return $package->_new_void( !!$args{const} );
}

sub new_va_list {
    my $either = shift;
    verify_args( {}, @_ ) or confess $@;
    my $package = ref($either) || $either;
    return $either->_new_va_list();
}

our %new_arbitrary_PARAMS = (
    parcel    => undef,
    specifier => undef,
);

sub new_arbitrary {
    my ( $either, %args ) = @_;
    verify_args( \%new_arbitrary_PARAMS, %args ) or confess $@;
    my $package = ref($either) || $either;
    my $parcel = Clownfish::Parcel->acquire( $args{parcel} );
    return $package->_new_arbitrary( $parcel, $args{specifier} );
}

1;

__END__

__POD__

=head1 NAME

Clownfish::Type - A variable's type.

=head1 METHODS

=head2 new

    my $type = MyType->new(
        specifier   => 'char',    # default undef
        indirection => undef,     # default 0
        const       => 1,         # default undef
        parcel      => undef,     # default undef
        c_string    => undef,     # default undef
    );

Generic constructor.

=over

=item *

B<specifier> - The C name for the type, not including any indirection or array
subscripts.  

=item *

B<indirection> - integer indicating level of indirection. Example: the C type
"float**" has a specifier of "float" and indirection 2.

=item *

B<const> - should be true if the type is const.

=item *

B<parcel> - A Clownfish::Parcel or a parcel name.

=item *

B<c_string> - The C representation of the type.

=back

=head2 new_integer

    my $type = Clownfish::Type->new_integer(
        const     => 1,       # default: undef
        specifier => 'char',  # required
    );

Return a Type representing an integer primitive.

Support is limited to a subset of the standard C integer types:

    int8_t
    int16_t
    int32_t
    int64_t
    uint8_t
    uint16_t
    uint32_t
    uint64_t
    char
    short
    int
    long
    size_t

Many others are not supported: "signed" or "unsigned" anything, "long long",
"ptrdiff_t", "off_t", etc.  

The following Charmonizer typedefs are supported:

    bool_t

=over

=item * B<const> - Should be true if the type is const.

=item * B<specifier> - Must match one of the supported types.

=back

=head2 new_float

    my $type = Clownfish::Type->new_float(
        const     => 1,           # default: undef
        specifier => 'double',    # required
    );

Return a Type representing a floating point primitive.

Two specifiers are supported:

    float
    double

=over

=item * B<const> - Should be true if the type is const.

=item * B<specifier> - Must match one of the supported types.

=back

=cut

=head2 new_composite

    my $type = Clownfish::Type->new_composite(
        child       => $char_type,    # required
        indirection => undef,         # default 0
        array       => '[]',          # default undef,
        const       => 1,             # default undef
    );

Constructor for a composite type which is made up of repetitions of a single,
uniform subtype.

=over

=item *

B<child> - The Type which the composite is comprised of.

=item *

B<indirection> - integer indicating level of indirection. Example: the C type
"float**" has indirection 2.

=item *

B<array> - A string describing an array postfix.  

=item *

B<const> - should be 1 if the type is const.

=back

=head2 new_object

    my $type = Clownfish::Type->new_object(
        specifier   => "Lobster",       # required
        parcel      => "Crustacean",    # default: the default Parcel.
        const       => undef,           # default undef
        indirection => 1,               # default 1
        incremented => 1,               # default 0
        decremented => 0,               # default 0
        nullable    => 1,               # default 0
    );

Create a Type representing an object.  The Type's C<specifier> must match the
last component of the class name -- i.e. for the class "Crustacean::Lobster"
it must be "Lobster".

=over

=item * B<specifier> - Required.  Must follow the rules for
L<Clownfish::Class> class name components.

=item * B<parcel> - A L<Clownfish::Parcel> or a parcel name.

=item * B<const> - Should be true if the Type is const.  Note that this refers
to the object itself and not the pointer.

=item * B<indirection> - Level of indirection.  Must be 1 if supplied.

=item * B<incremented> - Indicate whether the caller must take responsibility
for an added refcount.

=item * B<decremented> - Indicate whether the caller must account for
for a refcount decrement.

=item * B<nullable> - Indicate whether the object specified by this type may
be NULL.

=back

The Parcel's prefix will be prepended to the specifier by new_object().

=head2 new_void

    my $type = Clownfish::Type->new_void(
        specifier => 'void',    # default: void
        const     => 1,         # default: undef
    );

Return a Type representing a the 'void' keyword in C.  It can be used either
for a void return type, or in conjuction with with new_composite() to support
the C<void*> opaque pointer type.

=over

=item * B<specifier> - Must be "void" if supplied.

=item * B<const> - Should be true if the type is const.  (Useful in the
context of C<const void*>).

=back

=head2 new_va_list

    my $type = Clownfish::Type->new_va_list(
        specifier => 'va_list',    # default: va_list
    );

Create a Type representing C's va_list, from stdarg.h.

=over

=item * B<specifier>.  Must be "va_list" if supplied.

=back

=head2 new_arbitrary

    my $type = Clownfish::Type->new_arbitrary(
        specifier => 'floatint_t',    # required
        parcel    => 'Crustacean',    # default: undef
    );

"Arbitrary" types are a hack that spares us from having to support C types
with complex declaration syntaxes -- such as unions, structs, enums, or
function pointers -- from within Clownfish itself.

The only constraint is that the C<specifier> must end in "_t".  This allows us
to create complex types in a C header file...

    typedef union { float f; int i; } floatint_t;

... pound-include the C header, then use the resulting typedef in a Clownfish
header file and have it parse as an "arbitrary" type.

    floatint_t floatint;

=over

=item * B<specifier> - The name of the type, which must end in "_t".

=item * B<parcel> - A L<Clownfish::Parcel> or a parcel name.

=back

If C<parcel> is supplied and C<specifier> begins with a capital letter, the
Parcel's prefix will be prepended to the specifier:

    foo_t         -> foo_t                # no prefix prepending
    Lobster_foo_t -> crust_Lobster_foo_t  # prefix prepended

=cut

=head2 equals

    do_stuff() if $type->equals($other);

Returns true if two Clownfish::Type objects are equivalent.

=head2 similar

    do_stuff() if $type->similar($other_type);

Weak checking of type which allows for covariant return types.  Calling this
method on anything other than an object type is an error.

=head2 to_c

    # Declare variable "foo".
    print $type->to_c . " foo;\n";

Return the C representation of the type.

=head2 set_c_string

Set the C representation of the type.

=head2 get_specifier get_parcel get_indirection get_array const nullable set_specifier set_nullable

Accessors.

=head2 is_object is_primitive is_integer is_floating is_composite is_void

    do_stuff() if $type->is_object;

Identify the flavor of Type, which is determined by the constructor which was
used to create it.

=over

=item * is_object: Clownfish::Type->new_object

=item * is_primitive: Either Clownfish::Type->new_integer or
Clownfish::Type->new_float

=item * is_integer: Clownfish::Type->new_integer

=item * is_floating: Clownfish::Type->new_float

=item * is_void: Clownfish::Type->new_void

=item * is_composite: Clownfish::Type->new_composite

=back

=head2 is_string_type

Returns true if $type represents a Clownfish type which holds unicode
strings.

=head2 incremented

Returns true if the Type is incremented.  Only applicable to object Types.

=head2 decremented

Returns true if the Type is decremented.  Only applicable to object Types.

=cut