# Copyright 2022 The Pigweed Authors
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
# the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
"""pw_ide CLI command handlers."""

import logging
from pathlib import Path
import shlex
import shutil
import subprocess
import sys
from typing import cast, Callable, List, Optional, Set, Tuple, Union

from pw_cli.color import colors

from pw_ide.cpp import (
    ClangdSettings,
    compdb_generate_file_path,
    CppCompilationDatabase,
    CppCompilationDatabasesMap,
    CppIdeFeaturesState,
    delete_compilation_databases,
    delete_compilation_database_caches,
    MAX_COMMANDS_TARGET_FILENAME,
)

from pw_ide.exceptions import (
    BadCompDbException,
    InvalidTargetException,
    MissingCompDbException,
)

from pw_ide.python import PythonPaths

from pw_ide.settings import (
    PigweedIdeSettings,
    SupportedEditor,
    SupportedEditorName,
)

from pw_ide import vscode
from pw_ide.vscode import VscSettingsManager, VscSettingsType


def _no_color(msg: str) -> str:
    return msg


def _split_lines(msg: Union[str, List[str]]) -> Tuple[str, List[str]]:
    """Turn a list of strings into a tuple of the first and list of rest."""
    if isinstance(msg, str):
        return (msg, [])

    return (msg[0], msg[1:])


class StatusReporter:
    """Print user-friendly status reports to the terminal for CLI tools.

    The output of ``demo()`` looks something like this, but more colorful:

    .. code-block:: none

       • FYI, here's some information:
         Lorem ipsum dolor sit amet, consectetur adipiscing elit.
         Donec condimentum metus molestie metus maximus ultricies ac id dolor.
       ✓ This is okay, no changes needed.
       ✓ We changed some things successfully!
       ⚠ Uh oh, you might want to be aware of this.
       ❌ This is bad! Things might be broken!

    You can instead redirect these lines to logs without formatting by
    substituting ``LoggingStatusReporter``. Consumers of this should be
    designed to take any subclass and not make assumptions about where the
    output will go. But the reason you would choose this over plain logging is
    because you want to support pretty-printing to the terminal.

    This is also "themable" in the sense that you can subclass this, override
    the methods with whatever formatting you want, and supply the subclass to
    anything that expects an instance of this.

    Key:

    - info: Plain ol' informational status.
    - ok: Something was checked and it was okay.
    - new: Something needed to be changed/updated and it was successfully.
    - wrn: Warning, non-critical.
    - err: Error, critical.

    This doesn't expose the %-style string formatting that is used in idiomatic
    Python logging, but this shouldn't be used for performance-critical logging
    situations anyway.
    """

    def _report(  # pylint: disable=no-self-use
        self,
        msg: Union[str, List[str]],
        color: Callable[[str], str],
        char: str,
        func: Callable,
        silent: bool,
    ) -> None:
        """Actually print/log/whatever the status lines."""
        first_line, rest_lines = _split_lines(msg)
        first_line = color(f'{char} {first_line}')
        spaces = ' ' * len(char)
        rest_lines = [color(f'{spaces} {line}') for line in rest_lines]

        if not silent:
            for line in [first_line, *rest_lines]:
                func(line)

    def demo(self):
        """Run this to see what your status reporter output looks like."""
        self.info(
            [
                'FYI, here\'s some information:',
                'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
                'Donec condimentum metus molestie metus maximus ultricies '
                'ac id dolor.',
            ]
        )
        self.ok('This is okay, no changes needed.')
        self.new('We changed some things successfully!')
        self.wrn('Uh oh, you might want to be aware of this.')
        self.err('This is bad! Things might be broken!')

    def info(self, msg: Union[str, List[str]], silent: bool = False) -> None:
        self._report(msg, _no_color, '\u2022', print, silent)

    def ok(self, msg: Union[str, List[str]], silent: bool = False) -> None:
        self._report(msg, colors().blue, '\u2713', print, silent)

    def new(self, msg: Union[str, List[str]], silent: bool = False) -> None:
        self._report(msg, colors().green, '\u2713', print, silent)

    def wrn(self, msg: Union[str, List[str]], silent: bool = False) -> None:
        self._report(msg, colors().yellow, '\u26A0', print, silent)

    def err(self, msg: Union[str, List[str]], silent: bool = False) -> None:
        self._report(msg, colors().red, '\u274C', print, silent)


class LoggingStatusReporter(StatusReporter):
    """Print status lines to logs instead of to the terminal."""

    def __init__(self, logger: logging.Logger) -> None:
        self.logger = logger
        super().__init__()

    def _report(
        self,
        msg: Union[str, List[str]],
        color: Callable[[str], str],
        char: str,
        func: Callable,
        silent: bool,
    ) -> None:
        first_line, rest_lines = _split_lines(msg)

        if not silent:
            for line in [first_line, *rest_lines]:
                func(line)

    def info(self, msg: Union[str, List[str]], silent: bool = False) -> None:
        self._report(msg, _no_color, '', self.logger.info, silent)

    def ok(self, msg: Union[str, List[str]], silent: bool = False) -> None:
        self._report(msg, _no_color, '', self.logger.info, silent)

    def new(self, msg: Union[str, List[str]], silent: bool = False) -> None:
        self._report(msg, _no_color, '', self.logger.info, silent)

    def wrn(self, msg: Union[str, List[str]], silent: bool = False) -> None:
        self._report(msg, _no_color, '', self.logger.warning, silent)

    def err(self, msg: Union[str, List[str]], silent: bool = False) -> None:
        self._report(msg, _no_color, '', self.logger.error, silent)


def _make_working_dir(
    reporter: StatusReporter, settings: PigweedIdeSettings, quiet: bool = False
) -> None:
    if not settings.working_dir.exists():
        settings.working_dir.mkdir()
        reporter.new(
            'Initialized the Pigweed IDE working directory at '
            f'{settings.working_dir}'
        )
    else:
        if not quiet:
            reporter.ok(
                'Pigweed IDE working directory already present at '
                f'{settings.working_dir}'
            )


def _report_unrecognized_editor(reporter: StatusReporter, editor: str) -> None:
    supported_editors = ', '.join(sorted([ed.value for ed in SupportedEditor]))
    reporter.wrn(f'Unrecognized editor: {editor}')
    reporter.wrn('This may not be an automatically-supported editor.')
    reporter.wrn(f'Automatically-supported editors: {supported_editors}')


def cmd_clear(
    compdb: bool,
    cache: bool,
    editor: Optional[SupportedEditorName],
    editor_backups: Optional[SupportedEditorName],
    silent: bool = False,
    reporter: StatusReporter = StatusReporter(),
    pw_ide_settings: PigweedIdeSettings = PigweedIdeSettings(),
) -> None:
    """Clear components of the IDE features.

    In contrast to the ``reset`` subcommand, ``clear`` allows you to specify
    components to delete. You will not need this command under normal
    circumstances.
    """
    if compdb:
        delete_compilation_databases(pw_ide_settings)
        reporter.wrn('Cleared compilation databases', silent)

    if cache:
        delete_compilation_database_caches(pw_ide_settings)
        reporter.wrn('Cleared compilation database caches', silent)

    if editor is not None:
        try:
            validated_editor = SupportedEditor(editor)
        except ValueError:
            _report_unrecognized_editor(reporter, cast(str, editor))
            sys.exit(1)

        if validated_editor == SupportedEditor.VSCODE:
            vsc_settings_manager = VscSettingsManager(pw_ide_settings)
            vsc_settings_manager.delete_all_active_settings()

        reporter.wrn(
            f'Cleared active settings for {validated_editor.value}', silent
        )

    if editor_backups is not None:
        try:
            validated_editor = SupportedEditor(editor_backups)
        except ValueError:
            _report_unrecognized_editor(reporter, cast(str, editor))
            sys.exit(1)

        if validated_editor == SupportedEditor.VSCODE:
            vsc_settings_manager = VscSettingsManager(pw_ide_settings)
            vsc_settings_manager.delete_all_backups()

        reporter.wrn(
            f'Cleared backup settings for {validated_editor.value}',
            silent=silent,
        )


def cmd_reset(
    hard: bool = False,
    reporter: StatusReporter = StatusReporter(),
    pw_ide_settings: PigweedIdeSettings = PigweedIdeSettings(),
) -> None:
    """Reset IDE settings.

    This will clear your .pw_ide working directory and active settings for
    supported editors, restoring your repository to a pre-"pw ide setup" state.
    Any clangd caches in the working directory will not be removed, so that they
    don't need to be generated again later. All backed up supported editor
    settings will also be left in place.

    Adding the --hard flag will completely delete the .pw_ide directory and all
    supported editor backup settings, restoring your repository to a
    pre-`pw ide setup` state.

    This command does not affect this project's pw_ide and editor settings or
    your own pw_ide and editor override settings.
    """
    delete_compilation_databases(pw_ide_settings)
    vsc_settings_manager = VscSettingsManager(pw_ide_settings)
    vsc_settings_manager.delete_all_active_settings()

    if hard:
        try:
            shutil.rmtree(pw_ide_settings.working_dir)
        except FileNotFoundError:
            pass

        vsc_settings_manager.delete_all_backups()

    reporter.wrn('Pigweed IDE settings were reset!')


def cmd_setup(
    reporter: StatusReporter = StatusReporter(),
    pw_ide_settings: PigweedIdeSettings = PigweedIdeSettings(),
) -> None:
    """Set up or update your Pigweed project IDE features.

    This will automatically set up your development environment with all the
    features that Pigweed IDE supports, with sensible defaults.

    At minimum, this command will create the .pw_ide working directory and
    create settings files for all supported editors. Projects can define
    additional setup steps in .pw_ide.yaml.

    When new IDE features are introduced in the future (either by Pigweed or
    your downstream project), you can re-run this command to set up the new
    features. It will not overwrite or break any of your existing configuration.
    """
    _make_working_dir(reporter, pw_ide_settings)

    if pw_ide_settings.editor_enabled('vscode'):
        cmd_vscode()

    for command in pw_ide_settings.setup:
        subprocess.run(shlex.split(command))


def cmd_vscode(
    include: Optional[List[VscSettingsType]] = None,
    exclude: Optional[List[VscSettingsType]] = None,
    no_override: bool = False,
    reporter: StatusReporter = StatusReporter(),
    pw_ide_settings: PigweedIdeSettings = PigweedIdeSettings(),
) -> None:
    """Configure support for Visual Studio Code.

    This will replace your current Visual Studio Code (VSC) settings for this
    project (in ``.vscode/settings.json``, etc.) with the following sets of
    settings, in order:

    - The Pigweed default settings
    - Your project's settings, if any (in ``.vscode/pw_project_settings.json``)
    - Your personal settings, if any (in ``.vscode/pw_user_settings.json``)

    In other words, settings files lower on the list can override settings
    defined by those higher on the list. Settings defined in the sources above
    are not active in VSC until they are merged and output to the current
    settings file by running:

    .. code-block:: bash

       pw ide vscode

    Refer to the Visual Studio Code documentation for more information about
    these settings: https://code.visualstudio.com/docs/getstarted/settings

    This command also manages VSC tasks (``.vscode/tasks.json``) and extensions
    (``.vscode/extensions.json``). You can explicitly control which of these
    settings types ("settings", "tasks", and "extensions") is modified by
    this command by using the ``--include`` or ``--exclude`` options.

    Your current VSC settings will never change unless you run ``pw ide``
    commands. Since the current VSC settings are an artifact built from the
    three settings files described above, you should avoid manually editing
    that file; it will be replaced the next time you run ``pw ide vscode``. A
    backup of your previous settings file will be made, and you can diff it
    against the new file to see what changed.

    These commands will never modify your VSC user settings, which are
    stored outside of the project repository and apply globally to all VSC
    instances.

    The settings files are system-specific and shouldn't be checked into the
    repository, except for the project settings (those with ``pw_project_``),
    which can be used to define consistent settings for everyone working on the
    project.

    Note that support for VSC can be disabled at the project level or the user
    level by adding the following to .pw_ide.yaml or .pw_ide.user.yaml
    respectively:

    .. code-block:: yaml

        editors:
          vscode: false

    Likewise, it can be enabled by setting that value to true. It is enabled by
    default.
    """
    if not pw_ide_settings.editor_enabled('vscode'):
        reporter.wrn('Visual Studio Code support is disabled in settings!')
        sys.exit(1)

    if not vscode.DEFAULT_SETTINGS_PATH.exists():
        vscode.DEFAULT_SETTINGS_PATH.mkdir()

    vsc_manager = VscSettingsManager(pw_ide_settings)

    if include is None:
        include_set = set(VscSettingsType.all())
    else:
        include_set = set(include)

    if exclude is None:
        exclude_set: Set[VscSettingsType] = set()
    else:
        exclude_set = set(exclude)

    types_to_update = cast(
        List[VscSettingsType], tuple(include_set - exclude_set)
    )

    for settings_type in types_to_update:
        active_settings_existed = vsc_manager.active(settings_type).is_present()

        if no_override and active_settings_existed:
            reporter.ok(
                f'Visual Studio Code active {settings_type.value} '
                'already present; will not overwrite'
            )

        else:
            with vsc_manager.active(settings_type).modify(
                reinit=True
            ) as active_settings:
                vsc_manager.default(settings_type).sync_to(active_settings)
                vsc_manager.project(settings_type).sync_to(active_settings)
                vsc_manager.user(settings_type).sync_to(active_settings)

            verb = 'Updated' if active_settings_existed else 'Created'
            reporter.new(
                f'{verb} Visual Studio Code active ' f'{settings_type.value}'
            )


# TODO(chadnorvell): Break up this function.
# The linting errors are a nuisance but they're beginning to have a point.
def cmd_cpp(  # pylint: disable=too-many-arguments, too-many-locals, too-many-branches, too-many-statements
    should_list_targets: bool,
    should_get_target: bool,
    target_to_set: Optional[str],
    compdb_file_paths: Optional[List[Path]],
    build_dir: Optional[Path],
    use_default_target: bool = False,
    should_run_ninja: bool = False,
    should_run_gn: bool = False,
    override_current_target: bool = True,
    clangd_command: bool = False,
    clangd_command_system: Optional[str] = None,
    reporter: StatusReporter = StatusReporter(),
    pw_ide_settings: PigweedIdeSettings = PigweedIdeSettings(),
) -> None:
    """Configure C/C++ code intelligence support.

    Code intelligence can be provided by clangd or other language servers that
    use the clangd compilation database format, defined at:
    https://clang.llvm.org/docs/JSONCompilationDatabase.html

    This command helps you use clangd with Pigweed projects, which use multiple
    toolchains within a distinct environment, and often define multiple targets.
    This means compilation units are likely have multiple compile commands, and
    clangd is not equipped to deal with this out of the box. We handle this by:

    - Processing the compilation database produced the build system into
      multiple internally-consistent compilation databases, one for each target
      (where a "target" is a particular build for a particular system using a
      particular toolchain).

    - Providing commands to select which target you want to use for code
      analysis.

    Refer to the Pigweed documentation or your build system's documentation to
    learn how to produce a clangd compilation database. Once you have one, run
    this command to process it (or provide a glob to process multiple):

    .. code-block:: bash

        pw ide cpp --process {path to compile_commands.json}

    If you're using GN to generate the compilation database, you can do that and
    process it in a single command:

    .. code-block:: bash

       pw ide cpp --gn

    You can do the same for a Ninja build (whether it was generated by GN or
    another way):

    .. code-block:: bash

       pw ide cpp --ninja

    You can now examine the targets that are available to you:

    .. code-block:: bash

        pw ide cpp --list

    ... and select the target you want to use:

    .. code-block:: bash

        pw ide cpp --set host_clang

    As long as your editor or language server plugin is properly configured, you
    will now get code intelligence features relevant to that particular target.

    You can see what target is selected by running:

    .. code-block:: bash

        pw ide cpp

    Whenever you switch to a target you haven't used before, clangd will need to
    index the build, which may take several minutes. These indexes are cached,
    so you can switch between targets without re-indexing each time.

    If your build configuration changes significantly (e.g. you add a new file
    to the project), you will need to re-process the compilation database for
    that change to be recognized. Your target selection will not change, and
    your index will only need to be incrementally updated.

    You can generate the clangd command your editor needs to run with:

    .. code-block:: bash

        pw ide cpp --clangd-command

    If your editor uses JSON for configuration, you can export the same command
    in that format:

    .. code-block:: bash

        pw ide cpp --clangd-command-for json
    """
    _make_working_dir(reporter, pw_ide_settings, quiet=True)

    # If true, no arguments were provided so we do the default behavior.
    default = True

    build_dir = (
        build_dir if build_dir is not None else pw_ide_settings.build_dir
    )

    if compdb_file_paths is not None:
        should_process = True

        if len(compdb_file_paths) == 0:
            compdb_file_paths = pw_ide_settings.compdb_paths_expanded
    else:
        should_process = False
        # This simplifies typing in the rest of this method. We rely on
        # `should_process` instead of the status of this variable.
        compdb_file_paths = []

    # Order of operations matters from here on. It should be possible to run
    # a build system command to generate a compilation database, then process
    # the compilation database, then successfully set the target in a single
    # command.

    # Use Ninja to generate the initial compile_commands.json
    if should_run_ninja:
        default = False

        ninja_commands = ['ninja', '-t', 'compdb']
        reporter.info(f'Running Ninja: {" ".join(ninja_commands)}')

        output_compdb_file_path = build_dir / compdb_generate_file_path()

        try:
            # Ninja writes to STDOUT, so we capture to a file.
            with open(output_compdb_file_path, 'w') as compdb_file:
                result = subprocess.run(
                    ninja_commands,
                    cwd=build_dir,
                    stdout=compdb_file,
                    stderr=subprocess.PIPE,
                )
        except FileNotFoundError:
            reporter.err(f'Could not open path! {str(output_compdb_file_path)}')

        if result.returncode == 0:
            reporter.info('Ran Ninja successfully!')
            should_process = True
            compdb_file_paths.append(output_compdb_file_path)
        else:
            reporter.err('Something went wrong!')
            # Convert from bytes and remove trailing newline
            err = result.stderr.decode().split('\n')[:-1]

            for line in err:
                reporter.err(line)

            sys.exit(1)

    # Use GN to generate the initial compile_commands.json
    if should_run_gn:
        default = False

        gn_commands = ['gn', 'gen', str(build_dir), '--export-compile-commands']

        try:
            with open(build_dir / 'args.gn') as args_file:
                gn_args = [
                    line
                    for line in args_file.readlines()
                    if not line.startswith('#')
                ]
        except FileNotFoundError:
            gn_args = []

        gn_args_string = 'none' if len(gn_args) == 0 else ', '.join(gn_args)

        reporter.info(
            [f'Running GN: {" ".join(gn_commands)} (args: {gn_args_string})']
        )

        result = subprocess.run(gn_commands, capture_output=True)
        gn_status_lines = ['Ran GN successfully!']

        if result.returncode == 0:
            # Convert from bytes and remove trailing newline
            out = result.stdout.decode().split('\n')[:-1]

            for line in out:
                gn_status_lines.append(line)

            reporter.info(gn_status_lines)
            should_process = True
            output_compdb_file_path = build_dir / compdb_generate_file_path()
            compdb_file_paths.append(output_compdb_file_path)
        else:
            reporter.err('Something went wrong!')
            # Convert from bytes and remove trailing newline
            err = result.stderr.decode().split('\n')[:-1]

            for line in err:
                reporter.err(line)

            sys.exit(1)

    if should_process:
        default = False
        prev_targets = len(CppIdeFeaturesState(pw_ide_settings))
        compdb_databases: List[CppCompilationDatabasesMap] = []
        last_processed_path = Path()

        for compdb_file_path in compdb_file_paths:
            # If the path is a dir, append the default compile commands
            # file name.
            if compdb_file_path.is_dir():
                compdb_file_path /= compdb_generate_file_path()

            try:
                compdb_databases.append(
                    CppCompilationDatabase.load(
                        Path(compdb_file_path), build_dir
                    ).process(
                        settings=pw_ide_settings,
                        path_globs=pw_ide_settings.clangd_query_drivers(),
                    )
                )
            except MissingCompDbException:
                reporter.err(f'File not found: {str(compdb_file_path)}')

                if '*' in str(compdb_file_path):
                    reporter.wrn(
                        'It looks like you provided a glob that '
                        'did not match any files.'
                    )

                sys.exit(1)
            # TODO(chadnorvell): Recover more gracefully from errors.
            except BadCompDbException:
                reporter.err(
                    'File does not match compilation database format: '
                    f'{str(compdb_file_path)}'
                )
                sys.exit(1)

            last_processed_path = compdb_file_path

        if len(compdb_databases) == 0:
            reporter.err(
                'No compilation databases found in: '
                f'{str(compdb_file_paths)}'
            )
            sys.exit(1)

        try:
            CppCompilationDatabasesMap.merge(*compdb_databases).write()
        except TypeError:
            reporter.err('Could not serialize file to JSON!')

        total_targets = len(CppIdeFeaturesState(pw_ide_settings))
        new_targets = total_targets - prev_targets

        if len(compdb_file_paths) == 1:
            processed_text = str(last_processed_path)
        else:
            processed_text = f'{len(compdb_file_paths)} compilation databases'

        reporter.new(
            [
                f'Processed {processed_text} '
                f'to {pw_ide_settings.working_dir}',
                f'{total_targets} targets are now available '
                f'({new_targets} are new)',
            ]
        )

    if use_default_target:
        defined_default = pw_ide_settings.default_target
        max_commands_target: Optional[str] = None

        try:
            with open(
                pw_ide_settings.working_dir / MAX_COMMANDS_TARGET_FILENAME
            ) as max_commands_target_file:
                max_commands_target = max_commands_target_file.readline()
        except FileNotFoundError:
            pass

        if defined_default is None and max_commands_target is None:
            reporter.err('Can\'t use default target because none is defined!')
            reporter.wrn('Have you processed a compilation database yet?')
            sys.exit(1)

        target_to_set = (
            defined_default
            if defined_default is not None
            else max_commands_target
        )

    if target_to_set is not None:
        default = False

        # Always set the target if it's not already set, but if it is,
        # respect the --no-override flag.
        should_set_target = (
            CppIdeFeaturesState(pw_ide_settings).current_target is None
            or override_current_target
        )

        if should_set_target:
            try:
                CppIdeFeaturesState(
                    pw_ide_settings
                ).current_target = target_to_set
            except InvalidTargetException:
                reporter.err(
                    [
                        f'Invalid target! {target_to_set} not among the '
                        'defined targets.',
                        'Check .pw_ide.yaml or .pw_ide.user.yaml for defined '
                        'targets.',
                    ]
                )
                sys.exit(1)
            except MissingCompDbException:
                reporter.err(
                    [
                        f'File not found for target! {target_to_set}',
                        'Did you run pw ide cpp --process '
                        '{path to compile_commands.json}?',
                    ]
                )
                sys.exit(1)

            reporter.new(
                'Set C/C++ language server analysis target to: '
                f'{target_to_set}'
            )
        else:
            reporter.ok(
                'Target already is set and will not be overridden: '
                f'{CppIdeFeaturesState(pw_ide_settings).current_target}'
            )

    if clangd_command:
        default = False
        reporter.info(
            [
                'Command to run clangd with Pigweed paths:',
                ClangdSettings(pw_ide_settings).command(),
            ]
        )

    if clangd_command_system is not None:
        default = False
        reporter.info(
            [
                'Command to run clangd with Pigweed paths for '
                f'{clangd_command_system}:',
                ClangdSettings(pw_ide_settings).command(clangd_command_system),
            ]
        )

    if should_list_targets:
        default = False
        targets_list_status = [
            'C/C++ targets available for language server analysis:'
        ]

        for target in sorted(
            CppIdeFeaturesState(pw_ide_settings).enabled_available_targets
        ):
            targets_list_status.append(f'\t{target}')

        reporter.info(targets_list_status)

    if should_get_target or default:
        reporter.info(
            'Current C/C++ language server analysis target: '
            f'{CppIdeFeaturesState(pw_ide_settings).current_target}'
        )


def cmd_python(
    should_print_venv: bool, reporter: StatusReporter = StatusReporter()
) -> None:
    """Configure Python code intelligence support.

    You can generate the path to the Python virtual environment interpreter that
    your editor/language server should use with:

    .. code-block:: bash

       pw ide python --venv
    """
    # If true, no arguments were provided and we should do the default
    # behavior.
    default = True

    if should_print_venv or default:
        reporter.info(
            [
                'Location of the Pigweed Python virtual environment:',
                PythonPaths().interpreter,
            ]
        )
