from __future__ import annotations
from dataclasses import dataclass
import platform
import re
import subprocess
from typing import List, Optional

if platform.system() == "Windows":
    import ctypes
    from ctypes import wintypes


@dataclass
class MachineSpec:
    os: str
    arch: str
    config: Optional[str] = None
    triplet: Optional[str] = None

    @staticmethod
    def make_from_local_system() -> MachineSpec:
        os = detect_os()
        arch = detect_arch()
        config = None

        if os == "linux":
            try:
                output = subprocess.run(["ldd", "--version"],
                                        stdout=subprocess.PIPE,
                                        stderr=subprocess.STDOUT,
                                        encoding="utf-8").stdout
                if "musl" in output:
                    config = "musl"
            except:
                pass

        return MachineSpec(os, arch, config)

    @staticmethod
    def parse(raw_spec: str) -> MachineSpec:
        os = None
        arch = None
        config = None
        triplet = None

        tokens = raw_spec.split("-")
        if len(tokens) in {3, 4}:
            arch = tokens[0]
            m = TARGET_TRIPLET_ARCH_PATTERN.match(arch)
            if m is not None:
                kernel = tokens[-2]
                system = tokens[-1]

                if kernel == "w64":
                    os = "windows"
                elif kernel == "nto":
                    os = "qnx"
                else:
                    os = kernel

                if arch[0] == "i":
                    arch = "x86"
                elif arch == "arm":
                    if system.endswith("eabihf"):
                        arch = "armhf"
                    elif os == "qnx" and system.endswith("eabi"):
                        arch = "armeabi"
                elif arch == "armeb":
                    arch = "armbe8"
                elif arch == "aarch64":
                    arch = "arm64"
                elif arch == "aarch64_be":
                    arch = "arm64be"
                if system.endswith("_ilp32"):
                    arch += "ilp32"

                if system.startswith("musl"):
                    config = "musl"
                elif kernel == "w64":
                    config = "mingw"

                triplet = raw_spec

        if os is None:
            os, arch, *rest = tokens
            if rest:
                assert len(rest) == 1
                config = rest[0]

        return MachineSpec(os, arch, config, triplet)

    def evolve(self,
               os: Optional[str] = None,
               arch: Optional[str] = None,
               config: Optional[str] = None,
               triplet: Optional[str] = None) -> MachineSpec:
        return MachineSpec(
            os if os is not None else self.os,
            arch if arch is not None else self.arch,
            config if config is not None else self.config,
            triplet if triplet is not None else self.triplet,
        )

    def default_missing(self, recommended_vscrt: Optional[str] = None) -> MachineSpec:
        config = self.config
        if config is None and self.toolchain_is_msvc:
            if recommended_vscrt is not None:
                config = recommended_vscrt
            else:
                config = "mt"
        return self.evolve(config=config)

    def maybe_adapt_to_host(self, host_machine: MachineSpec) -> MachineSpec:
        if self.identifier == host_machine.identifier and host_machine.triplet is not None:
            return host_machine
        if self.os == "windows" and host_machine.os == "windows":
            if host_machine.arch in {"x86_64", "x86"}:
                return host_machine
            if self.arch == host_machine.arch:
                return host_machine
        return self

    @property
    def identifier(self) -> str:
        parts = [self.os, self.arch]
        if self.config is not None:
            parts += [self.config]
        return "-".join(parts)

    @property
    def os_dash_arch(self) -> str:
        return f"{self.os}-{self.arch}"

    @property
    def os_dash_config(self) -> str:
        parts = [self.os]
        if self.config is not None:
            parts += [self.config]
        return "-".join(parts)

    @property
    def config_is_optimized(self) -> bool:
        if self.toolchain_is_msvc:
            return self.config in {"md", "mt"}
        return True

    @property
    def meson_optimization_options(self) -> List[str]:
        if self.config_is_optimized:
            optimization = "s"
            ndebug = "true"
        else:
            optimization = "0"
            ndebug = "false"
        return [
            f"-Doptimization={optimization}",
            f"-Db_ndebug={ndebug}",
        ]

    @property
    def executable_suffix(self) -> str:
        return ".exe" if self.os == "windows" else ""

    @property
    def msvc_platform(self) -> str:
        return "x64" if self.arch == "x86_64" else self.arch

    @property
    def is_apple(self) -> str:
        return self.os in {"macos", "ios", "watchos", "tvos", "xros"}

    @property
    def system(self) -> str:
        return "darwin" if self.is_apple else self.os

    @property
    def subsystem(self) -> str:
        return self.os_dash_config if self.is_apple else self.os

    @property
    def kernel(self) -> str:
        return KERNELS.get(self.os, self.os)

    @property
    def cpu_family(self) -> str:
        arch = self.arch
        return CPU_FAMILIES.get(arch, arch)

    @property
    def cpu(self) -> str:
        arch = self.arch

        mappings_to_search = [
            CPU_TYPES_PER_OS_OVERRIDES.get(self.os, {}),
            CPU_TYPES,
        ]
        for m in mappings_to_search:
            cpu = m.get(arch, None)
            if cpu is not None:
                return cpu

        return arch

    @property
    def endian(self) -> str:
        return "big" if self.arch in BIG_ENDIAN_ARCHS else "little"

    @property
    def pointer_size(self) -> int:
        arch = self.arch
        if arch in {"x86_64", "s390x"}:
            return 8
        if (arch.startswith("arm64") and not arch.endswith("ilp32")) or arch.startswith("mips64"):
            return 8
        return 4

    @property
    def libdatadir(self) -> str:
        return "libdata" if self.os == "freebsd" else "lib"

    @property
    def toolchain_is_msvc(self) -> bool:
        return self.os == "windows" and self.config != "mingw"

    @property
    def toolchain_can_strip(self) -> bool:
        return not self.toolchain_is_msvc

    def __eq__(self, other):
        if isinstance(other, MachineSpec):
            return other.identifier == self.identifier
        return False


def detect_os() -> str:
    os = platform.system().lower()
    if os == "darwin":
        os = "macos"
    return os


def detect_arch() -> str:
    if platform.system() == "Windows":
        return detect_arch_windows()
    arch = platform.machine().lower()
    return ARCHS.get(arch, arch)

def detect_arch_windows():
    try:
        code = detect_arch_windows_modern()
    except AttributeError:
        code = detect_arch_windows_legacy()
    if code == PROCESSOR_ARCHITECTURE_INTEL:
        return "x86"
    elif code in {PROCESSOR_ARCHITECTURE_AMD64, IMAGE_FILE_MACHINE_AMD64}:
        return "x86_64"
    elif code in {PROCESSOR_ARCHITECTURE_ARM64, IMAGE_FILE_MACHINE_ARM64}:
        return "arm64"
    else:
        raise RuntimeError(f"unrecognized native architecture code: {code!r}")

def detect_arch_windows_modern():
    kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)

    try:
        is_wow64_process = kernel32.IsWow64Process2
    except AttributeError:
        raise

    is_wow64_process.argtypes = (
        wintypes.HANDLE,
        ctypes.POINTER(wintypes.WORD),
        ctypes.POINTER(wintypes.WORD),
    )
    is_wow64_process.restype = wintypes.BOOL

    process_machine = wintypes.WORD(0)
    native_machine  = wintypes.WORD(0)

    ok = is_wow64_process(
        kernel32.GetCurrentProcess(),
        ctypes.byref(process_machine),
        ctypes.byref(native_machine)
    )
    if not ok:
        raise ctypes.WinError(ctypes.get_last_error())

    return native_machine.value

def detect_arch_windows_legacy():
    class SYSTEM_INFO(ctypes.Structure):
        _fields_ = [
            ("wProcessorArchitecture",      wintypes.WORD),
            ("wReserved",                   wintypes.WORD),
            ("dwPageSize",                  wintypes.DWORD),
            ("lpMinimumApplicationAddress", ctypes.c_void_p),
            ("lpMaximumApplicationAddress", ctypes.c_void_p),
            ("dwActiveProcessorMask",       ctypes.c_void_p),
            ("dwNumberOfProcessors",        wintypes.DWORD),
            ("dwProcessorType",             wintypes.DWORD),
            ("dwAllocationGranularity",     wintypes.DWORD),
            ("wProcessorLevel",             wintypes.WORD),
            ("wProcessorRevision",          wintypes.WORD),
        ]

    kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)

    get_native_system_info = kernel32.GetNativeSystemInfo
    get_native_system_info.argtypes = (ctypes.POINTER(SYSTEM_INFO),)
    get_native_system_info.restype = None

    info = SYSTEM_INFO()
    get_native_system_info(ctypes.byref(info))
    return info.wProcessorArchitecture


ARCHS = {
    "amd64": "x86_64",
    "armv7l": "armhf",
    "aarch64": "arm64",
}

KERNELS = {
    "windows": "nt",

    "macos":   "xnu",
    "ios":     "xnu",
    "watchos": "xnu",
    "tvos":    "xnu",

    "qnx":     "nto",
}

CPU_FAMILIES = {
    "armbe8":       "arm",
    "armeabi":      "arm",
    "armhf":        "arm",
    "armv6kz":      "arm",

    "arm64":        "aarch64",
    "arm64be":      "aarch64",
    "arm64beilp32": "aarch64",
    "arm64e":       "aarch64",
    "arm64eoabi":   "aarch64",

    "mipsel":       "mips",
    "mips64el":     "mips64",

    "powerpc":      "ppc"
}

CPU_TYPES = {
    "arm":          "armv7",
    "armbe8":       "armv6",
    "armhf":        "armv7hf",
    "armeabi":      "armv7eabi",
    "armv6kz":      "armv6",

    "arm64":        "aarch64",
    "arm64be":      "aarch64",
    "arm64beilp32": "aarch64",
    "arm64e":       "aarch64",
    "arm64eoabi":   "aarch64",
}

CPU_TYPES_PER_OS_OVERRIDES = {
    "linux": {
        "arm":        "armv5t",
        "armbe8":     "armv6t",
        "armhf":      "armv7a",
        "armv6kz":    "armv6t",

        "mips":       "mips1",
        "mipsel":     "mips1",

        "mips64":     "mips64r2",
        "mips64el":   "mips64r2",
    },
    "android": {
        "x86":        "i686",
    },
    "qnx": {
        "arm":        "armv6",
        "armeabi":    "armv7",
    },
}

BIG_ENDIAN_ARCHS = {
    "arm64be",
    "arm64beilp32",
    "armbe8",
    "mips",
    "mips64",
    "ppc",
    "ppc64",
    "s390x",
}

TARGET_TRIPLET_ARCH_PATTERN = re.compile(r"^(i.86|x86_64|arm\w*|aarch64(_be)?|mips\w*|powerpc|s390x)$")

PROCESSOR_ARCHITECTURE_INTEL = 0
PROCESSOR_ARCHITECTURE_AMD64 = 9
PROCESSOR_ARCHITECTURE_ARM64 = 12

IMAGE_FILE_MACHINE_AMD64 = 0x8664
IMAGE_FILE_MACHINE_ARM64 = 0xAA64
