#!/usr/bin/env python
import argparse
import json
import os.path
import re
import shlex
import subprocess
import sys

try:
    basestring
except NameError:
    basestring = str


def freeze(obj):
    if isinstance(obj, dict):
        return freeze(set([freeze(x) for x in obj.items()]))
    if isinstance(obj, list):
        return tuple([freeze(x) for x in obj])
    if isinstance(obj, set):
        return frozenset({freeze(x) for x in obj})
    if isinstance(obj, tuple):
        return tuple([freeze(x) for x in obj])
    return obj


def parsecommand(command, resolvepath):
    if (isinstance(command, basestring)):
        command = shlex.split(command)
    words = iter(command)
    next(words)  # remove the initial 'cc' / 'c++'

    options = []
    defines = []
    includes = []
    system_includes = set()
    iquote_includes = set()

    for word in words:
        if word == '-o':
            next(words)
            continue
        elif word.startswith('-I'):
            include = word[2:]
            includes.append(resolvepath(include))
        elif word == '-isystem':
            include = next(words)
            include = resolvepath(include)
            if include not in includes:
                includes.append(include)
            system_includes.add(include)
        elif word == '-iquote':
            include = next(words)
            include = resolvepath(include)
            if include not in includes:
                includes.append(include)
            iquote_includes.add(include)
        elif word.startswith('-D'):
            defines.append(word[2:])
        elif word == '-c':
            continue
        elif word in ['-arch', '-include', '-x']:
            options.append(word)
            options.append(next(words))
        elif word.startswith('-'):
            options.append(word)

    return {
        'options': options,
        'defines': defines,
        'includes': includes,
        'system_includes': system_includes,
        'iquote_includes': iquote_includes,
    }


def get_include_path(include_path, source_dir):
    result = os.path.relpath(include_path, source_dir)
    # For paths that are not below the source root write the absolute path
    # to the CMakeLists instead of ../../../usr/local/include
    if os.path.isabs(include_path) and result.startswith(os.path.pardir):
        return include_path
    return result


class CompilationDatabase(object):
    def __init__(self):
        self.targets = {}

    def read(self, input):
        database = json.load(input)
        for entry in database:
            directory = entry['directory']

            def resolve(path):
                if not os.path.isabs(path):
                    path = os.path.join(directory, path)
                return os.path.normcase(os.path.normpath(path))

            if 'command' in entry.keys():
                command = freeze(
                    parsecommand(
                        entry['command'], resolve))
            if 'arguments' in entry.keys():
                command = freeze(
                    parsecommand(
                        entry['arguments'], resolve))

            file = resolve(entry['file'])
            self.targets.setdefault(command, set()).add(file)

    def write(self, output, directory=None, name='autogenerated'):
        output.write('cmake_minimum_required(VERSION 2.8.8)\n')
        output.write('project({})\n\n'.format(name))

        used_names = {""}
        disallowed_characters = re.compile("[^A-Za-z0-9_.+\-]")

        for (config, files) in self.targets.items():
            files = list(files)
            config = {k: v for (k, v) in config}
            name = os.path.basename(os.path.commonprefix(files).rstrip("/_"))
            name = re.sub(disallowed_characters, "", name)
            if name in used_names:
                index = 2
                while True:
                    candidate = '{}_{}'.format(name, index)
                    if candidate not in used_names:
                        name = candidate
                        break
                    index = index + 1
            used_names.add(name)

            output.write('add_library(%s OBJECT\n' % name)
            for file in files:
                output.write('    %s\n' % file)
            output.write(')\n')

            if config.get('options'):
                output.write('target_compile_options(%s PRIVATE\n' % name)
                for option in config['options']:
                    output.write('    %s\n' % option)
                output.write(')\n')

            if config.get('defines'):
                output.write('target_compile_definitions(%s PRIVATE\n' % name)
                for define in config['defines']:
                    output.write('    %s\n' % define)
                output.write(')\n')

            output.write('target_include_directories(%s PRIVATE\n' % name)
            for include in config['includes']:
                if directory is not None:
                    include = get_include_path(include, directory)
                output.write('    %s\n' % include)
            output.write(')\n')

            if config.get('system_includes'):
                output.write(
                    'target_include_directories(%s SYSTEM PRIVATE\n' % name)
                for include in config['system_includes']:
                    if directory is not None:
                        include = get_include_path(include, directory)
                    output.write('    %s\n' % include)
                output.write(')\n\n')
            if config.get('iquote_includes'):
                output.write(
                    'target_include_directories(%s BEFORE PRIVATE\n' % name)
                for include in config['iquote_includes']:
                    if directory is not None:
                        include = get_include_path(include, directory)
                    output.write('    %s\n' % include)
                output.write(')\n\n')


def get_default_name(compilation_database):
    filename = os.path.realpath(compilation_database.name)
    if not os.path.isfile(filename):
        return 'autogenerated'
    projectdir = os.path.dirname(filename)
    try:
        output = subprocess.Popen(
            ['git', 'rev-parse', '--show-toplevel'],
            cwd=projectdir, stdout=subprocess.PIPE).communicate()[0]
        output = output.strip()
        return os.path.basename(output)
    except Exception:
        return 'autogenerated'


def main():
    parser = argparse.ArgumentParser(description="""
        Convert a compile_commands.json file to a CMakeLists.txt file.
    """)

    infile = 'compile_commands.json' if os.isatty(sys.stdin.fileno()) else '-'
    parser.add_argument(
        'infile', nargs='?', type=argparse.FileType('r'), default=infile,
        help="""
path of the compilation database (default: compile_commands.json or stdin)
        """
    )
    outfile = 'CMakeLists.txt' if os.isatty(sys.stdout.fileno()) else '-'
    parser.add_argument(
        'outfile', nargs='?', type=argparse.FileType('w'), default=outfile,
        help="""
path of the CMake file (default: CMakeLists.txt or stdout)
        """
    )

    parser.add_argument(
        '-n', '--name', help="""
name of the CMake project (default: taken from Git root, or 'autogenerated')
        """
    )
    args = parser.parse_args()

    if args.name is None:
        args.name = get_default_name(args.infile)

    database = CompilationDatabase()
    database.read(args.infile)
    database.write(args.outfile, name=args.name)


if __name__ == '__main__':
    main()
