#!/usr/bin/env python

import io
import subprocess
import sys
import os
import errno

try:
    from setuptools import setup
except ImportError:
    from distutils.core import setup

from distutils.core import Extension, Command, Distribution
from distutils.ccompiler import new_compiler
from distutils.sysconfig import customize_compiler
from distutils.util import change_root
from distutils import log
from distutils import sysconfig


PYCAIRO_VERSION = '1.18.2'
CAIRO_VERSION_REQUIRED = '1.13.1'
XPYB_VERSION_REQUIRED = '1.3'


def get_command_class(name):
    # in case pip loads with setuptools this returns the extended commands
    return Distribution({}).get_command_class(name)


def _check_output(command):
    try:
        return subprocess.check_output(command)
    except OSError as e:
        if e.errno == errno.ENOENT:
            raise SystemExit(
                "%r not found.\nCommand %r" % (command[0], command))
        raise SystemExit(e)
    except subprocess.CalledProcessError as e:
        raise SystemExit(e)


def pkg_config_version_check(pkg, version):
    command = [
        "pkg-config",
        "--print-errors",
        "--exists",
        '%s >= %s' % (pkg, version),
    ]

    _check_output(command)


def pkg_config_parse(opt, pkg):
    command = ["pkg-config", opt, pkg]
    ret = _check_output(command)
    output = ret.decode()
    opt = opt[-2:]
    return [x.lstrip(opt) for x in output.split()]


def filter_compiler_arguments(compiler, args):
    """Given a compiler instance and a list of compiler warning flags
    returns the list of supported flags.
    """

    if compiler.compiler_type == "msvc":
        # TODO
        return []

    extra = []

    def check_arguments(compiler, args):
        p = subprocess.Popen(
            [compiler.compiler[0]] + args + extra + ["-x", "c", "-E", "-"],
            stdin=subprocess.PIPE,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE)
        stdout, stderr = p.communicate(b"int i;\n")
        if p.returncode != 0:
            text = stderr.decode("ascii", "replace")
            return False, [a for a in args if a in text]
        else:
            return True, []

    def check_argument(compiler, arg):
        return check_arguments(compiler, [arg])[0]

    # clang doesn't error out for unknown options, force it to
    if check_argument(compiler, '-Werror=unknown-warning-option'):
        extra += ['-Werror=unknown-warning-option']
    if check_argument(compiler, '-Werror=unused-command-line-argument'):
        extra += ['-Werror=unused-command-line-argument']

    # first try to remove all arguments contained in the error message
    supported = list(args)
    while 1:
        ok, maybe_unknown = check_arguments(compiler, supported)
        if ok:
            return supported
        elif not maybe_unknown:
            break
        for unknown in maybe_unknown:
            if not check_argument(compiler, unknown):
                supported.remove(unknown)

    # hm, didn't work, try each argument one by one
    supported = []
    for arg in args:
        if check_argument(compiler, arg):
            supported.append(arg)
    return supported


def add_ext_cflags(ext, compiler):
    args = [
        "-Wall",
        "-Warray-bounds",
        "-Wcast-align",
        "-Wconversion",
        "-Wextra",
        "-Wformat=2",
        "-Wformat-nonliteral",
        "-Wformat-security",
        "-Wimplicit-function-declaration",
        "-Winit-self",
        "-Winline",
        "-Wmissing-format-attribute",
        "-Wmissing-noreturn",
        "-Wnested-externs",
        "-Wold-style-definition",
        "-Wpacked",
        "-Wpointer-arith",
        "-Wreturn-type",
        "-Wshadow",
        "-Wsign-compare",
        "-Wstrict-aliasing",
        "-Wundef",
        "-Wunused-but-set-variable",
    ]

    if sys.version_info[:2] not in [(3, 3), (3, 4)]:
        args += [
            "-Wswitch-default",
        ]

    args += [
        "-Wno-missing-field-initializers",
        "-Wno-unused-parameter",
    ]

    # silence clang for unused gcc CFLAGS added by Debian
    args += [
        "-Wno-unused-command-line-argument",
    ]

    args += [
        "-fno-strict-aliasing",
        "-fvisibility=hidden",
    ]

    ext.extra_compile_args += filter_compiler_arguments(compiler, args)


class build_tests(Command):
    description = "build test libraries and extensions"
    user_options = [
        ("force", "f", "force a rebuild"),
        ("enable-xpyb", None, "Build with xpyb support (default=disabled)"),
    ]

    def initialize_options(self):
        self.force = False
        self.build_base = None
        self.enable_xpyb = False

    def finalize_options(self):
        self.set_undefined_options(
            'build',
            ('build_base', 'build_base'))
        self.enable_xpyb = bool(self.enable_xpyb)
        self.force = bool(self.force)

    def run(self):
        cmd = self.reinitialize_command("build_ext")
        cmd.inplace = True
        cmd.enable_xpyb = self.enable_xpyb
        cmd.force = self.force
        cmd.ensure_finalized()
        cmd.run()

        import cairo

        tests_dir = os.path.join("tests", "cmodule")

        ext = Extension(
            name='tests.cmod',
            sources=[
                os.path.join(tests_dir, "cmodule.c"),
                os.path.join(tests_dir, "cmodulelib.c"),
            ],
            include_dirs=[
                tests_dir,
                cairo.get_include(),
            ],
            depends=[
                os.path.join(tests_dir, "cmodulelib.h"),
                os.path.join(cairo.get_include(), "pycairo.h"),
            ],
            define_macros=[("PY_SSIZE_T_CLEAN", None)],
        )

        compiler = new_compiler()
        customize_compiler(compiler)

        add_ext_cflags(ext, compiler)

        if compiler.compiler_type == "msvc":
            ext.libraries += ['cairo']
        else:
            pkg_config_version_check('cairo', CAIRO_VERSION_REQUIRED)
            ext.include_dirs += pkg_config_parse('--cflags-only-I', 'cairo')
            ext.library_dirs += pkg_config_parse('--libs-only-L', 'cairo')
            ext.libraries += pkg_config_parse('--libs-only-l', 'cairo')

        dist = Distribution({"ext_modules": [ext]})

        build_cmd = dist.get_command_obj("build")
        build_cmd.build_base = os.path.join(self.build_base, "pycairo_tests")
        build_cmd.ensure_finalized()

        cmd = dist.get_command_obj("build_ext")
        cmd.inplace = True
        cmd.force = self.force
        cmd.ensure_finalized()
        cmd.run()


class test_cmd(Command):
    description = "run tests"
    user_options = [
        ("enable-xpyb", None, "Build with xpyb support (default=disabled)"),
    ]

    def initialize_options(self):
        self.enable_xpyb = None

    def finalize_options(self):
        self.enable_xpyb = bool(self.enable_xpyb)

    def run(self):
        import pytest

        # ensure the C extension is build inplace
        cmd = self.reinitialize_command("build_tests")
        cmd.enable_xpyb = self.enable_xpyb
        cmd.ensure_finalized()
        cmd.run()

        status = pytest.main(["tests"])
        if status != 0:
            raise SystemExit(status)


class install_pkgconfig(Command):
    description = "install .pc file"
    user_options = [
        ('pkgconfigdir=', None, 'pkg-config file install directory'),
    ]

    def initialize_options(self):
        self.root = None
        self.install_base = None
        self.install_data = None
        self.pkgconfigdir = None
        self.compiler_type = None
        self.outfiles = []

    def finalize_options(self):
        self.set_undefined_options(
            'install_lib',
            ('install_base', 'install_base'),
            ('install_data', 'install_data'),
        )

        self.set_undefined_options(
            'install',
            ('root', 'root'),
        )

        self.set_undefined_options(
            'build_ext',
            ('compiler_type', 'compiler_type'),
        )

    def get_outputs(self):
        return self.outfiles

    def get_inputs(self):
        return []

    def run(self):
        # https://github.com/pygobject/pycairo/issues/83
        # The pkg-config file contains absolute paths depending on the
        # prefix. pip uses wheels as cache and when installing with --user
        # and then to a virtualenv, the wheel gets reused containing the
        # wrong paths. So in case bdist_wheel is used, just skip this command.
        cmd = self.distribution.get_command_obj("bdist_wheel", create=False)
        if cmd is not None:
            log.info(
                "Skipping install_pkgconfig, not supported with bdist_wheel")
            return

        # same for bdist_egg
        cmd = self.distribution.get_command_obj("bdist_egg", create=False)
        if cmd is not None:
            log.info(
                "Skipping install_pkgconfig, not supported with bdist_egg")
            return

        if self.compiler_type == "msvc":
            log.info(
                "Skipping install_pkgconfig, not supported with MSVC")
            return

        if self.pkgconfigdir is None:
            python_lib = sysconfig.get_python_lib(True, True,
                                                  self.install_data)
            pkgconfig_dir = os.path.join(os.path.dirname(python_lib),
                                         'pkgconfig')
        else:
            pkgconfig_dir = change_root(self.root, self.pkgconfigdir)
        self.mkpath(pkgconfig_dir)

        pcname = "py3cairo.pc" if sys.version_info[0] == 3 else "pycairo.pc"
        target = os.path.join(pkgconfig_dir, pcname)

        log.info("Writing %s" % target)
        log.info("pkg-config prefix: %s" % self.install_base)
        with open(target, "wb") as h:
            h.write((u"""\
prefix=%(prefix)s

Name: Pycairo
Description: Python %(py_version)d bindings for cairo
Version: %(version)s
Requires: cairo
Cflags: -I${prefix}/include/pycairo
Libs:
""" % {
                "prefix": self.install_base,
                "version": PYCAIRO_VERSION,
                "py_version": sys.version_info[0]}).encode("utf-8"))

        self.outfiles.append(target)


class install_pycairo_header(Command):
    description = "install pycairo header"
    user_options = []

    def initialize_options(self):
        self.install_data = None
        self.install_lib = None
        self.force = None
        self.outfiles = []

    def finalize_options(self):
        self.set_undefined_options(
            'install_lib',
            ('install_data', 'install_data'),
            ('install_lib', 'install_lib'),
        )

        self.set_undefined_options(
            'install',
            ('force', 'force'),
        )

    def get_outputs(self):
        return self.outfiles

    def get_inputs(self):
        return [os.path.join('cairo', 'pycairo.h')]

    def run(self):
        # https://github.com/pygobject/pycairo/issues/92
        # https://github.com/pygobject/pycairo/issues/98
        hname = "py3cairo.h" if sys.version_info[0] == 3 else "pycairo.h"
        source = self.get_inputs()[0]

        # for things using get_include()
        lib_hdir = os.path.join(self.install_lib, "cairo", "include")
        self.mkpath(lib_hdir)
        lib_header_path = os.path.join(lib_hdir, hname)
        (out, _) = self.copy_file(source, lib_header_path)
        self.outfiles.append(out)

        cmd = self.distribution.get_command_obj("bdist_wheel", create=False)
        if cmd is not None:
            return
        cmd = self.distribution.get_command_obj("bdist_egg", create=False)
        if cmd is not None:
            return

        # for things using pkg-config
        data_hdir = os.path.join(self.install_data, "include", "pycairo")
        self.mkpath(data_hdir)
        header_path = os.path.join(data_hdir, hname)
        (out, _) = self.copy_file(source, header_path)
        self.outfiles.append(out)


du_install_lib = get_command_class("install_lib")


class install_lib(du_install_lib):

    def initialize_options(self):
        self.install_base = None
        self.install_lib = None
        self.install_data = None
        du_install_lib.initialize_options(self)

    def finalize_options(self):
        du_install_lib.finalize_options(self)
        self.set_undefined_options(
            'install',
            ('install_base', 'install_base'),
            ('install_lib', 'install_lib'),
            ('install_data', 'install_data'),
        )

    def run(self):
        du_install_lib.run(self)
        # bdist_egg doesn't run install, so run our commands here instead
        self.run_command("install_pkgconfig")
        self.run_command("install_pycairo_header")


du_build_ext = get_command_class("build_ext")


class build_ext(du_build_ext):

    user_options = du_build_ext.user_options + [
        ("enable-xpyb", None, "Build with xpyb support (default=disabled)"),
    ]

    def initialize_options(self):
        du_build_ext.initialize_options(self)
        self.enable_xpyb = None
        self.compiler_type = None

    def finalize_options(self):
        du_build_ext.finalize_options(self)

        self.set_undefined_options(
            'build',
            ('enable_xpyb', 'enable_xpyb'),
        )

        self.compiler_type = new_compiler(compiler=self.compiler).compiler_type

    def run(self):
        ext = self.extensions[0]

        # If we are using MSVC, don't use pkg-config,
        # just assume that INCLUDE and LIB contain
        # the paths to the Cairo headers and libraries,
        # respectively.
        if self.compiler_type == "msvc":
            ext.libraries += ['cairo']
        else:
            pkg_config_version_check('cairo', CAIRO_VERSION_REQUIRED)
            ext.include_dirs += pkg_config_parse('--cflags-only-I', 'cairo')
            ext.library_dirs += pkg_config_parse('--libs-only-L', 'cairo')
            ext.libraries += pkg_config_parse('--libs-only-l', 'cairo')

            compiler = new_compiler(compiler=self.compiler)
            customize_compiler(compiler)
            add_ext_cflags(ext, compiler)

        if self.enable_xpyb:
            if sys.version_info[0] != 2:
                raise SystemExit("xpyb only supported with Python 2")
            pkg_config_version_check("xpyb", XPYB_VERSION_REQUIRED)

            ext.define_macros += [("HAVE_XPYB", None)]
            ext.include_dirs += pkg_config_parse('--cflags-only-I', 'xpyb')
            ext.library_dirs += pkg_config_parse('--libs-only-L', 'xpyb')
            ext.libraries += pkg_config_parse('--libs-only-l', 'xpyb')

        du_build_ext.run(self)


du_build = get_command_class("build")


class build(du_build):

    user_options = du_build.user_options + [
        ("enable-xpyb", None, "Build with xpyb support (default=disabled)"),
    ]

    def initialize_options(self):
        du_build.initialize_options(self)
        self.enable_xpyb = False

    def finalize_options(self):
        du_build.finalize_options(self)
        self.enable_xpyb = bool(self.enable_xpyb)


def main():

    cairo_ext = Extension(
        name='cairo._cairo',
        sources=[
            'cairo/device.c',
            'cairo/bufferproxy.c',
            'cairo/error.c',
            'cairo/cairomodule.c',
            'cairo/context.c',
            'cairo/font.c',
            'cairo/matrix.c',
            'cairo/path.c',
            'cairo/pattern.c',
            'cairo/region.c',
            'cairo/surface.c',
            'cairo/enums.c',
            'cairo/misc.c',
            'cairo/glyph.c',
            'cairo/rectangle.c',
            'cairo/textcluster.c',
            'cairo/textextents.c',
        ],
        depends=[
            'cairo/compat.h',
            'cairo/private.h',
            'cairo/pycairo.h',
        ],
        define_macros=[
            ("PYCAIRO_VERSION_MAJOR", PYCAIRO_VERSION.split('.')[0]),
            ("PYCAIRO_VERSION_MINOR", PYCAIRO_VERSION.split('.')[1]),
            ("PYCAIRO_VERSION_MICRO", PYCAIRO_VERSION.split('.')[2]),
        ],
    )

    with io.open('README.rst', encoding="utf-8") as h:
        long_description = h.read()

    cmdclass = {
        "build": build,
        "build_ext": build_ext,
        "install_lib": install_lib,
        "install_pkgconfig": install_pkgconfig,
        "install_pycairo_header": install_pycairo_header,
        "test": test_cmd,
        "build_tests": build_tests,
    }

    setup(
        name="pycairo",
        version=PYCAIRO_VERSION,
        url="https://pycairo.readthedocs.io",
        description="Python interface for cairo",
        long_description=long_description,
        maintainer="Christoph Reiter",
        maintainer_email="reiter.christoph@gmail.com",
        ext_modules=[cairo_ext],
        packages=["cairo"],
        package_data={
            "cairo": [
                "__init__.pyi",
                "py.typed",
            ],
        },
        classifiers=[
            'Operating System :: OS Independent',
            'Programming Language :: Python :: 2',
            'Programming Language :: Python :: 2.7',
            'Programming Language :: Python :: 3',
            'Programming Language :: Python :: 3.4',
            'Programming Language :: Python :: 3.5',
            'Programming Language :: Python :: 3.6',
            'Programming Language :: Python :: 3.7',
            'Programming Language :: Python :: 3.8',
            'Programming Language :: Python :: Implementation :: CPython',
            'Programming Language :: Python :: Implementation :: PyPy',
            ('License :: OSI Approved :: '
             'GNU Lesser General Public License v2 (LGPLv2)'),
            'License :: OSI Approved :: Mozilla Public License 1.1 (MPL 1.1)',
        ],
        cmdclass=cmdclass,
    )


if __name__ == "__main__":
    main()
