# Copyright 2020 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.
"""Helpful commands for working with a Git repository."""

from pathlib import Path
import subprocess
from typing import Collection, Pattern

from pw_cli import git_repo
from pw_cli.decorators import deprecated
from pw_presubmit.tools import PresubmitToolRunner

# Moved to pw_cli.git_repo.
TRACKING_BRANCH_ALIAS = git_repo.TRACKING_BRANCH_ALIAS


class LoggingGitRepo(git_repo.GitRepo):
    """A version of GitRepo that defaults to using a PresubmitToolRunner."""

    def __init__(self, repo_root: Path):
        super().__init__(repo_root, PresubmitToolRunner())


@deprecated('Extend GitRepo to expose needed functionality.')
def git_stdout(
    *args: Path | str, show_stderr=False, repo: Path | str = '.'
) -> str:
    """Runs a git command in the specified repo."""
    tool_runner = PresubmitToolRunner()
    return (
        tool_runner(
            'git',
            ['-C', str(repo), *args],
            stderr=None if show_stderr else subprocess.DEVNULL,
            check=True,
            pw_presubmit_ignore_dry_run=True,
        )
        .stdout.decode()
        .strip()
    )


def find_git_repo(path_in_repo: Path) -> git_repo.GitRepo:
    """Tries to find the root of the git repo that owns path_in_repo."""
    return git_repo.find_git_repo(path_in_repo, PresubmitToolRunner())


@deprecated('Use GitRepo().tracking_branch().')
def tracking_branch(
    repo_path: Path | None = None,
    fallback: str | None = None,
) -> str | None:
    """Returns the tracking branch of the current branch.

    Since most callers of this function can safely handle a return value of
    None, suppress exceptions and return None if there is no tracking branch.

    Args:
      repo_path: repo path from which to run commands; defaults to Path.cwd()

    Raises:
      ValueError: if repo_path is not in a Git repository

    Returns:
      the remote tracking branch name or None if there is none
    """
    repo = repo_path if repo_path is not None else Path.cwd()
    return find_git_repo(repo).tracking_branch(fallback)


@deprecated('Use GitRepo().list_files().')
def list_files(
    commit: str | None = None,
    pathspecs: Collection[Path | str] = (),
    repo_path: Path | None = None,
) -> list[Path]:
    """Lists files with git ls-files or git diff --name-only.

    Args:
      commit: commit to use as a base for git diff
      pathspecs: Git pathspecs to use in git ls-files or diff
      repo_path: repo path from which to run commands; defaults to Path.cwd()

    Returns:
      A sorted list of absolute paths
    """
    repo = repo_path if repo_path is not None else Path.cwd()
    return find_git_repo(repo).list_files(commit, pathspecs)


@deprecated('Use GitRepo.has_uncommitted_changes().')
def has_uncommitted_changes(repo: Path | None = None) -> bool:
    """Returns True if the Git repo has uncommitted changes in it.

    This does not check for untracked files.
    """
    return LoggingGitRepo(
        repo if repo is not None else Path.cwd()
    ).has_uncommitted_changes()


def describe_files(
    git_root: Path,  # pylint: disable=unused-argument
    working_dir: Path,
    commit: str | None,
    pathspecs: Collection[Path | str],
    exclude: Collection[Pattern],
    project_root: Path | None = None,
) -> str:
    """Completes 'Doing something to ...' for a set of files in a Git repo."""
    return git_repo.describe_git_pattern(
        working_dir=working_dir,
        commit=commit,
        pathspecs=pathspecs,
        exclude=exclude,
        tool_runner=PresubmitToolRunner(),
        project_root=project_root,
    )


@deprecated('Use find_git_repo().root().')
def root(repo_path: Path | str = '.') -> Path:
    """Returns the repository root as an absolute path.

    Raises:
      FileNotFoundError: the path does not exist
      subprocess.CalledProcessError: the path is not in a Git repo
    """
    return find_git_repo(Path(repo_path)).root()


def within_repo(repo_path: Path | str = '.') -> Path | None:
    """Similar to root(repo_path), returns None if the path is not in a repo."""
    try:
        return root(repo_path)
    except git_repo.GitError:
        return None


def is_repo(repo_path: Path | str = '.') -> bool:
    """True if the path is tracked by a Git repo."""
    return within_repo(repo_path) is not None


def path(
    repo_path: Path | str,
    *additional_repo_paths: Path | str,
    repo: Path | str = '.',
) -> Path:
    """Returns a path relative to a Git repository's root."""
    return root(repo).joinpath(repo_path, *additional_repo_paths)


@deprecated('Use GitRepo.commit_message().')
def commit_message(commit: str = 'HEAD', repo: Path | str = '.') -> str:
    """Returns the commit message of the revision."""
    return LoggingGitRepo(Path(repo)).commit_message(commit)


@deprecated('Use GitRepo.commit_author().')
def commit_author(commit: str = 'HEAD', repo: Path | str = '.') -> str:
    """Returns the commit author of the revision."""
    return LoggingGitRepo(Path(repo)).commit_author(commit)


@deprecated('Use GitRepo.commit_hash().')
def commit_hash(
    rev: str = 'HEAD', short: bool = True, repo: Path | str = '.'
) -> str:
    """Returns the commit hash of the revision."""
    return LoggingGitRepo(Path(repo)).commit_hash(rev, short)


@deprecated('Use GitRepo.list_submodules().')
def discover_submodules(
    superproject_dir: Path, excluded_paths: Collection[Pattern | str] = ()
) -> list[Path]:
    """Query git and return a list of submodules in the current project.

    Args:
        superproject_dir: Path object to directory under which we are looking
                          for submodules. This will also be included in list
                          returned unless excluded.
        excluded_paths: Pattern or string that match submodules that should not
                        be returned. All matches are done on posix style paths.

    Returns:
        List of "Path"s which were found but not excluded, this includes
        superproject_dir unless excluded.
    """
    return LoggingGitRepo(superproject_dir).list_submodules(excluded_paths) + [
        superproject_dir.resolve()
    ]
