#!/bin/env perl
###
### pstampparse.pl
###
###     Run the postage stamp request parser for a given request id
###

use warnings;
use strict;

use Sys::Hostname;
use Getopt::Long qw( GetOptions );
use PS::IPP::PStamp::RequestFile qw( :standard );
use PS::IPP::PStamp::Job qw( :standard );
use File::Temp qw(tempfile);
use File::Basename qw(basename);
use Carp;
use POSIX;

my $verbose;
my $dbname;
my $dbserver;
my $req_id;
my $request_file_name;
my $mode = "list_uri";
my $out_dir;
my $product;
my $save_temps;
my $no_update;

GetOptions(
    'file=s'    =>  \$request_file_name,
    'req_id=s'  =>  \$req_id,
    'out_dir=s' =>  \$out_dir,
    'product=s' =>  \$product,
    'mode=s'    =>  \$mode,
    'dbname=s'  =>  \$dbname,
    'dbserver=s'=>  \$dbserver,
    'verbose'   =>  \$verbose,
    'save-temps'=>  \$save_temps,
    'no-update' =>  \$no_update,
);

die "invalid mode '$mode'" unless ($mode eq "list_uri") or ($mode eq "list_job") or ($mode eq "queue_job");
die "--file is required"     if !defined($request_file_name);

if ($mode ne "list_uri") {
    die "req_id is required"   if !$req_id;
    die "out_dir is required"  if !$out_dir;
    die "product is required"  if !$product;
}

use IPC::Cmd 0.36 qw( can_run run );

use PS::IPP::Metadata::Config;
use PS::IPP::Metadata::Stats;
use PS::IPP::Metadata::List qw( parse_md_list );

use PS::IPP::Config qw( :standard );
my $ipprc = PS::IPP::Config->new(); # IPP Configuration

my $missing_tools;

my $pstamptool  = can_run('pstamptool')  or (warn "Can't find pstamptool"  and $missing_tools = 1);
my $pstampdump  = can_run('pstampdump') or (warn "Can't find pstampdump" and $missing_tools = 1);
my $dvoImagesAtCoords  = can_run('dvoImagesAtCoords') or (warn "Can't find dvoImagesAtCoords" and $missing_tools = 1);
my $fields  = can_run('fields') or (warn "Can't find fields" and $missing_tools = 1);

if ($missing_tools) {
    warn("Can't find required tools.");
    exit ($PS_EXIT_CONFIG_ERROR);
}

# list_job is a deugging mode
$no_update = 1 if $mode eq "list_job";

my $mdcParser = PS::IPP::Metadata::Config->new; # Parser for metadata config files

#
# get the data from the extension header
#
my $fields_output;
{
    my $command = "echo $request_file_name | $fields -x 0 EXTNAME EXTVER REQ_NAME";
    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
        run(command => $command, verbose => $verbose);
    # fields doesn't return zero when it succeeds
    #unless ($success) {
    #    print STDERR @$stderr_buf;
    #}
    $fields_output = join "", @$stdout_buf;
}
my (undef, $extname, $extver, $req_name) = split " ", $fields_output;

# make sure the file contains what we are expecting

# we shouldn't get here if this is the case so just die. No need to notify the client
my_die("$request_file_name is not a PS1_PS_REQEST", $PS_EXIT_PROG_ERROR) if $extname ne "PS1_PS_REQUEST";
my_die("REQ_NAME not found in $request_file_name", $PS_EXIT_PROG_ERROR)  if (!$req_name);
my_die("wrong EXTVER $extver found in $request_file_name", $PS_EXIT_PROG_ERROR) if ($extver ne "1");


# check for duplicate request name
my $duplicate_req_name = 0;
if ($req_id and !$no_update) {
    my $command = "$pstamptool -listreq  -name $req_name -not_req_id $req_id";
    $command .= " -dbname $dbname" if $dbname;
    $command .= " -dbserver $dbserver" if $dbserver;
    # no verbose so that error message about request not found doesn't appear in parse_error.txt
    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
        run(command => $command, verbose => 0);
    my $exitStatus = $error_code >> 8;
    if ($success) {
        # -listreq succeeded duplicate request name
        print STDERR "REQ_NAME $req_name has already been used\n";
        insertFakeJobForRow(undef, 0, $PSTAMP_DUP_REQUEST);
        $duplicate_req_name = 1;
        my $datestr = strftime "%Y%m%d%H%M%S.$req_id", gmtime;
        $req_name = "ERROR.$datestr";
        #exit 0;
    }
}

if ($req_id and !$no_update) {
    # update the database with the request name. This will be used as the
    # the output data store's product name
    my $command = "$pstamptool -updatereq -req_id $req_id  -name $req_name";
    $command .= " -outProduct $product";
    $command .= " -dbname $dbname" if $dbname;
    $command .= " -dbserver $dbserver" if $dbserver;
    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
        run(command => $command, verbose => $verbose);
    unless ($success) {
        my_die("$command failed", $PS_EXIT_UNKNOWN_ERROR);
    }
}
if ($duplicate_req_name) {
    exit 0;
}

#
# now convert the request table to an array of metadata config docs
#

my $rows;
{
    my $command = "$pstampdump $request_file_name";
    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
        run(command => $command, verbose => $verbose);
    unless ($success) {
        print STDERR @$stderr_buf;
    }
    my $table =  $mdcParser->parse(join "", @$stdout_buf) or
        my_die("Unable to parse metdata config doc", $PS_EXIT_UNKNOWN_ERROR);

    $rows = parse_md_list($table);
}

my @rowList;
my $num_jobs = 0;
my $imageList;
my $stage;
my $need_magic;
foreach my $row (@$rows) {
    # XXX: TODO: sanity check all parameters

    # XXX: TODO: We shouldn't really just die in this loop.
    # If we encounter an error for a particular row
    # add a job with the proper fault code. If there is a db or config error we should probably just
    # trash the request.

    my $rownum   = $row->{ROWNUM};
    my $job_type = $row->{JOB_TYPE};
    
    # parameters that select the images of interest
    my $project  = $row->{PROJECT};

    # note: resolve_project avoids running pstamptool every time by remembering the
    # last project resolved
    my $proj_hash = resolve_project($ipprc, $project, $dbname, $dbserver);
    if (!$proj_hash) {
        print STDERR "project $project not found\n"  if $verbose;
        insertFakeJobForRow($row, 1, $PSTAMP_UNKNOWN_PRODUCT);
        $num_jobs++;
        next;
    }
    my $req_type = $row->{REQ_TYPE};
    $stage       = $row->{IMG_TYPE};
    my $id      = $row->{ID};
    my $component = $row->{COMPONENT};
    my $tess_id = $row->{TESS_ID};

    my $filter  = $row->{REQFILT};
    my $mjd_min = $row->{MJD_MIN};
    my $mjd_max = $row->{MJD_MAX};
    my $label   = $row->{LABEL};

    my $option_mask= $row->{OPTION_MASK};
    my $inverse = ($option_mask & $PSTAMP_SELECT_INVERSE) ? 1 : 0;
    $row->{inverse} = $inverse;

    my $skycenter = $row->{skycenter} = ! ($row->{COORD_MASK} & $PSTAMP_CENTER_IN_PIXELS);

    my $search_component = (!defined($component) or ($component eq "null")) ? "" : $component;
    
    if (!$skycenter and !$search_component) {
        print STDERR "COMPONENT must be specified for pixel coordinate ROI\n" if $verbose;
        insertFakeJobForRow($row, 1, $PSTAMP_INVALID_REQUEST);
        $num_jobs++;
        next;
    }

    $search_component = "" if $search_component eq "all";

    if (($job_type eq "stamp") and ! validROI($row)) {
        print STDERR "invalid ROI for row $rownum\n" if $verbose;
        insertFakeJobForRow($row, 1, $PSTAMP_INVALID_REQUEST);
        $num_jobs++;
        next;
    }

    
    # $mode list_uri is a debugging mode (it may used by the http interface)
    # if this happens just croak
    my_die("job_type is list_uri but mode is $mode", $PS_EXIT_PROG_ERROR) if ($job_type eq "list_uri") and ($mode ne "list_uri");

    my $image_db   = $proj_hash->{dbname};
    my $camera     = $proj_hash->{camera};
    $need_magic = $proj_hash->{need_magic};

    # Temporary hack so that MOPS can get at non-magicked data
    if ($product and ($product eq "mops-pstamp-results")) {
        $need_magic = 0;
    }

    # collect rows with the same images of interest in a list so that they
    # can be looked up together
    if (@rowList) {
        my $firstRow = $rowList[0];
        # note order of these parameters matters
        if (same_images_of_interest($firstRow, $row)) {

            # add this row to the list and move on
            push @rowList, $row;

            next;

        } else {
            # this row has different selectors
            # queue the jobs for the ones we've collected
            $num_jobs += queueJobs($mode, \@rowList, $imageList);
            @rowList = ();
        }
    }

    # look up images for the current row
    if ($req_type eq "bycoord") {
        if (!$skycenter) {
            print STDERR "center must be specified in sky coordintes for bycoord" if $verbose;
            insertFakeJobForRow($row, 1, $PSTAMP_INVALID_REQUEST);
            $num_jobs++;
            next;
        }
        print STDERR "lookup bycoord is not yet implemented" if $verbose;
        insertFakeJobForRow($row, 1, $PSTAMP_NOT_IMPLEMENTED);
        $num_jobs++;
        next;
    } else {
        # Call PS::IPP::PStamp::Job locate_images subroutine to get the images for this
        # request specification. An array reference is returned.
        my ($x, $y);

        $imageList = locate_images($ipprc, $image_db, $req_type, $stage, $id, $tess_id, $search_component,
                $inverse, $need_magic, $x, $y, $mjd_min, $mjd_max, $filter, $label, $verbose);

        if (!$imageList or !@$imageList) {
            print STDERR "no matching images found for row $rownum\n" if $verbose;
            # note in this case queueJobs inserts the fake job for these rows
        }
        $row->{need_magic} = $need_magic;
        push @rowList, $row;
    }
}

if (@rowList) {
    $num_jobs += queueJobs($mode, \@rowList, $imageList);
}

if (($mode eq "queue_jobs") and ($num_jobs eq 0)) {
    print STDERR "no jobs created for $req_name\n" if $verbose;
    insertFakeJobForRow(undef, 0, $PSTAMP_UNKNOWN_ERROR);
}

# PAU

exit 0;


sub queueJobsForRow
{
    my $row = shift;
    my $stage = shift;
    my $imageList = shift;
    my $have_skycells = shift;
    my $need_magic = shift;

    my $num_jobs = 0;
    my $rownum = $row->{ROWNUM};
    my $option_mask = $row->{OPTION_MASK};
    my $components = $row->{components};

    my $roi_string;

    # note values were insured to be numbers in validROI()
    my $x = $row->{CENTER_X};
    my $y = $row->{CENTER_Y};
    my $w = $row->{WIDTH};
    my $h = $row->{HEIGHT};
    my $coord_mask = $row->{COORD_MASK};
    if ($x && ($x ne "null") && $y && ($y ne "null") && $w && ($w ne "null") && $h && ($h ne "null")) {
        if ($coord_mask & $PSTAMP_CENTER_IN_PIXELS) {
            $roi_string = "-pixcenter $x $y";
        } else {
            $roi_string = "-skycenter $x $y";
        }
        if ($coord_mask & $PSTAMP_RANGE_IN_PIXELS) {
            $roi_string .= " -pixrange $w $h";
        } else {
            $roi_string .= " -arcrange $w $h";
        }
    }

    # loop over images
    my $job_num = 0;
    foreach my $image (@$imageList) {
        my $component;
        if ($have_skycells) {
            $component = $image->{skycell_id};
        } else {
            $component = $image->{class_id};
        }

        # skip this component if it is not in the list for this row
        next if ! $components->{$component};

        $job_num++;

        my $imagefile = $image->{image};
        if (($stage ne "stack") and ($need_magic and !$image->{magicked})) {
            # XXX: should we add a faulted job so the client can know what happened if no images come back?
            # This test is made in locate_images now so this code never runs. This leads to no feedback
            # to users, but speeds up processing significatnly
            print STDERR "skipping non-magicked image $imagefile\n" if $verbose;

            # for now assume yes.

            insertFakeJobForRow($row, $job_num, $PSTAMP_NOT_DESTREAKED);
            $num_jobs++;

            next;
        }
        my $exp_id = $image->{exp_id};
            
        my $args = $roi_string ? $roi_string : "";
        if ($stage eq "raw" or $stage eq "chip") {
            $args .= " -class_id $component" if $component;
        }

        # add astrometry file for raw and chip images if one is available
        if (($stage eq "chip") || ($stage eq "raw")) {
            $args .= " -astrom $image->{astrom}" if $image->{astrom};
        }

        $args .= " -file $imagefile";

        if (($option_mask & $PSTAMP_SELECT_MASK) &&  $image->{mask} ) {
            $args .= " -mask $image->{mask}";
        }
        if (($option_mask & $PSTAMP_SELECT_WEIGHT) and $image->{weight} ) {
            $args .= " -variance $image->{weight}";
        }

        my $base = basename($image->{path_base});
        # XXX: TODO use filerule for this. I don't have a camera defined here
        if (($stage eq 'chip') and ($image->{camera} eq 'GPC1')) {
            $base = "${base}.${component}";
        }

        my $output_base = "$out_dir/${rownum}_${job_num}_${base}";
        my $argslist = "${output_base}.args";

        # copy the argument list to a file
        open ARGSLIST, ">$argslist" or my_die("failed to open $argslist", $PS_EXIT_UNKNOWN_ERROR);
        print ARGSLIST "$args\n";
        close ARGSLIST or my_die("failed to close $argslist", $PS_EXIT_UNKNOWN_ERROR);

        my $newState = "run";
        my $fault = 0;

        if (($stage ne 'stack') and ($stage ne 'raw')) {
            if (($image->{state} eq 'goto_purged') or ($image->{data_state} eq 'purged')) {
                $newState = 'stop';
                $fault = $PSTAMP_GONE;
            } elsif (($image->{data_state} ne 'full') or ($image->{state} ne 'full' )) {
                # XXX here is where we need to queue an update job
                # for now just say that the image is not available
                $newState = 'stop';
                $fault = $PSTAMP_NOT_AVAILABLE;
            }
        }

        $num_jobs++;
        my $command = "$pstamptool -addjob  -req_id $req_id -job_type $row->{JOB_TYPE}"
                        . " -outputBase $output_base -rownum $rownum -state $newState -options $option_mask";
        $command .= " -fault $fault" if $fault;
        $command .= " -exp_id $exp_id" if $exp_id;
        $command .= " -dbname $dbname" if $dbname;
        $command .= " -dbserver $dbserver" if $dbserver;

        if ($mode eq "list_job") { 
            # this is a debugging mode, just print the pstamptool that would have run
            # this is sort of like the mode -noupdate that some other tools support
            print "$command\n";
        } elsif (!$no_update) {
            # mode eq "queue_job"
            my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
                run(command => $command, verbose => $verbose);
            unless ($success) {
                print STDERR @$stderr_buf;
                # XXX TODO: now what? Should we mark the error state for the request?
                # should we keep going for other uris? If so how do we report that some
                # of the work that the request wanted isn't going to get done
                my_die("failed to queue job for request $req_id", $PS_EXIT_UNKNOWN_ERROR);
            }
        } else {
            print "skipping command: $command\n";
        }
    }
    if ( $num_jobs == 0 ) {
        print STDERR "no jobs for row $rownum\n" if $verbose;
        insertFakeJobForRow($row, 1, $PSTAMP_NO_OVERLAP);
        $num_jobs = 1;
    }
    return $num_jobs;
}

# queue jobs for a collection of request specifications that have the same Images of Interest
sub queueJobs
{
    my $mode = shift;
    my $rowList = shift;
    my $imageList = shift;

    my $firstRow = $rowList[0];
    my $stage    = $firstRow->{IMG_TYPE};
    my $job_type = $firstRow->{JOB_TYPE};
    my $need_magic = $firstRow->{need_magic};

    my $num_jobs = 0;

    if ($mode eq "list_uri") {
        foreach my $image (@$imageList) {
            print "$image->{image}\n";
        }
    } elsif ($job_type eq "get_image") {
        print STDERR "get_image jobs not implemented yet" if $verbose;
        foreach my $row (@$rowList) {
            insertFakeJobForRow($row, 1, $PSTAMP_NOT_IMPLEMENTED);
            $num_jobs++;
        }
    } else {
        if (!$imageList or (scalar @$imageList eq 0)) {
            # we didn't find any images for this set of rows. Insert a fake job to carry
            # the status back to the requestor
            foreach my $row (@$rowList) {
                insertFakeJobForRow($row, 1, $PSTAMP_NO_IMAGE_MATCH);
                $num_jobs++;
            }
            return $num_jobs;
        }

        my $have_skycells;
        if (($stage eq "raw") or ($stage eq "chip")) {
            $have_skycells = 0;
        } else {
            $have_skycells = 1;
        }
        
        my $thisRun;

        my $npoints = 0;
        my ($pointsList, $pointsListName);
        if (scalar @$imageList > 1) {
            ($pointsList, $pointsListName) = tempfile ("/tmp/pointsList.XXXX", UNLINK => !$save_temps);
            foreach my $row (@$rowList) {
                $row->{components} = {};
                if ($row->{skycenter}) {
                    print $pointsList "$row->{ROWNUM} $row->{CENTER_X} $row->{CENTER_Y}\n";
                    $npoints++;
                } else {
                    # this row's center is in pixel coordinates add all images to the component list for this row
                    foreach my $i (@$imageList) {
                        my $component = $have_skycells ? $i->{skycell_id} : $i->{class_id};
                        my_die( "image found with no value for component", $PS_EXIT_UNKNOWN_ERROR)
                                if !$component;
                        $row->{components}->{$component} = 1;
                    }
                }
            }
            close $pointsList;
        } else {
            # only one image. Avoid the expense of dvoImagesAtCoords. 
            # queue the job and let ppstamp figure out if the center is valid for the image
            # the resulting result code will be the same.
            foreach my $row (@$rowList) {
                $row->{components} = {};
                my $i = $imageList->[0];
                my $component = $have_skycells ? $i->{skycell_id} : $i->{class_id};
                my_die( "image found with no value for component", $PS_EXIT_UNKNOWN_ERROR) if !$component;
                $row->{components}->{$component} = 1;
            }
        }

        my $tess_dir_abs;
        my $last_tess_id = "";
        while ($thisRun = getOneRun($stage, $imageList)) {
            if ($npoints) {
                my_die( "pointsListName is not defined", $PS_EXIT_PROG_ERROR) if !$pointsListName;
                # we collected a set of sky coordinates above.
                # filter the images so that only those that contain the centers of the ROIs are processed
                my $command = "$dvoImagesAtCoords $pointsListName";
                if ($have_skycells) {
                    my $tess_id = $thisRun->[0]->{tess_id};
                    if ($tess_id ne $last_tess_id) {
                        $tess_dir_abs = $ipprc->tessellation_catdir( $tess_id );
                        $tess_dir_abs = $ipprc->convert_filename_absolute( $tess_dir_abs );
                        $last_tess_id = $tess_id;
                    }
                    $command .= " -D CATDIR $tess_dir_abs";
                } else {
                    my $astrom = $thisRun->[0]->{astrom};
                    my_die( "no astrometry file found", $PS_EXIT_UNKNOWN_ERROR) if !$astrom;
                    my $astrom_resolved = $ipprc->file_resolve($astrom);
                    $command .= " -astrom $astrom_resolved";
                }
                my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
                    run(command => $command, verbose => $verbose);
                unless ($success) {
                    # don't fail if the program exited normally and exit status was PSTAMP_NO_OVERLAP
                    # That just means that the coordinate didn't match any image/skycell
                    if (!WIFEXITED($error_code) || (WEXITSTATUS($error_code) ne $PSTAMP_NO_OVERLAP)) {
                        print STDERR @$stderr_buf;
                        my $rc = WIFEXITED($error_code) ? WEXITSTATUS($error_code) : $PS_EXIT_SYS_ERROR;
                        my_die( "dvoImagesAtCoords failed: $rc", $rc);
                    }
                }
                # now we have a list of row numbers and components
                # eventually we might want to multiple stamp requests for the same image
                # into the same ppstamp job but not yet. For now we will queue a new
                my @lines = split "\n", join "", @$stdout_buf;
                foreach my $line (@lines) {
                    # parse the line, ignoring the ra and dec
                    my ($rownum, undef, undef, $component) = split " ", $line;

                    # I guess since we need this function we should be using a hash for rowList 
                    my $row = findRow($rownum, $rowList);
                    $row->{components}->{$component} = 1;
                }
            }
            
            foreach my $row (@$rowList) {
                $num_jobs += queueJobsForRow($row, $stage, $thisRun, $have_skycells, $need_magic);
            }
        }
    }
    return $num_jobs;
}
sub insertFakeJobForRow
{
    my $row = shift;
    my $job_num = shift;
    my $fault = shift;

    my ($job_type, $rownum);
    if ($row) {
        $job_type = $row->{JOB_TYPE};
        $rownum = $row->{ROWNUM};
    } else {
        $job_type = "none";
        $rownum = 0;
    }

    my $command = "$pstamptool -addjob  -req_id $req_id -job_type $job_type"
                        . " -rownum $rownum -state stop -fault $fault";
    $command .= " -dbname $dbname" if $dbname;
    $command .= " -dbserver $dbserver" if $dbserver;

    if ($mode eq "list_job") { 
        # this is a debugging mode, just print the pstamptool that would have run
        # this is sort of like the mode -noupdate that some other tools support
        print "$command\n";
    } elsif (!$no_update) {
        # mode eq "queue_job"
        my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
            run(command => $command, verbose => $verbose);
        unless ($success) {
            print STDERR @$stderr_buf;
            # XXX TODO: now what? Should we mark the error state for the request?
            # should we keep going for other uris? If so how do we report that some
            # of the work that the request wanted isn't going to get done
            my_die("failed to queue job for request $req_id", $PS_EXIT_UNKNOWN_ERROR);
        }
    } else {
        print "skipping command: $command\n";
    }
}

sub get_run_id
{
    my $stage = shift;
    my $image = shift;

    if ($stage eq "raw") {
        return $image->{exp_id};
    } elsif ($stage eq "chip") {
        return $image->{chip_id};
    } elsif ($stage eq "warp") {
        return $image->{warp_id};
    } elsif ($stage eq "stack") {
        return $image->{stack_id};
    } elsif ($stage eq "diff") {
        return $image->{diff_id};
    } else {
        my_die("unenexpected stage: $stage found", $PS_EXIT_PROG_ERROR);
    }
}

# extract components from the imageList that have the same run id and return the list
sub getOneRun {
    my $stage = shift;
    my $imageList = shift;

    # return if array is empty
    return undef if ! @$imageList;

    my $last_run_id = 0;
    my @runList;
    while ($imageList->[0]) {
        my $run_id = get_run_id($stage, $imageList->[0]);

        last if ($last_run_id and ($run_id ne $last_run_id));

        my $image = shift @$imageList;
        $image->{stage} = $stage;
        push @runList, $image;
        $last_run_id = $run_id;
    }
    return \@runList;
}

sub same_images_of_interest {
    my $r1 = shift;
    my $r2 = shift;

    return 0 if ($r1->{PROJECT}  ne $r2->{PROJECT});
    return 0 if ($r1->{JOB_TYPE} ne $r2->{JOB_TYPE});
    return 0 if ($r1->{REQ_TYPE} ne $r2->{REQ_TYPE});
    return 0 if ($r1->{IMG_TYPE} ne $r2->{IMG_TYPE});
    return 0 if ($r1->{ID} ne $r2->{ID});
    return 0 if ($r1->{TESS_ID} ne $r2->{TESS_ID});
    return 0 if ($r1->{REQFILT} ne $r2->{REQFILT});
    return 0 if ($r1->{LABEL} ne $r2->{LABEL});
    return 0 if ($r1->{MJDMIN} ne $r2->{MJDMAX});
    return 0 if ($r1->{MJDMAX} ne $r2->{MJDMAX});
    return 0 if ($r1->{inverse} ne $r2->{inverse});

    if (defined($r1->{COMPONENT})) {
        return 0 if !defined $r2->{COMPONENT} or ($r1->{COMPONENT} ne $r2->{COMPONENT});
    } elsif (defined($r2->{COMPONENT})) {
        # if first row has no component all of the images will be retrieved, so
        # the fact that this row has a component is ok. Fall through to return 1
        # XXX Nope this doesn't work. It is consistent with the other logic in some way
        # that i haven't fully thought through
        return 0;
    }

    return 1;
}

sub validNumber
{
    my $val = shift;

    return $val =~ /^([+-]?)(?=\d|\.\d)\d*(\.\d*)?([Ee]([+-]?\d+))?$/;
}

sub validROI
{
    my $row = shift;
    return 0 if !validNumber($row->{CENTER_X});
    return 0 if !validNumber($row->{CENTER_Y});
    return 0 if !validNumber($row->{WIDTH});
    return 0 if !validNumber($row->{HEIGHT});

    return 1;
}

sub findRow
{
    my $rownum = shift;
    my $rowList = shift;

    foreach my $row (@$rowList) {
        return $row if $row->{ROWNUM} eq $rownum;
    }
    return undef;
}

sub my_die
{
    my $msg = shift;
    my $fault = shift;

    carp $msg;

    # we don't fault the request here pstamp_parser_run.pl handles that if necessary

    return $fault;
}
