##
##  OSSP cfg - Configuration Parsing
##  Copyright (c) 2002-2006 Ralf S. Engelschall <rse@engelschall.com>
##  Copyright (c) 2002-2006 The OSSP Project <http://www.ossp.org/>
##
##  This file is part of OSSP cfg, a configuration parsing library which
##  can be found at http://www.ossp.org/pkg/lib/cfg/.
##
##  Permission to use, copy, modify, and distribute this software for
##  any purpose with or without fee is hereby granted, provided that
##  the above copyright notice and this permission notice appear in all
##  copies.
##
##  THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
##  WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
##  MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
##  IN NO EVENT SHALL THE AUTHORS AND COPYRIGHT HOLDERS AND THEIR
##  CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
##  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
##  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
##  USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
##  ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
##  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
##  OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
##  SUCH DAMAGE.
##
##  cfg.pm: Perl Binding (Perl part)
##

package OSSP::cfg;

use 5.008;
use strict;
use warnings;
use Carp;
use XSLoader;
use Exporter;

##
##  API Definition
##

#   API version
our $VERSION = do { my @v = ('0.9.11' =~ m/\d+/g); sprintf("%d.".("%02d"x$#v), @v); };

#   API inheritance
our @ISA = qw(Exporter);

#   API symbols
my $symbols = {
    'const' => [qw(
        CFG_OK
        CFG_ERR_ARG
        CFG_ERR_USE
        CFG_ERR_MEM
        CFG_ERR_SYS
        CFG_ERR_FMT
        CFG_ERR_INT
        CFG_ERR_SYN
        CFG_ERR_NDE
        CFG_FMT_CFG
        CFG_FMT_XML
        CFG_NODE_TYPE_SEQ
        CFG_NODE_TYPE_DIR
        CFG_NODE_TYPE_OPT
        CFG_NODE_TYPE_ARG
        CFG_NODE_ATTR_PARENT
        CFG_NODE_ATTR_LBROTH
        CFG_NODE_ATTR_RBROTH
        CFG_NODE_ATTR_CHILD1
        CFG_NODE_ATTR_CHILDL
        CFG_NODE_ATTR_CHILDS
        CFG_NODE_ATTR_NODES
        CFG_NODE_ATTR_DEPTH
        CFG_NODE_ATTR_SRCNAME
        CFG_NODE_ATTR_SRCPOS
        CFG_NODE_ATTR_TYPE
        CFG_NODE_ATTR_TOKEN
        CFG_NODE_ATTR_DATA
        CFG_DATA_TYPE_PTR
        CFG_DATA_TYPE_STR
        CFG_DATA_TYPE_INT
        CFG_DATA_TYPE_FLT
        CFG_DATA_CTRL_CLONE
        CFG_DATA_CTRL_DESTROY
        CFG_DATA_ATTR_TYPE
        CFG_DATA_ATTR_VALUE
        CFG_DATA_ATTR_CTRL
    )],
    'func' => [qw(
        cfg_create
        cfg_destroy
        cfg_error
        cfg_version
        cfg_import
        cfg_export
        cfg_node_create
        cfg_node_destroy
        cfg_node_clone
        cfg_node_set
        cfg_node_get
        cfg_node_root
        cfg_node_select
        cfg_node_find
        cfg_node_apply
        cfg_node_cmp
        cfg_node_link
        cfg_node_unlink
        cfg_data_set
        cfg_data_get
        cfg_data_ctrl
    )]
};

#   API symbol exportation
our %EXPORT_TAGS = (
    'all'   => [ @{$symbols->{'const'}}, @{$symbols->{'func'}} ],
    'const' => [ @{$symbols->{'const'}} ],
    'func'  => [ @{$symbols->{'func'}}  ]
);
our @EXPORT_OK = @{$EXPORT_TAGS{'all'}};
our @EXPORT    = ();

##
##  High-Level Perl Module OO-style API
##  (just an OO wrapper around the C-style API)
##

sub new {
    my $proto = shift;
    my $class = ref($proto) || $proto;
    my $self = {};
    bless ($self, $class);
    $self->{-cfg} = undef;
    $self->{-rc}  = $self->CFG_OK;
    my $rc = cfg_create($self->{-cfg});
    if ($rc != $self->CFG_OK) {
        croak(sprintf("OSSP::cfg::new: cfg_create: %s (%d)", $self->error($rc), $rc));
    }
    return $self;
}

sub DESTROY ($) {
    my ($self) = @_;
    $self->{-rc} = cfg_destroy($self->{-cfg}) if (defined($self->{-cfg}));
    if ($self->{-rc} != $self->CFG_OK) {
        carp(sprintf("OSSP::cfg::DESTROY: cfg_destroy: %s (%d)", $self->error($self->{-rc}), $self->{-rc}));
        return;
    }
    $self->{-cfg} = undef;
    $self->{-rc}  = undef;
    return;
}

sub import { # ($$$$)
    #   ATTENTION: The OSSP cfg API function "import" conflicts with
    #   the standardized "import" method the Perl world expects from
    #   their modules. In order to keep the Perl binding consist
    #   with the C API, we solve the conflict under run-time by
    #   distinguishing between the two types of "import" calls.
    if (defined($_[0]) and ref($_[0]) =~ m/^OSSP::cfg/) {
        #   the regular OSSP::cfg "import" method
        my ($self, $node, $fmt, $in) = @_;
        $self->{-rc} = cfg_import($self->{-cfg}, $node, $fmt, $in, length($in));
        return ($self->{-rc} == $self->CFG_OK);
    }
    else {
        #   the special Perl "import" method
        #   (usually inherited from the Exporter)
        no strict "refs";
        return OSSP::cfg->export_to_level(1, @_);
    }
}

sub export { # ($$$)
    #   ATTENTION: The OSSP cfg API function "export" conflicts with
    #   the standardized "export" method the Perl world expects from
    #   their modules. In order to keep the Perl binding consist
    #   with the C API, we solve the conflict under run-time by
    #   distinguishing between the two types of "export" calls.
    if (defined($_[0]) and ref($_[0]) =~ m/^OSSP::cfg/) {
        #   the regular OSSP::cfg "export" method
        my ($self, $node, $fmt) = @_;
        my $out;
        $self->{-rc} = cfg_export($self->{-cfg}, $node, $fmt, $out, 0);
        return ($self->{-rc} == $self->CFG_OK ? $out : undef);
    }
    else {
        #   the special Perl "export" method
        #   (usually inherited from the Exporter)
        return Exporter::export(@_);
    }
}

sub error ($;$) {
    my ($self, $rc) = @_;
    $rc = $self->{-rc} if (not defined($rc));
    my $error;
    if (cfg_error($self->{-cfg}, $rc, $error) != $self->CFG_OK) {
        croak("OSSP::cfg::error: cfg_error: INTERNAL ERROR");
    }
    return wantarray ? ($error, $rc) : $error;
}

sub version () {
    my ($self) = @_;
    return cfg_version();
}

sub node_create ($) {
    my ($self) = @_;
    my $node;
    $self->{-rc} = cfg_node_create($self->{-cfg}, $node);
    return ($self->{-rc} == $self->CFG_OK ? $node : undef);
}

sub node_destroy ($$) {
    my ($self, $node) = @_;
    $self->{-rc} = cfg_node_destroy($self->{-cfg}, $node);
    return ($self->{-rc} == $self->CFG_OK);
}

sub node_clone ($$) {
    my ($self, $node) = @_;
    my $node2;
    $self->{-rc} = cfg_node_clone($self->{-cfg}, $node, $node2);
    return ($self->{-rc} == $self->CFG_OK ? $node2 : undef);
}

sub node_set ($$$$) {
    my ($self, $node, $attr, $arg) = @_;
    $self->{-rc} = cfg_node_set($self->{-cfg}, $node, $attr, $arg);
    return ($self->{-rc} == $self->CFG_OK);
}

sub node_get ($$$) {
    my ($self, $node, $attr) = @_;
    my $arg;
    $self->{-rc} = cfg_node_get($self->{-cfg}, $node, $attr, $arg);
    return ($self->{-rc} == $self->CFG_OK ? $arg : undef);
}

sub node_root ($;$) {
    my ($self, $node_new) = @_;
    $node_new ||= \0;
    my $node_old;
    $self->{-rc} = cfg_node_root($self->{-cfg}, $node_new, $node_old);
    return ($self->{-rc} == $self->CFG_OK ? $node_old : undef);
}

sub node_select ($$$) {
    my ($self, $node, $spec) = @_;
    my $result;
    $self->{-rc} = cfg_node_select($self->{-cfg}, $node, $result, $spec);
    return ($self->{-rc} == $self->CFG_OK ? $result : undef);
}

sub node_find ($$$$) {
    my ($self, $node, $cb_fct_cmp, $cb_ctx_cmp) = @_;
    my $cont;
    $self->{-rc} = cfg_node_find($self->{-cfg}, $node, $cb_fct_cmp, $cb_ctx_cmp, $cont);
    return ($self->{-rc} == $self->CFG_OK ? $cont : undef);
}

sub node_apply ($$$$) {
    my ($self, $node, $cb_fct_cmp, $cb_ctx_cmp, $cb_fct_cb, $cb_ctx_cb) = @_;
    $self->{-rc} = cfg_node_apply($self->{-cfg}, $node, $cb_fct_cmp, $cb_ctx_cmp, $cb_fct_cb, $cb_ctx_cb);
    return ($self->{-rc} == $self->CFG_OK);
}

sub node_cmp ($$$) {
    my ($self, $node, $token) = @_;
    $self->{-rc} = cfg_node_cmp($self->{-cfg}, $node, $token);
    return ($self->{-rc} == $self->CFG_OK);
}

sub node_link ($$$$) {
    my ($self, $node, $id, $node2) = @_;
    $self->{-rc} = cfg_node_link($self->{-cfg}, $node, $id, $node2);
    return ($self->{-rc} == $self->CFG_OK);
}

sub node_unlink ($$) {
    my ($self, $node) = @_;
    $self->{-rc} = cfg_node_unlink($self->{-cfg}, $node);
    return ($self->{-rc} == $self->CFG_OK);
}

sub data_set ($$$$) {
    my ($self, $data, $attr, $value) = @_;
    $self->{-rc} = cfg_data_set($data, $attr, $value);
    return ($self->{-rc} == $self->CFG_OK);
}

sub data_get ($$$) {
    my ($self, $data, $attr) = @_;
    my $value;
    $self->{-rc} = cfg_data_get($data, $attr, $value);
    return ($self->{-rc} == $self->CFG_OK ? $value : undef);
}

sub data_ctrl ($$$;$) {
    my ($self, $data, $ctrl, $value) = @_;
    $self->{-rc} = cfg_data_ctrl($data, $ctrl, $value);
    return ($self->{-rc} == $self->CFG_OK);
}

##
##  Low-Level Perl XS C-style API
##  (actually just the activation of the XS part)
##

#   auto-loading constants
sub AUTOLOAD {
    my $constname;
    our $AUTOLOAD;
    ($constname = $AUTOLOAD) =~ s/.*:://;
    croak "&OSSP::cfg::constant not defined" if ($constname eq 'constant');
    my ($error, $val) = constant($constname);
    croak $error if ($error);
    { no strict 'refs'; *$AUTOLOAD = sub { $val }; }
    goto &$AUTOLOAD;
}

#   static-loading functions
XSLoader::load('OSSP::cfg', $VERSION);

##
##  Highest-Level Perl Convenience API
##  (based on OSSP::cfg Perl API above)
##

package OSSP::cfg::simple;

use 5.008;
use strict;
use warnings;
use Carp;
use IO::File;

#   object constructor
sub new ($;$) {
    my ($proto, $input) = @_;
    my $class = ref($proto) || $proto;
    my $self = {};
    bless ($self, $class);
    $self->{-cfg} = new OSSP::cfg
        or croak "failed to create OSSP::cfg object";
    $self->parse($input) if (defined($input));
    return $self;
}

#   object destructor
sub DESTROY ($) {
    my ($self) = @_;
    undef $self->{-cfg};
    return;
}

#   parse textual syntax into node tree
sub parse ($$) {
    my ($self, $input) = @_;
    my $cfg = $self->{-cfg};
    if (length($input) < 1024 and $input !~ m|[{}&\n\s]|s) {
        if (-f $input) {
            my $io = new IO::File "<" . $input
               or croak "unable to read from configuration file \"".$input."\"";
            { local $/; $input = <$io>; }
            $io->close();
        }
    }
    $cfg->import(\0, $cfg->CFG_FMT_CFG, $input)
        or die "failed to parse configuration: " . $cfg->error();
    return $self;
}

#   format textual syntax from node tree
sub format ($$) {
    my ($self) = @_;
    my $cfg = $self->{-cfg};
    my $root = $cfg->node_root();
    my $str = $cfg->export($root, $cfg->CFG_FMT_CFG)
        or die "failed to format configuration: " . $cfg->error();
    return $str;
}

#   pack Perl complex data structure into node tree
sub pack ($$) {
    my ($self, $tree) = @_;

    #   destroy old and create new OSSP::cfg object
    undef $self->{-cfg};
    my $cfg = new OSSP::cfg
        or croak "failed to create OSSP::cfg object";
    $self->{-cfg} = $cfg;

    #   start parsing with a sequence
    my $node = &parse_seq($cfg, $tree);

    #   parse a sequence (reference) scalar
    sub parse_seq {
        my ($cfg, $seq) = @_;
        my $node = undef;
        if (ref($seq) eq 'ARRAY') {
            $node = $cfg->node_create();
            $cfg->node_set($node, $cfg->CFG_NODE_ATTR_TYPE, $cfg->CFG_NODE_TYPE_SEQ);
            if (@{$seq} > 0) {
                my $child = &parse_dir($cfg, $seq->[0]);
                $cfg->node_set($node, $cfg->CFG_NODE_ATTR_CHILD1, $child);
                my $sibling = $child;
                foreach my $dir (@{$seq}[1..$#{$seq}]) {
                    $child = &parse_dir($cfg, $dir);
                    $cfg->node_set($sibling, $cfg->CFG_NODE_ATTR_RBROTH, $child);
                    $sibling = $child;
                }
            }
        }
        return $node;
    }

    #   parse a directive (reference) scalar
    sub parse_dir {
        my ($cfg, $dir) = @_;
        my $node = undef;
        if (ref($dir) eq 'ARRAY') {
            $node = $cfg->node_create();
            $cfg->node_set($node, $cfg->CFG_NODE_ATTR_TYPE, $cfg->CFG_NODE_TYPE_DIR);
            if (@{$dir} > 0) {
                my $child = &parse_tok($cfg, $dir->[0]);
                $cfg->node_set($node, $cfg->CFG_NODE_ATTR_CHILD1, $child);
                my $sibling = $child;
                foreach my $tok (@{$dir}[1..$#{$dir}]) {
                    $child = &parse_tok($cfg, $tok);
                    $cfg->node_set($sibling, $cfg->CFG_NODE_ATTR_RBROTH, $child);
                    $sibling = $child;
                }
            }
        }
        return $node;
    }

    #   parse a token scalar
    sub parse_tok {
        my ($cfg, $tok) = @_;
        my $node;
        if (ref($tok) eq 'ARRAY') {
            #   token is actually a sub-sequence
            $node = &parse_seq($cfg, $tok); # recursion
        }
        else {
            #   token is a plain one
            $node = $cfg->node_create();
            $cfg->node_set($node, $cfg->CFG_NODE_ATTR_TYPE, $cfg->CFG_NODE_TYPE_ARG);
            $cfg->node_set($node, $cfg->CFG_NODE_ATTR_TOKEN, $tok);
        }
        return $node;
    }

    #   set generated node tree into OSSP::cfg object
    $cfg->node_root($node);

    return $self;
}

#   unpack Perl complex data structure from node tree
sub unpack ($;%) {
    my ($self, %arg) = @_;
    my $cfg = $self->{-cfg};

    #   start traversing the node tree from the root node
    my $root = $cfg->node_root();
    my $tree = &traverse($cfg, \%arg, $root);

    #   node processing function
    sub traverse {
        my ($cfg, $arg, $node) = @_;
        return if (not(ref($node) and $$node != 0));
        my $type = $cfg->node_get($node, $cfg->CFG_NODE_ATTR_TYPE);
        if ($type == $cfg->CFG_NODE_TYPE_SEQ) {
            #   node is a sequence
            my $seq = [];
            $node = $cfg->node_get($node, $cfg->CFG_NODE_ATTR_CHILD1);
            while (ref($node) and $$node != 0) {
                my $tree = &traverse($cfg, $arg, $node); # recursion
                push(@{$seq}, $tree) if (defined($tree));
                $node = $cfg->node_get($node, $cfg->CFG_NODE_ATTR_RBROTH);
            }
            #   optional post-processing of data structure
            $seq = &unpack_postproc_merge   ($seq, $arg->{-merge})   if (defined($arg->{-merge}));
            $seq = &unpack_postproc_index   ($seq, $arg->{-index})   if (defined($arg->{-index}));
            $seq = &unpack_postproc_strip   ($seq, $arg->{-strip})   if (defined($arg->{-strip}));
            $seq = &unpack_postproc_flatten ($seq, $arg->{-flatten}) if (defined($arg->{-flatten}));
            return $seq;
        }
        elsif ($type == $cfg->CFG_NODE_TYPE_DIR) {
            #   node is a directive
            my $dir = [];
            $node = $cfg->node_get($node, $cfg->CFG_NODE_ATTR_CHILD1);
            while (ref($node) and $$node != 0) {
                my $tree = &traverse($cfg, $arg, $node); # recursion
                push(@{$dir}, $tree) if (defined($tree));
                $node = $cfg->node_get($node, $cfg->CFG_NODE_ATTR_RBROTH);
            }
            return $dir;
        }
        else {
            #   node is a token
            my $token = $cfg->node_get($node, $cfg->CFG_NODE_ATTR_TOKEN);
            return $token;
        }
    }
    return $tree;
}

#   unpack helper function:
#   optionally merge arguments of DIRectives starting with same TOKen.
#   complexity: O(n) (actually 3*n)
sub unpack_postproc_merge {
    my ($seq, $re) = @_;
    
    #   pass 1: determine directive names and their 
    #   occurences in sequence. complexity: O(n)
    my $dir = {};
    for (my $i = 0; $i < @{$seq}; $i++) {
        if (    ref($seq->[$i]) eq 'ARRAY' 
            and not ref($seq->[$i]->[0])
            and $seq->[$i]->[0] =~ m/^$re$/s) {
            my $name = $seq->[$i]->[0];
            $dir->{$name} = [] if (not defined($dir->{$name}));
            push(@{$dir->{$name}}, $i);
        }
    }

    #   pass 2: append arguments of second and following sequences
    #   to arguments of first directive. complexity: O(n)
    foreach my $name (keys(%{$dir})) {
        if (@{$dir->{$name}} > 1) {
            my $f = $dir->{$name}->[0];
            foreach my $n (@{$dir->{$name}}[1..$#{$dir->{$name}}]) {
                foreach my $tok (@{$seq->[$n]}[1..$#{$seq->[$n]}]) {
                    push(@{$seq->[$f]}, $tok);
                }
                $seq->[$n] = undef;
            }
        }
    }

    #   pass 3: shrink sequence by eliminating removing entries
    #   complexity: O(n)
    for (my $i = 0; $i < @{$seq}; $i++) {
        if (not defined($seq->[$i])) {
            splice(@{$seq}, $i, 1);
            $i--; # compensate for splice operation
        }
    }

    return $seq;
}

#   unpack helper function:
#   optionally annotate SEQuence nodes with a DIRective name indexing hash
#   complexity: O(n)
sub unpack_postproc_index {
    my ($seq, $re) = @_;

    #   create an index hash
    my $idx = {};
    for (my $i = 0; $i < @{$seq}; $i++) {
        if (ref($seq->[$i]) eq 'ARRAY') {
            #   determine name of directive, i.e.,
            #   first non-sub-sequence element
            my $name = {};
            for (my $j = 0; $j < @{$seq->[$j]}; $j++) {
                $name = $seq->[$i]->[$j];
                last if (not ref($name));
            }
            if (    not ref($name)
                and $name =~ m/^$re$/s 
                and not defined($idx->{$name})) {
                #    remember position of directive's
                #    first occurrence in sequence
                $idx->{$name} = ($i + 1);
            }
        }
    }

    #   insert index at first position of sequence array following
    #   perlref(1)'s "Pseudo-hashes: Using an array as a hash" approach.
    unshift(@{$seq}, $idx);

    return $seq;
}

#   unpack helper function:
#   strip directive command/start token
#   complexity: O(n)
sub unpack_postproc_strip {
    my ($seq, $re) = @_;

    foreach my $dir (@{$seq}) {
        if (    ref($dir) eq 'ARRAY'
            and $dir->[0] =~ m/^$re$/s) {
            #   remove first token of directive array
            shift(@{$dir});
        }
    }

    return $seq;
}

#   unpack helper function:
#   flatten empty and single token directives
sub unpack_postproc_flatten {
    my ($seq, $empty) = @_;

    for (my $i = 0; $i < @{$seq}; $i++) {
        if (ref($seq->[$i]) eq 'ARRAY') {
            if (@{$seq->[$i]} == 0) {
                #   replace empty directive array
                #   (can exist after "dirstrip" processing!)
                $seq->[$i] = $empty;
            }
            elsif (@{$seq->[$i]} == 1) {
                #   simplify single token directive array
                $seq->[$i] = $seq->[$i]->[0];
            }
        }
    }

    return $seq;
}

1;

