<?php

namespace Psalm\Internal\Provider;

use Psalm\CodeLocation;
use Psalm\Codebase;
use Psalm\Internal\Analyzer\ClassLikeAnalyzer;
use Psalm\Internal\Analyzer\IssueData;
use Psalm\Internal\Codebase\Analyzer;
use UnexpectedValueException;

use function array_filter;
use function array_keys;
use function array_merge;
use function array_unique;
use function explode;

/**
 * Used to determine which files reference other files, necessary for using the --diff
 * option from the command line.
 *
 * @psalm-import-type FileMapType from Analyzer
 * @internal
 */
final class FileReferenceProvider
{
    private bool $loaded_from_cache = false;

    /**
     * A lookup table used for getting all the references to a class not inside a method
     * indexed by file
     *
     * @var array<string, array<string,bool>>
     */
    private static array $nonmethod_references_to_classes = [];

    /**
     * A lookup table used for getting all the methods that reference a class
     *
     * @var array<string, array<string,bool>>
     */
    private static array $method_references_to_classes = [];

    /**
     * A lookup table used for getting all the files that reference a class member
     *
     * @var array<string, array<string,bool>>
     */
    private static array $file_references_to_class_members = [];

    /**
     * A lookup table used for getting all the files that reference a class property
     *
     * @var array<string, array<string,bool>>
     */
    private static array $file_references_to_class_properties = [];

    /**
     * A lookup table used for getting all the files that reference a method's return value
     *
     * @var array<string, array<string,bool>>
     */
    private static array $file_references_to_method_returns = [];

    /**
     * A lookup table used for getting all the files that reference a missing class member
     *
     * @var array<string, array<string,bool>>
     */
    private static array $file_references_to_missing_class_members = [];

    /**
     * @var array<string, array<string, true>>
     */
    private static array $files_inheriting_classes = [];

    /**
     * A list of all files deleted since the last successful run
     *
     * @var array<int, string>|null
     */
    private static ?array $deleted_files = null;

    /**
     * A lookup table used for getting all the files referenced by a file
     *
     * @var array<string, array{a:array<int, string>, i:array<int, string>}>
     */
    private static array $file_references = [];

    /**
     * @var array<string, array<string, bool>>
     */
    private static array $method_references_to_class_members = [];

    /**
     * @var array<string, array<string, bool>>
     */
    private static array $method_dependencies = [];

    /**
     * @var array<string, array<string, bool>>
     */
    private static array $method_references_to_class_properties = [];

    /**
     * @var array<string, array<string, bool>>
     */
    private static array $method_references_to_method_returns = [];

    /**
     * @var array<string, array<string, bool>>
     */
    private static array $method_references_to_missing_class_members = [];

    /**
     * @var array<string, array<string, bool>>
     */
    private static array $references_to_mixed_member_names = [];

    /**
     * @var array<string, array<int, CodeLocation>>
     */
    private static array $class_method_locations = [];

    /**
     * @var array<string, array<int, CodeLocation>>
     */
    private static array $class_property_locations = [];

    /**
     * @var array<string, array<int, CodeLocation>>
     */
    private static array $class_locations = [];

    /**
     * @var array<string, string>
     */
    private static array $classlike_files = [];

    /**
     * @var array<string, array<string, int>>
     */
    private static array $analyzed_methods = [];

    /**
     * @var array<string, array<int, IssueData>>
     */
    private static array $issues = [];

    /**
     * @var array<string, FileMapType>
     */
    private static array $file_maps = [];

    /**
     * @var array<string, array{int, int}>
     */
    private static array $mixed_counts = [];

    /**
     * @var array<string, array<int, array<string, bool>>>
     */
    private static array $method_param_uses = [];

    private FileProvider $file_provider;
    public ?FileReferenceCacheProvider $cache = null;

    public function __construct(FileProvider $file_provider, ?FileReferenceCacheProvider $cache = null)
    {
        $this->file_provider = $file_provider;
        $this->cache = $cache;
    }

    /**
     * @return array<int, string>
     */
    public function getDeletedReferencedFiles(): array
    {
        if (self::$deleted_files === null) {
            self::$deleted_files = array_filter(
                array_keys(self::$file_references),
                fn(string $file_name): bool => !$this->file_provider->fileExists($file_name)
            );
        }

        return self::$deleted_files;
    }

    /**
     * @param lowercase-string $fq_class_name_lc
     */
    public function addNonMethodReferenceToClass(string $source_file, string $fq_class_name_lc): void
    {
        self::$nonmethod_references_to_classes[$fq_class_name_lc][$source_file] = true;
    }

    /**
     * @return array<string, array<string,bool>>
     */
    public function getAllNonMethodReferencesToClasses(): array
    {
        return self::$nonmethod_references_to_classes;
    }

    /**
     * @param array<string, array<string,bool>> $references
     */
    public function addNonMethodReferencesToClasses(array $references): void
    {
        foreach ($references as $key => $reference) {
            if (isset(self::$nonmethod_references_to_classes[$key])) {
                self::$nonmethod_references_to_classes[$key] = array_merge(
                    $reference,
                    self::$nonmethod_references_to_classes[$key],
                );
            } else {
                self::$nonmethod_references_to_classes[$key] = $reference;
            }
        }
    }

    /**
     * @param array<string, string> $map
     */
    public function addClassLikeFiles(array $map): void
    {
        self::$classlike_files += $map;
    }

    public function addFileReferenceToClassMember(
        string $source_file,
        string $referenced_member_id,
        bool $inside_return
    ): void {
        self::$file_references_to_class_members[$referenced_member_id][$source_file] = true;

        if ($inside_return) {
            self::$file_references_to_method_returns[$referenced_member_id][$source_file] = true;
        }
    }

    public function addFileReferenceToClassProperty(string $source_file, string $referenced_property_id): void
    {
        self::$file_references_to_class_properties[$referenced_property_id][$source_file] = true;
    }

    public function addFileReferenceToMissingClassMember(string $source_file, string $referenced_member_id): void
    {
        self::$file_references_to_missing_class_members[$referenced_member_id][$source_file] = true;
    }

    /**
     * @return array<string, array<string,bool>>
     */
    public function getAllFileReferencesToClassMembers(): array
    {
        return self::$file_references_to_class_members;
    }

    /**
     * @return array<string, array<string,bool>>
     */
    public function getAllFileReferencesToClassProperties(): array
    {
        return self::$file_references_to_class_properties;
    }

    /**
     * @return array<string, array<string,bool>>
     */
    public function getAllFileReferencesToMethodReturns(): array
    {
        return self::$file_references_to_method_returns;
    }

    /**
     * @return array<string, array<string,bool>>
     */
    public function getAllFileReferencesToMissingClassMembers(): array
    {
        return self::$file_references_to_missing_class_members;
    }

    /**
     * @param array<string, array<string,bool>> $references
     */
    public function addFileReferencesToClassMembers(array $references): void
    {
        foreach ($references as $key => $reference) {
            if (isset(self::$file_references_to_class_members[$key])) {
                self::$file_references_to_class_members[$key] = array_merge(
                    $reference,
                    self::$file_references_to_class_members[$key],
                );
            } else {
                self::$file_references_to_class_members[$key] = $reference;
            }
        }
    }

    /**
     * @param array<string, array<string,bool>> $references
     */
    public function addFileReferencesToClassProperties(array $references): void
    {
        foreach ($references as $key => $reference) {
            if (isset(self::$file_references_to_class_properties[$key])) {
                self::$file_references_to_class_properties[$key] = array_merge(
                    $reference,
                    self::$file_references_to_class_properties[$key],
                );
            } else {
                self::$file_references_to_class_properties[$key] = $reference;
            }
        }
    }

    /**
     * @param array<string, array<string,bool>> $references
     */
    public function addFileReferencesToMethodReturns(array $references): void
    {
        foreach ($references as $key => $reference) {
            if (isset(self::$file_references_to_method_returns[$key])) {
                self::$file_references_to_method_returns[$key] = array_merge(
                    $reference,
                    self::$file_references_to_method_returns[$key],
                );
            } else {
                self::$file_references_to_method_returns[$key] = $reference;
            }
        }
    }

    /**
     * @param array<string, array<string,bool>> $references
     */
    public function addFileReferencesToMissingClassMembers(array $references): void
    {
        foreach ($references as $key => $reference) {
            if (isset(self::$file_references_to_missing_class_members[$key])) {
                self::$file_references_to_missing_class_members[$key] = array_merge(
                    $reference,
                    self::$file_references_to_missing_class_members[$key],
                );
            } else {
                self::$file_references_to_missing_class_members[$key] = $reference;
            }
        }
    }

    public function addFileInheritanceToClass(string $source_file, string $fq_class_name_lc): void
    {
        self::$files_inheriting_classes[$fq_class_name_lc][$source_file] = true;
    }

    public function addMethodParamUse(string $method_id, int $offset, string $referencing_method_id): void
    {
        self::$method_param_uses[$method_id][$offset][$referencing_method_id] = true;
    }

    /**
     * @return  array<int, string>
     */
    private function calculateFilesReferencingFile(Codebase $codebase, string $file): array
    {
        $referenced_files = [];

        $file_classes = ClassLikeAnalyzer::getClassesForFile($codebase, $file);

        foreach ($file_classes as $file_class_lc => $_) {
            if (isset(self::$nonmethod_references_to_classes[$file_class_lc])) {
                $new_files = array_keys(self::$nonmethod_references_to_classes[$file_class_lc]);

                $referenced_files = [...$referenced_files, ...$new_files];
            }

            if (isset(self::$method_references_to_classes[$file_class_lc])) {
                $new_referencing_methods = array_keys(self::$method_references_to_classes[$file_class_lc]);

                foreach ($new_referencing_methods as $new_referencing_method_id) {
                    $fq_class_name_lc = explode('::', $new_referencing_method_id)[0];

                    try {
                        $referenced_files[] = $codebase->scanner->getClassLikeFilePath($fq_class_name_lc);
                    } catch (UnexpectedValueException $e) {
                        if (isset(self::$classlike_files[$fq_class_name_lc])) {
                            $referenced_files[] = self::$classlike_files[$fq_class_name_lc];
                        }
                    }
                }
            }
        }

        return array_unique($referenced_files);
    }

    /**
     * @return  array<int, string>
     */
    private function calculateFilesInheritingFile(Codebase $codebase, string $file): array
    {
        $referenced_files = [];

        $file_classes = ClassLikeAnalyzer::getClassesForFile($codebase, $file);

        foreach ($file_classes as $file_class_lc => $_) {
            if (isset(self::$files_inheriting_classes[$file_class_lc])) {
                $referenced_files = [
                    ...$referenced_files,
                    ...array_keys(self::$files_inheriting_classes[$file_class_lc]),
                ];
            }
        }

        return array_unique($referenced_files);
    }

    public function removeDeletedFilesFromReferences(): void
    {
        $deleted_files = $this->getDeletedReferencedFiles();

        if ($deleted_files) {
            foreach ($deleted_files as $file) {
                unset(self::$file_references[$file]);
            }

            if ($this->cache) {
                $this->cache->setCachedFileReferences(self::$file_references);
            }
        }
    }

    /**
     * @return array<int, string>
     */
    public function getFilesReferencingFile(string $file): array
    {
        return self::$file_references[$file]['a'] ?? [];
    }

    /**
     * @return array<int, string>
     */
    public function getFilesInheritingFromFile(string $file): array
    {
        return self::$file_references[$file]['i'] ?? [];
    }

    /**
     * @return array<string, array<string, bool>>
     */
    public function getAllMethodReferencesToClassMembers(): array
    {
        return self::$method_references_to_class_members;
    }

    /**
     * @return array<string, array<string, bool>>
     */
    public function getAllMethodDependencies(): array
    {
        return self::$method_dependencies;
    }

    /**
     * @return array<string, array<string, bool>>
     */
    public function getAllMethodReferencesToClassProperties(): array
    {
        return self::$method_references_to_class_properties;
    }

    /**
     * @return array<string, array<string, bool>>
     */
    public function getAllMethodReferencesToMethodReturns(): array
    {
        return self::$method_references_to_method_returns;
    }

    /**
     * @return array<string, array<string, bool>>
     */
    public function getAllMethodReferencesToClasses(): array
    {
        return self::$method_references_to_classes;
    }

    /**
     * @return array<string, array<string, bool>>
     */
    public function getAllMethodReferencesToMissingClassMembers(): array
    {
        return self::$method_references_to_missing_class_members;
    }

    /**
     * @return array<string, array<string,bool>>
     */
    public function getAllReferencesToMixedMemberNames(): array
    {
        return self::$references_to_mixed_member_names;
    }

    /**
     * @return array<string, array<int, array<string, bool>>>
     */
    public function getAllMethodParamUses(): array
    {
        return self::$method_param_uses;
    }

    /**
     * @psalm-suppress MixedPropertyTypeCoercion
     */
    public function loadReferenceCache(bool $force_reload = true): bool
    {
        if ($this->cache && (!$this->loaded_from_cache || $force_reload)) {
            $this->loaded_from_cache = true;

            $file_references = $this->cache->getCachedFileReferences();

            if ($file_references === null) {
                return false;
            }

            self::$file_references = $file_references;

            $nonmethod_references_to_classes = $this->cache->getCachedNonMethodClassReferences();

            if ($nonmethod_references_to_classes === null) {
                return false;
            }

            self::$nonmethod_references_to_classes = $nonmethod_references_to_classes;

            $method_references_to_classes = $this->cache->getCachedMethodClassReferences();

            if ($method_references_to_classes === null) {
                return false;
            }

            self::$method_references_to_classes = $method_references_to_classes;

            $method_references_to_class_members = $this->cache->getCachedMethodMemberReferences();

            if ($method_references_to_class_members === null) {
                return false;
            }

            self::$method_references_to_class_members = $method_references_to_class_members;

            $method_dependencies = $this->cache->getCachedMethodDependencies();

            if ($method_dependencies === null) {
                return false;
            }

            self::$method_dependencies = $method_dependencies;

            $method_references_to_class_properties = $this->cache->getCachedMethodPropertyReferences();

            if ($method_references_to_class_properties === null) {
                return false;
            }

            self::$method_references_to_class_properties = $method_references_to_class_properties;

            $method_references_to_method_returns = $this->cache->getCachedMethodMethodReturnReferences();

            if ($method_references_to_method_returns === null) {
                return false;
            }

            self::$method_references_to_method_returns = $method_references_to_method_returns;

            $method_references_to_missing_class_members = $this->cache->getCachedMethodMissingMemberReferences();

            if ($method_references_to_missing_class_members === null) {
                return false;
            }

            self::$method_references_to_missing_class_members = $method_references_to_missing_class_members;

            $file_references_to_class_members = $this->cache->getCachedFileMemberReferences();

            if ($file_references_to_class_members === null) {
                return false;
            }

            self::$file_references_to_class_members = $file_references_to_class_members;

            $file_references_to_class_properties = $this->cache->getCachedFilePropertyReferences();

            if ($file_references_to_class_properties === null) {
                return false;
            }

            self::$file_references_to_class_properties = $file_references_to_class_properties;

            $file_references_to_method_returns = $this->cache->getCachedFileMethodReturnReferences();

            if ($file_references_to_method_returns === null) {
                return false;
            }

            self::$file_references_to_method_returns = $file_references_to_method_returns;

            $file_references_to_missing_class_members = $this->cache->getCachedFileMissingMemberReferences();

            if ($file_references_to_missing_class_members === null) {
                return false;
            }

            self::$file_references_to_missing_class_members = $file_references_to_missing_class_members;

            $references_to_mixed_member_names = $this->cache->getCachedMixedMemberNameReferences();

            if ($references_to_mixed_member_names === null) {
                return false;
            }

            self::$references_to_mixed_member_names = $references_to_mixed_member_names;

            $analyzed_methods = $this->cache->getAnalyzedMethodCache();

            if ($analyzed_methods === false) {
                return false;
            }

            self::$analyzed_methods = $analyzed_methods;

            $issues = $this->cache->getCachedIssues();

            if ($issues === null) {
                return false;
            }

            self::$issues = $issues;

            $method_param_uses = $this->cache->getCachedMethodParamUses();

            if ($method_param_uses === null) {
                return false;
            }

            self::$method_param_uses = $method_param_uses;

            $mixed_counts = $this->cache->getTypeCoverage();

            if ($mixed_counts === false) {
                return false;
            }

            self::$mixed_counts = $mixed_counts;

            $classlike_files = $this->cache->getCachedClassLikeFiles();

            if ($classlike_files === null) {
                return false;
            }

            self::$classlike_files = $classlike_files;

            self::$file_maps = $this->cache->getFileMapCache() ?: [];

            return true;
        }

        return false;
    }

    /**
     * @param  array<string, string|bool>  $visited_files
     */
    public function updateReferenceCache(Codebase $codebase, array $visited_files): void
    {
        foreach ($visited_files as $file => $_) {
            $all_file_references = array_unique(
                array_merge(
                    self::$file_references[$file]['a'] ?? [],
                    $this->calculateFilesReferencingFile($codebase, $file),
                ),
            );

            $inheritance_references = array_unique(
                array_merge(
                    self::$file_references[$file]['i'] ?? [],
                    $this->calculateFilesInheritingFile($codebase, $file),
                ),
            );

            self::$file_references[$file] = [
                'a' => $all_file_references,
                'i' => $inheritance_references,
            ];
        }

        if ($this->cache) {
            $this->cache->setCachedFileReferences(self::$file_references);
            $this->cache->setCachedMethodClassReferences(self::$method_references_to_classes);
            $this->cache->setCachedNonMethodClassReferences(self::$nonmethod_references_to_classes);
            $this->cache->setCachedMethodMemberReferences(self::$method_references_to_class_members);
            $this->cache->setCachedMethodDependencies(self::$method_dependencies);
            $this->cache->setCachedMethodPropertyReferences(self::$method_references_to_class_properties);
            $this->cache->setCachedMethodMethodReturnReferences(self::$method_references_to_method_returns);
            $this->cache->setCachedFileMemberReferences(self::$file_references_to_class_members);
            $this->cache->setCachedFilePropertyReferences(self::$file_references_to_class_properties);
            $this->cache->setCachedFileMethodReturnReferences(self::$file_references_to_method_returns);
            $this->cache->setCachedMethodMissingMemberReferences(self::$method_references_to_missing_class_members);
            $this->cache->setCachedFileMissingMemberReferences(self::$file_references_to_missing_class_members);
            $this->cache->setCachedMixedMemberNameReferences(self::$references_to_mixed_member_names);
            $this->cache->setCachedMethodParamUses(self::$method_param_uses);
            $this->cache->setCachedIssues(self::$issues);
            $this->cache->setCachedClassLikeFiles(self::$classlike_files);
            $this->cache->setFileMapCache(self::$file_maps);
            $this->cache->setTypeCoverage(self::$mixed_counts);
            $this->cache->setAnalyzedMethodCache(self::$analyzed_methods);
        }
    }

    /**
     * @param lowercase-string $fq_class_name_lc
     */
    public function addMethodReferenceToClass(string $calling_function_id, string $fq_class_name_lc): void
    {
        if (!isset(self::$method_references_to_classes[$fq_class_name_lc])) {
            self::$method_references_to_classes[$fq_class_name_lc] = [$calling_function_id => true];
        } else {
            self::$method_references_to_classes[$fq_class_name_lc][$calling_function_id] = true;
        }
    }

    public function addMethodReferenceToClassMember(
        string $calling_function_id,
        string $referenced_member_id,
        bool $inside_return
    ): void {
        if (!isset(self::$method_references_to_class_members[$referenced_member_id])) {
            self::$method_references_to_class_members[$referenced_member_id] = [$calling_function_id => true];
        } else {
            self::$method_references_to_class_members[$referenced_member_id][$calling_function_id] = true;
        }

        if ($inside_return) {
            if (!isset(self::$method_references_to_method_returns[$referenced_member_id])) {
                self::$method_references_to_method_returns[$referenced_member_id] = [$calling_function_id => true];
            } else {
                self::$method_references_to_method_returns[$referenced_member_id][$calling_function_id] = true;
            }
        }
    }

    public function addMethodDependencyToClassMember(
        string $calling_function_id,
        string $referenced_member_id
    ): void {
        if (!isset(self::$method_dependencies[$referenced_member_id])) {
            self::$method_dependencies[$referenced_member_id] = [$calling_function_id => true];
        } else {
            self::$method_dependencies[$referenced_member_id][$calling_function_id] = true;
        }
    }

    public function addMethodReferenceToClassProperty(string $calling_function_id, string $referenced_property_id): void
    {
        if (!isset(self::$method_references_to_class_properties[$referenced_property_id])) {
            self::$method_references_to_class_properties[$referenced_property_id] = [$calling_function_id => true];
        } else {
            self::$method_references_to_class_properties[$referenced_property_id][$calling_function_id] = true;
        }
    }

    public function addMethodReferenceToMissingClassMember(
        string $calling_function_id,
        string $referenced_member_id
    ): void {
        if (!isset(self::$method_references_to_missing_class_members[$referenced_member_id])) {
            self::$method_references_to_missing_class_members[$referenced_member_id] = [$calling_function_id => true];
        } else {
            self::$method_references_to_missing_class_members[$referenced_member_id][$calling_function_id] = true;
        }
    }

    public function addCallingLocationForClassMethod(CodeLocation $code_location, string $referenced_member_id): void
    {
        if (!isset(self::$class_method_locations[$referenced_member_id])) {
            self::$class_method_locations[$referenced_member_id] = [$code_location];
        } else {
            self::$class_method_locations[$referenced_member_id][] = $code_location;
        }
    }

    public function addCallingLocationForClassProperty(
        CodeLocation $code_location,
        string $referenced_property_id
    ): void {
        if (!isset(self::$class_property_locations[$referenced_property_id])) {
            self::$class_property_locations[$referenced_property_id] = [$code_location];
        } else {
            self::$class_property_locations[$referenced_property_id][] = $code_location;
        }
    }

    public function addCallingLocationForClass(CodeLocation $code_location, string $referenced_class): void
    {
        if (!isset(self::$class_locations[$referenced_class])) {
            self::$class_locations[$referenced_class] = [$code_location];
        } else {
            self::$class_locations[$referenced_class][] = $code_location;
        }
    }

    public function isClassMethodReferenced(string $method_id): bool
    {
        return !empty(self::$file_references_to_class_members[$method_id])
            || !empty(self::$method_references_to_class_members[$method_id]);
    }

    public function isClassPropertyReferenced(string $property_id): bool
    {
        return !empty(self::$file_references_to_class_properties[$property_id])
            || !empty(self::$method_references_to_class_properties[$property_id]);
    }

    public function isMethodReturnReferenced(string $method_id): bool
    {
        return !empty(self::$file_references_to_method_returns[$method_id])
            || !empty(self::$method_references_to_method_returns[$method_id]);
    }

    public function isClassReferenced(string $fq_class_name_lc): bool
    {
        return isset(self::$method_references_to_classes[$fq_class_name_lc])
            || isset(self::$nonmethod_references_to_classes[$fq_class_name_lc]);
    }

    public function isMethodParamUsed(string $method_id, int $offset): bool
    {
        return !empty(self::$method_param_uses[$method_id][$offset]);
    }

    /**
     * @param array<string, array<string,bool>> $references
     */
    public function setNonMethodReferencesToClasses(array $references): void
    {
        self::$nonmethod_references_to_classes = $references;
    }

    /**
     * @return array<string, array<int, CodeLocation>>
     */
    public function getAllClassMethodLocations(): array
    {
        return self::$class_method_locations;
    }

    /**
     * @return array<string, array<int, CodeLocation>>
     */
    public function getAllClassPropertyLocations(): array
    {
        return self::$class_property_locations;
    }

    /**
     * @return array<string, array<int, CodeLocation>>
     */
    public function getAllClassLocations(): array
    {
        return self::$class_locations;
    }

    /**
     * @return array<int, CodeLocation>
     */
    public function getClassMethodLocations(string $method_id): array
    {
        return self::$class_method_locations[$method_id] ?? [];
    }

    /**
     * @return array<int, CodeLocation>
     */
    public function getClassPropertyLocations(string $property_id): array
    {
        return self::$class_property_locations[$property_id] ?? [];
    }

    /**
     * @return array<int, CodeLocation>
     */
    public function getClassLocations(string $fq_class_name_lc): array
    {
        return self::$class_locations[$fq_class_name_lc] ?? [];
    }

    /**
     * @param array<string, array<string,bool>> $references
     */
    public function addMethodReferencesToClassMembers(array $references): void
    {
        foreach ($references as $key => $reference) {
            if (isset(self::$method_references_to_class_members[$key])) {
                self::$method_references_to_class_members[$key] = array_merge(
                    $reference,
                    self::$method_references_to_class_members[$key],
                );
            } else {
                self::$method_references_to_class_members[$key] = $reference;
            }
        }
    }

    /**
     * @param array<string, array<string,bool>> $references
     */
    public function addMethodDependencies(array $references): void
    {
        foreach ($references as $key => $reference) {
            if (isset(self::$method_dependencies[$key])) {
                self::$method_dependencies[$key] = array_merge(
                    $reference,
                    self::$method_dependencies[$key],
                );
            } else {
                self::$method_dependencies[$key] = $reference;
            }
        }
    }

    /**
     * @param array<string, array<string,bool>> $references
     */
    public function addMethodReferencesToClassProperties(array $references): void
    {
        foreach ($references as $key => $reference) {
            if (isset(self::$method_references_to_class_properties[$key])) {
                self::$method_references_to_class_properties[$key] = array_merge(
                    $reference,
                    self::$method_references_to_class_properties[$key],
                );
            } else {
                self::$method_references_to_class_properties[$key] = $reference;
            }
        }
    }

    /**
     * @param array<string, array<string,bool>> $references
     */
    public function addMethodReferencesToMethodReturns(array $references): void
    {
        foreach ($references as $key => $reference) {
            if (isset(self::$method_references_to_method_returns[$key])) {
                self::$method_references_to_method_returns[$key] = array_merge(
                    $reference,
                    self::$method_references_to_method_returns[$key],
                );
            } else {
                self::$method_references_to_method_returns[$key] = $reference;
            }
        }
    }

    /**
     * @param array<string, array<string,bool>> $references
     */
    public function addMethodReferencesToClasses(array $references): void
    {
        foreach ($references as $key => $reference) {
            if (isset(self::$method_references_to_classes[$key])) {
                self::$method_references_to_classes[$key] = array_merge(
                    $reference,
                    self::$method_references_to_classes[$key],
                );
            } else {
                self::$method_references_to_classes[$key] = $reference;
            }
        }
    }

    /**
     * @param array<string, array<string,bool>> $references
     */
    public function addMethodReferencesToMissingClassMembers(array $references): void
    {
        foreach ($references as $key => $reference) {
            if (isset(self::$method_references_to_missing_class_members[$key])) {
                self::$method_references_to_missing_class_members[$key] = array_merge(
                    $reference,
                    self::$method_references_to_missing_class_members[$key],
                );
            } else {
                self::$method_references_to_missing_class_members[$key] = $reference;
            }
        }
    }

    /**
     * @param array<string, array<int, array<string, bool>>> $references
     */
    public function addMethodParamUses(array $references): void
    {
        foreach ($references as $method_id => $method_param_uses) {
            if (isset(self::$method_param_uses[$method_id])) {
                foreach ($method_param_uses as $offset => $reference_map) {
                    if (isset(self::$method_param_uses[$method_id][$offset])) {
                        self::$method_param_uses[$method_id][$offset] = array_merge(
                            self::$method_param_uses[$method_id][$offset],
                            $reference_map,
                        );
                    } else {
                        self::$method_param_uses[$method_id][$offset] = $reference_map;
                    }
                }
            } else {
                self::$method_param_uses[$method_id] = $method_param_uses;
            }
        }
    }

    /**
     * @param array<string, array<string,bool>> $references
     */
    public function setCallingMethodReferencesToClasses(array $references): void
    {
        self::$method_references_to_classes = $references;
    }

    /**
     * @param array<string, array<string,bool>> $references
     */
    public function setCallingMethodReferencesToClassMembers(array $references): void
    {
        self::$method_references_to_class_members = $references;
    }

    /**
     * @param array<string, array<string,bool>> $references
     */
    public function setMethodDependencies(array $references): void
    {
        self::$method_dependencies = $references;
    }

    /**
     * @param array<string, array<string,bool>> $references
     */
    public function setCallingMethodReferencesToClassProperties(array $references): void
    {
        self::$method_references_to_class_properties = $references;
    }

    /**
     * @param array<string, array<string,bool>> $references
     */
    public function setCallingMethodReferencesToMethodReturns(array $references): void
    {
        self::$method_references_to_method_returns = $references;
    }

    /**
     * @param array<string, array<string,bool>> $references
     */
    public function setCallingMethodReferencesToMissingClassMembers(array $references): void
    {
        self::$method_references_to_missing_class_members = $references;
    }

    /**
     * @param array<string, array<string,bool>> $references
     */
    public function setFileReferencesToClassMembers(array $references): void
    {
        self::$file_references_to_class_members = $references;
    }

    /**
     * @param array<string, array<string,bool>> $references
     */
    public function setFileReferencesToClassProperties(array $references): void
    {
        self::$file_references_to_class_properties = $references;
    }

    /**
     * @param array<string, array<string,bool>> $references
     */
    public function setFileReferencesToMethodReturns(array $references): void
    {
        self::$file_references_to_method_returns = $references;
    }

    /**
     * @param array<string, array<string,bool>> $references
     */
    public function setFileReferencesToMissingClassMembers(array $references): void
    {
        self::$file_references_to_missing_class_members = $references;
    }

    /**
     * @param array<string, array<string,bool>> $references
     */
    public function setReferencesToMixedMemberNames(array $references): void
    {
        self::$references_to_mixed_member_names = $references;
    }

    /**
     * @param array<string, array<int, array<string, bool>>> $references
     */
    public function setMethodParamUses(array $references): void
    {
        self::$method_param_uses = $references;
    }

    /**
     * @param array<string, array<int, CodeLocation>> $references
     */
    public function addClassMethodLocations(array $references): void
    {
        foreach ($references as $referenced_member_id => $locations) {
            if (isset(self::$class_method_locations[$referenced_member_id])) {
                self::$class_method_locations[$referenced_member_id] = [
                    ...self::$class_method_locations[$referenced_member_id],
                    ...$locations,
                ];
            } else {
                self::$class_method_locations[$referenced_member_id] = $locations;
            }
        }
    }

    /**
     * @param array<string, array<int, CodeLocation>> $references
     */
    public function addClassPropertyLocations(array $references): void
    {
        foreach ($references as $referenced_member_id => $locations) {
            if (isset(self::$class_property_locations[$referenced_member_id])) {
                self::$class_property_locations[$referenced_member_id] = [
                    ...self::$class_property_locations[$referenced_member_id],
                    ...$locations,
                ];
            } else {
                self::$class_property_locations[$referenced_member_id] = $locations;
            }
        }
    }

    /**
     * @param array<string, array<int, CodeLocation>> $references
     */
    public function addClassLocations(array $references): void
    {
        foreach ($references as $referenced_member_id => $locations) {
            if (isset(self::$class_locations[$referenced_member_id])) {
                self::$class_locations[$referenced_member_id] = [
                    ...self::$class_locations[$referenced_member_id],
                    ...$locations,
                ];
            } else {
                self::$class_locations[$referenced_member_id] = $locations;
            }
        }
    }

    /**
     * @return array<string, array<int, IssueData>>
     */
    public function getExistingIssues(): array
    {
        return self::$issues;
    }

    public function clearExistingIssuesForFile(string $file_path): void
    {
        unset(self::$issues[$file_path]);
    }

    public function clearExistingFileMapsForFile(string $file_path): void
    {
        unset(self::$file_maps[$file_path]);
    }

    public function addIssue(string $file_path, IssueData $issue): void
    {
        // don’t save parse errors ever, as they're not responsive to AST diffing
        if ($issue->type === 'ParseError') {
            return;
        }

        if (!isset(self::$issues[$file_path])) {
            self::$issues[$file_path] = [$issue];
        } else {
            self::$issues[$file_path][] = $issue;
        }
    }

    /**
     * @param array<string, array<string, int>> $analyzed_methods
     */
    public function setAnalyzedMethods(array $analyzed_methods): void
    {
        self::$analyzed_methods = $analyzed_methods;
    }

    /**
     * @param array<string, FileMapType> $file_maps
     */
    public function setFileMaps(array $file_maps): void
    {
        self::$file_maps = $file_maps;
    }

    /**
     * @return array<string, array{int, int}>
     */
    public function getTypeCoverage(): array
    {
        return self::$mixed_counts;
    }

    /**
     * @param array<string, array{int, int}> $mixed_counts
     */
    public function setTypeCoverage(array $mixed_counts): void
    {
        self::$mixed_counts = array_merge(self::$mixed_counts, $mixed_counts);
    }

    /**
     * @return array<string, array<string, int>>
     */
    public function getAnalyzedMethods(): array
    {
        return self::$analyzed_methods;
    }

    /**
     * @return array<string, FileMapType>
     */
    public function getFileMaps(): array
    {
        return self::$file_maps;
    }

    public static function clearCache(): void
    {
        self::$files_inheriting_classes = [];
        self::$deleted_files = null;
        self::$file_references = [];
        self::$file_references_to_class_members = [];
        self::$file_references_to_class_properties = [];
        self::$file_references_to_method_returns = [];
        self::$method_references_to_class_members = [];
        self::$method_dependencies = [];
        self::$method_references_to_class_properties = [];
        self::$method_references_to_method_returns = [];
        self::$method_references_to_classes = [];
        self::$nonmethod_references_to_classes = [];
        self::$file_references_to_missing_class_members = [];
        self::$method_references_to_missing_class_members = [];
        self::$references_to_mixed_member_names = [];
        self::$class_method_locations = [];
        self::$class_property_locations = [];
        self::$class_locations = [];
        self::$analyzed_methods = [];
        self::$issues = [];
        self::$file_maps = [];
        self::$method_param_uses = [];
        self::$classlike_files = [];
        self::$mixed_counts = [];
    }
}
