# Subclasses disutils.command.build_ext,
# replacing it with a SIP version that compiles .sip -> .cpp
# before calling the original build_ext command.
# Written by Giovanni Bajo <rasky at develer dot com>
# Based on Pyrex.Distutils, written by Graham Fawcett and Darrel Gallion.

import os
import subprocess

from distutils.sysconfig import customize_compiler, get_python_lib
import distutils.command.build_ext

import toml

from sipbuild.code_generator import set_globals, parse, generateCode
from sipbuild.exceptions import UserException
from sipbuild.module import copy_sip_h, resolve_abi_version
from sipbuild.version import SIP_VERSION, SIP_VERSION_STR

##################################################################

def replace_suffix(path, new_suffix):
    return os.path.splitext(path)[0] + new_suffix

def find_on_path(names, mainname):
    """From a list of names of executables, find the 1st one on a path.

    mainname is the generic name to report
    """
    path = os.getenv('PATH', os.path.defpath)
    pathparts = path.split(os.path.pathsep)
    for cmd in names:
        for dirname in pathparts:
            cmdtry = os.path.join(dirname.strip('"'), cmd)
            if os.path.isfile(cmdtry) and os.access(cmdtry, os.X_OK):
                return cmdtry
    raise RuntimeError('Could not find %s executable' % mainname)

def read_command_output(cmd):
    """Get text from a run command."""
    p = subprocess.Popen(
        cmd,
        stdout=subprocess.PIPE,
        universal_newlines=True)
    stdout, stderr = p.communicate()
    if p.returncode != 0:
        raise RuntimeError('Command %s returned error' % str(cmd))
    return stdout.strip()

class build_ext(distutils.command.build_ext.build_ext):

    description = ('Compile SIP descriptions, then build C/C++ extensions '
                   '(compile/link to build directory)')

    user_options = distutils.command.build_ext.build_ext.user_options + [
        ('qmake-exe=', None,
         'override qmake executable'),
        ('qt-include-dir=', None,
         'override Qt include directory'),
        ('qt-library-dir=', None,
         'override Qt library directory'),
        ('qt-libinfix=', None,
         'override Qt infix setting'),
        ]

    def initialize_options(self):
        distutils.command.build_ext.build_ext.initialize_options(self)
        self.qmake_exe = None
        self.qt_include_dir = None
        self.qt_library_dir = None
        self.qt_libinfix = None

    def _get_qmake(self, build_cmd):
        """Get qmake executable."""
        return (
            build_cmd.qmake_exe or
            os.environ.get('QMAKE_EXE') or
            find_on_path(
                ('qmake-qt5', 'qmake5', 'qmake', 'qmake5.exe', 'qmake.exe'),
                'qmake')
        )

    def _get_qt_inc_dir(self, build_cmd):
        """Get Qt include directory."""
        return (
            build_cmd.qt_include_dir or
            os.environ.get('QT_INCLUDE_DIR') or
            read_command_output(
                [self._get_qmake(build_cmd), '-query', 'QT_INSTALL_HEADERS'])
            )

    def _get_qt_library_dir(self, build_cmd):
        """Get Qt library directory."""
        return (
            build_cmd.qt_library_dir or
            os.environ.get('QT_LIBRARY_DIR') or
            read_command_output(
                [self._get_qmake(build_cmd), '-query', 'QT_INSTALL_LIBS'])
            )

    def _get_qt_libinfix(self, build_cmd):
        """Get QT_LIBINFIX setting.

        This is not much fun, as we have to try to find qconfig.pri,
        and parse it.
        """

        infix = build_cmd.qt_libinfix
        if infix is not None:
            return infix
        if 'QT_LIBINFIX' in os.environ:
            return os.environ['QT_LIBINFIX']

        # use this to find location of qconfig file
        archdir = read_command_output(
            [self._get_qmake(build_cmd), '-query', 'QT_INSTALL_ARCHDATA'])
        qconfig = os.path.join(archdir, 'mkspecs', 'qconfig.pri')

        libinfix = ''
        for line in open(qconfig):
            p = [x.strip() for x in line.split('=')]
            if p[0] == 'QT_LIBINFIX':
                libinfix = p[1]

        return libinfix

    def _is_qt_framework(self, build_cmd):
        """Is the Qt a framework?"""
        return os.path.exists(
            os.path.join(
                self._get_qt_library_dir(build_cmd), 'QtCore.framework'))

    def _get_cpp_includes(self, build_cmd):
        """Get list of include directories to add."""
        inc_dir = self._get_qt_inc_dir(build_cmd)
        incdirs = [inc_dir]
        for mod in ('QtCore', 'QtGui', 'QtWidgets', 'QtXml'):
            if self._is_qt_framework(build_cmd):
                incdirs.append(
                    os.path.join(
                        self._get_qt_library_dir(build_cmd),
                        mod+'.framework', 'Headers') )
            else:
                incdirs.append(os.path.join(inc_dir, mod))
        return incdirs

    def swig_sources(self, sources, extension=None):
        """Compile SIP files and setup Qt compile options."""

        if not self.extensions:
            return

        build_cmd = self.get_finalized_command('build_ext')

        # add directory of input files as include path
        indirs = list(set([os.path.dirname(x) for x in sources]))

        # Add the SIP and Qt include directories to the include path
        extension.include_dirs += indirs

        libinfix = self._get_qt_libinfix(build_cmd)

        # link against libraries
        if extension.language == 'c++':
            extension.include_dirs += self._get_cpp_includes(build_cmd)
            lib_dir = self._get_qt_library_dir(build_cmd)
            if self._is_qt_framework(build_cmd):
                # Mac OS framework
                extension.extra_link_args = [
                    '-F', os.path.join(lib_dir),
                    '-framework', 'QtGui'+libinfix,
                    '-framework', 'QtCore'+libinfix,
                    '-framework', 'QtXml'+libinfix,
                    '-framework', 'QtWidgets'+libinfix,
                    '-Wl,-rpath,@executable_path/Frameworks',
                    '-Wl,-rpath,' + lib_dir
                    ]
                extension.extra_compile_args = [
                    '-F', lib_dir,
                    ]
            else:
                extension.libraries = [
                    'Qt5Gui'+libinfix,
                    'Qt5Core'+libinfix,
                    'Qt5Xml'+libinfix,
                    'Qt5Widgets'+libinfix,
                ]
            extension.library_dirs = [lib_dir]

            # may cause problems with compilers which don't allow this
            if self.compiler.compiler_type == 'unix':
                extension.extra_compile_args.append('-std=c++11')

        depends = extension.depends

        # Filter dependencies list: we are interested only in .sip files,
        # since the main .sip files can only depend on additional .sip
        # files. For instance, if a .h changes, there is no need to
        # run sip again.
        depends = [f for f in depends if os.path.splitext(f)[1] == '.sip']

        # Create the temporary directory if it does not exist already
        if not os.path.isdir(self.build_temp):
            os.makedirs(self.build_temp)

        # Collect the names of the source (.sip) files
        sip_sources = []
        sip_sources = [source for source in sources if source.endswith('.sip')]
        other_sources = [source for source in sources
                         if not source.endswith('.sip')]
        generated_sources = []

        for sip in sip_sources:
            sip_basename = os.path.basename(sip)[:-4]
            sip_builddir = os.path.join(self.build_temp, 'sip-' + sip_basename)
            if not os.path.exists(sip_builddir) or self.force:
                os.makedirs(sip_builddir, exist_ok=True)
                self._sip_compile(sip, sip_builddir)
            out = [
                os.path.join(sip_builddir, fn)
                for fn in sorted(os.listdir(sip_builddir))
                if fn.endswith(".cpp")
            ]
            generated_sources.extend(out)

        return generated_sources + other_sources

    def _sip_compile(self, source, sip_builddir):
        """Compile sip file to sources."""
        sip_module = 'PyQt5.sip'
        pyqt5_include_dir = os.path.join(get_python_lib(plat_specific=1),
                                         'PyQt5', 'bindings')
        pyqt5_toml = os.path.join(pyqt5_include_dir, 'QtCore', 'QtCore.toml')
        pyqt5_cfg = toml.load(pyqt5_toml)
        abi_version = pyqt5_cfg.get('sip-abi-version')
        abi_major, abi_minor = abi_version.split('.')

        # header file location changed to major version in sip-6.2.0
        if SIP_VERSION >= 0x60200:
            hdr_version = abi_major
        else:
            hdr_version = abi_version
        copy_sip_h(hdr_version, sip_builddir, sip_module)

        sip_major_version = SIP_VERSION >> 16
        common_args = [SIP_VERSION, SIP_VERSION_STR, int(abi_major), int(abi_minor)]
        if sip_major_version >= 6:
            set_globals(*common_args, sip_module, UserException, [pyqt5_include_dir])
        else:
            set_globals(*common_args, UserException, [pyqt5_include_dir])

        pt, _fq_name, _uses_limited_api, _sip_files, tags, disabled_features = parse(
            source,
            True,   # strict mode
            [],     # list of additional version/platform tags
            [],     # list of timeline backstops
            [],     # list of disabled features
            False,  # protected is public
        )

        generate_args = [
            pt,
            sip_builddir,
            None,   # source files suffix (default .c, .cpp)
            False,  # whether to enable support for exceptions
            False,  # whether to generate code with tracing enabled
            False,  # whether to always release and reacquire the GIL
            0,      # number of files to split the generated code into
            tags,
            disabled_features,
            False,  # whether to enable the automatic generation of docstrings
            False,  # whether to generate code for a debug build of Python
        ]
        if sip_major_version < 6:
            generate_args.append(sip_module)
        generateCode(*generate_args)

    def build_extensions(self):
        # remove annoying flag which causes warning for c++ sources
        # https://stackoverflow.com/a/36293331/351771
        customize_compiler(self.compiler)
        try:
            self.compiler.compiler_so.remove("-Wstrict-prototypes")
        except (AttributeError, ValueError):
            pass
        distutils.command.build_ext.build_ext.build_extensions(self)
