#!/usr/bin/env python3
#
# This is simple script to detect libraries and configure standard features.
#
import os
import sys
from optparse import OptionParser
import subprocess

include_search_paths = []

def get_default_include_paths(cfg):
    out = os.popen("%s -v -x c -E /dev/null 2>&1" % cfg['CC'][0], 'r', 1)
    add = False
    paths = []
    for line in out.readlines():
        if (line == "#include <...> search starts here:\n"):
            add = True
            continue

        if (line == "End of search list.\n"):
            add = False

        if (add):
            paths.append(line.rstrip().lstrip())

    return paths

def header_exists(cfg, *filenames):
    for filename in filenames:
        sys.stderr.write("Checking for '%s' ... " % filename)
        for include_path in include_search_paths:
            fpath =  include_path + '/' + filename
            try:
                st = os.stat(fpath)
                sys.stderr.write("Yes\n")
                sys.stderr.write(" (%s)\n\n" % fpath)
                return True
            except os.error:
                pass

        sys.stderr.write("No\n\n")

    return False

def headers_exists(cfg, *filenames):
    for filename in filenames:
        if not header_exists(cfg, filename):
            return False
    return True

def c_try_compile(cfg, code, msg, cflags=""):
    sys.stderr.write(msg)

    ret = os.system("echo '%s' | %s %s -c -x c -o /dev/null - > /dev/null 2>&1" %
                    (code, cfg['CC'][0], cfg['CFLAGS'][0]) + ' ' + cflags)

    if ret:
        sys.stderr.write("No\n")
        return False
    else:
        sys.stderr.write("Yes\n")
        return True

def c_compiler_exists(cfg):
    return c_try_compile(cfg, "int main(void) { return 0; }",
                         "Checking for working compiler (%s) ... " %
                         cfg["CC"][0])

def define_fortify_source(cfg):
    return c_try_compile(cfg, "int main(void) {\n" +
                              "#if !defined _FORTIFY_SOURCE &&" +
                              "defined __OPTIMIZE__ && __OPTIMIZE__\n" +
                              "        return 0;\n" +
                              "#else\n" +
                              " #error FORTIFY_SOURCE not usable\n" +
                              "#endif\n" +
                              "}", "Whether to define _FORTIFY_SOURCE ... ");

def check_for_cflags(cfg, cflags):
    return c_try_compile(cfg, "int main(void) { return 0; }",
                         "Checking for %s ... " % cflags, cflags)

def python_version(cfg):
    sys.stderr.write("Checking for python-config Python version ... ")

    if (cfg['PYTHON_CONFIG'][0] == ''):
        sys.stderr.write('NA\n')
        return ''

    cmd = subprocess.Popen([cfg['PYTHON_CONFIG'][0], '--includes'],
                           stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    res = str(cmd.communicate())
    res = res[res.find('/python')+7:]
    res = res.split(' ')[0]

    sys.stderr.write("%s\n" % res)
    return res

def check_for_swig(cfg):
    sys.stderr.write("Checking for working swig ... ")

    ret = os.system("%s -version > /dev/null 2>&1" % cfg['SWIG'][0])

    if ret:
        sys.stderr.write('No\n')
        cfg['SWIG'][0] = ''
    else:
        sys.stderr.write('Yes\n')

def check_for_python_config(cfg):
    sys.stderr.write("Checking for python-config ... ")

    ret = os.system("%s --libs > /dev/null 2>&1" % cfg['PYTHON_CONFIG'][0])

    if ret:
        sys.stderr.write('No\n')
        cfg['PYTHON_CONFIG'][0] = ''
    else:
        sys.stderr.write('Yes\n')

#
# Adds prefix to *dir vars but only if the path does not start with '/'
#
def cfg_get_value(cfg_key):
    if (cfg_key.endswith('dir')):
        value = cfg[cfg_key][0]
        if (value.startswith('/')):
            return value
        else:
            prefix = cfg['prefix'][0]
            if (prefix.endswith('/')):
                return prefix + value
            else:
                return prefix + '/' + value
    else:
        return cfg[cfg_key][0]

#
# Library checking api
#
class libraries:
    def __init__(self, libraries, cfg):
        self.libraries = libraries
        self.cfg = cfg;
        # Create dictionary for check results
        self.results = dict()
    #
    # Print summary
    #
    def print_summary(self):
        sys.stderr.write("Settings and variables\n")
        sys.stderr.write("----------------------\n")

        for i in cfg:
            value = cfg_get_value(i)
            sys.stderr.write("%16s : '%s'\n" % (i, value))
            sys.stderr.write("                  - %s\n\n" % cfg[i][1])

        sys.stderr.write("Libraries to link against\n")
        sys.stderr.write("-------------------------\n")

        for i in self.libraries:
            sys.stderr.write("%16s" % i[0])

            if (self.results[i[0]]):
                sys.stderr.write(" : Enabled\n")
            else:
                sys.stderr.write(" : Disabled\n")

            sys.stderr.write("                  - %s\n\n" % i[1])
    #
    # Enable/Disable library
    #
    def set(self, name, val):
        if name not in map(lambda s: s[0], self.libraries):
            sys.stderr.write("ERROR: Invalid library '%s'\n" % name)
            exit(1)
        else:
            self.results[name] = val

    def get(self, name):
        if name not in self.results:
            sys.stderr.write("ERROR: Invalid library '%s'\n" % name)
            exit(1)
        else:
            return self.results[name]

    #
    # Calls a function on arguments, all is stored in array if
    # not set previously
    # (I know this smells like a lisp, but I can't help myself)
    #
    def check(self):
        sys.stderr.write("Checking for libraries\n")
        sys.stderr.write("----------------------\n")
        for i in self.libraries:
            if i[0] not in self.results:
                if len(i) > 6 and not self.get(i[6]):
                    sys.stderr.write("Disabling %s because of missing %s\n\n" % (i[0], i[6]))
                    self.results[i[0]] = False
                else:
                    self.results[i[0]] = i[2][0](self.cfg, *i[2][1:])


                # Resolve dynamic CFLAGS && LDFLAGS
                if self.results[i[0]]:
                    if hasattr(i[3], '__call__'):
                        i[3] = i[3](self.cfg)
                    if hasattr(i[4], '__call__'):
                        i[4] = i[4](self.cfg)
        sys.stderr.write("\n")
    #
    # Writes '#define HAVE_XXX_H' into passed file
    #
    def write_config_h(self, f):
        for i in self.libraries:
            f.write("/*\n * %s\n */\n" % i[1])
            s=i[0].upper().replace('-', '_').replace('.', '_').replace('/', '_')
            if self.results[i[0]]:
                f.write("#define HAVE_%s\n" % s)
            else:
                f.write("//#define HAVE_%s\n" % s)
            f.write("\n")

        f.write("/* %s */\n" % self.cfg['LIBSDL_VERSION'][1])
        f.write("#define LIBSDL_VERSION %s\n\n" % self.cfg['LIBSDL_VERSION'][0])

    #
    # Writes LDLIBS and CFLAGS into passed file
    #
    def write_config_mk(self, f):
        for i in self.libraries:
            f.write("# %s - %s\n" % (i[0], i[1]))
            if self.results[i[0]]:
                f.write("HAVE_%s=yes\n" % i[0].upper())
            else:
                f.write("HAVE_%s=no\n" % i[0].upper())

        # Write library specific CFLAGS
        for i in self.libraries:
            if self.results[i[0]]:
                if i[3]:
                    f.write("# %s cflags\n" % i[0])
                    f.write("%s_CFLAGS=%s\n" % (i[0].upper(), i[3]))

        # Write all libraries the library should link with
        for module in self.get_modules():
            f.write("# %s linker flags\n" % (module))
            f.write("LDLIBS_%s=" % (module))
            f.write("%s\n" % self.get_ldflags(module))
    #
    # Return list of linker flags needed to build particular module
    # (module may be core, loaders, backends, etc...
    #
    def get_ldflags(self, module):
        res = ''
        for i in self.libraries:
            if module in i[5] and self.results[i[0]]:
                if i[4] == '':
                    continue;
                if res != '':
                    res += ' '
                res += i[4]
        return res
    #
    # Returns list of cflags needed to build module
    #
    def get_cflags(self, module):
        res = ''
        for i in self.libraries:
            if module in i[5] and self.results[i[0]]:
                res += ' ' + i[3]
        return res

    #
    # Builds a list of GFXprim libraries that may need to be linked against
    # third party libs
    #
    def get_modules(self):
        modules = {}
        for i in self.libraries:
            for module in i[5]:
                modules[module] = True
        return modules.keys()

def die_screaming(msg):
    sys.stderr.write("\n************************************\n")
    sys.stderr.write("ERROR: ")
    sys.stderr.write(msg)
    sys.stderr.write("\n************************************\n")
    exit(1)

#
# Check for basic compiling tools
#
def basic_checks(cfg):
    sys.stderr.write("Basic checks\n")
    sys.stderr.write("------------\n")

    if not c_compiler_exists(cfg):
        die_screaming("No C compiler found")

    sys.stderr.write("\nInclude search paths\n");
    sys.stderr.write("--------------------\n");

    global include_search_paths


    if (cfg['include_path'][0]):
        include_search_paths.append(cfg['include_path'][0])
    else:
        include_search_paths = get_default_include_paths(cfg)

    for path in include_search_paths:
        sys.stderr.write(' "' + path + '"\n');
    sys.stderr.write("\n");

    check_for_swig(cfg)
    check_for_python_config(cfg)

    if define_fortify_source(cfg):
        cfg['CFLAGS'][0] = cfg['CFLAGS'][0] + " -D_FORTIFY_SOURCE=2"

    if check_for_cflags(cfg, "-Wimplicit-fallthrough=0"):
        cfg['CFLAGS_WNIF'][0] = "-Wimplicit-fallthrough=0"

    cfg['PYTHON_VER'][0] = python_version(cfg)

    if cfg['libdir'][0] == '':
        sys.stderr.write("Checking for lib directory ... ")

        if os.path.isdir(cfg['prefix'][0] + '/lib64'):
            cfg['libdir'][0] = 'lib64'
        else:
            cfg['libdir'][0] = 'lib'

        sys.stderr.write(cfg['libdir'][0] + '\n');

    sys.stderr.write('\n')

#
# Write configuration files
#
def write_config_h(cfg, libs):
    f = open("config.h", "w")
    f.write("/*\n * This file is genereated by configure script\n */\n");
    f.write("#ifndef CONFIG_H\n#define CONFIG_H\n\n")
    libs.write_config_h(f);
    f.write("/* Shortcut for DRM */\n");
    f.write("#if (defined(HAVE_DRM_DRM_H) || defined(HAVE_LIBDRM_DRM_H))\n# define HAVE_LINUX_DRM\n#endif\n\n");
    f.write("/*\n * OS to compile for\n */\n");
    f.write("#define OS_%s 1\n\n" % (cfg['os'][0].upper()))
    f.write("#endif /* CONFIG_H */\n");
    sys.stderr.write("Config 'config.h' written\n")
    f.close()

def write_config_mk(cfg, libs):
    f = open('config.gen.mk', 'w')

    for i in cfg:
        f.write("# %s\n%s=%s\n" % (cfg[i][1], i, cfg_get_value(i)))

    libs.write_config_mk(f);
    f.close()
    sys.stderr.write("Config 'config.gen.mk' written\n")

#
# Generate app compilation helper
#
def write_gfxprim_config(cfg, libs):
    modules = libs.get_modules()
    f = open('gfxprim-config', 'w')
    f.write('#!/bin/sh\n'
            '#\n# Generated by configure, do not edit directly\n#\n\n'
            'USAGE="Usage: $0 --list-modules --cflags --ldflags --libs --static-libs --static-libs-module_foo --libs-module_foo"\n'
            '\nif test $# -eq 0; then\n'
            '\techo "$USAGE"\n'
            '\texit 1\n'
            'fi\n\n'
            'while test -n "$1"; do\n'
            '\tcase "$1" in\n')

    # General switches cflags and ldflags
    f.write('\t--help) echo "$USAGE"; exit 0;;\n')
    f.write('\t--list-modules) echo "%s"; exit 0;;\n' % ' '.join(modules))
    f.write('\t--cflags) echo -n "-I%s/include/gfxprim/%s";;\n' %
            (cfg['prefix'][0], libs.get_cflags('core')))
    f.write('\t--ldflags) echo -n "-L%s/%s";;\n' % (cfg['prefix'][0], cfg['libdir'][0]))
    f.write('\t--libs) echo -n "-pthread -lgfxprim ";;\n')
    f.write('\t--static-libs) echo -n "-static -pthread -lgfxprim ";;\n')

    # ldflags for specific modules
    for i in modules:
        ldflags = '-lgfxprim-' + i

        if i == "widgets":
            ldflags += " -lgfxprim-backends -rdynamic"

        f.write('\t--libs-%s) echo -n "%s ";;\n' % (i, ldflags))
        f.write('\t--static-libs-%s) echo -n "%s %s ";;\n' % (i, ldflags, libs.get_ldflags(i)))

    f.write('\t*) echo "Invalid option \'$1\'"; echo $USAGE; exit 1;;\n')

    f.write('\tesac\n\tshift\ndone\necho\n')
    f.close()
    os.system('chmod +x gfxprim-config')

def write_pkgconfig(cfg, libs):
    modules = libs.get_modules()
    prefix = cfg['prefix'][0]

    for m in modules:
        module_name = 'gfxprim'
        if m != 'core':
            module_name += '-' + m

        f = open('build/' + module_name + '.pc', 'w')
        f.write('prefix=' + prefix + '\n')
        f.write('exec_prefix=${prefix}\n')
        f.write('includedir=' + cfg_get_value('includedir') + '\n')
        f.write('libdir=' + cfg_get_value('libdir') + '\n')
        f.write('\n')
        f.write('Name: ' + module_name + '\n')
        f.write('Description: gfxprim library ' + m + '\n')
        #TODO: Write version to configure.h and include/core/gp_version.h
        f.write('Version: 1.0.0\n')
        if m != 'core':
            f.write('Requires: gfxprim >= 1.0.0')
            if m == 'widgets':
                f.write(' gfxprim-backends >= 1.0.0')
            f.write('\n')
        f.write('Cflags: -I' + cfg_get_value('includedir') + '/gfxprim\n')
        f.write('Libs: -L' + cfg_get_value('libdir') + ' -l' + module_name)
        if m == 'widgets':
            f.write(' -lgfxprim-backends -rdynamic')
        f.write('\n')


def cfg_parse_args(cfg, args):
    for i in args:
        par = i.split('=');
        if (len(par) != 2):
             die_screaming('Invalid argument %s' % i)

        if (par[0] not in cfg):
            die_screaming('Invalid config key %s' % i)

        cfg[par[0]][0] = par[1]

def cmd_output(cmds):
    for cmd in cmds:
        line = os.popen(cmd, 'r', 1).readline()

        if (line):
            return line.rstrip()

    return None

def freetype_libs(cfg):
    cmds = [
        'freetype-config --libs 2> /dev/null',
        'pkg-config --libs freetype2 2> /dev/null'
    ]

    flags = cmd_output(cmds);

    if flags:
        return flags

    die_screaming("Cannot get freetype LDFLAGS")

def freetype_cflags(cfg):
    cmds = [
        'freetype-config --cflags 2> /dev/null',
        'pkg-config --cflags freetype2 2> /dev/null'
    ]

    flags = cmd_output(cmds);

    if flags:
        return flags

    die_screaming("Cannot get Freetype CFLAGS")

def wayland_cflags(cfg):
    cmds = [
        'pkg-config --cflags xkbcommon wayland-client 2> /dev/null'
    ]

    flags = cmd_output(cmds);
    if flags is None:
        die_screaming("Cannot get Wayland CFLAGS")

    return flags

def wayland_libs(cfg):
    cmds = [
        'pkg-config --libs wayland-client 2> /dev/null'
    ]

    flags = cmd_output(cmds);
    if flags is None:
        die_screaming("Cannot get Wayland LDFLAGS")

    return flags

def sdl_exists(cfg):
    if (cfg['LIBSDL_VERSION'][0] == '' or cfg['LIBSDL_VERSION'][0] == '2'):
        if (header_exists(cfg, "SDL2/SDL.h")):
            cfg['LIBSDL_VERSION'][0] = '2'
            return True;

    if (cfg['LIBSDL_VERSION'][0] == '' or cfg['LIBSDL_VERSION'][0] == '1'):
        if (header_exists(cfg, "SDL/SDL.h")):
            cfg['LIBSDL_VERSION'][0] = '1'
            return True

    return False;

def sdl_libs(cfg):
    if (cfg['LIBSDL_VERSION'][0] == '2'):
        return '`sdl2-config --libs`'

    return '`sdl-config --libs`'

if __name__ ==  '__main__':
    #
    # Dictionary for default configuration parameters
    #
    cfg = {'CC'            : ['gcc', 'Path/name of the C compiler'],
           'CFLAGS'        : ['-W -Wall -Wextra -O2 -ggdb', 'C compiler flags'],
           'CFLAGS_WNIF'   : ['', 'Disable implicit fallthrough warnings'],
           'LDFLAGS'       : ['', 'Linker flags'],
           'LIBSDL_VERSION': ['', 'Version of the SDL library "1" or "2"'],
           'PYTHON_BIN'    : ['python', 'Path/name of python interpreter'],
           'SWIG'          : ['swig', 'Simplified Wrapper and Interface Generator'],
           'PYTHON_CONFIG' : ['python-config', 'Python config helper'],
           'PYTHON_VER'    : ['', 'Python version (derived from python config)'],
           'include_path'  : ['', 'Path to the system headers'],
           'prefix'        : ['/usr/local/', 'Installation prefix'],
           'bindir'        : ['bin', 'Where to install binaries'],
           'libdir'        : ['', 'Where to install libraries'],
           'includedir'    : ['include', 'Where to install headers'],
           'mandir'        : ['share/man', 'Where to install man pages'],
           'docdir'        : ['share/doc/', 'Where to install documentation'],
           # Here comes autoconf compatibility cruft, not used for anything yet
           'infodir'       : ['share/info', 'Where to install info pages'],
           'datadir'       : ['share', 'Where to place readonly arch independend datafiles'],
           'sysconfdir'    : ['etc', 'Where to place configuration'],
           'localstatedir' : ['local/var/', 'Where to place runtime modified datafiles'],
           'build'         : ['', 'WARNING not used'],
           'host'          : ['', 'WARNING not used'],
           # Knobs to disable stuff
           'static-libs'   : ['yes', "Build static libs"],
           'dynamic-libs'  : ['yes', "Build dynamic libs"],
           'build-tests'   : ['yes', "Build tests"],
           'build-demos'   : ['yes', "Build demos"],
           # OS to build for
           'os'            : ['linux', 'OS to build the library for']
    }

    #
    # Library detection/enable disable
    #
    # name, description, [detection], cflags, ldflags, list of modules library is needed for
    #
    l = libraries([["libpng",
                    "Portable Network Graphics Library",
                    [header_exists, "png.h"], "", "-lpng", ["loaders"]],
                   ["libsdl",
                    "Simple Direct Media Layer",
                    [sdl_exists], "", sdl_libs, ["backends"]],
                   ["jpeg",
                    "Library to load, handle and manipulate images in the JPEG format",
                    [header_exists, "jpeglib.h"], "", "-ljpeg", ["loaders"]],
                   ["webp",
                    "A lossy image compression format",
                    [header_exists, "webp/types.h"], "", "-lwebp", ["loaders"]],
                   ["heif",
                    "A HEIF file format.",
                    [header_exists, "libheif/heif.h"], "", "-lheif", ["loaders"]],
                   ["openjpeg",
                    "An open-source JPEG 2000 library",
                    [header_exists, "openjpeg-2.0/openjpeg.h"], "", "-lopenjp2", ["loaders"]],
                   ["giflib",
                    "Library to handle, display and manipulate GIF images",
                    [header_exists, "gif_lib.h"], "", "-lgif", ["loaders"]],
                   ["tiff",
                    "Tag Image File Format (TIFF) library",
                    [header_exists, "tiffio.h"], "", "-ltiff", ["loaders"]],
                   ["zlib",
                    "Standard (de)compression library",
                    [header_exists, "zlib.h"], "", "-lz", ["loaders"]],
                   ["libarchive",
                    "http://www.libarchive.org/",
                    [header_exists, "archive.h"], "", "-larchive", ["loaders"]],
                   ["libXrandr",
                    "Xrandr library",
                    [header_exists, "X11/extensions/Xrandr.h"], "", "-lXrandr", ["backends"]],
                   ["libX11",
                    "X11 library",
                    [headers_exists, "X11/Xlib.h", "X11/extensions/Xfixes.h"], "", "-lX11 -lXfixes", ["backends"]],
                   ["libxcb",
                    "XCB library",
                    [headers_exists, "xcb/xcb.h", "xcb/xcb_keysyms.h", "xcb/xfixes.h"], "", "-lxcb -lxcb-keysyms -lxcb-shm -lxcb-xfixes", ["backends"]],
                   ["xcb-util-errors",
                    "XCB errors library",
                    [header_exists, "xcb/xcb_errors.h"], "", "-lxcb-errors", ["backends"]],
                   ["X_SHM",
                    "MIT-SHM X Extension",
                    [header_exists, "X11/extensions/XShm.h"], "", "-lXext", ["backends"]],
                   ["aalib",
                    "Portable ascii art GFX library",
                    [header_exists, "aalib.h"], "", "-laa", ["backends"]],
                   ["freetype",
                    "A high-quality and portable font engine",
                    [header_exists, "ft2build.h", "freetype2/ft2build.h"],
                    freetype_cflags, freetype_libs, ["core"]],
                   ["fontconfig",
                    "A library for configuring and customizing font access",
                    [header_exists, "fontconfig/fontconfig.h"],
                    "", "-lfontconfig", ["core"]],
                   ["dl",
                    "Dynamic linker",
                    [header_exists, "dlfcn.h"], "", "-ldl", ["core", "widgets"]],
                   ["V4L2",
                    "Video for linux 2",
                    [header_exists, "linux/videodev2.h"], "", "", ["grabbers"]],
                   ["pthread",
                    "Posix Threads",
                    [header_exists, "pthread.h"], "-pthread", "-pthread", ["core"]],
                   ["mmap",
                    "mmap() call",
                    [header_exists, "sys/mman.h"], "", "", ["utils", "grabbers"]],
                   ["drm/drm.h",
                    "an UAPI drm/drm.h header",
                    [header_exists, "drm/drm.h"], "", "", ["backends"]],
                   ["libdrm/drm.h",
                    "fallback for UAPI drm.h in libdrm/drm.h",
                    [header_exists, "libdrm/drm.h"], "", "", ["backends"]],
                   ["xkbcommon",
                    "Keymap handling library for toolkits and window systems",
                    [header_exists, "xkbcommon/xkbcommon.h"],
                     "", "-lxkbcommon", ["backends"]],
                   ["wayland",
                    "Wayland windowing system",
                    [header_exists, "wayland-client.h", "wayland/wayland-client.h"],
                     wayland_cflags, wayland_libs, ["backends"], "xkbcommon"],
                   ["backtrace",
                    "C stack trace writeout",
                    [c_try_compile, "#include <execinfo.h>\nint main(void) { backtrace(0, 0); }",
                      "Checking for backtrace() ... "], "", "", ["core"]]], cfg)

    parser = OptionParser();

    # Get configuration parameters from environment variables
    for i in cfg:
        if i in os.environ:
            cfg[i][0] = os.environ[i]

    # Enable disable libraries for linking
    parser.add_option("-e", "--enable", dest="enable", action="append",
                      help="force enable library linking", metavar="libfoo")
    parser.add_option("-d", "--disable", dest="disable", action="append",
                      help="disable library linking", metavar="libfoo")

    # Add cfg config options
    for i in cfg:
        parser.add_option("", "--"+i, dest=i, metavar=cfg[i][0], help=cfg[i][1])

    (options, args) = parser.parse_args();

    #
    # Enable/Disable libraries as user requested
    # These are not checked later
    #
    if options.enable:
        for i in options.enable:
            l.set(i, True);
    if options.disable:
        for i in options.disable:
            l.set(i, False);

    for i in cfg:
        if getattr(options, i) is not None:
            cfg[i][0] = getattr(options, i)

    #
    # Handle args such as CC=gcc passed after options
    #
    cfg_parse_args(cfg, args)

    basic_checks(cfg);

    cfg['CFLAGS'][0] += " -std=gnu99";

    l.check()
    l.print_summary()

    write_config_h(cfg, l)
    write_config_mk(cfg, l)
    write_gfxprim_config(cfg, l)
    write_pkgconfig(cfg, l)
