package Rsync::Config::Renderer;

use strict;
use warnings;

our $VERSION = '0.2';

use Exception::Class (
    'Rsync::Config::Renderer::Exception::Invalid::Indent' => {
        description => 'Invalid value: need a non-negative integer!',
        alias       => 'throw_invalid_indent',
    },
    'Rsync::Config::Renderer::Exception::Invalid::IndentChar' => {
        description => 'Invalid value: need a non-empty string!',
        alias       => 'throw_invalid_indent_char',
    },
);

sub new {
    my ( $class, %opt ) = @_;
    my $self;

    $self = bless { $class->_default_options, %opt }, $class;
    return $self;
}

sub indent {
    my $self = shift;

    return $self->_default_options('indent') if !ref $self;
    if (@_) {
        $self->{indent} = $self->_valid_indent(@_);
        return $self;
    }
    return $self->{indent};
}

sub indent_char {
    my $self = shift;

    return $self->_default_options('indent_char') if !ref $self;
    if (@_) {
        $self->{indent_char} = $self->_valid_indent_char(@_);
        return $self;
    }
    return $self->{indent_char};
}

sub indent_string {
    my $self = shift;
    my %opt;
    for my $opt (qw(indent indent_char)) {
        my $valid = '_valid_' . $opt;
        $opt{$opt} =
              @_        ? $self->$valid(shift)
            : ref $self ? $self->$opt
            : $self->_default_options($opt);
    }
    return $opt{indent_char} x $opt{indent};
}

sub render {
    my $self  = shift;
    my @lines = grep { !ref $_ } @_;
    my %opt   = (
        prefix => q{},
        suffix => "\n",
        map { %{$_} } grep { ref $_ eq 'HASH' } @_,
    );
    my @indent_args = ();
    for my $opt (qw(indent indent_char)) {
        next if !exists $opt{$opt};
        push @indent_args, $opt{$opt};
    }
    for my $line (@lines) {
        $line = $self->indent_string(@indent_args)
            . $opt{prefix}
            . $line
            . $opt{suffix};
    }
    return wantarray ? @lines : join q{}, @lines;
}

sub _default_options {
    my $class    = shift;
    my %defaults = (
        indent      => 0,
        indent_char => "\t",
    );
    return @_ ? $defaults{ shift() } : %defaults;
}

sub _valid_indent {
    my ( $class, $indent ) = @_;
    if ( !defined $indent || $indent !~ /^\d+$/xm ) {
        throw_invalid_indent;
    }
    return $indent;
}

sub _valid_indent_char {
    my ( $class, $char ) = @_;
    if ( !defined $char || !length $char ) {
        throw_invalid_indent;
    }
    return $char;
}

1;

__END__

=head1 NAME

Rsync::Config::Renderer - rsync config rendering

=head1 VERSION

0.1.2

=head1 DESCRIPTION

Rsync::Config::Renderer is the module used to indent all output
generated by Rsync::Config . Is also the base class for all other
modules.

=head1 SYNOPSIS

    use Rsync::Config::Renderer;
    my $renderer = Rsync::Config::Renderer->new(
                        indent => 1, indent_char => "\t" );
    print $renderer->indent_string, q{foobar};
    # prints: "<TAB>foobar";

    $renderer->indent(4)->indent_char(q{.});
    print $renderer->indent_string, q{foobar};
    # prints: "....foobar";

    print $renderer->render( 'foobar', 22, 'baz' );
    # prints:
    # ....foobar
    # ....22
    # ....baz

=head1 SUBROUTINES/METHODS

=head2 new

    my $renderer = Rsync::Config::Renderer->new(%options);

Class contructor.

Returns a Rsync::Config::Renderer object.

Currently recognized options are: I<indent> (default: B<0>)
and I<indent_char> (default: B<\t>).
Each option is described with its respective accessor/mutator.

All other passed options are silently loaded into the generated object,
but currently no accessors are being automatically created for them.

=head2 indent

    my $current_indent = $renderer->indent;
    $renderer->indent(4);

Both accessor and mutator, I<indent> can be used to get the current
indentation level or to change it.

If no arguments are provided, it will return the current indentation level.

If arguments are provided, first is considered to be the new indentation
level and applied to the current object, all others being ignored.

As mutator, the current object will be returned (useful for method chaining).

To disable indenting, pass a value of B<0> (digit I<zero>).

If invalid value is passed for setting the indentation level, a fatal error
is threw. Valid values are non-negative integers.

It can also be called as a class method, returning the default indentation
level. In this case, no mutator mode is possible.

=head2 indent_char

    my $current_indent_char = $renderer->indent_char;
    $renderer->indent_char(q{.});

Both accessor and mutator, I<indent_char> can be used to get the current
indentation "character" or to change it.

If no arguments are provided, it will return the current indentation
"character".

If arguments are provided, first is considered to be the new indentation
character and applied to the current object, all others being ignored.

As mutator, the current object will be returned (useful for method chaining).

If invalid value is passed for setting the indentation "character", a fatal
error is threw. Valid value is any non-empty sequence of characters.

It can also be called as a class method, returning the default indentation
character. In this case, no mutator mode is possible.

=head2 render

    print $renderer->render(@lines, { prefix => "* ", suffix => ".\n" });

Crude rendering method. Returns the lines (as scalar or list, depeding on
calling context), indented appropriately.

Options may be passed in a hash reference. All options recognized by B<new>
are handled by B<render> with the same effect.

The particular options to B<render> are I<prefix> (default: none) and
I<suffix> (default: I<\n>), which are the strings to be prepended and
respectively appended to each line.

It can also be called as a class method.

=head2 indent_string($indent, $indent_char)

    my $indent_str = $renderer->indent_string();
    my $indent_str = $renderer->indent_string($indent, $indent_char);
    my $indent_str = Rsync::Config::Renderer->indent_string();
    my $indent_str = Rsync::Config::Renderer->indent_string(
                                              $indent, $indent_char);

Returns a string containing the current indentation.

If I<$indent> and I<$indent_char> are provided, they will override current
object's attributes.

It can also be called as a class method.

=head1 DEPENDENCIES

L<Exception::Class>.

=head1 DIAGNOSTICS

=over 4

=item C<< Invalid value: need a non-negative integer! >>

Occurs when passing invalid values to B<indent>. Self-explanatory.

=item C<< Invalid value: need a non-empty string! >>

Occurs when passing invalid values to B<indent_char>. Self-explanatory.

=back

=head1 CONFIGURATION AND ENVIRONMENT

This module does not use any configuration files or environment variables.

=head1 INCOMPATIBILITIES

None known to the author.

=head1 BUGS AND LIMITATIONS

No bugs known to the author.

=head1 SEE ALSO

L<Rsync::Config>.

=head1 AUTHOR

Manuel SUBREDU C<< <diablo@packages.ro> >>.

=head1 LICENSE AND COPYRIGHT

Copyright (c) 2006, Manuel SUBREDU C<< <diablo@packages.ro> >>.
All rights reserved.

This module is free software; you can redistribute it and/or
modify it under the same terms as Perl itself.
See L<perlartistic>.

=head1 DISCLAIMER OF WARRANTY

BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE
ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH
YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL
NECESSARY SERVICING, REPAIR, OR CORRECTION.

IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE
LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL,
OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE
THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
