#############################################################################
##
## Copyright (C) 2021 The Qt Company Ltd.
## Contact: https://www.qt.io/licensing/
##
## This file is part of Qt for Python.
##
## $QT_BEGIN_LICENSE:LGPL$
## Commercial License Usage
## Licensees holding valid commercial Qt licenses may use this file in
## accordance with the commercial license agreement provided with the
## Software or, alternatively, in accordance with the terms contained in
## a written agreement between you and The Qt Company. For licensing terms
## and conditions see https://www.qt.io/terms-conditions. For further
## information use the contact form at https://www.qt.io/contact-us.
##
## GNU Lesser General Public License Usage
## Alternatively, this file may be used under the terms of the GNU Lesser
## General Public License version 3 as published by the Free Software
## Foundation and appearing in the file LICENSE.LGPL3 included in the
## packaging of this file. Please review the following information to
## ensure the GNU Lesser General Public License version 3 requirements
## will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
##
## GNU General Public License Usage
## Alternatively, this file may be used under the terms of the GNU
## General Public License version 2.0 or (at your option) the GNU General
## Public license version 3 or any later version approved by the KDE Free
## Qt Foundation. The licenses are as published by the Free Software
## Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
## included in the packaging of this file. Please review the following
## information to ensure the GNU General Public License requirements will
## be met: https://www.gnu.org/licenses/gpl-2.0.html and
## https://www.gnu.org/licenses/gpl-3.0.html.
##
## $QT_END_LICENSE$
##
#############################################################################

import os
import platform
import sys
from sysconfig import get_config_var

from setuptools._distutils import log
from setuptools._distutils import sysconfig as sconfig
from setuptools._distutils.errors import DistutilsSetupError

from .options import OPTION
from .qtinfo import QtInfo
from .utils import configure_cmake_project, parse_cmake_project_message_info
from .wheel_utils import get_qt_version


# Return a prefix suitable for the _install/_build directory
def prefix():
    virtual_env_name = os.environ.get('VIRTUAL_ENV', None)
    has_virtual_env = False
    if virtual_env_name is not None:
        name = os.path.basename(virtual_env_name)
        has_virtual_env = True
    else:
        name = "qfp"
    if OPTION["DEBUG"]:
        name += "d"
    if is_debug_python():
        name += "p"
    if OPTION["LIMITED_API"] == "yes":
        name += "a"
    return name, has_virtual_env


def is_debug_python():
    return getattr(sys, "gettotalrefcount", None) is not None


def _get_py_library_win(build_type, py_version, py_prefix, py_libdir,
                        py_include_dir):
    """Helper for finding the Python library on Windows"""
    if py_include_dir is None or not os.path.exists(py_include_dir):
        py_include_dir = os.path.join(py_prefix, "include")
    if py_libdir is None or not os.path.exists(py_libdir):
        # For virtual environments on Windows, the py_prefix will contain a
        # path pointing to it, instead of the system Python installation path.
        # Since INCLUDEPY contains a path to the system location, we use the
        # same base directory to define the py_libdir variable.
        py_libdir = os.path.join(os.path.dirname(py_include_dir), "libs")
        if not os.path.isdir(py_libdir):
            raise DistutilsSetupError("Failed to locate the 'libs' directory")
    dbg_postfix = "_d" if build_type == "Debug" else ""
    if OPTION["MAKESPEC"] == "mingw":
        static_lib_name = f"libpython{py_version.replace('.', '')}{dbg_postfix}.a"
        return os.path.join(py_libdir, static_lib_name)
    v = py_version.replace(".", "")
    python_lib_name = f"python{v}{dbg_postfix}.lib"
    return os.path.join(py_libdir, python_lib_name)


def _get_py_library_unix(build_type, py_version, py_prefix, py_libdir,
                         py_include_dir):
    """Helper for finding the Python library on UNIX"""
    if py_libdir is None or not os.path.exists(py_libdir):
        py_libdir = os.path.join(py_prefix, "lib")
    if py_include_dir is None or not os.path.exists(py_include_dir):
        directory = f"include/python{py_version}"
        py_include_dir = os.path.join(py_prefix, directory)
    lib_exts = ['.so']
    if sys.platform == 'darwin':
        lib_exts.append('.dylib')
    lib_suff = getattr(sys, 'abiflags', None)
    lib_exts.append('.so.1')
    # Suffix for OpenSuSE 13.01
    lib_exts.append('.so.1.0')
    # static library as last gasp
    lib_exts.append('.a')

    libs_tried = []
    for lib_ext in lib_exts:
        lib_name = f"libpython{py_version}{lib_suff}{lib_ext}"
        py_library = os.path.join(py_libdir, lib_name)
        if os.path.exists(py_library):
            return py_library
        libs_tried.append(py_library)

    # Try to find shared libraries which have a multi arch
    # suffix.
    py_multiarch = get_config_var("MULTIARCH")
    if py_multiarch:
        try_py_libdir = os.path.join(py_libdir, py_multiarch)
        libs_tried = []
        for lib_ext in lib_exts:
            lib_name = f"libpython{py_version}{lib_suff}{lib_ext}"
            py_library = os.path.join(try_py_libdir, lib_name)
            if os.path.exists(py_library):
                return py_library
            libs_tried.append(py_library)

    # PYSIDE-535: See if this is PyPy.
    if hasattr(sys, "pypy_version_info"):
        vi = sys.version_info[:2]
        version_quirk = ".".join(map(str, vi)) if vi >= (3, 9) else "3"
        pypy_libdir = os.path.join(os.path.dirname(py_libdir), "bin")
        for lib_ext in lib_exts:
            lib_name = f"libpypy{version_quirk}-c{lib_ext}"
            pypy_library = os.path.join(pypy_libdir, lib_name)
            if os.path.exists(pypy_library):
                return pypy_library
            libs_tried.append(pypy_library)
    _libs_tried = ', '.join(libs_tried)
    raise DistutilsSetupError(f"Failed to locate the Python library with {_libs_tried}")


def get_py_library(build_type, py_version, py_prefix, py_libdir, py_include_dir):
    """Find the Python library"""
    if sys.platform == "win32":
        py_library = _get_py_library_win(build_type, py_version, py_prefix,
                                         py_libdir, py_include_dir)
    else:
        py_library = _get_py_library_unix(build_type, py_version, py_prefix,
                                          py_libdir, py_include_dir)
    if py_library.endswith('.a'):
        # Python was compiled as a static library
        log.error(f"Failed to locate a dynamic Python library, using {py_library}")
    return py_library


class BuildInfoCollectorMixin(object):
    build_base: str
    build_lib: str
    cmake: str
    cmake_toolchain_file: str
    internal_cmake_install_dir_query_file_path: str
    is_cross_compile: bool
    plat_name: str
    python_target_path: str

    def __init__(self):
        pass

    def collect_and_assign(self):
        script_dir = os.getcwd()

        # build_base is not set during install command, so we default to
        # the 'build command's build_base value ourselves.
        build_base = self.build_base
        if not build_base:
            self.build_base = "build"
            build_base = self.build_base

        sources_dir = os.path.join(script_dir, "sources")

        if self.is_cross_compile:
            config_tests_dir = os.path.join(script_dir, build_base, "config.tests")
            python_target_info_dir = os.path.join(sources_dir, "shiboken6", "config.tests",
                                                  "target_python_info")
            cmake_cache_args = []

            if self.python_target_path:
                cmake_cache_args.append(("Python_ROOT_DIR", self.python_target_path))

            if self.cmake_toolchain_file:
                cmake_cache_args.append(("CMAKE_TOOLCHAIN_FILE", self.cmake_toolchain_file))
            python_target_info_output = configure_cmake_project(
                python_target_info_dir,
                self.cmake,
                temp_prefix_build_path=config_tests_dir,
                cmake_cache_args=cmake_cache_args)
            python_target_info = parse_cmake_project_message_info(python_target_info_output)
            self.python_target_info = python_target_info

        build_type = "Debug" if OPTION["DEBUG"] else "Release"
        if OPTION["RELWITHDEBINFO"]:
            build_type = 'RelWithDebInfo'

        # Prepare parameters
        if not self.is_cross_compile:
            platform_arch = platform.architecture()[0]
            self.py_arch = platform_arch[:-3]

            py_executable = sys.executable
            _major, _minor, *_ = sys.version_info
            py_version = f"{_major}.{_minor}"
            py_include_dir = get_config_var("INCLUDEPY")
            py_libdir = get_config_var("LIBDIR")
            # distutils.sysconfig.get_config_var('prefix') returned the
            # virtual environment base directory, but
            # sysconfig.get_config_var returns the system's prefix.
            # We use 'base' instead (although, platbase points to the
            # same location)
            py_prefix = get_config_var("base")
            if not py_prefix or not os.path.exists(py_prefix):
                py_prefix = sys.prefix
            self.py_prefix = py_prefix
            if sys.platform == "win32":
                py_scripts_dir = os.path.join(py_prefix, "Scripts")
            else:
                py_scripts_dir = os.path.join(py_prefix, "bin")
            self.py_scripts_dir = py_scripts_dir
        else:
            # We don't look for an interpreter when cross-compiling.
            py_executable = None

            python_info = self.python_target_info['python_info']
            py_version = python_info['version'].split('.')
            py_version = f"{py_version[0]}.{py_version[1]}"
            py_include_dir = python_info['include_dirs']
            py_libdir = python_info['library_dirs']
            py_library = python_info['libraries']
            self.py_library = py_library

            # Prefix might not be set because the project that extracts
            # the info is using internal API to get it. It shouldn't be
            # critical though, because we don't really use neither
            # py_prefix nor py_scripts_dir in important places
            # when cross-compiling.
            if 'prefix' in python_info:
                py_prefix = python_info['prefix']
                self.py_prefix = py_prefix

                py_scripts_dir = os.path.join(py_prefix, 'bin')
                if os.path.exists(py_scripts_dir):
                    self.py_scripts_dir = py_scripts_dir
                else:
                    self.py_scripts_dir = None
            else:
                py_prefix = None
                self.py_prefix = py_prefix
                self.py_scripts_dir = None

        self.qtinfo = QtInfo()
        qt_version = get_qt_version()

        # Used for test blacklists and registry test.
        if self.is_cross_compile:
            # Querying the host platform architecture makes no sense when cross-compiling.
            build_classifiers = f"py{py_version}-qt{qt_version}-{self.plat_name}-"
        else:
            build_classifiers = f"py{py_version}-qt{qt_version}-{platform.architecture()[0]}-"
            if hasattr(sys, "pypy_version_info"):
                pypy_version = ".".join(map(str, sys.pypy_version_info[:3]))
                build_classifiers += f"pypy.{pypy_version}-"
        build_classifiers += f"{build_type.lower()}"
        self.build_classifiers = build_classifiers

        venv_prefix, has_virtual_env = prefix()

        # The virtualenv name serves as the base of the build dir
        # and we consider it is distinct enough that we don't have to
        # append the build classifiers, thus keeping dir names shorter.
        build_name = f"{venv_prefix}"
        if self.is_cross_compile and has_virtual_env:
            build_name += f"-{self.plat_name}"

        # If short paths are requested and no virtual env is found, at
        # least append the python version for more uniqueness.
        if OPTION["SHORTER_PATHS"] and not has_virtual_env:
            build_name += f"-p{py_version}"
        # If no virtual env is found, use build classifiers for
        # uniqueness.
        elif not has_virtual_env:
            build_name += f"-{self.build_classifiers}"

        common_prefix_dir = os.path.join(script_dir, build_base)
        build_dir = os.path.join(common_prefix_dir, build_name, "build")
        install_dir = os.path.join(common_prefix_dir, build_name, "install")

        # Change the setuptools build_lib dir to be under the same
        # directory where the cmake build and install dirs are so
        # there's a common subdirectory for all build-related dirs.
        # Example:
        # Replaces
        #   build/lib.macosx-10.14-x86_64-3.7' with
        #   build/{venv_prefix}/package'
        setup_tools_build_lib_dir = os.path.join(common_prefix_dir, build_name, "package")
        self.build_lib = setup_tools_build_lib_dir

        self.script_dir = script_dir
        self.sources_dir = sources_dir
        self.build_dir = build_dir
        self.install_dir = install_dir
        self.py_executable = py_executable
        self.py_include_dir = py_include_dir

        if not self.is_cross_compile:
            self.py_library = get_py_library(build_type, py_version, py_prefix,
                                             py_libdir, py_include_dir)
        self.py_version = py_version
        self.build_type = build_type

        if self.is_cross_compile:
            site_packages_no_prefix = self.python_target_info['python_info']['site_packages_dir']
            self.site_packages_dir = os.path.join(install_dir, site_packages_no_prefix)
        else:
            self.site_packages_dir = sconfig.get_python_lib(1, 0, prefix=install_dir)

    def post_collect_and_assign(self):
        # self.build_lib is only available after the base class
        # finalize_options is called.
        self.st_build_dir = os.path.join(self.script_dir, self.build_lib)
