package PDF::Imposition::Schema;
use strict;
use warnings;

use File::Basename qw/fileparse/;
use File::Spec;
use CAM::PDF;
use PDF::API2;
use File::Temp ();
use File::Copy;
use POSIX ();

=head1 NAME

PDF::Imposition::Schema - Base class for the imposition schemas.

=head1 SYNOPSIS

Please don't use this class directly, but use L<PDF::Imposition> or
the right schema class, which inherit from this (which in turns
defines the shared methods). B<This class does not do anything useful
by itself, but only provides some shared methods>.

    use PDF::Imposition;
    my $imposer = PDF::Imposition->new(file => "test.pdf",
                                       # either use 
                                       outfile => "out.pdf",
                                       # or suffix
                                       suffix => "-2up"
                                      );
    $imposer->impose;
or 

    use PDF::Imposition;
    my $imposer = PDF::Imposition->new();
    $imposer->file("test.pdf");
    
    $imposer->outfile("out.pdf");
    # or
    $imposer->suffix("-imp");

    $imposer->impose;
  
=cut





=head1 METHODS

=head2 Constructor 

=head3 new(file => "file.pdf", suffix => "-imp", cover => 0, [...])

Costructor. Options should be passed as list. The options are the same
of the read-write accessors describe below, so passing
C<< $self->file("file.pdf") >> is exactly the same of passing
C<< $self->new(file => "file.pdf") >>.

=cut

sub new {
    my ($class, %options) = @_;
    foreach my $k (keys %options) {
        # clean the options from internals
        delete $options{$k} if index($k, "_") == 0;
    }
    my $self = \%options;
    bless $self, $class;
}

=head2 Read/write accessors

All the following accessors accept an argument, which sets the
value.

=head3 file

Unsurprisingly, the input file, which must exist.

=cut

sub file {
    my $self = shift;
    if (@_ == 1) {
        $self->{file} = shift;
    }
    my $f = $self->{file} || "";
    die "$f is not a file" unless -f $f;
    return $f;
}

sub _tmp_dir {
    my $self = shift;
    unless ($self->{_tmp_dir}) {
        $self->{_tmp_dir} = File::Temp->newdir(CLEANUP => 1);
    }
    return $self->{_tmp_dir};
}


=head3 outfile

The destination file of the imposition. You may prefer to use the
suffix method below, which takes care of the filename.

=head3 suffix

The suffix of the file. By default, '-imp', so test.pdf imposed will
be saved as 'test-imp.pdf'. If test-imp.pdf already exists, it will be
replaced merciless.

=cut

sub outfile {
    my $self = shift;
    if (@_ == 1) {
        $self->{outfile} = shift;
    }
    my $f = $self->{outfile};
    unless ($f) {
        my ($name, $path, $suffix) = fileparse($self->file, qr{\.pdf}i);
        die $self->file . " has a suffix not recognized" unless $suffix;
        $f = File::Spec->catfile($path, $name . $self->suffix . $suffix);
        $self->{outfile} = $f;
    }
    return $f;
}

sub suffix {
    my $self = shift;
    if (@_ == 1) {
        $self->{suffix} = shift;
    }
    return $self->{suffix} || "-imp";
}


=head3 cover

This option is ignored for 2x4x2 and 2side schemas. Often it happens
that we want the last page of the pdf to be the last one on the
physical booklet after folding. If C<cover> is set to a true value,
the last page of the logical pdf will be placed on the last page of
the last signature.

Individual schema implementations are in charge to check and act on this setting.

Es.

  $imposer->cover(1);

=cut

sub cover {
    my $self = shift;
    if (@_ == 1) {
        $self->{cover} = shift;
    }
    return $self->{cover};
}



=head2 Internal accessors

The following methods are used internally but documented for schema's
authors.

L<CAM::PDF> is used to get the properties, and L<PDF::API2> to arrange
the pages. L<CAM::PDF> is also used to convert PDF 1.6-1.5 to PDF
v1.4, which it's the only version L<PDF::API2> understands.

=head3 dimensions

Returns an hashref with the original pdf dimensions in points.

  { w => 800, h => 600 }

=head3 orig_width

=head3 orig_height

=head3 total_pages

Returns the number of pages


=cut

sub _populate_orig {
    my $self = shift;
    my $pdf = CAM::PDF->new($self->file);
    my ($x, $y, $w, $h) = $pdf->getPageDimensions(1); # use the first page
    warn $self->file . "use x-y offset, cannot proceed safely" if ($x + $y);
    die "Cannot retrieve paper dimensions" unless $w && $h;
    $self->{_dimensions} = { w => sprintf('%.2f', $w),
                             h => sprintf('%.2f', $h) };
    $self->{_total_orig_pages} = $pdf->numPages;
    undef $pdf;
}

sub dimensions {
    my $self = shift;
    unless ($self->{_dimensions}) {
        $self->_populate_orig;
    }
    # return a copy
    return { %{$self->{_dimensions}} };
}

sub total_pages {
    my $self = shift;
    unless ($self->{_total_orig_pages}) {
        $self->_populate_orig;
    }
    return $self->{_total_orig_pages};
}

sub orig_width {
    return shift->dimensions->{w};
}

sub orig_height {
    return shift->dimensions->{h};
}



=head3 in_pdf_obj

Internal usage. It's the PDF::API2 object used as source.

=head3 out_pdf_obj

Internal usage. The PDF::API2 object used as output.

=cut

sub in_pdf_obj {
    my $self = shift;
    unless ($self->{_input_pdf_obj}) {
        my ($basename, $path, $suff) = fileparse($self->file, qr{\.pdf}i);
        my $tmpfile = File::Spec->catfile($self->_tmp_dir,
                                          $basename . $suff);
        copy($self->file, $tmpfile) or die "copy to $tmpfile failed $!";
        my $input;
        eval {
            $input = PDF::API2->open($tmpfile);
        };
        if ($@) {
            # dirty trick to get a pdf 1.4
            my $src = CAM::PDF->new($tmpfile);
            my $tmpfile_copy =
              File::Spec->catfile($self->_tmp_dir,
                                  $basename . "-v14" . $suff);
            $src->cleansave();
            $src->output($tmpfile_copy);
            undef $src;
            $input = PDF::API2->open($tmpfile_copy);
        }
        die "Missing input" unless $input;
        $self->{_input_pdf_obj} = $input;
    }
    return $self->{_input_pdf_obj};
}

sub out_pdf_obj {
    my $self = shift;
    unless ($self->{_output_pdf_obj}) {
        my $pdf = PDF::API2->new();
        my ($basename, $path, $suff) = fileparse($self->file, qr{\.pdf}i);
        $pdf->info(
                   Creator => 'PDF::Imposition',
                   Producer => 'PDF::API2',
                   Title => $basename,
                   CreationDate => $self->_orig_file_timestamp,
                   ModDate => $self->_now_timestamp,
                  );
        $self->{_output_pdf_obj} = $pdf;
    }
    return $self->{_output_pdf_obj};
}

sub _cleanup_objs {
    my $self = shift;
    foreach my $f (qw/_output_pdf_obj _input_pdf_obj/) {
        delete $self->{$f};
    }
}

=head3 get_imported_page($pagenumber)

Retrieve the page form object from the input pdf to the output pdf,
and return it. The method return undef if the page is out of range.

=cut


sub get_imported_page {
    my ($self, $page) = @_;
    if ((!defined $page) || ($page <= 0) || ($page > $self->total_pages)) {
        return undef;
    }
    return  $self->out_pdf_obj->importPageIntoForm($self->in_pdf_obj, $page)
}

=head3 impose

Do the job and leave the output in C<< $self->outfile >>, cleaning up
the internal objects.

=cut

sub impose {
    my $self = shift;
    my $out = $self->_do_impose;
    $self->in_pdf_obj->end;
    $self->out_pdf_obj->end;
    $self->_cleanup_objs;
    return $out;
}

sub _orig_file_timestamp {
    my $self = shift;
    my $mtime = (stat($self->file))[9];
    return $self->_format_timestamp($mtime);
}

sub _now_timestamp {
    return shift->_format_timestamp(time());
}

sub _format_timestamp {
    my ($self, $epoc) = @_;
    return POSIX::strftime(q{%Y%m%d%H%M%S+00'00'}, localtime($epoc));
}

1;

=head1 SEE ALSO

L<PDF::Imposition>

=cut