#!/usr/bin/env python

import os
import sys
import subprocess

if sys.version_info < (3,):

    def decode_utf8(x):
        return x


else:
    import codecs

    def decode_utf8(x):
        return codecs.utf_8_decode(x)[0]


# Workaround for MinGW. See:
# http://www.scons.org/wiki/LongCmdLinesOnWin32
if os.name == "nt":
    import subprocess

    def mySubProcess(cmdline, env):
        # print "SPAWNED : " + cmdline
        startupinfo = subprocess.STARTUPINFO()
        startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
        proc = subprocess.Popen(
            cmdline,
            stdin=subprocess.PIPE,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            startupinfo=startupinfo,
            shell=False,
            env=env,
        )
        data, err = proc.communicate()
        rv = proc.wait()
        if rv:
            print("=====")
            print(err.decode("utf-8"))
            print("=====")
        return rv

    def mySpawn(sh, escape, cmd, args, env):

        newargs = " ".join(args[1:])
        cmdline = cmd + " " + newargs

        rv = 0
        if len(cmdline) > 32000 and cmd.endswith("ar"):
            cmdline = cmd + " " + args[1] + " " + args[2] + " "
            for i in range(3, len(args)):
                rv = mySubProcess(cmdline + args[i], env)
                if rv:
                    break
        else:
            rv = mySubProcess(cmdline, env)

        return rv


def add_sources(sources, dir, extension):
    for f in os.listdir(dir):
        if f.endswith("." + extension):
            sources.append(dir + "/" + f)


# Try to detect the host platform automatically.
# This is used if no `platform` argument is passed
if sys.platform.startswith("linux"):
    host_platform = "linux"
elif sys.platform.startswith("freebsd"):
    host_platform = "freebsd"
elif sys.platform == "darwin":
    host_platform = "osx"
elif sys.platform == "win32" or sys.platform == "msys":
    host_platform = "windows"
else:
    raise ValueError("Could not detect platform automatically, please specify with platform=<platform>")

env = Environment(ENV=os.environ)

is64 = sys.maxsize > 2 ** 32
if (
    env["TARGET_ARCH"] == "amd64"
    or env["TARGET_ARCH"] == "emt64"
    or env["TARGET_ARCH"] == "x86_64"
    or env["TARGET_ARCH"] == "arm64-v8a"
):
    is64 = True

opts = Variables([], ARGUMENTS)
opts.Add(
    EnumVariable(
        "platform",
        "Target platform",
        host_platform,
        allowed_values=("linux", "freebsd", "osx", "windows", "android", "ios", "javascript"),
        ignorecase=2,
    )
)
opts.Add(EnumVariable("bits", "Target platform bits", "64" if is64 else "32", ("32", "64")))
opts.Add(BoolVariable("use_llvm", "Use the LLVM compiler - only effective when targeting Linux or FreeBSD", False))
opts.Add(BoolVariable("use_mingw", "Use the MinGW compiler instead of MSVC - only effective on Windows", False))
# Must be the same setting as used for cpp_bindings
opts.Add(EnumVariable("target", "Compilation target", "debug", allowed_values=("debug", "release"), ignorecase=2))
opts.Add(
    PathVariable(
        "headers_dir", "Path to the directory containing Godot headers", "godot-headers", PathVariable.PathIsDir
    )
)
opts.Add(PathVariable("custom_api_file", "Path to a custom JSON API file", None, PathVariable.PathIsFile))
opts.Add(
    EnumVariable(
        "generate_bindings",
        "Generate GDNative API bindings",
        "auto",
        allowed_values=["yes", "no", "auto", "true"],
        ignorecase=2,
    )
)
opts.Add(EnumVariable("android_arch", "Target Android architecture", "armv7", ["armv7", "arm64v8", "x86", "x86_64"]))
opts.Add("macos_deployment_target", "macOS deployment target", "default")
opts.Add("macos_sdk_path", "macOS SDK path", "")
opts.Add(EnumVariable("macos_arch", "Target macOS architecture", "universal", ["universal", "x86_64", "arm64"]))
opts.Add(EnumVariable("ios_arch", "Target iOS architecture", "arm64", ["universal", "arm64", "x86_64"]))
opts.Add(BoolVariable("ios_simulator", "Target iOS Simulator", False))
opts.Add(
    "IPHONEPATH",
    "Path to iPhone toolchain",
    "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain",
)
opts.Add(
    "android_api_level",
    "Target Android API level",
    "18" if ARGUMENTS.get("android_arch", "armv7") in ["armv7", "x86"] else "21",
)
opts.Add(
    "ANDROID_NDK_ROOT",
    "Path to your Android NDK installation. By default, uses ANDROID_NDK_ROOT from your defined environment variables.",
    os.environ.get("ANDROID_NDK_ROOT", None),
)
opts.Add(BoolVariable("generate_template_get_node", "Generate a template version of the Node class's get_node.", True))

opts.Add(BoolVariable("build_library", "Build the godot-cpp library.", True))

opts.Update(env)
Help(opts.GenerateHelpText(env))

# Detect and print a warning listing unknown SCons variables to ease troubleshooting.
unknown = opts.UnknownVariables()
if unknown:
    print("WARNING: Unknown SCons variables were passed and will be ignored:")
    for item in unknown.items():
        print("    " + item[0] + "=" + item[1])

# This makes sure to keep the session environment variables on Windows.
# This way, you can run SCons in a Visual Studio 2017 prompt and it will find
# all the required tools
if host_platform == "windows" and env["platform"] != "android":
    if env["bits"] == "64":
        env = Environment(TARGET_ARCH="amd64")
    elif env["bits"] == "32":
        env = Environment(TARGET_ARCH="x86")

    opts.Update(env)

# Require C++17
if host_platform == "windows" and env["platform"] == "windows" and not env["use_mingw"]:
    # MSVC
    env.Append(CCFLAGS=["/std:c++17"])
else:
    env.Append(CCFLAGS=["-std=c++17"])

if env["target"] == "debug":
    env.Append(CPPDEFINES=["DEBUG_ENABLED", "DEBUG_METHODS_ENABLED"])

if env["platform"] == "linux" or env["platform"] == "freebsd":
    if env["use_llvm"]:
        env["CXX"] = "clang++"

    env.Append(CCFLAGS=["-fPIC", "-Wwrite-strings"])
    env.Append(LINKFLAGS=["-Wl,-R,'$$ORIGIN'"])

    if env["target"] == "debug":
        env.Append(CCFLAGS=["-Og", "-g"])
    elif env["target"] == "release":
        env.Append(CCFLAGS=["-O3"])

    if env["bits"] == "64":
        env.Append(CCFLAGS=["-m64"])
        env.Append(LINKFLAGS=["-m64"])
    elif env["bits"] == "32":
        env.Append(CCFLAGS=["-m32"])
        env.Append(LINKFLAGS=["-m32"])

elif env["platform"] == "osx":
    # Use Clang on macOS by default
    env["CXX"] = "clang++"

    if env["bits"] == "32":
        raise ValueError("Only 64-bit builds are supported for the macOS target.")

    if env["macos_arch"] == "universal":
        env.Append(LINKFLAGS=["-arch", "x86_64", "-arch", "arm64"])
        env.Append(CCFLAGS=["-arch", "x86_64", "-arch", "arm64"])
    else:
        env.Append(LINKFLAGS=["-arch", env["macos_arch"]])
        env.Append(CCFLAGS=["-arch", env["macos_arch"]])

    if env["macos_deployment_target"] != "default":
        env.Append(CCFLAGS=["-mmacosx-version-min=" + env["macos_deployment_target"]])
        env.Append(LINKFLAGS=["-mmacosx-version-min=" + env["macos_deployment_target"]])

    if env["macos_sdk_path"]:
        env.Append(CCFLAGS=["-isysroot", env["macos_sdk_path"]])
        env.Append(LINKFLAGS=["-isysroot", env["macos_sdk_path"]])

    env.Append(
        LINKFLAGS=[
            "-framework",
            "Cocoa",
            "-Wl,-undefined,dynamic_lookup",
        ]
    )

    if env["target"] == "debug":
        env.Append(CCFLAGS=["-Og", "-g"])
    elif env["target"] == "release":
        env.Append(CCFLAGS=["-O3"])

elif env["platform"] == "ios":
    if env["ios_simulator"]:
        sdk_name = "iphonesimulator"
        env.Append(CCFLAGS=["-mios-simulator-version-min=10.0"])
    else:
        sdk_name = "iphoneos"
        env.Append(CCFLAGS=["-miphoneos-version-min=10.0"])

    try:
        sdk_path = decode_utf8(subprocess.check_output(["xcrun", "--sdk", sdk_name, "--show-sdk-path"]).strip())
    except (subprocess.CalledProcessError, OSError):
        raise ValueError("Failed to find SDK path while running xcrun --sdk {} --show-sdk-path.".format(sdk_name))

    compiler_path = env["IPHONEPATH"] + "/usr/bin/"
    env["ENV"]["PATH"] = env["IPHONEPATH"] + "/Developer/usr/bin/:" + env["ENV"]["PATH"]

    env["CC"] = compiler_path + "clang"
    env["CXX"] = compiler_path + "clang++"
    env["AR"] = compiler_path + "ar"
    env["RANLIB"] = compiler_path + "ranlib"
    env["SHLIBSUFFIX"] = ".dylib"

    if env["ios_arch"] == "universal":
        if env["ios_simulator"]:
            env.Append(LINKFLAGS=["-arch", "x86_64", "-arch", "arm64"])
            env.Append(CCFLAGS=["-arch", "x86_64", "-arch", "arm64"])
        else:
            env.Append(LINKFLAGS=["-arch", "arm64"])
            env.Append(CCFLAGS=["-arch", "arm64"])
    else:
        env.Append(LINKFLAGS=["-arch", env["ios_arch"]])
        env.Append(CCFLAGS=["-arch", env["ios_arch"]])

    env.Append(CCFLAGS=["-isysroot", sdk_path])
    env.Append(LINKFLAGS=["-isysroot", sdk_path, "-F" + sdk_path,])

    if env["target"] == "debug":
        env.Append(CCFLAGS=["-Og", "-g"])
    elif env["target"] == "release":
        env.Append(CCFLAGS=["-O3"])

elif env["platform"] == "windows":
    if host_platform == "windows" and not env["use_mingw"]:
        # MSVC
        env.Append(CPPDEFINES=["TYPED_METHOD_BIND"])
        env.Append(LINKFLAGS=["/WX"])
        if env["target"] == "debug":
            env.Append(CCFLAGS=["/Z7", "/Od", "/EHsc", "/D_DEBUG", "/MDd"])
        elif env["target"] == "release":
            env.Append(CCFLAGS=["/O2", "/EHsc", "/DNDEBUG", "/MD"])

    elif host_platform == "linux" or host_platform == "freebsd" or host_platform == "osx":
        # Cross-compilation using MinGW
        if env["bits"] == "64":
            env["CXX"] = "x86_64-w64-mingw32-g++"
            env["AR"] = "x86_64-w64-mingw32-ar"
            env["RANLIB"] = "x86_64-w64-mingw32-ranlib"
            env["LINK"] = "x86_64-w64-mingw32-g++"
        elif env["bits"] == "32":
            env["CXX"] = "i686-w64-mingw32-g++"
            env["AR"] = "i686-w64-mingw32-ar"
            env["RANLIB"] = "i686-w64-mingw32-ranlib"
            env["LINK"] = "i686-w64-mingw32-g++"

    elif host_platform == "windows" and env["use_mingw"]:
        # Don't Clone the environment. Because otherwise, SCons will pick up msvc stuff.
        env = Environment(ENV=os.environ, tools=["mingw"])
        opts.Update(env)

        # Still need to use C++17.
        env.Append(CCFLAGS=["-std=c++17"])
        # Don't want lib prefixes
        env["IMPLIBPREFIX"] = ""
        env["SHLIBPREFIX"] = ""

        # Long line hack. Use custom spawn, quick AR append (to avoid files with the same names to override each other).
        env["SPAWN"] = mySpawn
        env.Replace(ARFLAGS=["q"])

    # Native or cross-compilation using MinGW
    if host_platform == "linux" or host_platform == "freebsd" or host_platform == "osx" or env["use_mingw"]:
        # These options are for a release build even using target=debug
        env.Append(CCFLAGS=["-O3", "-Wwrite-strings"])
        env.Append(
            LINKFLAGS=[
                "--static",
                "-Wl,--no-undefined",
                "-static-libgcc",
                "-static-libstdc++",
            ]
        )

elif env["platform"] == "android":
    if host_platform == "windows":
        # Don't Clone the environment. Because otherwise, SCons will pick up msvc stuff.
        env = Environment(ENV=os.environ, tools=["mingw"])
        opts.Update(env)

        # Long line hack. Use custom spawn, quick AR append (to avoid files with the same names to override each other).
        env["SPAWN"] = mySpawn
        env.Replace(ARFLAGS=["q"])

    # Verify NDK root
    if not "ANDROID_NDK_ROOT" in env:
        raise ValueError(
            "To build for Android, ANDROID_NDK_ROOT must be defined. Please set ANDROID_NDK_ROOT to the root folder of your Android NDK installation."
        )

    # Validate API level
    api_level = int(env["android_api_level"])
    if env["android_arch"] in ["x86_64", "arm64v8"] and api_level < 21:
        print("WARN: 64-bit Android architectures require an API level of at least 21; setting android_api_level=21")
        env["android_api_level"] = "21"
        api_level = 21

    # Setup toolchain
    toolchain = env["ANDROID_NDK_ROOT"] + "/toolchains/llvm/prebuilt/"
    if host_platform == "windows":
        toolchain += "windows"
        import platform as pltfm

        if pltfm.machine().endswith("64"):
            toolchain += "-x86_64"
    elif host_platform == "linux":
        toolchain += "linux-x86_64"
    elif host_platform == "osx":
        toolchain += "darwin-x86_64"
    env.PrependENVPath("PATH", toolchain + "/bin")  # This does nothing half of the time, but we'll put it here anyways

    # Get architecture info
    arch_info_table = {
        "armv7": {
            "march": "armv7-a",
            "target": "armv7a-linux-androideabi",
            "tool_path": "arm-linux-androideabi",
            "compiler_path": "armv7a-linux-androideabi",
            "ccflags": ["-mfpu=neon"],
        },
        "arm64v8": {
            "march": "armv8-a",
            "target": "aarch64-linux-android",
            "tool_path": "aarch64-linux-android",
            "compiler_path": "aarch64-linux-android",
            "ccflags": [],
        },
        "x86": {
            "march": "i686",
            "target": "i686-linux-android",
            "tool_path": "i686-linux-android",
            "compiler_path": "i686-linux-android",
            "ccflags": ["-mstackrealign"],
        },
        "x86_64": {
            "march": "x86-64",
            "target": "x86_64-linux-android",
            "tool_path": "x86_64-linux-android",
            "compiler_path": "x86_64-linux-android",
            "ccflags": [],
        },
    }
    arch_info = arch_info_table[env["android_arch"]]

    # Setup tools
    env["CC"] = toolchain + "/bin/clang"
    env["CXX"] = toolchain + "/bin/clang++"
    env["AR"] = toolchain + "/bin/" + arch_info["tool_path"] + "-ar"
    env["SHLIBSUFFIX"] = ".so"

    env.Append(
        CCFLAGS=["--target=" + arch_info["target"] + env["android_api_level"], "-march=" + arch_info["march"], "-fPIC"]
    )  # , '-fPIE', '-fno-addrsig', '-Oz'])
    env.Append(CCFLAGS=arch_info["ccflags"])
    env.Append(LINKFLAGS=["--target=" + arch_info["target"] + env["android_api_level"], "-march=" + arch_info["march"]])

    if env["target"] == "debug":
        env.Append(CCFLAGS=["-Og", "-g"])
    elif env["target"] == "release":
        env.Append(CCFLAGS=["-O3"])

elif env["platform"] == "javascript":
    env["ENV"] = os.environ
    env["CC"] = "emcc"
    env["CXX"] = "em++"
    env["AR"] = "emar"
    env["RANLIB"] = "emranlib"
    env.Append(CPPFLAGS=["-s", "SIDE_MODULE=1"])
    env.Append(LINKFLAGS=["-s", "SIDE_MODULE=1"])
    env["SHOBJSUFFIX"] = ".bc"
    env["SHLIBSUFFIX"] = ".wasm"
    # Use TempFileMunge since some AR invocations are too long for cmd.exe.
    # Use POSIX-style paths, required with TempFileMunge.
    env["ARCOM_POSIX"] = env["ARCOM"].replace("$TARGET", "$TARGET.posix").replace("$SOURCES", "$SOURCES.posix")
    env["ARCOM"] = "${TEMPFILE(ARCOM_POSIX)}"

    # All intermediate files are just LLVM bitcode.
    env["OBJPREFIX"] = ""
    env["OBJSUFFIX"] = ".bc"
    env["PROGPREFIX"] = ""
    # Program() output consists of multiple files, so specify suffixes manually at builder.
    env["PROGSUFFIX"] = ""
    env["LIBPREFIX"] = "lib"
    env["LIBSUFFIX"] = ".a"
    env["LIBPREFIXES"] = ["$LIBPREFIX"]
    env["LIBSUFFIXES"] = ["$LIBSUFFIX"]
    env.Replace(SHLINKFLAGS="$LINKFLAGS")
    env.Replace(SHLINKFLAGS="$LINKFLAGS")

    if env["target"] == "debug":
        env.Append(CCFLAGS=["-O0", "-g"])
    elif env["target"] == "release":
        env.Append(CCFLAGS=["-O3"])

env.Append(
    CPPPATH=[
        ".",
        env["headers_dir"],
        "#include",
        "#gen/include",
    ]
)

# Generate bindings?
json_api_file = ""

if "custom_api_file" in env:
    json_api_file = env["custom_api_file"]
else:
    json_api_file = os.path.join(os.getcwd(), env["headers_dir"], "extension_api.json")

if env["generate_bindings"] == "auto":
    # Check if generated files exist
    should_generate_bindings = not os.path.isfile(os.path.join(os.getcwd(), "gen", "src", "classes", "object.cpp"))
else:
    should_generate_bindings = env["generate_bindings"] in ["yes", "true"]

if should_generate_bindings:
    # Actually create the bindings here
    import binding_generator

    binding_generator.generate_bindings(json_api_file, env["generate_template_get_node"])

# Sources to compile
sources = []
add_sources(sources, "src", "cpp")
add_sources(sources, "src/classes", "cpp")
add_sources(sources, "src/core", "cpp")
add_sources(sources, "src/variant", "cpp")
add_sources(sources, "gen/src/variant", "cpp")
add_sources(sources, "gen/src/classes", "cpp")

env["arch_suffix"] = env["bits"]
if env["platform"] == "android":
    env["arch_suffix"] = env["android_arch"]
elif env["platform"] == "ios":
    env["arch_suffix"] = env["ios_arch"]
    if env["ios_simulator"]:
        env["arch_suffix"] += ".simulator"
elif env["platform"] == "javascript":
    env["arch_suffix"] = "wasm"
elif env["platform"] == "osx":
    env["arch_suffix"] = env["macos_arch"]

library = None
env["OBJSUFFIX"] = ".{}.{}.{}{}".format(env["platform"], env["target"], env["arch_suffix"], env["OBJSUFFIX"])
library_name = "libgodot-cpp.{}.{}.{}{}".format(env["platform"], env["target"], env["arch_suffix"], env["LIBSUFFIX"])

if env["build_library"]:
    library = env.StaticLibrary(target=env.File("bin/%s" % library_name), source=sources)
    Default(library)

env.Append(CPPPATH=[env.Dir(f) for f in ["gen/include", "include", "godot-headers"]])
env.Append(LIBPATH=[env.Dir("bin")])
env.Append(LIBS=library_name)
Return("env")