#!/usr/bin/env python3

# Copyright © Canonical Ltd.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or 3
# as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import logging
import sys
import os
import argparse
import concurrent.futures
from pathlib import Path
from typing import Optional, TypedDict
from collections import OrderedDict
import bisect

logging.basicConfig(level=logging.INFO)
_logger = logging.getLogger(__name__)

LIBRARY_NOT_FOUND_ERROR_MSG: str = """\
    Please install libclang-19-dev and python3-clang-19.
    Also, ensure that the required environment variables are set correctly. (e.g.)
        export MIR_SYMBOLS_MAP_GENERATOR_CLANG_SO_PATH=/usr/lib/llvm-19/lib/libclang.so.1
        export MIR_SYMBOLS_MAP_GENERATOR_CLANG_LIBRARY_PATH=/usr/lib/llvm-19/lib"""

try:
    import clang.cindex
except ModuleNotFoundError as e:
    _logger.error("Could not import clang in python.")
    _logger.exception(e)
    sys.exit(1)


HIDDEN_SYMBOLS = {
    "miral::SessionLockListener::?SessionLockListener*;",
    "miral::WaylandExtensions::Context::?Context*;",
    "miral::WaylandExtensions::Context::Context*;",
    "vtable?for?miral::WaylandExtensions::Context;",
    "mir::DefaultServerConfiguration::pointer_input_dispatcher*;",
    "miral::CursorScale::CursorScale*;",
    "miral::Keymap::Keymap*;",
    "miral::InputConfiguration::InputConfiguration*;",
    "miral::OutputFilter::OutputFilter*;",

    "mir::input::Transformer::?Transformer*;",
    "non-virtual?thunk?to?mir::input::Transformer::?Transformer*;",
    "typeinfo?for?mir::input::Transformer;",
    "vtable?for?mir::input::Transformer;",
    "miral::MouseKeysConfig::MouseKeysConfig*;",
    "miral::MouseKeysConfig::disable*;",
    "miral::MouseKeysConfig::disabled*;",
    "miral::MouseKeysConfig::enable*;",
    "miral::MouseKeysConfig::enabled*;",
    "miral::TouchEmulator::disable*;",
    "miral::TouchEmulator::disabled*;",
    "miral::TouchEmulator::enable*;",
    "miral::TouchEmulator::enabled*;",
    "miral::TouchEmulator::operator*;",
}

class HeaderDirectory(TypedDict):
    path: str
    is_internal: bool


class LibraryInfo(TypedDict):
    header_directories: list[HeaderDirectory]
    map_file: str

def get_version_from_library_version_str(version: str) -> str:
    """
    Given a string like MIR_SERVER_INTERNAL_5.0.0, returns the version
    string (e.g. 5.0.0)
    """
    return version.split("_")[-1]


def get_major_version_from_str(version: str) -> int:
    """
    Given a string like 5.0.0, returns the major version (e.g. 5).
    """
    return int(version.split('.')[0])


class Symbol:
    name: str
    version: str
    is_c_symbol: bool

    def __init__(self, name: str, version: str, is_c_symbol: bool):
        self.name = name
        self.version = version
        self.is_c_symbol = is_c_symbol

    def is_internal(self) -> bool:
        return self.version.startswith("INTERNAL_")



def should_generate_as_class_or_struct(node: clang.cindex.Cursor):
    return ((node.kind == clang.cindex.CursorKind.CLASS_DECL
          or node.kind == clang.cindex.CursorKind.STRUCT_DECL)
          and node.is_definition())


def is_operator(node: clang.cindex.Cursor) -> bool:
    if node.spelling.startswith("operator"):
        remainder = node.spelling[len("operator"):]
        if any(not c.isalnum() for c in remainder):
            return True

    return False


def create_node_symbol_name(node: clang.cindex.Cursor) -> str:
    if is_operator(node):
        return "operator"
    elif node.kind == clang.cindex.CursorKind.DESTRUCTOR:
        return f"?{node.spelling[1:]}"
    else:
        return node.spelling


def has_vtable(node: clang.cindex.Cursor):
    # This method assumes that the node is a class/struct
    for child in node.get_children():
        if ((child.kind == clang.cindex.CursorKind.CXX_METHOD
            or child.kind == clang.cindex.CursorKind.DESTRUCTOR
            or child.kind == clang.cindex.CursorKind.CONSTRUCTOR
            or child.kind == clang.cindex.CursorKind.CONVERSION_FUNCTION)
            and child.is_virtual_method()):
            return True

    # Otherwise, look up the tree
    base_classes: list[clang.cindex.Cursor] = []
    for child in node.semantic_parent.get_children():
        if child.kind != clang.cindex.CursorKind.CXX_BASE_SPECIFIER:
            continue

        class_or_struct_node = clang.cindex.conf.lib.clang_getCursorDefinition(child)
        if class_or_struct_node is None:
            continue

        base_classes.append(class_or_struct_node)

    for base in base_classes:
        if has_vtable(base):
            return True

    return False


def has_virtual_base_class(node: clang.cindex.Cursor):
    # This method assumes that the node is a class/struct

    result = False
    for child in node.get_children():
        if child.kind != clang.cindex.CursorKind.CXX_BASE_SPECIFIER:
            continue

        if clang.cindex.conf.lib.clang_isVirtualBase(child):
            result = True
        else:
            class_or_struct_node = clang.cindex.conf.lib.clang_getCursorDefinition(child)
            if class_or_struct_node is None:
                continue

            result = has_virtual_base_class(class_or_struct_node)

        if result:
            break

    return result


def is_function_inline(node: clang.cindex.Cursor):
    # This method assumes that the node is a FUNCTION_DECL.
    # Use libclang's dedicated query rather than node.is_definition(): the
    # latter is only a proxy for "inline" and, crucially, reports False once
    # PARSE_SKIP_FUNCTION_BODIES is in effect (the body it would key off is
    # skipped), which would wrongly emit symbols for inline free functions.
    # clang_Cursor_isFunctionInlined answers the question directly and is
    # stable regardless of whether function bodies were parsed.
    #
    # Also skip functions with internal linkage (static storage class): they
    # are never exported from the shared library regardless of whether they
    # are defined inline.  StorageClass is a declaration-level property and
    # is equally stable under PARSE_SKIP_FUNCTION_BODIES.
    if node.storage_class == clang.cindex.StorageClass.STATIC:
        return True
    return bool(clang.cindex.conf.lib.clang_Cursor_isFunctionInlined(node))


def get_namespace_str(node: clang.cindex.Cursor) -> list[str]:
    if node.kind == clang.cindex.CursorKind.TRANSLATION_UNIT:
        return []

    spelling = node.spelling
    n_template_args = node.type.get_num_template_arguments()
    if n_template_args != -1:
        template_types = [node.type.get_template_argument_type(i).spelling for i in range(n_template_args)]
        spelling += "<" + ",".join(template_types) + ">"
    if is_operator(node):
        spelling = "operator"
    elif node.kind == clang.cindex.CursorKind.DESTRUCTOR:
        spelling = f"?{node.spelling[1:]}"

    return get_namespace_str(node.semantic_parent) + [spelling]


def traverse_ast(node: clang.cindex.Cursor, filename: str, result: set[str]) -> set[str]:
    # Ignore private and protected variables
    if (node.access_specifier == clang.cindex.AccessSpecifier.PRIVATE):
        return result

    # Check if we need to output a symbol
    if ((node.location.file is not None and node.location.file.name == filename)
        and ((node.kind == clang.cindex.CursorKind.FUNCTION_DECL and not is_function_inline(node))
          or node.kind == clang.cindex.CursorKind.CXX_METHOD
          or node.kind == clang.cindex.CursorKind.VAR_DECL
          or node.kind == clang.cindex.CursorKind.CONSTRUCTOR
          or node.kind == clang.cindex.CursorKind.DESTRUCTOR
          or node.kind == clang.cindex.CursorKind.CONVERSION_FUNCTION
          or should_generate_as_class_or_struct(node))
          and not node.is_pure_virtual_method()
          and not node.is_anonymous()):

        namespace_str = "::".join(get_namespace_str(node))
        _logger.debug(f"Emitting node {namespace_str} in file {node.location.file.name}")

        def add_symbol_str(s: str):
            if '<' in s or '>' in s:
                s = s.replace('<', '?').replace('>', '?').replace(',', '*')
            s += ';'
            if not s in HIDDEN_SYMBOLS:
                result.add(s)

        # Classes and structs have a specific output
        if (node.kind == clang.cindex.CursorKind.CLASS_DECL
            or node.kind == clang.cindex.CursorKind.STRUCT_DECL):
            if has_vtable(node):
                add_symbol_str(f"vtable?for?{namespace_str}")
            if has_virtual_base_class(node):
                add_symbol_str(f"VTT?for?{namespace_str}")
            add_symbol_str(f"typeinfo?for?{namespace_str}")
        else:
            def add_internal(s: str):
                add_symbol_str(f"{s}*")
            add_internal(namespace_str)

            # Check if we're marked virtual
            is_virtual = False
            if ((node.kind == clang.cindex.CursorKind.CXX_METHOD
                or node.kind == clang.cindex.CursorKind.DESTRUCTOR
                or node.kind == clang.cindex.CursorKind.CONSTRUCTOR
                or node.kind == clang.cindex.CursorKind.CONVERSION_FUNCTION)
                and node.is_virtual_method()):
                add_internal(f"non-virtual?thunk?to?{namespace_str}")
                is_virtual = True
            else:
                # Check if we're marked override
                for child in node.get_children():
                    if child.kind == clang.cindex.CursorKind.CXX_OVERRIDE_ATTR:
                        add_internal(f"non-virtual?thunk?to?{namespace_str}")
                        is_virtual = True
                        break

            # If this is a virtual/override, then we iterate the virtual base
            # classes.. If we find find the provided method in those classes
            # we need to generate a a virtual?thunk?to? for the method/constructor/deconstructor.
            if is_virtual:
                def search_class_hierarchy_for_virtual_thunk(derived: clang.cindex.Cursor):
                    assert (derived.kind == clang.cindex.CursorKind.CLASS_DECL
                            or derived.kind == clang.cindex.CursorKind.STRUCT_DECL
                            or derived.kind == clang.cindex.CursorKind.CLASS_TEMPLATE
                            or derived.kind == clang.cindex.CursorKind.TYPEDEF_DECL)

                    # Find the base classes for the derived class
                    base_classes: list[clang.cindex.Cursor] = []
                    for child in derived.get_children():
                        if child.kind != clang.cindex.CursorKind.CXX_BASE_SPECIFIER:
                            continue

                        class_or_struct_node = clang.cindex.conf.lib.clang_getCursorDefinition(child)
                        if class_or_struct_node is None:
                            continue

                        base_classes.append(class_or_struct_node)
                        if not clang.cindex.conf.lib.clang_isVirtualBase(child):
                            continue

                        # Search the immediate base classes for the function name
                        for other_child in class_or_struct_node.get_children():
                            if (other_child.displayname == node.displayname):
                                add_internal(f"virtual?thunk?to?{namespace_str}")
                                return True

                    # Looks like it wasn't in any of our direct ancestors, so let's
                    # try their ancestors too
                    for base in base_classes:
                        if search_class_hierarchy_for_virtual_thunk(base):
                            return True

                    return False

                search_class_hierarchy_for_virtual_thunk(node.semantic_parent)
    elif node.location.file is not None:
        _logger.debug(f"NOT emitting node {node.spelling} in file {node.location.file.name}")


    # Traverse down the tree if we can
    is_file = node.kind == clang.cindex.CursorKind.TRANSLATION_UNIT
    is_containing_node = (node.kind == clang.cindex.CursorKind.CLASS_DECL
        or node.kind == clang.cindex.CursorKind.STRUCT_DECL
        or node.kind == clang.cindex.CursorKind.NAMESPACE)
    if is_file or is_containing_node:
        if clang.cindex.conf.lib.clang_Location_isInSystemHeader(node.location):
            _logger.debug(f"Node is in a system header={node.location.file.name}")
            return result

        for child in node.get_children():
            traverse_ast(child, filename, result)
    else:
        _logger.debug(f"Nothing to process for node={node.spelling} in file={node.location.file.name}")

    return result



def _parse_single_header(file_path: str, parse_args: list[str]) -> set[str]:
    """Parse a single header file and return the set of symbol strings it defines.

    Defined at module level (not as a closure) so it is picklable for use with
    concurrent.futures.ProcessPoolExecutor.  On Linux, ProcessPoolExecutor uses
    fork by default, so the clang.cindex.Config already set up in the parent
    process (SO path, library path) is inherited by each worker without any
    extra initialisation.
    """
    idx = clang.cindex.Index.create()
    parse_options = clang.cindex.TranslationUnit.PARSE_SKIP_FUNCTION_BODIES
    tu = idx.parse(file_path, args=parse_args, options=parse_options)
    result: set[str] = set()
    traverse_ast(tu.cursor, file_path, result)
    return result


def process_directory(directory: Path, search_dirs: list[str]) -> set[str]:
    files = list(directory.rglob('*.h'))

    args = ['-std=c++26', '-x', 'c++-header']
    for dir in search_dirs:
        args.append(f"-I{dir}")

    # Parse all header files in parallel.  Each worker creates its own
    # clang.cindex.Index so libclang state is never shared across processes.
    combined: set[str] = set()
    with concurrent.futures.ProcessPoolExecutor() as executor:
        futures = [
            executor.submit(_parse_single_header, f.as_posix(), args)
            for f in files
        ]
        for future in concurrent.futures.as_completed(futures):
            combined |= future.result()

    return combined


def read_symbols_from_file(f: argparse.FileType, library_name: str) -> list[Symbol]:
    """
    Returns a list of symbols for each stanza in the symbols.map file.
    The returned list is ordered by version.
    """
    # WARNING: This is a very naive way to parse these files
    library_name = library_name.upper() + "_"
    retval: list[Symbol] = []
    version_str = None

    for line in f.readlines():
        if line.startswith(library_name):
            # This denotes that a new version
            version_str = line[len(library_name):].split()[0]
        elif line.startswith("    "):
            line = line.strip()
            assert line.endswith(';')
            retval.append(Symbol(line, version_str, False))
        elif line.startswith("  "):
            line = line.strip()
            if line.startswith("global:") or line.startswith("local:") or line == 'extern "C++" {' or line.startswith("}"):
                continue

            # This is a c-symbol
            assert line.endswith(';')
            retval.append(Symbol(line, version_str, True))
    return retval


def get_added_symbols(previous_symbols: list[Symbol], new_symbols: Optional[set[str]], is_internal: bool) -> list[str]:
    if new_symbols is None:
        return []
    added_symbols: set[str] = new_symbols.copy()
    for symbol in previous_symbols:
        if symbol.is_internal() == is_internal and symbol.name in added_symbols:
            added_symbols.remove(symbol.name)
    added_symbols = list(added_symbols)
    added_symbols.sort()
    return added_symbols


def report_symbols_diff(previous_symbols: list[Symbol], new_symbols: Optional[set[str]], is_internal: bool) -> bool:
    """
    Prints the diff between the previous symbols and the new symbols.
    When new_symbols is None (no headers directory was provided), no diff is performed.
    Returns True if any symbols were added or deleted, False otherwise.
    """
    if new_symbols is None:
        print("")
        print("  \033[1mDiff skipped (no headers directory provided)\033[0m")
        return False

    added_symbols = get_added_symbols(previous_symbols, new_symbols, is_internal)

    if len(added_symbols) > 0:
        print("")
        print("  \033[1mNew Symbols 🟢🟢🟢\033[0m")
        for s in added_symbols:
            print(f"\033[92m    {s}\033[0m")

    # Deleted symbols — only consider wildcard C++ symbols (generated by this tool).
    # C symbols (is_c_symbol=True) and exact quoted C++ symbols (name starts with '"')
    # are manually maintained in the map and are not generated by the tool, so they
    # cannot be compared against new_symbols.
    deleted_symbols = set()
    for symbol in previous_symbols:
        if (is_internal == symbol.is_internal()
                and not symbol.is_c_symbol
                and not symbol.name.startswith('"')
                and symbol.name not in new_symbols):
            deleted_symbols.add(symbol.name)
    deleted_symbols = list(deleted_symbols)
    deleted_symbols.sort()

    if len(deleted_symbols) > 0:
        print("")
        print("  \033[1mDeleted Symbols 🔻🔻🔻\033[0m")
        for s in deleted_symbols:
            print(f"\033[91m    {s}\033[0m")

    symbols_changed = len(deleted_symbols) > 0 or len(added_symbols) > 0

    if not symbols_changed:
        print("")
        print("  \033[1mNo symbol changes! ❤️\033[0m")

    return symbols_changed


def main():
    parser = argparse.ArgumentParser(description="This tool parses the header files of a provided in the Mir project "
                                        "and creates a list of new internal and external symbols.\n"
                                        "By default, the script updates the corresponding symbols.map file automatically. "
                                        "To view this diff only, the user may provide the --diff option.\n"
                                        "Please install libclang-19-dev and python3-clang-19 before using this tool.\n"
                                        "You must also specify the 'MIR_SYMBOLS_MAP_GENERATOR_CLANG_SO_PATH' and 'MIR_SYMBOLS_MAP_GENERATOR_CLANG_LIBRARY_PATH'"
                                        " environment variables, for example:"
                                        "\n    export MIR_SYMBOLS_MAP_GENERATOR_CLANG_SO_PATH=/usr/lib/llvm-19/lib/libclang.so.1"
                                        "\n    export MIR_SYMBOLS_MAP_GENERATOR_CLANG_LIBRARY_PATH=/usr/lib/llvm-19/lib"
                                        , formatter_class=argparse.RawTextHelpFormatter)
    parser.add_argument('--library-name', type=str,
                    help='name of library',
                    dest="library_name",
                    required=True)
    parser.add_argument('--symbols-map-path', type=argparse.FileType('r+', encoding='latin-1'),
                    help='absolute path to a symbols map',
                    dest="symbols_map_path",
                    required=True)
    parser.add_argument('--external-headers-directory', type=Path,
                    help=f'absolute path to the directory containing the public-facing headers of this library',
                    dest="external_headers")
    parser.add_argument('--internal-headers-directory', type=Path,
                    help=f'absolute path to the directory containing private headers of this library',
                    dest="internal_headers")
    parser.add_argument('--version', type=str,
                    help='current version of the library',
                    required=True)
    parser.add_argument('--diff', action='store_true',
                    help='if true a diff should be output to the console')
    parser.add_argument('--include-dirs', type=str, help="colon separated list of directories to search for symbols",
                        required=True, dest='include_dirs')

    args = parser.parse_args()

    if args.diff:
        _logger.info("symbols_map_generator is running in 'diff' mode")
    else:
        _logger.info("symbols_map_generator is running in 'output symbols' mode")

    # Point libclang to a file on the system
    if 'MIR_SYMBOLS_MAP_GENERATOR_CLANG_SO_PATH' in os.environ:
        file_path = os.environ['MIR_SYMBOLS_MAP_GENERATOR_CLANG_SO_PATH']
        _logger.info(f"MIR_SYMBOLS_MAP_GENERATOR_CLANG_SO_PATH is {file_path}")
        clang.cindex.Config.set_library_file(file_path)
    else:
        clang_so_paths = [
            "/usr/lib/llvm-19/lib/libclang.so.1",
            "/usr/lib/x86_64-linux-gnu/libclang-19.so.1",
            "/usr/lib/aarch64-linux-gnu/libclang-19.so.1",
            "/usr/lib/libclang-19.so.1",
            "/usr/lib/llvm-18/lib/libclang-18.so.1",
            "/usr/lib/x86_64-linux-gnu/libclang-18.so.1",
            "/usr/lib/aarch64-linux-gnu/libclang-18.so.1",
            "/usr/lib/libclang-18.so.1",
        ]

        # Find libclang.so.1
        found_so = None
        for path in clang_so_paths:
            if os.path.isfile(path):
                found_so = path
                _logger.debug(f"Found libclang.so.1 at: {path}")
                break

        if found_so is not None:
            clang.cindex.Config.set_library_file(found_so)
            _logger.info(f"Using found libclang.so.1 at: {found_so}")
        else:
            _logger.error("Could not find libclang.so.1 in the environment. "
                          "You may want to set MIR_SYMBOLS_MAP_GENERATOR_CLANG_SO_PATH "
                          "if libclang.so.1 is manually installed in a non-standard directory.")
            exit(1)

    if 'MIR_SYMBOLS_MAP_GENERATOR_CLANG_LIBRARY_PATH' in os.environ:
        library_path = os.environ['MIR_SYMBOLS_MAP_GENERATOR_CLANG_LIBRARY_PATH']
        _logger.info(f"MIR_SYMBOLS_MAP_GENERATOR_CLANG_LIBRARY_PATH is {library_path}")
        clang.cindex.Config.set_library_path(library_path)
    else:
        clang_lib_dirs = [
            "/usr/lib/llvm-19/lib",
            "/usr/lib/llvm-18/lib",
            "/usr/lib/x86_64-linux-gnu",
            "/usr/lib/aarch64-linux-gnu",
            "/usr/lib",
        ]

        # Find library directory
        found_lib = None
        for lib_dir in clang_lib_dirs:
            if os.path.isdir(lib_dir):
                # Check if directory contains libclang files
                for file in os.listdir(lib_dir):
                    if file.startswith('libclang') and '.so' in file:
                        found_lib = lib_dir
                        _logger.debug(f"Found clang library directory at: {lib_dir}")
                        break
            if found_lib:
                break
        if found_lib is not None:
            clang.cindex.Config.set_library_path(found_lib)
            _logger.info(f"Using found clang library directory at: {found_lib}")
        else:
            _logger.error("Could not find clang library directory. "
                          "You may want to set MIR_SYMBOLS_MAP_GENERATOR_CLANG_LIBRARY_PATH "
                          "if clang is not installed in the standard directory.")
            exit(1)

    include_dirs = args.include_dirs.split(":")
    _logger.debug(f"Parsing with include directories: {include_dirs}")

    library = args.library_name
    version = args.version

    # Remove the patch version since we're not interested in it
    split_version = version.split('.')
    if len(split_version) == 3:
        version = f"{split_version[0]}.{split_version[1]}"

    _logger.info(f"Symbols map generation is beginning for library={library} with version={version}")

    # Create a set that includes all of the available symbols.
    # None means "no headers directory was provided" — diff checks are skipped for that type.
    external_symbols: Optional[set[str]] = None
    if args.external_headers:
        _logger.info(f"Processing external headers directory: {args.external_headers}")
        external_symbols = process_directory(
            args.external_headers,
            include_dirs
        )

    internal_symbols: Optional[set[str]] = None
    if args.internal_headers:
        _logger.info(f"Processing internal headers directory: {args.internal_headers}")
        internal_symbols = process_directory(
            args.internal_headers,
            include_dirs
        )

    previous_symbols = read_symbols_from_file(args.symbols_map_path, library)

    if args.diff:
        print("External Symbols Diff:")
        has_changed_external_symbols = report_symbols_diff(previous_symbols, external_symbols, False)

        print("")
        print("Internal Symbols Diff:")
        has_changed_internal_symbols = report_symbols_diff(previous_symbols, internal_symbols, True)
        print("")
        if has_changed_external_symbols or has_changed_internal_symbols:
            exit(1)
    else:
        _logger.info(f"Outputting the symbols file to: {args.symbols_map_path.name}")

        # We now have a list of new external and internal symbols. Our goal now is to add them to the correct stanzas
        new_external_symbols = get_added_symbols(previous_symbols, external_symbols, False)
        new_internal_symbols = get_added_symbols(previous_symbols, internal_symbols, True)

        next_major = get_major_version_from_str(version)
        next_version = f"{library.upper()}_{version}"
        next_internal_version = f"{library.upper()}_INTERNAL_{version}"
        data_to_output: OrderedDict[str, dict[str, list[str]]] = OrderedDict()

        # Remake the stanzas for the previous symbols
        for symbol in previous_symbols:
            major = get_major_version_from_str(get_version_from_library_version_str(symbol.version))

            # If we are going up by a major version, then we should add
            # all existing symbols to the new stanza
            if major == next_major:
                symbol_version = f"{library.upper()}_{symbol.version}"
            else:
                symbol_version = f"{library.upper()}_{version}"

            if not symbol_version in data_to_output:
                data_to_output[symbol_version] = {
                    "c": [],
                    "c++": []
                }
            if symbol.is_c_symbol:
                bisect.insort(data_to_output[symbol_version]["c"], symbol.name)
            else:
                bisect.insort(data_to_output[symbol_version]["c++"], symbol.name)

        # Add the external symbols
        for symbol in new_external_symbols:
            if not next_version in data_to_output:
                data_to_output[next_version] = {
                    "c": [],
                    "c++": []
                }
            bisect.insort(data_to_output[next_version]["c++"], symbol)

        # Add the internal symbols
        for symbol in new_internal_symbols:
            if not next_internal_version in data_to_output:
                data_to_output[next_internal_version] = {
                    "c": [],
                    "c++": []
                }
            bisect.insort(data_to_output[next_internal_version]["c++"], symbol)

        # Finally, output them to the file
        f = args.symbols_map_path
        f.seek(0)
        prev_internal_version_str = None
        prev_external_version_str = None
        for i, (version, symbols_dict) in enumerate(data_to_output.items()):
            # Only the first stanza should contain the local symbols
            if i == 0:
                closing_line = "local: *;\n"
            else:
                closing_line = ""

            # Add the correct previous version. This will depend on
            is_internal = "_INTERNAL_" in version
            if is_internal:
                if prev_internal_version_str is not None:
                    closing_line += "} " + prev_internal_version_str + ";"
                else:
                    closing_line += "};"
                prev_internal_version_str = version
            else:
                if prev_external_version_str is not None:
                    closing_line += "} " + prev_external_version_str + ";"
                else:
                    closing_line += "};"
                prev_external_version_str = version

            if not symbols_dict["c"]:
                c_symbols_str = ""
            else:
                c_symbols_str = "\n  " + "\n  ".join(symbols_dict["c"]) + "\n"
            cpp_symbols_str = "\n    ".join(symbols_dict["c++"])
            output_str = f'''{version} {"{"}
global:{c_symbols_str}
  extern "C++" {"{"}
    {cpp_symbols_str}
  {'};'}
{closing_line}

'''

            f.write(output_str)

        f.truncate()

if __name__ == "__main__":
    try:
        main()
    except Exception as e:
        _logger.exception(LIBRARY_NOT_FOUND_ERROR_MSG)
        sys.exit(1)
