# ABSTRACT: YAML Parser
use strict;
use warnings;
package YAML::PP::Parser;

our $VERSION = '0.026'; # VERSION

use constant TRACE => $ENV{YAML_PP_TRACE} ? 1 : 0;
use constant DEBUG => ($ENV{YAML_PP_DEBUG} || $ENV{YAML_PP_TRACE}) ? 1 : 0;

use YAML::PP::Common qw/
    YAML_PLAIN_SCALAR_STYLE YAML_SINGLE_QUOTED_SCALAR_STYLE
    YAML_DOUBLE_QUOTED_SCALAR_STYLE
    YAML_LITERAL_SCALAR_STYLE YAML_FOLDED_SCALAR_STYLE
    YAML_FLOW_SEQUENCE_STYLE YAML_FLOW_MAPPING_STYLE
/;
use YAML::PP::Render;
use YAML::PP::Lexer;
use YAML::PP::Grammar qw/ $GRAMMAR /;
use YAML::PP::Exception;
use YAML::PP::Reader;
use Carp qw/ croak /;


sub new {
    my ($class, %args) = @_;
    my $reader = delete $args{reader} || YAML::PP::Reader->new;
    my $default_yaml_version = delete $args{default_yaml_version};
    my $self = bless {
        default_yaml_version => $default_yaml_version || '1.2',
        lexer => YAML::PP::Lexer->new(
            reader => $reader,
        ),
    }, $class;
    my $receiver = delete $args{receiver};
    if ($receiver) {
        $self->set_receiver($receiver);
    }
    return $self;
}

sub clone {
    my ($self) = @_;
    my $clone = {
        default_yaml_version => $self->default_yaml_version,
        lexer => YAML::PP::Lexer->new(),
    };
    return bless $clone, ref $self;
}

sub receiver { return $_[0]->{receiver} }
sub set_receiver {
    my ($self, $receiver) = @_;
    my $callback;
    if (ref $receiver eq 'CODE') {
        $callback = $receiver;
    }
    else {
        $callback = sub {
            my ($self, $event, $info) = @_;
            return $receiver->$event($info);
        };
    }
    $self->{callback} = $callback;
    $self->{receiver} = $receiver;
}
sub reader { return $_[0]->lexer->{reader} }
sub set_reader {
    my ($self, $reader) = @_;
    $self->lexer->set_reader($reader);
}
sub lexer { return $_[0]->{lexer} }
sub callback { return $_[0]->{callback} }
sub set_callback { $_[0]->{callback} = $_[1] }
sub level { return $#{ $_[0]->{offset} } }
sub offset { return $_[0]->{offset} }
sub set_offset { $_[0]->{offset} = $_[1] }
sub events { return $_[0]->{events} }
sub set_events { $_[0]->{events} = $_[1] }
sub new_node { return $_[0]->{new_node} }
sub set_new_node { $_[0]->{new_node} = $_[1] }
sub tagmap { return $_[0]->{tagmap} }
sub set_tagmap { $_[0]->{tagmap} = $_[1] }
sub tokens { return $_[0]->{tokens} }
sub set_tokens { $_[0]->{tokens} = $_[1] }
sub event_stack { return $_[0]->{event_stack} }
sub set_event_stack { $_[0]->{event_stack} = $_[1] }
sub default_yaml_version { return $_[0]->{default_yaml_version} }
sub yaml_version { return $_[0]->{yaml_version} }
sub set_yaml_version { $_[0]->{yaml_version} = $_[1] }
sub yaml_version_directive { return $_[0]->{yaml_version_directive} }
sub set_yaml_version_directive { $_[0]->{yaml_version_directive} = $_[1] }

sub rule { return $_[0]->{rule} }
sub set_rule {
    my ($self, $name) = @_;
    no warnings 'uninitialized';
    DEBUG and $self->info("set_rule($name)");
    $self->{rule} = $name;
}

sub init {
    my ($self) = @_;
    $self->set_offset([]);
    $self->set_events([]);
    $self->set_new_node(0);
    $self->set_tagmap({
        '!!' => "tag:yaml.org,2002:",
    });
    $self->set_tokens([]);
    $self->set_rule(undef);
    $self->set_event_stack([]);
    $self->set_yaml_version($self->default_yaml_version);
    $self->set_yaml_version_directive(undef);
    $self->lexer->init;
}

sub parse_string {
    my ($self, $yaml) = @_;
    $self->set_reader(YAML::PP::Reader->new( input => $yaml ));
    $self->parse();
}

sub parse_file {
    my ($self, $file) = @_;
    $self->set_reader(YAML::PP::Reader::File->new( input => $file ));
    $self->parse();
}

my %nodetypes = (
    MAPVALUE     => 'NODETYPE_COMPLEX',
    MAP          => 'NODETYPE_MAP',
    SEQ          => 'NODETYPE_SEQ',
    SEQ0         => 'NODETYPE_SEQ',
    FLOWMAP      => 'NODETYPE_FLOWMAP',
    FLOWMAPVALUE => 'NODETYPE_FLOWMAPVALUE',
    FLOWSEQ      => 'NODETYPE_FLOWSEQ',
    FLOWSEQ_NEXT => 'FLOWSEQ_NEXT',
    DOC          => 'FULLNODE',
    DOC_END      => 'DOCUMENT_END',
    STR          => 'STREAM',
);

sub parse {
    my ($self) = @_;
    TRACE and warn "=== parse()\n";
    TRACE and $self->debug_yaml;
    $self->init;
    $self->lexer->init;
    eval {
        $self->start_stream;
        $self->set_rule( 'STREAM' );

        $self->parse_tokens();

        $self->end_stream;
    };
    if (my $error = $@) {
        if (ref $error) {
            croak "$error\n ";
        }
        croak $error;
    }

    DEBUG and $self->highlight_yaml;
    TRACE and $self->debug_tokens;
}

sub lex_next_tokens {
    my ($self) = @_;

    DEBUG and $self->info("----------------> lex_next_tokens");
    TRACE and $self->debug_events;

    my $indent = $self->offset->[-1];
    my $event_types = $self->events;
    my $next_tokens = $self->lexer->fetch_next_tokens($indent);
    return unless @$next_tokens;

    my $next = $next_tokens->[0];

    return 1 if ($next->{name} ne 'SPACE');
    my $flow = $event_types->[-1] =~ m/^FLOW/;
    my $space = length $next->{value};
    my $tokens = $self->tokens;

    if (not $space) {
        shift @$next_tokens;
    }
    else {
        push @$tokens, shift @$next_tokens;
    }
    if ($flow) {
        if ($space >= $indent) {
            return 1;
        }
        $self->exception("Bad indendation in " . $self->events->[-1]);
    }
    $next = $next_tokens->[0];
    if ($space > $indent ) {
        return 1 if $indent < 0;
        unless ($self->new_node) {
            $self->exception("Bad indendation in " . $self->events->[-1]);
        }
        return 1;
    }
    if ($self->new_node) {
        if ($space < $indent) {
            $self->scalar_event({ style => YAML_PLAIN_SCALAR_STYLE, value => '' });
            $self->remove_nodes($space);
        }
        else {
            # unindented sequence starts
            my $exp = $self->events->[-1];
            my $seq_start = $next->{name} eq 'DASH';
            if ( $seq_start and ($exp eq 'MAPVALUE' or $exp eq 'MAP')) {
            }
            else {
                $self->scalar_event({ style => YAML_PLAIN_SCALAR_STYLE, value => '' });
            }
        }
    }
    else {
        if ($space < $indent) {
            $self->remove_nodes($space);
        }
    }

    my $exp = $self->events->[-1];

    if ($exp eq 'SEQ0' and $next->{name} ne 'DASH') {
        TRACE and $self->info("In unindented sequence");
        $self->end_sequence;
        $exp = $self->events->[-1];
    }

    if ($self->offset->[-1] != $space) {
        $self->exception("Expected " . $self->events->[-1]);
    }
    return 1;
}

my %next_event = (
    MAP => 'MAPVALUE',
    MAPVALUE => 'MAP',
    SEQ => 'SEQ',
    SEQ0 => 'SEQ0',
    DOC => 'DOC_END',
    STR => 'STR',
    FLOWSEQ => 'FLOWSEQ_NEXT',
    FLOWSEQ_NEXT => 'FLOWSEQ',
    FLOWMAP => 'FLOWMAPVALUE',
    FLOWMAPVALUE => 'FLOWMAP',
);

my %event_to_method = (
    MAP => 'mapping',
    FLOWMAP => 'mapping',
    SEQ => 'sequence',
    SEQ0 => 'sequence',
    FLOWSEQ => 'sequence',
    DOC => 'document',
    STR => 'stream',
    VAL => 'scalar',
    ALI => 'alias',
    MAPVALUE => 'mapping',
);

#sub process_events {
#    my ($self, $res) = @_;
#
#    my $event_stack = $self->event_stack;
#    return unless @$event_stack;
#
#    if (@$event_stack == 1 and $event_stack->[0]->[0] eq 'properties') {
#        return;
#    }
#
#    my $event_types = $self->events;
#    my $properties;
#    my @send_events;
#    for my $event (@$event_stack) {
#        TRACE and warn __PACKAGE__.':'.__LINE__.$".Data::Dumper->Dump([\$event], ['event']);
#        my ($type, $info) = @$event;
#        if ($type eq 'properties') {
#            $properties = $info;
#        }
#        elsif ($type eq 'scalar') {
#            $info->{name} = 'scalar_event';
#            $event_types->[-1] = $next_event{ $event_types->[-1] };
#            push @send_events, $info;
#        }
#        elsif ($type eq 'begin') {
#            my $name = $info->{name};
#            $info->{name} = $event_to_method{ $name } . '_start_event';
#            push @{ $event_types }, $name;
#            push @{ $self->offset }, $info->{offset};
#            push @send_events, $info;
#        }
#        elsif ($type eq 'end') {
#            my $name = $info->{name};
#            $info->{name} = $event_to_method{ $name } . '_end_event';
#            $self->$type($name, $info);
#            push @send_events, $info;
#            if (@$event_types) {
#                $event_types->[-1] = $next_event{ $event_types->[-1] };
#            }
#        }
#        elsif ($type eq 'alias') {
#            if ($properties) {
#                $self->exception("Parse error: Alias not allowed in this context");
#            }
#            $info->{name} = 'alias_event';
#            $event_types->[-1] = $next_event{ $event_types->[-1] };
#            push @send_events, $info;
#        }
#    }
#    @$event_stack = ();
#    for my $info (@send_events) {
#        DEBUG and $self->debug_event( $info );
#        $self->callback->($self, $info->{name}, $info);
#    }
#}

my %fetch_method = (
    '"' => 'fetch_quoted',
    "'" => 'fetch_quoted',
    '|' => 'fetch_block',
    '>' => 'fetch_block',
    ''  => 'fetch_plain',
);

sub parse_tokens {
    my ($self) = @_;
    my $event_types = $self->events;
    my $offsets = $self->offset;
    my $tokens = $self->tokens;
    my $next_tokens = $self->lexer->next_tokens;

    unless ($self->lex_next_tokens) {
        $self->end_document(1);
        return 0;
    }
    unless ($self->new_node) {
        if ($self->level > 0) {
            my $new_rule = $nodetypes{ $event_types->[-1] }
                or die "Did not find '$event_types->[-1]'";
            $self->set_rule( $new_rule );
        }
    }

    my $rule_name = $self->rule;
    DEBUG and $self->info("----------------> parse_tokens($rule_name)");
    my $rule = $GRAMMAR->{ $rule_name }
        or die "Could not find rule $rule_name";

    TRACE and $self->debug_rules($rule);
    TRACE and $self->debug_yaml;
    DEBUG and $self->debug_next_line;

    RULE: while ($rule_name) {
        DEBUG and $self->info("RULE: $rule_name");
        TRACE and $self->debug_tokens($next_tokens);

        unless (@$next_tokens) {
            $self->exception("No more tokens");
        }
        TRACE and warn __PACKAGE__.':'.__LINE__.$".Data::Dumper->Dump([\$next_tokens->[0]], ['next_token']);
        my $got = $next_tokens->[0]->{name};
        if ($got eq 'CONTEXT') {
            my $context = shift @$next_tokens;
            my $indent = $offsets->[-1];
            $indent++ unless $self->lexer->flowcontext;
            my $method = $fetch_method{ $context->{value} };
            my $partial = $self->lexer->$method($indent, $context->{value});
            next RULE;
        }
        my $def = $rule->{ $got };
        if ($def) {
            push @$tokens, shift @$next_tokens;
        }
        elsif ($def = $rule->{DEFAULT}) {
            $got = 'DEFAULT';
        }
        else {
            $self->expected(
                expected => [keys %$rule],
                got => $next_tokens->[0],
            );
        }

        DEBUG and $self->got("---got $got");
        if (my $sub = $def->{match}) {
            DEBUG and $self->info("CALLBACK $sub");
            $self->$sub(@$tokens ? $tokens->[-1] : ());
        }
        my $eol = $got eq 'EOL';
        my $new = $def->{new};
        if ($new) {
            DEBUG and $self->got("NEW: $new");
            $rule_name = $new;
            $self->set_rule($rule_name);
        }
        elsif ($eol) {
        }
        elsif ($def->{return}) {
            $rule_name = $nodetypes{ $event_types->[-1] }
                or die "Unexpected event type $event_types->[-1]";
            $self->set_rule($rule_name);
        }
        else {
            $rule_name .= " - $got"; # for debugging
            $rule = $def;
            next RULE;
        }
        if ($eol) {
            unless ($self->lex_next_tokens) {
                $self->end_document(1);
                return 0;
            }
            unless ($self->new_node) {
                if ($self->level > 0) {
                    $rule_name = $nodetypes{ $event_types->[-1] }
                        or die "Did not find '$event_types->[-1]'";
                    $self->set_rule( $rule_name );
                }
            }
            $rule_name = $self->rule;
        }
        $rule = $GRAMMAR->{ $rule_name }
            or die "Unexpected rule $rule_name";

    }

    die "Unexpected";
}

sub end_sequence {
    my ($self) = @_;
    my $event_types = $self->events;
    pop @{ $event_types };
    pop @{ $self->offset };
    my $info = { name => 'sequence_end_event' };
    $self->callback->($self, $info->{name} => $info );
    $event_types->[-1] = $next_event{ $event_types->[-1] };
}

sub remove_nodes {
    my ($self, $space) = @_;
    my $offset = $self->offset;
    my $event_types = $self->events;

    my $exp = $event_types->[-1];
    while (@$offset) {
        if ($offset->[ -1 ] <= $space) {
            last;
        }
        if ($exp eq 'MAPVALUE') {
            $self->scalar_event({ style => YAML_PLAIN_SCALAR_STYLE, value => '' });
            $exp = 'MAP';
        }
        my $info = { name => $exp };
        $info->{name} = $event_to_method{ $exp } . '_end_event';
        pop @{ $event_types };
        pop @{ $offset };
        $self->callback->($self, $info->{name} => $info );
        $event_types->[-1] = $next_event{ $event_types->[-1] };
        $exp = $event_types->[-1];
    }
    return $exp;
}

sub start_stream {
    my ($self) = @_;
    push @{ $self->events }, 'STR';
    push @{ $self->offset }, -1;
    $self->callback->($self, 'stream_start_event', {
        name => 'stream_start_event',
    });
}

sub start_document {
    my ($self, $implicit) = @_;
    push @{ $self->events }, 'DOC';
    push @{ $self->offset }, -1;
    my $directive = $self->yaml_version_directive;
    my %directive;
    if ($directive) {
        my ($major, $minor) = split m/\./, $self->yaml_version;
        %directive = ( version_directive => { major => $major, minor => $minor } );
    }
    $self->callback->($self, 'document_start_event', {
        name => 'document_start_event',
        implicit => $implicit,
        %directive,
    });
    $self->set_yaml_version_directive(undef);
    $self->set_rule( 'FULLNODE' );
    $self->set_new_node(1);
}

sub start_sequence {
    my ($self, $offset) = @_;
    my $offsets = $self->offset;
    if ($offsets->[-1] == $offset) {
        push @{ $self->events }, 'SEQ0';
    }
    else {
        push @{ $self->events }, 'SEQ';
    }
    push @{ $offsets }, $offset;
    my $event_stack = $self->event_stack;
    my $info = { name => 'sequence_start_event' };
    if (@$event_stack and $event_stack->[-1]->[0] eq 'properties') {
        my $properties = pop @$event_stack;
        $self->node_properties($properties->[1], $info);
    }
    $self->callback->($self, 'sequence_start_event', $info);
}

sub start_flow_sequence {
    my ($self, $offset) = @_;
    my $offsets = $self->offset;
    my $new_offset = $offsets->[-1];
    my $event_types = $self->events;
    if ($new_offset < 0) {
        $new_offset = 0;
    }
    elsif ($self->new_node) {
        if ($event_types->[-1] !~ m/^FLOW/) {
            $new_offset++;
        }
    }
    push @{ $self->events }, 'FLOWSEQ';
    push @{ $offsets }, $new_offset;

    my $event_stack = $self->event_stack;
    my $info = { style => YAML_FLOW_SEQUENCE_STYLE, name => 'sequence_start_event'  };
    if (@$event_stack and $event_stack->[-1]->[0] eq 'properties') {
        $self->fetch_inline_properties($event_stack, $info);
    }
    $self->callback->($self, 'sequence_start_event', $info);
}

sub start_flow_mapping {
    my ($self, $offset) = @_;
    my $offsets = $self->offset;
    my $new_offset = $offsets->[-1];
    my $event_types = $self->events;
    if ($new_offset < 0) {
        $new_offset = 0;
    }
    elsif ($self->new_node) {
        if ($event_types->[-1] !~ m/^FLOW/) {
            $new_offset++;
        }
    }
    push @{ $self->events }, 'FLOWMAP';
    push @{ $offsets }, $new_offset;

    my $event_stack = $self->event_stack;
    my $info = { name => 'mapping_start_event', style => YAML_FLOW_MAPPING_STYLE };
    if (@$event_stack and $event_stack->[-1]->[0] eq 'properties') {
        $self->fetch_inline_properties($event_stack, $info);
    }
    $self->callback->($self, 'mapping_start_event', $info);
}

sub end_flow_sequence {
    my ($self) = @_;
    my $event_types = $self->events;
    pop @{ $event_types };
    pop @{ $self->offset };
    my $info = { name => 'sequence_end_event' };
    $self->callback->($self, $info->{name}, $info);
    $event_types->[-1] = $next_event{ $event_types->[-1] };
}

sub end_flow_mapping {
    my ($self) = @_;
    my $event_types = $self->events;
    pop @{ $event_types };
    pop @{ $self->offset };
    my $info = { name => 'mapping_end_event' };
    $self->callback->($self, $info->{name}, $info);
    $event_types->[-1] = $next_event{ $event_types->[-1] };
}

sub start_mapping {
    my ($self, $offset) = @_;
    my $offsets = $self->offset;
    push @{ $self->events }, 'MAP';
    push @{ $offsets }, $offset;
    my $event_stack = $self->event_stack;
    my $info = { name => 'mapping_start_event' };
    if (@$event_stack and $event_stack->[-1]->[0] eq 'properties') {
        my $properties = pop @$event_stack;
        $self->node_properties($properties->[1], $info);
    }
    $self->callback->($self, 'mapping_start_event', $info);
}

sub end_document {
    my ($self, $implicit) = @_;

    my $event_types = $self->events;
    if ($event_types->[-1] =~ m/FLOW/) {
        die "Unexpected end of flow context";
    }
    if ($self->new_node) {
        $self->scalar_event({ style => YAML_PLAIN_SCALAR_STYLE, value => '' });
    }
    $self->remove_nodes(-1);

    if ($event_types->[-1] eq 'STR') {
        return;
    }
    my $last = pop @{ $event_types };
    if ($last ne 'DOC' and $last ne 'DOC_END') {
        $self->exception("Unexpected event type $last");
    }
    pop @{ $self->offset };
    $self->callback->($self, 'document_end_event', {
        name => 'document_end_event',
        implicit => $implicit,
    });
    if ($self->yaml_version eq '1.2') {
        # In YAML 1.2, directives are only for the following
        # document. In YAML 1.1, they are global
        $self->set_tagmap({ '!!' => "tag:yaml.org,2002:" });
    }
    $event_types->[-1] = $next_event{ $event_types->[-1] };
    $self->set_rule('STREAM');
}

sub end_stream {
    my ($self) = @_;
    my $last = pop @{ $self->events };
    $self->exception("Unexpected event type $last") unless $last eq 'STR';
    pop @{ $self->offset };
    $self->callback->($self, 'stream_end_event', {
        name => 'stream_end_event',
    });
}

sub fetch_inline_properties {
    my ($self, $stack, $info) = @_;
    my $properties = $stack->[-1];

    $properties = $properties->[1];
    my $property_offset;
    if ($properties) {
        for my $p (@{ $properties->{inline} }) {
            my $type = $p->{type};
            if (exists $info->{ $type }) {
                $self->exception("A node can only have one $type");
            }
            $info->{ $type } = $p->{value};
            unless (defined $property_offset) {
                $property_offset = $p->{offset};
                $info->{offset} = $p->{offset};
            }
        }
        delete $properties->{inline};
        undef $properties unless $properties->{newline};
    }

    unless ($properties) {
        pop @$stack;
    }
}

sub node_properties {
    my ($self, $properties, $info) = @_;
    if ($properties) {
        for my $p (@{ $properties->{newline} }) {
            my $type = $p->{type};
            if (exists $info->{ $type }) {
                $self->exception("A node can only have one $type");
            }
            $info->{ $type } = $p->{value};
        }
        undef $properties;
    }
}

sub scalar_event {
    my ($self, $info) = @_;
    my $event_types = $self->events;
    my $event_stack = $self->event_stack;
    if (@$event_stack and $event_stack->[-1]->[0] eq 'properties') {
        my $properties = pop @$event_stack;
        $properties = $self->node_properties($properties->[1], $info);
    }

    $info->{name} = 'scalar_event';
    $self->callback->($self, 'scalar_event', $info);
    $self->set_new_node(0);
    $event_types->[-1] = $next_event{ $event_types->[-1] };
}

sub alias_event {
    my ($self, $info) = @_;
    my $event_stack = $self->event_stack;
    if (@$event_stack and $event_stack->[-1]->[0] eq 'properties') {
        $self->exception("Parse error: Alias not allowed in this context");
    }
    my $event_types = $self->events;
    $info->{name} = 'alias_event';
    $self->callback->($self, 'alias_event', $info);
    $self->set_new_node(0);
    $event_types->[-1] = $next_event{ $event_types->[-1] };
}

sub yaml_to_tokens {
    my ($class, $type, $input) = @_;
    my $yp = YAML::PP::Parser->new( receiver => sub {} );
    my @docs = eval {
        $type eq 'string' ? $yp->parse_string($input) : $yp->parse_file($input);
    };
    my $error = $@;

    my $tokens = $yp->tokens;
    if ($error) {
        my $remaining_tokens = $yp->_remaining_tokens;
        push @$tokens, map { +{ %$_, name => 'ERROR' } } @$remaining_tokens;
    }
    return $error, $tokens;
}

sub _remaining_tokens {
    my ($self) = @_;
    my @tokens;
    my $next = $self->lexer->next_tokens;
    push @tokens, @$next;
    my $next_line = $self->lexer->next_line;
    my $remaining = '';
    if ($next_line) {
        if ($self->lexer->offset > 0) {
            $remaining = $next_line->[1] . $next_line->[2];
        }
        else {
            $remaining = join '', @$next_line;
        }
    }
    $remaining .= $self->reader->read;
    $remaining = '' unless defined $remaining;
    push @tokens, { name => "ERROR", value => $remaining };
    return \@tokens;
}

sub event_to_test_suite {
    my ($self, $event) = @_;
    if (ref $event eq 'ARRAY') {
        return YAML::PP::Common::event_to_test_suite($event->[1]);
    }
    return YAML::PP::Common::event_to_test_suite($event);
}

sub debug_events {
    my ($self) = @_;
    $self->note("EVENTS: ("
        . join (' | ', @{ $_[0]->events }) . ')'
    );
    $self->debug_offset;
}

sub debug_offset {
    my ($self) = @_;
    $self->note(
        qq{OFFSET: (}
        . join (' | ', map { defined $_ ? sprintf "%-3d", $_ : '?' } @{ $_[0]->offset })
        . qq/) level=@{[ $_[0]->level ]}]}/
    );
}

sub debug_yaml {
    my ($self) = @_;
    my $line = $self->lexer->line;
    $self->note("LINE NUMBER: $line");
    my $next_tokens = $self->lexer->next_tokens;
    if (@$next_tokens) {
        $self->debug_tokens($next_tokens);
    }
}

sub debug_next_line {
    my ($self) = @_;
    my $next_line = $self->lexer->next_line || [];
    my $line = $next_line->[0];
    $line = '' unless defined $line;
    $line =~ s/( +)$/'·' x length $1/e;
    $line =~ s/\t/▸/g;
    $self->note("NEXT LINE: >>$line<<");
}

sub note {
    my ($self, $msg) = @_;
    $self->_colorize_warn(["yellow"], "============ $msg");
}

sub info {
    my ($self, $msg) = @_;
    $self->_colorize_warn(["cyan"], "============ $msg");
}

sub got {
    my ($self, $msg) = @_;
    $self->_colorize_warn(["green"], "============ $msg");
}

sub _colorize_warn {
    my ($self, $colors, $text) = @_;
    require Term::ANSIColor;
    warn Term::ANSIColor::colored($colors, $text), "\n";
}

sub debug_event {
    my ($self, $event) = @_;
    my $str = YAML::PP::Common::event_to_test_suite($event);
    require Term::ANSIColor;
    warn Term::ANSIColor::colored(["magenta"], "============ $str"), "\n";
}

sub debug_rules {
    my ($self, $rules) = @_;
    local $Data::Dumper::Maxdepth = 2;
    $self->note("RULES:");
    for my $rule ($rules) {
        if (ref $rule eq 'ARRAY') {
            my $first = $rule->[0];
            if (ref $first eq 'SCALAR') {
                $self->info("-> $$first");
            }
            else {
                if (ref $first eq 'ARRAY') {
                    $first = $first->[0];
                }
                $self->info("TYPE $first");
            }
        }
        else {
            eval {
                my @keys = sort keys %$rule;
                $self->info("@keys");
            };
        }
    }
}

sub debug_tokens {
    my ($self, $tokens) = @_;
    $tokens ||= $self->tokens;
    require Term::ANSIColor;
    for my $token (@$tokens) {
        my $type = Term::ANSIColor::colored(["green"],
            sprintf "%-22s L %2d C %2d ",
                $token->{name}, $token->{line}, $token->{column} + 1
        );
        local $Data::Dumper::Useqq = 1;
        local $Data::Dumper::Terse = 1;
        require Data::Dumper;
        my $str = Data::Dumper->Dump([$token->{value}], ['str']);
        chomp $str;
        $str =~ s/(^.|.$)/Term::ANSIColor::colored(['blue'], $1)/ge;
        warn "$type$str\n";
    }

}

sub highlight_yaml {
    my ($self) = @_;
    require YAML::PP::Highlight;
    my $tokens = $self->tokens;
    my $highlighted = YAML::PP::Highlight->ansicolored($tokens);
    warn $highlighted;
}

sub exception {
    my ($self, $msg, %args) = @_;
    my $next = $self->lexer->next_tokens;
    my $line = @$next ? $next->[0]->{line} : $self->lexer->line;
    my $offset = @$next ? $next->[0]->{column} : $self->lexer->offset;
    $offset++;
    my $next_line = $self->lexer->next_line;
    my $remaining = '';
    if ($next_line) {
        if ($self->lexer->offset > 0) {
            $remaining = $next_line->[1] . $next_line->[2];
        }
        else {
            $remaining = join '', @$next_line;
        }
    }
    my $caller = $args{caller} || [ caller(0) ];
    my $e = YAML::PP::Exception->new(
        got => $args{got},
        expected => $args{expected},
        line => $line,
        column => $offset,
        msg => $msg,
        next => $next,
        where => $caller->[1] . ' line ' . $caller->[2],
        yaml => $remaining,
    );
    croak $e;
}

sub expected {
    my ($self, %args) = @_;
    my $expected = $args{expected};
    @$expected = sort grep { m/^[A-Z_]+$/ } @$expected;
    my $got = $args{got}->{name};
    my @caller = caller(0);
    $self->exception("Expected (@$expected), but got $got",
        caller => \@caller,
        expected => $expected,
        got => $args{got},
    );
}

sub cb_tag {
    my ($self, $token) = @_;
    my $stack = $self->event_stack;
    if (! @$stack or $stack->[-1]->[0] ne 'properties') {
        push @$stack, [ properties => {} ];
    }
    my $last = $stack->[-1]->[1];
    my $tag = $self->_read_tag($token->{value}, $self->tagmap);
    $last->{inline} ||= [];
    push @{ $last->{inline} }, {
        type => 'tag',
        value => $tag,
        offset => $token->{column},
    };
}

sub _read_tag {
    my ($self, $tag, $map) = @_;
    if ($tag eq '!') {
        return "!";
    }
    elsif ($tag =~ m/^!<(.*)>/) {
        return $1;
    }
    elsif ($tag =~ m/^(![^!]*!|!)(.+)/) {
        my $alias = $1;
        my $name = $2;
        $name =~ s/%([0-9a-fA-F]{2})/chr hex $1/eg;
        if (exists $map->{ $alias }) {
            $tag = $map->{ $alias }. $name;
        }
        else {
            if ($alias ne '!' and $alias ne '!!') {
                die "Found undefined tag handle '$alias'";
            }
            $tag = "!$name";
        }
    }
    else {
        die "Invalid tag";
    }
    return $tag;
}

sub cb_anchor {
    my ($self, $token) = @_;
    my $anchor = $token->{value};
    $anchor = substr($anchor, 1);
    my $stack = $self->event_stack;
    if (! @$stack or $stack->[-1]->[0] ne 'properties') {
        push @$stack, [ properties => {} ];
    }
    my $last = $stack->[-1]->[1];
    $last->{inline} ||= [];
    push @{ $last->{inline} }, {
        type => 'anchor',
        value => $anchor,
        offset => $token->{column},
    };
}

sub cb_property_eol {
    my ($self, $res) = @_;
    my $stack = $self->event_stack;
    my $last = $stack->[-1]->[1];
    my $inline = delete $last->{inline} or return;
    my $newline = $last->{newline} ||= [];
    push @$newline, @$inline;
}

sub cb_mapkey {
    my ($self, $token) = @_;
    my $stack = $self->event_stack;
    my $info = {
        style => YAML_PLAIN_SCALAR_STYLE,
        value => $token->{value},
        offset => $token->{column},
    };
    if (@$stack and $stack->[-1]->[0] eq 'properties') {
        $self->fetch_inline_properties($stack, $info);
    }
    push @{ $stack }, [ scalar => $info ];
}

sub cb_send_mapkey {
    my ($self, $res) = @_;
    my $last = pop @{ $self->event_stack };
    $self->scalar_event($last->[1]);
    $self->set_new_node(1);
}

sub cb_send_scalar {
    my ($self, $res) = @_;
    my $last = pop @{ $self->event_stack };
    $self->scalar_event($last->[1]);
}

sub cb_empty_mapkey {
    my ($self, $token) = @_;
    my $stack = $self->event_stack;
    my $info = {
        style => YAML_PLAIN_SCALAR_STYLE,
        value => '',
        offset => $token->{column},
    };
    if (@$stack and $stack->[-1]->[0] eq 'properties') {
        $self->fetch_inline_properties($stack, $info);
    }
    $self->scalar_event($info);
    $self->set_new_node(1);
}

sub cb_send_flow_alias {
    my ($self, $token) = @_;
    my $alias = substr($token->{value}, 1);
    $self->alias_event({ value => $alias });
}

sub cb_send_alias {
    my ($self, $token) = @_;
    my $alias = substr($token->{value}, 1);
    $self->alias_event({ value => $alias });
}

sub cb_send_alias_from_stack {
    my ($self, $token) = @_;
    my $last = pop @{ $self->event_stack };
    $self->alias_event($last->[1]);
}

sub cb_alias {
    my ($self, $token) = @_;
    my $alias = substr($token->{value}, 1);
    push @{ $self->event_stack }, [ alias => {
        value => $alias,
        offset => $token->{column},
    }];
}

sub cb_question {
    my ($self, $res) = @_;
    $self->set_new_node(1);
}

sub cb_flow_question {
    my ($self, $res) = @_;
}

sub cb_empty_complexvalue {
    my ($self, $res) = @_;
    $self->scalar_event({ style => YAML_PLAIN_SCALAR_STYLE, value => '' });
}

sub cb_questionstart {
    my ($self, $token) = @_;
    $self->start_mapping($token->{column});
}

sub cb_complexcolon {
    my ($self, $res) = @_;
    $self->set_new_node(1);
}

sub cb_seqstart {
    my ($self, $token) = @_;
    my $column = $token->{column};
    $self->start_sequence($column);
    $self->set_new_node(1);
}

sub cb_seqitem {
    my ($self, $res) = @_;
    $self->set_new_node(1);
}

sub cb_take_quoted {
    my ($self, $token) = @_;
    my $subtokens = $token->{subtokens};
    my $stack = $self->event_stack;
    my $info = {
        style => $subtokens->[0]->{value} eq '"'
            ? YAML_DOUBLE_QUOTED_SCALAR_STYLE
            : YAML_SINGLE_QUOTED_SCALAR_STYLE,
        value => $token->{value},
        offset => $token->{column},
    };
    if (@$stack and $stack->[-1]->[0] eq 'properties') {
        $self->fetch_inline_properties($stack, $info);
    }
    push @{ $stack }, [ scalar => $info ];
}

sub cb_quoted_multiline {
    my ($self, $token) = @_;
    my $subtokens = $token->{subtokens};
    my $stack = $self->event_stack;
    my $info = {
        style => $subtokens->[0]->{value} eq '"'
            ? YAML_DOUBLE_QUOTED_SCALAR_STYLE
            : YAML_SINGLE_QUOTED_SCALAR_STYLE,
        value => $token->{value},
        offset => $token->{column},
    };
    if (@$stack and $stack->[-1]->[0] eq 'properties') {
        $self->fetch_inline_properties($stack, $info);
    }
    push @{ $stack }, [ scalar => $info ];
    $self->cb_send_scalar;
}

sub cb_take_quoted_key {
    my ($self, $token) = @_;
    $self->cb_take_quoted($token);
    $self->cb_send_mapkey;
}

sub cb_send_plain_multi {
    my ($self, $token) = @_;
    my $stack = $self->event_stack;
    my $info = {
        style => YAML_PLAIN_SCALAR_STYLE,
        value => $token->{value},
        offset => $token->{column},
    };
    if (@$stack and $stack->[-1]->[0] eq 'properties') {
        $self->fetch_inline_properties($stack, $info);
    }
    push @{ $stack }, [ scalar => $info ];
    $self->cb_send_scalar;
}

sub cb_start_plain {
    my ($self, $token) = @_;
    my $stack = $self->event_stack;
    my $info = {
            style => YAML_PLAIN_SCALAR_STYLE,
            value => $token->{value},
            offset => $token->{column},
    };
    if (@$stack and $stack->[-1]->[0] eq 'properties') {
        $self->fetch_inline_properties($stack, $info);
    }
    push @{ $stack }, [ scalar => $info ];
}

sub cb_start_flowseq {
    my ($self, $token) = @_;
    $self->start_flow_sequence($token->{column});
}

sub cb_start_flowmap {
    my ($self, $token) = @_;
    $self->start_flow_mapping($token->{column});
}

sub cb_end_flowseq {
    my ($self, $res) = @_;
    $self->end_flow_sequence;
    $self->set_new_node(0);
}

sub cb_flow_comma {
    my ($self) = @_;
    my $event_types = $self->events;
    $self->set_new_node(0);
    if ($event_types->[-1] =~ m/^FLOWSEQ/) {
        $event_types->[-1] = $next_event{ $event_types->[-1] };
    }
}

sub cb_flow_colon {
    my ($self) = @_;
    $self->set_new_node(1);
}

sub cb_empty_flow_mapkey {
    my ($self, $token) = @_;
    my $stack = $self->event_stack;
    my $info = {
        style => YAML_PLAIN_SCALAR_STYLE,
        value => '',
        offset => $token->{column},
    };
    if (@$stack and $stack->[-1]->[0] eq 'properties') {
        $self->fetch_inline_properties($stack, $info);
    }
    $self->scalar_event($info);
}

sub cb_end_flowmap {
    my ($self, $res) = @_;
    $self->end_flow_mapping;
    $self->set_new_node(0);
}

sub cb_end_flowmap_empty {
    my ($self, $res) = @_;
    $self->cb_empty_flowmap_value;
    $self->end_flow_mapping;
    $self->set_new_node(0);
}

sub cb_flow_plain {
    my ($self, $token) = @_;
    my $stack = $self->event_stack;
    my $info = {
        style => YAML_PLAIN_SCALAR_STYLE,
        value => $token->{value},
        offset => $token->{column},
    };
    if (@$stack and $stack->[-1]->[0] eq 'properties') {
        $self->fetch_inline_properties($stack, $info);
    }
    $self->scalar_event($info);
}

sub cb_flowkey_plain {
    my ($self, $token) = @_;
    my $stack = $self->event_stack;
    my $info = {
        style => YAML_PLAIN_SCALAR_STYLE,
        value => $token->{value},
        offset => $token->{column},
    };
    if (@$stack and $stack->[-1]->[0] eq 'properties') {
        $self->fetch_inline_properties($stack, $info);
    }
    $self->scalar_event($info);
}

sub cb_flowkey_quoted {
    my ($self, $token) = @_;
    my $stack = $self->event_stack;
    my $subtokens = $token->{subtokens};
    my $info = {
        style => $subtokens->[0]->{value} eq '"'
            ? YAML_DOUBLE_QUOTED_SCALAR_STYLE
            : YAML_SINGLE_QUOTED_SCALAR_STYLE,
        value => $token->{value},
        offset => $token->{column},
    };
    if (@$stack and $stack->[-1]->[0] eq 'properties') {
        $self->fetch_inline_properties($stack, $info);
    }
    $self->scalar_event($info);
}

sub cb_empty_flowmap_value {
    my ($self, $token) = @_;
    my $stack = $self->event_stack;
    my $info = {
        style => YAML_PLAIN_SCALAR_STYLE,
        value => '',
        offset => $token->{column},
    };
    if (@$stack and $stack->[-1]->[0] eq 'properties') {
        $self->fetch_inline_properties($stack, $info);
    }
    $self->scalar_event($info);
}

sub cb_insert_map_alias {
    my ($self, $res) = @_;
    my $stack = $self->event_stack;
    my $scalar = pop @$stack;
    my $info = $scalar->[1];
    $self->start_mapping($info->{offset});
    $self->alias_event($info);
    $self->set_new_node(1);
}

sub cb_insert_map {
    my ($self, $res) = @_;
    my $stack = $self->event_stack;
    my $scalar = pop @$stack;
    my $info = $scalar->[1];
    $self->start_mapping($info->{offset});
    $self->scalar_event($info);
    $self->set_new_node(1);
}

sub cb_insert_empty_map {
    my ($self, $token) = @_;
    my $stack = $self->event_stack;
    my $info = {
        style => YAML_PLAIN_SCALAR_STYLE,
        value => '',
        offset => $token->{column},
    };
    if (@$stack and $stack->[-1]->[0] eq 'properties') {
        $self->fetch_inline_properties($stack, $info);
    }
    $self->start_mapping($info->{offset});
    $self->scalar_event($info);
    $self->set_new_node(1);
}

sub cb_send_block_scalar {
    my ($self, $token) = @_;
    my $type = $token->{subtokens}->[0]->{value};
    my $stack = $self->event_stack;
    my $info = {
        style => $type eq '|'
            ? YAML_LITERAL_SCALAR_STYLE
            : YAML_FOLDED_SCALAR_STYLE,
        value => $token->{value},
        offset => $token->{column},
    };
    if (@$stack and $stack->[-1]->[0] eq 'properties') {
        $self->fetch_inline_properties($stack, $info);
    }
    push @{ $self->event_stack }, [ scalar => $info ];
    $self->cb_send_scalar;
}

sub cb_end_document {
    my ($self, $token) = @_;
    $self->end_document(0);
}

sub cb_end_document_empty {
    my ($self, $token) = @_;
    $self->end_document(0);
}

sub cb_doc_start_implicit {
    my ($self, $token) = @_;
    $self->start_document(1);
}

sub cb_doc_start_explicit {
    my ($self, $token) = @_;
    $self->start_document(0);
}

sub cb_end_doc_start_document {
    my ($self, $token) = @_;
    $self->end_document(1);
    $self->start_document(0);
}

sub cb_tag_directive {
    my ($self, $token) = @_;
    my ($name, $tag_alias, $tag_url) = split ' ', $token->{value};
    $self->tagmap->{ $tag_alias } = $tag_url;
}

sub cb_reserved_directive {
}

sub cb_set_yaml_version_directive {
    my ($self, $token) = @_;
    if ($self->yaml_version_directive) {
        croak "Found duplicate YAML directive";
    }
    my ($version) = $token->{value} =~ m/^%YAML (1\.[12])/;
    $self->set_yaml_version($version);
    $self->set_yaml_version_directive(1);
}

1;
