#
#
# Important Triplets:
#
# clang-cl (clang-cl /clang:-dumpmachine)
#
#   x86_64-pc-windows-msvc
#   i386-pc-windows-msvc
#
# clang on FreeBSD (clang -dumpmachine):
#
#   x86_64-unknown-freebsd12.2
#   i386-unknown-freebsd12.2
#
# NOTE: gcc also knows "-dumpmachine"
#
#

from __future__ import print_function, absolute_import

import argparse
import collections
import datetime
import copy
import getopt
import os
import sys

import ninja_syntax

tool = build = host = None

#
# Global build variables (ordered because they must be written ordered
# -- and with simple attribute access
#
class BuildVars(collections.OrderedDict):

    def __getattr__(self, n):
        try:
            return self[n]
        except KeyError:
            raise AttributeError(n)

    def __setattr__(self, n, v):
        # private v
        if n.startswith("_OrderedDict__"):
            return super(BuildVars, self).__setattr__(n, v)
        self[n] = v


def make_obj_name(name, newext):
    bn = os.path.basename(name)
    if not bn:
        return bn
    root, ext = os.path.splitext(bn)
    return root + newext

options = argparse.Namespace(
    user_includes = [],
    sys_includes = [],
    sys_libpath = [],
    user_libpath = [],
    link_with_python = None,
    python_limited_api = None,
)

gbv = BuildVars()
gbv.intdir = "build/_builddir-test/tmp"
gbv.srcdir = "."
gbv.builddir = "$intdir"
gbv.outdir = "$intdir/out"

opts, args = getopt.getopt(
    sys.argv[1:],
    "B:H:t:I:L:",
    ["build=",
     "host=",
     "tool=",
     "include=",
     "libpath=",
     "sys-include=",
     "sys-libpath=",
     "CC=",
     "LINK=",
     "intdir=",              # intermediate files
     "builddir=",            # Ninja builddir
     "outdir=",              # The built objects
     "link-with-python=",    # link with libpython
     "python-limited-api=",  # Use Py_LIMITED_API
     ])
for opt, val in opts:
    if opt in ("-t", "--tool"):
        if tool is None:
            tool = argparse.Namespace(local=False, msvc=False, clang=False)
        if val == "msvc":
            tool.msvc = True
            tool.compile_only = "/c"
            tool.define_format = "/D {}"
            tool.include_format = "/I {}"
            tool.lib_format = "{}"
            tool.libpath_format = "/libpath:{}"
            tool.dependencies = "msvc"
        elif val == "clang-cl":
            tool.msvc = tool.clang = True
            tool.compile_only = "/c"
            tool.define_format = "/D {}"
            tool.include_format = "/I {}"
            tool.lib_format = "{}"
            tool.libpath_format = "/libpath:{}"
            tool.dependencies = "msvc"
        elif val == "clang":
            tool.clang = True
            tool.compile_only = "-c"
            tool.define_format = "-D{}"
            tool.include_format = "-I{}"
            tool.lib_format = "-l{}"
            tool.libpath_format = "-L{}"
            tool.dependencies = "gcc"
        elif val in ("local", "posix"):
            tool.local = True
            tool.compile_only = "-c"
            tool.define_format = "-D{}"
            tool.include_format = "-I{}"
            tool.lib_format = "-l{}"
            tool.libpath_format = "-L{}"
            tool.dependencies = "gcc"
        else:
            raise getopt.GetoptError("unknown tool value: {}".format(val), opt)
    elif opt in ("-B", "--build"):
        build = argparse.Namespace(type=None,
                                   posix=False, windows=False,
                                   pathmod=None)
        if val == "windows":
            # build on Windows with clang-cl
            build.windows = True
            build.type = "windows"
        elif val == "posix":
            build.posix = True
            build.type = "posix"
        else:
            raise getopt.GetoptError("unknwon build value: {}".format(val),
                                     opt)
    elif opt in ("-H", "--host"):
        if host is None:
            host = argparse.Namespace(windows=False)
        if val == "windows":
            host.type = "windows"
            host.windows = True
            host.posix = False
            host.objext = ".obj"
            host.pydext = ".pyd"
        elif val == "posix":
            host.type = "posix"
            host.windows = False
            host.posix = True
            host.objext = ".o"
            host.pydext = ".so"
        else:
            raise getopt.GetoptError("unknown host value: {}".format(val),
                                     opt)
    elif opt in ("-I", "--include"):
        options.user_includes.append(val)
    elif opt in ("-L", "--libpath"):
        options.user_libpath.append(val)
    elif opt == "--sys-include":
        options.sys_includes.append(val)
    elif opt == "--sys-libpath":
        options.sys_libpath.append(val)
    elif opt == "--CC":
        gbv.cc = val
    elif opt == "--LINK":
        gbv.link = val
    elif opt == "--intdir":
        gbv.intdir = val
    elif opt == "--builddir":
        gbv.builddir = val
    elif opt == "--outdir":
        gbv.outdir = val
    elif opt == "--link-with-python":
        options.link_with_python = val
    elif opt == "--python-limited-api":
        if val.lower().startswith("0x"):
            options.python_limited_api = val
        else:
            # Here we are Python 3.7+ when using the limited API
            options.python_limited_api = "0x03070000"
    else:
        raise getopt.GetoptError("Unhandled option `{}'".format(opt), opt)

if tool is None:
    print("ERROR: no tool given", file=sys.stderr)
    sys.exit(1)

if build.windows and host.posix:
    print("ERROR: cross-compiling on Windows not supported", file=sys.stderr)
    sys.exit(1)

if build.windows:
    import ntpath as pathmod
else:
    import posixpath as pathmod
build.pathmod = pathmod

if tool.msvc:
    if tool.clang:
        if not getattr(gbv, "cc", None):
            gbv.cc = "clang-cl"
        if not getattr(gbv, "link", None):
            gbv.link = "lld-link"
    else:
        gbv.cc = "cl"
        gbv.link = "link"
elif tool.clang:
    gbv.cc = "clang"
    gbv.link = "clang"   # link C through the compiler
elif tool.local:
    gbv.cc = "cc"
    gbv.link = "cc"       # link through the compiler
else:
    raise RuntimeError("tool condition is not handled")

speedups_sources = [
    "$srcdir/configmix/_speedups.c",
]

ccflags = []
ccwarnings = []
ldflags = []

defines = [
#    "PY_SSIZE_T_CLEAN",   # in the source
    "HAVE_THREADS",
]
if options.python_limited_api:
    defines.append("Py_LIMITED_API={}".format(options.python_limited_api))

# XXX TBD: handle debug/release build _DEBUG/NDEBUG

includes = []
includes.extend(options.sys_includes)
includes.extend(options.user_includes)


libpath = []
libpath.extend(options.sys_libpath)
libpath.extend(options.user_libpath)

libs = []
if host.windows:
    if tool.msvc:
        # automatically included via #pragma
        # libs.append("python3.lib")
        pass
else:
    if options.link_with_python:
        libs.append(options.link_with_python)

if host.windows:
    defines.append("WIN32")
    # XXX TBD Handle arch -> WIN64
    defines.append("WIN64")
    defines.append("_WINDOWS")
    # for a user dll
    defines.append("_USRDLL")
    defines.append("_WINDLL")

    defines.append("WIN32_LEAN_AND_MEAN")
    # WinXP: no extravagant Windows features
    defines.append("_WIN32_WINNT=0x0501")

    if tool.msvc:
        # XXX TBD warnings

        defines.append("_CRT_SECURE_NO_WARNINGS")

        ccflags.append("/Zi")
        ccflags.append("/MD")   # link to dll runtime
        #ccflags.append("/EHsc")   # no C++ here
        ccflags.append("/Gy")   # enable function level linking

        # XXX TBD machine
        if tool.clang:
            ccflags.append("--target=x86_64-pc-windows-msvc")
        else:
            ccflags.append("-m64")

        if tool.clang:  # or tool.gcc
            ccflags.append("-fno-strict-aliasing")

        ldflags.append("/dll")
        ldflags.append("/debug")    # PDB output
        # 32-bit: -> 5.01   64-bit: 5.02
        ldflags.append("/subsystem:windows,5.02")
        ldflags.append("/incremental:no")
        #
        ldflags.append("/manifest:NO")


    if tool.clang:
        ccflags.append("-fms-compatibility-version=16.00")
        ccflags.append("-O2")

        ccwarnings.append("-Wno-nonportable-include-path")
        ccwarnings.append("-Wno-microsoft-template")
        ccwarnings.append("-Wno-pragma-pack")
elif host.posix:
    defines.append("PIC")

    ccwarnings.extend(["-Wall", "-Wextra", "-pedantic"])

    ccflags.append("-g")
    ccflags.append("-fPIC")
    ccflags.append("-fvisibility=hidden")
    ccflags.append("-pthread")

    if tool.clang:  # || tool.gcc
        ccflags.append("-ffunction-sections")
        ccflags.append("-fdata-sections")
        ccflags.append("-fno-strict-aliasing")

    if tool.clang:
        ccflags.append("-faddrsig")    # use with --icf=all/safe when linking

    ldflags.append("-shared")
    ldflags.append("-Wl,-z,relro,-z,now")
    ldflags.append("-Wl,--build-id=sha1")
    # XXX TBD only when building in debug code
    if options.link_with_python:
        ldflags.append("-Wl,-z,defs")

    if tool.clang: # || tool.gcc
        ldflags.append("-Wl,--gc-sections")

    if tool.clang:
        ldflags.append("-Wl,--icf=safe")

    if options.python_limited_api:
        host.pydext = ".abi3" + host.pydext

defines.append("NDEBUG")

gbv.cppdefines = [tool.define_format.format(d) for d in defines]
gbv.includes = [tool.include_format.format(pathmod.normpath(i))
                for i in includes]
gbv.ccflags = ccflags
gbv.ccwarnings = ccwarnings
gbv.ldflags = ldflags
gbv.ldlibpath = [tool.libpath_format.format(pathmod.normpath(l))
                 for l in libpath]
gbv.ldlibs = [tool.lib_format.format(l) for l in libs]

n = ninja_syntax.Writer(sys.stdout)

n.comment('This file is used to build test Python extensions.')
n.comment(
    'It is generated by {} at {}Z.'.format(
        os.path.basename(__file__),
        datetime.datetime.utcnow().isoformat()))
n.newline()
n.comment('Created using command: {!r}'.format(sys.argv))
n.newline()
for k, v in gbv.items():
    n.variable(k, v)
n.newline()
if tool.msvc:
    # Note: this includes clang-cl
    n.rule("compile-pyextension-unit",
           "$cc /nologo /showIncludes /c $cppdefines $ccwarnings $includes $ccflags /Fd$intdir/$intsubdir/ /Fo$out $in",
           deps=tool.dependencies)
else:
    n.rule("compile-pyextension-unit",
           "$cc -MD -MF $intdir/_deps -MT $out $cppdefines $ccwarnings $includes $ccflags -c -o $out $in",
           deps=tool.dependencies,
           depfile="$intdir/_deps")
n.newline()
if tool.msvc:
    # XXX TBD: in "release" builds use /pdbaltpath:$out.pdb
    n.rule("link-pyextension", "$link /nologo $ldflags $ldlibpath /implib:$out.lib /pdb:$out.pdb /out:$out $in $ldlibs")
else:
    n.rule("link-pyextension", "$link $cppdefines $ccwarnings $ccflags $ldflags -o $out $in $ldlibpath $ldlibs")
n.newline()

n.comment("_speedups")
for f in speedups_sources:
    n.build(pathmod.normpath("$intdir/$intsubdir/"+make_obj_name(f, host.objext)),
            "compile-pyextension-unit",
            inputs=pathmod.normpath(f),
            variables={"intsubdir": "speedups"})
n.newline()
linkinputs = [pathmod.normpath("$intdir/$intsubdir/"+make_obj_name(f, host.objext))
              for f in speedups_sources]
linkoutput = "$outdir/_speedups" + host.pydext
if tool.msvc:
    implicit_outputs = [
        pathmod.normpath(linkoutput + ".pdb"),
        pathmod.normpath(linkoutput + ".lib")]
    if not tool.clang:
        implicit_outputs.append(pathmod.normpath(linkoutput + ".exp"))
else:
    implicit_outputs = None
n.build(linkoutput,
        "link-pyextension",
        inputs=linkinputs,
        implicit_outputs=implicit_outputs,
        variables={"intsubdir": "speedups"})
n.newline()
