# -*- mode: perl; coding: utf-8 -*-
package YATT::LRXML::NodeCursor; # Location, Zipper?
use strict;
use warnings FATAL => qw(all);

use base qw(YATT::Class::Configurable);
use YATT::Fields qw(^tree ^cf_metainfo cf_path);
sub Path () {'YATT::LRXML::NodeCursor::Path'}

use YATT::Util::Symbol;
use YATT::LRXML::Node qw(stringify_node
			 stringify_attlist
			 create_node
			 create_node_from
			 copy_array);

use Carp;

# XXX: Configurable に init と clone のプロトコルを…って、
# fields の中身に依存するから、やばいか。

BEGIN {
  package YATT::LRXML::NodeCursor::Path;
  use base qw(YATT::Class::ArrayScanner);
  use YATT::Fields qw(cf_path cur_postype prev_postype);

  use YATT::LRXML::Node qw(node_type ATTRIBUTE_TYPE);

  use YATT::Util::Enum -prefix => 'POSTYPE_', qw(UNKNOWN ATTLIST BODY);

  sub init {
    my ($self, $array, $path, $index0) = splice @_, 0, 4;
    $self->SUPER::init(array => $array
		       , index => ($index0 || 0)
		       + YATT::LRXML::Node::_BODY
		       , path => $path, @_)
      ->after_next;
  }

  sub clone {
    my MY $orig = shift;
    ref($orig)->new($orig->{cf_array}, $orig->{cf_path}
		    # XXX: To compensate init()
		    , $orig->{cf_index} - YATT::LRXML::Node::_BODY);
  }

  sub parent {
    my MY $path = shift; $path->{cf_path}
  }

  sub after_next {
    (my MY $path) = @_;
    return $path unless defined $path->{cf_index}
      and $path->{cf_index} <= $#{$path->{cf_array}};
    my $val = $path->{cf_array}->[$path->{cf_index}];
    $path->{prev_postype} = $path->{cur_postype};
    if (not defined $path->{cur_postype}
	or $path->{cur_postype} == POSTYPE_ATTLIST) {
      $path->{cur_postype} = ref $val && node_type($val) == ATTRIBUTE_TYPE
	? POSTYPE_ATTLIST : POSTYPE_BODY;
    }
    $path
  }

  sub is_beginning {
    (my MY $path) = @_;
    return 1 unless defined $path->{prev_postype};
    return unless $path->{cur_postype} == POSTYPE_BODY;
    $path->{prev_postype} == POSTYPE_ATTLIST;
  }
}

sub initargs {qw(tree)}

sub new_opened {
  my ($class, $tree) = splice @_, 0, 2;
  $class->new($tree, path => $class->Path->new($tree), @_);
}

sub new_path {
  my MY $self = shift;
  $self->Path->new($self->{tree}, shift); # XXX: tree でいいの?
}

sub clone_path {
  my MY $self = shift;
  my Path $path = shift || $self->{cf_path};
  $self->Path->new($path->{cf_array}, $path ? $path->{cf_path} : undef);
}

sub clone {
  (my MY $self, my ($path)) = @_;
  # XXX: 他のパラメータは? 特に、継承先で足したパラメータ。
  ref($self)->new($self->{tree}
		  , metainfo => $self->{cf_metainfo}
		  , path => ($path || ($self->{cf_path} ? $self->{cf_path}->clone
				       : undef)));
}

sub variant_builder {
  my MY $self = shift;
  my Path $orig = $self->{cf_path};
  my $variant = do {
    if (@_) {
      $self->create_node(@_);
    } else {
      $self->create_node_from($orig->{cf_array});
    }
  };
  $self->adopter_for($variant, $orig->{cf_path});
}

sub adopter_for {
  (my MY $self, my ($array, $path)) = @_;
  $self->clone($self->Path->new($array, $path || $self->{cf_path}))
}

sub add_node {
  my MY $self = shift;
  my Path $path = $self->{cf_path};
  push @{$path->{cf_array}}, @_;
  $self;
}

sub create_attribute {
  (my MY $self, my ($name)) = splice @_, 0, 2;
  $self->create_node([attribute => 0], $name, @_);
}

sub add_attribute {
  (my MY $self, my ($name)) = splice @_, 0, 2;
  $self->add_node(my $attr = $self->create_node([attribute => 0], $name, @_));
  $attr;
}

sub add_filtered_copy {
  (my MY $self, my ($node, $filter, $primary_only)) = @_;
  my $boundary = $primary_only ? 'is_primary_attribute' : 'readable';
  for (; $node->$boundary(); $node->next) {
    my @node = do {
      if ($node->is_attribute) {
	my ($sub, @rest) = ref $filter eq 'ARRAY' ? @$filter : $filter;
	$sub->(@rest, $node->node_name, $node->current);
      } else {
	copy_array($node->current);
      }
    };
    $self->add_node(@node) if @node;
  }
  $self;
}

sub copy_from {
  (my MY $clone, my MY $orig) = @_;
  for (my $n = $orig->clone; $n->readable; $n->next) {
    $clone->add_node(copy_array($n->current));
  }
  $clone;
}

sub clone_filtered_by {
  my MY $orig = shift;
  # XXX: $orig を next してしまって、良いのか？ clone した方が良いかも?
  my MY $clone = $orig->variant_builder;
  my ($hash, $all) = @_;
  my $boundary = $all ? 'readable' : 'is_primary_attribute';
  for (; $orig->$boundary(); $orig->next) {
    my @name;
    if ($orig->is_attribute and @name = $orig->node_path
	and $hash->{$name[0]}) {
      ${$hash->{$name[0]}} = $orig->current;
      next;
    }
    $clone->add_node(copy_array($orig->current));
  }
  $clone;
}

sub copy {
  (my MY $self, my ($node)) = @_;
  copy_array($node);
}

sub copy_renamed {
  (my MY $self, my ($name, $node)) = @_;
  if (defined $name) {
    $self->create_node_from
      ($node, $name, copy_array(YATT::LRXML::Node::node_children($node)));
  } else {
    copy_array($node);
  }
}

sub make_wrapped {
  (my MY $self, my ($type, $name)) = splice @_, 0, 3;
  my Path $orig = $self->{cf_path};
  my $wrap = $self->create_node($type || 'unknown', $name, $orig->{cf_array});
  my $path = $self->Path->new($wrap, $orig);
  ref($self)->new($self->{tree}
		  , metainfo => $self->{cf_metainfo}
		  , path => $path);
}

sub filter_or_add_from {
  (my MY $self, my ($node, $except, %opts)) = @_;
  my $boundary = delete $opts{primary_only}
    ? 'is_primary_attribute' : 'readable';
  croak "Invalid option: " . join(",", keys %opts) if %opts;

  my ($name, @filtered);
  for (; $node->$boundary(); $node->next) {
    if ($node->is_attribute
	and defined ($name = $node->node_name)
	and exists $except->{$name}) {
      # clone は？
      # name を書き換えても良いのでは？
      my $cur = $node->current;
      push @filtered, do {
	if (defined $except->{$name}) {
	  $self->copy_renamed($cur, $except->{$name});
	} else {
	  $cur
	}
      };
    } else {
      $self->add_node($node->current);
    }
  }

  @filtered;
}

sub open {
  my MY $self = shift;
  my $obj;
  unless (defined (my Path $path = $self->{cf_path})) {
    $self->clone($self->new_path);
  } elsif (not defined ($obj = $path->{cf_array}->[$path->{cf_index}])
	   or ref $obj ne 'ARRAY') {
    $obj;
  } else {
    # 本当に clone が良いのだろうか?
    $self->clone($self->Path->new($obj, $path));
  }
}

# cursor 本体ではなく、path だけが欲しいときのために。
# ← open をカスタマイズしたい時に用いる。
sub open_path {
  my MY $self = shift;
  unless (defined (my Path $path = $self->{cf_path})) {
    $self->new_path;
  } else {
    my $obj = $path->{cf_array}->[$path->{cf_index}];
    die "Not an object!" unless defined $obj && ref $obj eq 'ARRAY';
    $self->Path->new($obj, $path);
  }
}

sub can_open {
  my MY $self = shift;
  my Path $path = $self->{cf_path};
  my $obj = $path->{cf_array}->[$path->{cf_index}];
  defined $obj && ref $obj eq 'ARRAY';
}

sub close {
  my MY $self = shift;
  if (my Path $parent = $self->{cf_path}->parent) {
    $parent->{cf_index}++;
    $self->clone($parent);
  } else {
    return
  }
}

sub parent {
  my MY $self = shift;
  $self->clone($self->{cf_path}->parent);
}

sub can_close {
  my MY $self = shift;
  defined $self->{cf_path};
}

BEGIN {
  my @delegate_to_path =
    qw(read
       current
       next
       prev
       array
     );
  foreach my $meth (@delegate_to_path) {
    *{globref(__PACKAGE__, $meth)} = sub {
      my MY $self = shift;
      return unless defined $self->{cf_path};
      $self->{cf_path}->$meth(@_);
    };
  }

  my @delegate_and_self = qw(go_next);
  foreach my $meth (@delegate_and_self) {
    *{globref(__PACKAGE__, $meth)} = sub {
      my MY $self = shift;
      return unless defined $self->{cf_path};
      $self->{cf_path}->$meth(@_);
      $self;
    };
  }

  foreach my $meth (grep {/^(node|is)_/} YATT::LRXML::Node->exports) {
    my $for_text = do {no strict 'refs'; \&{"text_$meth"}};
    my $sub = YATT::LRXML::Node->can($meth);
    *{globref(__PACKAGE__, $meth)} = sub {
      my MY $cursor = shift;
      return unless $cursor->readable;
      if (ref(my $value = $cursor->current)) {
	$sub->($value, @_);
      } else {
	$for_text->($value, @_);
      }
    };
  }

  foreach my $meth (my @delegate_to_meta = qw(filename)) {
    *{globref(__PACKAGE__, $meth)} = sub {
      my MY $cursor = shift;
      defined (my $meta = $cursor->{cf_metainfo})
	or return;
      $meta->$meth(@_);
    };
  }
}

sub rewind {
  my MY $self = shift;
  if (my Path $path = $self->{cf_path}) {
    $path->{cf_index} = YATT::LRXML::Node::_BODY;
  }
  $self
}

sub readable {
  my MY $self = shift;
  defined $self->{cf_path} && $self->{cf_path}->readable;
}

# value, size は全体。
sub value {
  my MY $self = shift;
  unless (defined $self->{cf_path}) {
    $self->{tree}
  } else {
    $self->{cf_path}->value;
  }
}

sub array_size {
  my MY $self = shift;
  YATT::LRXML::Node::node_size(do {
    unless (defined (my Path $path = $self->{cf_path})) {
      $self->{tree};
    } else {
      $path->{cf_array};
    }
  });
}

sub size {
  my MY $self = shift;
  unless (defined (my Path $path = $self->{cf_path})) {
    YATT::LRXML::Node::node_size($self->{tree});
  } elsif (not defined (my $obj = $path->{cf_array}->[$path->{cf_index}])) {
    0
  } elsif (ref $obj) {
    YATT::LRXML::Node::node_size($obj);
  } else {
    1;
  }
}

sub has_parent {
  my MY $self = shift;
  defined (my Path $path = $self->{cf_path}) or return 0;
  $path->{cf_path}
}

sub depth {
  my MY $self = shift;
  my $depth = 0;
  while (defined (my Path $path = $self->{cf_path})) {
    $depth++;
  }
  $depth;
}

sub startline {
  my MY $self = shift;
  $self->metainfo->cget('startline');
}

sub linenum {
  (my MY $self, my ($offset_atstart)) = @_;
  my $linenum = $self->startline;
  my Path $path = $self->{cf_path};
  my $offset = $offset_atstart;
  while ($path) {
    $linenum += $self->count_lines_of(map {
      $path->{cf_array}[$_]
    } YATT::LRXML::Node::_BODY .. $path->{cf_index} - 1 + ($offset || 0));
    $path = $path->{cf_path};
    undef $offset;
  }
  $linenum;
}

sub count_lines_of {
  # XXX: 他でも使うように。
  my ($pack) = shift;
  my $sum = 0;
  foreach my $item (@_) {
    next unless defined $item;
    $sum += do {
      if (ref $item) {
	YATT::LRXML::Node::node_nlines($item);
      } else {
	$item =~ tr:\n::;
      }
    };
  }
  $sum;
}

sub node_is_beginning {
  my MY $self = shift;
  my Path $path = $self->{cf_path} or return;
  $path->is_beginning;
}

sub node_is_end {
  my MY $self = shift;
  my Path $path = $self->{cf_path} or return;
  defined $path->{cf_index} or return;
  $path->{cf_index} >= $#{$path->{cf_array}};
}

*stringify = *stringify_current; *stringify = *stringify_current;

sub stringify_current {
  my MY $self = shift;
  my Path $path = $self->{cf_path};
  unless (defined $path) {
    stringify_node($self->{tree});
  } elsif (ref (my $value = $path->current)) {
    stringify_node($value);
  } else {
    $value;
  }
}

sub stringify_all {
  my MY $self = shift;
  my Path $path = $self->{cf_path};
  unless (defined $path) {
    stringify_node($self->{tree});
  } else {
    stringify_node($path->{cf_array});
  }
}

sub path_list {
  my MY $self = shift;
  my @path;
  if (my Path $path = $self->{cf_path}) {
  # XXX: 一、ずれてるじゃん、と。引くの?
    do {
      unshift @path, $path->{cf_index} - YATT::LRXML::Node::_BODY;
      $path = $path->{cf_path};
    } while $path;
  }
  wantarray ? @path : join ", ", @path;
}

sub parse_typespec {
  my MY $self = shift;
  my ($head, @rest) = $self->node_children;
  unless (defined $head) {
    ()
  } elsif ($head =~ s{^(\w+((?:\:\w+)*))?(?:([|/?!])(.*))?}{}s) {
    # $1 can undef.
    ($1 && $2 ? [split /:/, $1] : $1
     , default => @rest ? [defined $4 ? ($4) : (), @rest] : $4
     , default_mode => $3)
  } else {
    (undef);
  }
}

sub next_is_body {
  my MY $self = shift;
  my Path $path = $self->{cf_path} or return;
  my $next = $path->{cf_index} + 1;
  return if $next >= @{$path->{cf_array}};
  my $item = $path->{cf_array}[$next];
  return unless defined $item;
  return 1 unless ref $item;
  not YATT::LRXML::Node::is_primary_attribute($item);
}

sub text_is_attribute { 0 }
sub text_is_bare_attribute { 0 }
sub text_is_primary_attribute { 0 }
sub text_is_quoted_by_element { 0 }
sub text_node_size { 1 }
sub text_node_type { YATT::LRXML::Node::TEXT_TYPE }
sub text_node_body { shift }
sub text_node_type_name { 'text' }
sub text_node_flag { 0 }
sub text_node_name { undef }
sub text_node_children {
  if (ref $_[0]) {
    YATT::LRXML::Node::node_children($_[0])
  } else {
    $_[0];
  }
}

1;
