# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.

import os
import shlex
import sys
from typing import Optional


class Env(object):
    def __init__(self, src=None) -> None:
        self._dict = {}
        if src is None:
            self.update(os.environ)
        else:
            self.update(src)

    def update(self, src) -> None:
        for k, v in src.items():
            self.set(k, v)

    def copy(self) -> "Env":
        return Env(self._dict)

    def _key(self, key):
        # The `str` cast may not appear to be needed, but without it we run
        # into issues when passing the environment to subprocess.  The main
        # issue is that in python2 `os.environ` (which is the initial source
        # of data for the environment) uses byte based strings, but this
        # project uses `unicode_literals`.  `subprocess` will raise an error
        # if the environment that it is passed has a mixture of byte and
        # unicode strings.
        # It is simplest to force everything to be `str` for the sake of
        # consistency.
        key = str(key)
        if sys.platform.startswith("win"):
            # Windows env var names are case insensitive but case preserving.
            # An implementation of PAR files on windows gets confused if
            # the env block contains keys with conflicting case, so make a
            # pass over the contents to remove any.
            # While this O(n) scan is technically expensive and gross, it
            # is practically not a problem because the volume of calls is
            # relatively low and the cost of manipulating the env is dwarfed
            # by the cost of spawning a process on windows.  In addition,
            # since the processes that we run are expensive anyway, this
            # overhead is not the worst thing to worry about.
            for k in list(self._dict.keys()):
                if str(k).lower() == key.lower():
                    return k
        elif key in self._dict:
            return key
        return None

    def get(self, key, defval=None):
        key = self._key(key)
        if key is None:
            return defval
        return self._dict[key]

    def __getitem__(self, key):
        val = self.get(key)
        if key is None:
            raise KeyError(key)
        return val

    def unset(self, key) -> None:
        if key is None:
            raise KeyError("attempting to unset env[None]")

        key = self._key(key)
        if key:
            del self._dict[key]

    def __delitem__(self, key) -> None:
        self.unset(key)

    def __repr__(self):
        return repr(self._dict)

    def set(self, key, value) -> None:
        if key is None:
            raise KeyError("attempting to assign env[None] = %r" % value)

        if value is None:
            raise ValueError("attempting to assign env[%s] = None" % key)

        # The `str` conversion is important to avoid triggering errors
        # with subprocess if we pass in a unicode value; see commentary
        # in the `_key` method.
        key = str(key)
        value = str(value)

        # The `unset` call is necessary on windows where the keys are
        # case insensitive.   Since this dict is case sensitive, simply
        # assigning the value to the new key is not sufficient to remove
        # the old value.  The `unset` call knows how to match keys and
        # remove any potential duplicates.
        self.unset(key)
        self._dict[key] = value

    def __setitem__(self, key, value) -> None:
        self.set(key, value)

    def __iter__(self):
        return self._dict.__iter__()

    def __len__(self) -> int:
        return len(self._dict)

    def keys(self):
        return self._dict.keys()

    def values(self):
        return self._dict.values()

    def items(self):
        return self._dict.items()


def add_path_entry(
    env, name, item, append: bool = True, separator: str = os.pathsep
) -> None:
    """Cause `item` to be added to the path style env var named
    `name` held in the `env` dict.  `append` specifies whether
    the item is added to the end (the default) or should be
    prepended if `name` already exists."""
    val = env.get(name, "")
    if len(val) > 0:
        val = val.split(separator)
    else:
        val = []
    if append:
        val.append(item)
    else:
        val.insert(0, item)
    env.set(name, separator.join(val))


def add_flag(env, name, flag: str, append: bool = True) -> None:
    """Cause `flag` to be added to the CXXFLAGS-style env var named
    `name` held in the `env` dict.  `append` specifies whether the
    flag is added to the end (the default) or should be prepended if
    `name` already exists."""
    val = shlex.split(env.get(name, ""))
    if append:
        val.append(flag)
    else:
        val.insert(0, flag)
    env.set(name, " ".join(val))


_path_search_cache = {}
_not_found = object()


def tpx_path() -> str:
    return "xplat/testinfra/tpx/ctp.tpx"


def path_search(env, exename: str, defval: Optional[str] = None) -> Optional[str]:
    """Search for exename in the PATH specified in env.
    exename is eg: `ninja` and this function knows to append a .exe
    to the end on windows.
    Returns the path to the exe if found, or None if either no
    PATH is set in env or no executable is found."""

    path = env.get("PATH", None)
    if path is None:
        return defval

    # The project hash computation code searches for C++ compilers (g++, clang, etc)
    # repeatedly.  Cache the result so we don't end up searching for these over and over
    # again.
    cache_key = (path, exename)
    result = _path_search_cache.get(cache_key, _not_found)
    if result is _not_found:
        result = _perform_path_search(path, exename)
        _path_search_cache[cache_key] = result
    return result


def _perform_path_search(path, exename: str) -> Optional[str]:
    is_win = sys.platform.startswith("win")
    if is_win:
        exename = "%s.exe" % exename

    for bindir in path.split(os.pathsep):
        full_name = os.path.join(bindir, exename)
        if os.path.exists(full_name) and os.path.isfile(full_name):
            if not is_win and not os.access(full_name, os.X_OK):
                continue
            return full_name

    return None
