#!/usr/bin/perl
#
# Copyright (c) 2016-2024, Bill MacAllister <bill@ca-zephyr.org>
# File: ring-daemon
# Description: Update pictures by either creating/re-creating alternate
#   or updating meta-data.

use strict;
use Cwd;
use DBI;
use Getopt::Long;
use Image::ExifTool 'ImageInfo';
use Image::Magick;
use Pod::Usage;
use Rings::Common;

my %SIZE_IDS    = ();
my @ACTION_LIST = ('180', 'LEFT', 'RIGHT', 'SIZE', 'INFO');

my $opt_action;
my $opt_debug;
my $opt_help;
my $opt_id;
my $opt_manual;
my $opt_oneshot;

##############################################################################
# Subroutines
##############################################################################

# ------------------------------------------------------------------------
# Look up a list of pids to process

sub get_pids {
    my ($pid_start, $pid_end, $action) = @_;

    my @pid_list = ();

    if ($CONF->debug) {
        dbg("action:$action");
        dbg("pid_start:$pid_start pid_end:$pid_end");
    }
    if ($pid_start == 'pending') {
        my $sel = 'SELECT pid FROM picture_action_queue ';
        $sel .= "WHERE status = 'PENDING' ";
        $sel .= 'AND action = ? ';
        $sel .= 'ORDER BY pid ';

        if ($CONF->debug) {
            dbg($sel);
        }

        my $sth = $DBH->prepare($sel);
        $sth->execute($action);
        if ($sth->err) {
            print("INFO: action = $action");
            sql_die($sel, $sth->err, $sth->errstr);
        }
        while (my $row = $sth->fetchrow_hashref) {
            push @pid_list, $row->{pid};
        }
    } else {
        if ($pid_start > 0 && $pid_end < $pid_start) {
            msg('warn', "End PID smaller than Start PID");
        }
        my $sel = 'SELECT pid ';
        $sel .= 'FROM pictures_information ';
        $sel .= 'WHERE pid >= ? AND pid <= ? ';
        $sel .= 'ORDER BY pid ';

        if ($CONF->debug) {
            dbg($sel);
        }

        my $sth = $DBH->prepare($sel);
        $sth->execute($pid_start, $pid_end);
        if ($sth->err) {
            print("INFO: pid_start = $pid_start, pid_end = $pid_end");
            sql_die($sel, $sth->err, $sth->errstr);
        }
        while (my $row = $sth->fetchrow_hashref) {
            push @pid_list, $row->{pid};
        }
    }

    return \@pid_list;
}

# ------------------------------------------------------------------------
# For a given set of pids either generate associated picture sizes or
# read meta data and store it.

sub process_pids {
    my ($action, $pid_list_ref) = @_;
    my @pid_list = @{$pid_list_ref};

    # process the pictures

    my $cnt = 0;
    foreach my $pid (@pid_list) {
        msg('info', "Performing action $action for picture $pid");
        my $sel = 'SELECT picture_details.mime_type, ';
        $sel .= 'pictures_information.picture_lot, ';
        $sel .= 'pictures_information.source_file, ';
        $sel .= 'picture_types.file_type ';
        $sel .= 'FROM picture_details ';
        $sel .= 'JOIN pictures_information ';
        $sel .= 'ON (pictures_information.pid = picture_details.pid) ';
        $sel .= 'JOIN picture_types ';
        $sel .= 'ON (picture_types.mime_type = picture_details.mime_type) ';
        $sel .= 'WHERE picture_details.pid = ? ';
        $sel .= "AND picture_details.size_id = 'raw' ";

        if ($CONF->debug) {
            dbg($sel);
        }

        my $sth = $DBH->prepare($sel);
        $sth->execute($pid);
        if ($sth->err) {
            print("INFO: pid = $pid");
            sql_die($sel, $sth->err, $sth->errstr);
        }
        my $pic_lot;
        my $source_file;
        my $raw_mime_type;
        my $raw_file_type;
        if (my $row = $sth->fetchrow_hashref) {
            $pic_lot       = $row->{picture_lot};
            $source_file   = $row->{source_file};
            $raw_mime_type = $row->{mime_type};
            $raw_file_type = $row->{file_type};
        } else {
            msg('error', "PROBLEM SQL: $sel");
            my $msg = "Problems getting raw_picture information for $pid";
            msg('error', $msg);
            queue_error($pid, $action, $msg);
            next;
        }

        # Get the raw picture
        my $raw_file = "${pid}.$raw_file_type";
        my $pic_path = $CONF->picture_root . "/$pic_lot/raw/$raw_file";
        if (!-e $pic_path || -d $pic_path) {
            my $msg = "Raw picture not found for $pid ($pic_path)";
            msg('error', $msg);
            queue_error($pid, $action, $msg);
            next;
        }
        my $pic_size = -s $pic_path;
        if ($pic_size < 1) {
            my $msg = "Raw picture is zero length for $pid ($pic_path)";
            msg('error', $msg);
            queue_error($pid, $action, $msg);
            next;
        }

        my %pic = get_meta_data($pic_path);

        # Duplicate check
        my @dup_list = dup_check($pic{'size'}, $pic{'signature'});
        if (scalar(@dup_list) > 0) {
            my $msg   = "$pid is duplicated by: ";
            my $comma = '';
            for my $p (@dup_list) {
                $msg .= ${comma} . ${p};
                $comma = ', ';
            }
            msg('error', $msg);
            queue_error($pid, $action, $msg);
            next;
        }

        my $mime_type = $pic{'mimetype'};
        my $file_type = $raw_file_type;
        if ($action eq 'SIZE') {
            foreach my $s (sort keys %SIZE_IDS) {
                if ($s eq 'raw') {
                    next;
                }
                resize_all($pic_lot, $pid, $file_type, \%pic);
            }
            store_meta_data($pic_lot, $pid, \%pic);
            queue_action_reset($pid, $action);
        } elsif ($action =~ /^(180|LEFT|RIGHT)/xmsi) {
            rotate_picture($action, $pic_path);
            my %pic = get_meta_data($pic_path);
            store_meta_data($pic_lot, $pid, \%pic);
            resize_all($pic_lot, $pid, $file_type, \%pic);
            queue_action_reset($pid, $action);
        } elsif ($action = 'INFO') {
            my %pic = get_meta_data($pic_path);
            store_meta_data($pic_lot, $pid, \%pic);
            queue_action_reset($pid, $action);
        } else {
            queue_error($pid, $action, "Unknown action $action");
        }
    }
    return;
}

# ------------------------------------------------------------------------
# Resize all resolutions for a given picture

sub resize_all {
    my ($pic_lot, $pid, $file_type, $pic_ref) = @_;

    foreach my $s (sort keys %SIZE_IDS) {
        if ($s eq 'raw') {
            next;
        }
        my $ppath = create_picture_dirs($pic_lot, $s);
        my $pbase = "${pid}.${file_type}";
        my $pfile = "${ppath}/${pbase}";
        my $pic   = create_picture($pid, $s, $pfile, $pic_ref);
    }

    return;
}

##############################################################################
# Main Routine
##############################################################################

# -- get options
GetOptions(
    'action=s' => \$opt_action,
    'debug'    => \$opt_debug,
    'help'     => \$opt_help,
    'id=s'     => \$opt_id,
    'manual'   => \$opt_manual,
    'oneshot'  => \$opt_oneshot
);

# help the poor souls out
if (@ARGV && $ARGV[0] eq 'help') {
    $opt_help = 1;
}
if ($opt_help) {
    pod2usage(-verbose => 0);
}
if ($opt_manual) {
    pod2usage(-verbose => 2);
}
if ($opt_action) {
    my %valid_actions = ();
    for my $a (@ACTION_LIST) {
        $valid_actions{$a} = 1;
    }
    @ACTION_LIST = ();
    my @new_actions = split /,/, $opt_action;
    for my $a (@new_actions) {
        if ($valid_actions{$a}) {
            push @ACTION_LIST, $a;
        } else {
            msg('info', "ERROR: unknown action requested $a");
            pod2usage(-verbose => 0);
        }
    }
}

# Set the picture range to process from the command line
my $pid_start = 'pending';
if ($ARGV[0]) {
    $pid_start = $ARGV[0];
    if ($pid_start < 1 && $pid_start ne 'pending') {
        msg('info', 'ERROR: a starting pid or "pending" is required');
        pod2usage(-verbose => 0);
    }
}
my $pid_end = $pid_start;
if ($ARGV[1]) {
    $pid_end = $ARGV[1];
}
if ($pid_start > 0) {
    if ($pid_start > $pid_end) {
        msg('info', 'ERROR: ending PID must be => starting PID');
        pod2usage(-verbose => 0);
    }
    if (!$opt_id) {
        msg('info', 'ERROR: --id=<ring identifier> is required');
        pod2usage(-verbose => 0);
    }
}

my %id_list = ();
if ($opt_id) {
    $opt_oneshot = 1;
    $id_list{$opt_id} = 1;
} else {
    %id_list = get_id_list();
}
if ($opt_debug) {
    print("Debugging started");
}

my $end  = 0;
my $rest = 60;
while ($end == 0) {
    if ($opt_oneshot || $pid_start > 0) {
        $end = 1;
    }
    my $update_flag = 0;
    for my $id (keys %id_list) {
        my $ring_conf = "/etc/rings/${id}.conf";
        get_config($ring_conf);
        if ($opt_debug) {
            $CONF->debug(1);
        }
        if ($CONF->debug) {
            dbg("ring_conf = $ring_conf\n");
        }
        db_connect();
        %SIZE_IDS = get_picture_sizes();
        for my $action (@ACTION_LIST) {
            my $this_list_ref = get_pids($pid_start, $pid_end, $action);
            if (scalar(@{$this_list_ref}) > 0) {
                $update_flag = 1;
                process_pids($action, $this_list_ref);
            }
        }
        if ($CONF->queue_sleep > $rest) {
            $rest = $CONF->queue_sleep;
        }
        db_disconnect();
    }
    if (!$update_flag && !$opt_oneshot) {
        sleep $rest;
    }
}

exit;

__END__

=head1 NAME

ring-daemon - resize and store pictures

=head1 SYNOPSIS

ring-daemon <start pid>|pending [<end pid>] [--action=<action>]
[--id=<ring id>] [--oneshot] [--debug] [--help] [--manual]

=head1 DESCRIPTION

Resize or store meta data for pictures in the rings database.  When
regenerating sizes the script will regenerate all picture sizes for
the requested pictures.  If the first arguement is 'pending' then the
script will process all the entries in the picture_action_queue table
continuously, sleeping when there are no entries in the queue.  If
--oneshot is specified with pending then the queue table is read once
and the script exits after processing the queue entries read.

If there are problems with a queue entry its status is set to ERROR
and an error message is stored in the picture_action_queue table.
This effectively removes the entry from processing.

=head1 OPTIONS AND ARGUMENTS

=over 4

=item --start=<integer>|pending

When a start and end picture id is specified an action must also be
specified.

The a start of 'pending' is specified then the picture IDs to process
and actions are read from the picture_action_queue table.

=item --end=int

The picture id to end at.

=item --id=<ring identifier>

If a starting and ending picture identifiers are specified then --id
is required.  If --start=pending then --id is ignored.

=item --action=180|LEFT|RIGHT|SIZE|INFO

The action to perform.  This is only useful when a start and end
picture ID is specified.

=item --oneshot

Process the pending queue entries once and then exit.

=item --help

Displays help text.

=item --manual

Displays more complete help text.

=item --debug

Turns on debugging displays.

=back

=head1 AUTHOR

Bill MacAllister <bill@ca-zephyr.org>

=head1 COPYRIGHT AND LICENSE

Copyright (C) 2016-2024, Bill MacAllister <bill@ca-zephyr.org>.

This code is free software; you can redistribute it and/or modify it
under the same terms as Perl. For more details, see the full
text of the at https://opensource.org/licenses/Artistic-2.0.

This program 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.

=cut
