#!/usr/bin/env -S python3 -tt
# -*- coding: utf-8 -*-
#
# SPDX-License-Identifier: Apache-2.0
# Copyright Pionix GmbH and Contributors to EVerest
#
"""
author: aw@pionix.de
FIXME (aw): Module documentation.
"""

from ev_cli import __version__
from ev_cli import helpers
from ev_cli.type_parsing import TypeParser
from ev_cli.error_parsing import ErrorParser

from datetime import datetime
from pathlib import Path
import jinja2 as j2
import argparse
import stringcase
from typing import List


# FIXME (aw): remove these global variables

# global variables
everest_dirs: List[Path] = []
work_dir: Path = None

# jinja template environment and global variable
env = j2.Environment(loader=j2.FileSystemLoader(Path(__file__).parent / 'templates'),
                     lstrip_blocks=True, trim_blocks=True, undefined=j2.StrictUndefined,
                     keep_trailing_newline=True)

templates = {}
validators = {}

# Function declarations


def setup_jinja_env():
    env.globals['timestamp'] = datetime.utcnow()
    # FIXME (aw): which repo to use? everest or everest-framework?
    env.filters['snake_case'] = helpers.snake_case
    env.filters['create_dummy_result'] = helpers.create_dummy_result

    templates.update({
        'interface_base': env.get_template('interface-Base.hpp.j2'),
        'interface_exports': env.get_template('interface-Exports.hpp.j2'),
        'interface_impl.hpp': env.get_template('interface-Impl.hpp.j2'),
        'interface_impl.cpp': env.get_template('interface-Impl.cpp.j2'),
        'types.hpp': env.get_template('types.hpp.j2'),
        'module.hpp': env.get_template('module.hpp.j2'),
        'module.cpp': env.get_template('module.cpp.j2'),
        'ld-ev.hpp': env.get_template('ld-ev.hpp.j2'),
        'ld-ev.cpp': env.get_template('ld-ev.cpp.j2'),
        'cmakelists': env.get_template('CMakeLists.txt.j2'),
        'index.rst': env.get_template('index.rst.j2'),
    })


def generate_tmpl_data_for_if(interface, if_def, type_file):
    helpers.parsed_enums.clear()
    helpers.parsed_types.clear()
    helpers.type_headers.clear()
    types = []
    enums = []
    vars = []
    for var, var_info in if_def.get('vars', {}).items():
        (type_info, enum_info) = helpers.extended_build_type_info(var, var_info, type_file)
        if enum_info and type_file:
            enums.append(enum_info)

        vars.append(type_info)

    cmds = []
    for cmd, cmd_info in if_def.get('cmds', {}).items():
        args = []
        for arg, arg_info in cmd_info.get('arguments', {}).items():
            (type_info, enum_info) = helpers.extended_build_type_info(arg, arg_info, type_file)
            if enum_info and type_file:
                enums.append(enum_info)

            args.append(type_info)

        result_type_info = None
        if 'result' in cmd_info:
            result_info = cmd_info['result']

            (result_type_info, enum_info) = helpers.extended_build_type_info('result', result_info, type_file)
            if enum_info and type_file:
                enums.append(enum_info)

        cmds.append({'name': cmd, 'args': args, 'result': result_type_info})

    if type_file:
        for parsed_enum in helpers.parsed_enums:
            enum_info = {
                'name': parsed_enum['name'],
                'description': parsed_enum['description'],
                'enum_type': stringcase.capitalcase(parsed_enum['name']),
                'enum': parsed_enum['enums']
            }
            enums.append(enum_info)

    if type_file:
        for parsed_type in helpers.parsed_types:
            parsed_type['name'] = stringcase.capitalcase(parsed_type['name'])
            if 'properties' in parsed_type:
                for prop in parsed_type['properties']:
                    if 'type_dict' in prop['info']:
                        path = Path('generated/types') / \
                            prop['info']['type_dict']['type_relative_path'].with_suffix('.hpp')
                        helpers.type_headers.add(path.as_posix())

            types.append(parsed_type)

    error_lists = if_def.get('errors', [])
    # Use a dict to avoid duplicate error definitions
    errors_dict = {}
    for entry in error_lists:
        if not 'reference' in entry:
            raise Exception(f'Error definition {entry} does not have a reference.')
        for error in ErrorParser.resolve_error_reference(entry['reference']):
            if error.namespace not in errors_dict:
                errors_dict[error.namespace] = {}
            if error.name in errors_dict[error.namespace]:
                raise Exception(f'Error definition {error.namespace}/{error.name} already referenced.')
            errors_dict[error.namespace][error.name] = error
    errors = []
    for value in errors_dict.values():
        errors.extend(value.values())

    tmpl_data = {
        'info': {
            'base_class_header': f'generated/interfaces/{interface}/Implementation.hpp',
            'interface': interface,
            'desc': if_def['description'],
            'type_headers': sorted(helpers.type_headers)
        },
        'enums': enums,
        'types': types,
        'vars': vars,
        'cmds': cmds,
        'errors': errors,
    }

    return tmpl_data


def generate_tmpl_data_for_module(module, module_def):
    provides = []
    for impl, impl_info in module_def.get('provides', {}).items():
        config = []
        for conf_id, conf_info in impl_info.get('config', {}).items():
            type_info = helpers.build_type_info(conf_id, conf_info['type'])
            config.append(type_info)

        provides.append({
            'id': impl,
            'type': impl_info['interface'],
            'desc': impl_info['description'],
            'config': config,
            'class_name': f'{impl_info["interface"]}Impl',
            'base_class': f'{impl_info["interface"]}ImplBase',
            'base_class_header': f'generated/interfaces/{impl_info["interface"]}/Implementation.hpp'
        })

    requires = []
    for requirement_id, req_info in module_def.get('requires', {}).items():
        # min_connections=1 and max_connections=1 is the default if not provided otherwise (see manifest meta schema)
        is_vector = not (
            ('min_connections' not in req_info or req_info['min_connections'] == 1) and
            ('max_connections' not in req_info or req_info['max_connections'] == 1))
        requires.append({
            'id': requirement_id,
            'is_vector': is_vector,
            'type': req_info['interface'],
            'class_name': f'{req_info["interface"]}Intf',
            'exports_header': f'generated/interfaces/{req_info["interface"]}/Interface.hpp'
        })

    module_config = []
    for conf_id, conf_info in module_def.get('config', {}).items():
        type_info = helpers.build_type_info(conf_id, conf_info['type'])
        module_config.append(type_info)

    tmpl_data = {
        'info': {
            'name': module,
            'class_name': module,  # FIXME (aw): enforce capital case?
            'desc': module_def['description'],
            'module_header': f'{module}.hpp',
            'module_config': module_config,
            'ld_ev_header': 'ld-ev.hpp',
            'enable_external_mqtt': module_def.get('enable_external_mqtt', False),
            'enable_telemetry': module_def.get('enable_telemetry', False),
            'enable_global_errors': module_def.get('enable_global_errors', False)
        },
        'provides': provides,
        'requires': requires,
    }

    return tmpl_data


def construct_impl_file_paths(impl):
    interface = impl['type']
    common_part = f'{impl["id"]}/{interface}'
    return (f'{common_part}Impl.hpp', f'{common_part}Impl.cpp')


def set_impl_specific_path_vars(tmpl_data, output_path):
    """Set cpp_file_rel_path and class_header vars to implementation template data."""
    for impl in tmpl_data['provides']:
        (impl['class_header'], impl['cpp_file_rel_path']) = construct_impl_file_paths(impl)


def generate_module_loader_files(rel_mod_dir, output_dir):
    loader_files = []
    (_, _, mod) = rel_mod_dir.rpartition('/')

    mod_path = work_dir / f'modules/{rel_mod_dir}/manifest.yaml'
    if not mod_path.exists():
        raise Exception(f'Could not find module manifest ({mod_path}')

    mod_def = helpers.load_validated_module_def(mod_path, validators['module'])
    tmpl_data = generate_tmpl_data_for_module(mod, mod_def)

    set_impl_specific_path_vars(tmpl_data, mod_path.parent)

    # ld-ev.hpp
    tmpl_data['info']['hpp_guard'] = 'LD_EV_HPP'

    loader_files.append({
        'filename': 'ld-ev.hpp',
        'path': output_dir / mod / 'ld-ev.hpp',
        'printable_name': f'{mod}/ld-ev.hpp',
        'content': templates['ld-ev.hpp'].render(tmpl_data),
        'template_path': Path(templates['ld-ev.hpp'].filename),
        'last_mtime': mod_path.stat().st_mtime
    })

    # ld-ev.cpp
    loader_files.append({
        'filename': 'ld-ev.cpp',
        'path': output_dir / mod / 'ld-ev.cpp',
        'printable_name': f'{mod}/ld-ev.cpp',
        'content': templates['ld-ev.cpp'].render(tmpl_data),
        'template_path': Path(templates['ld-ev.cpp'].filename),
        'last_mtime': mod_path.stat().st_mtime
    })

    return loader_files


def generate_module_files(rel_mod_dir, update_flag, licenses):
    (_, _, mod) = rel_mod_dir.rpartition('/')

    mod_files = {'core': [], 'interfaces': [], 'docs': []}
    mod_path = work_dir / f'modules/{rel_mod_dir}/manifest.yaml'
    mod_def = helpers.load_validated_module_def(mod_path, validators['module'])

    default_license_dir = Path(__file__).parent / 'licenses'
    current_license_dir = work_dir / 'licenses'
    additional_license_dir = Path(licenses)
    license_dirs = [default_license_dir, current_license_dir, additional_license_dir]
    license_url = mod_def['metadata']['license']
    license_header = helpers.get_license_header(license_dirs, license_url)

    if not license_header:
        print(f'Could not find license "{license_url}" in {license_dirs}.')
        print('Consider providing a additonal custom license directory with --licenses')
        exit(1)

    tmpl_data = generate_tmpl_data_for_module(mod, mod_def)
    output_path = mod_path.parent
    # FIXME (aw): we might move the following function into generate_tmp_data_for_module
    set_impl_specific_path_vars(tmpl_data, output_path)

    cmakelists_blocks = {
        'version': 'v1',
        'format_str': '# ev@{uuid}:{version}',
        'regex_str': '^(?P<indent>\s*)# ev@(?P<uuid>[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}):(?P<version>.*)$',
        'definitions': {
            'add_general': {
                'id': 'bcc62523-e22b-41d7-ba2f-825b493a3c97',
                'content': '# insert your custom targets and additional config variables here'
            },
            'add_other': {
                'id': 'c55432ab-152c-45a9-9d2e-7281d50c69c3',
                'content': '# insert other things like install cmds etc here'
            }
        }
    }

    impl_hpp_blocks = {
        'version': 'v1',
        'format_str': '// ev@{uuid}:{version}',
        'regex_str': '^(?P<indent>\s*)// ev@(?P<uuid>[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}):(?P<version>.*)$',
        'definitions': {
            'add_headers': {
                'id': '75ac1216-19eb-4182-a85c-820f1fc2c091',
                'content': '// insert your custom include headers here'
            },
            'public_defs': {
                'id': '8ea32d28-373f-4c90-ae5e-b4fcc74e2a61',
                'content': '// insert your public definitions here'
            },
            'protected_defs': {
                'id': 'd2d1847a-7b88-41dd-ad07-92785f06f5c4',
                'content': '// insert your protected definitions here'
            },
            'private_defs': {
                'id': '3370e4dd-95f4-47a9-aaec-ea76f34a66c9',
                'content': '// insert your private definitions here'
            },
            'after_class': {
                'id': '3d7da0ad-02c2-493d-9920-0bbbd56b9876',
                'content': '// insert other definitions here'
            }
        }
    }

    mod_hpp_blocks = {
        'version': 'v1',
        'format_str': '// ev@{uuid}:{version}',
        'regex_str': '^(?P<indent>\s*)// ev@(?P<uuid>[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}):(?P<version>.*)$',
        'definitions': {
            'add_headers': {
                'id': '4bf81b14-a215-475c-a1d3-0a484ae48918',
                'content': '// insert your custom include headers here'
            },
            'public_defs': {
                'id': '1fce4c5e-0ab8-41bb-90f7-14277703d2ac',
                'content': '// insert your public definitions here'
            },
            'protected_defs': {
                'id': '4714b2ab-a24f-4b95-ab81-36439e1478de',
                'content': '// insert your protected definitions here'
            },
            'private_defs': {
                'id': '211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8',
                'content': '// insert your private definitions here'
            },
            'after_class': {
                'id': '087e516b-124c-48df-94fb-109508c7cda9',
                'content': '// insert other definitions here'
            }
        }
    }

    # provided interface implementations (impl cpp & hpp)
    for impl in tmpl_data['provides']:
        interface = impl['type']
        (impl_hpp_file, impl_cpp_file) = construct_impl_file_paths(impl)

        # load template data for interface
        if_def, last_mtime = load_interface_definition(interface)

        if_tmpl_data = generate_tmpl_data_for_if(interface, if_def, False)

        if_tmpl_data['info'].update({
            'hpp_guard': helpers.snake_case(f'{impl["id"]}_{interface}').upper() + '_IMPL_HPP',
            'config': impl['config'],
            'class_name': interface + 'Impl',
            'class_parent': interface + 'ImplBase',
            'module_header': f'../{mod}.hpp',
            'module_class': mod,
            'interface_implementation_id': impl['id']
        })

        if_tmpl_data['info']['blocks'] = helpers.load_tmpl_blocks(
            impl_hpp_blocks, output_path / impl_hpp_file, update_flag)
        if_tmpl_data['info']['license_header'] = license_header

        # FIXME (aw): time stamp should include parent interfaces modification dates
        mod_files['interfaces'].append({
            'abbr': f'{impl["id"]}.hpp',
            'path': output_path / impl_hpp_file,
            'printable_name': impl_hpp_file,
            'content': templates['interface_impl.hpp'].render(if_tmpl_data),
            'template_path': Path(templates['interface_impl.hpp'].filename),
            'last_mtime': last_mtime,
            'license_header': license_header
        })

        mod_files['interfaces'].append({
            'abbr': f'{impl["id"]}.cpp',
            'path': output_path / impl_cpp_file,
            'printable_name': impl_cpp_file,
            'content': templates['interface_impl.cpp'].render(if_tmpl_data),
            'template_path': Path(templates['interface_impl.cpp'].filename),
            'last_mtime': last_mtime,
            'license_header': license_header
        })

    cmakelists_file = output_path / 'CMakeLists.txt'
    tmpl_data['info']['blocks'] = helpers.load_tmpl_blocks(cmakelists_blocks, cmakelists_file, update_flag)
    tmpl_data['info']['license_header'] = license_header
    mod_files['core'].append({
        'abbr': 'cmakelists',
        'path': cmakelists_file,
        'content': templates['cmakelists'].render(tmpl_data),
        'template_path': Path(templates['cmakelists'].filename),
        'last_mtime': mod_path.stat().st_mtime
    })

    # module.hpp
    tmpl_data['info']['hpp_guard'] = helpers.snake_case(mod).upper() + '_HPP'
    mod_hpp_file = output_path / f'{mod}.hpp'
    tmpl_data['info']['blocks'] = helpers.load_tmpl_blocks(mod_hpp_blocks, mod_hpp_file, update_flag)
    mod_files['core'].append({
        'abbr': 'module.hpp',
        'path': mod_hpp_file,
        'content': templates['module.hpp'].render(tmpl_data),
        'template_path': Path(templates['module.hpp'].filename),
        'last_mtime': mod_path.stat().st_mtime,
        'license_header': license_header
    })

    # module.cpp
    mod_cpp_file = output_path / f'{mod}.cpp'
    mod_files['core'].append({
        'abbr': 'module.cpp',
        'path': mod_cpp_file,
        'content': templates['module.cpp'].render(tmpl_data),
        'template_path': Path(templates['module.cpp'].filename),
        'last_mtime': mod_path.stat().st_mtime,
        'license_header': license_header
    })

    # docs/index.rst
    mod_files['docs'].append({
        'abbr': 'index.rst',
        'path': output_path / 'docs' / 'index.rst',
        'content': templates['index.rst'].render(tmpl_data),
        'template_path': Path(templates['index.rst'].filename),
        'last_mtime': mod_path.stat().st_mtime
    })

    for file_info in [*mod_files['core'], *mod_files['interfaces'], *mod_files['docs']]:
        file_info['printable_name'] = file_info['path'].relative_to(output_path)

    return mod_files


def load_interface_definition(interface):
    if_path = helpers.resolve_everest_dir_path(f'interfaces/{interface}.yaml')

    if_def = helpers.load_validated_interface_def(if_path, validators['interface'])

    if 'vars' not in if_def:
        if_def['vars'] = {}
    if 'cmds' not in if_def:
        if_def['cmds'] = {}

    last_mtime = if_path.stat().st_mtime

    return if_def, last_mtime


def generate_interface_headers(interface, all_interfaces_flag, output_dir):
    if_parts = {'base': None, 'exports': None, 'types': None}

    try:
        if_def, last_mtime = load_interface_definition(interface)
    except Exception as e:
        if not all_interfaces_flag:
            raise
        else:
            # FIXME (aw): should we really silently ignore that?
            print(f'Ignoring interface {interface} with reason: {e}')
            return

    tmpl_data = generate_tmpl_data_for_if(interface, if_def, False)

    output_path = output_dir / interface
    output_path.mkdir(parents=True, exist_ok=True)

    tmpl_data['info']['interface_name'] = f'{interface}'
    tmpl_data['info']['namespace'] = [f'{interface}']

    # generate Base file (providers view)
    tmpl_data['info']['hpp_guard'] = helpers.snake_case(interface).upper() + '_IMPLEMENTATION_HPP'
    tmpl_data['info']['class_name'] = f'{interface}ImplBase'

    base_file = output_path / 'Implementation.hpp'

    if_parts['base'] = {
        'path': base_file,
        'content': templates['interface_base'].render(tmpl_data),
        'template_path': Path(templates['interface_base'].filename),
        'last_mtime': last_mtime,
        'printable_name': base_file.relative_to(output_path.parent)
    }

    # generate Exports file (users view)
    tmpl_data['info']['hpp_guard'] = helpers.snake_case(interface).upper() + '_INTERFACE_HPP'
    tmpl_data['info']['class_name'] = f'{interface}Intf'

    exports_file = output_path / 'Interface.hpp'

    if_parts['exports'] = {
        'path': exports_file,
        'content': templates['interface_exports'].render(tmpl_data),
        'template_path': Path(templates['interface_exports'].filename),
        'last_mtime': last_mtime,
        'printable_name': exports_file.relative_to(output_path.parent)
    }

    # generate Types file
    tmpl_data['info']['hpp_guard'] = helpers.snake_case(interface).upper() + '_TYPES_HPP'

    types_file = output_path / 'Types.hpp'

    if_parts['types'] = {
        'path': types_file,
        'content': templates['types.hpp'].render(tmpl_data),
        'template_path': Path(templates['types.hpp'].filename),
        'last_mtime': last_mtime,
        'printable_name': types_file.relative_to(output_path.parent)
    }

    return if_parts


def module_create(args):
    create_strategy = 'force-create' if args.force else 'create'

    detected_projects = helpers.detect_everest_projects(args.everest_projects, args.build_dir)
    if detected_projects:
        helpers.everest_dirs.extend(detected_projects)

    mod_files = generate_module_files(args.module, False, args.licenses)

    if args.only == 'which':
        helpers.print_available_mod_files(mod_files)
        return
    else:
        try:
            helpers.filter_mod_files(args.only, mod_files)
        except Exception as err:
            print(err)
            return

    for file_info in mod_files['core'] + mod_files['interfaces'] + mod_files['docs']:
        if not args.disable_clang_format:
            helpers.clang_format(args.clang_format_file, file_info)

        helpers.write_content_to_file(file_info, create_strategy, args.diff)


def module_update(args):
    detected_projects = helpers.detect_everest_projects(args.everest_projects, args.build_dir)
    if detected_projects:
        helpers.everest_dirs.extend(detected_projects)

    # Always generate type info before updating module
    for type_with_namespace in list_types_with_namespace():
        _tmpl_data, _last_mtime = TypeParser.generate_type_info(type_with_namespace, all_types=True)

    primary_update_strategy = 'force-update' if args.force else 'update'
    update_strategy = {'module.cpp': 'update-if-non-existent'}
    for file_name in ['cmakelists', 'module.hpp']:
        update_strategy[file_name] = primary_update_strategy

    # FIXME (aw): refactor out this only handling and rename it properly
    mod_files = generate_module_files(args.module, True, args.licenses)

    if args.only == 'which':
        helpers.print_available_mod_files(mod_files)
        return
    else:
        try:
            helpers.filter_mod_files(args.only, mod_files)
        except Exception as err:
            print(err)
            return

    if not args.disable_clang_format:
        for file_info in mod_files['core'] + mod_files['interfaces']:
            helpers.clang_format(args.clang_format_file, file_info)

    for file_info in mod_files['core']:
        helpers.write_content_to_file(file_info, update_strategy[file_info['abbr']], args.diff, '', True)

    for file_info in mod_files['interfaces']:
        if file_info['abbr'].endswith('.hpp'):
            helpers.write_content_to_file(file_info, primary_update_strategy, args.diff, '', True)
        else:
            helpers.write_content_to_file(file_info, 'update-if-non-existent', args.diff, '', True)


def module_genld(args):
    output_dir = Path(args.output_dir).resolve() if args.output_dir else work_dir / \
        'build/generated/generated/modules'
    primary_update_strategy = 'force-update' if args.force else 'update'

    loader_files = generate_module_loader_files(args.module, output_dir)

    if not args.disable_clang_format:
        for file_info in loader_files:
            helpers.clang_format(args.clang_format_file, file_info)

    for file_info in loader_files:
        helpers.write_content_to_file_and_check_template(file_info, primary_update_strategy)


def module_get_templates(args):
    interface_files = args.separator.join(
        [templates['ld-ev.hpp'].filename,
         templates['ld-ev.cpp'].filename])

    print(f'{interface_files}')


def interface_genhdr(args):
    # Always generate type info before generating interfaces
    for type_with_namespace in list_types_with_namespace():
        _tmpl_data, _last_mtime = TypeParser.generate_type_info(type_with_namespace, all_types=True)

    output_dir = Path(args.output_dir).resolve() if args.output_dir else work_dir / \
        'build/generated/include/generated/interfaces'
    primary_update_strategy = 'force-update' if args.force else 'update'

    interfaces = args.interfaces
    all_interfaces = False
    if not interfaces:
        all_interfaces = True
        interfaces = []
        for everest_dir in everest_dirs:
            if_dir = everest_dir / 'interfaces'
            interfaces += [if_path.stem for if_path in if_dir.iterdir() if (if_path.is_file()
                                                                            and if_path.suffix == '.yaml')]

    for interface in interfaces:
        if_parts = generate_interface_headers(interface, all_interfaces, output_dir)

        if not args.disable_clang_format:
            # FIXME (aw): this broken, because in case all_interfaces is true, if_parts might be none for invalid interface files
            helpers.clang_format(args.clang_format_file, if_parts['base'])
            helpers.clang_format(args.clang_format_file, if_parts['exports'])
            helpers.clang_format(args.clang_format_file, if_parts['types'])

        helpers.write_content_to_file_and_check_template(if_parts['base'], primary_update_strategy, args.diff)
        helpers.write_content_to_file_and_check_template(if_parts['exports'], primary_update_strategy, args.diff)
        helpers.write_content_to_file_and_check_template(if_parts['types'], primary_update_strategy, args.diff)


def interface_get_templates(args):
    interface_files = args.separator.join(
        [templates['interface_base'].filename,
         templates['interface_exports'].filename])

    print(f'{interface_files}')


def helpers_genuuids(args):
    if (args.count <= 0):
        raise Exception(f'Invalid number ("{args.count}") of uuids to generate')
    helpers.generate_some_uuids(args.count)


def helpers_yaml2json(args):
    helpers.yaml2json(Path(args.input).resolve(), Path(args.output).resolve())


def helpers_json2yaml(args):
    helpers.json2yaml(Path(args.input).resolve(), Path(args.output).resolve())


def list_types_with_namespace(types=None) -> List:
    if not types:
        types = []
        for everest_dir in everest_dirs:
            types_dir = everest_dir / 'types'
            types += list(types_dir.glob('**/*.yaml'))

    types_with_namespace = []
    for type_path in types:
        relative_path = None
        for everest_dir in everest_dirs:
            types_dir = everest_dir / 'types'
            try:
                relative_path = type_path.relative_to(types_dir).with_suffix('')
                break
            except ValueError:
                pass
        uppercase_path = []
        for part in relative_path.parts:
            uppercase_path.append(stringcase.capitalcase(part))
        namespace = '::'.join(relative_path.parts)
        type_with_namespace = {
            'path': type_path,
            'relative_path': relative_path,
            'namespace': namespace,
            'uppercase_path': uppercase_path,
        }

        types_with_namespace.append(type_with_namespace)

    return types_with_namespace


def types_genhdr(args):
    print('Generating global type headers.')
    output_dir = Path(args.output_dir).resolve() if args.output_dir else work_dir / \
        'build/generated/generated/types'

    primary_update_strategy = 'force-update' if args.force else 'update'

    types = None
    all_types = False
    if 'types' not in args:
        all_types = True
    else:
        types = args.types

    types_with_namespace = list_types_with_namespace(types=types)

    for type_with_namespace in types_with_namespace:
        type_parts = TypeParser.generate_type_headers(type_with_namespace, all_types, output_dir)

        if not args.disable_clang_format:
            helpers.clang_format(args.clang_format_file, type_parts['types'])

        helpers.write_content_to_file_and_check_template(type_parts['types'], primary_update_strategy, args.diff)


def types_get_templates(args):
    interface_files = templates['types.hpp'].filename

    print(f'{interface_files}')


def main():
    global validators, everest_dirs, work_dir
    everest_dir = Path(__file__).parent.parent.parent.parent.parent.parent
    everest_framework_dir = everest_dir / 'lib' / 'everest' / 'framework'
    schemas_dir = everest_framework_dir / 'schemas'
    everest_dir_default = [str(everest_dir), str(Path.cwd().parent / 'EVerest')]

    parser = argparse.ArgumentParser(description='Everest command line tool')
    parser.add_argument('--version', action='version', version=f'%(prog)s {__version__}')

    common_parser = argparse.ArgumentParser(add_help=False)

    common_parser.add_argument('--work-dir', '-wd', type=str,
                               help='work directory containing the manifest definitions (default: .)', default=str(Path.cwd()))
    common_parser.add_argument('--everest-dir', '-ed', nargs='*',
                               help=f'everest directory containing the interface definitions (default: {everest_dir_default})',
                               default=everest_dir_default)
    common_parser.add_argument('--everest-projects', '-ep', nargs='*',
                               help='everest project names. used in auto detection of their directories to get eg. interface defintions (default: EVerest)',
                               default=['EVerest'])
    common_parser.add_argument('--schemas-dir', '-sd', type=str,
                               help=f'everest framework directory containing the schema definitions (default: {schemas_dir})',
                               default=str(schemas_dir))
    common_parser.add_argument('--licenses', '-lc', type=str,
                               help='license directory from which ev-cli will attempt to parse custom license texts (default ../licenses)',
                               default=str(Path.cwd() / '../licenses'))
    common_parser.add_argument('--build-dir', '-bd', type=str,
                               help='everest build directory from which ev-cli will attempt to parse the everest framework schema definitions (default ./build)',
                               default=str(Path.cwd() / 'build'))
    common_parser.add_argument('--clang-format-file', type=str, default=str(Path.cwd()),
                               help='Path to the directory, containing the .clang-format file (default: .)')
    common_parser.add_argument('--disable-clang-format', action='store_true', default=False,
                               help='Set this flag to disable clang-format')

    subparsers = parser.add_subparsers(metavar='<command>', help='available commands', required=True)
    parser_mod = subparsers.add_parser('module', aliases=['mod'], help='module related actions')
    parser_if = subparsers.add_parser('interface', aliases=['if'], help='interface related actions')
    parser_hlp = subparsers.add_parser('helpers', aliases=['hlp'], help='helper actions')
    parser_types = subparsers.add_parser('types', aliases=['ty'], help='type related actions')

    mod_actions = parser_mod.add_subparsers(metavar='<action>', help='available actions', required=True)
    mod_create_parser = mod_actions.add_parser('create', aliases=['c'], parents=[
                                               common_parser], help='create module(s)')
    mod_create_parser.add_argument('module', type=str, help='name of the module, that should be created')
    mod_create_parser.add_argument('-f', '--force', action='store_true', help='force overwriting - use with care!')
    mod_create_parser.add_argument('-d', '--diff', '--dry-run', action='store_true',
                                   help='show resulting diff on create or overwrite')
    mod_create_parser.add_argument('--only', type=str,
                                   help='Comma separated filter list of module files, that should be created.  '
                                   'For a list of available files use "--only which".')
    mod_create_parser.set_defaults(action_handler=module_create)

    mod_update_parser = mod_actions.add_parser('update', aliases=['u'], parents=[
                                               common_parser], help='update module(s)')
    mod_update_parser.add_argument('module', type=str, help='name of the module, that should be updated')
    mod_update_parser.add_argument('-f', '--force', action='store_true', help='force overwriting')
    mod_update_parser.add_argument('-d', '--diff', '--dry-run', action='store_true', help='show resulting diff')
    mod_update_parser.add_argument('--only', type=str,
                                   help='Comma separated filter list of module files, that should be updated.  '
                                   'For a list of available files use "--only which".')
    mod_update_parser.set_defaults(action_handler=module_update)

    mod_genld_parser = mod_actions.add_parser(
        'generate-loader', aliases=['gl'], parents=[common_parser], help='generate everest loader')
    mod_genld_parser.add_argument(
        'module', type=str, help='name of the module, for which the loader should be generated')
    mod_genld_parser.add_argument('-f', '--force', action='store_true', help='force overwriting')
    mod_genld_parser.add_argument('-o', '--output-dir', type=str, help='Output directory for generated loader '
                                  'files (default: {everest-dir}/build/generated/generated/modules)')
    mod_genld_parser.set_defaults(action_handler=module_genld)

    if_actions = parser_if.add_subparsers(metavar='<action>', help='available actions', required=True)
    if_genhdr_parser = if_actions.add_parser(
        'generate-headers', aliases=['gh'], parents=[common_parser], help='generate headers')
    if_genhdr_parser.add_argument('-f', '--force', action='store_true', help='force overwriting')
    if_genhdr_parser.add_argument('-o', '--output-dir', type=str, help='Output directory for generated interface '
                                  'headers (default: {everest-dir}/build/generated/generated/interfaces)')
    if_genhdr_parser.add_argument('-d', '--diff', '--dry-run', action='store_true', help='show resulting diff')
    if_genhdr_parser.add_argument('interfaces', nargs='*', help='a list of interfaces, for which header files should '
                                  'be generated - if no interface is given, all will be processed and non-processable '
                                  'will be skipped')
    if_genhdr_parser.set_defaults(action_handler=interface_genhdr)

    hlp_actions = parser_hlp.add_subparsers(metavar='<action>', help='available actions', required=True)
    hlp_genuuid_parser = hlp_actions.add_parser('generate-uuids', help='generate uuids')
    hlp_genuuid_parser.add_argument('count', type=int, default=3)
    hlp_genuuid_parser.set_defaults(action_handler=helpers_genuuids)

    hlp_yaml2json_parser = hlp_actions.add_parser('yaml2json', help='convert yaml into json')
    hlp_yaml2json_parser.add_argument('input', type=str, help='path to yaml input file')
    hlp_yaml2json_parser.add_argument('output', type=str, help='path to json output file')
    hlp_yaml2json_parser.set_defaults(action_handler=helpers_yaml2json)

    hlp_json2yaml_parser = hlp_actions.add_parser('json2yaml', help='convert json into yaml')
    hlp_json2yaml_parser.add_argument('input', type=str, help='path to json input file')
    hlp_json2yaml_parser.add_argument('output', type=str, help='path to yaml output file')
    hlp_json2yaml_parser.set_defaults(action_handler=helpers_json2yaml)

    types_actions = parser_types.add_subparsers(metavar='<action>', help='available actions', required=True)
    types_genhdr_parser = types_actions.add_parser(
        'generate-headers', aliases=['gh'], parents=[common_parser], help='generate type headers')
    types_genhdr_parser.add_argument('-f', '--force', action='store_true', help='force overwriting')
    types_genhdr_parser.add_argument('-o', '--output-dir', type=str, help='Output directory for generated type '
                                     'headers (default: {everest-dir}/build/generated/generated/types)')
    types_genhdr_parser.add_argument('-d', '--diff', '--dry-run', action='store_true', help='show resulting diff')
    types_genhdr_parser.add_argument('types', nargs='*', help='a list of types, for which header files should '
                                     'be generated - if no type is given, all will be processed and non-processable '
                                     'will be skipped')
    types_genhdr_parser.set_defaults(action_handler=types_genhdr)

    for sub_parser, get_template_function in [
        (mod_actions, module_get_templates),
        (if_actions, interface_get_templates),
        (types_actions, types_get_templates)
    ]:
        get_templates_parser = sub_parser.add_parser(
            'get-templates', aliases=['gt'], parents=[common_parser], help='get paths to template files')
        get_templates_parser.add_argument(
            '-s', '--separator', type=str, default='\n', help='separator between template files')
        get_templates_parser.set_defaults(action_handler=get_template_function)

    args = parser.parse_args()

    if 'everest_dir' in args:
        # FIXME (aw): the helper commands do not set everest_dir, work_dir and schema_dirs, but the following common
        #             code has to run for all other commands - we need some better check here than just checking for
        #             'everest_dir' in args!
        for entry in args.everest_dir:
            everest_dir = Path(entry).resolve()
            everest_dirs.append(everest_dir)

        helpers.everest_dirs = everest_dirs

        work_dir = Path(args.work_dir).resolve()

        setup_jinja_env()

        schemas_dir = Path(args.schemas_dir).resolve()
        if not schemas_dir.exists():
            print(f'The default ("{schemas_dir}") xor supplied (via --schemas-dir) schemas directory'
                  ' doesn\'t exist.\n'
                  f'dir: {schemas_dir}')
            cmake_cache_path = Path(args.build_dir) / 'CMakeCache.txt'
            found_dir = helpers.get_path_from_cmake_cache('everest-framework', cmake_cache_path, '--schemas-dir')
            if not found_dir:
                exit(1)
            schemas_dir = found_dir / 'schemas'
            if not schemas_dir.exists():
                exit(1)

        validators = helpers.load_validators(schemas_dir)

        TypeParser.validators = validators
        TypeParser.templates = templates

        ErrorParser.validators = validators

    args.action_handler(args)


if __name__ == '__main__':
    try:
        main()
    except helpers.EVerestParsingException as e:
        raise SystemExit(e)
