# -*- python -*-
#     Copyright 2023, Kay Hayen, mailto:kay.hayen@gmail.com
#
#     Part of "Nuitka", an optimizing Python compiler that is compatible and
#     integrates with CPython, but also works on its own.
#
#     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.
#

"""
The Onefile scons file for the bootstrap binary, check out Backend.scons for
where the actual compiled binaries are build. If you have Scons or Windows
platform knowledge, please be especially invited and contribute improvements.

This file is used to build a bootstrap binary that only unpacks the distribution
contents to have a launch it.
"""

# Make nuitka package importable from calling installation

import sys
import os
import types

sys.modules["nuitka"] = types.ModuleType("nuitka")
sys.modules["nuitka"].__path__ = [os.environ["NUITKA_PACKAGE_DIR"]]

# We are in the build.build package really.
import nuitka.build  # pylint: disable=unused-import

__package__ = "nuitka.build"  # pylint: disable=redefined-builtin

# isort:start

from SCons.Script import (  # pylint: disable=I0021,import-error
    ARGUMENTS,
    Environment,
    GetOption,
)

from nuitka.Tracing import (
    my_print,
    scons_details_logger,
    scons_logger,
    setQuiet,
)
from nuitka.utils.FileOperations import getExternalUsePath
from nuitka.utils.Utils import isMacOS

from .SconsCaching import enableCcache, enableClcache
from .SconsCompilerSettings import (
    addConstantBlobFile,
    checkWindowsCompilerFound,
    importEnvironmentVariableSettings,
    reportCCompiler,
    setupCCompiler,
    switchFromGccToGpp,
)
from .SconsHacks import getEnhancedToolDetect, makeGccUseLinkerFile
from .SconsProgress import enableSconsProgressBar, setSconsProgressBarTotal
from .SconsSpawn import enableSpawnMonitoring
from .SconsUtils import (
    addClangClPathFromMSVC,
    changeKeyboardInterruptToErrorExit,
    createDefinitionsFile,
    createEnvironment,
    getArgumentBool,
    getArgumentDefaulted,
    getArgumentList,
    getArgumentRequired,
    getExecutablePath,
    getMsvcVersionString,
    initScons,
    isClangName,
    isGccName,
    prepareEnvironment,
    provideStaticSourceFile,
    raiseNoCompilerFoundErrorExit,
    scanSourceDir,
    setArguments,
    setupScons,
    writeSconsReport,
)

# spell-checker: ignore ccversion,ccflags,werror,cppdefines,cpppath,cppflags
# spell-checker: ignore cxxflags,ldflags,libpath,linkflags

# Set the arguments.
setArguments(ARGUMENTS)

# Set up the basic stuff.
initScons()

# The directory containing the C files generated by Nuitka to be built using
# scons. They are referred to as sources from here on.
source_dir = getArgumentRequired("source_dir")

# The directory containing Nuitka provided C files to be built and where it
# should be used.
nuitka_src = getArgumentRequired("nuitka_src")
static_src = os.path.join(source_dir, "static")

# The name of executable that we produce
result_exe = getArgumentDefaulted("result_exe", None)

# The suffix for an extension module (in module mode).
module_suffix = getArgumentDefaulted("module_suffix", None)

# Full names shall be used, no remapping for cacheable filenames.
full_names = getArgumentBool("full_names", False)

# Module mode: Create a Python extension module, create an executable otherwise.
module_mode = getArgumentBool("module_mode", False)

# Debug mode: Less optimizations, debug information in the resulting binary.
debug_mode = getArgumentBool("debug_mode", False)

# Deployment mode
deployment_mode = getArgumentBool("deployment", False)

# Experimental indications. Do things that are not yet safe to do.
no_deployment = getArgumentList("no_deployment", "")

# Experimental indications. Do things that are not yet safe to do.
experimental = getArgumentList("experimental", "")

# Tracing mode. Output program progress.
trace_mode = getArgumentBool("trace_mode", False)

# LTO mode: Use link time optimizations of C compiler if available and known
# good with the compiler in question.
lto_mode = getArgumentDefaulted("lto_mode", "auto")

# Windows subsystem mode: Disable console for windows builds.
disable_console = getArgumentBool("disable_console", False)

# Unstripped mode: Do not remove debug symbols.
unstripped_mode = getArgumentBool("unstripped_mode", False)

# Target arch, uses for compiler choice and quick linking of constants binary
# data.
target_arch = ARGUMENTS["target_arch"]

# MinGW compiler mode, optional and interesting to Windows only.
mingw_mode = getArgumentBool("mingw_mode", False)

# Clang compiler mode, forced on macOS and FreeBSD (excluding PowerPC), optional on Linux.
clang_mode = getArgumentBool("clang_mode", False)

# Clang on Windows with no requirement to use MinGW64 or using MSYS2 MinGW flavor,
# is changed to ClangCL from Visual Studio.
clangcl_mode = False
if clang_mode and os.name == "nt" and not mingw_mode:
    clang_mode = False
    clangcl_mode = True

# Show scons mode, output information about Scons operation
show_scons_mode = getArgumentBool("show_scons", False)
scons_details_logger.is_quiet = not show_scons_mode

if int(os.getenv("NUITKA_QUIET", "0")):
    setQuiet()

# Home of Python to be compiled against, used to find tools like clcache and
# ccache, no linking against Python is needed here.
python_prefix = getArgumentRequired("python_prefix")

python_prefix_external = getExternalUsePath(python_prefix)

# Forced MSVC version (windows-only)
msvc_version = getArgumentDefaulted("msvc_version", None)

# Disable ccache/clcache usage if that is requested
disable_ccache = getArgumentBool("disable_ccache", False)

# Preprocessor defines and link libraries from plugins
cpp_defines = getArgumentList("cpp_defines", "")
cpp_include_dirs = getArgumentList("cpp_include_dirs", "")
link_dirs = getArgumentList("link_dirs", "")
link_libraries = getArgumentList("link_libraries", "")

# Allow automatic downloads for ccache, etc.
assume_yes_for_downloads = getArgumentBool("assume_yes_for_downloads", False)

# Onefile bootstrap is optional.
onefile_splash_screen = getArgumentBool("onefile_splash_screen", False)

# Minimum version required on macOS.
macos_min_version = getArgumentDefaulted("macos_min_version", "")

# Target arch for macOS.
macos_target_arch = getArgumentDefaulted("macos_target_arch", "")

# gcc compiler cf_protection option
cf_protection = getArgumentDefaulted("cf_protection", "auto")

if getArgumentBool("progress_bar", True) and not show_scons_mode:
    enableSconsProgressBar()

# Amount of jobs to use.
job_count = GetOption("num_jobs")

# Prepare environment for compiler detection.
mingw_mode = prepareEnvironment(mingw_mode=mingw_mode)

# TODO: Merge to prepareEnvironment as well.
if "CXX" in os.environ:
    os.environ["CXX"] = os.path.normpath(os.environ["CXX"])

    if os.path.isdir(os.environ["CXX"]):
        sys.exit("Error, the CXX variable must point to file, not directory.")

    cxx_dirname = os.path.dirname(os.environ["CXX"])

    if os.name == "nt" and isGccName(os.path.basename(os.environ["CXX"])):
        if show_scons_mode:
            my_print("Scons: Environment CXX seems to be a gcc, enable mingw_mode.")
        mingw_mode = True

    if os.path.isdir(cxx_dirname):
        os.environ["PATH"] = os.pathsep.join(
            [cxx_dirname] + os.environ["PATH"].split(os.pathsep)
        )

# Patch the compiler detection.
Environment.Detect = getEnhancedToolDetect()

# Create Scons environment, the main control tool. Don't include "mingw" on
# Windows immediately, we will default to MSVC if available.
env = createEnvironment(
    mingw_mode=mingw_mode,
    msvc_version=msvc_version,
    target_arch=target_arch,
    experimental=experimental,
    no_deployment=no_deployment,
)

scons_details_logger.info("Initial CC: %r" % env.get("CC"))
scons_details_logger.info(
    "Initial CCVERSION: %r" % (env.get("CCVERSION"),),
)

if "CC" in os.environ:
    # If the environment variable CC is set, use that.
    env["CCVERSION"] = None

    scons_details_logger.info("Overridden with environment CC: %r" % env["CC"])
elif clangcl_mode:
    # If possible, add Clang directory from MSVC if available.
    addClangClPathFromMSVC(env=env)
elif clang_mode and not mingw_mode:
    # If requested by the user, use the clang compiler, overriding what was
    # said in environment.

    env["CC"] = "clang"
    env["CCVERSION"] = None

# On Windows, in case MSVC was not found and not previously forced, use the
# winlibs MinGW64 as a download, and use it as a fallback.
env = checkWindowsCompilerFound(
    env=env,
    target_arch=target_arch,
    clang_mode=clang_mode,
    msvc_version=msvc_version,
    assume_yes_for_downloads=assume_yes_for_downloads,
)

env.the_compiler = env["CC"] or env["CXX"]
env.the_cc_name = os.path.normcase(os.path.basename(env.the_compiler))
env.standalone_mode = True  # We are only used in this case.
env.debug_mode = debug_mode
env.unstripped_mode = unstripped_mode
env.disable_console = disable_console
env.source_dir = source_dir
env.nuitka_src = nuitka_src
env.low_memory = False  # Never a concern in this case.
env.macos_min_version = macos_min_version
env.macos_target_arch = macos_target_arch

# Requested or user provided, detect if it's clang even from environment
if isClangName(env.the_cc_name):
    clang_mode = True
    env["CCVERSION"] = None

# We consider clang to be a form of gcc for the most things, they strive to
# be compatible.
env.gcc_mode = isGccName(env.the_cc_name) or clang_mode
env.clang_mode = clang_mode

# Only use MSVC if not already clear, we are using MinGW.
env.msvc_mode = os.name == "nt" and not env.gcc_mode
env.mingw_mode = os.name == "nt" and env.gcc_mode
env.clangcl_mode = clangcl_mode

# gcc compiler cf_protection option
env.cf_protection = cf_protection

# Consider switching from gcc to its g++ compiler as a workaround that makes us work without C11.
switchFromGccToGpp(
    env=env,
)

if env.the_compiler is None or getExecutablePath(env.the_compiler, env=env) is None:
    raiseNoCompilerFoundErrorExit()

if show_scons_mode:
    my_print("Scons: Compiler used", end=" ")
    my_print(getExecutablePath(env.the_compiler, env=env), end=" ")

    if os.name == "nt" and env.msvc_mode:
        my_print("(MSVC %s)" % getMsvcVersionString(env))

    my_print()

# Set build directory and scons general settings.
setupScons(env, source_dir)

# Report the C compiler used.
reportCCompiler(env, "Onefile", output_func=scons_logger.info)


# Set up C compiler settings.
setupCCompiler(
    env=env,
    lto_mode=lto_mode,
    pgo_mode="no",  # TODO: Have this here too, decompression might benefit.
    job_count=job_count,
    onefile_compile=True,
)

# Avoid them as appearing to be different files. TODO: Find out which
# clang version has this, clang-8 does not.
if env.gcc_mode and not env.clang_mode and env.gcc_version >= (8,):
    if source_dir != "." and not full_names:
        # TODO: This also lies for modules codes.
        env.Append(
            CCFLAGS=[
                "--file-prefix-map=%s=%s" % (os.path.normpath(source_dir), "."),
                "--file-prefix-map=%s=%s"
                % (
                    os.path.normpath(os.path.join(source_dir, "static_src")),
                    os.path.normpath(os.path.join(nuitka_src, "static_src")),
                ),
            ]
        )

if env.msvc_mode:
    # With Clang on Windows, there is also an linker to use.
    env.Append(
        CCFLAGS=[
            "/EHsc",  # No C++ exception handling code.
            "/J",  # default char type is unsigned.
            "/Gd",  # Use C calling convention by default.
            "/bigobj",  # Product object files with larger internal limits.
        ]
    )

    # No incremental linking.
    env.Append(LINKFLAGS=["/INCREMENTAL:NO"])

if debug_mode:
    if env.gcc_mode:
        # Allow gcc/clang to point out all kinds of inconsistency to us by
        # raising an error.
        env.Append(
            CCFLAGS=[
                "-Wall",
                "-Werror",
            ]
        )

    elif env.msvc_mode:
        # Disable warnings that system headers already show.
        env.Append(
            CCFLAGS=[
                "/W4",
                "/wd4505",
                "/wd4127",
                "/wd4100",
                "/wd4702",
                "/wd4189",
                "/wd4211",
                "/WX",
            ]
        )

        # Disable warnings, the first 3 are for Python headers, maybe not needed.
        env.Append(CCFLAGS=["/wd4512", "/wd4510", "/wd4610", "/wd4996"])

if trace_mode:
    env.Append(CPPDEFINES=["_NUITKA_TRACE"])

if deployment_mode:
    env.Append(CPPDEFINES=["_NUITKA_DEPLOYMENT_MODE"])

env.Append(CPPDEFINES=["_NUITKA_ONEFILE_MODE"])

# The static include files reside in Nuitka installation, which may be where
# the "nuitka.build" package lives.
nuitka_include = os.path.join(nuitka_src, "include")

if not os.path.exists(os.path.join(nuitka_include, "nuitka", "prelude.h")):
    sys.exit(
        "Error, cannot locate Nuitka includes at '%s', broken installation."
        % nuitka_include
    )

# We have include files in the build directory and the static include directory
# that is located inside Nuitka installation, as well as an inline copy of zstd
# that should be found.
env.Append(
    CPPPATH=[
        source_dir,
        nuitka_include,
        os.path.join(env.nuitka_src, "static_src"),
        os.path.join(env.nuitka_src, "inline_copy", "zstd"),
    ],
)

# Tell compiler to create a program.
if env.msvc_mode:
    env.Append(CCFLAGS=["/MT"])  # Multithreaded, static version of C run time.

if isMacOS():
    addConstantBlobFile(
        env=env,
        blob_filename=os.path.join(source_dir, "__payload.bin"),
        resource_desc=("mac_section", "default on macOS"),
        target_arch=target_arch,
    )


def discoverSourceFiles():
    result = []

    # Scan for Nuitka created source files, and add them too.
    result.extend(scanSourceDir(env=env, dirname=source_dir, plugins=False))
    result.extend(
        scanSourceDir(
            env=env,
            dirname=os.path.join(source_dir, "plugins"),
            plugins=True,
        )
    )

    # Main onefile bootstrap program
    result.append(
        provideStaticSourceFile(
            env=env,
            sub_path="OnefileBootstrap.c",
            c11_mode=env.c11_mode,
        )
    )

    if onefile_splash_screen:
        result.append(
            provideStaticSourceFile(
                env=env,
                sub_path="OnefileSplashScreen.cpp",
                c11_mode=False,
            )
        )

    return result


source_files = discoverSourceFiles()

target = env.Program(result_exe, source_files)

# Use compiler/linker flags provided via environment variables
importEnvironmentVariableSettings(env)

# Remove the target file to avoid cases where it falsely doesn't get rebuild
# and then lingers from previous builds,
if os.path.exists(target[0].abspath):
    os.unlink(target[0].abspath)

if job_count:
    scons_details_logger.info("Told to run compilation on %d CPUs." % job_count)

# Plugin contributed C defines should be used too.
env.Append(CPPDEFINES=cpp_defines)
# Plugin contributed C include directories should be used too.
env.Append(CPPPATH=cpp_include_dirs)
# Plugin contributed link dirs should be used too.
env.Append(LIBPATH=link_dirs)
# Plugin contributed link libraries should be used too.
env.Append(LIBS=link_libraries)

# Work around windows bugs and use watchdogs to track progress of compilation.
enableSpawnMonitoring(
    env=env,
    module_mode=module_mode,
    source_files=source_files,
)

# Before we go, also lets turn KeyboardInterrupt into a mere error exit as the
# scons traceback is not going to be very interesting to us.
changeKeyboardInterruptToErrorExit()

# Check if ccache is installed, and complain if it is not.
if env.gcc_mode and not disable_ccache:
    enableCcache(
        env=env,
        source_dir=source_dir,
        python_prefix=python_prefix_external,
        assume_yes_for_downloads=assume_yes_for_downloads,
    )

if env.msvc_mode and not disable_ccache:
    enableClcache(
        env=env,
        source_dir=source_dir,
    )


def createBuildDefinitionsFile():
    onefile_definitions = {}

    # TODO: Do this via a plugin too, so it can provide these externally.
    if onefile_splash_screen:
        onefile_definitions["_NUITKA_ONEFILE_SPLASH_SCREEN"] = 1

        # spell-checker: ignore Windowscodecs,Shlwapi
        env.Append(LIBS=["Ole32", "Windowscodecs", "User32", "Gdi32", "Shlwapi"])

    createDefinitionsFile(source_dir, "onefile_definitions.h", onefile_definitions)


createBuildDefinitionsFile()

# Hack to make Scons use tempfile for gcc linking, to avoid line length limits,
# which can make linking fail with many modules otherwise. Most needed on Windows,
# but useful on other platforms too.
if env.gcc_mode:
    makeGccUseLinkerFile(
        source_files=source_files,
        module_mode=module_mode,
        env=env,
    )

writeSconsReport(
    env=env,
    source_dir=source_dir,
)

setSconsProgressBarTotal(name="Onefile", total=len(source_files))

scons_details_logger.info("Launching Scons target: %s" % target)
env.Default(target)
