#
# Copyright (c) 2004-2007 - Consultas, PKG.fr
#
# This file is part of A2P.
#
# A2P is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# A2P is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with A2P; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
#
# $Id: afpds2tex.pm 3 2007-10-18 16:20:19Z root $
#

package AFPDS::afpds2tex;

use strict ;
use Encode 'from_to' ;
use Time::HiRes qw( gettimeofday tv_interval usleep );
use File::stat ;
use A2P::Globals ;
use A2P::Syslog ;
use A2P::Common ;
use A2P::Com    qw( GetCom comINF comREQ comFILE );
use A2P::Tools  qw( debugbuffer compute mychomp myreadline ) ;
use AFPDS::Flux ;
use AFPDS::ControlRecord ;
use A2P::XML ;
use A2P::JobStatus 'a2pjobstate' ;

BEGIN {
    our $VERSION = sprintf "%s", q$Rev: 1023 $ =~ /([0-9.]+)\s+/ ;
}
our $VERSION ;

# Files handles & generation flag
my ( $TexFile, $ArchFile, $AfpFile, $LCKNAME, $AFPDSFILE );
my $GenFlag = 0 ;
my $ErrFlag = 0 ;
my $AfpName = "";
my $Job     = "";
my $self    = 0 ;
my $file    = 0 ;
my $mtime   = 0 ;

# @skip will be used to continue an afp job when an error occurs on a sub job
# It should be reset to () at each new job
my @skip = () ;

my $RateTimer = [] ;
my $afpbuffer = "" ;

my $min_conv_time = 0 ;

sub _job_tag {
    return $Job ? "$Job".($file?"-$file":"").": " : ""
}

# Function to open TeX file and his shell environment file for writing
sub OpenFiles {
    my $filename = $_[1] ;

    open($TexFile, ">$filename.tex")
        or &Abort(107, "$filename.tex",  "$!"), return 0;

    $LCKNAME = "$filename.tex.LCK" ;
    open(LCK, ">$LCKNAME")
        or &Abort(107, $LCKNAME,  "$!"), return 0;

    open($ArchFile, ">$filename.arch")
        or &Abort(107, "$filename.arch", "$!"), return 0;

    return 1 unless ( $PARALLELE_VALIDATION or $KEEP_AFPJOB );

    open($AfpFile,  ">$filename.afp")
        or &Abort(107, "$filename.afp" , "$!"), return 0;

    return 1 ;
}

# Function to close TeX file and his shell environment file
sub CloseFiles {
    close($TexFile)  if (defined($TexFile));
    close($ArchFile) if (defined($ArchFile));
    close($AfpFile)  if (defined($AfpFile));
    if (defined($LCKNAME) and -e $LCKNAME) {
        close(LCK);
        unlink $LCKNAME ;
    }
    $GenFlag = 0 ;
}

# Arguments:
# 1 - The texname to process
# 2 - AFPDS file to process
sub ConvertFile {
       $self      = shift ;
    my $texbase   = shift ;
    my $afpdsfile = shift ;
       $Job       = shift ; # Current job name for queueing announcement
    my $startpos  = shift || 0 ; # The start position in AFP file for splitting

    # Forget any previous status
    $ErrFlag = 0 ;
    @skip = () ;

    # Same computation as in SpoolManager module
    ( $AfpName ) = $afpdsfile =~ $AFPNAME_REGEX ;
    &Debug("Afpname set to '$AfpName' from Texbase '$afpdsfile'");

    # First job state update in spool
    &a2pjobstate( $Job , 2 , '.', { JOBID => $Job } )
         or &Info("Can't initialize job status at step 2 with JOBID $Job");

    $afpdsfile = $AFPSPOOL . '/' . $afpdsfile
        unless ( -e $afpdsfile );

    # Provided texbase must not be an existing folder
    return &Abort( 119, $texbase) if ( -d $texbase );

    # Create texbase as folder
    mkdir $texbase , 0775
        or return &Abort( 106, $texbase, "$!");

    # Here, texbase will be a folder to delete, so inform JobManager of it
    $self->Return( $Job , $texbase );

    # Opening files for reading
    open($AFPDSFILE, "<", "$afpdsfile")
        or return &Abort( 107, $afpdsfile, "$!");

    # Read AFPDS files in binary mode
    binmode($AFPDSFILE)
        or return &Abort( 108, $afpdsfile, "$!");

    &Debug("Starting AFPDS conversion of $afpdsfile to $texbase folder");

    ############################################################################
    # Check if records are terminated by CR-LF:
    # search for pattern 0x0D0A in the last 10 chars of afpds file
    ############################################################################
    my ( $readmode , $buffer , $EOF , $inXML ) = ( 0 , "" , 0 , 0 ) ;
    {
        my ( $EOL , $offset ) = ( 0 , 9 );

        # 1. Check file contents greater than 7 chars, or this is probably
        #    a corrupted file
        seek $AFPDSFILE , 0 , 2 ;
        return &Abort(124, $afpdsfile) if ( tell($AFPDSFILE) <= 7 );

        # 2. Load the last 10 chars in the buffer
        seek $AFPDSFILE , -10 , 2 ;
        return &Abort(109, $afpdsfile, "$!")
            if ( read($AFPDSFILE , $buffer , 10) != 10);

        # 3. Scan loaded buffer for 0x0D0A string starting from offset 8
        #    in the buffer
        while ( $offset-- > 0 ) {
            $EOL = &compute( \$buffer , $offset , 2 );
            $readmode += 2 , last if ( $EOL == 0x0D0A );
        }

        &Debug(sprintf(
            "Readmode is $readmode as found CRLF=0x%04x at offset %d",
            $EOL,$offset));

        # Keep End of File information
        $EOF = tell($AFPDSFILE);
    }

    # Get file time creation
    my $stat = stat($afpdsfile);
    $mtime = $stat->mtime() ;

    ############################################################################
    # Rewind file to startpos (beginning by default)
    seek $AFPDSFILE , $startpos , 0 ;

    ############################################################################
    #
    # Read the AFPDS file
    #
    ############################################################################
    my ( $long , $char , $cctrl , $tmp , $maxrepeat );
    $file = 1 ;
    $afpbuffer = "" ;

    # Timer to get a chrono on the current conversion
    $RateTimer = [ &gettimeofday() ] ;

    # Set minimum conversion time (used for big jobs to limit convertion rate)
    $min_conv_time = $USLEEP ;
    # MAX_CONV_RATE is a maximum sub-job number to generate by seconds
    $min_conv_time = 1_000_000 / $MAX_CONV_RATE
        if ($MAX_CONV_RATE);

    # Set chrono to return progression status when required
    my @StartTime = &gettimeofday();
    my $tv_timing = $CONV_STATUS_TIMING ;

    $buffer = getc( $AFPDSFILE ) ;
    while ( defined( $buffer ) and ! eof($AFPDSFILE) ) {

        # Update afpbuffer
        $afpbuffer .= $buffer ;

        #  Manage the 0x5A char
        if ( ord( $buffer ) == 0x5A and ! $inXML ) {

            #  Read the field length
            read $AFPDSFILE , $buffer , 8 ;
            $long = &compute( \$buffer , 0 , 2 ) ;

            # Update afpbuffer
            $afpbuffer .= $buffer ;

            # Keep preamble for debugging
            my $preamble = $buffer ;

            # Abort if field is too short
            &Abort(114,$long) if ( $long < 8 );

            # Compute other attributes

            # cf doc IBM AFP - programming guide and line data ref - 54438842
            my $ident    = &compute( \$buffer , 2 , 3 ) ;
            my $flag     = &compute( \$buffer , 5 , 1 ) ;
            my $reserved = &compute( \$buffer , 6 , 2 ) ;

            # In case file is line delimited, then $readmode = 2, so we
            # need to check and found 2 more chars to be CRLF
            $long -= 8 - $readmode ;

            &Debug(sprintf("Long is %d, Ident is 0x%x and Flag is 0x%x",
                $long, $ident, $flag ));

            if ( $long > 0 ) {
                # Read the value of $ident
                read $AFPDSFILE , $buffer , $long ;

                # Update afpbuffer
                $afpbuffer .= $buffer ;

                # Some lines are full filled with spaces...
                if ( $readmode > 0 ) {
                    # The value should end with 0x0D0A,
                    # else read until end of line...
                    $tmp = &compute( \$buffer , -2 , 2 );
                    if ( $tmp != 0x0D0A ) {

                        # Read till the real end of line
                        my $plus = &myreadline( \*$AFPDSFILE , \$buffer );

                        # Update afpbuffer
                        $afpbuffer .= substr($buffer,-$plus) if $plus ;

                        $long += $plus ;

                        # Definitive check
                        $tmp = &compute( \$buffer , -2 , 2 );
                         if ( $tmp != 0x0D0A ) {
                            &debugbuffer( $preamble . $buffer );
                             &Abort(110,$tmp);
                        }
                    }

                    $long -= &mychomp( \$buffer, 'keep spaces' ) ;
                }

                # Check we read as byte as we need
                if ( length($buffer) != $long ) {
                    &debugbuffer($preamble . $buffer);
                    &Abort(111,length($buffer),$long);
                }

            } else {
                # Reset the buffer if nothing has been needed to read
                $buffer = "" ;
            }

            if (defined($self->{AFPDS})) {
                &debugbuffer( $preamble . $buffer );

                my @params = ( $ident, $flag, $reserved, $long, $buffer,
                    $texbase );
                # Don't try to analyse 5A record if @skip is not empty
                &Abort( 200, $self->{AFPDS}->geterror() )
                    if ( ! @skip and $self->{AFPDS}->got5Achar(@params) < 0 );

            } else {
                # Rise abort if we got 0x5A AFP record without AFPDS Flux
                # Object initialization
                &Abort(3);
            }

            # Check got5Achar didn't rise an error
            &Abort( 118, $ident, $flag, $reserved, $long,
                join('',map { sprintf("%X",$_) } split(//,$buffer) ))
                    unless (defined($self->{AFPDS}));

        ########################################################################
        ##  Manage the 0x7B char '#' for C930 control record (or RC)          ##
        ########################################################################
        } elsif ( ord( $buffer ) == 0x7B or $inXML ) {

            my ( $begin , $Type ) = ( $buffer , "" );
            read( $AFPDSFILE , $Type , 3 ) ;

            # And check this is really a numbered RC
            read( $AFPDSFILE , $buffer , 1 ) ;

            # Update begin to later update afpbuffer
            my $rawtype = $Type . $buffer ;

            # Must found another '#' as format validation if not in XML reading
            &Abort(112,$Type)
                unless ( $inXML or ord( $buffer ) == 0x7B );

            &from_to( $Type , $FROM_CONVERT , $TO_CONVERT ) if ($DO_CONVERT);

            my $afpds = $self->{AFPDS} ;

            # Generate a print if still after the first print analysis
            if ( ! $inXML and $GenFlag
            and ( $Type eq '001' or $Type eq '100' )) {
                if (defined($afpds)) {
                    &Debug("File#${file}: Start TeX file generation");

                    # Output this TeX file or rise an ABTERM
                    unless ( @skip or &generateTeX ) {
                        &Abort(103, $file, defined($self->{RC}->{$Type}) ?
                            $self->{RC}->{$Type}->getoutfile()
                            :
                            "with undefined '$Type' RC"
                            );
                    }

                    # Increment file index
                    $file ++ ;

                } else {
                    # Abort if AFPDS object has vanished
                    &Abort(4) ;
                }
            }

            # Initialise a new AFPDS flux only on record Type 001 or 100
            if (! $inXML and ( $Type eq '001' or $Type eq '100' )) {
                &UPSTAT('GETNEWJOB');

                # Reset skipping array to try another document
                if (@skip) {
                    # Starting new job after ABTERM
                    &Abort(500,$file); # Warn we are trying to start a new job
                    @skip = () ;
                }

                # Destroy last afpds object if needed and create new one
                delete $self->{AFPDS} if (defined($afpds));
                undef $afpds ;

                &Debug("Initializing AFPDS objects");
                $afpds = new AFPDS::Flux( $afpdsfile , $self , $Job );

                # And don't forget to keep a copy
                $self->{AFPDS} = $afpds ;
            }

            # Update job index in flux in case it has changed
            $afpds->setjobindex( $file );

            # Still check AFPDS Flux object exists
            &Abort(117,$Type) unless (defined($afpds));

            my $RC ;
            my ( $tA , $tB ) = ( ord($begin) == 0x7B , ord($buffer) == 0x7B ) ;

            # Continue last not sized RC unless it is the stop one
            if ( $inXML and
                (( $inXML eq '200' and ! ( $Type eq '201' and $tA and $tB )) or
                ( $inXML eq '201' and ! ( $Type eq '202'  and $tA and $tB ))))
            {
                &Debug("In XML $inXML: Type would be '" . ( $tA ? '#' : '.' ) .
                    $Type . ( $tB ? '#' : '.' ) . "'");

                # Still reading an XML, seek to beginning of line to avoid
                # missing an EOL if the begin string
                seek $AFPDSFILE, -5 , 1 ;

                # Type has been saved in inXML
                $Type = $inXML ;

                # Get last created RC
                $RC = $self->{RC}->{$Type} ;

            } else {
                # Update afpbuffer
                $afpbuffer .= $rawtype ;

                # Destroy last control record if needed and create new one
                delete $self->{RC}->{$Type} if (defined($self->{RC}->{$Type}));

                # Get new RC object
                $RC = new AFPDS::ControlRecord( $Type,$texbase,$AfpName,$file );

                &Debug("Found a control record type '$Type'");
            }

            # Check object creation
            &Abort(115, $file, $Type , $texbase ) unless (defined($RC));

            $buffer = "" ;

            # Initialization should return the length of the record control
            # Else it should return 0 to stop reading as record type is unknown
            $tmp = defined($RC) ? $RC->get_required_size() + $readmode : 0 ;
            &Abort(113,$tmp,$Type) unless ($tmp);

            # Read now ControlRecord and analyse it
            if ( $tmp > 0 and read( $AFPDSFILE, $buffer, $tmp ) == $tmp ) {

                &debugbuffer($buffer);
                &Abort(300,$Type,$RC->geterror())
                    if ( defined($RC) and $RC->init( $buffer ) < 0 );

                # Check initialization is done
                &Abort( 116, $Type, sprintf( "%02X" x length($buffer),
                    map { ord } split( //, $buffer ) ) )
                        unless (defined($RC));

                # Set job_tag for this RC library
                $RC->job_tag($Job);

                # Check if we have reached a afpds splitting event
                if ( ! @skip and $RC->can_split_job() ) {
                    # Compute the pos in file for the next job to convert
                    my $pos = tell($AFPDSFILE) - length($afpbuffer)
                         - length($buffer);

                    # Get a tag
                    my @split_tags = $RC->get_split_tags ;

                    # Set this job is the last of a split event
                    $afpds->updateEnv( {
                        # Set SPLIT_FILE with first position in file and length
                        SPLIT_FILE  => $startpos . ':' . $pos,
                        SPLIT_TAG   => $split_tags[0],
                        SPLIT_TAG2  => $split_tags[1]
                        } );

                    # Skip the rest to leave as a splitting event
                    push @skip, 'splitted' ;

                    # Finish that job
                    last ;
                }

                if ( ! @skip and ($Type eq '001' or $Type eq '100')) {

                    my $pagedef = $RC->getpagedef() ;

                    &UPSTAT('USEPAGEDEF_' . $pagedef );

                    # Load the perl Library for the PAGEDEF only on record
                    # Type 001 or 100
                    my $loadlib = "perllib::p1" . lc( $pagedef );

                    # Abort if we won't be able to load perl module
                    if ($pagedef) {
                        &Abort(104,$pagedef,$loadlib)
                            unless ( grep {
                                -s $_ . '/perllib/p1' . lc( $pagedef ) . '.pm'
                            } @INC );

                    } else {
                        &Abort(6);
                    }

                    unless (@skip) {
                        &Debug("Loading library with '$loadlib' evaluation");
                        eval "use " . $loadlib ;

                        $afpds->setpagedef(   $pagedef );
                        $afpds->setpclcond(   $RC->ispcloutput() );
                        $afpds->setformdef(   $RC->getformdef()  );
                        $afpds->setoutfile(   $RC->getoutfile()  );
                        $afpds->setTRCforTeX( $RC->getTRCforTeX());

                        &Debug("Perl library loaded");
                    }

                    # Close previous file handles if it's not the first job
                    $self->CloseFiles if ($GenFlag);
                    $GenFlag = $self->OpenFiles( $RC->getoutfile() );
                }

                unless (@skip) {
                    # Env is updated in Flux and will be returned to JobManager
                    # when completly analysed
                    $afpds->updateEnv( $RC->getenv() );

                    # Update ArchFile with RECORD content when necessary
                    local $\ = "\n" ;
                    print $ArchFile $RC->getRecord
                        if (defined($ArchFile) and $RC->getRecord());

                    # Keep a record copy in our RC hash
                    $self->{RC}->{$Type} = $RC ;
                }

            # Handle not sized Control Record type 200-202
            } elsif ($tmp < 0 and $Type =~ /^20[0-2]$/) {

                my $line = "" ;
                # Read till the end of line
                &myreadline( \*$AFPDSFILE , \$line );

                # Update afpbuffer (first char always added at loop begining)
                $afpbuffer .= substr($line,($inXML and $Type eq '201')?1:0) ;

                &mychomp( \$line );

                # Check record
                if ( $Type eq '202' ) {
                    # Stopping an input

                    # Forget any information just behind RC 202 string
                    if ($line !~ /^$/) {
                        &from_to( $line , $FROM_CONVERT , $TO_CONVERT )
                            if ($DO_CONVERT);
                        &Info(_job_tag."RC 202 tagged with '$line'");
                    }

                    # Check last record 100 to set the PDF name and service flag
                    &Abort(120, $Type , 100)
                        unless (defined($self->{RC}->{100}));

                    # Check last record 200 to get the working folder
                    &Abort(120, $Type , 200)
                        unless (defined($self->{RC}->{200}));

                    # Check base directory availability for pdf file
                    my $base = @skip ? undef : $self->{RC}->{100}->getbase ;
                    &Abort(122, $base)
                        unless ( defined($base) and $base and -d $base );

                    my $outbase = $base . '/' . $LOCKID . '_' . $Job ;
                    my $env = {
                        'PDFFILE'     => $outbase . "-$file.pdf",
                        'XML_A2P'     => $outbase . "_a2p-$file.xml",
                        'XML_CONTENT' => $outbase . "_service-$file.xml"
                    } ;

                    # Update a2p xml attributs before saving
                    my @attribut = ( pdf => $env->{PDFFILE} );
                    unless (@skip) {
                        $self->{RC}->{200}->set_a2p_attribut(@attribut)
                            or &Abort(123 , @attribut);
                    }

                    @attribut = ( file => $env->{XML_CONTENT} );
                    unless (@skip) {
                        $self->{RC}->{200}->set_a2p_attribut(@attribut)
                            or &Abort(123 , @attribut);
                    }

                    # Save A2P xml
                    unless (@skip) {
                        $self->{RC}->{200}->save_a2pxml_file($env->{XML_A2P})
                            or &Abort(121 , 200, $env->{XML_A2P});
                    }

                    # Check last record 201 to save the content
                    &Abort(120, $Type , 201)
                        unless (defined($self->{RC}->{201}));

                    # Save RC 201 content
                    unless (@skip) {
                        my $content = $env->{XML_CONTENT} ;
                        $self->{RC}->{201}->save_a2pxml_file($content)
                            or &Abort( 121 , 201, $content );
                    }

                    # Tag Flux as e-service and not arch
                    &Debug("Updating env to handle content and a2p xml files");
                    $afpds->updateEnv( $env )
                        unless (@skip);

                    &Debug("Service is ready to handle E-Service request");
                    # Release inXML flag
                    $inXML = 0 ;

                } elsif ( $Type eq '201' ) {
                    if ($inXML) {

                        &from_to( $line , $FROM_CONVERT , $TO_CONVERT )
                            if ($CONVERT_CONTENT_FILE and $DO_CONVERT);

                        # Add EOL to line
                        $line .= "\n" ;

                        # Add the complete line to content
                        $RC->addcontentref( \$line ) unless (@skip);
                        &Debug("Added '$line' to content file");

                    } else {
                        # Forget any information just behind RC 201 string
                        if ($line !~ /^$/) {
                            &from_to( $line , $FROM_CONVERT , $TO_CONVERT )
                                if ($DO_CONVERT);
                            &Info(_job_tag."RC 201 tagged with '$line'");
                        }

                        # Flag we are inXML handling
                        $inXML = $Type ;
                    }

                } elsif ( $Type eq '200' ) {
                    # Check last record 100 to get the working folder
                    &Abort(120, $Type , 100)
                        unless (defined($self->{RC}->{100}));

                    unless (@skip) {
                        my $basefile = $self->{RC}->{100}->getoutfile() ;

                        # Update
                        $line = $begin . $line if $inXML ;
                        &from_to( $line , $FROM_CONVERT , $TO_CONVERT )
                            if ($DO_CONVERT);

                        $RC->seta2pxml( $line , $basefile );

                        # Tag Flux as e-service
                        unless ($inXML) {
                            &Debug("Updating env with expected values");
                            $afpds->updateEnv( {
                                DO_ESERVICE => 'yes',
                                DO_PDF      => 'yes',
                            } );
                        }

                        # Flag we are inXML handling if not complete and valid
                        $inXML = $RC->a2pxml_is_valid() || $Type ;

                        # Remark: service name '200' is an error as prohibited
                        #         (checked in XML.pm)
                        if ($inXML and $inXML ne $Type) {
                            &Debug("Updating env with requested service");
                            $afpds->updateEnv( {
                                E_SERVICE => $inXML
                            } );

                            # Release inXML flag
                            $inXML = 0 ;
                        }
                    }
                }

                # Keep a record copy in our RC hash
                $self->{RC}->{$Type} = $RC unless (@skip);

            } else {
                # Bad length read for that record
                &debugbuffer($buffer);
                &Abort(102,$Type);
            }

            # Update afpbuffer
            $afpbuffer .= $buffer ;

        # End of ControlRecord management

        } else {
            # This is a standard line, first char is the control char
            $cctrl = $buffer ;
            &from_to( $cctrl , $FROM_CONVERT , $TO_CONVERT ) if ($DO_CONVERT);
            &Debug("Found standard line with control char <$cctrl>");

            # The next char is used to select TRC font
            read( $AFPDSFILE , $char , 1 );

            # Update afpbuffer
            $afpbuffer .= $char ;

            # Check AFPDS Flux is defined
            if (defined($self->{AFPDS})) {

                # ErFo001 Fix: Select TRC font
                $self->{AFPDS}->selectTRCfont( ord($char) )
                    unless (@skip);

                # Read the rest of the line
                $buffer = "";
                $long = &myreadline( \*$AFPDSFILE , \$buffer );

                # Update afpbuffer
                $afpbuffer .= $buffer ;

                $tmp = &compute( \$buffer , -2 , 2 );
                if ( $tmp != 0x0D0A ) { # Buffer must end with 0x0D0A
                    &debugbuffer( $buffer );
                    &Abort(101,$tmp);
                }

            } else {
                &Abort(5);
            }

            &mychomp( \$buffer );
            $long = length( $buffer ) ;

            &from_to( $buffer , $FROM_CONVERT , $TO_CONVERT ) if ($DO_CONVERT);

            # Compute the new AFP line
            $self->{AFPDS}->afpline( $cctrl , $buffer )
                unless (@skip);
        }

        my $currentpos  = tell($AFPDSFILE) ;
        my $currenttime = &tv_interval(\@StartTime) ;

        # Return convertion status after required timing
        if ( $currenttime >= $tv_timing ) {
            my $fileratio = $currentpos/$EOF ;

            # Compute convertion ratio
            my $ratio = sprintf("%d",$fileratio*100);

            # Compute termination estimation
            my $remain = sprintf("%.1f",$currenttime/$fileratio-$currenttime);
            &Debug("Convertion done at $ratio%, should finish in $remain s");

            # Return information to JobManager
            $self->Return( $Job => &GetCom( comINF ,
                'AFP2TeX Conversion status' =>
                    "$ratio% done, $remain sec remaining" ));

            # Update job state in spool
            &a2pjobstate( $Job, 2, '.' , { STATUS => $ratio.'%' })
                or &Info("Can't update status conversion progress to $ratio%");

            # Update next timing report
            $tv_timing += $CONV_STATUS_TIMING ;
        }

        &Debug("Loop at AFPDS file off-set " . sprintf("%Xh",$currentpos) .
            ($inXML?" in not sized RC '$inXML'":""));

        $buffer = getc( $AFPDSFILE ) ;

    } # End while read file

    &Debug("File#" . $file . ": Last TeX file generation");

    # Output last converted TeX file
    &Abort(2) unless ( @skip or &generateTeX );

    # Keep stats on conversion rate
    my $tv_interval = &tv_interval($RateTimer) ;
    &MAXSTAT('CONV-RATE',int( 100 * $file / $tv_interval ) / 100)
        if ($tv_interval);

ABORTED_FILE:
    # Freeing objects
    delete $self->{AFPDS}
        if (defined($self->{AFPDS}) and ! $self->{AFPDS}->issplitted());
    delete $self->{RC} if (  defined($self->{RC}) );

    # Reset vars
    $file = 0 ;

    return $ErrFlag ? &End($ErrFlag) : &End(0, "Conversion done" );
}

my ( $conv_time , $conv_count ) = ( 0, 0 );
sub generateTeX {
    my $afpds = $self->{AFPDS} ;

    # Check few things on RC
    # At least a RC 001 or a RC 100 must have been provided
    return &Error(_job_tag."No RC 001 or RC 100 defined")
        unless (defined($self->{RC}->{'001'}) or defined($self->{RC}->{'100'}));
    # If a RC 200 has been provided, we must also have a RC 201 and a RC 202
    return &Error(_job_tag."Found wrong combination of record 200-202")
        if (defined($self->{RC}->{'200'}) and
        $self->{RC}->{'200'}->a2pxml_is_valid and
        !(defined($self->{RC}->{'201'}) and defined($self->{RC}->{'202'})));

    # Output the convertex TeX file
    return &Error(_job_tag."Error while generating TeX file")
        unless ($afpds->gentex( $TexFile ));

    # Output to AfpFile and check TeX convertion rate
    if (defined($afpbuffer) and length($afpbuffer)) {

        # 1. Output to AfpFile
        if (eof($AFPDSFILE)) {
            print $AfpFile $afpbuffer if (defined($AfpFile));

        } else {
            # Output AfpFile without the last char '#'
            print $AfpFile substr($afpbuffer,0,-1)
                if (defined($AfpFile));

            # And reset afpbuffer with only char '#' ( in ebcdic)
            $afpbuffer = chr(0x7B);
        }

        # 2. Wait a little to match required TeX convertion rate
        #    essentially on big job
        if ( $MAX_CONV_RATE and $file > $MAX_CONV_RATE ) {
            my $prev_conv_time = $conv_time ; # Default to zero

            # Still sleep a little if we can
            if ( $conv_time ) {
                &MAXSTAT('CONV-RATE-PRE-SLEEP-TIME',$conv_time);
                usleep $conv_time  ;
            }

            # Get the delay since the beginning of that conversion
            $conv_time = &tv_interval($RateTimer) * 1_000_000 ;

            # min_conv_time is the time by conversion to reach computed before
            # each conversion start
            my $diff = $conv_time - $file * $min_conv_time ;
            my $count = $file > $conv_count ? $file - $conv_count : 1 ;
            if ( $diff < 0 ) {
                # Get the time we should have sleep to reach max_conv_rate
                my $usleep = abs($diff) ;

                # Really not sleep if time to sleep is too short
                unless ( $usleep < $USLEEP ) {
                    &UPSTAT('CONV-RATE-SLEEPING');
                    &MAXSTAT('CONV-RATE-SLEEP-TIME',$usleep);
                    &Debug("Waiting $usleep ms to lower speed as " .
                        $MAX_CONV_RATE . " Max conversion rate was reached");
                    usleep $usleep ;
                }
                $conv_time = $usleep / $count ;

            } elsif ($prev_conv_time) {
                &UPSTAT('CONV-RATE-SLEPT-TOO-MUCH');
                # diff is the excess time we have slept
                my $correction = $count > 0 ? $diff / $count : 0 ;

                # Always reset $conv_time to keep the sleep next time
                if ( $correction > 0 and $correction < $prev_conv_time
                and $conv_time > $USLEEP ) {
                    # Adjust conv_time to sleep less next time
                    $conv_time = $prev_conv_time - $correction ;

                } else {
                    $conv_time = $prev_conv_time ;
                }
            }

            # Remember conversion count
            $conv_count = $file ;
        }
    }

    # Return the job environment to JobManager
    &Debug("File#" . $file . ": Shell env file generation");

    # Set MTIME job env to file mtime
    $afpds->updateEnv( { MTIME => $mtime } );

    # Announce last job to master thread
    $self->Return( $Job , $afpds->getEnv() );

    # And keep some stats
    $self->KeepStats( $Job, $afpds->getstat() );

    # Update job state in spool
    &a2pjobstate( $Job, 2, 'o' )
        or &Info(_job_tag."Can't update status to step 2 done");

    # Return true
    1 ;
}


################################################################################
sub End {
    # End jobs here as it closes properly any opened file
    my $ret  = shift || 0 ;
    my $msg  = shift || "";

    if (defined($msg)) {
        $ret and $msg ? &Error($msg) : &Debug($msg) ;
    }

    # Now clean everything
    &Debug("Closing files");

    &CloseFiles();
    close($AFPDSFILE) if (defined($AFPDSFILE));

    return $ErrFlag ? $ErrFlag : $ret ;
}

our $AbortMessages = {
    0   => "Unknown error",
    1   => "Last TeX file not generated",
    2   => "Can't generate TeX file",
    3   => "Can't handle RC 5A without AFPDS flux defined",
    4   => "Expected defined AFPDS flux here",
    5   => "Can't processing standard AFP line without AFPDS Flux defined",
    6   => "Can't load PAGEDEF as it isn't defined",
    100 => "Can't use not defined '%s' controlrecord",
    101 => "Found AFPDS text line with bad end of line <0x%04X>",
    102 => "Unknown control record '%s'",
    103 => "File#%s: Bad TeX file generation for print %s",
    104 => "Unable to load PAGEDEF %s as Perl module %s not found",
    105 => "freed error number",
    106 => "Can't create %s folder: %s",
    107 => "Can't open %s: %s",
    108 => "Can't process %s in binary mode: %s",
    109 => "Can't read last 10 chars of %s: %s",
    110 => "Found AFPDS Field with bad end of line <0x%04X>",
    111 => "Found AFPDS Field with bad length: %s vs %s",
    112 => "Bad control record type format found for record type '%s'",
    113 => "Bad control record length (%s) for record type '%s'",
    114 => "Got bad AFPDS format as RC 5A is too short (%s < 8)",
    115 => "Can't create RC object %d of type '%s' with TEXBASE '%s'",
    116 => "Can't initialize RC type '%s' with buffer '%s'",
    117 => "Can't use RC type '%s' on with no AFPDS Flux defined",
    118 => "AFP error on RC 5A with ident %6X, flag %08b, reserved 0x%04X, ".
           "long %d and buffer %s",
    119 => "Can't use existing folder '%s' to compute AFPDS file",
    120 => "Can't use RC type %d without previously defined RC type %d",
    121 => "Can't save XML content for RC type %d in %s",
    122 => "Folder '%s' not available",
    123 => "Can't update '%s' attribut with value '%s' in A2P XML",
    124 => "File '%s' is too short or corrupted",
    200 => "Got RC 5A error %d, %s",
    300 => "Got RC #%s# init error %d, %s",
    500 => "trying to convert job %d after errors"
};

my @AbortErrors = (
    0, 2, 3, 4, 5, 106, 107, 108, 109, 119, 122
    );

# Must be called with number identifying message and then abort processing
sub Abort {
    $ErrFlag = shift || 0 ;

    unless ( defined($skip[$ErrFlag]) and $skip[$ErrFlag]++ ) {
        @AbortTime = &gettimeofday();

        my $file_offset = sprintf("AFPDS file off-set %Xh",tell($AFPDSFILE)) ;

        my @ERR = $ErrFlag >= 500 ?
            () : ( _job_tag . "Aborting at $file_offset" ) ;
        my $msg = $ErrFlag >= 500 ? $file_offset.": " : "" ;
        $msg .= _job_tag . $AbortMessages->{$ErrFlag} ;

        my $ABTERM = $ErrFlag < 100 ?
            $msg." (Err$ErrFlag)" : sprintf( $msg." (Err%d)" , @_, $ErrFlag ) ;

        # Still return the ABTERM message to JobManager
        $self->Return( $Job => &GetCom( comINF , 'ABTERM '.$ErrFlag, $ABTERM ));

        push @ERR, $ABTERM ;
        $ErrFlag < 500 ? &Error( @ERR ) : &Warn( @ERR );

        # Return information to JobManager
        map { $self->Return( $Job => &GetCom( comINF , 'ERROR' , $_ )) } @ERR ;

        # Update status for afpjob
        map { &a2pjobstate( $Job, 2, 'A', { ABTERM => "$_" } ) } @ERR ;

        # Keep the ABTERM on the current JID if really started
        if ( $file ) {
            # Update status for subjob designed by JID key in %Infos
            my $Infos = { JID => $Job . "-" . $file } ;
            $Infos->{STATUS} = 'ABTERM' if ( $ErrFlag < 500 );
            map {
                $Infos->{ERRORS} = "$_" ;
                &a2pjobstate( $Job, 2, 'A', $Infos );
            } @ERR
        }
    }

    if (grep { $_ == $ErrFlag } @AbortErrors) {
        goto ABORTED_FILE ;

    } else {
        # Don't Abort file conversion to try next sub jobs
        return $ErrFlag ;
    }
}

END {
    if ($ErrFlag and @AbortTime) {
        my $time = sprintf("%.2f sec",&tv_interval(\@AbortTime));
        &Error("Last conversion aborted $time ago with $ErrFlag abort number");
    }
}

&Debug("Module " . __PACKAGE__ . " v$VERSION loaded");

1;
