#!/usr/bin/env perl
#
#   Copyright (c) International Business Machines  Corp., 2002,2012
#
#   This program is free software;  you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation; either version 2 of the License, or (at
#   your option) any later version.
#
#   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.  See the GNU
#   General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with this program;  if not, see
#   <http://www.gnu.org/licenses/>.
#
#
# genhtml
#
#   This script generates HTML output from .info files as created by the
#   geninfo script. Call it with --help and refer to the genhtml man page
#   to get information on usage and available options.
#
#
# History:
#   2002-08-23 created by Peter Oberparleiter <Peter.Oberparleiter@de.ibm.com>
#                         IBM Lab Boeblingen
#        based on code by Manoj Iyer <manjo@mail.utexas.edu> and
#                         Megan Bock <mbock@us.ibm.com>
#                         IBM Austin
#   2002-08-27 / Peter Oberparleiter: implemented frame view
#   2002-08-29 / Peter Oberparleiter: implemented test description filtering
#                so that by default only descriptions for test cases which
#                actually hit some source lines are kept
#   2002-09-05 / Peter Oberparleiter: implemented --no-sourceview
#   2002-09-05 / Mike Kobler: One of my source file paths includes a "+" in
#                the directory name.  I found that genhtml.pl died when it
#                encountered it. I was able to fix the problem by modifying
#                the string with the escape character before parsing it.
#   2002-10-26 / Peter Oberparleiter: implemented --num-spaces
#   2003-04-07 / Peter Oberparleiter: fixed bug which resulted in an error
#                when trying to combine .info files containing data without
#                a test name
#   2003-04-10 / Peter Oberparleiter: extended fix by Mike to also cover
#                other special characters
#   2003-04-30 / Peter Oberparleiter: made info write to STDERR, not STDOUT
#   2003-07-10 / Peter Oberparleiter: added line checksum support
#   2004-08-09 / Peter Oberparleiter: added configuration file support
#   2005-03-04 / Cal Pierog: added legend to HTML output, fixed coloring of
#                "good coverage" background
#   2006-03-18 / Marcus Boerger: added --custom-intro, --custom-outro and
#                overwrite --no-prefix if --prefix is present
#   2006-03-20 / Peter Oberparleiter: changes to custom_* function (rename
#                to html_prolog/_epilog, minor modifications to implementation),
#                changed prefix/noprefix handling to be consistent with current
#                logic
#   2006-03-20 / Peter Oberparleiter: added --html-extension option
#   2008-07-14 / Tom Zoerner: added --function-coverage command line option;
#                added function table to source file page
#   2008-08-13 / Peter Oberparleiter: modified function coverage
#                implementation (now enabled per default),
#                introduced sorting option (enabled per default)
#   April/May 2020 / Henry Cox/Steven Dovich - Mediatek, inc
#                Add support for differential line coverage categorization,
#                date- and owner- binning.
#   June/July 2020 / Henry Cox - Mediatek, inc
#                Add support for differential branch coverage categorization,
#                Add a bunch of navigation features - href to next code block
#                of type T, of type T in date- or owner bin B, etc.
#                Add sorted tables for date/owner bin summaries.
#   October 2020 / Henry Cox - Mediatek, inc
#                Add "--hierarchical" display option.
#

use strict;
use warnings;

use File::Basename;
use File::Copy;
use File::Path;
use File::Spec;
use File::Temp;
use Scalar::Util qw/looks_like_number/;
use Digest::MD5 qw(md5_base64);
use Cwd qw/abs_path realpath cwd/;
use DateTime;
#use Regexp::Common qw(time);  # damn - not installed
use Date::Parse;
use FileHandle;
use Carp;
use Storable;
use Time::HiRes;    # for profiling
use POSIX;
use Data::Dumper;

use lib "/usr/lib/lcov";
use lcovutil qw (set_tool_name define_errors parse_ignore_errors
                 $tool_name $tool_dir $lcov_version $lcov_url
                 ignorable_error
                 $ERROR_MISMATCH $ERROR_SOURCE $ERROR_BRANCH $ERROR_FORMAT
                 $ERROR_EMPTY $ERROR_VERSION $ERROR_UNUSED $ERROR_PACKAGE
                 $ERROR_CORRUPT $ERROR_NEGATIVE $ERROR_COUNT $ERROR_UNSUPPORTED
                 $ERROR_DEPRECATED $ERROR_INCONSISTENT_DATA $ERROR_CALLBACK
                 $ERROR_RANGE $ERROR_PATH
                 $ERROR_PARALLEL $ERROR_CHILD report_parallel_error
                 report_exit_status
                 summarize_messages
                 $br_coverage $func_coverage
                 info $verbose init_verbose_flag debug $debug $devnull
                 parseOptions
                 strip_directories
                 parse_cov_filters summarize_cov_filters
                 $FILTER_BRANCH_NO_COND $FILTER_LINE_CLOSE_BRACE @cov_filter
                 rate get_overall_line $default_precision check_precision
                 die_handler warn_handler parse_w3cdtf);

# Global constants
our $title = "LCOV - differential code coverage report";
lcovutil::set_tool_name(basename($0));

our $debugScheduler = 0;

# if false, then keep track of only enough information to be able to
# produce a valid HTML report.
#   - Do not keep track of everything - say, to enable serialize/deserialize
#     of the complete coverage DB.
#   - In practice: this means to throw away the 'FileDetails' structure after
#     the source file HTML has been constructed.  'FileDetails' is the lion's
#     share of the memory footprint.
# This has the effect of reducing memory footprint and improving parallel
# performance.
our $buildSerializableDatabase = 0;

# Specify coverage rate limits (in %) for classifying file entries
# HI:   $hi_limit <= rate <= 100          graph color: green
# MED: $med_limit <= rate <  $hi_limit    graph color: orange
# LO:          0  <= rate <  $med_limit   graph color: red

# For line coverage/all coverage types if not specified
our $hi_limit  = 90;
our $med_limit = 75;

# For line coverage
our $ln_hi_limit;
our $ln_med_limit;

# For function coverage
our $fn_hi_limit;
our $fn_med_limit;

# For branch coverage
our $br_hi_limit;
our $br_med_limit;

# For MC/DC coverage
our $mcdc_hi_limit;
our $mcdc_med_limit;

# Width of overview image
our $overview_width = 80;

# Resolution of overview navigation: this number specifies the maximum
# difference in lines between the position a user selected from the overview
# and the position the source code window is scrolled to.
our $nav_resolution = 4;

# Clicking a line in the overview image should show the source code view at
# a position a bit further up so that the requested line is not the first
# line in the window. This number specifies that offset in lines.
our $nav_offset = 10;

# Clicking on a function name should show the source code at a position a
# few lines before the first line of code of that function. This number
# specifies that offset in lines.
our $func_offset = 2;

our $overview_title = "top level";

# Width for line coverage information in the source code view
our $line_field_width = 12;

# Width for branch coverage information in the source code view
our $br_field_width = 16;

# Width for MC/DC coverage information in the source code view
our $mcdc_field_width = 14;

# Width for owner name in the source code view
our $owner_field_width = 20;

# Width for block age in the source code view
our $age_field_width = 5;

# Width for TLA entry in the source code view
our $tla_field_width = 3;

# Internal Constants

# Header types
our $HDR_DIR      = 0;
our $HDR_FILE     = 1;
our $HDR_SOURCE   = 2;
our $HDR_TESTDESC = 3;
our $HDR_FUNC     = 4;

# Sort types
our $SORT_FILE   = 0;
our $SORT_LINE   = 1;
our $SORT_FUNC   = 2;
our $SORT_BRANCH = 3;
our $SORT_MCDC   = 4;
# function detail sort types
our $SORT_MISSING_LINE   = 5;    # by number of not-hit lines in function
our $SORT_MISSING_BRANCH = 6;    # by number of not-hit branches in function
our $SORT_MISSING_MCDC = 7; # by number of not-hit MC/DC expressions in function

# Fileview heading types
our $HEAD_NO_DETAIL     = 1;
our $HEAD_DETAIL_HIDDEN = 2;
our $HEAD_DETAIL_SHOWN  = 3;

# Additional offsets used when converting branch coverage data to HTML
our $BR_LEN   = -3;
our $BR_OPEN  = -2;         # 2nd last element
our $BR_CLOSE = -1;         # last  element

# Data related prototypes
sub print_usage(*);
sub gen_html();
sub html_create($$);
sub process_file($$$$$);
sub compute_title($$);
sub get_prefix($@);
sub shorten_prefix($);
sub get_relative_base_path($);
sub read_testfile($);
sub get_date_string($);
sub remove_unused_descriptions();
sub get_affecting_tests($$$$);
sub apply_prefix($@);
sub get_html_prolog($);
sub get_html_epilog($);
#sub write_dir_page($$$$$$$;$);
sub write_summary_pages($$$$$$$$);
sub classify_rate($$$$);
sub parse_dir_prefix(@);

# HTML related prototypes
sub escape_html($);
sub escape_id($);
sub get_bar_graph_code($$$);

sub write_png_files();
sub write_htaccess_file();
sub write_css_file();
sub write_description_file($$);
sub write_function_table(*$$$$$$$$$$$$);

sub write_html(*$);
sub write_html_prolog(*$$);
sub write_html_epilog(*$;$);

sub write_header(*$$$$$$$);
sub write_header_prolog(*$);
sub write_header_line(*@);
sub write_header_epilog(*$);

sub write_file_table(*$$$$$$);
sub write_file_table_prolog(*$$$@);
sub write_file_table_entry(*$$@);
sub write_file_table_detail_entry(*$$$$@);
sub write_file_table_epilog(*);

sub write_test_table_prolog(*$);
sub write_test_table_entry(*$$);
sub write_test_table_epilog(*);

sub write_source($$$$$$$$);
sub write_source_prolog(*$$$);
sub write_source_line(*$$$$$$$);
sub write_source_epilog(*);

sub write_frameset(*$$$);
sub write_overview_line(*$$$);
sub write_overview(*$$$$);

# External prototype (defined in genpng)
sub gen_png($$$$$@);

sub simplify_function_name($);

package SummaryInfo;

our @selectCallbackScript;
our $selectCallback;
our @cleanDirectoryList;

our @tlaPriorityOrder = ("UNC",
                         "LBC",
                         "UIC",
                         "UBC",

                         "GBC",
                         "GIC",
                         "GNC",
                         "CBC",

                         "EUB",
                         "ECB",
                         'ECC',
                         'EUC',
                         "DUB",
                         "DCB",);

# this list doesn't contain the non-displaying elements
our @tlaOrderNoExclude = ("UNC",
                          "LBC",
                          "UIC",
                          "UBC",

                          "GBC",
                          "GIC",
                          "GNC",
                          "CBC",

                          "EUB",
                          "ECB",
                          "DUB",
                          "DCB",);

our %tlaLocation = ("UNC" => 1,
                    "LBC" => 3,
                    "UIC" => 3,
                    "UBC" => 3,

                    "GBC" => 3,
                    "GIC" => 3,
                    "GNC" => 1,
                    "CBC" => 3,

                    "EUB" => 3,
                    "ECB" => 3,
                    'ECC' => 1,
                    'EUC' => 1,
                    "DUB" => 2,
                    "DCB" => 2,);

our %tlaToTitle = ("UNC" => "Uncovered New Code (+ => 0):\n" .
                       "Newly added code is not tested",
                   "LBC" => "Lost Baseline Coverage (1 => 0):\n" .
                       "Unchanged code is no longer tested",
                   "UIC" => "Uncovered Included Code (# => 0):\n" .
                       "Previously unused code is untested",
                   "UBC" => "Uncovered Baseline Code (0 => 0):\n" .
                       "Unchanged code was untested before, is untested now",

                   "GBC" => "Gained Baseline Coverage (0 => 1):\n" .
                       "Unchanged code is tested now",
                   "GIC" => "Gained Included Coverage (# => 1):\n" .
                       "Previously unused code is tested now",
                   "GNC" => "Gained New Coverage (+ => 1):\n" .
                       "Newly added code is tested",
                   "CBC" => "Covered Baseline Code (1 => 1):\n" .
                       "Unchanged code was tested before and is still tested",

                   "EUB" => "Excluded Uncovered Baseline (0 => #):\n" .
                       "Previously untested code is unused now",
                   "ECB" => "Excluded Covered Baseline (1 => #):\n" .
                       "Previously tested code is unused now",
                   "EUC" => "Excluded Uncovered Current:\n" .
                       "Untested current coverpoint is excluded",
                   "ECC" => "Excluded Covered Current:\n" .
                       "Tested current code is excluded",
                   "DUB" => "Deleted Uncovered Baseline (0 => -):\n" .
                       "Previously untested code has been deleted",
                   "DCB" => "Deleted Covered Baseline (1 => -):\n" .
                       "Previously tested code has been deleted",);

our %tlaToLegacy = ("UNC" => "Missed",
                    "GNC" => "Hit",);

our %tlaToLegacySrcLabel = ("UNC" => "MIS",
                            "GNC" => "HIT",);

our @defaultCutpoints = (7, 30, 180);
our @cutpoints;
our @ageGroupHeader;
our %ageHeaderToBin;

our @truncateOwnerTableLevels;    # default: truncate everywhere if enabled
our $ownerTableElements;          # default: do not truncate
our $compactSummaryTables = 1;    # on by default

use constant {
              TYPE          => 0,
              NAME          => 1,
              PARENT        => 2,
              RELATIVE_DIR  => 3,
              FULL_DIR      => 4,
              LINE_DATA     => 5,
              BRANCH_DATA   => 6,
              MCDC_DATA     => 7,
              FUNCTION_DATA => 8,

              FILE_DETAILS => 9, # SourceFile struct - only used for 'file' type
              SOURCES      => 9, # used by top and directory types
              IS_ABSOLUTE  => 10,    # used by directory type only

              # coverage data list for type
              DATA   => 0,
              AGE    => 1,
              OWNERS => 2,           # not used by Function coverage - no owner
};

sub type2str
{
    my $t = shift;
    return 'line' if ($t == LINE_DATA);
    return 'branch' if ($t == BRANCH_DATA);
    return 'MC/DC' if ($t == MCDC_DATA);
    die("unexpected type '$t'") unless ($t == FUNCTION_DATA);
    return 'function';
}

sub _initCounts
{
    my %hash;
    foreach my $key ('found', 'hit', 'GNC', 'UNC', 'CBC', 'GBC',
                     'LBC', 'UBC', 'ECB', 'EUB', 'GIC', 'UIC',
                     'DCB', 'DUB'
    ) {
        $hash{$key} = 0;
    }
    return \%hash;
}

sub noBaseline
{
    # no baseline - so we will have only 'UIC' and 'GIC' code
    #   legacy display order is 'hit' followed by 'not hit'
    @tlaPriorityOrder = ('GNC', 'UNC');
    %tlaToTitle = ('UNC' => 'Not Hit',
                   'GNC' => 'Hit',);
}

sub setAgeGroups
{
    #my $numGroups = scalar(@_) + 1;
    @cutpoints = sort({ $a <=> $b } @_);
    if (@ageGroupHeader) {
        # labels were specified by user
        @ageGroupHeader = split($lcovutil::split_char,
                                join($lcovutil::split_char, @ageGroupHeader));
        goto done
            if (scalar(@ageGroupHeader) == scalar(@cutpoints) + 1);
        # mismatched number - generate warning
        lcovutil::ignorable_error($lcovutil::ERROR_USAGE,
               "expected number of 'age' labels to match 'date-bin' cutpoints");
        # if message ignored, then assign default labels
    }
    @ageGroupHeader = ();
    my $prefix = "[..";
    foreach my $days (@cutpoints) {
        my $header = $prefix . $days . "] days";
        push(@ageGroupHeader, $header);
        $prefix = "(" . $days . ",";
    }
    push(@ageGroupHeader, "(" . $cutpoints[-1] . "..) days");
    done:
    %ageHeaderToBin = ();
    my $bin = 0;
    foreach my $header (@ageGroupHeader) {
        $ageHeaderToBin{$header} = $bin;
        ++$bin;
    }
}

sub findAgeBin
{
    my $age = shift;
    defined($age) or die("undefined age");
    my $bin;
    for ($bin = 0; $bin <= $#cutpoints; $bin++) {
        last
            if ($age <= $cutpoints[$bin]);
    }
    return $bin;
}

sub new
{
    my ($class, $type, $name, $is_absolute_dir) = @_;
    defined($name) || $type eq 'top' or
        die("SummaryInfo name should be defined, except at top-level");
    my $self = [$type, # 'type' expected to be one of 'file', 'directory', 'top'
                $name,
                undef,              # parent
                undef,              # relative dir,
                undef,              # full directory path,
                [undef, [], {}],    # line data
                [undef, [], {}],    # branch data
                [undef, [], {}],    # MC/DC data
                [undef, []]         # function data
    ];
    if ($type eq 'file') {
        $self->[FILE_DETAILS] = undef;    # will be SourceFile object
    } else {
        $self->[SOURCES]     = {};
        $self->[IS_ABSOLUTE] = $is_absolute_dir
            if ($type eq 'directory');
    }

    for my $type (LINE_DATA, BRANCH_DATA, MCDC_DATA, FUNCTION_DATA) {
        $self->[$type]->[DATA] = _initCounts();
        # no age data unless annotations are enabled
        next unless @SourceFile::annotateScript;
        my $ageList = $self->[$type]->[AGE];
        foreach my $i (0 .. $#cutpoints + 1) {
            my $h = _initCounts();
            $h->{_LB}    = ($i == 0) ? undef : $cutpoints[$i - 1];
            $h->{_UB}    = ($i == $#cutpoints + 1) ? undef : $cutpoints[$i];
            $h->{_INDEX} = $i;
            push(@$ageList, $h);
        }
    }

    bless $self, $class;

    return $self;
}

# deserialization:  copy the coverage portion of the undumped data
sub copyGuts
{
    my ($self, $that) = @_;
    my @copy = (RELATIVE_DIR, FULL_DIR, LINE_DATA, BRANCH_DATA,
                MCDC_DATA, FUNCTION_DATA);
    if ($that->[TYPE] eq 'file') {
        push(@copy, FILE_DETAILS);
    } elsif ($that->[TYPE] eq 'directory') {
        push(@copy, IS_ABSOLUTE);
    }
    for my $key (@copy) {
        $self->[$key] = $that->[$key];
    }
}

sub name
{
    my $self = shift;
    return $self->[NAME];
}

sub unsetDirs
{
    my $self = shift;
    # need to reset after fork failure - we set the values
    # just before forking, but now have to put them back.
    die('bad usage: unsetDirs()')
        unless (defined($self->[FULL_DIR]) &&
                defined($self->[RELATIVE_DIR]));
    $self->[FULL_DIR]     = undef;
    $self->[RELATIVE_DIR] = undef;
}

sub relativeDir
{
    my ($self, $dir_string) = @_;
    die("bad usage: relativeDir(" .
            (defined($dir_string) ? $dir_string : '') . ') current: '
            .
            (defined($self->[RELATIVE_DIR]) ? $self->[RELATIVE_DIR] : '<undef>')
        )
        unless ((!defined($dir_string) && defined($self->[RELATIVE_DIR])) ||
                (defined($dir_string) && !defined($self->[RELATIVE_DIR])));
    $self->[RELATIVE_DIR] = $dir_string
        if defined($dir_string);
    return $self->[RELATIVE_DIR];
}

sub fullDir
{
    my ($self, $dir_string) = @_;
    die("bad usage fullDir()")
        unless ((!defined($dir_string) && defined($self->[FULL_DIR])) ||
                (defined($dir_string) && !defined($self->[FULL_DIR])));
    $self->[FULL_DIR] = $dir_string
        if defined($dir_string);
    return $self->[FULL_DIR];
}

sub type
{
    my $self = shift;
    return $self->[TYPE];
}

sub is_directory
{
    my ($self, $is_absolute) = @_;
    return (
        $self->type() eq 'directory' ?
            ((defined($is_absolute) && $is_absolute) ? $self->[IS_ABSOLUTE] : 1)
        :
            0);
}

sub parent
{
    my $self = shift;
    return $self->[PARENT];
}

sub setParent
{
    my ($self, $parent) = @_;
    die("expected parent dir")
        unless (ref($parent) eq "SummaryInfo" &&
                ($main::flat ? 'top' : 'directory') eq $parent->type());
    $self->[PARENT] = $parent;
}

sub sources
{
    my $self = shift;
    die("bad usage") if $self->type() eq 'file';
    return keys(%{$self->[SOURCES]});
}

sub fileDetails
{
    my ($self, $data) = @_;
    $self->type() eq 'file' or die("source details only available for file");
    !(defined($data) && defined($self->[FILE_DETAILS])) or
        die("attempt to set details in initialized struct");
    !defined($data) || ref($data) eq 'SourceFile' or
        die("unexpected data arg " . ref($data));
    $self->[FILE_DETAILS] = $data
        if defined($data);
    return $self->[FILE_DETAILS];
}

sub get_sorted_keys
{
    # sort_type in ($SORT_FILE, $SORT_LINE, $SORT_FUNC, $SORT_BRANCH, $SORT_MCDC)
    my ($self, $sort_type, $include_dirs) = @_;
    die("invalid usage") if $self->type() eq 'file';

    my $sources = $self->[SOURCES];

    my @keys = $self->sources();
    my @l;
    foreach my $k (@keys) {
        my $data = $sources->{$k};
        next
            if ($data->type() eq 'directory' &&
                (!defined($include_dirs) ||
                 0 == $include_dirs));
        push(@l, $k);
    }
    if ($sort_type == $SORT_FILE) {
        # alphabetic
        return sort(@l);
    }
    my $covtype;
    if ($sort_type == $SORT_LINE) {
        # Sort by number of instrumented lines without coverage
        $covtype = LINE_DATA;
    } elsif ($sort_type == $SORT_FUNC) {
        # Sort by number of instrumented functions without coverage
        $covtype = FUNCTION_DATA;
    } elsif ($sort_type == $SORT_MCDC) {
        # Sort by number of MC/DC points without coverage
        $covtype = MCDC_DATA;
    } else {
        die("unexpected sort type $sort_type")
            unless ($sort_type == $SORT_BRANCH);
        # Sort by number of instrumented branches without coverage
        $covtype = BRANCH_DATA;
    }

    if ($main::opt_missed) {
        # sort by directory first then secondary key
        return
            sort({
                     my $da = $sources->{$a};
                     my $db = $sources->{$b};
                     # directories then files if list includes both
                     $da->type() cmp $db->type() or
                         $db->get_missed($covtype)
                         <=> $da->get_missed($covtype) or
                         # sort alphabetically in case of tie
                         $da->name() cmp $db->name()
            } @l);
    } else {
        return
            sort({
                     my $da = $sources->{$a};
                     my $db = $sources->{$b};
                     $da->type() cmp $db->type() or
                         $da->get_rate($covtype) <=> $db->get_rate($covtype) or
                         $da->name() cmp $db->name()
            } @l);
    }
}

sub get_source
{
    my ($self, $name) = @_;
    die("bad usage") if $self->type() eq 'file';
    return
        exists($self->[SOURCES]->{$name}) ? $self->[SOURCES]->{$name} : undef;
}

sub remove_source
{
    my ($self, $name) = @_;
    die("bad usage") if $self->type() eq 'file';
    delete $self->[SOURCES]->{$name};
}

sub get
{
    my ($self, $key, $type) = @_;
    $type = LINE_DATA
        if !defined($type);

    my $hash = $self->[$type]->[DATA];
    if ($key eq "missed") {
        my $missed = 0;
        foreach my $k ('UBC', 'UNC', 'UIC', 'LBC') {
            $missed += $hash->{$k}
                if (exists($hash->{$k}));
        }
        return $missed;
    } else {
        die("unexpected 'get' key $key")
            unless exists($hash->{$key});
        return $hash->{$key};
    }
}

# Return a relative value for the specified found&hit values
# which is used for sorting the corresponding entries in a
# file list.
#
sub get_rate
{
    my ($self, $covtype) = @_;

    my $hash  = $self->[$covtype]->[DATA];
    my $found = $hash->{found};
    my $hit   = $hash->{hit};

    if ($found == 0) {
        #return 100;
        return 1000;
    }
    #return (100.0 * $hit) / $found;
    return int($hit * 1000 / $found) * 10 + 2 - (1 / $found);
}

sub get_missed
{
    my ($self, $covtype) = @_;

    my $hash  = $self->[$covtype]->[DATA];
    my $found = $hash->{found};
    my $hit   = $hash->{hit};

    return $found - $hit;
}

sub contains_owner
{
    my ($self, $owner) = @_;
    return exists($self->[LINE_DATA]->[OWNERS]->{$owner});
}

sub owners
{
    # return possibly empty list of line owners in this file
    #   - filter only those which have 'missed' lines
    my ($self, $showAll, $covType) = @_;

    (!defined($covType) ||
     $covType == LINE_DATA   ||
     $covType == BRANCH_DATA ||
     $covType == MCDC_DATA) or
        die("unsupported coverage type '$covType'");

    my $hash = $self->[defined($covType) ? $covType : LINE_DATA]->[OWNERS];

    return keys(%$hash)
        if $showAll;

    my @rtn;
    OWNER:
    foreach my $name (keys(%$hash)) {
        my $h = $hash->{$name};
        foreach my $tla ('UNC', 'UBC', 'UIC', 'LBC') {
            if (exists($h->{$tla})) {
                die("unexpected 0 (zero) value for $tla of $name in " .
                    $self->path())
                    if (0 == $h->{$tla});
                push(@rtn, $name);
                next OWNER;
            }
        }
    }
    return @rtn;
}

sub owner_tlaCount
{
    my ($self, $name, $tla, $covType) = @_;
    die("$name not found in owner data for" . $self->path())
        unless exists($self->[LINE_DATA]->[OWNERS]->{$name});

    return 0    # not supported, yet
        if $covType == FUNCTION_DATA;

    (!defined($covType) ||
     $covType == LINE_DATA   ||
     $covType == BRANCH_DATA ||
     $covType == MCDC_DATA) or
        die("unsupported coverage type '$covType'");

    my $hash =
        $self->[defined($covType) ? $covType : LINE_DATA]->[OWNERS]->{$name};
    return $hash->{$tla}
        if (exists($hash->{$tla}));

    if ($tla eq "found") {
        my $total = 0;
        foreach my $k (keys(%$hash)) {
            # count only code that can be hit (ie., not excluded)
            $total += $hash->{$k}
                if ('EUB' ne $k &&
                    'ECB' ne $k);
        }
        return $total;
    } elsif ($tla eq "hit") {
        my $hit = 0;
        foreach my $k ('CBC', 'GBC', 'GIC', 'GNC') {
            $hit += $hash->{$k}
                if (exists($hash->{$k}));
        }
        return $hit;
    } elsif ($tla eq "missed") {
        my $missed = 0;
        foreach my $k ('UBC', 'UNC', 'UIC', 'LBC') {
            $missed += $hash->{$k}
                if (exists($hash->{$k}));
        }
        return $tla eq "missed" ? $missed : -$missed;
    }
    die("unexpected TLA $tla")
        unless exists($tlaLocation{$tla});
    return 0;
}

sub hasOwnerInfo
{
    my $self = shift;

    return %{$self->[LINE_DATA]->[OWNERS]} ? 1 : 0;
}

sub hasDateInfo
{
    my $self = shift;
    # we get date- and owner information at the same time from the
    #  annotation-script - so, if we have owner info, then we have date info too.
    return %{$self->[LINE_DATA]->[OWNERS]} ? 1 : 0;
}

sub findOwnerList
{
    # return [ [owner, lineCovData, branchCovData, functionCov]] for each owner
    #  where lineCovData = [missedCount, totalCount]
    #        branchCovData = [missed, total] or undef if not enabled
    #        functionCov = [missed, total] or undef if not enabled
    #   - sorted in descending order number of missed lines
    my ($self, $callback_type, $truncate_me, $all) = @_;

    my @owners;
    foreach my $owner (keys(%{$self->[LINE_DATA]->[OWNERS]})) {
        my $lineMissed = $self->owner_tlaCount($owner, 'missed', LINE_DATA);
        my $branchMissed =
            $lcovutil::br_coverage ?
            $self->owner_tlaCount($owner, 'missed', BRANCH_DATA) :
            0;
        my $funcMissed =
            $lcovutil::func_coverage ?
            $self->owner_tlaCount($owner, 'missed', FUNCTION_DATA) :
            0;
        my $mcdcMissed =
            $lcovutil::mcdc_coverage ?
            $self->owner_tlaCount($owner, 'missed', MCDC_DATA) :
            0;
        # filter owners who have unexercised code, if requested

        if ($all ||
            (0 != $lineMissed || 0 != $branchMissed || 0 != $funcMissed)) {
            my $lineCb   = OwnerDetailCallback->new($self, $owner, LINE_DATA);
            my $branchCb = OwnerDetailCallback->new($self, $owner, BRANCH_DATA);
            my $mcdcCb   = OwnerDetailCallback->new($self, $owner, MCDC_DATA);
            my $functionCb =
                OwnerDetailCallback->new($self, $owner, FUNCTION_DATA);
            my $lineTotal = $self->owner_tlaCount($owner, 'found', LINE_DATA);
            my $branchTotal =
                $lcovutil::br_coverage ?
                $self->owner_tlaCount($owner, 'found', BRANCH_DATA) :
                0;
            my $mcdcTotal =
                $lcovutil::mcdc_coverage ?
                $self->owner_tlaCount($owner, 'found', MCDC_DATA) :
                0;
            my $funcTotal =
                $lcovutil::func_coverage ?
                $self->owner_tlaCount($owner, 'found', FUNCTION_DATA) :
                0;
            push(@owners,
                 [$owner,
                  [$lineMissed, $lineTotal, $lineCb],
                  [$branchMissed, $branchTotal, $branchCb],
                  [$mcdcMissed, $mcdcTotal, $mcdcCb],
                  [$funcMissed, $funcTotal, $functionCb]
                 ]);
        }
    }
    @owners = sort({
                       $b->[1]->[0]     <=> $a->[1]->[0] ||    # missed
                           $b->[1]->[1] <=> $a->[1]->[1] ||    # then total
                           $a->[0] cmp $b->[0]
    } @owners);    # then by name
    my $truncated;
    if ($truncate_me                          &&
        defined($ownerTableElements)          &&
        $ownerTableElements < scalar(@owners) &&
        (0 == scalar(@truncateOwnerTableLevels) ||
            grep(/$callback_type/, @truncateOwnerTableLevels))
    ) {
        # don't truncate the 'primary' key owner table

        $truncated = (scalar(@owners) - $ownerTableElements);
        #lcovutil::info("truncating $truncated elements in header table\n");
        splice(@owners, $ownerTableElements);
    } else {
        $truncated = 0;
    }
    return (scalar(@owners) ? \@owners : undef, $truncated);
}

sub append
{
    my ($self, $record) = @_;

    # keep track of the records that get merged into me..
    defined($record->[NAME]) or
        die("attempt to append anonymous SummaryInfo record");
    !exists($self->[SOURCES]->{$record->[NAME]}) or
        die("duplicate merge record " . $record->[NAME]);
    $self->[SOURCES]->{$record->[NAME]} = $record;

    die($record->name() . " already has parent " . $record->parent()->name())
        if (defined($record->parent()) && $record->[PARENT] != $self);
    $record->[PARENT] = $self
        if !defined($record->parent());

    foreach my $group (LINE_DATA, FUNCTION_DATA, BRANCH_DATA, MCDC_DATA) {
        my $mine  = $self->[$group]->[DATA];
        my $yours = $record->[$group]->[DATA];
        while (my ($key, $value) = each(%$yours)) {
            $mine->{$key} += $yours->{$key};
        }
    }

    # there will be no date info if we didn't also collect owner data
    #   merge the date- and owner data, if we aren't going to display it
    #   (In future, probably want to serialize the data for future processing)
    if (%{$record->[LINE_DATA]->[OWNERS]}) {
        foreach my $covType (LINE_DATA, FUNCTION_DATA, BRANCH_DATA, MCDC_DATA) {
            for (my $bin = 0; $bin <= $#ageGroupHeader; ++$bin) {
                foreach my $key (keys %{$self->[$covType]->[DATA]}) {
                    # duplicate line-coverage buckets
                    my $ageval = $self->age_sample($bin);
                    if ($covType == LINE_DATA) {
                        $self->lineCovCount($key, "age", $ageval,
                                   $record->lineCovCount($key, "age", $ageval));
                    } elsif ($covType == BRANCH_DATA) {
                        $self->branchCovCount($key, "age", $ageval,
                                 $record->branchCovCount($key, "age", $ageval));
                    } elsif ($covType == MCDC_DATA) {
                        $self->mcdcCovCount($key, "age", $ageval,
                                   $record->mcdcCovCount($key, "age", $ageval));
                    } else {
                        $self->functionCovCount($key, 'age', $ageval,
                               $record->functionCovCount($key, "age", $ageval));
                    }
                }
            }

            my $ownerList = $self->[$covType]->[OWNERS];
            while (my ($name, $yours) = each(%{$record->[$covType]->[OWNERS]}))
            {
                if (!exists($ownerList->{$name})) {
                    $ownerList->{$name} = {};
                }
                my $mine = $ownerList->{$name};
                while (my ($tla, $count) = each(%$yours)) {
                    if (exists($mine->{$tla})) {
                        $mine->{$tla} += $count;
                    } else {
                        $mine->{$tla} = $count;
                    }
                }
            }
        }
    }
    return $self;
}

sub age_sample
{
    my ($self, $i) = @_;
    my $bin = $self->[LINE_DATA]->[AGE]->[$i];
    return ($i < $#ageGroupHeader) ? $bin->{_UB} : ($bin->{_LB} + 1);
}

sub lineCovCount
{
    my ($self, $key, $group, $age, $delta) = @_;

    $delta = 0 unless defined($delta);

    if ($key eq 'missed') {
        my $found = $self->lineCovCount('found', $group, $age);
        my $hit   = $self->lineCovCount('hit', $group, $age);
        return $found - $hit;
    }

    if ($group eq "age") {
        my $a   = $self->[LINE_DATA]->[AGE];
        my $bin = SummaryInfo::findAgeBin($age);
        exists($a->[$bin]) && exists($a->[$bin]->{$key}) or
            die("unexpected key '$key' for bin '$bin'");
        $a->[$bin]->{$key} += $delta;
        return $a->[$bin]->{$key};
    }

    my $d = $self->[$group]->[DATA];
    defined($d) or
        die("SummaryInfo::value: unrecognized group $group\n");
    defined($d->{$key}) or
        die("SummaryInfo::value: unrecognized key $key\n");

    $d->{$key} += $delta;
    return $d->{$key};
}

sub branchCovCount
{
    my ($self, $key, $group, $age, $delta) = @_;

    $delta = 0 unless defined($delta);

    if ($key eq 'missed') {
        my $found = $self->branchCovCount('found', $group, $age);
        my $hit   = $self->branchCovCount('hit', $group, $age);
        return $found - $hit;
    }

    my $branch = $self->[BRANCH_DATA];
    if ($group eq "age") {
        my $a   = $branch->[AGE];
        my $bin = SummaryInfo::findAgeBin($age);
        unless (exists($a->[$bin]) && exists($a->[$bin]->{$key})) {
	    # LCOV_UNREACHABLE_START
            lcovutil::ignorable_error($lcovutil::ERROR_INTERNAL,
                                      "unexpected key '$key' for bin '$bin'");
            return;
	    # LCOV_UNREACHABLE_STOP
        }
        $a->[$bin]->{$key} += $delta;
        return $a->[$bin]->{$key};
    }
    my $d = $branch->[DATA];
    defined($d) or
        die("SummaryInfo::value: unrecognized branch group $group\n");
    defined($d->{$key}) or
        die("SummaryInfo::value: unrecognized branch key $key\n");

    $d->{$key} += $delta;
    return $d->{$key};
}

sub mcdcCovCount
{
    my ($self, $key, $group, $age, $delta) = @_;

    $delta = 0 unless defined($delta);

    if ($key eq 'missed') {
        my $found = $self->mcdcCovCount('found', $group, $age);
        my $hit   = $self->mcdcCovCount('hit', $group, $age);
        return $found - $hit;
    }

    my $mcdc = $self->[MCDC_DATA];
    if ($group eq "age") {
        my $a   = $mcdc->[AGE];
        my $bin = SummaryInfo::findAgeBin($age);
        unless (exists($a->[$bin]) && exists($a->[$bin]->{$key})) {
	    # LCOV_UNREACHABLE_START
            lcovutil::ignorable_error($lcovutil::ERROR_INTERNAL,
                                      "unexpected key '$key' for bin '$bin'");
            return;
	    # LCOV_UNREACHABLE_STOP
        }
        $a->[$bin]->{$key} += $delta;
        return $a->[$bin]->{$key};
    }
    my $d = $mcdc->[DATA];
    defined($d) or
        die("SummaryInfo::value: unrecognized MC/DC group $group\n");
    defined($d->{$key}) or
        die("SummaryInfo::value: unrecognized MC/DC key $key\n");

    $d->{$key} += $delta;
    return $d->{$key};
}

sub functionCovCount
{
    my ($self, $key, $group, $age, $delta) = @_;

    $delta = 0 unless defined($delta);

    if ($key eq 'missed') {
        my $found = $self->functionCovCount('found', $group, $age);
        my $hit   = $self->functionCovCount('hit', $group, $age);
        return $found - $hit;
    }

    my $func = $self->[FUNCTION_DATA];
    if ($group eq "age") {
        my $a   = $func->[AGE];
        my $bin = SummaryInfo::findAgeBin($age);
        unless (exists($a->[$bin]) && exists($a->[$bin]->{$key})) {
	    # LCOV_UNREACHABLE_START
            lcovutil::ignorable_error($lcovutil::ERROR_INTERNAL,
                                      "unexpected key '$key' for bin '$bin'");
            return;
	    # LCOV_UNREACHABLE_STOP
        }
        $a->[$bin]->{$key} += $delta;
        return $a->[$bin]->{$key};
    }
    my $d = $func->[DATA];
    defined($d) or
        die("SummaryInfo::value: unrecognized function group $group\n");
    defined($d->{$key}) or
        die("SummaryInfo::value: unrecognized function key $key\n");

    $d->{$key} += $delta;
    return $d->{$key};
}

sub removeLine
{
    my ($self, $lineData) = @_;

    my $l = $self->[LINE_DATA]->[DATA];
    if ($lineData->in_curr()) {
        $l->{hit}   -= $lineData->curr_count() != 0;
        $l->{found} -= 1;
    }
    if ($lcovutil::br_coverage) {
        my $b = $lineData->current_branch();
        if (defined($b)) {
            my ($found, $hit) = $b->totals();
            my $br = $self->[BRANCH_DATA]->[DATA];
            $br->{hit}   -= $hit;
            $br->{found} -= $found;
        }
    }
    if ($lcovutil::mcdc_coverage) {
        my $b = $lineData->current_mcdc();
        if (defined($b)) {
            my ($found, $hit) = $b->totals();
            my $br = $self->[MCDC_DATA]->[DATA];
            $br->{hit}   -= $hit;
            $br->{found} -= $found;
        }
    }
    if ($lcovutil::func_coverage) {
        my $f = $lineData->current_function();
        if (defined($f)) {
            my $fn = $self->[FUNCTION_DATA]->[DATA];
            $fn->{hit}   -= $f->hit() != 0;
            $fn->{found} -= 1;
        }
    }
}

sub is_empty
{
    my $self = shift;
    my $t    = $self->[LINE_DATA]->[DATA];
    foreach my $category (keys %$t) {
        return 0 if 0 != $t->{$category};
    }
    return 1;
}

sub type_count
{
    my ($type, $status, $self, $delta) = @_;
    my $l = $self->[$type]->[DATA];
    $l->{$status} += $delta if defined($delta);
    return $l->{$status};
}

sub lines_found
{
    return type_count(LINE_DATA, 'found', @_);
}

sub lines_hit
{
    return type_count(LINE_DATA, 'hit', @_);
}

sub function_found
{
    return type_count(FUNCTION_DATA, 'found', @_);
}

sub function_hit
{
    return type_count(FUNCTION_DATA, 'hit', @_);
}

sub branch_found
{
    return type_count(BRANCH_DATA, 'found', @_);
}

sub branch_hit
{
    return type_count(BRANCH_DATA, 'hit', @_);
}

sub mcdc_found
{
    return type_count(MCDC_DATA, 'found', @_);
}

sub mcdc_hit
{
    return type_count(MCDC_DATA, 'hit', @_);
}

sub selected
{
    my $rtn;
    eval { $rtn = $selectCallback->select(@_); };
    if ($@) {
        $rtn = 1;    # return everything
        lcovutil::ignorable_error($lcovutil::ERROR_CALLBACK,
                                  "select(..) failed: $@");
    }
    return $rtn;
}

sub tlaSummary
{
    my ($self, $t) = @_;
    my $type;
    if ($t eq 'line') {
        $type = LINE_DATA;
    } elsif ($t eq 'function') {
        $type = FUNCTION_DATA;
    } elsif ($t eq 'mcdc') {
        $type = MCDC_DATA;
    } else {
        die("unknown type $t") unless 'branch' eq $t;
        $type = BRANCH_DATA;
    }
    my $str = '';
    my $sep = '';
    my $d   = $self->[$type]->[DATA];
    foreach my $tla (@tlaOrderNoExclude) {
        my $count = $d->{$tla};
        next if $count == 0;
        $str .= $sep . $tla . ':' . $count;
        $sep = ' ';
    }
    return $str;
}

sub checkCoverageCriteria
{
    my $self = shift;
    my $type = $self->type();

    if ($type eq 'top') {
        # simplistic top-level criteria..
        CoverageCriteria::check_failUnder($main::current_data);
    }
    return
        unless ($CoverageCriteria::criteriaCallback &&
                (0 == scalar(@CoverageCriteria::criteriaCallbackLevels) ||
                 grep(/$type/, @CoverageCriteria::criteriaCallbackLevels)));

    my $start = Time::HiRes::gettimeofday();
    my %data;
    foreach my $t (LINE_DATA, FUNCTION_DATA, BRANCH_DATA, MCDC_DATA) {
        my $key = type2str($t);
        my $d   = $self->[$t]->[DATA];
        foreach my $k (keys %$d) {
            # $k will be 'hit', 'found' or one of the TLAs
            my $count = $d->{$k};
            next if $count == 0;
            if (exists $data{$key}) {
                $data{$key}->{$k} = $count;
            } else {
                $data{$key} = {$k => $count};
            }
        }
    }
    if (grep(/^date$/, @CoverageCriteria::criteriaCallbackTypes)) {
        foreach my $t (LINE_DATA, FUNCTION_DATA, BRANCH_DATA, MCDC_DATA) {
            my $key = type2str($t);

            next unless exists($self->[$t]->[AGE]);
            my $ageBins = $self->[$t]->[AGE];
            foreach my $i (0 .. $#cutpoints + 1) {
                my $bin = $ageBins->[$i];
                foreach my $k (@tlaPriorityOrder, 'found', 'hit') {
                    my $count = $bin->{$k};
                    next if $count == 0;
                    my $b;
                    if (exists($data{$key})) {
                        $b = $data{$key};
                    } else {
                        $b = {};
                        $data{$key} = $b;
                    }
                    if (exists($b->{$i})) {
                        $b->{$i}->{$k} = $count;
                    } else {
                        $b->{$i} = {$k => $count};
                    }
                }
            }
        }
    }
    if (grep(/^owner$/, @CoverageCriteria::criteriaCallbackTypes)) {
        foreach my $t (LINE_DATA, BRANCH_DATA, MCDC_DATA) {
            my $key = type2str($t);
            next unless exists($self->[$t]->[OWNERS]);
            my $ownerBins = $self->[$t]->[OWNERS];
            while (my ($owner, $bin) = each(%$ownerBins)) {
                my $b;
                if (exists($data{$key})) {
                    $b = $data{$key};
                } else {
                    $b = {};
                    $data{$key} = $b;
                }
                my $d = {};
                $b->{$owner} = $d;
                foreach my $k (@tlaPriorityOrder, 'found', 'hit') {
                    next unless exists($bin->{$k});
                    my $count = $bin->{$k};
                    next if $count == 0;
                    $d->{$k} = $count;
                }
            }
        }
    }
    my $name = $self->type() eq 'top' ? 'top' : $self->name();
    my $cmd =
        join(' ', @CoverageCriteria::coverageCriteriaScript) .
        ' \'' . $name . '\' ' . $self->type() . ' \'json_encoded_data\'';
    # command:  script name (top|dir|file) jsonString args..
    lcovutil::info(1, "criteria: '$cmd'\n");
    CoverageCriteria::executeCallback($self->type(), $name, \%data);
    my $end = Time::HiRes::gettimeofday();
    $lcovutil::profileData{criteria}{$name} = $end - $start;
}

package OwnerDetailCallback;
# somewhat of a hack...I want a class which has a callback 'get'
#  that matches the SummaryInfo::get method - but returns owner-specific
#  information

use constant {
              SUMMARY => 0,
              OWNER   => 1,
              TYPE    => 2,
};

sub new
{
    my ($class, $summary, $owner, $covType) = @_;
    die("missing type") unless defined($covType);

    my $self = [$summary, $owner, $covType];
    bless $self, $class;
    return $self;
}

sub label
{
    my $self = shift;
    return $self->owner();
}

sub cb_type
{
    my $self = shift;
    return 'owner';
}

sub get
{
    my ($self, $key, $type) = @_;

    my ($summary, $owner, $covType) = @$self;

    die("unexpected type $type")
        unless !defined($type) || ($type == $covType);

    return $summary->owner_tlaCount($owner, $key, $covType);
}

sub owner
{
    my $self = shift;
    return $self->[OWNER];
}

sub covType
{
    my $self = shift;
    return $self->[TYPE];
}

package DateDetailCallback;
# as above:  callback class to return date-specific TLA counts

use constant {
              SUMMARY => 0,
              AGE     => 1,
              TYPE    => 2,
              BIN     => 3,
};

sub new
{
    my ($class, $summary, $age, $covType) = @_;
    $covType = SummaryInfo::LINE_DATA unless defined($covType);

    my $self = [$summary, $age, $covType, SummaryInfo::findAgeBin($age)];
    bless $self, $class;
    return $self;
}

sub get
{
    my ($self, $key, $type) = @_;

    my ($summary, $age, $covType) = @$self;

    die("unexpected type $type")
        unless (!defined($type) || ($type == $covType));

    return $summary->lineCovCount($key, 'age', $age)
        if $covType == SummaryInfo::LINE_DATA;

    return $summary->functionCovCount($key, 'age', $age)
        if $covType == SummaryInfo::FUNCTION_DATA;

    return $summary->mcdcCovCount($key, 'age', $age)
        if $covType == SummaryInfo::MCDC_DATA;

    die('$covType coverage not yet implemented')
        if $covType != SummaryInfo::BRANCH_DATA;

    return $summary->branchCovCount($key, 'age', $age);
}

sub label
{
    my $self = shift;
    return $self->age();
}

sub cb_type
{
    my $self = shift;
    return 'date';
}

sub age
{
    my $self = shift;
    return $self->[AGE];
}

sub bin
{
    my $self = shift;
    return $self->[BIN];
}

sub covType
{
    my $self = shift;
    return $self->[TYPE];
}

package FileOrDirectoryCallback;
# callback class used by 'write_file_table' to retrieve count of the
# various coverpoint categories in the file or directory (i.e., total
# number).
# Other callbacks classes are used to retrieve per-owner counts, etc.

sub new
{
    # dirSummary: SummaryInfo object
    my ($class, $path, $summary) = @_;

    my $self = [$path, $summary];

    bless $self, $class;
    return $self;
}

# page_link is HTML reference to file table page next level down -
#   for top-level page:  link to directory-level page
#   for directory-level page:  link to source file details
sub page_link
{
    my $self      = shift;
    my $data      = $self->summary();
    my $page_link = '';
    $page_link =
        ($main::flat && '.' ne $data->relativeDir()) ?
        File::Spec->catfile($data->relativeDir(), $self->name()) :
        $self->name();
    if ($data->type() eq 'file') {
        if ($main::no_sourceview) {
            return "";
        }
        $page_link .= ".gcov";
        $page_link .= ".frameset"
            if ($main::frames);
    } else {
        $page_link =~ s/^$lcovutil::dirseparator//;
        $page_link .= $lcovutil::dirseparator . "index";
    }
    $page_link .= '.' . $main::html_ext;
    return $lcovutil::case_insensitive ? lc($page_link) : $page_link;
}

sub data
{
    my $self = shift;
    # ($found, $hit, $fn_found, $fn_hit, $br_found, $br_hit, $page_link,
    #  $fileSummary, $fileDetails)

    my $summary = $self->summary();
    my @rtn;
    foreach my $type (SummaryInfo::LINE_DATA, SummaryInfo::FUNCTION_DATA,
                      SummaryInfo::BRANCH_DATA, SummaryInfo::MCDC_DATA
    ) {
        my $hash = $summary->[$type]->[SummaryInfo::DATA];
        push(@rtn, $hash->{found}, $hash->{hit});
    }
    my $link       = $self->page_link();
    my $sourceFile = $summary->fileDetails()
        if 'file' eq $summary->type();
    push(@rtn, $link, $summary, $sourceFile);
    return @rtn;
}

sub secondaryElementFileData
{
    my ($self, $name) = @_;
    my $summary    = $self->summary();
    my $sourceFile = $summary->fileDetails()
        if 'file' eq $summary->type();
    return [$summary->name(), $summary, $sourceFile, $self->page_link()];
}

sub name
{
    my $self = shift;
    return $self->[0];
}

sub summary
{
    # return undef or SummaryInfo object
    my $self = shift;
    return $self->[1];
}

sub findOwnerList
{
    my $self = shift;
    # return [ [owner, lineCovData, branchCovData]] for each owner
    #  where lineCovData = [missedCount, totalCount, callback]
    #        branchCovData = [missed, total, callback] or undef if not enabled
    #   - sorted in descending order number of missed lines
    return $self->summary()->findOwnerList(@_);
}

sub dateDetailCallback
{
    # callback to compute count in particular date bin
    my ($self, $ageval, $covtype) = @_;
    $covtype == SummaryInfo::LINE_DATA       ||
        $covtype == SummaryInfo::BRANCH_DATA ||
        $covtype == SummaryInfo::MCDC_DATA   ||
        $covtype == SummaryInfo::FUNCTION_DATA or
        die("'$covtype' type not supported");

    return DateDetailCallback->new($self->summary(), $ageval, $covtype);
}

sub ownerDetailCallback
{
    # callback to compute count in particular owner bin
    my ($self, $owner, $covtype) = @_;
    $covtype == SummaryInfo::LINE_DATA ||
        $covtype == SummaryInfo::BRANCH_DATA or
        $covtype == SummaryInfo::MCDC_DATA   or
        die("'$covtype' type not supported");

    return OwnerDetailCallback->new($self->summary(), $owner, $covtype);
}

sub totalCallback
{
    my ($self, $covtype) = @_;
    # callback to compute total elements of 'covtype' in each TLA
    if (SummaryInfo::LINE_DATA == $covtype) {
        return $self->summary();
    } else {
        return CovTypeSummaryCallback->new($self->summary(), $covtype);
    }
}

package FileOrDirectoryOwnerCallback;
# callback class used by 'write_file_table' to retrieve owner-
#  specific coverage numbers (for all entries in the directory)

sub new
{
    my ($class, $owner, $dirSummary) = @_;

    my $self = [$owner, $dirSummary];
    bless $self, $class;
    return $self;
}

sub name
{
    my $self = shift;
    return $self->[0];
}

sub summary
{
    my $self = shift;
    return $self->[1];
}

sub data
{
    my $self = shift;

    my $lineCb = OwnerDetailCallback->new($self->[1], $self->[0],
                                          SummaryInfo::LINE_DATA);
    my $branchCb =
        OwnerDetailCallback->new($self->[1], $self->[0],
                                 SummaryInfo::BRANCH_DATA);
    my $mcdcCb =
        OwnerDetailCallback->new($self->[1], $self->[0],
                                 SummaryInfo::MCDC_DATA);
    # ($found, $hit, $fn_found, $fn_hit, $br_found, $br_hit, $page_link,
    #  $fileSummary, $fileDetails)
    # this is the 'totals' callback for this owner - so there is no
    #  associated file or summary info.  Pass undef.
    return ($lineCb->get('found'), $lineCb->get('hit'),
            0, 0,    # fn_found, fn_hit (not tracked per-owner)
            $branchCb->get('found'), $branchCb->get('hit'),
            $mcdcCb->get('found'), $mcdcCb->get('hit'));
}

sub totalCallback
{
    # callback to compute total 'covtype' elements in each TLA
    my ($self, $covtype) = @_;
    die("$covtype not supported by OwnerDetail callback")
        unless ($covtype == SummaryInfo::LINE_DATA ||
                $covtype == SummaryInfo::BRANCH_DATA ||
                $covtype == SummaryInfo::MCDC_DATA);

    return OwnerDetailCallback->new($self->[1], $self->name(), $covtype);
}

sub findFileList
{
    my ($self, $all) = @_;

    # return [ [filename, lineCovData, branchCovData]] for each file
    #   such that this owner has at least 1 line.
    #  where lineCovData = [missedCount, totalCount, OwnerDetailCallback]
    #        branchCovData = [missed, total, dateDetailCallback]
    #                         or undef if not enabled
    #   - sorted in descending order number of missed lines
    my $dirSummary = $self->[1];
    my $owner      = $self->[0];
    my @files;
    my $skipped = 0;
    foreach my $file ($dirSummary->sources()) {
        my $source = $dirSummary->get_source($file);
        next unless $source->contains_owner($owner);

        my $lineCb =
            OwnerDetailCallback->new($source, $owner, SummaryInfo::LINE_DATA);
        my $brCb =
            OwnerDetailCallback->new($source, $owner, SummaryInfo::BRANCH_DATA);
        my $mcdcCb =
            OwnerDetailCallback->new($source, $owner, SummaryInfo::MCDC_DATA);
        my $funcCb =
            OwnerDetailCallback->new($source, $owner,
                                     SummaryInfo::FUNCTION_DATA);
        my $total      = $lineCb->get('found');
        my $br_total   = $lcovutil::br_coverage ? $brCb->get('found') : 0;
        my $fn_total   = $lcovutil::func_coverage ? $funcCb->get('found') : 0;
        my $mcdc_total = $lcovutil::mcdc_coverage ? $mcdcCb->get('found') : 0;
        next
            if (0 == $total &&
                0 == $br_total   &&
                0 == $mcdc_total &&
                0 == $fn_total);
        my $missed      = $lineCb->get('missed');
        my $br_missed   = $lcovutil::br_coverage ? $brCb->get('missed') : 0;
        my $mcdc_missed = $lcovutil::mcdc_coverage ? $mcdcCb->get('missed') : 0;
        my $fn_missed   = $lcovutil::func_coverage ? $funcCb->get('missed') : 0;

        if ($all ||
            0 != $missed    ||
            0 != $br_missed ||
            0 != $fn_missed ||
            0 != $mcdc_missed) {

            push(@files,
                 [$file,
                  [$missed, $total, $lineCb],
                  [$br_missed, $br_total, $brCb],
                  [$mcdc_missed, $mcdc_total, $mcdcCb],
                  [$fn_missed, $fn_total, $funcCb]
                 ]);
        } else {
            ++$skipped;
        }
    }
    return [$skipped, @files];
}

sub secondaryElementFileData
{
    my ($self, $name) = @_;
    my $dirSummary    = $self->[1];
    my $file          = File::Basename::basename($name);
    my $sourceSummary = $dirSummary->get_source($name);

    my $page_link;
    if ($sourceSummary->is_directory()) {
        $page_link =
            File::Spec->catfile($name, "index-bin_owner." . $main::html_ext);
    } elsif ($main::no_sourceview) {
        $page_link = "";
    } else {
        $name      = $file;
        $page_link = $name . ".gcov.";
        $page_link .= "frameset."
            if $main::frames;
        $page_link .= $main::html_ext;
    }
    $page_link = lc($page_link) if $lcovutil::case_insensitive;
    # pass owner in callback data
    my $sourceFile = $sourceSummary->fileDetails()
        if 'file' eq $sourceSummary->type();
    return [$name, $sourceSummary, $sourceFile, $page_link, $self->[0]];
}

package FileOrDirectoryDateCallback;
# callback class used by 'write_file_table' to retrieve date-
#  specific coverage numbers (for all entries in the directory)

sub new
{
    my ($class, $bin, $dirSummary) = @_;

    my $self = [$bin, $dirSummary->age_sample($bin), $dirSummary];
    bless $self, $class;
    return $self;
}

sub name
{
    my $self = shift;
    return $SummaryInfo::ageGroupHeader[$self->[0]];
}

sub summary
{
    my $self = shift;
    return $self->[2];
}

sub data
{
    my $self = shift;

    my @rtn;
    foreach my $covType (SummaryInfo::LINE_DATA, SummaryInfo::FUNCTION_DATA,
                         SummaryInfo::BRANCH_DATA, SummaryInfo::MCDC_DATA
    ) {
        my $cb = DateDetailCallback->new($self->[2], $self->[1], $covType);
        push(@rtn, $cb->get('found'), $cb->get('hit'));
    }
    # ($found, $hit, $fn_found, $fn_hit, $br_found, $br_hit, $page_link,
    #  $fileSummary, $fileDetails)
    # this is the top-level 'total' callback - so no associated file or
    # summary info
    return @rtn;
}

sub totalCallback
{
    # callback to compute total elements of 'covtype' in each TLA
    my ($self, $covtype) = @_;
    return DateDetailCallback->new($self->[2], $self->[1], $covtype);
}

sub findFileList
{
    my ($self, $all) = @_;

    # return [ [filename, lineCovData, branchCovData]] for each file
    #   such that this owner has at least 1 line.
    #  where lineCovData = [missedCount, totalCount, OwnerDetailCallback]
    #        branchCovData = [missed, total, dateDetailCallback]
    #                         or undef if not enabled
    #   - sorted in descending order number of missed lines
    my $dirSummary = $self->[2];
    my $ageval     = $self->[1];
    my @files;
    my $skipped = 0;
    foreach my $file ($dirSummary->sources()) {
        my $source = $dirSummary->get_source($file);

        my $lineCb =
            DateDetailCallback->new($source, $ageval, SummaryInfo::LINE_DATA);
        my $brCb =
            DateDetailCallback->new($source, $ageval, SummaryInfo::BRANCH_DATA);
        my $mcdcCb =
            DateDetailCallback->new($source, $ageval, SummaryInfo::MCDC_DATA);
        my $funcCb =
            DateDetailCallback->new($source, $ageval,
                                    SummaryInfo::FUNCTION_DATA);
        my $total      = $lineCb->get('found');
        my $br_total   = $lcovutil::br_coverage ? $brCb->get('found') : 0;
        my $mcdc_total = $lcovutil::mcdc_coverage ? $mcdcCb->get('found') : 0;
        my $fn_total   = $lcovutil::func_coverage ? $funcCb->get('found') : 0;
        next
            if (0 == $total &&
                0 == $br_total   &&
                0 == $mcdc_total &&
                0 == $fn_total);

        my $missed      = $lineCb->get('missed');
        my $br_missed   = $lcovutil::br_coverage ? $brCb->get('missed') : 0;
        my $mcdc_missed = $lcovutil::mcdc_coverage ? $mcdcCb->get('missed') : 0;
        my $fn_missed   = $lcovutil::func_coverage ? $funcCb->get('missed') : 0;
        if ($all ||
            0 != $missed      ||
            0 != $br_missed   ||
            0 != $mcdc_missed ||
            0 != $fn_missed) {
            push(@files,
                 [$file,
                  [$missed, $total, $lineCb],
                  [$br_missed, $br_total, $brCb],
                  [$mcdc_missed, $mcdc_total, $mcdcCb],
                  [$fn_missed, $fn_total, $funcCb]
                 ]);
        } else {
            ++$skipped;
        }
    }
    return [$skipped, @files];
}

sub secondaryElementFileData
{
    my ($self, $name) = @_;
    my $dirSummary    = $self->[2];
    my $file          = File::Basename::basename($name);
    my $sourceSummary = $dirSummary->get_source($name);

    my $page_link;
    if ($sourceSummary->is_directory()) {
        $page_link =
            File::Spec->catfile($name, "index-bin_date." . $main::html_ext);
    } elsif ($main::no_sourceview) {
        $page_link = "";
    } else {
        $name      = $file;
        $page_link = $name . ".gcov.";
        $page_link .= "frameset."
            if $main::frames;
        $page_link .= $main::html_ext;
    }
    $page_link = lc($page_link) if $lcovutil::case_insensitive;
    # pass bin index in callback data
    my $sourceFile = $sourceSummary->fileDetails()
        if 'file' eq $sourceSummary->type();
    return [$name, $sourceSummary, $sourceFile, $page_link, $self->[0]];
}

package CovTypeSummaryCallback;
# callback class to return total branches in each TLA category
sub new
{
    my ($class, $summary, $covType) = @_;
    defined($summary) or
        die("no summary");
    die("$covType not supported yet")
        unless ($covType == SummaryInfo::LINE_DATA ||
                $covType == SummaryInfo::BRANCH_DATA ||
                $covType == SummaryInfo::MCDC_DATA   ||
                $covType == SummaryInfo::FUNCTION_DATA);
    my $self = [$summary, $covType];
    bless $self, $class;
    return $self;
}

sub get
{
    my ($self, $key) = @_;

    return $self->[0]->get($key, $self->[1]);
}

sub owner
{
    my $self = shift;
    die("CovTypeSummaryCallback::owner not supported for " . $self->[1])
        unless ($self->[1] == SummaryInfo::BRANCH_DATA ||
                $self->[1] == SummaryInfo::MCDC_DATA);
    return $self->[0]->owner();
}

sub age
{
    my $self = shift;
    return $self->[0]->age();
}

sub bin
{
    my $self = shift;
    return $self->[0]->bin();
}

sub covType
{
    my $self = shift;
    return $self->[1];
}

package PrintCallback;
# maintain some callback data from one line to the next

use constant {
              FILE_INFO  => 0,    # SourceFile struct
              LINE_DATA  => 1,    # FileCoverageInfo struct
              TLA        => 2,
              OWNER      => 3,
              AGE        => 4,
              NEXT_OWNER => 5,
              NEXT_AGE   => 6,
              LINENO     => 7,
};

sub new
{
    my ($class, $sourceFileStruct, $lineCovInfo) = @_;
    my $self = [$sourceFileStruct,
                $lineCovInfo,
                "",      # current TLA
                "",      # owner
                "",      # age
                {},      # next header line for corresponding owner
                {},      # next header line for corresponding date bin
                undef    # line number
    ];
    bless $self, $class;
    return $self;
}

sub sourceDetail
{
    my $self = shift;
    return $self->[FILE_INFO];
}

sub lineData
{
    my $self = shift;
    return $self->[LINE_DATA];
}

sub lineNo
{
    my ($self, $lineNo) = @_;
    $self->[LINENO] = $lineNo
        if defined($lineNo);
    return $self->[LINENO];
}

sub tla
{
    my ($self, $newTLA, $lineNo) = @_;
    # NOTE:  'undef' TLA means that this line is not code (it is a comment,
    #    blank line, opening brace or something).
    # We return 'same' as previous line' in that case so the category
    #   block can be larger (e.g., 1 CBC line, a 2 line comment, then 3 more
    #   lines) can get just one label (first line).
    # This reduces visual clutter.
    # Note that the 'block finding' code has to do the same thing (else the
    #   HTML links won't be generated correctly)
    if (defined($newTLA) &&
        $newTLA ne $self->[TLA]) {
        $self->[TLA] = $newTLA;
        return $newTLA;
    }
    return " " x $main::tla_field_width;    # same TLA as previous line.
}

sub age
{
    my ($self, $newval, $lineNo) = @_;
    if (defined($newval) && $newval ne $self->[AGE]) {
        $self->[AGE] = $newval;
        return $newval;
    }
    return " " x $main::age_field_width;    # same age as previous line.
}

sub owner
{
    my ($self, $newval, $lineNo) = @_;
    if (defined($newval) && $newval ne $self->[OWNER]) {
        $self->[OWNER] = $newval;
        return $newval;
    }
    return " " x $main::owner_field_width;    # same owner as previous line.
}

sub current
{
    my ($self, $key) = @_;

    if ($key eq 'tla') {
        return $self->[TLA];
    } elsif ($key eq 'owner') {
        return $self->[OWNER];
    } elsif ($key eq 'age') {
        return $self->[AGE];
    } else {
        ($key eq 'dateBucket') or
            die("unexpected key $key");
        return SummaryInfo::findAgeBin($self->[AGE]);
    }
}

sub nextOwner
{
    my ($self, $owner, $tla, $value) = @_;
    my $map = $self->[NEXT_OWNER];

    my $key = $tla . ' ' . $owner;
    if (defined($value)) {
        $map->{$key} = $value;
        return $value;
    }
    return exists($map->{$key}) ? $map->{$key} : undef;
}

sub nextDate
{
    my ($self, $date, $tla, $value) = @_;
    my $map = $self->[NEXT_AGE];
    my $key = $tla . ' ' . $date;
    if (defined($value)) {
        $map->{$key} = $value;
        return $value;
    }
    return $map->{$key};
}

package ReadBaselineSource;

use base 'ReadCurrentSource';

sub new
{
    my ($class, $diffData) = @_;

    my $self = $class->SUPER::new();
    push(@$self, $diffData);

    return $self;
}

sub open
{
    my ($self, $filename) = @_;
    my $diffmap = $self->[1];
    if (defined($diffmap) && $diffmap->containsFile($filename)) {
        # if there are any diffs, then need to load the current source, then
        #   walk the diff to insert and remove changed lines

        my $currentSrc = $self->_load($filename, 'baseline');
        my $src        = $diffmap->recreateBaseline($filename, $currentSrc);
        return $self->parseLines($filename, $src);
    }
    # else no diff data here - just read the file
    return ReadCurrentSource::open($self, $filename, 'baseline');
}

package LineData;

use constant {
              TYPE           => 0,
              LINENO_BASE    => 1,
              LINENO_CURRENT => 2,    # location of this line in current data
              LINE_DATA      => 3,
              BRANCH_DATA    => 4,
              MCDC_DATA      => 5,
              FUNCTION_DATA  => 6,

              # data elements in line data
              TLA           => 0,
              LINE_BASELINE => 1,
              LINE_CURRENT  => 2,

              # data elements for branch/function data
              DATA_BASELINE     => 0,
              DATA_CURRENT      => 1,
              DATA_DIFFERENTIAL => 2,
};

sub new
{
    my ($class, $type) = @_;
    # [ type, lineNo_base, lineNo_current,
    #   bucket, base_count, curr_count    <- line coverage count data
    #   base_branch, curr_branch, differential_branch ] <- branch coverage count data
    # $type in ('insert', 'equal', 'delete')
    my $self = [$type, undef, undef,
                ['UNK', undef, undef],    # line coverage data
                [],                       # branch coverage data
                [],                       # MCDC coverage data
                []
    ];    # function coverage
    bless $self, $class;
    return $self;
}

sub to_list
{
    # used by script-level 'select' callback
    my $self = shift;

    my @rtn = ($self->[TYPE],
               [$self->tla(),
                $self->in_curr() ? $self->curr_count() : undef,
                $self->in_base() ? $self->base_count() : undef
               ]);
    # @todo perhaps visit MC/DC here too
    my $branch = $self->differential_branch();
    if (defined($branch)) {
        my @data;
        push(@rtn, \@data);
        foreach my $block ($branch->blocks()) {
            my $elements = $block->elements();
            my @b;
            push(@data, \@b);
            foreach my $br (@$elements) {
                my $tla = $br->tla();
                my ($base_count, $curr_count) = $br->diff_count();
                push(@b, [$tla, $curr_count, $base_count]);
            }
        }
    } else {
        $branch = $self->current_branch();
        if (defined($branch)) {
            my @data;
            push(@rtn, \@data);
            foreach my $block ($branch->blocks()) {
                foreach my $br (@{$block->elements()}) {
                    push(@data, $br->count());
                }
            }
        } else {
            push(@rtn, undef);
        }
    }
    return \@rtn;
}

sub tla
{
    my ($self, $tla) = @_;
    my $linecov = $self->[LINE_DATA];
    $linecov->[TLA] = $tla
        if defined($tla);
    return $linecov->[TLA];
}

sub type
{
    my $self = shift;
    return $self->[TYPE];
}

sub lineNo
{
    my ($self, $which, $lineNo, $filename) = @_;
    my $loc;
    if ($which eq "current") {
        $loc = LINENO_CURRENT;
    } else {
        die("unknown key '$which' - should be 'base' or 'current'")
            unless $which eq "base";
        $loc = LINENO_BASE;
    }
    lcovutil::ignorable_error($lcovutil::ERROR_INCONSISTENT_DATA,
                        "\"$filename\": inconsistent '$which' line location: " .
                            $self->[$loc] . " -> $lineNo")
        if (defined($lineNo) &&
            defined($self->[$loc]) &&
            $self->[$loc] != $lineNo);

    $self->[$loc] = $lineNo
        if defined($lineNo);
    return $self->[$loc];
}

sub in_base
{
    # @return true or false:  is this object present in the baseline?
    my $self = shift;
    # coverpoint is in baseline data if line coverage number is defined
    return defined($self->[LINE_DATA]->[LINE_BASELINE]);
}

sub in_curr
{
    # @return true or false:  is this object present in the current version?
    # storing negative number for 'current' location of deleted line -
    #   first location above or below the deleted region
    my $self = shift;
    # coverpoint is in current data if line coverage number is defined
    #  otherwise, the line may be present without coverage data associated
    #  with it (say, excluded now - or possibly filtered out)
    return defined($self->[LINE_DATA]->[LINE_CURRENT]);
}

sub base_count
{
    # return line hit count in baseline
    my ($self, $inc) = @_;
    die("non-zero count but not in base")
        if (defined($inc) && !defined($self->[LINENO_BASE]));
    my $linecov = $self->[LINE_DATA];
    if (defined($inc)) {
        if (defined($linecov->[LINE_BASELINE])) {
            $linecov->[LINE_BASELINE] += $inc;
        } else {
            $linecov->[LINE_BASELINE] = $inc;
        }
    }

    return $linecov->[LINE_BASELINE];
}

sub curr_count
{
    # return line hit count in current
    my ($self, $inc) = @_;
    die("non-zero count but not in current")
        if (defined($inc) &&
            (!defined($self->[LINENO_CURRENT]) || $self->[LINENO_CURRENT] < 0));
    my $linecov = $self->[LINE_DATA];
    if (defined($inc)) {
        if (defined($linecov->[LINE_CURRENT])) {
            $linecov->[LINE_CURRENT] += $inc;
        } else {
            $linecov->[LINE_CURRENT] = $inc;  # first assignment; += undef warns
        }
    }
    return $linecov->[LINE_CURRENT];
}

sub _mergeBranchData
{
    my ($self, $loc, $branchData) = @_;
    my $branch = $self->[BRANCH_DATA];
    die("unexpected $loc branch data") if defined($branch->[$loc]);
    $branch->[$loc] = Storable::dclone($branchData);
}

sub baseline_branch
{
    my ($self, $branchData) = @_;
    die("has baseline branch data but not in baseline")
        if (defined($branchData) && !defined($self->[LINENO_BASE]));
    if (defined($branchData)) {
        $self->_mergeBranchData(DATA_BASELINE, $branchData);
    }
    my $branch = $self->[BRANCH_DATA];
    return $branch->[DATA_BASELINE];
}

sub current_branch
{
    my ($self, $branchData) = @_;
    die("has current branch data but not in current")
        if (defined($branchData) &&
            (!defined($self->[LINENO_CURRENT]) || $self->[LINENO_CURRENT] < 0));
    if (defined($branchData)) {
        $self->_mergeBranchData(DATA_CURRENT, $branchData);
    }
    my $branch = $self->[BRANCH_DATA];
    return $branch->[DATA_CURRENT];
}

sub differential_branch
{
    my ($self, $differential) = @_;
    my $branch = $self->[BRANCH_DATA];
    if (defined($differential)) {
        $branch->[DATA_DIFFERENTIAL] = $differential;
    }
    return $branch->[DATA_DIFFERENTIAL];
}

sub _mergeMcdcData
{
    # this is nearly identical to 'function' implementation...maybe share
    my ($self, $loc, $mcdcData) = @_;
    my $mcdc = $self->[MCDC_DATA];
    die("unexpected $loc MC/DC data") if defined($mcdc->[$loc]);
    $mcdc->[$loc] = Storable::dclone($mcdcData);
}

sub baseline_mcdc
{
    my ($self, $mcdcData) = @_;
    die("has baseline MC/DC data but not in baseline")
        if (defined($mcdcData) && !defined($self->[LINENO_BASE]));
    if (defined($mcdcData)) {
        $self->_mergeMcdcData(DATA_BASELINE, $mcdcData);
    }
    my $mcdc = $self->[MCDC_DATA];
    return $mcdc->[DATA_BASELINE];
}

sub current_mcdc
{
    my ($self, $mcdcData) = @_;
    die("has current MC/DC data but not in current")
        if (defined($mcdcData) &&
            (!defined($self->[LINENO_CURRENT]) || $self->[LINENO_CURRENT] < 0));
    if (defined($mcdcData)) {
        $self->_mergeMcdcData(DATA_CURRENT, $mcdcData);
    }
    my $mcdc = $self->[MCDC_DATA];
    return $mcdc->[DATA_CURRENT];
}

sub differential_mcdc
{
    my ($self, $differential) = @_;
    my $mcdc = $self->[MCDC_DATA];
    if (defined($differential)) {
        $mcdc->[DATA_DIFFERENTIAL] = $differential;
    }
    return $mcdc->[DATA_DIFFERENTIAL];
}

sub _mergeFunctionData
{
    my ($self, $loc, $functionData) = @_;
    die('expected FunctionEntry found ' . ref($functionData))
        unless 'FunctionEntry' eq ref($functionData);
    my $function = $self->[FUNCTION_DATA];
    die("unexpected $loc function data") if defined($function->[$loc]);
    # also clone hit count data
    $function->[$loc] = $functionData->cloneWithEndLine(1, 1);
}

sub baseline_function
{
    my ($self, $functionData) = @_;
    die("has baseline function data but not in baseline")
        if (defined($functionData) && !defined($self->[LINENO_BASE]));
    if (defined($functionData)) {
        $self->_mergeFunctionData(DATA_BASELINE, $functionData);
    }
    my $function = $self->[FUNCTION_DATA];
    return $function->[DATA_BASELINE];
}

sub current_function
{
    my ($self, $functionData) = @_;
    die("has current function data but not in current")
        if (defined($functionData) &&
            (!defined($self->[LINENO_CURRENT]) || $self->[LINENO_CURRENT] < 0));
    if (defined($functionData)) {
        $self->_mergeFunctionData(DATA_CURRENT, $functionData);
    }
    my $function = $self->[FUNCTION_DATA];
    return $function->[DATA_CURRENT];
}

sub differential_function
{
    my ($self, $differential) = @_;
    my $function = $self->[FUNCTION_DATA];
    if (defined($differential)) {
        $function->[DATA_DIFFERENTIAL] = $differential;
    }
    return $function->[DATA_DIFFERENTIAL];
}

# forward-declare some types
package DiffMap;

use constant {
              EQUAL  => 0,
              INSERT => 1,
              DELETE => 2,

};

our @type2str = ('equal', 'insert', 'delete');

# structure holding coverage data for a particular file:
#  - associated with a line line number:
#     - line coverage
#     - branch coverage
#  - function coverage (not directly associated with line number
package FileCoverageInfo;

use constant {
              VERSION             => 0,
              LINEMAP             => 1,
              DELETED_LINE_LEADER => 2,
              FUNCTIONMAP         => 3,
};

sub new
{
    my ($class, $filename, $base_data, $current_data, $diffMap, $verbose) = @_;

    # [hash of lineNumber -> LineData struct, optional FunctionMap]
    my $self = [
             [defined($base_data) ? $base_data->version() : undef,
              $current_data->version()
             ],
             {}, # the line data map
             {}  # currentLineNo -> [deleted lines for which this is the leader]
    ];
    bless $self, $class;

    $diffMap->show_map($filename)
        if ((defined($verbose) && $verbose) ||
            (defined($lcovutil::verbose) && $lcovutil::verbose > 1));

    # line coverage categorization includes date- and owner- bins in
    #   the vanilla case when there is no baseline.
    my @args = ($filename, $base_data, $current_data, $diffMap, $verbose);
    $self->_categorizeLineCov(@args);
    $self->_categorizeBranchCov(@args)
        if ($lcovutil::br_coverage);
    $self->_categorizeMcdcCov(@args)
        if ($lcovutil::mcdc_coverage);
    $self->_categorizeFunctionCov(@args)
        if ($lcovutil::func_coverage);

    return $self;
}

sub version
{
    my ($self, $which) = @_;
    return $which eq 'current' ? $self->[VERSION]->[1] : $self->[VERSION]->[0];
}

sub lineMap
{
    my $self = shift;
    return $self->[LINEMAP];
}

sub functionMap
{
    # simply a map of function leader name -> differential FunctionEntry
    my $self = shift;
    return (scalar(@$self) >= FUNCTIONMAP) ? $self->[FUNCTIONMAP] : undef;
}

sub line
{
    my ($self, $lineNo) = @_;
    my $lineMap = $self->lineMap();
    return exists($lineMap->{$lineNo}) ? $lineMap->{$lineNo} : undef;
}

sub deletedLineData
{
    my ($self, $currentLineNumber) = @_;

    my $map = $self->[DELETED_LINE_LEADER];
    return
        exists($map->{$currentLineNumber}) ? $map->{$currentLineNumber} : undef;
}

sub recategorizeTlaAsBaseline
{
    # intended use:  this file appears to have been added to the "coverage"
    # suite - but the file itself is old/has been around for a long time.
    #   - by default, we will see this as "Included Code"
    #      - which means that 'un-exercised' code will be "UIC"
    #   - but:  non-zero UIC will fail our Jenkins coverage ratchet.
    # As a workaround:  treat this file as if the baseline data was the same
    # as 'current' - so code will be categorized as "CBC/UBC" - which will not
    # trigger the coverage criteria.
    my $self    = shift;
    my $lineMap = $self->lineMap();
    my %remap = ('UIC' => 'UBC',
                 'GIC' => 'CBC');

    while (my ($line, $data) = each(%$lineMap)) {
        die("unexpected $line 'in_base'") if $data->in_base();

        my $lineTla = $data->tla();
        if (exists($remap{$lineTla})) {
            # don't remap GNC, UNC, etc
            $data->tla($remap{$lineTla});
        }

        # branch coverage...
        if ($lcovutil::br_coverage && defined($data->differential_branch())) {
            my $br = $data->differential_branch();

            foreach my $block ($br->blocks()) {
                my $diff = $block->elements();
                foreach my $b (@$diff) {
                    my $tla = $b->tla();
                    if (exists($remap{$tla})) {
                        $b->[BranchElement::TLA] = $remap{$tla};
                    }
                }
            }
        }    # if branch data

        # MC/DC coverage...
        if ($lcovutil::mcdc_coverage && defined($data->differential_mcdc())) {
            my $mcdc = $data->differential_mcdc();

            while (my ($groupSize, $group) = each(%{$mcdc->groups()})) {
                foreach my $cond (@$group) {
                    # remap both the true and false sense..
                    foreach my $sense (0, 1) {
                        my $c   = $cond->count($sense);
                        my $tla = $c->[0];
                        if (exists($remap{$tla})) {
                            $c->[0] = $remap{$tla};
                        }
                    }
                }
            }
        }    # if MCDC data

        # function coverage..
        if ($lcovutil::func_coverage && defined($data->differential_function()))
        {
            my $func = $data->differential_function();
            my $hit  = $func->hit();
            my $tla  = $hit->[1];
            if (exists($remap{$tla})) {
                $hit->[1] = $remap{$tla};
            }

            while (my ($alias, $data) = each(%{$func->aliases()})) {
                my $tla = $data->[1];
                if (exists($remap{$tla})) {
                    $data->[1] = $remap{$tla};
                }
            }
        }
    }    # if function data
}

sub _categorize
{
    my ($baseCount, $currCount, $excluded) = @_;
    my $tla;
    if (0 == $baseCount) {
        $tla = (0 == $currCount) ? "UBC" : "GBC";
    } elsif (0 == $currCount) {
        $tla = "LBC";
    } else {
        $tla = "CBC";
    }
    if (defined($excluded) && $excluded) {
        return $currCount == 0 ? 'EUC' : 'ECC';
    }
    return $tla;
}

sub _categorizeIfExcluded
{
    my ($count, $excluded, $missTla, $hitTla) = @_;
    unless ($excluded) {
        return $count == 0 ? $missTla : $hitTla;
    } else {
        return $count == 0 ? 'EUC' : 'ECC';
    }
}

sub _findLineData
{
    my ($self, $diffMap, $filename, $base_lineNo) = @_;
    my $current_lineNo =
        $diffMap->lookup($filename, $diffMap->OLD, $base_lineNo);
    my $type = $diffMap->type($filename, $diffMap->OLD, $base_lineNo);

    my $lineDataMap = $self->lineMap();
    my $linedata;

    if ($type != DiffMap::DELETE) {
        if (!defined($lineDataMap->{$current_lineNo})) {
            $linedata = LineData->new($type);
            $lineDataMap->{$current_lineNo} = $linedata;
            $linedata->lineNo('current', $current_lineNo, $filename);
        } else {
            $linedata = $lineDataMap->{$current_lineNo};
        }
        $linedata->lineNo('base', $base_lineNo, $filename);
    } else {
        # nothing walks the keylist so a prefix is sufficient to distinguish
        # records that should be summarized but not displayed
        my $dline = "<<<" . $base_lineNo;
        if (!exists($lineDataMap->{$dline})) {
            $linedata = LineData->new($type);
            $linedata->lineNo('base', $base_lineNo, $filename);
            $lineDataMap->{$dline} = $linedata;
            # look up and/or down to find the first baseline line
            # which is not deleted - and store that as the corresponding
            # 'current' line.
            # this way, we can know the extents of the deleted region
            my $c = $diffMap->find_deleted_line_leader($filename, $base_lineNo);
            $linedata->lineNo('current', -$c, $filename);
        } else {
            $linedata = $lineDataMap->{$dline};
        }
    }
    return $linedata;
}

# categorize line coverage numbers
sub _categorizeLineCov
{
    my ($self, $filename, $base_data, $current_data, $diffMap, $verbose) = @_;
    my $lineDataMap = $self->lineMap();

    print("categorize lines $filename\n")
        if ($verbose);

    # $lineCovBase, $lineCovCurrent are CountData objects
    my $lineCovBase    = $base_data->sum() if defined($base_data);
    my $lineCovCurrent = $current_data->sum();

    if ($lcovutil::check_data_consistency) {
        # walk the branch coverpoints to check for data consistency:
        #  - we expect a line coverpoint in every location which has branches
        #  - if not found, the generate message and/or create a fake coverpoint
        # LLVM seems to like to generate inconsistent data.
        my $branchCurrent = $current_data->sumbr();
        foreach my $line ($branchCurrent->keylist()) {
            # just ignore bogus data - we already warned when we read the data
            next if ($line <= 0);

            my $type = $diffMap->type($filename, $diffMap->NEW, $line);
            if ($type == DiffMap::DELETE) {
		# LCOV_UNREACHABLE_START
                lcovutil::ignorable_error($lcovutil::ERROR_INTERNAL,
                    "'current' line $filename:$line should not be marked 'delete'"
                );
                delete($branchCurrent->{$line});
                next;
		# LCOV_UNREACHABLE_STOP
            }
        }
    }

    # it is sufficient to just walk the 'global' (merged) line
    #  coverage dataset because we only care/we only show total
    #  coverage - not changed coverage per testcase.
    #  (This observation is also true for branch and function
    #  coverages.)
    foreach my $line ($lineCovCurrent->keylist()) {
        # just ignore bogus data - we already warned when we read the data
        next if ($line <= 0);
        my $type = $diffMap->type($filename, $diffMap->NEW, $line);
        if ($type == DiffMap::DELETE) {
            # can happen in some inconsistent case, when there are certain
            # out-of-range references in a file which contained diffs - and we
            # ignored the error check
            lcovutil::ignorable_error($lcovutil::ERROR_INCONSISTENT_DATA,
                "'current' line $filename:$line should not be marked 'delete'");
            $lineCovCurrent->remove($line);
            next;
        }
        my $linedata;
        if (!exists($lineDataMap->{$line})) {
            $linedata = LineData->new($type);
            $lineDataMap->{$line} = $linedata;
        } else {
            $linedata = $lineDataMap->{$line};
        }
        # @todo support exclusion markers for linecov??
        my $val = $lineCovCurrent->value($line);
        $linedata->lineNo("current", $line, $filename);
        $linedata->curr_count($val);
        $linedata->tla($val == 0 ? 'UNC' : 'GNC')
            if (!defined($lineCovBase));
    }
    return unless (defined($lineCovBase));

    foreach my $bline ($lineCovBase->keylist()) {
        # just ignore bogus data - we already warned when we read the data
        next if ($bline <= 0);
        my $linedata = $self->_findLineData($diffMap, $filename, $bline);
        my $val      = $lineCovBase->value($bline);
        $linedata->base_count($val);
    }
    # walk the diff data here to mark deleted regions so we can highlight
    #   them in the source view
    my $deletedLineData = $diffMap->compute_deleted_lines($filename);
    $self->[DELETED_LINE_LEADER] = $deletedLineData
        if $deletedLineData;

    if ($verbose) {
        print("  line data map:\n");
        foreach my $line (sort keys %$lineDataMap) {
            my $data = $lineDataMap->{$line};
            print("    $line: ",
                  $data->type(),
                  ' curr:',
                  $data->in_curr() ? $data->lineNo('current') : '-',
                  ' base:',
                  $data->in_base() ? $data->lineNo('base') : '-',
                  "\n");
        }
    }
    foreach my $line (sort keys %$lineDataMap) {
        my $linedata = $lineDataMap->{$line};
        my $tla;
        if ($linedata->type() == DiffMap::INSERT) {
            if (!$linedata->in_curr()) {
                # can get here if the 'diff' file is wrong with respect to
                # baseline vs. current coverage data - e.g., showing that
                # an unchanged line has a difference
                lcovutil::ignorable_error($lcovutil::ERROR_INCONSISTENT_DATA,
                    "$filename:$line: 'diff' data claims this line is inserted but it is not in 'current' coverage data"
                );
                next;
            }
            $tla = ($linedata->curr_count() > 0) ? "GNC" : "UNC";
            print("  insert $line $tla\n") if ($verbose);
        } elsif ($linedata->type() == DiffMap::DELETE) {
            if (!$linedata->in_base()) {
                # similarly:  can get here if the diff vs baseline/current data
                # is inconsistent.
                lcovutil::ignorable_error($lcovutil::ERROR_INCONSISTENT_DATA,
                    "$filename:$line: 'diff' data claims this line is deleted but it is not in 'baseline' coverage data"
                );
                next;
            }
            $tla = ($linedata->base_count() > 0) ? "DCB" : "DUB";
            print("  delete $line $tla\n") if ($verbose);
        } else {
            die("FileCoverageInfo:: deleted segment line=$line file=$filename")
                unless $linedata->type() == DiffMap::EQUAL;

            if ($linedata->in_base() && $linedata->in_curr()) {
                $tla =
                    _categorize($linedata->base_count(),
                                $linedata->curr_count());
            } elsif ($linedata->in_base()) {
                $tla = ($linedata->base_count() > 0) ? "ECB" : "EUB";
                $linedata->tla($tla);
            } else {
                die("FileCoverageInfo:: non-executed line line=$line file=$filename"
                ) unless $linedata->in_curr();
                $tla = ($linedata->curr_count() > 0) ? "GIC" : "UIC";
            }
            print("  equal $line $tla in:" .
                  ($linedata->in_base() ? ' base' : '') .
                  ($linedata->in_curr() ? ' curr' : '') . "\n")
                if ($verbose);
        }
        $linedata->tla($tla);
    }
}

sub cloneBlock
{
    my ($cloneInto, $cloneFrom, $missTla, $hitTla, $categorizeExcluded) = @_;

    foreach my $br (@{$cloneFrom->elements()}) {
        my $count = $br->count();
        my $tla =
            _categorizeIfExcluded($count,
                                  $categorizeExcluded && $br->is_excluded(),
                                  $missTla, $hitTla);
        push(@$br, $tla, [undef, $count]);    # undef == "not in baseline"
        $cloneInto->appendElement($br);
    }
}

sub _cloneBranchLocation
{
    my ($cloneInto, $cloneFrom, $missTla, $hitTla, $categorizeExcluded) = @_;
    foreach my $code ($cloneFrom->codes(1)) {
        foreach my $from (@{$cloneFrom->getList($code)}) {
            my $to = BranchBlock->new();
            cloneBlock($to, $from, $missTla, $hitTla, $categorizeExcluded);
            $cloneInto->insertBlock($to);
        }
    }
}

# categorize branch coverage numbers
sub _categorizeBranchCov
{
    my ($self, $filename, $base_data, $current_data, $diffMap, $verbose) = @_;
    my $lineDataMap = $self->lineMap();

    my $branchBaseline = $base_data->sumbr() if defined($base_data);
    my $branchCurrent  = $current_data->sumbr();

    my %branchCovLines;
    # look through the 'current' data, to find all the branch data
    # keep track of hit count in baseline, current - in element 2 of the
    #  retained branch data.  These counts are useful - for example, to report
    #  the hit count in the baseline for LBC branches:
    #   - e.g., in random testing: is this branch lost because it was a
    #     low probability event (..baselinecount is a small number)>
    #     Or because we did something bad and no longer reach what had been
    #     a high probability event?
    foreach my $line ($branchCurrent->keylist()) {
        next if ($line <= 0);    # ignore bogus
        unless (exists($lineDataMap->{$line})) {
            # unless ignored, should have been caught or fixed during
            #  TraceInfo::_checkConsistency
            lcovutil::ignorable_error($lcovutil::ERROR_INCONSISTENT_DATA,
                "\"$filename\":$line line has branchcov but no linecov data (skipping)."
            );
            next;
        }
        my $data = $lineDataMap->{$line};

        $branchCovLines{$line} = 1;
        # we expect that the line number matches...
        $data->lineNo("current", $line, $filename);
        # append this branch data for the line
        my $currBranchData = $branchCurrent->value($line);
        $data->current_branch($currBranchData);
        if (!defined($branchBaseline)) {
            my $categorized = BranchLocation->new($line);
            $data->differential_branch($categorized);
            die("no branch data for deleted $filename:$line")
                unless defined($currBranchData);
            _cloneBranchLocation($categorized, $currBranchData, 'UNC', 'GNC',
                                 1);
        }
    }    # foreach line in 'current' branch data

    return unless defined($branchBaseline);

    # now look through the baseline to find matching data

    foreach my $base_line ($branchBaseline->keylist()) {
        my $data      = $self->_findLineData($diffMap, $filename, $base_line);
        my $curr_line = $data->lineNo('current');
        my $type      = $data->type();
        if ($type != DiffMap::DELETE) {
            $branchCovLines{$curr_line} = 1;
        } else {
            # the line has been deleted...just record the data
            my $deleteKey = "<<<" . $base_line;
            $branchCovLines{$deleteKey} = 1;
        }
        my $baseBranchData = $branchBaseline->value($base_line);
        $data->baseline_branch($baseBranchData);
    }    # foreach line in baseline data

    # go through all the branch data for each line, and categorize everything
    foreach my $line (keys(%branchCovLines)) {
        my $data        = $self->lineMap()->{$line};
        my $type        = $data->type();
        my $curr        = $data->current_branch();
        my $base        = $data->baseline_branch();
        my $categorized = BranchLocation->new($line);
        $data->differential_branch($categorized);

        if ($type == DiffMap::INSERT) {
            # can get here if diff data vs baseline/current is not consistent
            lcovutil::ignorable_error($lcovutil::ERROR_INCONSISTENT_DATA,
                "baseline branch data should not be defined for inserted line $filename:$line"
            ) if defined($base);
            if (defined($curr)) {
                _cloneBranchLocation($categorized, $curr, 'UNC', 'GNC', 1);
            } else {
                # LCOV_UNREACHABLE_START
                lcovutil::ignorable_error($lcovutil::ERROR_INTERNAL,
                    "current branch data should not be defined for inserted line $filename:$line"
                );
                # LCOV_UNREACHABLE_STOP
            }
        } elsif ($type == DiffMap::DELETE) {
            # similarly: get here if diff data vs baseline/current is not consistent
            lcovutil::ignorable_error($lcovutil::ERROR_INCONSISTENT_DATA,
                "current branch data should not be defined for deleted line $filename:$line"
            ) if defined($curr);

            if (defined($base)) {
                _cloneBranchLocation($categorized, $base, 'DUB', 'DCB');
            } else {
                # LCOV_UNREACHABLE_START
                lcovutil::ignorable_error($lcovutil::ERROR_INTERNAL,
                    "baseline branch data should not be defined for deleted line $filename:$line"
                );
                # LCOV_UNREACHABLE_STOP
            }
        } else {
            lcovutil::ignorable_error($lcovutil::ERROR_INTERNAL,
                     "unexpected branch coverage type $type at $filename:$line")
                if $type != DiffMap::EQUAL;
            # go through all the codes baseline - handle whether in
            #   current or not
            my $numCurr = $curr->numBlocks()
                if defined($curr);
            # sort the set of codes - so blocks get created in an easily
            #  explained order
            my @baselineCodes = $base->codes(1)
                if defined($base);

            foreach my $code (@baselineCodes) {
                my $baseList = $base->getList($code);
                if (defined($curr) && $curr->containsCode($code)) {
                    my $currList = $curr->getList($code);
                    my $blkIdx   = 0;
                    foreach my $baseBlock (@$baseList) {
                        my $catBlock = BranchBlock->new();
                        if ($blkIdx <= $#$currList) {
                            my $currBlock = $currList->[$blkIdx++];
                            my $elemIdx   = 0;
                            foreach my $base_br (@{$baseBlock->elements()}) {
                                my $curr_br =
                                    $currBlock->getElement($elemIdx++);
                                my $baseCount = $base_br->count();
                                my $currCount = $curr_br->count();
                                my $tla =
                                    _categorize($baseCount, $currCount,
                                                $curr_br->is_excluded());

                                push(@$curr_br, $tla, [$baseCount, $currCount]);
                                $catBlock->appendElement($curr_br);
                            }
                        } else {
                            # this block not in current - clone and mark
                            #   that it was deleted
                            cloneBlock($catBlock, $baseBlock, 'DUB', 'DCB');
                        }
                        $categorized->insertBlock($catBlock);
                    }
                    # now handle any blocks that are in current but not base
                    while ($blkIdx <= $#$currList) {
                        my $catBlock  = BranchBlock->new();
                        my $currBlock = $currList->[$blkIdx++];
                        cloneBlock($catBlock, $currBlock, 'UIC', 'GIC', 1);
                        $categorized->insertBlock($catBlock);
                    }
                } else {
                    # this code not in current - so clone/mark as deleted
                    foreach my $baseBlock (@$baseList) {
                        my $b = BranchBlock->new();
                        cloneBlock($b, $baseBlock, 'DUB', 'DCB');
                        $categorized->insertBlock($b);
                    }
                }
            }
            # now find all the codes in current that were not in base..
            if (defined($curr)) {
                foreach my $code ($curr->codes(1)) {
                    next if defined($base) && $base->containsCode($code);
                    foreach my $currBlock (@{$curr->getList($code)}) {
                        my $b = BranchBlock->new();
                        cloneBlock($b, $currBlock, 'UIC', 'GIC', 1);
                        $categorized->insertBlock($b);
                    }
                }
            }
        }
    }
}

sub _cloneMcdcEntry
{
    my ($cloneInto, $cloneFrom, $missTla, $hitTla, $categorizeExcluded) = @_;
    while (my ($groupSize, $group) = each(%{$cloneFrom->groups()})) {
        foreach my $expr (@$group) {
            foreach my $sense (0, 1) {
                my $count = $expr->count($sense);
                my $excluded =
                    $categorizeExcluded && $expr->is_excluded($sense);
                my $tla =
                    _categorizeIfExcluded($count, $excluded, $missTla, $hitTla);
                $count = [$tla, undef, $count]
                    ;    # [TLA, baseline value, current value]
                $cloneInto->insertExpr('unknownFile', $groupSize,
                                       $sense, $count,
                                       $expr->index(), $expr->expression(),
                                       $excluded);
            }
        }
    }
}

sub _categorizeMcdcCov
{
    my ($self, $filename, $base_data, $current_data, $diffMap, $verbose) = @_;
    my $lineDataMap = $self->lineMap();

    my $mcdcBaseline = $base_data->mcdc() if defined($base_data);
    my $mcdcCurrent  = $current_data->mcdc();

    my %mcdcCovLines;
    # look through the 'current' data, to find all the MC/DC data
    # keep track of hit count in baseline, current - in element 2 of the
    #  retained MC/DC data.  These counts are useful - for example, to report
    #  the hit count in the baseline for LBC elements:
    #   - e.g., in random testing: is this branch lost because it was a
    #     low probability event (..baseline count is a small number)>
    #     Or because we did something bad and no longer reach what had been
    #     a high probability event?
    foreach my $line ($mcdcCurrent->keylist()) {
        next if ($line <= 0);    # ignore bogus
        unless (exists($lineDataMap->{$line})) {
            # unless ignored, should have been caught or fixed during
            #  TraceInfo::_checkConsistency
            lcovutil::ignorable_error($lcovutil::ERROR_INCONSISTENT_DATA,
                "\"$filename\":$line line has MC/DC but no linecov data (skipping)."
            );
            next;
        }
        my $data = $lineDataMap->{$line};

        $mcdcCovLines{$line} = 1;
        # we expect that the line number matches...
        $data->lineNo("current", $line, $filename);
        # append this MC/DC data for the line (this is an MCDC_Block)
        my $currMcdcData = $mcdcCurrent->value($line);
        $data->current_mcdc($currMcdcData);
        if (!defined($mcdcBaseline)) {
            my $categorized = MCDC_Block->new($line);
            $data->differential_mcdc($categorized);
            _cloneMcdcEntry($categorized, $currMcdcData, 'UNC', 'GNC', 1);
        }
    }    # foreach line in 'current' branch data

    return unless defined($mcdcBaseline);

    # now look through the baseline to find matching data

    foreach my $base_line ($mcdcBaseline->keylist()) {
        next if ($base_line <= 0);    # ignore bogus
        my $data      = $self->_findLineData($diffMap, $filename, $base_line);
        my $curr_line = $data->lineNo('current');
        my $type      = $data->type();
        if ($type != DiffMap::DELETE) {
            $mcdcCovLines{$curr_line} = 1;
        } else {
            # the line has been deleted...just record the data
            my $deleteKey = "<<<" . $base_line;
            $mcdcCovLines{$deleteKey} = 1;
        }
        my $baseMcdcData = $mcdcBaseline->value($base_line);
        $data->baseline_mcdc($baseMcdcData);
    }    # foreach line in baseline data

    # go through all the MC/DC data for each line, and categorize everything
    foreach my $line (keys(%mcdcCovLines)) {
        my $data        = $self->lineMap()->{$line};
        my $type        = $data->type();
        my $curr        = $data->current_mcdc();
        my $base        = $data->baseline_mcdc();
        my $categorized = MCDC_Block->new($line);
        $data->differential_mcdc($categorized);
        # handle case that baseline and/or current do not conta MC/DC data
        my $currGroups = defined($curr) ? $curr->groups() : ();
        my $baseGroups = defined($base) ? $base->groups() : ();

        if ($type == DiffMap::INSERT) {
            # can get here if diff data vs baseline/current is not consistent
            lcovutil::ignorable_error($lcovutil::ERROR_INCONSISTENT_DATA,
                "baseline MC/DC data should not be defined for inserted line $filename:$line"
            ) if defined($base);
            _cloneMcdcEntry($categorized, $curr, 'UNC', 'GNC', 1);
        } elsif ($type == DiffMap::DELETE) {
            # similarly: get here if diff data vs baseline/current is not consistent
            lcovutil::ignorable_error($lcovutil::ERROR_INCONSISTENT_DATA,
                "current MC/DC data should not be defined for deleted line $filename:$line"
            ) if defined($curr);
            _cloneMcdcEntry($categorized, $base, 'DUB', 'DCB');
        } else {
            lcovutil::ignorable_error($lcovutil::ERROR_INTERNAL,
                      "unexpected MC/DC coverage type $type at $filename:$line")
                if $type != DiffMap::EQUAL;
            # group might or might not be in both baseline and current
            while (my ($groupSize, $bgroup) = each(%$baseGroups)) {

                if (defined($currGroups) && exists($currGroups->{$groupSize})) {
                    my $cgroup = $currGroups->{$groupSize};

                    for (my $i = 0; $i < $groupSize; ++$i) {
                        my $b = $bgroup->[$i];
                        my $c = $cgroup->[$i];
                        die("mismatched expressions")
                            unless $b->expression() eq $c->expression();

                        foreach my $sense (0, 1) {
                            my $b_count  = $b->count($sense);
                            my $c_count  = $c->count($sense);
                            my $excluded = $c->is_excluded($sense);
                            my $tla =
                                _categorize($b_count, $c_count, $excluded);
                            $categorized->insertExpr(
                                             'unknownFile', $groupSize,
                                             $sense, [$tla, $b_count, $c_count],
                                             $i, $c->expression(),
                                             $excluded);
                        }
                    }
                } else {
                    # not found in current
                    foreach my $b (@$bgroup) {
                        foreach my $sense (0, 1) {
                            my $b_count = $b->count($sense);
                            my $tla     = $b_count ? 'ECB' : 'EUB';
                            $categorized->insertExpr('unknownFile', $groupSize,
                                                $sense, [$tla, $b_count, undef],
                                                $b->index(), $b->expression());
                        }
                    }
                }
            }    #foreach group in base
        }    # endif deleted line
    }    # foreach line
}

sub _cloneFunctionEntry
{
    my ($cloneInto, $cloneFrom, $missTla, $hitTla) = @_;

    my $hit = $cloneFrom->hit();
    my $tla = (0 == $hit) ? $missTla : $hitTla;
    $cloneInto->setCountDifferential([$hit, $tla]);
    my $aliases = $cloneFrom->aliases();
    foreach my $alias (keys %$aliases) {
        $hit = $aliases->{$alias};
        $tla = (0 == $hit) ? $missTla : $hitTla;
        $cloneInto->addAliasDifferential($alias, [$hit, $tla]);
    }
}

sub _categorizeFunctionCov
{
    my ($self, $filename, $base_data, $current_data, $diffMap, $verbose) = @_;
    die("map should not be defined yet") unless !defined($self->functionMap());
    my $differentialMap = {};
    push(@$self, $differentialMap);
    my $lineDataMap = $self->lineMap();

    my $funcBase    = $base_data->func() if defined($base_data);
    my $funcCurrent = $current_data->func();
    # use merged line coverage to categorize function by checking for
    #   edits in function range
    # $lineCovBase and $lineCovCurrent are 'CountData' objects
    #  - of lineNo->hit count
    my $lineCovBase    = $base_data->sum() if defined($base_data);
    my $lineCovCurrent = $current_data->sum();

    my %funcCovLines;
    foreach my $key ($funcCurrent->keylist()) {
        my $func = $funcCurrent->findKey($key);
        my $line = $func->line();
        next if ($line <= 0);    # ignore bogus
        my $type = $diffMap->type($filename, $diffMap->NEW, $line);
        $funcCovLines{$line} = 1;
        lcovutil::ignorable_error($lcovutil::ERROR_INTERNAL,
                 "'current' line $filename:$line should not be marked 'delete'")
            if $type == DiffMap::DELETE;
        my $data;
        if (!exists($lineDataMap->{$line})) {
            $data = LineData->new($type);
            $lineDataMap->{$line} = $data;
        } else {
            $data = $lineDataMap->{$line};
            lcovutil::ignorable_error($lcovutil::ERROR_INTERNAL,
                                      "type mismatch " . $data->type() .
                                          " -> $type for $filename:$line")
                unless $data->type() == $type;
        }
        # we expect that the line number matches...
        $data->lineNo("current", $line, $filename);
        # function data for the line
        $data->current_function($func);
        if (!defined($funcBase)) {
            my $name = $func->name();
            my $categorized =
                FunctionEntry->new($name, $funcCurrent,
                                   $line, $func->end_line());
            $differentialMap->{$name} = $categorized;
            $data->differential_function($categorized);
            _cloneFunctionEntry($categorized, $func, 'UNC', 'GNC');
        }
    }    # foreach function in current data

    return unless (defined($funcBase));

    # look through the baseline to find matching data
    foreach my $key ($funcBase->keylist()) {
        my $func = $funcBase->findKey($key);
        my $line = $func->line();
        next if ($line <= 0);    # ignore bogus
        my $data = $self->_findLineData($diffMap, $filename, $line);

        my $type      = $data->type();
        my $curr_line = $data->lineNo('current');
        if ($type != DiffMap::DELETE) {
            $funcCovLines{$curr_line} = 1;
        } else {
            # the line has been deleted...just record the data
            my $deleteKey = "<<<" . $line;
            $funcCovLines{$deleteKey} = 1;
        }
        $data->baseline_function($func);
    }    # foreach function in baseline data

    # go through function data for each line and categorize...
    foreach my $line (keys %funcCovLines) {
        my $data = $lineDataMap->{$line};
        my $type = $data->type();
        my $curr = $data->current_function();
        my $base = $data->baseline_function();
        my $categorized;
        if (defined($curr)) {
            # also copy the end line
            $categorized = $curr->cloneWithEndLine(1);
        } else {
            # not in current - don't copy end line
            # @todo if needed, could compute where the end line of the deleted
            #  function is now
            $categorized = $base->cloneWithEndLine(0);
        }
        my $name = $categorized->name();
        $differentialMap->{$name} = $categorized;
        $data->differential_function($categorized);

        if (!defined($base)) {
            # either this line was inserted or the line hasn't changed but
            #   wasn't recognized as a function before (e.g., unused template)
            lcovutil::ignorable_error($lcovutil::ERROR_INCONSISTENT_DATA,
                "$filename:$line: unexpected undef baseline function data for deleted $name"
            ) if $type == DiffMap::DELETE;
            _cloneFunctionEntry($categorized,
                                $curr,
                                $type == DiffMap::INSERT ? 'UNC' : 'UIC',
                                $type == DiffMap::INSERT ? 'GNC' : 'GIC');
        } elsif (!defined($curr)) {
            lcovutil::ignorable_error($lcovutil::ERROR_INCONSISTENT_DATA,
                "$filename:$line: unexpected undef current function data for inserted $name"
            ) if $type == DiffMap::INSERT;
            _cloneFunctionEntry($categorized,
                                $base,
                                $type == DiffMap::DELETE ? 'DUB' : 'EUB',
                                $type == DiffMap::DELETE ? 'DCB' : 'ECB');
        } else {
            lcovutil::ignorable_error($lcovutil::ERROR_INTERNAL,
                   "unexpected function coverage type $type at $filename:$line")
                if $type != DiffMap::EQUAL;
            # if we know end lines for this function, then check if there
            # have been any changes in the function body.  If any changes,
            # then mark GNC or UNC
            my $changed;
            my $end_line = $curr->end_line();

            if (defined($end_line)) {
                # we keep list of functions and branch coverpoints contained
                # in the function - and can report per-function coverage
                # in the function detail view.  Maybe user will prioritize
                # functions which are hit but whose coverage is low
                $changed = 0;
                for (my $line = $curr->line(); $line <= $end_line; ++$line) {
                    # data for this line
                    my $type = $diffMap->type($filename, $diffMap->NEW, $line);

                    # claim a change if:
                    #   - line is new and is source code, OR
                    #   - line is unchanged and was code before and isn't code
                    #      now, or line wasn't code before and is now
                    if ($type == DiffMap::INSERT &&
                        defined($lineCovCurrent->value($line)))   # line is code
                    {
                        $changed = 1;
                        last;
                    } else {
                        # line is same
                        my $bline =
                            $diffMap->lookup($filename, $diffMap->NEW, $line);
                        if (defined($lineCovBase->value($bline)) ^
                            defined($lineCovCurrent->value($line))) {
                            $changed = 1;
                            last;
                        }
                    }
                }    # end for each current line in current function
                $end_line = $base->end_line();
                if (defined($end_line) && !$changed) {
                    # check for baseline lines which were deleted
                    for (my $bline = $base->line();
                         $bline <= $end_line;
                         ++$bline) {
                        # data for this line
                        my $type =
                            $diffMap->type($filename, $diffMap->OLD, $bline);
                        # claim a change if line is deleted and was code
                        #  before.
                        # note that we already checked unchanged lines, above
                        if ($type !=
                            DiffMap::EQUAL &&    # line is in old but not in new
                            defined($lineCovBase->value($bline))
                            )    # line is code
                        {
                            $changed = 1;
                            last;
                        }
                    }    # end for each current line in current function
                }
                lcovutil::info(1, "$name body at $filename:$line is changed\n")
                    if ($changed);
            }
            my $tla = _categorize($base->hit(), $curr->hit());
            if (defined($changed) &&
                $changed) {
                if ($tla eq 'UBC') {
                    $tla = 'UNC';
                } elsif ($tla eq 'GBC' ||
                         $tla eq 'CBC') {
                    $tla = 'GNC';
                }    # else is LBC - leave it that way
                lcovutil::info(2,
                            "$name recategorized to $tla at $filename:$line\n");
            }

            $categorized->setCountDifferential([$curr->hit(), $tla]);
            # particular alias may be in both versions
            my $base_aliases = $base->aliases();
            my $curr_aliases = $curr->aliases();
            foreach my $alias (keys %$base_aliases) {
                my $hit = $base_aliases->{$alias};

                my $tla;
                if (exists($curr_aliases->{$alias})) {
                    my $hitCurr = $curr_aliases->{$alias};
                    $tla = _categorize($hit, $hitCurr);
                    $hit = $hitCurr;
                    # adjust TLA if function range known..
                    if (defined($changed) &&
                        $changed) {
                        if ($tla eq 'UBC') {
                            $tla = 'UNC';
                        } elsif ($tla eq 'GBC' ||
                                 $tla eq 'CBC') {
                            $tla = 'GNC';
                        }    # else is LBC - leave it that way
                        lcovutil::info(2,
                            "$name alias $alias recategorized to $tla at $filename:$line\n"
                        );
                    }
                } else {
                    $tla = (0 == $hit) ? 'EUB' : 'ECB';
                }
                $categorized->addAliasDifferential($alias, [$hit, $tla]);
            }
            # now look for aliases that are in current but not in baseline
            foreach my $alias (keys %$curr_aliases) {
                next if exists($base_aliases->{$alias});

                my $hit = $curr_aliases->{$alias};
                my $tla = (0 == $hit) ? "UIC" : "GIC";
                $categorized->addAliasDifferential($alias, [$hit, $tla]);
            }
        }

        if ('UNK' eq $data->tla()) {
            # there is a function here - but no line - manufacture some data
            my $d = $categorized->hit();
            my ($hit, $funcTla) = @$d;
            $data->tla($funcTla);
            if (defined($base) &&
                $data->in_base()) {
                $data->base_count($base->hit());
            }
            if (defined($curr) &&
                $data->in_curr()) {
                $data->curr_count($hit);
            }
        }
    }
}

package DiffMap;

# @todo Could convert this use a callback API - similar to annotate, etc.
#  that would be more consistent and might execute faster.

use constant {
              LINEMAP  => 0,
              FILEMAP  => 1,
              BASELINE => 2,
              # keep track of line number where file entry is found in diff file
              #   - use line number in error messages.
              DIFF_FILENAME => 3,
              LOCATION      => 4,
              UNCHANGED     => 5,
              ALIASES       => 6,
              DIFF_ROOT     => 7,

              OLD  => 0,
              NEW  => 1,
              TYPE => 2,

              _START => 0,
              _END   => 1,
};

sub new
{
    my $class = shift;
    my $self = [{},          # linemap
                {},          # filemap: new_filename->old_filename
                {},          # baseline: filename -> baseline lineno -> text
                undef,       # diff filename
                [{}, {}],    # def location
                             # element 0: old filename -> line number where this
                             #             entry starts
                             # element 1: new filename -> line number
                {}           # unchanged
    ];
    bless $self, $class;
    return $self;
}

sub load
{
    my ($self, $path, $info, $buildDirs) = @_;
    $self->_read_udiff($path);

    # find list of soft links in [buildDirs] which may point to files in $info
    #  we need to keep all the aliases as there may be files in baseline
    #  which are not in current
    if (@$buildDirs) {
        my @stack = @$buildDirs;
        while (@stack) {
            my $dir = pop(@stack);
            die("unexpected non-directory '$dir'") unless -d $dir;
            $dir = File::Spec->catdir($main::cwd, $dir)
                unless File::Spec->file_name_is_absolute($dir);

            opendir(my $dh, $dir) or die("can't open directory $dir: $!");
            while (my $entry = readdir($dh)) {
                next if $entry eq '.' || $entry eq '..';
                my $path = File::Spec->catfile($dir, $entry);
                if (-d $path) {
                    push(@stack, $path);
                } elsif (-l $path) {
                    my $l = Cwd::realpath($path);
                    next if (!-e $l || TraceFile::skipCurrentFile($l));
                    # may need to turn $l into relative path??
                    $self->[ALIASES]->{$path} = $l if (-f $l);
                    #lcovutil::info("add alias '$path' -> '$l'\n");
                    # really, this should be a file...
                    die("unexpected soft link $path to directory")
                        unless -f $l;
                }
                # else just ignore file entry
            }
            closedir($dh);
        }
    }
    return $self;
}

sub empty
{
    my $self = shift;
    return !scalar(%{$self->[LINEMAP]});
}

sub findName
{
    my ($self, $file) = @_;
    my $f = $lcovutil::case_insensitive ? lc($file) : $file;
    $f = $self->[ALIASES]->{$f} if exists($self->[ALIASES]->{$f});

    if (File::Spec->file_name_is_absolute($f) &&
        !exists($self->[LINEMAP]->{$f})) {
        my $p =
            $lcovutil::case_insensitive ?
            lc($self->[DIFF_ROOT]) :
            $self->[DIFF_ROOT];
        $p .= $lcovutil::dirseparator;
        my $l = length($p);
        if (length($f) > $l &&
            $p eq substr($f, 0, $l)) {
            my $s = substr($f, $l);
            if (exists($self->[LINEMAP]->{$s})) {
                $f = $s;
            } elsif (exists($self->[ALIASES]->{$s})) {
                $f = $self->[ALIASES]->{$s};
            }
        }
    }
    return $f;
}

sub containsFile
{
    my ($self, $file) = @_;
    $file = $self->findName($file);
    return exists($self->[LINEMAP]->{$file});
}

sub recreateBaseline
{
    my ($self, $filename, $currentSrcLines) = @_;

    my $diffs = $self->[LINEMAP]->{$self->findName($filename)};
    die("no diff data for $filename") unless defined $diffs;

    my $deleted = $self->[BASELINE]->{$self->findName($filename)};
    my @lines;
    my $didWarn = 0;
    foreach my $chunk (@$diffs) {
        if ($chunk->[TYPE] == EQUAL) {
            my ($from, $to) = @{$chunk->[NEW]};
            my $f = $from - 1;
            my $t = $to - 1;
            if ($t <= $#$currentSrcLines) {
                push(@lines, @{$currentSrcLines}[$f .. $t]);
            } else {
                lcovutil::ignorable_error($lcovutil::ERROR_INCONSISTENT_DATA,
                    "$filename: inconsistent diff data vs current source code: diff refers to 'current' line range [$f:$t] but source code has only "
                        . scalar(@$currentSrcLines)
                        . " lines")
                    unless $didWarn;
                $didWarn = 1;
                # if error is ignored, then append up to the data we have
                while ($f <= $#$currentSrcLines) {
                    push(@lines, $currentSrcLines->[$f++]);
                }
            }
        } elsif ($chunk->[TYPE] == DiffMap::DELETE) {
            my $r = $chunk->[OLD];
            for (my $i = $r->[_START]; $i <= $r->[_END]; ++$i) {
                die("missing baseline line $i")
                    unless defined($deleted) && exists($deleted->{$i});
                my $d = $deleted->{$i};
                die("undef deleted line $filename:$i") unless defined($d);
                push(@lines, $d);
            }
        }
        # else 'insert':  nothing to do/those lines are not in baseline
    }
    return \@lines;
}

sub _findChunk
{
    my ($lineMap, $vers, $line, $filename) = @_;
    my $notThis = $vers == OLD ? INSERT : DELETE;
    # LCOV_EXCL_START
    my $linearSearch = sub {
        my $idx = 0;
        while ($idx <= $#$lineMap) {
            my $chunk = $lineMap->[$idx];
            my $v     = $chunk->[$vers];
            last
                if ($v->[_START] <= $line &&
                    $line <= $v->[_END] &&
                    $chunk->[TYPE] != $notThis);
            ++$idx;
        }
        return $idx;
    };
    # LCOV_EXCL_STOP

    # might be marginally faster to use linear search up to some small
    #   list size threshold...maybe 5 or 10?
    my $max = $#$lineMap;
    my $min = 0;
    my $mid;
    while (1) {
        $mid = int(($max + $min) / 2);
        my $chunk = $lineMap->[$mid];
        my $d     = $chunk->[$vers];
        if ($line < $d->[_START]) {
            $max = $mid;
        } elsif ($line > $d->[_END]) {
            $min = $mid;
        } else {
            # this entry is in range - match is either this one
            #  or the one on one side or the other.
            # There are at most 2 entries for any particular line -
            #  and there are two only if this is the insert or delete point
            if ($chunk->[TYPE] != $notThis) {
                die("bad lookup")
                    unless (!$lcovutil::debug ||
                            $mid == &$linearSearch());
                return $chunk;
            }
            # is it one side or the other?
            die("unexpectedly at beginning") if $mid == 0;
            my $prev = $lineMap->[$mid - 1];
            my $pd   = $prev->[$vers];
            if ($pd->[_END] >= $line) {
                die("unexpected prev element")
                    unless ($pd->[_START] <= $line &&
                            $prev->[TYPE] != $notThis);
                die("bad lookup 2")
                    unless (!$lcovutil::debug ||
                            $mid - 1 == &$linearSearch());
                return $prev;
            }
            if ($mid == $#$lineMap) {
                # we are at last element
                die("wrong place")
                    unless (!$lcovutil::debug ||
                            &$linearSearch() == $#$lineMap + 1);
                return undef;
            }
            my $next = $lineMap->[$mid + 1];
            my $nd   = $next->[$vers];
            die("expected 'next' to match but did not")
                unless ($nd->[_START] <= $line &&
                        $nd->[_END] >= $line &&
                        $next->[TYPE] != $notThis);
            die("bad lookup 3")
                unless (!$lcovutil::debug ||
                        $mid - 1 == &$linearSearch());
            return $next;
        }
        my $diff = $max - $min;
        if ($diff <= 1) {
            $mid = $min;
            $mid = $max
                if $line > $lineMap->[$min]->[$vers]->[_END];
            last;
        }
    }
    my $chunk = $lineMap->[$mid];
    my $d     = $chunk->[$vers];
    if ($d->[_START] <= $line &&
        $d->[_END] >= $line) {
        if ($chunk->[TYPE] != $notThis) {
            die("bad lookup 4")
                unless (!$lcovutil::debug ||
                        $mid == &$linearSearch());
            return $chunk;
        }
        if ($mid <= $#$lineMap) {
            # we aren't at the last element in the list
            my $next = $lineMap->[$mid + 1];
            my $nd   = $next->[$vers];
            if ($nd->[_START] <= $line &&
                $nd->[_END] >= $line &&
                $next->[TYPE] != $notThis) {
                die("wrong lookup")
                    unless (!$lcovutil::debug ||
                            $mid + 1 == &$linearSearch());
                return $next;
            }
        }
    }
    die("didn't find element for $filename:$line: $#$lineMap")
        unless (!$lcovutil::debug ||
                $#$lineMap + 1 == &$linearSearch());
    return undef;
}

sub lookup
{
    # given line in OLD or NEW version, return corresponding line in the
    # other.
    #  $vers == OLD if $line is in OLD (ie., we are looking for NEW
    #     and
    #  $vers == NEW if $line is in NEW and we are looking for line number in OLD

    my ($self, $file, $vers, $line) = @_;

    $file = $self->findName($file);

    if (!exists($self->[LINEMAP]->{$file})) {
        #mapping is identity when no diff was read
        return $line;
    }

    # search from end of list to find last chunk which starts before this line
    my $lineMap = $self->[LINEMAP]->{$file};
    my $chunk   = _findChunk($lineMap, $vers, $line, $file);
    my $alt     = ($vers == OLD) ? NEW : OLD;
    my $c;
    if (defined($chunk)) {
        my $delta = $line - $chunk->[$vers]->[_START];
        $c = $chunk->[$alt]->[_START] + $delta;
        $c += $chunk->[TYPE] == DiffMap::DELETE;
    } else {
        # this element is past the last chunk
        my $v     = $lineMap->[-1];
        my $delta = $line - $v->[$vers]->[_END];
        $c = $v->[$alt]->[_END] + $delta;
    }
    return $c;
}

sub type
{
    my ($self, $file, $vers, $line) = @_;

    $file = $self->findName($file);

    if (!defined($self->[LINEMAP]->{$file})) {
        #mapping is identity when no diff was read
        if (defined($main::show_tla) &&
            (@main::base_filenames ||
                $main::diff_filename)
        ) {
            return DiffMap::EQUAL;    # categories will be "GIC", "UIC"
        } else {
            return DiffMap::INSERT;    # categories will be "GNC", "UNC"
        }
    }

    if (!defined($self->[FILEMAP]->{$file})) {
        #mapping with no filemap when baseline file was deleted
        return DiffMap::DELETE;
    }
    my $lineMap = $self->[LINEMAP]->{$file};
    my $chunk   = _findChunk($lineMap, $vers, $line, $file);
    # '$line' is past the end of the last chunk in the diff -
    #   all the lines which weren't mentioned must be equal
    return EQUAL unless defined($chunk);

    die("DiffMap::type(): got undef type at $file, $vers, $line")
        unless defined($chunk->[TYPE]);
    return $chunk->[TYPE];
}

sub find_deleted_line_leader
{
    my ($self, $filename, $base_lineNo) = @_;

    # find the 'delete' chunk which contains this line -
    #   - the line number of the leader in the baseline is the line
    #     immediately before the first line in the chunk (in the 'OLD'
    #     section
    #   - the line number of the leader in the current file is the
    #     first line in the 'NEW' section of this chunk.
    #       - we expect the 'NEW' begin and end lines are the same
    #       - we also expect the leader to be the 'end' line of the
    #         'NEW' section in the previous chunk.

    my $file    = $self->findName($filename);
    my $lineMap = $self->[LINEMAP]->{$file};
    # there must be a lineMap, if we have a deleted line
    die("lineMap must exist $filename:$base_lineNo") unless defined($lineMap);

    my $chunk = _findChunk($lineMap, OLD, $base_lineNo, $filename);
    # should not fall off end of 'diff' - there must be a 'delete' chunk
    die("missing 'delete' chunk in diff $filename:$base_lineNo")
        unless defined($chunk);
    die("unexpected chunk $filename:$base_lineNo")
        unless ($chunk->[TYPE] == DELETE &&
                $chunk->[NEW]->[_START] == $chunk->[NEW]->[_END]);
    return $chunk->[NEW]->[_START];
}

sub compute_deleted_lines
{
    my ($self, $filename) = @_;
    my $file    = $self->findName($filename);
    my $lineMap = $self->[LINEMAP]->{$file};
    return undef unless defined($lineMap);
    my %hash;
    foreach my $chunk (@$lineMap) {
        next unless $chunk->[TYPE] == DELETE;
        $hash{$chunk->[NEW]->[_START]} = $chunk->[OLD];
    }
    return %hash ? \%hash : undef;
}

sub baseline_file_name
{
    # file may have been moved between baseline and current...
    my ($self, $current_name) = @_;

    my $key = $self->findName($current_name);
    if (exists($self->[FILEMAP]->{$key})) {
        return $self->[FILEMAP]->{$key};
    }
    return $current_name;
}

sub files
{
    my $self = shift;
    return keys(%{$self->[FILEMAP]});
}

sub dump_map
{
    my $self = shift;
    foreach my $file (keys %{$self->[FILEMAP]}) {
        my $currfile =
            defined($self->[FILEMAP]->{$file}) ? $self->[FILEMAP]->{$file} :
            "[deleted]";
        printf("In $file (was: $currfile):\n");
        foreach my $chunk (@{$self->[LINEMAP]->{$file}}) {
            _printChunk($chunk);
        }
    }
    return $self;
}

sub check_version_match
{
    my ($self, $baseline, $current) = @_;
    return unless $lcovutil::versionCallback;

    # skip files which were dropped (no longer in project) or were
    #  just added to the project (was not in the baseline - so we aren't
    #  looking for differences.
    foreach my $curr ($current->files()) {
        next unless $baseline->file_exists($curr);
        my $currData     = $current->data($curr);
        my $curr_version = $currData->version();
        my $baseData     = $baseline->data($curr);
        my $base_version = $baseData->version();

        # silently compare version...
        my $versionMatch =
            lcovutil::checkVersionMatch($curr, $base_version, $curr_version,
                                        "diff entry compare", 1);
        if ($self->containsFile($curr)) {
            # file is in 'diff' data:  we expect the version to be different
            #   between baseline/current
            if ($versionMatch) {
                lcovutil::ignorable_error($lcovutil::ERROR_INCONSISTENT_DATA,
                               "File \"$curr\" appears in 'diff' data file '" .
                                   $self->[DIFF_FILENAME] .
                                   "' but 'baseline' and 'current' versions '" .
                                   ($curr_version ? $curr_version : '<undef>') .
                                   "' match");
            }
        } else {
            # not in 'diff':  we expect the versions to be identical
            if (!$versionMatch) {
                lcovutil::ignorable_error($lcovutil::ERROR_INCONSISTENT_DATA,
                              "File \"$curr\" version changed from '" .
                                  ($base_version ? $base_version : '<undef') .
                                  "' to '" .
                                  ($curr_version ? $curr_version : '<undef>') .
                                  "' but file not found in 'diff' data file '" .
                                  $self->[DIFF_FILENAME] . "'.");
            }
        }
    }
}

sub check_path_consistency
{
    # check that paths which appear in diff also appear in baseline or current
    #  .info files - if not, then there is likely a path consistency issue
    # $baseline and $current are both TraceFile structs -
    # return 0 if inconsistency found

    my ($self, $baseline, $current) = @_;
    (ref($baseline) eq 'TraceFile' && ref($current) eq 'TraceFile') or
        die("wrong arg types");

    # check that files which are in both baseline and current and are NOT
    # in the 'diff' data have the same version.
    # That is:  files whose version differs should appear in the diff data
    $self->check_version_match($baseline, $current);

    my %diffMap;        # current_filename -> where_used
    my %diffBaseMap;    # current_directory -> []
                        # for every 'current' filename in the udiff file
    foreach my $f ($self->files()) {
        $diffMap{$f} = 0;    # this file not used in baseline or current (yet)
        my $b = File::Basename::basename($f);
        $diffBaseMap{$b} = [[], {}]
            unless exists($diffBaseMap{$b});
        push(@{$diffBaseMap{$b}->[0]}, $f);
    }
    # foreach unchanged file in udiff data
    foreach my $f (keys %{$self->[UNCHANGED]}) {
        # unchanged in baseline and current
        $diffMap{$f} = 3;
    }
    my %missed;
    # for each file in 'current' info:
    foreach my $curr ($current->files()) {
        my $b = File::Basename::basename($curr);
        if ($self->containsFile($curr)) {
            my $alias =
                exists($self->[ALIASES]->{$curr}) ? $self->[ALIASES]->{$curr} :
                $curr;
            $diffMap{$alias} |= 1;    # used in current
            $diffBaseMap{$b}->[1]->{$alias} = 0
                unless exists($diffBaseMap{$b}->[1]->{$alias});
            ++$diffBaseMap{$b}->[1]->{$alias};
        } elsif (!exists($self->[UNCHANGED]->{$self->findName($curr)})) {
            $missed{$curr} = 1;       # in current but not in diff
        }
    }
    # for each file in 'baseline' info:
    foreach my $base ($baseline->files()) {
        my $b = File::Basename::basename($base);
        if ($self->containsFile($base)) {
            my $alias =
                exists($self->[ALIASES]->{$base}) ? $self->[ALIASES]->{$base} :
                $base;
            $diffMap{$alias} |= 2;    # used in baseline
            $diffBaseMap{$b}->[1]->{$alias} = 0
                unless exists($diffBaseMap{$b}->[1]->{$alias});
            ++$diffBaseMap{$b}->[1]->{$alias};
        } elsif (!exists($self->[UNCHANGED]->{$self->findName($base)})) {
            # in baseline but not in diff
            if (exists($missed{$base})) {
                $missed{$base} |= 2;
            } else {
                $missed{$base} = 2;
            }
        }
    }
    my $ok = 1;
    foreach my $f (sort keys(%missed)) {
        my $b = File::Basename::basename($f);
        next unless exists($diffBaseMap{$b});

        # this basename is in the diff file and didn't match any other
        #   trace filename entry (i.e., same filename in more than one
        #   source code directory) - then warn about possible pathname issue
        my ($diffFiles, $sourceFiles) = @{$diffBaseMap{$b}};
        # find the files which appear in the 'diff' list which have the
        #   same basename and were not matched - those might be candidates
        my @unused;
        for my $d (@$diffFiles) {
            # my $location = $self->[DIFF_FILENAME] . ':' . $self->[LOCATION]->[NEW]->{$d};
            push(@unused, $d)
                unless exists($sourceFiles->{$d});
        }
        next unless @unused;

        # my $baseData = $baseline->data($f);
        # my $baseLocation = join(":", ${$baseData->location()});
        # my $currData = $current->data($f);
        # my $currLocation = join(":", ${$currData->location()});

        my $type;
        if (2 == $missed{$f}) {
            $type = "baseline";
        } elsif (1 == $missed{$f}) {
            $type = "current";
        } else {
            $type = "both baseline and current";
        }
        my $single = 1 == scalar(@unused);
        # @todo could print line numbers in baseline, current .info files and
        #   in diff file ..
        if (lcovutil::warn_once($lcovutil::ERROR_MISMATCH, $f)) {
            my $suffix =
                $main::elide_path_mismatch ? ' (elided)' :
                lcovutil::explain_once(
                'elide-path-mismatch',
                '  Perhaps see "--elide-path-mismatch", "--substitute" and "--build-directory" options in \'man '
                    . $lcovutil::tool_name . '\'');
            lcovutil::ignorable_warning(
                                       $lcovutil::ERROR_MISMATCH,
                                       "source file '$f' (in $type .info file" .
                                           ($missed{$f} == 3 ? "s" : "") .
                                           ") has same basename as 'diff' "
                                           .
                                           ($single ? 'entry ' : "entries:\n\t"
                                           ) .
                                           "'" .
                                           join("'\n\t", @unused) .
                                           "' - but a different path." .
                                           ($single ? "  " : "\n\t") .
                                           'Possible pathname mismatch?' .
                                           $suffix);
        }
        if ($main::elide_path_mismatch &&
            $missed{$f} == 3 &&
            $single) {
            $self->[FILEMAP]->{$f} = $f;
            $self->[LINEMAP]->{$f} = $self->[LINEMAP]->{$unused[0]};
        } else {
            $ok = 0;
        }
    }
    return $ok;
}

sub _printChunk
{
    my $chunk = shift;
    printf("  %6s\t[%d:%d]\t[%d:%d]\n",
           $DiffMap::type2str[$chunk->[TYPE]],
           $chunk->[OLD]->[_START],
           $chunk->[OLD]->[_END],
           $chunk->[NEW]->[_START],
           $chunk->[NEW]->[_END]);
}

sub _newChunk
{
    my ($type, $baseline_start, $current_start) = @_;
    # new chunk starts and ends on the same line - until we see more lines and
    #   extend either the old or new range
    return [[$baseline_start, $baseline_start],
            [$current_start, $current_start],
            $type
    ];
}

sub show_map
{
    my ($self, $file) = @_;

    $file = $self->findName($file);
    return $self unless exists($self->[FILEMAP]->{$file});
    my $currfile =
        defined($self->[FILEMAP]->{$file}) ? $self->[FILEMAP]->{$file} :
        "[deleted]";
    print("In $file" . ($currfile ne $file ? " (was: $currfile)" : '') . ":\n");
    foreach my $chunk (@{$self->[LINEMAP]->{$file}}) {
        _printChunk($chunk);
    }
    return $self;
}

sub _read_udiff
{
    my $self      = shift;
    my $diff_file = shift;    # Name of diff file
    my $line;                 # Contents of current line
    my $file_old;             # Name of old file in diff section
    my $file_new;             # Name of new file in diff section
    my $filename;             # Name of common filename of diff section

    # Check if file exists and is readable
    stat($diff_file);
    if (!(-r _)) {
        die("cannot read udiff file $diff_file!\n");
    }

    $self->[DIFF_FILENAME] = $diff_file;

    my $diffFile = InOutFile->in($diff_file);
    my $diffHdl  = $diffFile->hdl();

    my $chunk;
    my $old_block = 0;
    my $new_block = 0;
    # would like to use Regexp::Common::time - but module not installed
    #my $time = $RE{time}{iso};
    my $time =
        '[1-9]{1}[0-9]{3}\-[0-9]{2}\-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]*)?( .[0-9]+)?';
    # Parse diff file line by line
    my $verbose         = 0;
    my $skipCurrentFile = 0;
    while (<$diffHdl>) {
        chomp($_);
        s/\r//g;
        $line = $_;

        # the 'diff' new/old file name line may be followed by a timestamp
        #   If so, remove it so our regexp matches more easily.
        # p4 and git diff outputs do not have the timestamp
        if ($line =~ /^[-+=]{3} \S.*(\s+$time)$/) {
            $line =~ s/\Q$1\E$//;
        }
        foreach ($line) {
            /^Git Root: (.+)$/ && do {
                $self->[DIFF_ROOT] = ReadCurrentSource::resolve_path($1, 1);
                last;
            };

            # Filename of unchanged file:
            # === <filename>
            /^=== (.+)$/ && do    # note: filename may contain whitespace
            {
                if ($filename && !$skipCurrentFile) {
                    die("not case insensitive")
                        unless !$lcovutil::case_insensitive ||
                        ($filename eq lc($filename));
                    push(@{$self->[LINEMAP]->{$filename}}, $chunk);
                    undef $filename;
                }
                my $file = $1;
                $file = lcovutil::strip_directories($file, $main::strip);
                my $key = ReadCurrentSource::resolve_path($file, 1);
                # skip diff data for excluded files - but skip quietly
                $skipCurrentFile = TraceFile::skipCurrentFile($key, 'diff');
                last if $skipCurrentFile;

                $key = lc($key) if $lcovutil::case_insensitive;
                if (exists($self->[UNCHANGED]->{$key})) {
                    # unchanged entry flag value should be 1
                    die("$diff_file:$.:  $key already in linemap - marked unchanged"
                            . ($file eq $key ? '' : " (substituted '$file')"))
                        unless $self->[UNCHANGED]->{$key};
                    lcovutil::ignorable_error($lcovutil::ERROR_FORMAT,
                        "$diff_file:$.:  duplicate 'unchanged' entry for $key\n"
                    );
                    last;
                }
                $verbose = (defined($main::verboseScopeRegexp) &&
                            $key =~ m/$main::verboseScopeRegexp/);
                print("  $key: unchanged\n") if $verbose;
                if (exists($self->[LINEMAP]->{$key})) {
		    # LCOV_UNREACHABLE_START
                    lcovutil::ignorable_error($lcovutil::ERROR_INTERNAL,
                        "$diff_file:$.:  $key 'unchanged' entry found but diff records exist"
                            . ($file eq $key ? '' : " (substituted '$file')"));
                    # message ignored - delete the diff records
                    delete($self->[LINEMAP]->{$key});
		    # LCOV_UNREACHABLE_STOP
                }
                $self->[UNCHANGED]->{$key} = 1;
                last;
            };

            next if $skipCurrentFile;

            # Filename of old file:
            # --- <filename>
            /^--- (.+)$/ && do {
                if ($filename) {
                    die("not case insensitive")
                        unless !$lcovutil::case_insensitive ||
                        ($filename eq lc($filename));
                    push(@{$self->[LINEMAP]->{$filename}}, $chunk);
                    undef $filename;
                }
                $file_old = $1;
                $file_old =
                    lcovutil::strip_directories($file_old, $main::strip);
                $file_old = ReadCurrentSource::resolve_path($file_old, 1);
                $file_old = lc($file_old) if $lcovutil::case_insensitive;
                $self->[LOCATION]->[OLD]->{$file_old} = $.;
                last;
            };
            # Filename of new file:
            # +++ <filename>
            /^\+\+\+ (.+)$/ && do {
                # Add last file to resulting hash
                $file_new = $1;
                $file_new =
                    lcovutil::strip_directories($file_new, $main::strip);
                my $key = ReadCurrentSource::resolve_path($file_new, 1);
                $key = lc($key)
                    if $lcovutil::case_insensitive;
                my $notNull = $file_new ne $lcovutil::devnull;
                $filename = $notNull ? $key : undef;

                if ($filename) {
                    if (exists($self->[UNCHANGED]->{$key})) {
			# LCOV_UNREACHABLE_START
                        lcovutil::ignorable_error(
                            $lcovutil::ERROR_INTERNAL,
                            "$diff_file:$.: $filename marked unchanged but diff record found"
                                .
                                ($file_new eq $key ? '' :
                                     " (substituted '$file_new')"));
                        # OK - error ignored...remove the 'unchanged' marker
                        delete($self->[UNCHANGED]->{$key});
			# LCOV_UNREACHABLE_STOP
                    }
                    $self->[LINEMAP]->{$key} = [];
                }
                $verbose = (defined($main::verboseScopeRegexp) &&
                            $key =~ m/$main::verboseScopeRegexp/);
                print("file $key\n") if $verbose;
                if ($notNull) {
                    $self->[FILEMAP]->{$key} = $file_old;
                    # keep track of location where this file was found
                    $self->[LOCATION]->[NEW]->{$key} =
                        $.;    # record original name too.. $file_new
                }
                # new file - chunk starts here
                $chunk = _newChunk(EQUAL, 1, 1);
                last;
            };
            # Start of diff block:
            # @@ -old_start,old_num, +new_start,new_num @@
            /^\@\@\s+-(\d+),(\d+)\s+\+(\d+),(\d+)\s+\@\@.*$/ && do {
                if ($1 > ($chunk->[OLD]->[_END])) {
                    # old start skips "equal" lines
                    if ($chunk->[TYPE] != EQUAL) {
                        if ($filename) {
                            push(@{$self->[LINEMAP]->{$filename}}, $chunk);
                            _printChunk($chunk) if ($verbose);
                        }
                        $chunk =
                            _newChunk(EQUAL,
                                      $chunk->[OLD]->[_END] + 1,
                                      $chunk->[NEW]->[_END] + 1);
                    }
                }
                # "END" will be incremented on content lines
                $chunk->[OLD]->[_END] = $1 - 1;
                $chunk->[NEW]->[_END] = $3 - 1;
                $old_block            = $2;
                $new_block            = $4;
                #printf "equal [%d:%d] [%d:%d]\n", $l[0], $l[1], $l[2], $l[3];
                last;
            };
            # Unchanged line
            # <line starts with blank>
            /^ / && do {
                if ($old_block == 0 && $new_block == 0) {
                    last;
                }
                if ($chunk->[TYPE] != EQUAL) {
                    if ($filename) {
                        push(@{$self->[LINEMAP]->{$filename}}, $chunk);
                        _printChunk($chunk) if ($verbose);
                    }
                    # next chunk starts right after current one
                    $chunk = _newChunk(EQUAL,
                                       $chunk->[OLD]->[_END] + 1,
                                       $chunk->[NEW]->[_END] + 1);
                } else {
                    $chunk->[NEW]->[_END] += 1;
                    $chunk->[OLD]->[_END] += 1;
                }
                last;
            };
            # Line as seen in old file
            # <line starts with '-'>
            /^-(.*)$/ && do {
                if ($old_block == 0 && $new_block == 0) {
                    last;
                }
                my $baseline_lineno = $chunk->[OLD]->[_END] + 1;
                # really only need to keep track of baseline content if user
                #   is planning on doing any filtering
                my $lines;
                if (exists($self->[BASELINE]->{$file_old})) {
                    $lines = $self->[BASELINE]->{$file_old};
                } else {
                    $lines = {};
                    $self->[BASELINE]->{$file_old} = $lines;
                }
                $lines->{$baseline_lineno} = $1;

                if ($chunk->[TYPE] != DELETE) {
                    if ($filename) {
                        push(@{$self->[LINEMAP]->{$filename}}, $chunk);
                        _printChunk($chunk) if ($verbose);
                    }
                    $chunk = _newChunk(DELETE, $baseline_lineno,
                                       $chunk->[NEW]->[_END]);
                } else {
                    $chunk->[OLD]->[_END] = $baseline_lineno;
                }
                last;
            };
            # Line as seen in new file
            # <line starts with '+'>
            /^\+/ && do {
                if ($old_block == 0 && $new_block == 0) {
                    last;
                }
                if ($chunk->[TYPE] != INSERT) {
                    if ($filename) {
                        push(@{$self->[LINEMAP]->{$filename}}, $chunk);
                        _printChunk($chunk) if ($verbose);
                    }
                    $chunk = _newChunk(INSERT,
                                       $chunk->[OLD]->[_END],
                                       $chunk->[NEW]->[_END] + 1);
                } else {
                    $chunk->[NEW]->[_END] += 1;
                }
                last;
            };
            # Empty line
            /^$/ && do {
                if ($old_block == 0 && $new_block == 0) {
                    last;
                }
                if ($chunk->[TYPE] != EQUAL) {
                    if ($filename) {
                        push(@{$self->[LINEMAP]->{$filename}}, $chunk);
                        _printChunk($chunk) if ($verbose);
                    }
                    $chunk = _newChunk(EQUAL,
                                       $chunk->[OLD]->[_END] + 1,
                                       $chunk->[NEW]->[_END] + 1);
                } else {
                    $chunk->[NEW]->[_END] += 1;
                    $chunk->[OLD]->[_END] += 1;
                }
                last;
            };
        }
    }

    # Add final diff file section to resulting hash
    if ($filename) {
        push(@{$self->[LINEMAP]->{$filename}}, $chunk);
        _printChunk($chunk) if ($verbose);
    }

    # default root
    $self->[DIFF_ROOT] = $main::cwd unless defined($self->[DIFF_ROOT]);

    if ($self->empty()) {
        # this is probably OK - there are no differences between 'baseline' and current.
        lcovutil::ignorable_error($lcovutil::ERROR_EMPTY,
            "'diff' data file $diff_file contains no differences (this may be OK, if there are no difference between 'baseline' and 'current').\n"
                . "Make sure to use 'diff -u' when generating the diff file.");
    }
    return $self;
}

package InInterestingRegion;

use constant {
              NEXT         => 0,
              PREV         => 1,
              STACK        => 2,
              ANNOTATE_OBJ => 3,
              LINECOV_HASH => 4,
              # non-code line in a contiguous region before or after the
              # 'interesting' code lie - should also be included -
              # keep track of the length of that region, above and below
              LINES_BEFORE => 5,
              LINES_AFTER  => 6,

};

our $num_context_lines = 5;

sub new
{
    my ($class, $srcFileStruct, $lineCovHash) = @_;

    my $self = [undef, undef,
                Storable::dclone($srcFileStruct->interesting_lines()),
                $srcFileStruct, $lineCovHash
    ];
    # we will call the $annotateObj->line(int) to collect annotate data
    #   for certain lines - to find out of a non-code line is in a
    #   contiguous region by the same author, in the same SHA or etc.
    $self->[NEXT] = shift(@{$self->[STACK]});
    if (defined($self->[NEXT]) && $self->[NEXT] != 1) {
        _computeContextBefore($self, 0);
    }
    return bless $self, $class;
}

sub _computeContextBefore
{
    my ($self, $prev) = @_;

    my $count    = 0;
    my $data     = $self->[LINECOV_HASH];
    my $annotate = $self->[ANNOTATE_OBJ];
    # find contiguous source region which matches the select criteria.
    #   That might include non-code lined (e.g., comments)
    for (my $l = $self->[NEXT] - 1; $l > $prev; --$l) {
        my $lineData     = $data->{$l} if exists($data->{$l});
        my $annotateData = $annotate->line($l);
        last
            unless SummaryInfo::selected($lineData, $annotateData,
                                         $self->[ANNOTATE_OBJ]->path(), $l);
        ++$count;
    }
    # region of interest is the (possibly empty) contiguous region we found
    # plus the number of context lines
    $self->[LINES_BEFORE] = $num_context_lines + $count;
}

sub interesting
{
    my ($self, $lineNum) = @_;
    die("unexpected lineNum '$lineNum'") if $lineNum < 1;
    if (defined($self->[NEXT])) {
        if ($lineNum == $self->[NEXT]) {
            $self->[PREV] = $self->[NEXT];
            $self->[NEXT] = shift(@{$self->[STACK]});
            # compute following context lines - starting from here
            #  and going to either end of file or next interesting line
            my $max = defined($self->[NEXT]) ? $self->[NEXT] :
                $self->[ANNOTATE_OBJ]->num_lines();
            my $count    = 0;
            my $data     = $self->[LINECOV_HASH];
            my $annotate = $self->[ANNOTATE_OBJ];
            for (my $l = $self->[PREV] + 1; $l < $max; ++$l) {
                my $lineData     = $data->{$l} if exists($data->{$l});
                my $annotateData = $annotate->line($l);
                last
                    if !SummaryInfo::selected($lineData, $annotateData,
                                             $self->[ANNOTATE_OBJ]->path(), $l);
                ++$count;
            }
            $self->[LINES_AFTER] = $num_context_lines + $count;

            # and how many lines before the next interesting one?
            if (defined($self->[NEXT])) {
                $self->_computeContextBefore($self->[PREV]);
            } else {
                $self->[LINES_BEFORE] = 0;
            }
            return 1;
        } elsif ($lineNum >= $self->[NEXT] - $self->[LINES_BEFORE]) {
            return 1;
        }
    }
    if (defined($self->[PREV])) {
        if ($lineNum <= $self->[PREV] + $self->[LINES_AFTER]) {
            return 1;
        }
        $self->[PREV] = undef;
    }
    return 0;
}

package SourceLine;

use constant {
              LINE         => 0,
              TEXT         => 1,
              OWNER        => 2,
              FULL_NAME    => 3,
              DATE         => 4,
              AGE          => 5,
              COMMIT       => 6,
              LINE_TLA     => 7,
              BRANCH_TLA   => 8,
              MCDC_TLA     => 9,
              FUNCTION_TLA => 10,
};

sub new
{
    my $class = shift;
    # [lineNo, text, abbrev_name, full_name, date, age, commitID, lineTLA, branchTLA,
    #  MCDC_TLA, functionTLA]
    my @data = @_;
    my $self = \@data;
    bless $self, $class;
    return $self;
}

sub to_list
{
    # used by 'select' callback - if not a package
    my $self = shift;
    if ($#$self >= OWNER) {
        my @rtn = @{$self}[OWNER, FUNCTION_TLA];
        return \@rtn;
    }
    return undef;
}

sub owner
{
    my $self = shift;
    return $#$self >= OWNER ? $self->[OWNER] : undef;
}

sub full_name
{
    my $self = shift;
    return $#$self >= FULL_NAME ? $self->[FULL_NAME] : undef;
}

# line coverage TLA
sub tla
{
    my ($self, $tla) = @_;
    $self->[LINE_TLA] = $tla
        if (defined($tla));
    return $#$self >= LINE_TLA ? $self->[LINE_TLA] : undef;
}

sub branchElem
{
    my ($self, $branchElem) = @_;
    $self->[BRANCH_TLA] = $branchElem
        if defined($branchElem);
    return $#$self >= BRANCH_TLA ? $self->[BRANCH_TLA] : undef;
}

sub mcdcElem
{
    my ($self, $mcdcElem) = @_;
    $self->[MCDC_TLA] = $mcdcElem
        if defined($mcdcElem);
    return $#$self >= MCDC_TLA ? $self->[MCDC_TLA] : undef;
}

sub functionElem
{
    my ($self, $funcElem) = @_;
    $self->[FUNCTION_TLA] = $funcElem
        if defined($funcElem);
    return $#$self >= FUNCTION_TLA ? $self->[FUNCTION_TLA] : undef;
}

sub commit
{
    my $self = shift;
    return $#$self >= COMMIT ? $self->[COMMIT] : undef;
}

sub date
{
    my $self = shift;
    return $#$self >= DATE ? $self->[DATE] : undef;
}

sub age
{
    my $self = shift;
    return $#$self >= AGE ? $self->[AGE] : undef;
}

sub line
{
    my $self = shift;
    return $self->[LINE];
}

sub text
{
    my $self = shift;
    return $self->[TEXT];
}

package SourceFile;
our @annotateScript;
our $annotateCallback;
our $annotateTooltip = 'Line %l: commit %C on %d by %F';
our $annotatedFiles  = 0;
our $totalFiles      = 0;

use constant {
              PATH              => 0,
              LINES             => 1,
              INTERESTING_LINES => 2,
              LINE_OWNERS       => 3,
              LINE_CATEGORIES   => 4,
              BRANCH_OWNERS     => 5,
              BRANCH_CATEGORIES => 6,
              MCDC_OWNERS       => 7,
              MCDC_CATEGORIES   => 8,
};

sub new
{
    my ($class, $filepath, $fileSummary, $fileCovInfo, $hasNoBaselineData) = @_;

    (ref($fileSummary) eq 'SummaryInfo' &&
     ref($fileCovInfo) eq "FileCoverageInfo") or
        die("unexpected input args");

    my $self = [$filepath,
                [],       # lines
                undef,    # list of interesting lines
    ];
    foreach my $t (LINE_OWNERS, LINE_CATEGORIES,
                   BRANCH_OWNERS, BRANCH_CATEGORIES,
                   MCDC_OWNERS, MCDC_CATEGORIES
    ) {
        # table of owner -> hash of TLAs->list of lines for coverpoint type
        # table of TLA -> list of lines holding that TLA for coverpoint
        push(@$self, {});
    }
    bless $self, $class;

    $fileSummary->fileDetails($self);

    # use the line coverage count to synthesize a fake file, if we can't
    #   find an actual file
    $self->_load($fileCovInfo);

    if ($hasNoBaselineData) {
        my $fileAge = $self->age();
        if (defined($fileAge) &&
            (!defined($main::age_basefile) ||
                $fileAge > $main::age_basefile)
        ) {
            # go through the fileCov data and change UIC->UBC, GIC->CBC
            #  - pretend that we already saw this file data - this is the first
            #    coverage report which contains this data.
            $fileCovInfo->recategorizeTlaAsBaseline();
        }
    }

    # sort lines in ascending numerical order - we want the 'owner'
    #   and 'tla' line lists to be sorted - and it is probably faster to
    #   sort the file list once than to sort each of the sub-lists
    #   individually afterward.
    # DCB, DUB category keys have leading "<<<" characters - which we strip
    #  in order to compare
    my $currentTla;
    my $regionStartLine;
    my $lineCovData = $fileCovInfo->lineMap();

    my @lineArray;    # interesting line numbers
    my $inRegion;
    if (defined($selectCallback)) {
        # This implementation looks for all the 'line' coverpoints - then
        #   expands the selected region around those points - e.g., by
        #   adding noncode lines which are in the selected changelist and
        #   surrounding the selected region with 'num_context_lines' of
        #   context.
        # A side effect of this criteria is that disjoint noncode regions -
        #   e.g., comments or unused #ifdef code - will not be selected.
        # A different implementation would go through the annotated source
        #   and mark all selected lines, then go through again to add
        #   context.
        # It isn't clear which approach is best - but the current approach
        #   seems to match user expectations - so I'm going with it, at
        #   least for now.
        #   Not too hard to do - but significant overkill - to make the
        #   approach configurable.  Let's see if there is user demand.
        while (my ($line, $lne) = each(%$lineCovData)) {
            # ignore deleted lines as they don't appear in source listing
            my $tla = $lne->tla();
            # no annotations for deleted line
            my $annotateData = $self->line($line)
                unless grep(/$tla/, ('DUB', 'DCB'));
            # callback arguments are (LineData, SourceLine)
            if (SummaryInfo::selected($lne, $annotateData,
                                      $self->path(), $line)) {
                push(@lineArray, $line);
            }
        }
        @lineArray                 = sort({ $a <=> $b } @lineArray);
        $self->[INTERESTING_LINES] = \@lineArray;
        $inRegion = InInterestingRegion->new($self, $lineCovData);
    }
    foreach my $line (sort({
                               my $ka =
                                   ("<" ne substr($a, 0, 1)) ? $a :
                                   substr($a, 3);
                               my $kb =
                                   ("<" ne substr($b, 0, 1)) ? $b :
                                   substr($b, 3);
                               $ka <=> $kb
                      } keys(%{$lineCovData}))
    ) {
        # is this line within N of an interesting one?
        my $lne = $lineCovData->{$line};
        my $tla = $lne->tla();
        # deleted line associated with first 'current' line above -
        # or line below, if there is no current line above
        my $lineNum =
            grep(/$tla/, ('DUB', 'DCB')) ? -$lne->lineNo('current') : $line;

        if (defined($inRegion) &&
            !$inRegion->interesting($lineNum)) {
            lcovutil::info(1,
                           "  drop $line" .
                               ($line eq $lineNum ? '' : " ($lineNum)") . "\n");
            $fileSummary->removeLine($lne);
            delete($lineCovData->{$line});
            next;
        }
        $self->_countLineTlaData($line, $lne, $fileSummary);

        $self->_countBranchTlaData($line, $lne, $fileSummary)
            if ($lcovutil::br_coverage && defined($lne->differential_branch()));

        $self->_countMcdcTlaData($line, $lne, $fileSummary)
            if ($lcovutil::mcdc_coverage && defined($lne->differential_mcdc()));

        $self->_countFunctionTlaData($line, $lne, $fileSummary)
            if ($lcovutil::func_coverage &&
                defined($lne->differential_function()));
    }
    if (defined($main::show_dateBins) &&
        %$lineCovData &&                    # haven't filtered out everything
        !$self->isProjectFile()
    ) {
        lcovutil::info("no owner/date info for '$filepath'\n");
    }
    return $self;
}

sub simplify
{
    my $self = shift;
    # retain only the information required to populate the hyperlinks in
    #   the parent 'directory details' table
    # This struct can be huge, for a large, complicated file
    $self->[INTERESTING_LINES] = undef;

    my %keep;
    # keep the first line in each date bin..
    for (my $bin = 0; $bin <= $#ageGroupHeader; ++$bin) {
        foreach my $tla (keys %{$self->[LINE_CATEGORIES]}) {
            # at least for the moment, skip deleted lines
            next if grep(/$tla/, ('DUB', 'DCB'));
            my $line = $self->nextInDateBin($bin, $tla);
            $keep{$line} = 1 if defined($line);
        }
        if ($lcovutil::br_coverage) {
            foreach my $tla (keys %{$self->[BRANCH_CATEGORIES]}) {
                next if grep(/$tla/, ('DUB', 'DCB', 'EUC', 'ECC'));
                my $line = $self->nextBranchInDateBin($bin, $tla);
                $keep{$line} = 1 if defined($line);
            }
        }
        if ($lcovutil::mcdc_coverage) {
            foreach my $tla (keys %{$self->[MCDC_CATEGORIES]}) {
                next if grep(/$tla/, ('DUB', 'DCB', 'EUC', 'ECC'));
                my $line = $self->nextMcdcInDateBin($bin, $tla);
                $keep{$line} = 1 if defined($line);
            }
        }
    }

    # retain first location of each TLA (globally) and each TLA for each owner
    foreach my $bin (LINE_OWNERS, LINE_CATEGORIES,
                     BRANCH_OWNERS, BRANCH_CATEGORIES,
                     MCDC_OWNERS, MCDC_CATEGORIES
    ) {
        while (my ($key, $list) = each(%{$self->[$bin]})) {
            if ('ARRAY' eq ref($list)) {
                next if grep(/$key/, ('DUB', 'DCB', 'EUC', 'ECC'));
                # retain the first entry and any interesting entries
                $keep{$list->[0]} = 1;
                @$list = grep(exists($keep{$_}), @$list);
            } else {
                die("unexpected type") unless 'HASH' eq ref($list);
                while (my ($tla, $l) = each(%$list)) {
                    next if grep(/$tla/, ('DUB', 'DCB', 'EUC', 'ECC'));
                    # retain the first entry
                    $keep{$l->[0]} = 1;
                    @$l = grep(exists($keep{$_}), @$l);
                }
            }
        }
    }
    # throw away everything we aren't interested in
    for (my $l = $self->num_lines(); $l > 0; --$l) {
        if (!exists($keep{$l})) {
            $self->[LINES]->[$l - 1] = undef;
        }
    }
}

sub _countBranchTlaData
{
    my ($self, $line, $lineData, $fileSummary) = @_;
    my $differentialData = $lineData->differential_branch();

    my ($src_age, $developer, $srcLine);
    my $lineTla = $lineData->tla();

    if ($SummaryInfo::tlaLocation{$lineTla} & 0x1) {
        # skip check if line was deleted - no age/owner data for deleted code
        $srcLine = $self->line($line);
        if (!defined($srcLine)) {
            lcovutil::ignorable_error($lcovutil::ERROR_UNMAPPED_LINE,
                       "no data for 'branch' line:$line, file:" . $self->path())
                if (!$lcovutil::warn_once_per_file ||
                    lcovutil::warn_once($lcovutil::ERROR_UNMAPPED_LINE,
                                        $self->path()));
        } else {
            $srcLine->branchElem($differentialData);
            if (@SourceFile::annotateScript &&
                !grep(/^$lineTla$/, ('DUB', 'DCB'))) {
                # deleted lines don't have owner data...
                # if this line is not in the project (e.g., from some 3rd party
                #   library - then we might not have file history for it.
                $src_age   = $srcLine->age();
                $developer = $srcLine->owner();

                if (defined($developer)) {
                    my $shash = $self->[BRANCH_OWNERS];
                    if (!exists($shash->{$developer})) {
                        $shash->{$developer} = {};
                        $shash->{$developer}->{lines} = [];
                    }
                    my $l = $shash->{$developer}->{lines};
                    # NOTE: string compare as either line number could be
                    #   "<<<X" if it was deleted
                    push(@$l, $line)
                        unless @$l && $l->[-1] eq $line;
                }
            }
        }
    }

    foreach my $block ($differentialData->blocks()) {
        my $diff    = $block->elements();
        my $blockId = $block->idx();
        foreach my $br (@$diff) {
            my $tla = $br->tla();
            # don't count excluded coverpoints
            unless (defined($tla)) {
		# LCOV_UNREACHABLE_START
                lcovutil::ignorable_error($lcovutil::ERROR_INTERNAL,
                          "undef TLA for branch $blockId of " . $self->path() .
                              ":$line - lineTLA:$lineTla taken:" . $br->data());
                next;
		# LCOV_UNREACHABLE_STOP
            }
            next if grep(/$tla/, ('EUC', 'ECC'));
            $fileSummary->[SummaryInfo::BRANCH_DATA]->[SummaryInfo::DATA]
                ->{$tla} += 1;
            # keep track of all the branch TLAs found on this line...
            $self->[BRANCH_CATEGORIES]->{$tla} = []
                unless exists($self->[BRANCH_CATEGORIES]->{$tla});
            my $l = $self->[BRANCH_CATEGORIES]->{$tla};
            # see 'string compare' comment above
            push(@$l, $line)
                unless @$l && $l->[-1] eq $line;
            next
                if (0 == ($SummaryInfo::tlaLocation{$tla} & 0x1));
            # skip "DUB' and 'DCB' categories - which are not in current
            #   and thus have no line associated
            # and the age...
            #lcovutil::info("$l: $tla" . $lineData->in_curr() . "\n");

            unless (defined($srcLine) && defined($src_age)) {
                # just count totals
                $fileSummary->branchCovCount($tla, 'noGroup', undef, 1);
                next;
            }

            # increment count of branches of this age we found for this TLA
            $fileSummary->branchCovCount($tla, "age", $src_age, 1);

            # HGC:  could clean this up...no need to keep track
            #   of 'hit' as we can just compute from CBC + GNC + ...
            # found another line...
            my $hit = grep(/$tla/, ('GBC', 'GIC', 'GNC', 'CBC'));
            $fileSummary->branchCovCount("found", "age", $src_age, 1);
            $fileSummary->branchCovCount("hit", "age", $src_age, 1)
                if $hit;
            next
                unless defined($developer);
            # add this line to those that belong to this owner..
            # first:  increment line count in 'file summary'
            my $ohash =
                $fileSummary->[SummaryInfo::BRANCH_DATA]->[SummaryInfo::OWNERS];
            if (!exists($ohash->{$developer})) {
                my %data = ('hit'   => $hit ? 1 : 0,
                            'found' => 1,
                            $tla    => 1);
                $ohash->{$developer} = \%data;
            } else {
                my $d = $ohash->{$developer};
                $d->{$tla} = 0
                    unless exists($d->{$tla});
                $d->{$tla}  += 1;
                $d->{found} += 1;
                $d->{hit}   += 1
                    if $hit;
            }
            # now append this branchTLA to the owner...
            my $dhash = $self->[BRANCH_OWNERS]->{$developer};
            die("expected initialized") unless defined($dhash);
            $dhash->{$tla} = []
                unless exists($dhash->{$tla});
            my $ll = $dhash->{$tla};
            # see 'string compare' comment above
            push(@$ll, $line)
                unless @$ll && $ll->[-1] eq $line;
        }
    }
}

sub _countMcdcTlaData
{
    my ($self, $line, $lineData, $fileSummary) = @_;
    my $differentialData = $lineData->differential_mcdc();

    my ($src_age, $developer, $srcLine);
    my $lineTla = $lineData->tla();
    if ($SummaryInfo::tlaLocation{$lineTla} & 0x1) {
        # skip check if line was deleted - no age/owner data for deleted code
        $srcLine = $self->line($line);
        if (!defined($srcLine)) {
            lcovutil::ignorable_error($lcovutil::ERROR_UNMAPPED_LINE,
                        "no data for 'MC/DC' line:$line, file:" . $self->path())
                if (!$lcovutil::warn_once_per_file ||
                    lcovutil::warn_once($lcovutil::ERROR_UNMAPPED_LINE,
                                        $self->path()));
        } else {
            $srcLine->mcdcElem($differentialData);
            if (@SourceFile::annotateScript &&
                !grep(/^$lineTla$/, ('DUB', 'DCB'))) {
                # deleted lines don't have owner data...
                # if this line is not in the project (e.g., from some 3rd party
                #   library - then we might not have file history for it.
                $src_age   = $srcLine->age();
                $developer = $srcLine->owner();

                if (defined($developer)) {
                    my $shash = $self->[MCDC_OWNERS];
                    if (!exists($shash->{$developer})) {
                        $shash->{$developer} = {};
                        $shash->{$developer}->{lines} = [];
                    }
                    my $l = $shash->{$developer}->{lines};
                    # see 'string compare' comment in branch method, above
                    push(@$l, $line)
                        unless @$l && $l->[-1] eq $line;
                }
            }
        }
    }

    while (my ($groupSize, $group) = each(%{$differentialData->groups()})) {
        foreach my $expr (@$group) {
            foreach my $sense (0, 1) {
                my $count = $expr->count($sense);
                my $tla   = $count->[0];
                # don't count excluded coverpoints
                next if grep(/$tla/, ('EUC', 'ECC'));
                unless (defined($tla)) {
		    # LCOV_UNREACHABLE_START
                    lcovutil::ignorable_error($lcovutil::ERROR_INTERNAL,
                                        "undef TLA for sense $sense of MC/DC " .
                                            $expr->index() .
                                            ' of ' . $self->path() .
                                            ":$line - lineTLA:$lineTla taken:" .
                                            $count->[1]);
                    next;
		    # LCOV_UNREACHABLE_STOP
                }
                $fileSummary->[SummaryInfo::MCDC_DATA]->[SummaryInfo::DATA]
                    ->{$tla} += 1;
                # keep track of all the branch TLAs found on this line...
                if (!exists($self->[MCDC_CATEGORIES]->{$tla})) {
                    $self->[MCDC_CATEGORIES]->{$tla} = [];
                }
                my $l = $self->[MCDC_CATEGORIES]->{$tla};
                # see 'string compare' comment in branch method, above
                push(@$l, $line)
                    unless @$l && $l->[-1] eq $line;

                next
                    if (0 == ($SummaryInfo::tlaLocation{$tla} & 0x1));
                # skip "DUB' and 'DCB' categories - which are not in current
                #   and thus have no line associated

                # and the age...
                #lcovutil::info("$l: $tla" . $lineData->in_curr() . "\n");

                unless (defined($srcLine) && defined($src_age)) {
                    # just count totals
                    $fileSummary->mcdcCovCount($tla, 'noGroup', undef, 1);
                    next;
                }

                # increment count of branches of this age we found for this TLA
                $fileSummary->mcdcCovCount($tla, "age", $src_age, 1);

                my $hit = grep(/$tla/, ('GBC', 'GIC', 'GNC', 'CBC'));
                $fileSummary->mcdcCovCount("found", "age", $src_age, 1);
                $fileSummary->mcdcCovCount("hit", "age", $src_age, 1)
                    if $hit;

                next
                    unless defined($developer);

                # add this line to those that belong to this owner..

                # first:  increment line count in 'file summary'
                my $ohash =
                    $fileSummary->[SummaryInfo::MCDC_DATA]
                    ->[SummaryInfo::OWNERS];
                if (!exists($ohash->{$developer})) {
                    my %data = ('hit'   => $hit ? 1 : 0,
                                'found' => 1,
                                $tla    => 1);
                    $ohash->{$developer} = \%data;
                } else {
                    my $d = $ohash->{$developer};
                    $d->{$tla} = 0
                        unless exists($d->{$tla});
                    $d->{$tla}  += 1;
                    $d->{found} += 1;
                    $d->{hit}   += 1
                        if $hit;
                }

                # now append this mcdc TLA to the owner...
                my $dhash = $self->[MCDC_OWNERS]->{$developer};
                die("expected initialized") unless defined($dhash);
                $dhash->{$tla} = []
                    unless exists($dhash->{$tla});
                my $ll = $dhash->{$tla};
                # see 'string compare' comment in branch method, above
                push(@$ll, $line)
                    unless @$ll && $ll->[-1] eq $line;
            }
        }
    }
}

sub _countLineTlaData
{
    my ($self, $line, $lineData, $fileSummary) = @_;
    # there is differential line coverage data...
    my $tla = $lineData->tla();

    if (!exists($SummaryInfo::tlaLocation{$tla})) {
        # this case can happen if the line number annotations are
        #   wrong in the .info file - so the first line of some function
        #   or some branch coverage line number turns out not to be an
        #   executable source code line
        lcovutil::ignorable_error($lcovutil::ERROR_UNKNOWN_CATEGORY,
               "unexpected category $tla for line " . $self->path() . ":$line");
        return;
    }
    # one more line in this bucket...
    $fileSummary->[SummaryInfo::LINE_DATA]->[SummaryInfo::DATA]->{$tla} += 1;
    # create the category list, if necessary
    $self->[LINE_CATEGORIES]->{$tla} = []
        unless exists($self->[LINE_CATEGORIES]->{$tla});

    push(@{$self->[LINE_CATEGORIES]->{$tla}}, $line);

    # and the age data...

    if ($SummaryInfo::tlaLocation{$tla} & 0x1) {
        # skip "DUB' and 'DCB' categories - which are not in current
        #   and thus have no line associated
        # @todo if desired, we _could_ enhance the 'annotate' callback
        #       to look up the baseline SHA/CL and get age/author information
        #       from that.  Seems unnecessary, though.

        #lcovutil::info("$l: $tla" . $lineData->in_curr() . "\n");

        my $l = $self->line($line);

        if (!defined($l)) {
            lcovutil::ignorable_error($lcovutil::ERROR_UNMAPPED_LINE,
                      "no data for line:$line, TLA:$tla, file:" . $self->path())
                if (!$lcovutil::warn_once_per_file ||
                    lcovutil::warn_once($lcovutil::ERROR_UNMAPPED_LINE,
                                        $self->path()));
            return;
        }
        # set the TLA of this line...
        $l->tla($tla);

        my $src_age = $l->age();
        # if this line is not in the project (e.g., from some 3rd party
        #   library - then we might not have file history for it.
        return
            unless defined($src_age);

        # increment count of lines of this age we found for this TLA
        $fileSummary->lineCovCount($tla, "age", $src_age, 1);

        if ($lineData->in_curr()) {
            # HGC:  could clean this up...no need to keep track
            #   of 'hit' as we can just compute from CBC + GNC + ...
            # found another line...
            $fileSummary->lineCovCount("found", "age", $src_age, 1);
            if ($lineData->curr_count() > 0) {
                $fileSummary->lineCovCount("hit", "age", $src_age, 1);
            }
        }

        if (defined($l->owner())) {
            # add this line to those that belong to this owner..
            my $developer = $l->owner();

            # first:  increment line count in 'file summary'
            my $ohash =
                $fileSummary->[SummaryInfo::LINE_DATA]->[SummaryInfo::OWNERS];
            $ohash->{$developer} = {}
                unless exists($ohash->{$developer});
            my $d = $ohash->{$developer};
            $d->{$tla} = 0
                unless exists($d->{$tla});
            $d->{$tla} += 1;

            # now push this line onto the list of in this file, belonging
            #   to this owner
            $self->[LINE_OWNERS]->{$developer} = {}
                unless exists($self->[LINE_OWNERS]->{$developer});
            my $dhash = $self->[LINE_OWNERS]->{$developer};
            $dhash->{lines} = []
                unless exists($dhash->{lines});
            push(@{$dhash->{lines}}, $line);
            $dhash->{$tla} = []
                unless exists($dhash->{$tla});
            # and the list of lines with this TLA, belonging to this user
            push(@{$dhash->{$tla}}, $line);
        }
    }
}

sub _accountFunction
{
    my ($fileSummary, $tla, $src_age) = @_;

    unless (defined($tla)) {
	# LCOV_UNREACHABLE_START
        lcovutil::ignorable_error($lcovutil::ERROR_INTERNAL,
                                  "undef function TLA for age '$src_age' of " .
                                      $fileSummary->name());
        return 1;    # error
	# LCOV_UNREACHABLE_STOP
    }

    $fileSummary->[SummaryInfo::FUNCTION_DATA]->[SummaryInfo::DATA]->{$tla} +=
        1;

    if (defined($src_age)) {
        $fileSummary->functionCovCount($tla, 'age', $src_age, 1);

        my $hit = grep(/$tla/, ('GBC', 'GIC', 'GNC', 'CBC'));
        $fileSummary->functionCovCount("found", "age", $src_age, 1);
        $fileSummary->functionCovCount("hit", "age", $src_age, 1)
            if $hit;
    }
    return 0;
}

sub _countFunctionTlaData
{
    my ($self, $line, $lineData, $fileSummary) = @_;
    my $func = $lineData->differential_function();

    my %foundFunctionTlas;
    my ($src_age, $developer, $srcLine);

    my $h         = $func->hit();
    my $mergedTla = $h->[1];
    if (!defined($mergedTla)) {
	# LCOV_UNREACHABLE_START
        lcovutil::ignorable_error($lcovutil::ERROR_INTERNAL,
                                  "undef TLA for function '" .
                                      $func->name() .
                                      "' hit " . $h->[0] . " at line " .
                                      $line . " (" . $lineData->tla() .
                                      ' ' . $lineData->curr_count() . ")");
        #die("this should not happen");
        # This is new code - somehow miscategorized.
        $mergedTla = $h->[0] == 0 ? 'UNC' : 'GNC';
        $h->[1] = $mergedTla;
        #return;
	# LCOV_UNREACHABLE_STOP
    }

    if (!grep(/^$mergedTla$/, ('DUB', 'DCB'))) {
        # deleted lines don't have owner data...
        $srcLine = $self->line($line);
        # info might not be available, if no annotations
        if (defined($srcLine)) {
            # should probably look at the source code to find the open/close
            #  parens - then claim the age is the youngest line
            $src_age = $srcLine->age();
            $srcLine->functionElem($func);
        }
    }

    if ($main::merge_function_aliases) {
        _accountFunction($fileSummary, $mergedTla, $src_age);
    } else {
        my $aliases = $func->aliases();
        foreach my $alias (keys %$aliases) {
            my $data = $aliases->{$alias};
            my $tla  = $data->[1];
            if (!defined($tla)) {
		# LCOV_UNREACHABLE_START
                lcovutil::ignorable_error($lcovutil::ERROR_INTERNAL,
                    "undef TLA for alias:'$alias' hit:" .
                        $data->[0] . " of function '" . $func->name() .
                        "' hit " . $h->[0] . " at line " . $line . " (" .
                        $lineData->tla() . ' ' . $lineData->curr_count() . ")");
                # die("this should not happen either");
                $tla = $data->[0] == 0 ? 'UNC' : 'GNC';
                $data->[1] = $tla;
		# LCOV_UNREACHABLE_STOP
            }
            _accountFunction($fileSummary, $tla, $src_age);
        }
    }
}

sub path
{
    my $self = shift;
    return $self->[PATH];
}

sub isProjectFile
{
    # return 'true' if no owner/date information for this file.
    my $self = shift;
    return scalar(%{$self->[LINE_OWNERS]});
}

sub line
{
    my $self = shift;
    my $i    = shift;
    die("bad line index '$i'")
        unless ($i =~ /^[0-9]+$/);
    return $self->[LINES]->[$i - 1];
}

# how old is the oldest (or youngest) line in this file?
sub age
{
    my ($self, $youngest) = @_;
    return undef unless $self->isProjectFile();

    my $age = $self->line(1)->age();
    foreach my $line (@{$self->lines()}) {
        my $a = $line->age();
        if (defined($youngest)) {
            $age = $a
                if ($a < $age);
        } else {
            $age = $a
                if ($a > $age);
        }
    }
    return $age;
}

sub lines
{
    my $self = shift;
    return $self->[LINES];
}

sub num_lines
{
    my $self = shift;
    return scalar(@{$self->[LINES]});
}

sub is_empty
{
    my $self = shift;
    # anything interesting here?
    return defined($self->interesting_lines()) &&
        0 == @{$self->interesting_lines()};
}

sub interesting_lines
{
    my $self = shift;
    # return list of lines containing coverpoints, which are marked by
    #  the $selectionCallback - see the --select-script parameter
    return $self->[INTERESTING_LINES];
}

sub binarySearchLine
{
    my ($list, $after) = @_;

    defined($list) && 0 != scalar(@$list) or
        die("invalid location list");

    my $max = $#$list;
    my $min = 0;
    my $mid;
    while (1) {
        $mid = int(($max + $min) / 2);
        my ($v) = $list->[$mid] =~ /^<*([0-9]+)$/; # handle deleted line markers
        if ($v > $after) {
            $max = $mid;
        } elsif ($v < $after) {
            $min = $mid;
        } else {
            while ($list->[$mid] =~ /<<</) {
                # skip deleted
                ++$mid;
                return undef if $mid > $#$list;
            }
            return $mid;
        }
        my $diff = $max - $min;
        if ($diff <= 1) {
            $mid = $min;
            my ($v) = $list->[$min] =~ /^<*([0-9]+)$/;
            $mid = $max
                if $v < $after;
            last;
        }
    }
    while ($list->[$mid] =~ /<<</) {
        # skip deleted
        ++$mid;
        return undef if $mid > $#$list;
    }
    return $list->[$mid] >= $after ? $mid : undef;
}

sub nextTlaGroup
{
    # return number of first line of next group of specified linecov TLA -
    #  for example, if line [5:8] and [13:17] are 'CNC', then:
    #     5 = nextTlaGroup('CBC')     : "after == undef"
    #    13 = nextTlaGroup('CBC', 5)  : skip contiguous group of CBC lines
    #  undef = nexTlaGroup('CBC', 13) : no following group
    my ($self, $tla, $after) = @_;
    if (!exists($SummaryInfo::tlaLocation{$tla})) {
        lcovutil::ignorable_error($lcovutil::ERROR_UNKNOWN_CATEGORY,
                                  "unknown linecov TLA '$tla'");
        return undef;
    }

    # note the the "$self->line(...)" argument is 1-based (not zero-base)
    my $line;
    if (defined($after) &&
        defined($self->line($after)->tla())) {
        die("$after is not in $tla group")
            unless ($self->line($after)->tla() eq $tla);
        # skip to the end of the current section...
        # if the TLA of this group is unset (non-code line: comment,
        #   blank, etc) - then look for next code line.  If that line's
        #   TLA matches, then treat as a contiguous group.
        # This way, we avoid visual clutter from having a lot of single-line
        #   TLA segments.
        my $lastline = scalar(@{$self->[LINES]});
        for ($line = $after + 1; $line <= $lastline; ++$line) {
            my $t = $self->line($line)->tla();
            last
                if (defined($t) &&
                    $t ne $tla);
        }
    } else {
        $line = 1;
    }
    my $locations = $self->[LINE_CATEGORIES]->{$tla};
    my $idx       = binarySearchLine($locations, $line);
    return defined($idx) ? $locations->[$idx] : undef;
}

sub nextCategoryTlaGroup
{
    # return number of first line of next group of specified branchcov TLA -
    # note that all branch lines are independent - so we will
    # report and go the next branch, even if it is on the adjacent line
    my ($type, $category, $self, $tla, $after) = @_;
    if (!exists($SummaryInfo::tlaLocation{$tla})) {
        lcovutil::ignorable_error($lcovutil::ERROR_UNKNOWN_CATEGORY,
                                  "unknown $type TLA '$tla'");
        return undef;
    }
    die("no $type data for TLA '$tla'")
        unless exists($self->[$category]->{$tla});
    my $locations = $self->[$category]->{$tla};
    $after = defined($after) ? $after + 1 : 1;
    my $idx = binarySearchLine($locations, $after);
    return defined($idx) ? $locations->[$idx] : undef;
}

sub nextBranchTlaGroup
{
    # ($self, $tla, $after_line)
    return nextCategoryTlaGroup('branch', BRANCH_CATEGORIES, @_);
}

sub nextMcdcTlaGroup
{
    return nextCategoryTlaGroup('MC/DC', MCDC_CATEGORIES, @_);
}

sub nextInDateBin
{
    my ($self, $dateBin, $tla, $after) = @_;

    if (!exists($SummaryInfo::tlaLocation{$tla})) {
        lcovutil::ignorable_error($lcovutil::ERROR_UNKNOWN_CATEGORY,
                                  "unknown linecov TLA '$tla'");
        return undef;
    }
    $dateBin <= $#SummaryInfo::ageGroupHeader or
        die("unexpected age group $dateBin");
    # note the the "$self->line(...)" argument is 1-based (not zero-base)
    my $line;
    if (defined($after)) {
        die("$after is not in $tla group")
            unless ($self->line($after)->tla() eq $tla);
        my $lastline = scalar(@{$self->[LINES]});
        # skip to the end of the current section...
        for ($line = $after + 1; $line <= $lastline; ++$line) {
            my $t = $self->line($line)->tla();
            my $a = $self->line($line)->age();
            if (!defined($a)) {
                lcovutil::ignorable_error($lcovutil::ERROR_UNMAPPED_LINE,
                            "no age data for line:$line, file:" . $self->path())
                    if (!$lcovutil::warn_once_per_file ||
                        lcovutil::warn_once($lcovutil::ERROR_UNMAPPED_LINE,
                                            $self->path()));
                return undef;
            }
            last
                if (defined($t) &&
                    ($t ne $tla ||
                     $dateBin != SummaryInfo::findAgeBin($a)));
        }
    } else {
        $line = 1;
    }
    # the data isn't stored by date bin (at least for now) - so the
    #   only way to find it currently is by searching forward.
    my $tlaLocations = $self->[LINE_CATEGORIES]->{$tla};
    my $idx          = binarySearchLine($tlaLocations, $line);
    return undef unless defined($idx);
    my $max = scalar(@$tlaLocations);
    for (; $idx < $max; ++$idx) {
        $line = $tlaLocations->[$idx];
        my $l = $self->line($line);
        if (!defined($l)) {
            lcovutil::ignorable_error($lcovutil::ERROR_UNMAPPED_LINE,
                                "no data for line:$line, file:" . $self->path())
                if (!$lcovutil::warn_once_per_file ||
                    lcovutil::warn_once($lcovutil::ERROR_UNMAPPED_LINE,
                                        $self->path()));
            return undef;
        }
        my $a = $l->age();
        if (!defined($a)) {
            lcovutil::ignorable_error($lcovutil::ERROR_UNMAPPED_LINE,
                            "no age data for line:$line, file:" . $self->path())
                if (!$lcovutil::warn_once_per_file ||
                    lcovutil::warn_once($lcovutil::ERROR_UNMAPPED_LINE,
                                        $self->path()));
            return undef;
        }
        my $bin = SummaryInfo::findAgeBin($a);
        if ($bin == $dateBin) {
            my $t = $l->tla();
            return $line
                if (defined($t) &&
                    $t eq $tla);
        }
    }
    return undef;
}

sub nextInOwnerBin
{
    my ($self, $owner, $tla, $after) = @_;

    if (!exists($SummaryInfo::tlaLocation{$tla})) {
        lcovutil::ignorable_error($lcovutil::ERROR_UNKNOWN_CATEGORY,
                                  "unknown linecov TLA '$tla'");
        return undef;
    }
    exists($self->[LINE_OWNERS]->{$owner}) &&
        exists($self->[LINE_OWNERS]->{$owner}->{$tla}) or
        die("$owner not responsible for any $tla lines in" . $self->path());

    # note the the "$self->line(...)" argument is 1-based (not zero-base)
    my $line;
    if (defined($after)) {

        ($self->line($after)->tla() eq $tla) or
            die("$after is not in $tla group");

        my $lastline = scalar(@{$self->[LINES]});
        # skip to the end of the current section...
        for ($line = $after + 1; $line <= $lastline; ++$line) {
            my $l = $self->line($line);
            if (!defined($l)) {
                lcovutil::ignorable_error($lcovutil::ERROR_UNMAPPED_LINE,
                                "no data for line:$line, file:" . $self->path())
                    if (!$lcovutil::warn_once_per_file ||
                        lcovutil::warn_once($lcovutil::ERROR_UNMAPPED_LINE,
                                            $self->path()));
                return undef;
            }
            my $t = $l->tla();
            my $o = $l->owner();
            if (!defined($o)) {
                lcovutil::ignorable_error($lcovutil::ERROR_UNMAPPED_LINE,
                          "no owner data for line:$line, file:" . $self->path())
                    if (!$lcovutil::warn_once_per_file ||
                        lcovutil::warn_once($lcovutil::ERROR_UNMAPPED_LINE,
                                            $self->path()));
                return undef;
            }
            last
                if (defined($t) &&
                    ($t ne $tla ||
                     $o ne $owner));
        }
    } else {
        $line = 1;
    }

    my $locations = $self->[LINE_OWNERS]->{$owner}->{$tla};
    my $idx       = binarySearchLine($locations, $line);
    return defined($idx) ? $locations->[$idx] : undef;
}

sub nextCategoryInDateBin
{
    my ($type, $category, $self, $dateBin, $tla, $after) = @_;

    if (!exists($SummaryInfo::tlaLocation{$tla})) {
        lcovutil::ignorable_error($lcovutil::ERROR_UNKNOWN_CATEGORY,
                                  "unknown $type TLA '$tla'");
        return undef;
    }
    $dateBin <= $#SummaryInfo::ageGroupHeader or
        die("unexpected age group $dateBin");

    # note the the "$self->line(...)" argument is 1-based (not zero-base)
    $after = defined($after) ? $after + 1 : 1;

    die("no $type $tla in " . $self->path())
        unless exists($self->[$category]->{$tla});

    my $lines = $self->[$category]->{$tla};
    my $idx   = binarySearchLine($lines, $after);

    return undef unless defined($idx);
    my $max = scalar(@$lines);
    for (; $idx < $max; ++$idx) {
        my $line = $lines->[$idx];
        my $l    = $self->line($line);
        if (!defined($l)) {
            lcovutil::ignorable_error($lcovutil::ERROR_UNMAPPED_LINE,
                                "no data for line:$line, file:" . $self->path())
                if (!$lcovutil::warn_once_per_file ||
                    lcovutil::warn_once($lcovutil::ERROR_UNMAPPED_LINE,
                                        $self->path()));
            next;
        }
        my $a = $l->age();
        if (!defined($a)) {
            lcovutil::ignorable_error($lcovutil::ERROR_UNMAPPED_LINE,
                            "no age data for line:$line, file:" . $self->path())
                if (!$lcovutil::warn_once_per_file ||
                    lcovutil::warn_once($lcovutil::ERROR_UNMAPPED_LINE,
                                        $self->path()));
            return undef;
        }
        my $bin = SummaryInfo::findAgeBin($a);
        if ($bin == $dateBin) {
            return $line;
        }
    }
    return undef;
}

sub nextBranchInDateBin
{
    # ($self, $dateBin, $tla, $after) = @_;
    return nextCategoryInDateBin('branch', BRANCH_CATEGORIES, @_);
}

sub nextMcdcInDateBin
{
    return nextCategoryInDateBin('MC/DC', MCDC_CATEGORIES, @_);
}

sub nextCategoryInOwnerBin
{
    my ($type, $category, $self, $owner, $tla, $after) = @_;

    if (!exists($SummaryInfo::tlaLocation{$tla})) {
        lcovutil::ignorable_error($lcovutil::ERROR_UNKNOWN_CATEGORY,
                                  "unknown $type TLA '$tla'");
        return undef;
    }

    # note the the "$self->line(...)" argument is 1-based (not zero-base)
    $after = defined($after) ? $after + 1 : 1;

    if (exists($self->[$category]->{$owner})) {
        my $od = $self->[$category]->{$owner};
        if (exists($od->{$tla})) {
            my $l   = $od->{$tla};
            my $idx = binarySearchLine($l, $after);
            return defined($idx) ? $l->[$idx] : undef;
        }
    }
    return undef;
}

sub nextBranchInOwnerBin
{
    # ($self, $owner, $tla, $after) = @_;
    return nextCategoryInOwnerBin('branch', BRANCH_OWNERS, @_);
}

sub nextMcdcInOwnerBin
{
    # ($self, $owner, $tla, $after) = @_;
    return nextCategoryInOwnerBin('MC/DC', MCDC_OWNERS, @_);
}

sub _computeAge
{
    my ($when, $path) = @_;

    # if SOURCE_DATE_EPOCH is set, then use that as 'now':  age of this
    #   file is as of the epoch date.
    # if this file is newer than that ($when is in the future of epoch) - then
    #  warn and put this file into the 'zero age' bin.
    my $now =
        exists($ENV{SOURCE_DATE_EPOCH}) ?
        DateTime->from_epoch(epoch => $ENV{SOURCE_DATE_EPOCH}) :
        DateTime->now();
    my $then = lcovutil::parse_w3cdtf($when);
    if ($then > $now) {
        if (lcovutil::warn_once($lcovutil::ERROR_INCONSISTENT_DATA, $path)) {
            # issue annotation warning at most once per file
            # also attempt to clarify where the date comes from
            my $data =
                exists($ENV{SOURCE_DATE_EPOCH}) ?
                (
                lcovutil::warn_once($lcovutil::ERROR_INCONSISTENT_DATA,
                                    'SOURCE_DATE_EPOCH') ?
                    "computed from your 'SOURCE_DATE_EPOCH=$ENV{SOURCE_DATE_EPOCH}' environment variable - see 'man genhtml'"
                :
                    "'SOURCE_DATE_EPOCH=$ENV{SOURCE_DATE_EPOCH}'") :
                "'now'";

            lcovutil::ignorable_error($lcovutil::ERROR_INCONSISTENT_DATA,
                "File \"$path\": '$now' ($data) is older than annotate time '$when'"
            );
        }
        return 0;
    }
    return $then->delta_days($now)->in_units('days');
}

sub _load
{
    my ($self, $fileCovInfo) = @_;

    my $start = Time::HiRes::gettimeofday();
    ++$totalFiles;

    my $path = ReadCurrentSource::resolve_path($self->path());
    # try to simplify path - 'realpath' will fail if the file does
    #   not exist - but that might be fine if it exists in the repo
    my $repo_path = Cwd::realpath($path);
    if (!defined($repo_path)) {
        if (!defined($annotateCallback) &&
            !$main::synthesizeMissingFile) {
            my $suffix =
                lcovutil::explain_once('synthesize',
                           ' (see --synthesize-missing option to work around)');
            lcovutil::ignorable_error($lcovutil::ERROR_SOURCE,
                      "\"" . $self->path() . "\" does not exist: $!" . $suffix);
        }
        $repo_path = $self->path();
    }

    # check for version mismatch...
    if (@lcovutil::extractVersionScript) {
        my $currentVersion = $fileCovInfo->version('current');
        my $version        = lcovutil::extractFileVersion($repo_path);
        if (defined($version) &&
            '' ne $version) {
            if (defined($currentVersion)) {
                lcovutil::checkVersionMatch($repo_path, $version,
                                            $currentVersion, 'load');
            } else {
                my $suffix = lcovutil::explain_once('missingVersionMsg',
                    "\n\tSee the 'compute_file_version' section in man lcovrc(5)."
                );
                lcovutil::ignorable_error($lcovutil::ERROR_VERSION,
                    "'$repo_path': computed '$version' but version not defined in 'current' data file "
                        . $main::info_filenames[0]
                        . $suffix);
            }
        } elsif (defined($currentVersion)) {
            lcovutil::ignorable_error($lcovutil::ERROR_VERSION,
                "'$repo_path': version mismatch: your '--version-script' returned empty but 'current' data in "
                    . $main::info_filenames[0]
                    . " defines '$currentVersion'");
        }
        my $end = Time::HiRes::gettimeofday();
        $lcovutil::profileData{check_version}{$self->path()} = $end - $start;
    }

    if (defined($annotateCallback) &&
        # also skip if we already emitted 'missing file' error
        (defined($lcovutil::versionCallback) ||
         !lcovutil::fileExistenceBeforeCallbackError($repo_path))
    ) {
        my $begin = Time::HiRes::gettimeofday();
        my ($status, $lines);
        eval { ($status, $lines) = $annotateCallback->annotate($repo_path); };
        if ($@) {
            my $context = MessageContext::context();
            lcovutil::ignorable_error($lcovutil::ERROR_CALLBACK,
                          'annotate(' . $self->path() . ") failed$context: $@");
            $status = 1;
            # set $lines so exit status error below prints something
            $lines = [[$@]];
        }
        if (!$status && defined($lines)) {
            my $found;   # check that either all lines are annotated or none are
            my $lineNum = 0;
            foreach my $line (@$lines) {
                my ($text, $abbrev, $full, $when, $commit) = @$line;
                ++$lineNum;    # source line numbers start at 1 (one)
                my $age = _computeAge($when, $path);
                if ($commit ne 'NONE') {
                    die("inconsistent 'annotate' data for '$repo_path': both 'commit' and 'no commit' lines"
                    ) if (defined($found) && !$found);
                    $found = 1;
                    defined($abbrev) or
                        die("owner is undef for $repo_path:$lineNum");
                } else {
                    die("inconsistent 'annotate' data for '$repo_path': both 'no commit' and 'commit' lines"
                    ) if (defined($found) && $found);
                    $found  = 0;
                    $abbrev = "no.body";
                }
                $full = $abbrev unless defined($full);
                push @{$self->[LINES]},
                    SourceLine->new($lineNum, $text, $abbrev, $full,
                                    $when, $age, $commit);
            }
            my $end = Time::HiRes::gettimeofday();
            $lcovutil::profileData{annotate}{$self->path()} = $end - $begin;

            ++$annotatedFiles if $found;
            $self->_synthesize($fileCovInfo, 1);    # fake annotations too
            return $self;
        } else {
            # non-zero exit status:  something bad happened in annotation
            # if we ignore the error - then fall through and just try to
            # load the file
            my $text = '';
            $text = ': ' . $lines->[0]->[0] . '...'
                if $lines && @$lines;
            # might be useful to provide more than one line of context - if
            #  there is more than one line?
            lcovutil::report_exit_status($lcovutil::ERROR_ANNOTATE_SCRIPT,
                                         "annotate command failed",
                                         $status, '', $text);
        }    # end if error
    }    # end if annotate script exists

    # Check if file exists and is readable
    my $begin = Time::HiRes::gettimeofday();
    if (!-r $path) {
        # perhaps this error should be ignorable: if the source is not
        #  found and the 'source' error is ignored, then synthesize
        # Note that we can't extract version data from synthesized code.
        if ($main::synthesizeMissingFile ||
            # don't complain if we weren't going to do anything with the file -
            # not generating source view and not checking versions or annotating
            ($main::no_sourceview &&
             0 == scalar(@lcovutil::extractVersionScript) &&
             !lcovutil::is_filter_enabled())
        ) {
            $self->_synthesize($fileCovInfo,
                               defined($SourceFile::annotateCallback));
            my $end = Time::HiRes::gettimeofday();
            $lcovutil::profileData{synth}{$self->path()} = $end - $begin;
            return $self;
        }
        die($self->path() .
                " is not readable or doesn't exist. See the '--substitute', '--filter missing' and '--synthesize-missing' $lcovutil::tool_name options for methods to fix the path or ignore the problem."
        );
    }
    $self->_bare_load($path);
    $self->_synthesize($fileCovInfo, defined($SourceFile::annotateCallback));
    my $end = Time::HiRes::gettimeofday();
    $lcovutil::profileData{load}{$self->path()} = $end - $begin;
    return $self;
}

sub _synthesize
{
    my ($self, $fileCovInfo, $annotate) = @_;

    my $lineData    = $fileCovInfo->lineMap();
    my $currentLast = scalar(@{$self->[LINES]});
    my $last_line   = 0;
    while (my ($l, $d) = each(%$lineData)) {
        $last_line = $l if ('<' ne substr($l, 0, 1) && $l > $last_line);
    }
    my %functionDecls;
    if ($lcovutil::func_coverage) {
        while (my ($fnName, $funcEntry) = each(%{$fileCovInfo->functionMap()}))
        {
            my $tla = $funcEntry->hit()->[1];
            # deleted function doesn't count
            next if grep(/^$tla$/, ('DUB', 'DCB'));
            my $line = $funcEntry->line();
            my $end  = $funcEntry->end_line();
            $last_line = $line if $line > $last_line;
            $functionDecls{$line} = "/* BEGIN: function \"$fnName\" */"
                if ($line >= $currentLast);
            if (defined($end)) {
                $functionDecls{$end} = "/* END: function \"$fnName\" */"
                    if ($end >= $currentLast);
                if ($end > $last_line) {
                    # function end is not an executable line,
                    # but is after last executable line
                    $last_line = $end;
                }
            }
        }
    }
    return $self if ($last_line < 1 ||
                     $currentLast >= $last_line);
    my $why;
    if (0 == $currentLast) {
        # Synthesize source data from coverage trace to replace unreadable file
        my $suffix =
            $lcovutil::ignore[$lcovutil::ERROR_SOURCE] ?
            ' - synthesizing fake content.' :
            '.';
        lcovutil::ignorable_error($lcovutil::ERROR_SOURCE,
                                  "cannot read '" . $self->path() .
                                      "' - file is empty or unreadable$suffix")
            unless $main::no_sourceview;
        $why = 'empty or unreadable';
    } else {
        my $suffix = '';
        if ($lcovutil::ignore[$lcovutil::ERROR_RANGE]) {
            my $num    = $last_line - $currentLast;
            my $plural = $num == 1 ? 'line' : "$num lines";
            $suffix = ' ... synthesizing fake content for last ' . $plural;
        }
        lcovutil::ignorable_error($lcovutil::ERROR_RANGE,
                                $self->path() .
                                    ' contains only ' . $currentLast .
                                    ' lines but coverage data refers to line ' .
                                    $last_line . $suffix);
        $why = 'not long enough';
    }
    # Simulate gcov behavior
    my $notFound = "/* " . $self->path() . " $why */";
    my $synth    = "/* (content generated from coverage data) */";
    my $idx      = 1;
    my @fakeline = (undef,    # line number
                    undef);    # source text
    if ($annotate) {
        my $now =
            exists($ENV{SOURCE_DATE_EPOCH}) ?
            DateTime->from_epoch(epoch => $ENV{SOURCE_DATE_EPOCH}) :
            DateTime->now();
        push(@fakeline, 's.n.thetic',             # owner
             'faked synthetic user',              # full name
             $now,                                # when
             _computeAge($now, $self->path()),    # age
             'synthesized');    # commit ID
    }
    for (my $line = $currentLast + 1; $line <= $last_line; $line++) {
        my $mod = $idx++ % 20;
        my $text;
        # if there is function decl here...mark it.
        if (exists($functionDecls{$line})) {
            $text = $functionDecls{$line};
        } else {
            $text = (($mod == 1) ? $notFound :
                         ($mod == 2) ? $synth :
                         "/* ... */");
        }
        splice(@fakeline, 0, 2, $line, $text);
        push(@{$self->[LINES]}, SourceLine->new(@fakeline));
    }
    return $self;
}

sub _bare_load
{
    my ($self, $path) = @_;

    my $lineno = 0;
    local *HANDLE;    # File handle for reading the diff file

    open(HANDLE, "<", $path) or
        die("unable to open '" . $self->path() . "': $!\n");
    while (my $line = <HANDLE>) {
        chomp $line;
        $line =~ s/\r//g;    # Also remove CR from line-end

        $lineno++;
        push @{$self->[LINES]}, SourceLine->new($lineno, $line);
    }
    close(HANDLE) or die("unable to close $path: $!\n");
    return $self;
}

# a class to hold either line or branch counts for each testcase -
#   used by the "--show-details" feature.
package TestcaseTlaCount;

sub new
{
    # $testcaseCounts: either 'CountData' or 'BranchData' structure
    #     - the data for this testcase
    # $fileDetails: SourceFile structure - data for the filename
    # $covtype:  'line' or 'branch'
    my ($class, $testcaseCounts, $fileDetails, $covtype) = @_;

    my %tlaData;
    if ($covtype != SummaryInfo::FUNCTION_DATA) {
        $tlaData{found} = $testcaseCounts->found();
        $tlaData{hit}   = $testcaseCounts->hit();
    } else {
        $tlaData{found} =
            $testcaseCounts->numFunc($main::merge_function_aliases);
        $tlaData{hit} = $testcaseCounts->numHit($main::merge_function_aliases);
    }
    if ($main::show_tla &&
        defined($fileDetails)) {
        for my $tla (("CBC", "GBC", "GIC", "GNC")) {
            $tlaData{$tla} = 0;
        }
        if (SummaryInfo::LINE_DATA == $covtype) {
            foreach my $line ($testcaseCounts->keylist()) {
                # skip uncovered lines...
                next if $testcaseCounts->value($line) == 0;
                my $lineData =
                    $fileDetails->line($line);    # "SourceLine" structure
                my $tla = $lineData->tla();
                die("unexpected TLA '$tla' in CounntData for line $line")
                    unless exists($tlaData{$tla});
                $tlaData{$tla} += 1;
            }
        } elsif (SummaryInfo::BRANCH_DATA == $covtype) {
            foreach my $line ($testcaseCounts->keylist()) {
                my $lineData =
                    $fileDetails->line($line);    # "SourceLine" structure
                my $branchEntry = $lineData->branchElem();
                foreach my $blk ($branchEntry->blocks()) {
                    my $block = $blk->elements();
                    foreach my $br (@$block) {
                        my $tla   = $br->tla();
                        my $count = $br->count();
                        die("unexpected branch TLA $tla for count $count at " .
                            $fileDetails->path() . ":$line")
                            unless (($count != 0) == exists($tlaData{$tla}));
                        next if (0 == $count);
                        $tlaData{$tla} += 1;
                    }
                }
            }
        } elsif (SummaryInfo::MCDC_DATA == $covtype) {
            foreach my $line ($testcaseCounts->keylist()) {
                my $lineData =
                    $fileDetails->line($line);    # "SourceLine" structure
                my $mcdcEntry = $lineData->mcdcElem();
                while (my ($groupSize, $group) = each(%{$mcdcEntry->groups()}))
                {
                    foreach my $expr (@$group) {
                        foreach my $sense (0, 1) {
                            # use 'current' count...
                            my ($tla, $b_count, $c_count) =
                                @{$expr->count($sense)};
                            die("unexpected MC/DC TLA $tla for count $c_count at "
                                    . $fileDetails->path()
                                    . ":$line")
                                unless (
                                     ($c_count != 0) == exists($tlaData{$tla}));
                            next if (0 == $c_count);
                            $tlaData{$tla} += 1;
                        }
                    }
                }
            }
        } else {
            die("$covtype not supported yet")
                unless $covtype == SummaryInfo::FUNCTION_DATA;
            foreach my $key ($testcaseCounts->keylist()) {
                my $func = $testcaseCounts->findKey($key);
                my $line = $func->line();
                # differential FunctionEntry
                my $lineData     = $fileDetails->line($line);
                my $differential = $lineData->functionElem();
                my @data;
                if ($main::merge_function_aliases) {
                    push(@data, $differential->hit());
                } else {
                    my $aliases = $differential->aliases();
                    foreach my $alias (keys %$aliases) {
                        push(@data, $aliases->{$alias});
                    }
                }
                foreach my $d (@data) {
                    my ($count, $tla) = @$d;
                    die("unexpected branch TLA $tla for count $count")
                        unless (($count != 0) == exists($tlaData{$tla}));
                    #next if (0 == $count);
                    $tlaData{$tla} += 1;
                }
            }
        }
    }
    my $self = [$testcaseCounts, $fileDetails, \%tlaData, $covtype];
    bless $self, $class;
    return $self;
}

sub covtype
{
    my $self = shift;
    return $self->[3];
}

sub count
{
    my ($self, $tla) = @_;

    my $tlaCounts = $self->[2];
    return exists($tlaCounts->{$tla}) ? $tlaCounts->{$tla} : 0;
}

package GenHtml;

use constant {
              TOP              => 0,
              WORKLIST         => 1,
              PENDING          => 2,
              JOBS             => 3,
              CHILD_DATA       => 4,
              RETRY_COUNT      => 5,
              TMPDIR           => 6,
              DELAY_TIMER      => 7,
              CURRENT_PARALLEL => 8,
};

sub new
{
    my ($class, $current_data) = @_;
    my $self = [
           SummaryInfo->new("top", ''),    # top has empty name
           [], # worklist: dependencies are complete - this can run immediately
           {}, # %pending; # map of name->list of as-yet incomplete dependencies
               # This task can be scheduled as soon as its last dependency
               #  is complete.
           [], # jobs which are to be scheduled for execution
           {}, # childData - for callback
           {}, # jobID -> fail count for this job
           File::Temp->newdir("genhtml_XXXX",
                              DIR     => $lcovutil::tmp_dir,
                              CLEANUP => 1),
           0,    # delay timer
           0     # current count of parallel jobs
    ];
    bless $self, $class;

    lcovutil::info(1, "Writing temporary data to " . $self->[TMPDIR] . "\n");
    my $pending           = $self->[PENDING];
    my $worklist          = $self->[WORKLIST];
    my $top_level_summary = $self->[0];
    # no parent data for top-level
    my $toplevelPerTestData =
        ($main::hierarchical || $main::flat) ? [{}, {}, {}, {}] : undef;
    $pending->{""} = [
                      ['top',
                       [$top_level_summary, $toplevelPerTestData,
                        $top_level_summary->name()
                       ],
                       ['root'],
                       ['root'],
                       [undef, undef]
                      ],
                      {}
    ];

    # if the worklist is sorted alphabetically, then each segment tends to
    #   contain files from the same directory.
    # However, large files (which may take a long time to process) often
    #   cluster inside the same directory - so a random (hash) order often
    #   performs better.
    # Better is to look at profile data from the prior execution of
    #   this job - say, from last night's regressions - and to distribute
    #   the files we think will be long-running (...because they ran a long
    #   the last time) into different segments so they can run in parallel.
    # We also want to schedule those long-running files so they are executed
    #   first.  That way, we don't end up starting some long job late in the
    #   game - and then have to wait a long time for a single task to finish
    #   (as opposed to it running in parallel with a lot of other stuff).
    my @predicted;
    my @unknown;    # no history
    foreach my $f ($current_data->files()) {
        my $traceInfo = $current_data->data($f);
        my $filename  = ReadCurrentSource::resolve_path($traceInfo->filename());

        my $predicted;
        if (   defined($lcovutil::profileHistoryCallback)
            &&
            defined($predicted =
                        $lcovutil::profileHistoryCallback->history($filename))
        ) {
            push(@predicted, [$f, $filename, $predicted]);
        } else {
            # put the ones we don't know about at the start of the list
            #  - work on them first, trusting that we will have parallelism
            #    capacity for the ones we know about
            push(@unknown, [$f, $filename]);
        }
    }
    lcovutil::info(
          scalar(@unknown) . ' (of ' . (scalar(@predicted) + scalar(@unknown)) .
              ") files do not appear in \'--history\' profile.\n")
        if (defined($lcovutil::profileHistoryCallback) &&
            @unknown);

    # sort the 'predicted' ones based on expected execution time
    #  tie break alphabetically
    @predicted =
        sort({ $b->[2] <=> $a->[2] or $a->[1] cmp $b->[1] } @predicted);
    # interleave the long-running files so they get distributed between segments
    my @fileOrder;
    my $offset = 0;
    my $idx    = 0;
    for (my $i = 0; $i <= $#predicted; ++$i) {
        push(@fileOrder, $predicted[$idx]);
        $idx += $lcovutil::maxParallelism;
        if ($idx > $#predicted) {
            ++$offset;
            $idx = $offset;
        }
    }
    die("wrong calculation") unless $#predicted == $#fileOrder;
    push(@fileOrder, @unknown);    # unknown ones to the back

    foreach my $d (@fileOrder) {
        my ($f, $filename) = @$d;
        #my $traceInfo = $current_data->data($f);
        #my $filename  = ReadCurrentSource::resolve_path($traceInfo->filename());
        my ($vol, $parentDir, $file) = File::Spec->splitpath($filename);
        if (!File::Spec->file_name_is_absolute($filename)) {
            if ($parentDir) {
                $parentDir = File::Spec->catfile($main::cwd, $parentDir);
            } else {
                $parentDir = $main::cwd;
            }
            $filename = File::Spec->catfile($parentDir, $file);
        }
        my $short_name =
            $main::no_prefix ? $filename :
            main::apply_prefix($filename, @main::dir_prefix);
        my $is_absolute = File::Spec->file_name_is_absolute($short_name);
        $short_name =~ s|^$lcovutil::dirseparator||;
        my @short_path = File::Spec->splitdir($short_name);
        my @path       = File::Spec->splitdir($parentDir);

        my @rel_dir_stack =
            @short_path;    # @rel_dir_stack: relative path to parent dir of $f
        pop(@rel_dir_stack);    # remove filename from the end
        my $pendingParent;
        if ($main::hierarchical) {
            # excludes trailing '/'
            my $base = substr($filename, 0, -(length($short_name) + 1));
            my $relative_path = "";
            my $path          = $base;
            $pendingParent =
                $pending->{""};    # this is the top-level object dependency
            my @sp;
            my @p = split($lcovutil::dirseparator, substr($base, 0, -1))
                ;                  # remove trailing '/'
            while (scalar(@rel_dir_stack)) {
                my $element = shift(@rel_dir_stack);
                $relative_path .= $element;
                $path          .= $lcovutil::dirseparator . $element;
                push(@p, $element);
                push(@sp, $element);
                if (exists($pending->{$path})) {
                    $pendingParent = $pending->{$path};
                } else {
                    my $perTestData = [{}, {}, {}, {}];

                    my @dirData = (SummaryInfo->new("directory", $relative_path,
                                                    $is_absolute),
                                   $perTestData,
                                   $path);
                    add_dependency($pendingParent, $path);
                    my @spc = @sp;
                    my @pc  = @p;
                    $pendingParent = [
                                      ['dir', \@dirData,
                                       \@spc, \@pc,
                                       $pendingParent
                                      ],
                                      {}
                    ];
                    die("unexpected pending entry")
                        if exists($pending->{$dirData[2]});
                    $pending->{$dirData[2]} = $pendingParent;
                }
                $relative_path .= $lcovutil::dirseparator;
            }
        } elsif (!$main::flat) {
            # not hierarchical
            my $relative_path = File::Spec->catdir(@rel_dir_stack);
            if (!exists($pending->{$parentDir})) {
                my $perTestData = [{}, {}, {}, {}];
                my @dirData = (SummaryInfo->new(
                                       "directory", $relative_path, $is_absolute
                               ),
                               $perTestData,
                               $parentDir);
                $pending->{$parentDir} = [
                                     ['dir', \@dirData, \@rel_dir_stack, \@path,
                                      $pending->{$top_level_summary->name()}
                                     ],
                                     {}
                ];
            }
            $pendingParent = $pending->{$parentDir};

            # my directory is dependency for the top-level
            add_dependency($pending->{$top_level_summary->name()}, $parentDir);
        }

        # current file is a dependency of the top-level for a flat (2-level)
        #  report - or of the parent directory otherwise
        my $mergeInto = $main::flat ? $pending->{$top_level_summary->name()} :
            $pendingParent;
        add_dependency($mergeInto, $f);
        # this file is ready to be processed
        # 4-element tuple: [summary, [{line},{branch},{fn}], file, mcdc_hash]
        # index [3] (mcdc_hash) accessed at line 13484; must be present
        my @fileData =
            (SummaryInfo->new("file", $filename), [{}, {}, {}], $f, {});
        push(@$worklist,
             ['file', \@fileData, \@short_path, \@path, $mergeInto]);
    }

    $self->compute();
    # remove empty directories produced when no data in the directory
    #   was selected.  We want to clean both the directory and its parents.
    foreach my $dir (@cleanDirectoryList) {
        while ($dir) {
            my $d = File::Spec->catfile($main::output_directory, $dir);
            if (-d $d) {
                print("removing empty $d\n");
                if (!rmdir($d)) {
                    # directory not empty - stop
                    last;
                }
            }
            $d = File::Basename::dirname($dir);    # look up
            last if $d eq $dir;
            $dir = $d;
        }
    }
    $lcovutil::profileData{mergeDelayTimer} = $self->[DELAY_TIMER];
    printf("overall delay: %0.3fs\n", $self->[DELAY_TIMER])
        if $main::debugScheduler;
    return $self;
}

sub top()
{
    my $self = shift;
    return $self->[0];
}

sub add_dependency
{
    my ($parent, $name) = @_;
    lcovutil::debug(1,
                    "add depend '$name' in -> " . $parent->[0]->[1]->[2] . "\n")
        unless exists($parent->[1]->{$name});
    print("add depend $name -> " . $parent->[0]->[1]->[2] . "\n")
        if $main::debugScheduler > 1;
    $parent->[1]->{$name} = 1;
}

sub completed_dependency
{
    # return 0 when last dependency is removed
    my ($self, $parent, $name) = @_;

    my $pending = $self->[PENDING];
    die("missing pending '$parent'")
        unless exists($pending->{$parent});
    my $pendingParent = $pending->{$parent};
    die("missing pending entry for $name (in $parent")
        unless exists($pendingParent->[1]->{$name});
    delete($pendingParent->[1]->{$name});
    print("completed $name -> $parent " . scalar(%{$pendingParent->[1]}) . "\n")
        if $main::debugScheduler > 1;
    # LCOV_EXCL_START
    if ($main::debugScheduler > 1) {
        # print parent dependencies
        while (my ($k, $d) = each(%{$pendingParent->[1]})) {
            die("unexpected parent dependency $k ARRAY: " . $d->[0] . ' ' .
                join('/', @{$d->[2]}))
                if ('ARRAY' eq ref($d) || 1 != $d);
            print("  $k\n");
        }
    }
    # LCOV_EXCL_STOP
    if (!%{$pendingParent->[1]}) {
        # no more dependencies - schedule this one
        my $p = $pendingParent->[0];
        print("completed dependencies - schedule " . $p->[0] . ' ' .
              join('/', @{$p->[2]}) . "\n")
            if $main::debugScheduler > 1;
        push(@{$self->[WORKLIST]}, $p);
        delete($pending->{$parent});
    }
}

sub merge_one
{
    my ($self, $parentPerTestData, $fullname, $perTest, $summary,
        $parentSummary, $parentPath, $parallel)
        = @_;
    my $type = $summary->type();
    if ('file' eq $type) {
        my $base_name = File::Basename::basename($fullname);
        for (my $i = 0; $i < scalar(@$perTest); $i++) {
            $parentPerTestData->[$i]->{$base_name} = $perTest->[$i];
        }
    } elsif ('directory' eq $type) {
        for (my $i = 0; $i < scalar(@$perTest); $i++) {
            while (my ($basename, $data) = each(%{$perTest->[$i]})) {
                $parentPerTestData->[$i]->{$basename} = $data;
            }
        }
    } else {
        die("unexpected type $type")
            unless $type eq 'top';
        # set the top-level to this (restored) value...
        $self->[0] = $summary if $parallel;
    }
    if ($type ne 'top') {
        my $mergeInto = $main::flat ? $self->top() : $parentSummary;
        $mergeInto->append($summary);

        my $parentName = $main::flat ? '' : $parentPath;
        $self->completed_dependency($parentName, $fullname);
    }
}

sub compute_one
{
    my ($type, $name, $summary, $parentSummary, $perTestData,
        $rel_dir, $base_dir, $trunc_dir)
        = @_;

    my $start = Time::HiRes::gettimeofday();

    if ('file' eq $type) {
        my ($testdata, $testfncdata, $testbrdata, $testcase_mcdc) =
            main::process_file($summary, $parentSummary,
                               $trunc_dir, $rel_dir, $name);
        $perTestData = [$testdata, $testfncdata, $testbrdata, $testcase_mcdc];
    } elsif ('dir' eq $type) {
        # process the directory...
        main::write_summary_pages($name, 1,    # this is a directory,
                                  $summary, $main::show_details,
                                  $rel_dir, $base_dir, $trunc_dir,
                                  $perTestData);
    } else {
        die("unexpected task")
            unless 'top' eq $type;
        # Create sorted pages
        main::write_summary_pages($name, 0,    # 0 == list directories
                                  $summary,
                                  # generate top-level 'details' in flat or hierarchical modes
                                  $main::show_details &&
                                      ($main::flat || $main::hierarchical),
                                  ".", "", undef, $perTestData);
    }
    $summary->checkCoverageCriteria();
    if ('file' eq $type &&
        1 < $lcovutil::maxParallelism &&
        !($main::buildSerializableDatabase || $main::show_details)) {

        if ($main::no_sourceview ||
            $main::frames ||
            !$main::show_tla) {
            $summary->[SummaryInfo::FILE_DETAILS] = undef;
        } elsif ($main::show_tla) {
            # just store the location of the first coverpoint in each display
            #   group - rather than returning the whole detail structure?
            $summary->[SummaryInfo::FILE_DETAILS]->simplify();
        }
    }
    my $end = Time::HiRes::gettimeofday();
    $lcovutil::profileData{file}{$name} = $end - $start;

    return $perTestData;
}

sub _waitChild
{
    my ($self, $waitForPending, $noHang) = @_;

    my $currentSize = 0;
    my $consumption = 0;
    if (0 != $lcovutil::maxMemory) {
        $currentSize = lcovutil::current_process_size();
        $consumption = ($self->[CURRENT_PARALLEL] + 1) * $currentSize;
    }

    my $reaped = 0;
    if ($waitForPending ||
        $self->[CURRENT_PARALLEL] >= $lcovutil::maxParallelism ||
        ($self->[CURRENT_PARALLEL] > 1 && $consumption > $lcovutil::maxMemory)
    ) {

        my $children = $self->[CHILD_DATA];

        # wait here for child:
        #   - while we are oversubscribed
        #       - too many parallel jobs or
        #       - consuming too much memory), OR
        #   - if the worklist is empty but there are pending jobs
        #       - the pending jobs will be put onto the worklist when
        #         their dependency finishes
        my $worklist = $self->[WORKLIST];
        my $pending  = $self->[PENDING];
        lcovutil::info(1,
            "memory constraint ($consumption > $lcovutil::maxMemory violated: waiting."
                . (scalar(@$worklist) + scalar(keys(%$pending)))
                . " remaining\n")
            if ($consumption > $lcovutil::maxMemory);
        my $start = Time::HiRes::gettimeofday();
        my $child = wait();
        my $end   = Time::HiRes::gettimeofday();
        $self->[DELAY_TIMER] += $end - $start;
        return 0 unless $child > 0;
        my $childstatus = $?;

        unless (exists($children->{$child})) {
            lcovutil::report_unknown_child($child);
            return $reaped;
        }
        $self->merge_child($child, $childstatus);

        $reaped += 1;
    }
    if ($noHang) {
        while (1) {
            my $start = Time::HiRes::gettimeofday();
            my $child = waitpid(-1, POSIX::WNOHANG);
            if (0 < $child) {
                my $end = Time::HiRes::gettimeofday();
                $self->[DELAY_TIMER] += $end - $start;
                my $childstatus = $?;
                my $children    = $self->[CHILD_DATA];
                unless (exists($children->{$child})) {
                    lcovutil::report_unknown_child($child);
                    next;
                }
                $self->merge_child($child, $childstatus);
                $reaped += 1;
            } else {
                last;
            }
        }
    }
    print("reaped $reaped current:", $self->[CURRENT_PARALLEL],
          " jobs:", scalar(@{$self->[JOBS]}),
          " work:", scalar(@{$self->[WORKLIST]}),
          " pending:", scalar(%{$self->[PENDING]}),
          "\n") if ($main::debugScheduler && $reaped);

    return $reaped;
}

sub _reschedule
{
    my ($self, $jobID, $jobs) = @_;
    # clean up
    foreach my $task (@$jobs) {
        my $selfSummary = $task->[1];
        $selfSummary->unsetDirs();    # reset state
    }
    push(@{$self->[JOBS]}, [$jobs, $jobID]);    # put job back
    print(" reschedule $jobID current:",
          $self->[CURRENT_PARALLEL],
          " jobs: ", scalar(@{$self->[JOBS]}), "\n")
        if $main::debugScheduler;
}

sub _report_fail_and_reschedule
{
    my ($self, $jobId, $jobs, $childPid, $reason) = @_;
    my $counts = $self->[RETRY_COUNT];
    if (exists($counts->{$jobId})) {
        $counts->{$jobId} += 1;
    } else {
        $counts->{$jobId} = 1;
    }
    lcovutil::report_fork_failure("compute job $jobId (child $childPid)",
                                  $reason, 0, $counts->{$jobId});
    $self->_reschedule($jobId, $jobs);
}

sub _process_child
{
    my ($self, $jobs, $jobId, $startTime) = @_;

    # clear the profile data - we want just my contribution
    my $childStart   = Time::HiRes::gettimeofday();
    my $tmp          = '' . $self->[TMPDIR];
    my $stdout_file  = File::Spec->catfile($tmp, "genhtml_$$.log");
    my $stderr_file  = File::Spec->catfile($tmp, "genhtml_$$.err");
    my $currentState = lcovutil::initial_state();
    # clear counters so we store count only what we saw in the child
    $SourceFile::annotatedFiles = 0;
    $SourceFile::totalFiles     = 0;

    my $status = 0;
    my @rtnData;
    my ($stdout, $stderr, $code) = Capture::Tiny::capture {

        foreach my $t (@$jobs) {
            my ($task, $sSummary, $rel_dir, $full_dir_path, $base_dir,
                $trunc_dir)
                = @$t;
            my @taskData;
            push(@rtnData, \@taskData);

            my ($type, $data, $short_path, $path, $parentData) = @$task;
            my ($selfSummary, $perTestData, $name) = @$data;
            die("inconsistent")
                unless $selfSummary == $sSummary;
            my ($parentSummary, $parentPerTestData, $parentPath);
            ($parentSummary, $parentPerTestData, $parentPath) =
                @{$parentData->[0]->[1]}
                unless 'top' eq $type;

            eval {
                my $thisTestData =
                    compute_one($type, $name, $selfSummary, $parentSummary,
                                $perTestData, $rel_dir, $base_dir, $trunc_dir);

                # clear the parent pointer that we hacked into place.  Don't want that
                #   extra data returned by dumper.
                $selfSummary->[SummaryInfo::PARENT]  = undef;
                $selfSummary->[SummaryInfo::SOURCES] = {}
                    if $selfSummary->type() ne 'file';
                my $objname =
                    $selfSummary->type() eq 'top' ? "" : $selfSummary->name();
                my $criteria = $CoverageCriteria::coverageCriteria{$objname}
                    if exists($CoverageCriteria::coverageCriteria{$objname});

                push(@taskData,
                     $thisTestData, $parentPerTestData,
                     $selfSummary, $criteria);
            };    # eval
            if ($@) {
                $status = 1;
                print(STDERR $@);
                # @todo maybe this should be an ignorable error
                next;
            }

        }    # foreach
    };    # end capture
          # print stdout and stderr ...
    foreach my $d ([$stdout_file, $stdout], [$stderr_file, $stderr]) {
        next
            unless ($d->[1]);    # only print if there is something to print
        my $f = InOutFile->out($d->[0]);
        my $h = $f->hdl();
        print($h $d->[1]);
    }

    my $file     = File::Spec->catfile($tmp, "dumper_$$");
    my $childEnd = Time::HiRes::gettimeofday();
    $lcovutil::profileData{child}{$jobId} = $childEnd - $childStart;
    my $data;
    eval {
        $data = Storable::store([\@rtnData,
                                 [$SourceFile::annotatedFiles,
                                  $SourceFile::totalFiles
                                 ],
                                 lcovutil::compute_update($currentState),
                                 [$childStart, $childEnd]
                                ],
                                $file);
        my $done = Time::HiRes::gettimeofday();
        printf("  %d: dump %d %d jobs %0.3fs %0.3fMb %s\n",
               $jobId, $$, scalar(@$jobs),
               $done - $childEnd,
               (-f $file ? (-s $file) : 0) / (1024 * 1024),
               DateTime->now())
            if ($main::debugScheduler);
    };
    if ($@ || !defined($data)) {
        lcovutil::ignorable_error($lcovutil::ERROR_PARALLEL,
                  "Job $jobId: child $$ serialize failed" . ($@ ? ": $@" : ''));
        $status = 1;
    }
    return $status;
}

sub _segment_worklist
{
    my ($self, $segmentID) = @_;
    my $worklist = $self->[WORKLIST];

    my $nTasks = scalar(@$worklist);
    # how many cores are available?
    my $nCores = $lcovutil::maxParallelism - $self->[CURRENT_PARALLEL];
    # let's distribute them evenly
    #   could be more sophisticated and look at the compute time used
    #   by each of those tasks previously - then balance the expected load
    my $tasksPerCore =
        $lcovutil::maxParallelism > 1 ? ($nTasks + $nCores - 1) / $nCores :
        1;
    $tasksPerCore = $lcovutil::max_tasks_per_core
        if ($tasksPerCore > $lcovutil::max_tasks_per_core);
    while (@$worklist &&
           scalar(@{$self->[JOBS]}) <= $nCores) {
        my @jobs;
        push(@{$self->[JOBS]}, [\@jobs, $segmentID++]);
        foreach my $task (splice(@$worklist, 0, $tasksPerCore)) {

            my ($type, $data, $short_path, $path, $parentData) = @$task;
            my ($selfSummary, $perTestData, $name) = @$data;
            #main::info("$type: $name\n");
            # break initialization into 2 statements - see
            # https://stackoverflow.com/questions/26676488/why-is-the-variable-not-available
            my ($parentSummary, $parentPerTestData, $parentPath);
            ($parentSummary, $parentPerTestData, $parentPath) =
                @{$parentData->[0]->[1]}
                unless 'top' eq $type;

            my $rel_dir;
            my $full_dir_path;
            if ($type eq 'file') {
                $rel_dir =
                    File::Spec->catdir(@{$short_path}[0 .. $#$short_path - 1]);
                die("file error for " . File::Spec->catdir(@$short_path))
                    if '' eq $rel_dir;
                $full_dir_path = File::Spec->catdir(@{$path}[0 .. $#$path - 1]);
            } elsif ($type eq 'dir') {
                $rel_dir       = File::Spec->catdir(@$short_path);
                $full_dir_path = File::Spec->catdir(@$path);
            } else {
                $rel_dir       = '.';
                $full_dir_path = '.';
            }
            $rel_dir = lc($rel_dir) if ($lcovutil::case_insensitive);
            my $p = File::Spec->catdir($main::output_directory, $rel_dir);
            File::Path::make_path($p) unless -d $p || $main::no_html;
            my $base_dir  = main::get_relative_base_path($rel_dir);
            my $trunc_dir = ($rel_dir eq '') ? 'root' : $rel_dir;

            push(@jobs,
                 [$task, $selfSummary, $rel_dir,
                  $full_dir_path, $base_dir, $trunc_dir
                 ]);
        }    # foreach task assigned to this core
    }
    return $segmentID;
}

sub compute
{
    my $self = shift;

    my $worklist       = $self->[WORKLIST];
    my $pending        = $self->[PENDING];
    my $joblist        = $self->[JOBS];
    my $children       = $self->[CHILD_DATA];
    my $failedAttempts = 0;
    my $segmentID      = 0;

    WORK:
    while ($self->[CURRENT_PARALLEL] ||
           @$joblist  ||
           @$worklist ||
           %$pending) {

        if (1 < $lcovutil::maxParallelism) {
            # wait here for child:
            #   - while we are oversubscribed
            #       - too many parallel jobs or
            #       - consuming too much memory), OR
            #   - if the worklist is empty and there are no jobs ready to
            #     go (these are jobs which failed and were reschedule - e.g.,
            #     due to out-of-memory).
            #     There are two possibilities:
            #       - there are pending jobs:
            #         they will be put on the worklist when their dependency
            #         finishes (at least one of those dependencies must be
            #         running now - we could go check that...)
            #       - there are no pending jobs:  so there must be just
            #         one remaining top-level job currently running -
            #         when it finishes, we are done.
            # Also do a nonblocking wait here to collect any children that
            #   have finished - so we have a better chance to parallelize
            #   more/have more tasks that we can dispatch
            print(" currentParallel:",
                  $self->[CURRENT_PARALLEL],
                  " pending:", scalar(%$pending), "\n")
                if ($main::debugScheduler > 1 ||
                    ($main::debugScheduler &&
                     0 == scalar(@$worklist) &&
                     0 == scalar(@$joblist)));
            if ($self->_waitChild(
                            0 == scalar(@$worklist) && 0 == scalar(@$joblist), 1
            )) {
                # reaped at least one child process
                next WORK;
            }
        }
        # LCOV_EXCL_START
        unless (@$worklist || @$joblist) {
            foreach my $e (keys %$pending) {
                # something went wrong - print some debug data
                lcovutil::info("me: $e: depends\n");
                my $p = $pending->{$e};
                while (my ($k, $d) = each(%{$p->[1]})) {
                    lcovutil::info("  $k\n");
                    die("unexpected data for depend $k: $d") unless $d == 1;
                }
                lcovutil::info("   parent: " . $p->[0]->[0] . ' ' .
                               join('/', @{$p->[0]->[2]}) . "\n");
            }
            # can get here if child process hit a 'die' (as opposed to an error)
            # and "--keep-going" was specified.
            die("unexpected empty worklist??");
        }
        # LCOV_EXCL_STOP

        # schedule a few items from the worklist if we can..
        $segmentID = $self->_segment_worklist($segmentID);

        while (@$joblist) {
            if (1 < $lcovutil::maxParallelism) {
                # need to wait here if insufficient resources
                #  (only wait until resource available - pending and
                #  worklist are managed above)
                my $reaped = $self->_waitChild(0);
                die("unexpected reap count $reaped") if $reaped > 1;
            }
            my $d = pop(@$joblist);
            my ($jobs, $jobId) = @$d;

            my $start = Time::HiRes::gettimeofday();
            foreach my $task (@$jobs) {
                # keep track of where this data is stored - need it later for
                #  top-level 'flat' view
                my $selfSummary = $task->[1];
                my $relDir      = $task->[2];
                my $fullDir     = $task->[3];
                $selfSummary->relativeDir($relDir);
                $selfSummary->fullDir($fullDir);
            }

            # distribute this job to parallel execution unless there is
            #   nothing else running.  In that case, we won't continue
            #   until this job is finished - so might as well do it
            #   locally and not incur parallelism overhead.
            if (1 < $lcovutil::maxParallelism &&
                ($self->[CURRENT_PARALLEL] > 0 ||
                    0 != scalar(@$joblist) ||
                    scalar(@$jobs) > 1)
            ) {
                $lcovutil::profileData{nJobs}{$jobId} = scalar(@$jobs);

                $lcovutil::deferWarnings = 1;
                my $pid = fork();
                if (!defined($pid)) {
                    # fork failed
                    ++$failedAttempts;
                    # report_fork_failure sleeps a bit if it doesn't error out
                    lcovutil::report_fork_failure("process segment $jobId",
                                                  $!, $failedAttempts);
                    # put job back into schedule
                    $self->_reschedule($jobId, $jobs);
                    next;
                }
                # fork succeeded - so reset 'consecutive fails' counter
                $failedAttempts = 0;
                if (0 == $pid) {
                    # I'm the child
                    my $status = $self->_process_child($jobs, $jobId, $start);
                    exit($status);
                } else {
                    $children->{$pid} = [$jobs, $jobId, $start];
                    ++$self->[CURRENT_PARALLEL];
                    print("forked $jobId current: ",
                          $self->[CURRENT_PARALLEL],
                          " jobs:", scalar(@$joblist), "\n")
                        if $main::debugScheduler;
                }
            } else {
                #not parallel
                lcovutil::info(1, "serial execution of task $jobId\n")
                    if ($lcovutil::maxParallelism > 1);
                die("unexpected distribution") unless 1 == scalar(@$jobs);
                my ($task, $sSummary, $rel_dir, $full_dir_path,
                    $base_dir, $trunc_dir)
                    = @{$jobs->[0]};

                my ($type, $data, $short_path, $path, $parentData) = @$task;
                my ($selfSummary, $perTestData, $name) = @$data;
                die("inconsistent") unless $selfSummary == $sSummary;
                my ($parentSummary, $parentPerTestData, $parentPath);
                ($parentSummary, $parentPerTestData, $parentPath) =
                    @{$parentData->[0]->[1]}
                    unless 'top' eq $type;

                my $thisTestData =
                    compute_one($type, $name, $selfSummary, $parentSummary,
                                $perTestData, $rel_dir, $base_dir, $trunc_dir);

                $self->merge_one($parentPerTestData, $name,
                                 $thisTestData, $selfSummary,
                                 $parentSummary, $parentPath);
                my $end = Time::HiRes::gettimeofday();
                $lcovutil::profileData{$type}{$name} = $end - $start;
            }
        }    # while (job list not empty)
    }    # while (work remains)
}

sub merge_child($$$)
{
    my $mergeStart = Time::HiRes::gettimeofday();
    my ($self, $childPid, $childstatus) = @_;
    my $children = $self->[CHILD_DATA];

    --$self->[CURRENT_PARALLEL];
    my ($jobs, $jobId, $start) = @{$children->{$childPid}};
    delete($children->{$childPid});

    my $tmp      = '' . $self->[TMPDIR];
    my $dumpfile = File::Spec->catfile($tmp, "dumper_$childPid");
    my $childLog = File::Spec->catfile($tmp, "genhtml_$childPid.log");
    my $childErr = File::Spec->catfile($tmp, "genhtml_$childPid.err");
    foreach my $f ($childLog, $childErr) {
        if (!-f $f) {
            $f = '';    # there was no output
            next;
        }
        if (open(RESTORE, "<", $f)) {
            # slurp into a string and eval..
            my $str = do { local $/; <RESTORE> };    # slurp whole thing
            close(RESTORE) or die("unable to close $f: $!\n");
            unlink $f;
            $f = $str;
        } else {
            $f = "unable to open $f: $!";
            if (0 == $childstatus) {
                lcovutil::report_parallel_error('genhtml',
                                    $lcovutil::ERROR_PARALLEL, $childPid, 0, $f,
                                    keys(%$children));
            }
        }
    }
    my $signal = $childstatus & 0xFF;
    # non-parallel execution prints stuff to stdout - so we should too
    print(STDOUT $childLog)
        if ((0 != $childstatus &&
             $signal != POSIX::SIGKILL &&
             $lcovutil::max_fork_fails != 0) ||
            $lcovutil::verbose >= 0);
    print(STDERR $childErr);

    # now undump the data ...
    my $dStart = Time::HiRes::gettimeofday();
    my $dumped = Storable::retrieve($dumpfile)
        if (-f $dumpfile && 0 == $childstatus);
    my $dEnd = Time::HiRes::gettimeofday();
    printf("  %d restore %d: %0.3fs %s (parallel %d)\n",
           $jobId, $childPid, $dEnd - $dStart,
           DateTime->now(), $self->[CURRENT_PARALLEL])
        if $main::debugScheduler;

    if (defined($dumped)) {
        eval {
            my ($jobData, $countData, $update, $otherData) = @$dumped;

            die("error processing child")
                if (scalar(@$jobData) != scalar(@$jobs));

            $SourceFile::annotatedFiles += $countData->[0];
            $SourceFile::totalFiles     += $countData->[1];
            lcovutil::update_state(@$update);

            $lcovutil::profileData{startDelay}{$jobId} =
                $otherData->[0] - $start;
            $lcovutil::profileData{mergeDelay}{$jobId} =
                $mergeStart - $otherData->[1];

            foreach my $t (@$jobs) {
                my $task = $t->[0];
                my ($type, $data, $short_path, $fullname, $parentData) = @$task;
                my ($childSummary, $perTestData, $name) = @$data;
                my ($parentSummary, $parentPerTestData, $parentPath);
                ($parentSummary, $parentPerTestData, $parentPath) =
                    @{$parentData->[0]->[1]}
                    unless 'top' eq $type;

                my $taskData = shift(@$jobData);
                # there was an error while processing the child
                die("error processing child " . scalar(@$taskData))
                    if (scalar(@$taskData) != 4);

                my ($thisTestData, $pPerTestData, $summary, $criteria) =
                    @$taskData;
                $childSummary->copyGuts($summary);
                $self->merge_one($parentPerTestData, $name, $thisTestData,
                                 $childSummary, $parentSummary, $parentPath, 1);

                # and save the deserialized criteria data
                if (defined($criteria)) {
                    my $name =
                        $childSummary->type() eq 'top' ? "" :
                        $childSummary->name();
                    $CoverageCriteria::coverageCriteria{$name} = $criteria;
                    $criteria->[1] = 0 if $criteria->[1] eq '';
                    my $v = $criteria->[1];
                    die('unexpected criteria data \'' .
                        join(' ', @$criteria) . '\'')
                        unless (Scalar::Util::looks_like_number($v));
                    $CoverageCriteria::coverageCriteriaStatus =
                        ($v != 0 || 0 != scalar(@{$criteria->[2]})) ? $v : 0;
                }
            }    # foreach job
        };
        if ($@) {
            $childstatus = 1 << 8 unless $childstatus;
            print(STDERR $@);
            lcovutil::report_parallel_error('genhtml',
                             $lcovutil::ERROR_PARALLEL, $childPid, $childstatus,
                             "unable to deserialize $dumpfile: $@",
                             keys(%$children));
        }
    }
    if (!-f $dumpfile) {
        $self->_report_fail_and_reschedule($jobId, $jobs, $childPid,
                                         "serialized data $dumpfile not found");
    } elsif (!defined($dumped) || $childstatus != 0) {
        my $signal = $childstatus & 0xFF;
        if (POSIX::SIGKILL == $signal) {
            $self->_report_fail_and_reschedule($jobId, $jobs, $childPid,
                                "killed by OS - possibly due to out-of-memory");
        } else {
            #print("stdout: $childLog\nstderr: $childErr\n");
            my $msg = "compute job $jobId";
            $msg .= ": unable to deserialize $dumpfile"
                unless defined($dumped);
            lcovutil::report_parallel_error('genhtml', $lcovutil::ERROR_CHILD,
                               $childPid, $childstatus, $msg, keys(%$children));
        }
    }
    my $end = Time::HiRes::gettimeofday();
    $lcovutil::profileData{segment}{$jobId}       = $end - $start;
    $lcovutil::profileData{merge_segment}{$jobId} = $end - $mergeStart;

    unlink $dumpfile
        if -f $dumpfile;
}

package main;

# Global variables & initialization

lcovutil::save_cmd_line(\@ARGV, "/usr/bin");

# TraceFile Instance containing all data from the 'current' .info file
#  - constructed at start of GenHtml
our $current_data;
# TraceFile Instance containing all data from the baseline .info file (if any)
#   - constructed in GenHtml (if needed)
our $base_data;
# Instance containing all data from diff file
our $diff_data = DiffMap->new();
our @opt_dir_prefix;    # Array of prefixes to remove from all sub directories
our @dir_prefix;
our %test_description;    # Hash containing test descriptions if available
our $current_date = get_date_string(undef);

our @info_filenames;      # List of .info files to use as data source
our $header_title;        # Title at top of HTML report page (above table)
our $footer;              # String at bottom of HTML report page
our $test_title;          # Title for output as written to each page header
our $output_directory;    # Name of directory in which to store output
our @base_filenames;      # Optional: names of files containing baseline data
our $age_basefile;        # how old is the baseline data file?
our $baseline_title;      # Optional name of baseline - written to page headers
our $baseline_date;       # Optional date that baseline was created
our $diff_filename;       # Optional name of file containing baseline data
our $strip;               # If set, strip leading directories when applying diff
our $desc_filename;       # Name of file containing test descriptions
our $css_filename;        # Optional name of external stylesheet file to use
our $show_details;        # If set, generate detailed directory view
our $no_prefix;           # If set, do not remove filename prefix
our $show_tla;            # categorize coverage data (or not)
our $show_functionProportions = 0
    ; # show proportion of lines/branches contained in the function which are hit
our $show_hitTotalCol;    # include the 'hit' or 'missed' total count in tables
                          #   - this is part of the 'legacy' view
    #   - also enabled when full differential categories are used
    #     (i.e., but not when there is no baseline data - so no
    #     categories apart from 'GNC' and 'UNC'
our $use_legacyLabels;
our $show_dateBins;          # show last modified and last author info
our $show_ownerBins;         # show list of people who have edited code
                             #   (in this file/this directory/etc)
our $show_nonCodeOwners;     # show last modified and last author info for
                             #  non-code lines (e.g., comments)
our $show_zeroTlaColumns;    # unless set, improve readability by removing
                             #  summary table columns which contain
                             #  only zero (blank) entries
our $show_simplifiedColors;
our $treatNewFileAsBaseline;
our $elide_path_mismatch =
    0;    # handle case that file in 'baseline' and 'current' .info
          # data matches some name in the 'diff' file such that
          # the basename is the same but the pathname is different
          # - then pretend that the names DID match
our $synthesizeMissingFile =
    0;    # create file content if not found for annotation
our $hierarchical = 0;    # if true: show directory hierarchy
                          # default: legacy two-level report
our $flat         = 0;    # if true: single table of all files in top level

our $sort_tables = 1;   # If set, provide directory listings with sorted entries
our $no_sort;           # Disable sort
our $frames;            # If set, use frames for source code view
our $keep_descriptions; # If set, do not remove unused test case descriptions
our $suppress_function_aliases;    # if set, don't show list of collapsed
                                   # function aliases
our $merge_function_aliases;
our $no_sourceview;     # If set, do not create a source code view for each file
our $no_html;           # don't generate HTML if set
our $legend;            # If set, include legend in output
our $tab_size = 8;      # Number of spaces to use in place of tab
our $html_prolog_file;  # Custom HTML prolog file (up to and including <body>)
our $html_epilog_file;  # Custom HTML epilog file (from </body> onwards)
our $html_prolog;       # Actual HTML prolog
our $html_epilog;       # Actual HTML epilog
our $html_ext  = "html";    # Extension for generated HTML files
our $html_gzip = 0;         # Compress with gzip
our $opt_missed;            # List/sort lines by missed counts
our $dark_mode;             # Use dark mode palette or normal
our $charset = "UTF-8";     # Default charset for HTML pages
our @fileview_sortlist;
our @fileview_sortname = ("", "-sort-l", "-sort-f", "-sort-b", '-sort-m');
our @fileview_prefixes = ("");
our @funcview_sortlist;
our @rate_name    = ("Lo", "Med", "Hi");
our @rate_png     = ("ruby.png", "amber.png", "emerald.png");
our $rc_desc_html = 0;      # lcovrc: genhtml_desc_html

# simplify/shorten names in 'function detail table'
our @simplifyFunctionScript;      # the arg list
our $simplifyFunctionCallback;    # the actual callback

our $cwd = cwd();                 # Current working directory

# for debugging
our $verboseScopeRegexp;          # dump categorization processing if match

#
# Code entry point
#

$SIG{__WARN__} = \&lcovutil::warn_handler;
$SIG{__DIE__}  = \&lcovutil::die_handler;

STDERR->autoflush;
STDOUT->autoflush;

my @datebins;
my (@rc_date_bins, @rc_annotate_script, @rc_select_script, @rc_date_labels,
    @rc_simplifyFunctionScript);

my %genhtml_rc_opts = (
               "genhtml_css_file"            => \$css_filename,
               "genhtml_header"              => \$header_title,
               "genhtml_footer"              => \$footer,
               "genhtml_line_field_width"    => \$line_field_width,
               "genhtml_overview_width"      => \$overview_width,
               "genhtml_nav_resolution"      => \$nav_resolution,
               "genhtml_nav_offset"          => \$nav_offset,
               "genhtml_keep_descriptions"   => \$keep_descriptions,
               "genhtml_no_prefix"           => \$no_prefix,
               "genhtml_no_source"           => \$no_sourceview,
               "genhtml_num_spaces"          => \$tab_size,
               "genhtml_frames"              => \$frames,
               "genhtml_legend"              => \$legend,
               "genhtml_html_prolog"         => \$html_prolog_file,
               "genhtml_html_epilog"         => \$html_epilog_file,
               "genhtml_html_extension"      => \$html_ext,
               "genhtml_html_gzip"           => \$html_gzip,
               "genhtml_precision"           => \$lcovutil::default_precision,
               "genhtml_function_coverage"   => \$lcovutil::func_coverage,
               "genhtml_branch_coverage"     => \$lcovutil::br_coverage,
               "genhtml_hi_limit"            => \$hi_limit,
               "genhtml_med_limit"           => \$med_limit,
               "genhtml_line_hi_limit"       => \$ln_hi_limit,
               "genhtml_line_med_limit"      => \$ln_med_limit,
               "genhtml_function_hi_limit"   => \$fn_hi_limit,
               "genhtml_function_med_limit"  => \$fn_med_limit,
               "genhtml_branch_hi_limit"     => \$br_hi_limit,
               "genhtml_branch_med_limit"    => \$br_med_limit,
               "genhtml_mcdc_hi_limit"       => \$mcdc_hi_limit,
               "genhtml_mcdc_med_limit"      => \$mcdc_med_limit,
               "genhtml_branch_field_width"  => \$br_field_width,
               "genhtml_mcdc_field_width"    => \$mcdc_field_width,
               "genhtml_owner_field_width"   => \$owner_field_width,
               "genhtml_age_field_width"     => \$age_field_width,
               "genhtml_sort"                => \$sort_tables,
               "genhtml_charset"             => \$charset,
               "genhtml_desc_html"           => \$rc_desc_html,
               'merge_function_aliases'      => \$merge_function_aliases,
               'suppress_function_aliases'   => \$suppress_function_aliases,
               "genhtml_missed"              => \$opt_missed,
               "genhtml_dark_mode"           => \$dark_mode,
               "genhtml_hierarchical"        => \$hierarchical,
               "genhtml_flat_view"           => \$flat,
               "genhtml_show_navigation"     => \$show_tla,
               "genhtml_show_noncode_owners" => \$show_nonCodeOwners,
               "genhtml_show_function_proportion" => \$show_functionProportions,
               'genhtml_show_owner_table'         => \$show_ownerBins,
               'genhtml_annotate_script'          => \@rc_annotate_script,
               'genhtml_annotate_tooltip' => \$SourceFile::annotateTooltip,
               "select_script"            => \@rc_select_script,
               "simplify_function"        => \@rc_simplifyFunctionScript,
               'num_context_lines' => \$InInterestingRegion::num_context_lines,
               'genhtml_date_bins' => \@rc_date_bins,
               'genhtml_date_labels'        => \@rc_date_labels,
               'truncate_owner_table'       => \@truncateOwnerTableLevels,
               'owner_table_entries'        => \$ownerTableElements,
               'compact_summary_tables'     => \$compactSummaryTables,
               'genhtml_synthesize_missing' => \$synthesizeMissingFile,
               'scope_regexp'               => \$verboseScopeRegexp,);

my $save;
my $serialize;
my $validateHTML = exists($ENV{LCOV_VALIDATE});

my %genhtml_options = (
                     "output-directory|o=s" => \$output_directory,
                     "header-title=s"       => \$header_title,
                     "footer=s"             => \$footer,
                     "title|t=s"            => \$test_title,
                     "description-file|d=s" => \$desc_filename,
                     "keep-descriptions|k"  => \$keep_descriptions,
                     "css-file|c=s"         => \$css_filename,
                     "baseline-file|b=s"    => \@base_filenames,
                     "baseline-title=s"     => \$baseline_title,
                     "baseline-date=s"      => \$baseline_date,
                     "current-date=s"       => \$current_date,
                     "diff-file=s"          => \$diff_filename,
                     "annotate-script=s"    => \@SourceFile::annotateScript,
                     "select-script=s"   => \@SummaryInfo::selectCallbackScript,
                     "simplify-script=s" => \@simplifyFunctionScript,
                     "new-file-as-baseline" => \$treatNewFileAsBaseline,
                     'elide-path-mismatch'  => \$elide_path_mismatch,
                     'synthesize-missing'   => \$synthesizeMissingFile,
                     # if 'show-owners' is set: generate the owner table
                     #    if it is passed a value: show all the owners,
                     #    regardless of whether they have uncovered code or not
                     'show-owners:s'     => \$show_ownerBins,
                     'show-noncode'      => \$show_nonCodeOwners,
                     'show-zero-columns' => \$show_zeroTlaColumns,
                     'simplified-colors' => \$show_simplifiedColors,
                     "date-bins=s"       => \@datebins,
                     'date-labels=s'     => \@SummaryInfo::ageGroupHeader,
                     "prefix|p=s"        => \@opt_dir_prefix,
                     "num-spaces=i"      => \$tab_size,
                     "no-prefix"         => \$no_prefix,
                     "no-sourceview"     => \$no_sourceview,
                     'no-html'           => \$no_html,
                     "show-details|s"    => \$show_details,
                     "frames|f"          => \$frames,
                     "legend"            => \$legend,
                     'save'              => \$save,
                     'serialize=s'       => \$serialize,
                     'scheduler+'        => \$debugScheduler,
                     "html-prolog=s"     => \$html_prolog_file,
                     "html-epilog=s"     => \$html_epilog_file,
                     "html-extension=s"  => \$html_ext,
                     "html-gzip"         => \$html_gzip,
                     "hierarchical"      => \$hierarchical,
                     "flat"              => \$flat,
                     "sort-tables"       => \$sort_tables,
                     "no-sort"           => \$no_sort,
                     "precision=i"       => \$lcovutil::default_precision,
                     "missed"            => \$opt_missed,
                     "dark-mode"         => \$dark_mode,
                     "show-navigation"   => \$show_tla,
                     "show-proportion"   => \$show_functionProportions,
                     "merge-aliases"     => \$merge_function_aliases,
                     "suppress-aliases"  => \$suppress_function_aliases,
                     'validate'          => \$validateHTML,);

Getopt::Long::Configure('default');

# Parse command line options
if (
    !lcovutil::parseOptions(\%genhtml_rc_opts, \%genhtml_options,
                            \$output_directory)
) {
    print(STDERR "Use $tool_name --help to get usage information\n");
    exit(1);
}

$merge_function_aliases = 1
    if ($suppress_function_aliases ||
        defined($lcovutil::cov_filter[$lcovutil::FILTER_FUNCTION_ALIAS]));

$buildSerializableDatabase = 1 if $serialize;
$no_sourceview             = 1 if $no_html;

# Copy related values if not specified
$ln_hi_limit    = $hi_limit if (!defined($ln_hi_limit));
$ln_med_limit   = $med_limit if (!defined($ln_med_limit));
$fn_hi_limit    = $hi_limit if (!defined($fn_hi_limit));
$fn_med_limit   = $med_limit if (!defined($fn_med_limit));
$br_hi_limit    = $hi_limit if (!defined($br_hi_limit));
$br_med_limit   = $med_limit if (!defined($br_med_limit));
$mcdc_hi_limit  = $hi_limit if (!defined($mcdc_hi_limit));
$mcdc_med_limit = $med_limit if (!defined($mcdc_med_limit));
$frames         = undef unless (defined($frames) && $frames);

foreach my $rc ([\@datebins, \@rc_date_bins],
                [\@SummaryInfo::ageGroupHeader, \@rc_date_labels],
                [\@SourceFile::annotateScript, \@rc_annotate_script],
                [\@SummaryInfo::selectCallbackScript, \@rc_select_script],
                [\@simplifyFunctionScript, \@rc_simplifyFunctionScript],

) {
    @{$rc->[0]} = @{$rc->[1]} unless (@{$rc->[0]});
}

foreach my $cb ([\$SourceFile::annotateCallback, \@SourceFile::annotateScript],
                [\$SummaryInfo::selectCallback,
                 \@SummaryInfo::selectCallbackScript
                ],
                [\$simplifyFunctionCallback, \@simplifyFunctionScript],
) {
    lcovutil::configure_callback($cb->[0], @{$cb->[1]})
        if scalar(@{$cb->[1]});
}

# we won't apply simplifications if we don't generate the table
#  (we still check that the callback is valid - even though we don't use it)
$simplifyFunctionCallback = undef
    if $no_sourceview;

if (defined($lcovutil::stop_on_error) &&
    !$lcovutil::stop_on_error) {
    # in the spirit of "don't stop" - don't worry about missing files.
    $synthesizeMissingFile = 1;
}

# Merge sort options
$sort_tables = 0
    if ($no_sort);
die(
  "unsupported use of mutually exclusive '--flat' and '--hierarchical' options")
    if ($flat && $hierarchical);

$show_tla = 1
    if (@base_filenames || defined($diff_filename));
if ($show_tla &&
    (0 == scalar(@base_filenames) &&
        !defined($diff_filename))
) {
    # no baseline - so not a differential report.
    #  modify some settings to generate corresponding RTL code.
    $use_legacyLabels = 1;
    SummaryInfo::noBaseline();
} else {
    $show_hitTotalCol = 1;
}

if (@SourceFile::annotateScript) {
    $show_dateBins = 1;
    if (0 == scalar(@datebins)) {
        # default: 7, 30, 180 days
        @datebins = @SummaryInfo::defaultCutpoints;
    } else {
        my %uniqify =
            map { $_, 1 }
            split($lcovutil::split_char,
                  join($lcovutil::split_char, @datebins));
        @datebins = sort(keys %uniqify);
    }
    SummaryInfo::setAgeGroups(@datebins);

    if (defined($show_ownerBins)) {
        @truncateOwnerTableLevels =
            split($lcovutil::split_char,
                  join($lcovutil::split_char, @truncateOwnerTableLevels));
        foreach my $l (@truncateOwnerTableLevels) {
            lcovutil::ignorable_error($lcovutil::ERROR_USAGE,
                "Unknown 'truncate_owner_table' level '$l': should be 'top', 'directory', or 'file'."
            ) unless grep(/^$l$/, ('top', 'directory', 'file'));
        }

        lcovutil::ignorable_error($lcovutil::ERROR_USAGE,
            "Unsupported value 'owner_table_entries = $ownerTableElements': expected positive integer."
            )
            unless (!defined($ownerTableElements) ||
                    (Scalar::Util::looks_like_number($ownerTableElements) &&
                     0 < $ownerTableElements));
    }
} else {
    $treatNewFileAsBaseline = undef;
    die("\"--show-owners\" option requires \"--annotate-script\" for revision control integration"
    ) if defined($show_ownerBins);
    die("\"--date-bins\" option requires \"--annotate-script\" for revision control integration"
    ) if (0 != scalar(@datebins));
}
if (0 != (defined($diff_filename) ^ (0 != scalar(@base_filenames)))) {
    if (@base_filenames) {
        lcovutil::ignorable_warning($lcovutil::ERROR_USAGE,
            "Specified --baseline-file without --diff-file: assuming no source differences.  Hope that is OK.\n"
        );
    } else {
        lcovutil::ignorable_warning($lcovutil::ERROR_USAGE,
            "Specified --diff-file without --baseline-file: assuming baseline code coverage was empty (nothing covered).  Hope that is OK\n"
        );
        # OK..just assume that the baseline is empty...
    }
    $show_tla = 1;
}

if (defined($header_title)) {
    $title = $header_title;
} else {
    # use the default title bar.
    $title =~ s/ differential//   # not a differential report, if no baseline...
        unless defined($diff_filename) || 0 != scalar(@base_filenames);
}
push(@fileview_prefixes, "-date")
    if ($show_dateBins);
push(@fileview_prefixes, "-owner")
    if (defined($show_ownerBins));

# use LCOV original colors if no baseline file
#  (so no differential coverage)
if ($use_legacyLabels ||
    !$show_tla        ||
    (defined($show_simplifiedColors) &&
        $show_simplifiedColors)
) {
    lcovutil::use_vanilla_color();
}

if ($dark_mode) {
    # if 'dark_mode' is set, then update the color maps
    # For the moment - just reverse the foreground and background
    my %reverse;
    while (my ($key, $value) = each(%lcovutil::normal_palette)) {
        $reverse{lc($value)} = $key;
    }

    foreach my $tla (@SummaryInfo::tlaPriorityOrder) {
        # swap
        my $bg = $lcovutil::tlaColor{$tla};

        my $key = substr($bg, 1);
        # if this color is in normal_palette, then swap to dark_palette
        #   version.  Otherwise, just swap the 'tlaColor' and 'tlaTextColor'
        #   (foreground and background)
        if (exists($reverse{lc($key)})) {
            my $k = $reverse{lc($key)};
            $lcovutil::tlaColor{$tla} = '#' . uc($lcovutil::dark_palette{$k});
        } else {
            $lcovutil::tlaColor{$tla} = $lcovutil::tlaTextColor{$tla};
        }
        $lcovutil::tlaTextColor{$tla} = $bg;
    }
}

@info_filenames = AggregateTraces::find_from_glob(@ARGV);

# Split the list of prefixes if needed
parse_dir_prefix(@opt_dir_prefix);

# Check for info filename
if (!@info_filenames) {
    die("No filename specified\n" .
        "Use $tool_name --help to get usage information\n");
}

# Generate a title if none is specified
$test_title = compute_title(\@ARGV, \@info_filenames)
    unless $test_title;

if (@base_filenames) {
    my @base_patterns = @base_filenames unless $baseline_title;
    @base_filenames = AggregateTraces::find_from_glob(@base_filenames);

    $baseline_title = compute_title(\@base_patterns, \@base_filenames)
        unless $baseline_title;
    my $baseline_create;

    if ($baseline_date) {
        eval {
            my $epoch = Date::Parse::str2time($baseline_date);
            $baseline_create = DateTime->from_epoch(epoch => $epoch);
        };
        if ($@) {
            #did not parse
            lcovutil::info(
                "failed to parse date '$baseline_date' - falling back to file creation time\n"
            );
        }
    }
    if (!defined($baseline_create)) {
        # if not specified, use 'last modified' of first baseline trace file
        my $create = (stat($base_filenames[0]))[9];
        $baseline_create = DateTime->from_epoch(epoch => $create);
        $baseline_date   = get_date_string($create)
            unless defined($baseline_date);
    }
    $age_basefile =
        $baseline_create->delta_days(DateTime->now())->in_units('days');
}

# Make sure css_filename is an absolute path (in case we're changing
# directories)
if ($css_filename) {
    if (!File::Spec->file_name_is_absolute($css_filename)) {
        $css_filename = File::Spec->catfile($cwd, $css_filename);
    }
}

# Get HTML prolog and epilog
$html_prolog = get_html_prolog($html_prolog_file);
$html_epilog = get_html_epilog($html_epilog_file);

# Issue a warning if --no-sourceview is enabled together with --frames
if (defined($frames)) {
    if ($no_sourceview) {
        lcovutil::ignorable_warning($lcovutil::ERROR_USAGE,
             "option --frames disabled because --no-sourceview was specified.");
        $frames = undef;
    } elsif ($show_tla) {
        lcovutil::info(
            "Note:  file table to source location navigation hyperlinks are disabled when --frames is enabled\n"
        );
    }
}

# Issue a warning if --no-prefix is enabled together with --prefix
if ($no_prefix && @dir_prefix) {
    lcovutil::ignorable_warning($lcovutil::ERROR_USAGE,
                    "option --prefix disabled because --no-prefix was ignored");
    @dir_prefix = ();
}

@fileview_sortlist = ($SORT_FILE);
@funcview_sortlist = ($SORT_FILE);

if ($sort_tables) {
    push(@fileview_sortlist, $SORT_LINE);
    push(@fileview_sortlist, $SORT_FUNC) if ($lcovutil::func_coverage);
    push(@fileview_sortlist, $SORT_BRANCH) if ($lcovutil::br_coverage);
    push(@fileview_sortlist, $SORT_MCDC) if ($lcovutil::mcdc_coverage);
    push(@funcview_sortlist, $SORT_LINE);
    if ($show_functionProportions) {
        push(@funcview_sortlist, $SORT_MISSING_LINE);
        push(@funcview_sortlist, $SORT_MISSING_BRANCH)
            if ($lcovutil::br_coverage);
        push(@funcview_sortlist, $SORT_MISSING_MCDC)
            if ($lcovutil::mcdc_coverage);
    }
}

if ($frames) {
    # Include genpng code needed for overview image generation
    do(File::Spec->catfile($tool_dir, 'genpng'));
}

# Make sure precision is within valid range
check_precision();

# Make sure output_directory exists, create it if necessary
$output_directory = '.'
    if !defined($output_directory);
if ($output_directory && !-d $output_directory) {
    info(1, "make_path $output_directory\n");
    File::Path::make_path($output_directory);
}

if ($save && $output_directory)
{    # save copy of .info and diff files: useful for debugging user cases later
    foreach my $d (['baseline_', @base_filenames],
                   ['', $diff_filename],
                   ['current_', @info_filenames]
    ) {
        my $prefix = shift @$d;
        $prefix = '' unless @base_filenames;
        foreach my $from (@$d) {
            next unless defined($from);
            my $to = File::Spec->catfile($output_directory,
                                     $prefix . File::Basename::basename($from));
            File::Copy::copy($from, $to) unless -f $to;
        }
    }
}

# save command line in output directory - useful for later debugging:
my $f = File::Spec->catfile($output_directory, 'cmd_line');
open(CMD, '>', $f) or die("unable to open $f: $!");
print(CMD $lcovutil::profileData{config}{cmdLine} . "\n");
close(CMD) or die("unable to close $f: $!\n");

my $exit_status = 0;
# Do something
my $now = Time::HiRes::gettimeofday();
my $top;
eval { $top = gen_html(); };
if ($@) {
    $exit_status = 1;
    print(STDERR $@);
}

my $then = Time::HiRes::gettimeofday();
$lcovutil::profileData{overall} = $then - $now;

if (0 == $exit_status) {
    # warn about unused include/exclude directives
    lcovutil::warn_file_patterns();
    ReadCurrentSource::warn_sourcedir_patterns();
}

# now check the coverage criteria (if any)
if (0 == $exit_status &&
    ($CoverageCriteria::coverageCriteriaStatus ||
        @CoverageCriteria::coverageCriteriaScript)
) {
    CoverageCriteria::summarize();
    # fail for signal or status
    $exit_status = (($CoverageCriteria::coverageCriteriaStatus & 0xFF) |
                    ($CoverageCriteria::coverageCriteriaStatus >> 8));
}

if ($serialize) {
    #my $f = File::Spec->catfile($output_directory, 'coverage.dat');
    my $data;
    eval { $data = Storable::store($top, $serialize); };
    if ($@ || !defined($data)) {
        print("unable to serialize coverage data", $@ ? ": $@" : '');
        $exit_status = 1;
    }
}

lcovutil::cleanup_callbacks();
lcovutil::save_profile(File::Spec->catfile($output_directory, 'genhtml'),
                       File::Spec->catfile($output_directory, 'profile.html'));

if (0 == $exit_status && $validateHTML) {
    ValidateHTML->new($output_directory, '.' . $html_ext);
}

# exit with non-zero status if --keep-going and some errors detected
$exit_status = 1
    if (0 == $exit_status && lcovutil::saw_error());

exit($exit_status);

#
# print_usage(handle)
#
# Print usage information.
#

sub print_usage(*)
{
    local *HANDLE = $_[0];

    print(HANDLE <<END_OF_USAGE);
Usage: $tool_name [OPTIONS] TRACEFILE_PATTERN(S)

Create HTML output for coverage data found in TRACEFILE. Note that TRACEFILE
may also be a list of filenames.

COMMON OPTIONS
  -h, --help                        Print this help, then exit
      --version                     Print version number, then exit
  -v, --verbose                     Increase verbosity level
  -q, --quiet                       Decrease verbosity level (e.g. to turn off
                                    progress messages)
      --debug                       Increase debug verbosity level
      --config-file FILENAME        Specify configuration file location
      --rc SETTING=VALUE            Override configuration file setting
      --ignore-errors ERRORS        Continue after ERRORS (see man page for
                                    full list of errors and their meaning)
      --keep-going                  Do not stop if an error occurs
      --tempdir DIRNAME             Write temporary and intermediate data here
      --preserve                    Keep intermediate files for debugging

OPERATION
  -o, --output-directory OUTDIR     Write HTML output to OUTDIR
  -d, --description-file DESCFILE   Read test case descriptions from DESCFILE
  -k, --keep-descriptions           Do not remove unused test descriptions
  -b, --baseline-file BASEFILE      Use BASEFILE as baseline file glob match pattern
      --annotate-script SCRIPT      Use SCRIPT to get revision control data
      --criteria-script SCRIPT      Use SCRIPT to check for acceptance criteria
      --version-script SCRIPT       Use SCRIPT to check for compatibility of
                                    source code and coverage data
      --resolve-script SCRIPT       Call script to find source file frpm path
      --(no-)checksum               Compare (ignore) source line checksum
      --diff-file UDIFF             Unified diff file UDIFF describes source
                                    code changes between baseline and current
      --new-file-as-baseline        Classify new files as baseline data
      --elide-path-mismatch         Identify matching files if their basename
                                    matches even though dirname does not
      --date-bins day[,day,...]     Use DAY number of days as upper age limit
                                    for the corresponding date bin
  -p, --prefix PREFIX               Remove PREFIX from all directory names
      --no-prefix                   Do not remove prefix from directory names
      --(no-)function-coverage      Enable (disable) function coverage display
      --(no-)branch-coverage        Enable (disable) branch coverage display
      --filter FILTERS              Apply FILTERS to input data (see man page
                                    for full list of filters and their effects)
      --include PATTERN             Only show output for files matching PATTERN
      --exclude PATTERN             Skip output for files matching PATTERN
      --source-directory DIR        Search DIR for source files
      --substitute REGEXP           Change source file names according to REGEXP
      --erase-functions REGEXP      Exclude data for functions matching REGEXP
      --omit-lines REGEXP           Ignore data in lines matching REGEXP
      --forget-test-names           Merge data for all tests names
      --synthesize-missing          Generate fake source for missing files
  -j, --parallel [N]                Use parallel processing with at most N jobs
      --memory MB                   Use at most MB memory in parallel processing
      --profile [FILENAME]          Write performance statistics to FILENAME
                                    (default: OUTDIR/genhtml.json)
      --save                        Write copy of input files to OUTDIR

HTML OUTPUT
  -f, --frames                      Use HTML frames for source code view
  -t, --title TITLE                 Use TITLE as label for current data
      --baseline-title TITLE        Use TITLE as label for baseline data
      --current-date DATE           Use DATE as date label for current data
      --baseline-date DATE          Use DATE as date label for baseline data
  -c, --css-file CSSFILE            Use external style sheet file CSSFILE
      --header-title BANNER         Banner at top of each HTML page
      --footer FOOTER               Footer at bottom of each HTML page
      --no-sourceview               Do not create source code view
      --num-spaces NUM              change appearance of tabs in source view
                                    NUM == 0 will not touch tabs
                                    NUM > 0 will replace them with NUM spaces
                                    NUM < 0 will only change their appearance
      --legend                      Include color legend in HTML output
      --html-prolog FILE            Use FILE as HTML prolog for generated pages
      --html-epilog FILE            Use FILE as HTML epilog for generated pages
      --html-extension EXT          Use EXT as filename extension for pages
      --html-gzip                   Use gzip to compress HTML
      --(no-)sort                   Enable (disable) sorted coverage views
      --demangle-cpp [OPT]          Demangle C++ function names
      --precision NUM               Set precision of coverage rate
      --missed                      Show miss counts as negative numbers
      --dark-mode                   Use the dark-mode CSS
      --simplified-colors           Use reduced color scheme for categories
      --hierarchical                Generate multilevel HTML report,
                                    matching source code directory structure
      --flat                        Generate flat HTML report, with all files
                                    listed on top-level page
  -s, --show-details                Generate detailed directory view
      --show-owners [all]           Show owner summary table. If optional
                                    value provided, show all the owners,
                                    regardless of whether they have uncovered
                                    code or not
      --show-noncode                Show author in summary table even if none
                                    of their lines are recognized as code
      --show-zero-columns           Keep summary columns for categories with
                                    no entries (default: remove)
      --show-navigation             Include 'goto first hit/not hit' and
                                    'goto next hit/not hit' hyperlinks in
                                    non-differential source code detail page
      --show-proportion             Show function coverage rates
      --suppress-aliases            Merge data for function aliases

For more information see the genhtml man page.
END_OF_USAGE

}

#
# print_overall_rate(trace, ln_do, fn_do, br_do, mcdc_do, summary)
#
# Print overall coverage rates for the specified coverage types.
#

sub print_overall_rate($$$$$$)
{
    my ($currentTrace, $ln_do, $fn_do, $br_do, $mcdc_do, $summary) = @_;

    # use verbosity level -1:  so print unless user says "-q -q"...really quiet
    info(-1, "Overall coverage rate:\n");
    my @types;
    push(@types, SummaryInfo::LINE_DATA) if $ln_do;
    push(@types, SummaryInfo::FUNCTION_DATA) if $fn_do;
    push(@types, SummaryInfo::BRANCH_DATA) if $br_do;
    push(@types, SummaryInfo::MCDC_DATA) if $mcdc_do;

    # use source file count from current - we don't care about files
    #  that were deleted and are in baseline
    info(-1, "  source files: %d\n", scalar($currentTrace->files()));
    my $width  = length("source files");
    my $indent = '     ';
    for my $type (@types) {
        my $name   = SummaryInfo::type2str($type);
        my $plural = "ch" eq substr($name, -2, 2) ? "es" : "s";
        my $label  = "$name$plural";
        my $fill   = '.' x ($width - length($label));
        my $found  = $summary->get("found", $type);
        # user was confused about why there was no coverage data
        my $explain =
            ($type == SummaryInfo::LINE_DATA &&
             0 == $found &&
             $SummaryInfo::selectCallback) ?
            " (your '--select-script' criteria may not match any coverpoints)" :
            '';
        info(-1,
             "  $name$plural$fill: %s$explain\n",
             get_overall_line($found, $summary->get("hit", $type), $name));
        if ($main::show_tla) {
            for my $tla (@SummaryInfo::tlaOrderNoExclude) {
                my $v = $summary->get($tla, $type);
                next if $v == 0;
                my $label =
                    $main::use_legacyLabels ?
                    $SummaryInfo::tlaToLegacySrcLabel{$tla} :
                    $tla;
                my $f = '.' x ($width - (length($indent) + length($label)));
                info(-1, "  $indent$label$f: $v\n");
            }
        }
    }
    summarize_cov_filters();
    summarize_messages();
}

#
# gen_html()
#
# Generate a set of HTML pages from contents of .info file INFO_FILENAME.
# Files will be written to the current directory. If provided, test case
# descriptions will be read from .tests file TEST_FILENAME and included
# in output.
#
# Die on error.
#

sub gen_html()
{
    # Read in all specified .info files
    my $now            = Time::HiRes::gettimeofday();
    my $readSourceFile = ReadCurrentSource->new();
    ($current_data) = AggregateTraces::merge($readSourceFile, @info_filenames);
    my $then = Time::HiRes::gettimeofday();
    $lcovutil::profileData{parse_current} = $then - $now;

    info("Found %d entries.\n", scalar($current_data->files()));

    # Read and apply diff data if specified - need this before we
    #  try to read and process the baseline..
    if ($diff_filename) {
        $now = Time::HiRes::gettimeofday();
        info("Reading diff file $diff_filename\n");
        $diff_data->load($diff_filename, $current_data,
                         \@lcovutil::build_directory);
        $then = Time::HiRes::gettimeofday();
        $lcovutil::profileData{parse_diff} = $then - $now;
    }

    # Read and apply baseline data if specified
    if (@base_filenames) {
        $now = Time::HiRes::gettimeofday();
        my $readBaseSource = ReadBaselineSource->new($diff_data);
        $then = Time::HiRes::gettimeofday();
        $lcovutil::profileData{parse_source} = $then - $now;
        # Read baseline file

        $now = Time::HiRes::gettimeofday();
        ($base_data) = AggregateTraces::merge($readBaseSource, @base_filenames);
        info("Found %d baseline entries.\n", scalar($base_data->files()));
        $then = Time::HiRes::gettimeofday();
        $lcovutil::profileData{parse_baseline} = $then - $now;
    } elsif (defined($diff_filename)) {
        # no data..
        $base_data = TraceFile->new();
    }

    if ($diff_filename) {
        # check for files which appear in the udiff but which dont appear
        # in either the current or baseline trace data.  Those may be
        # mapping issues - different pathname in .info file vs udiff
        if (!$diff_data->check_path_consistency($base_data, $current_data)) {
            lcovutil::ignorable_error($lcovutil::ERROR_PATH,
                  "possible path inconsistency in baseline/current/udiff data");
        }
    }

    if ($no_prefix) {
        # User requested that we leave filenames alone
        info("User asked not to remove filename prefix\n");
    } elsif (!@dir_prefix) {
        # Get prefix common to most directories in list
        my $prefix = get_prefix(1, $current_data->files());

        if ($prefix) {
            info("Found common filename prefix \"$prefix\"\n");
            $dir_prefix[0] = $prefix;
        } else {
            info("No common filename prefix found!\n");
            $no_prefix = 1;
        }
    } else {
        my $msg  = "Using user-specified filename prefix ";
        my $dirs = $current_data->directories();
        my $i    = 0;
        # somewhat of a hack:  the layout code doesn't react well when
        #  the 'prefix' directory contains source files (as opposed to
        #  containing a directory which contains source files).
        # Rather than trying to handle that special case, just munge the
        #  prefix to be something we like better.
        while ($i <= $#dir_prefix) {
            my $p = $dir_prefix[$i];
            # remove redundant /'s
            $p =~ s/$lcovutil::dirseparator+$//;
            $p = substr($p, 0, -1)
                if $lcovutil::dirseparator eq substr($p, -1);
            while (exists($dirs->{$p}) && $p) {
                $p = File::Basename::dirname($p);
            }
            unless ($p) {
                lcovutil::info("skipping prefix $dir_prefix[$i]\n");
                splice(@dir_prefix, $i, 1);
                next;
            }
            lcovutil::info(
                          "using prefix '$p' (rather than '$dir_prefix[$i]')\n")
                if ($p ne $dir_prefix[$i]);
            $dir_prefix[$i] = $p;
            $msg .= ", " unless 0 == $i;
            $msg .= "\"" . $p . "\"";
            ++$i;
        }
        info($msg . "\n");
    }

    # Read in test description file if specified
    if ($desc_filename) {
        info("Reading test description file $desc_filename\n");
        %test_description = %{read_testfile($desc_filename)};

        # Remove test descriptions which are not referenced
        # from %current_data if user didn't tell us otherwise
        if (!$keep_descriptions) {
            remove_unused_descriptions();
        }
    }

    # add quotes to args - so string concat works if there are embedded spaces, etc
    foreach my $s (\@SourceFile::annotateScript,
                   \@CoverageCriteria::coverageCriteriaScript,
                   \@lcovutil::extractVersionScript) {
        my $count = scalar(@$s);
        next unless $count > 1;
        foreach my $e (@$s) {
            $e = "'$e'" if ($e =~ /\s/);
        }
    }

    unless ($no_html) {
        info(1, "Writing .css and .png files.\n");
        write_css_file();
        write_png_files();
    }
    if ($html_gzip) {
        info(1, "Writing .htaccess file.\n");
        write_htaccess_file();
    }

    info("Generating output.\n");

    my $genhtml = GenHtml->new($current_data);

    if (@SourceFile::annotateScript &&
        0 == $SourceFile::annotatedFiles) {
        lcovutil::ignorable_error($lcovutil::ERROR_ANNOTATE_SCRIPT,
              "\"--annotate-script '" .
                  join(' ', @SourceFile::annotateScript) .
                  "'\" did not find any revision-controlled files in your sandbox"
        );
    }

    # Check if there are any test case descriptions to write out
    if (%test_description) {
        info("Writing test case description file.\n");
        write_description_file(\%test_description, $genhtml->top());
    }

    print_overall_rate($current_data, 1, $lcovutil::func_coverage,
                       $lcovutil::br_coverage, $lcovutil::mcdc_coverage,
                       $genhtml->top());

    return $genhtml->top();
}

#
# html_create(handle, filename)
#

sub html_create($$)
{
    my $handle   = $_[0];
    my $filename = File::Spec->catfile($output_directory, $_[1]);
    $filename = lc($filename) if $lcovutil::case_insensitive;

    if ($html_gzip) {
        open($handle, "|-", "gzip -c > $filename") or
            die("cannot open $filename for writing (gzip): $!\n");
    } else {
        open($handle, ">", $filename) or
            die("cannot open $filename for writing: $!\n");
    }
}

# $ctrls = [$view_type, $sort_type, $bin_prefix]
# $perTestcaseResult = [\%line, \%func, \%branch]
#sub write_dir_page($$$$$$$;$)
sub write_dir_page
{
    my ($callback_type, $ctrls, $page_suffix,
        $title, $rel_dir, $base_dir,
        $trunc_dir, $summary, $perTestcaseResult) = @_;

    my $bin_prefix = $ctrls->[3];
    # Generate directory overview page including details
    html_create(*HTML_HANDLE,
                File::Spec->catfile(
                              $rel_dir, "index$bin_prefix$page_suffix.$html_ext"
                ));
    if (!defined($trunc_dir)) {
        $trunc_dir = "";
    }
    $title .= " - " if ($trunc_dir ne "");
    write_html_prolog(*HTML_HANDLE, $base_dir, "LCOV - $title$trunc_dir");
    my $activeTlaColsForType =
        write_header(*HTML_HANDLE, $callback_type, $ctrls, $trunc_dir,
                     $rel_dir, $summary, undef, undef);
    if (0 != $summary->sources()) {
        write_file_table(*HTML_HANDLE, $callback_type, $base_dir,
                         $perTestcaseResult,
                         $summary, $ctrls, $activeTlaColsForType);
    } else {
        my $msg =
            "Coverage data table is empty - no coverpoints exist in the selected subset.";
        write_html(*HTML_HANDLE, <<END_OF_HTML);
          <br>
          <table width="100%" border=0 cellspacing=0 cellpadding=0>
            <tr><td class="subTableHeader">$msg</td></tr>
          </table>
          <br>
END_OF_HTML
    }
    write_html_epilog(*HTML_HANDLE, $base_dir);
    close(*HTML_HANDLE) or die("unable to close HTML handle: $!\n");
}

sub write_summary_pages($$$$$$$$)
{
    my ($name, $summaryType, $summary, $show_details,
        $rel_dir, $base_dir, $trunc_dir, $testhashes) = @_;

    my $callback_type = $summaryType == 1 ? 'directory' : 'top';
    foreach my $c ($summary->sources()) {
        my $child = $summary->get_source($c);
        # filter this one out if no data
        if ($child->is_empty()) {
            $summary->remove_source($c);
        }
    }
    return if $main::no_html;
    if (0 == $summary->sources() && $summaryType == 1) {
        # remove empty directory
        push(@cleanDirectoryList, $rel_dir);
        return;
    }

    if ($main::show_tla) {
        info('Processing '
                 .
                 ($summaryType == 1 ? ('directory: ' . $summary->name()) :
                      'top level:') .
                 "\n");
        my $sep = $summaryType == 1 ? $summary->name() : '';
        foreach my $t ('line',
                       $lcovutil::br_coverage ? 'branch' : undef,
                       $lcovutil::mcdc_coverage ? 'mcdc' : undef,
                       $lcovutil::func_coverage ? 'function' : undef
        ) {
            my $s = $summary->tlaSummary($t) if defined($t);
            next unless $s;
            info("  $t: $s\n");
        }
    }

    my $start = Time::HiRes::gettimeofday();
    my @summaryBins;
    if (defined($main::show_ownerBins)) {
        if (0 != scalar($summary->owners(
                                   $main::show_ownerBins, SummaryInfo::LINE_DATA
            ))
            ||
            (   $lcovutil::br_coverage &&
                0 != scalar($summary->owners(
                                 $main::show_ownerBins, SummaryInfo::BRANCH_DATA
                )))
            ||
            (   $lcovutil::mcdc_coverage &&
                0 != scalar($summary->owners(
                                   $main::show_ownerBins, SummaryInfo::MCDC_DATA
                )))
        ) {
            # at least one owner will appear - so table will be referenced
            push(@summaryBins, 'owner');
        } else {
            lcovutil::info(1,
                   $summary->name() . " has no visible owners..eliding page\n");
        }
    }
    push(@summaryBins, 'date') if defined($main::show_dateBins);

    my $singleSource = 1 == scalar($summary->sources());
    my @dirPageCalls;
    foreach my $sort_type (@main::fileview_sortlist) {
        my @ctrls = ($summaryType,    # 1 == 'list files'
                     "name",          # primary key
                     $sort_type, "", $singleSource);
        my $sort_str = $main::fileview_sortname[$sort_type];
        # 'fileview prefixes' is ('', '-date', '-owner)
        foreach my $bin_prefix (@main::fileview_prefixes) {
            # Generate directory overview page (without details)
            # no per-testcase data in this page...
            $ctrls[3] = $bin_prefix;
            # need copy because we are calling multiple child processes
            my @copy = @ctrls;
            push(@dirPageCalls,
                 [\@copy, $sort_str, $test_title, $rel_dir,
                  $base_dir, $trunc_dir, $summary
                 ]);

            if ($show_details) {
                # Generate directory overview page including details
                push(@dirPageCalls,
                     [\@copy, "-detail" . $sort_str,
                      $test_title, $rel_dir,
                      $base_dir, $trunc_dir,
                      $summary, $testhashes
                     ]);
            }
        }
        $ctrls[3] = "";    # no bin...
        foreach my $primary_key (@summaryBins) {
            # we don't associate function owner - so we elide 'function' columns in the
            #  'owner detail' pages - and thus won't create a sort-by-function link.
            #  Don't generate the unreferenced page
            next if $sort_type == $SORT_FUNC && $primary_key eq 'owner';
            # we elide the 'line' sort links for date/owner page if there
            #  is only one file
            next
                if $sort_type == $SORT_LINE &&
                $primary_key ne 'name' &&
                scalar($summary->sources()) < 2;
            $ctrls[1] = $primary_key;
            my @copy = @ctrls;
            push(@dirPageCalls,
                 [\@copy, '-bin_' . $primary_key . $sort_str,
                  $test_title, $rel_dir, $base_dir, $trunc_dir, $summary
                 ]);
        }
        last if $singleSource;
    }

    foreach my $params (@dirPageCalls) {
        write_dir_page($callback_type, @$params);
        last    # only write 'index.html' - not the sorted versions
            if ($summary->is_empty());
    }
    my $end = Time::HiRes::gettimeofday();
    $lcovutil::profileData{html}{$name} = $end - $start;
}

sub write_function_page($$$$$$$$$$$$$$$)
{
    # Generate function table for this file
    my ($fileCovInfo, $base_dir, $rel_dir, $trunc_dir,
        $base_name, $title, $sumcount, $funcdata,
        $testfncdata, $sumbrcount, $testbrdata, $mcdc,
        $testcase_mcdc, $sort_type, $summary) = @_;
    # $fileCovInfo is array [function hash, line hash, branch hash]
    my $filename;
    if ($sort_type == $main::SORT_FILE) {
        $filename = File::Spec->catfile($rel_dir, "$base_name.func.$html_ext");
    } elsif ($sort_type == $main::SORT_LINE) {
        # by declaration line number
        $filename =
            File::Spec->catfile($rel_dir, "$base_name.func-c.$html_ext");
    } elsif ($sort_type == $main::SORT_MISSING_LINE) {
        # by number of un-exercised lines
        $filename =
            File::Spec->catfile($rel_dir, "$base_name.func-l.$html_ext");
    } elsif ($sort_type == $main::SORT_MISSING_MCDC) {
        # by number of un-exercised MC/DC expressions
        return unless %{$fileCovInfo->[3]};
        $filename =
            File::Spec->catfile($rel_dir, "$base_name.func-m.$html_ext");
    } else {
        die("Unexpected sort $sort_type")
            unless ($sort_type == $main::SORT_MISSING_BRANCH);
        # don't emit page if there are no branches in the file
        return unless %{$fileCovInfo->[2]};
        # by declaration line number
        $filename =
            File::Spec->catfile($rel_dir, "$base_name.func-b.$html_ext");
    }
    html_create(*HTML_HANDLE, $filename);
    my $pagetitle =
        "LCOV - $title - " .
        File::Spec->catfile($trunc_dir, $base_name) . " - functions";
    write_html_prolog(*HTML_HANDLE, $base_dir, $pagetitle);
    write_header(*HTML_HANDLE,
                 'file',    # function table always written from 'file' level
                 [4, 'name', $sort_type,],
                 File::Spec->catfile($trunc_dir, $base_name),
                 File::Spec->catfile($rel_dir, $base_name),
                 $summary, undef, $funcdata->[0]);
    write_function_table(*HTML_HANDLE, $fileCovInfo,
                         "$base_name.gcov.$html_ext", $sumcount,
                         $funcdata, $testfncdata,
                         $sumbrcount, $testbrdata,
                         $mcdc, $testcase_mcdc,
                         $base_name, $base_dir,
                         $sort_type);
    write_html_epilog(*HTML_HANDLE, $base_dir, 1);
    close(*HTML_HANDLE) or die("unable to close HTML handle: $!\n");
}

#
# process_file(file_summary, parent_dir_summary, trunc_dir, rel_dir, filename)
#

sub process_file($$$$$)
{
    my ($fileSummary, $parent_dir_summary, $trunc_dir, $rel_dir, $filename) =
        @_;
    my $trunc_name = apply_prefix($filename, @dir_prefix);
    info("Processing file $trunc_name"
             .
             (($main::diff_filename && $diff_data->containsFile($filename)) ?
                  ' (source code changed)' :
                  ''));

    my $base_name = basename($filename);
    my $base_dir  = get_relative_base_path($rel_dir);
    my @source;
    my $pagetitle;
    local *HTML_HANDLE;

    my $fileData = $current_data->data($filename);    # TraceInfo struct
    my ($testdata, $sumcount, $funcdata,
        $checkdata, $testfncdata, $testbrdata,
        $sumbrcount, $mcdc_summary, $testcase_mcdc) = $fileData->get_info();

    my ($lines_found, $lines_hit, $br_found, $br_hit,
        $mcdc_found, $mcdc_hit, $fn_found, $fn_hit)
        = ($fileData->found(), $fileData->hit(),
           $fileData->branch_found(), $fileData->branch_hit(),
           $fileData->mcdc_found(), $fileData->mcdc_hit(),
           $fileData->function_found(), $fileData->function_hit());

    $fileSummary->lines_found($lines_found);
    $fileSummary->lines_hit($lines_hit);
    $fileSummary->function_found($fn_found);
    $fileSummary->function_hit($fn_hit);
    $fileSummary->branch_found($br_found);
    $fileSummary->branch_hit($br_hit);
    $fileSummary->mcdc_found($mcdc_found);
    $fileSummary->mcdc_hit($mcdc_hit);

    # handle case that file was moved between baseline and current
    my $baseline_filename = $diff_data->baseline_file_name($filename);
    # when looking up the baseline file, handle the case that the
    #  pathname does not match exactly - see comment in TraceFile::data
    my $fileBase = $base_data->data($baseline_filename, 1)
        if defined($base_data);
    my $fileCurrent = $current_data->data($filename);
    # build coverage differential categories
    my $now = Time::HiRes::gettimeofday();
    my $fileCovInfo =
        FileCoverageInfo->new($filename,
                              $fileBase,
                              $fileCurrent,
                              $diff_data,
                              defined($main::verboseScopeRegexp) &&
                                  $filename =~ m/$main::verboseScopeRegexp/);
    my $then    = Time::HiRes::gettimeofday();
    my $elapsed = $then - $now;
    $lcovutil::profileData{categorize}{$filename} = $elapsed;
    info(sprintf(" (%0.3fs)\n", $elapsed));
    my $r = "  lines=$lines_found hit=$lines_hit";
    $r .= " functions=$fn_found hit=$fn_hit"
        if $lcovutil::func_coverage && $fn_found != 0;
    $r .= " branches=$br_found hit=$br_hit"
        if $lcovutil::br_coverage && $br_found != 0;
    $r .= " MC/DC=$mcdc_found hit=$mcdc_hit"
        if $lcovutil::mcdc_coverage && $mcdc_found != 0;
    info($r . "\n");

    my $fileHasNoBaselineInfo = $main::treatNewFileAsBaseline &&
        (!defined($fileBase) ||
         ($fileBase->is_empty() && @main::base_filenames));
    # if this file is older than the baseline and there is no associated
    #   baseline data - then it appears to have been added to the build
    #   recently
    # We want to treat the code as "CBC" or "UBC" (not "GIC" and "UIC")
    #   because we only just turned section "on" - and we don't want the
    #   coverage ratchet to fail the build if UIC is nonzero

    # NOTE:  SourceFile constructor modifies some input data:
    #   - $fileSummary struct is also modified: update total counts in
    #     each bucket, counts in each date range
    #   - $fileCovInfo: change GIC->CBC, UIC->UBC if $fineNotInBaseline and
    #     source code is older than baseline file
    my $srcfile = SourceFile->new($filename, $fileSummary, $fileCovInfo,
                                  $fileHasNoBaselineInfo);
    my $endSrc = Time::HiRes::gettimeofday();
    $lcovutil::profileData{source}{$filename} = $endSrc - $then;

    if (@main::base_filenames) {
        # summarize the bin data if we had baseline data for comparison
        foreach my $t (['line', $lines_found],
                       ['branch', $lcovutil::br_coverage ? $br_found : 0],
                       ['mcdc', $lcovutil::mcdc_coverage ? $mcdc_found : 0],
                       ['function', $lcovutil::func_coverage ? $fn_found : 0]
        ) {
            next if 0 == $t->[1];
            info('    ' .
                 $t->[0] . ': ' . $fileSummary->tlaSummary($t->[0]) . "\n");
        }
    }

    if ($srcfile->is_empty()) {
        return;
    }
    # somewhat of a hack:  we are ultimately going to merge $fileSummary
    #   (the data for this particular file) into $parent_dir (the data
    #    for the parent directory) - but we need to do that in the caller
    #    (because we are building $fileSummary in a child process that we are
    #    going to pass back.  But we also use the parent and its name
    #    in HTML generation...
    #    we clear this setting before we return the generated summary
    $fileSummary->setParent($parent_dir_summary);

    my $from = Time::HiRes::gettimeofday();
    # Return after this point in case user asked us not to generate
    # source code view
    if (!$no_sourceview) {
        # Generate source code view for this file
        my $differentialFunctionMap;
        if ($lcovutil::func_coverage) {
            $differentialFunctionMap = $fileCovInfo->functionMap();
            if ($SummaryInfo::selectCallback) {
                # prune hash to remove functions which are outside the
                #  selected region
                my $lineCovMap = $fileCovInfo->lineMap();
                my %tempMap;
                while (my ($f, $fn) = each(%$differentialFunctionMap)) {
                    my $line = $fn->line();
                    $tempMap{$f} = $fn if exists($lineCovMap->{$line});
                }
                $differentialFunctionMap = \%tempMap;
            }
        }
        html_create(*HTML_HANDLE,
                    File::Spec->catfile($rel_dir, "$base_name.gcov.$html_ext"));
        $pagetitle = "LCOV - $test_title - " .
            File::Spec->catfile($trunc_dir, $base_name);
        write_html_prolog(*HTML_HANDLE, $base_dir, $pagetitle);
        write_header(*HTML_HANDLE,
                     'file',
                     [2, 'name', 0],
                     File::Spec->catfile($trunc_dir, $base_name),
                     File::Spec->catfile($rel_dir, $base_name),
                     $fileSummary,
                     $srcfile,
                     $differentialFunctionMap);

        @source = write_source(*HTML_HANDLE, $srcfile, $sumcount,
                               $checkdata, $fileCovInfo, $funcdata,
                               $sumbrcount, $mcdc_summary);

        write_html_epilog(*HTML_HANDLE, $base_dir, 1);
        close(*HTML_HANDLE) or die("unable to close HTML handle: $!\n");

        if ($lcovutil::func_coverage) {
            # Create function tables
            my $lineCovMap = $fileCovInfo->lineMap();
            # simply map between function leader name and differential data
            # compute number of hit/missed coverpoints in each function
            my %lineCov;
            my %branchCov;
            my %mcdcCov;
            if ($main::show_functionProportions) {
                while (my ($name, $funcEntry) = each(%$differentialFunctionMap))
                {
                    my $lineData = $funcEntry->findMyLines($sumcount);
                    if (defined($lineData)) {
                        my $found = 0;
                        my $hit   = 0;
                        foreach my $d (@$lineData) {
                            ++$found;
                            ++$hit if (0 != $d->[1]);
                        }
                        $lineCov{$name} = [$found, $hit];
                    }
                    if ($lcovutil::br_coverage) {
                        my $branchData =
                            $funcEntry->findMyBranches($sumbrcount);
                        if (defined($branchData) &&
                            0 != scalar(@$branchData)) {
                            # there are branches here..
                            my $found = 0;
                            my $hit   = 0;
                            foreach my $branch (@$branchData) {
                                my ($f, $h) = $branch->totals();
                                $found += $f;
                                $hit   += $h;
                            }
                            $branchCov{$name} = [$found, $hit];
                        }
                    }
                    if ($lcovutil::mcdc_coverage) {
                        my $mcdcData = $funcEntry->findMyMcdc($mcdc_summary);
                        if (defined($mcdcData) &&
                            0 != scalar(@$mcdcData)) {
                            # there are MC/DC expressions here..
                            my $found = 0;
                            my $hit   = 0;
                            foreach my $mcdc (@$mcdcData) {
                                my ($f, $h) = $mcdc->totals();
                                $found += $f;
                                $hit   += $h;
                            }
                            $mcdcCov{$name} = [$found, $hit];
                        }
                    }
                }
            }
            if (%$differentialFunctionMap) {
                my $funcStart = Time::HiRes::gettimeofday();
                foreach my $sort_type (@funcview_sortlist) {
                    write_function_page([$differentialFunctionMap,
                                         \%lineCov, \%branchCov, \%mcdcCov,
                                        ],
                                        $base_dir,
                                        $rel_dir,
                                        $trunc_dir,
                                        $base_name,
                                        $test_title,
                                        $sumcount,
                                        $funcdata,
                                        $testfncdata,
                                        $sumbrcount,
                                        $testbrdata,
                                        $mcdc_summary,
                                        $testcase_mcdc,
                                        $sort_type,
                                        $fileSummary);
                }
                my $funcEnd = Time::HiRes::gettimeofday();
                my $elapsed = $funcEnd - $funcStart;
                $lcovutil::profileData{function_html}{$filename} = $elapsed;
                #info(sprintf("    function HTML %0.3fs\n", $elapsed));
            }
        }

        # Additional files are needed in case of frame output
        if ($frames) {
            # Create overview png file
            my $simplified = defined($main::show_simplifiedColors) &&
                $main::show_simplifiedColors;
            my $png = File::Spec->catfile($rel_dir, "$base_name.gcov.png");
            $png = lc($png) if $lcovutil::case_insensitive;
            gen_png(File::Spec->catfile($output_directory, $png),
                    $main::show_tla && !$simplified,
                    $main::dark_mode,
                    $overview_width,
                    $tab_size,
                    @source);

            # Create frameset page
            html_create(*HTML_HANDLE,
                        File::Spec->catfile(
                                  $rel_dir, "$base_name.gcov.frameset.$html_ext"
                        ));
            write_frameset(*HTML_HANDLE, $base_dir, $base_name, $pagetitle);
            close(*HTML_HANDLE) or die("unable to close HTML handle: $!\n");

            # Write overview frame
            html_create(*HTML_HANDLE,
                        File::Spec->catfile(
                                  $rel_dir, "$base_name.gcov.overview.$html_ext"
                        ));
            write_overview(*HTML_HANDLE, $base_dir, $base_name,
                           $pagetitle, scalar(@source));
            close(*HTML_HANDLE) or die("unable to close HTML handle: $!\n");
        }
    }
    my $to = Time::HiRes::gettimeofday();
    $elapsed = $to - $from;
    $lcovutil::profileData{html}{$filename} = $elapsed;
    #info(sprintf("    HTML %0.3fs\n", $elapsed));

    return ($testdata, $testfncdata, $testbrdata, $testcase_mcdc);
}

sub compute_title($$)
{
    my ($patterns, $info_files) = @_;

    my $title;
    if (1 == scalar(@$info_files)) {
        # just one coverage DB file
        $title = basename($info_files->[0]);
    } elsif (1 == scalar(@$patterns)) {
        # just one pattern...
        $title = $patterns->[0];
        $title = substr($title, length($main::cwd) + 1)
            if (File::Spec->file_name_is_absolute($title) &&
                length($main::cwd) < length($title) &&
                $main::cwd eq substr($title, 0, length($main::cwd)));
    } else {
        $title = scalar(@$info_files) . ' coverage DB files';
    }
    return $title;
}

#
# get_prefix(min_dir, filename_list)
#
# Search FILENAME_LIST for a directory prefix which is common to as many
# list entries as possible, so that removing this prefix will minimize the
# sum of the lengths of all resulting shortened filenames while observing
# that no filename has less than MIN_DIR parent directories.
#

sub get_prefix($@)
{
    my ($min_dir, @filename_list) = @_;
    my %prefix;     # mapping: prefix -> sum of lengths
    my $current;    # Temporary iteration variable

    # Find list of prefixes
    my @munged;
    foreach (@filename_list) {
        # Need explicit assignment to get a copy of $_ so that
        # shortening the contained prefix does not affect the list
        my $current = ReadCurrentSource::resolve_path($_);
        my ($vol, $parentDir, $file) = File::Spec->splitpath($current);
        if (!File::Spec->file_name_is_absolute($current)) {
            if ($parentDir) {
                $parentDir = File::Spec->catfile($main::cwd, $parentDir);
            } else {
                $parentDir = $main::cwd;
            }
            $current = File::Spec->catfile($parentDir, $file);
        }
        push(@munged, $current);
        while ($current = shorten_prefix($current)) {
            # Skip rest if the remaining prefix has already been
            # added to hash
            if (exists($prefix{$current})) { last; }

            # Initialize with 0
            $prefix{$current} = "0";
        }

    }

    # Remove all prefixes that would cause filenames to have less than
    # the minimum number of parent directories
    foreach my $filename (@munged) {
        my $dir = dirname($filename);

        for (my $i = 0; $i < $min_dir; $i++) {
            delete($prefix{$dir});
            $dir = shorten_prefix($dir);
        }
    }

    # Check if any prefix remains
    return undef if (!%prefix);

    # Calculate sum of lengths for all prefixes
    foreach $current (keys(%prefix)) {
        foreach (@munged) {
            # Add original length
            $prefix{$current} += length($_);

            # Check whether prefix matches
            if (substr($_, 0, length($current)) eq $current) {
                # Subtract prefix length for this filename
                $prefix{$current} -= length($current);
            }
        }
    }

    # Find and return prefix with minimal sum
    $current = (keys(%prefix))[0];

    foreach (keys(%prefix)) {
        if ($prefix{$_} < $prefix{$current}) {
            $current = $_;
        }
    }
    return ($current);
}

#
# shorten_prefix(prefix)
#
# Return PREFIX shortened by last directory component.
#

sub shorten_prefix($)
{
    my ($vol, $dir, $name) = File::Spec->splitpath($_[0]);

    return File::Spec->catdir($vol, $dir);
}

#
# get_relative_base_path(subdirectory)
#
# Return a relative path string which references the base path when applied
# in SUBDIRECTORY.
#
# Example: get_relative_base_path("fs/mm") -> "../../"
#

sub get_relative_base_path($)
{
    # Make an empty directory path a special case
    if (!$_[0]) { return (""); }

    # Count number of /s in path
    my $index = ($_[0] =~ s/$lcovutil::dirseparator/$lcovutil::dirseparator/g);

    # Add a ../ to $result for each / in the directory path + 1
    my $result = "";
    for (; $index >= 0; $index--) {
        $result .= "..$lcovutil::dirseparator";
    }
    return $result;
}

#
# read_testfile(test_filename)
#
# Read in file TEST_FILENAME which contains test descriptions in the format:
#
#   TN:<whitespace><test name>
#   TD:<whitespace><test description>
#
# for each test case. Return a reference to a hash containing a mapping
#
#   test name -> test description.
#
# Die on error.
#

sub read_testfile($)
{
    my $file = shift;
    my %result;
    my $test_name;
    my $changed_testname;
    local *TEST_HANDLE;

    open(TEST_HANDLE, "<", $file) or
        die("cannot open description file '$file': $!\n");

    while (<TEST_HANDLE>) {
        chomp($_);
        s/\r//g;
        # Match lines beginning with TN:<whitespace(s)>
        next if /^#/;    # skip comment
        if (/^TN:\s*(.*?)\s*$/) {
            # Store name for later use
            $test_name = $1;
            if ($test_name =~ s/\W/_/g) {
                $changed_testname = 1;
            }
        }

        # Match lines beginning with TD:<whitespace(s)>
        if (/^TD:\s*(.*?)\s*$/) {
            if (!defined($test_name)) {
                lcovutil::ignorable_error($lcovutil::ERROR_FORMAT,
                    "\"$file\":$.: Found test description without prior test name."
                );
                next;
            }
            # Check for empty line
            if ($1) {
                # Add description to hash
                $result{$test_name} .= " $1";
            } else {
                # Add empty line
                $result{$test_name} .= "\n\n";
            }
        }
    }

    close(TEST_HANDLE) or die("unable to close HTML file: $!\n");
    if (!%result) {
        lcovutil::ignorable_error($lcovutil::ERROR_EMPTY,
                                  "no test descriptions found in '$file'.");
    }

    if ($changed_testname) {
        lcovutil::ignorable_error($lcovutil::ERROR_FORMAT,
            "invalid characters removed from testname in descriptions file '$file'."
        );
    }

    return \%result;
}

#
# escape_html(STRING)
#
# Return a copy of STRING in which all occurrences of HTML special characters
# are escaped.
#

sub escape_html($)
{
    my $string = $_[0];

    if (!$string) { return ""; }

    $string =~ s/&/&amp;/g;      # & -> &amp;
    $string =~ s/</&lt;/g;       # < -> &lt;
    $string =~ s/>/&gt;/g;       # > -> &gt;
    $string =~ s/\"/&quot;/g;    # " -> &quot;

    if ($tab_size > 0) {
        while ($string =~ /^([^\t]*)(\t)/) {
            my $replacement = " " x ($tab_size - (length($1) % $tab_size));
            $string =~ s/^([^\t]*)(\t)/$1$replacement/;
        }
    }

    $string =~ s/\n/<br>/g;      # \n -> <br>

    return $string;
}

#
# get_date_string()
#
# Return the current date in the form: yyyy-mm-dd
#

sub get_date_string($)
{
    my $time = $_[0];
    my @timeresult;

    if (!defined($time)) {
        if (defined $ENV{'SOURCE_DATE_EPOCH'}) {
            @timeresult = gmtime($ENV{'SOURCE_DATE_EPOCH'});
        } else {
            @timeresult = localtime();
        }
    } else {
        @timeresult = localtime($time);
    }
    my ($year, $month, $day, $hour, $min, $sec) = @timeresult[5, 4, 3, 2, 1, 0];

    return
        sprintf("%d-%02d-%02d %02d:%02d:%02d",
                $year + 1900,
                $month + 1, $day, $hour, $min, $sec);
}

#
# write_description_file(descriptions, overall_found, overall_hit,
#                        total_fn_found, total_fn_hit, total_br_found,
#                        total_br_hit)
#
# Write HTML file containing all test case descriptions. DESCRIPTIONS is a
# reference to a hash containing a mapping
#
#   test case name -> test case description
#
# Die on error.
#

sub write_description_file($$)
{
    my %description = %{$_[0]};
    my $summary     = $_[1];
    my $test_name;
    local *HTML_HANDLE;

    html_create(*HTML_HANDLE, "descriptions.$html_ext");

    write_html_prolog(*HTML_HANDLE, "", "LCOV - test case descriptions");
    write_header(*HTML_HANDLE, 'top', [3, 'name', 0],
                 "", "", $summary, undef, undef);

    write_test_table_prolog(*HTML_HANDLE,
                            "Test case descriptions - alphabetical list");

    foreach $test_name (sort(keys(%description))) {
        my $desc = $description{$test_name};

        $desc = escape_html($desc) if (!$rc_desc_html);
        write_test_table_entry(*HTML_HANDLE, $test_name, $desc);
    }

    write_test_table_epilog(*HTML_HANDLE);
    write_html_epilog(*HTML_HANDLE, "");

    close(*HTML_HANDLE) or die("unable to close HTML handle: $!\n");
}

#
# write_png_files()
#
# Create all necessary .png files for the HTML-output in the current
# directory. .png-files are used as bar graphs.
#
# Die on error.
#

sub write_png_files()
{
    my %data;
    local *PNG_HANDLE;

    $data{"ruby.png"} =
        $dark_mode ?
        [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00,
         0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01,
         0x00, 0x00, 0x00, 0x01, 0x01, 0x03, 0x00, 0x00, 0x00, 0x25,
         0xdb, 0x56, 0xca, 0x00, 0x00, 0x00, 0x06, 0x50, 0x4c, 0x54,
         0x45, 0x80, 0x1b, 0x18, 0x00, 0x00, 0x00, 0x39, 0x4a, 0x74,
         0xf4, 0x00, 0x00, 0x00, 0x0a, 0x49, 0x44, 0x41, 0x54, 0x08,
         0xd7, 0x63, 0x60, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0xe2,
         0x21, 0xbc, 0x33, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e,
         0x44, 0xae, 0x42, 0x60, 0x82
        ] :
        [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00,
         0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01,
         0x00, 0x00, 0x00, 0x01, 0x01, 0x03, 0x00, 0x00, 0x00, 0x25,
         0xdb, 0x56, 0xca, 0x00, 0x00, 0x00, 0x07, 0x74, 0x49, 0x4d,
         0x45, 0x07, 0xd2, 0x07, 0x11, 0x0f, 0x18, 0x10, 0x5d, 0x57,
         0x34, 0x6e, 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 0x73,
         0x00, 0x00, 0x0b, 0x12, 0x00, 0x00, 0x0b, 0x12, 0x01, 0xd2,
         0xdd, 0x7e, 0xfc, 0x00, 0x00, 0x00, 0x04, 0x67, 0x41, 0x4d,
         0x41, 0x00, 0x00, 0xb1, 0x8f, 0x0b, 0xfc, 0x61, 0x05, 0x00,
         0x00, 0x00, 0x06, 0x50, 0x4c, 0x54, 0x45, 0xff, 0x35, 0x2f,
         0x00, 0x00, 0x00, 0xd0, 0x33, 0x9a, 0x9d, 0x00, 0x00, 0x00,
         0x0a, 0x49, 0x44, 0x41, 0x54, 0x78, 0xda, 0x63, 0x60, 0x00,
         0x00, 0x00, 0x02, 0x00, 0x01, 0xe5, 0x27, 0xde, 0xfc, 0x00,
         0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60,
         0x82
        ];

    $data{"amber.png"} =
        $dark_mode ?
        [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00,
         0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01,
         0x00, 0x00, 0x00, 0x01, 0x01, 0x03, 0x00, 0x00, 0x00, 0x25,
         0xdb, 0x56, 0xca, 0x00, 0x00, 0x00, 0x06, 0x50, 0x4c, 0x54,
         0x45, 0x99, 0x86, 0x30, 0x00, 0x00, 0x00, 0x51, 0x83, 0x43,
         0xd7, 0x00, 0x00, 0x00, 0x0a, 0x49, 0x44, 0x41, 0x54, 0x08,
         0xd7, 0x63, 0x60, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0xe2,
         0x21, 0xbc, 0x33, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e,
         0x44, 0xae, 0x42, 0x60, 0x82
        ] :
        [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00,
         0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01,
         0x00, 0x00, 0x00, 0x01, 0x01, 0x03, 0x00, 0x00, 0x00, 0x25,
         0xdb, 0x56, 0xca, 0x00, 0x00, 0x00, 0x07, 0x74, 0x49, 0x4d,
         0x45, 0x07, 0xd2, 0x07, 0x11, 0x0f, 0x28, 0x04, 0x98, 0xcb,
         0xd6, 0xe0, 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 0x73,
         0x00, 0x00, 0x0b, 0x12, 0x00, 0x00, 0x0b, 0x12, 0x01, 0xd2,
         0xdd, 0x7e, 0xfc, 0x00, 0x00, 0x00, 0x04, 0x67, 0x41, 0x4d,
         0x41, 0x00, 0x00, 0xb1, 0x8f, 0x0b, 0xfc, 0x61, 0x05, 0x00,
         0x00, 0x00, 0x06, 0x50, 0x4c, 0x54, 0x45, 0xff, 0xe0, 0x50,
         0x00, 0x00, 0x00, 0xa2, 0x7a, 0xda, 0x7e, 0x00, 0x00, 0x00,
         0x0a, 0x49, 0x44, 0x41, 0x54, 0x78, 0xda, 0x63, 0x60, 0x00,
         0x00, 0x00, 0x02, 0x00, 0x01, 0xe5, 0x27, 0xde, 0xfc, 0x00,
         0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60,
         0x82
        ];
    $data{"emerald.png"} =
        $dark_mode ?
        [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00,
         0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01,
         0x00, 0x00, 0x00, 0x01, 0x01, 0x03, 0x00, 0x00, 0x00, 0x25,
         0xdb, 0x56, 0xca, 0x00, 0x00, 0x00, 0x06, 0x50, 0x4c, 0x54,
         0x45, 0x00, 0x66, 0x00, 0x0a, 0x0a, 0x0a, 0xa4, 0xb8, 0xbf,
         0x60, 0x00, 0x00, 0x00, 0x0a, 0x49, 0x44, 0x41, 0x54, 0x08,
         0xd7, 0x63, 0x60, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0xe2,
         0x21, 0xbc, 0x33, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e,
         0x44, 0xae, 0x42, 0x60, 0x82
        ] :
        [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00,
         0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01,
         0x00, 0x00, 0x00, 0x01, 0x01, 0x03, 0x00, 0x00, 0x00, 0x25,
         0xdb, 0x56, 0xca, 0x00, 0x00, 0x00, 0x07, 0x74, 0x49, 0x4d,
         0x45, 0x07, 0xd2, 0x07, 0x11, 0x0f, 0x22, 0x2b, 0xc9, 0xf5,
         0x03, 0x33, 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 0x73,
         0x00, 0x00, 0x0b, 0x12, 0x00, 0x00, 0x0b, 0x12, 0x01, 0xd2,
         0xdd, 0x7e, 0xfc, 0x00, 0x00, 0x00, 0x04, 0x67, 0x41, 0x4d,
         0x41, 0x00, 0x00, 0xb1, 0x8f, 0x0b, 0xfc, 0x61, 0x05, 0x00,
         0x00, 0x00, 0x06, 0x50, 0x4c, 0x54, 0x45, 0x1b, 0xea, 0x59,
         0x0a, 0x0a, 0x0a, 0x0f, 0xba, 0x50, 0x83, 0x00, 0x00, 0x00,
         0x0a, 0x49, 0x44, 0x41, 0x54, 0x78, 0xda, 0x63, 0x60, 0x00,
         0x00, 0x00, 0x02, 0x00, 0x01, 0xe5, 0x27, 0xde, 0xfc, 0x00,
         0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60,
         0x82
        ];
    $data{"snow.png"} =
        $dark_mode ?
        [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00,
         0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01,
         0x00, 0x00, 0x00, 0x01, 0x01, 0x03, 0x00, 0x00, 0x00, 0x25,
         0xdb, 0x56, 0xca, 0x00, 0x00, 0x00, 0x06, 0x50, 0x4c, 0x54,
         0x45, 0xdd, 0xdd, 0xdd, 0x00, 0x00, 0x00, 0xae, 0x9c, 0x6c,
         0x92, 0x00, 0x00, 0x00, 0x0a, 0x49, 0x44, 0x41, 0x54, 0x08,
         0xd7, 0x63, 0x60, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0xe2,
         0x21, 0xbc, 0x33, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e,
         0x44, 0xae, 0x42, 0x60, 0x82
        ] :
        [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00,
         0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01,
         0x00, 0x00, 0x00, 0x01, 0x01, 0x03, 0x00, 0x00, 0x00, 0x25,
         0xdb, 0x56, 0xca, 0x00, 0x00, 0x00, 0x07, 0x74, 0x49, 0x4d,
         0x45, 0x07, 0xd2, 0x07, 0x11, 0x0f, 0x1e, 0x1d, 0x75, 0xbc,
         0xef, 0x55, 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 0x73,
         0x00, 0x00, 0x0b, 0x12, 0x00, 0x00, 0x0b, 0x12, 0x01, 0xd2,
         0xdd, 0x7e, 0xfc, 0x00, 0x00, 0x00, 0x04, 0x67, 0x41, 0x4d,
         0x41, 0x00, 0x00, 0xb1, 0x8f, 0x0b, 0xfc, 0x61, 0x05, 0x00,
         0x00, 0x00, 0x06, 0x50, 0x4c, 0x54, 0x45, 0xff, 0xff, 0xff,
         0x00, 0x00, 0x00, 0x55, 0xc2, 0xd3, 0x7e, 0x00, 0x00, 0x00,
         0x0a, 0x49, 0x44, 0x41, 0x54, 0x78, 0xda, 0x63, 0x60, 0x00,
         0x00, 0x00, 0x02, 0x00, 0x01, 0xe5, 0x27, 0xde, 0xfc, 0x00,
         0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60,
         0x82
        ];

    $data{"glass.png"} = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
                          0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52,
                          0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,
                          0x01, 0x03, 0x00, 0x00, 0x00, 0x25, 0xdb, 0x56,
                          0xca, 0x00, 0x00, 0x00, 0x04, 0x67, 0x41, 0x4d,
                          0x41, 0x00, 0x00, 0xb1, 0x8f, 0x0b, 0xfc, 0x61,
                          0x05, 0x00, 0x00, 0x00, 0x06, 0x50, 0x4c, 0x54,
                          0x45, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x55,
                          0xc2, 0xd3, 0x7e, 0x00, 0x00, 0x00, 0x01, 0x74,
                          0x52, 0x4e, 0x53, 0x00, 0x40, 0xe6, 0xd8, 0x66,
                          0x00, 0x00, 0x00, 0x01, 0x62, 0x4b, 0x47, 0x44,
                          0x00, 0x88, 0x05, 0x1d, 0x48, 0x00, 0x00, 0x00,
                          0x09, 0x70, 0x48, 0x59, 0x73, 0x00, 0x00, 0x0b,
                          0x12, 0x00, 0x00, 0x0b, 0x12, 0x01, 0xd2, 0xdd,
                          0x7e, 0xfc, 0x00, 0x00, 0x00, 0x07, 0x74, 0x49,
                          0x4d, 0x45, 0x07, 0xd2, 0x07, 0x13, 0x0f, 0x08,
                          0x19, 0xc4, 0x40, 0x56, 0x10, 0x00, 0x00, 0x00,
                          0x0a, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9c, 0x63,
                          0x60, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0x48,
                          0xaf, 0xa4, 0x71, 0x00, 0x00, 0x00, 0x00, 0x49,
                          0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82
    ];

    if ($sort_tables) {
        $data{"updown.png"} =
            $dark_mode ?
            [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00,
             0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x0a,
             0x00, 0x00, 0x00, 0x0e, 0x08, 0x06, 0x00, 0x00, 0x00, 0x16,
             0xa3, 0x8d, 0xab, 0x00, 0x00, 0x00, 0x43, 0x49, 0x44, 0x41,
             0x54, 0x28, 0xcf, 0x63, 0x60, 0x40, 0x03, 0x77, 0xef, 0xde,
             0xfd, 0x7f, 0xf7, 0xee, 0xdd, 0xff, 0xe8, 0xe2, 0x8c, 0xe8,
             0x8a, 0x90, 0xf9, 0xca, 0xca, 0xca, 0x8c, 0x18, 0x0a, 0xb1,
             0x99, 0x82, 0xac, 0x98, 0x11, 0x9f, 0x22, 0x64, 0xc5, 0x8c,
             0x84, 0x14, 0xc1, 0x00, 0x13, 0xc3, 0x80, 0x01, 0xea, 0xbb,
             0x91, 0xf8, 0xe0, 0x21, 0x29, 0xc0, 0x89, 0x89, 0x42, 0x06,
             0x62, 0x13, 0x05, 0x00, 0xe1, 0xd3, 0x2d, 0x91, 0x93, 0x15,
             0xa4, 0xb2, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44,
             0xae, 0x42, 0x60, 0x82
            ] :
            [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00,
             0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x0a,
             0x00, 0x00, 0x00, 0x0e, 0x08, 0x06, 0x00, 0x00, 0x00, 0x16,
             0xa3, 0x8d, 0xab, 0x00, 0x00, 0x00, 0x3c, 0x49, 0x44, 0x41,
             0x54, 0x28, 0xcf, 0x63, 0x60, 0x40, 0x03, 0xff, 0xa1, 0x00,
             0x5d, 0x9c, 0x11, 0x5d, 0x11, 0x8a, 0x24, 0x23, 0x23, 0x23,
             0x86, 0x42, 0x6c, 0xa6, 0x20, 0x2b, 0x66, 0xc4, 0xa7, 0x08,
             0x59, 0x31, 0x23, 0x21, 0x45, 0x30, 0xc0, 0xc4, 0x30, 0x60,
             0x80, 0xfa, 0x6e, 0x24, 0x3e, 0x78, 0x48, 0x0a, 0x70, 0x62,
             0xa2, 0x90, 0x81, 0xd8, 0x44, 0x01, 0x00, 0xe9, 0x5c, 0x2f,
             0xf5, 0xe2, 0x9d, 0x0f, 0xf9, 0x00, 0x00, 0x00, 0x00, 0x49,
             0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82
            ];
    }

    foreach (keys(%data)) {
        my $f = File::Spec->catfile($main::output_directory, $_);
        open(PNG_HANDLE, ">", $f) or
            die("cannot create $f: $!\n");
        binmode(PNG_HANDLE);
        print(PNG_HANDLE map(chr, @{$data{$_}}));
        close(PNG_HANDLE) or die("unable to close PNG handle: $!\n");
    }
}

#
# write_htaccess_file()
#

sub write_htaccess_file()
{
    local *HTACCESS_HANDLE;
    my $htaccess_data;

    my $f = File::Spec->catdir($main::output_directory, '.htaccess');
    open(*HTACCESS_HANDLE, ">", $f) or
        die("cannot open $f for writing: $!\n");

    $htaccess_data = (<<"END_OF_HTACCESS")
AddEncoding x-gzip .html
END_OF_HTACCESS
        ;

    print(HTACCESS_HANDLE $htaccess_data);
    close(*HTACCESS_HANDLE) or die("unable to close .htaccess: $!\n");
}

#
# write_css_file()
#
# Write the cascading style sheet file gcov.css to the current directory.
# This file defines basic layout attributes of all generated HTML pages.
#

sub write_css_file()
{
    local *CSS_HANDLE;

    my $f = File::Spec->catdir($main::output_directory, 'gcov.css');
    # Check for a specified external style sheet file
    if ($css_filename) {
        # Simply copy that file
        system("cp", $css_filename, $f) and
            die("cannot copy file $css_filename: $!\n");
        return;
    }

    open(CSS_HANDLE, ">", $f) or
        die("cannot open $f for writing: $!\n");

    # *************************************************************

    # *************************************************************
    my $ownerBackground = "#COLOR_17";    # very light pale grey/blue
    my $ownerCovHi      = "#COLOR_18";    # light green
    my $ownerCovMed     = "#COLOR_19";    # light yellow
    my $ownerCovLo      = "#COLOR_20";    # lighter red
        # use same background color as file entry unless in hierarchical report
    my $directoryBackground = $main::hierarchical ? '#COLOR_18' : '#COLOR_06';
    my $css_data            = ($_ = <<"END_OF_CSS")
        /* All views: initial background and text color */
        body
        {
          color: #COLOR_00;
          background-color: #COLOR_14;
        }

        /* All views: standard link format*/
        a:link
        {
          color: #COLOR_15;
          text-decoration: underline;
        }

        /* All views: standard link - visited format */
        a:visited
        {
          color: #COLOR_01;
          text-decoration: underline;
        }

        /* All views: standard link - activated format */
        a:active
        {
          color: #COLOR_11;
          text-decoration: underline;
        }

        /* All views: main title format */
        td.title
        {
          text-align: center;
          padding-bottom: 10px;
          font-family: sans-serif;
          font-size: 20pt;
          font-style: italic;
          font-weight: bold;
        }
        /* table footnote */
        td.footnote
        {
          text-align: left;
          padding-left: 100px;
          padding-right: 10px;
          background-color: #COLOR_08; /* light blue table background color */
          /* dark blue table header color
          background-color: #COLOR_03; */
          white-space: nowrap;
          font-family: sans-serif;
          font-style: italic;
          font-size:70%;
        }
        /* "Line coverage date bins" leader */
        td.subTableHeader
        {
          text-align: center;
          padding-bottom: 6px;
          font-family: sans-serif;
          font-weight: bold;
          vertical-align: center;
        }

        /* All views: header item format */
        td.headerItem
        {
          text-align: right;
          padding-right: 6px;
          font-family: sans-serif;
          font-weight: bold;
          vertical-align: top;
          white-space: nowrap;
        }

        /* All views: header item value format */
        td.headerValue
        {
          text-align: left;
          color: #COLOR_15;
          font-family: sans-serif;
          font-weight: bold;
          white-space: nowrap;
        }

        /* All views: header item coverage table heading */
        td.headerCovTableHead
        {
          text-align: center;
          padding-right: 6px;
          padding-left: 6px;
          padding-bottom: 0px;
          font-family: sans-serif;
          white-space: nowrap;
        }

        /* All views: header item coverage table entry */
        td.headerCovTableEntry
        {
          text-align: right;
          color: #COLOR_15;
          font-family: sans-serif;
          font-weight: bold;
          white-space: nowrap;
          padding-left: 12px;
          padding-right: 4px;
          background-color: #COLOR_08;
        }

        /* All views: header item coverage table entry for high coverage rate */
        td.headerCovTableEntryHi
        {
          text-align: right;
          color: #COLOR_00;
          font-family: sans-serif;
          font-weight: bold;
          white-space: nowrap;
          padding-left: 12px;
          padding-right: 4px;
          background-color: #COLOR_04;
        }

        /* All views: header item coverage table entry for medium coverage rate */
        td.headerCovTableEntryMed
        {
          text-align: right;
          color: #COLOR_00;
          font-family: sans-serif;
          font-weight: bold;
          white-space: nowrap;
          padding-left: 12px;
          padding-right: 4px;
          background-color: #COLOR_13;
        }

        /* All views: header item coverage table entry for low coverage rate */
        td.headerCovTableEntryLo
        {
          text-align: right;
          color: #COLOR_00;
          font-family: sans-serif;
          font-weight: bold;
          white-space: nowrap;
          padding-left: 12px;
          padding-right: 4px;
          background-color: #COLOR_10;
        }

        /* All views: header legend value for legend entry */
        td.headerValueLeg
        {
          text-align: left;
          color: #COLOR_00;
          font-family: sans-serif;
          font-size: 80%;
          white-space: nowrap;
          padding-top: 4px;
        }

        /* All views: color of horizontal ruler */
        td.ruler
        {
          background-color: #COLOR_03;
        }

        /* All views: version string format */
        td.versionInfo
        {
          text-align: center;
          padding-top: 2px;
          font-family: sans-serif;
          font-style: italic;
        }

        /* Directory view/File view (all)/Test case descriptions:
           table headline format */
        td.tableHead
        {
          text-align: center;
          color: #COLOR_14;
          background-color: #COLOR_03;
          font-family: sans-serif;
          font-size: 120%;
          font-weight: bold;
          white-space: nowrap;
          padding-left: 4px;
          padding-right: 4px;
        }

        span.tableHeadSort
        {
          padding-right: 4px;
        }

        /* Directory view/File view (all): filename entry format */
        td.coverFile
        {
          text-align: left;
          padding-left: 10px;
          padding-right: 20px;
          color: #COLOR_15;
          background-color: #COLOR_08;
          font-family: monospace;
        }

        /* Directory view/File view (all): directory name entry format */
        td.coverDirectory
        {
          text-align: left;
          padding-left: 10px;
          padding-right: 20px;
          color: #COLOR_15;
          background-color: $directoryBackground;
          font-family: monospace;
        }

        /* Directory view/File view (all): filename entry format */
        td.overallOwner
        {
          text-align: center;
          font-weight: bold;
          font-family: sans-serif;
          background-color: #COLOR_08;
          padding-right: 10px;
          padding-left: 10px;
        }

        /* Directory view/File view (all): filename entry format */
        td.ownerName
        {
          text-align: right;
          font-style: italic;
          font-family: sans-serif;
          background-color: $ownerBackground;
          padding-right: 10px;
          padding-left: 20px;
        }

        /* Directory view/File view (all): bar-graph entry format*/
        td.coverBar
        {
          padding-left: 10px;
          padding-right: 10px;
          background-color: #COLOR_08;
        }

        /* Directory view/File view (all): bar-graph entry format*/
        td.owner_coverBar
        {
          padding-left: 10px;
          padding-right: 10px;
          background-color: $ownerBackground;
        }

        /* Directory view/File view (all): bar-graph outline color */
        td.coverBarOutline
        {
          background-color: #COLOR_00;
        }

        /* Directory view/File view (all): percentage entry for files with
           high coverage rate */
        td.coverPerHi
        {
          text-align: right;
          padding-left: 10px;
          padding-right: 10px;
          background-color: #COLOR_04;
          font-weight: bold;
          font-family: sans-serif;
        }

        /* 'owner' entry:  slightly lighter color than 'coverPerHi' */
        td.owner_coverPerHi
        {
          text-align: right;
          padding-left: 10px;
          padding-right: 10px;
          background-color: $ownerCovHi;
          font-weight: bold;
          font-family: sans-serif;
        }

        /* Directory view/File view (all): line count entry */
        td.coverNumDflt
        {
          text-align: right;
          padding-left: 10px;
          padding-right: 10px;
          background-color: #COLOR_08;
          white-space: nowrap;
          font-family: sans-serif;
        }

        /* td background color and font for the 'owner' section of the table */
        td.ownerTla
        {
          text-align: right;
          padding-left: 10px;
          padding-right: 10px;
          background-color: $ownerBackground;
          white-space: nowrap;
          font-family: sans-serif;
          font-style: italic;
        }

        /* Directory view/File view (all): line count entry for files with
           high coverage rate */
        td.coverNumHi
        {
          text-align: right;
          padding-left: 10px;
          padding-right: 10px;
          background-color: #COLOR_04;
          white-space: nowrap;
          font-family: sans-serif;
        }

        td.owner_coverNumHi
        {
          text-align: right;
          padding-left: 10px;
          padding-right: 10px;
          background-color: $ownerCovHi;
          white-space: nowrap;
          font-family: sans-serif;
        }

        /* Directory view/File view (all): percentage entry for files with
           medium coverage rate */
        td.coverPerMed
        {
          text-align: right;
          padding-left: 10px;
          padding-right: 10px;
          background-color: #COLOR_13;
          font-weight: bold;
          font-family: sans-serif;
        }

        td.owner_coverPerMed
        {
          text-align: right;
          padding-left: 10px;
          padding-right: 10px;
          background-color: $ownerCovMed;
          font-weight: bold;
          font-family: sans-serif;
        }

        /* Directory view/File view (all): line count entry for files with
           medium coverage rate */
        td.coverNumMed
        {
          text-align: right;
          padding-left: 10px;
          padding-right: 10px;
          background-color: #COLOR_13;
          white-space: nowrap;
          font-family: sans-serif;
        }

        td.owner_coverNumMed
        {
          text-align: right;
          padding-left: 10px;
          padding-right: 10px;
          background-color: $ownerCovMed;
          white-space: nowrap;
          font-family: sans-serif;
        }

        /* Directory view/File view (all): percentage entry for files with
           low coverage rate */
        td.coverPerLo
        {
          text-align: right;
          padding-left: 10px;
          padding-right: 10px;
          background-color: #COLOR_10;
          font-weight: bold;
          font-family: sans-serif;
        }

        td.owner_coverPerLo
        {
          text-align: right;
          padding-left: 10px;
          padding-right: 10px;
          background-color: $ownerCovLo;
          font-weight: bold;
          font-family: sans-serif;
        }

        /* Directory view/File view (all): line count entry for files with
           low coverage rate */
        td.coverNumLo
        {
          text-align: right;
          padding-left: 10px;
          padding-right: 10px;
          background-color: #COLOR_10;
          white-space: nowrap;
          font-family: sans-serif;
        }

        td.owner_coverNumLo
        {
          text-align: right;
          padding-left: 10px;
          padding-right: 10px;
          background-color: $ownerCovLo;
          white-space: nowrap;
          font-family: sans-serif;
        }

        /* File view (all): "show/hide details" link format */
        a.detail:link
        {
          color: #COLOR_06;
          font-size:80%;
        }

        /* File view (all): "show/hide details" link - visited format */
        a.detail:visited
        {
          color: #COLOR_06;
          font-size:80%;
        }

        /* File view (all): "show/hide details" link - activated format */
        a.detail:active
        {
          color: #COLOR_14;
          font-size:80%;
        }

        /* File view (detail): test name entry */
        td.testName
        {
          text-align: right;
          padding-right: 10px;
          background-color: #COLOR_08;
          font-family: sans-serif;
        }

        /* File view (detail): test percentage entry */
        td.testPer
        {
          text-align: right;
          padding-left: 10px;
          padding-right: 10px;
          background-color: #COLOR_08;
          font-family: sans-serif;
        }

        /* File view (detail): test lines count entry */
        td.testNum
        {
          text-align: right;
          padding-left: 10px;
          padding-right: 10px;
          background-color: #COLOR_08;
          font-family: sans-serif;
        }

        /* Test case descriptions: test name format*/
        dt
        {
          font-family: sans-serif;
          font-weight: bold;
        }

        /* Test case descriptions: description table body */
        td.testDescription
        {
          padding-top: 10px;
          padding-left: 30px;
          padding-bottom: 10px;
          padding-right: 30px;
          background-color: #COLOR_08;
        }

        /* Source code view: function entry */
        td.coverFn
        {
          text-align: left;
          padding-left: 10px;
          padding-right: 20px;
          color: #COLOR_15;
          background-color: #COLOR_08;
          font-family: monospace;
        }

        /* Source code view: function entry zero count*/
        td.coverFnLo
        {
          text-align: right;
          padding-left: 10px;
          padding-right: 10px;
          background-color: #COLOR_10;
          font-weight: bold;
          font-family: sans-serif;
        }

        /* Source code view: function entry nonzero count*/
        td.coverFnHi
        {
          text-align: right;
          padding-left: 10px;
          padding-right: 10px;
          background-color: #COLOR_08;
          font-weight: bold;
          font-family: sans-serif;
        }

        td.coverFnAlias
        {
          text-align: right;
          padding-left: 10px;
          padding-right: 20px;
          color: #COLOR_15;
          /* make this a slightly different color than the leader - otherwise,
             otherwise the alias is hard to distinguish in the table */
          background-color: #COLOR_17; /* very light pale grey/blue */
          font-family: monospace;
        }

        /* Source code view: function entry zero count*/
        td.coverFnAliasLo
        {
          text-align: right;
          padding-left: 10px;
          padding-right: 10px;
          background-color: $ownerCovLo; /* lighter red */
          font-family: sans-serif;
        }

        /* Source code view: function entry nonzero count*/
        td.coverFnAliasHi
        {
          text-align: right;
          padding-left: 10px;
          padding-right: 10px;
          background-color: #COLOR_08;
          font-weight: bold;
          font-family: sans-serif;
        }

        /* Source code view: source code format */
        pre.source
        {
          font-family: monospace;
          white-space: pre;
          margin-top: 2px;
          tab-size: #TAB_SIZE;
        }

        /* elided/removed code */
        span.elidedSource
        {
          font-family: sans-serif;
          /*font-size: 8pt; */
          font-style: italic;
          background-color: lightgrey;
        }

        /* Source code view: line number format */
        span.lineNum
        {
          background-color: #COLOR_09;
        }

        /* Source code view: line number format when there are deleted
           lines in the corresponding location */
        span.lineNumWithDelete
        {
          foreground-color: #COLOR_09;
          background-color: lightgrey;
        }

        /* Source code view: format for Cov legend */
        span.coverLegendCov
        {
          padding-left: 10px;
          padding-right: 10px;
          padding-bottom: 2px;
          background-color: #COLOR_07;
        }

        /* Source code view: format for NoCov legend */
        span.coverLegendNoCov
        {
          padding-left: 10px;
          padding-right: 10px;
          padding-bottom: 2px;
          background-color: #COLOR_12;
        }

        /* Source code view: format for the source code heading line */
        pre.sourceHeading
        {
          white-space: pre;
          font-family: monospace;
          font-weight: bold;
          margin: 0px;
        }

        /* All views: header legend value for low rate */
        td.headerValueLegL
        {
          font-family: sans-serif;
          text-align: center;
          white-space: nowrap;
          padding-left: 4px;
          padding-right: 2px;
          background-color: #COLOR_10;
          font-size: 80%;
        }

        /* All views: header legend value for med rate */
        td.headerValueLegM
        {
          font-family: sans-serif;
          text-align: center;
          white-space: nowrap;
          padding-left: 2px;
          padding-right: 2px;
          background-color: #COLOR_13;
          font-size: 80%;
        }

        /* All views: header legend value for hi rate */
        td.headerValueLegH
        {
          font-family: sans-serif;
          text-align: center;
          white-space: nowrap;
          padding-left: 2px;
          padding-right: 4px;
          background-color: #COLOR_04;
          font-size: 80%;
        }

        /* All views except source code view: legend format for low coverage */
        span.coverLegendCovLo
        {
          padding-left: 10px;
          padding-right: 10px;
          padding-top: 2px;
          background-color: #COLOR_10;
        }

        /* All views except source code view: legend format for med coverage */
        span.coverLegendCovMed
        {
          padding-left: 10px;
          padding-right: 10px;
          padding-top: 2px;
          background-color: #COLOR_13;
        }

        /* All views except source code view: legend format for hi coverage */
        span.coverLegendCovHi
        {
          padding-left: 10px;
          padding-right: 10px;
          padding-top: 2px;
          background-color: #COLOR_04;
        }

        a.branchTla:link
        {
          color: #COLOR_00;
        }

        a.branchTla:visited
        {
          color: #COLOR_00;
        }

        a.mcdcTla:link
        {
          color: #COLOR_00;
        }

        a.mcdcTla:visited
        {
          color: #COLOR_00;
        }
END_OF_CSS
        ;

    foreach my $tla (@SummaryInfo::tlaPriorityOrder) {
        my $title = $SummaryInfo::tlaToTitle{$tla};
        my $color = $lcovutil::tlaColor{$tla};
        foreach my $elem ("td", "span") {
            my $align = $elem eq 'td' ? "right" : "left";
            $css_data .= ($_ = <<"END_OF_SPAN")

        /* Source code view/table entry background: format for lines classified as "$title" */
        $elem.tla$tla
        {
          text-align: $align;
          background-color: $color;
        }
        $elem.tlaBg$tla {
          background-color: $color;
        }
END_OF_SPAN
                ;
        }
        # the href anchor tag background
        $css_data .= ($_ = <<"END_OF_SPAN");
        a.tlaBg$tla {
          background-color: $color;
          color: #COLOR_00;
        }

        td.headerCovTableHead$tla {
          text-align: center;
          padding-right: 6px;
          padding-left: 6px;
          padding-bottom: 0px;
          font-family: sans-serif;
          white-space: nowrap;
          background-color: $color;
        }
END_OF_SPAN
    }

    # 'span' tags for date bins...
    #   probably should have one for each bin...
    $css_data .= ($_ = <<"END_OF_DATE_SPAN")

        /* Source code view: format for date/owner bin that is not hit */
        span.missBins
        {
          background-color: #COLOR_10 /* red */
        }
END_OF_DATE_SPAN
        ;

    # *************************************************************

    # Remove leading tab from all lines
    $css_data =~ s/^\t//gm;
    $css_data =~ s/^        //gm;    # and 8 spaces...

    # Apply palette
    my $palette =
        $dark_mode ? \%lcovutil::dark_palette : \%lcovutil::normal_palette;

    while (my ($key, $color) = each(%$palette)) {
        $css_data =~ s/$key/$color/gm;
    }

    if ($tab_size < 0) {
        my $abs_tab_size = abs($tab_size);
        $css_data =~ s/#TAB_SIZE/$abs_tab_size/gm;
    } else {
        $css_data =~ s/^.*#TAB_SIZE.*\n//gm;
    }

    print(CSS_HANDLE $css_data);

    close(CSS_HANDLE) or die("unable to close CSS handle: $!\n");
}

#
# get_bar_graph_code(base_dir, cover_found, cover_hit)
#
# Return a string containing HTML code which implements a bar graph display
# for a coverage rate of cover_hit * 100 / cover_found.
#

sub get_bar_graph_code($$$)
{
    my ($base_dir, $found, $hit) = @_;
    my $graph_code;

    # Check number of instrumented lines
    if ($found == 0) { return ""; }

    my $alt       = rate($hit, $found, "%");
    my $width     = rate($hit, $found, undef, 0);
    my $remainder = 100 - $width;

    # Decide which .png file to use
    my $png_name =
        $rate_png[classify_rate($found, $hit, $ln_med_limit, $ln_hi_limit)];

    if ($width == 0) {
        # Zero coverage
        $graph_code = (<<END_OF_HTML);
                <table border=0 cellspacing=0 cellpadding=1><tr><td class="coverBarOutline"><img src="${base_dir}snow.png" width=100 height=10 alt="$alt"></td></tr></table>
END_OF_HTML
    } elsif ($width == 100) {
        # Full coverage
        $graph_code = (<<END_OF_HTML);
                <table border=0 cellspacing=0 cellpadding=1><tr><td class="coverBarOutline"><img src="${base_dir}$png_name" width=100 height=10 alt="$alt"></td></tr></table>
END_OF_HTML
    } else {
        # Positive coverage
        $graph_code = (<<END_OF_HTML);
                <table border=0 cellspacing=0 cellpadding=1><tr><td class="coverBarOutline"><img src="${base_dir}$png_name" width=$width height=10 alt="$alt"><img src="${base_dir}snow.png" width=$remainder height=10 alt="$alt"></td></tr></table>
END_OF_HTML
    }

    # Remove leading tabs from all lines
    $graph_code =~ s/^\t+//gm;
    chomp($graph_code);

    return ($graph_code);
}

#
# sub classify_rate(found, hit, med_limit, high_limit)
#
# Return 0 for low rate, 1 for medium rate and 2 for hi rate.
#

sub classify_rate($$$$)
{
    my ($found, $hit, $med, $hi) = @_;

    if ($found == 0) {
        return 2;
    }
    my $rate = rate($hit, $found);
    if ($rate < $med) {
        return 0;
    } elsif ($rate < $hi) {
        return 1;
    }
    return 2;
}

#
# write_html(filehandle, html_code)
#
# Write out HTML_CODE to FILEHANDLE while removing a leading tabulator mark
# in each line of HTML_CODE.
#

sub write_html(*$)
{
    local *HTML_HANDLE = $_[0];
    my $html_code = $_[1];

    # Remove leading tab from all lines
    $html_code =~ s/^\t//gm;

    print(HTML_HANDLE $html_code) or
        die("cannot write HTML data ($!)\n");
}

#
# write_html_prolog(filehandle, base_dir, pagetitle)
#
# Write an HTML prolog common to all HTML files to FILEHANDLE. PAGETITLE will
# be used as HTML page title. BASE_DIR contains a relative path which points
# to the base directory.
#

sub write_html_prolog(*$$)
{
    my $basedir   = $_[1];
    my $pagetitle = $_[2];
    my $prolog;

    $prolog = $html_prolog;
    $prolog =~ s/\@pagetitle\@/$pagetitle/g;
    $prolog =~ s/\@basedir\@/$basedir/g;

    write_html($_[0], $prolog);
}

#
# write_header_prolog(filehandle, base_dir)
#
# Write beginning of page header HTML code.
#

sub write_header_prolog(*$)
{
    # *************************************************************

    write_html($_[0], <<END_OF_HTML);
          <table width="100%" border=0 cellspacing=0 cellpadding=0>
            <tr><td class="title">$title</td></tr>
            <tr><td class="ruler"><img src="$_[1]glass.png" width=3 height=3 alt=""></td></tr>

            <tr>
              <td width="100%">
                <table cellpadding=1 border=0 width="100%">
END_OF_HTML

    # *************************************************************
}

#
# write_header_line(handle, content)
#
# Write a header line with the specified table contents.
#

sub write_header_line(*@)
{
    my ($handle, @content) = @_;

    write_html($handle, "          <tr>\n");
    # label order has to match data that is passed
    my @labels = qw/width class colspan title/;
    foreach my $entry (@content) {
        my @d    = @$entry;
        my $text = splice(@d, 2, 1) if (scalar(@d) > 1);
        # entry may not contain some data - e.g., does not have colspan or title
        die("unexpected entry format") unless scalar(@labels) >= scalar(@d);
        my $str = "            <td";
        for (my $i = 0; $i <= $#d; ++$i) {
            my $e = $d[$i];
            $str .= ' ' . $labels[$i] . "=\"$e\""
                if defined($e);
        }
        $str .= '>';
        $str .= $text
            if defined($text);
        $str .= "</td>\n";
        # so 'str' looked like '<td width="value" colspan="value">whatever</td>'
        write_html($handle, $str);
    }
    write_html($handle, "          </tr>\n");    # then end the row
}

#
# write_header_epilog(filehandle, base_dir)
#
# Write end of page header HTML code.
#

sub write_header_epilog(*$)
{
    # *************************************************************

    write_html($_[0], <<END_OF_HTML);
                  <tr><td><img src="$_[1]glass.png" width=3 height=3 alt=""></td></tr>
                </table>
              </td>
            </tr>

            <tr><td class="ruler"><img src="$_[1]glass.png" width=3 height=3 alt=""></td></tr>
          </table>

END_OF_HTML
    # *************************************************************
}

#
# write_file_table_prolog(handle, file_heading, binHeading, primary_key, ([heading, num_cols], ...))
#
# Write heading for file table.
#

sub write_file_table_prolog(*$$$@)
{
    my ($handle, $file_heading, $bin_heading, $primary_key, @columns) = @_;
    my $num_columns = 0;
    my $file_width  = 40;
    my $col;
    my $width;

    foreach $col (@columns) {
        my ($heading, $cols, $titles) = @{$col};

        $num_columns += $cols;
    }
    $num_columns++ if (defined($bin_heading));
    $width = int((100 - $file_width) / $num_columns);

    # Table definition
    write_html($handle, <<END_OF_HTML);
          <center>
          <table width="80%" cellpadding=1 cellspacing=1 border=0>

            <tr>
              <td width="$file_width%"><br></td>
END_OF_HTML
    if (defined($bin_heading)) {
        # owner or date column
        write_html($handle, <<END_OF_HTML);
              <td width="$width%"></td>
END_OF_HTML
    }
    # Empty first row
    foreach $col (@columns) {
        my ($heading, $cols) = @{$col};

        while ($cols-- > 0) {
            write_html($handle, <<END_OF_HTML);
            <td width="$width%"></td>
END_OF_HTML
        }
    }
    # Next row
    if ($primary_key eq "name") {
        my $spanType = defined($bin_heading) ? "colspan" : "rowspan";
        ++$num_columns;
        write_html($handle, <<END_OF_HTML);
            </tr>

            <tr>
              <td class="tableHead" $spanType=2>$file_heading</td>
END_OF_HTML
    } else {
        my $t = ucfirst($primary_key);
        # a bit of a hack...just substitute the primary key and related
        #   strings into the 'file heading' link - so we display the
        #   'sort' widget
        if ($primary_key eq 'owner' &&
            $file_heading =~ /^([^ ]+) <span/) {
            my $viewType = $1;
            $file_heading =~ s/$viewType/$t/;
            $file_heading =~ s/file name/$primary_key/g;
            $t            = $file_heading;
            $file_heading = $viewType;
        }
        $num_columns += 2;
        write_html($handle, <<END_OF_HTML);
            </tr>

            <tr>
              <td class="tableHead" rowspan=2>$t</td>
              <td class="tableHead" rowspan=2>$file_heading</td>
END_OF_HTML
    }
    # Heading row
    foreach $col (@columns) {
        my ($heading, $cols, $titles) = @{$col};
        my $colspan = "";
        my $rowspan = "";
        $colspan = " colspan=$cols" if ($cols > 1);
        $rowspan = " rowspan=2" if (!defined($titles));
        write_html($handle, <<END_OF_HTML);
        <td class="tableHead"$colspan$rowspan>$heading</td>
END_OF_HTML
    }
    write_html($handle, <<END_OF_HTML);
            </tr>
            <tr>
END_OF_HTML

    # title row
    if (defined($bin_heading)) {
        # Next row
        my $str = ucfirst($bin_heading);
        write_html($handle, <<END_OF_HTML);
              <td class="tableHead">Name</td>
              <td class="tableHead">$str</td>
END_OF_HTML
    }

    foreach $col (@columns) {
        my ($heading, $cols, $titles) = @{$col};
        my $colspan = "";
        my $rowspan = "";

        if (defined($titles)) {
            foreach my $t (@$titles) {
                my $span  = "";
                my $popup = '';
                if ("ARRAY" eq ref($t)) {
                    my ($tla, $num, $help) = @$t;
                    $span  = " colspan=" . $num if $num > 1;
                    $popup = " title=\"$help\""
                        if (defined $help);
                    $t = $tla;
                }
                write_html($handle, <<END_OF_HTML);
                    <td class="tableHead"$span$popup> $t</td>
END_OF_HTML
            }
        }
    }
    write_html($handle, <<END_OF_HTML);
            </tr>
END_OF_HTML
    return $num_columns;
}

sub escape_id($)
{
    my ($name) = @_;

    # Name/ID attribute requirements according to HTML 4.01 Transitional
    $name =~ s/[^A-Za-z0-9-_:.]/_/g;

    return $name;
}

# write_file_table_entry(handle, base_dir,
#                        [ name, [filename, fileDetails, fileHref, cbdata],
#                          activeTlaCols,
#                          rowspan, primary_key, is_secondary, fileview,
#                          page_type, page_link, dirSummary, showDetailCol,
#                          asterisk ],
#                        ([ found, hit, med_limit, hi_limit, graph ], ..)
#
# Write an entry of the file table.
# $fileview:  0 == 'table is listing directories', 1 == 'list files'
#

sub write_file_table_entry(*$$@)
{
    my ($handle, $base_dir, $data, @entries) = @_;
    my ($name, $callbackData, $activeTlaCols,
        $rowspan, $primary_key, $is_secondary,
        $fileview, $page_type, $page_link,
        $dirSummary, $showBinDetailColumn, $asterisk) = @$data;
    my ($filename, $fileSummary, $fileDetails, $file_link, $cbData) =
        @$callbackData;
    die("unexpected callback arg types: write_file_table_entry(" .
        ref($fileSummary) . ', ' .
        ref($fileDetails) .
        ", $file_link, " . (defined($cbData) ? $cbData : 'undef') . ')')
        unless (!defined($fileDetails) || 'SourceFile' eq ref($fileDetails)) &&
        'SummaryInfo' eq ref($fileSummary);
    my $esc_name = escape_html($name);
    if ($main::flat &&
        !$is_secondary) {
        my $relDir = $fileSummary->relativeDir();
        die("relative directory not set") unless $relDir;
        $esc_name = escape_html(File::Spec->catfile($relDir, $name))
            if '.' ne $relDir;
    }
    my $obj_name = $esc_name
        .
        ($fileSummary->type() eq 'directory' &&
             $primary_key eq 'name' ? escape_html($lcovutil::dirseparator) :
             '');
    my $namecode = $obj_name;
    my $owner;
    my $full_path =
        'directory' eq $fileSummary->type() ? $fileSummary->fullDir() :
        $fileSummary->name();
    $full_path = escape_html($full_path);

    # Add link to source if provided
    my $source_link;
    my $suppressFileHref =
        $main::no_sourceview && $fileSummary->type() eq 'file';
    if ($is_secondary == 2) {
        if (defined($page_link) &&
            $page_link ne "") {
            # could use $fileSummary->name() in the popup
            $source_link =
                $suppressFileHref ? $obj_name :
                ("<a href=\"$page_link\" title=\"Click to go to " .
                 $fileSummary->type() . " $full_path\">$obj_name</a>");
        } elsif (defined($file_link) &&
                 $file_link ne "") {
            my $target = File::Basename::basename($fileSummary->name());
            if ($main::flat) {
                $target =
                    File::Spec->catfile($fileSummary->relativeDir(), $target);
            }
            if ($fileSummary->type() eq 'directory') {
                $target = File::Basename::dirname($target);
                $target = File::Basename::basename($target)
                    if ($main::hierarchical);
            }
            $target = escape_html($target);
            $source_link =
                $suppressFileHref ? $target :
                (
                "<a href=\"$file_link\" title=\"Click to go to $full_path source detail\">$target</a>"
                );
        } else {
            $source_link = $fileSummary->name();
            $source_link = File::Basename::basename($source_link)
                if ($main::hierarchical);
            $source_link = escape_html($source_link);
        }
    }
    my $anchor = 'NAME';
    if (!$is_secondary &&
        defined($page_link) &&
        $page_link ne "") {
        # could use $fileSummary->name() in the popup
        $namecode =
            $suppressFileHref ? $obj_name :
            ("<a href=\"$page_link\" title=\"Click to go to " .
             $fileSummary->type() . " $full_path\">$obj_name</a>");
        $owner = "";
    } elsif ($is_secondary &&
             $primary_key ne 'name' &&
             defined($file_link) &&
             $file_link ne "") {
        if ($main::flat) {
            $file_link =
                File::Spec->catfile($fileSummary->relativeDir(), $file_link);
        } elsif ($main::hierarchical &&
                 'directory' eq $fileSummary->type()) {
            die("no parent for " . $fileSummary->name())
                unless defined($fileSummary->parent());
            my $pname = $fileSummary->parent()->name();
            die("unexpected parent '$pname' in $file_link")
                unless (length($pname) < length($file_link) &&
                        $pname eq substr($file_link, 0, length($pname)));
            $file_link = substr($file_link, length($pname) + 1);

            #lcovutil::info("$filename: path is $file_link, base is '$base_dir' full path $full_path\n");
        }
        $namecode =
            $suppressFileHref ? $obj_name :
            (
            "<a href=\"$file_link\" title=\"Click to go to $full_path source detail\">$obj_name</a>"
            );
    } elsif ($is_secondary &&
             $primary_key ne 'name' &&
             $name eq $filename) {
        # get here when we suppressed the sourceview - so the file link
        # is not defined
        $namecode = $obj_name;
    } elsif (defined($primary_key)) {
        $namecode = $obj_name;
        if (!$suppressFileHref) {
            # we want the the HREF anchor on the column 1 entry -
            #   the column 0 entry may span many rows - so navigation to that
            #   entry (e.g., to find all the files in the "(7..30] days" bin)
            #   may be rendered such that the first element in the bin is not
            #   visible (you have to scroll up to see it).
            # the fix is to put the anchor in the next column
            if ($primary_key eq 'owner') {
                $anchor = '<a id="N' . escape_id($name) . '">NAME</a>';
            } elsif ($primary_key eq 'date') {
                die("unexpected bin")
                    unless (!$is_secondary &&
                            exists($SummaryInfo::ageHeaderToBin{$name})) ||
                    (defined($cbData) &&
                     $cbData <= $#SummaryInfo::ageGroupHeader);
                my $bin =
                    $is_secondary ? $cbData :
                    $SummaryInfo::ageHeaderToBin{$name};
                $anchor = "<a id=\"B$bin\">NAME</a>";
            }
        }
    }

    my $tableHref;
    if (defined($file_link) &&
        !$main::no_sourceview &&
        $main::show_tla       &&
        defined($fileDetails)) {

        $tableHref = "href=\"$file_link#L__LINE__\"";
        # href to anchor in frame doesn't seem to work in either firefox
        #   or chrome.  However, this seems like the right syntax.
        $tableHref = undef    # $tableHref . ' target="source"'
            if $main::frames;
    }
    # First column: name
    my $nameClass =
        $fileSummary->type() eq 'directory' ? 'coverDirectory' : 'coverFile';
    my $prefix;
    if ($is_secondary) {
        $prefix = "owner_";
    } else {
        $prefix    = "";
        $nameClass = 'ownerName' if $primary_key ne 'name';
    }
    if ($is_secondary &&
        (   $primary_key eq 'name' ||
            ($primary_key ne 'name' &&
                $fileview == 0))
    ) {
        if ($fileview == 0 &&
            $primary_key ne 'name' &&
            $main::flat) {
            # link to (flat) file detail
            my $relDir = $fileSummary->relativeDir();
            die("relative directory not set") unless $relDir;
            $namecode =
                $suppressFileHref ? $obj_name :
                ("<a href=\"" .
                    File::Spec->catfile($relDir, "$filename.gcov.$html_ext") .
                    "\" title=\"Click to go to $esc_name source detail\">$obj_name</a>"
                );
        } else {
            # link to the entry in date/owner 'summary' table (keyed other way up)
            $namecode = "<a href=\"";

            my $bin = "";
            if ($fileview == 0 &&
                $primary_key ne 'name') {
                $namecode .= $filename . $lcovutil::dirseparator;
                $bin = $cbData if defined($cbData);
            }
            $namecode .= "index";
            my $help;
            if ($page_type eq 'owner') {
                $bin = $esc_name
                    if ($primary_key eq 'name');
                $namecode .= "-bin_owner." . $html_ext . "#N" . escape_id($bin);
                $help = "owner $bin";
            } elsif ($page_type eq 'date') {
                $bin = $SummaryInfo::ageHeaderToBin{$name}
                    if ($primary_key eq 'name');
                $namecode .= "-bin_date." . $html_ext . "#B$bin";
                $help =
                    "the period '" . $SummaryInfo::ageGroupHeader[$bin] . "'";
            } else {
                die("unexpected type '$page_type'");
            }
            $nameClass = 'ownerName' if $primary_key eq 'name';
            $namecode .=
                "\" title=\"go to coverage summary for $help\">$obj_name</a>";
        }
    }
    write_html($handle, "            <tr>\n");
    my $elide_note = '';
    if ($is_secondary == 2) {
        die("unexpected cbData for $page_type, $primary_key, $name, " .
            (defined($cbData) ? $cbData : 'undef'))
            unless ($page_type eq 'owner' ||
                    ($page_type eq 'date' &&
                     ((defined($cbData) &&
                       $cbData <= $#SummaryInfo::ageGroupHeader) ||
                      (!defined($cbData) &&
                        exists($SummaryInfo::ageHeaderToBin{$name})))));
        my $entry;
        if ($primary_key eq 'name') {
            die("source_link undefined: "
                    .
                    (
                    defined($fileSummary) ?
                        (
                        $fileSummary->type() . " $name:" . $fileSummary->name())
                    :
                        "")) unless defined($source_link);
            $entry = $source_link;
        } else {
            # select the appropriate bin...
            my $id =
                defined($cbData) ?
                ($page_type eq 'owner' ? $cbData :
                     $SummaryInfo::ageGroupHeader[$cbData]) :
                $name;
            # need an anchor here - this is the destination of an HREF from
            #  the header 'owner' or 'date' summary table
            $entry = '<a id="';
            if ($page_type eq 'owner') {
                $entry .= 'N' . $id;
            } else {
                die("unexpected page type $page_type")
                    unless $page_type eq 'date';
                $entry .= 'B' . $SummaryInfo::ageHeaderToBin{$id};
            }
            $entry .= "\">$id</a>";
        }
        my $class =
            $primary_key eq 'name' ?
            ($fileSummary->type() eq 'directory' ? 'coverDirectory' :
                 'coverFile') :
            'ownerName';
        write_html($handle, "              <td class=\"$class\">$entry</td>\n");
        $elide_note = '<sup>&lowast;&lowast;</sup>';
    }
    my $span = (1 == $rowspan) ? "" : " rowspan=$rowspan";
    write_html($handle,
     "              <td class=\"$nameClass\"$span>$namecode$elide_note</td>\n");

    # no 'owner' column if the entire directory is not part of the project
    #  (i.e., no files in this directory are in the repo)
    if ((defined($showBinDetailColumn) && $dirSummary->hasOwnerInfo()) ||
        (defined($primary_key)         &&
            $primary_key ne 'name' &&
            !$is_secondary)
    ) {
        $anchor =~ s/NAME/Total/;
        $anchor .= "<sup>$asterisk</sup>"
            if defined($asterisk);
        write_html($handle, <<END_OF_HTML);
              <td class="overallOwner">$anchor</td>
END_OF_HTML
    }
    foreach my $entry (@entries) {
        my ($found, $hit, $med, $hi, $graph, $summary, $covType) = @{$entry};
        my $bar_graph;
        my $class;
        my $rate;

        # Generate bar graph if requested
        if ($graph) {
            if (!$is_secondary) {
                $class     = $prefix . 'coverBar';
                $bar_graph = get_bar_graph_code($base_dir, $found, $hit);
            } else {
                # graph is distracting for the second-level elements - skip them
                $bar_graph = "";
                $class     = 'coverFile';
            }
            write_html($handle, <<END_OF_HTML);
              <td class="$class" align="center">
                $bar_graph
              </td>
END_OF_HTML
        }
        # Get rate color and text
        if ($found == 0) {
            $rate  = "-";
            $class = "Hi";
        } else {
            $rate  = rate($hit, $found, "&nbsp;%");
            $class = $rate_name[classify_rate($found, $hit, $med, $hi)];
        }
        # Show negative number of items without coverage
        $hit -= $found    # negative number
            if ($main::opt_missed);

        write_html($handle, <<END_OF_HTML);
              <td class="${prefix}coverPer$class">$rate</td>
END_OF_HTML
        if ($summary) {
            my @keys = ("found");
            if ($main::show_hitTotalCol) {
                push(@keys, $opt_missed ? "missed" : "hit");
            }
            if ($main::show_tla) {
                push(@keys, @{$activeTlaCols->{$covType}});
            }
            foreach my $key (@keys) {
                next if grep(/$key/, ('EUC', 'ECC'));
                my $count = $summary->get($key);
                #print("$name: $key " . $summary->get($key));
                $class = $page_type ne "owner" ? "coverNumDflt" : "ownerTla";
                my $v = "";
                if (defined($count) && 0 != $count) {
                    $count = -$count if $key eq 'missed';
                    $v     = $count;
                    # want to colorize the UNC, LBC, UIC rows if not zero
                    $class = "tla$key"
                        if (!$main::use_legacyLabels &&
                            grep(/^$key$/, ("UNC", "LBC", "UIC")));

                    my $column = '';
                    # look in file details to build link to first line
                    # '$tableHref is undef unless the data we need is available
                    if (defined($tableHref) &&
                        !grep(/^$key$/, ('DUB', 'DCB')) &&
                        grep(/^$key$/, @SummaryInfo::tlaOrderNoExclude)) {
                        my $line;
                        my $label =
                            $main::use_legacyLabels ?
                            $SummaryInfo::tlaToLegacySrcLabel{$key} :
                            $key;
                        # need to keep $fileDetails object so we know the
                        #  'first location in ...' for hyperlink from the
                        #  file table to the line in the file
                        my $title = "\"Go to first $label " .
                            SummaryInfo::type2str($covType) . ' ';
                        if ('fileOrDir' eq $page_type) {
                            # go to first line of the indicated type in the file
                            $line = $fileDetails->nextTlaGroup($key)
                                if $covType == SummaryInfo::LINE_DATA;
                            $line = $fileDetails->nextBranchTlaGroup($key)
                                if $covType == SummaryInfo::BRANCH_DATA;
                            $line = $fileDetails->nextMcdcTlaGroup($key)
                                if $covType == SummaryInfo::MCDC_DATA;
                        } elsif ('owner' eq $page_type) {
                            my $owner = $summary->owner();
                            $column = $owner;
                            $title .= "in '$owner' bin ";
                            $line = $fileDetails->nextInOwnerBin($owner, $key)
                                if $covType == SummaryInfo::LINE_DATA;
                            $line =
                                $fileDetails->nextBranchInOwnerBin($owner, $key)
                                if $covType == SummaryInfo::BRANCH_DATA;
                            $line =
                                $fileDetails->nextMcdcInOwnerBin($owner, $key)
                                if $covType == SummaryInfo::MCDC_DATA;
                        } elsif ('date' eq $page_type) {
                            my $agebin = $summary->bin();
                            $column = $agebin;
                            $title .=
                                "in '$SummaryInfo::ageGroupHeader[$agebin]' bin ";
                            $line = $fileDetails->nextInDateBin($agebin, $key)
                                if $covType == SummaryInfo::LINE_DATA;
                            $line =
                                $fileDetails->nextBranchInDateBin($agebin, $key)
                                if $covType == SummaryInfo::BRANCH_DATA;
                            $line =
                                $fileDetails->nextMcdcInDateBin($agebin, $key)
                                if $covType == SummaryInfo::MCDC_DATA;
                        } else {
                            die("unexpected page detail type '$page_type'");
                        }
                        $title .= "in $filename\"";
                        if ($class eq "tla$key") {
                            # add another CSS style to set the TLA-specific background
                            #  color
                            $class .= " tlaBg$key";
                        }
                        if (defined($line)) {
                            my $href = $tableHref;
                            $href =~ s/__LINE__/$line/;
                            $v = "<a $href ";
                            if ($class =~ /tlaBg/) {
                                $v .= "class=\"tlaBg$key\" ";
                            }
                            $v .= "title=$title>$count</a>";
                        } elsif ($covType != SummaryInfo::FUNCTION_DATA &&
                                 (defined($tableHref) ||
                                  (defined($file_link) &&
                                    defined($fileDetails) &&
                                    $main::show_tla &&
                                    !$main::no_sourceview))
                        ) {
                            # LCOV_UNREACHABLE_START
                            #debugging:  why is link missing?
                            lcovutil::ignorable_error(
                                $lcovutil::ERROR_INTERNAL,
                                "undefined line for $filename $name $page_type page $column type $key $covType"
                                    .
                                    (defined($tableHref) ? '' : ' missing href')
                            );
                            #LCOV_UNREACHABLE_STOP
                        }
                    }
                }    # if count nonzero
                write_html($handle, <<END_OF_HTML);
              <td class="$class">$v</td>
END_OF_HTML
            }    # foreach key

        } else {
            write_html($handle, <<END_OF_HTML);
              <td class="${prefix}coverNum$class">$hit / $found</td>
END_OF_HTML
        }
    }
    # End of row
    write_html($handle, <<END_OF_HTML);
            </tr>
END_OF_HTML
}

#
# write_file_table_detail_entry(filehandle, base_dir, test_name, bin_type, activeTlaCols, ([found, hit], ...))
#
# Write entry for detail section in file table.
#

sub write_file_table_detail_entry(*$$$$@)
{
    my ($handle, $base_dir, $test, $showBinDetail, $activeTlaCols, @entries) =
        @_;

    if ($test eq "") {
        $test = "<span style=\"font-style:italic\">&lt;unnamed&gt;</span>";
    } elsif ($test =~ /^(.*),diff$/) {
        $test = $1 . " (converted)";
    }
    # Testname
    write_html($handle, <<END_OF_HTML);
            <tr>
              <td class="testName" colspan=2>$test</td>
END_OF_HTML
    # Test data
    foreach my $entry (@entries) {
        my ($found, $hit, $covtype, $callback) = @{$entry};
        my $rate = rate($hit, $found, "&nbsp;%");
        if (SummaryInfo::LINE_DATA == $covtype &&
            defined($showBinDetail)) {
            write_html($handle, "    <td class=\"testPer\"></td>\n");
        }
        write_html($handle, "    <td class=\"testPer\">$rate</td>\n");
        write_html($handle, "              <td class='testNum'>$found</td>\n");
        if ($main::show_hitTotalCol) {
            write_html($handle,
                       "              <td class='testNum'>$hit</td>\n");
        }
        if ($main::show_tla) {
            foreach my $tla (@{$activeTlaCols->{$covtype}}) {
                my $count = $callback->count($tla);
                $count = "" if 0 == $count;
                write_html($handle, "    <td class=coverNumDflt>$count</td>\n");
            }
        }
    }
    write_html($handle, "    </tr>\n");
}

#
# write_file_table_epilog(filehandle)
#
# Write end of file table HTML code.
#

sub write_file_table_epilog(*)
{
    # *************************************************************
    write_html($_[0], <<END_OF_HTML);
          </table>
          </center>
          <br>

END_OF_HTML
}

#
# write_test_table_prolog(filehandle, table_heading)
#
# Write heading for test case description table.
#

sub write_test_table_prolog(*$)
{
    # *************************************************************

    write_html($_[0], <<END_OF_HTML);
          <center>
          <table width="80%" cellpadding=2 cellspacing=1 border=0>

            <tr>
              <td class="tableHead">$_[1]</td>
            </tr>

            <tr>
              <td class="testDescription">
                <dl>
END_OF_HTML

    # *************************************************************
}

#
# write_test_table_entry(filehandle, test_name, test_description)
#
# Write entry for the test table.
#

sub write_test_table_entry(*$$)
{
    # *************************************************************
    my $name = escape_id($_[1]);

    write_html($_[0], <<END_OF_HTML);
          <dt>$_[1]<a id="T$name">&nbsp;</a></dt>
          <dd>$_[2]<br><br></dd>
END_OF_HTML

    # *************************************************************
}

#
# write_test_table_epilog(filehandle)
#
# Write end of test description table HTML code.
#

sub write_test_table_epilog(*)
{
    # *************************************************************

    write_html($_[0], <<END_OF_HTML);
                </dl>
              </td>
            </tr>
          </table>
          </center>
          <br>

END_OF_HTML

    # *************************************************************
}

sub fmt_centered($$)
{
    my ($width, $text) = @_;
    my $w0 = length($text);
    my $w1 = $width > $w0 ? int(($width - $w0) / 2) : 0;
    my $w2 = $width > $w0 ? $width - $w0 - $w1 : 0;

    return (" " x $w1) . $text . (" " x $w2);
}

#
# write_source_prolog(filehandle)
#
# Write start of source code table.
#

sub write_source_prolog(*$$$)
{
    my ($handle, $fileHasProjectData, $showBranches, $showMcdc) = @_;
    my $lineno_heading = " " x 9;
    my $branch_heading = "";
    my $mcdc_heading   = "";
    my $tlaWidth       = 4;
    my $age_heading    = "";
    my $owner_heading  = "";
    my $tla_heading    = "";

    if (defined($main::show_dateBins) &&
        $fileHasProjectData) {
        $age_heading   = fmt_centered(5, "Age");
        $owner_heading = fmt_centered(20, "Owner");
    }
    if (defined($main::show_tla)) {
        $tla_heading = fmt_centered($tlaWidth, "TLA");
    }
    my $line_heading   = fmt_centered($line_field_width, "Line data");
    my $source_heading = " Source code";

    if ($showBranches) {
        $branch_heading = fmt_centered($br_field_width, "Branch data") . " ";
    }
    if ($showMcdc) {
        $mcdc_heading = fmt_centered($mcdc_field_width, "MC/DC data") . " ";
    }

    # *************************************************************

    write_html($handle, <<END_OF_HTML);
          <table cellpadding=0 cellspacing=0 border=0>
            <tr>
              <td><br></td>
            </tr>
            <tr>
              <td>
<pre class="sourceHeading">${age_heading} ${owner_heading} ${lineno_heading}${branch_heading}${mcdc_heading}${tla_heading}${line_heading} ${source_heading}</pre>
<pre class="source">
END_OF_HTML

    # *************************************************************
}

#
# get_block_len(block)
#
# Calculate total text length of all branches in a block of branches.
#

sub get_block_len($)
{
    my ($block) = @_;
    my $len = 0;

    foreach my $branch (@{$block}) {
        $len += $branch->[$BR_LEN];
    }
    return $len;
}

#
# get_block_list(brdata | mcdc_data)
#
# common method for branch and MC/DC records
# Group elements that belong to the same basic record
#
# Returns: [block1, block2, ...] <- in order of increasing block number/group size
# block:   [branch1, branch2, ...] <- in order of increasing branch/expression index
# branch:  [block_num, branch_num, taken_count, text_length, open, close]
#

sub get_block_list($)
{
    my $data = shift;
    return () unless defined($data);

    my @blocks;

    if ('BranchLocation' eq ref($data)) {

        my $block = [];
        my $last_blkId;

        # Group branches
        foreach my $blk ($data->blocks(1)) {
            my $blockData = $blk->elements();
            my $blkId     = $blk->idx();
            my $branch    = 0;
            foreach my $br_elem (@$blockData) {

                if (defined($last_blkId) && $blkId ne $last_blkId) {
                    push(@blocks, $block);
                    $block = [];
                }
                my $br = [$blkId, $branch, $br_elem, 3, 0, 0];
                push(@{$block}, $br);
                $last_blkId = $blkId;
                ++$branch;
            }
        }
        push(@blocks, $block) if (scalar(@{$block}) > 0);
    } else {
        die('unexpected ' . ref($data)) unless 'MCDC_Block' eq ref($data);
        # Group MC/DC groups
        foreach my $groupSize (sort keys %{$data->groups()}) {
            my $exprs = $data->groups()->{$groupSize};
            my $block = [];
            push(@blocks, $block);
            foreach my $expr (@$exprs) {
                # display 'true' then 'false' sense - to match what we do for branches
                # and to match behaviour of ?: operator
                foreach my $sense (1, 0) {
                    my $cond =
                        [$groupSize, $expr->index(), $sense, $expr, 3, 0, 0];
                    push(@{$block}, $cond);
                }
            }
        }
    }
    # Add braces to first and last branch in group
    foreach my $block (@blocks) {
        $block->[0]->[$BR_OPEN] = 1;
        $block->[0]->[$BR_LEN]++;
        $block->[$#$block]->[$BR_CLOSE] = 1;
        $block->[$#$block]->[$BR_LEN]++;
    }
    return @blocks;
}

# distribute blocks into lines - trying to keep groups on a single line,
# if possible
sub distribute_blocks($$)
{
    my ($blocks, $field_width) = @_;

    my $line_len = 0;
    my $line     = [];    # [branch2|" ", branch|" ", ...]
    my @lines;            # [line1, line2, ...]
    my @result;

    # Distribute blocks to lines
    foreach my $block (@$blocks) {
        my $block_len = get_block_len($block);

        # Does this block fit into the current line?
        if ($line_len + $block_len <= $field_width) {
            # Add it
            $line_len += $block_len;
            push(@{$line}, @{$block});
            next;
        } elsif ($block_len <= $field_width) {
            # It would fit if the line was empty - add it to new line
            push(@lines, $line);
            $line_len = $block_len;
            $line     = [@{$block}];
            next;
        }
        my $split = ($block_len / $field_width) > 2;
        # this block is going to take up a bunch of lines -
        # add it to a new line

        # Split the block into several lines
        foreach my $branch (@{$block}) {
            if ($line_len + $branch->[$BR_LEN] >= $field_width ||
                $split) {
                # Start a new line
                if (($line_len + 1 <= $field_width) &&
                    scalar(@{$line}) > 0 &&
                    !$line->[scalar(@$line) - 1]->[$BR_CLOSE]) {
                    # Try to align branch symbols to be in
                    # one # row
                    push(@{$line}, " ");
                }
                push(@lines, $line);
                $line_len = 0;
                $line     = [];
                $split    = 0;
            }
            push(@{$line}, $branch);
            $line_len += $branch->[$BR_LEN];
        }
    }
    push(@lines, $line);

    return @lines;
}

#
# get_branch_html(brdata, printCallbackStruct)
#
# Return a list of HTML lines which represent the specified branch coverage
# data in source code view.
#

sub get_branch_html($$)
{
    my ($brdata, $cbdata) = @_;
    my $differentialBranch;
    my $fileDetail = $cbdata->sourceDetail();
    if (defined($main::show_tla)) {
        my $lineNo   = $cbdata->lineNo();
        my $lineData = $cbdata->lineData()->line($lineNo);
        $differentialBranch = $lineData->differential_branch()
            if defined($lineData);
    }
    # build the 'blocks' array from differential data if we have it..
    my @blocks = get_block_list(
                  defined($differentialBranch) ? $differentialBranch : $brdata);

    my @lines = distribute_blocks(\@blocks, $br_field_width);

    # Convert to HTML
    #  branch and MC/DC code is similar - but merging only makes things
    #  more complicated as the details of the text, popups, etc are different
    # maybe revisit later
    my @result;
    my %tlaLinks;

    foreach my $line (@lines) {
        my $current     = "";
        my $current_len = 0;

        foreach my $branch (@$line) {
            # Skip alignment space
            if ($branch eq " ") {
                $current .= " ";
                $current_len++;
                next;
            }
            my ($block_num, $br_num, $br, $len, $open, $close) = @{$branch};
            my $class;
            my $prefix;
            my ($tla, $base_count);
            unless (defined($differentialBranch)) {
                # vanilla case - no differential coverage info
                if ($br->data() eq '-') {
                    $class = "tlaUNC";
                } elsif ($br->data() == 0) {
                    $class = "tlaUNC";
                } else {
                    $class = 'tlaGBC';
                }
                $prefix = '';
            } else {
                $tla        = $br->tla;
                $base_count = ($br->diff_count())[0];
                $class      = "tla$tla";
                my $label =
                    $main::use_legacyLabels ?
                    $SummaryInfo::tlaToLegacySrcLabel{$tla} :
                    $tla;
                $prefix = $label . ": ";
            }
            my ($char, $title);

            my $br_name = $br->type_name();
            $br_name .= ' ' if $br_name;
            $br_name .= 'branch ';
            $br_name .=
                defined($br->expr()) ? '"' . $br->expr() . '"' : $br_num;
            my $taken = $br->data();
            if ($br->is_excluded()) {
                $class = 'tla' . ($br->count() ? 'ECC' : 'EUC');
                $char  = 'x';
                $title = "$br_name marked unreachable";
            } elsif ($taken eq '-') {
                $char  = "#";
                $title = "${prefix}$br_name was not executed";
                $title .=
                    " (previously taken $base_count time" .
                    (1 == $base_count ? '' : 's') . ')'
                    if (defined($tla) &&
                        ($tla eq 'LBC' || $tla eq 'ECB'));
            } elsif ($taken == 0) {
                $char  = "-";
                $title = "${prefix}$br_name was not taken";
                $title .=
                    " (previously taken $base_count time" .
                    (1 == $base_count ? '' : 's') . ')'
                    if (defined($tla) &&
                        ($tla eq 'LBC' || $tla eq 'ECB'));
            } else {
                $char  = "+";
                $title = "${prefix}$br_name was taken $taken time" .
                    (($taken > 1) ? "s" : "");
            }
            $title = escape_html($title) if defined($br->expr());
            $current .= "[" if ($open);

            if (!$main::no_sourceview &&
                !$br->is_excluded() &&
                defined($differentialBranch)) {
                my $href;
                if (exists($tlaLinks{$tla})) {
                    $href = $tlaLinks{$tla};
                } else {
                    my $line = $differentialBranch->line();
                    my $next = $fileDetail->nextBranchTlaGroup($tla, $line);
                    $href =
                        "<a href=\"#" .
                        (defined($next) ? "L$next" : 'top') .
                        "\" title=\"TITLE\" class=\"branchTla\">$char</a>";
                    $tlaLinks{$tla} = $href;
                }
                $href =~ s#TITLE#$title#;
                $current .= "<span class=\"$class\"> $href </span>";
            } else {
                $current .=
                    "<span class=\"$class\" title=\"$title\"> $char </span>";
            }
            $current .= "]" if ($close);
            $current_len += $len;
        }

        # Right-align result text
        if ($current_len < $br_field_width) {
            $current = (" " x ($br_field_width - $current_len)) . $current;
        }
        push(@result, $current);
    }

    return @result;
}

sub get_mcdc_html($$)
{
    # @todo can be shared with branch, MC/DC
    my ($mcdc_data, $cbdata) = @_;
    my $differentialMcdc;
    my $fileDetail = $cbdata->sourceDetail();
    if (defined($main::show_tla)) {
        my $lineNo   = $cbdata->lineNo();
        my $lineData = $cbdata->lineData()->line($lineNo);
        $differentialMcdc = $lineData->differential_mcdc()
            if defined($lineData);
    }
    # build the 'blocks' array from differential data if we have it..
    my @blocks = get_block_list(
                   defined($differentialMcdc) ? $differentialMcdc : $mcdc_data);

    my @lines = distribute_blocks(\@blocks, $mcdc_field_width);

    # Convert to HTML
    #  branch and MC/DC code is similar - but merging only makes things
    #  more complicated as the details of the text, popups, etc are different
    # maybe revisit later
    my @result;
    my %tlaLinks;

    foreach my $line (@lines) {
        my $current     = "";
        my $current_len = 0;

        foreach my $cond (@$line) {
            # Skip alignment space
            unless ('ARRAY' eq ref($cond)) {
                die("unexpected 'cond' entry '$cond'") unless ($cond eq " ");
                $current .= " ";
                $current_len++;
                next;
            }

            my ($groupSize, $exprIdx, $sense, $expr, $len, $open, $close) =
                @$cond;
            die("unexpected expr type") unless ref($expr) eq 'MCDC_Expression';

            my $count = $expr->count($sense);
            my $taken;
            my $class;
            my $prefix;
            my $tla;
            my $base_count;
            if ('ARRAY' ne ref($count)) {
                # vanilla case - no differential coverage info
                die("differential MC/DC coverage but no TLA")
                    if defined($differentialMcdc);
                $taken = $count;
                if ($count == 0) {
                    $class = "tlaUNC";
                } else {
                    $class = 'tlaGBC';
                }
                $prefix = '';
            } else {
                die("differential MC/DC coverage but no TLA")
                    unless defined($differentialMcdc);
                ($tla, $base_count, $taken) = @$count;

                $class = "tla$tla";
                my $label =
                    $main::use_legacyLabels ?
                    $SummaryInfo::tlaToLegacySrcLabel{$tla} :
                    $tla;
                $prefix = $label . ": ";
            }
            my ($char, $title);

            my $expr_name = $expr->expression();
            $title =
                ${prefix} . ($sense ? 'True' : 'False') .
                ' sense of expression "' . $expr_name . '" ';
            if ($expr->parent()->num_groups() > 1) {
                $title .= 'in group "' . $expr->groupSize() . '" ';
            }

            my $excluded = $expr->is_excluded($sense);
            if ($excluded) {
                $char  = 'x';
                $class = 'tla' . ($taken ? 'ECC' : 'EUC');
                $title .= "marked unreachable";
            } elsif (!defined($taken)) {
                $char = '-';
                $title .= 'was dropped';
                $title .=
                    ' (previously ' .
                    (1 == $base_count ? '' : 'not ') . 'sensitized)'
                    if (defined($tla) &&
                        ($tla eq 'LBC' || $tla eq 'ECB'));

            } elsif ($taken == 0) {
                $char = $sense ? 't' : 'f';
                $title .= 'was not sensitized';
                $title .=
                    ' (previously ' .
                    (1 == $base_count ? '' : 'not ') . 'sensitized)'
                    if (defined($tla) &&
                        ($tla eq 'LBC' || $tla eq 'ECB'));
            } else {
                $char = $sense ? 'T' : 'F';
                $title .= 'was sensitized';
            }
            $title = escape_html($title);
            $current .= "[" if ($open);

            if (!$main::no_sourceview &&
                !$excluded &&
                defined($differentialMcdc)) {
                my $href;
                if (exists($tlaLinks{$tla})) {
                    $href = $tlaLinks{$tla};
                } else {
                    my $line = $differentialMcdc->line();
                    my $next = $fileDetail->nextMcdcTlaGroup($tla, $line);
                    $href =
                        "<a href=\"#" .
                        (defined($next) ? "L$next" : 'top') .
                        "\" title=\"TITLE\" class=\"mcdcTla\">SENSE</a>";
                    # cache the href as 'next' lookup is moderately expensive
                    $tlaLinks{$tla} = $href;
                }
                # keep the link but update the sense character
                $href =~ s/SENSE/$char/;
                $href =~ s#TITLE#$title#;
                $current .= "<span class=\"$class\"> $href </span>";
            } else {
                $current .=
                    "<span class=\"$class\" title=\"$title\"> $char </span>";
            }
            $current .= "]" if ($close);
            $current_len += $len;
        }

        # Right-align result text
        if ($current_len < $mcdc_field_width) {
            $current = (" " x ($mcdc_field_width - $current_len)) . $current;
        }
        push(@result, $current);
    }

    return @result;
}

#
# format_count(count, width)
#
# Return a right-aligned representation of count that fits in width characters.
#

sub format_count($$)
{
    my ($count, $width) = @_;
    my $result;
    my $exp;
    my $negative = 0 > $count;
    if ($negative) {
        $width -= 2;
        $count = -$count;
    }

    $result = sprintf("%*.0f", $width, $count);
    while (length($result) > $width) {
        last if ($count < 10);
        $exp++;
        $count  = int($count / 10);
        $result = sprintf("%*s", $width, ">$count*10^$exp");
    }
    if ($negative) {
        # surround number with parens
        $result =~ s/^( *)(\S+)$/$1\($2\)/;
    }
    return $result;
}

#
# write_source_line(filehandle, cbdata, source, hit_count, brdata, mcdc,
#                   printCallbackStruct)
#
# Write formatted source code line. Return a line in a format as needed
# by gen_png()
#

sub write_source_line(*$$$$$$$)
{
    my ($handle, $srcline, $count, $showBranches, $brdata, $showMcdc, $mcdc,
        $cbdata)
        = @_;
    my $line         = $cbdata->lineNo();
    my $fileCovInfo  = $cbdata->lineData();
    my $sourceDetail = $cbdata->sourceDetail();
    my $source       = $srcline->text();
    my $src_owner    = $srcline->owner();
    my $src_age      = $srcline->age();
    my $source_format;
    my $count_format;
    my $result;
    my $anchor_start      = "";
    my $anchor_end        = "";
    my $count_field_width = $line_field_width - 1;
    my $html;
    my $tla;
    my $base_count;
    my $curr_count;
    my $bucket;

    my @prevData = ($cbdata->current('tla'),
                    $cbdata->current('owner'),
                    $cbdata->current('age'));

    # Get branch HTML data for this line
    my @br_html = get_branch_html($brdata, $cbdata) if ($showBranches);

    my @mcdc_html = get_mcdc_html($mcdc, $cbdata) if ($showMcdc);

    my $thisline = $fileCovInfo->lineMap()->{$line};
    my $tlaIsHref;
    if (!defined($thisline) ||
        (!defined($count) &&
            !($thisline->in_base() || $thisline->in_curr()))
    ) {
        $result        = "";
        $source_format = "";
        $count_format  = " " x $count_field_width;
        $tla           = $cbdata->tla(undef, $line);
    } else {
        $base_count = $thisline->base_count();
        $curr_count = $thisline->curr_count();
        $bucket     = $thisline->tla();
        # use callback data to keep track of the most recently seen TLA -
        #   $tla is either "   " (3 spaces) if same as previous or if no TLA
        #   we just stick "TLA " (4 characters) into the fixed-with source
        #   line - right before the count.
        $tla = $cbdata->tla($bucket, $line);
        my $class = "tla$bucket";
        if ($main::show_tla && $tla =~ /$bucket/) {
            # maybe we want to link only the uncovered code categories?
            my $next = $sourceDetail->nextTlaGroup($bucket, $line);
            $tlaIsHref = 1;
            # if no next segment in this category - then go to top to page.
            $next = defined($next) ? "L$next" : "top";
            my $anchorClass = "";
            if ($bucket ne 'UNK') {
                $class .= " tlaBg$bucket";
                $anchorClass = " class=\"tlaBg$bucket\" ";
            }
            my $label =
                $main::use_legacyLabels ?
                $SummaryInfo::tlaToLegacySrcLabel{$bucket} :
                $bucket;
            my $popup = " title=\"Next $label group\"";
            $label .= ' ' x ($main::tla_field_width - length($label));
            $tla = "<a href=\"#$next\"$anchorClass$popup>$label</a>";
        }
        $source_format = "<span class=\"$class\">";

        my $pchar;
        if (exists($lcovutil::pngChar{$bucket})) {
            $pchar = $lcovutil::pngChar{$bucket};
        } else {
            lcovutil::ignorable_error($lcovutil::ERROR_UNKNOWN_CATEGORY,
                                      "unexpected TLA '$bucket'");
            $pchar = '';
            $count = 0 unless defined($count);
        }
        if ($bucket eq "ECB" ||
            $bucket eq "EUB") {
            !defined($count) or
                die("excluded code should have undefined count");
            # show previous count for excluded code that had been hit
            if ($bucket eq "ECB") {
                $count_format = format_count(-$base_count, $count_field_width);
            } else {
                $count_format = " " x $count_field_width;
            }
            $result = $pchar . $base_count;
        } else {
            if (!defined($count)) {
                # this is manufactured data...make something up
                $count = $curr_count;
            }
            defined($count) && "" ne $count or
                die("code should have defined count");
            if ($bucket eq 'LBC') {
                $count_format = format_count(-$base_count, $count_field_width);
            } else {
                $count_format = format_count($count, $count_field_width);
            }
            $result = $pchar . $count;
        }
        # '$result' is used to generate the PNG frame
        info(2,
             "    $bucket: line=$line " .
                 (defined($count) ? "count= $count " : "") . ' curr=' .
                 (defined($curr_count) ? $curr_count : '-') . ' base=' .
                 (defined($base_count) ? $base_count : '-') . "\n");
    }
    $result .= ":" . $source;

    my $tooltip =
        @SourceFile::annotateScript ? $SourceFile::annotateTooltip : '';
    if ($tooltip ne '') {
        my $lineData = $sourceDetail->line($line);
        my $commitId = $lineData->commit();
        if (defined($commitId) &&
            $commitId ne 'NONE') {
            my $date = $lineData->date();
            $date =~ s/(T.+)$//;    # just the year/month/day part
            foreach my $p (['%U', $lineData->owner()],
                           ['%F', $lineData->full_name()],
                           ['%D', $lineData->date()],
                           ['%d', $date],
                           ['%A', $lineData->age()],
                           ['%C', $commitId],
                           ['%l', $line],
            ) {
                $tooltip =~ s/$p->[0]/$p->[1]/g;
            }
            $tooltip = " title=\"$tooltip\"";
        } else {
            $tooltip = '';
        }
    }
    # Write out a line number navigation anchor every $nav_resolution
    # lines if necessary
    $anchor_start = "<span id=\"L$line\"$tooltip>";
    $anchor_end   = "</span>";

    # *************************************************************

    $html = $anchor_start;
    # we want to colorize the date/owner part of un-hit lines only
    my $html_continuation_leader = "";    # for continued lines
    if (defined($main::show_dateBins) &&
        $sourceDetail->isProjectFile()) {
        DATE_SECTION: {

            my $ageLen   = $main::age_field_width;
            my $ownerLen = $main::owner_field_width;

            # need to space over on continuation lines
            $html_continuation_leader = ' ' x ($ageLen + $ownerLen + 2);

            if (!defined($count) &&
                (!defined($main::show_nonCodeOwners) ||
                    0 == $main::show_nonCodeOwners)
                &&
                (   !defined($bucket) ||
                    ($bucket ne 'EUB' &&
                        $bucket ne 'ECB'))
            ) {
                # suppress date- and owner entry on non-code lines
                $html .= $html_continuation_leader;
                last DATE_SECTION;
            }

            my $span    = "";
            my $endspan = "";
            my $bgclass = "";
            if (defined($count) && 0 == $count) {
                # probably want to pick a color based on which
                #   date bin it is in.
                # right now, picking based on TLA.
                $bgclass = " class=\"tlaBg$bucket\""
                    if (defined($bucket) &&
                        $bucket ne "EUB" &&
                        $bucket ne "ECB");

                #$html .= "<span class=\"missBins\">";
                if ("" ne $source_format) {
                    OWNER: {
                        if (!defined($src_owner) || !defined($src_age)) {
                            # maybe this should be a different error type?
                            lcovutil::ignorable_error(
                                      $lcovutil::ERROR_UNMAPPED_LINE,
                                      "undefined owner/age for $bucket $line " .
                                          $sourceDetail->path());
                            last OWNER;
                        }
                        # add a 'title="tooltip"' popup - to give owner, date, etc
                        my $title = "span title=\"$src_owner $src_age days ago";
                        if (defined($main::show_dateBins)) {
                            my $bin = SummaryInfo::findAgeBin($src_age);
                            $title .=
                                " (bin $bin: " .
                                $SummaryInfo::ageGroupHeader[$bin] . ")";
                        }
                        $title .= "\"";
                        $span = $source_format;
                        $span =~ s/span/$title/;
                        $endspan = "</span>";
                    }
                }
            }    # OWNER block

            # determine if this line is going to be the target of a 'date', 'owner'
            # or TLA navigation href.
            #  - is is possible for any of these to have changed from the previous
            #    line, even if the others are unchanged:
            #      - same TLA but different author
            #      - same author but different date bin, .. and so on
            # If it is a leader, then we need to insert the 'owner' and/or 'date'
            #   link to go to the next group in this bin - even if the owner or
            #   date bin has not changed from the previous line)
            my $tlaChanged = defined($bucket) && $prevData[0] ne $bucket;

            my $needOwnerHref = @SourceFile::annotateScript &&
                ($tlaChanged ||
                 (defined($bucket) && $prevData[1] ne $src_owner));

            my $newBin = SummaryInfo::findAgeBin($src_age);
            defined($prevData[2]) || $line == 1 or
                die("unexpected uninitialized age");

            my $needDateHref = (
                           $tlaChanged || (defined($bucket) &&
                               SummaryInfo::findAgeBin($prevData[2]) != $newBin)
            );

            if ($needDateHref) {
                my $matchLine = $cbdata->nextDate($newBin, $bucket);
                $needDateHref = 0
                    if (defined($matchLine) && $matchLine != $line);
            }
            if ($needOwnerHref) {
                # slightly complicated logic:
                #   - there can be non-code lines owned by someone else
                #     between code lines owned by '$src_owner', such that the
                #     all the code lines have the same TLA.
                # In that case, we just insert an href at the top of the
                # block to take us past all of them - to the next code block
                # owned by $src_owner with this TLA, which separated by at least
                # one line of code either owned by someone else, or with a different
                # TLA.
                my $matchLine = $cbdata->nextOwner($src_owner, $bucket);
                # don't insert the owner href if this isn't the line we wanted
                $needOwnerHref = 0
                    if (defined($matchLine) && $matchLine != $line);
            }

            my $age   = $cbdata->age($src_age, $line);
            my $owner = $cbdata->owner($src_owner);

            # this HTML block turns into
            #   "<span ...>int name</span> " <- note trailing space
            #  .. but the age and name might be hrefs

            $html .= $span;
            # then 5 characters of 'age' (might be empty)
            if ($needDateHref) {
                # next line with this TLA, in this date bin
                my $next =
                    $sourceDetail->nextInDateBin($newBin, $bucket, $line);
                $cbdata->nextDate($newBin, $bucket, $next);

                $next = defined($next) ? "L$next" : "top";
                my $dateBin = $SummaryInfo::ageGroupHeader[$newBin];
                my $label =
                    $main::use_legacyLabels ?
                    $SummaryInfo::tlaToLegacySrcLabel{$bucket} :
                    $bucket;
                my $popup =
                    " title=\"next $label in &ldquo;$dateBin&rdquo; bin\"";
                $html .= ((' ' x ($ageLen - length($src_age))) .
                          "<a href=\"#$next\"$popup$bgclass>$src_age</a> ");
            } else {
                $html .= sprintf("%${ageLen}s ", $age);
            }

            if ($needOwnerHref) {
                # next line with this TLA, by this owner..
                my $next =
                    $sourceDetail->nextInOwnerBin($src_owner, $bucket, $line);
                $cbdata->nextOwner($src_owner, $bucket, $next);
                $next = defined($next) ? "L$next" : "top";
                my $label =
                    $main::use_legacyLabels ?
                    $SummaryInfo::tlaToLegacySrcLabel{$bucket} :
                    $bucket;
                my $popup =
                    " title=\"next $label in &ldquo;" .
                    escape_html($src_owner) . "&rdquo; bin\"";
                # NOTE:  see note below about firefox nested span bug.
                #  this code just arranges to wrap an explicit 'span' around
                #  the space.
                my $nspace = $ownerLen - length($src_owner);
                my $space  = $nspace > 0 ? (' ' x $nspace) : '';
                my $href   = "<a href=\"#$next\"$popup$bgclass>" .
                    escape_html(substr($src_owner, 0, $ownerLen)) . '</a>';
                if ('' ne $bgclass &&
                    '' ne $space) {
                    $html .= "$endspan$href$span";
                } else {
                    $html .= $href;
                }
                $html .= $space;
            } else {
                $html .= sprintf("%-${ownerLen}s", $owner);
            }
            $html .= $endspan . ' ';    # add trailing space
        }
    }    # DATE_SECTION

    # use a different HTML tag if there are deleted lines here
    my $deletedLines = $fileCovInfo->deletedLineData($line);
    my $lineNumTag   = 'lineNum';
    my $lineNumTitle = '';
    if (defined($deletedLines)) {
        my $first = $deletedLines->[0];
        my $last  = $deletedLines->[1];
        die("invalid deleted lines array") unless $first <= $last;
        my $count = $last - $first + 1;
        $lineNumTitle = ' title="'
            .
            (($first != $last) ?
                 "removed $count lines ([$first:$last] inclusive)" :
                 "removed line $first") .
            ' from baseline version"';
        $lineNumTag = 'lineNumWithDelete';
    }
    my $lineNumSpan = "<span class=\"$lineNumTag\"$lineNumTitle>";

    $html .= sprintf("$lineNumSpan%8d</span> ", $line);
    #printf("tla= " . $tla);
    #printf("html= " . $html);

    $html .= shift(@br_html) . ":" if ($showBranches);
    $html .= shift(@mcdc_html) . ":" if ($showMcdc);

    $tla = ""
        if (!defined($main::show_tla));

    # 'source_format is the colorization, then the 3-letter TLA,
    #    then the hit count, then the source line
    if ($tlaIsHref) {
        # there seems to be a bug in firefox:
        #      <span class="foo"><a href...>link</a> whatever</span>
        #   renders as if the 'span' didn't exist (so the colorization of the
        #   link end - and the rest of the line doesn't pick up attributes
        #   from class 'foo'.
        # If I emit it as:
        #     <a href...>link</a><span ....> wheatever</span>
        #   (i.e., do not nest anchor inside the span) - then it works
        $html .= "$tla$source_format $count_format : ";
    } else {
        $html .= "$source_format$tla $count_format : ";
    }

    $html .= escape_html($source);
    $html .= "</span>" if ($source_format);
    $html .= $anchor_end . "\n";

    write_html($handle, $html);

    # Add lines for overlong branch and/or MC/DC information
    while (@br_html || @mcdc_html) {
        my $br = @br_html ? shift(@br_html) : '';
        my $mc = @mcdc_html ? shift(@mcdc_html) : '';
        if ($mc) {
            # space over far enough to line up with MC/DC extension column
            # remove the span and other HTML
            my $s = $br;
            foreach my $tag ('a', 'span') {
                $s =~ s/(<\/$tag>|<$tag.+?>)//g;
            }
            $br .= ' ' x ($br_field_width - length($s)) . ' ';
            die("unexpected lengths") unless $br_field_width >= length($s);
        }
        write_html($handle,
                   "$html_continuation_leader$lineNumSpan" .
                       ' ' x 8 . "</span> $br$mc\n");
    }
    # *************************************************************

    return ($result);
}

#
# write_source_epilog(filehandle)
#
# Write end of source code table.
#

sub write_source_epilog(*)
{
    # *************************************************************

    write_html($_[0], <<END_OF_HTML);
        </pre>
              </td>
            </tr>
          </table>
          <br>

END_OF_HTML

    # *************************************************************
}

#
# write_html_epilog(filehandle, base_dir[, break_frames])
#
# Write HTML page footer to FILEHANDLE. BREAK_FRAMES should be set when
# this page is embedded in a frameset, clicking the URL link will then
# break this frameset.
#

sub write_html_epilog(*$;$)
{
    my $basedir    = $_[1];
    my $break_code = "";
    my $epilog;

    if (defined($_[2])) {
        $break_code = " target=\"_parent\"";
    }
    my $f =
        defined($main::footer) ? $footer :
        "Generated by: <a href=\"$lcov_url\"$break_code>$lcov_version</a>";

    # *************************************************************

    write_html($_[0], <<END_OF_HTML);
          <table width="100%" border=0 cellspacing=0 cellpadding=0>
            <tr><td class="ruler"><img src="$_[1]glass.png" width=3 height=3 alt=""></td></tr>
            <tr><td class="versionInfo">$f</td></tr>
          </table>
          <br>
END_OF_HTML

    $epilog = $html_epilog;
    $epilog =~ s/\@basedir\@/$basedir/g;

    write_html($_[0], $epilog);
}

#
# write_frameset(filehandle, basedir, basename, pagetitle)
#
#

sub write_frameset(*$$$)
{
    my $frame_width = $overview_width + 40;

    # *************************************************************

    write_html($_[0], <<END_OF_HTML);
        <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN">

        <html lang="en">

        <head>
          <meta http-equiv="Content-Type" content="text/html; charset=$charset">
          <title>$_[3]</title>
          <link rel="stylesheet" type="text/css" href="$_[1]gcov.css">
        </head>

        <frameset cols="$frame_width,*">
          <frame src="$_[2].gcov.overview.$html_ext" name="overview">
          <frame src="$_[2].gcov.$html_ext" name="source">
          <noframes>
            <center>Frames not supported by your browser!<br></center>
          </noframes>
        </frameset>

        </html>
END_OF_HTML

    # *************************************************************
}

#
# sub write_overview_line(filehandle, basename, line, link)
#
#

sub write_overview_line(*$$$)
{
    my $y1 = $_[2] - 1;
    my $y2 = $y1 + $nav_resolution - 1;
    my $x2 = $overview_width - 1;

    # *************************************************************

    write_html($_[0], <<END_OF_HTML);
            <area shape="rect" coords="0,$y1,$x2,$y2" href="$_[1].gcov.$html_ext#L$_[3]" target="source" alt="overview">
END_OF_HTML

    # *************************************************************
}

#
# write_overview(filehandle, basedir, basename, pagetitle, lines)
#
#

sub write_overview(*$$$$)
{
    my $index;
    my $max_line = $_[4] - 1;
    my $offset;

    # *************************************************************

    write_html($_[0], <<END_OF_HTML);
        <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

        <html lang="en">

        <head>
          <title>$_[3]</title>
          <meta http-equiv="Content-Type" content="text/html; charset=$charset">
          <link rel="stylesheet" type="text/css" href="$_[1]gcov.css">
        </head>

        <body>
          <map name="overview">
END_OF_HTML

    # *************************************************************

    # Make $offset the next higher multiple of $nav_resolution
    $offset = ($nav_offset + $nav_resolution - 1) / $nav_resolution;
    $offset = sprintf("%d", $offset) * $nav_resolution;

    # Create image map for overview image
    for ($index = 1; $index <= $_[4]; $index += $nav_resolution) {
        # Enforce nav_offset
        if ($index < $offset + 1) {
            write_overview_line($_[0], $_[2], $index, 1);
        } else {
            write_overview_line($_[0], $_[2], $index, $index - $offset);
        }
    }

    # *************************************************************
    my $f = $lcovutil::case_insensitive ? lc($_[2]) : $_[2];
    write_html($_[0], <<END_OF_HTML);
          </map>

          <center>
          <a href="$f.gcov.$html_ext" target="source">Top</a><br><br>
          <img src="$f.gcov.png" width=$overview_width height=$max_line alt="Overview" border=0 usemap="#overview">
          </center>
        </body>
        </html>
END_OF_HTML

    # *************************************************************
}

sub max($$)
{
    my ($a, $b) = @_;

    return $a if ($a > $b);
    return $b;
}

sub buildDateSummaryTable($$$$$$$$$)
{
    my ($summary, $covType, $covCountCallback,
        $fileDetail, $nextLocationCallback, $title,
        $detailLink, $numRows, $activeTlaList) = @_;

    $title =
        "<a href=\"$detailLink\" title=\"Click to include date bin details in file table below\">$title</a>"
        if (defined($detailLink));

    my @table;

    my @dateSummary = [undef,               #width
                       "subTableHeader",    # class
                       $title,              # text
                       $numRows,            # colspan
    ];
    # only insert the label if there is data
    my $first = 1;
    my $page  = $detailLink;
    if (defined($page)) {
        $page =~ s/^index/index-bin_/;
        $page =~ s/^index-bin_-/index-bin_/;
    }

    my ($lim_med, $lim_high);
    if ($covType == SummaryInfo::LINE_DATA) {
        $lim_med  = $ln_med_limit;
        $lim_high = $ln_hi_limit;
    } elsif ($covType == SummaryInfo::BRANCH_DATA) {
        $lim_med  = $br_med_limit;
        $lim_high = $br_hi_limit;
    } elsif ($covType == SummaryInfo::MCDC_DATA) {
        $lim_med  = $mcdc_med_limit;
        $lim_high = $mcdc_hi_limit;
    } elsif ($covType == SummaryInfo::FUNCTION_DATA) {
        $lim_med  = $fn_med_limit;
        $lim_high = $fn_hi_limit;
    } else {
        die("unexpected cover type $covType");
    }
    for (my $bin = 0; $bin <= $#SummaryInfo::ageGroupHeader; ++$bin) {
        my $ageval = $summary->age_sample($bin);
        my $found  = &$covCountCallback($summary, "found", "age", $ageval);
        next
            if 0 == $found;
        my $hit = &$covCountCallback($summary, "hit", "age", $ageval);
        my $style =
            $rate_name[classify_rate($found, $hit, $lim_med, $lim_high)];
        my $rate = rate($hit, $found, "&nbsp;%");
        my $href = $SummaryInfo::ageGroupHeader[$bin];
        if (defined($detailLink)) {
            $href =
                "<a href=\"$page#B$bin\" title=\"Click to go to coverage summary for the period '$href'\">$href</a>";
        }
        $hit -= $found    # negative number
            if ($main::opt_missed);
        my @dataRow = ([undef, "headerItem", $href . ":"],
                       [undef, "headerCovTableEntry$style", $rate],
                       [undef, "headerCovTableEntry", $found]);
        if ($main::show_hitTotalCol) {
            push(@dataRow, [undef, "headerCovTableEntry", $hit]);
        }
        if ($main::show_tla) {
            for my $tla (@$activeTlaList) {
                my $value = &$covCountCallback($summary, $tla, "age", $ageval);
                my $class =
                    !$main::use_legacyLabels &&
                    0 != $value &&
                    grep(/^$tla$/, ("UNC", "LBC", "UIC")) ? "tla$tla" :
                    "headerCovTableEntry";
                # suppress zeros - make table less busy/easier to read
                if ("0" eq $value) {
                    $value = "";
                } elsif (!$main::no_sourceview &&
                         defined($fileDetail)           &&
                         defined($nextLocationCallback) &&
                         !grep(/^$tla$/, ('DUB', 'DCB'))) {
                    # no link for deleted lines (DUB, DCB) because there
                    # is no TLA for those categories at the location in
                    # the 'current' sourceview.  That location is just a
                    # normal line - maybe "not code", maybe 'CBC' or some
                    # other TLA.

                    # link to first entry
                    my $firstAppearance =
                        &$nextLocationCallback($fileDetail, $bin, $tla);
                    defined($firstAppearance) or
                        die(
                          "$tla: unexpected date bin $bin undef appearance for "
                              . $fileDetail->path());
                    my $dateBin = $SummaryInfo::ageGroupHeader[$bin];
                    my ($label, $color);
                    if ($main::use_legacyLabels) {
                        $label = $SummaryInfo::tlaToLegacy{$tla};
                        $color = "";
                    } else {
                        $label = $tla;
                        $color = " class=\"tlaBg$tla\"";
                    }
                    my $popup =
                        " title=\"goto first $label " .
                        SummaryInfo::type2str($covType) .
                        " in &ldquo;$dateBin&rdquo; bin\"";
                    $value =
                        "<a href=\"#L$firstAppearance\"$popup$color>$value</a>";
                }
                push(@dataRow, [undef, $class, $value]);
            }
        }
        if ($first) {
            push(@table, \@dateSummary);
            $first = 0;
        }
        push(@table, \@dataRow);
    }
    return \@table;
}

sub buildOwnerSummaryTable($$$$$$$$$$)
{
    my ($ownerList, $num_truncated, $summary,
        $covType, $fileDetail, $nextLocationCallback,
        $title, $detailLink, $numRows,
        $activeTlaList) = @_;

    $title .= " (containing " .
        ($main::show_ownerBins ? "" : "un-exercised ") . "code)";
    $title .=
        ":  $num_truncated author" .
        ($num_truncated == 1 ? '' : 's') . ' truncated'
        if $num_truncated;

    $title =
        "<a href=\"$detailLink\" title=\"Click to include ownership details in file table\">$title</a>"
        if (defined($detailLink));

    my $page = $detailLink;
    if (defined($page)) {
        $page =~ s/^index/index-bin_/;
        $page =~ s/^index-bin_-/index-bin_/;
    }

    my @table;
    my @ownerSummary = [undef,               #width
                        "subTableHeader",    # class
                        $title,              # text
                        $numRows,            # colspan
    ];

    my ($lim_med, $lim_high);
    if ($covType == SummaryInfo::LINE_DATA) {
        $lim_med  = $ln_med_limit;
        $lim_high = $ln_hi_limit;
    } elsif ($covType == SummaryInfo::BRANCH_DATA) {
        $lim_med  = $br_med_limit;
        $lim_high = $br_hi_limit;
    } elsif ($covType == SummaryInfo::MCDC_DATA) {
        $lim_med  = $mcdc_med_limit;
        $lim_high = $mcdc_hi_limit;
    } elsif ($covType == SummaryInfo::FUNCTION_DATA) {
        $lim_med  = $fn_med_limit;
        $lim_high = $fn_hi_limit;
    } else {
        die("unexpected cover type $covType");
    }
    my $first = 1;
    # owners are sorted from most uncovered lines to least
    foreach my $od (@$ownerList) {
        my ($name, $lineData, $branchData, $funcData, $mcdcData) = @$od;
        my $d;
        if ($covType == SummaryInfo::LINE_DATA) {
            $d = $lineData;
        } elsif ($covType == SummaryInfo::BRANCH_DATA) {
            $d = $branchData;
        } else {
            die("unexpected type '$covType'")
                unless ($covType == SummaryInfo::MCDC_DATA);
            $d = $mcdcData;
        }
        my ($missed, $found) = @$d;

        # only put user in table if they are responsible for at least one point
        next if $found == 0 or ($missed == 0 && $main::show_ownerBins ne 'all');

        if ($first) {
            $first = 0;
            push(@table, \@ownerSummary);
        }
        my $hit = $found - $missed;

        my $style =
            $rate_name[classify_rate($found, $hit, $lim_med, $lim_high)];
        my $rate = rate($hit, $found, '&nbsp;%');

        my $esc_name = escape_html($name);
        my $href     = $esc_name;
        if (defined($detailLink)) {
            $href =
                "<a href=\"$page#N" . escape_id($name) .
                "\" title=\"Click to go to coverage summary for owner '$esc_name'\">$esc_name</a>";
        }
        $hit -= $found    # negative number
            if ($main::opt_missed);
        my @dataRow = ([undef, "headerItem", $href . ":"],
                       [undef, "owner_coverPer$style", $rate],
                       [undef, "ownerTla", $found]);
        if ($main::show_hitTotalCol) {
            push(@dataRow, [undef, "ownerTla", $hit]);
        }
        if ($main::show_tla) {
            for my $tla (@$activeTlaList) {
                my $value = $summary->owner_tlaCount($name, $tla, $covType);
                # suppress zeros - make table less busy/easier to read
                my $class =
                    !$main::use_legacyLabels &&
                    0 != $value &&
                    grep(/^$tla$/, ("UNC", "LBC", "UIC")) ? "tla$tla" :
                    "ownerTla";
                if ("0" eq $value) {
                    $value = "";
                } elsif (!$main::no_sourceview &&
                         defined($fileDetail) &&
                         defined($nextLocationCallback)) {
                    my $firstAppearance =
                        &$nextLocationCallback($fileDetail, $name, $tla);
                    defined($firstAppearance) or
                        die(
                          "$tla: unexpected owner $name undef appearance for " .
                              $fileDetail->path());
                    my ($label, $color);
                    if ($main::use_legacyLabels) {
                        $label = $SummaryInfo::tlaToLegacy{$tla};
                        $color = "";
                    } else {
                        $label = $tla;
                        $color = " class=\"tlaBg$tla\"";
                    }
                    my $popup =
                        " title=\"goto first $label " .
                        SummaryInfo::type2str($covType) .
                        " in &ldquo;$name&rdquo; bin\"";
                    $value =
                        "<a href=\"#L$firstAppearance\"$popup$color>$value</a>";
                }
                push(@dataRow, [undef, $class, $value]);
            }
        }
        push(@table, \@dataRow);
    }
    return \@table;
}

sub buildHeaderSummaryTableRow
{
    my ($summary, $covType, $fileDetail, $nextLocationCallback, $activeTlaList)
        = @_;

    $fileDetail = undef
        if (!$main::show_tla &&
            (defined($fileDetail) && !$fileDetail->isProjectFile()));
    my @row;
    for my $tla (@$activeTlaList) {
        my $value = $summary->get($tla, $covType);
        # for the moment, no background colorization for non-zero counts
        # in "concerning" categories.  The table header is immediately
        # above - so no need to colorize to draw emphasis - just makes the
        # table busier
        my $showConcerningTla = 0;
        my $class =
            $showConcerningTla       &&
            !$main::use_legacyLabels &&
            0 != $value              &&
            grep(/^$tla$/, ("UNC", "LBC", "UIC")) ? "tla$tla" :
            "headerCovTableEntry";
        if ("0" eq $value) {
            # suppress zeros - make table less busy/easier to read
            $value = "";
        } elsif (!$main::no_sourceview &&
                 defined($fileDetail) &&
                 !($tla eq "DCB" || $tla eq "DUB")) {
            # deleted lines don't appear..
            my $firstAppearance = &$nextLocationCallback($fileDetail, $tla);
            defined($firstAppearance) or
                die(
                "$tla: unexpected undef appearance for " . $fileDetail->path());
            my $label =
                $main::use_legacyLabels ? $SummaryInfo::tlaToLegacy{$tla} :
                $tla;
            my $popup = " title=\"goto first $label " .
                SummaryInfo::type2str($covType) . "\"";
            $value = "<a href=\"#L$firstAppearance\"$popup>$value</a>";
        }
        push(@row, [undef, $class, $value]);
    }
    return @row;
}

# build an HTML string for the directory or file pathname, such
#  that each element is clickable - and takes you to the 'index' file
#  in the corresponding (transitive) parent directory
sub build_html_path($$$$$)
{
    my ($path, $key, $bin_type, $isFile, $isAbsolute) = @_;

    $path =~ s|^/||;
    my @path      = File::Spec->splitdir($path);
    my $html_path = "";
    if ($main::hierarchical &&
        scalar(@path) > 1) {
        pop(@path);    # remove 'self' - at the tail
        my $p   = "";
        my $sep = "";
        # need one fewer "../" entries for file pathname because the
        #  index file we are looking for is in the current directory (i.e.,
        #  not '../index.html'
        my $len = scalar(@path) - $isFile;
        foreach my $elem (@path) {
            my $base = "../" x $len;
            $elem       = $lcovutil::dirseparator . $elem if $isAbsolute;
            $isAbsolute = 0;
            my $e = escape_html($elem);
            $html_path .=
                "$sep<a href=\"${base}index$key$bin_type.$html_ext\" title=\"Click to go to directory $e\">$e</a>";
            $sep = $lcovutil::dirseparator;
            --$len;
        }
    }
    return $html_path;
}

#
# write_header(filehandle, ctrl, trunc_file_name, rel_file_name,
#              summaryInfo, optionalFileDetailInfo, optionalFunctionData))
#  ctrl = (type, primary_key, sort_type, bin_type)
#
# Write a complete standard page header. TYPE may be (0, 1, 2, 3, 4)
# corresponding to (directory view header, file view header, source view
# header, test case description header, function view header)
#
# bin_type in (undef, "", "-owner", "-date")
#  - if 'bin' is set, then create link to 'vanilla' view of self, and
#    to corresponding view of parent
#      - i.e., from 'owner detail' directory page to "owner detail"
#        toplevel, and to my correspondign vanilla directory page.
# return hash of coverType -> list of non-zero TLAs for that type (or to
#        list of all TLA types if user has asked not to suppress all-zero
#        columns)

sub write_header(*$$$$$$$)
{
    local *HTML_HANDLE = shift;
    my ($callback_type, $ctrl, $trunc_name, $rel_filename, $summary,
        $fileDetail, $differentialFunctionMap)
        = @_;
    my ($type, $primary_key, $sort_type, $bin_type) = @$ctrl;
    my $base_dir;
    my $view;
    my @row_left;
    my @row_right;
    my $esc_trunc_name = escape_html($trunc_name);

    $bin_type = ""
        unless defined($bin_type);
    my $base_name     = File::Basename::basename($rel_filename);
    my $show_dateBins = $main::show_dateBins &&
        (!defined($fileDetail) || $fileDetail->isProjectFile());

    my $key = $primary_key ne "name" ? "-bin_$primary_key" : "";
    my $isAbsolutePath = (
                     $summary->is_directory(1) || ($summary->type() eq 'file' &&
                                            $summary->parent()->is_directory(1))
    );
    my $html_path =
        build_html_path($trunc_name, $key, $bin_type,
                        $summary->type() eq 'file',
                        $isAbsolutePath);

    # Prepare text for "current view" field
    if ($type == $HDR_DIR) {
        # Main overview
        $base_dir = "";
        if ($bin_type ne "" ||
            $primary_key ne 'name') {
            # this is the header of the 'top-level' page, for either 'owner'
            #   or 'date' binning - link back to vanilla top-level page
            $view =
                "<a href=\"index.$html_ext\" title=\"Click to go to default view of top-level page.\">$overview_title</a>";
        } else {
            $view = $overview_title;
        }
    } elsif ($type == $HDR_FILE) {
        # Directory overview
        $base_dir = get_relative_base_path($rel_filename);
        my $self_link;
        if ($main::hierarchical) {
            my $base = escape_html(File::Basename::basename($rel_filename));
            if ($base eq $rel_filename &&
                $isAbsolutePath) {
                $base = $lcovutil::dirseparator . $base;
            }
            $self_link = $html_path;
            $self_link .= $lcovutil::dirseparator if ('' ne $html_path);
            if ('name' ne $primary_key ||
                '' ne $bin_type) {
                $self_link .=
                    "<a href=\"index.$html_ext\" title=\"Click to go to default view of this directory.\">$base</a>";
            } else {
                $self_link .= $base;
            }
        } else {
            $esc_trunc_name = $lcovutil::dirseparator . $esc_trunc_name
                if $isAbsolutePath;
            $self_link = $esc_trunc_name;
            if ('name' ne $primary_key ||
                '' ne $bin_type) {
                # go back to the 'vanilla' view of this directory
                $self_link =
                    "<a href=\"index.$html_ext\" title=\"Click to go to default view of this directory.\">$esc_trunc_name</a>";
            }
        }
        $view =
            "<a href=\"$base_dir" .
            "index$key$bin_type.$html_ext\" title=\"Click to go to top-level\">"
            . "$overview_title</a> - $self_link";
    } elsif ($type == $HDR_SOURCE || $type == $HDR_FUNC) {
        # File view
        my $dir_name = dirname($rel_filename);

        my $esc_base_name = escape_html($base_name);
        my $esc_dir_name  = escape_html($dir_name);
        $esc_dir_name = $lcovutil::dirseparator . $esc_dir_name
            if $isAbsolutePath;

        $base_dir = get_relative_base_path($dir_name);
        # if using frames, to break frameset when clicking any of the links
        my $parent = $frames ? " target=\"_parent\"" : "";
        $view =
            "<a href=\"$base_dir" .
            "index.$html_ext\"$parent title=\"Click to go to top-level\">" .
            "$overview_title</a> - ";
        if ($main::flat) {
            # for flat view, point only to the top-levevl and show path
            #  to this file.  No other links
            $view .= File::Spec->catfile($esc_dir_name, $esc_base_name);
        } else {
            if ($main::hierarchical) {
                $html_path =~ s/<a/<a$parent/g;
                $view .= $html_path;
            } else {
                $view .=
                    "<a href=\"index.$html_ext\"$parent title=\"Click to go to directory $esc_dir_name\">$esc_dir_name</a>";
            }
            $view .= " - $esc_base_name";
        }

        # Add function suffix
        if ($lcovutil::func_coverage &&
            defined($differentialFunctionMap) &&
            %$differentialFunctionMap) {
            $view .= "<span style=\"font-size: 80%;\">";
            if ($type == $HDR_SOURCE) {
                my $suffix = $sort_tables ? '-c' : '';
                $view .=
                    " (source / <a href=\"$base_name.func$suffix.$html_ext\" title=\"Click to go to function table\">functions</a>)";
            } elsif ($type == $HDR_FUNC) {
                $view .=
                    " (<a href=\"$base_name.gcov.$html_ext\" title=\"Click to go to source detail\">source</a> / functions)";
            }
            $view .= "</span>";
        }
    } elsif ($type == $HDR_TESTDESC) {
        # Test description header
        $base_dir = "";
        $view =
            "<a href=\"$base_dir" . "index.$html_ext\">" .
            "$overview_title</a> - test case descriptions";
    }

    # Prepare text for "test" field
    my $test = escape_html($test_title);

    # Append link to test description page if available
    if (%test_description && ($type != $HDR_TESTDESC)) {
        if ($frames && ($type == $HDR_SOURCE || $type == $HDR_FUNC)) {
            # Need to break frameset when clicking this link
            $test .=
                " ( <span style=\"font-size:80%;\">" .
                "<a href=\"$base_dir" .
                "descriptions.$html_ext\" target=\"_parent\">" .
                "view descriptions</a></span> )";
        } else {
            $test .=
                " ( <span style=\"font-size:80%;\">" .
                "<a href=\"$base_dir" .
                "descriptions.$html_ext\">" . "view descriptions</a></span> )";
        }
    }

    # Write header
    write_header_prolog(*HTML_HANDLE, $base_dir);

    # Left row
    push(@row_left,
         [["10%", "headerItem", "Current view:"], ["10%", "headerValue", $view]
         ]);
    my $label = defined($baseline_title) ? "Current" : "Test";
    push(@row_left,
         [[undef, "headerItem", "$label:"], [undef, "headerValue", $test]]);
    push(@row_left,
         [[undef, "headerItem", "$label Date:"],
          [undef, "headerValue", $current_date]
         ]);
    if (defined($baseline_title)) {
        push(@row_left,
             [[undef, "headerItem", "Baseline:"],
              [undef, "headerValue", $baseline_title]
             ]);
        push(@row_left,
             [[undef, "headerItem", "Baseline Date:"],
              [undef, "headerValue", $baseline_date]
             ]);
    }
    my $hasBranchData = $lcovutil::br_coverage && 0 != $summary->branch_found();
    my $hasMcdcData   = $lcovutil::mcdc_coverage && 0 != $summary->mcdc_found();

    # if top-level page, link to command line and profile
    if ($summary->type() eq 'top' &&
        $bin_type eq ''                &&
        $primary_key eq 'name'         &&
        $sort_type eq $main::SORT_FILE &&
        defined($lcovutil::profile)) {

        push(@row_left,
             [[undef, 'headerItem', 'genhtml Process:'],
              [undef, 'headerValue',
               '<span><a href="cmdline.html">command line</a></span>'
              ]
             ],
             [[undef, 'headerItem', ''],    #blank
              [undef, 'headerValue',
               '<span><a href="profile.html">profile data</a></span>'
              ]
             ]);
    }
    if ($type != $HDR_SOURCE &&
        $type != $HDR_FUNC   &&
        (defined($main::show_ownerBins) ||
            @SourceFile::annotateScript)
    ) {
        # we are going to have 3 versions of the page:
        #   flat, with owner bin data, with date bin data
        # so label which one this is
        my ($tableLabel, $tableKey);
        if ($bin_type eq '-owner') {
            $tableLabel = 'Group by:';
            $tableKey   = 'Owner';
        } elsif ($bin_type eq '-date') {
            $tableLabel = 'Group by:';
            $tableKey   = 'Date bin';
        } else {
            $bin_type eq "" or
                die("unexpected bin detail type $bin_type");
            $tableLabel = 'Summarize by:';
            if ($primary_key eq 'date') {
                $tableKey = 'Date bin';
            } elsif ($primary_key eq 'owner') {
                $tableKey = 'Owner';
            }    # else ($primary_key eq 'name';
        }
        # label this only if there is both a primary and secondary
        push(@row_left,
             [[undef, 'headerItem', $tableLabel],
              [undef, 'headerValue', $tableKey]
             ]) if (defined($tableKey));
    }

    # Right row
    if ($legend && ($type == $HDR_SOURCE || $type == $HDR_FUNC)) {
        # kind of hacky - using spaces to try to align
        my $text = <<END_OF_HTML;
            Lines:&nbsp&nbsp&nbsp&nbsp
            <span class="coverLegendCov">hit</span>
            <span class="coverLegendNoCov">not hit</span>
END_OF_HTML
        if ($hasBranchData) {
            $text .= <<END_OF_HTML;
            <br>Branches:
            <span class="coverLegendCov">+</span> taken
            <span class="coverLegendNoCov">-</span> not taken
            <span class="coverLegendNoCov">#</span> not executed
END_OF_HTML
        }
        if ($hasMcdcData) {
            $text .= <<END_OF_HTML;
            <br>MC/DC:&nbsp&nbsp&nbsp
            <span class="coverLegendCov">T</span> 'true' sensitized
            <span class="coverLegendNoCov">t</span> 'true' not sensitized
            <span class="coverLegendCov">F</span> 'false' sensitized
            <span class="coverLegendNoCov">f</span> 'false' not sensitized
END_OF_HTML
        }
        push(@row_left,
             [[undef, "headerItem", "Legend:"],
              [undef, "headerValueLeg", $text]
             ]);
    } elsif ($legend && ($type != $HDR_TESTDESC)) {
        my $text = <<END_OF_HTML;
            Rating:
            <span class="coverLegendCovLo" title="Coverage rates below $med_limit % are classified as low">low: &lt; $med_limit %</span>
            <span class="coverLegendCovMed" title="Coverage rates between $med_limit % and $hi_limit % are classified as medium">medium: &gt;= $med_limit %</span>
            <span class="coverLegendCovHi" title="Coverage rates of $hi_limit % and more are classified as high">high: &gt;= $hi_limit %</span>
END_OF_HTML
        push(@row_left,
             [[undef, "headerItem", "Legend:"],
              [undef, "headerValueLeg", $text]
             ]);
    }
    my %activeTlaColsForType;
    my @nonZeroTlas;
    if ($type != $HDR_TESTDESC && $main::show_tla) {
        # compute which TLAs have non-zero entries for line/branch/function
        my @visit = (SummaryInfo::LINE_DATA);
        push(@visit, SummaryInfo::BRANCH_DATA)
            if $hasBranchData;
        push(@visit, SummaryInfo::MCDC_DATA)
            if $hasMcdcData;
        push(@visit, SummaryInfo::FUNCTION_DATA)
            if $lcovutil::func_coverage;

        foreach my $covtype (@visit) {
            if ($main::show_zeroTlaColumns) {
                $activeTlaColsForType{$covtype} =
                    \@SummaryInfo::tlaOrderNoExclude;
            } else {
                my @found;
                for my $tla (@SummaryInfo::tlaOrderNoExclude) {
                    push(@found, $tla)
                        if (0 != $summary->get($tla, $covtype));
                }
                $activeTlaColsForType{$covtype} = \@found;
            }
        }
        foreach my $tla (@SummaryInfo::tlaOrderNoExclude) {
            # check to see that there are non-zero entries
            next if grep(/$tla/, ('EUC', 'ECC'));
            foreach my $covtype (@visit) {
                if (grep({ $tla eq $_ } @{$activeTlaColsForType{$covtype}})) {
                    push(@nonZeroTlas, $tla);
                    last;
                }
            }
        }
    }
    # if $showAllTlasInSummary is set, then put all TLA categories in the
    #   summary tables at the top of the page (but only the non-zero categories
    #   in the 'file detail' tables.
    # Otherwise, only show non-zero categories in both places
    my $showAllTlasInSummary = 0;    # turn off for now
    my $tlaSummaryTypes =
        $showAllTlasInSummary ? \@$SummaryInfo::tlaOrderNoExclude :
        \@nonZeroTlas;

    if ($type == $HDR_TESTDESC) {
        push(@row_right, [["80%"]]);
    } else {
        my $totalTitle = "Covered + Uncovered code";
        my $hitTitle   = "Exercised code only";
        if (@main::base_filenames) {
            $totalTitle .= " (not including EUB, ECB, DUB, DCB categories)";
            $hitTitle   .= " (CBC + GBC + GNC + GIC)";
        }

        my @headerRow = (["5%", undef, undef],
                         ["5%", "headerCovTableHead", "Coverage"],
                         ["5%", "headerCovTableHead",
                          "Total", undef,
                          $totalTitle
                         ]);
        if ($main::show_hitTotalCol) {
            # legacy view, or we have all the differential categories
            #-  thus also want a summary
            $hitTitle = $main::use_legacyLabels ? undef : $hitTitle;
            push(@headerRow,
                 ["5%", "headerCovTableHead",
                  $main::opt_missed ? "Missed" : "Hit",
                  undef, $hitTitle
                 ]);
        }
        if ($main::show_tla) {
            # Show selected TLAs in title row - either all of them, or
            # only types which have non-zero entries
            for my $tla (@$tlaSummaryTypes) {

                my ($title, $label, $class);
                if ($main::use_legacyLabels) {
                    $label = $SummaryInfo::tlaToLegacy{$tla};
                    $class = "headerCovTableHead";
                } else {
                    $label = $tla;
                    $title = $SummaryInfo::tlaToTitle{$tla};
                    $class = "headerCovTableHead$tla";
                }
                push(@headerRow, ["5%", $class, $label, undef, $title]);
            }
        }
        push(@row_right, \@headerRow);
    }
    # Line coverage
    my $tot = $summary->lines_found();    #might have been modified...
    my $hit = $summary->lines_hit();
    my $style =
        $rate_name[classify_rate($tot, $hit, $ln_med_limit, $ln_hi_limit)];
    my $rate = rate($hit, $tot, "&nbsp;%");
    $hit -= $tot
        if $main::opt_missed;             # negative number
    my @dataRow = ([undef, "headerItem", "Lines:"],
                   [undef, "headerCovTableEntry$style", $rate],
                   [undef, "headerCovTableEntry", $tot]);
    if ($main::show_hitTotalCol) {
        push(@dataRow, [undef, "headerCovTableEntry", $hit]);
    }
    if ($main::show_tla) {
        my @tlaRow =
            buildHeaderSummaryTableRow($summary, SummaryInfo::LINE_DATA,
                                       $fileDetail, \&SourceFile::nextTlaGroup,
                                       $tlaSummaryTypes);
        push(@dataRow, @tlaRow);
    }
    push(@row_right, \@dataRow)
        if ($type != $HDR_TESTDESC);
    # Function coverage
    if ($lcovutil::func_coverage) {
        my $tot = $summary->function_found();
        my $hit = $summary->function_hit();
        $style =
            $rate_name[classify_rate($tot, $hit, $fn_med_limit, $fn_hi_limit)];
        $rate = rate($hit, $tot, "&nbsp;%");
        $hit -= $tot
            if $main::opt_missed;    # negative number
        my @dataRow = ([undef, "headerItem", "Functions:"],
                       [undef, "headerCovTableEntry$style", $rate],
                       [undef, "headerCovTableEntry", $tot]);
        if ($main::show_hitTotalCol) {
            push(@dataRow, [undef, "headerCovTableEntry", $hit]);
        }
        if ($main::show_tla) {
            # no file position for function (yet)
            my @tlaRow =
                buildHeaderSummaryTableRow($summary,
                    SummaryInfo::FUNCTION_DATA, undef, undef, $tlaSummaryTypes);
            push(@dataRow, @tlaRow);
        }
        push(@row_right, \@dataRow)
            if ($type != $HDR_TESTDESC);
    }
    # Branch coverage
    if ($hasBranchData) {
        my $tot = $summary->branch_found();
        my $hit = $summary->branch_hit();
        $style =
            $rate_name[classify_rate($tot, $hit, $br_med_limit, $br_hi_limit)];
        $rate = rate($hit, $tot, "&nbsp;%");
        $hit -= $tot
            if $main::opt_missed;    # negative number
        my @dataRow = ([undef, "headerItem", "Branches:"],
                       [undef, "headerCovTableEntry$style", $rate],
                       [undef, "headerCovTableEntry", $tot]);
        if ($main::show_hitTotalCol) {
            push(@dataRow, [undef, "headerCovTableEntry", $hit]);
        }
        if ($main::show_tla) {
            my @tlaRow =
                buildHeaderSummaryTableRow($summary, SummaryInfo::BRANCH_DATA,
                                  $fileDetail, \&SourceFile::nextBranchTlaGroup,
                                  $tlaSummaryTypes);
            push(@dataRow, @tlaRow);
        }
        push(@row_right, \@dataRow)
            if ($type != $HDR_TESTDESC);
    }

    # MC/DC coverage
    if ($hasMcdcData) {
        my $tot = $summary->mcdc_found();
        my $hit = $summary->mcdc_hit();
        $style =
            $rate_name[classify_rate($tot, $hit, $mcdc_med_limit,
                                     $mcdc_hi_limit)];
        $rate = rate($hit, $tot, "&nbsp;%");
        $hit -= $tot
            if $main::opt_missed;    # negative number
        my @dataRow = ([undef, "headerItem", "MC/DC:"],
                       [undef, "headerCovTableEntry$style", $rate],
                       [undef, "headerCovTableEntry", $tot]);
        if ($main::show_hitTotalCol) {
            push(@dataRow, [undef, "headerCovTableEntry", $hit]);
        }
        if ($main::show_tla) {
            my @tlaRow =
                buildHeaderSummaryTableRow($summary, SummaryInfo::MCDC_DATA,
                                    $fileDetail, \&SourceFile::nextMcdcTlaGroup,
                                    $tlaSummaryTypes);
            push(@dataRow, @tlaRow);
        }
        push(@row_right, \@dataRow)
            if ($type != $HDR_TESTDESC);
    }

    # Aged coverage
    if ($show_dateBins) {
        # make a space in the table between before date bins
        my $dateBinDetailPage = "index-date.$html_ext"
            if ($type != $HDR_SOURCE &&
                $type != $HDR_FUNC);
        my $table =
            buildDateSummaryTable(
                        $summary, SummaryInfo::LINE_DATA,
                        \&SummaryInfo::lineCovCount, $fileDetail,
                        \&SourceFile::nextInDateBin, "Line coverage date bins:",
                        $dateBinDetailPage, scalar(@dataRow),
                        $tlaSummaryTypes);
        push(@row_right, @$table)
            if ($type != $HDR_TESTDESC);

        if ($lcovutil::func_coverage) {
            my $fn_table =
                buildDateSummaryTable($summary,
                                      SummaryInfo::FUNCTION_DATA,
                                      \&SummaryInfo::functionCovCount,
                                      $fileDetail,
                                      undef,
                                      "Function coverage date bins:",
                                      $dateBinDetailPage,
                                      scalar(@dataRow),
                                      $tlaSummaryTypes);
            push(@row_right, @$fn_table)
                if ($type != $HDR_TESTDESC);
        }

        if ($hasBranchData) {
            my $br_table =
                buildDateSummaryTable($summary,
                                      SummaryInfo::BRANCH_DATA,
                                      \&SummaryInfo::branchCovCount,
                                      $fileDetail,
                                      \&SourceFile::nextBranchInDateBin,
                                      "Branch coverage date bins:",
                                      $dateBinDetailPage,
                                      scalar(@dataRow),
                                      $tlaSummaryTypes);
            push(@row_right, @$br_table)
                if ($type != $HDR_TESTDESC);
        }

        if ($hasMcdcData) {
            my $mcdc_table =
                buildDateSummaryTable($summary,
                                      SummaryInfo::MCDC_DATA,
                                      \&SummaryInfo::mcdcCovCount,
                                      $fileDetail,
                                      \&SourceFile::nextMcdcInDateBin,
                                      "MC/DC coverage date bins:",
                                      $dateBinDetailPage,
                                      scalar(@dataRow),
                                      $tlaSummaryTypes);
            push(@row_right, @$mcdc_table)
                if ($type != $HDR_TESTDESC);
        }
    }
    # owner bins..
    if (defined($main::show_ownerBins)) {
        # first, make sure there is owner data here (ie., owner data
        #   was collected, or both that there is owner data and some
        #   owners have uncovered code)
        # This it the header table - so we want to truncate the owner
        #   list if it is too long (and the user asked us to)
        my ($ownerList, $truncated) =
            $summary->findOwnerList($callback_type, 1,
                       $main::show_ownerBins && $main::show_ownerBins eq 'all');
        if (defined($ownerList)) {
            my $ownerBinDetailPage = "index-owner.$html_ext"
                if ($type != $HDR_SOURCE &&
                    $type != $HDR_FUNC);
            my $table =
                buildOwnerSummaryTable($ownerList,
                                       $truncated,
                                       $summary,
                                       SummaryInfo::LINE_DATA,
                                       $fileDetail,
                                       \&SourceFile::nextInOwnerBin,
                                       "Line coverage ownership bins",
                                       $ownerBinDetailPage,
                                       scalar(@dataRow),
                                       $tlaSummaryTypes);
            push(@row_right, @$table)
                if ($type != $HDR_TESTDESC);

            if ($hasBranchData) {
                my $br_table =
                    buildOwnerSummaryTable($ownerList,
                                           $truncated,
                                           $summary,
                                           SummaryInfo::BRANCH_DATA,
                                           $fileDetail,
                                           \&SourceFile::nextBranchInOwnerBin,
                                           "Branch coverage ownership bins",
                                           $ownerBinDetailPage,
                                           scalar(@dataRow),
                                           $tlaSummaryTypes);
                push(@row_right, @$br_table)
                    if ($type != $HDR_TESTDESC);
            }

            if ($hasMcdcData) {
                my $mcdc_table =
                    buildOwnerSummaryTable($ownerList,
                                           $truncated,
                                           $summary,
                                           SummaryInfo::MCDC_DATA,
                                           $fileDetail,
                                           \&SourceFile::nextMcdcInOwnerBin,
                                           "MC/DC coverage ownership bins",
                                           $ownerBinDetailPage,
                                           scalar(@dataRow),
                                           $tlaSummaryTypes);
                push(@row_right, @$mcdc_table)
                    if ($type != $HDR_TESTDESC);
            }
        }
    }

    # Print rows
    my $num_rows = max(scalar(@row_left), scalar(@row_right));
    for (my $i = 0; $i < $num_rows; $i++) {
        my $left  = $row_left[$i];
        my $right = $row_right[$i];

        if (!defined($left)) {
            $left = [[undef, undef, undef], [undef, undef, undef]];
        }
        if (!defined($right)) {
            $right = [];
        }
        write_header_line(*HTML_HANDLE, @{$left},
                          [$i == 0 ? "5%" : undef, undef, undef],
                          @{$right});
    }

    # Fourth line
    write_header_epilog(*HTML_HANDLE, $base_dir);
    return \%activeTlaColsForType;
}

sub get_sort_code($$$)
{
    my ($link, $alt, $base) = @_;
    my $png;
    my $link_start;
    my $link_end;

    if (!defined($link)) {
        $png        = "glass.png";
        $link_start = "";
        $link_end   = "";
    } else {
        $png        = "updown.png";
        $link_start = '<a href="' . $link . '">';
        $link_end   = "</a>";
    }
    my $help = " title=\"Click to sort table by $alt\"";
    $alt = "Sort by $alt";
    return " <span $help class=\"tableHeadSort\">" . $link_start .
        '<img src="' . $base . $png . '" width=10 height=14 ' .
        'alt="' . $alt . '"' . $help . ' border=0>' . $link_end . '</span>';
}

sub get_file_code($$$$$$)
{
    my ($type, $text, $sort_button, $bin_type, $primary_key, $base) = @_;
    my $result = $text;
    my $link;

    my $key = 'name' ne $primary_key ? "-bin_$primary_key" : "";
    if ($sort_button) {
        $link = "index$key$bin_type";
        $link .= '-detail'
            unless ($type == $HEAD_NO_DETAIL);
        $link .= ".$html_ext";
    }
    $result .= get_sort_code($link, "file name", $base);

    return $result;
}

sub get_line_code($$$$$$$)
{
    my ($type, $sort_type, $text, $sort_button, $bin_type, $primary_key, $base)
        = @_;
    my $result    = $text;
    my $key       = ('name' eq $primary_key) ? '' : "-bin_$primary_key";
    my $sort_link = "index" . $key . $bin_type . "-sort-l.$html_ext"
        if $sort_button;

    if ($type == $HEAD_NO_DETAIL) {
        # Just text
    } elsif ($type == $HEAD_DETAIL_HIDDEN) {
        # Text + link to detail view
        my $help = "title=\"Click to go to per-testcase coverage details\"";
        my $detail_link =
            'index' . $key . $bin_type .
            '-detail' . $fileview_sortname[$sort_type] . '.' . $html_ext;
        $result .=
            " ( <a $help " .
            'class="detail" href="' . $detail_link . '">show details</a> )';
    } else {
        # Text + link to standard view
        my $help = "title=\"Click to hide per-testcase coverage details\"";
        $result .=
            " ( <a $help " . 'class="detail" href="index' .
            $key . $bin_type . $fileview_sortname[$sort_type] .
            '.' . $html_ext . '">hide details</a> )';
    }
    # Add sort button
    $result .= get_sort_code($sort_link, "line coverage", $base);
    # we don't have a 'detail' bin page
    $result =~ s/index-bin_(.+?)-detail((-sort-.)?\.)/index-$1$2/g;

    return $result;
}

sub get_func_code($$$$$$)
{
    my ($type, $text, $sort_button, $bin_type, $primary_key, $base) = @_;
    my $result = $text;
    my $link;
    my $key = 'name' ne $primary_key ? "-bin_$primary_key" : "";

    if ($sort_button) {
        $link = "index$key$bin_type";
        $link .= '-detail'
            unless ($type == $HEAD_NO_DETAIL);
        $link .= "-sort-f.$html_ext";
    }
    $result .= get_sort_code($link, "function coverage", $base);
    return $result;
}

sub get_br_code($$$$$$)
{
    my ($type, $text, $sort_button, $bin_type, $primary_key, $base) = @_;
    my $result = $text;
    my $link;
    my $key = 'name' ne $primary_key ? "-bin_$primary_key" : "";

    if ($sort_button) {
        $link = "index$key$bin_type";
        $link .= '-detail'
            unless ($type == $HEAD_NO_DETAIL);
        $link .= "-sort-b.$html_ext";
    }
    $result .= get_sort_code($link, "branch coverage", $base);
    return $result;
}

sub get_mcdc_code($$$$$$)
{
    my ($type, $text, $sort_button, $bin_type, $primary_key, $base) = @_;
    my $result = $text;
    my $link;
    my $key = 'name' ne $primary_key ? "-bin_$primary_key" : "";

    if ($sort_button) {
        $link = "index$key$bin_type";
        $link .= '-detail'
            unless ($type == $HEAD_NO_DETAIL);
        $link .= "-sort-m.$html_ext";
    }
    $result .= get_sort_code($link, "MC/DC coverage", $base);
    return $result;
}

#
# write_file_table(filehandle, callback_type, base_dir, perTestcaseData,
#                  parentSummary, ctrlSettings, activeTlaCols)
#   ctrlSettings = [fileview, sort_type, details_type, sort_name]
#   perTestcaseData = [testhash, testfnchash, testbrhash]
#   activeTlaCols = {coverage_type, [tlas with nonzero count]}
#
# Write a complete file table. OVERVIEW is a reference to a hash containing
# the following mapping:
#
#   filename -> "lines_found,lines_hit,funcs_found,funcs_hit,page_link,
#                func_link" + other file details
#
# TESTHASH is a reference to the following hash:
#
#   filename -> \%testdata
#   %testdata: name of test affecting this file -> \%testcount
#   %testcount: line number -> execution count for a single test
#
# Heading of first column is "Filename" if FILEVIEW is true, "Directory name"
# otherwise.
#

sub write_file_table(*$$$$$$)
{
    local *HTML_HANDLE = $_[0];
    my $callback_type = $_[1];
    my $base_dir      = $_[2];
    my $perTestcaseData =
        $_[3];    # undef or [lineCov, funcCov, branchCov, mcdcCov]
    my $dirSummary = $_[4];    # SummaryInfo object
    my ($fileview, $primary_key, $sort_type, $bin_type) = @{$_[5]};
    my $activeTlaCols = $_[6];
    # $fileview == 0 if listing directories, 1 if listing files
    # $primary_key in ("name", "owner", "date"). If $primary_key is:
    #   - 'name': leftmost column is file/directory name -
    #     this is the original/vanilla genhtml behaviour
    #   - 'owner': leftmost column is author name.  Details for that owner
    #     (for all files in the project, or for all files in this drectory)
    #     are shown in a contiguous block.
    #   - 'date': leftmost column is date bin.  Details for that bin are
    #     shown in a contiguous block.
    # $sort_type in ("", "-sort-l", "-sort-b", "-sort-f")
    # $bin_type in ("", "-owner", "-date")
    #   - if $bin_type not "", expand non-empty entries after file/directory
    #     overall count (i.e. - show all the "owners" for this file/directory,
    #     or all date bins for this file/directory)
    #   - $bin_type is applied only if $primary_key is "name"

    $primary_key eq "name" || $bin_type eq "" or
        die(
        "primary key '$primary_key' does not support '$bin_type' detail reporting"
        );

    my $hasBranchData =
        $lcovutil::br_coverage && 0 != $dirSummary->branch_found();
    my $hasMcdcData =
        $lcovutil::mcdc_coverage && 0 != $dirSummary->mcdc_found();
    my $includeFunctionColumns = $lcovutil::func_coverage &&
        0 != $dirSummary->function_found() &&
        'owner' ne $primary_key;

    # Determine HTML code for column headings
    my $hide = $HEAD_NO_DETAIL;
    my $show = $HEAD_NO_DETAIL;
    if (($dirSummary->type() eq 'directory' ||
         $main::flat ||
         $main::hierarchical) &&
        $show_details
    ) {
        # "detailed" if line coverage hash not empty
        my $detailed =
            defined($perTestcaseData) && scalar(%{$perTestcaseData->[0]});
        $hide = $detailed ? $HEAD_DETAIL_HIDDEN : $HEAD_NO_DETAIL;
        $show = $detailed ? $HEAD_DETAIL_SHOWN : $HEAD_DETAIL_HIDDEN;
    }
    my $file_col_title = ($fileview || $main::flat) ? 'File' : 'Directory';
    # don't insert the 'sort' controls if there is just a single source file
    my $use_sort_button = $sort_tables && 1 < scalar($dirSummary->sources());
    my $file_code =
        get_file_code($hide, $file_col_title,
                      $use_sort_button && $sort_type != $SORT_FILE,
                      $bin_type, $primary_key, $base_dir);
    my $line_code = get_line_code($show, $sort_type,
                                  "Line Coverage",
                                  $use_sort_button && $sort_type != $SORT_LINE,
                                  $bin_type, $primary_key, $base_dir);
    my $func_code = get_func_code($hide,
                                  "Function Coverage",
                                  $use_sort_button && $sort_type != $SORT_FUNC,
                                  $bin_type, $primary_key, $base_dir);
    my $br_code = get_br_code($hide,
                              "Branch Coverage",
                              $use_sort_button && $sort_type != $SORT_BRANCH,
                              $bin_type, $primary_key, $base_dir);
    my $mcdc_code = get_mcdc_code($hide,
                                  "MC/DC Coverage",
                                  $use_sort_button && $sort_type != $SORT_MCDC,
                                  $bin_type, $primary_key, $base_dir);

    my @head_columns;

    my @lineCovCols     = (["Rate", 2], "Total");
    my @mcdcCovCols     = ("Rate", "Total");
    my @branchCovCols   = ("Rate", "Total");
    my @functionCovCols = ("Rate", "Total");
    if ($main::show_hitTotalCol) {
        my $t = $main::opt_missed ? "Missed" : "Hit";
        push(@lineCovCols, $t);
        push(@mcdcCovCols, $t);
        push(@branchCovCols, $t);
        push(@functionCovCols, $t);
    }
    if ($main::show_tla) {
        my @visit = ([SummaryInfo::LINE_DATA, \@lineCovCols]);
        push(@visit, [SummaryInfo::MCDC_DATA, \@mcdcCovCols])
            if $hasMcdcData;
        push(@visit, [SummaryInfo::BRANCH_DATA, \@branchCovCols])
            if $hasBranchData;
        push(@visit, [SummaryInfo::FUNCTION_DATA, \@functionCovCols])
            if $includeFunctionColumns;
        foreach my $d (@visit) {
            my ($covtype, $cols) = @$d;
            foreach my $tla (@{$activeTlaCols->{$covtype}}) {
                my $label =
                    $main::use_legacyLabels ? $SummaryInfo::tlaToLegacy{$tla} :
                    $tla;
                push(@$cols,
                     [$label,
                      1,
                      $main::use_legacyLabels ?
                          undef :
                          $SummaryInfo::tlaToTitle{$tla}
                     ]);
            }
        }
    }
    push(@head_columns, [$line_code, $#lineCovCols + 2, \@lineCovCols]);
    push(@head_columns, [$mcdc_code, $#mcdcCovCols + 1, \@mcdcCovCols])
        if $hasMcdcData;
    push(@head_columns, [$br_code, $#branchCovCols + 1, \@branchCovCols])
        if $hasBranchData;
    push(@head_columns, [$func_code, $#functionCovCols + 1, \@functionCovCols])
        if $includeFunctionColumns;

    my $showBinDetail = undef;
    if ($bin_type eq '-date' &&
        $dirSummary &&
        $dirSummary->hasDateInfo()) {
        $showBinDetail = 'date';
    } elsif ($bin_type eq '-owner' &&
             $dirSummary &&
             $dirSummary->hasOwnerInfo()) {
        $showBinDetail = 'owner';
    }
    my $num_columns =
        write_file_table_prolog(*HTML_HANDLE, $file_code,
                               defined($showBinDetail) ? $showBinDetail : undef,
                               $primary_key, @head_columns);

    my @tableRows;
    if ($primary_key eq 'name') {

        # sorted list of all the file or directory names
        foreach my $name ($dirSummary->get_sorted_keys($sort_type, 1)) {
            my $entrySummary = $dirSummary->get_source($name);

            if ($entrySummary->type() eq 'directory') {
                if ('directory' eq $dirSummary->type()) {
                    $name = File::Basename::basename($name);
                } elsif ($entrySummary->is_directory(1)) {
                    $name = $lcovutil::dirseparator . $name;
                }
            } else {
                die("unexpected summary type")
                    unless 'file' eq $entrySummary->type();
                $name = File::Basename::basename($name);
            }
            push(@tableRows,
                 FileOrDirectoryCallback->new($name, $entrySummary));
        }

    } elsif ($primary_key eq 'owner') {

        # retrieve sorted list of owner names - alphabetically, by name
        #   or by number of missed lines or missed branches
        my $all = defined($main::show_ownerBins) && $main::show_ownerBins;

        my %owners;
        foreach my $d ([1, SummaryInfo::LINE_DATA, 0],
                       [$lcovutil::mcdc_coverage, SummaryInfo::MCDC_DATA, 1],
                       [$lcovutil::br_coverage, SummaryInfo::BRANCH_DATA, 2],
        ) {
            my ($enable, $dataType, $idx) = @$d;
            next unless $enable;

            foreach my $owner ($dirSummary->owners($all, $dataType)) {
                # (line count, branch count, mcdc count, function count)
                $owners{$owner} = [[0, 0], [0, 0], [0, 0], [0, 0]]
                    unless exists($owners{$owner});

                $owners{$owner}->[$idx] = [
                        $dirSummary->owner_tlaCount($owner, 'found', $dataType),
                        $dirSummary->owner_tlaCount($owner, 'hit', $dataType)
                ];
            }
        }
        my @sorted = sort(keys(%owners));    # default sort order
                                             # now, sort the owner list...
        foreach my $d ([1, $SORT_LINE, 0],
                       [$lcovutil::mcdc_coverage, $SORT_MCDC, 1],
                       [$lcovutil::br_coverage, $SORT_BRANCH, 2],
        ) {
            my ($enable, $s, $idx) = @$d;
            if ($sort_type eq $s) {
                # sort by number of missed lines/branches/MCDC expressions
                @sorted = sort({
                                   my $la = $owners{$a}->[$idx];
                                   my $lb = $owners{$b}->[$idx];
                                   ($lb->[0] - $lb->[1])
                                       <=> ($la->[0] - $la->[1]) ||
                                       $a cmp $b    # then by name
                } keys(%owners));
                last;
            }
        }

        # don't truncate the 'owner detail table'
        #  if user asked to see the page, then they want to be able to navigate
        #  to all users
        #if (defined($ownderTableElements) &&
        #    $ownerTableElements < scalar(@sorted)) &&
        #    (0 == scalar(@truncateOwnerTableLevels) ||
        #     grep(/$callback_type/, @truncateOwnerTableLevels)))) {
        #    # truncate owner list in header table
        #    splice(@sorted, $ownerTableElements);
        #}

        foreach my $owner (@sorted) {
            push(@tableRows,
                 FileOrDirectoryOwnerCallback->new($owner, $dirSummary));
        }

    } elsif ($primary_key eq 'date') {

        for (my $bin = 0; $bin <= $#SummaryInfo::ageGroupHeader; ++$bin) {
            my $ageval = $dirSummary->age_sample($bin);
            my $lines  = $dirSummary->lineCovCount('found', 'age', $ageval);
            if (0 != $lines ||
                ($lcovutil::br_coverage &&
                    0 != $dirSummary->branchCovCount('found', 'age', $ageval))
                ||
                ($lcovutil::mcdc_coverage &&
                    0 != $dirSummary->mcdcCovCount('found', 'age', $ageval)) ||
                ($lcovutil::func_coverage &&
                    0 != $dirSummary->functionCovCount('found', 'age', $ageval))
            ) {
                push(@tableRows,
                     FileOrDirectoryDateCallback->new($bin, $dirSummary));
            }
        }
    } else {
        die("unsupported primary key '$primary_key'");
    }

    my $all = defined($main::show_ownerBins) && $main::show_ownerBins eq 'all';
    # show only the 'Total' row if all row entries are suppressed
    #  because they have no un-exercised coverpoints
    my $elideEmptyRows = 0;
    if ($primary_key eq 'owner') {
        $elideEmptyRows = $main::show_ownerBins ne 'all';
    } elsif ($primary_key eq 'date') {
        $elideEmptyRows = 1;
    }
    my $suppressedEmptyRow = 0;
    # if only one secondary entry, we don't need the 'Total' header
    my $suppressedSecondaryHeader = 0;

    foreach my $primaryCb (@tableRows) {

        # we need to find the 'owner' and 'date' row data for this file before
        #   we write anything else, because we need to know the number of
        #   rows that the $primary cell will span

        my @secondaryRows;
        my $skippedSecondaryRows = 0;

        if (defined($showBinDetail)) {

            if (defined($primaryCb->summary())) {
                my $source = $primaryCb->summary();

                if ($showBinDetail eq 'owner') {
                    # do I need an option to suppress the list of owners?
                    #  maybe too much information, in some circumstances?
                    # are there any non-empty owner tables here?
                    # If user explicitly asks to see the 'owner detail' page,
                    #  then they must be interested in which code is written by
                    #   each author - so we should not truncate the list
                    my ($ownerList, $truncated) =
                        $primaryCb->findOwnerList($callback_type, 0, $all);
                    die("unexpected truncate count") unless $truncated == 0;
                    push(@secondaryRows, @$ownerList)
                        if defined($ownerList);
                }

                if ($showBinDetail eq 'date') {
                    for (my $bin = 0;
                         $bin <= $#SummaryInfo::ageGroupHeader;
                         ++$bin) {
                        my $ageval = $source->age_sample($bin);
                        my $lineCb =
                            $primaryCb->dateDetailCallback($ageval,
                                                        SummaryInfo::LINE_DATA);
                        my $lineTotal  = $lineCb->get('found');
                        my $hit        = $lineCb->get('hit');
                        my $lineMissed = $lineTotal - $hit;

                        my $branchCb =
                            $primaryCb->dateDetailCallback($ageval,
                                                      SummaryInfo::BRANCH_DATA);
                        my $branchTotal =
                            $lcovutil::br_coverage ? $branchCb->get('found') :
                            0;
                        $hit =
                            $lcovutil::br_coverage ? $branchCb->get('hit') : 0;
                        my $branchMissed = $branchTotal - $hit;

                        my $mcdcCb =
                            $primaryCb->dateDetailCallback($ageval,
                                                        SummaryInfo::MCDC_DATA);
                        my $mcdcTotal =
                            $lcovutil::mcdc_coverage ? $mcdcCb->get('found') :
                            0;
                        $hit =
                            $lcovutil::mcdc_coverage ? $mcdcCb->get('hit') : 0;
                        my $mcdcMissed = $mcdcTotal - $hit;

                        my $functionCb =
                            $primaryCb->dateDetailCallback($ageval,
                                                    SummaryInfo::FUNCTION_DATA);
                        my $functionTotal =
                            $lcovutil::func_coverage ?
                            $functionCb->get('found') :
                            0;
                        $hit =
                            $lcovutil::func_coverage ? $functionCb->get('hit') :
                            0;
                        my $functionMissed = $functionTotal - $hit;

                        next
                            if 0 == $lineTotal &&
                            0 == $branchTotal  &&
                            0 == $mcdcTotal    &&
                            0 == $functionTotal;

                        push(@secondaryRows,
                             [$SummaryInfo::ageGroupHeader[$bin],
                              [$lineMissed, $lineTotal, $lineCb],
                              [$branchMissed, $branchTotal, $branchCb],
                              [$mcdcMissed, $mcdcTotal, $mcdcCb],
                              [$functionMissed, $functionTotal, $functionCb]
                             ]);
                    }
                }
            }
        }    # if showBinDetail
        elsif ($primary_key ne 'name') {
            ($skippedSecondaryRows, @secondaryRows) =
                @{$primaryCb->findFileList($all)};
        }

        my ($found, $hit, $fn_found, $fn_hit,
            $br_found, $br_hit, $mcdc_found, $mcdc_hit,
            $page_link, $fileSummary, $fileDetails) = $primaryCb->data();
        # a bit of a hack: if this is top-level page (such that the links
        #   are to directory pages rather than to source code detail pages)
        #   and this is the 'owner bin detail' (or the 'date bin detail') view,
        #   then link to the same 'bin detail' view of the directory page
        # This enables the user who is tracking down code written by a
        #   particular user (or on a particular date) to go link-to-link
        #   without having to select the 'bin' link in the destination header.
        if ($fileview == 0 &&
            $bin_type ne "") {
            $page_link =~ s/index.$html_ext$/index$bin_type.$html_ext/;
        }

        my @columns;

        my @tableCallbackData = ($primaryCb->name(), $primaryCb->summary(),
                                 $fileDetails, $page_link);
        my $showLineGraph = 1;
        # Line coverage
        push(@columns,
             [$found, $hit, $med_limit, $hi_limit, $showLineGraph,
              $primaryCb->totalCallback(SummaryInfo::LINE_DATA),
              SummaryInfo::LINE_DATA
             ]);
        # MC/DC coverage
        if ($hasMcdcData) {
            push(@columns,
                 [$mcdc_found,
                  $mcdc_hit,
                  $mcdc_med_limit,
                  $mcdc_hi_limit,
                  0,
                  $primaryCb->totalCallback(SummaryInfo::MCDC_DATA),
                  SummaryInfo::MCDC_DATA
                 ]);
        }
        # Branch coverage
        if ($hasBranchData) {
            push(@columns,
                 [$br_found,
                  $br_hit,
                  $br_med_limit,
                  $br_hi_limit,
                  0,
                  $primaryCb->totalCallback(SummaryInfo::BRANCH_DATA),
                  SummaryInfo::BRANCH_DATA
                 ]);
        }
        # Function coverage
        if ($includeFunctionColumns) {
            # no 'owner' callbacks for function...
            my $cbStruct =
                $primaryCb->totalCallback(SummaryInfo::FUNCTION_DATA);

            push(@columns,
                 [$fn_found, $fn_hit, $fn_med_limit, $fn_hi_limit, 0,
                  $cbStruct, SummaryInfo::FUNCTION_DATA
                 ]);
        }
        my $elide_secondary_header = $compactSummaryTables &&
            (scalar(@secondaryRows) == 1 && $skippedSecondaryRows == 0);
        $suppressedSecondaryHeader ||= $elide_secondary_header;
        if (!$elide_secondary_header) {
            # pass 'dirSummary' to print method:  we omit the 'owner' column if
            #  none of the files in this directory have any owner information
            #  (i.e., none of them are found in the repo)
            my $numRows = (1 + scalar(@secondaryRows));
            my $asterisk;
            if ($elideEmptyRows && (1 == $numRows || $skippedSecondaryRows)) {
                $asterisk           = '&lowast;';
                $suppressedEmptyRow = 1;
            }
            write_file_table_entry(*HTML_HANDLE,
                                   $base_dir,
                                   [$primaryCb->name(), \@tableCallbackData,
                                    $activeTlaCols, $numRows,
                                    $primary_key, 0,
                                    $fileview, "fileOrDir",
                                    $page_link, $dirSummary,
                                    $showBinDetail, $asterisk
                                   ],
                                   @columns);
        }

        # sort secondary rows...
        if ($sort_type == $SORT_FILE) {
            # alphabetic
            @secondaryRows = sort({ $a->[0] cmp $b->[0] } @secondaryRows);
        } else {
            my $sortElem;
            if ($sort_type == $SORT_LINE) {
                $sortElem = 1;
            } elsif ($sort_type == $SORT_BRANCH) {
                $sortElem = 2;
            } elsif ($sort_type == $SORT_MCDC) {
                $sortElem = 3;
            } elsif ($sort_type == $SORT_FUNC) {
                $sortElem = 4;
            }
            @secondaryRows =
                sort({
                         my $ca = $a->[$sortElem];
                         my $cb = $b->[$sortElem];
                         # sort based on 'missed'
                         $cb->[0] <=> $ca->[0];
                } @secondaryRows);
        }
        foreach my $secondary (@secondaryRows) {
            my ($name, $line, $branch, $mcdc, $func) = @$secondary;
            my ($lineMissed, $lineTotal, $lineCb) = @$line;
            my $lineHit = $lineTotal - $lineMissed;

            my $fileInfo = $primaryCb->secondaryElementFileData($name);

            my $entry_type = $lineCb->cb_type();

            my @ownerColData;
            push(@ownerColData,
                 [$lineTotal, $lineHit, $med_limit,
                  $hi_limit, $showLineGraph, $lineCb,
                  SummaryInfo::LINE_DATA
                 ]);
            if ($hasMcdcData) {
                my ($mcdcMissed, $mcdcTotal, $mcdcCallback) = @$mcdc;
                # need to compute the totals...
                push(@ownerColData,
                     [$mcdcTotal, $mcdcTotal - $mcdcMissed,
                      $mcdc_med_limit, $mcdc_hi_limit,
                      0, $mcdcCallback,
                      SummaryInfo::MCDC_DATA
                     ]);
            }
            if ($hasBranchData) {
                my ($branchMissed, $branchTotal, $brCallback) = @$branch;
                # need to compute the totals...
                push(@ownerColData,
                     [$branchTotal, $branchTotal - $branchMissed,
                      $br_med_limit, $br_hi_limit,
                      0, $brCallback,
                      SummaryInfo::BRANCH_DATA
                     ]);
            }
            if ($includeFunctionColumns) {
                my ($funcMissed, $funcTotal, $funcCallback) = @$func;
                # need to compute the totals...
                push(@ownerColData,
                     [$funcTotal, $funcTotal - $funcMissed,
                      $fn_med_limit, $fn_hi_limit, 0, $funcCallback,
                      SummaryInfo::FUNCTION_DATA
                     ]);
            }
            if ($dirSummary->type() eq 'directory') {
                # use the basename (not the path or full path) in the
                #  secondary key list:
                #  - these are files in the current directory, so we lose
                #    no information by eliding the directory part
                $name = File::Basename::basename($name);
            }
            $name = apply_prefix($name, @dir_prefix);
            write_file_table_entry(*HTML_HANDLE, $base_dir,
                                   # 'owner' page type - no span, no page link
                                   [$name,
                                    $fileInfo,
                                    $activeTlaCols,
                                    1,
                                    $primary_key,
                                    1 + $elide_secondary_header,
                                    $fileview,
                                    $entry_type,
                                   ],
                                   @ownerColData);
        }

        next
            unless ($show_details &&
                    defined($perTestcaseData) &&
                    $primary_key eq "name");

        # we know that the top-level callback item must hold a file.
        my $filename = $primaryCb->name();

        my ($testhash, $testfnchash, $testbrhash, $testcase_mcdc) =
            @$perTestcaseData;

        my $testdata = $testhash->{$filename};

        # Check whether we should write test specific coverage as well
        next if (!defined($testdata));

        my $testfncdata = $testfnchash->{$filename};
        my $testbrdata  = $testbrhash->{$filename};
        my $test_mcdc   = $testcase_mcdc->{$filename};

        # Filter out those tests that actually affect this file
        my $affecting_tests =
            get_affecting_tests($testdata, $testfncdata,
                                $testbrdata, $test_mcdc);

        # Does any of the tests affect this file at all?
        if (!%$affecting_tests) { next; }

        while (my ($testname, $data) = each(%$affecting_tests)) {

            ($found, $hit, $fn_found, $fn_hit, $br_found, $br_hit, $mcdc_found,
             $mcdc_hit) = @$data;

            my $showgraph = 0;
            my @results;
            push(@results,
                 [$found, $hit,
                  SummaryInfo::LINE_DATA,
                  TestcaseTlaCount->new(
                                      $testdata->value($testname), $fileDetails,
                                      SummaryInfo::LINE_DATA)
                 ]);
            # there might not be optional test data for some tests - that type was
            #  not enabled
            push(@results,
                 [$mcdc_found,
                  $mcdc_hit,
                  SummaryInfo::MCDC_DATA,
                  TestcaseTlaCount->new(
                                     $test_mcdc->value($testname), $fileDetails,
                                     SummaryInfo::MCDC_DATA)
                 ])
                if ($hasMcdcData &&
                    defined($test_mcdc->value($testname)));

            push(@results,
                 [$br_found,
                  $br_hit,
                  SummaryInfo::BRANCH_DATA,
                  TestcaseTlaCount->new(
                                    $testbrdata->value($testname), $fileDetails,
                                    SummaryInfo::BRANCH_DATA)
                 ])
                if ($hasBranchData &&
                    defined($testbrdata->value($testname)));

            push(@results,
                 [$fn_found,
                  $fn_hit,
                  SummaryInfo::FUNCTION_DATA,
                  TestcaseTlaCount->new(
                                   $testfncdata->value($testname), $fileDetails,
                                   SummaryInfo::FUNCTION_DATA)
                 ])
                if ($includeFunctionColumns &&
                    defined($testfncdata->value($testname)));

            my $href = $testname;
            # Insert link to description of available
            if ($test_description{$testname}) {
                $href =
                    "<a href=\"$base_dir" . "descriptions.$html_ext#T" .
                    escape_id($testname) . "\">" . "$testname</a>";
            }
            write_file_table_detail_entry(*HTML_HANDLE, $base_dir,
                               $href, $showBinDetail, $activeTlaCols, @results);
        }
    }
    foreach my $note (
        [   $suppressedEmptyRow,
            "<sup>&lowast;</sup> 'Detail' entries with no 'missed' coverpoints are elided.  Use the '--show-owners all' flag to retain them."
        ],
        [   $suppressedSecondaryHeader,
            '<sup>&lowast;&lowast;</sup> Bin \'Total\' header elided when bin contains only one entry.'
        ],
        [   $lcovutil::func_coverage && !$includeFunctionColumns,
            "Note:  'Function Coverage' columns elided as function owner is not identified."
        ]
    ) {
        next unless $note->[0];
        write_html(*HTML_HANDLE, <<END_OF_HTML);
        <tr>
          <td class="footnote" colspan=$num_columns>$note->[1]</td>
         </tr>
END_OF_HTML

    }

    write_file_table_epilog(*HTML_HANDLE);
}

#
# get_affecting_tests(testdata, testfncdata, testbrdata, pertest_mcdc)
#
# HASHREF contains a mapping filename -> (linenumber -> exec count). Return
# a hash containing mapping filename -> "lines found, lines hit" for each
# filename which has a nonzero hit count.
#

sub get_affecting_tests($$$$)
{
    my ($testdata, $testfncdata, $testbrdata, $testmcdcdata) = @_;
    my %result;

    foreach my $testname ($testdata->keylist()) {
        # Get (line number -> count) hash for this test case
        my $testcount = $testdata->value($testname);
        my ($found, $hit) = $testcount->get_found_and_hit();
        # no other coverpoints are hit, if the line isn't hit
        next if $hit == 0;

        # Calculate sum
        my $testfnccount  = $testfncdata->value($testname);
        my $testbrcount   = $testbrdata->value($testname);
        my $testmcdccount = $testmcdcdata->value($testname);
        # might be no data for this testcase
        my ($fn_found, $fn_hit) =
            defined($testfnccount) ? $testfnccount->get_found_and_hit() :
            (0, 0);
        my ($br_found, $br_hit) =
            defined($testbrcount) ? $testbrcount->get_found_and_hit() : (0, 0);
        my ($mcdc_found, $mcdc_hit) =
            defined($testmcdccount) ? $testmcdccount->get_found_and_hit() :
            (0, 0);
        $result{$testname} = [$found, $hit, $fn_found, $fn_hit,
                              $br_found, $br_hit, $mcdc_found, $mcdc_hit
        ];
    }
    return \%result;
}

#
# write_source(filehandle, source_filename, count_data, checksum_data,
#              converted_data, func_data, sumbrcount, mcdc_summary)
#
# Write an HTML view of a source code file. Returns a list containing
# data as needed by gen_png().
#
# Die on error.
#

sub write_source($$$$$$$$)
{
    local *HTML_HANDLE = shift;
    my ($srcfile, $count_data, $checkdata, $fileCovInfo,
        $funcdata, $sumbrcount, $mcdc_summary) = @_;
    my @result;

    # suppress branch and MCDC columns if there is no data in the file
    my $showBranches = $lcovutil::br_coverage   && 0 != $sumbrcount->found();
    my $showMcdc     = $lcovutil::mcdc_coverage && 0 != $mcdc_summary->found();

    write_source_prolog(*HTML_HANDLE, $srcfile->isProjectFile(),
                        $showBranches, $showMcdc);
    my $line_number = 0;
    my $cbdata      = PrintCallback->new($srcfile, $fileCovInfo);

    my ($region, $empty);
    if ($SummaryInfo::selectCallback) {
        $region = InInterestingRegion->new($srcfile, $fileCovInfo->lineMap());
        $empty  = '';
        if ($srcfile->isProjectFile()) {
            $empty .= ' ' x $main::age_field_width;
            $empty .= ' ' x $main::owner_field_width;
        }
        # using 8 characters for line number field, with different
        # foreground/background - to make distinctive
        $empty .=
            '  <span class="lineNumWithDelete">' .
            (' ' x (8 - 3)) . '...</span>';
        $empty .= ' ' x $main::br_field_width if $showBranches;
        $empty .= ' ' x $main::mcdc_field_width if $showMcdc;
        $empty .= ' ' x $main::tla_field_width;
        $empty .= ' ' x ($line_field_width - 1);
        $empty .=
            '<span class="elidedSource">     (elided NUM_LINES ignored lines)</span>';
    }

    my $prevLine = 0;
    foreach my $srcline (@{$srcfile->lines()}) {
        $line_number++;
        $cbdata->lineNo($line_number);
        if (defined($region) && !$region->interesting($line_number)) {
            lcovutil::info(1, "skip line $line_number\n");
            next;
        }
        if ($prevLine != $line_number - 1) {
            $cbdata->age(-1, $line_number);
            $cbdata->owner(undef, $line_number);
            $cbdata->tla(undef, $line_number);

            lcovutil::info(1, "continuation_line $line_number\n");
            my $l = $empty;
            my $c = $line_number - ($prevLine + 1);
            $l =~ s/NUM_LINES/$c/;
            $l =~ s/lines/line/ if $c == 1;
            push(@result, $l);
            write_html(*HTML_HANDLE, $l . "\n");
        }
        $prevLine = $line_number;

        # Source code matches coverage data?
        lcovutil::ignorable_error($lcovutil::ERROR_MISMATCH,
                 "checksum mismatch at " . $srcfile->path() . ":$line_number\n")
            if (
               defined($checkdata->value($line_number)) &&
               ($checkdata->value($line_number) ne md5_base64($srcline->text()))
            );
        push(@result,
             write_source_line(HTML_HANDLE, $srcline,
                               $count_data->value($line_number), $showBranches,
                               $sumbrcount->value($line_number), $showMcdc,
                               $mcdc_summary->value($line_number), $cbdata));
    }
    if ($prevLine != $line_number) {
        # need another blank line at the bottom...
        my $l = $empty;
        my $c = $line_number - $prevLine;
        $l =~ s/NUM_LINES/$c/;
        push(@result, $l);
        write_html(*HTML_HANDLE, $l . "\n");
    }

    write_source_epilog(*HTML_HANDLE);
    return (@result);
}

sub funcview_get_label($$$$)
{
    my ($name, $base, $col, $sort_type) = @_;
    my $link;

    if (!defined($col)) {
        if ($sort_tables && $sort_type != $SORT_FILE) {
            $link = "$name.func.$html_ext";
        }
        return "Function Name" . get_sort_code($link, "function name", $base);
    } elsif ($col eq 'hit') {
        if ($sort_tables && $sort_type != $SORT_LINE) {
            $link = "$name.func-c.$html_ext";
        }
        return "Hit count" . get_sort_code($link, "function hit count", $base);
    } elsif ($col eq 'missed_line') {
        if ($sort_tables && $sort_type != $SORT_MISSING_LINE) {
            $link = "$name.func-l.$html_ext";
        }
        return "Lines" .
            get_sort_code($link, "unexercised lines in function", $base);
    } elsif ($col eq 'missed_mcdc') {
        if ($sort_tables && $sort_type != $SORT_MISSING_MCDC) {
            $link = "$name.func-m.$html_ext";
        }
        return "MC/DC"
            .
            get_sort_code($link, "unexercised MC/DC expressions in function",
                          $base);
    } else {
        die("unexpected sort $col") unless ($col eq 'missed_branch');
        if ($sort_tables && $sort_type != $SORT_MISSING_BRANCH) {
            $link = "$name.func-b.$html_ext";
        }
        return "Branches" .
            get_sort_code($link, "unexercised branches in function", $base);
    }
}

#
# funcview_get_sorted(funcdata, sort_type, mergedView)
#
# Depending on the value of sort_type, return a list of functions sorted
# by name (type 0) or by the associated call count (type 1).
#

sub funcview_get_sorted($$$)
{
    my ($funcData, $type, $merged) = @_;

    my ($differential_func, $lineFuncData, $branchFuncData, $mcdcFuncData) =
        @$funcData;
    my @rtn = keys(%$differential_func);

    if ($type == $SORT_LINE) {
        @rtn = sort({
                        my $da = $differential_func->{$a}->hit();
                        my $db = $differential_func->{$b}->hit();
                        $da->[0] <=> $db->[0] or
                            # sort by function name if count matches
                            $a cmp $b
        } @rtn);
    } elsif ($type == $SORT_MISSING_LINE) {
        @rtn = sort({
                        my $linea =
                            exists($lineFuncData->{$a}) ? $lineFuncData->{$a} :
                            undef;
                        my $lineb =
                            exists($lineFuncData->{$b}) ? $lineFuncData->{$b} :
                            undef;
                        my $missedA =
                            defined($linea) ? $linea->[0] - $linea->[1] : -1;
                        my $missedB =
                            defined($lineb) ? $lineb->[0] - $lineb->[1] : -1;

                        # highest to lowest 'missed lines', then by
                        # function name if count matches
                        $missedB <=> $missedA or $a cmp $b
        } @rtn);
    } elsif ($type == $SORT_MISSING_BRANCH) {
        @rtn = sort({
                        my $bra =
                            exists($branchFuncData->{$a}) ?
                            $branchFuncData->{$a} :
                            undef;
                        my $brb =
                            exists($branchFuncData->{$b}) ?
                            $branchFuncData->{$b} :
                            undef;
                        my $missedA =
                            defined($bra) ? $bra->[0] - $bra->[1] : -1;
                        my $missedB =
                            defined($brb) ? $brb->[0] - $brb->[1] : -1;

                        # highest to lowest 'missed branches', then by
                        # function name if count matches
                        $missedB <=> $missedA or $a cmp $b
        } @rtn);
    } elsif ($type == $SORT_MISSING_MCDC) {
        @rtn = sort({
                        my $bra =
                            exists($mcdcFuncData->{$a}) ?
                            $mcdcFuncData->{$a} :
                            undef;
                        my $brb =
                            exists($mcdcFuncData->{$b}) ?
                            $mcdcFuncData->{$b} :
                            undef;
                        my $missedA =
                            defined($bra) ? $bra->[0] - $bra->[1] : -1;
                        my $missedB =
                            defined($brb) ? $brb->[0] - $brb->[1] : -1;

                        # highest to lowest 'missed expressions', then by
                        # function name if count matches
                        $missedB <=> $missedA or $a cmp $b
        } @rtn);

    } elsif (!defined($main::no_sort)) {
        # sort alphabetically by function name
        @rtn = sort(@rtn);
    }
    return @rtn;
}

#
# write_function_table(filehandle, funcData, source_file, sumcount, funcdata,
#                      testfncdata, sumbrcount, testbrdata, mcdc_summary, testcase_mcdc,
#                      base_name, base_dir, sort_type)
#
# Write an HTML table listing all functions in a source file, including
# also function call counts and line coverages inside of each function.
#
# Die on error.
#

sub write_function_table(*$$$$$$$$$$$$)
{
    local *HTML_HANDLE = shift;
    my ($funcData, $source, $sumcount, $funcdata,
        $testfncdata, $sumbrcount, $testbrdata, $mcdc_summary,
        $testcase_mcdc, $name, $base, $type) = @_;
    my ($differentialMap, $funcLineCovMap, $funcBranchCovMap, $funcMcdcCovMap)
        = @$funcData;
    # Get HTML code for headings
    my $func_code   = funcview_get_label($name, $base, undef, $type);
    my $count_code  = funcview_get_label($name, $base, 'hit', $type);
    my $line_code   = funcview_get_label($name, $base, 'missed_line', $type);
    my $branch_code = funcview_get_label($name, $base, 'missed_branch', $type);
    my $mcdc_code   = funcview_get_label($name, $base, 'missed_mcdc', $type);
    my $showTlas    = $main::show_tla && 0 != scalar(keys %$differentialMap);
    my $tlaRow      = "";
    my $lineProportionRow   = '';
    my $branchProportionRow = '';
    my $mcdcProportionRow   = '';
    my $countWidth          = 20;

    if ($showTlas) {
        my $label = $main::use_legacyLabels ? 'Hit?' : 'TLA';
        $tlaRow     = "<td width=\"10%\" class=\"tableHead\">$label</td>";
        $countWidth = 10;
    }
    if ($main::show_functionProportions) {
        $lineProportionRow = "<td class=\"tableHead\">$line_code</td>"
            if (%$funcLineCovMap);
        $branchProportionRow = "<td class=\"tableHead\">$branch_code</td>"
            if $lcovutil::br_coverage && %$funcBranchCovMap;
        $mcdcProportionRow = "<td class=\"tableHead\">$mcdc_code</td>"
            if $lcovutil::mcdc_coverage && %$funcMcdcCovMap;
    }

    die("no functions in file") unless (%$differentialMap);
    write_html(*HTML_HANDLE, <<END_OF_HTML);
          <center>
          <table cellpadding=1 cellspacing=1 border=0>
            <tr><td><br></td></tr>
            <tr>
              <td class="tableHead">$func_code</td>
              $tlaRow
              <td class="tableHead">$count_code</td>
              $lineProportionRow
              $branchProportionRow
              $mcdcProportionRow
            </tr>
END_OF_HTML

    foreach my $func (
         funcview_get_sorted($funcData, $type, $main::merge_function_aliases)) {

        my $funcEntry = $differentialMap->{$func};
        my ($count, $tla) = @{$funcEntry->hit()};
        next
            if grep(/^$tla$/, ('DUB', 'DCB')); # don't display deleted functions
        my $startline = $funcEntry->line() - $func_offset;
        my $name      = simplify_function_name($func);
        my $countstyle;

        # Escape special characters
        $name = escape_html($name);
        if ($startline < 1) {
            $startline = 1;
        }
        if ($count == 0) {
            $countstyle = "coverFnLo";
        } else {
            $countstyle = "coverFnHi";
        }
        my $tlaRow              = "";
        my $lineProportionRow   = '';
        my $branchProportionRow = '';
        my $mcdcProportionRow   = '';
        if ($showTlas) {
            my $label =
                $main::use_legacyLabels ? $SummaryInfo::tlaToLegacy{$tla} :
                $tla;
            $tlaRow = "<td class=\"tla$tla\">$label</td>";
        }
        if ($main::show_functionProportions) {
            if (exists($funcLineCovMap->{$func})) {
                my ($found, $hit) = @{$funcLineCovMap->{$func}};
                # colorize based on hit proportion
                my $style =
                    $rate_name[classify_rate($found, $hit, $ln_med_limit,
                                             $ln_hi_limit)];
                my $rate = rate($hit, $found, "&nbsp;%");
                $lineProportionRow =
                    "<td class=coverPer$style>$rate ($hit / $found)</td>";
            } else {
                $lineProportionRow = "<td class=\"coverFn\"></td>"
                    if %$funcLineCovMap;
            }
            if ($lcovutil::br_coverage && %$funcBranchCovMap) {
                if (exists($funcBranchCovMap->{$func})) {
                    my ($found, $hit) = @{$funcBranchCovMap->{$func}};
                    # colorize based on hit proportion
                    my $style = $rate_name[
                        classify_rate($found, $hit, $br_med_limit, $br_hi_limit)
                    ];
                    my $rate = rate($hit, $found, "&nbsp;%");
                    $branchProportionRow =
                        "<td class=coverPer$style>$rate ($hit / $found)</td>";
                } else {
                    $branchProportionRow = "<td class=\"coverFn\"></td>";
                }
            }
            if ($lcovutil::mcdc_coverage && %$funcMcdcCovMap) {
                if (exists($funcMcdcCovMap->{$func})) {
                    my ($found, $hit) = @{$funcMcdcCovMap->{$func}};
                    # colorize based on hit proportion
                    my $style = $rate_name[
                        classify_rate($found, $hit, $mcdc_med_limit,
                                      $mcdc_hi_limit)
                    ];
                    my $rate = rate($hit, $found, "&nbsp;%");
                    $mcdcProportionRow =
                        "<td class=coverPer$style>$rate ($hit / $found)</td>";
                } else {
                    $mcdcProportionRow = "<td class=\"coverFn\"></td>";
                }
            }
        }

        write_html(*HTML_HANDLE, <<END_OF_HTML);
            <tr>
              <td class="coverFn"><a href="$source#L$startline">$name</a></td>
              $tlaRow
              <td class="$countstyle">$count</td>
              $lineProportionRow
              $branchProportionRow
              $mcdcProportionRow
            </tr>
END_OF_HTML

        if ((!defined($main::suppress_function_aliases) ||
             0 == $main::suppress_function_aliases) &&
            $funcEntry->numAliases() > 1
        ) {
            my $aliases   = $funcEntry->aliases();
            my @aliasList = keys(%$aliases);
            if ($main::show_functionProportions) {
                $lineProportionRow = "<td class=\"coverFn\"></td>"
                    if %$funcLineCovMap;
                $branchProportionRow = $lineProportionRow
                    if $lcovutil::br_coverage && %$funcBranchCovMap;
                $mcdcProportionRow = $lineProportionRow
                    if $lcovutil::mcdc_coverage && %$funcMcdcCovMap;
            }
            if (0 != $type) {
                @aliasList =
                    sort({
                             my $da = $aliases->{$a};
                             my $db = $aliases->{$b};
                             $da->[0] <=> $db->[0] or $a cmp $b
                    } @aliasList);
            } else {
                @aliasList = sort(@aliasList);
            }
            foreach my $alias (@aliasList) {
                my ($hit, $tla) = @{$aliases->{$alias}};
                # don't display deleted functions
                next if grep(/^$tla$/, ('DUB', 'DCB'));    #
                my $style = "coverFnAlias" . ($hit == 0 ? "Lo" : "Hi");
                $tlaRow = "";
                if ($showTlas) {
                    my $label =
                        $main::use_legacyLabels ?
                        $SummaryInfo::tlaToLegacy{$tla} :
                        $tla;
                    $tlaRow = "<td class=\"tla$tla\">$label</td>";
                }

                # Escape special characters
                $alias = escape_html(simplify_function_name($alias));

                write_html(*HTML_HANDLE, <<END_OF_HTML);
            <tr>
              <td class="coverFnAlias"><a href="$source#L$startline">$alias</a></td>
              $tlaRow
              <td class="$style">$hit</td>
              $lineProportionRow
              $branchProportionRow
              $mcdcProportionRow
            </tr>
END_OF_HTML
            }
        }
    }
    write_html(*HTML_HANDLE, <<END_OF_HTML);
          </table>
          <br>
          </center>
END_OF_HTML
}

#
# remove_unused_descriptions()
#
# Removes all test descriptions from the global hash %test_description which
# are not present in %current_data.
#

sub remove_unused_descriptions()
{
    my $filename;     # The current filename
    my %test_list;    # Hash containing found test names
    my $test_data;    # Reference to hash test_name -> count_data
    my $before;       # Initial number of descriptions
    my $after;        # Remaining number of descriptions

    $before = scalar(keys(%test_description));

    foreach $filename ($current_data->files()) {
        ($test_data) = $current_data->data($filename)->get_info();
        foreach ($test_data->keylist()) {
            $test_list{$_} = "";
        }
    }

    # Remove descriptions for tests which are not in our list
    foreach (keys(%test_description)) {
        if (!exists($test_list{$_})) {
            delete($test_description{$_});
        }
    }

    $after = scalar(keys(%test_description));
    if ($after < $before) {
        info("Removed " . ($before - $after) .
             " unused descriptions, $after remaining.\n");
    }
}

#
# apply_prefix(filename, PREFIXES)
#
# If FILENAME begins with PREFIX from PREFIXES, remove PREFIX from FILENAME
# and return resulting string, otherwise return FILENAME.
#

sub apply_prefix($@)
{
    my $filename = shift;

    foreach my $prefix (@_) {
        if ($prefix eq $filename) {
            return 'root';
        }
        if ($prefix ne "" &&
            $filename =~ /^\Q$prefix$lcovutil::dirseparator\E(.*)$/) {
            return substr($filename, length($prefix) + 1);
        }
    }

    return $filename;
}

#
# get_html_prolog(FILENAME)
#
# If FILENAME is defined, return contents of file. Otherwise return default
# HTML prolog. Die on error.
#

sub get_html_prolog($)
{
    my $filename = $_[0];
    my $result   = "";

    if (defined($filename)) {
        local *HANDLE;

        open(HANDLE, "<", $filename) or
            die("cannot open html prolog $filename: $!\n");
        while (<HANDLE>) {
            $result .= $_;
        }
        close(HANDLE) or die("unable to close HTML handle: $!\n");
    } else {
        $result = <<END_OF_HTML;
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

<html lang="en">

<head>
  <meta http-equiv="Content-Type" content="text/html; charset=$charset">
  <title>\@pagetitle\@</title>
  <link rel="stylesheet" type="text/css" href="\@basedir\@gcov.css">
</head>

<body>

END_OF_HTML
    }
    return $result;
}

#
# get_html_epilog(FILENAME)
#
# If FILENAME is defined, return contents of file. Otherwise return default
# HTML epilog. Die on error.
#
sub get_html_epilog($)
{
    my $filename = $_[0];
    my $result   = "";

    if (defined($filename)) {
        local *HANDLE;

        open(HANDLE, "<", $filename) or
            die("cannot open html epilog $filename: $!\n");
        while (<HANDLE>) {
            $result .= $_;
        }
        close(HANDLE) or die("unable to close HTML handle: $!\n");
    } else {
        $result = <<END_OF_HTML;

</body>
</html>
END_OF_HTML
    }

    return $result;
}

#
# parse_dir_prefix(@dir_prefix)
#
# Parse user input about the prefix list
#

sub parse_dir_prefix(@)
{
    my (@opt_dir_prefix) = @_;

    return if (!@opt_dir_prefix);

    foreach my $item (@opt_dir_prefix) {
        if ($item =~ /$lcovutil::split_char/) {
            # Split and add comma-separated parameters
            push(@dir_prefix, split($lcovutil::split_char, $item));
        } else {
            # Add single parameter
            push(@dir_prefix, $item);
        }
    }
}

#
# simplify_function_name($name)
#
# apply @function_simplify_patterns to $name and return
#  goal is to shorten really long demangled names/template expansions
#
sub simplify_function_name($)
{
    my $name = shift;
    if ($simplifyFunctionCallback) {

        eval { $name = $simplifyFunctionCallback->simplify($name); };
        if ($@) {
            my $context = MessageContext::context();
            lcovutil::ignorable_error($lcovutil::ERROR_CALLBACK,
                                      "simplify($name) failed$context: $@");
        }
    }
    return $name;
}
