from enum import Enum
from abc import ABC, abstractmethod
import os
import re
import subprocess
import tomllib

# The file used to write output build definitions.
_gOutputFile = ''

# The relative directory that is currently being processed. When files are
# referenced they are relative to this path.
_gRelativeDir = ''

# Global compiler flags
_gProjectCflags = []
_gProjectCppflags = []

_gProjectVersion = 'unknown'
_gProjectOptions = []

# Caches the list of dependencies found in .toml config files
# Structure:
# DependencyTargetType
#   SHARED_LIBRARY = 1
#   STATIC_LIBRARY = 2
#   HEADER_LIBRARY = 3
# See meson_impl.py
# external_dep = {
#   'zlib': {
#       # target_name: target_type
#       'libz': 2
#   },
# }
external_dep = {}


class IncludeDirectories:
    def __init__(self, name: str, dirs: []):
        self.name = name
        self.dirs = dirs

    def __iter__(self):
        return iter([self])


class File:
    def __init__(self, name: str):
        self.name = name


class Machine:
    def __init__(self, system, cpu, cpu_family):
        self._system = system
        self._cpu = cpu
        self._cpu_family = cpu_family

    def system(self):
        return self._system

    def set_system(self, system: str):
        self._system = system

    def cpu_family(self):
        return self._cpu_family

    def cpu(self):
        return self._cpu


class DependencyTargetType(Enum):
    SHARED_LIBRARY = 1
    STATIC_LIBRARY = 2
    HEADER_LIBRARY = 3


class DependencyTarget:
    def __init__(self, target_name: str, target_type: DependencyTargetType):
        self.target_name = target_name
        self.target_type = target_type


class Dependency:
    _id_generator = 1000

    def __init__(
        self,
        name: str,
        version='',
        found=False,
        targets=[],
        compile_args=[],
        include_directories=[],
        dependencies=[],
        sources=[],
        link_with=[],
        link_whole=[],
    ):
        self.name = name
        self.targets = targets
        self._version = version
        self._found = found
        self.compile_args = compile_args
        self.include_directories = include_directories
        self.dependencies = dependencies
        self.sources = sources
        self.link_with = link_with
        self.link_whole = link_whole
        Dependency._id_generator += 1
        self.unique_id = Dependency._id_generator

    def version(self):
        return self._version

    def found(self):
        return self._found

    def partial_dependency(self, compile_args=''):
        return self

    def __iter__(self):
        return iter([self])

    def __hash__(self):
        return hash(self.unique_id)

    def __eq__(self, other):
        return self.unique_id == other.unique_id


class CommandReturn:
    def __init__(self, completed_process):
        self.completed_process = completed_process

    def returncode(self):
        return self.completed_process.returncode

    def stdout(self):
        return self.completed_process.stdout


class Program:
    def __init__(self, command, found: bool):
        self.command = command
        self._found = found

    # Running commands from the ambient system may give wrong/misleading results, since
    # some build systems use hermetic installations of tools like python.
    def run_command(self, *commands, capture_output=False):
        command_line = [self.command]
        for command in commands:
            command_line += command

        completed_process = subprocess.run(
            command_line, check=False, capture_output=capture_output
        )
        return CommandReturn(completed_process)

    def found(self):
        return self._found

    def full_path(self):
        return 'full_path'


class PythonModule:
    def find_installation(self, name: str):
        if name == 'python3':
            return Program(name, found=True)
        exit('Unhandled python installation: ' + name)


class EnableState(Enum):
    ENABLED = 1
    DISABLED = 2
    AUTO = 3


class FeatureOption:
    def __init__(self, name, state=EnableState.AUTO):
        self.name = name
        self.state = state

    def allowed(self):
        return self.state == EnableState.ENABLED or self.state == EnableState.AUTO

    def enabled(self):
        return self.state == EnableState.ENABLED

    def disabled(self):
        return self.state == EnableState.DISABLED

    def disable_auto_if(self, value: bool):
        if value and self.state == EnableState.AUTO:
            self.state = EnableState.DISABLED
        return self

    def disable_if(self, value: bool, error_message: str):
        if not value:
            return self
        if self.state == EnableState.ENABLED:
            exit(error_message)
        return FeatureOption(self.name, state=EnableState.DISABLED)

    def require(self, value: bool, error_message: str):
        if value:
            return self
        if self.state == EnableState.ENABLED:
            exit(error_message)
        return FeatureOption(self.name, state=EnableState.DISABLED)

    def set(self, value: str):
        value = value.lower()
        if value == 'auto':
            self.state = EnableState.AUTO
        elif value == 'enabled':
            self.state = EnableState.ENABLED
        elif value == 'disabled':
            self.state = EnableState.DISABLED
        else:
            exit('Unable to set feature to: %s' % value)


class ArrayOption:
    def __init__(self, name: str, value: []):
        self.name = name
        self.strings = value

    def set(self, value: str):
        if value == '':
            self.strings = []
        else:
            self.strings = [value]


class ComboOption:
    def __init__(self, name: str, value: str):
        self.name = name
        self.value = value

    def set(self, value: str):
        self.value = value


class BooleanOption:
    def __init__(self, name, value: bool):
        assert type(value) is bool
        self.name = name
        self.value = value

    def set(self, value: str):
        self.value = bool(value)


# Value can be string or other type
class SimpleOption:
    def __init__(self, name, value):
        self.name = name
        self.value = value

    def set(self, value: str):
        if type(self.value) is int:
            self.value = int(value)
        else:
            self.value = value


class Environment:
    def set(self, var, val):
        return

    def append(self, var, val):
        return


class StaticLibrary:
    def __init__(self, target_name, link_with=[], link_whole=[]):
        self.target_name = target_name
        self.link_with = link_with
        self.link_whole = link_whole


class SharedLibrary:
    name = ''

    def __init__(self, name):
        self.name = name


class Executable:
    def __init__(self, name):
        self.name = name


class CustomTargetItem:
    def __init__(self, custom_target, index):
        self.target = custom_target
        self.index = index


class CustomTarget:
    def __init__(self, name, outputs=[], generates_h=False, generates_c=False):
        self._name = name
        self._outputs = outputs
        self._generates_h = generates_h
        self._generates_c = generates_c

    @property
    def outputs(self):
        return self._outputs

    def generates_h(self):
        return self._generates_h

    def generates_c(self):
        return self._generates_c

    def target_name(self):
        return self._name

    def target_name_h(self):
        if self._generates_h and self._generates_c:
            return self._name + '.h'
        return self._name

    def target_name_c(self):
        if self._generates_h and self._generates_c:
            return self._name + '.c'
        return self._name

    def header_outputs(self):
        hdrs = []
        for out in self._outputs:
            if out.endswith('.h'):
                hdrs.append(out)
        return hdrs

    def __iter__(self):
        return iter([self])

    def __getitem__(self, index):
        return CustomTargetItem(self, index)

    def full_path(self):
        return 'fullpath'


class Meson:
    def __init__(self, compiler):
        self._compiler = compiler

    def get_compiler(self, language_string, native=False):
        return self._compiler

    def set_compiler(self, compiler):
        self._compiler = compiler

    def project_version(self):
        return _gProjectVersion

    def project_source_root(self):
        return os.getcwd()

    def is_cross_build(self):
        return True

    def can_run_host_binaries(self):
        return False

    def current_source_dir(self):
        return os.getcwd()

    def current_build_dir(self):
        return '@CURRENT_BUILD_DIR@'

    def project_build_root(self):
        return '@PROJECT_BUILD_ROOT@'

    def add_devenv(self, env):
        return


class Compiler(ABC):
    def __init__(self, cpu_family):
        self._id = 'clang'
        self._cpu_family = cpu_family

    @abstractmethod
    def has_header_symbol(
        self,
        header: str,
        symbol: str,
        args=None,
        dependencies=None,
        include_directories=None,
        no_builtin_args: bool = False,
        prefix=None,
        required: bool = False,
    ) -> bool:
        pass

    @abstractmethod
    def check_header(self, header: str, prefix: str = '') -> bool:
        pass

    @abstractmethod
    def has_function(self, function, args=None, prefix='', dependencies='') -> bool:
        pass

    @abstractmethod
    def links(self, snippet: str, name: str, args=None, dependencies=None) -> bool:
        pass

    def get_id(self):
        return self._id

    def is_symbol_supported(self, header: str, symbol: str):
        if header == 'sys/mkdev.h' or symbol == 'program_invocation_name':
            return False
        return True

    def is_function_supported(self, function: str):
        if (
            function == 'qsort_s'
            or function == 'pthread_setaffinity_np'
            or function == 'secure_getenv'
        ):
            return False
        return True

    def is_link_supported(self, name: str):
        if name == 'GNU qsort_r' or name == 'BSD qsort_r':
            return False
        return True

    def is_header_supported(self, header: str):
        if (
            header == 'xlocale.h'
            or header == 'pthread_np.h'
            or header == 'renderdoc_app.h'
        ):
            return False
        return True

    def get_define(self, define: str, prefix: str):
        if define == 'ETIME':
            return define
        exit('Unhandled define: ' + define)

    def get_supported_function_attributes(self, attributes: list[str]):
        # Assume all are supported
        return attributes

    def has_function_attribute(self, attribute: str):
        return True

    def has_argument(self, name: str):
        result = True
        print("has_argument '%s': %s" % (name, str(result)))
        return result

    def has_link_argument(self, name: str):
        result = True
        print("has_link_argument '%s': %s" % (name, str(result)))
        return result

    def compiles(self, snippet, name: str):
        # Exclude what is currently not working.
        result = True
        if name == '__uint128_t':
            result = False
        print("compiles '%s': %s" % (name, str(result)))
        return result

    def has_member(self, struct, member, prefix):
        # Assume it does
        return True

    def get_argument_syntax(self):
        return 'gcc'

    def get_supported_arguments(self, args):
        supported_args = []
        for arg in args:
            if (
                arg.startswith('-flifetime-dse')
                or arg.startswith('-Wno-format-truncation')
                or arg.startswith('-Wno-nonnull-compare')
                or arg.startswith('-Wno-class-memaccess')
                or arg.startswith('-Wno-format-truncation')
            ):
                continue
            supported_args.append(arg)
        return supported_args

    def get_supported_link_arguments(self, args):
        return args

    def find_library(self, name, required=False):
        if name == 'ws2_32' or name == 'elf' or name == 'm' or name == 'sensors':
            return Dependency(name, found=required)
        exit('Unhandled library: ' + name)

    def sizeof(self, string):
        table = _get_sizeof_table(self._cpu_family)

        if string not in table:
            exit('Unhandled compiler sizeof: ' + string)
        return table[string]


class PkgConfigModule:
    def generate(
        self,
        lib,
        name='',
        description='',
        extra_cflags=None,
        filebase='',
        version='',
        libraries=None,
        libraries_private=None,
    ):
        pass


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


def fprint(args):
    print(args, file=_gOutputFile)


def set_relative_dir(dir):
    global _gRelativeDir
    _gRelativeDir = dir


def open_output_file(name):
    global _gOutputFile
    _gOutputFile = open(name, 'w')


def close_output_file():
    global _gOutputFile
    _gOutputFile.close()


def get_relative_dir(path_or_file=''):
    if isinstance(path_or_file, File):
        return path_or_file.name

    assert isinstance(path_or_file, str)
    if path_or_file == '':
        return _gRelativeDir
    return os.path.join(_gRelativeDir, path_or_file)


def get_relative_gen_dir(path=''):
    return os.path.join(_gRelativeDir, path)


def project(name, language_list, version, license, meson_version, default_options):
    if type(version) is str:
        _gProjectVersion = version
    else:
        assert type(version) is list
        version_file = version[0]
        assert type(version_file) is File
        with open(version_file.name, 'r') as file:
            for line in file:
                _gProjectVersion = line.strip()
                break

    for option in default_options:
        value_pair = option.split('=')
        _gProjectOptions.append(SimpleOption(value_pair[0], value_pair[1]))


def get_project_options():
    return _gProjectOptions


def add_project_arguments(args, language=[], native=False):
    global _gProjectCflags, _gProjectCppflags
    if type(args) is not list:
        args = [args]
    for lang in language:
        for arg in args:
            if isinstance(arg, list):
                add_project_arguments(arg, language=language, native=native)
                continue
            assert isinstance(arg, str)
            if lang == 'c':
                print('cflags: ' + arg)
                _gProjectCflags.append(arg)
            elif lang == 'cpp':
                print('cppflags: ' + arg)
                _gProjectCppflags.append(arg)
            else:
                exit('Unhandle arguments language: ' + lang)


def get_project_cflags():
    return _gProjectCflags


def get_project_cppflags():
    return _gProjectCppflags


def _get_sizeof_table(cpu_family):
    table_32 = {'void*': 4}
    table_64 = {'void*': 8}
    if cpu_family == 'arm' or cpu_family == 'x86_64':
        table = table_32
    elif cpu_family == 'aarch64':
        table = table_64
    else:
        exit('sizeof unhandled cpu family: %s' % cpu_family)
    return table


def get_linear_list(arg_list):
    args = []
    for arg in arg_list:
        if type(arg) is list:
            args.extend(get_linear_list(arg))
        else:
            args.append(arg)
    return args


def load_dependencies(config):
    with open(config, 'rb') as f:
        data = tomllib.load(f)
        project_configs = data.get('project_config')
        base_config = data.get('base_project_config')
        # global dependencies
        for dep_name, targets in base_config.get('ext_dependencies').items():
            dep_targets = {
                t.get('target_name'): t.get('target_type') for t in targets
            }
            external_dep[dep_name] = dep_targets
        # project specific dependencies
        for project_config in project_configs:
            dependencies = project_config.get('ext_dependencies')
            for dep_name, targets in dependencies.items():
                dep_targets = {
                    t.get('target_name'): t.get('target_type') for t in targets
                }
                external_dep[dep_name] = dep_targets


def dependency(*names, required=True, version=''):
    for name in names:
        print('dependency: %s' % name)
        if name == '':
            return Dependency('null', version, found=False)

        if name in external_dep:
            targets = external_dep.get(name)
            return Dependency(
                name,
                targets=[
                    DependencyTarget(t, DependencyTargetType(targets[t]))
                    for t in targets
                ],
                version=version,
                found=True,
            )
        # TODO(bpnguyen): Move these hardcoded dependencies to a global config
        if (
            name == 'backtrace'
            or name == 'curses'
            or name == 'expat'
            or name == 'libconfig'
            or name == 'libmagma_virt'
            or name == 'libva'
            or name == 'libzstd'
            or name == 'libdrm'
            or name == 'libglvnd'
            or name == 'libudev'
            or name == 'libunwind'
            or name == 'llvm'
            or name == 'libxml-2.0'
            or name == 'lua54'
            or name == 'valgrind'
            or name == 'wayland-scanner'
            or name == 'SPIRV-Tools'
        ):
            return Dependency(name, version, found=False)

        if (
            name == 'libarchive'
            or name == 'libelf'
            or name == 'threads'
            or name == 'vdpau'
        ):
            return Dependency(name, version, found=required)

        exit('Unhandled dependency: ' + name)


def get_set_of_deps(deps, set_of_deps=set()):
    for dep in deps:
        if type(dep) is list:
            set_of_deps = get_set_of_deps(dep, set_of_deps)
        elif dep not in set_of_deps:
            set_of_deps.add(dep)
            set_of_deps = get_set_of_deps(dep.dependencies, set_of_deps)
    return set_of_deps


def get_include_dirs(paths) -> list[str]:
    dir_list = []
    for path in paths:
        if type(path) is list:
            dir_list.extend(get_include_dirs(p for p in path))
        elif type(path) is IncludeDirectories:
            dir_list.extend(path.dirs)
        else:
            assert type(path) is str
            dir_list.append(get_relative_dir(path))
    return dir_list


def get_include_directories(includes) -> list[IncludeDirectories]:
    dirs = []
    if type(includes) is list:
        for inc in includes:
            dirs.extend(get_include_directories(inc))
    elif type(includes) is IncludeDirectories:
        dirs.extend(includes)
    else:
        assert type(includes) is str
        exit('get_include_directories got string: %s' % includes)
    return dirs


def get_static_libs(arg_list):
    libs = []
    for arg in arg_list:
        if type(arg) is list:
            libs.extend(get_static_libs(arg))
        else:
            assert type(arg) is StaticLibrary
            libs.extend(get_static_libs(arg.link_with))
            libs.append(arg)
    return libs


def get_whole_static_libs(arg_list):
    libs = []
    for arg in arg_list:
        if type(arg) is list:
            libs.extend(get_whole_static_libs(arg))
        else:
            assert type(arg) is StaticLibrary
            libs.extend(get_whole_static_libs(arg._link_whole))
            libs.append(arg)
    return libs


def get_list_of_relative_inputs(list_or_string):
    if isinstance(list_or_string, list):
        ret = []
        for item in list_or_string:
            ret.extend(get_list_of_relative_inputs(item))
        return ret

    return [get_relative_dir(list_or_string)]


def get_command_line_from_args(args: list):
    command_line = ''
    for arg in args:
        command_line += ' ' + arg
    # Escape angle brackets
    command_line = re.sub(r'(<|>)', '\\\\\\\\\g<1>', command_line)
    return command_line


def replace_wrapped_input_with_target(args, python_script, python_script_target_name):
    outargs = []
    for index, arg in enumerate(args):
        pattern = '(.*?)(' + python_script + ')'
        replace = '\g<1>' + python_script_target_name
        outargs.append(re.sub(pattern, replace, arg))
    return outargs
