<?php
/**
 * A local file represents a chunk of text has a file system location.
 *
 * @author    Greg Sherwood <gsherwood@squiz.net>
 * @copyright 2006-2023 Squiz Pty Ltd (ABN 77 084 670 600)
 * @copyright 2023 PHPCSStandards and contributors
 * @license   https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/HEAD/licence.txt BSD Licence
 */

namespace PHP_CodeSniffer\Files;

use PHP_CodeSniffer\Config;
use PHP_CodeSniffer\Ruleset;
use PHP_CodeSniffer\Util\Cache;
use PHP_CodeSniffer\Util\Common;
use PHP_CodeSniffer\Util\Writers\StatusWriter;

class LocalFile extends File
{


    /**
     * Creates a LocalFile object and sets the content.
     *
     * @param string                   $path    The absolute path to the file.
     * @param \PHP_CodeSniffer\Ruleset $ruleset The ruleset used for the run.
     * @param \PHP_CodeSniffer\Config  $config  The config data for the run.
     *
     * @return void
     */
    public function __construct(string $path, Ruleset $ruleset, Config $config)
    {
        $this->path = trim($path);
        if (Common::isReadable($this->path) === false) {
            parent::__construct($this->path, $ruleset, $config);
            $error = 'Error opening file; file no longer exists or you do not have access to read the file';
            $this->addMessage(true, $error, 1, 1, 'Internal.LocalFile', [], 5, false);
            $this->ignored = true;
            return;
        }

        // Before we go and spend time tokenizing this file, just check
        // to see if there is a tag up top to indicate that the whole
        // file should be ignored. It must be on one of the first two lines.
        if ($config->annotations === true) {
            $handle = fopen($this->path, 'r');
            if ($handle !== false) {
                $firstContent  = fgets($handle);
                $firstContent .= fgets($handle);
                fclose($handle);

                if (stripos($firstContent, 'phpcs:ignorefile') !== false) {
                    // We are ignoring the whole file.
                    $this->ignored = true;
                    return;
                }
            }
        }

        $this->reloadContent();

        parent::__construct($this->path, $ruleset, $config);
    }


    /**
     * Loads the latest version of the file's content from the file system.
     *
     * @return void
     */
    public function reloadContent()
    {
        $this->setContent(file_get_contents($this->path));
    }


    /**
     * Processes the file.
     *
     * @return void
     */
    public function process()
    {
        if ($this->ignored === true) {
            return;
        }

        if ($this->configCache['cache'] === false) {
            parent::process();
            return;
        }

        $hash  = md5_file($this->path);
        $hash .= fileperms($this->path);
        $cache = Cache::get($this->path);
        if ($cache !== false && $cache['hash'] === $hash) {
            // We can't filter metrics, so just load all of them.
            $this->metrics = $cache['metrics'];

            if ($this->configCache['recordErrors'] === true) {
                // Replay the cached errors and warnings to filter out the ones
                // we don't need for this specific run.
                $this->configCache['cache'] = false;
                $this->replayErrors($cache['errors'], $cache['warnings']);
                $this->configCache['cache'] = true;
            } else {
                $this->errorCount          = $cache['errorCount'];
                $this->warningCount        = $cache['warningCount'];
                $this->fixableErrorCount   = $cache['fixableErrorCount'];
                $this->fixableWarningCount = $cache['fixableWarningCount'];
            }

            if (PHP_CODESNIFFER_VERBOSITY > 0
                || (PHP_CODESNIFFER_CBF === true && empty($this->config->files) === false)
            ) {
                StatusWriter::write('[loaded from cache]... ', 0, 0);
            }

            $this->numTokens = $cache['numTokens'];
            $this->fromCache = true;
            return;
        }

        if (PHP_CODESNIFFER_VERBOSITY > 1) {
            StatusWriter::writeNewline();
        }

        parent::process();

        $cache = [
            'hash'                => $hash,
            'errors'              => $this->errors,
            'warnings'            => $this->warnings,
            'metrics'             => $this->metrics,
            'errorCount'          => $this->errorCount,
            'warningCount'        => $this->warningCount,
            'fixableErrorCount'   => $this->fixableErrorCount,
            'fixableWarningCount' => $this->fixableWarningCount,
            'numTokens'           => $this->numTokens,
        ];

        Cache::set($this->path, $cache);

        // During caching, we don't filter out errors in any way, so
        // we need to do that manually now by replaying them.
        if ($this->configCache['recordErrors'] === true) {
            $this->configCache['cache'] = false;
            $this->replayErrors($this->errors, $this->warnings);
            $this->configCache['cache'] = true;
        }
    }


    /**
     * Clears and replays error and warnings for the file.
     *
     * Replaying errors and warnings allows for filtering rules to be changed
     * and then errors and warnings to be reapplied with the new rules. This is
     * particularly useful while caching.
     *
     * @param array<int, array<int, array<string, mixed>>> $errors   The list of errors to replay.
     * @param array<int, array<int, array<string, mixed>>> $warnings The list of warnings to replay.
     *
     * @return void
     */
    private function replayErrors(array $errors, array $warnings)
    {
        $this->errors            = [];
        $this->warnings          = [];
        $this->errorCount        = 0;
        $this->warningCount      = 0;
        $this->fixableErrorCount = 0;
        $this->fixableWarningCount = 0;

        $this->replayingErrors = true;

        foreach ($errors as $line => $lineErrors) {
            foreach ($lineErrors as $column => $colErrors) {
                foreach ($colErrors as $error) {
                    $this->activeListener = $error['listener'];
                    $this->addMessage(
                        true,
                        $error['message'],
                        $line,
                        $column,
                        $error['source'],
                        [],
                        $error['severity'],
                        $error['fixable']
                    );
                }
            }
        }

        foreach ($warnings as $line => $lineErrors) {
            foreach ($lineErrors as $column => $colErrors) {
                foreach ($colErrors as $error) {
                    $this->activeListener = $error['listener'];
                    $this->addMessage(
                        false,
                        $error['message'],
                        $line,
                        $column,
                        $error['source'],
                        [],
                        $error['severity'],
                        $error['fixable']
                    );
                }
            }
        }

        $this->replayingErrors = false;
    }
}
