#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import os
import shlex
import subprocess
import sys
import sysconfig

# When building with readthedocs, install the requirements too
if "READTHEDOCS" in os.environ:
    reqs = "requirements.txt"
    if os.path.isfile(reqs):
        subprocess.check_call(
                [sys.executable, "-m", "pip", "install", "-r", reqs])

from setuptools import setup, Command
from setuptools.command.build_py import build_py as _build_py
# Explicitly use the build_ext from distutils for now so we don't get the
# old one that tries to wrap Cython for us.  This shold be fixed with newer
# versions of setuptools.
from distutils.command.build_ext import build_ext as _build_ext
from setuptools.command.bdist_egg import bdist_egg as _bdist_egg
from setuptools.extension import Extension

import warnings
warnings.simplefilter("always")

from glob import glob

opj = os.path.join


cythonize_dir = "build"

macros = [
    # Disable .c line numbers in exception tracebacks
    ("CYTHON_CLINE_IN_TRACEBACK", 0),
]

depends = glob(opj("src", "cysignals", "*.h"))

if sys.platform == 'cygwin':
    # On Cygwin FD_SETSIZE defaults to a rather low 64; we set it higher
    # for use with PSelecter
    # See https://github.com/sagemath/cysignals/pull/57
    macros.append(('FD_SETSIZE', 512))
    depends.append(opj("src", "cysignals", "implementation_cygwin.c"))

# Disable sanity checking in GNU libc. This is required because of
# false positives in the longjmp() check.
undef_macros = ["_FORTIFY_SOURCE"]

kwds = dict(include_dirs=[opj("src"),
                          opj("src", "cysignals")],
            depends=depends,
            define_macros=macros,
            undef_macros=undef_macros,
            extra_compile_args=["-pthread", "-Wp,-U_FORTIFY_SOURCE"],
            extra_link_args=["-pthread"],)

extensions = [
    Extension("cysignals.signals", ["src/cysignals/signals.pyx"], **kwds),
    Extension("cysignals.pysignals", ["src/cysignals/pysignals.pyx"], **kwds),
    Extension("cysignals.alarm", ["src/cysignals/alarm.pyx"], **kwds),
    Extension("cysignals.pselect", ["src/cysignals/pselect.pyx"], **kwds),
    Extension("cysignals.tests", ["src/cysignals/tests.pyx"], **kwds),
]


classifiers = [
    'Development Status :: 5 - Production/Stable',
    'Intended Audience :: Developers',
    ('License :: OSI Approved :: '
     'GNU Lesser General Public License v3 or later (LGPLv3+)'),
    'Operating System :: POSIX',
    'Programming Language :: C',
    'Programming Language :: Cython',
    'Programming Language :: Python',
    'Programming Language :: Python :: 3',
    'Programming Language :: Python :: 3.6',
    'Programming Language :: Python :: 3.7',
    'Programming Language :: Python :: 3.8',
    'Programming Language :: Python :: 3.9',
    'Programming Language :: Python :: 3.10',
    'Programming Language :: Python :: 3.11',
    'Programming Language :: Python :: 3.12',
    'Topic :: System',
    'Topic :: Software Development :: Debuggers',
]


def is_newer_than(file1, file2):
    """
    Return True if file1 is newer than file2.

    Also returns True if file2 does not exist (file1 *must* exist).
    """

    if not os.path.isfile(file2):
        return True

    return os.stat(file1).st_mtime > os.stat(file2).st_mtime


class configure(Command):
    description = "generate and run the configure script"
    user_options = [
        ('configure-flags=', 'c', 'additional flags to pass to --configure')
    ]

    # Hard-coded list of outputs generated by ./configure; this should be
    # kept in sync with configure.ac.  We could also perhaps search the
    # source tree for .in files, but this list does not change often.
    configure_outputs = [
        opj('src', 'config.h'),
        opj('src', 'cysignals', 'cysignals_config.h'),
        opj('src', 'cysignals', 'signals.pxd')
    ]

    # NOTE: Perhaps --prefix and other configure flags can be determined
    # based on other setup.py arguments, but so far cysignals has not really
    # needed this (it always just ran ./configure without arguments by default)
    # so I think we can live without this for now.
    def initialize_options(self):
        self.configure_flags = None

    def finalize_options(self):
        if self.configure_flags is None:
            self.configure_flags = []
        else:
            self.configure_flags = shlex.split(self.configure_flags)

    def run(self):
        # Re-make the configure script if necessary
        if is_newer_than('configure.ac', 'configure'):
            subprocess.check_call(['make', 'configure'])

        def needs_reconfigure(filename):
            # If the configure script or the file's input template are
            # newer, configure should be re-run
            return (is_newer_than('configure', filename) or
                    is_newer_than(filename + '.in', filename))

        if any(needs_reconfigure(f) for f in self.configure_outputs):
            subprocess.check_call(['sh', 'configure',
                                   f'CC={sysconfig.get_config_var("CC")}',
                                   f'CXX={sysconfig.get_config_var("CXX")}']
                                  + self.configure_flags)


class build_ext(_build_ext):
    def run(self):
        # Make sure configure has been run
        self.run_command('configure')
        dist = self.distribution
        ext_modules = dist.ext_modules
        if ext_modules:
            dist.ext_modules[:] = self.cythonize(ext_modules)

        super().run()

    def cythonize(self, extensions):
        # Run Cython with -Werror on continuous integration services
        # with Python 3.6 or later
        from Cython.Compiler import Options
        Options.warning_errors = False

        from Cython.Build.Dependencies import cythonize
        return cythonize(extensions,
                build_dir=cythonize_dir,
                include_path=["src", os.path.join(cythonize_dir, "src")],
                compiler_directives=dict(
                    binding=True,
                    language_level=2,
                    ))


class build_py(_build_py):
    # Override the build_py command to run configure as a prerequisite
    def run(self):
        self.run_command('configure')
        super().run()


class no_egg(_bdist_egg):
    def run(self):
        from distutils.errors import DistutilsOptionError
        raise DistutilsOptionError(
            "The package cysignals will not function correctly when built as "
            "egg. Therefore, it cannot be installed using "
            "'python setup.py install' or 'easy_install'. Instead, use "
            "'pip install' to install cysignals.")


with open("VERSION") as f:
    VERSION = f.read().strip()

with open('README.rst') as f:
    README = f.read()


setup(
    name="cysignals",
    author=u"Martin R. Albrecht, François Bissey, Volker Braun, Jeroen Demeyer",
    author_email="sage-devel@googlegroups.com",
    version=VERSION,
    url="https://github.com/sagemath/cysignals",
    license="GNU Lesser General Public License, version 3 or later",
    description="Interrupt and signal handling for Cython",
    long_description=README,
    long_description_content_type='text/x-rst',
    classifiers=classifiers,
    ext_modules=extensions,
    packages=["cysignals"],
    package_dir={"": "src"},
    package_data={"cysignals": ["*.pxd", "*.h"]},
    data_files=[(opj("share", "cysignals"), [opj("src", "scripts", "cysignals-CSI-helper.py")])],
    scripts=glob(opj("src", "scripts", "cysignals-CSI")),
    cmdclass=dict(
        configure=configure,
        build_py=build_py,
        build_ext=build_ext,
        bdist_egg=no_egg
    ),
)
