import os
import fs
from pathlib import Path

from dataclasses import dataclass, is_dataclass, fields
from enum import Enum
from inspect import isclass
from typing import Optional, List, Any, Set, Callable, Tuple, Generic, TypeVar
from typing_inspect import get_origin, is_optional_type
from zetsubou.fastbuild.to_fastbuild import compiler_family_to_fastbuild

from zetsubou.project.config_matrix import ConfigVariant, get_config_matrix_os_name
from zetsubou.project.model.config_string import EDefaultConfigSlots
from zetsubou.project.model.configuration import Configuration
from zetsubou.project.runtime.emit import EDefaultTargets, Emitter, EmitContext
from zetsubou.project.runtime.project_loader import ProjectTemplate
from zetsubou.project.runtime.resolve import ResolvedTarget, TargetVariant
from zetsubou.project.model.target import Target, TargetReference, Source
from zetsubou.project.model.kind import ETargetKind, is_target_kind_linkable, is_target_library, is_target_prebuild_step
from zetsubou.project.model.toolchain import Toolchain, ToolchainDefinition
from zetsubou.utils.common import join_unique, list_to_string, split, filter_none

import zetsubou.fastbuild.target_kinds as target_kinds
import zetsubou.fastbuild.vcxproject as vcxproject
import zetsubou.fastbuild.vssolution as vssolution
from zetsubou.fastbuild.compiler import Compiler
from zetsubou.system.windows.msvc import arch_to_msvc_platform
from zetsubou.utils.subprocess import call_process_venv


FASTBUILD_BFF_HEADER = '// Generated by Zetsubou, do not manually edit!'
FASTBUILD_ALL_TARGET = 'AllTargets'
PROJ_OUTPUT_DIR = '^$(SolutionDir)/build'
FBUILD_OUTPUT_DIR = 'build/fbuild'
DEFAULT_INDENT = 4
T1 = TypeVar('T1')
T2 = TypeVar('T2')


class bff_writer:
    indent : List[int]
    str_buff : List[str]

    def __init__(self):
        self.indent = [0]
        self.str_buff = []

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        pass

    def newline(self):
        self.str_buff.append('\n')
        self.str_buff.append(" " * sum(self.indent))

    def write(self, content : str):
        assert(isinstance(content, str))
        self.str_buff.append(content)

    def write_line(self, content : str):
        self.newline()
        self.write(content)

    def push_indent(self, val : int = DEFAULT_INDENT):
        self.indent.append(val)

    def pop_indent(self, val : int = 0):
        popped = self.indent.pop()
        if val != 0:
            assert(popped == val)

    def push_cursor_indent(self):
        indent_sum = sum(self.indent)
        cursor = self.cursor_pos()
        self.push_indent(cursor - indent_sum)

    def cursor_pos(self):
        buff_len = len(self.str_buff)
        itr = -1
        pos = 0
        while itr != -buff_len:
            for char in self.str_buff[itr]:
                if char == '\n':
                    return pos
                else:
                    pos += 1
            itr -= 1
        return pos

    def to_string(self):
        return ''.join(self.str_buff)

    @staticmethod
    def is_empty(obj) -> bool:
        if not is_optional_type(type(obj)):
            return False
        if bff_writer.is_list(obj):
            return len(obj) == 0
        return obj is None

    @staticmethod
    def is_list(obj) -> bool:
        t = type(obj)
        return t == list or get_origin(t) == list

    @staticmethod
    def is_single_value(obj) -> bool:
        t = type(obj)
        if bff_writer.is_list(obj):
            return False
        if t == TargetReference:
            return True
        if t == dict or is_dataclass(t):
            return False
        if t in [str, int, bool]:
            return True
        if isclass(t) and issubclass(t, Enum):
            return True
        return False

    @staticmethod
    def is_struct(obj) -> bool:
        return not bff_writer.is_single_value(obj)

    @staticmethod
    def get_fields(obj):
        t = type(obj)
        if t is dict:
            return obj.items()
        elif is_dataclass(obj):
            return { f.name : getattr(obj, f.name) for f in fields(obj) }.items()
        else:
            return vars(obj).items()


@dataclass
class bff_using(Generic[T1, T2]):
    name : str
    obj : T1
    ref : T2


@dataclass
class bff_includes:
    includes: List[str]
    system_includes: List[str]


class bff_struct:
    writer : bff_writer
    field_map : Set[str]

    STRUCT_BEGIN = '['
    STRUCT_END = ']'

    def __init__(self, writer : bff_writer, is_keyword : bool = False) -> None:
        self.writer = writer
        self.field_map = set()

        if is_keyword:
            self.STRUCT_BEGIN = '{'
            self.STRUCT_END = '}'

    def __enter__(self):
        self.writer.write_line(self.STRUCT_BEGIN)
        self.writer.push_indent()

        return self

    def next(self):
        self.writer.newline()

    def has_field(self, name : str):
        return name in self.field_map

    def write_field(self, name, value, no_quotations : bool = False):
        self.next()

        if not self.has_field(name):
            self.field_map.add(name)
            write_field(self.writer, name, value, no_quotations)
        else:
            write_append_field(self.writer, name, value, False, no_quotations)

    def write_assign(self, name : str, value : str):
        write_assign(self.writer, name)
        self.writer.write(value)

    def write_usings(self, usings : List[Any]):
        for using in usings:
            # Register names for Using objects, so we append their fields instead of overwritting
            if isinstance(using, bff_using):
                for name, _ in bff_writer.get_fields(using.ref):
                    self.field_map.add(name)
                write_using(self.writer, using.name)
            else:
                write_using(self.writer, using)

    def write_object(self, obj : Any, hidden_fields : List[str] = []):
        for name, value in self.writer.get_fields(obj):
            if not self.writer.is_empty(value) and name not in hidden_fields:
                self.write_field(name, value)

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.writer.pop_indent()
        self.writer.write_line(self.STRUCT_END)


class bff_foreach(bff_struct):
    collection = str
    def __init__(self, writer : bff_writer, collection : str):
        super().__init__(writer, is_keyword=True)
        self.collection = collection

    def __enter__(self):
        self.writer.write_line(f"ForEach( .Itr in {self.collection} )")
        super().__enter__()

    def __exit__(self, exc_type, exc_val, exc_tb):
        super().__exit__(exc_type, exc_val, exc_tb)


class bff_list:
    writer : bff_writer
    obj : List[Any]
    list_len : int
    list_struct : bool
    list_iter : int

    LIST_BEGIN = '{'
    LIST_END = '}'
    LIST_SEPARATOR = ','

    def __init__(self, writer : bff_writer, obj : List[Any]) -> None:
        self.writer = writer
        self.obj = obj
        self.list_len = len(obj)
        self.list_iter = 0
        self.list_struct = False if self.list_len == 0 else writer.is_struct(obj[0])

    def __enter__(self):
        self.writer.write(bff_list.LIST_BEGIN)
        if self.list_struct:
            self.writer.push_indent()
        else:
            self.writer.write(' ')
            self.writer.push_cursor_indent()
        return self

    def next(self):
        if self.list_iter != 0:
            self.writer.write(bff_list.LIST_SEPARATOR)
            if not self.list_struct:
                self.writer.newline()
        self.list_iter += 1

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.writer.pop_indent()
        if self.list_struct:
            self.writer.write_line(bff_list.LIST_END)
        else:
            self.writer.write(' ')
            self.writer.write(bff_list.LIST_END)


class bff_indent:
    writer : bff_writer
    indent : int

    def __init__(self, *, writer : bff_writer, indent : int = DEFAULT_INDENT) -> None:
        self.writer = writer
        self.indent = indent

    def __enter__(self):
        self.writer.push_indent(self.indent)
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.writer.pop_indent(self.indent)


def write_value(writer : bff_writer, value, no_quotations : bool = False):
    if writer.is_empty(value):
        return

    if type(value) in [float, int, bool]:
        writer.write(str(value).lower())
        return

    serialized : Optional[str] = to_str(value)
    if serialized is not None:
        if no_quotations:
            writer.write(serialized)
        else:
            if serialized.find('"') != -1:
                writer.write(f"'{serialized}'")
            else:
                writer.write(f'"{serialized}"')
        return

    if writer.is_list(value):
        with bff_list(writer, value) as array:
            for element in value:
                if not writer.is_empty(element):
                    array.next()
                    write_value(writer, element, no_quotations=no_quotations)
        return

    elif writer.is_struct(value):
        with bff_struct(writer) as struct:
            for name, prop in writer.get_fields(value):
                if not writer.is_empty(prop):
                    struct.next()
                    write_field(writer, name, prop, no_quotations=no_quotations)
        return

    writer.write(f'"NOT_IMPLEMENTED: {type(value)}"')


def write_comment_header(writer : bff_writer, text : str):
    separator = '/////////////////////////////////////////////////////////'
    writer.write_line(separator)
    writer.write_line(f'// {text}')
    writer.write_line(separator)


def write_assign(writer : bff_writer, name : str):
    writer.write(f'.{name} = ')


def write_field(writer : bff_writer, name : str, value, no_quotations : bool = False):
    write_assign(writer, name)
    write_value(writer, value, no_quotations=no_quotations)


def write_append_field(writer : bff_writer, name : str, value, outer_scope:bool=False, no_quotations : bool = False):
    writer.write(f'^{name} + ' if outer_scope else f'.{name} + ')
    write_value(writer, value, no_quotations=no_quotations)


def write_include(writer : bff_writer, include):
    writer.write_line(f'#include "{include}"')


def write_using(writer : bff_writer, using):
    writer.write_line(f'Using( .{using} )')


def write_includes(writer : bff_writer, includes : List[Any], relative_to : str = ''):
    if includes is not None:
        for include in includes:
            if relative_to != '':
                include = fs.path.relativefrom(relative_to, include)
            write_include(writer, include)


def write_keyword(writer : bff_writer, keyword : str, name_param : str):
    writer.write_line(f"{keyword}( '{name_param}' )")


def write_object(writer : bff_writer, obj, *, name : str = '', local_usings : List[str] = []):
    if name == '' and hasattr(obj, 'Alias'):
        name = getattr(obj, 'Alias')

    writer.newline()
    write_keyword(writer, obj.keyword(), name)

    with bff_struct(writer, is_keyword=True) as struct:
        struct.write_usings(local_usings)
        struct.write_object(obj)

    writer.newline()


##############################################################################################################


def get_linker_output_name(target: ResolvedTarget):
    # support custom!
    if target.target.config.kind == ETargetKind.DYNAMIC_LIBRARY:
        return f'{target.target.target}.dll'
    if target.target.config.kind == ETargetKind.EXECUTABLE:
        return f'{target.target.target}.exe'


def create_bff_writer():
    writer = bff_writer()
    writer.write(FASTBUILD_BFF_HEADER)
    return writer


def get_min_max_msvc_version(context : EmitContext, proj : ProjectTemplate) -> Tuple[str, str]:
    vmin = None
    vmax = None

    for plat in proj.platforms:
        if plat.host_system == context.host_system and plat.target_arch == context.host_arch:
            for tool in proj.platform_toolchains[plat.platform]:
                if vmin is None or tool.definition.ide_version < vmin:
                    vmin = tool.definition.ide_version
                if vmax is None or tool.definition.ide_version > vmax:
                    vmax = tool.definition.ide_version

    return (str(vmin), str(vmax))


def find_cli_tool(tool_name : str, proj : ProjectTemplate):
    tool = proj.find_cli_tool(tool_name)
    return tool


def find_toolchain(plat_name : str, tool_name : str, proj : ProjectTemplate) -> bff_using[Toolchain, ToolchainDefinition]:
    tool = proj.find_toolchain(plat_name, tool_name)
    if tool is not None:
        return bff_using(name=tool.name, obj=tool, ref=tool.definition)
    return None


def find_configuration(name : str, proj : ProjectTemplate) -> Configuration:
    return proj.find_config(name)


def find_base_platform_name(name : str, proj : ProjectTemplate) -> str:
    platform = proj.find_platform(name)
    return arch_to_msvc_platform(platform.target_arch)


def filter_target_linkable(ref : TargetReference):
    return is_target_kind_linkable(ref.target.config.kind)


def filter_target_libraries(ref : Target):
    return is_target_library(ref.config.kind)


def filter_target_executable(ref : Target):
    return not is_target_library(ref.config.kind)


def filter_target_prebuild_step(ref : Target):
    return is_target_prebuild_step(ref.config.kind)


def to_str(obj) -> Optional[str]:
    if isinstance(obj, str):
        return obj
    elif isinstance(obj, TargetReference) or issubclass(type(obj), Enum):
        return obj.name
    else:
        return None


def get_upward_project_path(root : str, path : str):
    depth = root.count('/') + 2
    return f'{depth * "../"}{path}'


class FastbuildEmitter(Emitter):
    subprojects = []
    main_file_name = ''


    def __init__(self):
        self.main_file_name = self.get_main_bff_path()


    def project_to_output_path(self, context : EmitContext, path : str, project_subdir : str = ''):
        abs_dir_path = path if os.path.isabs(path) else fs.path.join(context.fs_root, path)
        proj_path = fs.path.join(context.ROOT_DIR, project_subdir)
        abs_out_path = fs.path.join(context.fs_root, proj_path)
        result = fs.path.relativefrom(abs_out_path, abs_dir_path)
        return result


    def get_target_output_path(self, context : EmitContext, target: ResolvedTarget):
        return self.project_to_output_path(context, fs.path.dirname(target.target.get_loaded_from_file()))


    def get_bin_output_path(self, config_name : str):
        return f'{PROJ_OUTPUT_DIR}/obj/{config_name}'


    def get_compiler_output_path(self, config_str : str, subdir : str):
        return f'build/{subdir}/{config_str}'


    @staticmethod
    def get_copy_distributed_files_target_name(config_variant_name:str):
        return f'COPY_DistributedFiles_{config_variant_name}'


    @staticmethod
    def get_runner_path():
        return 'cd ^$(SolutionDir) &amp; .\\build\\scripts\\runner.bat'


    @staticmethod
    def get_vcxproj_path(name : str):
        return f'build/projects/{name}.vcxproj'


    @staticmethod
    def get_main_bff_path():
        return f'{FBUILD_OUTPUT_DIR}/fbuild.bff'


    @staticmethod
    def get_fbuild_build_command(context: EmitContext, target_name : str):
        cmd = f'fbuild.exe -config {FastbuildEmitter.get_main_bff_path()} -ide -wrapper -cache {target_name}'
        if context.project_template.project.config.verbose_build:
            cmd += ' -verbose'
        return cmd


    @staticmethod
    def get_zetsubou_build_command(context: EmitContext, target_name: str):
        return f'zetsubou build {context.project_file} --ide --nologo --target-variant {target_name}'


    def get_build_command(self, context: EmitContext, target_name : str):
        return f'{FastbuildEmitter.get_runner_path()} {FastbuildEmitter.get_zetsubou_build_command(context, target_name)}'


    @staticmethod
    def get_fbuild_rebuild_command(context: EmitContext, target_name : str):
        cmd = f'fbuild.exe -config {FastbuildEmitter.get_main_bff_path()} -ide -wrapper -cache -clean {target_name}'
        if context.project_template.project.config.verbose_build:
            cmd += ' -verbose'
        return cmd


    def get_rebuild_command(self, context:EmitContext, target_name:str):
        return f'{FastbuildEmitter.get_runner_path()} {FastbuildEmitter.get_fbuild_rebuild_command(context, target_name)}'


    @staticmethod
    def get_zetsubout_update_build_command(context: EmitContext):
        return f'zetsubou gen {context.project_file} --ide --nologo'


    def get_update_build_command(self, context: EmitContext):
        return f'{FastbuildEmitter.get_runner_path()} {FastbuildEmitter.get_zetsubout_update_build_command(context)}'


    @staticmethod
    def get_zetsubout_update_rebuild_command(context: EmitContext):
        return f'zetsubou regen {context.project_file} --ide --nologo'


    # Formats path containing {root} and {config_variant} like so:
    # {root}/mydir -> C:\Projects\MyProject\mydir
    # build/generated/{config_variant} -> build/generated/Windows_Debug_MSVC_x64_v193_v143
    @staticmethod
    def format_path(format_path:str, context:EmitContext, config_variant:ConfigVariant):
        return format_path.format(root=context.fs_root, config_variant=get_config_matrix_os_name(config_variant.config_string))


    def get_update_rebuild_command(self, context: EmitContext):
        return f'{FastbuildEmitter.get_runner_path()} {FastbuildEmitter.get_zetsubout_update_rebuild_command(context)}'


    def emit_runner_script(self, context: EmitContext):
        relative_activate = self.project_to_output_path(context, context.fs_venv.activate).replace('/', '\\')
        relative_deactivate = self.project_to_output_path(context, context.fs_venv.deactivate).replace('/', '\\')
        script = ''

        script += f'CALL {relative_activate}\n'
        script +=  'CALL %* \n'
        script +=  'SET taskexitcode=%errorlevel%\n'
        script += f'CALL {relative_deactivate}\n'
        script +=  'if %taskexitcode% NEQ 0 (\n'
        script +=  '    exit /b %taskexitcode%\n'
        script +=  ')\n'

        context.write_file('build/scripts/runner.bat', script)


    def fill_base_project(self, context : EmitContext, fbuild: target_kinds.LinkableTarget, target: ResolvedTarget, target_variant: TargetVariant, toolchain : Toolchain):
        subdir = 'bin' if target.get_kind() == ETargetKind.EXECUTABLE else 'lib'
        lib_dir = self.get_compiler_output_path(target_variant.config_variant.config_string, subdir)
        fbuild.LinkerOutput = f'{lib_dir}/{get_linker_output_name(target)}'

        fbuild.LinkerOptions = list_to_string(join_unique([
            target_variant.linker_flags.public,
            target_variant.linker_flags.private
        ]))

        # Append library paths as flags with /LIBPATH:
        lib_paths = join_unique([
            [ lib_dir ],
            toolchain.definition.LinkerPaths,
            target_variant.linker_paths.public,
            target_variant.linker_paths.private
        ])

        link_libs_with_refs = join_unique([
            target_variant.link_libraries.public,
            target_variant.link_libraries.private
        ])

        def filter_target_ref(ref):
            return isinstance(ref, TargetReference)

        refs, libs = split(filter_target_ref, link_libs_with_refs)

        # Linking only to matching variant of other targets
        def map_target_ref_to_str(ref):
            if isinstance(ref, TargetReference):
                return ProjectTemplate.compile_target_variant_name(ref.target.config.kind, ref.name, target_variant.config_variant.config_string)
            return ref

        fbuild.Libraries = list(map(map_target_ref_to_str, filter(filter_target_linkable, refs)))
        fbuild.LinkerOptions += list_to_string( [ toolchain.definition.i_linker.link(l) for l in libs ])
        fbuild.LinkerOptions += list_to_string( [ toolchain.definition.i_linker.dir(l)  for l in lib_paths ])

        # Depend on copying distributed files to bin
        fbuild.PreBuildDependencies = [ FastbuildEmitter.get_copy_distributed_files_target_name(target_variant.config_variant.config_string) ]


    def compiler_relative_path(self, context : EmitContext, path : str, target : ResolvedTarget, target_variant : TargetVariant):
        formatted_path = self.format_path(path, context, target_variant.config_variant)
        if not os.path.isabs(formatted_path):
            return fs.path.join( fs.path.dirname(target.target.get_loaded_from_file()), formatted_path)
        else:
            return formatted_path


    def fill_object(self, context : EmitContext, fbuild: target_kinds.ObjTarget, target: ResolvedTarget, target_variant: TargetVariant, root_dir : str, toolchain : Toolchain, source_path : str, includes : bff_includes):
        fbuild.CompilerOutputPath = self.get_compiler_output_path(target_variant.config_variant.config_string, 'obj')
        fbuild.CompilerOutputPrefix = f'{target.target.target}_'

        fbuild.CompilerOptions = list_to_string(join_unique([
            target_variant.compiler_flags.public,
            target_variant.compiler_flags.private
        ]))

        defines = join_unique([
            target_variant.defines.public,
            target_variant.defines.private
        ])

        fbuild.CompilerOptions += list_to_string([ toolchain.definition.i_compiler.cppstd(toolchain.profile.cppstd) ])
        fbuild.CompilerOptions += list_to_string([ toolchain.definition.i_compiler.define(d) for d in defines ])

        fbuild.CompilerOptions += list_to_string([ toolchain.definition.i_compiler.include(i) for i in includes.includes ])
        fbuild.CompilerOptions += list_to_string([ toolchain.definition.i_compiler.system_include(i) for i in includes.system_includes ])

        out_path = self.compiler_relative_path(context, source_path, target, target_variant)

        excluded_paths = list(map(lambda p: self.compiler_relative_path(context, p, target, target_variant), target_variant.source_exclude.paths))

        fbuild.CompilerInputPath = out_path
        fbuild.CompilerInputPattern = target_variant.source.patterns
        fbuild.CompilerInputExcludePath = excluded_paths
        fbuild.CompilerInputExcludePattern = target_variant.source_exclude.patterns


    def fill_library(self, context : EmitContext, fbuild : target_kinds.StaticLibTarget, target: ResolvedTarget, target_variant: TargetVariant, root_dir : str, toolchain : Toolchain):
        # self.fill_object(context, fbuild, target, target_variant, root_dir, toolchain)
        fbuild.CompilerOutputPath = self.get_compiler_output_path(target_variant.config_variant.config_string, 'obj')

        fbuild.LibrarianOptions = list_to_string(join_unique([
            target_variant.librarian_flags.public,
            target_variant.librarian_flags.private
        ]))

        fbuild.LibrarianOutput = f'{self.get_compiler_output_path(target_variant.config_variant.config_string, "lib")}/{target.target.target}.lib'


    def emit_vcxproj(self, writer : bff_writer, vcxproj : vcxproject.VCXProject, project_configs_list : str, includes : List[str]):
        # Conflig list predefine
        writer.newline()
        write_field(writer, project_configs_list, [])

        # Variant includes
        writer.newline()
        write_includes(writer, includes)

        # VCXProject
        writer.newline()
        write_keyword(writer, vcxproject.VCXProject.keyword(), vcxproj.Alias)

        with bff_struct(writer, is_keyword=True) as struct:
            struct.write_object(vcxproj, hidden_fields=['Alias','ProjectConfigs'])
            struct.next()
            write_assign(writer, 'ProjectConfigs')
            writer.write(f'.{project_configs_list}')


    def emit_imported_target(self, context: EmitContext, target: ResolvedTarget, target_name : str, project_configs_list : str) -> List[str]:
        includes = []

        imported_lib_name = f'configs/{target_name}_config.bff'
        write_path = f'{FBUILD_OUTPUT_DIR}/{target_name}/{imported_lib_name}'
        includes.append(imported_lib_name)

        with create_bff_writer() as writer:
            for config_variant in context.config_matrix.variants:
                target_variant = target.variants[config_variant.config_string]

                writer.newline()
                alias = ProjectTemplate.compile_target_variant_name(target.get_kind(), target_name, config_variant.config_string)

                # Target config tuple to be imported into VCXProject
                config_list_entry = f'{get_config_matrix_os_name(alias)}_Config'

                writer.newline()
                write_assign(writer, config_list_entry)

                with bff_struct(writer) as struct:
                    custom_platform = context.config_matrix.get_config_name(EDefaultConfigSlots.platform, target_variant.config_variant.config_string)
                    vs_platform = find_base_platform_name(custom_platform, context.project_template)

                    struct.write_field('Platform', vs_platform)
                    struct.write_field('Config', target_variant.config_variant.config_string) # context.config_matrix.get_config_name('platform-configuration-toolchain', target_variant.config_variant.config_string))

                writer.newline()
                writer.write(f'.{project_configs_list} + {{ .{config_list_entry} }}')

        bff_content = writer.to_string()
        context.write_file(write_path, bff_content)
        return includes


    def emit_custom_build_step(self, context: EmitContext, target: ResolvedTarget, target_name : str, project_configs_list : str) -> List[str]:
        includes = []

        for config_variant in context.config_matrix.variants:
            target_variant = target.variants[config_variant.config_string]
            target_variant_name = ProjectTemplate.compile_target_variant_name(ETargetKind.BUILD_STEP, target_name, config_variant.config_string)
            fb_target_variant_name = get_config_matrix_os_name(target_variant_name)

            cli_tool = find_cli_tool(target.target.config.compiler, context.project_template)
            cli_fullpath = cli_tool.executable_fullpath

            exclude_src_paths = list(map(lambda p: self.compiler_relative_path(context, p, target, target_variant), target_variant.source_exclude.paths))
            output_dir = self.compiler_relative_path(context, cli_tool.output_dir, target, target_variant)

            sub_parts = f'{fb_target_variant_name}_parts'

            with create_bff_writer() as writer:
                writer.newline()
                write_field(writer, sub_parts, [])

                # For each source path
                for idx, source_path in enumerate(target_variant.source.paths):
                    part_name = f'{fb_target_variant_name}_PART_{idx}'

                    writer.newline()
                    write_keyword(writer, 'Exec', part_name)

                    with bff_struct(writer, is_keyword=True) as struct:
                        struct.write_field('ExecExecutable', cli_fullpath)
                        struct.write_field('ExecInputPath', self.compiler_relative_path(context, source_path, target, target_variant))
                        struct.write_field('ExecInputPattern', target_variant.source.patterns)
                        struct.write_field('ExecInputExcludePath', exclude_src_paths)
                        struct.write_field('ExecInputExcludePattern', target_variant.source_exclude.patterns)
                        struct.write_field('ExecArguments', list_to_string(cli_tool.options).replace('%3', output_dir))
                        struct.write_field('ExecOutput', os.path.join(output_dir, cli_tool.output_file))

                    writer.newline()
                    writer.write(f'.{sub_parts} + {{ "{part_name}" }}')
                    writer.newline()

            write_keyword(writer, 'Alias', target_variant_name)
            with bff_struct(writer, is_keyword=True) as struct:
                writer.newline()
                struct.write_assign('Targets', f'.{sub_parts}')

            writer.newline()
            writer.newline()

            custom_platform = context.config_matrix.get_config_name(EDefaultConfigSlots.platform, target_variant.config_variant.config_string)

            proj_config = vcxproject.ProjectConfig()
            proj_config.Platform = find_base_platform_name(custom_platform, context.project_template)
            proj_config.Config = target_variant.config_variant.config_string
            proj_config.Target = target_variant_name
            proj_config.ProjectBuildCommand = self.get_build_command(context, target_variant_name)

            config_name = f'{fb_target_variant_name}_Config'
            write_assign(writer, config_name)
            with bff_struct(writer) as struct:
                struct.write_object(proj_config)

            writer.newline()
            writer.write(f'.{project_configs_list} + {{ .{config_name} }}')

            custom_step_path = f'configs/{fb_target_variant_name}.bff'
            write_path = f'{FBUILD_OUTPUT_DIR}/{target_name}/{custom_step_path}'
            includes.append(custom_step_path)

            bff_content = writer.to_string()
            context.write_file(write_path, bff_content)

        return includes


    def gather_includes(self, context : EmitContext, target : ResolvedTarget, target_variant : TargetVariant, toolchain : Toolchain) -> bff_includes:
        raw_includes = join_unique([
            [".\\"],
            target_variant.includes.public,
            target_variant.includes.private
        ])

        raw_includes_sys = join_unique([
            target_variant.system_includes.public,
            target_variant.system_includes.private,
            toolchain.definition.IncludeDirectories,
        ])

        def preprocess_includes(raw : List[str]) -> List[str]:
            return list(map(lambda p: self.compiler_relative_path(context, p, target, target_variant), raw))

        return bff_includes(
            includes=preprocess_includes(raw_includes),
            system_includes=preprocess_includes(raw_includes_sys)
        )


    def emit_static_library(self, context: EmitContext, target: ResolvedTarget, target_name : str, project_configs_list : str) -> List[str]:
        includes = []

        for config_variant in context.config_matrix.variants:
            target_variant = target.variants[config_variant.config_string]
            toolchain = find_toolchain(target_variant.platform, target_variant.compiler, context.project_template)

            obj_list : List[target_kinds.ObjTarget] = []
            intelisense_includes : List[str] = []

            for idx, source_path in enumerate(target_variant.source.paths):

                headers = self.gather_includes(context, target, target_variant, toolchain.obj)
                intelisense_includes += headers.includes
                intelisense_includes += headers.system_includes

                obj_lib = target_kinds.ObjTarget()
                obj_lib.Alias = ProjectTemplate.compile_target_variant_name(ETargetKind.OBJECT_LIBRARY, target_name, config_variant.config_string)
                obj_lib.Alias += f'_{idx}'
                self.fill_object(context, obj_lib, target, target_variant, context.ROOT_DIR, toolchain.obj, source_path, headers)
                obj_list.append(obj_lib)

            static_lib = target_kinds.StaticLibTarget()
            static_lib.Alias = ProjectTemplate.compile_target_variant_name(ETargetKind.STATIC_LIBRARY, target_name, config_variant.config_string)
            self.fill_library(context, static_lib, target, target_variant, context.ROOT_DIR, toolchain.obj)
            static_lib.LibrarianAdditionalInputs = [ obj.Alias for obj in obj_list ]

            static_lib_filename = get_config_matrix_os_name(static_lib.Alias)
            static_lib_path = f'configs/{static_lib_filename}.bff'
            write_path = f'{FBUILD_OUTPUT_DIR}/{target_name}/{static_lib_path}'
            includes.append(static_lib_path)

            with create_bff_writer() as writer:
                tool_relative = fs.path.relativefrom(fs.path.dirname(write_path), f'{FBUILD_OUTPUT_DIR}/toolchains/toolchain_{toolchain.obj.name}.bff')

                write_include(writer, tool_relative)
                writer.newline()

                self.emit_buildable_target_variant(context, writer, static_lib, obj_list, target_variant, project_configs_list, intelisense_includes)

                bff_content = writer.to_string()
                context.write_file(write_path, bff_content)

        return includes


    def emit_executable(self, context: EmitContext, target: ResolvedTarget, target_name : str, project_configs_list : str) -> List[str]:
        return self.emit_linkable_target(context, target, target_name, project_configs_list, target_kinds.AppTarget)


    def emit_dynamic_library(self, context: EmitContext, target: ResolvedTarget, target_name : str, project_configs_list : str) -> List[str]:
        return self.emit_linkable_target(context, target, target_name, project_configs_list, target_kinds.DynLibTarget)


    def emit_linkable_target(self, context: EmitContext, target: ResolvedTarget, target_name : str, project_configs_list : str, ctor : Callable[[], target_kinds.LinkableTarget]) -> List[str]:
        includes = []

        for config_variant in context.config_matrix.variants:
            target_variant = target.variants[config_variant.config_string]
            toolchain = find_toolchain(target_variant.platform, target_variant.compiler, context.project_template)

            obj_list : List[target_kinds.ObjTarget] = []
            intelisense_includes : List[str] = []

            for idx, source_path in enumerate(target_variant.source.paths):

                headers = self.gather_includes(context, target, target_variant, toolchain.obj)
                intelisense_includes += headers.includes
                intelisense_includes += headers.system_includes

                obj_lib = target_kinds.ObjTarget()
                obj_lib.Alias = ProjectTemplate.compile_target_variant_name(ETargetKind.OBJECT_LIBRARY, target_name, config_variant.config_string)
                obj_lib.Alias += f'_{idx}'

                self.fill_object(context, obj_lib, target, target_variant, context.ROOT_DIR, toolchain.obj, source_path, headers)
                obj_list.append(obj_lib)

            lnk_target = ctor()
            lnk_target.Alias = ProjectTemplate.compile_target_variant_name(target.get_kind(), target_name, config_variant.config_string)
            self.fill_base_project(context, lnk_target, target, target_variant, toolchain.obj)
            lnk_target.Libraries += [ obj.Alias for obj in obj_list ]

            lnk_target_filename = get_config_matrix_os_name(lnk_target.Alias)
            lnk_target_path = f'configs/{lnk_target_filename}.bff'
            write_path = f'{FBUILD_OUTPUT_DIR}/{target_name}/{lnk_target_path}'

            includes.append(lnk_target_path)

            with create_bff_writer() as writer:
                tool_relative = fs.path.relativefrom(fs.path.dirname(write_path), f'{FBUILD_OUTPUT_DIR}/toolchains/toolchain_{toolchain.obj.name}.bff')

                write_include(writer, tool_relative)
                writer.newline()

                self.emit_buildable_target_variant(context, writer, lnk_target, obj_list, target_variant, project_configs_list, intelisense_includes)

                bff_content = writer.to_string()
                context.write_file(write_path, bff_content)

        return includes


    def emit_buildable_target_variant(self, context: EmitContext, writer : bff_writer, out_target : target_kinds.BaseTarget,
        obj_list : List[target_kinds.BaseTarget], target_variant : TargetVariant, append_to_list : str, includes : List[str]):

        toolchain = find_toolchain(target_variant.platform, target_variant.compiler, context.project_template)

        # Sub objects, per source
        for obj in obj_list:
            write_keyword(writer, obj.keyword(), obj.Alias)
            with bff_struct(writer, is_keyword=True) as struct:
                struct.write_usings([ toolchain ])
                struct.write_object(obj)
            writer.newline()

        # Target definition
        write_keyword(writer, out_target.keyword(), out_target.Alias)
        with bff_struct(writer, is_keyword=True) as struct:
            struct.write_usings([ toolchain ])
            struct.write_object(out_target)

        writer.newline()

        # Target config tuple to be imported into VCXProject
        config_list_entry = f'{get_config_matrix_os_name(out_target.Alias)}_Config'
        write_assign(writer, config_list_entry)

        with bff_struct(writer) as struct:
            custom_platform = context.config_matrix.get_config_name(EDefaultConfigSlots.platform, target_variant.config_variant.config_string)
            vs_platform = find_base_platform_name(custom_platform, context.project_template)

            def relative_include_to_solution(include_path : str):
                if os.path.isabs(include_path):
                    return include_path
                return f'^$(SolutionDir)\\{include_path}'

            includes = list(map(relative_include_to_solution, includes))

            proj_config = vcxproject.ProjectConfig()
            proj_config.Platform = vs_platform
            proj_config.Config = target_variant.config_variant.config_string
            proj_config.Target = out_target.Alias
            proj_config.PlatformToolset = toolchain.obj.toolset
            proj_config.ProjectBuildCommand = self.get_build_command(context, out_target.Alias)
            proj_config.ProjectRebuildCommand = self.get_rebuild_command(context, out_target.Alias)
            proj_config.OutputDirectory = self.get_bin_output_path(target_variant.config_variant.config_string)
            proj_config.IncludeSearchPath = ';'.join(includes).replace('/', '\\')

            struct.write_object(proj_config)

        writer.newline()
        writer.write(f'.{append_to_list} + {{ .{config_list_entry} }}')


    def emit_toolchain(self, context: EmitContext, toolchain: Toolchain):
        writer = create_bff_writer()
        writer.write_line('#once')

        compiler_family = compiler_family_to_fastbuild(toolchain.definition.CompilerFamily)
        compiler = Compiler(
            Executable=toolchain.definition.Compiler,
            CompilerFamily=compiler_family
        )
        write_object(writer, compiler, name=toolchain.name)

        writer.newline()

        # Emit struct which will be then used by compilation targtes
        write_assign(writer, toolchain.name)

        with bff_struct(writer) as struct:
            struct.write_object(toolchain.definition, hidden_fields=['Compiler', 'CompilerFamily'] + toolchain.definition.get_hidden_fields())
            struct.write_field('Compiler', toolchain.name)

        path = f'{FBUILD_OUTPUT_DIR}/toolchains/toolchain_{toolchain.name}.bff'
        context.write_file(path, writer.to_string())
        return path


    def emit_solution(self, context: EmitContext):
        with create_bff_writer() as writer:
            #
            # Files to include as global project
            #
            project_files = []
            def append_proj_files(element):
                if element is not None:
                    project_files.append(element)

            if context.conan is not None and context.project_template.project.conan is not None:
                append_proj_files(context.project_template.project.conan.build_tools)
                append_proj_files(context.project_template.project.conan.dependencies)
            append_proj_files(context.project_template.project.get_loaded_from_file())

            #
            # Environment
            #
            writer.write_line('#import SystemRoot')
            writer.newline()
            writer.write_line('Settings')
            with bff_struct(writer, is_keyword=True) as struct:
                struct.write_field('Environment', [
                    'SystemRoot=$SystemRoot$',
                    'TMP=$SystemRoot$\\temp',
                ])

            writer.newline()

            project_name = context.project_template.project.project
            sln = vssolution.VSSolution()
            sln.Alias = project_name

            #
            # Toolchains
            #
            for plat in context.project_template.platforms:
                if plat.host_system == context.host_system and plat.host_arch == context.host_arch:
                    for tool in context.project_template.platform_toolchains[plat.platform]:
                        tool_inc = self.emit_toolchain(context, tool)
                        if tool_inc != '':
                            write_include(writer, fs.path.relativefrom(FBUILD_OUTPUT_DIR, tool_inc))

            writer.newline()

            #
            # Copy imported files to bin
            #
            write_comment_header(writer, 'Copy imported files to bin')

            def path_join_if_not_abs(path:str, fs_root:str):
                if os.path.isabs(path):
                    return path
                return os.path.join(fs_root, path)

            def grep_files(file_src:Source, fs_root:str = ''):
                results : List[str] = []
                paths : List[str] = []
                if fs_root != '':
                    paths = list(map(lambda p : path_join_if_not_abs(p, fs_root), file_src.paths))
                else:
                    paths = file_src.paths

                for search_path in paths:
                    for pattern in file_src.patterns:
                        results = join_unique([results, Path(search_path).glob(pattern)])
                return results

            for config_variant in context.config_matrix.variants:
                writer.newline()

                # Gather files per configuration
                dist_files = []

                resolved_target : ResolvedTarget
                for resolved_target in join_unique([context.resolved_targets, context.conan.resolved_targets]):
                    target_fs_root = os.path.dirname(resolved_target.target.get_loaded_from_file())
                    target_variant = resolved_target.variants[config_variant.config_string]
                    found_files = grep_files(target_variant.distribute_files, target_fs_root)
                    dist_files = join_unique([dist_files, list(found_files)])

                write_keyword(writer, 'Copy', FastbuildEmitter.get_copy_distributed_files_target_name(config_variant.config_string))
                with bff_struct(writer, is_keyword=True) as struct:
                    struct.write_field('Source', [str(fullpath) for fullpath in dist_files])
                    # Slash at the end is very important! It indicates that we are copying to folder, not to a file
                    struct.write_field('Dest', f"{self.get_compiler_output_path(config_variant.config_string, 'bin')}/")

            #
            # Config matrix
            #
            writer.newline()
            write_comment_header(writer, 'Config Matrix')

            # Include Targets bff's
            for include in self.subprojects:
                write_include(writer, fs.path.relativefrom(FBUILD_OUTPUT_DIR, include))

            writer.newline()
            for config_variant in context.config_matrix.variants:
                writer.newline()
                variant_name = get_config_matrix_os_name(config_variant.config_string)
                write_assign(writer, variant_name)
                with bff_struct(writer) as struct:
                    custom_platform = context.config_matrix.get_config_name(EDefaultConfigSlots.platform, config_variant.config_string)
                    vs_platform = find_base_platform_name(custom_platform, context.project_template)

                    struct.write_field('Config', config_variant.config_string)
                    struct.write_field('Platform', vs_platform)
                    struct.write_field('SolutionConfig', context.config_matrix.get_config_name(
                        EDefaultConfigSlots.configuration, config_variant.config_string))
                    struct.write_field('SolutionPlatform', context.config_matrix.get_config_name(
                        f'{EDefaultConfigSlots.platform.name}-{EDefaultConfigSlots.toolchain.name}', config_variant.config_string))

            writer.newline()
            writer.newline()
            write_field(writer, 'ConfigMatrix',
                [ f'.{get_config_matrix_os_name(config_variant.config_string)}' for config_variant in context.config_matrix.variants ],
                no_quotations=True)

            #
            # All alias
            #
            all_target_proj = f'{EDefaultTargets.All.name}-proj'

            writer.newline()
            write_comment_header(writer, 'All Alias')
            all_projects = [ f'{target.name}-proj' for target in context.resolved_targets ]

            writer.newline()
            write_field(writer, FASTBUILD_ALL_TARGET, all_projects)

            writer.newline()
            write_keyword(writer, 'Alias', EDefaultTargets.All.name)
            with bff_struct(writer, is_keyword=True) as struct:
                struct.next()
                write_assign(writer, 'Targets')
                writer.write(f'.{FASTBUILD_ALL_TARGET}')

            # Emit separate All aliases per config
            writer.newline()
            for config in context.config_matrix.variants:
                this_all_name = f'{EDefaultTargets.All.name}-{config.config_string}'
                write_keyword(writer, 'Alias', this_all_name)
                with bff_struct(writer, is_keyword=True) as struct:
                    compilable_targets = filter(filter_target_linkable, context.resolved_targets)
                    struct.write_field('Targets', [
                        ProjectTemplate.compile_target_variant_name(t.get_kind(), t.target.target, config.config_string) for t in compilable_targets
                    ])

            writer.newline()
            write_comment_header(writer, 'Solution')
            writer.newline()

            write_keyword(writer, vcxproject.VCXProject.keyword(), all_target_proj)
            with bff_struct(writer, is_keyword=True) as struct:
                struct.write_field('ProjectOutput', self.get_vcxproj_path(EDefaultTargets.All.name))
                struct.write_field('ProjectInputPaths', [ FBUILD_OUTPUT_DIR ])
                struct.write_field('ProjectBasePath', 'build')
                struct.write_field('ProjectFiles', project_files)
                struct.write_field('OutputDirectory', PROJ_OUTPUT_DIR)

                struct.next()
                struct.write_field('Configs', [])

                with bff_foreach(writer, '.ConfigMatrix'):
                    writer.newline()
                    write_assign(writer, 'ThisConfig')

                    with bff_struct(writer) as struct:
                        write_using(writer, 'Itr')
                        all_config_name = f'{EDefaultTargets.All.name}-$Config$'
                        struct.write_field('Target', all_config_name)
                        struct.write_field('ProjectBuildCommand', self.get_build_command(context, all_config_name))
                        struct.write_field('ProjectRebuildCommand', self.get_rebuild_command(context, all_config_name))

                    writer.newline()
                    write_append_field(writer, 'Configs', '.ThisConfig', outer_scope=True, no_quotations=True)

                writer.newline()
                struct.write_assign('ProjectConfigs', '.Configs')

            #
            # Update solution
            #
            update_target_proj = f'{EDefaultTargets.Update.name}-proj'

            writer.newline()
            write_keyword(writer, vcxproject.VCXProject.keyword(), update_target_proj)
            with bff_struct(writer, is_keyword=True) as struct:
                struct.write_field('ProjectOutput', self.get_vcxproj_path(EDefaultTargets.Update.name))
                struct.write_field('OutputDirectory', PROJ_OUTPUT_DIR)

                struct.next()
                struct.next()
                write_assign(writer, 'GenerateProjectsCommands')
                with bff_struct(writer) as substruct:
                    substruct.write_field('ProjectBuildCommand', self.get_update_build_command(context))
                    substruct.write_field('ProjectRebuildCommand', self.get_update_rebuild_command(context))

                struct.next()
                struct.write_field('Configs', [])

                with bff_foreach(writer, '.ConfigMatrix'):
                    writer.newline()
                    write_assign(writer, 'ThisConfig')

                    with bff_struct(writer):
                        write_using(writer, 'Itr')
                        write_using(writer, 'GenerateProjectsCommands')

                    writer.newline()
                    write_append_field(writer, 'Configs', '.ThisConfig', outer_scope=True, no_quotations=True)

                writer.newline()
                struct.write_assign('ProjectConfigs', '.Configs')

            #
            # Default targets - commands
            #
            default_targets = [ all_target_proj, update_target_proj ]

            #
            # Solution
            #
            writer.newline()
            write_keyword(writer, sln.keyword(), f'{sln.Alias}')
            with bff_struct(writer, is_keyword=True) as struct:
                struct.write_field('SolutionOutput', f'{project_name}.sln')
                struct.write_field('SolutionProjects', default_targets + all_projects)
                struct.next()
                writer.write('.SolutionConfigs = .ConfigMatrix')
                struct.next()
                writer.write(f".SolutionBuildProject = '{all_target_proj}'")
                writer.newline()
                write_assign(writer, 'SolutionDependencies')
                with bff_struct(writer) as substruct:
                    substruct.write_field('Projects', all_projects)
                    substruct.write_field('Dependencies', [all_target_proj])

                min_ver, max_ver = get_min_max_msvc_version(context, context.project_template)
                struct.write_field('SolutionVisualStudioVersion', max_ver)
                struct.write_field('SolutionMinimumVisualStudioVersion', min_ver)

                # Folders
                folders = []
                def add_folder(name : str, title : str, targets : List[str]):
                    folders.append(f'.{name}')
                    writer.newline()
                    write_assign(writer, name)
                    with bff_struct(writer) as commands:
                        commands.write_field('Path', title)
                        commands.write_field('Projects', targets)

                writer.newline()
                add_folder('Folder_Commands', '0. Commands', default_targets)
                add_folder('Folder_Conan', '1. Dependencies', [ f'{target.target}-proj' for target in context.conan.dependencies ])
                add_folder('Folder_Libraries', '2. Libraries', [ f'{target.target}-proj' for target in filter(filter_target_libraries, context.project_template.targets) ])
                add_folder('Folder_Apps', '3. Apps', [ f'{target.target}-proj' for target in filter(filter_target_executable, context.project_template.targets) ])

                writer.newline()
                write_field(writer, 'SolutionFolders', folders, no_quotations=True)

            context.write_file(self.main_file_name, writer.to_string())

        self.emit_runner_script(context)

        return self.main_file_name


    def emit_target(self, context: EmitContext, target: ResolvedTarget):
        target_name = target.target.target
        target_file_name = f'{target_name}.bff'
        target_file_path = f'{FBUILD_OUTPUT_DIR}/{target_name}/{target_file_name}'
        project_configs_list = f'{target_name}_ProjectConfigs'

        project_paths = []
        config_includes = []

        if target.target.config.kind == ETargetKind.EXECUTABLE:
            config_includes = self.emit_executable(context, target, target_name, project_configs_list)

        elif target.target.config.kind == ETargetKind.DYNAMIC_LIBRARY:
            config_includes = self.emit_dynamic_library(context, target, target_name, project_configs_list)

        elif target.target.config.kind == ETargetKind.STATIC_LIBRARY:
            config_includes = self.emit_static_library(context, target, target_name, project_configs_list)

        elif target.target.config.kind == ETargetKind.IMPORTED_TARGET or target.target.config.kind == ETargetKind.HEADER_ONLY:
            config_includes = self.emit_imported_target(context, target, target_name, project_configs_list)

        elif target.target.config.kind == ETargetKind.BUILD_STEP:
            config_includes = self.emit_custom_build_step(context, target, target_name, project_configs_list)

        else:
            raise NotImplementedError(f"FASTBuild bff emission for target kind '{target.target.config.kind.name}' is not yet implemented!")

        vcxproj = vcxproject.VCXProject()
        vcxproj.Alias = f'{target_name}-proj'
        vcxproj.ProjectOutput = self.get_vcxproj_path(target_name)
        vcxproj.ProjectConfigs = project_configs_list
        vcxproj.ProjectBasePath = self.get_target_output_path(context, target)
        vcxproj.ProjectInputPaths = [vcxproj.ProjectBasePath] + project_paths
        vcxproj.ProjectFiles = [
            # f'{FBUILD_OUTPUT_DIR}/{target_name}/{target_file_name}',
            self.project_to_output_path(context, target.target.get_loaded_from_file())
        ]

        writer = create_bff_writer()
        self.emit_vcxproj(writer, vcxproj, project_configs_list, config_includes)

        context.write_file(target_file_path, writer.to_string())
        self.subprojects.append(target_file_path)
