# Copyright 2013-2021 Lawrence Livermore National Security, LLC and other
# Spack Project Developers. See the top-level COPYRIGHT file for details.
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
import collections
import multiprocessing.pool
import os
import re
import shutil

import macholib.mach_o
import macholib.MachO

import llnl.util.lang
import llnl.util.tty as tty

import spack.bootstrap
import spack.platforms
import spack.repo
import spack.spec
import spack.util.executable as executable

is_macos = (str(spack.platforms.real_host()) == 'darwin')


class InstallRootStringError(spack.error.SpackError):
    def __init__(self, file_path, root_path):
        """Signal that the relocated binary still has the original
        Spack's store root string

        Args:
            file_path (str): path of the binary
            root_path (str): original Spack's store root string
        """
        super(InstallRootStringError, self).__init__(
            "\n %s \ncontains string\n %s \n"
            "after replacing it in rpaths.\n"
            "Package should not be relocated.\n Use -a to override." %
            (file_path, root_path))


class BinaryStringReplacementError(spack.error.SpackError):
    def __init__(self, file_path, old_len, new_len):
        """The size of the file changed after binary path substitution

        Args:
            file_path (str): file with changing size
            old_len (str): original length of the file
            new_len (str): length of the file after substitution
        """
        super(BinaryStringReplacementError, self).__init__(
            "Doing a binary string replacement in %s failed.\n"
            "The size of the file changed from %s to %s\n"
            "when it should have remanined the same." %
            (file_path, old_len, new_len))


class BinaryTextReplaceError(spack.error.SpackError):
    def __init__(self, old_path, new_path):
        """Raised when the new install path is longer than the
        old one, so binary text replacement cannot occur.

        Args:
            old_path (str): original path to be substituted
            new_path (str): candidate path for substitution
        """

        msg = "New path longer than old path: binary text"
        msg += " replacement not possible."
        err_msg = "The new path %s" % new_path
        err_msg += " is longer than the old path %s.\n" % old_path
        err_msg += "Text replacement in binaries will not work.\n"
        err_msg += "Create buildcache from an install path "
        err_msg += "longer than new path."
        super(BinaryTextReplaceError, self).__init__(msg, err_msg)


def _patchelf():
    """Return the full path to the patchelf binary, if available, else None."""
    if is_macos:
        return None

    patchelf = executable.which('patchelf')
    if patchelf is None:
        with spack.bootstrap.ensure_bootstrap_configuration():
            patchelf = spack.bootstrap.ensure_patchelf_in_path_or_raise()

    return patchelf.path


def _elf_rpaths_for(path):
    """Return the RPATHs for an executable or a library.

    The RPATHs are obtained by ``patchelf --print-rpath PATH``.

    Args:
        path (str): full path to the executable or library

    Return:
        RPATHs as a list of strings.
    """
    # If we're relocating patchelf itself, use it
    patchelf_path = path if path.endswith("/bin/patchelf") else _patchelf()
    patchelf = executable.Executable(patchelf_path)

    output = ''
    try:
        output = patchelf('--print-rpath', path, output=str, error=str)
        output = output.strip('\n')
    except executable.ProcessError as e:
        msg = 'patchelf --print-rpath {0} produced an error [{1}]'
        tty.warn(msg.format(path, str(e)))

    return output.split(':') if output else []


def _make_relative(reference_file, path_root, paths):
    """Return a list where any path in ``paths`` that starts with
    ``path_root`` is made relative to the directory in which the
    reference file is stored.

    After a path is made relative it is prefixed with the ``$ORIGIN``
    string.

    Args:
        reference_file (str): file from which the reference directory
            is computed
        path_root (str): root of the relative paths
        paths: (list) paths to be examined

    Returns:
        List of relative paths
    """
    start_directory = os.path.dirname(reference_file)
    pattern = re.compile(path_root)
    relative_paths = []

    for path in paths:
        if pattern.match(path):
            rel = os.path.relpath(path, start=start_directory)
            path = os.path.join('$ORIGIN', rel)

        relative_paths.append(path)

    return relative_paths


def _normalize_relative_paths(start_path, relative_paths):
    """Normalize the relative paths with respect to the original path name
    of the file (``start_path``).

    The paths that are passed to this function existed or were relevant
    on another filesystem, so os.path.abspath cannot be used.

    A relative path may contain the signifier $ORIGIN. Assuming that
    ``start_path`` is absolute, this implies that the relative path
    (relative to start_path) should be replaced with an absolute path.

    Args:
        start_path (str): path from which the starting directory
            is extracted
        relative_paths (str): list of relative paths as obtained by a
            call to :ref:`_make_relative`

    Returns:
        List of normalized paths
    """
    normalized_paths = []
    pattern = re.compile(re.escape('$ORIGIN'))
    start_directory = os.path.dirname(start_path)

    for path in relative_paths:
        if path.startswith('$ORIGIN'):
            sub = pattern.sub(start_directory, path)
            path = os.path.normpath(sub)
        normalized_paths.append(path)

    return normalized_paths


def _placeholder(dirname):
    """String of  of @'s with same length of the argument"""
    return '@' * len(dirname)


def _decode_macho_data(bytestring):
    return bytestring.rstrip(b'\x00').decode('ascii')


def macho_make_paths_relative(path_name, old_layout_root,
                              rpaths, deps, idpath):
    """
    Return a dictionary mapping the original rpaths to the relativized rpaths.
    This dictionary is used to replace pa
ths in mach-o binaries.
    Replace old_dir with relative path from dirname of path name
    in rpaths and deps; idpath is replaced with @rpath/libname.
    """
    paths_to_paths = dict()
    if idpath:
        paths_to_paths[idpath] = os.path.join(
            '@rpath', '%s' % os.path.basename(idpath))
    for rpath in rpaths:
        if re.match(old_layout_root, rpath):
            rel = os.path.relpath(rpath, start=os.path.dirname(path_name))
            paths_to_paths[r
path] = os.path.join('@loader_path', '%s' % rel)
        else:
            paths_to_paths[rpath] = rpath
    for dep in deps:
        if re.match(old_layout_root, dep):
            rel = os.path.relpath(dep, start=os.path.dirname(path_name))
            paths_to_paths[dep] = os.path.join('@loader_path', '%s' % rel)
        else:
            paths_to_paths[dep] = dep
    return paths_to_paths


def macho_make_paths_normal(orig_path_name, rpaths, deps, idpath):
    """
    Return a dictionary mapping the relativized rpaths to the original rpaths.
    This dictionary is used to replace paths in mach-o binaries.
    Replace '@loader_path' with the dirname of the origname path name
    in rpaths and deps; idpath is replaced with the original path name
    """
    rel_to_orig = dict()
    if idpath:
        rel_to_orig[idpath] = orig_path_name

    for rpath in rpaths:
        if re.match('@loader_path', rpath):
            norm = os.path.normpath(re.sub(re.escape('@loader_path'),
                                           os.path.dirname(orig_path_name),
                                           rpath))
            rel_to_orig[rpath] = norm
        else:
            rel_to_orig[rpath] = rpath
    for dep in deps:
        if re.match('@loader_path', dep):
            norm = os.path.normpath(re.sub(re.escape('@loader_path'),
                                           os.path.dirname(orig_p