package SL::File;

use strict;

use parent qw(Rose::Object);

use SL::File::Backend;
use SL::File::Object;
use SL::DB;
use SL::DB::History;
use SL::DB::ShopImage;
use SL::DB::File;
use SL::Helper::UserPreferences;
use SL::Controller::Helper::ThumbnailCreator qw(file_probe_type);
use SL::JSON;

use constant RENAME_OK          => 0;
use constant RENAME_EXISTS      => 1;
use constant RENAME_NOFILE      => 2;
use constant RENAME_SAME        => 3;
use constant RENAME_NEW_VERSION => 4;

sub get {
  my ($self, %params) = @_;
  die "no id or dbfile" unless $params{id} || $params{dbfile};
  $params{dbfile} = SL::DB::Manager::File->get_first(query => [id => $params{id}]) if !$params{dbfile};
  die 'not found' unless $params{dbfile};
  $main::lxdebug->message(LXDebug->DEBUG2(), "object_id=".$params{dbfile}->object_id." object_type=".$params{dbfile}->object_type." dbfile=".$params{dbfile});
  SL::File::Object->new(db_file => $params{dbfile}, id => $params{dbfile}->id, loaded => 1);
}

sub get_version_count {
  my ($self, %params) = @_;
  die "no id or dbfile" unless $params{id} || $params{dbfile};
  $params{dbfile} = SL::DB::Manager::File->get_first(query => [id => $params{id}]) if !$params{dbfile};
  die 'not found' unless $params{dbfile};
  my $backend = $self->_get_backend($params{dbfile}->backend);
  return $backend->get_version_count(%params);
}

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

  my @files;
  return @files unless $params{object_type};
  return @files unless defined($params{object_id});

  my @query = (
    object_id   => $params{object_id},
    object_type => $params{object_type}
  );
  push @query, (file_name     => $params{file_name})     if $params{file_name};
  push @query, (file_type     => $params{file_type})     if $params{file_type};
  push @query, (mime_type     => $params{mime_type})     if $params{mime_type};
  push @query, (source        => $params{source})        if $params{source};
  push @query, (print_variant => $params{print_variant}) if $params{print_variant};
  push @query, (uid           => $params{uid})           if $params{uid};

  my $sortby = $params{sort_by} || 'itime DESC,file_name ASC';

  @files = @{ SL::DB::Manager::File->get_all(query => [@query], sort_by => $sortby) };
  map { SL::File::Object->new(db_file => $_, id => $_->id, loaded => 1) } @files;
}

sub get_all_versions {
  my ($self, %params) = @_;
  my @versionobjs;
  my @fileobjs;
  if ( $params{dbfile} ) {
    push @fileobjs, SL::File::Object->new(db_file => $params{dbfile}, id => $params{dbfile}->id, loaded => 1);
  } else {
    @fileobjs = $self->get_all(%params);
  }
  foreach my $fileobj (@fileobjs) {
    $main::lxdebug->message(LXDebug->DEBUG2(), "obj=" . $fileobj . " id=" . $fileobj->id." versions=".$fileobj->version_count);
    my $maxversion = $fileobj->version_count;
    $fileobj->version($maxversion);
    push @versionobjs, $fileobj;
    if ($maxversion > 1) {
      for my $version (2..$maxversion) {
        $main::lxdebug->message(LXDebug->DEBUG2(), "clone for version=".($maxversion-$version+1));
        eval {
          my $clone = $fileobj->clone;
          $clone->version($maxversion-$version+1);
          $clone->newest(0);
          $main::lxdebug->message(LXDebug->DEBUG2(), "clone version=".$clone->version." mtime=". $clone->mtime);
          push @versionobjs, $clone;
          1;
        } or do {$::lxdebug->message(LXDebug::WARN(), "clone for version=".($maxversion-$version+1) . "failed: " . $@)};
      }
    }
  }
  return @versionobjs;
}

sub get_all_count {
  my ($self, %params) = @_;
  return 0 unless $params{object_type};

  my @query = (
    object_id   => $params{object_id},
    object_type => $params{object_type}
  );
  push @query, (file_name     => $params{file_name})     if $params{file_name};
  push @query, (file_type     => $params{file_type})     if $params{file_type};
  push @query, (mime_type     => $params{mime_type})     if $params{mime_type};
  push @query, (source        => $params{source})        if $params{source};
  push @query, (print_variant => $params{print_variant}) if $params{print_variant};
  push @query, (uid           => $params{uid})           if $params{uid};

  my $cnt = SL::DB::Manager::File->get_all_count(query => [@query]);
  return $cnt;
}

sub delete_all {
  my ($self, %params) = @_;
  return 0 unless defined($params{object_id}) || $params{object_type};
  my $files = SL::DB::Manager::File->get_all(
    query => [
      object_id   => $params{object_id},
      object_type => $params{object_type}
    ]
  );
  foreach my $file (@{$files}) {
    $params{dbfile} = $file;
    $self->delete(%params);
  }
}

sub delete {
  my ($self, %params) = @_;
  die "no id or dbfile in delete" unless $params{id} || $params{dbfile};
  my $rc = 0;
  SL::DB->client->with_transaction(sub {
    $rc = $self->_delete(%params);
    1;
  }) or do { die SL::DB->client->error };
  return $rc;
}

sub _delete {
  my ($self, %params) = @_;
  $params{dbfile} = SL::DB::Manager::File->get_first(query => [id => $params{id}]) if !$params{dbfile};

  my $backend = $self->_get_backend($params{dbfile}->backend);
  if ( $params{dbfile}->file_type eq 'document' && $params{dbfile}->source ne 'created')
  {
    ## must unimport
    my $hist = SL::DB::Manager::History->get_first(
      where => [
        addition  => 'IMPORT',
        trans_id  => $params{dbfile}->object_id,
        what_done => $params{dbfile}->id
      ]
    );

    if ($hist) {
      if (!$main::auth->assert('import_ar | import_ap', 1)) {
        die 'no permission to unimport';
      }
      my $file = $backend->get_filepath(dbfile => $params{dbfile});
      $main::lxdebug->message(LXDebug->DEBUG2(), "del file=" . $file . " to=" . $hist->snumbers);
      File::Copy::copy($file, $hist->snumbers) if $file;
      $hist->addition('UNIMPORT');
      $hist->save;
    }
  }
  if ($backend->delete(%params)) {
    my $do_delete = 0;
    if ( $params{last} || $params{version} || $params{all_but_notlast} ) {
      if ( $backend->get_version_count(%params) > 0 ) {
        $params{dbfile}->mtime(DateTime->now_local);
        $params{dbfile}->save;
      } else {
        $do_delete = 1;
      }
    } else {
      $do_delete = 1;
    }
    $params{dbfile}->delete if $do_delete;
    return 1;
  }
  return 0;
}

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

  my $obj;
  SL::DB->client->with_transaction(sub {
    $obj = $self->_save(%params);
    1;
  }) or do { die SL::DB->client->error };
  return $obj;
}

sub _save {
  my ($self, %params) = @_;
  my $file = $params{dbfile};
  my $exists = 0;

  if ($params{id}) {
    $file = SL::DB::File->new(id => $params{id})->load;
    die 'dbfile not exists'     unless $file;
  } elsif (!$file) {
  $main::lxdebug->message(LXDebug->DEBUG2(), "obj_id=" .$params{object_id});
    die 'no object type set'    unless $params{object_type};
    die 'no object id set'      unless defined($params{object_id});

    $exists = $self->get_all_count(%params);
    die 'filename still exist' if $exists && $params{fail_if_exists};
    if ($exists) {
      my ($obj1) = $self->get_all(%params);
      $file = $obj1->db_file;
    } else {
      $file = SL::DB::File->new();
      $file->assign_attributes(
        object_id      => $params{object_id},
        object_type    => $params{object_type},
        source         => $params{source},
        file_type      => $params{file_type},
        file_name      => $params{file_name},
        mime_type      => $params{mime_type},
        title          => $params{title},
        description    => $params{description},
        print_variant  => $params{print_variant},
        uid            => $params{uid},
      );
      $file->itime($params{mtime})    if $params{mtime};
      $params{itime} = $params{mtime} if $params{mtime};
    }
  } else {
    $exists = 1;
  }
  if ($exists) {
    #change attr on existing file
    $file->file_name  ($params{file_name})   if $params{file_name};
    $file->mime_type  ($params{mime_type})   if $params{mime_type};
    $file->title      ($params{title})       if $params{title};
    $file->description($params{description}) if $params{description};
  }
  if ( !$file->backend ) {
    $file->backend($self->_get_backend_by_file_type($file));
    # load itime for new file
    $file->save->load;
  }

  $file->mtime(DateTime->now_local) unless $params{mtime};
  $file->mtime($params{mtime}     ) if     $params{mtime};

  my $backend = $self->_get_backend($file->backend);
  $params{dbfile} = $file;
  $backend->save(%params);

  $file->save;
  #ShopImage
  if($file->object_type eq "shop_image"){
    my $image_content = $params{file_contents};
    my $thumbnail = file_probe_type($image_content);
    my $shopimage = SL::DB::ShopImage->new();
    $shopimage->assign_attributes(
                                  file_id                => $file->id,
                                  thumbnail_content      => $thumbnail->{thumbnail_img_content},
                                  org_file_height        => $thumbnail->{file_image_height},
                                  org_file_width         => $thumbnail->{file_image_width},
                                  thumbnail_content_type => $thumbnail->{thumbnail_img_content_type},
                                  object_id              => $file->object_id,
                                 );
    $shopimage->save;
  }
  if ($params{file_type} eq 'document' && $params{source} ne 'created') {
    SL::DB::History->new(
      addition    => 'IMPORT',
      trans_id    => $params{object_id},
      snumbers    => $params{file_path},
      employee_id => SL::DB::Manager::Employee->current->id,
      what_done   => $params{dbfile}->id
    )->save();
  }
  return $params{obj} if $params{dbfile} && $params{obj};
  return SL::File::Object->new(db_file => $file, id => $file->id, loaded => 1);
}

sub rename {
  my ($self, %params) = @_;
  return RENAME_NOFILE unless $params{id} || $params{dbfile};
  my $file = $params{dbfile};
  $file = SL::DB::Manager::File->get_first(query => [id => $params{id}]) if !$file;
  return RENAME_NOFILE unless $file;

  $main::lxdebug->message(LXDebug->DEBUG2(), "rename id=" . $file->id . " to=" . $params{to});
  if ($params{to}) {
    return RENAME_SAME   if $params{to} eq $file->file_name;
    return RENAME_EXISTS if $self->get_all_count( object_id     => $file->object_id,
                                                  object_type   => $file->object_type,
                                                  mime_type     => $file->mime_type,
                                                  source        => $file->source,
                                                  file_type     => $file->file_type,
                                                  uid           => $file->uid,
                                                  file_name     => $params{to}
                                                ) > 0;

    my $backend = $self->_get_backend($file->backend);
    $backend->rename(dbfile => $file) if $backend;
    $file->file_name($params{to});
    $file->save;
  }
  return RENAME_OK;
}

sub get_backend_class {
  my ($self, $backendname) = @_;
  die "no backend name set" unless $backendname;
  $self->_get_backend($backendname);
}

sub get_other_sources {
  my ($self) = @_;
  my $pref = SL::Helper::UserPreferences->new(namespace => 'file_sources');
  $pref->login("#default#");
  my @sources;
  foreach my $tuple (@{ $pref->get_all() }) {
    my %lkeys  = %{ SL::JSON::from_json($tuple->{value}) };
    my $source = {
      'name'        => $tuple->{key},
      'description' => $lkeys{desc},
      'directory'   => $lkeys{dir}
    };
    push @sources, $source;
  }
  return @sources;
}

sub sync_from_backend {
  my ($self, %params) = @_;
  return unless $params{file_type};
  my $file = SL::DB::File->new;
  $file->file_type($params{file_type});
  my $backend = $self->_get_backend($self->_get_backend_by_file_type($file));
  return unless $backend;
  $backend->sync_from_backend(%params);
}

#
# internal
#
sub _get_backend {
  my ($self, $backend_name) = @_;
  my $class = 'SL::File::Backend::' . $backend_name;
  my $obj   = undef;
  die $::locale->text('no backend enabled') if $backend_name eq 'None';
  eval {
    eval "require $class";
    $obj = $class->new;
    die $::locale->text('backend "#1" not enabled',$backend_name) unless $obj->enabled;
    1;
  } or do {
    if ( $obj ) {
      die $@;
    } else {
      die $::locale->text('backend "#1" not found',$backend_name);
    }
  };
  return $obj;
}

sub _get_backend_by_file_type {
  my ($self, $dbfile) = @_;

  $main::lxdebug->message(LXDebug->DEBUG2(), "_get_backend_by_file_type=" .$dbfile." type=".$dbfile->file_type);
  return "Filesystem" unless $dbfile;
  return $::instance_conf->get_doc_storage_for_documents   if $dbfile->file_type eq 'document';
  return $::instance_conf->get_doc_storage_for_attachments if $dbfile->file_type eq 'attachment';
  return $::instance_conf->get_doc_storage_for_images      if $dbfile->file_type eq 'image';
  return "Filesystem";
}

1;

__END__

=pod

=encoding utf8

=head1 NAME

SL::File - The intermediate Layer for handling files

=head1 SYNOPSIS

  # In a controller or helper ( see SL::Controller::File or SL::Helper::File )
  # you can create, remove, delete etc. a file in a backend independent way

  my $file  = SL::File->save(
                     object_id     => $self->object_id,
                     object_type   => $self->object_type,
                     mime_type     => 'application/pdf',
                     file_type     => 'document',
                     file_contents => 'this is no pdf');

  my $file1  = SL::File->get(id => $id);
  SL::File->delete(id => $id);
  SL::File->delete(dbfile => $file1);
  SL::File->delete_all(object_id   => $object_id,
                       object_type => $object_type,
                       file_type   => $filetype      # may be optional
                      );
  SL::File->rename(id => $id,to => $newname);
  my $files1 = SL::File->get_all(object_id   => $object_id,
                                 object_type => $object_type,
                                 file_type   => 'image',   # may be optional
                                 source      => 'uploaded' # may be optional
                                );

  # Alternativelly some operation can be done with the filemangement object wrapper
  # and additional oparations see L<SL::File::Object>

=head1 OVERVIEW

The Filemanagemt can handle files in a storage independent way. Internal the File
use the configured storage backend for the type of file.
These backends must be configured in L<SL::Controller::ClientConfig> or an extra database table.

There are three types of files:

=over 2

=item - document,

which can be generated files (for sales), scanned files or uploaded files (for purchase) for an ERP-object.
They can exist in different versions. The versioning is handled implicit. All versions of a file may be
deleted by the user if she/he is allowed to do this.

=item - attachment,

which have additional information for an ERP-objects. They are uploadable. If a filename still exists
on a ERP-Object the new uploaded file is a new version of this or it must be renamed by user.

There are generic attachments for a specific document group (like sales_invoices). This attachments can be
combinide/merged with the document-file in the time of printing.
Today only PDF-Attachmnets can be merged with the generated document-PDF.

=item - image,

they are like attachments, but they may be have thumbnails for displaying.
So the must have an image format like png,jpg. The versioning is like attachments

=back

For each type of files the backend can configured in L<SL::Controller::ClientConfig>.

The files have also the parameter C<Source>:

=over 2

=item - created, generated by LaTeX

=item - uploaded

=item - scanner, import from scanner

( or scanner1, scanner2 if there are different scanner, be configurable via UserPreferences )

=item - email, received by email and imported by hand or automatic.

=back

The files from source 'scanner' or 'email' are not allowed to delete else they must be send back to the sources.
This means they are moved back into the correspondent source directories.

The scanner and email import must be configured  via Table UserPreferences:

=begin text

 id |  login  |  namespace   | version |   key    |                        value
----+---------+--------------+---------+----------+------------------------------------------------------
  1 | default | file_sources | 0.00000 | scanner1 | {"dir":"/var/tmp/scanner1","desc":"Scanner Einkauf" }
  2 | default | file_sources | 0.00000 | emails   | {"dir":"/var/tmp/emails"  ,"desc":"Empfangene Mails"}

=end text

.

The Fileinformation is stored in the table L<SL::DB::File> for saving the information.
The modul and object_id describe the link to the object.

The interface SL::File:Object encapsulate SL::DB:File, see L<SL::DB::Object>

The storage backends are extra classes which depends from L<SL::File::Backend>.
So additional backend classes can be added.

The implementation of versioning is done in the different backends.

=head1 METHODS

=over 4

=item C<save>

Creates a new SL::DB:File object or save an existing object for a specific backend depends of the C<file_type>
and config, like

=begin text

          SL::File->save(
                         object_id    => $self->object_id,
                         object_type  => $self->object_type,
                         content_type => 'application/pdf'
                        );

=end text

.

The file data is stored in the backend. If the file_type is "document" and the source is not "created" the file is imported,
so in the history the import is documented also as a hint to can unimport the file later.

Available C<PARAMS>:

=over 4

=item C<id>

The id of SL::DB::File for an existing file

=item C<object_id>

The Id of the ERP-object for a new file.

=item C<object_type>

The Type of the ERP-object like "sales_quotation" for a new file. A clear mapping to the class/model exists in the controller.

=item C<file_type>

The type may be "document", "attachment" or "image" for a new file.

=item C<source>

The type may be "created", "uploaded" or email sources or scanner sources for a new file.

=item C<file_name>

The file_name of the file for a new file. This name is used in the WebGUI and as name for download.

=item C<mime_type>

The mime_type of a new file. This is used for downloading or for email attachments.

=item C<description> or C<title>

The description or title of a new file. This must be discussed if this attribute is needed.

=back

=item C<delete PARAMS>

The file data is deleted in the backend. If the file comes from source 'scanner' or 'email'
they moved back to the source folders. This is documented in the history.

Available C<PARAMS>:

=over 4

=item C<id>

The id of SL::DB::File

=item C<dbfile>

As alternative if the SL::DB::File as object is available.

=back

=item C<delete_all PARAMS>

All file data of an ERP-object is deleted in the backend.

=over 4

=item C<object_id>

The Id of the ERP-object.

=item C<object_type>

The Type of the ERP-object like "sales_quotation". A clear mapping to the class/model exists in the controller.

=back

=item C<rename PARAMS>

The Filename of the file is changed

Available C<PARAMS>:

=over 4

=item C<id>

The id of SL::DB::File

=item C<to>

The new filename

=back

=item C<get PARAMS>

The actual file object is retrieved. The id of the object is needed.

Available C<PARAMS>:

=over 4

=item C<id>

The id of SL::DB::File

=back

=item C<get_all PARAMS>

All last versions of file data objects which are related to an ERP-Document,Part,Customer,Vendor,... are retrieved.

Available C<PARAMS>:

=over 4

=item C<object_id>

The Id of the ERP-object.

=item C<object_type>

The Type of the ERP-object like "sales_quotation". A clear mapping to the class/model exists in the controller.

=item C<file_type>

The type may be "document", "attachment" or "image". This parameter is optional.

=item C<file_name>

The name of the file . This parameter is optional.

=item C<mime_type>

The MIME type of the file . This parameter is optional.

=item C<source>

The type may be "created", "uploaded" or email sources or scanner soureces. This parameter is optional.

=item C<sort_by>

An optional parameter in which sorting the files are retrieved. Default is decrementing itime and ascending filename

=back

=item C<get_all_versions PARAMS>

All versions of file data objects which are related to an ERP-Document,Part,Customer,Vendor,... are retrieved.
If only the versions of one file are wanted, additional parameter like file_name must be set.
If the param C<dbfile> set, only the versions of this file are returned.

Available C<PARAMS> ar the same as L<get_all>



=item C<get_all_count PARAMS>

The count of available files is returned.
Available C<PARAMS> ar the same as L<get_all>


=item C<get_content PARAMS>

The data of a file can retrieved. A reference to the data is returned.

Available C<PARAMS>:

=over 4

=item C<id>

The id of SL::DB::File

=item C<dbfile>

If no Id exists the object SL::DB::File as param.

=back

=item C<get_file_path PARAMS>

Sometimes it is more useful to have a path to the file not the contents. If the backend has not stored the content as file
it is in the responsibility of the backend to create a tempory session file.

Available C<PARAMS>:

=over 4

=item C<id>

The id of SL::DB::File

=item C<dbfile>

If no Id exists the object SL::DB::File as param.

=back

=item C<get_other_sources>

A helpful method to get the sources for scanner and email from UserPreferences. This method is need from SL::Controller::File

=item C<sync_from_backend>

For Backends which may be changed outside of kivitendo a synchronization of the database is done.
This sync must be triggered by a periodical task.

Needed C<PARAMS>:

=over 4

=item C<file_type>

The synchronization is done file_type by file_type.

=back

=back

=head1 AUTHOR

Martin Helmling E<lt>martin.helmling@opendynamic.deE<gt>

=cut
