# Copyright (c) 2017 CEF Python, see the Authors file.
# All rights reserved. Licensed under BSD 3-clause license.
# Project website: https://github.com/cztomczak/cefpython

"""Called by build.py internally. Builds C++ projects using
distutils/setuptools compilers. This tool is executed by build.py
on Windows only currently. Output directories are in
build/build_cefpython/.

Usage:
    build_cpp_projects.py [--force]

TODO: Linux/Mac support, see makefiles, add include dirs using
      compiler.add_include_dir, compiler/linker flags,
      refactor macros.
"""

# import setuptools so that distutils msvc compiler is patched
# noinspection PyUnresolvedReferences
import setuptools
from distutils.ccompiler import new_compiler
from common import *
import shutil

# Macros
MACROS = [
    "WIN32", "_WIN32", "_WINDOWS",
    ("WINVER", "0x0601"),  ("_WIN32_WINNT", "0x0601"),  # Windows 7+
    "NDEBUG", "_NDEBUG",
    "_CRT_SECURE_NO_WARNINGS",
]
cefpython_app_MACROS = MACROS + [
    "BROWSER_PROCESS",
]
subprocess_MACROS = MACROS + [
    "RENDERER_PROCESS",
    "UNICODE", "_UNICODE",
    "NOMINMAX",
    "WIN32_LEAN_AND_MEAN",
    ("_HAS_EXCEPTIONS", "0"),
]

# Compiler args
COMPILER_ARGS = [
    "/EHsc",
]
subprocess_COMPILER_ARGS = [
    "/MT",
]

# Linker args
subprocess_LINKER_ARGS = [
    "/MANIFEST:NO",
    "/LARGEADDRESSAWARE",
]

# Command line args
FORCE_FLAG = False


def main():
    command_line_args()
    clean_build_directories_if_forced()
    build_cefpython_app_library()
    build_library(lib_name="client_handler",
                  macros=MACROS,
                  sources_dir=CLIENT_HANDLER_DIR,
                  output_dir=BUILD_CLIENT_HANDLER)
    build_library(lib_name="cpp_utils",
                  macros=MACROS,
                  sources_dir=CPP_UTILS_DIR,
                  output_dir=BUILD_CPP_UTILS)
    build_subprocess_executable()
    print("[build_cpp_projects.py] Done building C++ projects")


def command_line_args():
    global FORCE_FLAG
    if "--force" in sys.argv:
        FORCE_FLAG = True


def clean_build_directories_if_forced():
    build_dirs = [BUILD_CEFPYTHON_APP, BUILD_CLIENT_HANDLER, BUILD_CPP_UTILS,
                  BUILD_SUBPROCESS]
    if FORCE_FLAG:
        print("[build_cpp_projects.py] Clean C++ projects build directories")
        for bdir in build_dirs:
            if os.path.isdir(bdir):
                shutil.rmtree(bdir)


def get_compiler(static=False):
    # NOTES:
    # - VS2008 and VS2010 are both using distutils/msvc9compiler.py
    compiler = new_compiler()
    # Must initialize so that "compile_options" and others are available
    compiler.initialize()
    if static:
        compiler.compile_options.remove("/MD")
        # Overwrite function that adds /MANIFESTFILE, as for subprocess
        # manifest is disabled. Otherwise warning LNK4075 is generated.
        if hasattr(compiler, "manifest_setup_ldargs"):
            compiler.manifest_setup_ldargs = lambda *_: None
    return compiler


def build_library(lib_name, macros, output_dir,
                  sources=None, sources_dir=None):
    assert bool(sources_dir) ^ bool(sources)  # xor
    print("[build_cpp_projects.py] Build library: {lib_name}"
          .format(lib_name=lib_name))
    compiler = get_compiler()
    if not os.path.exists(output_dir):
        os.makedirs(output_dir)
    if sources_dir:
        assert not sources
        sources = get_sources(sources_dir)
    (changed, objects) = smart_compile(compiler,
                                       macros=macros,
                                       extra_args=COMPILER_ARGS,
                                       sources=sources,
                                       output_dir=output_dir)
    lib_path = os.path.join(output_dir, lib_name + LIB_EXT)
    if changed or not os.path.exists(lib_path):
        compiler.create_static_lib(objects, lib_name,
                                   output_dir=output_dir)
        print("[build_cpp_projects.py] Created library: {lib_name}"
              .format(lib_name=lib_name))
    else:
        print("[build_cpp_projects.py] Library is up-to-date: {lib_name}"
              .format(lib_name=lib_name))


def build_cefpython_app_library():
    sources = get_sources(SUBPROCESS_DIR, exclude_names=["main.cpp"])
    main_message_loop_dir = os.path.join(SUBPROCESS_DIR, "main_message_loop")
    sources.extend(get_sources(main_message_loop_dir))
    build_library(lib_name="cefpython_app",
                  macros=cefpython_app_MACROS,
                  sources=sources,
                  output_dir=BUILD_CEFPYTHON_APP)


def build_subprocess_executable():
    print("[buil_cpp_projects.py] Build executable: subprocess")
    compiler = get_compiler(static=True)
    sources = get_sources(SUBPROCESS_DIR,
                          exclude_names=["print_handler_gtk.cpp"])
    (changed, objects) = smart_compile(compiler,
                                       macros=subprocess_MACROS,
                                       extra_args=subprocess_COMPILER_ARGS,
                                       sources=sources,
                                       output_dir=BUILD_SUBPROCESS)
    executable_path = os.path.join(BUILD_SUBPROCESS,
                                   "subprocess" + EXECUTABLE_EXT)
    if changed or not os.path.exists(executable_path):
        lib_dir = os.path.join(CEF_BINARIES_LIBRARIES, "lib")
        lib_dir_vs = os.path.join(lib_dir,
                                  get_msvs_for_python(vs_prefix=True))
        compiler.link_executable(objects,
                                 output_progname="subprocess",
                                 output_dir=BUILD_SUBPROCESS,
                                 libraries=["libcef",
                                            "libcef_dll_wrapper_MT"],
                                 library_dirs=[lib_dir, lib_dir_vs],
                                 # TODO linker flags for Linux/Mac
                                 extra_preargs=None,
                                 extra_postargs=subprocess_LINKER_ARGS)
    else:
        print("[build_cpp_projects.py] Executable is up-to-date: subprocess")


def get_sources(sources_dir, exclude_names=None):
    if not exclude_names:
        exclude_names = list()
    sources = glob.glob(os.path.join(sources_dir, "*.cpp"))
    if MAC:
        sources.extend(glob.glob(os.path.join(sources_dir, "*.mm")))
    ret = list()
    for source_file in sources:
        filename = os.path.basename(source_file)
        if "_win.cpp" in filename and not WINDOWS:
            continue
        if "_linux.cpp" in filename and not LINUX:
            continue
        if "x11" in filename and not LINUX:
            continue
        if "gtk" in filename and not LINUX:
            continue
        if "_mac.cpp" in filename and not MAC:
            continue
        exclude = False
        for name in exclude_names:
            if name in filename:
                exclude = True
                break
        if not exclude:
            ret.append(source_file)
    return ret


def smart_compile(compiler, macros, extra_args, sources, output_dir):
    """Smart compile will only recompile files that need recompiling."""
    if not os.path.exists(output_dir):
        os.makedirs(output_dir)
    any_changed = False
    objects = list()
    for source_file in sources:
        header_file = source_file.replace(".cpp", ".h")
        header_file = header_file.replace(".mm", ".h")
        assert header_file.endswith(".h")
        if not os.path.isfile(header_file):
            header_file = None
        obj_file = os.path.join(output_dir, os.path.basename(source_file))
        obj_file = obj_file.replace(".cpp", OBJ_EXT)
        obj_file = obj_file.replace(".mm", OBJ_EXT)
        assert obj_file.endswith(OBJ_EXT)
        if os.path.exists(obj_file):
            # Recompile source file if its time is newer than obj file,
            # Also check its header file time. Also check times of any
            # possible includes: cefpython_fixed.h, src/common/ files.
            obj_time = os.path.getmtime(obj_file)
            source_time = os.path.getmtime(source_file)
            header_time = os.path.getmtime(header_file) if header_file else 0
            cefpython_h_fixed_time = os.path.getmtime(
                    CEFPYTHON_API_HFILE_FIXED)
            common_files_time = get_directory_mtime(os.path.join(SRC_DIR,
                                                                 "common"))
            changed = ((source_time > obj_time)
                       or (header_time > obj_time)
                       or (cefpython_h_fixed_time > obj_time)
                       or (common_files_time > obj_time))
        else:
            changed = True
        if changed:
            any_changed = True
        else:
            objects.append(obj_file)
    if any_changed:
        # If any has changed must recompile all given sources (for a library
        # or executable). This is because we don't know which sources include
        # which header files so must recompile everything.
        objects = list()
        # Compile each source file separately so that when compiling
        # source files from different directories, object files are
        # all put in the same output_directory. Otherwise distutils
        # will create lots of subdirs in output_directory.
        macros = macros_as_tuples(macros)
        common_dir = os.path.join(SRC_DIR, "common")
        original_dir = os.getcwd()
        for source_file in sources:
            source_dir = os.path.dirname(source_file)
            os.chdir(source_dir)
            source_basename = os.path.basename(source_file)
            oneobj = compiler.compile([source_basename],
                                      output_dir=output_dir,
                                      macros=macros,
                                      # TODO include dirs for Linux/Mac
                                      include_dirs=[SRC_DIR,
                                                    common_dir,
                                                    get_python_include_path()],
                                      # TODO compiler flags for Linux/Mac
                                      extra_preargs=None,
                                      extra_postargs=extra_args)
            assert len(oneobj) == 1
            objects.append(os.path.join(source_dir, oneobj[0]))
        os.chdir(original_dir)
    assert len(objects)
    return any_changed, objects


def macros_as_tuples(macros):
    """Return all macros as tuples. Required by distutils.ccompiler."""
    ret_macros = list()
    for macro in macros:
        if isinstance(macro, str):
            ret_macros.append((macro, ""))
        else:
            assert isinstance(macro, tuple)
            ret_macros.append(macro)
    return ret_macros


def get_directory_mtime(directory):
    # For example check src/common/ directory for newest modification time
    assert os.path.isdir(directory)
    files = glob.glob(os.path.join(directory, "*"))
    ret_mtime = 0
    for header_file in files:
        mtime = os.path.getmtime(header_file)
        if mtime > ret_mtime:
            ret_mtime = mtime
    assert ret_mtime
    return ret_mtime


if __name__ == "__main__":
    main()
