# -*- perl -*-
#
# Copyright (C) 2011-2025 Alexis Bienvenüe <paamc@passoire.fr>
#
# This file is part of Auto-Multiple-Choice
#
# Auto-Multiple-Choice 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.
#
# Auto-Multiple-Choice 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 Auto-Multiple-Choice.  If not, see
# <http://www.gnu.org/licenses/>.

use warnings;
use 5.012;

package AMC::State;

# This package helps storing state of important files after the user
# has printed exam sheets. All requested files are stored in a archive
# file, along with MD5 sum of each of these files and printing
# information. When printing again, if all the files are unchanged,
# only printing information is added. If some of the files has been
# modified since the last print, a new ARCHIVE file is created.

use AMC::Basic;

use Archive::Zip qw( :ERROR_CODES :CONSTANTS );
use Digest::MD5;
use XML::Simple;

sub new {
    my (%o) = (@_);

    my $self = {
        directory   => '',
        archivefile => '',            # absolute
        descfile    => 'state.xml',
    };

    for my $k ( keys %o ) {
        $self->{$k} = $o{$k} if ( defined( $self->{$k} ) );
    }

    $self->{archive} = Archive::Zip->new();
    $self->{data}    = {};

    $self->{directory} =~ s/\/$//;

    bless $self;

    return ($self);
}

sub archive {
    my ($self) = @_;
    return ( $self->{archive} );
}

# reads the ARCHIVE file. If no filename is given, looks for the last ARCHIVE
# file created in given directory.
sub read {
    my ( $self, $archivefile ) = @_;
    if ( !$archivefile ) {
        $archivefile = $self->{archivefile};
    }
    if ( !$archivefile ) {

        # look for the last one in the directory
        opendir( my $dh, $self->{directory} )
          or debug "Error opening directory $self->{directory}: $!";
        my @st = sort { $b cmp $a }
          grep { /^saved-[0-9-]+\.zip$/ && -f $self->{directory} . "/$_" }
          readdir($dh);
        closedir $dh;

        if (@st) {
            debug "Archive file found: $st[0]";
            $archivefile = $self->{directory} . "/" . $st[0];
        } else {
            debug "No ARCHIVE file";
            $archivefile = '';
        }
    }

    $self->{data} = { 'md5' => {}, print => [] };

    if ( -f $archivefile ) {

        # opens the ARCHIVE file to look at the content
        debug "Reading ARCHIVE $archivefile";
        if ( $self->{archive}->read($archivefile) != AZ_OK ) {
            debug "Error reading $archivefile";
        } else {

            # look for XML description file, with MD5 sums and
            # printing information, and read it
            my $xml = $self->{archive}->contents( $self->{descfile} );
            if ($xml) {
                $self->{data} = XMLin(
                    $xml,
                    ForceArray => 1,
                    ContentKey => 'content',
                    KeyAttr    => ['file']
                );
                $self->{archivefile} = $archivefile;
            }
        }
    } else {
        debug "No file $archivefile";
        $self->{archivefile} = '';
    }
}

# writes ARCHIVE file to disk, including XML file with MD5 sums and
# printing information
sub write {
    my ( $self, $archivefile ) = @_;
    if ($archivefile) {
        $self->{archivefile} = $archivefile;
    } else {
        $archivefile = $self->{archivefile};
    }
    if ( !$archivefile ) {

        # if no filename is given, create it from date and time
        my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst ) =
          localtime(time);

        $archivefile =
          $self->{directory} . "/"
          . sprintf(
            "saved-%d-%02d-%02d-%02d-%02d-%02d.zip",
            $year + 1900,
            $mon + 1, $mday, $hour, $min, $sec
          );
        $self->{archivefile} = $archivefile;
    }

    # adds XML file to ARCHIVE
    my $xml = XMLout(
        $self->{data},
        ContentKey => 'content',
        KeyAttr    => ['file'],
        RootName   => 'state',
    );
    $self->{archive}->removeMember( $self->{descfile} );
    utf8::encode $xml;
    $self->{archive}->addString( $xml, $self->{descfile} );

    # writes to disk
    debug "Writing ARCHIVE to $archivefile ...";
    unless ( $self->{archive}->overwriteAs($archivefile) == AZ_OK ) {
        debug "Error writing to $archivefile";
    }
}

# adds printing information
sub add_print {
    my ( $self, %oo ) = @_;

    my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst ) =
      localtime(time);

    push @{ $self->{data}->{print} },
      {
        %oo,
        date => sprintf(
            "%d-%02d-%02d %02d:%02d:%02d",
            $year + 1900,
            $mon + 1, $mday, $hour, $min, $sec
        )
      };

}

# clears all MD5 sums from current state
sub clear_md5 {
    my ($self) = @_;
    $self->{data}->{'md5'} = {};
}

# returns MD5 sum stocked in the ARCHIVE state for a given file
sub get_md5 {
    my ( $self, $file ) = @_;
    my $r = $self->{data}->{'md5'}->{$file}->{content};
    return ( defined($r) ? $r : '' );
}

# sets MD5 sum in current state
sub set_md5 {
    my ( $self, $file, $sum ) = @_;
    $self->{data}->{'md5'}->{$file}->{content} = $sum;
}

# check if given files are all unchanged
sub check_local_md5 {
    my ( $self, @files ) = @_;
    my $ok = 1;

  FILE: for (@files) {
        if ( !$self->check_md5($_) ) {
            $ok = 0;
            last FILE;
        }
    }
    return ($ok);
}

# check if given file is unchanged. $localfile is the absolute path of
# the on-disk file, and $file is the name of the in-ARCHIVE file. Un empty
# $localfile is set to file $file in state directory (given when using
# new)
sub check_md5 {
    my ( $self, $file, $localfile ) = @_;

    if ( !$localfile ) {
        if ( $file =~ /^\// ) {
            $localfile = $file;
            $file =~ s/.*\///;
        } else {
            $localfile = $self->{directory} . "/" . $file;
        }
    }

    my $z = $self->get_md5($file);
    debug "Archive MD5: $z";

    if ($z) {
        if ( open( FILE, $localfile ) ) {
            binmode(FILE);
            my $md5_local = Digest::MD5->new->addfile(*FILE)->hexdigest;
            close(FILE);

            debug "Local MD5: $md5_local";

            return ( $z eq $md5_local );
        } else {
            debug "Open failed for $localfile";
            return (0);
        }
    } else {
        debug "No ARCHIVE MD5 for $file";
        return (0);
    }
}

# adds MD5 sum of given on-disk file to current state
sub add_md5 {
    my ( $self, $file, $localfile ) = @_;

    if ( !$localfile ) {
        if ( $file =~ /^\// ) {
            $localfile = $file;
            $file =~ s/.*\///;
        } else {
            $localfile = $self->{directory} . "/" . $file;
        }
    }

    if ( open( FILE, $localfile ) ) {
        binmode(FILE);
        my $md5_local = Digest::MD5->new->addfile(*FILE)->hexdigest;
        close(FILE);

        debug "Adding MD5: $md5_local";

        $self->set_md5( $file, $md5_local );
    } else {
        debug "Open failed for $localfile";
    }
}

# adds file to ARCHIVE and sets corresponding MD5 sum in current state
sub add_file {
    my ( $self, $file, $localfile ) = @_;

    if ( !$localfile ) {
        if ( $file =~ /^\// ) {
            $localfile = $file;
            $file =~ s/.*\///;
        } else {
            $localfile = $self->{directory} . "/" . $file;
        }
    }

    if ( -f $localfile ) {
        debug "Adding $localfile [$file]...";
        my $encoded_file = $file;
        utf8::encode $encoded_file;
        if ( $self->{archive}->addFile( $localfile, $encoded_file ) ) {
            $self->add_md5( $file, $localfile );
            debug "OK.";
            return (1);
        } else {
            debug "Error adding $localfile : $!";
            return (0);
        }
    } else {
        debug "ARCHIVE: No file $localfile";
        return (0);
    }
}

# adds a list of files all in the state sirectory
sub add_local_files {
    my ( $self, @files ) = @_;
    my $i = 0;
    for (@files) { $i += $self->add_file($_); }
    return ($i);
}

1;
