#!/usr/bin/env python3

import argparse
import os
import platform
import sys
from pathlib import Path


LIBFFI_VERSION = '3.6.0'
LIBFFI_VERSION_NUMBER = '30600'

def normalize_arch(target_arch):
    aliases = {
        'x86': 'ia32',
        'x86_64': 'x64',
        'arm64': 'arm64',
        'aarch64': 'arm64',
    }
    return aliases.get(target_arch, target_arch)


def get_target(os_name, target_arch):
    target_arch = normalize_arch(target_arch)

    if target_arch == 'arm':
        return ('ARM_WIN32' if os_name == 'win' else 'ARM', 'arm')

    if target_arch == 'arm64':
        return ('ARM_WIN64' if os_name == 'win' else 'AARCH64', 'aarch64')

    if target_arch == 'ia32':
        if os_name in ('freebsd', 'openbsd'):
            return ('X86_FREEBSD', 'x86')
        if os_name in ('ios', 'mac'):
            return ('X86_DARWIN', 'x86')
        if os_name == 'win':
            return ('X86_WIN32', 'x86')
        return ('X86', 'x86')

    if target_arch == 'x64':
        return ('X86_WIN64' if os_name == 'win' else 'X86_64', 'x86')

    if target_arch == 'riscv64':
        return ('RISCV', 'riscv')

    if target_arch == 'loong64':
        return ('LOONGARCH64', 'loongarch')

    if target_arch in ('mips', 'mipsel', 'mips64el'):
        if os_name in ('freebsd', 'linux', 'openbsd'):
            return ('MIPS', 'mips')

    if target_arch == 'ppc64':
        if os_name == 'aix':
            return ('POWERPC_AIX', 'powerpc')
        if os_name == 'mac':
            return ('POWERPC_DARWIN', 'powerpc')
        if os_name in ('freebsd', 'openbsd'):
            return ('POWERPC_FREEBSD', 'powerpc')
        if os_name == 'linux':
            return ('POWERPC', 'powerpc')

    raise ValueError(f'Unsupported libffi target {os_name}/{target_arch}.')


def has_long_double(os_name, target_arch):
    target_arch = normalize_arch(target_arch)

    if os_name == 'win':
        return '0'

    if os_name == 'mac' and target_arch == 'arm64':
        return '0'

    if target_arch == 'arm':
        return '0'

    return '1'


def uses_exec_trampoline_table(os_name, target_arch):
    target_arch = normalize_arch(target_arch)
    return os_name == 'mac' and target_arch in ('arm', 'arm64')


def render_ffi_header(base_dir, os_name, target_arch, target):
    template = (base_dir / 'include' / 'ffi.h.in').read_text(encoding='utf-8')
    replacements = {
        '@FFI_EXEC_TRAMPOLINE_TABLE@':
            '1' if uses_exec_trampoline_table(os_name, target_arch) else '0',
        '@FFI_VERSION_NUMBER@': LIBFFI_VERSION_NUMBER,
        '@FFI_VERSION_STRING@': LIBFFI_VERSION,
        '@HAVE_LONG_DOUBLE@': has_long_double(os_name, target_arch),
        '@TARGET@': target,
        '@VERSION@': LIBFFI_VERSION,
    }

    for source, replacement in replacements.items():
        template = template.replace(source, replacement)

    return template


def render_fficonfig(os_name, target_arch):
    target_arch = normalize_arch(target_arch)
    lines = [
        '/* Auto-generated by generate-headers.py. */',
        '#ifndef LIBFFI_FFICONFIG_H_',
        '#define LIBFFI_FFICONFIG_H_',
        '',
        '#define HAVE_ALLOCA 1',
        '#define HAVE_MEMCPY 1',
        '#define HAVE_MEMORY_H 1',
        '#define HAVE_STDINT_H 1',
        '#define HAVE_STRING_H 1',
        '#define STDC_HEADERS 1',
    ]

    if os_name != 'win':
        lines.extend([
            '#define HAVE_AS_CFI_PSEUDO_OP 1',
            '#define HAVE_HIDDEN_VISIBILITY_ATTRIBUTE 1',
            '#define HAVE_MKOSTEMP 1',
            '#define HAVE_RO_EH_FRAME 1',
            '#define EH_FRAME_FLAGS "a"',
        ])

    if target_arch in ('ia32', 'x64') and os_name != 'win':
        lines.append('#define HAVE_AS_X86_PCREL 1')

    if uses_exec_trampoline_table(os_name, target_arch):
        lines.append('#define FFI_EXEC_TRAMPOLINE_TABLE 1')
    elif os_name in ('freebsd', 'mac', 'openbsd'):
        lines.append('#define FFI_MMAP_EXEC_WRIT 1')

    if os_name != 'win':
        lines.extend([
            '#define HAVE_DLFCN_H 1',
            '#define HAVE_MMAP 1',
            '#define HAVE_MMAP_ANON 1',
            '#define HAVE_MMAP_FILE 1',
            '#define HAVE_SYS_MMAN_H 1',
            '#define HAVE_UNISTD_H 1',
        ])

    if os_name in ('freebsd', 'ios', 'mac', 'openbsd'):
        lines.extend([
            '#define HAVE_MMAP_DEV_ZERO 1',
        ])

    if os_name == 'mac' and target_arch == 'arm64':
        lines.extend([
            '#if defined(__arm64e__)',
            '#define HAVE_ARM64E_PTRAUTH 1',
            '#endif',
        ])

    lines.extend([
        '',
        '#ifdef HAVE_HIDDEN_VISIBILITY_ATTRIBUTE',
        '#ifdef LIBFFI_ASM',
        '#ifdef __APPLE__',
        '#define FFI_HIDDEN(name) .private_extern name',
        '#else',
        '#define FFI_HIDDEN(name) .hidden name',
        '#endif',
        '#else',
        '#define FFI_HIDDEN __attribute__ ((visibility ("hidden")))',
        '#endif',
        '#else',
        '#ifdef LIBFFI_ASM',
        '#define FFI_HIDDEN(name)',
        '#else',
        '#define FFI_HIDDEN',
        '#endif',
        '#endif',
        '',
        '#endif  /* LIBFFI_FFICONFIG_H_ */',
        '',
    ])

    return '\n'.join(lines)


def generate_headers(output_dir, target_arch, os_name):
    base_dir = Path(__file__).resolve().parent
    output_dir = Path(str(output_dir).strip().strip('"'))
    output_dir.mkdir(parents=True, exist_ok=True)

    target, target_dir = get_target(os_name, target_arch)
    ffitarget_src = base_dir / 'src' / target_dir / 'ffitarget.h'
    if not ffitarget_src.exists():
        raise FileNotFoundError(f'Missing libffi target header: {ffitarget_src}')

    (output_dir / 'ffi.h').write_text(
        render_ffi_header(base_dir, os_name, target_arch, target),
        encoding='utf-8')
    (output_dir / 'fficonfig.h').write_text(
        render_fficonfig(os_name, target_arch),
        encoding='utf-8')
    (output_dir / 'ffitarget.h').write_text(
        ffitarget_src.read_text(encoding='utf-8'),
        encoding='utf-8')


def detect_os_name():
    if sys.platform.startswith('win'):
        return 'win'
    if sys.platform == 'darwin':
        return 'mac'
    if sys.platform.startswith('linux'):
        return 'linux'
    if sys.platform.startswith('freebsd'):
        return 'freebsd'
    raise ValueError(f'Unsupported host platform {sys.platform!r}')


def detect_target_arch():
    candidates = [
        os.environ.get('TARGET_ARCH'),
        os.environ.get('npm_config_arch'),
        os.environ.get('VSCMD_ARG_TGT_ARCH'),
        os.environ.get('Platform'),
        os.environ.get('PROCESSOR_ARCHITECTURE'),
        platform.machine(),
    ]

    aliases = {
        'amd64': 'x64',
        'x86_64': 'x64',
        'x64': 'x64',
        'win32': 'x64',
        'i386': 'ia32',
        'i686': 'ia32',
        'ia32': 'ia32',
        'x86': 'ia32',
        'arm64': 'arm64',
        'aarch64': 'arm64',
        'arm': 'arm',
        'riscv64': 'riscv64',
        'loong64': 'loong64',
        'ppc64': 'ppc64',
        'mips': 'mips',
        'mipsel': 'mipsel',
        'mips64el': 'mips64el',
    }

    for candidate in candidates:
        if not candidate:
            continue
        normalized = aliases.get(candidate.lower())
        if normalized is not None:
            return normalized

    raise ValueError('Unable to determine target architecture for libffi headers')


def main(argv=None):
    parser = argparse.ArgumentParser(description='Generate libffi headers')
    parser.add_argument('--output-dir', required=True)
    parser.add_argument('--target-arch')
    parser.add_argument('--os')
    args = parser.parse_args(argv)

    try:
        generate_headers(args.output_dir,
                         args.target_arch or detect_target_arch(),
                         args.os or detect_os_name())
    except Exception as exc:  # pylint: disable=broad-except
        print(exc, file=sys.stderr)
        return 1

    return 0


if __name__ == '__main__':
    sys.exit(main())
