# $Id: Completion.pm,v 1.20 2003/09/10 13:36:26 pajas Exp $

package XML::XSH::Completion;

use XML::XSH::CompletionList;
use XML::XSH::Functions qw();
use strict;

our @PATH_HASH;
our $M=qr/(?:^|[;}]|\s+{)\s*/;
our $match_sv=qr/\$([a-zA-Z0-9_]*)$/; # scalar variable completion
our $match_nv=qr/\%([a-zA-Z0-9_]*)$/; # node-list variable completion
our $match_command=qr/${M}[^=\s]*$/; # command completion
our $match_func=qr/${M}(?:call|undef|undefine)\s+(\S*)$/; # function name completion
our $match_nodetype=qr/${M}x?(?:insert|add)\s+(\S*)$/; # node-type completion
our $match_doc=qr/${M}(?:close|doc[-_]info|dtd|enc)\s+(\S*)$|${M}clone\s+[a-zA-Z0-9_]*\s*=\s*[a-zA-Z0-9_]*$|${M}create\s+[a-zA-Z0-9_]*/; # docid completion
our $match_clone_doc=qr/${M}clone\s+[a-zA-Z0-9_]*$/;
our $match_help=qr/${M}(?:\?|help)\s+(\S*)$/; # help topic completion
our $match_open_flag1=qr/${M}open(?:\s+|([-_]))([A-Z]*)$/;
our $match_open_flag2=qr/${M}(?:open\s+|(open[-_]))(html|xml|docbook|HTML|XML|DOCBOOK)(?:\s+[A-Z]*|([-_])[A-Za-z]*)$/;
our $match_open_doc=qr/${M}(open)(?:\s+|_|-)(?:(?:html|xml|docbook|HTML|XML|DOCBOOK)(?:\s+|_|-))?(?:(?:file|pipe|string|FILE|PIPE|STRING)\s+)?([a-zA-Z0-9_]*)$/;
our $match_open_filename=qr/${M}(?:open(?:\s+|_|-)(?:(?:html|xml|docbook|HTML|XML|DOCBOOK)(?:\s+|_|-))?(?:(?:file|FILE)\s+)?)?[a-zA-Z0-9_]+\s*=\s*(\S*)$/;

our $match_save_flag1=qr/${M}save(?:\s+|([-_]))([A-Z]*)$/;
our $match_save_flag2=qr/${M}(?:save\s+|(save[-_]))(html|xml|xinclude|HTML|XML|XINCLUDE|XInclude)(?:\s+[A-Z]*|([-_])[A-Za-z]*)$/;
our $match_save_doc=qr/${M}(save)(?:\s+|_|-)(?:(?:html|xml|xinclude|HTML|XML|XInclude|XINCLUDE)(?:\s+|_|-))?(?:(?:file|pipe|string|FILE|PIPE|STRING)\s+)?([a-zA-Z0-9_]*)$/;
our $match_save_filename=qr/${M}save(?:\s+|_|-)(?:(?:html|xml|xinclude|HTML|XML|XInclude|XINCLUDE)(?:\s+|_|-))?(?:(?:file|FILE)\s+)?[a-zA-Z0-9_]+\s+(\S*)$/;
our $match_filename=qr/${M}(?:\.|include)\s+(\S*)$/;
our $match_dir=qr/${M}(?:lcd)\s+(\S*)$/;
our $match_path_filename=qr/${M}(?:system\s|exec\s|\!)\s*\S*$|\s\|\s*\S*$/;
our $match_no_xpath=join '|',@XML::XSH::CompletionList::XSH_NOXPATH_COMMANDS;
our $match_no=qr/${M}(?:${match_no_xpath}|create\s+[a-zA-Z0-9_]*\s)\s*$/;

# PATH-completion: system, !, exec, |, 

our @nodetypes = qw(element attribute attributes text cdata pi comment chunk entity_reference);
our @openflags1 = qw(HTML XML DOCBOOK);
our @openflags2 = qw(FILE PIPE STRING);
our @saveflags1 = qw(HTML XML XINCLUDE);
our @saveflags2 = qw(FILE PIPE STRING);

sub perl_complete {
  my($word,$line,$pos) = @_;
  my $endpos=$pos+length($word);
  cpl('perl',$word,$line,$pos,$endpos);
}

sub gnu_complete {
  my($text, $line, $start, $endpos) = @_;
  &main::_term()->Attribs->{completion_append_character} = ' ';
  my @result=cpl('gnu',$text,$line,$start,$endpos);
  # find longest common match. Can anybody show me how to persuade
  # T::R::Gnu to do this automatically? Seems expensive.
  return () unless @result;
  my($newtext) = $text;
  for (my $i = length($text)+1;;$i++) {
    last unless length($result[0]) && length($result[0]) >= $i;
    my $try = substr($result[0],0,$i);
    my @tries = grep {substr($_,0,$i) eq $try} @result;
    # warn "try[$try]tries[@tries]";
    if (@tries == @result) {
      $newtext = $try;
        } else {
	  last;
        }
  }
  ($newtext,@result);
}

sub complete_set_term_char {
  my ($type,$char)=@_;
  if ($type eq 'perl') {
    $readline::rl_completer_terminator_character = $char;
  } else {
    &main::_term()->Attribs->{completion_append_character} = $char;
  }
}

sub complete_filename {
  my ($type,$word)=@_;
  if ($type eq 'perl') {
    return eval { map { s:\@$::; $_ } readline::rl_filename_list($word); };
  } else {
    return eval { map { s:\@$::; $_ } Term::ReadLine::Gnu::XS::rl_filename_list($word) };
  }
}

sub rehash_path_hash {
  my %result;
  my $dh;
  my $pdelim= $^O eq 'MSWin32' ? '\\' : '/';
  my $delim=($^O eq 'MSWin32' ? ';' : ':');
  my @path=grep /\S/,split($delim,$ENV{PATH});
  foreach my $dir (@path) {
    local *DIR;
    if (opendir DIR, $dir) {
      my @files=grep { -f "$dir$pdelim$_" and -x "$dir$pdelim$_" } readdir(DIR);
      @result{@files}=();
      closedir DIR;
    }
  }
  @PATH_HASH=sort keys %result;
}

sub complete_system_command {
  my ($type,$word)=@_;
  my $pdelim= $^O eq 'MSWin32' ? '\\' : '/';
  if (index($word,$pdelim)>=0) {
    return grep -x,complete_filename($type,$word);
  }
  unless (defined @PATH_HASH) {
    rehash_path_hash();
  }
  return grep {index($_,$word)==0} @PATH_HASH;
}

sub cpl {
  my($type,$word,$line,$pos,$endpos) = @_;
  if (substr($line,0,$endpos)=~$match_sv) {
    return map {'$'.$_} grep { index($_,$1)==0 } XML::XSH::Functions::string_vars;
  } elsif (substr($line,0,$endpos)=~$match_nv) {
    return map {'%'.$_} grep { index($_,$1)==0 } XML::XSH::Functions::nodelist_vars;
  } elsif (substr($line,0,$endpos)=~$match_func) {
    return grep { index($_,$1)==0 } XML::XSH::Functions::defs;
  } elsif (substr($line,0,$endpos)=~$match_nodetype) {
    return grep { index($_,$1)==0 } @nodetypes;
  } elsif (substr($line,0,$endpos)=~$match_help) {
    return grep { index($_,$1)==0 } keys %XML::XSH::Help::HELP;
  } elsif (substr($line,0,$endpos)=~$match_open_flag1) {
    my $prefix;
    $prefix='open'.$1 if ($1 ne "");
    return grep { index(uc($_),uc($word))==0 } map {$prefix.$_} @openflags1, @openflags2;
  } elsif (substr($line,0,$endpos)=~$match_open_flag2) {
    my $prefix;
    if ($3 ne "") {
      $prefix=$1.uc($2).$3;
      return grep { index(uc($_),uc($word))==0 } map {$prefix.$_} @openflags2;
    } else {
      return grep { index($_,uc($word))==0 } @openflags2;
    }
  } elsif (substr($line,0,$endpos)=~$match_save_flag1) {
    if ($1) {
      my $prefix;
      $prefix='save'.$1;
      return grep { index(uc($_),uc($word))==0 } map {$prefix.$_} @saveflags1, @saveflags2;
    } else {
      return grep { index($_,uc($word))==0 } @saveflags1, @saveflags2, XML::XSH::Functions::docs();
    }
  } elsif (substr($line,0,$endpos)=~$match_save_flag2) {
    my $prefix;
    if ($3 ne "") {
      $prefix=$1.uc($2).$3;
      return grep { index(uc($_),uc($word))==0 } map {$prefix.$_} @saveflags2;
    } else {
      return grep { index($_,uc($word))==0 } @saveflags2, XML::XSH::Functions::docs();
    }
  } elsif (substr($line,0,$pos)=~$match_command) {
    return grep { index($_,$word)==0 } @XML::XSH::CompletionList::XSH_COMMANDS;
  } elsif (substr($line,0,$endpos)=~$match_doc ||
	   substr($line,0,$endpos)=~$match_save_doc) {
    return grep { index($_,$word)==0 } XML::XSH::Functions::docs();
  } elsif (substr($line,0,$endpos)=~$match_clone_doc ||
	   substr($line,0,$endpos)=~$match_open_doc) {
    complete_set_term_char($type,'=');
    return grep { index($_,$word)==0 } XML::XSH::Functions::docs();
  } elsif (substr($line,0,$endpos)=~$match_open_filename ||
	   substr($line,0,$endpos)=~$match_save_filename ||
	   substr($line,0,$endpos)=~$match_filename) {
    my @result=complete_filename($type,$word);
    if (@result==1 and -d $result[0]) {
      complete_set_term_char($type,'');
    } else {
      complete_set_term_char($type,' ');
    }
    return @result;
  } elsif (substr($line,0,$endpos)=~$match_dir) {
    my @result=grep -d, complete_filename($type,$word);
    if (@result==1) {
      complete_set_term_char($type,' ');
    } else {
      complete_set_term_char($type,'');
    }
    return @result;
  } elsif (substr($line,0,$endpos)=~$match_path_filename) {
    my @result=complete_system_command($type,$word);
    if (@result==1 and -d $result[0]) {
      complete_set_term_char($type,'');
    } else {
      complete_set_term_char($type,' ');
    }
    return @result;

  } elsif (substr($line,0,$endpos)=~$match_no) {
    return ();
  } else {
    complete_set_term_char($type,'');
    return xpath_complete($line,$word,$pos);
  }
}

sub xpath_complete_str {
  my $str = reverse($_[0]);
  my $debug = $_[1];
  my $result="";
  my $NAMECHAR = '[-_.[:alnum:]]';
  my $NNAMECHAR = '[-:_.[:alnum:]]';
  my $NAME = "${NAMECHAR}*${NNAMECHAR}*[_.[:alpha:]]";

  my $WILDCARD = '\*(?!\*|${NAME}|\)|\]|\.)';
  my $OPER = qr/(?:[,=<>\+\|]|-(?!${NAME})|(?:vid|dom|dna|ro)(?=\s*\]|\s*\)|\s*[0-9]+(?!${NNAMECHAR})|\s+{$NAMECHAR}|\s+\*))/;

  print "'$str'\n" if $debug;
  my $localmatch;

 STEP0:
  if ($str =~ /\G\s*[\]\)]/gsco) {
    print "No completions after ] or )\n" if $debug;
    return;
  }

 STEP1:
  if ( $str =~ /\G(${NAMECHAR}+)?(?::(${NAMECHAR}+))?/gsco ) {
    if ($2 ne "") {
      $localmatch=reverse($2).":".reverse($1);
      if ($1 ne "") {
	$result=reverse($2).':*[starts-with(local-name(),"'.reverse($1).'")]'.$result;
      } else {
	$result=reverse($2).':*'.$result;
      }
    } else {
      $localmatch=reverse($1);
      $result='*[starts-with(name(),"'.$localmatch.'")]'.$result;
    }
  } else {
    $result='*'.$result;
  }
  if ($str =~ /\G\@/gsco) {
    $result="@".$result;
  }

 STEP2:
  print "STEP2-LOCALMATCH: $localmatch\n" if $debug;
  print "STEP2: $result\n" if $debug;
  print "STEP2-STR: ".reverse(substr($str,pos($str)))."\n" if $debug;
  while ($str =~ m/\G(::|:|\@|${NAME}\$?|\/\/|\/|${WILDCARD}|\)|\])/gsco) {
    print "STEP2-MATCH: '$1'\n" if $debug;
    if ($1 eq ')' or $1 eq ']') {
      # eat ballanced upto $1
      my @ballance=(($1 eq ')' ? '(' : '['));
      $result=$1.$result;
      print "STEP2: Ballanced $1\n" if $debug;
      do {
	$result=reverse($1).$result if $str =~ m/\G([^]["'()]+)/gsco; # skip normal characters
	return ($result,$localmatch) unless $str =~ m/\G(.)/gsco;
	if ($1 eq $ballance[$#ballance]) {
	  pop @ballance;
	} elsif ($1 eq ')') {
	  push @ballance, '(';
	} elsif ($1 eq ']') {
	  push @ballance, '[';
	} elsif ($1 eq '"') {
	  push @ballance, '"';
	} elsif ($1 eq "'") {
	  push @ballance, "'";
	} else {
	  print STDERR "Error 2: lost in an expression on '$1' ";
	  print STDERR reverse(substr($str,pos()))."\n";
	  print "-> $result\n";
	  return undef;
	}
	$result=$1.$result;
      }	while (@ballance);
    } else {
      $result=reverse($1).$result;
    }
  }

 STEP3:
  print "STEP3: $result\n" if $debug;
  print "STEP3-STR: ".reverse(substr($str,pos($str)))."\n" if $debug;
  if (substr($result,0,1) eq '/') {
    if ($str =~ /\G['"]/gsco) {
      print STDERR "Error 1: unballanced '$1'\n";
      return undef;
    } elsif ($str =~ /\G(?:\s+['"]|\(|\[|${OPER})/gsco) {
      return ($result,$localmatch);
    }
    return ($result,$localmatch); # uncertain!!!
  } else {
    return ($result,$localmatch) if ($str=~/\G\s+(?=${OPER})/gsco);
  }

 STEP4:
  print "STEP4: $result\n" if $debug;
  print "STEP4-STR: ".reverse(substr($str,pos($str)))."\n" if $debug;
  my @ballance;
  do {
    $str =~ m/\G([^]["'()]+)/gsco; # skip normal characters
    print "STEP4-MATCH '".reverse($1)."'\n" if $debug;
    return ($result,$localmatch) unless $str =~ m/\G(.)/gsco;
    print "STEP4-BALLANCED '$1'\n" if $debug;
    if (@ballance and $1 eq $ballance[$#ballance]) {
      pop @ballance;
    } elsif ($1 eq ')') {
      push @ballance, '(';
    } elsif ($1 eq ']') {
      push @ballance, '[';
    } elsif ($1 eq '"') {
      push @ballance, '"';
    } elsif ($1 eq "'") {
      push @ballance, "'";
    } elsif (not(@ballance) and $1 eq '[') {
      print "STEP4-PRED2STEP '$1'\n" if $debug;
      $result='/'.$result;
      goto STEP2;
    }
  } while (@ballance);
  goto STEP4;
}

sub xpath_complete {
  my ($line, $word,$pos)=@_;
  return () unless $XML::XSH::Functions::XPATH_COMPLETION;
  my $str=XML::XSH::Functions::toUTF8($XML::XSH::Functions::QUERY_ENCODING,
				      substr($line,0,$pos).$word);
  my ($xp,$local) = xpath_complete_str($str,0);
#  XML::XSH::Functions::__debug("COMPLETING $_[0] local $local as $xp\n");
  return () if $xp eq "";
  my ($docid,$q) = ($xp=~/^(?:([a-zA-Z_][a-zA-Z0-9_]*):(?!:))?((?:.|\n)*)$/);
  if ($docid ne "" and not XML::XSH::Functions::_doc($docid)) {
    $q=$docid.":".$q;
    $docid="";
  }
  my ($id,$query,$doc)=XML::XSH::Functions::_xpath([$docid,$q]);
  return () unless (ref($doc));
  my $ql= eval { XML::XSH::Functions::find_nodes([$id,$query]) };
  return () if $@;
  my %names;
  @names{ map { 
    XML::XSH::Functions::fromUTF8($XML::XSH::Functions::QUERY_ENCODING,
				  substr(substr($str,0,
						length($str)
						-length($local)).
					 $_->nodeName(),$pos))
  } @$ql}=();

  my @completions = sort { $a cmp $b } keys %names;
#  print "completions so far: @completions\n";

  if (($XML::XSH::Functions::XPATH_AXIS_COMPLETION eq 'always' or
       $XML::XSH::Functions::XPATH_AXIS_COMPLETION eq 'when-empty' and !@completions)
      and $str =~ /[ \n\t\r|([=<>+-\/]([[:alpha:]][-:[:alnum:]]*)?$/ and $1 !~ /::/) {
    # complete XML axis
    my ($pre,$axpart)=($word =~ /^(.*[^[:alnum:]])?([[:alpha:]][-[:alnum:]:]*)/);
#    print "\nWORD: $word\nPRE: $pre\nPART: $axpart\nSTR:$str\n";
    foreach my $axis (qw(following preceding following-sibling
			 preceding-sibling
			 parent ancestor ancestor-or-self descendant self
			 descendant-or-self child attribute namespace)) {
      if ($axis =~ /^${axpart}/) {
	push @completions, "${pre}${axis}::";
      }
    }
  }
  return @completions;
}

1;