#!/usr/bin/env python3

import re
from collections import defaultdict
from pathlib import Path


def get_program_parameters():
    import argparse
    description = 'Generate a find_package(VTK COMPONENTS ...) that lists all modules referenced by a set of files.'
    epilogue = '''
This uses the VTK source folder to determine the modules and headers.
Then the user files/folders are looked at to find the headers being used.
Finally a find_package() is output with the modules you need for
 inclusion in your CMakeLists.txt file.

Note:
1) If include file(s) are not found when building your application.
    You may need to search the VTK source to find where the file is.
    Then look at contents of vtk.module in that folder and add the
     module name to the find_package statement. 
2) If linking fails, it usually means that the needed module has not been
     built, so you may need to add it to your VTK build and rebuild VTK.
3) More modules than strictly necessary may be included.     
    '''
    parser = argparse.ArgumentParser(description=description, epilog=epilogue,
                                     formatter_class=argparse.RawDescriptionHelpFormatter)
    parser.add_argument('vtk_path', help='The path to the VTK source tree.')
    parser.add_argument('application', nargs='+', help='Paths to the application files or folders.')
    args = parser.parse_args()
    return args.vtk_path, args.application


def check_paths(vtk_src_dir, application_srcs):
    """
    Check that the paths are valid.

    :param vtk_src_dir: The path to the VTK source.
    :param application_srcs: The user source files to be scanned for modules.
    :return: True if paths, files exist.
    """
    ok = True
    if not Path(vtk_src_dir).is_dir():
        print('The path to your VTK Source folder does not exist.')
        print(Path(vtk_src_dir))
        ok = False
    bad_paths = list()
    for f in application_srcs:
        p = Path(f)
        if not (p.is_dir() or p.is_file()):
            bad_paths.append(p)
    if bad_paths:
        print('These application paths or files do not exist.')
        for p in bad_paths:
            print(p)
            ok = False
    return ok


def find_vtk_modules(vtk_src_dir):
    """
    Build a dict of the VTK Module name, library name(if it exists) and any header files.

    :param vtk_src_dir: The path to the VTK source folder
    :return: Module name, library name and headers.
    """
    vtk_modules = defaultdict(dict)
    modules = [f for f in Path(vtk_src_dir).rglob('vtk.module')]
    # Includes in the module path
    file_patterns = ['vtk*.h', 'vtk*.h.in', 'QVTK*.h', '*QtQuick.h']
    for module in modules:
        content = module.read_text()
        args = content.split('\n')
        #  Assuming NAME is always there.
        if 'NAME' in args:
            name = args[args.index('NAME') + 1].strip()
            if 'LIBRARY_NAME' in args:
                library_name = args[args.index('LIBRARY_NAME') + 1].strip()
            else:
                library_name = None
            headers = list()
            for pattern in file_patterns:
                if pattern == 'vtk*.h.in':
                    # These files will be converted to header files in the build folder of VTK.
                    headers.extend([f.with_suffix('').name for f in module.parent.glob(pattern)])
                else:
                    headers.extend([f.name for f in module.parent.glob(pattern)])
            vtk_modules[name]['library_name'] = library_name
            vtk_modules[name]['headers'] = headers
    return vtk_modules


def build_headers_modules(modules):
    """
    Make a dictionary whose key is the header filename and value is the module.

    :param modules: The modules.
    :return: Headers and their corresponding module.
    """
    # The headers should be unique to a module, however we will not assume this.
    headers_modules = defaultdict(set)
    for k, v in modules.items():
        if 'headers' in v:
            for k1 in v['headers']:
                headers_modules[k1].add(k)
    return headers_modules


def find_application_includes(path):
    """
    Build a set that contains the vtk includes found in the file.

    :param path: The path to the application file.
    :return: The includes that were found.
    """
    includes = set()
    include_hdr1 = re.compile(r'((?:vtk|QVTK).*\.h)')
    include_hdr2 = re.compile(r'(\w+QtQuick\.h)')
    content = path.read_text()
    incs = include_hdr1.findall(content)
    includes.update(incs)
    incs = include_hdr2.findall(content)
    includes.update(incs)
    return includes


def generate_find_package(vtk_src_dir, application_srcs):
    """
    Generate the find_package statement.
    
    :param vtk_src_dir: The VTK source folder.
    :param application_srcs: A list of application folders and or files.
    :return: The find_package statement.
    """
    vtk_modules = find_vtk_modules(vtk_src_dir)
    # Test to see if VTK source is provided
    if len(vtk_modules) == 0:
        print(vtk_src_dir, 'is not a VTK source directory. It does not contain any vtk.module files.')
        return None
    vtk_headers_modules = build_headers_modules(vtk_modules)

    valid_extensions = ['.h', '.hxx', '.txx', '.cpp', '.cxx', '.cc']

    # Build a set of includes for all command line files
    all_includes = set()
    for app_src in application_srcs:
        p = Path(app_src)
        if p.is_file():
            if p.suffix in valid_extensions:
                all_includes.update(find_application_includes(p))
        elif p.is_dir():
            paths = list()
            for ext in valid_extensions:
                paths.extend([f for f in p.rglob('*' + ext) if f.is_file()])
            for path in paths:
                all_includes.update(find_application_includes(path))
    if len(all_includes) == 0:
        print('No VTK includes found in the application files.')
        return None

    # Build a set that contains all modules referenced in the user files.
    all_modules = set()
    for inc in all_includes:
        if inc in vtk_headers_modules:
            for m in vtk_headers_modules[inc]:
                all_modules.add(m)

    if 'VTK::RenderingCore' in all_modules:
        all_modules.add('VTK::RenderingOpenGL2')
        all_modules.add('VTK::InteractionStyle')
        all_modules.add('VTK::RenderingFreeType')
        all_modules.add('VTK::RenderingGL2PSOpenGL2')
        all_modules.add('VTK::RenderingContextOpenGL2')
    if 'VTK::DomainsChemistry' in all_modules:
        all_modules.add('VTK::DomainsChemistryOpenGL2')
    if 'VTK::RenderingVolume' in all_modules:
        all_modules.add('VTK::RenderingVolumeOpenGL2')
    if 'VTK::RenderingContext2D' in all_modules:
        all_modules.add('VTK::RenderingContextOpenGL2')
    if 'VTK::IOExport' in all_modules:
        all_modules.add('VTK::RenderingContextOpenGL2')
        all_modules.add('VTK::IOExportOpenGL2')
        all_modules.add('VTK::IOExportPDF')
        all_modules.add('VTK::RenderingContextOpenGL2')

    res = ['All modules referenced in your files:', 'find_package(VTK', ' COMPONENTS']
    for m in sorted(all_modules):
        res.append(' ' * 2 + m.replace('VTK::', ''))
    res.append(')')
    res.append(
        'Your application code includes ' + str(len(all_modules)) + ' of ' + str(
            len(vtk_modules)) + ' vtk modules.')
    return res


def main():
    vtk_src_dir, application_srcs = get_program_parameters()
    if not check_paths(vtk_src_dir, application_srcs):
        return

    res = generate_find_package(vtk_src_dir, application_srcs)
    if res:
        print('\n'.join(res))


if __name__ == '__main__':
    print('If you have built VTK and\n modules.json is available in the build folder.')
    print(' Please consider using FindNeededModules.py instead.')
    main()
