#############################################################################
##
## Copyright (C) 2019 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$
##
#############################################################################

"""
This script is used by Coin (coin_test_instructions.py specifically) to
test installation of generated wheels, and test building of the
"buildable" examples samplebinding and scriptableapplication.

It can also be invoked regularly from the command line via
python testing/wheel_tester.py --qmake=some-value --cmake=some-value

The qmake and cmake arguments can also be omitted, and they will be
looked up in your PATH.

Make sure that some generated wheels already exist in the dist/
directory (e.g. setup.py bdist_wheel was already executed).
"""

from argparse import ArgumentParser, RawTextHelpFormatter
from pathlib import Path
import os
import sys
import tempfile

try:
    this_file = __file__
except NameError:
    this_file = sys.argv[0]
this_file = os.path.abspath(this_file)
this_dir = os.path.dirname(this_file)
setup_script_dir = os.path.abspath(os.path.join(this_dir, '..'))
sys.path.append(setup_script_dir)

from build_scripts.utils import find_files_using_glob
from build_scripts.utils import find_glob_in_path
from build_scripts.utils import run_process, run_process_output
from build_scripts.utils import rmtree
import distutils.log as log
import platform

log.set_verbosity(1)


def find_executable(executable, command_line_value):
    value = command_line_value
    option_str = '--{}'.format(executable)

    if value:
        log.info("{} option given: {}".format(option_str, value))
        if not os.path.exists(value):
            raise RuntimeError("No executable exists at: {}".format(value))
    else:
        log.info("No {} option given, trying to find {} in PATH.".format(option_str, executable))
        paths = find_glob_in_path(executable)
        log.info("{} executables found in PATH: {}".format(executable, paths))
        if not paths:
            raise RuntimeError(
                "No {} option was specified and no {} was found "
                "in PATH.".format(option_str, executable))
        else:
            value = paths[0]
            log.info("Using {} found in PATH: {}".format(executable, value))
    log.info("")
    return value


QMAKE_PATH = None
CMAKE_PATH = None


def get_wheels_dir():
    return os.path.join(setup_script_dir, "dist")


def get_examples_dir():
    return os.path.join(setup_script_dir, "examples")


def package_prefix_names():
    # Note: shiboken6_generator is not needed for compile_using_nuitka,
    # but building modules with cmake needs it.
    return ["shiboken6", "shiboken6_generator", "PySide6"]


def clean_egg_info():
    # After a successful bdist_wheel build, some .egg-info directories
    # are left over, which confuse pip when invoking it via
    # python -m pip, making pip think that the packages are already
    # installed in the root source directory.
    # Clean up the .egg-info directories to fix this, it should be
    # safe to do so.
    paths = find_files_using_glob(setup_script_dir, "*.egg-info")
    for p in paths:
        log.info("Removing {}".format(p))
        rmtree(p)


def install_wheel(wheel_path):
    log.info("Installing wheel: {}".format(wheel_path))
    exit_code = run_process([sys.executable, "-m", "pip", "install", wheel_path])
    log.info("")
    if exit_code:
        raise RuntimeError("Error while installing wheel {}".format(wheel_path))


def try_install_wheels(wheels_dir, py_version):
    clean_egg_info()
    all_wheels_pattern = "*.whl"
    all_wheels = find_files_using_glob(wheels_dir, all_wheels_pattern)

    if len(all_wheels) > 1:
        log.info("Found the following wheels in {}: ".format(wheels_dir))
        for wheel in all_wheels:
            log.info(wheel)
    else:
        log.info("No wheels found in {}".format(wheels_dir))
    log.info("")

    for p in package_prefix_names():
        log.info(f"Trying to install {p}:")
        pattern = "{}-*cp{}*.whl".format(p, int(float(py_version)))
        files = find_files_using_glob(wheels_dir, pattern)
        if files and len(files) == 1:
            wheel_path = files[0]
            install_wheel(wheel_path)
        elif len(files) > 1:
            raise RuntimeError(f"More than one wheel found for specific {p} version.")
        else:
            raise RuntimeError(f"No {p} wheels compatible with Python {py_version} found "
                               f"for testing.")


def is_unix():
    if sys.platform.startswith("linux") or sys.platform == "darwin":
        return True
    return False


def generate_build_cmake():
    args = [CMAKE_PATH]
    if is_unix():
        args.extend(["-G", "Unix Makefiles"])
    else:
        args.extend(["-G", "NMake Makefiles"])
    args.append("-DCMAKE_BUILD_TYPE=Release")
    args.append("-Dpython_interpreter={}".format(sys.executable))

    # Specify prefix path so find_package(Qt5) works.
    qmake_dir = os.path.abspath(os.path.join(os.path.dirname(QMAKE_PATH), ".."))
    args.append("-DCMAKE_PREFIX_PATH={}".format(qmake_dir))

    args.append("..")

    exit_code = run_process(args)
    if exit_code:
        raise RuntimeError("Failure while running cmake.")
    log.info("")


def generate_build_qmake():
    exit_code = run_process([QMAKE_PATH, "..", "python_interpreter={}".format(sys.executable)])
    if exit_code:
        raise RuntimeError("Failure while running qmake.")
    log.info("")


def raise_error_pyinstaller(msg):
    print()
    print(f"PYINST: {msg}")
    print(f"PYINST:   sys.version         = {sys.version.splitlines()[0]}")
    print(f"PYINST:   platform.platform() = {platform.platform()}")
    print("PYINST: See the error message above.")
    print()
    for line in run_process_output([sys.executable, "-m", "pip", "list"]):
        print(f"PyInstaller pip list:   {line}")
    print()
    raise(RuntimeError(msg))


def compile_using_pyinstaller():
    _ = os.path.join("..", "hello.py")
    spec_path = os.path.join("..", "hello_app.spec")
    exit_code = run_process([sys.executable, "-m", "PyInstaller", spec_path])
    # to create the spec file, this setting was used:
    # "--name=hello_app", "--console", "--log-level=DEBUG", src_path])
    # By using a spec file, we avoid all the probing that might disturb certain
    # platforms and also save some analysis time.
    if exit_code:
        # 2019-04-28 Raising on error is again enabled
        raise_error_pyinstaller("Failure while compiling script using PyInstaller.")
    log.info("")


def test_nuitka(example):
    testprog = "Nuitka"
    name = os.path.splitext(os.path.basename(example))[0]
    print(f"Running {testprog} test of {name}")
    current_dir = os.getcwd()
    result = False
    tmpdirname = tempfile.mkdtemp()
    try:
        os.chdir(tmpdirname)
        cmd = [sys.executable, "-m", "nuitka", "--run", example]#, "--standalone"]
        exit_code = run_process(cmd)
        result = True
    except RuntimeError as e:
        print(str(e))
    finally:
        os.chdir(current_dir)
    print(f"Executable is in {tmpdirname}")
    return result


def run_nuitka_test(example):
    if test_nuitka(example):
        log.info("")
    else:
        raise RuntimeError(f"Failure running {example} with Nuitka.")


def run_make():
    args = []
    if is_unix():
        executable = "make"
    else:
        executable = "nmake"
    args.append(executable)

    exit_code = run_process(args)
    if exit_code:
        raise RuntimeError("Failure while running {}.".format(executable))
    log.info("")


def run_make_install():
    args = []
    if is_unix():
        executable = "make"
    else:
        executable = "nmake"
    args.append(executable)
    args.append("install")

    exit_code = run_process(args)
    if exit_code:
        raise RuntimeError("Failed while running {} install.".format(executable))
    log.info("")


def run_compiled_script(binary_path):
    args = [binary_path]
    exit_code = run_process(args)
    if exit_code:
        raise_error_pyinstaller("Failure while executing compiled script: {}".format(binary_path))
    log.info("")


def execute_script(script_path):
    args = [sys.executable, script_path]
    exit_code = run_process(args)
    if exit_code:
        raise RuntimeError("Failure while executing script: {}".format(script_path))
    log.info("")


def prepare_build_folder(src_path, build_folder_name):
    build_path = os.path.join(src_path, build_folder_name)

    # The script can be called for Python 3 wheels, so
    # preparing a build folder should clean any previous existing build.
    if os.path.exists(build_path):
        log.info("Removing {}".format(build_path))
        rmtree(build_path)

    log.info("Creating {}".format(build_path))
    os.makedirs(build_path)
    os.chdir(build_path)


def try_build_examples():
    examples_dir = get_examples_dir()


    # Disabled PyInstaller until it supports PySide 6
    if False:
        # But because it is most likely to break, we put it here for now.
        log.info("Attempting to build hello.py using PyInstaller.")
        # PyInstaller is loaded by coin_build_instructions.py, but not when
        # testing directly this script.
        src_path = os.path.join(examples_dir, "installer_test")
        prepare_build_folder(src_path, "pyinstaller")
        compile_using_pyinstaller()
        run_compiled_script(os.path.join(src_path,
                            "pyinstaller", "dist", "hello_app", "hello_app"))

    src_path = Path(examples_dir) / "installer_test"
    log.info("Attempting to build hello.py using Nuitka.")
    # Nuitka is loaded by coin_build_instructions.py, but not when
    # testing directly this script.
    run_nuitka_test(os.fspath(src_path / "hello.py"))

    log.info("Attempting to build and run samplebinding using cmake.")
    src_path = os.path.join(examples_dir, "samplebinding")
    prepare_build_folder(src_path, "cmake")
    generate_build_cmake()
    run_make()
    run_make_install()
    execute_script(os.path.join(src_path, "main.py"))

    log.info("*** Defunct: build scriptableapplication using cmake.")
    # log.info("Attempting to build scriptableapplication using cmake.")
    # src_path = os.path.join(examples_dir, "scriptableapplication")
    # prepare_build_folder(src_path, "cmake")
    # generate_build_cmake()
    # run_make()

    log.info("Attempting to build scriptableapplication using qmake.")
    src_path = os.path.join(examples_dir, "scriptableapplication")
    prepare_build_folder(src_path, "qmake")
    generate_build_qmake()
    run_make()


def run_wheel_tests(install_wheels):
    wheels_dir = get_wheels_dir()
    py_version = "{v.major}.{v.minor}".format(v=sys.version_info)

    if install_wheels:
        log.info("Attempting to install wheels.\n")
        try_install_wheels(wheels_dir, py_version)

    log.info("Attempting to build examples.\n")
    try_build_examples()

    log.info("All tests passed!")


if __name__ == "__main__":
    parser = ArgumentParser(description="wheel_tester",
                            formatter_class=RawTextHelpFormatter)
    parser.add_argument('--no-install-wheels', '-n', action='store_true',
                        help='Do not install wheels'
                             ' (for developer builds with virtualenv)')
    parser.add_argument("--qmake", type=str,
                        help="Path to qmake")
    parser.add_argument("--cmake", type=str,
                        help="Path to cmake")
    options = parser.parse_args()
    QMAKE_PATH = find_executable('qmake', options.qmake)
    CMAKE_PATH = find_executable('cmake', options.cmake)

    run_wheel_tests(not options.no_install_wheels)
