# MIT License
#
# Copyright The SCons Foundation
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

"""Handle lists of directory paths.

These are the path lists that get set as ``CPPPATH``, ``LIBPATH``,
etc.) with as much caching of data and efficiency as we can, while
still keeping the evaluation delayed so that we Do the Right Thing
(almost) regardless of how the variable is specified.
"""

import os

import SCons.Memoize
import SCons.Node
import SCons.Util

#
# Variables to specify the different types of entries in a PathList object:
#

TYPE_STRING_NO_SUBST = 0        # string with no '$'
TYPE_STRING_SUBST = 1           # string containing '$'
TYPE_OBJECT = 2                 # other object

def node_conv(obj):
    """
    This is the "string conversion" routine that we have our substitutions
    use to return Nodes, not strings.  This relies on the fact that an
    :class:`~SCons.Node.FS.EntryProxy` object has a ``get()`` method that
    returns the underlying Node that it wraps, which is a bit of
    architectural dependence that we might need to break or modify in the
    future in response to additional requirements.
    """
    try:
        get = obj.get
    except AttributeError:
        if isinstance(obj, SCons.Node.Node) or SCons.Util.is_Sequence( obj ):
            result = obj
        else:
            result = str(obj)
    else:
        result = get()
    return result

class _PathList:
    """An actual PathList object.

    Initializes a :class:`PathList` object, canonicalizing the input and
    pre-processing it for quicker substitution later.

    The stored representation of the :class:`PathList` is a list of tuples
    containing (type, value), where the "type" is one of the ``TYPE_*``
    variables defined above.  We distinguish between:

    *   Strings that contain no ``$`` and therefore need no
        delayed-evaluation string substitution (we expect that there
        will be many of these and that we therefore get a pretty
        big win from avoiding string substitution)

    *   Strings that contain ``$`` and therefore need substitution
        (the hard case is things like ``${TARGET.dir}/include``,
        which require re-evaluation for every target + source)

    *   Other objects (which may be something like an
        :class:`~SCons.Node.FS.EntryProxy`
        that needs a method called to return a Node)

    Pre-identifying the type of each element in the :class:`PathList`
    up-front and storing the type in the list of tuples is intended to
    reduce the amount of calculation when we actually do the substitution
    over and over for each target.
    """

    def __init__(self, pathlist, split=True) -> None:
        if SCons.Util.is_String(pathlist):
            if split:
                pathlist = pathlist.split(os.pathsep)
            else:  # no splitting, but still need a list
                pathlist = [pathlist]
        elif not SCons.Util.is_Sequence(pathlist):
            pathlist = [pathlist]

        pl = []
        for p in pathlist:
            try:
                found = '$' in p
            except (AttributeError, TypeError):
                type = TYPE_OBJECT
            else:
                if not found:
                    type = TYPE_STRING_NO_SUBST
                else:
                    type = TYPE_STRING_SUBST
            pl.append((type, p))

        self.pathlist = tuple(pl)

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

    def __getitem__(self, i): return self.pathlist[i]

    def subst_path(self, env, target, source):
        """
        Performs construction variable substitution on a pre-digested
        PathList for a specific target and source.
        """
        result = []
        for type, value in self.pathlist:
            if type == TYPE_STRING_SUBST:
                value = env.subst(value, target=target, source=source,
                                  conv=node_conv)
                if SCons.Util.is_Sequence(value):
                    result.extend(SCons.Util.flatten(value))
                elif value:
                    result.append(value)
            elif type == TYPE_OBJECT:
                value = node_conv(value)
                if value:
                    result.append(value)
            elif value:
                result.append(value)
        return tuple(result)


class PathListCache:
    """A class to handle caching of PathList lookups.

    This class gets instantiated once and then deleted from the namespace,
    so it's used as a Singleton (although we don't enforce that in the
    usual Pythonic ways).  We could have just made the cache a dictionary
    in the module namespace, but putting it in this class allows us to
    use the same Memoizer pattern that we use elsewhere to count cache
    hits and misses, which is very valuable.

    Lookup keys in the cache are computed by the :meth:`_PathList_key` method.
    Cache lookup should be quick, so we don't spend cycles canonicalizing
    all forms of the same lookup key.  For example, ``x:y`` and ``['x', 'y']``
    logically represent the same list, but we don't bother to
    split string representations and treat those two equivalently.
    (Note, however, that we do, treat lists and tuples the same.)

    The main type of duplication we're trying to catch will come from
    looking up the same path list from two different clones of the
    same construction environment.  That is, given::

        env2 = env1.Clone()

    both ``env1`` and ``env2`` will have the same ``CPPPATH`` value, and we can
    cheaply avoid re-parsing both values of ``CPPPATH`` by using the
    common value from this cache.
    """
    def __init__(self) -> None:
        self._memo = {}

    def _PathList_key(self, pathlist):
        """Returns the key for memoization of PathLists.

        Note that we want this to be pretty quick, so we don't completely
        canonicalize all forms of the same list.  For example,
        ``dir1:$ROOT/dir2`` and ``['$ROOT/dir1', 'dir']`` may logically
        represent the same list if you're executing from ``$ROOT``, but
        we're not going to bother splitting strings into path elements,
        or massaging strings into Nodes, to identify that equivalence.
        We just want to eliminate obvious redundancy from the normal
        case of re-using exactly the same cloned value for a path.
        """
        if SCons.Util.is_Sequence(pathlist):
            pathlist = tuple(SCons.Util.flatten(pathlist))
        return pathlist

    @SCons.Memoize.CountDictCall(_PathList_key)
    def PathList(self, pathlist, split=True):
        """Entry point for getting PathLists.

        Returns the cached :class:`_PathList` object for the specified
        pathlist, creating and caching a new object as necessary.
        """
        pathlist = self._PathList_key(pathlist)
        try:
            memo_dict = self._memo['PathList']
        except KeyError:
            memo_dict = {}
            self._memo['PathList'] = memo_dict
        else:
            try:
                return memo_dict[pathlist]
            except KeyError:
                pass

        result = _PathList(pathlist, split)

        memo_dict[pathlist] = result

        return result

PathList = PathListCache().PathList

# TODO: removing the class object here means Sphinx doesn't pick up its
#   docstrings: they're fine for reading here, but are not in API Docs.
del PathListCache

# Local Variables:
# tab-width:4
# indent-tabs-mode:nil
# End:
# vim: set expandtab tabstop=4 shiftwidth=4:
