# --------------------------------------------------------------------

import re
import os
import sys
import glob
import copy
import warnings
from distutils import log
from distutils import sysconfig
from distutils.util import execute
from distutils.util import split_quoted
from distutils.errors import DistutilsError
from distutils.text_file import TextFile


try:
    from cStringIO import StringIO
except ImportError:
    from io import StringIO

try:
    import setuptools
except ImportError:
    setuptools = None

if setuptools:
    from setuptools import setup as _setup
    from setuptools import Extension as _Extension
    from setuptools import Command
else:
    from distutils.core import setup as _setup
    from distutils.core import Extension as _Extension
    from distutils.core import Command


def import_command(cmd):
    try:
        from importlib import import_module
    except ImportError:

        def import_module(n):
            return __import__(n, fromlist=[None])

    try:
        if not setuptools:
            raise ImportError
        mod = import_module('setuptools.command.' + cmd)
        return getattr(mod, cmd)
    except ImportError:
        mod = import_module('distutils.command.' + cmd)
        return getattr(mod, cmd)


_config = import_command('config')
_build = import_command('build')
_build_ext = import_command('build_ext')
_install = import_command('install')

try:
    from setuptools import modified
except ImportError:
    try:
        from setuptools import dep_util as modified
    except ImportError:
        from distutils import dep_util as modified

try:
    from packaging.version import Version
except ImportError:
    try:
        from setuptools.extern.packaging.version import Version
    except ImportError:
        from distutils.version import StrictVersion as Version

# --------------------------------------------------------------------

# Cython

CYTHON = '3.0.0'


def cython_req():
    return CYTHON


def cython_chk(VERSION, verbose=True):
    #
    def warn(message):
        if not verbose:
            return
        ruler, ws, nl = '*' * 80, ' ', '\n'
        pyexe = sys.executable
        advise = '$ %s -m pip install --upgrade cython' % pyexe

        def printer(*s):
            sys.stderr.write(' '.join(s) + '\n')

        printer(ruler, nl)
        printer(ws, message, nl)
        printer(ws, ws, advise, nl)
        printer(ruler)

    #
    try:
        import Cython
    except ImportError:
        warn('You need Cython to generate C source files.')
        return False
    #
    CYTHON_VERSION = Cython.__version__
    m = re.match(r'(\d+\.\d+(?:\.\d+)?).*', CYTHON_VERSION)
    if not m:
        warn(f'Cannot parse Cython version string {CYTHON_VERSION!r}')
        return False
    REQUIRED = Version(VERSION)
    PROVIDED = Version(m.groups()[0])
    if PROVIDED < REQUIRED:
        warn(f'You need Cython >= {VERSION} (you have version {CYTHON_VERSION})')
        return False
    #
    if verbose:
        log.info('using Cython %s' % CYTHON_VERSION)
    return True


def cython_run(
    source,
    target=None,
    depends=(),
    includes=(),
    workdir=None,
    force=False,
    VERSION='0.0',
):
    if target is None:
        target = os.path.splitext(source)[0] + '.c'
    cwd = os.getcwd()
    try:
        if workdir:
            os.chdir(workdir)
        alldeps = [source]
        for dep in depends:
            alldeps += glob.glob(dep)
        if not (force or modified.newer_group(alldeps, target)):
            log.debug("skipping '%s' -> '%s' (up-to-date)", source, target)
            return
    finally:
        os.chdir(cwd)
    require = 'Cython >= %s' % VERSION
    if setuptools and not cython_chk(VERSION, verbose=False):
        if sys.modules.get('Cython'):
            removed = getattr(sys.modules['Cython'], '__version__', '')
            log.info('removing Cython %s from sys.modules' % removed)
            pkgname = re.compile(r'cython(\.|$)', re.IGNORECASE)
            for modname in list(sys.modules.keys()):
                if pkgname.match(modname):
                    del sys.modules[modname]
        try:
            install_setup_requires = setuptools._install_setup_requires
            with warnings.catch_warnings():
                if hasattr(setuptools, 'SetuptoolsDeprecationWarning'):
                    category = setuptools.SetuptoolsDeprecationWarning
                    warnings.simplefilter('ignore', category)
                log.info("fetching build requirement '%s'" % require)
                install_setup_requires({'setup_requires': [require]})
        except Exception:
            log.info("failed to fetch build requirement '%s'" % require)
    if not cython_chk(VERSION):
        raise DistutilsError("unsatisfied build requirement '%s'" % require)
    #
    log.info("cythonizing '%s' -> '%s'", source, target)
    from cythonize import cythonize

    args = []
    if workdir:
        args += ['--working', workdir]
    args += [source]
    if target:
        args += ['--output-file', target]
    err = cythonize(args)
    if err:
        raise DistutilsError(f"Cython failure: '{source}' -> '{target}'")


# --------------------------------------------------------------------


def fix_config_vars(names, values):
    values = list(values)
    if 'CONDA_BUILD' in os.environ:
        return values
    if sys.platform == 'darwin':
        if 'ARCHFLAGS' in os.environ:
            ARCHFLAGS = os.environ['ARCHFLAGS']
            for i, flag in enumerate(list(values)):
                flag, count = re.subn(r'-arch\s+\w+', ' ', str(flag))
                if count and ARCHFLAGS:
                    flag = flag + ' ' + ARCHFLAGS
                values[i] = flag
        if 'SDKROOT' in os.environ:
            SDKROOT = os.environ['SDKROOT']
            for i, flag in enumerate(list(values)):
                flag, count = re.subn(r'-isysroot [^ \t]*', ' ', str(flag))
                if count and SDKROOT:
                    flag = flag + ' ' + '-isysroot ' + SDKROOT
                values[i] = flag
    return values


def get_config_vars(*names):
    # Core Python configuration
    values = sysconfig.get_config_vars(*names)
    # Do any distutils flags fixup right now
    return fix_config_vars(names, values)


# --------------------------------------------------------------------


class PetscConfig:
    def __init__(self, petsc_dir, petsc_arch, dest_dir=None):
        if dest_dir is None:
            dest_dir = os.environ.get('DESTDIR')
        self.configdict = {}
        if not petsc_dir:
            raise DistutilsError('PETSc not found')
        if not os.path.isdir(petsc_dir):
            raise DistutilsError('invalid PETSC_DIR: %s' % petsc_dir)
        self.version = self._get_petsc_version(petsc_dir)
        self.configdict = self._get_petsc_config(petsc_dir, petsc_arch)
        self.PETSC_DIR = self['PETSC_DIR']
        self.PETSC_ARCH = self['PETSC_ARCH']
        self.DESTDIR = dest_dir
        language_map = {'CONLY': 'c', 'CXXONLY': 'c++'}
        self.language = language_map[self['PETSC_LANGUAGE']]

    def __getitem__(self, item):
        return self.configdict[item]

    def get(self, item, default=None):
        return self.configdict.get(item, default)

    def configure(self, extension, compiler=None):
        self.configure_extension(extension)
        if compiler is not None:
            self.configure_compiler(compiler)

    def _get_petsc_version(self, petsc_dir):
        import re

        version_re = {
            'major': re.compile(r'#define\s+PETSC_VERSION_MAJOR\s+(\d+)'),
            'minor': re.compile(r'#define\s+PETSC_VERSION_MINOR\s+(\d+)'),
            'micro': re.compile(r'#define\s+PETSC_VERSION_SUBMINOR\s+(\d+)'),
            'release': re.compile(r'#define\s+PETSC_VERSION_RELEASE\s+(-*\d+)'),
        }
        petscversion_h = os.path.join(petsc_dir, 'include', 'petscversion.h')
        with open(petscversion_h, 'rt') as f:
            data = f.read()
        major = int(version_re['major'].search(data).groups()[0])
        minor = int(version_re['minor'].search(data).groups()[0])
        micro = int(version_re['micro'].search(data).groups()[0])
        release = int(version_re['release'].search(data).groups()[0])
        return (major, minor, micro), (release == 1)

    def _get_petsc_config(self, petsc_dir, petsc_arch):
        from os.path import join, isdir, exists

        PETSC_DIR = petsc_dir
        PETSC_ARCH = petsc_arch
        #
        confdir = join('lib', 'petsc', 'conf')
        if not (PETSC_ARCH and isdir(join(PETSC_DIR, PETSC_ARCH))):
            petscvars = join(PETSC_DIR, confdir, 'petscvariables')
            PETSC_ARCH = makefile(open(petscvars, 'rt')).get('PETSC_ARCH')
        if not (PETSC_ARCH and isdir(join(PETSC_DIR, PETSC_ARCH))):
            PETSC_ARCH = ''
        #
        variables = join(PETSC_DIR, confdir, 'variables')
        if not exists(variables):
            variables = join(PETSC_DIR, PETSC_ARCH, confdir, 'variables')
        petscvariables = join(PETSC_DIR, PETSC_ARCH, confdir, 'petscvariables')
        #
        with open(variables) as f:
            contents = f.read()
        with open(petscvariables) as f:
            contents += f.read()
        #
        confstr = 'PETSC_DIR  = %s\n' % PETSC_DIR
        confstr += 'PETSC_ARCH = %s\n' % PETSC_ARCH
        confstr += contents
        return makefile(StringIO(confstr))

    def _configure_ext(self, ext, dct, append=False):
        extdict = ext.__dict__
        for key, values in dct.items():
            if key in extdict:
                for value in values:
                    if value not in extdict[key]:
                        if not append:
                            extdict[key].insert(0, value)
                        else:
                            extdict[key].append(value)

    def configure_extension(self, extension):
        # includes and libraries
        # paths in PETSc config files point to final installation location, but
        # we might be building against PETSc in staging location (DESTDIR) when
        # DESTDIR is set, so append DESTDIR (if nonempty) to those paths
        petsc_inc = flaglist(prepend_to_flags(self.DESTDIR, self['PETSC_CC_INCLUDES']))
        lib_flags = prepend_to_flags(
            self.DESTDIR,
            '-L{} {}'.format(self['PETSC_LIB_DIR'], self['PETSC_LIB_BASIC']),
        )
        petsc_lib = flaglist(lib_flags)
        # runtime_library_dirs is not supported on Windows
        if sys.platform != 'win32':
            # if DESTDIR is set, then we're building against PETSc in a staging
            # directory, but rpath needs to point to final install directory.
            rpath = [strip_prefix(self.DESTDIR, self['PETSC_LIB_DIR'])]
            if sys.modules.get('petsc') is not None:
                if sys.platform == 'darwin':
                    rpath = ['@loader_path/../../petsc/lib']
                else:
                    rpath = ['$ORIGIN/../../petsc/lib']
            petsc_lib['runtime_library_dirs'].extend(rpath)
        # Link in extra libraries on static builds
        if self['BUILDSHAREDLIB'] != 'yes':
            petsc_ext_lib = split_quoted(self['PETSC_EXTERNAL_LIB_BASIC'])
            petsc_lib['extra_link_args'].extend(petsc_ext_lib)
        self._configure_ext(extension, petsc_inc, append=True)
        self._configure_ext(extension, petsc_lib)

    def configure_compiler(self, compiler):
        if compiler.compiler_type != 'unix':
            return
        getenv = os.environ.get
        # distutils C/C++ compiler
        (cc, cflags, ccshared, cxx) = get_config_vars('CC', 'CFLAGS', 'CCSHARED', 'CXX')
        ccshared = getenv('CCSHARED', ccshared or '')
        cflags = getenv('CFLAGS', cflags or '')
        cflags = cflags.replace('-Wstrict-prototypes', '')
        # distutils linker
        (ldflags, ldshared, so_ext) = get_config_vars('LDFLAGS', 'LDSHARED', 'SO')
        ld = cc
        ldshared = getenv('LDSHARED', ldshared)
        ldflags = getenv('LDFLAGS', cflags + ' ' + (ldflags or ''))
        ldcmd = split_quoted(ld) + split_quoted(ldflags)
        ldshared = [
            flg
            for flg in split_quoted(ldshared)
            if flg not in ldcmd and (flg.find('/lib/spack/env') < 0) and (flg.find('/libexec/spack/') < 0)
        ]
        ldshared = str.join(' ', ldshared)

        #
        def get_flags(cmd):
            if not cmd:
                return ''
            cmd = split_quoted(cmd)
            if os.path.basename(cmd[0]) == 'xcrun':
                del cmd[0]
                while True:
                    if cmd[0] == '-sdk':
                        del cmd[0:2]
                        continue
                    if cmd[0] == '-log':
                        del cmd[0]
                        continue
                    break
            return ' '.join(cmd[1:])

        # PETSc C compiler
        PCC = self['PCC']
        PCC_FLAGS = get_flags(cc) + ' ' + self['PCC_FLAGS']
        PCC_FLAGS = PCC_FLAGS.replace('-fvisibility=hidden', '')
        PCC_FLAGS = PCC_FLAGS.replace('-Wpedantic', '-Wno-pedantic')
        PCC_FLAGS = PCC_FLAGS.replace('-Wextra-semi-stmt', '-Wno-extra-semi-stmt')
        PCC = getenv('PCC', PCC) + ' ' + getenv('PCCFLAGS', PCC_FLAGS)
        PCC_SHARED = str.join(' ', (PCC, ccshared, cflags))
        # PETSc C++ compiler
        PCXX = PCC if self.language == 'c++' else self.get('CXX', cxx)
        # PETSc linker
        PLD = self['PCC_LINKER']
        PLD_FLAGS = get_flags(ld) + ' ' + self['PCC_LINKER_FLAGS']
        PLD_FLAGS = PLD_FLAGS.replace('-fvisibility=hidden', '')
        PLD = getenv('PLD', PLD) + ' ' + getenv('PLDFLAGS', PLD_FLAGS)
        PLD_SHARED = str.join(' ', (PLD, ldshared, ldflags))
        #
        compiler.set_executables(
            compiler=PCC,
            compiler_cxx=PCXX,
            linker_exe=PLD,
            compiler_so=PCC_SHARED,
            linker_so=PLD_SHARED,
        )
        compiler.shared_lib_extension = so_ext

    def log_info(self):
        PETSC_DIR = self['PETSC_DIR']
        PETSC_ARCH = self['PETSC_ARCH']
        version = '.'.join([str(i) for i in self.version[0]])
        release = ('development', 'release')[self.version[1]]
        version_info = version + ' ' + release
        integer_size = '%s-bit' % self['PETSC_INDEX_SIZE']
        scalar_type = self['PETSC_SCALAR']
        precision = self['PETSC_PRECISION']
        language = self['PETSC_LANGUAGE']
        compiler = self['PCC']
        linker = self['PCC_LINKER']
        log.info('PETSC_DIR:    %s' % PETSC_DIR)
        log.info('PETSC_ARCH:   %s' % PETSC_ARCH)
        log.info('version:      %s' % version_info)
        log.info('integer-size: %s' % integer_size)
        log.info('scalar-type:  %s' % scalar_type)
        log.info('precision:    %s' % precision)
        log.info('language:     %s' % language)
        log.info('compiler:     %s' % compiler)
        log.info('linker:       %s' % linker)


# --------------------------------------------------------------------


class Extension(_Extension):
    pass


# --------------------------------------------------------------------

cmd_petsc_opts = [
    ('petsc-dir=', None, 'define PETSC_DIR, overriding environmental variables'),
    ('petsc-arch=', None, 'define PETSC_ARCH, overriding environmental variables'),
]


class config(_config):
    Configure = PetscConfig

    user_options = _config.user_options + cmd_petsc_opts

    def initialize_options(self):
        _config.initialize_options(self)
        self.petsc_dir = None
        self.petsc_arch = None

    def get_config_arch(self, arch):
        return config.Configure(self.petsc_dir, arch)

    def run(self):
        _config.run(self)
        self.petsc_dir = config.get_petsc_dir(self.petsc_dir)
        if self.petsc_dir is None:
            return
        petsc_arch = config.get_petsc_arch(self.petsc_dir, self.petsc_arch)
        log.info('-' * 70)
        log.info('PETSC_DIR:   %s' % self.petsc_dir)
        arch_list = petsc_arch
        if not arch_list:
            arch_list = [None]
        for arch in arch_list:
            conf = self.get_config_arch(arch)
            archname = conf.PETSC_ARCH or conf['PETSC_ARCH']
            scalar_type = conf['PETSC_SCALAR']
            precision = conf['PETSC_PRECISION']
            language = conf['PETSC_LANGUAGE']
            compiler = conf['PCC']
            linker = conf['PCC_LINKER']
            log.info('-' * 70)
            log.info('PETSC_ARCH:  %s' % archname)
            log.info(' * scalar-type: %s' % scalar_type)
            log.info(' * precision:   %s' % precision)
            log.info(' * language:    %s' % language)
            log.info(' * compiler:    %s' % compiler)
            log.info(' * linker:      %s' % linker)
        log.info('-' * 70)

    # @staticmethod
    def get_petsc_dir(petsc_dir):
        if not petsc_dir:
            return None
        petsc_dir = os.path.expandvars(petsc_dir)
        if not petsc_dir or '$PETSC_DIR' in petsc_dir:
            try:
                import petsc

                petsc_dir = petsc.get_petsc_dir()
            except ImportError:
                log.warn('PETSC_DIR not specified')
                return None
        petsc_dir = os.path.expanduser(petsc_dir)
        petsc_dir = os.path.abspath(petsc_dir)
        return config.chk_petsc_dir(petsc_dir)

    get_petsc_dir = staticmethod(get_petsc_dir)

    # @staticmethod
    def chk_petsc_dir(petsc_dir):
        if not os.path.isdir(petsc_dir):
            log.error('invalid PETSC_DIR: %s (ignored)' % petsc_dir)
            return None
        return petsc_dir

    chk_petsc_dir = staticmethod(chk_petsc_dir)

    # @staticmethod
    def get_petsc_arch(petsc_dir, petsc_arch):
        if not petsc_dir:
            return None
        petsc_arch = os.path.expandvars(petsc_arch)
        if not petsc_arch or '$PETSC_ARCH' in petsc_arch:
            petsc_arch = ''
            petsc_conf = os.path.join(petsc_dir, 'lib', 'petsc', 'conf')
            if os.path.isdir(petsc_conf):
                petscvariables = os.path.join(petsc_conf, 'petscvariables')
                if os.path.exists(petscvariables):
                    conf = makefile(open(petscvariables, 'rt'))
                    petsc_arch = conf.get('PETSC_ARCH', '')
        petsc_arch = petsc_arch.split(os.pathsep)
        petsc_arch = unique(petsc_arch)
        petsc_arch = [arch for arch in petsc_arch if arch]
        return config.chk_petsc_arch(petsc_dir, petsc_arch)

    get_petsc_arch = staticmethod(get_petsc_arch)

    # @staticmethod
    def chk_petsc_arch(petsc_dir, petsc_arch):
        valid_archs = []
        for arch in petsc_arch:
            arch_path = os.path.join(petsc_dir, arch)
            if os.path.isdir(arch_path):
                valid_archs.append(arch)
            else:
                log.warn('invalid PETSC_ARCH: %s (ignored)' % arch)
        return valid_archs

    chk_petsc_arch = staticmethod(chk_petsc_arch)


class build(_build):
    user_options = _build.user_options
    user_options += [
        (
            'inplace',
            'i',
            'ignore build-lib and put compiled extensions into the source '
            'directory alongside your pure Python modules',
        )
    ]
    user_options += cmd_petsc_opts

    boolean_options = _build.boolean_options
    boolean_options += ['inplace']

    def initialize_options(self):
        _build.initialize_options(self)
        self.inplace = None
        self.petsc_dir = None
        self.petsc_arch = None

    def finalize_options(self):
        _build.finalize_options(self)
        if self.inplace is None:
            self.inplace = False
        self.set_undefined_options(
            'config', ('petsc_dir', 'petsc_dir'), ('petsc_arch', 'petsc_arch')
        )
        self.petsc_dir = config.get_petsc_dir(self.petsc_dir)
        self.petsc_arch = config.get_petsc_arch(self.petsc_dir, self.petsc_arch)

    sub_commands = [('build_src', lambda *args: True)] + _build.sub_commands


class build_src(Command):
    description = 'build C sources from Cython files'

    user_options = [
        ('force', 'f', 'forcibly build everything (ignore file timestamps)'),
    ]

    boolean_options = ['force']

    def initialize_options(self):
        self.force = False

    def finalize_options(self):
        self.set_undefined_options(
            'build',
            ('force', 'force'),
        )

    def run(self):
        sources = getattr(self, 'sources', [])
        for source in sources:
            cython_run(force=self.force, VERSION=cython_req(), **source)


class build_ext(_build_ext):
    user_options = _build_ext.user_options + cmd_petsc_opts

    def initialize_options(self):
        _build_ext.initialize_options(self)
        self.inplace = None
        self.petsc_dir = None
        self.petsc_arch = None
        self._outputs = []

    def finalize_options(self):
        _build_ext.finalize_options(self)
        self.set_undefined_options('build', ('inplace', 'inplace'))
        self.set_undefined_options(
            'build', ('petsc_dir', 'petsc_dir'), ('petsc_arch', 'petsc_arch')
        )

    def _copy_ext(self, ext):
        extclass = ext.__class__
        fullname = self.get_ext_fullname(ext.name)
        modpath = str.split(fullname, '.')
        pkgpath = os.path.join('', *modpath[0:-1])
        name = modpath[-1]
        sources = list(ext.sources)
        newext = extclass(name, sources)
        newext.__dict__.update(copy.deepcopy(ext.__dict__))
        newext.name = name
        return pkgpath, newext

    def _build_ext_arch(self, ext, pkgpath, arch):
        build_temp = self.build_temp
        build_lib = self.build_lib
        try:
            self.build_temp = os.path.join(build_temp, arch)
            self.build_lib = os.path.join(build_lib, pkgpath, arch)
            _build_ext.build_extension(self, ext)
        finally:
            self.build_temp = build_temp
            self.build_lib = build_lib

    def get_config_arch(self, arch):
        return config.Configure(self.petsc_dir, arch)

    def build_extension(self, ext):
        if not isinstance(ext, Extension):
            return _build_ext.build_extension(self, ext)
        petsc_arch = self.petsc_arch
        if not petsc_arch:
            petsc_arch = [None]
        for arch in petsc_arch:
            config = self.get_config_arch(arch)
            ARCH = arch or config['PETSC_ARCH']
            if ARCH not in self.PETSC_ARCH_LIST:
                self.PETSC_ARCH_LIST.append(ARCH)
            self.DESTDIR = config.DESTDIR
            ext.language = config.language
            config.log_info()
            pkgpath, newext = self._copy_ext(ext)
            config.configure(newext, self.compiler)
            self._build_ext_arch(newext, pkgpath, ARCH)
        return None

    def run(self):
        self.build_sources()
        _build_ext.run(self)

    def build_sources(self):
        if 'build_src' in self.distribution.cmdclass:
            self.run_command('build_src')

    def build_extensions(self, *args, **kargs):
        self.PETSC_ARCH_LIST = []
        _build_ext.build_extensions(self, *args, **kargs)
        if not self.PETSC_ARCH_LIST:
            return
        self.build_configuration(self.PETSC_ARCH_LIST)

    def build_configuration(self, arch_list):
        #
        template, variables = self.get_config_data(arch_list)
        config_data = template % variables
        #
        build_lib = self.build_lib
        dist_name = self.distribution.get_name()
        config_file = os.path.join(
            build_lib, dist_name, 'lib', dist_name.replace('4py', '') + '.cfg'
        )

        #
        def write_file(filename, data):
            with open(filename, 'w') as fh:
                fh.write(config_data)

        execute(
            write_file,
            (config_file, config_data),
            msg='writing %s' % config_file,
            verbose=self.verbose,
        )

    def get_config_data(self, arch_list):
        DESTDIR = self.DESTDIR
        template = (
            '\n'.join(
                [
                    'PETSC_DIR  = %(PETSC_DIR)s',
                    'PETSC_ARCH = %(PETSC_ARCH)s',
                ]
            )
            + '\n'
        )
        variables = {
            'PETSC_DIR': strip_prefix(DESTDIR, self.petsc_dir),
            'PETSC_ARCH': os.path.pathsep.join(arch_list),
        }
        return template, variables

    def copy_extensions_to_source(self):
        build_py = self.get_finalized_command('build_py')
        for ext in self.extensions:
            inp_file, reg_file = self._get_inplace_equivalent(build_py, ext)

            arch_list = ['']
            if isinstance(ext, Extension) and self.petsc_arch:
                arch_list = self.petsc_arch[:]

            file_pairs = []
            inp_head, inp_tail = os.path.split(inp_file)
            reg_head, reg_tail = os.path.split(reg_file)
            for arch in arch_list:
                inp_file = os.path.join(inp_head, arch, inp_tail)
                reg_file = os.path.join(reg_head, arch, reg_tail)
                file_pairs.append((inp_file, reg_file))

            for inp_file, reg_file in file_pairs:
                if os.path.exists(reg_file) or not ext.optional:
                    dest_dir, _ = os.path.split(inp_file)
                    self.mkpath(dest_dir)
                    self.copy_file(reg_file, inp_file, level=self.verbose)

    def get_outputs(self):
        self.check_extensions_list(self.extensions)
        outputs = []
        for ext in self.extensions:
            fullname = self.get_ext_fullname(ext.name)
            filename = self.get_ext_filename(fullname)
            if isinstance(ext, Extension) and self.petsc_arch:
                head, tail = os.path.split(filename)
                for arch in self.petsc_arch:
                    outfile = os.path.join(self.build_lib, head, arch, tail)
                    outputs.append(outfile)
            else:
                outfile = os.path.join(self.build_lib, filename)
                outputs.append(outfile)
        return list(set(outputs))

    def get_source_files(self):
        orig = log.set_threshold(log.WARN)
        try:
            return super().get_source_files()
        finally:
            log.set_threshold(orig)


class install(_install):
    def initialize_options(self):
        with warnings.catch_warnings():
            if setuptools:
                if hasattr(setuptools, 'SetuptoolsDeprecationWarning'):
                    category = setuptools.SetuptoolsDeprecationWarning
                    warnings.simplefilter('ignore', category)
            _install.initialize_options(self)
        self.old_and_unmanageable = True


cmdclass_list = [
    config,
    build,
    build_src,
    build_ext,
    install,
]

# --------------------------------------------------------------------


def setup(**attrs):
    cmdclass = attrs.setdefault('cmdclass', {})
    for cmd in cmdclass_list:
        cmdclass.setdefault(cmd.__name__, cmd)
    build_src.sources = attrs.pop('cython_sources', None)
    use_setup_requires = False  # handle Cython requirement ourselves
    if setuptools and build_src.sources and use_setup_requires:
        version = cython_req()
        if not cython_chk(version, verbose=False):
            reqs = attrs.setdefault('setup_requires', [])
            reqs += ['Cython>=' + version]
    return _setup(**attrs)


# --------------------------------------------------------------------

if setuptools:
    try:
        from setuptools.command import egg_info as mod_egg_info

        _FileList = mod_egg_info.FileList

        class FileList(_FileList):
            def process_template_line(self, line):
                level = log.set_threshold(log.ERROR)
                try:
                    _FileList.process_template_line(self, line)
                finally:
                    log.set_threshold(level)

        mod_egg_info.FileList = FileList
    except (ImportError, AttributeError):
        pass

# --------------------------------------------------------------------


def append(seq, item):
    if item not in seq:
        seq.append(item)


def append_dict(conf, dct):
    for key, values in dct.items():
        if key in conf:
            for value in values:
                if value not in conf[key]:
                    conf[key].append(value)


def unique(seq):
    res = []
    for item in seq:
        if item not in res:
            res.append(item)
    return res


def flaglist(flags):
    conf = {
        'define_macros': [],
        'undef_macros': [],
        'include_dirs': [],
        'libraries': [],
        'library_dirs': [],
        'runtime_library_dirs': [],
        'extra_compile_args': [],
        'extra_link_args': [],
    }

    if isinstance(flags, str):
        flags = flags.split()

    switch = '-Wl,'
    newflags = []
    linkopts = []
    for f in flags:
        if f.startswith(switch):
            if len(f) > 4:
                append(linkopts, f[4:])
        else:
            append(newflags, f)
    if linkopts:
        newflags.append(switch + ','.join(linkopts))
    flags = newflags

    append_next_word = None

    for word in flags:
        if append_next_word is not None:
            append(append_next_word, word)
            append_next_word = None
            continue

        switch, value = word[0:2], word[2:]

        if switch == '-I':
            append(conf['include_dirs'], value)
        elif switch == '-D':
            try:
                idx = value.index('=')
                macro = (value[:idx], value[idx + 1 :])
            except ValueError:
                macro = (value, None)
            append(conf['define_macros'], macro)
        elif switch == '-U':
            append(conf['undef_macros'], value)
        elif switch == '-l':
            append(conf['libraries'], value)
        elif switch == '-L':
            append(conf['library_dirs'], value)
        elif switch == '-R':
            append(conf['runtime_library_dirs'], value)
        elif word.startswith('-Wl'):
            linkopts = word.split(',')
            append_dict(conf, flaglist(linkopts[1:]))
        elif word == '-rpath':
            append_next_word = conf['runtime_library_dirs']
        elif word == '-Xlinker':
            append_next_word = conf['extra_link_args']
        else:
            # log.warn("unrecognized flag '%s'" % word)
            pass
    return conf


def prepend_to_flags(path, flags):
    """Prepend a path to compiler flags with absolute paths"""
    if not path:
        return flags

    def append_path(m):
        switch = m.group(1)
        open_quote = m.group(4)
        old_path = m.group(5)
        close_quote = m.group(6)
        if os.path.isabs(old_path):
            moded_path = os.path.normpath(path + os.path.sep + old_path)
            return switch + open_quote + moded_path + close_quote
        return m.group(0)

    return re.sub(r'((^|\s+)(-I|-L))(\s*["\']?)(\S+)(["\']?)', append_path, flags)


def strip_prefix(prefix, string):
    if not prefix:
        return string
    return re.sub(r'^' + prefix, '', string)


# --------------------------------------------------------------------

# Regexes needed for parsing Makefile-like syntaxes
_variable_rx = re.compile(r'([a-zA-Z][a-zA-Z0-9_]+)\s*=\s*(.*)')
_findvar1_rx = re.compile(r'\$\(([A-Za-z][A-Za-z0-9_]*)\)')
_findvar2_rx = re.compile(r'\${([A-Za-z][A-Za-z0-9_]*)}')


def makefile(fileobj, dct=None):
    """Parse a Makefile-style file.

    A dictionary containing name/value pairs is returned.  If an
    optional dictionary is passed in as the second argument, it is
    used instead of a new dictionary.
    """
    fp = TextFile(file=fileobj, strip_comments=1, skip_blanks=1, join_lines=1)

    if dct is None:
        dct = {}
    done = {}
    notdone = {}

    while 1:
        line = fp.readline()
        if line is None:  # eof
            break
        m = _variable_rx.match(line)
        if m:
            n, v = m.group(1, 2)
            v = str.strip(v)
            if '$' in v:
                notdone[n] = v
            else:
                try:
                    v = int(v)
                except ValueError:
                    pass
                done[n] = v
                try:
                    del notdone[n]
                except KeyError:
                    pass
    fp.close()

    # do variable interpolation here
    while notdone:
        for name in list(notdone.keys()):
            value = notdone[name]
            m = _findvar1_rx.search(value) or _findvar2_rx.search(value)
            if m:
                n = m.group(1)
                found = True
                if n in done:
                    item = str(done[n])
                elif n in notdone:
                    # get it on a subsequent round
                    found = False
                else:
                    done[n] = item = ''
                if found:
                    after = value[m.end() :]
                    value = value[: m.start()] + item + after
                    if '$' in after:
                        notdone[name] = value
                    else:
                        try:
                            value = int(value)
                        except ValueError:
                            done[name] = str.strip(value)
                        else:
                            done[name] = value
                        del notdone[name]
            else:
                # bogus variable reference;
                # just drop it since we can't deal
                del notdone[name]
    # save the results in the global dictionary
    dct.update(done)
    return dct


# --------------------------------------------------------------------
