# Copyright 2019 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

# This file is imported by various thin wrappers (around gn, clang-format, ...),
# so it's meant to import very quickly. To keep it that way don't add more
# code, and even more importantly don't add more toplevel import statements,
# particularly for modules that are not builtin (see sys.builtin_modules_names,
# os isn't built in, but it's essential to this file).

import functools
import logging
import os
import sys

import gclient_eval
import gclient_utils
import subprocess2

@functools.lru_cache
def FindGclientRoot(from_dir, filename=".gclient"):
    """Tries to find the gclient root."""
    real_from_dir = os.path.abspath(from_dir)
    path = real_from_dir
    while not os.path.exists(os.path.join(path, filename)):
        split_path = os.path.split(path)
        if not split_path[1]:
            return None
        path = split_path[0]

    logging.info("Found gclient root at " + path)

    if path == real_from_dir:
        return path

    # If we did not find the file in the current directory, make sure we are in
    # a sub directory that is controlled by this configuration.
    entries_filename = os.path.join(path, filename + "_entries")
    if not os.path.exists(entries_filename):
        if not gclient_utils.IsEnvCog():
            # If .gclient_entries does not exist, a previous call to
            # gclient sync might have failed. In that case, we cannot verify
            # that the .gclient is the one we want to use. In order to not
            # to cause too much trouble, just issue a warning and return
            # the path anyway.
            print(
                "%s missing, %s file in parent directory %s might not be "
                "the file you want to use."
                % (entries_filename, filename, path),
                file=sys.stderr,
            )
        return path

    entries_content = gclient_utils.FileRead(entries_filename)
    scope = gclient_eval.ParseLocalConfig(entries_content, entries_filename)

    all_directories = set(
        os.path.relpath(
            os.path.realpath(os.path.join(path, *k.split("/"))),
            start=os.path.realpath(path),
        )
        for k in scope["entries"].keys()
    )
    path_to_check = os.path.relpath(
        os.path.realpath(real_from_dir), start=os.path.realpath(path)
    )
    while path_to_check:
        if path_to_check in all_directories:
            return path
        path_to_check = os.path.dirname(path_to_check)

    return None

@functools.lru_cache
def _GetPrimarySolutionPathInternal(cwd):
    gclient_root = FindGclientRoot(cwd)
    if gclient_root:
        # Some projects' top directory is not named 'src'.
        source_dir_name = GetGClientPrimarySolutionName(gclient_root) or "src"
        return os.path.join(gclient_root, source_dir_name)

    # Some projects might not use .gclient. Try to see whether we're in a git
    # checkout that contains a "buildtools" directory or "codereview.settings"
    # file.
    top_dir = cwd
    try:
        top_dir = subprocess2.check_output(
            ["git", "rev-parse", "--show-toplevel"], stderr=subprocess2.DEVNULL
        )
        top_dir = top_dir.decode("utf-8", "replace")
        top_dir = os.path.normpath(top_dir.strip())
    except subprocess2.CalledProcessError:
        pass

    if os.path.exists(
        os.path.join(top_dir, "codereview.settings")
    ) or os.path.exists(os.path.join(top_dir, "buildtools")):
        return top_dir
    return None

def GetPrimarySolutionPath(from_dir=None):
    """Returns the full path to the primary solution. (gclient_root + src)"""
    if not from_dir:
        from_dir = os.getcwd()
    from_dir = os.path.abspath(from_dir)
    return _GetPrimarySolutionPathInternal(from_dir)

@functools.lru_cache
def _GetBuildtoolsPathInternal(cwd, override):
    if override is not None:
        return override

    primary_solution = GetPrimarySolutionPath()
    if not primary_solution:
        return None

    buildtools_path = os.path.join(primary_solution, "buildtools")
    if os.path.exists(buildtools_path):
        return buildtools_path

    # buildtools may be in the gclient root.
    gclient_root = FindGclientRoot(os.getcwd())
    if not gclient_root:
        return None
    buildtools_path = os.path.join(gclient_root, "buildtools")
    if os.path.exists(buildtools_path):
        return buildtools_path

    # also check for src/buildtools in the gclient root
    src_buildtools_path = os.path.join(gclient_root, "src", "buildtools")
    if os.path.exists(src_buildtools_path):
        return src_buildtools_path

    return None

def GetBuildtoolsPath():
    """Returns the full path to the buildtools directory.
    This is based on the root of the checkout containing the current directory."""
    # Overriding the build tools path by environment is highly unsupported and
    # may break without warning.  Do not rely on this for anything important.
    override = os.environ.get("CHROMIUM_BUILDTOOLS_PATH")
    return _GetBuildtoolsPathInternal(os.getcwd(), override)

def GetBuildtoolsPlatformBinaryPath():
    """Returns the full path to the binary directory for the current platform."""
    buildtools_path = GetBuildtoolsPath()
    if not buildtools_path:
        return None

    if sys.platform.startswith(("cygwin", "win")):
        subdir = "win"
    elif sys.platform == "darwin":
        subdir = "mac"
    elif sys.platform.startswith("linux"):
        subdir = "linux64"
    else:
        raise gclient_utils.Error("Unknown platform: " + sys.platform)
    return os.path.join(buildtools_path, subdir)

def GetExeSuffix():
    """Returns '' or '.exe' depending on how executables work on this platform."""
    if sys.platform.startswith(("cygwin", "win")):
        return ".exe"
    return ""

@functools.lru_cache
def _GetGClientConfigInner(gclient_root_dir_path, filename):
    gclient_config_file = os.path.join(gclient_root_dir_path, filename)
    if not os.path.exists(gclient_config_file):
        return None
    gclient_config_contents = gclient_utils.FileRead(gclient_config_file)
    return gclient_eval.ParseLocalConfig(
        gclient_config_contents, gclient_config_file
    )

def GetGClientConfig(gclient_root_dir_path=None, filename=".gclient"):
    """Returns the parsed .gclient config contents as a dict, or None if not found."""
    if not gclient_root_dir_path:
        gclient_root_dir_path = FindGclientRoot(os.getcwd(), filename)
    if not gclient_root_dir_path:
        return None
    return _GetGClientConfigInner(gclient_root_dir_path, filename)

@functools.lru_cache
def _GetGClientSolutions(gclient_root_dir_path):
    config = GetGClientConfig(gclient_root_dir_path)
    return config.get("solutions", []) if config else []

def GetGClientPrimarySolutionName(gclient_root_dir_path):
    """Returns the name of the primary solution in the .gclient file specified."""
    solutions = _GetGClientSolutions(gclient_root_dir_path)
    if solutions:
        return solutions[0].get("name")
    return None

def GetGClientPrimarySolutionURL(gclient_root_dir_path):
    """Returns the URL of the primary solution in the .gclient file specified."""
    solutions = _GetGClientSolutions(gclient_root_dir_path)
    if solutions:
        return solutions[0].get("url")
    return None

def FindInPath(name: str):
    """Shutil.which replacement that skips depot_tools in PATH."""
    env_path = os.getenv("PATH")
    if not env_path:
        return
    for bin_dir in env_path.split(os.pathsep):
        if bin_dir.rstrip(os.sep).endswith("depot_tools"):
            # skip depot_tools to avoid calling gn.py infinitely.
            continue
        path = os.path.join(bin_dir, name + GetExeSuffix())
        if os.path.isfile(path):
            return path
