<?php declare(strict_types=1);
/*
 * This file is part of phpunit/php-code-coverage.
 *
 * (c) Sebastian Bergmann <sebastian@phpunit.de>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */
namespace SebastianBergmann\CodeCoverage\Node;

use function array_merge;
use function assert;
use function count;
use function max;
use IteratorAggregate;
use RecursiveIteratorIterator;
use SebastianBergmann\CodeCoverage\Data\ProcessedClassType;
use SebastianBergmann\CodeCoverage\Data\ProcessedFunctionType;
use SebastianBergmann\CodeCoverage\Data\ProcessedTraitType;
use SebastianBergmann\CodeCoverage\StaticAnalysis\LinesOfCode;
use SebastianBergmann\CodeCoverage\Test\TestSizes;

/**
 * @template-implements IteratorAggregate<int, AbstractNode>
 *
 * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
 *
 * @no-named-arguments Parameter names are not covered by the backward compatibility promise for phpunit/php-code-coverage
 *
 * @phpstan-import-type TestSizeSet from TestSizes
 * @phpstan-import-type TestSizeCounts from TestSizes
 */
final class Directory extends AbstractNode implements IteratorAggregate
{
    /**
     * @var list<Directory|File>
     */
    private array $children = [];

    /**
     * @var list<Directory>
     */
    private array $directories = [];

    /**
     * @var list<File>
     */
    private array $files = [];

    /**
     * @var ?array<string, ProcessedClassType>
     */
    private ?array $classes = null;

    /**
     * @var ?array<string, ProcessedTraitType>
     */
    private ?array $traits = null;

    /**
     * @var ?array<string, ProcessedFunctionType>
     */
    private ?array $functions         = null;
    private ?LinesOfCode $linesOfCode = null;
    private int $numFiles             = -1;
    private int $numExecutableLines   = -1;
    private int $numExecutedLines     = -1;

    /**
     * @var ?TestSizeCounts
     */
    private ?array $numExecutedLinesByTestSize     = null;
    private int $numExecutableBranches             = -1;
    private int $numExecutedBranches               = -1;
    private int $numExecutablePaths                = -1;
    private int $numExecutedPaths                  = -1;
    private int $numFilesWithoutBranchCoverageData = -1;
    private int $numClasses                        = -1;
    private int $numTestedClasses                  = -1;

    /**
     * @var ?TestSizeCounts
     */
    private ?array $numTestedClassesByTestSize = null;
    private int $numTraits                     = -1;
    private int $numTestedTraits               = -1;

    /**
     * @var ?TestSizeCounts
     */
    private ?array $numTestedTraitsByTestSize = null;
    private int $numMethods                   = -1;
    private int $numTestedMethods             = -1;

    /**
     * @var ?TestSizeCounts
     */
    private ?array $numTestedMethodsByTestSize = null;
    private int $numFunctions                  = -1;
    private int $numTestedFunctions            = -1;

    /**
     * @var ?TestSizeCounts
     */
    private ?array $numTestedFunctionsByTestSize = null;

    /**
     * @return non-negative-int
     */
    public function count(): int
    {
        if ($this->numFiles === -1) {
            $this->numFiles = 0;

            foreach ($this->children as $child) {
                $this->numFiles += count($child);
            }
        }

        return max(0, $this->numFiles);
    }

    /**
     * @return RecursiveIteratorIterator<Iterator>
     */
    public function getIterator(): RecursiveIteratorIterator
    {
        return new RecursiveIteratorIterator(
            new Iterator($this),
            RecursiveIteratorIterator::SELF_FIRST,
        );
    }

    public function addDirectory(string $name): self
    {
        $directory = new self($name, $this);

        assert($directory instanceof self);

        $this->children[]    = $directory;
        $this->directories[] = $directory;

        return $directory;
    }

    public function addFile(File $file): void
    {
        $this->children[] = $file;
        $this->files[]    = $file;

        $this->numExecutableLines         = -1;
        $this->numExecutedLines           = -1;
        $this->numExecutedLinesByTestSize = null;
    }

    /**
     * @return list<Directory>
     */
    public function directories(): array
    {
        return $this->directories;
    }

    /**
     * @return list<File>
     */
    public function files(): array
    {
        return $this->files;
    }

    /**
     * @return list<Directory|File>
     */
    public function children(): array
    {
        return $this->children;
    }

    /**
     * @return array<string, ProcessedClassType>
     */
    public function classes(): array
    {
        if ($this->classes === null) {
            $this->classes = [];

            foreach ($this->children as $child) {
                $this->classes = array_merge(
                    $this->classes,
                    $child->classes(),
                );
            }
        }

        return $this->classes;
    }

    /**
     * @return array<string, ProcessedTraitType>
     */
    public function traits(): array
    {
        if ($this->traits === null) {
            $this->traits = [];

            foreach ($this->children as $child) {
                $this->traits = array_merge(
                    $this->traits,
                    $child->traits(),
                );
            }
        }

        return $this->traits;
    }

    /**
     * @return array<string, ProcessedFunctionType>
     */
    public function functions(): array
    {
        if ($this->functions === null) {
            $this->functions = [];

            foreach ($this->children as $child) {
                $this->functions = array_merge(
                    $this->functions,
                    $child->functions(),
                );
            }
        }

        return $this->functions;
    }

    public function linesOfCode(): LinesOfCode
    {
        if ($this->linesOfCode === null) {
            $linesOfCode           = 0;
            $commentLinesOfCode    = 0;
            $nonCommentLinesOfCode = 0;

            foreach ($this->children as $child) {
                $childLinesOfCode = $child->linesOfCode();

                $linesOfCode           += $childLinesOfCode->linesOfCode();
                $commentLinesOfCode    += $childLinesOfCode->commentLinesOfCode();
                $nonCommentLinesOfCode += $childLinesOfCode->nonCommentLinesOfCode();
            }

            $this->linesOfCode = new LinesOfCode($linesOfCode, $commentLinesOfCode, $nonCommentLinesOfCode);
        }

        return $this->linesOfCode;
    }

    /**
     * @return non-negative-int
     */
    public function numberOfExecutableLines(): int
    {
        if ($this->numExecutableLines === -1) {
            $this->numExecutableLines = 0;

            foreach ($this->children as $child) {
                $this->numExecutableLines += $child->numberOfExecutableLines();
            }
        }

        return max(0, $this->numExecutableLines);
    }

    /**
     * @return non-negative-int
     */
    public function numberOfExecutedLines(): int
    {
        if ($this->numExecutedLines === -1) {
            $this->numExecutedLines = 0;

            foreach ($this->children as $child) {
                $this->numExecutedLines += $child->numberOfExecutedLines();
            }
        }

        return max(0, $this->numExecutedLines);
    }

    /**
     * @param TestSizeSet $testSizes
     */
    public function numberOfExecutedLinesByTestSize(int $testSizes): int
    {
        if ($this->numExecutedLinesByTestSize === null) {
            $this->numExecutedLinesByTestSize = TestSizes::ZERO_COUNTS;

            foreach ($this->children as $child) {
                foreach (TestSizes::COMBINATIONS as $combination) {
                    $this->numExecutedLinesByTestSize[$combination] += $child->numberOfExecutedLinesByTestSize($combination);
                }
            }
        }

        return $this->numExecutedLinesByTestSize[$testSizes];
    }

    /**
     * @return non-negative-int
     */
    public function numberOfExecutableBranches(): int
    {
        if ($this->numExecutableBranches === -1) {
            $this->numExecutableBranches = 0;

            foreach ($this->children as $child) {
                $this->numExecutableBranches += $child->numberOfExecutableBranches();
            }
        }

        return max(0, $this->numExecutableBranches);
    }

    /**
     * @return non-negative-int
     */
    public function numberOfExecutedBranches(): int
    {
        if ($this->numExecutedBranches === -1) {
            $this->numExecutedBranches = 0;

            foreach ($this->children as $child) {
                $this->numExecutedBranches += $child->numberOfExecutedBranches();
            }
        }

        return max(0, $this->numExecutedBranches);
    }

    /**
     * @return non-negative-int
     */
    public function numberOfExecutablePaths(): int
    {
        if ($this->numExecutablePaths === -1) {
            $this->numExecutablePaths = 0;

            foreach ($this->children as $child) {
                $this->numExecutablePaths += $child->numberOfExecutablePaths();
            }
        }

        return max(0, $this->numExecutablePaths);
    }

    /**
     * @return non-negative-int
     */
    public function numberOfExecutedPaths(): int
    {
        if ($this->numExecutedPaths === -1) {
            $this->numExecutedPaths = 0;

            foreach ($this->children as $child) {
                $this->numExecutedPaths += $child->numberOfExecutedPaths();
            }
        }

        return max(0, $this->numExecutedPaths);
    }

    /**
     * @return non-negative-int
     */
    public function numberOfFilesWithoutBranchCoverageData(): int
    {
        if ($this->numFilesWithoutBranchCoverageData === -1) {
            $this->numFilesWithoutBranchCoverageData = 0;

            foreach ($this->children as $child) {
                $this->numFilesWithoutBranchCoverageData += $child->numberOfFilesWithoutBranchCoverageData();
            }
        }

        return max(0, $this->numFilesWithoutBranchCoverageData);
    }

    public function numberOfClasses(): int
    {
        if ($this->numClasses === -1) {
            $this->numClasses = 0;

            foreach ($this->children as $child) {
                $this->numClasses += $child->numberOfClasses();
            }
        }

        return $this->numClasses;
    }

    public function numberOfTestedClasses(): int
    {
        if ($this->numTestedClasses === -1) {
            $this->numTestedClasses = 0;

            foreach ($this->children as $child) {
                $this->numTestedClasses += $child->numberOfTestedClasses();
            }
        }

        return $this->numTestedClasses;
    }

    /**
     * @param TestSizeSet $testSizes
     */
    public function numberOfTestedClassesByTestSize(int $testSizes): int
    {
        if ($this->numTestedClassesByTestSize === null) {
            $this->numTestedClassesByTestSize = TestSizes::ZERO_COUNTS;

            foreach ($this->children as $child) {
                foreach (TestSizes::COMBINATIONS as $combination) {
                    $this->numTestedClassesByTestSize[$combination] += $child->numberOfTestedClassesByTestSize($combination);
                }
            }
        }

        return $this->numTestedClassesByTestSize[$testSizes];
    }

    public function numberOfTraits(): int
    {
        if ($this->numTraits === -1) {
            $this->numTraits = 0;

            foreach ($this->children as $child) {
                $this->numTraits += $child->numberOfTraits();
            }
        }

        return $this->numTraits;
    }

    public function numberOfTestedTraits(): int
    {
        if ($this->numTestedTraits === -1) {
            $this->numTestedTraits = 0;

            foreach ($this->children as $child) {
                $this->numTestedTraits += $child->numberOfTestedTraits();
            }
        }

        return $this->numTestedTraits;
    }

    /**
     * @param TestSizeSet $testSizes
     */
    public function numberOfTestedTraitsByTestSize(int $testSizes): int
    {
        if ($this->numTestedTraitsByTestSize === null) {
            $this->numTestedTraitsByTestSize = TestSizes::ZERO_COUNTS;

            foreach ($this->children as $child) {
                foreach (TestSizes::COMBINATIONS as $combination) {
                    $this->numTestedTraitsByTestSize[$combination] += $child->numberOfTestedTraitsByTestSize($combination);
                }
            }
        }

        return $this->numTestedTraitsByTestSize[$testSizes];
    }

    public function numberOfMethods(): int
    {
        if ($this->numMethods === -1) {
            $this->numMethods = 0;

            foreach ($this->children as $child) {
                $this->numMethods += $child->numberOfMethods();
            }
        }

        return $this->numMethods;
    }

    public function numberOfTestedMethods(): int
    {
        if ($this->numTestedMethods === -1) {
            $this->numTestedMethods = 0;

            foreach ($this->children as $child) {
                $this->numTestedMethods += $child->numberOfTestedMethods();
            }
        }

        return $this->numTestedMethods;
    }

    /**
     * @param TestSizeSet $testSizes
     */
    public function numberOfTestedMethodsByTestSize(int $testSizes): int
    {
        if ($this->numTestedMethodsByTestSize === null) {
            $this->numTestedMethodsByTestSize = TestSizes::ZERO_COUNTS;

            foreach ($this->children as $child) {
                foreach (TestSizes::COMBINATIONS as $combination) {
                    $this->numTestedMethodsByTestSize[$combination] += $child->numberOfTestedMethodsByTestSize($combination);
                }
            }
        }

        return $this->numTestedMethodsByTestSize[$testSizes];
    }

    public function numberOfFunctions(): int
    {
        if ($this->numFunctions === -1) {
            $this->numFunctions = 0;

            foreach ($this->children as $child) {
                $this->numFunctions += $child->numberOfFunctions();
            }
        }

        return $this->numFunctions;
    }

    public function numberOfTestedFunctions(): int
    {
        if ($this->numTestedFunctions === -1) {
            $this->numTestedFunctions = 0;

            foreach ($this->children as $child) {
                $this->numTestedFunctions += $child->numberOfTestedFunctions();
            }
        }

        return $this->numTestedFunctions;
    }

    /**
     * @param TestSizeSet $testSizes
     */
    public function numberOfTestedFunctionsByTestSize(int $testSizes): int
    {
        if ($this->numTestedFunctionsByTestSize === null) {
            $this->numTestedFunctionsByTestSize = TestSizes::ZERO_COUNTS;

            foreach ($this->children as $child) {
                foreach (TestSizes::COMBINATIONS as $combination) {
                    $this->numTestedFunctionsByTestSize[$combination] += $child->numberOfTestedFunctionsByTestSize($combination);
                }
            }
        }

        return $this->numTestedFunctionsByTestSize[$testSizes];
    }
}
