use strict;
use warnings;

package Text::Parser::Multiline 0.800;

# ABSTRACT: Adds multi-line support to the Text::Parser object.

use Exporter 'import';
our (@EXPORT_OK) = ();
our (@EXPORT)    = ();
use Role::Tiny;


requires(
    qw(save_record lines_parsed has_aborted __read_file_handle),
    qw(join_last_line is_line_continued) );

use Exception::Class (
    'Text::Parser::Multiline::Error',
    'Text::Parser::Multiline::Error::UnexpectedEOF' => {
        isa   => 'Text::Parser::Multiline::Error',
        alias => 'throw_unexpected_eof',
    },
    'Text::Parser::Multiline::Error::UnexpectedContinuation' => {
        isa   => 'Text::Parser::Multiline::Error',
        alias => 'throw_unexpected_continuation',
    }
);

around save_record       => \&__around_save_record;
around is_line_continued => \&__around_is_line_continued;
after __read_file_handle => \&__after__read_file_handle;

my $orig_save_record = sub {
    return;
};

my %save_record_proc = (
    join_last => \&__join_last_proc,
    join_next => \&__join_next_proc,
);

sub __around_save_record {
    my ( $orig, $self ) = ( shift, shift );
    $orig_save_record = $orig;
    my $type = $self->setting('multiline_type');
    $save_record_proc{$type}->( $orig, $self, @_ );
}

sub __around_is_line_continued {
    my ( $orig, $self ) = ( shift, shift );
    my $type = $self->setting('multiline_type');
    return $orig->( $self, @_ ) if $type eq 'join_next';
    __around_is_line_part_of_last( $orig, $self, @_ );
}

sub __around_is_line_part_of_last {
    my ( $orig, $self ) = ( shift, shift );
    return 0 if not $orig->( $self, @_ );
    throw_unexpected_continuation error =>
        "$_[0] has a continuation character on the first line"
        if $self->lines_parsed() == 1;
    return 1;
}

sub __after__read_file_handle {
    my $self      = shift;
    return $self->__after_at_eof() if $self->setting('multiline_type') eq 'join_next';
    my $last_line = $self->__pop_last_line();
    $orig_save_record->( $self, $last_line ) if defined $last_line;
}

sub __after_at_eof {
    my $self      = shift;
    my $remaining = $self->__pop_last_line();
    throw_unexpected_eof error =>
        "$remaining is still waiting to be continued. Unexpected EOF at line #"
        . $self->lines_parsed()
        if defined $remaining;
}

sub __join_next_proc {
    my ( $orig, $self ) = ( shift, shift );
    $self->__append_last_stash(@_);
    return if $self->is_line_continued(@_);
    $orig->( $self, $self->__pop_last_line() );
}

sub __join_last_proc {
    my ( $orig, $self ) = ( shift, shift );
    return $self->__append_last_stash(@_)
        if $self->is_line_continued(@_);
    my $last_line = $self->__pop_last_line();
    $orig->( $self, $last_line ) if defined $last_line;
    $self->__save_this_line( $orig, @_ );
}

sub __save_this_line {
    my ( $self, $orig ) = ( shift, shift );
    return $self->__append_last_stash(@_)
        if not $self->has_aborted;
}

sub __append_last_stash {
    my ( $self, $line ) = @_;
    my $last_line = $self->__pop_last_line();
    $last_line = $self->__strip_append_line( $line, $last_line );
    $self->__stash_line($last_line);
}

sub __strip_append_line {
    my ( $self, $line, $last ) = ( shift, shift, shift );
    return $line if not defined $last;
    return $self->join_last_line( $last, $line );
}

sub __stash_line {
    my $self = shift;
    $self->{__temp_joined_line} = shift;
}

sub __pop_last_line {
    my $self = shift;
    return undef if not exists $self->{__temp_joined_line};
    my $last_line = $self->{__temp_joined_line};
    delete $self->{__temp_joined_line};
    return $last_line;
}

1;

__END__

=pod

=encoding UTF-8

=head1 NAME

Text::Parser::Multiline - Adds multi-line support to the Text::Parser object.

=head1 VERSION

version 0.800

=head1 SYNOPSIS

=head1 RATIONALE

Some text formats allow users to split a single line into multiple lines, with a continuation character in the beginning or in the end, usually to improve human readability.

To handle these types of text formats with the native L<Text::Parser> class, the derived class would need to have a C<save_record> method that would:

=over 4

=item *

Detect if the line is continued, and if it is, save it in a temporary location

=item *

Keep appending (or joining) any continued lines to this temporary location

=item *

Once the line continuation stops, then create a record and save the record with C<save_record> method

=back

It should also look for error conditions:

=over 4

=item *

If the end of file is reached, and a joined line is still waiting incomplete, throw an exception "unexpected EOF"

=item *

If the first line in a text input happens to be a continuation of a previous line, that is impossible, since it is the first line ; so throw an exception

=back

This gets further complicated by the fact that whereas some multi-line text formats have a way to indicate that the line continues I<after> the current line (like a back-slash character at the end of the line or something), and some other text formats indicate that the current line is a continuation of the I<previous> line. For example, in bash, Tcl, etc., the continuation character is C<\> (back-slash) which, if added to the end of a line of code would imply "there is more on the next line". In contrast, L<SPICE|https://bwrcs.eecs.berkeley.edu/Classes/IcBook/SPICE/> has a continuation character (C<+>) on the next line, indicating that the text on that line should be joined with the I<previous> line.

This extension allows users to use the familiar C<save_record> interface to save records, as if all the multi-line text inputs were joined.

=head1 OVERVIEW

To create a multi-line text parser you need to know:

=over 4

=item *

L<Determine|Text::Parser/new> if your parser is a C<'join_next'> type or a C<'join_last'> type. This depends on which line has the continuation character.

=item *

Recognize if a line has a continuation pattern

=item *

How to strip the continuation character and join with last line

=back

So here are the things you need to do if you have to write a multi-line text parser:

=over 4

=item *

As usual inherit from L<Text::Parser>, never this class (C<use parent 'Text::Parser'>)

=item *

Override the C<new> constructor to add C<multiline_type> option by default. Read about the option L<here|Text::Parser/new>.

=item *

Override the C<is_line_continued> method to detect if there is a continuation character on the line.

=item *

Override the C<join_last_line> to join the previous line and the current line after stripping any continuation characters.

=item *

Implement your C<save_record> as if you always get joined lines, and

=back

=head1 REQUIRED METHODS

The following methods are required to compose this role into an object or a class. There are some default implementations for both these methods, but for most practical purposes you'd want to override those in your own parser class.

=head3 C<$self->E<gt>C<is_line_continued($line)>

Takes a string argument as input. Returns a boolean that indicates if the current line is continued from the previous line, or is continued on the next line (depending on the type of multi-line text format).

=head2 C<$self->E<gt>C<join_last_line($last_line, $current_line)>

Takes two string arguments. The first is the line previously read which is expected to be continued on this line. The function should return a string that has stripped any continuation characters, and joined the current line with the previous line.

=head1 BUGS

Please report any bugs or feature requests on the bugtracker website
L<http://rt.cpan.org/Public/Dist/Display.html?Name=Text-Parser> or by email
to L<bug-text-parser at rt.cpan.org|mailto:bug-text-parser at rt.cpan.org>.

When submitting a bug or request, please include a test-file or a
patch to an existing test-file that illustrates the bug or desired
feature.

=head1 AUTHOR

Balaji Ramasubramanian <balajiram@cpan.org>

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2018 by Balaji Ramasubramanian.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=cut
