####################################################################################################################################
# Classify files and generate code totals
####################################################################################################################################
package pgBackRestTest::Common::CodeCountTest;

####################################################################################################################################
# Perl includes
####################################################################################################################################
use strict;
use warnings FATAL => qw(all);
use Carp qw(confess);
use English '-no_match_vars';

use Exporter qw(import);
    our @EXPORT = qw();

use pgBackRestDoc::Common::Log;

use pgBackRestTest::Common::ExecuteTest;

####################################################################################################################################
# Scan all files and assign types
####################################################################################################################################
sub codeCountScan
{
    my $oStorage = shift;
    my $strBasePath = shift;

    # Load YAML
    require YAML::XS;
    YAML::XS->import(qw(Load Dump));

    my $hCodeCount = {};

    # Build manifest of all files
    my $hManifest = $oStorage->manifest($strBasePath);
    my $strYamlDetail = undef;

    foreach my $strFile (sort(keys(%{$hManifest})))
    {
        # Only interested in files
        next if $hManifest->{$strFile}{type} ne 'f';

        # Exclude these directories/files entirely
        next if ($strFile =~ '^\.' ||
                 $strFile =~ '\.DS_Store$' ||
                 $strFile =~ '\.gitignore$' ||
                 $strFile =~ '\.md$' ||
                 $strFile =~ '\.log$' ||
                 $strFile eq 'LICENSE' ||
                 $strFile =~ '^doc/example/' ||
                 $strFile =~ '^doc/output/' ||
                 $strFile =~ '^doc/resource/fake\-cert' ||
                 $strFile =~ '\.png$' ||
                 $strFile =~ '\.cache$' ||
                 $strFile =~ '^doc/site/' ||
                 $strFile =~ '^src/build/autom4te.cache/' ||
                 $strFile eq 'test/Dockerfile' ||
                 $strFile eq 'test/Vagrantfile' ||
                 $strFile =~ '^test/\.vagrant/' ||
                 $strFile =~ '^test/certificate/' ||
                 $strFile =~ '^test/code-count/' ||
                 $strFile =~ '^test/data/' ||
                 $strFile =~ '^test/patch/' ||
                 $strFile =~ '^test/result/' ||
                 $strFile =~ '^test/scratch' ||
                 $strFile =~ '^test/src/valgrind\.suppress\.' ||
                 $strFile eq 'test/uncrustify.cfg');

        # Classify the source file
        my $strClass = 'test/harness';

        if ($strFile =~ '^doc/xml/' || $strFile eq 'doc/manifest.xml')
        {
            $strClass = 'doc/source';
        }
        elsif ($strFile =~ '^doc/' || $strFile =~ '^doc/resource/')
        {
            $strClass = 'doc/core';
        }
        elsif ($strFile =~ '^build/' || $strFile =~ '^src/build/' || $strFile =~ 'meson\.build$' ||
               $strFile =~ 'meson_options\.txt$')
        {
            $strClass = 'build';
        }
        elsif ($strFile =~ '^test/lib/pgBackRestTest/Module/' || $strFile =~ '^test/src/module/')
        {
            $strClass = 'test/module';
        }
        elsif ($strFile =~ '^src/')
        {
            $strClass = 'core';
        }

        # Append auto if an auto-generated file
        if ($strFile =~ '\.auto\..$' || $strFile =~ '\.auto\..\.inc$' || $strFile =~ 'Auto\.pm$')
        {
            $strClass .= '/auto';
        }

        # Append vendor if a vendorized file
        if ($strFile =~ '\.vendor\..$' || $strFile =~ '\.vendor\..\.inc$')
        {
            $strClass .= '/vendor';
        }

        # Force unrecognized file types
        my $strForceLang = undef;
        my $strType = undef;

        if ($strFile =~ '\.xs$')
        {
            $strType = 'xs';
            $strForceLang = 'XS';
        }
        elsif ($strFile =~ '\.h$' || $strFile =~ '\.h\.in$' || $strFile =~ '\.xsh$')
        {
            $strType = 'c/h';
            $strForceLang = 'C/C++ Header';
        }
        elsif ($strFile =~ '\.c$' || $strFile =~ '\.c.inc$')
        {
            $strType = 'c';
            $strForceLang = 'C';
        }
        elsif ($strFile =~ '\.t$' || $strFile =~ '\.pl$' || $strFile =~ '\.pm$' || $strFile =~ '\.PL$')
        {
            $strType = 'perl';
            $strForceLang = 'Perl';
        }
        elsif ($strFile =~ '\.yaml$')
        {
            $strType = 'yaml';
            $strForceLang = 'YAML';
        }
        elsif ($strFile =~ 'meson\.build$' || $strFile =~ 'meson_options\.txt$')
        {
            $strType = 'meson';
            $strForceLang = 'make';
        }
        elsif ($strFile =~ '\.xml$')
        {
            $strType = 'xml';
            $strForceLang = 'XML';
        }
        elsif ($strFile =~ '\.css$')
        {
            $strType = 'css';
            $strForceLang = 'CSS';
        }
        elsif ($strFile =~ '\.dtd$')
        {
            $strType = 'dtd';
            $strForceLang = 'DTD';
        }
        else
        {
            confess &log(ERROR, "unable to map type for ${strFile}");
        }

        # Load the file counts
        my $strYaml = executeTest(
            "cloc --yaml ${strBasePath}/${strFile}" .
            " --force-lang='${strForceLang}'", {bSuppressStdErr => true});

        # Error if the file was ignored
        if ($strYaml =~ '1 file ignored')
        {
            confess &log(ERROR, "file type for ${strBasePath}/${strFile} not recognized:\n${strYaml}");
        }

        # Clean out extraneous keys
        my $hFileCount = Load($strYaml);
        delete($hFileCount->{header});
        delete($hFileCount->{SUM});

        # There should only be one key - the file type
        if (keys(%{$hFileCount}) != 1)
        {
            confess &log(ERROR, "more than one file type in ${strBasePath}/${strFile}:\n" . Dump($hFileCount));
        }

        # Translate type
        my ($strTypeRaw) = keys(%{$hFileCount});
        $hFileCount = $hFileCount->{$strTypeRaw};

        # Append to yaml file
        $strYamlDetail .=
            (defined($strYamlDetail) ? "\n" : '') .
            "${strFile}:\n" .
            "  class: ${strClass}\n" .
            "  type: ${strType}\n";

        # Summarize
        $hCodeCount->{$strClass}{$strType}{code} += $hFileCount->{code};
        $hCodeCount->{$strClass}{$strType}{comment} += $hFileCount->{comment};

        $hCodeCount->{$strClass}{total}{code} += $hFileCount->{code};
        $hCodeCount->{$strClass}{total}{comment} += $hFileCount->{comment};

        $hCodeCount->{total}{code} += $hFileCount->{code};
        $hCodeCount->{total}{comment} += $hFileCount->{comment};
    }

    # Save the file
    $oStorage->put(
        'test/code-count/file-type.yaml',
        "# File types for source files in the project\n" . $strYamlDetail);

    # Reload the file to make sure there aren't any formatting issues
    Load(${$oStorage->get('test/code-count/file-type.yaml')});

    # Display code count summary
    &log(INFO, "code count summary:\n" . Dump($hCodeCount));
}

push @EXPORT, qw(codeCountScan);

1;
