The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
use strict;
use warnings;

package Jifty::Plugin::ActorMetadata::Mixin::Model::ActorMetadata;
use Jifty::DBI::Schema;
use base 'Jifty::DBI::Record::Plugin';

our @EXPORT = qw(current_user_can current_user_is_owner);
my %column_names;

=head1 NAME

Jifty::Plugin::ActorMetadata::Mixin::Model::ActorMetadata - ActorMetadata mixin

=head1 SYNOPSIS

  package MyApp::Model::CoffeeShop;
  use Jifty::DBI::Schema;
  use MyApp::Record schema {
      # custom column definitions
  };

  use Jifty::Plugin::ActorMetadata::Mixin::Model::ActorMetadata; # created_by, created_on, updated_on and updated_by

=head1 DESCRIPTION

=head1 SCHEMA

This mixin adds the following columns to the model schema:

=head2 created_by

=head2 created_on

=head2 updated_on

=head2 updated_by

=cut

# XXX: move this to somewhere
BEGIN {

Jifty::DBI::Schema->register_types(
    Date =>
        sub { type is 'date', input_filters are qw/Jifty::DBI::Filter::Date/ },
    Time =>
        sub { type is 'time', input_filters are qw/Jifty::DBI::Filter::Time/ },
    DateTime => sub {
        type is 'datetime',
        input_filters are qw/Jifty::DBI::Filter::DateTime/ },
    TimeStamp => sub {
        type is 'timestamp',
        filters are qw( Jifty::Filter::DateTime Jifty::DBI::Filter::DateTime),
    }
);
}

=head1 METHODS

# XXX: pod coverage should count parent classes.  this pod is useless

=head2 register_triggers

Adds the triggers to the model this mixin is added to.

=cut

sub register_triggers {
    my $self = shift;
    $self->add_trigger(name => 'before_create', callback => \&before_create);
}

=head2 register_triggers_for_column

=cut

sub register_triggers_for_column {
    my $self   = shift;
    my $column = shift;

    return
      unless $column_names{ ref $self || $self }{'updated_on'}
          && $column eq $column_names{ ref $self || $self }{'updated_on'}
          || $column_names{ ref $self || $self }{'updated_by'}
          && $column eq $column_names{ ref $self || $self }{'updated_by'};

    $self->add_trigger(name => 'after_set', callback => \&after_set);
    return 1;
}

=head2 before_create

Sets C<created_by>, C<created_on>, C<updated_on> and C<updated_by> based on the current user and time.

=cut

sub before_create {
    my $self = shift;
    my $args = shift;
    for my $by (qw/created_by updated_by/) {
        if ( $column_names{ ref $self || $self }{$by} ) {
            $args->{$column_names{ ref $self || $self }{$by}} ||= $self->current_user->id;
        }
    }

    for my $time ( qw/created_on updated_on/ ) {
        if ( $column_names{ ref $self || $self }{$time} ) {
            $args->{$column_names{ ref $self || $self }{$time}} ||= Jifty::DateTime->now;
        }
    }

    return 1;
}

=head2 after_set

update C<updated_on> and C<updated_by> based on the current user and current time.

=cut

sub after_set {
    my $self = shift;
    if ( $column_names{ ref $self || $self }{'updated_on'} ) {
        $self->__set(
            column => $column_names{ ref $self || $self }{'updated_on'},
            value  => Jifty::DateTime->now
        );
    }
    if ( $column_names{ ref $self || $self }{'updated_by'} ) {
        $self->__set(
            column => $column_names{ ref $self || $self }{'updated_by'},
            value  => $self->current_user->id
        );
    }

    return 1;
}

=head2 current_user_can

Rejects creation unless there's a current_user. 

=cut

# XXX: Move this to an abortable trigger

sub current_user_can {
    my $self = shift;
    my $action = shift;
    my %args = (@_);

    if ($action eq 'create') {
        return
          unless $self->current_user
            and $self->current_user->id
            || $self->current_user->is_bootstrap_user;
    }

#Rejects update or deletion unless the current_user is the creator.  (Jesse says: this feels like wrong logic for this mixin)
#    if ($action eq 'update' or $action eq 'delete') {
#        return undef unless $self->current_user_is_owner;
#    }

    return 1;
}

=head2 current_user_is_owner

=cut

sub current_user_is_owner {
    my $self = shift;

    my $created_by = $self->__value($column_names{ref $self || $self}{'created_by'});
    return unless $self->current_user && $created_by;

    return unless $self->current_user->id;

    return $self->current_user->id == $created_by;
}

=head2 import

to be more flexible, we allow some configurations like:
e.g.
use Jifty::Plugin::ActorMetadata::Mixin::Model::ActorMetadata 
    user_class => 'Foo::Model::Principal',
    map => { created_by => 'creator', created_on => 'created' }

current valid args are:
user_class => 'Foo::Model::User'  
        class that you want created_by and updated_by to be refers_to
map => { created_by => 'creator', ... }
        the real column name you want to use. this also controls whether
        a column will be added or not. i.e. if the hashref is 
        { created_by => 'creator', created_on => 'created' }, then columns
        'updated_by' and 'updated_on' will not be added.

=cut

sub import {
    my $self = shift;
    my %args = @_;
    my $user_class = $args{'user_class'} || Jifty->app_class('Model', 'User');

    my @columns = qw/created_on created_by updated_on updated_by/;
    my %map;

    # fiddle map
    if ( $args{'map'} && ref $args{'map'} eq 'HASH' ) {
        for my $column ( keys %{$args{'map'}} ) {
            $map{$column} = $args{'map'}{$column};
        }
    }
    else {
        @map{@columns} = @columns;
    }

    $column_names{scalar caller} = \%map;

    Jifty::DBI::Schema->import; # to import subs like schema, references
    my @ret = schema {
        if ( $map{'created_by'} ) {
            column $map{'created_by'} => references $user_class, 
                render_as 'hidden';
        }
        if ( $map{'created_on'} ) {
            column $map{'created_on'} => is TimeStamp,
                render_as 'hidden';
        }
        if ( $map{'updated_by'} ) {
            column $map{'updated_by'} => references $user_class,
                render_as 'hidden';
        }
        if ( $map{'updated_on'} ) {
            column $map{'updated_on'} => is TimeStamp,
                render_as 'hidden';
        }
    };
    require Jifty::Record;
    Jifty::Record->import( @ret );
    
# TODO
# below is the import sub from Jifty::DBI::Record::Plugin, 
# because of some caller stuff, I can't just call SUPER
# need to refactor, either here or Jifty::DBI::Record::Plugin
    my $caller = caller;

    for ($self->columns) {
        $caller->_init_methods_for_column($_);
        # virtual will be handled later
        $caller->COLUMNS->{$_->name} = $_ unless $_->virtual;
    }
    $self->export_to_level(1,undef);

    if (my $triggers =  $self->can('register_triggers') ) {
        $triggers->($caller)
    }

    if (my $triggers_for_column =  $self->can('register_triggers_for_column') ) {
        for my $column (keys %{$caller->_columns_hashref}) {
            $triggers_for_column->($caller, $column)
        }
    }
    push(@{ $caller->RECORD_MIXINS }, $self);
    $self->COLUMNS(undef); # reset columns for ActorMetadata.pm
}

1;