package SL::Controller::Helper::Sorted;

use strict;

use Exporter qw(import);
our @EXPORT = qw(make_sorted get_sort_spec get_current_sort_params _save_current_sort_params _get_models_handler_for_sorted _callback_handler_for_sorted);

use constant PRIV => '__sortedhelperpriv';

my $controller_sort_spec;

sub make_sorted {
  my ($class, %specs) = @_;

  $specs{MODEL} ||=  $class->_controller_name;
  $specs{MODEL}   =~ s{ ^ SL::DB:: (?: .* :: )? }{}x;

  while (my ($column, $spec) = each %specs) {
    next if $column =~ m/^[A-Z_]+$/;

    $spec = $specs{$column} = { title => $spec } if !ref $spec;

    $spec->{model}        ||= $specs{MODEL};
    $spec->{model_column} ||= $column;
  }

  my %model_sort_spec   = "SL::DB::Manager::$specs{MODEL}"->_sort_spec;
  $specs{DEFAULT_DIR}   = $specs{DEFAULT_DIR} ? 1 : defined($specs{DEFAULT_DIR}) ? $specs{DEFAULT_DIR} * 1 : $model_sort_spec{default}->[1];
  $specs{DEFAULT_BY}  ||= $model_sort_spec{default}->[0];
  $specs{FORM_PARAMS} ||= [ qw(sort_by sort_dir) ];
  $specs{ONLY}        ||= [];
  $specs{ONLY}          = [ $specs{ONLY} ] if !ref $specs{ONLY};

  $controller_sort_spec = \%specs;

  my %hook_params = @{ $specs{ONLY} } ? ( only => $specs{ONLY} ) : ();
  $class->run_before('_save_current_sort_params', %hook_params);

  SL::Controller::Helper::GetModels::register_get_models_handlers(
    $class,
    callback   => '_callback_handler_for_sorted',
    get_models => '_get_models_handler_for_sorted',
    ONLY       => $specs{ONLY},
  );

  # $::lxdebug->dump(0, "CONSPEC", \%specs);
}

sub get_sort_spec {
  my ($class_or_self) = @_;

  return $controller_sort_spec;
}

sub get_current_sort_params {
  my ($self, %params) = @_;

  my $sort_spec = $self->get_sort_spec;

  if (!$params{sort_by}) {
    my $priv          = $self->{PRIV()} || {};
    $params{sort_by}  = $priv->{by};
    $params{sort_dir} = $priv->{dir};
  }

  my $by          = $params{sort_by} || $sort_spec->{DEFAULT_BY};
  my %sort_params = (
    dir => defined($params{sort_dir}) ? $params{sort_dir} * 1 : $sort_spec->{DEFAULT_DIR},
    by  => $sort_spec->{$by} ? $by : $sort_spec->{DEFAULT_BY},
  );

  return %sort_params;
}

#
# private functions
#

sub _save_current_sort_params {
  my ($self)      = @_;

  my $sort_spec   = $self->get_sort_spec;
  $self->{PRIV()} = {
    by            =>   $::form->{ $sort_spec->{FORM_PARAMS}->[0] },
    dir           => !!$::form->{ $sort_spec->{FORM_PARAMS}->[1] } * 1,
  };

  # $::lxdebug->message(0, "saving current sort params to " . $self->{PRIV()}->{by} . ' / ' . $self->{PRIV()}->{dir});
}

sub _callback_handler_for_sorted {
  my ($self, %params) = @_;

  my $priv = $self->{PRIV()} || {};
  if ($priv->{by}) {
    my $sort_spec                             = $self->get_sort_spec;
    $params{ $sort_spec->{FORM_PARAMS}->[0] } = $priv->{by};
    $params{ $sort_spec->{FORM_PARAMS}->[1] } = $priv->{dir};
  }

  # $::lxdebug->dump(0, "CB handler for sorted; params nach modif:", \%params);

  return %params;
}

sub _get_models_handler_for_sorted {
  my ($self, %params) = @_;

  my %sort_params     = $self->get_current_sort_params;
  my $sort_spec       = $self->get_sort_spec->{ $sort_params{by} };

  $params{model}      = $sort_spec->{model};
  $params{sort_by}    = "SL::DB::Manager::$params{model}"->make_sort_string(sort_by => $sort_spec->{model_column}, sort_dir => $sort_params{dir});

  # $::lxdebug->dump(0, "GM handler for sorted; params nach modif:", \%params);

  return %params;
}

1;
__END__

=pod

=encoding utf8

=head1 NAME

SL::Controller::Helper::Sorted - A helper for semi-automatic handling
of sorting lists of database models in a controller

=head1 SYNOPSIS

In a controller:

  use SL::Controller::Helper::GetModels;
  use SL::Controller::Helper::Sorted;

  __PACKAGE__->make_sorted(
    DEFAULT_BY   => 'run_at',
    DEFAULT_DIR  => 1,
    MODEL        => 'BackgroundJobHistory',
    ONLY         => [ qw(list) ],

    error        => $::locale->text('Error'),
    package_name => $::locale->text('Package name'),
    run_at       => $::locale->text('Run at'),
  );

  sub action_list {
    my ($self) = @_;

    my $sorted_models = $self->get_models;
    $self->render('controller/list', ENTRIES => $sorted_models);
  }

In said template:

  [% USE L %]

  <table>
   <tr>
    <th>[% L.sortable_table_header('package_name') %]</th>
    <th>[% L.sortable_table_header('run_at') %]</th>
    <th>[% L.sortable_table_header('error') %]</th>
   </tr>

   [% FOREACH entry = ENTRIES %]
    <tr>
     <td>[% HTML.escape(entry.package_name) %]</td>
     <td>[% HTML.escape(entry.run_at) %]</td>
     <td>[% HTML.escape(entry.error) %]</td>
    </tr>
   [% END %]
  </table>

=head1 OVERVIEW

This specialized helper module enables controllers to display a
sortable list of database models with as few lines as possible.

For this to work the controller has to provide the information which
indexes are eligible for sorting etc. by a call to L<make_sorted> at
compile time.

The underlying functionality that enables the use of more than just
the sort helper is provided by the controller helper C<GetModels>. It
provides mechanisms for helpers like this one to hook into certain
calls made by the controller (C<get_callback> and C<get_models>) so
that the specialized helpers can inject their parameters into the
calls to e.g. C<SL::DB::Manager::SomeModel::get_all>.

A template on the other hand can use the method
C<sortable_table_header> from the layout helper module C<L>.

This module requires that the Rose model managers use their C<Sorted>
helper.

The C<Sorted> helper hooks into the controller call to the action via
a C<run_before> hook. This is done so that it can remember the sort
parameters that were used in the current view.

=head1 PACKAGE FUNCTIONS

=over 4

=item C<make_sorted %sort_spec>

This function must be called by a controller at compile time. It is
uesd to set the various parameters required for this helper to do its
magic.

There are two sorts of keys in the hash C<%sort_spec>. The first kind
is written in all upper-case. Those parameters are control
parameters. The second kind are all lower-case and represent indexes
that can be used for sorting (similar to database column names). The
second kind are also the indexes you use in a template when calling
C<[% L.sorted_table_header(...) %]>.

Control parameters include the following:

=over 4

=item * C<MODEL>

Optional. A string: the name of the Rose database model that is used
as a default in certain cases. If this parameter is missing then it is
derived from the controller's package (e.g. for the controller
C<SL::Controller::BackgroundJobHistory> the C<MODEL> would default to
C<BackgroundJobHistory>).

=item * C<DEFAULT_BY>

Optional. A string: the index to sort by if the user hasn't clicked on
any column yet (meaning: if the C<$::form> parameters for sorting do
not contain a valid index).

Defaults to the underlying database model's default sort column name.

=item * C<DEFAULT_DIR>

Optional. Default sort direction (ascending for trueish values,
descrending for falsish values).

Defaults to the underlying database model's default sort direction.

=item * C<FORM_PARAMS>

Optional. An array reference with exactly two strings that name the
indexes in C<$::form> in which the sort index (the first element in
the array) and sort direction (the second element in the array) are
stored.

Defaults to the values C<sort_by> and C<sort_dir> if missing.

=item * C<ONLY>

Optional. An array reference containing a list of action names for
which the sort parameters should be saved. If missing or empty then
all actions invoked on the controller are monitored.

=back

All keys that are written in all lower-case name indexes that can be
used for sorting. Each value to such a key can be either a string or a
hash reference containing certain elements. If the value is only a
string then such a hash reference is constructed, and the string is
used as the value for the C<title> key.

These possible elements are:

=over 4

=item * C<title>

Required. A user-displayable title to be used by functions like the
layout helper's C<sortable_table_header>. Does not have a default
value.

=item * C<model>

Optional. The name of a Rose database model this sort index refers
to. If missing then the value of C<$sort_spec{MODEL}> is used.

=item * C<model_column>

Optional. The name of the Rose database model column this sort index
refers to. It must be one of the columns named by the model's
C<Sorted> helper (not to be confused with the controller's C<Sorted>
helper!).

If missing it defaults to the key in C<%sort_spec> for which this hash
reference is the value.

=back

=back

=head1 INSTANCE FUNCTIONS

These functions are called on a controller instance.

=over 4

=item C<get_sort_spec>

Returns a hash containing the currently active sort parameters.

The key C<by> contains the active sort index referring to the
C<%sort_spec> given to L<make_sorted>.

The key C<dir> is either C<1> or C<0>.

=item C<get_current_sort_params>

Returns a hash reference to the sort spec structure given in the call
to L<make_sorted> after normalization (hash reference construction,
applying default parameters etc).

=back

=head1 BUGS

Nothing here yet.

=head1 AUTHOR

Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>

=cut
