# Copyright 2013-2017 The Meson development team

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at

#     http://www.apache.org/licenses/LICENSE-2.0

# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# This file contains the detection logic for external dependencies that
# are UI-related.

import os
import re
import shutil
import subprocess
from collections import OrderedDict

from .. import mlog
from .. import mesonlib
from ..mesonlib import MesonException, Popen_safe, version_compare
from ..environment import for_windows, detect_cpu

from .base import DependencyException, DependencyMethods
from .base import ExternalDependency, ExternalProgram
from .base import ExtraFrameworkDependency, PkgConfigDependency


class GLDependency(ExternalDependency):
    def __init__(self, environment, kwargs):
        super().__init__('gl', environment, None, kwargs)
        if DependencyMethods.PKGCONFIG in self.methods:
            try:
                pcdep = PkgConfigDependency('gl', environment, kwargs)
                if pcdep.found():
                    self.type_name = 'pkgconfig'
                    self.is_found = True
                    self.compile_args = pcdep.get_compile_args()
                    self.link_args = pcdep.get_link_args()
                    self.version = pcdep.get_version()
                    return
            except Exception:
                pass
        if DependencyMethods.SYSTEM in self.methods:
            if mesonlib.is_osx():
                self.is_found = True
                # FIXME: Use AppleFrameworks dependency
                self.link_args = ['-framework', 'OpenGL']
                # FIXME: Detect version using self.compiler
                self.version = '1'
                return
            if mesonlib.is_windows():
                self.is_found = True
                # FIXME: Use self.compiler.find_library()
                self.link_args = ['-lopengl32']
                # FIXME: Detect version using self.compiler
                self.version = '1'
                return

    def get_methods(self):
        if mesonlib.is_osx() or mesonlib.is_windows():
            return [DependencyMethods.PKGCONFIG, DependencyMethods.SYSTEM]
        else:
            return [DependencyMethods.PKGCONFIG]


class GnuStepDependency(ExternalDependency):
    def __init__(self, environment, kwargs):
        super().__init__('gnustep', environment, 'objc', kwargs)
        self.modules = kwargs.get('modules', [])
        self.detect()

    def detect(self):
        self.confprog = 'gnustep-config'
        try:
            gp = Popen_safe([self.confprog, '--help'])[0]
        except (FileNotFoundError, PermissionError):
            mlog.log('Dependency GnuStep found:', mlog.red('NO'), '(no gnustep-config)')
            return
        if gp.returncode != 0:
            mlog.log('Dependency GnuStep found:', mlog.red('NO'))
            return
        if 'gui' in self.modules:
            arg = '--gui-libs'
        else:
            arg = '--base-libs'
        fp, flagtxt, flagerr = Popen_safe([self.confprog, '--objc-flags'])
        if fp.returncode != 0:
            raise DependencyException('Error getting objc-args: %s %s' % (flagtxt, flagerr))
        args = flagtxt.split()
        self.compile_args = self.filter_args(args)
        fp, libtxt, liberr = Popen_safe([self.confprog, arg])
        if fp.returncode != 0:
            raise DependencyException('Error getting objc-lib args: %s %s' % (libtxt, liberr))
        self.link_args = self.weird_filter(libtxt.split())
        self.version = self.detect_version()
        self.is_found = True
        mlog.log('Dependency', mlog.bold('GnuStep'), 'found:',
                 mlog.green('YES'), self.version)

    def weird_filter(self, elems):
        """When building packages, the output of the enclosing Make
is sometimes mixed among the subprocess output. I have no idea
why. As a hack filter out everything that is not a flag."""
        return [e for e in elems if e.startswith('-')]

    def filter_args(self, args):
        """gnustep-config returns a bunch of garbage args such
        as -O2 and so on. Drop everything that is not needed."""
        result = []
        for f in args:
            if f.startswith('-D') \
                    or f.startswith('-f') \
                    or f.startswith('-I') \
                    or f == '-pthread' \
                    or (f.startswith('-W') and not f == '-Wall'):
                result.append(f)
        return result

    def detect_version(self):
        gmake = self.get_variable('GNUMAKE')
        makefile_dir = self.get_variable('GNUSTEP_MAKEFILES')
        # This Makefile has the GNUStep version set
        base_make = os.path.join(makefile_dir, 'Additional', 'base.make')
        # Print the Makefile variable passed as the argument. For instance, if
        # you run the make target `print-SOME_VARIABLE`, this will print the
        # value of the variable `SOME_VARIABLE`.
        printver = "print-%:\n\t@echo '$($*)'"
        env = os.environ.copy()
        # See base.make to understand why this is set
        env['FOUNDATION_LIB'] = 'gnu'
        p, o, e = Popen_safe([gmake, '-f', '-', '-f', base_make,
                              'print-GNUSTEP_BASE_VERSION'],
                             env=env, write=printver, stdin=subprocess.PIPE)
        version = o.strip()
        if not version:
            mlog.debug("Couldn't detect GNUStep version, falling back to '1'")
            # Fallback to setting some 1.x version
            version = '1'
        return version

    def get_variable(self, var):
        p, o, e = Popen_safe([self.confprog, '--variable=' + var])
        if p.returncode != 0 and self.required:
            raise DependencyException('{!r} for variable {!r} failed to run'
                                      ''.format(self.confprog, var))
        return o.strip()


class QtBaseDependency(ExternalDependency):
    def __init__(self, name, env, kwargs):
        super().__init__(name, env, 'cpp', kwargs)
        self.qtname = name.capitalize()
        self.qtver = name[-1]
        if self.qtver == "4":
            self.qtpkgname = 'Qt'
        else:
            self.qtpkgname = self.qtname
        self.root = '/usr'
        self.bindir = None
        mods = kwargs.get('modules', [])
        if isinstance(mods, str):
            mods = [mods]
        if not mods:
            raise DependencyException('No ' + self.qtname + '  modules specified.')
        type_text = 'cross' if env.is_cross_build() else 'native'
        found_msg = '{} {} {{}} dependency (modules: {}) found:' \
                    ''.format(self.qtname, type_text, ', '.join(mods))
        from_text = 'pkg-config'

        # Keep track of the detection methods used, for logging purposes.
        methods = []
        # Prefer pkg-config, then fallback to `qmake -query`
        if DependencyMethods.PKGCONFIG in self.methods:
            self._pkgconfig_detect(mods, kwargs)
            methods.append('pkgconfig')
        if not self.is_found and DependencyMethods.QMAKE in self.methods:
            from_text = self._qmake_detect(mods, kwargs)
            methods.append('qmake-' + self.name)
            methods.append('qmake')
        if not self.is_found:
            # Reset compile args and link args
            self.compile_args = []
            self.link_args = []
            from_text = '(checked {})'.format(mlog.format_list(methods))
            self.version = 'none'
            if self.required:
                err_msg = '{} {} dependency not found {}' \
                          ''.format(self.qtname, type_text, from_text)
                raise DependencyException(err_msg)
            if not self.silent:
                mlog.log(found_msg.format(from_text), mlog.red('NO'))
            return
        from_text = '`{}`'.format(from_text)
        if not self.silent:
            mlog.log(found_msg.format(from_text), mlog.green('YES'))

    def compilers_detect(self):
        "Detect Qt (4 or 5) moc, uic, rcc in the specified bindir or in PATH"
        if self.bindir:
            moc = ExternalProgram(os.path.join(self.bindir, 'moc'), silent=True)
            uic = ExternalProgram(os.path.join(self.bindir, 'uic'), silent=True)
            rcc = ExternalProgram(os.path.join(self.bindir, 'rcc'), silent=True)
        else:
            # We don't accept unsuffixed 'moc', 'uic', and 'rcc' because they
            # are sometimes older, or newer versions.
            moc = ExternalProgram('moc-' + self.name, silent=True)
            uic = ExternalProgram('uic-' + self.name, silent=True)
            rcc = ExternalProgram('rcc-' + self.name, silent=True)
        return moc, uic, rcc

    def _pkgconfig_detect(self, mods, kwargs):
        # We set the value of required to False so that we can try the
        # qmake-based fallback if pkg-config fails.
        kwargs['required'] = False
        modules = OrderedDict()
        for module in mods:
            modules[module] = PkgConfigDependency(self.qtpkgname + module, self.env, kwargs)
        for m in modules.values():
            if not m.found():
                self.is_found = False
                return
            self.compile_args += m.get_compile_args()
            self.link_args += m.get_link_args()
        self.is_found = True
        self.version = m.version
        # Try to detect moc, uic, rcc
        if 'Core' in modules:
            core = modules['Core']
        else:
            corekwargs = {'required': 'false', 'silent': 'true'}
            core = PkgConfigDependency(self.qtpkgname + 'Core', self.env, corekwargs)
        # Used by self.compilers_detect()
        self.bindir = self.get_pkgconfig_host_bins(core)
        if not self.bindir:
            # If exec_prefix is not defined, the pkg-config file is broken
            prefix = core.get_pkgconfig_variable('exec_prefix')
            if prefix:
                self.bindir = os.path.join(prefix, 'bin')

    def _find_qmake(self, qmake):
        # Even when cross-compiling, if we don't get a cross-info qmake, we
        # fallback to using the qmake in PATH because that's what we used to do
        if self.env.is_cross_build():
            qmake = self.env.cross_info.config['binaries'].get('qmake', qmake)
        return ExternalProgram(qmake, silent=True)

    def _qmake_detect(self, mods, kwargs):
        for qmake in ('qmake-' + self.name, 'qmake'):
            self.qmake = self._find_qmake(qmake)
            if not self.qmake.found():
                continue
            # Check that the qmake is for qt5
            pc, stdo = Popen_safe(self.qmake.get_command() + ['-v'])[0:2]
            if pc.returncode != 0:
                continue
            if not 'Qt version ' + self.qtver in stdo:
                mlog.log('QMake is not for ' + self.qtname)
                continue
            # Found qmake for Qt5!
            break
        else:
            # Didn't find qmake :(
            self.is_found = False
            return
        self.version = re.search(self.qtver + '(\.\d+)+', stdo).group(0)
        # Query library path, header path, and binary path
        mlog.log("Found qmake:", mlog.bold(self.qmake.get_name()), '(%s)' % self.version)
        stdo = Popen_safe(self.qmake.get_command() + ['-query'])[1]
        qvars = {}
        for line in stdo.split('\n'):
            line = line.strip()
            if line == '':
                continue
            (k, v) = tuple(line.split(':', 1))
            qvars[k] = v
        if mesonlib.is_osx():
            return self._framework_detect(qvars, mods, kwargs)
        incdir = qvars['QT_INSTALL_HEADERS']
        self.compile_args.append('-I' + incdir)
        libdir = qvars['QT_INSTALL_LIBS']
        # Used by self.compilers_detect()
        self.bindir = self.get_qmake_host_bins(qvars)
        self.is_found = True
        for module in mods:
            mincdir = os.path.join(incdir, 'Qt' + module)
            self.compile_args.append('-I' + mincdir)
            if for_windows(self.env.is_cross_build(), self.env):
                is_debug = self.env.cmd_line_options.buildtype.startswith('debug')
                dbg = 'd' if is_debug else ''
                if self.qtver == '4':
                    base_name = 'Qt' + module + dbg + '4'
                else:
                    base_name = 'Qt5' + module + dbg
                libfile = os.path.join(libdir, base_name + '.lib')
                if not os.path.isfile(libfile):
                    # MinGW can link directly to .dll
                    libfile = os.path.join(self.bindir, base_name + '.dll')
                    if not os.path.isfile(libfile):
                        self.is_found = False
                        break
            else:
                libfile = os.path.join(libdir, 'lib{}{}.so'.format(self.qtpkgname, module))
                if not os.path.isfile(libfile):
                    self.is_found = False
                    break
            self.link_args.append(libfile)
        return qmake

    def _framework_detect(self, qvars, modules, kwargs):
        libdir = qvars['QT_INSTALL_LIBS']
        for m in modules:
            fname = 'Qt' + m
            fwdep = ExtraFrameworkDependency(fname, False, libdir, self.env,
                                             self.language, kwargs)
            self.compile_args.append('-F' + libdir)
            if fwdep.found():
                self.is_found = True
                self.compile_args += fwdep.get_compile_args()
                self.link_args += fwdep.get_link_args()
        # Used by self.compilers_detect()
        self.bindir = self.get_qmake_host_bins(qvars)

    def get_qmake_host_bins(self, qvars):
        # Prefer QT_HOST_BINS (qt5, correct for cross and native compiling)
        # but fall back to QT_INSTALL_BINS (qt4)
        if 'QT_HOST_BINS' in qvars:
            return qvars['QT_HOST_BINS']
        else:
            return qvars['QT_INSTALL_BINS']

    def get_methods(self):
        return [DependencyMethods.PKGCONFIG, DependencyMethods.QMAKE]

    def get_exe_args(self, compiler):
        # Originally this was -fPIE but nowadays the default
        # for upstream and distros seems to be -reduce-relocations
        # which requires -fPIC. This may cause a performance
        # penalty when using self-built Qt or on platforms
        # where -fPIC is not required. If this is an issue
        # for you, patches are welcome.
        return compiler.get_pic_args()


class Qt4Dependency(QtBaseDependency):
    def __init__(self, env, kwargs):
        QtBaseDependency.__init__(self, 'qt4', env, kwargs)

    def get_pkgconfig_host_bins(self, core):
        # Only return one bins dir, because the tools are generally all in one
        # directory for Qt4, in Qt5, they must all be in one directory. Return
        # the first one found among the bin variables, in case one tool is not
        # configured to be built.
        applications = ['moc', 'uic', 'rcc', 'lupdate', 'lrelease']
        for application in applications:
            try:
                return os.path.dirname(core.get_pkgconfig_variable('%s_location' % application))
            except MesonException:
                pass


class Qt5Dependency(QtBaseDependency):
    def __init__(self, env, kwargs):
        QtBaseDependency.__init__(self, 'qt5', env, kwargs)

    def get_pkgconfig_host_bins(self, core):
        return core.get_pkgconfig_variable('host_bins')


# There are three different ways of depending on SDL2:
# sdl2-config, pkg-config and OSX framework
class SDL2Dependency(ExternalDependency):
    def __init__(self, environment, kwargs):
        super().__init__('sdl2', environment, None, kwargs)
        if DependencyMethods.PKGCONFIG in self.methods:
            try:
                kwargs['required'] = False
                pcdep = PkgConfigDependency('sdl2', environment, kwargs)
                if pcdep.found():
                    self.type_name = 'pkgconfig'
                    self.is_found = True
                    self.compile_args = pcdep.get_compile_args()
                    self.link_args = pcdep.get_link_args()
                    self.version = pcdep.get_version()
                    return
            except Exception as e:
                mlog.debug('SDL 2 not found via pkgconfig. Trying next, error was:', str(e))
        if DependencyMethods.SDLCONFIG in self.methods:
            sdlconf = shutil.which('sdl2-config')
            if sdlconf:
                stdo = Popen_safe(['sdl2-config', '--cflags'])[1]
                self.compile_args = stdo.strip().split()
                stdo = Popen_safe(['sdl2-config', '--libs'])[1]
                self.link_args = stdo.strip().split()
                stdo = Popen_safe(['sdl2-config', '--version'])[1]
                self.version = stdo.strip()
                self.is_found = True
                mlog.log('Dependency', mlog.bold('sdl2'), 'found:', mlog.green('YES'),
                         self.version, '(%s)' % sdlconf)
                return
            mlog.debug('Could not find sdl2-config binary, trying next.')
        if DependencyMethods.EXTRAFRAMEWORK in self.methods:
            if mesonlib.is_osx():
                fwdep = ExtraFrameworkDependency('sdl2', False, None, self.env,
                                                 self.language, kwargs)
                if fwdep.found():
                    self.is_found = True
                    self.compile_args = fwdep.get_compile_args()
                    self.link_args = fwdep.get_link_args()
                    self.version = '2'  # FIXME
                    return
            mlog.log('Dependency', mlog.bold('sdl2'), 'found:', mlog.red('NO'))

    def get_methods(self):
        if mesonlib.is_osx():
            return [DependencyMethods.PKGCONFIG, DependencyMethods.SDLCONFIG, DependencyMethods.EXTRAFRAMEWORK]
        else:
            return [DependencyMethods.PKGCONFIG, DependencyMethods.SDLCONFIG]


class WxDependency(ExternalDependency):
    wx_found = None

    def __init__(self, environment, kwargs):
        super().__init__('wx', environment, None, kwargs)
        self.version = 'none'
        if WxDependency.wx_found is None:
            self.check_wxconfig()
        else:
            self.wxc = WxDependency.wx_found
        if not WxDependency.wx_found:
            mlog.log("Neither wx-config-3.0 nor wx-config found; can't detect dependency")
            return

        # FIXME: This should print stdout and stderr using mlog.debug
        p, out = Popen_safe([self.wxc, '--version'])[0:2]
        if p.returncode != 0:
            mlog.log('Dependency wxwidgets found:', mlog.red('NO'))
        else:
            self.version = out.strip()
            # FIXME: Support multiple version reqs like PkgConfigDependency
            version_req = kwargs.get('version', None)
            if version_req is not None:
                if not version_compare(self.version, version_req, strict=True):
                    mlog.log('Wxwidgets version %s does not fullfill requirement %s' %
                             (self.version, version_req))
                    return
            mlog.log('Dependency wxwidgets found:', mlog.green('YES'))
            self.is_found = True
            self.requested_modules = self.get_requested(kwargs)
            # wx-config seems to have a cflags as well but since it requires C++,
            # this should be good, at least for now.
            p, out = Popen_safe([self.wxc, '--cxxflags'])[0:2]
            # FIXME: this error should only be raised if required is true
            if p.returncode != 0:
                raise DependencyException('Could not generate cargs for wxwidgets.')
            self.compile_args = out.split()

            # FIXME: this error should only be raised if required is true
            p, out = Popen_safe([self.wxc, '--libs'] + self.requested_modules)[0:2]
            if p.returncode != 0:
                raise DependencyException('Could not generate libs for wxwidgets.')
            self.link_args = out.split()

    def get_requested(self, kwargs):
        modules = 'modules'
        if modules not in kwargs:
            return []
        candidates = kwargs[modules]
        if not isinstance(candidates, list):
            candidates = [candidates]
        for c in candidates:
            if not isinstance(c, str):
                raise DependencyException('wxwidgets module argument is not a string')
        return candidates

    def check_wxconfig(self):
        for wxc in ['wx-config-3.0', 'wx-config']:
            try:
                p, out = Popen_safe([wxc, '--version'])[0:2]
                if p.returncode == 0:
                    mlog.log('Found wx-config:', mlog.bold(shutil.which(wxc)),
                             '(%s)' % out.strip())
                    self.wxc = wxc
                    WxDependency.wx_found = wxc
                    return
            except (FileNotFoundError, PermissionError):
                pass
        WxDependency.wxconfig_found = False
        mlog.log('Found wx-config:', mlog.red('NO'))

class VulkanDependency(ExternalDependency):
    def __init__(self, environment, kwargs):
        super().__init__('vulkan', environment, None, kwargs)

        if DependencyMethods.PKGCONFIG in self.methods:
            try:
                pcdep = PkgConfigDependency('vulkan', environment, kwargs)
                if pcdep.found():
                    self.type_name = 'pkgconfig'
                    self.is_found = True
                    self.compile_args = pcdep.get_compile_args()
                    self.link_args = pcdep.get_link_args()
                    self.version = pcdep.get_version()
                    return
            except Exception:
                pass

        if DependencyMethods.SYSTEM in self.methods:
            try:
                self.vulkan_sdk = os.environ['VULKAN_SDK']
                if not os.path.isabs(self.vulkan_sdk):
                    raise DependencyException('VULKAN_SDK must be an absolute path.')
            except KeyError:
                self.vulkan_sdk = None

            if self.vulkan_sdk:
                # TODO: this config might not work on some platforms, fix bugs as reported
                # we should at least detect other 64-bit platforms (e.g. armv8)
                lib_name = 'vulkan'
                if mesonlib.is_windows():
                    lib_name = 'vulkan-1'
                    lib_dir = 'Lib32'
                    inc_dir = 'Include'
                    if detect_cpu({}) == 'x86_64':
                        lib_dir = 'Lib'
                else:
                    lib_name = 'vulkan'
                    lib_dir = 'lib'
                    inc_dir = 'include'

                # make sure header and lib are valid
                inc_path = os.path.join(self.vulkan_sdk, inc_dir)
                header = os.path.join(inc_path, 'vulkan', 'vulkan.h')
                lib_path = os.path.join(self.vulkan_sdk, lib_dir)
                find_lib = self.compiler.find_library(lib_name, environment, lib_path)

                if not find_lib:
                    raise DependencyException('VULKAN_SDK point to invalid directory (no lib)')

                if not os.path.isfile(header):
                    raise DependencyException('VULKAN_SDK point to invalid directory (no include)')

                self.type_name = 'vulkan_sdk'
                self.is_found = True
                self.compile_args.append('-I' + inc_path)
                self.link_args.append('-L' + lib_path)
                self.link_args.append('-l' + lib_name)

                # TODO: find a way to retrieve the version from the sdk?
                # Usually it is a part of the path to it (but does not have to be)
                self.version = '1'
                return
            else:
                # simply try to guess it, usually works on linux
                libs = self.compiler.find_library('vulkan', environment, [])
                if libs is not None and self.compiler.has_header('vulkan/vulkan.h', '', environment):
                    self.type_name = 'system'
                    self.is_found = True
                    self.version = 1 # TODO
                    for lib in libs:
                        self.link_args.append(lib)
                    return

    def get_methods(self):
        return [DependencyMethods.PKGCONFIG, DependencyMethods.SYSTEM]
