#
# 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 Scalar::Util qw(reftype looks_like_number);

=pod

=head1 NAME

qpid::proton::Data

=head1 DESCRIPTION

The B<Data> class provides an interface for decoding, extract, creating and
encoding arbitrary AMQP data. A B<Data> object contains a tree of AMQP values.
Leaf nodes in this tree correspond to scalars in the AMQP type system such as
B<INT> or B<STRING>. Interior nodes in this tree correspond to compound values
in the AMQP type system such as B<LIST>, B<MAP>, B<ARRAY> or B<DESCRIBED>. The
root node of the tree is the B<Data> object itself and can have an arbitrary
number of children.

A B<Data> object maintains the notion of the current sibling node and a current
parent node. Siblings are ordered within their parent. Values are accessed
and/or added by using the B<next>, B<prev>, B<enter> and B<exit> methods to
navigate to the desired location in the tree and using the supplied variety of
mutator and accessor methods to access or add a value of the desired type.

The mutator methods will always add a value I<after> the current node in the
tree. If the current node has a next sibling the mutator method will overwrite
the value on this node. If there is no current node or the current node has no
next sibling then one will be added. The accessor methods always set the
add/modified node to the current node. The accessor methods read the value of
the current node and do not change which node is current.

=cut

package qpid::proton::Data;

=pod

=head1 CONSTRUCTOR

Creates a new instance with the specified capacity.

=over

=item my $data = qpid::proton::Data->new( CAPACITY );

=back

=cut

sub new {
    my ($class) = @_;
    my ($self) = {};
    my $capacity = $_[1] || 16;
    my $impl = $capacity;
    $self->{_free} = 0;

    if($capacity) {
        if (::looks_like_number($capacity)) {
            $impl = cproton_perl::pn_data($capacity);
            $self->{_free} = 1;
        }
    }

    $self->{_impl} = $impl;

    bless $self, $class;
    return $self;
}

sub DESTROY {
    my ($self) = @_;
    my $impl = $self->{_impl};

    cproton_perl::pn_data_free($impl) if $self->{_free};
}

=pod

=head1 ACTIONS

Clear all content for the data object.

=over

=item my $data->clear();

=back

=cut

sub clear {
    my ($self) = @_;
    my $impl = $self->{_impl};

    cproton_perl::pn_data_clear($impl);
}


=pod

=head1 NAVIGATION

The following methods allow for navigating through the nodes in the tree.

=cut


=pod

=over

=item $doc->enter;

=item if ($doc->enter()) { do_something_with_children; }

Sets the parent node to the current node and clears the current node.

Clearing the current node sets it I<before> the first child.

=item $doc->exit;

=item if ($doc->exit()) { do_something_with_parent; }

Sets the current node to the parent node, and the parent node to its own parent.

=item $doc->next;

=item $doc->prev;

Moves to the next/previous sibling and returns its type. If there is no next or
previous sibling then the current node remains unchanged.

=item $doc->rewind;

Clears the current node and sets the parent to the root node.

=back

=cut

sub enter {
    my ($self) = @_;
    my $impl = $self->{_impl};

    cproton_perl::pn_data_enter($impl);
}

sub exit {
    my ($self) = @_;
    my $impl = $self->{_impl};

    cproton_perl::pn_data_exit($impl);
}

sub rewind {
    my ($self) = @_;
    my $impl = $self->{_impl};

    cproton_perl::pn_data_rewind($impl);
}


=pod

=over

=item $doc->next;

=item if ($doc->next()) { do_something; }

Advances the current node to its next sibling and returns its type.

If there is no next sibling then the current node remains unchanged and
B<undef> is returned.

=item $doc->prev;

=item if ($doc->prev()) { do_something; }

Advances the current node to its previous sibling and returns its type.

If there is no previous sibling then the current node remains unchanged and
undef is returned.

=back

=cut

sub next {
    my ($self) = @_;
    my $impl = $self->{_impl};

    return cproton_perl::pn_data_next($impl);
}

sub prev {
    my ($self) = @_;
    my $impl = $self->{_impl};

    return cproton_perl::pn_data_prev($impl);
}


=pod

=head1 SUPPORTED TYPES

The following methods allow for inserting the various node types into the
tree.

=head2 NODE TYPE

You can retrieve the type of the current node.

=over

=item $type = $doc->get_type;

=back

=cut


sub get_type {
    my ($self) = @_;
    my $impl = $self->{_impl};
    my $type = cproton_perl::pn_data_type($impl);

    return qpid::proton::Mapping->find_by_type_value($type);
}


=pod

=head2 SCALAR TYPES

=cut

=pod

=head3 NULL

=over

=item $doc->put_null;

Inserts a null node.

=item $doc->is_null;

Returns true if the current node is null.

=back

=cut

sub put_null() {
    my ($self) = @_;
    my $impl = $self->{_impl};

    cproton_perl::pn_data_put_null($impl);
}

sub is_null {
    my ($self) = @_;
    my $impl = $self->{_impl};

    cproton_perl::pn_data_is_null($impl);
}

sub check {
    my ($self) = @_;
    my $impl = $self->{_impl};
    my $err = $_[1];

    # if we got a null then just exit
    return $err if !defined($err);

    if($err < 0) {
        die DataException->new("[$err]: " . cproton_perl::pn_data_error($impl));
    } else {
        return $err;
    }
}

=pod

=head3 BOOL

Handles a boolean (B<true>/B<false>) node.

=over

=item $doc->put_bool( VALUE );

=item $doc->get_bool;

=back

=cut

sub put_bool {
    my ($self) = @_;
    my $impl = $self->{_impl};
    my $value = $_[1] || 0;

    cproton_perl::pn_data_put_bool($impl, $value);
}

sub get_bool {
    my ($self) = @_;
    my $impl = $self->{_impl};

    cproton_perl::pn_data_get_bool($impl);
}

=pod

=head3 UBYTE

Handles an unsigned byte node.

=over

=item $data->put_ubyte( VALUE );

=item $data->get_ubyte;

=back

=cut

sub put_ubyte {
    my ($self) = @_;
    my $impl = $self->{_impl};
    my $value  = $_[1];

    die "ubyte must be defined" if !defined($value);
    die "ubyte must be non-negative" if $value < 0;

    check(cproton_perl::pn_data_put_ubyte($impl, int($value)));
}

sub get_ubyte {
    my ($self) = @_;
    my $impl = $self->{_impl};

    cproton_perl::pn_data_get_ubyte($impl);
}

=pod

=head3 BYTE

Handles a signed byte node.

=over

=item $data->put_byte( VALUE );

=item $data->get_byte;

=back

=cut

sub put_byte {
    my ($self) = @_;
    my $impl = $self->{_impl};
    my $value = $_[1];

    die "byte must be defined" if !defined($value);

    check(cproton_perl::pn_data_put_byte($impl, int($value)));
}

sub get_byte {
    my ($self) = @_;
    my $impl = $self->{_impl};

    cproton_perl::pn_data_get_byte($impl);
}

=pod

=head3 USHORT

Handles an unsigned short node.

=over

=item $data->put_ushort( VALUE );

=item $data->get_ushort;

=back

=cut

sub put_ushort {
    my ($self) = @_;
    my $impl = $self->{_impl};
    my $value = $_[1];

    die "ushort must be defined" if !defined($value);

    check(cproton_perl::pn_data_put_ushort($impl, int($value)));
}

sub get_ushort {
    my ($self) = @_;
    my $impl = $self->{_impl};

    cproton_perl::pn_data_get_ushort($impl);
}

=pod

=head3 SHORT

Handles a signed short node.

=over

=item $data->put_short( VALUE );

=item $data->get_short;

=back

=cut

sub put_short {
    my ($self) = @_;
    my $impl = $self->{_impl};
    my $value = $_[1];

    die "short must be defined" if !defined($value);

    check(cproton_perl::pn_data_put_short($impl, int($value)));
}

sub get_short {
    my ($self) = @_;
    my $impl = $self->{_impl};

    cproton_perl::pn_data_get_short($impl);
}

=pod

=head3 UINT

Handles an unsigned integer node.

=over

=item $data->put_uint( VALUE );

=item $data->get_uint;

=back

=cut

sub put_uint {
    my ($self) = @_;
    my $impl = $self->{_impl};
    my $value = $_[1];

    die "uint must be defined" if !defined($value);

    check(cproton_perl::pn_data_put_uint($impl, int($value)));
}

sub get_uint {
    my ($self) = @_;
    my $impl = $self->{_impl};

    cproton_perl::pn_data_get_uint($impl);
}

=pod

=head3 INT

Handles an integer node.

=over

=item $data->put_int( VALUE );

=item $data->get_int;

=back

=cut

sub put_int {
    my ($self) = @_;
    my $impl = $self->{_impl};
    my $value = $_[1];

    die "int must be defined" if !defined($value);

    check(cproton_perl::pn_data_put_int($impl, int($value)));
}

sub get_int {
    my ($self) = @_;
    my $impl = $self->{_impl};

    cproton_perl::pn_data_get_int($impl);
}

=pod

=head3 CHAR

Handles a character node.

=over

=item $data->put_char( VALUE );

=item $data->get_char;

=back

=cut

sub put_char {
    my ($self) = @_;
    my $impl = $self->{_impl};
    my $value = $_[1];

    die "char must be defined" if !defined($value);

    check(cproton_perl::pn_data_put_char($impl, int($value)));
}

sub get_char {
    my ($self) = @_;
    my $impl = $self->{_impl};

    cproton_perl::pn_data_get_char($impl);
}

=pod

=head3 ULONG

Handles an unsigned long node.

=over

=item $data->set_ulong( VALUE );

=item $data->get_ulong;

=back

=cut

sub put_ulong {
    my ($self) = @_;
    my $impl = $self->{_impl};
    my $value = $_[1];

    die "ulong must be defined" if !defined($value);

    check(cproton_perl::pn_data_put_ulong($impl, $value));
}

sub get_ulong {
    my ($self) = @_;
    my $impl = $self->{_impl};

    cproton_perl::pn_data_get_ulong($impl);
}

=pod

=head3 LONG

Handles a signed long node.

=over

=item $data->put_long( VALUE );

=item $data->get_long;

=back

=cut

sub put_long {
    my ($self) = @_;
    my $impl = $self->{_impl};
    my $value = $_[1];

    die "long must be defined" if !defined($value);

    cproton_perl::pn_data_put_long($impl, int($value));
}

sub get_long {
    my ($self) = @_;
    my $impl = $self->{_impl};

    cproton_perl::pn_data_get_long($impl);
}

=pod

=head3 TIMESTAMP

Handles a timestamp node.

=over

=item $data->put_timestamp( VALUE );

=item $data->get_timestamp;

=back

=cut

sub put_timestamp {
    my ($self) = @_;
    my $impl = $self->{_impl};
    my $value = $_[1];

    die "timestamp must be defined" if !defined($value);

    check(cproton_perl::pn_data_put_timestamp($impl, int($value)));
}

sub get_timestamp {
    my ($self) = @_;
    my $impl = $self->{_impl};

    cproton_perl::pn_data_get_timestamp($impl);
}

=pod

=head3 FLOAT

Handles a floating point node.

=over

=item $data->put_float( VALUE );

=item $data->get_float;

=back

=cut

sub put_float {
    my ($self) = @_;
    my $impl = $self->{_impl};
    my $value = $_[1];

    die "float must be defined" if !defined($value);

    check(cproton_perl::pn_data_put_float($impl, $value));
}

sub get_float {
    my ($self) = @_;
    my $impl = $self->{_impl};

    my $value = cproton_perl::pn_data_get_float($impl);

    cproton_perl::pn_data_get_float($impl);
}

=pod

=head3 DOUBLE

Handles a double node.

=over

=item $data->put_double( VALUE );

=item $data->get_double;

=back

=cut

sub put_double {
    my ($self) = @_;
    my $impl = $self->{_impl};
    my $value = $_[1];

    die "double must be defined" if !defined($value);

    check(cproton_perl::pn_data_put_double($impl, $value));
}

sub get_double {
    my ($self) = @_;
    my $impl = $self->{_impl};

    cproton_perl::pn_data_get_double($impl);
}

=pod

=head3 DECIMAL32

Handles a decimal32 node.

=over

=item $data->put_decimal32( VALUE );

=item $data->get_decimal32;

=back

=cut

sub put_decimal32 {
    my ($self) = @_;
    my $impl = $self->{_impl};
    my $value = $_[1];

    die "decimal32 must be defined" if !defined($value);

    check(cproton_perl::pn_data_put_decimal32($impl, $value));
}

sub get_decimal32 {
    my ($self) = @_;
    my $impl = $self->{_impl};

    cproton_perl::pn_data_get_decimal32($impl);
}

=pod

=head3 DECIMAL64

Handles a decimal64 node.

=over

=item $data->put_decimal64( VALUE );

=item $data->get_decimal64;

=back

=cut

sub put_decimal64 {
    my ($self) = @_;
    my $impl = $self->{_impl};
    my $value = $_[1];

    die "decimal64 must be defined" if !defined($value);

    check(cproton_perl::pn_data_put_decimal64($impl, $value));
}

sub get_decimal64 {
    my ($self) = @_;
    my $impl = $self->{_impl};

    cproton_perl::pn_data_get_decimal64($impl);
}

=pod

=head3 DECIMAL128

Handles a decimal128 node.

=over

=item $data->put_decimal128( VALUE );

=item $data->get_decimal128;

=back

=cut

sub put_decimal128 {
    my ($self) = @_;
    my $impl = $self->{_impl};
    my $value = $_[1];

    die "decimal128 must be defined" if !defined($value);

    my @binary = split //, pack("H[32]", sprintf("%032x", $value));
    my @bytes = ();

    foreach $char (@binary) {
        push(@bytes, ord($char));
    }
    check(cproton_perl::pn_data_put_decimal128($impl, \@bytes));
}

sub get_decimal128 {
    my ($self) = @_;
    my $impl = $self->{_impl};

    my $bytes = cproton_perl::pn_data_get_decimal128($impl);
    my $value = hex(unpack("H[32]", $bytes));

    return $value;
}

=pod

=head3 UUID

Handles setting a UUID value. UUID values can be set using a 128-bit integer
value or else a well-formed string.

=over

=item $data->put_uuid( VALUE );

=item $data->get_uuid;

=back

=cut

use Data::Dumper;

sub put_uuid {
    my ($self) = @_;
    my $impl = $self->{_impl};
    my $value = $_[1];

    die "uuid must be defined" if !defined($value);

    if($value =~ /[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}/) {
        $value =~ s/-//g;
        my @binary = split //, pack("H[32]", $value);
        my @bytes = ();

        foreach $char (@binary) {
            push(@bytes, ord($char));
         }

        check(cproton_perl::pn_data_put_uuid($impl, \@bytes));
    } else {
        die "uuid is malformed: $value";
    }
}

sub get_uuid {
    my ($self) = @_;
    my $impl = $self->{_impl};

    my $bytes = cproton_perl::pn_data_get_uuid($impl);

    my $value = unpack("H[32]", $bytes);
    $value = substr($value, 0, 8) . "-" .
        substr($value, 8, 4) . "-" .
        substr($value, 12, 4) . "-" .
        substr($value, 16, 4) . "-" .
        substr($value, 20);

    return $value;
}

=pod

=head3 BINARY

Handles a binary data node.

=over

=item $data->put_binary( VALUE );

=item $data->get_binary;

=back

=cut

sub put_binary {
    my ($self) = @_;
    my $impl = $self->{_impl};
    my $value = $_[1];

    die "binary must be defined" if !defined($value);

    check(cproton_perl::pn_data_put_binary($impl, $value)) if defined($value);
}

sub get_binary {
    my ($self) = @_;
    my $impl = $self->{_impl};

    cproton_perl::pn_data_get_binary($impl);
}

=pod

=head3 STRING

Handles a string node.

=over

=item $data->put_string( VALUE );

=item $data->get_string;

=back

=cut

sub put_string {
    my ($self) = @_;
    my $impl = $self->{_impl};
    my $value = $_[1];

    die "string must be defined" if !defined($value);

    check(cproton_perl::pn_data_put_string($impl, $value));
}

sub get_string {
    my ($self) = @_;
    my $impl = $self->{_impl};

    cproton_perl::pn_data_get_string($impl);
}

=pod

=head3 SYMBOL

Handles a symbol value.

=over

=item $data->put_symbol( VALUE );

=item $data->get_symbol;

=back

=cut

sub put_symbol {
    my ($self) = @_;
    my $impl = $self->{_impl};
    my $value = $_[1];

    die "symbol must be defined" if !defined($value);

    check(cproton_perl::pn_data_put_symbol($impl, $value));
}

sub get_symbol {
    my ($self) = @_;
    my $impl = $self->{_impl};

    cproton_perl::pn_data_get_symbol($impl);
}

=pod

=head3 DESCRIBED VALUE

A described node has two children: the descriptor and the value.

These are specified by entering the node and putting the
described values.

=over

=item $data->put_described;

=item $data->is_described;

=back

=cut

sub put_described {
    my ($self) = @_;
    my $impl = $self->{_impl};

    cproton_perl::pn_data_put_described($impl);
}

sub is_described {
    my ($self) = @_;
    my $impl = $self->{_impl};

    cproton_perl::pn_data_is_described($impl);
}

=pod

=head3 ARRAYS

Puts an array value.

Elements may be filled by entering the array node and putting the element values.
The values must all be of the specified array element type.

If an array is B<described> then the first child value of the array is the
descriptor and may be of any type.

=over

B<DESCRIBED> specifies whether the array is described or not.

B<TYPE> specifies the type of elements in the array.

=back

=over

=item $data->put_array( DESCRIBED, TYPE )

=item my ($count, $described, $array_type) = item $data->get_array

=back

=cut

sub put_array {
    my ($self) = @_;
    my $impl = $self->{_impl};
    my $described = $_[1] || 0;
    my $array_type = $_[2];

    die "array type must be defined" if !defined($array_type);

    check(cproton_perl::pn_data_put_array($impl,
                                          $described,
                                          $array_type->get_type_value));
}

sub get_array {
    my ($self) = @_;
    my $impl = $self->{_impl};

    my $count = cproton_perl::pn_data_get_array($impl);
    my $described = cproton_perl::pn_data_is_array_described($impl);
    my $type_value = cproton_perl::pn_data_get_array_type($impl);

    $type_value = qpid::proton::Mapping->find_by_type_value($type_value);

    return ($count, $described, $type_value);
}

sub get_array_type {
    my ($self) = @_;
    my $impl = $self->{_impl};

    cproton_perl::pn_data_get_array_type($impl);
}

=pod

=head3 LIST

Puts a list value.

Elements may be filled in by entering the list and putting element values.

=over

=item $data->put_list;

=item my $count = $data->get_list

=back

=cut

sub put_list {
    my ($self) = @_;
    my $impl = $self->{_impl};

    check(cproton_perl::pn_data_put_list($impl));
}

sub get_list {
    my ($self) = @_;
    my $impl = $self->{_impl};

    cproton_perl::pn_data_get_list($impl);
}

=pod

head3 MAP

Puts a map value.

Elements may be filled by entering the map node and putting alternating
key/value pairs.

=over

=item $data->put_map;

=item my $count = $data->get_map;

=back

=cut

sub put_map {
    my ($self) = @_;
    my $impl = $self->{_impl};

    check(cproton_perl::pn_data_put_map($impl));
}

sub get_map {
    my ($self) = @_;
    my $impl = $self->{_impl};

    cproton_perl::pn_data_get_map($impl);
}

sub put_list_helper {
    my ($self) = @_;
    my ($array) = $_[1];

    $self->put_list;
    $self->enter;

    for my $value (@{$array}) {
        if (qpid::proton::utils::is_num($value)) {
            if (qpid::proton::utils::is_float($value)) {
                $self->put_float($value);
            } else {
                $self->put_int($value);
            }
        } elsif (!defined($value)) {
            $self->put_null;
        } elsif ($value eq '') {
            $self->put_string($value);
        } elsif (ref($value) eq 'HASH') {
            $self->put_map_helper($value);
        } elsif (ref($value) eq 'ARRAY') {
            $self->put_list_helper($value);
        } else {
            $self->put_string($value);
        }
    }

    $self->exit;
}

sub get_list_helper {
    my ($self) = @_;
    my $result = [];
    my $type = $self->get_type;

    if ($cproton_perl::PN_LIST == $type->get_type_value) {
        my $size = $self->get_list;

        $self->enter;

        for(my $count = 0; $count < $size; $count++) {
            if ($self->next) {
                my $value_type = $self->get_type;
                my $value = $value_type->get($self);

                push(@{$result}, $value);
            }
        }

        $self->exit;
    }

    return $result;
}

sub put_map_helper {
    my ($self) = @_;
    my $hash = $_[1];

    $self->put_map;
    $self->enter;

    foreach(keys %{$hash}) {
        my $key = $_;
        my $value = $hash->{$key};

        my $keytype = ::reftype($key);
        my $valtype = ::reftype($value);

        if ($keytype eq ARRAY) {
            $self->put_list_helper($key);
        } elsif ($keytype eq "HASH") {
            $self->put_map_helper($key);
        } else {
            $self->put_string("$key");
        }

        if (::reftype($value) eq HASH) {
            $self->put_map_helper($value);
        } elsif (::reftype($value) eq ARRAY) {
            $self->put_list_helper($value);
        } else {
            $self->put_string("$value");
        }
    }

    $self->exit;
}

sub get_map_helper {
    my ($self) = @_;
    my $result = {};
    my $type = $self->get_type;

    if ($cproton_perl::PN_MAP == $type->get_type_value) {
        my $size = $self->get_map;

        $self->enter;

        for($count = 0; $count < $size; $count++) {
            if($self->next) {
                my $key = $self->get_type->get($self);
                if($self->next) {
                    my $value = $self->get_type->get($self);
                    $result->{$key} = $value;
                }
            }
        }

        $self->exit;

    }

    return $result;
}

1;
