package LaTeX::TikZ::Functor;

use strict;
use warnings;

=head1 NAME

LaTeX::TikZ::Functor - Build functor methods that recursively visit nodes of a LaTeX::TikZ::Set tree.

=head1 VERSION

Version 0.01

=cut

our $VERSION = '0.01';

=head1 DESCRIPTION

A functor takes a L<LaTeX::TikZ::Set> tree and clones it according to certain rules.

=cut

use Carp ();

use Sub::Name ();

use LaTeX::TikZ::Interface;

use LaTeX::TikZ::Tools;

use Any::Moose 'Util' => [ qw[find_meta does_role] ];

my $lts_tc = LaTeX::TikZ::Tools::type_constraint('LaTeX::TikZ::Set');

my @default_set_rules;
my @default_mod_rules;

my ($validate_rule, $insert_rule);
BEGIN {
 $validate_rule = Sub::Name::subname('validate_rule' => sub {
  my ($spec, $handler) = @_;

  my ($replace, $target);
  if (defined $spec and ref $spec eq ''
    and $spec =~ /^(\+?)([A-Za-z][A-Za-z0-9_]*(?:::[A-Za-z][A-Za-z0-9_]*)*)$/) {
   $replace = defined($1) && $1 eq '+';
   $target  = $2;
  } else {
   Carp::confess("Invalid rule spec $spec");
  }

  (my $pm = $target) =~ s{::}{/}g;
  $pm .= '.pm';
  require $pm;

  my $meta = find_meta($target);
  Carp::confess("No meta object associated with target $target")
                                                           unless defined $meta;
  my $is_role = $meta->isa(any_moose('Meta::Role'));

  my $is_set;
  if (does_role($target, 'LaTeX::TikZ::Set')) {
   $is_set = 1;
  } elsif (does_role($target, 'LaTeX::TikZ::Mod')) {
   $is_set = 0;
  } else {
   Carp::confess("Target $target is neither a set nor a mod");
  }

  Carp::confess("Invalid handler for rule spec $spec")
                             unless defined $handler and ref $handler eq 'CODE';

  return [ $target, $handler, $replace, $is_role, $is_set ];
 });

 $insert_rule = Sub::Name::subname('insert_rule' => sub {
  my ($rule, $list, $overwrite) = @_;

  my ($target, $replace, $is_role) = @{$rule}[0, 2, 3];

  if ($replace) {
   my @remove;

   for my $i (0 .. $#$list) {
    my $old_target = $list->[$i]->[0];
    if ($is_role ? does_role($old_target, $target)
                 : $old_target->isa($target)) {
     if (defined $rule) {
      splice @$list, $i, 1, $rule;
      $rule = undef;
     } else {
      push @remove, $i;
     }
    }
   }

   my $shift;
   for (@remove) {
    splice @$list, $_ - $shift, 1;
    ++$shift;
   }
   return 1 unless defined $rule;

  } else { # Replace only an existent rule

   for my $i (0 .. $#$list) {
    my $old_target = $list->[$i]->[0];
    if ($old_target eq $target) {
     Carp::confess("Default rule already defined for target $target")
                                                              unless $overwrite;
     splice @$list, $i, 1, $rule;
     return 1;
    }
   }
  }

  push @$list, $rule;
  return 0;
 });
}

=head1 METHODS

=head2 C<< new rules => [ $spec1 => $handler1, $spec2 => $handler2, ... ] >>

Creates a new functor object that will use both the default and these user-specified rules.
The functor is also a code reference that expects to be called against L<LaTeX::TikZ::Set> objects.

The default set and mod rules clone their relevant objects, so you get a clone functor (for the default set types) if you don't specify any user rule.

    # The default is a clone method
    my $clone = Tikz->functor;
    my $dup = $set->$clone;

If there is already a default rule for one of the C<$spec>s, it is replaced by the new one ; otherwise, the user rule is appended to the list of default rules.

    # A translator
    my $translate = Tikz->functor(
     # Only replace the way point sets are cloned
     'LaTeX::TikZ::Set::Point' => sub {
      my ($functor, $set, $x, $y) = @_;

      $set->new(
       point => [
        $set->x + $x,
        $set->y + $y,
       ],
       label => $set->label,
       pos   => $set->pos,
      );
     },
    );
    my $shifted = $set->$translate(1, 1);

But if one of the C<$spec>s begins with C<'+'>, the rule will replace I<all> default rules that apply to subclasses or subroles of C<$spec> (including C<$spec> itself).

    # A mod stripper
    my $strip = Tikz->functor(
     # Replace all existent mod rules by this simple one
     '+LaTeX::TikZ::Mod' => sub { return },
    );
    my $naked = $set->$strip;

The functor will map unhandled sets and mods to themselves without cloning them, since it has no way to know how to do it.
Thus, if you define your own L<LaTeX::TikZ::Set> or L<LaTeX::TikZ::Mod> object, be sure to register a default rule for it with the L</default_rule> method.

=cut

sub new {
 my ($class, %args) = @_;

 my @set_rules = @default_set_rules;
 my @mod_rules = @default_mod_rules;

 my @user_rules = @{$args{rules} || []};
 while (@user_rules) {
  my ($spec, $handler) = splice @user_rules, 0, 2;

  my $rule = $validate_rule->($spec, $handler);

  $insert_rule->($rule, $rule->[4] ? \@set_rules : \@mod_rules, 1);
 }

 my %dispatch  = map { $_->[0] => $_ } @set_rules, @mod_rules;

 my $self;

 $self = bless sub {
  my $set = shift;

  $lts_tc->assert_valid($set);

  my $rule = $dispatch{ref($set)};
  unless ($rule) {
   for (@set_rules) {
    if ($_->[2] ? $set->does($_->[0]) : $set->isa($_->[0])) {
     $rule = $_;
     last;
    }
   }
   $rule = [ undef, sub { $_[1] } ] unless $rule;
  }
  my $new_set = $rule->[1]->($self, $set, @_);
  my $is_new  = $new_set ne $set;

  my @new_mods;
MOD:
  for my $mod ($set->mods) {
   my $rule = $dispatch{ref($mod)};
   unless ($rule) {
    for (@mod_rules) {
     if ($_->[2] ? $mod->does($_->[0]) : $mod->isa($_->[0])) {
      $rule = $_;
      last;
     }
    }
    unless ($rule) {
     push @new_mods, $mod;
     next MOD;
    }
   }
   push @new_mods, $rule->[1]->($self, $mod, @_);
  }

  $new_set->mod(@new_mods) if $is_new;

  return $new_set;
 }, $class;
}

LaTeX::TikZ::Interface->register(
 functor => sub {
  shift;

  __PACKAGE__->new(rules => \@_);
 },
);

=head2 C<< default_rule $spec => $handler >>

Adds to all subsequently created functors a default rule for the class or role C<$spec>.

An exception is thrown if there is already a default rule for C<$spec> ; otherwise, the new rule is appended to the current list of rules.
But if C<$spec> begins with C<'+'>, the rule will replace I<all> default rules that apply to subclasses or subroles of C<$spec> (including C<$spec> itself).

Returns true if and only if an existent rule was replaced.

=cut

sub default_rule {
 shift;

 my $rule = $validate_rule->(@_);

 $insert_rule->(
  $rule,
  $rule->[4] ? \@default_set_rules : \@default_mod_rules,
  0,
 );
}

=head1 SEE ALSO

L<LaTeX::TikZ>.

=head1 AUTHOR

Vincent Pit, C<< <perl at profvince.com> >>, L<http://www.profvince.com>.

You can contact me by mail or on C<irc.perl.org> (vincent).

=head1 BUGS

Please report any bugs or feature requests to C<bug-latex-tikz at rt.cpan.org>, or through the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=LaTeX-TikZ>.
I will be notified, and then you'll automatically be notified of progress on your bug as I make changes.

=head1 SUPPORT

You can find documentation for this module with the perldoc command.

    perldoc LaTeX::TikZ

=head1 COPYRIGHT & LICENSE

Copyright 2010 Vincent Pit, all rights reserved.

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

=cut

1; # End of LaTeX::TikZ::Functor
