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

use strict;
use parent 'SL::Controller::Helper::GetModels::Base';

use Carp;
use List::MoreUtils qw(uniq);

use Rose::Object::MakeMethods::Generic (
  scalar => [ qw(by dir specs form_data) ],
  'scalar --get_set_init' => [ qw(form_params) ],
);

sub init {
  my ($self, %specs) = @_;

  $self->set_get_models(delete $specs{get_models});
  my %model_sort_spec   = $self->get_models->manager->_sort_spec;

  if (my $default = delete $specs{_default}) {
    $self->by ($default->{by});
    $self->dir($default->{dir});
  } else {
    $self->by ($model_sort_spec{default}[0]);
    $self->dir($model_sort_spec{default}[1]);
  }

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

    $spec = $specs{$column} = { title => $spec } if (ref($spec) || '') ne 'HASH';

    $spec->{model}        ||= $self->get_models->model;
    $spec->{model_column} ||= $column;
  }
  $self->specs(\%specs);

  $self->get_models->register_handlers(
    callback   => sub { shift; $self->_callback_handler_for_sorted(@_) },
    get_models => sub { shift; $self->_get_models_handler_for_sorted(@_) },
  );

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

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

  return %{ $self->form_data } if $self->form_data;

  my %sort_params;
  my ($by, $dir) = @{ $self->form_params };
  my $source = $self->get_models->source;

  if ($source->{ $by }) {
    %sort_params = (
      sort_by  => $source->{$by},
      sort_dir => defined($source->{$dir}) ? $source->{$dir} * 1 : undef,
    );
  } elsif (!$self->by) {
    %sort_params = %params;
  } else {
    %sort_params = (
      sort_by  => $self->by,
      sort_dir => $self->dir,
    );
  }

  $self->form_data(\%sort_params);

  return %sort_params;
}

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

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

  $params{$_} or croak("Missing parameter '$_'") for qw(report sortable_columns);

  my %current_sort_params = $self->read_params;

  foreach my $col (@{ $params{sortable_columns} }) {
    $params{report}->{columns}->{$col}->{link} = $self->get_models->get_callback(
      sort_by  => $col,
      sort_dir => ($current_sort_params{sort_by} eq $col ? 1 - $current_sort_params{sort_dir} : $current_sort_params{sort_dir}),
    );
  }

  $params{report}->set_sort_indicator($current_sort_params{sort_by}, 1 - $current_sort_params{sort_dir});

  if ($params{report}->{export}) {
    $params{report}->{export}->{variable_list} = [ uniq(
      @{ $params{report}->{export}->{variable_list} },
      @{ $self->form_params }
    )];
  }
}

#
# private functions
#

sub _callback_handler_for_sorted {
  my ($self, %params) = @_;
  my %spec = $self->read_params;

  if ($spec{sort_by}) {
    $params{ $self->form_params->[0] } = $spec{sort_by};
    $params{ $self->form_params->[1] } = $spec{sort_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->read_params;
  my $sort_spec       = $self->specs->{ $sort_params{sort_by} };

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

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

  return %params;
}


sub init_form_params {
  [ qw(sort_by sort_dir) ]
}

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.

Note that this string must be the untranslated English version of the
string. The titles will be translated whenever they're requested.

=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).

=item C<set_report_generator_sort_options %params>

This function does three things with an instance of
L<SL::ReportGenerator>:

=over 4

=item 1. it sets the sort indicator,

=item 2. it sets the the links for those column headers that are
sortable and

=item 3. it adds the C<FORM_PARAMS> fields to the list of variables in
the report generator's export options.

=back

The report generator instance must be passed as the parameter
C<report>. The parameter C<sortable_columns> must be an array
reference of column names that are sortable.

The report generator instance must already have its columns and export
options set via calls to its L<SL::ReportGenerator::set_columns> and
L<SL::ReportGenerator::set_export_options> functions.

=back

=head1 BUGS

Nothing here yet.

=head1 AUTHOR

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

=cut
