#!/usr/bin/env python2.7

from __future__ import print_function

import argparse
import errno
import os
import shlex
import subprocess
import sys
import tempfile


def check_c_compiles(cmd, code):
    with tempfile.NamedTemporaryFile(suffix='.c') as f:
        f.write(code)
        f.flush()
        cmd += [f.name, '-o', '/dev/null']
        proc = subprocess.Popen(
                  cmd,
                  stdout=subprocess.PIPE,
                  stderr=subprocess.PIPE)
        out, err = proc.communicate()
        ret = proc.returncode
        return ret == 0


def makedirs(path):
    try:
        os.makedirs(path)
    except OSError as e:
        if e.errno == errno.EEXIST and os.path.isdir(path):
            pass
        else:
            raise


def pkg_config(lib):
    def _pkgconfig(lib, query):
        lib = lib.replace('>=', ' >= ')
        cmd = 'pkg-config {} {}'.format(query, lib).split()
        popen = subprocess.Popen(cmd,
                                 stdout=subprocess.PIPE,
                                 stderr=subprocess.PIPE)
        out, err = popen.communicate()
        if popen.returncode != 0:
            print(cmd)
            print(err)
            raise ValueError("Could not find library: {}".format(lib))
        return out.split()

    lf = _pkgconfig(lib, '--libs-only-l')
    cf = _pkgconfig(lib, '--cflags')
    ldf = _pkgconfig(lib, '--libs-only-L')
    print("Found", lib, "in:", ldf)
    return (lf, cf, ldf)


def ninja_escape(s):
    s = s.replace(' ', '$ ')
    s = s.replace('\n', '$\n')
    return s


def ninja_join(words):
    indent = '        '
    if len(words) > 1:
        prefix = '$\n' + indent
    else:
        prefix = ''
    spacer = ' $\n' + indent
    return prefix + spacer.join(ninja_escape(s) for s in words)


def ninja_write_vars(f, **kwargs):
    for k, v in kwargs.items():
        f.write('{} = {}\n'.format(k, v))
    f.write('\n')


def ninja_write_rule(f, name, **kwargs):
    f.write('rule {}\n'.format(name))
    for k, v in kwargs.items():
        f.write('  {} = {}\n'.format(k, v))
    f.write('\n')


def remove_prefix(s, prefix):
    while prefix and s.startswith(prefix):
        s = s[len(prefix):]
    return s


def install(prefix, suffix, dest_dir, dest_suffix=''):
    src_path = os.path.join(prefix, suffix) if suffix else prefix
    fname = os.path.basename(src_path)
    if dest_suffix:
        dest_dir = os.path.join(dest_dir, dest_suffix)
    dst_path = os.path.join(dest_dir, fname)
    return [(src_path, dst_path)]


def install_dir(prefix, suffix, dest_dir, dest_suffix=''):
    result = []
    src_path = os.path.join(prefix, suffix)
    for root, dirs, files in os.walk(src_path):
        for f in files:
            fname = os.path.join(root, f)
            fsuffix = remove_prefix(remove_prefix(fname, prefix), '/')
            result += install(prefix, fsuffix, dest_dir, os.path.join(dest_suffix, os.path.dirname(fsuffix)))
    return result


def remove_duplicates(ls):
    result = []
    for x in ls:
        if x not in result:
            result.append(x)
    return result


class Executable(object):
    def __init__(self, name):
        self.name = name
        self.cc = 'cc'
        self.cflags = []
        self.lflags = []
        self.libs = []
        self.sources = []
        self.pos = []
        self.install_exec = []
        self.install_data = []

    def write_ninja(self, f):
        f.write('# Executable: {}\n'.format(self.name))
        ninja_write_vars(f, **{
            'cc_' + self.name: self.cc,
            'cflags_' + self.name: ninja_join(remove_duplicates(self.cflags)),
            'lflags_' + self.name: ninja_join(remove_duplicates(self.lflags)),
            'libs_' + self.name: ninja_join(remove_duplicates(self.libs))})
        ninja_write_rule(f, 'cc_' + self.name,
            command='$cc_{} -MMD -MT $out -MF $out.d $cflags_{} -c $in -o $out'.format(self.name, self.name),
            description='CC $out',
            depfile='$out.d',
            deps='gcc')
        ninja_write_rule(f, 'link_' + self.name,
            command='$cc_{} $lflags_{} -o $out $in $libs_{}'.format(self.name, self.name, self.name),
            description='LINK $out')
        ninja_write_rule(f, 'po2mo_' + self.name,
            command='msgfmt -o $out $in',
            description='GEN $out')
        f.write('# Compilation\n')
        for src in self.sources:
            f.write('build $build_dir/{}.{}.o: cc_{} $source_dir/{}\n'.format(src, self.name, self.name, src))
        f.write('# Translation\n')
        for po in self.pos:
            f.write('build $build_dir/{}.mo: po2mo_{} $source_dir/{}\n'.format(os.path.splitext(po)[0], self.name, po))
        f.write('# Linking\n')
        f.write('build $build_dir/{}: link_{} '.format(self.name, self.name))
        f.write(ninja_join(['$build_dir/{}.{}.o'.format(src, self.name) for src in self.sources]))
        f.write('\n')
        f.write('# Installation\n')
        for fin, fout in self.install_exec:
            f.write('build {}: install_exec {}\n'.format(fout, fin))
        for fin, fout in self.install_data:
            f.write('build {}: install_data {}\n'.format(fout, fin))
        f.write('build install_{}: phony '.format(self.name))
        f.write(ninja_join([fout for fin, fout in self.install_exec + self.install_data]))
        f.write('\n')
        f.write('# Uninstallation\n')
        for fin, fout in self.install_exec + self.install_data:
            f.write('build uninstall_{}: uninstall {}\n'.format(fout, fout))
        f.write('build uninstall_{}: phony '.format(self.name))
        f.write(ninja_join(['uninstall_{}'.format(fout) for fin, fout in self.install_exec + self.install_data]))
        f.write('\n')
        f.write('\n')



def generate_ninja(targets, source_dir, build_dir):
    ninja_file_name = os.path.join(build_dir, 'build.ninja')
    f = open(ninja_file_name, 'w')
    # `deps` was introduced in ninja 1.3.
    ninja_write_vars(f, ninja_required_version='1.3')
    ninja_write_vars(f, **{
        'source_dir':source_dir,
        'build_dir':build_dir
    })
    ninja_write_rule(f, 'install_exec',
        command='install -D -m0755 $in $out',
        description='INSTALL $out')
    ninja_write_rule(f, 'install_data',
        command='install -D -m0644 $in $out',
        description='INSTALL $out')
    ninja_write_rule(f, 'uninstall',
        command='rm -f $in',
        description='RM $in')
    for t in targets:
        t.write_ninja(f)
    f.write('# Targets\n')
    f.write('build all: phony ')
    f.write(ninja_join(['$build_dir/{}'.format(t.name) for t in targets]))
    f.write('\n')
    installs = sum([(t.install_exec + t.install_data) for t in targets], [])
    if installs:
        f.write('build install: phony ')
        f.write(ninja_join(['install_{}'.format(t.name) for t in targets]))
        f.write('\n')
        f.write('build uninstall: phony ')
        f.write(ninja_join(['uninstall_{}'.format(t.name) for t in targets]))
        f.write('\n')
    f.write('\n')
    f.write('default all\n')
    f.close()
    print('Wrote {}.'.format(ninja_file_name))
    print('Run `ninja -v -C {} all` to compile.'.format(build_dir))
    print('Run `ninja -v -C {} install` to install.'.format(build_dir))


# Parse CLI options
parser = argparse.ArgumentParser()
parser.add_argument('--uevent', dest='uevent', action='store_true',
                    help='Enable uevent support. Default: on under Linux')
parser.add_argument('--debug', dest='debug', action='store_true',
                    help='Enable debug build. Default: off')
parser.add_argument('--asan', dest='asan', action='store_true',
                    help='Enable AddressSanitizer. Default: off')
parser.add_argument('--tracing', dest='tracing', action='store_true',
                    help='Enable tracing. Default: off')
parser.add_argument('--memory-tracing', dest='memory_tracing', action='store_true',
                    help='Enable memory allocation tracing. Default: off')
parser.add_argument('--prefix', help='Prefix for constructing the file installation paths. Default: /usr/local', default=None)
parser.add_argument('--exec_prefix', help='Prefix for binary paths. Default: $prefix', default=None)
parser.add_argument('--bindir', help='Path where executables must be installed. Default: $exec_prefix/bin', default=None)
parser.add_argument('--sysconfdir', help='Path where config files must be installed. Default: /etc', default=None)
parser.add_argument('--datarootdir', help='Path where data files must be installed. Default: $prefix/share', default=None)
parser.add_argument('--localedir', help='Path where locale files must be installed. Default: $datarootdir/locale', default=None)
parser.add_argument('--docdir', help='Path where documentation files must be installed. Default: $datarootdir/doc/tint2', default=None)
parser.add_argument('--htmldir', help='Path where documentation files must be installed. Default: $docdir/html', default=None)
parser.add_argument('--mandir', help='Path where man files must be installed. Default: $datarootdir/man', default=None)
parser.add_argument('--home', dest='home', action='store_true',
                    help='Install to $HOME (sets all paths accordingly). Default: off')
args = parser.parse_args()

# Get relevant environment variables
CC = os.environ.get('CC', 'cc')
CFLAGS = shlex.split(os.environ.get('CFLAGS', ''))
LFLAGS = shlex.split(os.environ.get('LDFLAGS', ''))
LIBS = []

# Get paths
source_dir = os.path.realpath(os.path.dirname(os.path.realpath(__file__)) + '/..')
build_dir = os.path.join(os.getcwd(), 'build')
if not args.home:
    prefix = args.prefix or '/usr/local'
    exec_prefix = args.exec_prefix or prefix
    bindir = args.bindir or os.path.join(exec_prefix, 'bin')
    datarootdir = args.datarootdir or os.path.join(prefix, 'share')
    sysconfdir = args.sysconfdir or '/etc'
    docdir = args.docdir or os.path.join(datarootdir, 'doc/tint2')
    htmldir = args.htmldir or os.path.join(docdir, 'html')
    localedir = args.localedir or os.path.join(datarootdir, 'locale')
    mandir = args.mandir or os.path.join(datarootdir, 'man')
else:
    prefix = args.prefix or os.path.expanduser("~")
    exec_prefix = args.exec_prefix or prefix
    bindir = args.bindir or os.path.join(exec_prefix, 'bin')
    datarootdir = args.datarootdir or os.path.join(prefix, '.local/share')
    sysconfdir = args.sysconfdir or os.path.expanduser("~/.config/tint2")
    docdir = args.docdir or os.path.join(datarootdir, 'doc/tint2')
    htmldir = args.htmldir or os.path.join(docdir, 'html')
    localedir = args.localedir or os.path.join(datarootdir, 'locale')
    mandir = args.mandir or os.path.join(datarootdir, 'man')


# Check if C11 is supported by the compiler, fall back to C99
if check_c_compiles([CC],
                    '''#define print(x) _Generic((x), default : print_unknown)(x)
                    void print_unknown() {
                    }
                    int main () {
                        print(0);
                    }'''):
    CFLAGS += ['-std=c11', '-DHAS_GENERIC']
else:
    print("No C11 support.")
    CFLAGS += ['-std=c99']

# Set mandatory flags
CFLAGS += ['-g',
           '-Wall',
           '-Wextra',
           '-Wshadow',
           '-Wpointer-arith',
           '-Wno-deprecated',
           '-Wno-missing-field-initializers',
           '-Wno-unused-parameter',
           '-Wno-sign-compare',
           '-fno-strict-aliasing',
           '-pthread',
           '-D_BSD_SOURCE',
           '-D_DEFAULT_SOURCE',
           '-D_WITH_GETLINE',
           '-DENABLE_BATTERY']
LFLAGS += ['-fno-strict-aliasing',
           '-pthread']
LFLAGS += ['-L' + build_dir]

# Set platform dependent C flags
if sys.platform.startswith('linux'):
    CFLAGS += ['-D_POSIX_C_SOURCE=200809L']

if sys.platform.startswith('freebsd') or sys.platform.startswith('openbsd') or sys.platform.startswith('dragonfly'):
    CFLAGS += ['-I/usr/local/include']
    LFLAGS += ['-L/usr/local/lib']

if sys.platform.startswith('linux') or args.uevent:
    CFLAGS += ['-DENABLE_UEVENT']

# Turn on color messages if supported
if check_c_compiles([CC, '-fdiagnostics-color', '-c', '-x', 'c'], ''):
    CFLAGS += ['-fdiagnostics-color=always']

# Set project-specific include dirs
CFLAGS += ['-I.']
for inc in ['src',
            'src/battery',
            'src/clock',
            'src/systray',
            'src/taskbar',
            'src/launcher',
            'src/tooltip',
            'src/util',
            'src/execplugin',
            'src/button',
            'src/freespace',
            'src/separator']:
    CFLAGS += ['-I' + os.path.join(source_dir, inc)]

# Add mandatory library dependencies
LIBS += ['-lm', '-lrt']

# Add mandatory libray dependencies detected with pkg-config
for dep in ['x11',
            'xcomposite',
            'xdamage',
            'xinerama',
            'xext',
            'xrender',
            'xrandr>=1.3',
            'gmodule-2.0',
            'gio-2.0',
            'glib-2.0',
            'gobject-2.0',
            'pangocairo',
            'pango',
            'cairo',
            'imlib2>=1.4.2']:
    lib, cf, lf = pkg_config(dep)
    LIBS += lib
    CFLAGS += cf
    LFLAGS += lf

# Add optional library dependencies detected with pkg-config
try:
    lib, cf, lf = pkg_config('librsvg-2.0>=2.14.0')
    LIBS += lib
    CFLAGS += cf + ['-DHAVE_RSVG']
    LFLAGS += lf
except:
    print("No SVG support.")

try:
    lib, cf, lf = pkg_config('libstartup-notification-1.0>=0.12')
    LIBS += lib
    CFLAGS += cf + ['-DHAVE_SN', '-DSN_API_NOT_YET_FROZEN']
    LFLAGS += lf
except:
    print("No startup notification support.")

# Add library dependencies detected with using successful compilation test
bt = False
if not bt:
    try:
        lib, cf, lf = pkg_config('libunwind')
        LIBS += lib
        CFLAGS += cf + ['-DHAS_LIBUNWIND']
        LFLAGS += lf
        bt = True
        print("Backtrace support via libunwind.")
    except:
        print("No backtrace support via libunwind.")
if not bt:
    if check_c_compiles([CC, '-lbacktrace'], '''#include <backtrace.h>
            int main() {
                return 0;
            }'''):
        CFLAGS += ['-DHAS_BACKTRACE']
        LIBS += ['-lbacktrace']
        bt = True
        print("Backtrace support via libbacktrace.")
    else:
        print("No backtrace support via libbacktrace.")

# Add option-dependent flags
if not args.debug:
    CFLAGS += ['-O2']

if args.asan:
    asan_flags = ['-fsanitize=address']
    CFLAGS += asan_flags
    LFLAGS += asan_flags

if args.tracing:
    CFLAGS += ['-finstrument-functions',
               '-finstrument-functions-exclude-file-list=tracing.c',
               '-finstrument-functions-exclude-function-list=get_time,gettime']

if args.asan or args.memory_tracing or args.tracing:
    trace_flags = ['-O0',
                   '-fno-common',
                   '-fno-omit-frame-pointer',
                   '-rdynamic',
                   '-DUSE_REAL_MALLOC']
    CFLAGS += trace_flags
    LFLAGS += trace_flags + ['-fuse-ld=gold']

if args.memory_tracing:
    LIBS += ['-ldl']

# Define targets
tint2 = Executable('tint2')
tint2.cflags += CFLAGS
tint2.lflags += LFLAGS
tint2.libs += LIBS
tint2.sources = ['src/config.c',
                 'src/panel.c',
                 'src/util/server.c',
                 'src/main.c',
                 'src/init.c',
                 'src/util/signals.c',
                 'src/util/tracing.c',
                 'src/mouse_actions.c',
                 'src/drag_and_drop.c',
                 'src/default_icon.c',
                 'src/clock/clock.c',
                 'src/systray/systraybar.c',
                 'src/launcher/launcher.c',
                 'src/launcher/apps-common.c',
                 'src/launcher/icon-theme-common.c',
                 'src/launcher/xsettings-client.c',
                 'src/launcher/xsettings-common.c',
                 'src/taskbar/task.c',
                 'src/taskbar/taskbar.c',
                 'src/taskbar/taskbarname.c',
                 'src/tooltip/tooltip.c',
                 'src/execplugin/execplugin.c',
                 'src/button/button.c',
                 'src/freespace/freespace.c',
                 'src/separator/separator.c',
                 'src/tint2rc.c',
                 'src/util/area.c',
                 'src/util/bt.c',
                 'src/util/common.c',
                 'src/util/fps_distribution.c',
                 'src/util/strnatcmp.c',
                 'src/util/timer.c',
                 'src/util/cache.c',
                 'src/util/color.c',
                 'src/util/strlcat.c',
                 'src/util/print.c',
                 'src/util/gradient.c',
                 'src/util/test.c',
                 'src/util/uevent.c',
                 'src/util/window.c',
                 'src/battery/battery.c']

# Battery implementation is platform-specific
if sys.platform.startswith('linux'):
    tint2.sources += ['src/battery/linux.c']
elif sys.platform.startswith('freebsd') or \
     sys.platform.startswith('dragonfly') or \
     sys.platform.startswith('gnukfreebsd'):
    tint2.sources += ['src/battery/freebsd.c']
elif sys.platform.startswith('openbsd') or \
     sys.platform.startswith('netbsd'):
    tint2.sources += ['src/battery/openbsd.c']
else:
    print("No battery support for platform:", sys.platform)
    tint2.sources += ['src/battery/dummy.c']

if args.memory_tracing:
    tint2.sources += ['src/util/mem.c']

tint2.install_exec = install(build_dir, 'tint2', bindir)
tint2.install_data = (install(source_dir, 'tint2.svg', datarootdir, 'icons/hicolor/scalable/apps') +
                      install(source_dir, 'tint2.desktop', datarootdir, 'applications') +
                      install(source_dir, 'themes/tint2rc', sysconfdir, 'xdg/tint2') +
                      install(source_dir, 'default_icon.png', datarootdir, 'tint2') +
                      install(source_dir, 'AUTHORS', docdir) +
                      install(source_dir, 'ChangeLog', docdir) +
                      install(source_dir, 'README.md', docdir) +
                      install(source_dir, 'doc/tint2.md', docdir) +
                      install(source_dir, 'doc/manual.html', htmldir) +
                      install(source_dir, 'doc/readme.html', htmldir) +
                      install_dir(source_dir, 'doc/images', htmldir) +
                      install(source_dir, 'doc/tint2.1', mandir, 'man1'))

tint2conf = Executable('tint2conf')
tint2conf.cflags += CFLAGS
tint2conf.lflags += LFLAGS
tint2conf.libs += LIBS

for dep in ['gthread-2.0',
            'gtk+-x11-2.0']:
    lib, cf, lf = pkg_config(dep)
    tint2conf.libs += lib
    tint2conf.cflags += cf
    tint2conf.lflags += lf

tint2conf.cflags += ['-DTINT2CONF',
                     '-DINSTALL_PREFIX=\\"{}\\"'.format(prefix),
                     '-DLOCALEDIR=\\"{}\\"'.format(localedir),
                     '-DGETTEXT_PACKAGE=\\"tint2conf\\"',
                     '-DHAVE_VERSION_H',
                     '-Wno-shadow']

tint2conf.sources = ['src/util/bt.c',
                     'src/util/common.c',
                     'src/util/strnatcmp.c',
                     'src/util/cache.c',
                     'src/util/timer.c',
                     'src/util/test.c',
                     'src/util/print.c',
                     'src/util/signals.c',
                     'src/config.c',
                     'src/util/server.c',
                     'src/util/strlcat.c',
                     'src/launcher/apps-common.c',
                     'src/launcher/icon-theme-common.c',
                     'src/tint2conf/md4.c',
                     'src/tint2conf/main.c',
                     'src/tint2conf/properties.c',
                     'src/tint2conf/properties_rw.c',
                     'src/tint2conf/theme_view.c',
                     'src/tint2conf/background_gui.c',
                     'src/tint2conf/gradient_gui.c']
tint2conf.pos = [os.path.join('src/tint2conf/po', f) for f in os.listdir('src/tint2conf/po') if f.endswith('.po')]
tint2conf.install_exec = install(build_dir, 'tint2conf', bindir)
tint2conf.install_data = (install(source_dir, 'src/tint2conf/tint2conf.svg', datarootdir, 'icons/hicolor/scalable/apps') +
                          install(source_dir, 'src/tint2conf/tint2conf.desktop', datarootdir, 'applications') +
                          install(source_dir, 'src/tint2conf/tint2conf.xml', datarootdir, 'mime/packages'))

makedirs(build_dir)
assert 0 == os.system('cd {}; {}/get_version.sh'.format(build_dir, source_dir))
generate_ninja([tint2, tint2conf], source_dir, build_dir)
