# -*- mode: perl; coding: utf-8 -*-
package YATT::LRXML::EntityPath;
use strict;
use warnings FATAL => qw(all);
BEGIN {require Exporter; *import = \&Exporter::import}
our @EXPORT_OK = qw(parse_entpath is_nested_entpath);
our @EXPORT = @EXPORT_OK;

=pod

  term     ::= ( text | expr | pipeline ) ','? ;

  pipeline ::= container?  trail+ ;

  trail    ::= var | '[' term ']' ;

  container::= '[' term* ']'
             |  '{' ( dot_name (':' text | '=' term )
                    | other+ ','?
                    )* '}' ;

  var      ::= (':'+ | '.'+) name ( '(' term* ')' )? ;
  name     ::= \w+ ;
  dot_name ::= [\w\.]+ ;

  expr     ::= '=' text ;
  text     ::= word ( group word? )* ; -- group で始まるのは、container.

  group    ::= [\(\[\{] ( text | ',' )* [\}\]\)]

  word     ::= [\w\$\-\+\*/%<>] other* ;
  other    ::= [\w\$\-\+\*/%<>:\.!=] ;

=cut

# is_nested_entpath($entpath, ?head?)

sub is_nested_entpath {
  return unless defined $_[0] and ref $_[0] eq 'ARRAY';
  my $item = shift;
  return unless defined $item->[0] and ref $item->[0] eq 'ARRAY';
  return 1 unless defined $_[0];
  defined $item->[0][0] and $item->[0][0] eq $_[0];
}

our ($TRANS, $NODE, $ORIG);

sub mydie (@) {
  my $fmt = shift;
  my $diag = do {
    if ($TRANS and $NODE) {
      $TRANS->node_error($NODE, $fmt, @_);
    } else {
      sprintf $fmt, @_;
    };
  };
  die $diag;
}

sub parse_entpath {
  my ($pack, $orig, $trans, $node) = @_;
  return undef unless defined $orig;
  local $_ = $orig;
  local $ORIG = $orig;
  local $TRANS = $trans;
  local $NODE = $node;
  my @result;
  if (wantarray) {
    @result = &_parse_pipeline;
  } else {
    $result[0] = &_parse_pipeline;
  }
  if ($_ ne '') {
    mydie "Unexpected token '$_' in entpath '$orig'";
  }
  wantarray ? @result : $result[0];
}

my %open_head = qw| ( call [ array { hash |;
my %open_rest = qw| ( call [ aref  |;
my %close_ch  = qw( ( ) [ ] { } );

my $re_var  = qr{[:]+ (\w+) (\()?}x;
my $re_other= qr{[\w\$\-\+\*/%<>\.=\@\|!:]}x;
my $re_word = qr{[\w\$\-\+\*/%<>\.=\@\|!] $re_other*}x;

sub _parse_pipeline {
  my @pipe;
  if (s/^ \[ //x) {
    # container
    push @pipe, _parse_group(['array'], ']', \&_parse_term);
  } elsif (s/^ \{ //x) {
    push @pipe, &_parse_hash;
  }
  while (s/^$re_var | ^(\[) | ^(\{)//x) {
      if ($2) {
	# '('
	push @pipe, _parse_group([call => $1], ')', \&_parse_term);
      } elsif (defined $1) {
	# \w+
	push @pipe, [var => $1];
      } elsif (defined $3) {	# '['
	push @pipe, _parse_group(['aref'], ']', \&_parse_term, 'expr');
      } elsif (defined $4) {
	push @pipe, _parse_group(['var'], '}', \&_parse_term);
      } else {
	mydie "?? $_";
      }
  }
  wantarray ? @pipe : \@pipe;
}

my $re_grend = qr{ (?=[\)\]\}]) | $ }x;
my $re_text  = qr{($re_word)      # 1
		  (?: ([\(\[\{])  # 2
		  | $re_grend)?
		| $re_grend
	       }x;

sub _parse_term {
  my ($literal_type) = @_;
  $literal_type ||= 'text';
  # :foo()     [call => foo]
  # :foo(,)    [call => foo => [text => '']]
  # :foo(bar)  [call => foo => [text => 'bar']]
  # :foo(,,)   [call => foo => [text => ''], [text => '']]
  # :foo(bar,) [call => foo => [text => 'bar'], [text => '']]
  # :foo(bar,,)[call => foo => [text => 'bar'], [text => '']]
  if (s{^,}{}x) {
    return [$literal_type => ''];
  }
  if (my $is_expr = s{^=}{}) {
    return &_parse_expr;
  }
  my @result;
  unless (s{^$re_text}{}) {
    @result = &_parse_pipeline;
  } else {
    my $result = '';
  TEXT: {
      do {
	$result .= $1 if defined $1;
	$result .= $4 if defined $4;
	if (my $opn = $2 || $3) {
	  # open group
	  $result .= $opn;
	  $result .= &_parse_group_string($close_ch{$opn});
	} elsif (not defined $1 and not defined $4) {
	  last TEXT;
	}
      } while s{^(?: $re_text | ([\(\[\{]) | ([:\.]) ) }{}x;
    }
    @result = [$literal_type => $result];
  }
  s/^,//;
  @result;
}

sub _parse_expr {
  my $literal_type = 'expr';
  if (s{^,}{}x) {
    return [$literal_type => ''];
  }
  my $result = '';
 TEXT:
  while (s{^(?: $re_text | ([\(\[\{]) | ([:\.]) ) }{}x) {
    $result .= $1 if defined $1;
    $result .= $4 if defined $4;
    if (my $opn = $2 || $3) {
      # open group
      $result .= $opn;
      $result .= &_parse_group_string($close_ch{$opn});
    } elsif (not defined $1 and not defined $4) {
      last TEXT;
    }
  }
  s/^,//;
  [$literal_type => $result];
}

sub _parse_group {
  my ($group, $close, $sub, @rest) = @_;
  for (my ($len, $cnt) = length($_); $_ ne ''; $len = length($_), $cnt++) {
    if (s/^ ([\)\]\}])//x) {
      mydie "Paren mismatch: expect $close got $1 " if $1 ne $close;
      last;
    }
    my @pipe = $sub->(@rest);
    if ($cnt && $len == length($_)) {
      mydie "Can't match: $_" . (defined $close ? " for $close" : "");
    }
    push @$group, @pipe <= 1 ? @pipe : \@pipe;
  }
  $group;
}

sub _parse_group_string {
  my ($close) = @_;
  my $result = '';
  for (my ($len, $prev) = length($_); $_ ne ''
       ; $prev = $len, $len = length($_)) {
    if (s/^ ([\)\]\}])//x) {
      mydie "Paren mismatch: expect $close got $1 " if $1 ne $close;
      $result .= $1;
      last;
    }
    if (s/^($re_word | , )//x) {
      $result .= $1;
    } elsif (s/^([\(\[\{])//) {
      $result .= $1;
      $result .= &_parse_group_string($close_ch{$1});
    }
    if (defined $prev and $prev == length($_)) {
      mydie "Can't parse entity_path group $ORIG (near $_)\n"
    }
  }
  $result;
}

sub _parse_hash {
  my @hash = ('hash');
  for (my ($len, $cnt) = length($_); $_ ne ''; $len = length($_), $cnt++) {
    if (s/^ ([\)\]\}])//x) {
      mydie "Paren mismatch: expect \} got $1 " if $1 ne '}';
      last;
    }
    # {!=,:var} を許すには…
    if (s/^([\w\.\-]+) [:=] //x || s/^($re_other+) ,?//x) {
      # ↑ array でも許すべきか?
      my $str = $1;
      push @hash, [$str =~ s/^:// ? 'var' : 'text', $str];
    }
    my @value = &_parse_term;
    push @hash, @value > 1 ? \@value : $value[0];
    unless (length($_) < $len) {
      mydie "Infinite loop on parse_hash: $_";
    }
  }
  # XXX: Give more detailed diag!
  mydie "Odd number of hash elements" if (@hash - 1) % 2;
  \@hash;
}

1;
