import os, concurrent.futures
from .build_target import BuildTarget
from .build_dependency import BuildDependency
from .util import save_file_if_contents_changed
from .system import console


def get_cmake_path_list(paths):
    pathlist = '' 
    for path in paths: pathlist += f'\n    "{path}"'
    return pathlist


def get_flattened_deps(root: BuildDependency):
    # deps have to be sorted in [parent] [child] order for Unix linkers
    ordered = []
    def add_unique_items(deps):
        for child in deps:
            if child in ordered: # already in deps list, so we need to move it lower
                ordered.remove(child)
            ordered.append(child)
            add_unique_items(child.children)
    add_unique_items(root.children)

    if len(ordered) > 1:
        names = [dep.name for dep in ordered]
        console(f'  - Dependency Order:\n        {" ".join(names)}')
    return ordered


def save_dependencies_cmake(root: BuildDependency):
    if not root.build_dir_exists():
        return # probably CLEAN, so nothing to save
    outfile = f'{root.build_dir}/mama-dependencies.cmake'
    if not root.children:
        if os.path.exists(outfile):
            os.remove(outfile) # no more deps, get rid of the dependency file
        return
    text = '''
# This file is auto-generated by mama build. Do not modify by hand!
'''
    flatdeps = get_flattened_deps(root)
    for dep in flatdeps:
        includes  = get_cmake_path_list(dep.target.exported_includes)
        libraries = get_cmake_path_list(dep.target.exported_libs)
        text += f'''
# Package {dep.name}
set({dep.name}_INCLUDES {includes})
set({dep.name}_LIBS {libraries})
set(MAMA_INCLUDES ${{MAMA_INCLUDES}} ${{{dep.name}_INCLUDES}})
set(MAMA_LIBS     ${{MAMA_LIBS}}     ${{{dep.name}_LIBS}})
'''
    save_file_if_contents_changed(outfile, text)


def get_mama_dependencies_cmake(root: BuildDependency, build:str):
    if not root.children:
        return ''
    return f'''# get MAMA_INCLUDES and MAMA_LIBS for this platform; verbose for CLion CMake parser
    include("{root.dep_dir}/{build}/mama-dependencies.cmake")'''


def mama_cmake_path(root: BuildDependency):
    return f'{root.src_dir}/mama.cmake'


def save_mama_cmake(root: BuildDependency):
    save_dependencies_cmake(root)
    # note: we save verbose include directives, because CLion has a hard time detecting macro paths
    text = f'''
# This file is auto-generated by mama build. Do not modify by hand!
if(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
    set(CLANG TRUE)
elseif(CMAKE_CXX_COMPILER_ID MATCHES "GNU")
    set(GCC TRUE)
endif()
set(MAMA_INCLUDE "")
set(MAMA_LIBS "")
if(ANDROID OR ANDROID_NDK)
    set(MAMA_BUILD "android")
    {get_mama_dependencies_cmake(root, 'android')}
elseif(WIN32)
    set(MAMA_BUILD "windows")
    {get_mama_dependencies_cmake(root, 'windows')}
elseif(APPLE)
  if(IOS_PLATFORM)
    set(IOS TRUE)
    set(MAMA_BUILD "ios")
    {get_mama_dependencies_cmake(root, 'ios')}
  else()
    set(MACOS TRUE)
    set(MAMA_BUILD "macos")
    {get_mama_dependencies_cmake(root, 'macos')}
  endif()
elseif(UNIX)
    set(LINUX TRUE)
    set(MAMA_BUILD "linux")
    {get_mama_dependencies_cmake(root, 'linux')}
else()
    message(FATAL_ERROR "mama build: Unsupported Platform!")
    set(MAMA_BUILD "???")
endif()
'''
    save_file_if_contents_changed(mama_cmake_path(root), text)


def _load_child_dependencies(root: BuildDependency, parallel=True):
    changed = False
    if parallel:
        futures = []
        with concurrent.futures.ThreadPoolExecutor(max_workers=8) as e:
            for dep in root.children:
                futures.append(e.submit(_load_dependency_chain, dep))
        for f in futures:
            changed |= f.result()
    else:
        for dep in root.children:
            changed |= _load_dependency_chain(dep)
    return changed


def _load_dependency_chain(dep: BuildDependency):
    if dep.already_loaded:
        return dep.should_rebuild

    changed = dep.load()
    changed |= _load_child_dependencies(dep)
    return changed


def load_dependency_chain(root: BuildDependency):
    with concurrent.futures.ThreadPoolExecutor() as e:
        def load_dependency(dep: BuildDependency):
            if dep.already_loaded:
                return dep.should_rebuild
            changed = dep.load()
            futures = []
            for child in dep.children:
                futures.append(e.submit(load_dependency, child))
            for f in futures:
                changed |= f.result()
            dep.after_load()
            return changed
        load_dependency(root)


def execute_task_chain(root: BuildDependency):
    if not os.path.exists(mama_cmake_path(root)):
        save_mama_cmake(root) # save a dummy mama.cmake before build

    for dep in root.children:
        execute_task_chain(dep)
    
    save_mama_cmake(root)
    root.target.execute_tasks()


def find_dependency(root: BuildDependency, name) -> BuildDependency:
    if root.name == name:
        return root
    for dep in root.children:
        found = find_dependency(dep, name)
        if found: return found
    return None
