#!/usr/bin/env python3
import sys
if not sys.version_info >= (3, 3):
    print("openage requires Python 3.3 or higher")
    exit(1)

import argparse
import multiprocessing
import os
import subprocess
import shutil
import shlex
shlex.join = lambda args: ' '.join(shlex.quote(a) for a in args)


def getenv(*varnames, default=""):
    """
    fetches an environment variable.
    tries all given varnames until it finds an existing one.
    if none fits, returns default.
    """
    for v in varnames:
        if v in os.environ:
            return os.environ[v]

    return default


# argparsing
description = """./configure is a convenience script that creates the build \
directory and invokes cmake for an out-of-source build.

don't feel obliged to use this, especially if you're packaging openage."""

epilog = """environment variables like CXX and CXXFLAGS are honored, \
but overwritten by command-line arguments."""

ap = argparse.ArgumentParser(
    description=description,
    epilog=epilog,
    formatter_class=argparse.ArgumentDefaultsHelpFormatter)

ap.add_argument("--mode", "-m", choices=["debug",
                                         "release"],
                default="release",
                help="controls cmake build mode")
ap.add_argument("--optimize", "-O", choices=["auto",
                                             "0",
                                             "1",
                                             "g",
                                             "2",
                                             "max"], default="auto",
                help="controls optimization-related flags. " +
                     "is set according to mode if 'auto'. " +
                     "conflicts with --flags")
ap.add_argument("--compiler", "-c",
                default="",
                help="compiler suite")
ap.add_argument("--c-compiler",
                default=getenv("CC"),
                help="c compiler executable")
ap.add_argument("--cpp-compiler",
                default=getenv("CXX"),
                help="c++ compiler executable")
ap.add_argument("--with", action='append', dest='with_', metavar='OPTION',
                help="enable optional functionality")
ap.add_argument("--without", action='append', metavar='OPTION',
                help="disable optional functionality")
ap.add_argument("--flags", "-f",
                default=getenv("CXXFLAGS", "CCFLAGS", "CFLAGS"),
                help="compiler flags")
ap.add_argument("--ldflags", "-l",
                default=getenv("LDFLAGS"),
                help="linker flags")
ap.add_argument("--prefix", "-p", default="/usr/local",
                help="installation directory prefix")
ap.add_argument("--dry-run", action='store_true',
                help="do not actually invoke cmake or create any directories")
ap.add_argument("--raw-cmake-args", nargs=argparse.REMAINDER, default=[],
                help="everything that follows is passed to cmake directly")

args = ap.parse_args()

try:
    subprocess.call(['cowsay', '--', description])
    print("")
except:
    print(description)
    print("")

defines = {}

options = {
    "inotify":             "if_available",
    "gperftools-tcmalloc": False,
    "gperftools-profiler": "if_available",
}


def sanitize_option_name(option):
    if option not in options:
        ap.error("unknown option: '{}'".format(option))

if args.with_:
    for x in args.with_:
        sanitize_option_name(x)
        options[x] = True

if args.without:
    for x in args.without:
        sanitize_option_name(x)
        options[x] = False


# args.mode
mode = args.mode
if mode == 'debug':
    build_type = 'Debug'
else:
    build_type = 'Release'
defines.update(build_type=build_type)

# args.optimize
omode = args.optimize

# args.flags, args.ldflags
flags = shlex.split(args.flags)
ldflags = shlex.split(args.ldflags)

if omode == 'auto':
    # auto-set omode depending on mode
    if mode == 'debug':
        omode = 'g'
    else:
        omode = '2'
else:
    if flags:
        ap.error("--optimize is forbidden when flags are specified manually " +
                 "(conflicting: -O%s, -f %s)" % (omode, shlex.join(flags)))

# maps csuite names to lists of (cc, cxx, other aliases...)
csuites = {"gnu": ("gcc", "g++"), "llvm": ("clang", "clang++")}


def getcsuite(name):
    name = name.lower().strip()
    for csuite, aliases in csuites.items():
        if name == csuite or name in aliases:
            return csuite
    else:
        raise ValueError("unknown compiler: %s" % name)

# determine compiler binaries from args.{c_compiler, cxx_compiler, compiler}
if args.compiler and (args.c_compiler or args.cpp_compiler):
    ap.error("""either specify a compiler suite via --compiler, or manually \
specify the C and C++ compilers via --c-compiler and -cpp-compiler""")

if args.c_compiler or args.cpp_compiler:
    cc = args.c_compiler
    cxx = args.cpp_compiler
    if cc and cxx:
        # both CC and CXX have been specified manually
        try:
            csuite = getcsuite(cc)
        except ValueError:
            print("warning: could not identify compiler: %s" % cc)
            csuite = 'unknown'
    else:
        # only one has been specified manually; auto-determine the other
        if cc:
            known = cc
            unknownindex = 1
        else:
            known = cxx
            unknownindex = 0

        knowndir, knownbasename = os.path.split(known)
        knownbasename, knownext = os.path.splitext(knownbasename)
        try:
            knownbasename, knownver = knownbasename.split('-', maxsplit=1)
            knownver = "-%s" % knownver
        except ValueError:
            knownver = ""

        try:
            csuite = getcsuite(knownbasename)
        except ValueError:
            ap.error("could not identify compiler: %s" % known)

        unknownbasename = csuites[csuite.lower()][unknownindex]
        unknownbasename = "".join((unknownbasename, knownver, knownext))
        unknown = os.path.join(knowndir, unknownbasename)

        if not cc:
            cc = unknown
        if not cxx:
            cxx = unknown
else:
    # neither CC, nor CXX has been specified
    compiler = args.compiler
    if not compiler:
        if sys.platform.startswith('darwin'):
            compiler = 'llvm'
        else:
            # default to gnu compiler suite
            compiler = 'gnu'

    try:
        # look up csuite name (e.g. gcc -> gnu)
        csuite = getcsuite(compiler)
    except ValueError:
        ap.error("unknown compiler suite: %s. manually specify --c-compiler \
and --cpp-compiler, or use one of [%s]" % (csuite, ", ".join(csuites)))

    cc, cxx = csuites[csuite][:2]

# test whether the specified compilers actually exist
if not shutil.which(cc):
    ap.error('could not find c compiler executable: %s' % cc)

if not shutil.which(cxx):
    ap.error('could not find c++ compiler executable: %s' % cxx)

print('compiler suite: %s\n' % csuite)

defines.update(c_compiler=cc, cxx_compiler=cxx)

# if no flags have been given, populate them from the other args
if not flags and not ldflags:
    if omode == '0':
        flags.append("-O0")
    elif omode == '1':
        flags.append("-O1")
    elif omode == 'g':
        if csuite in {'gcc'}:
            flags.append("-Og")
        else:
            flags.append("-O1")
    elif omode == '2':
        flags.append("-O2")
    elif omode == 'max':
        flags.append("-O3")
        flags.append("-march=native")

        if csuite in {'gcc'}:
            flags.append("-flto=%d" % multiprocessing.cpu_count())
            ldflags.append("-flto=%d" % multiprocessing.cpu_count())

flags = shlex.join(flags)
ldflags = shlex.join(ldflags)
defines.update(c_flags=flags, cxx_flags=flags, exe_linker_flags=ldflags,
               module_linker_flags=ldflags, shared_linker_flags=ldflags)

# args.prefix
defines.update(install_prefix=args.prefix)


def sanitize(s, fallback='-'):
    """
    sanitizes a string for safe usage in a filename
    """

    def yieldsanitizedchars():
        # False if the previous char was regular.
        fallingback = True
        for c in s:
            if c == fallback and fallingback:
                fallingback = False
            elif c.isalnum() or c in "+-_=,":
                fallingback = False
                yield c
            elif not fallingback:
                fallingback = True
                yield fallback

    return "".join(yieldsanitizedchars())

bindir = ".bin/%s-%s-%s" % (sanitize(cc), sanitize(mode), sanitize(flags))
if not args.dry_run:
    os.makedirs(bindir, exist_ok=True)


def forcesymlink(linkto, name):
    """
    similar in function to ln -sf
    """
    if args.dry_run:
        return

    try:
        os.unlink(name)
    except FileNotFoundError:
        pass

    os.symlink(linkto, name)

# create the build dir and symlink it to 'bin'
forcesymlink(bindir, 'bin')
forcesymlink('%s/cpp/openage' % bindir, 'openage')

# the project root directory contains this configure file.
project_root = os.path.dirname(os.path.realpath(__file__))

# calculate cmake invocation from defines dict
invocation = ['cmake']
maxkeylen = max(len(k) for k in defines)
for k, v in sorted(defines.items()):
    print('%s | %s' % (k.rjust(maxkeylen), v))

    if k in ('c_compiler', 'cxx_compiler'):
        # work around this cmake 'feature':
        # when run in an existing build directory, if CC or CXX are given,
        # all other arguments are ignored... this is retarded.
        if os.path.exists(os.path.join(bindir, 'CMakeCache.txt')):
            continue

    invocation.append('-DCMAKE_%s=%s' % (k.upper(), shlex.quote(v)))

print("\nconfig options:\n")

maxkeylen = max(len(k) for k in options)
for k, v in sorted(options.items()):
    print('%s | %s' % (k.rjust(maxkeylen), v))

    invocation.append('-DWANT_%s=%s' % (k.upper().replace('-', '_'), v))


for raw_cmake_arg in args.raw_cmake_args:
    invocation.append(raw_cmake_arg)

invocation.append('--')
invocation.append(project_root)

# look for traces of an in-source build
if os.path.isfile('CMakeCache.txt'):
    print("\nwarning: found traces of an in-source build.")
    print("CMakeCache.txt was deleted to make building possible.")
    print("run 'make cleaninsourcebuild' to fully wipe the traces.")
    os.remove('CMakeCache.txt')

# switch to build directory
print('\nbindir:\n%s/\n' % os.path.join(project_root, bindir))
if not args.dry_run:
    os.chdir(bindir)

# invoke cmake
try:
    print('invocation:\n%s\n' % ' '.join(invocation))
    if args.dry_run:
        exit(0)
    else:
        exit(subprocess.call(invocation))
except FileNotFoundError:
    print("cmake was not found")
    exit(1)
