<?php

namespace HelpGent\WP_CLI\I18n;

use HelpGent\Gettext\Extractors\Po;
use HelpGent\Gettext\Merge;
use HelpGent\Gettext\Translation;
use HelpGent\Gettext\Translations;
use HelpGent\Gettext\Utils\ParsedComment;
use HelpGent\WP_CLI;
use HelpGent\WP_CLI_Command;
use HelpGent\WP_CLI\Utils;
use DirectoryIterator;
use IteratorIterator;
class MakePotCommand extends WP_CLI_Command
{
    /**
     * @var string
     */
    protected $source;
    /**
     * @var string
     */
    protected $destination;
    /**
     * @var array
     */
    protected $merge = [];
    /**
     * @var Translations[]
     */
    protected $exceptions = [];
    /**
     * @var bool
     */
    protected $subtract_and_merge;
    /**
     * @var array
     */
    protected $include = [];
    /**
     * @var array
     */
    protected $exclude = ['node_modules', '.*', 'vendor', 'Gruntfile.js', 'webpack.config.js', '*.min.js', 'test', 'tests'];
    /**
     * @var string
     */
    protected $slug;
    /**
     * @var array
     */
    protected $main_file_data = [];
    /**
     * @var string
     */
    protected $main_file_path;
    /**
     * @var bool
     */
    protected $skip_js = \false;
    /**
     * @var bool
     */
    protected $skip_php = \false;
    /**
     * @var bool
     */
    protected $skip_blade = \false;
    /**
     * @var bool
     */
    protected $skip_block_json = \false;
    /**
     * @var bool
     */
    protected $skip_theme_json = \false;
    /**
     * @var bool
     */
    protected $skip_audit = \false;
    /**
     * @var bool
     */
    protected $location = \true;
    /**
     * @var array
     */
    protected $headers = [];
    /**
     * @var string
     */
    protected $domain;
    /**
     * @var string
     */
    protected $package_name;
    /**
     * @var string
     */
    protected $file_comment;
    /**
     * @var string
     */
    protected $project_type = 'generic';
    /**
     * These Regexes copied from http://php.net/manual/en/function.sprintf.php#93552
     * and adjusted for better precision and updated specs.
     */
    const SPRINTF_PLACEHOLDER_REGEX = '/(?:
		(?<!%)                     # Don\'t match a literal % (%%).
		(
			%                          # Start of placeholder.
			(?:[0-9]+\\$)?              # Optional ordering of the placeholders.
			[+-]?                      # Optional sign specifier.
			(?:
				(?:0|\'.)?                 # Optional padding specifier - excluding the space.
				-?                         # Optional alignment specifier.
				[0-9]*                     # Optional width specifier.
				(?:\\.(?:[ 0]|\'.)?[0-9]+)? # Optional precision specifier with optional padding character.
				|                      # Only recognize the space as padding in combination with a width specifier.
				(?:[ ])?                   # Optional space padding specifier.
				-?                         # Optional alignment specifier.
				[0-9]+                     # Width specifier.
				(?:\\.(?:[ 0]|\'.)?[0-9]+)? # Optional precision specifier with optional padding character.
			)
			[bcdeEfFgGosuxX]           # Type specifier.
		)
	)/x';
    /**
     * "Unordered" means there's no position specifier: '%s', not '%2$s'.
     */
    const UNORDERED_SPRINTF_PLACEHOLDER_REGEX = '/(?:
		(?<!%)                     # Don\'t match a literal % (%%).
		%                          # Start of placeholder.
		[+-]?                      # Optional sign specifier.
		(?:
			(?:0|\'.)?                 # Optional padding specifier - excluding the space.
			-?                         # Optional alignment specifier.
			[0-9]*                     # Optional width specifier.
			(?:\\.(?:[ 0]|\'.)?[0-9]+)? # Optional precision specifier with optional padding character.
			|                      # Only recognize the space as padding in combination with a width specifier.
			(?:[ ])?                   # Optional space padding specifier.
			-?                         # Optional alignment specifier.
			[0-9]+                     # Width specifier.
			(?:\\.(?:[ 0]|\'.)?[0-9]+)? # Optional precision specifier with optional padding character.
		)
		[bcdeEfFgGosuxX]           # Type specifier.
	)/x';
    /**
     * Create a POT file for a WordPress project.
     *
     * Scans PHP, Blade-PHP and JavaScript files for translatable strings, as well as theme stylesheets and plugin files
     * if the source directory is detected as either a plugin or theme.
     *
     * ## OPTIONS
     *
     * <source>
     * : Directory to scan for string extraction.
     *
     * [<destination>]
     * : Name of the resulting POT file.
     *
     * [--slug=<slug>]
     * : Plugin or theme slug. Defaults to the source directory's basename.
     *
     * [--domain=<domain>]
     * : Text domain to look for in the source code, unless the `--ignore-domain` option is used.
     * By default, the "Text Domain" header of the plugin or theme is used.
     * If none is provided, it falls back to the project slug.
     *
     * [--ignore-domain]
     * : Ignore the text domain completely and extract strings with any text domain.
     *
     * [--merge[=<paths>]]
     * : Comma-separated list of POT files whose contents should be merged with the extracted strings.
     * If left empty, defaults to the destination POT file. POT file headers will be ignored.
     *
     * [--subtract=<paths>]
     * : Comma-separated list of POT files whose contents should act as some sort of denylist for string extraction.
     * Any string which is found on that denylist will not be extracted.
     * This can be useful when you want to create multiple POT files from the same source directory with slightly
     * different content and no duplicate strings between them.
     *
     * [--subtract-and-merge]
     * : Whether source code references and comments from the generated POT file should be instead added to the POT file
     * used for subtraction. Warning: this modifies the files passed to `--subtract`!
     *
     * [--include=<paths>]
     * : Comma-separated list of files and paths that should be used for string extraction.
     * If provided, only these files and folders will be taken into account for string extraction.
     * For example, `--include="src,my-file.php` will ignore anything besides `my-file.php` and files in the `src`
     * directory. Simple glob patterns can be used, i.e. `--include=foo-*.php` includes any PHP file with the `foo-`
     * prefix. Leading and trailing slashes are ignored, i.e. `/my/directory/` is the same as `my/directory`.
     *
     * [--exclude=<paths>]
     * : Comma-separated list of files and paths that should be skipped for string extraction.
     * For example, `--exclude=.github,myfile.php` would ignore any strings found within `myfile.php` or the `.github`
     * folder. Simple glob patterns can be used, i.e. `--exclude=foo-*.php` excludes any PHP file with the `foo-`
     * prefix. Leading and trailing slashes are ignored, i.e. `/my/directory/` is the same as `my/directory`. The
     * following files and folders are always excluded: node_modules, .git, .svn, .CVS, .hg, vendor, *.min.js.
     *
     * [--headers=<headers>]
     * : Array in JSON format of custom headers which will be added to the POT file. Defaults to empty array.
     *
     * [--location]
     * : Whether to write `#: filename:line` lines.
     * Defaults to true, use `--no-location` to skip the removal.
     * Note that disabling this option makes it harder for technically skilled translators to understand each message’s context.
     *
     * [--skip-js]
     * : Skips JavaScript string extraction. Useful when this is done in another build step, e.g. through Babel.
     *
     * [--skip-php]
     * : Skips PHP string extraction.
     *
     * [--skip-blade]
     * : Skips Blade-PHP string extraction.
     *
     * [--skip-block-json]
     * : Skips string extraction from block.json files.
     *
     * [--skip-theme-json]
     * : Skips string extraction from theme.json files.
     *
     * [--skip-audit]
     * : Skips string audit where it tries to find possible mistakes in translatable strings. Useful when running in an
     * automated environment.
     *
     * [--file-comment=<file-comment>]
     * : String that should be added as a comment to the top of the resulting POT file.
     * By default, a copyright comment is added for WordPress plugins and themes in the following manner:
     *
     *      ```
     *      Copyright (C) 2018 Example Plugin Author
     *      This file is distributed under the same license as the Example Plugin package.
     *      ```
     *
     *      If a plugin or theme specifies a license in their main plugin file or stylesheet, the comment looks like
     *      this:
     *
     *      ```
     *      Copyright (C) 2018 Example Plugin Author
     *      This file is distributed under the GPLv2.
     *      ```
     *
     * [--package-name=<name>]
     * : Name to use for package name in the resulting POT file's `Project-Id-Version` header.
     * Overrides plugin or theme name, if applicable.
     *
     * ## EXAMPLES
     *
     *     # Create a POT file for the WordPress plugin/theme in the current directory.
     *     $ wp i18n make-pot . languages/my-plugin.pot
     *
     *     # Create a POT file for the continents and cities list in WordPress core.
     *     $ wp i18n make-pot . continents-and-cities.pot --include="wp-admin/includes/continents-cities.php" --ignore-domain
     *
     *     # Create a POT file for the WordPress theme in the current directory with custom headers.
     *     $ wp i18n make-pot . languages/my-theme.pot --headers='{"Report-Msgid-Bugs-To":"https://github.com/theme-author/my-theme/","POT-Creation-Date":""}'
     *
     * @when before_wp_load
     *
     * @throws WP_CLI\ExitException
     */
    public function __invoke($args, $assoc_args)
    {
        $this->handle_arguments($args, $assoc_args);
        $translations = $this->extract_strings();
        if (!$translations) {
            WP_CLI::warning('No strings found');
        }
        $translations_count = \count($translations);
        if (1 === $translations_count) {
            WP_CLI::debug(\sprintf('Extracted %d string', $translations_count), 'make-pot');
        } else {
            WP_CLI::debug(\sprintf('Extracted %d strings', $translations_count), 'make-pot');
        }
        if (!PotGenerator::toFile($translations, $this->destination)) {
            WP_CLI::error('Could not generate a POT file.');
        }
        WP_CLI::success('POT file successfully generated.');
    }
    /**
     * Process arguments from command-line in a reusable way.
     *
     * @throws WP_CLI\ExitException
     *
     * @param array $args       Command arguments.
     * @param array $assoc_args Associative arguments.
     */
    public function handle_arguments($args, $assoc_args)
    {
        $array_arguments = array('headers');
        $assoc_args = Utils\parse_shell_arrays($assoc_args, $array_arguments);
        $this->source = \realpath($args[0]);
        $this->slug = Utils\get_flag_value($assoc_args, 'slug', Utils\basename($this->source));
        $this->skip_js = Utils\get_flag_value($assoc_args, 'skip-js', $this->skip_js);
        $this->skip_php = Utils\get_flag_value($assoc_args, 'skip-php', $this->skip_php);
        $this->skip_blade = Utils\get_flag_value($assoc_args, 'skip-blade', $this->skip_blade);
        $this->skip_block_json = Utils\get_flag_value($assoc_args, 'skip-block-json', $this->skip_block_json);
        $this->skip_theme_json = Utils\get_flag_value($assoc_args, 'skip-theme-json', $this->skip_theme_json);
        $this->skip_audit = Utils\get_flag_value($assoc_args, 'skip-audit', $this->skip_audit);
        $this->headers = Utils\get_flag_value($assoc_args, 'headers', $this->headers);
        $this->file_comment = Utils\get_flag_value($assoc_args, 'file-comment');
        $this->package_name = Utils\get_flag_value($assoc_args, 'package-name');
        $this->location = Utils\get_flag_value($assoc_args, 'location', \true);
        $ignore_domain = Utils\get_flag_value($assoc_args, 'ignore-domain', \false);
        if (!$this->source || !\is_dir($this->source)) {
            WP_CLI::error('Not a valid source directory.');
        }
        $this->main_file_data = $this->get_main_file_data();
        if ($ignore_domain) {
            WP_CLI::debug('Extracting all strings regardless of text domain', 'make-pot');
        }
        if (!$ignore_domain) {
            $this->domain = $this->slug;
            if (!empty($this->main_file_data['Text Domain'])) {
                $this->domain = $this->main_file_data['Text Domain'];
            }
            $this->domain = Utils\get_flag_value($assoc_args, 'domain', $this->domain);
            WP_CLI::debug(\sprintf('Extracting all strings with text domain "%s"', $this->domain), 'make-pot');
        }
        // Determine destination.
        $this->destination = "{$this->source}/{$this->slug}.pot";
        if (!empty($this->main_file_data['Domain Path'])) {
            // Domain Path inside source folder.
            $this->destination = \sprintf('%s/%s/%s.pot', $this->source, $this->unslashit($this->main_file_data['Domain Path']), $this->slug);
        }
        if (isset($args[1])) {
            $this->destination = $args[1];
        }
        WP_CLI::debug(\sprintf('Destination: %s', $this->destination), 'make-pot');
        if (!\is_dir(\dirname($this->destination)) && !\mkdir(\dirname($this->destination), 0777, \true)) {
            WP_CLI::error('Could not create destination directory.');
        }
        if (isset($assoc_args['merge'])) {
            if (\true === $assoc_args['merge']) {
                $this->merge = [$this->destination];
            } elseif (!empty($assoc_args['merge'])) {
                $this->merge = \explode(',', $assoc_args['merge']);
            }
            $this->merge = \array_filter($this->merge, function ($file) {
                if (!\file_exists($file)) {
                    WP_CLI::warning(\sprintf('Invalid file provided to --merge: %s', $file));
                    return \false;
                }
                return \true;
            });
            if (!empty($this->merge)) {
                WP_CLI::debug(\sprintf('Merging with existing POT %s: %s', WP_CLI\Utils\pluralize('file', \count($this->merge)), \implode(',', $this->merge)), 'make-pot');
            }
        }
        if (isset($assoc_args['subtract'])) {
            $this->subtract_and_merge = Utils\get_flag_value($assoc_args, 'subtract-and-merge', \false);
            $files = \explode(',', $assoc_args['subtract']);
            foreach ($files as $file) {
                if (!\file_exists($file)) {
                    WP_CLI::warning(\sprintf('Invalid file provided to --subtract: %s', $file));
                    continue;
                }
                WP_CLI::debug(\sprintf('Ignoring any string already existing in: %s', $file), 'make-pot');
                $this->exceptions[$file] = new Translations();
                Po::fromFile($file, $this->exceptions[$file]);
            }
        }
        if (isset($assoc_args['include'])) {
            $this->include = \array_filter(\explode(',', $assoc_args['include']));
            $this->include = \array_map([$this, 'unslashit'], $this->include);
            $this->include = \array_unique($this->include);
            WP_CLI::debug(\sprintf('Only including the following files: %s', \implode(',', $this->include)), 'make-pot');
        }
        if (isset($assoc_args['exclude'])) {
            $this->exclude = \array_filter(\array_merge($this->exclude, \explode(',', $assoc_args['exclude'])));
            $this->exclude = \array_map([$this, 'unslashit'], $this->exclude);
            $this->exclude = \array_unique($this->exclude);
        }
        WP_CLI::debug(\sprintf('Excluding the following files: %s', \implode(',', $this->exclude)), 'make-pot');
    }
    /**
     * Removes leading and trailing slashes of a string.
     *
     * @param string $text What to add and remove slashes from.
     * @return string String without leading and trailing slashes.
     */
    protected function unslashit($text)
    {
        return \ltrim(\rtrim(\trim($text), '/\\'), '/\\');
    }
    /**
     * Retrieves the main file data of the plugin or theme.
     *
     * @return array
     */
    protected function get_main_file_data()
    {
        $files = new IteratorIterator(new DirectoryIterator($this->source));
        /** @var DirectoryIterator $file */
        foreach ($files as $file) {
            // wp-content/themes/my-theme/style.css
            if ($file->isFile() && 'style' === $file->getBasename('.css') && $file->isReadable()) {
                $theme_data = FileDataExtractor::get_file_data($file->getRealPath(), \array_combine($this->get_file_headers('theme'), $this->get_file_headers('theme')));
                // Stop when it contains a valid Theme Name header.
                if (!empty($theme_data['Theme Name'])) {
                    WP_CLI::log('Theme stylesheet detected.');
                    WP_CLI::debug(\sprintf('Theme stylesheet: %s', $file->getRealPath()), 'make-pot');
                    $this->project_type = 'theme';
                    $this->main_file_path = $file->getRealPath();
                    return $theme_data;
                }
            }
            // wp-content/themes/my-themes/theme-a/style.css
            if ($file->isDir() && !$file->isDot() && \is_readable($file->getRealPath() . '/style.css')) {
                $theme_data = FileDataExtractor::get_file_data($file->getRealPath() . '/style.css', \array_combine($this->get_file_headers('theme'), $this->get_file_headers('theme')));
                // Stop when it contains a valid Theme Name header.
                if (!empty($theme_data['Theme Name'])) {
                    WP_CLI::log('Theme stylesheet detected.');
                    WP_CLI::debug(\sprintf('Theme stylesheet: %s', $file->getRealPath() . '/style.css'), 'make-pot');
                    $this->project_type = 'theme';
                    $this->main_file_path = $file->getRealPath();
                    return $theme_data;
                }
            }
            // wp-content/plugins/my-plugin/my-plugin.php
            if ($file->isFile() && $file->isReadable() && 'php' === $file->getExtension()) {
                $plugin_data = FileDataExtractor::get_file_data($file->getRealPath(), \array_combine($this->get_file_headers('plugin'), $this->get_file_headers('plugin')));
                // Stop when we find a file with a valid Plugin Name header.
                if (!empty($plugin_data['Plugin Name'])) {
                    WP_CLI::log('Plugin file detected.');
                    WP_CLI::debug(\sprintf('Plugin file: %s', $file->getRealPath()), 'make-pot');
                    $this->project_type = 'plugin';
                    $this->main_file_path = $file->getRealPath();
                    return $plugin_data;
                }
            }
        }
        WP_CLI::debug('No valid theme stylesheet or plugin file found, treating as a regular project.', 'make-pot');
        return [];
    }
    /**
     * Returns the file headers for themes and plugins.
     *
     * @param string $type Source type, either theme or plugin.
     *
     * @return array List of file headers.
     */
    protected function get_file_headers($type)
    {
        switch ($type) {
            case 'plugin':
                return ['Plugin Name', 'Plugin URI', 'Description', 'Author', 'Author URI', 'Version', 'License', 'Domain Path', 'Text Domain'];
            case 'theme':
                return ['Theme Name', 'Theme URI', 'Description', 'Author', 'Author URI', 'Version', 'License', 'Domain Path', 'Text Domain'];
            default:
                return [];
        }
    }
    /**
     * Creates a POT file and stores it on disk.
     *
     * @throws WP_CLI\ExitException
     *
     * @return Translations A Translation set.
     */
    protected function extract_strings()
    {
        $translations = new Translations();
        // Add existing strings first but don't keep headers.
        if (!empty($this->merge)) {
            $existing_translations = new Translations();
            Po::fromFile($this->merge, $existing_translations);
            $translations->mergeWith($existing_translations, Merge::ADD | Merge::REMOVE);
        }
        PotGenerator::setCommentBeforeHeaders($this->get_file_comment());
        $this->set_default_headers($translations);
        // POT files have no Language header.
        $translations->deleteHeader(Translations::HEADER_LANGUAGE);
        // Only relevant for PO files, not POT files.
        $translations->setHeader('PO-Revision-Date', 'YEAR-MO-DA HO:MI+ZONE');
        if ($this->domain) {
            $translations->setDomain($this->domain);
        }
        unset($this->main_file_data['Version'], $this->main_file_data['License'], $this->main_file_data['Domain Path'], $this->main_file_data['Text Domain']);
        $is_theme = isset($this->main_file_data['Theme Name']);
        // Set entries from main file data.
        foreach ($this->main_file_data as $header => $data) {
            if (empty($data)) {
                continue;
            }
            $translation = new Translation('', $data);
            if ($is_theme) {
                $translation->addExtractedComment(\sprintf('%s of the theme', $header));
            } else {
                $translation->addExtractedComment(\sprintf('%s of the plugin', $header));
            }
            if ($this->main_file_path && $this->location) {
                $translation->addReference(\ltrim(\str_replace(Utils\normalize_path("{$this->source}/"), '', Utils\normalize_path($this->main_file_path)), '/'));
            }
            $translations[] = $translation;
        }
        try {
            if (!$this->skip_php) {
                $options = [
                    // Extract 'Template Name' headers in theme files.
                    'wpExtractTemplates' => $is_theme,
                    // Extract 'Title' and 'Description' headers from pattern files.
                    'wpExtractPatterns' => $is_theme,
                    'include' => $this->include,
                    'exclude' => $this->exclude,
                    'extensions' => ['php'],
                    'addReferences' => $this->location,
                ];
                PhpCodeExtractor::fromDirectory($this->source, $translations, $options);
            }
            if (!$this->skip_blade) {
                $options = ['include' => $this->include, 'exclude' => $this->exclude, 'extensions' => ['blade.php'], 'addReferences' => $this->location];
                BladeCodeExtractor::fromDirectory($this->source, $translations, $options);
            }
            if (!$this->skip_js) {
                JsCodeExtractor::fromDirectory($this->source, $translations, ['include' => $this->include, 'exclude' => $this->exclude, 'extensions' => ['js', 'jsx'], 'addReferences' => $this->location]);
                MapCodeExtractor::fromDirectory($this->source, $translations, ['include' => $this->include, 'exclude' => $this->exclude, 'extensions' => ['map'], 'addReferences' => $this->location]);
            }
            if (!$this->skip_block_json) {
                BlockExtractor::fromDirectory($this->source, $translations, [
                    'schema' => JsonSchemaExtractor::BLOCK_JSON_SOURCE,
                    'schemaFallback' => JsonSchemaExtractor::BLOCK_JSON_FALLBACK,
                    // Only look for block.json files, nothing else.
                    'restrictFileNames' => ['block.json'],
                    'include' => $this->include,
                    'exclude' => $this->exclude,
                    'extensions' => ['json'],
                    'addReferences' => $this->location,
                ]);
            }
            if (!$this->skip_theme_json) {
                // This will look for the top-level theme.json file, as well as
                // any JSON file within the top-level styles/ directory.
                ThemeJsonExtractor::fromDirectory($this->source, $translations, ['schema' => JsonSchemaExtractor::THEME_JSON_SOURCE, 'schemaFallback' => JsonSchemaExtractor::THEME_JSON_FALLBACK, 'include' => $this->include, 'exclude' => $this->exclude, 'extensions' => ['json'], 'addReferences' => $this->location]);
            }
        } catch (\Exception $e) {
            WP_CLI::error($e->getMessage());
        }
        foreach ($this->exceptions as $file => $exception_translations) {
            /** @var Translation $exception_translation */
            foreach ($exception_translations as $exception_translation) {
                if (!$translations->find($exception_translation)) {
                    continue;
                }
                if ($this->subtract_and_merge) {
                    $translation = $translations[$exception_translation->getId()];
                    $exception_translation->mergeWith($translation);
                }
                unset($translations[$exception_translation->getId()]);
            }
            if ($this->subtract_and_merge) {
                PotGenerator::toFile($exception_translations, $file);
            }
        }
        if (!$this->skip_audit) {
            $this->audit_strings($translations);
        }
        return $translations;
    }
    /**
     * Audits strings.
     *
     * Goes through all extracted strings to find possible mistakes.
     *
     * @param Translations $translations Translations object.
     */
    protected function audit_strings($translations)
    {
        foreach ($translations as $translation) {
            /** @var Translation $translation */
            $references = $translation->getReferences();
            // File headers don't have any file references.
            $location = $translation->hasReferences() ? '(' . \implode(':', $references[0]) . ')' : '';
            // Check 1: Flag strings with placeholders that should have translator comments.
            if (!$translation->hasExtractedComments() && \preg_match(self::SPRINTF_PLACEHOLDER_REGEX, $translation->getOriginal(), $placeholders) >= 1) {
                $message = \sprintf('The string "%1$s" contains placeholders but has no "translators:" comment to clarify their meaning. %2$s', $translation->getOriginal(), $location);
                WP_CLI::warning($message);
            }
            // Check 2: Flag strings with different translator comments.
            if ($translation->hasExtractedComments()) {
                $comments = $translation->getExtractedComments();
                // Remove plugin header information from comments.
                $comments = \array_filter($comments, function ($comment) {
                    /** @var ParsedComment|string $comment */
                    /** @var string $file_header */
                    foreach ($this->get_file_headers($this->project_type) as $file_header) {
                        if (0 === \strpos($comment instanceof ParsedComment ? $comment->getComment() : $comment, $file_header)) {
                            return null;
                        }
                    }
                    return $comment;
                });
                $unique_comments = array();
                // Remove duplicate comments.
                $comments = \array_filter($comments, function ($comment) use(&$unique_comments) {
                    /** @var ParsedComment|string $comment */
                    if (\in_array($comment instanceof ParsedComment ? $comment->getComment() : $comment, $unique_comments, \true)) {
                        return null;
                    }
                    $unique_comments[] = $comment instanceof ParsedComment ? $comment->getComment() : $comment;
                    return $comment;
                });
                $comments_count = \count($comments);
                if ($comments_count > 1) {
                    $message = \sprintf("The string \"%1\$s\" has %2\$d different translator comments. %3\$s\n%4\$s", $translation->getOriginal(), $comments_count, $location, \implode("\n", $unique_comments));
                    WP_CLI::warning($message);
                }
            }
            $non_placeholder_content = \trim(\preg_replace('`^([\'"])(.*)\\1$`Ds', '$2', $translation->getOriginal()));
            $non_placeholder_content = \preg_replace(self::SPRINTF_PLACEHOLDER_REGEX, '', $non_placeholder_content);
            // Check 3: Flag empty strings without any translatable content.
            if ('' === $non_placeholder_content) {
                $message = \sprintf('Found string without translatable content. %s', $location);
                WP_CLI::warning($message);
            }
            // Check 4: Flag strings with multiple unordered placeholders (%s %s %s vs. %1$s %2$s %3$s).
            $unordered_matches_count = \preg_match_all(self::UNORDERED_SPRINTF_PLACEHOLDER_REGEX, $translation->getOriginal(), $unordered_matches);
            $unordered_matches = $unordered_matches[0];
            if ($unordered_matches_count >= 2) {
                $message = \sprintf('Multiple placeholders should be ordered. %s', $location);
                WP_CLI::warning($message);
            }
            if ($translation->hasPlural()) {
                \preg_match_all(self::SPRINTF_PLACEHOLDER_REGEX, $translation->getOriginal(), $single_placeholders);
                $single_placeholders = $single_placeholders[0];
                \preg_match_all(self::SPRINTF_PLACEHOLDER_REGEX, $translation->getPlural(), $plural_placeholders);
                $plural_placeholders = $plural_placeholders[0];
                // see https://developer.wordpress.org/plugins/internationalization/how-to-internationalize-your-plugin/#plurals
                if (\count($single_placeholders) < \count($plural_placeholders)) {
                    // Check 5: Flag things like _n( 'One comment', '%s Comments' )
                    $message = \sprintf('Missing singular placeholder, needed for some languages. See https://developer.wordpress.org/plugins/internationalization/how-to-internationalize-your-plugin/#plurals %s', $location);
                    WP_CLI::warning($message);
                } else {
                    // Reordering is fine, but mismatched placeholders is probably wrong.
                    \sort($single_placeholders);
                    \sort($plural_placeholders);
                    // Check 6: Flag things like _n( '%s Comment (%d)', '%s Comments (%s)' )
                    if ($single_placeholders !== $plural_placeholders) {
                        $message = \sprintf('Mismatched placeholders for singular and plural string. %s', $location);
                        WP_CLI::warning($message);
                    }
                }
            }
        }
    }
    /**
     * Returns the copyright comment for the given package.
     *
     * @return string File comment.
     */
    protected function get_file_comment()
    {
        if ('' === $this->file_comment) {
            return '';
        }
        if (isset($this->file_comment)) {
            return \implode("\n", \explode('\\n', $this->file_comment));
        }
        if (isset($this->main_file_data['Theme Name'])) {
            if (!empty($this->main_file_data['License'])) {
                return \sprintf(
                    "Copyright (C) %1\$s %2\$s\nThis file is distributed under the %3\$s.",
                    \date('Y'),
                    // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
                    $this->main_file_data['Author'],
                    $this->main_file_data['License']
                );
            }
            return \sprintf(
                "Copyright (C) %1\$s %2\$s\nThis file is distributed under the same license as the %3\$s theme.",
                \date('Y'),
                // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
                $this->main_file_data['Author'],
                $this->main_file_data['Theme Name']
            );
        }
        if (isset($this->main_file_data['Plugin Name'])) {
            if (!empty($this->main_file_data['License'])) {
                return \sprintf(
                    "Copyright (C) %1\$s %2\$s\nThis file is distributed under the %3\$s.",
                    \date('Y'),
                    // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
                    $this->main_file_data['Author'],
                    $this->main_file_data['License']
                );
            }
            return \sprintf(
                "Copyright (C) %1\$s %2\$s\nThis file is distributed under the same license as the %3\$s plugin.",
                \date('Y'),
                // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
                $this->main_file_data['Author'],
                $this->main_file_data['Plugin Name']
            );
        }
        return '';
    }
    /**
     * Sets default POT file headers for the project.
     *
     * @param Translations $translations Translations object.
     */
    protected function set_default_headers($translations)
    {
        $name = null;
        $version = $this->get_wp_version();
        $bugs_address = null;
        if (!$version && isset($this->main_file_data['Version'])) {
            $version = $this->main_file_data['Version'];
        }
        if (isset($this->main_file_data['Theme Name'])) {
            $name = $this->main_file_data['Theme Name'];
            $bugs_address = \sprintf('https://wordpress.org/support/theme/%s', $this->slug);
        } elseif (isset($this->main_file_data['Plugin Name'])) {
            $name = $this->main_file_data['Plugin Name'];
            $bugs_address = \sprintf('https://wordpress.org/support/plugin/%s', $this->slug);
        }
        if (null !== $this->package_name) {
            $name = $this->package_name;
        }
        if (null !== $name) {
            $translations->setHeader('Project-Id-Version', $name . ($version ? ' ' . $version : ''));
        }
        if (null !== $bugs_address) {
            $translations->setHeader('Report-Msgid-Bugs-To', $bugs_address);
        }
        $translations->setHeader('Last-Translator', 'FULL NAME <EMAIL@ADDRESS>');
        $translations->setHeader('Language-Team', 'LANGUAGE <LL@li.org>');
        $translations->setHeader('X-Generator', 'WP-CLI ' . WP_CLI_VERSION);
        foreach ($this->headers as $key => $value) {
            $translations->setHeader($key, $value);
        }
    }
    /**
     * Extracts the WordPress version number from wp-includes/version.php.
     *
     * @return string|false Version number on success, false otherwise.
     */
    protected function get_wp_version()
    {
        $version_php = $this->source . '/wp-includes/version.php';
        if (!\file_exists($version_php) || !\is_readable($version_php)) {
            return \false;
        }
        return \preg_match('/\\$wp_version\\s*=\\s*\'(.*?)\';/', \file_get_contents($version_php), $matches) ? $matches[1] : \false;
    }
}
