package Template::Plugin::Heritable;

use strict;
use warnings;

our $VERSION = "0.04";

use base qw(Template::Plugin);

=head1 NAME

Template::Plugin::Heritable - OO dispatching and inheritance for templates

=head1 SYNOPSIS

 [% USE Heritable %]

 [%# searches providers for a "view" template method on
     class (which should be a metamodel object, eg
     someobj.meta in Perl 6) %]
 [% Heritable.include(class, "view", { self = object }) %]

 [%# return list of paths it would look %]
 [% paths = Heritable.dispatch_paths(class, "view") %]

 [%# if you don't have the class of the object handy, then
     use 'invoke' instead %]
 [% Heritable.invoke(object, "method", { self = object } %]

 [%# call the next method in the inheritance tree from
     inside a template method %]
 [% next_template() %]

=head1 DESCRIPTION

C<Template::Plugin::Heritable> provides support for selecting an
appropriate template based on the class of an object.  It is also
possible to call the next template in the inheritance heirarchy/chain.

This provides a form of inheritance for template display.

The core of this is the I<template dispatch> mechanism, which deals in
terms of a suitable metamodel class.  The module currently deals in
the following metamodels;

=over 4

=item L<T2::Class>

T2 is a metamodel system for Tangram starting with L<Class::Tangram>.

=item L<Class::MOP>

Initial support for L<Class::MOP> classes.  Note that this is
currently only tested with L<Moose>; in particular it assumes
Moose-like type constraints.  If you want support for plain
Class::MOP, please send a test case.

=item L<mro>

Perl 5.10 introduces this module, allowing all classes to specify
their I<method resolution order>.  This only provides enough
information to call C<invoke> without an attribute name, for there is
still no attribute type information available in Perl 5.

=item L<DBIx::Class::ResultSet>

A ResultSet is something akin to a class, but missing exactly what
L<mro> isn't; it does not have inheritance.  The only thing that you
can inherit over is the classes that were used to construct your
model classes (ie, all the DBIx::Class internal classes).

There is something of a case that sometimes, a one-to-one relationship
is the same thing as an inheritance relationship.  However it is not
guaranteed that is a correct interpretation of a one to one
relationship; class dispatch does not use it, for instance.

Anyway, it doesn't really matter - this "metamodel" is treated just
the same as the L<mro> plugin when it comes to generating a list of
classes that go into the dispatch_paths list.  The only thing this
support does differently is know how to fetch the column types from
the ResultSet definition of the class.

=back

=head1 CONSTRUCTION

Basic use:

 [% USE Heritable %]

Specifying all options:

 [% USE Heritable({ prefix = "mypath",
                    suffix = ".tt",
                    class2path = somefunc,
                    class_attr2path = somefunc,
                    schema = myschema
                  }) %]

Here all dispatch paths returned by C<Heritable> will be prepended
with C<mypath/>.  Also, a custom method is specified to convert from
"C<Foo::Bar>"-style class names to a C<Template::Provider> path.

There is also a C<schema> object; this object is responsible for
converting objects to classes.  If you are using C<Class::MOP>, you
don't need to supply this; the metaclass is found via
C<$object-E<gt>meta>.

Normally, you wouldn't specify most of this this - and indeed there is
the issue there that this configuration information perhaps doesn't
belong every place you make a Heritable object dispatch.

For this reason, it is recommended that you have a single template for
object dispatching, and to pass through C<self> appropriately.

 [% PROCESS invoke
      object = SomeObject
      method = "foo"
 %]

the F<invoke> template might look like:

 [% USE Heritable({ suffix = ".tt"
                  });
    Heritable.include(object.meta, method, { self = object }) -%]

=cut

sub new {
    my $class = shift;
    my $context = shift;
    my $config = shift || {};
    bless({ context => $context,
	    config => $config },
	  (ref $class || $class));
}

=head1 METHODS

=head2 .dispatch_paths

=head2 .include

=head2 .invoke

 [% paths = Heritable.dispatch_paths( what, "name" ) %]

 [% Heritable.include( what, "name", { ... } ) %]
 [% Heritable.invoke( object, "name", { ... } ) %]

C<.dispatch_paths> returns a list of dispatch paths for C<what>.
C<what> is a metamodel object (see L<DESCRIPTION>).

C<.include> calls the first one that actually exists in the available
template providers.  It throws a (trappable) not found error if it was
not found.

C<.invoke> assumes that the metamodel object is either available as
C<object.meta> or via C<$schema-E<gt>class(ref $object)>.  Convenient
modules to make this Just Workâ„¢ with standard Perl 6
objects/classes are yet to be written, but for T2 and Class::MOP this
should work fine.

B<new in 0.03>: now supports 5.9.5+ 'mro' - if the symbol
C<&mro::get_linear_isa> is defined at runtime (for instance, you used
the L<mro> pragma or L<MRO::Compat> for earlier Perl versions), then
this will work.  However, C<mro> does not cover types of attributes,
so only C<invoke> with a method name (no attribute name) is currently
supported.

B<new in 0.03>: also supports DBIx::Class - pass
L<DBIx::Class::ResultSource> objects to C<.include>, and
L<DBIx::Class::Row> objects to C<.invoke>.

=head1 DISPATCH ALGORITHM

To figure out which template should be called to perform a function,
the class names are turned into L<Template::Provider> paths, with the
template to call ("C<view>" in the example in the synopsis) appended
to them.

For example, if the "class" object in the synopsis represents the
"Foo::Bar" class, which has superclass "Foo", the following locations
would be searched for a template (assuming you specified
C<TEMPLATE_EXTENSION = ".tt"> during your Template object
construction):

  foo/bar/view.tt
  foo/view.tt
  object/view.tt

It is also possible to dispatch based on attribute or association
types, by calling "attribute methods".  In this case, the dispatch
order also includes templates for the I<types> of the attribute or
association.

So, if you were using T2 classes and wrote:

  [% Heritable.include(class.attribute("baz"), "show") %]

Then the first of these templates found would be called (assuming
C<baz> is a property of the C<Foo> class, of type C<set>):

  foo/baz/show.tt
  object/baz/show.tt
  foo/types/set/show.tt
  object/types/set/show.tt

Note that C<foo/bar/baz/show.tt> was not searched for, even though
C<class> is actually C<Foo::Bar>.  If you wanted to do that, you
should use a 'multiple invocant' C<include>:

  [% Heritable.include([class, class.attribute("baz")],
                       "show", { ... }) %]

or simply

  [% Heritable.include([class, "baz"], "show", { ... }) %]

Either of these would then search for:

  foo/bar/baz/show.tt
  foo/baz/show.tt
  object/baz/show.tt
  foo/bar/types/set/show.tt
  foo/types/set/show.tt
  object/types/set/show.tt

Using Class::MOP, if an attribute's type is itself a type with an
inheritance chain, that those extra templates will also be added to
the list of checked template locations.

For instance, if you have two classes A and B, A having an attribute
"att" of type "Str", and you write:

  [% Heritable.invoke([ my_b, "att"], "show") %]

Then you get this dispatch path:

  b/att/show.tt
  a/att/show.tt
  moose/object/att/show.tt
  object/att/show.tt

  b/types/str/show.tt
  a/types/str/show.tt
  moose/object/types/str/show.tt
  object/types/str/show.tt

  b/types/value/show.tt
  a/types/value/show.tt
  moose/object/types/value/show.tt
  object/types/value/show.tt

  b/types/defined/show.tt
  a/types/defined/show.tt
  moose/object/types/defined/show.tt
  object/types/defined/show.tt

  b/types/item/show.tt
  a/types/item/show.tt
  moose/object/types/item/show.tt
  object/types/item/show.tt

B<New in Template::Heritable 0.03>: for convenience, the standard
method for converting classes to "paths" can be customised, for
instance if deep directory structures is inconvenient, and you like to
be able to combine blocks into a single file, you can use:

 [% USE Heritable({ path_delim = "_",
                    use_blocks = 1    }) %]

This would make the first part of the above dispatch list look like
this:

  b_att_show.tt
  a_att_show.tt
  moose_object_att_show.tt
  object_att_show.tt
  ...

Not only that, but as C<use_blocks> is set, if by some strange
co-incidence the module can find similarly named blocks, it will just
call those instead;

  [% b_att_show = BLOCK %]
    Here we show some B attributes.  But we don't want to
    miss out on showing the [% next_template() %]
  [% END %]
  [% a_att_show = BLOCK -%]
    A attributes.
  [% END %]

With the above block definitions in scope, calling

  [% Heritable.invoke([ my_b, "att"], "show") %]

Would print:

  Here we show some B attributes.  But we don't want to
  miss out on showing the A attributes.

And calling:

  [% Heritable.invoke([ my_a, "att"], "show") %]

Would print:

  A attributes

=cut

use Scalar::Util qw(blessed);
use Carp qw(carp croak confess);

sub _find_attribute {
    my $class = shift;
    my $attribute = shift;

    if ( $class->can("class_precedence_list") ) {
	for my $super ( $class->class_precedence_list ) {
	    if ( my $att = $super->meta->get_attribute($attribute) ) {
		 return $att;
	    }
	}
	return undef;
    }
    elsif ( $class->can("column_info") ) {
	return $class->column_info($attribute);
    }
    else {
	$class->get_attribute($attribute) ||
	    $class->get_association($attribute)
    }
}

sub dispatch_paths {
    my $self = shift;
    my $thingy = shift;
    my $method = shift;

    my ($class, $property);
    if ( ref $thingy and ref $thingy eq "ARRAY" ) {
	($class, $property) = @$thingy;
	if ( !blessed $property ) {
	    my $t_property = _find_attribute($class, $property)
		or croak("class ".$class->name." has no property ".
			 "'$property'");
	    if ( !blessed $t_property ) {
		$t_property->{name} = $property;
	    }
	    my $schema;
	    if ( !$self->ltrim and
		 (eval { $schema = $class->schema; 1 })) {
		$self->ltrim($schema);
	    }
	    $property = $t_property;
	}
    }
    elsif ( !blessed $thingy ) {
	if ( defined &mro::get_linear_isa ) {
	    $class = $thingy;
	}
	else {
	    croak("'$thingy' is not even blessed, how can I dispatch?");
	}
    }
    elsif ( $thingy->can("class_precedence_list") ) {
	$class = $thingy;
    }
    elsif ( $thingy->can("meta") ) {
	$class = $thingy->meta;
    }
    elsif ( $thingy->can("result_class") ) {
	#$class = $thingy->result_class;
	$class = $thingy;
	my $schema;
	if ( !$self->ltrim and
	     (eval { $schema = $thingy->schema; 1 })) {
	    $self->ltrim($schema);
	}
    }
    elsif ( $thingy->can("get_subclasses") ) {
	$class = $thingy;
    }
    elsif ( $thingy->can("get_class") ) {
	$class = $thingy->get_class;
	$property = $thingy;
    }

    if ( $property ) {
	return $self->_attr_dispatch_paths($class, $property, $method);
    } else {
	return $self->_class_dispatch_paths($class, $method);
    }
}

sub prefix {
    my $self = shift;
    if ( @_ ) {
	$self->{config}{prefix} = shift;
    } else {
	my $prefix = $self->{config}{prefix} || "";
	$prefix =~ s{([^/])$}{$1/};
	return $prefix;
    }
}

sub ltrim {
    my $self = shift;
    if ( @_ ) {
	$self->{config}{ltrim} = shift;
    } else {
	my $ltrim = $self->{config}{ltrim} || "";
	return $ltrim;
    }
}

sub tt_ext {
    my $self = shift;
    $self->{context}{TEMPLATE_EXTENSION}||"";
}

sub _class_supers {
    my $self = shift;
    my $class = shift or confess "no class passed to _class_supers";

    if ( blessed $class and $class->can("result_class") ) {
	$class = $class->result_class;
    }

    if ( !ref $class ) {
	return @{ mro::get_linear_isa($class) };
    }

    # get superclasses
    my @class_order;
    return map { $_->meta } $class->class_precedence_list
	if $class->can("class_precedence_list");
    @class_order = $class;
    my $head = $class;
    while ( $head = $head->get_superclass ) {
	push @class_order, $head;
    }

    return @class_order;
}

sub class2path {
    my $self = shift;
    return $self->{class2path} ||= do {
	my $prefix = $self->prefix;
	my $ltrim = $self->ltrim;
	sub {
	    ($prefix.
	     ($self->{config}{class2path} || sub {
		  my $class = shift;
		  if ( $ltrim ) {
		      $class =~ s{^(?:$ltrim)::}{};
		  }
		  $class =~ s{::}{/}g;
		  $prefix.lc($class);
	      })->(@_));
	};
    };
}

sub class_attr2path {
    my $self = shift;
    return $self->{class_attr2path} ||= do {
	my $prefix = $self->prefix;
	my $ltrim = $self->ltrim;
	sub {
	    ($prefix.
	     ($self->{config}{class_attr2path} || sub {
		  my $class = shift;
		  if ( $ltrim ) {
		      $class =~ s{^(?:$ltrim)::}{};
		  }
		  $class =~ s{::}{/}g;
		  (my $what = shift) =~ s{::}{/}g;
		  my $is_type = shift;
		  $prefix.lc($class)."/".($is_type?"types/":"").lc($what);
	      })->(@_));
	};
    };
}

sub _class_dispatch_paths {
    my $self = shift;
    my $class = shift;
    my $method = shift;

    my @supers = $self->_class_supers($class);
    my $make_path = $self->class2path;
    my $tt = $self->tt_ext;

    return ( ( map {( $make_path->($_)."/$method$tt" )}
	       (map { ref $_ ? $_->name : $_ } @supers), "object" ),
	   );
}

sub _attr_dispatch_paths {
    my $self = shift;
    my $class = shift;
    my $attribute = shift;
    my $method = shift;

    my @supers = $self->_class_supers($class);

    my $make_path = $self->class_attr2path;

    my $att_name = blessed $attribute ? $attribute->name : $attribute->{name};
    my ($type, @extra_types);
    if ( blessed $attribute and
	 $attribute->can("has_type_constraint") ) {
	if ( $attribute->has_type_constraint ) {
	    my $tc = $attribute->type_constraint;
	    # there is some de-facto dispatch ordering logic happening
	    # here
	    my %seen;
	    my $push;
	    $push = sub {
		my $type = shift;
		return if $seen{$type}++;
		push @extra_types, $type;
		if ( UNIVERSAL::can($type, "meta") ) {
		    $push->($_) for
			map { $_->meta->name }
			    $type->meta->class_precedence_list;
		}
	    };
	    do {
		$push->($tc->name);
	    } while ( $tc = $tc->parent );
	    $type = shift @extra_types;
	} else {
	    # hmm, everything has a type constraint really
	    $type = "Item";
	}
    } elsif ( blessed $attribute) {
	$type = $attribute->get_type;
    }
    else {
	$type = $attribute->{data_type};
    }
    my $tt = $self->tt_ext;

    my @paths = ( map {( $make_path->($_, $att_name)
			 ."/$method$tt" )}
		  (map { ref $_ ? $_->name : $_ } @supers), "object"
		);

    while ( defined $type ) {
	push @paths, ( map {( $make_path->($_, $type, 1)
			      ."/$method$tt" )}
		       (map { ref $_ ? $_->name : $_ } @supers), "object"
		     );
	$type = shift @extra_types;
    }

    @paths;
}

sub dispatch {
    my $self = shift;
    my @paths = $self->dispatch_paths(@_);

    for my $path ( @paths ) {
	if ( $self->{context}->template($path) ) {
	    return $path;
	}
    }
}

sub include {
    my $self = shift;
    my $invocant = shift;
    my $method = shift;
    my $vars = shift || {};

    my @paths = $self->dispatch_paths($invocant, $method);

    $self->_include_next($method, \@paths, @_);
}

sub invoke {
    my $self = shift;
    my $invocant = shift;

    my ($object, $property);
    if ( ref $invocant and ref $invocant eq "ARRAY" ) {
	($object, $property) = @$invocant;
    } elsif ( !blessed $invocant ) {
	croak("Can't invoke on '$invocant'");
    } else {
	$object = $invocant;
    }

    my $class;
    if ( $object->can("meta") ) {
	$class = $object->meta;
    }
    elsif ( $object->can("result_source_instance") ) {
	$class = $object->result_source_instance;
    } elsif ( my $schema = $self->{config}{schema} ) {
	$class = $schema->class(ref $object);
    } else {
	$class = ref $object;
    }

    return $self->include( ($property
			    ? [ $class, $property ]
			    : $class ), @_ );

}

=head1 DEFINED VARIABLES

=head2 next_template

These methods let you find the I<next> template to display in the
inheritance chain.

  The next template is [% next_template %]

  [% next_template.include({ ... }) %]

Note that if there is no next template you will get a nasty error.

=cut

sub _include_next {
    my $self = shift;
    my $method = shift;
    my @paths = @{(shift)};
    my $vars = shift || {};
    my $t;

    my $i = 0;
    do {
	$t = undef;
	eval {
	    $t = $self->{context}->template($paths[$i]);
	};
	$i++;
    } while ( $paths[$i] and !$t );

    $self->{context}->throw
	("Couldn't find next template method $method (tried: @paths), "
	 ."called from ".$self->{context}->stash->{component}->{name})
	    unless $t;

    @paths = @paths[$i..$#paths];

    $vars->{next_template} = @paths ? (sub {
	$self->_include_next($method, \@paths, @_);
    }) : sub {
	$self->{context}->throw("tried to call next_method from last "
				."link in inheritance chain");
    };

    my $output = $self->{context}->include($t, $vars);

    return $output;
}

1;

__END__

=head1 SEE ALSO

L<T2>, L<Template>, L<Class::MOP>, L<mro>, L<DBIx::Class::ResultSet>

=head1 AUTHOR

Sam Vilain, <samv@cpan.org>

=head1 LICENSE

Copyright (c) 2005-2007, Catalyst IT (NZ) Ltd.  This program is free
software; you may use it and/or redistribute it under the same terms
as Perl itself.

=head1 CHANGELOG

=over

=item 0.04, 19 Nov 2007

Minor release engineering fixes based on CPAN tester FAIL reports.

=item 0.03, 19 Nov 2007

Added support for L<mro>, L<DBIx::Class::ResultSet> APIs.

=item 0.02, 25 May 2006

Add support for C<Class::MOP>, though only C<Moose> classes are
currently tested; new test cases welcome.

=back

=cut