The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!/usr/bin/env perl

use strict;
use warnings;
use feature qw/state say/;
use 5.010;

use Getopt::Declare;
use Finnigan;

my $args = new Getopt::Declare q{
  [strict]
  [mutex: -h -w]
  [mutex: -a -r]
  [mutex: -header -n]
  -header			extract the log header (record structure)
  -d[ump]			dump the requested feature with file seek addresses
  -a[ll]			detailed dump of all field descriptors [requires: -d]
  -s[ize]			print record size [requires: -d]
  -n[unmber] <n:0+n>		extract the log entry number <n>
  -h[tml]			format as html
  -w[iki]			format as a wiki table
  -r[elative]			show relative addersess in the dump [requires: -d]
  <file>			input file [required]
}
  or exit(-1);

my $file = $args->{"<file>"};
-e $file or die "file '$file' does not exist";
-f $file or die "'$file' is not a plain file";
-s $file or die "'$file' has zero size";

# -----------------------------------------------------------------------------
open INPUT, "<$file" or die "can't open '$file': $!";
binmode INPUT;

my $file_header = Finnigan::FileHeader->decode(\*INPUT);
my $seq_row = Finnigan::SeqRow->decode(\*INPUT, $file_header->version);
my $cas_info = Finnigan::CASInfo->decode(\*INPUT);
my $rfi = Finnigan::RawFileInfo->decode(\*INPUT, $file_header->version);

my $run_header_addr = $rfi->preamble->run_header_addr;

# fast-forward to RunHeader
seek INPUT, $run_header_addr, 0;
my $run_header = Finnigan::RunHeader->decode(\*INPUT, $file_header->version);
my $first_scan = $run_header->sample_info->first_scan;
my $last_scan = $run_header->sample_info->last_scan;
my $params_addr = $run_header->params_addr;

# The following two structures must be skipped in order to reach the
# scan parameters header.

# Skip to the error log and read through
seek INPUT, $run_header->error_log_addr, 0;
my $error_log_length = Finnigan::Decoder->read(\*INPUT, ['length' => ['V', 'UInt32']])->{data}->{length}->{value};
foreach my $i ( 0 .. $error_log_length - 1) {
  Finnigan::Error->decode(\*INPUT);
}
# Read through the scan event hierarchy
my $nsegs = Finnigan::Decoder->read(\*INPUT, ['nsegs' => ['V', 'UInt32']])->{data}->{nsegs}->{value};
foreach my $i ( 0 .. $nsegs - 1) {
  my $n = Finnigan::Decoder->read(\*INPUT, ['n' => ['V', 'UInt32']])->{data}->{n}->{value};
  foreach my $j ( 0 .. $n - 1) {
    Finnigan::ScanEventTemplate->decode(\*INPUT, $file_header->version);
  }
}

# now at the start of the ScanParameters header (GenericDataHeader)
my $header = Finnigan::GenericDataHeader->decode(\*INPUT);

if ( $args->{'-header'} ) {
  if ( exists $args->{-d} ) {
    if ( exists $args->{-s} ) {
      my $size = $header->size;
      say "size: $size";
    }
    if ( exists $args->{-a}) {
      if ( exists $args->{-h} ) {
        $header->dump(style => 'html', filter => ['n']);
        foreach my $i (0 .. $header->n - 1) {
          $header->field($i)->dump(style => 'html', header => undef);
        }
      }
      elsif ( exists $args->{-w} ) {
        $header->dump(style => 'wiki', filter => ['n']);
        foreach my $i (0 .. $header->n - 1) {
          $header->field($i)->dump(style => 'wiki', header => undef);
        }
      }
      else {
        $header->dump(relative => exists $args->{-r}, filter => ['n']);
        foreach my $i (0 .. $header->n - 1) {
          $header->field($i)->dump(relative => exists $args->{-r});
        }
      }
    }
    else {
      if ( exists $args->{-h} ) {
        $header->dump(style => 'html', relative => exists $args->{-r});
      }
      elsif ( exists $args->{-w} ) {
        $header->dump(style => 'wiki', relative => exists $args->{-r});
      }
      else {
        $header->dump(relative => exists $args->{-r});
      }
    }
  }
  else {
    foreach my $i (0 .. $header->n - 1) {
      say $header->field($i)->type
        . "\t" . $header->field($i)->length
          . "\t" . $header->field($i)->label;
    }
  }
}
else {
  # Do the records. With the header on hand, skip to the ScanParameters stream
  seek INPUT, $params_addr, 0;

  my $record;
  my @range = ($first_scan - 1, $last_scan - 1);
  my @selected = @range;
  if ( exists $args->{-n}) {
    my $n = $args->{-n}{"<n>"};
    if ($n - 1 < $range[0] or $n - 1 > $range[-1]) {
      say STDERR "Record number $n is out of range [$first_scan .. $last_scan]";
      exit -1;
    }
    @selected = ($n - 1) x 2;
  }
  foreach my $i ( $range[0] .. $range[1] ) {
    exit if $i > $selected[1];
    $record = Finnigan::ScanParameters->decode(\*INPUT, $header->field_templates);
    if ( $i >= $selected[0] and $i <= $selected[1] ) {
      my $j = 0;
      if ( exists $args->{-d} ) {
        if ( exists $args->{-s} ) {
          my $size = $record->size;
          say "size: $size";
        }
        if ( exists $args->{-h} ) {
          $record->dump(style => 'html', relative => exists $args->{-r});
        }
        elsif ( exists $args->{-w} ) {
          $record->dump(style => 'wiki', relative => exists $args->{-r});
        }
        else {
          $record->dump(relative => exists $args->{-r});
        }
      }
      else {
        if ( exists $args->{-w} ) {
          say "|| seq || label || value ||";
          foreach my $key (@{$header->labels}) {
            say "|| " . ($i + 1)
              . " || " . $key
                . " || " . $record->{data}->{$key}->{value}
                  . " ||";
            $j++;
          }
        }
        else {
          foreach my $key (@{$header->labels}) {
            say $i + 1
              . "\t" . $key
                . "\t" . $record->{data}->{$key}->{value};
            $j++;
          }
        }
      } # if / if not -d
    } # if in range
  } # for each record
}

__END__
=head1 NAME

uf-params - list or dump the ScanParameters records in a Finnigan raw file

=head1 SYNOPSIS

uf-params [options] file

 Options:

  -header              extract the log header (record structure)
  -d[ump]              dump the requested feature showing file seek addresses
  -a[ll]               detailed dump of all field descriptors [requires: -d]
  -s[ize]              print record size [requires: -d]
  -n[unmber] <n:0+n>   extract the log entry number <n>
  -h[tml]              format as html
  -w[iki]              format as a wiki table
  -r[elative]          show relative addersess in the dump [requires: -d]
  <file>               input file [required]

=head1 OPTIONS

=over 4

=item B<-help>

Print a brief help message and exit.

=item B<-d[ump]>

Prints a table listing all fields in the requested object (a log entry
or the file header), with their seek addresses, sizes, names and
values. Individual entries can be selected with the B<-n[umber]>
option.

=item B<-n[umber]>

Gives the number of a single InstrumentLogRecord to extract

=item B<-h[tml]>

Format the dump output as an html table. When multiple entries are
specified, each will be rendered in its own table

=item B<-w[iki]>

Format the dump output as a wiki table.

=item B<-s[ize]>

Show structure size in bytes (works with the -d[ump] option).

=item B<-r[elative]>

Show relative addresses of all itmes in the dump. The default is to
show the absolute seek address. (works with the -d[ump] option)

=item B<-a[ll]>

Dump all GenericDataDescriptor entries in the file header (requires B<-header>)

=back

=head1 DESCRIPTION

B<uf-params> can be used to examine the ScanParameters? records in a
Finnigan raw file. These records contain a miscellany of data
pertaining to a single scan: ion injection time, retention time,
charge state and M/z of the precursor ion, M/z conversion
coefficients, and other data.

=head1 SEE ALSO

Finnigan::ScanParameters

=head1 EXAMPLES

=over 4

=item List all ScanParameters? records in the tabular form: <record
number, label, value>

  uf-params sample.raw

=item Print the parameters record for the fifth scan:

  uf-params -n 5 sample.raw

=item Dump the fifth ScanParameters? record in wiki format with total size and relative addresses:

  uf-params -dswr -n 5 sample.raw

=item Print the contents of the stream header in the tabular form: <type, length, label>

  uf-params -header sample.raw

=item Dump the header in the compact wiki format, with a stringified GenericDataDescriptor list:

  uf-params -header -dw sample.raw

=item Dump the header in the extended wiki format, showing the
location of echa GenericDataDescriptor's element:

  uf-params -header -daw sample.raw

=back

=cut