###
###     PStamp/Job.pm
###     subroutines and constants related to Postage Stamp Jobs
###

package PS::IPP::PStamp::Job;

use strict;
use warnings;

our $VERSION = '1.0';

use base qw( Exporter );

our @EXPORT_OK = qw( 
                    locate_images
                    resolve_project
                    );
our %EXPORT_TAGS = (standard => [@EXPORT_OK]);


use IPC::Cmd 0.36 qw( can_run run );
use PS::IPP::Metadata::List qw( parse_md_list );
use PS::IPP::Config qw( :standard );
use Carp;

### my @images = locate_images($image_db, $req_type, $img_type, $id, $component,
###            $mjd_min, $mjd_max, $filter);

sub locate_images {
    my $ipprc    = shift;   # required
    my $image_db = shift;   # required
    my $req_type = shift;   # required
    my $img_type = shift;   # required
    my $id       = shift;   # required unless req_type eq bycoord or byskycell
    my $component= shift;   # class_id or skycell_id
    my $inverse  = shift;
    my $x        = shift;
    my $y        = shift;
    my $mjd_min  = shift;
    my $mjd_max  = shift;
    my $filter   = shift;
    my $verbose  = shift;


    # we die in response to bad data in request files
    die "Unknown req_type: $req_type" if ($req_type ne "byid") and ($req_type ne "byexp")
                                        and ($req_type ne "bycoord") and ($req_type ne "bydiff");

    if (($req_type eq "byid") and ($img_type eq "diff")) {
        # lookups of all of the information for diff images requires a two level lookup to
        # get the exposure information. Switching the req_type allows us to keep that code
        # in one place
        $req_type = "bydiff";
        my $results = lookup_diff($ipprc, $image_db, $id, $component, 1, $inverse, $img_type, $verbose);
        return $results;
    }

    if ($req_type eq "bydiff") {
        my $results = lookup_diff($ipprc, $image_db, $id, $component, 0, $inverse, $img_type, $verbose);
        if (!$results) {
            return undef;
        }
        if ($img_type eq "diff") {
            # lookup_diff has done all of the work
            return $results;
        } 

        my $image = $results->[0];
        if (($img_type eq "raw") or ($img_type eq "chip")) {
            $req_type = "byexp";
            $id = $image->{exp_name};
            return undef if !$id;
            # fall through and lookup byexp
        } elsif ($img_type eq "warp") {
            $req_type = "byid";
            $id = $image->{warp_id};
            $component = $image->{skycell_id};
            return undef if !$id;
            # fall through and lookup by warp_id
        } elsif ($img_type eq "stack") {
            $req_type = "byid";
            $id = $image->{stack_id};
            return undef if !$id;
            # fall though and lookup by stack_id
        } else {
            # shouldn't I check this elsewhere?
            print STDERR "Error: $img_type is an unknown image type\n";
            return undef;
        }

    } elsif ($req_type eq "bycoord") {

        # run
        # regtool -dbname $image_db -processedimfile -time_begin $mjd_min -time_end = $mjd_max -filter $filter
        #
        my $results = lookup_bycoord($ipprc, $image_db, $x, $y, $mjd_min, $mjd_max, $filter, $verbose);

        # now take the results and lookup byexp
        # XXX: This needs work. What stops results from returning multiple images????
        # We should loop over the set of images returned building up the full results array
        $req_type = "byexp";
        $id = $results->{exp_name};
    }

    my $results = lookup($ipprc, $image_db, $req_type, $img_type, $id, $component, $verbose);

    return $results;
}

sub lookup {
    my $ipprc    = shift;
    my $image_db = shift;
    my $req_type = shift;
    my $img_type = shift;
    my $id       = shift;
    my $component= shift;
    my $verbose  = shift;

    my $missing_tools;
    my $regtool = can_run('regtool') or (warn "Can't find regtool" and $missing_tools = 1);
    my $chiptool = can_run('chiptool') or (warn "Can't find chiptool" and $missing_tools = 1);
    my $warptool = can_run('warptool') or (warn "Can't find warptool" and $missing_tools = 1);
    my $difftool = can_run('difftool') or (warn "Can't find difftool" and $missing_tools = 1);
    my $stacktool = can_run('stacktool') or (warn "Can't find stacktool" and $missing_tools = 1);
    if ($missing_tools) {
        warn("Can't find required tools.");
        exit ($PS_EXIT_CONFIG_ERROR);
    }
    my $command;
    my $id_opt;     # option for the lookup
    my $image_name;
    my $mask_name;
    my $weight_name;
    my $base_name;
    my $want_astrom;
    my $set_class_id;
    my $class_id;
    my $skycell_id;

    # special class_id value "null" means ignore
    if ($component and ($component eq "null")) {
        $component = undef;
    }

    if ($img_type eq "raw") {
        $class_id = $component;
        $command = "$regtool -processedimfile -dbname $image_db";
        $id_opt = "-exp_id";
        $command .= " -class_id $class_id" if $component;
        $want_astrom = 1;
        $set_class_id = 1;
    } elsif ($img_type eq "chip") {
        $class_id = $component;
        $command = "$chiptool -processedimfile -dbname $image_db";
        $command .= " -class_id $class_id" if $class_id;
        $id_opt = "-chip_id";
        $image_name   = "PPIMAGE.CHIP";
        $mask_name    = "PPIMAGE.CHIP.MASK";
        $weight_name  = "PPIMAGE.CHIP.VARIANCE";
        $base_name    = "path_base"; # name of the field for the chiptool output
        $want_astrom  = 1;
        $set_class_id = 1;
    } elsif ($img_type eq "warp") {
        $skycell_id = $component;
        $command = "$warptool -warped -dbname $image_db";
        $command .= " -skycell_id $skycell_id" if $skycell_id;
        $id_opt = "-warp_id";
        $image_name   = "PSWARP.OUTPUT";
        $mask_name    = "PSWARP.OUTPUT.MASK";
        $weight_name  = "PSWARP.OUTPUT.VARIANCE";
        $base_name    = "path_base"; # name of the field for the warptool output
    } elsif ($img_type eq "diff") {
        $skycell_id = $component;
        $command = "$difftool -diffskyfile -dbname $image_db";
        $command .= " -skycell_id $skycell_id" if $skycell_id;
        $id_opt = "-diff_id";
        $image_name  = "PPSUB.OUTPUT";
        $mask_name   = "PPSUB.OUTPUT.MASK";
        $weight_name = "PPSUB.OUTPUT.VARIANCE";
        $base_name   = "path_base";
    } elsif ($img_type eq "stack") {
        $command = "$stacktool -sumskyfile -dbname $image_db";
        $id_opt = "-stack_id";

        $image_name  = "PPSTACK.OUTPUT";
        $mask_name   = "PPSTACK.OUTPUT.MASK";
        $weight_name = "PPSTACK.OUTPUT.VARIANCE";
        $base_name   = "path_base";
    } else {
        die "Unknown img_type supplied: $img_type";
    }

    if ($req_type eq "byid") {
        $command .= " $id_opt $id";
    } elsif ($req_type eq "byexp") {
        $command .= " -exp_name $id";
    } else {
        die "Unknown req_type supplied: $req_type";
    }

    # run the tool and parse the output
    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
                run(command => $command, verbose => $verbose);
    unless ($success) {
        # not sure if we should die here
        print STDERR @$stderr_buf;
        return undef;
    }

    my $buf = join "", @$stdout_buf;
    if (!$buf) {
        return;
    }

    my $mdcParser = PS::IPP::Metadata::Config->new;
    my $images = parse_md_fast($mdcParser, $buf)
        or die ("Unable to parse metadata config doc");

    my $output = [];

    my $camera;
    foreach my $image (@$images) {
        my $base;

        next if $image->{fault};
        next if ($img_type ne "raw") and $image->{quality};

        if ($base_name) {
            $base = $image->{$base_name};
            # if base is undef then the output pruducts for this image are incomplete
            # This may only happen for warps. (Actually the test for ignored that I added
            # just above probably solved the problem I was trying to solve with this test)
            next if !$base;
        } else {
            # raw images don't have path_base yet. Manufacture one
            my $uri = $image->{uri};
            my $path_base = $uri =~ s/\.fits//g;
            $image->{$path_base} = $path_base;
        }
        if (!$camera) {
            # This assumes that all images have the same camera
            $camera = $image->{camera};
            $ipprc->define_camera($camera);
        }
        my $out = $image;

        # if uri is nil this will get overridded below
        # (this is for raw stage)
        $out->{image}  = $image->{uri};
        if ($set_class_id) {
            $class_id = $image->{class_id};
            $out->{component} = $class_id;
        }

        # find the mask and weight images
        if ($base) {
            $out->{image}  = $ipprc->filename($image_name,   $base, $class_id) if $image_name;
            $out->{mask}   = $ipprc->filename($mask_name,   $base, $class_id) if $mask_name;
            $out->{weight} = $ipprc->filename($weight_name, $base, $class_id) if $weight_name;
        }
        $out->{astrom} = find_astrometry($ipprc, $image_db, $image, $verbose) if $want_astrom;

        push @$output, $out;
    }

    return $output;
}
sub lookup_diff {
    my $ipprc    = shift;
    my $image_db = shift;
    my $id       = shift;
    my $skycell_id = shift;
    my $byid     = shift;
    my $inverse  = shift;
    my $img_type = shift;
    my $verbose = shift;

    my $missing_tools;
    my $difftool = can_run('difftool') or (warn "Can't find difftool" and $missing_tools = 1);
    if ($missing_tools) {
        warn("Can't find required tools.");
        exit ($PS_EXIT_CONFIG_ERROR);
    }

    my $command = "$difftool -diffskyfile -dbname $image_db";
    
    if ($byid) {
        $command .= " -diff_id $id";
    } else {
        $command .= " -diff_skyfile_id $id";
    }
    $command .= " -skycell_id $skycell_id" if $skycell_id;

    # run the tool and parse the output
    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
                run(command => $command, verbose => $verbose);
    unless ($success) {
        # not sure if we should die here
        print STDERR @$stderr_buf;
        return undef;
    }

    my $buf = join "", @$stdout_buf;
    if (!$buf) {
        return undef;
    }

    my $mdcParser = PS::IPP::Metadata::Config->new;
    my $images = parse_md_fast($mdcParser, $buf)
        or die ("Unable to parse metadata config doc");

    my $n = @$images;
    if (!$byid && ($n > 1)) {
        die ("difftool returned an unexpected number of diffskyfiles: $n");
    } elsif ($n == 0) {
        return undef;
    }

    my @results;
    foreach my $image (@$images) {
        my $skycell_id = $image->{skycell_id};

        if ($image->{fault}) {
            print STDERR "selected difference image $id $image->{diff_id} $skycell_id has fault: $image->{fault}\n";
            next;
        }

        # The standard way to do a diff is warp - stack 
        # so we interpret the requested image in that way

        my $stack_id = $image->{stack2};
        # XXX difftool currently returns max long long for null
        # this line is ready if we switch the code to return zero for null
        if ($stack_id and ($stack_id != 9223372036854775807)) {
            $image->{stack_id} = $stack_id;
        }

        my ($warp_id, $exp_id, $exp_name);
        if ($inverse and !$image->{bothways}) {
            print STDERR "Inverse images requested for diffRun that is not bothways. Ignoring.\n";
            $inverse = 0;
        }
        if ($inverse) {
            $warp_id =  $image->{warp2};
            $exp_id = $image->{exp_id_2};
            $exp_name = $image->{exp_name_2};
        } else {
            $warp_id =  $image->{warp1};
            $exp_id = $image->{exp_id_1};
            $exp_name = $image->{exp_name_1};
        }
        # XXX difftool currently returns max long long for null
        # this line is ready if we switch the code to return zero for null
        if ($warp_id and ($warp_id != 9223372036854775807)) {
            $image->{warp_id} = $warp_id;
            $image->{exp_id} = $exp_id;
            $image->{exp_name} = $exp_name;
        } else {
            print STDERR "unexpected result warp_id not defined\n";
            next;
        }
        if ($img_type eq "diff") {
            # the $image is going to be returned directly in this case so we need to duplicate
            # some of processing that lookup does for other img_types
            if ($image->{camera}) {
                $ipprc->define_camera($image->{camera});
                my $filerule_base = $inverse ? "PPSUB.INVERSE" : "PPSUB.OUTPUT";
                $image->{image}  = $ipprc->filename($filerule_base, $image->{path_base});
                $image->{mask}   = $ipprc->filename($filerule_base . ".MASK", $image->{path_base});
                $image->{weight} = $ipprc->filename($filerule_base . ".VARIANCE", $image->{path_base});
            } else {
                # XXX this will only happen if the minuend is not a warp
                print STDERR "WARNING: cannot resolve camera so cannot get weight and mask images\n";
                next;
            }
        }
        push @results, $image;
    }
    return \@results;
}

sub lookup_bycoord {
    my $ipprc    = shift;
    my $image_db = shift;
    my $x        = shift;
    my $y        = shift;
    my $mjd_min  = shift;
    my $mjd_max  = shift;
    my $filter   = shift;
    my $verbose  = shift;

    my $missing_tools;
    my $regtool = can_run('regtool') or (warn "Can't find regtool" and $missing_tools = 1);
    if ($missing_tools) {
        warn("Can't find required tools.");
        exit ($PS_EXIT_CONFIG_ERROR);
    }

    # XXX TODO:
    # Full lookups by coordinate require looking at the DVO database. However, 
    # dateobs and filter which is all that MOPS has asked for.

    my $args;
    if ($mjd_min) {
        my $dateobs_min = mjd_to_dateobs($mjd_min);
        $args .= " -dateobs_begin $dateobs_min";
    }
    if ($mjd_max) {
        my $dateobs_max = mjd_to_dateobs($mjd_max);
        $args .= " -dateobs_end $dateobs_max";
    }
    if ($filter) {
        $args .= " -filter $filter";
    }

    if (!$args) {
        # avoid returning every exposure in the DB
        print STDERR "no query arguments provided for bycoord\n";
        return undef;
    }

    # XXX TODO: This query doesn't work Something appears to be out of
    # sync about the times I'm passing and what regtool and or the DB expect
    # the query I used in the C version of locateimages used
    # WHERE dateobs >= FROM_UNIXTIME(%ld) AND dateobs <= FROM_UNIXTIME(%ld + exp_time)

    my $command = "$regtool -processedexp -dbname $image_db $args";

    # run the tool and parse the output
    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
                run(command => $command, verbose => $verbose);
    unless ($success) {
        # not sure if we should die here
        print STDERR @$stderr_buf;
        return undef;
    }
    my $mdcParser = PS::IPP::Metadata::Config->new; # Parser for metadata config files

    my $output = join "", @$stdout_buf;
    if (!$output) {
        print STDERR "no output returned from $command\n" if $verbose;
        return undef;
    }
    my $images = parse_md_fast($mdcParser, $output) or die ("Unable to parse metadata config doc");

    return $images;
}

# cache of results of ppConfigDump
my %astromSources;
my $last_exp_id = 0;
my $lastAstromFile;

# find the astrometry file for a given exposure
# return undef if no completed camRun exists
sub find_astrometry {
    my $ipprc = shift;
    my $image_db = shift;
    my $image = shift;      # hashref to output of the lookup tool
    my $verbose = shift;

    my $exp_id = $image->{exp_id};
    if (($exp_id eq $last_exp_id) and $lastAstromFile) {
        # running camtool 60 times is really expensive when the answer is the same
        return $lastAstromFile;
    }
    $last_exp_id = 0;
    $lastAstromFile = undef;

    my $missing_tools;
    my $camtool = can_run("camtool") or (warn "Can't find camtool" and $missing_tools = 1);
    my $ppConfigDump = can_run('ppConfigDump') or (warn "Can't find ppConfigDump" and $missing_tools = 1);
    if ($missing_tools) {
        warn("Can't find required tools.");
        exit ($PS_EXIT_CONFIG_ERROR);
    }

    my $command = "$camtool -dbname $image_db -processedexp -exp_id $exp_id";
    # run the tool and parse the output
    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
                run(command => $command, verbose => $verbose);
    unless ($success) {
        print STDERR @$stderr_buf;
        return undef;
    }

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

    my $output = join "", @$stdout_buf;
    if (!$output) {
        print STDERR "no output returned from $command\n" if $verbose;
        return undef;
    }
    my $camruns = parse_md_fast($mdcParser, $output);
    if (!$camruns) {
        return undef;
    }

    # If there are multiple cam runs for this exposure, take the last one
    # assuming that it has the best astrometry 
    my $camdata = pop @$camruns;
    if (!$camdata) {
        # no cam runs for this exposure id therefore best astrometry is whatever is in the header
        return undef;
    }
    my $camRoot = $camdata->{path_base};
    my $camera = $image->{camera};
    my $astromSource = $astromSources{$camera};
    if (!$astromSource) {
       my $command = "$ppConfigDump -camera $camera -dump-recipe PSWARP -";
        my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
            run(command => $command, verbose => $verbose);
        unless ($success) {
            $error_code = (($error_code >> 8) or $PS_EXIT_PROG_ERROR);
            die("Unable to perform ppConfigDump: $error_code");
        }
        my $metadata = $mdcParser->parse(join "", @$stdout_buf) or
            die("Unable to parse metadata config doc");
        $astromSource = metadataLookupStr($metadata, 'ASTROM.SOURCE');
        $astromSources{$camera} = $astromSource;
    }

    my $astromFile = $ipprc->filename($astromSource, $camRoot);
    if ($astromFile) {
        $lastAstromFile = $astromFile;
        $last_exp_id = $exp_id;
    }

    return $astromFile;
}

# splits meta data config input stream into single units to work around the pathalogically
# slow parser. This is similar to and adapted from code in various ippScripts.
sub parse_md_fast {
    my $mdcParser = shift;
    my $input = shift;
    my $output = ();

    my @whole = split /\n/, $input;
    my @single = ();

    my $n;
    while ( ($n = @whole) > 0) {
        my $value = shift @whole;
        push @single, $value;
        if ($value =~ /^\s*END\s*$/) {
	    push @single, "\n";

            my $list = parse_md_list( $mdcParser->parse( join("\n", @single ) ) ) or
                print STDERR "Unable to parse metdata config doc" and return undef;
#            my $num = @$list;
#            print STDERR "list has $num elments\n";
            push @$output, $list->[0];

            @single = ();
        }
    }
    return $output;
}

sub mjd_to_dateobs {
    my $mjd = shift;

    my $ticks = ($mjd - 40587.0) * 86400;

    my ($sec, $min, $hr, $day, $mon, $year) = gmtime($ticks);

    return sprintf "'%4d-%02d-%02d %02d:%02d:%02d'", $year+1900, $mon+1, $day, $hr, $min, $sec;
}

# resolve_project()
# get project specific information
my $last_project = "";
sub resolve_project {
    my $ipprc = shift;
    my $project_name = shift;

    if (!$project_name) {
        carp ("project is not defined");
        return undef;
    }

    if ($project_name eq $last_project) {
        return $last_project;
    }

    my $dbname = shift;
    my $dbserver = shift;

    my $verbose = 0;

    my $missing_tools;
    my $pstamptool = can_run("pstamptool") or (warn "Can't find pstamptool" and $missing_tools = 1);
    if ($missing_tools) {
        warn("Can't find required tools.");
        exit ($PS_EXIT_CONFIG_ERROR);
    }

    my $command = "$pstamptool -project -name $project_name";
    $command .= " -dbname $dbname" if defined $dbname;
    $command .= " -dbserver $dbserver" if defined $dbserver;
    # run the tool and parse the output
    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
                run(command => $command, verbose => $verbose);
    unless ($success) {
        print STDERR @$stderr_buf;
        return undef;
    }

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

    my $output = join "", @$stdout_buf;
    if (!$output) {
        print STDERR "no output returned from $command\n" if $verbose;
        return undef;
    }
    my $proj_hash = parse_md_fast($mdcParser, $output);

    $last_project = $proj_hash->[0];

    return $last_project;
}
1;
