package Bnc;

# Perl utility functions for BNC
#
# Revision: $Header: tags/BNC_2.13.1/scripts/Bnc.pm 10388 2024-03-12 14:10:17Z stuerze $

use strict;
use warnings;
use File::Basename;
use File::Spec::Functions qw(catfile);
use File::Temp qw(tempfile);
use Exporter;
use Time::Piece 1.30;
use PDL::Lite;    # to avoid namespace pollution
use PDL::Primitive;
use Log::Log4perl qw(:easy);

# use List::MoreUtils qw(uniq); # Prototype mismatch with PDL (uniq)

# =============================================================================
# callBnc ($bnc_ini, %$opts_ref)
# =============================================================================
# Call BNC
#
# Param  : $bnc_ini  [optional] The BNC config file that should be used. If not set, the default config is ised.
#          $opts_ref [required] Hash with BNC config options as key. They will overwrite the settings from the config file.
# Return : BNC exit status
# =============================================================================
sub callBnc {
    my ( $bnc, $bnc_ini, $opts ) = @_;

    my $config_file = "";
    if ( $bnc_ini && -s $bnc_ini ) {
        $config_file = "--conf $bnc_ini";
    }
    else {
        DEBUG("callBnc: Use default bnc-ini file");
    }

    my $opts_str = "";
    if ($opts) { $opts_str = options2string($opts) }

    #    my @cmd = (
    #                'xvfb-run',
    #                "--server-args='-screen 0, 1280x1024x8'",    # 1024x768x24
    #                "$bnc",
    #                "--nw",
    #                $opts_str,
    #    );

#my $rc = call_system("xvfb-run -a -e /home/user/xvfb.err --server-args='-screen 0 1280x1024x8' $bnc --nw $config_file $opts_str");
    return Common::runCmd("$bnc --nw $config_file $opts_str");
}

# Converts options map to string used for calling BNC.
sub options2string {
    my ($opts) = shift;

    my $opts_str = "";
    if ($opts) {
        foreach my $key ( keys %{$opts} ) {
            $opts_str .= "--key $key" . ' "' . $opts->{$key} . '" ';
        }
    }
    return $opts_str;
}

# =============================================================================
# parseMessageTypesFromFile
# =============================================================================
# Parse Message-Types with repetition rate from a BNC/scanRTCM logfile
#
# Param  : $logfile     [required] Path of the BNC/scanRTCM logfile
#          $caAbbr      [optional] caster name or abbreviation
#          $tmpPath     [optional] write working files to this path
#
# Return : Message-types for each mountpoint (as Hash-Ref)
#          Hash with mp as key and the list of messTypes as value
#          Example: $VAR1 = { 'RIO10' => ['1004(1)','1006(15)','1008(15)',...],
#                             'CLIB0' => ['1004(1)','1006(10)','1008(10)',...],
#                           };
# =============================================================================
sub parseMessageTypesFromFile {
    my ( $logfile, $caAbbr, $mytmpPath ) = @_;

    unless ( -s $logfile ) {
        ERROR "File [$logfile] is empty or does not exist";
        return;
    }

    my $tmp                = File::Temp->new( UNLINK => 1, SUFFIX => '.messtyps' );
    my $casterMessTypeFile = $tmp->filename;

    #my $caster = $logfilename =~ s/\.log\.messtyps//r;
    #INFO "Process caster '$caster'";

    INFO "Process $logfile";

    # scan beginns with that line: 16-02-18 23:58:02 WTZ37: Get data in RTCM 3.x format

    # First grep for message type lines in bnc-logfile and write them to local temp. directory
    # NOTE: 'sort -u' because Ephemeries message-types 1019 (GPS), 1020 (GLONASS), 1045 (Galileo) come
    #       for every sat. at the same time/second. This is done for getting the repetition rate
    system ("grep \"Received message type\" $logfile | sort -u > $casterMessTypeFile") == 0
      or ERROR "Fehler: $!";

    # --------------------------------------------
    # Get message types foreach station/mountpoint
    # --------------------------------------------
    my $cmd = "cat $casterMessTypeFile | awk '{print \$3, \$7}' | sort -u";
    my @messTypes = `$cmd`;    # ['DARX1: 1004\n','DARX1: 1006\n',... ]
    if ( scalar @messTypes < 1 ) {
        ERROR "Could not retrieve message types from file $casterMessTypeFile";
        return;
    }

    # Note: Skip first 1500 lines because BNC is weird here, all lines with same timestamp, buffer problem?

    # ------------------------------------------------------
    # Guess repetition rate for each mountpoint/message-type
    # ------------------------------------------------------
    # For that get the first $minSamples appearances of mountp with same message type
    # and build the differences between them
    my $minSamples = 4;
    foreach my $mpType (@messTypes) {
        chomp $mpType;    # 'AUBG3: 1004'
        my ( $mp, $mt ) = split ( /:\s*/, $mpType );
        my $cmd  = "grep \"$mp: Received message type $mt\" $logfile";
        my @rows = `$cmd`;
        if ( scalar @rows < 1 ) {
            ERROR "Could not retrieve message types for $mpType from file $casterMessTypeFile";
            next;
        }

        if ( scalar @rows <= $minSamples ) {
            WARN "Could not guess repetition rate for message type $mpType: only " . scalar @rows . " matches found";
            next;
        }

        my $repRate = _computeMessTypRepetitonRate( \@rows, $caAbbr );
        if ($repRate) {
            $mpType .= '(' . $repRate . ')';
        }
    }    # -----  end foreach messageType -----

    #if ( unlink $casterMessTypeFile ) { TRACE "Removed file [$casterMessTypeFile]" }

    # Return an HASH OF ARRAYS with mp as key and the list of messTypes as value
    my %messTypesHash;
    foreach (@messTypes) {
        my @ele = split ( ': ', $_ );
        my $mp  = shift @ele;
        push ( @{ $messTypesHash{$mp} }, shift @ele );
    }

    return \%messTypesHash;
}

# =============================================================================
# _computeMessTypRepetitonRate
# =============================================================================
# Guess repetition rate of message types
#
# Param  : $firstMatches [required] Array-Ref with first appearances of station
#          and mess type. Complete logfile lines, e.g.
#          '13-07-04 17:38:26 BRUX0: Received message type 1004 '
#          $caAbbr       [optional] caster abbreviation
# Return : repetition rate in secs, (median value)
# =============================================================================
sub _computeMessTypRepetitonRate {
    my ( $rows, $caAbbr ) = @_;

    my $maxGap = 600;    # if gap > 10min then we guess it is a new scan

    $rows->[0] =~ /.{18}([a-x0-9]+): Received message type (\d+)/i;
    my $stat  = $1;
    my $mestp = $2;

    # Create list of unix timestamps
    my @scans;
    my $scan = 0;
    my ( $prevTime, $deltaT ) = ( 0, 0 );
    foreach (@$rows) {
        my $uxtime = date2unix( substr ( $_, 0, 17 ) );
        if ( !$uxtime ) {
            ERROR "Could not parse date from line $_";
            next;
        }
        $deltaT = $uxtime - $prevTime;
        next if ( $deltaT == 0 );    # e.g. eph 1019,1020 one message for each sat.
        next if ( $deltaT <= 1 && $mestp =~ /10(19|20|42|43|44|45|46)|63/ );
        if ( $prevTime && $deltaT > $maxGap ) {
            $scan++;
        }
        push ( @{ $scans[$scan] }, $uxtime );
        $prevTime = $uxtime;
    }

    my @repRates;
    my $highest_nof_diffs = 0;
    foreach (@scans) {
        my @timestamps = @{$_};

        # Compute the differences
        my @diffs;
        for ( my $i = 1; $i <= $#timestamps; $i++ ) {
            push ( @diffs, $timestamps[$i] - $timestamps[ $i - 1 ] );
        }
        my $nof_diffs = scalar @diffs;

        if ( $nof_diffs < 2 ) {
            WARN("$stat: $mestp: only $nof_diffs diffs");
            next;
        }

        my ( $mean, $prms, $median, $min, $max, $adev, $rms_n ) = stats( pdl \@diffs );
        $mean  = sprintf ( "%.0f",  $mean );
        $rms_n = sprintf ( "%.02f", $rms_n );
        print $stat, ": ", $mestp, ": ", join ( ' ', @diffs ), "[Sig: $mean, $rms_n]\n";

        if ( $rms_n > 10 ) {
            WARN("$stat: $mestp: RMS too high: $rms_n");
            next;
        }

        # get the most frequent value
        my %counti = ();
        $counti{$_}++ foreach (@diffs);
        my ( $ni, $mfv ) = ( 0, 0 );
        while ( my ( $k, $v ) = each %counti ) {
            if ( $v > $ni ) {
                $ni  = $v;
                $mfv = $k;
            }
        }

        my $rounded_val = $mfv;                                  # init
        foreach ( ( 1, 5, 10, 15, 30, 60, 120, 150, 300 ) ) {    # most likely values
            my $mdiff = abs ( $mfv - $_ );
            if ( $mdiff <= 2 ) {
                $rounded_val = $_;
            }
        }
        push ( @repRates, [ $rounded_val, $nof_diffs ] );

        if ( $nof_diffs > $highest_nof_diffs ) {
            $highest_nof_diffs = $nof_diffs;
        }
    }    # -----  end foreach scan -----

    my @mostLikelyRates = grep { $_->[1] == $highest_nof_diffs } @repRates;
    my $mostLikelyRate  = $mostLikelyRates[0]->[0];
    foreach (@repRates) {
        if ( abs ( $_->[0] - $mostLikelyRate ) > 2 ) {
            ERROR "$stat: $caAbbr: $mestp: repetition rates from different scans differ: $mostLikelyRate $_->[0]";
            if ( scalar @repRates == 2 ) {
                return;
            }
        }
    }

    return $mostLikelyRate;
}

# =============================================================================
# parseConfig ($confFile)
# =============================================================================
# Parse the BNC config file.
#
# Param  : $confFile [required] BNC config file
# Return : Hash with configuration on success, otherwise undef
# Usage  : $bncConf = parseConf($bncConfFile);
#          $corrMount = $bncConf->{'PPP'}->{'corrMount'};
# =============================================================================
sub parseConfig {
    my ($confFile) = @_;

    -s $confFile || LOGDIE "BNC config file \"$confFile\" does not exist\n";
    TRACE "Parse BNC config file $confFile";
    open ( my $INP, '<', $confFile ) || die "Could not open file '$confFile': $!";
    my @confLines = <$INP>;
    close ($INP);

    my %conf;
    my $section;    # [General], [PPP]
    foreach (@confLines) {
        chomp;
        s/#.*//;     # entfernt Kommentare
        s/^\s*//;    # whitespace am Anfang entfernen
        s/\s+$//;    # entfernt alle whitespaces am Ende
        next unless length;
        if ( $_ =~ /\[(\S+)\]/ ) { $section = $1 }
        next if ( !$section );
        my ( $key, $val ) = split ( /\s*=\s*/, $_, 2 );
        if ( !defined $val ) { $val = "" }

        if ( $key eq "mountPoints" ) {

            # Simple parsing
            $val =~ s/^\/\///;
            my @mpts = split ( /,\s?\/{2}/, $val );
            $conf{$section}->{$key} = \@mpts;

            # Extended parsing
            my @mpts_def = ();
            foreach (@mpts) {

                #  user:passwd@igs-ip.net:2101/ASPA0 RTCM_3.0 ASM -14.33 189.28 no 1
                if ( $_ =~
                    /^([\w-]+):(.+[^@])@([\w\.-]+):(\d{3,5})\/([-\w]+) ([\w\.]+) ?(\w{3})? ([\+\-\d\.]+) ([\+\-\d\.]+) no (\w+)/i
                  )
                {
                    push ( @mpts_def, { caster => $3, port => $4, mp => $5, ntripVers => $10 } );
                }
                else { ERROR "$confFile: Could not parse mountPoints string $_" }
            }
            $conf{$section}->{'mountPoints_parsed'} = \@mpts_def;
        }
        elsif ( $key eq "cmbStreams" ) {
            my @cmbStrs = split ( /\s*,\s*/, $val );
            foreach (@cmbStrs) {
                s/"//g;
                s/\s+$//;    # entfernt alle whitespaces am Ende
            }
            $conf{$section}->{$key} = \@cmbStrs;
        }
        else { $conf{$section}->{$key} = $val }
    }

    my @nofPar = keys %conf;
    if ( scalar @nofPar < 1 ) {
        ERROR "No parameter found in BNC conf \"$confFile\"";
        return;
    }
    return \%conf;
}

# =============================================================================
# parseLogfile ($file, $sampling, $goBackSecs, $logMode )
# =============================================================================
# Parse BNCs' logfile
#
# Param  : $file       [required] BNC logfile
#          $sampling   [optional] sampling rate for logfile
#          $logMode    [optional] Flag. If set, remember the position of the file-read
#                      for the next read. Default: off
# Return : \%data
# =============================================================================
sub parseLogfile {
    my $file     = shift;
    my $sampling = shift // 1;
    my $logMode  = shift // 0;

    open ( my $fh, "<", $file ) || LOGDIE "Could not open file $file: $!\n";

    # Goto last position from last read
    #my $fPos = filePosition($file);
    #TRACE "Current file pos: $fPos";
    $logMode && seek ( $fh, filePosition($file), 0 );

    #$logMode && seek ( $fh, $fPos, 0 );
    my $ln = "";
    my ( @hlp, @epochs, @latencies, @restarts );
    my $rec = {};
    while (<$fh>) {
        chomp ( $ln = $_ );
        $rec = {};

        if ( $ln =~ /\bNEU/ ) {    # NEU displacements
            @hlp = split ( /\s+/, $ln );
            my $tp = Time::Piece->strptime( substr ( $hlp[2], 0, 19 ), '%Y-%m-%d_%H:%M:%S' );

            if ( $hlp[14] eq '-nan' || $hlp[15] eq '-nan' || $hlp[16] eq '-nan' ) {
                WARN("$hlp[2] $hlp[3]: NEU displacements are NAN");
                next;
            }

            #DEBUG ($tp->epoch, $hlp[3]);
            push (
                   @epochs,
                   {
                     time => $tp->epoch,
                     site => $hlp[3],
                     dN   => $hlp[14],
                     dE   => $hlp[15],
                     dU   => $hlp[16],
                     TRP  => $hlp[18] + $hlp[19],
                   }
            );
        }
        elsif ( index ( $ln, "latency", 25 ) > 1 ) {

# DEBUG ($ln);
# altes format:      15-10-06 15:29:02 POTS0: Mean latency 2.34 sec, min 1.58, max 3, rms 0.48, 43 epochs, 17 gaps
# neu in BNC 2.12.4: 17-06-06 15:35:02 OHI37 Observations: Mean latency 1.51 sec, min 0.57, max 2.7, rms 0.5, 203 epochs, 73 gaps
            @hlp = split ( /\s+/, $ln );

            # old latency log format
            if ( $hlp[2] =~ /:$/ ) {
                splice @hlp, 3, 0, 'Placeholder:';
                $hlp[2] =~ s/:$//;
            }
            $hlp[3] =~ s/:$//;

            my $tp = Time::Piece->strptime( "$hlp[0] $hlp[1]", '%y-%m-%d %H:%M:%S' );
            $rec = {
                     time    => $tp->epoch,
                     mp      => $hlp[2],
                     meanLat => $hlp[6] + 0.0,
                     epochs  => int ( $hlp[14] ),
                     type    => $hlp[3]
            };

            # Unter bestimmten Bedingungen werden die gaps nicht rausgeschrieben!
            if ( $ln =~ /gaps/ ) {
                $rec->{'gaps'} = int ( $hlp[16] );
            }

            push ( @latencies, $rec );
        }
        elsif ( index ( $ln, "Start BNC" ) > 1 ) {

            # 17-06-13 07:06:58 ========== Start BNC v2.12.3 (LINUX) ==========
            @hlp = split ( /\s+/, $ln );
            my $tp = Time::Piece->strptime( "$hlp[0] $hlp[1]", '%y-%m-%d %H:%M:%S' );
            push (
                   @restarts,
                   {
                     time    => $tp->epoch,
                     bncvers => $hlp[5]
                   }
            );
        }

    }    # -----  next line  -----

    $logMode && filePosition( $file, tell ($fh) );    # Remember pos for next read
    close $fh;

    # Sampling must be done afterwords, for each station separated!
    my @epochs_sampled;
    my @sites = map { $_->{'site'} } @epochs;

    #@sites = uniq @sites;
    my %hlp1 = ();
    @sites = grep { !$hlp1{$_}++ } @sites;
    foreach my $s (@sites) {
        my $epoch_selected = 0;
        foreach my $rec (@epochs) {
            next if ( $rec->{'site'} ne $s );
            if ( $rec->{'time'} - $epoch_selected >= $sampling ) {
                push ( @epochs_sampled, $rec );
                $epoch_selected = $rec->{'time'};
            }
        }
    }

    my %data = (
                 EPOCHS    => \@epochs_sampled,
                 LATENCIES => \@latencies,
                 RESTARTS  => \@restarts
    );

    return \%data;
}

# =============================================================================
# parsePPPLogfile ($file, $sampling, $goBackSecs, $logMode )
# =============================================================================
# Parse BNCs' PPP station logfile
#
# Param  : $file       [required] BNC PPP station
#          $sampling   [optional] sampling rate for logfile
#          $goBackSecs [optional] go back that seconds from now in logfile
#          $logMode    [optional] Flag. If set, remember the position of the file-read
#                      for the next read. Default: off
# Return : $station, \%data
# =============================================================================
sub parsePPPLogfile {
    my $file       = shift;
    my $sampling   = shift // 1;
    my $goBackSecs = shift // 0;
    my $logMode    = shift // 0;

    if ($logMode) { $goBackSecs = 0 }

    my $startSec;
    if ($goBackSecs) {
        $startSec = time () - $goBackSecs;
    }
    my $epo;
    my $old_epochSec = 0;
    my $epochSec     = 0;
    my $epochDiff    = 0;
    my ( @hlp, @N, @E, @U, %SATNUM, @TRP, @CLK, @OFF_GPS, @OFF_GLO, @OFF_GAL, @OFF_BDS);
    my ( @EPOCH, @EPOCH_CLK, @EPOCH_OFF_GPS, @EPOCH_OFF_GLO, @EPOCH_OFF_GAL, @EPOCH_OFF_BDS );
    my ( %AMB,        %RES,            %ELE,            %ION, %BIA );
    my ( $station,    $lki,            $sys,            $sat, $amb );
    open ( my $fh, "<", $file ) || LOGDIE "Could not open file $file: $!\n";

    # Goto last position from last read
    #my $fPos = filePosition($file);
    #TRACE "Current file pos: $fPos";
    $logMode && seek ( $fh, filePosition($file), 0 );

    #$logMode && seek ( $fh, $fPos, 0 );
    my $ln = "";
    while (<$fh>) {
        chomp ( $ln = $_ );

        if ( $ln =~ /\bof Epoch\b/ ) {

            # PPP of Epoch 2015-08-27_14:00:15.000
            if ( $ln =~ /PPP of Epoch (\d{4}-\d{2}-\d{2}_\d{2}:\d{2}:\d{2})\.\d+/ ) {
                $epo = $1;    #print "$epo\n";
            }
            else { ERROR "strange line: \"$ln\""; next }

            my $tp = Time::Piece->strptime( $epo, '%Y-%m-%d_%H:%M:%S' );
            $epochSec  = $tp->epoch();
            $epochDiff = $epochSec - $old_epochSec;
            next;
        }

        next if ( !$epo );
        next if ( defined $startSec && $epochSec < $startSec );
        next if ( $epochDiff && $epochDiff < $sampling );

        @hlp = split ( /\s+/, $ln );

        if ( $ln =~ /\bdN\b/ ) {
            push ( @EPOCH, $epochSec );    # besser $epo ?
            $old_epochSec = $epochSec;

            #2015-08-27_13:59:50.000 DIEP1 X = 3842152.9054 +- 0.0242 Y = 563402.0331 +- 0.0176 Z = 5042888.5182 +- 0.0319 dN = 0.0130 +- 0.0193 dE = -0.0032 +- 0.0178 dU = -0.0248 +- 0.0349
            $station = $hlp[1];

            if ( $hlp[19] eq '-nan' || $hlp[24] eq '-nan' || $hlp[29] eq '-nan' ) {
                WARN("$hlp[0] $station: NEU displacements are NAN");
            }

            push @N, $hlp[19];
            push @E, $hlp[24];
            push @U, $hlp[29];
        }
        elsif ( ( $ln =~ /\bAMB\b/ ) && ( $ln !~ /RESET/ ) ) {

            # 2015-08... AMB lIF G04 253.0000 -8.9924 +- 1.7825 el = 22.03 epo = 86
            $lki = $hlp[2];
            $sat = $hlp[3];
            $sys = substr ( $sat, 0, 1 );
            $amb = $hlp[4] + $hlp[5];
            push @{ $AMB{$lki}{$sys}{$sat}{EPOCH} },  $epochSec;
            push @{ $AMB{$lki}{$sys}{$sat}{DATA} },   $amb;
            push @{ $AMB{$lki}{$sys}{$sat}{NUMEPO} }, $hlp[13];
            push @{ $ELE{$sys}{$sat}{EPOCH} }, $epochSec;
            push @{ $ELE{$sys}{$sat}{DATA} },  $hlp[10];
        }
        elsif ( $ln =~ /\bRES\b/ && $ln !~ /Neglected/ ) {

            # 2015-08... RES lIF G30  -0.0076
            $sat = $hlp[3];
            $lki = $hlp[2];
            $sys = substr ( $sat, 0, 1 );

            #print "$epo $lki $sys $sat $res\n";
            push @{ $RES{$lki}{$sys}{$sat}{EPOCH} }, $epochSec;
            push @{ $RES{$lki}{$sys}{$sat}{DATA} },  $hlp[4];
        }
        elsif ( ( $ln =~ /\bION\b/ ) && ( $ln !~ /RESET/ ) ) {

            # 2018-12-01_20:37:58.000 ION      G02     0.0000    -0.3277 +-   2.4663
            $sat = $hlp[2];
            $sys = substr ( $sat, 0, 1 );
            push @{ $ION{$sys}{$sat}{EPOCH} }, $epochSec;
            push @{ $ION{$sys}{$sat}{DATA} },  $hlp[4];
        }
        elsif ( ( $ln =~ /\bBIA\b/ ) && ( $ln !~ /RESET/ ) ) {

            # 2020-12-09_00:55:19.000 BIA  c1  G       0.0000    +2.5149 +-   9.6543
            $lki = $hlp[2];
            $sys = $hlp[3];
            push @{ $BIA{$lki}{$sys}{EPOCH} }, $epochSec;
            push @{ $BIA{$lki}{$sys}{DATA} },  $hlp[4] + $hlp[5];
        }
        elsif ( $ln =~ /\bREC_CLK\b/ ) {
            push ( @EPOCH_CLK, $epochSec );
            push ( @CLK,       $hlp[2] + $hlp[3] );
        }
        elsif ( $ln =~ /\bOFF_GPS\b/ ) {                # 2015-08... OFF_GPS 52.6806 -3.8042 +- 9.0077
            push ( @EPOCH_OFF_GPS, $epochSec );
            push ( @OFF_GPS,       $hlp[2] + $hlp[3] );
        }
        elsif ( $ln =~ /\bOFF_GLO\b/ ) {                # 2015-08... OFF_GLO 52.6806 -3.8042 +- 9.0077
            push ( @EPOCH_OFF_GLO, $epochSec );
            push ( @OFF_GLO,       $hlp[2] + $hlp[3] );
        }
        elsif ( $ln =~ /\bOFF_GAL\b/ ) {                # 2015-08... OFF_GAL 52.6806 -3.8042 +- 9.0077
            push ( @EPOCH_OFF_GAL, $epochSec );
            push ( @OFF_GAL,       $hlp[2] + $hlp[3] );
        }
        elsif ( $ln =~ /\bOFF_BDS\b/ ) {                # 2015-08... OFF_BDS 52.6806 -3.8042 +- 9.0077
            push ( @EPOCH_OFF_BDS, $epochSec );
            push ( @OFF_BDS,       $hlp[2] + $hlp[3] );
        }
        elsif ( $ln =~ /\bSATNUM\b/ ) {     # 2015-09... SATNUM G  8
            push ( @{ $SATNUM{ $hlp[2] } }, $hlp[3] );
        }
        elsif ( $ln =~ /\bTRP\b/ ) {        # 2015-08... TRP  2.3803 +0.1009 +- 0.0324
            push ( @TRP, $hlp[2] + $hlp[3] );
        }
    }    # -----  next line  -----

    $logMode && filePosition( $file, tell ($fh) );    # Remember pos for next read
    close $fh;

    my $nof_epochs = scalar @EPOCH;
    DEBUG(   "$station: epochs:$nof_epochs, North displac.: "
           . scalar @N
           . ", East displac.: "
           . scalar @E
           . ", Up displac.: "
           . scalar @U
           . ", TRP:"
           . scalar @TRP
           );
    if ( $nof_epochs != scalar @N )                              { LOGDIE "number of epochs and residuals not equal\n" }
    if ( $nof_epochs != scalar @TRP )                            { LOGDIE "number of epochs and TRP not equal\n" }
    if ( @CLK     && scalar @EPOCH_CLK     != scalar @CLK )      { LOGDIE "number of epochs and CLK not equal\n" }
    if ( @OFF_GPS && scalar @EPOCH_OFF_GPS != scalar @OFF_GPS )  { LOGDIE "number of epochs and OFF_GPS not equal\n" }
    if ( @OFF_GLO && scalar @EPOCH_OFF_GLO != scalar @OFF_GLO )  { LOGDIE "number of epochs and OFF_GLO not equal\n" }
    if ( @OFF_GAL && scalar @EPOCH_OFF_GAL != scalar @OFF_GAL )  { LOGDIE "number of epochs and OFF_GAL not equal\n" }
    if ( @OFF_BDS && scalar @EPOCH_OFF_BDS != scalar @OFF_BDS )  { LOGDIE "number of epochs and OFF_BDS not equal\n" }

    if ( !$station ) { WARN "could not grep stationname from file: $file\n" }

    my %data = (
                 EPOCH   => \@EPOCH,
                 N       => \@N,
                 E       => \@E,
                 U       => \@U,
                 SATNUM  => \%SATNUM,
                 TRP     => \@TRP,
                 CLK     => \@CLK,
                 OFF_GPS => \@OFF_GPS,
                 OFF_GLO => \@OFF_GLO,
                 OFF_GAL => \@OFF_GAL,
                 OFF_BDS => \@OFF_BDS,
                 RES     => \%RES,
                 AMB     => \%AMB,
                 ELE     => \%ELE,
                 ION     => \%ION,
                 BIA     => \%BIA,
    );

    return ( $station, \%data, 0 );
}

# =============================================================================
# BncStillWorks ($bncConfFile)
# =============================================================================
# Checks if BNC is still working.
#
# BNC Jobs can still be alive (in processlist) but are not producing any more.
# This function checks if a BNC process is proper working.
#
# Param  : $bncConfFile [required] path of BNC config file
# Return : true if BNC is still working otherwise false.
# =============================================================================
sub BncStillWorks {
    my ($bncConfFile) = @_;

    my $timep = Time::Piece->new;

    # for safety if it is exatly at 00:00, add 30 sec
    my $min_tmp = $timep->strftime("%M");
    if ( $min_tmp =~ /00|15|30|45/ && $timep->strftime("%S") < 15 ) {
        $timep += 30;
        sleep 30;
    }
    my $yyyy = $timep->year;
    my $yy   = $timep->yy;
    my $doy  = sprintf "%03d", $timep->yday + 1;
    my $hh   = $timep->strftime("%H");
    my $h    = uc ( chr ( 65 + $hh ) );
    my $min  = $timep->min;
    my $startmin;
    if    ( $min < 15 )  { $startmin = "00" }
    elsif ( $min < 30 )  { $startmin = "15" }
    elsif ( $min < 45 )  { $startmin = "30" }
    elsif ( $min <= 59 ) { $startmin = "45" }
    my $bncConf        = parseConf($bncConfFile);
    my $bncLogFileStub = $bncConf->{'General'}->{'logFile'};

    # BNC log file
    # ------------
    my $bncLogFile = "${bncLogFileStub}_" . $timep->strftime("%y%m%d");    # -> bnc.log_160425
    unless ( -s $bncLogFile ) {
        WARN("BNC logfile \"$bncLogFile\" is empty or does not exist");
        return 0;
    }

    # RINEX Obs Generation
    # --------------------
    if ( $bncConf->{'General'}->{'rnxPath'} ) {
        my $rnxPath = $bncConf->{'General'}->{'rnxPath'};
        $rnxPath =~ s/\/$//;

        # Write Rnx3 files (i.e. long Rnx3 filenames) 2: on ('rnxV3filenames' is deprecated since 2.12.8!!!)
        my $writeRnxV3 = $bncConf->{'General'}->{'rnxV3'};
        my $rnxIntr    = $bncConf->{'General'}->{'rnxIntr'};
        my $fileMask;

        if ($writeRnxV3) {
            if ( $rnxIntr eq "1 hour" ) {
                $fileMask = "*_S_${yyyy}${doy}${hh}??_01H_30S_?O.rnx";
            }
            elsif ( $rnxIntr eq "15 min" ) {
                $fileMask = "*_S_${yyyy}${doy}${hh}${startmin}_15M_01S_?O.rnx";
            }
            else {    # daily?
                $fileMask = "*_S_${yyyy}${doy}????_01D_30S_?O.rnx";    # HRAG00ZAF_S_20191220000_01D_30S_MO.rnx
            }
        }
        else {                                                         # Rnx2
            if ( $rnxIntr eq "1 hour" ) {
                $fileMask = "????${doy}${h}.${yy}O";
            }
            elsif ( $rnxIntr eq "15 min" ) {
                $fileMask = "????${doy}${h}${startmin}.${yy}O";
            }
            else {                                                     # daily?
                $fileMask = "????${doy}*.${yy}O";
            }
        }

        my @rnxFiles = glob "$rnxPath/$fileMask";
        if ( scalar @rnxFiles < 1 ) {
            ERROR("BNC does not create RINEX Obs files. (Filemask: \"$fileMask\" Path: $rnxPath)");

            #return 0;
        }
    }

    # RINEX Ephemerides Generation
    # ----------------------------
    if ( $bncConf->{'General'}->{'ephPath'} ) {
        my $rnxPath = $bncConf->{'General'}->{'ephPath'};
        $rnxPath =~ s/\/$//;
        my $writeRnxV3 = $bncConf->{'General'}->{'ephV3'};
        my $rnxIntr    = $bncConf->{'General'}->{'ephIntr'};
        my $fileMask;

        if ($writeRnxV3) {
            if ( $rnxIntr eq "1 hour" ) {
                $fileMask = "BRD?00WRD_S_${yyyy}${doy}${hh}00_01H_?N.rnx";
            }
            elsif ( $rnxIntr eq "15 min" ) {
                $fileMask = "BRD?00WRD_S_${yyyy}${doy}${hh}${startmin}_15M_?N.rnx"; # BRDC00WRD_S_20191220900_15M_MN.rnx
            }
            else {                                                                  # daily?
                $fileMask = $fileMask = "BRD?00WRD_S_${yyyy}${doy}0000_01D_?N.rnx";
            }
        }
        else {                                                                      # Rnx2
            $fileMask = "BRD?${doy}*.${yy}N";
        }

        my @rnxFiles = glob "$rnxPath/$fileMask";
        if ( scalar @rnxFiles < 1 ) {
            ERROR("BNC does not create RINEX Nav files. (Filemask: \"$fileMask\" Path: $rnxPath)");

            #return 0;
        }
    }

    # Check jobs making PPP
    # ---------------------
    if ( $bncConf->{'PPP'}->{'corrMount'} && $bncConf->{'PPP'}->{'staTable'} ) {
        my $timeOfLastCoo = `grep "NEU:" $bncLogFile | tail -1 | cut -d ' ' -f1,2`;
        chomp $timeOfLastCoo;
        if ( !$timeOfLastCoo ) {
            ERROR "BNC does not compute coordinates";
            return 0;
        }

        my $tp    = Time::Piece->strptime( $timeOfLastCoo, '%y-%m-%d %H:%M:%S' );
        my $now   = Time::Piece->new;
        my $tdiff = $now - $tp;
        if ( $tdiff > 1200 ) {
            ERROR( "Last computed coordinates are " . $tdiff / 60 . " min old" );
            return 0;
        }
    }

    # BNC works
    return 1;
}

1;    # End of Bnc
