#
# Copyright (C) 2023-2024 Siemens AG
#
# SPDX-License-Identifier: GPL-2.0-only
#
"""Devtool ide-sdk IDE plugin for VSCode and VSCodium"""

import json
import logging
import os
import shutil
from devtool.ide_plugins import BuildTool, IdeBase, GdbCrossConfig, get_devtool_deploy_opts

logger = logging.getLogger('devtool')


class GdbCrossConfigVSCode(GdbCrossConfig):
    def __init__(self, image_recipe, modified_recipe, binary):
        super().__init__(image_recipe, modified_recipe, binary, False)

    def initialize(self):
        self._gen_gdbserver_start_script()


class IdeVSCode(IdeBase):
    """Manage IDE configurations for VSCode

    Modified recipe mode:
    - cmake: use the cmake-preset generated by devtool ide-sdk
    - meson: meson is called via a wrapper script generated by devtool ide-sdk

    Shared sysroot mode:
    In shared sysroot mode, the cross tool-chain is exported to the user's global configuration.
    A workspace cannot be created because there is no recipe that defines how a workspace could
    be set up.
    - cmake: adds a cmake-kit to .local/share/CMakeTools/cmake-tools-kits.json
             The cmake-kit uses the environment script and the tool-chain file
             generated by meta-ide-support.
    - meson: Meson needs manual workspace configuration.
    """

    @classmethod
    def ide_plugin_priority(cls):
        """If --ide is not passed this is the default plugin"""
        if shutil.which('code'):
            return 100
        return 0

    def setup_shared_sysroots(self, shared_env):
        """Expose the toolchain of the shared sysroots SDK"""
        datadir = shared_env.ide_support.datadir
        deploy_dir_image = shared_env.ide_support.deploy_dir_image
        real_multimach_target_sys = shared_env.ide_support.real_multimach_target_sys
        standalone_sysroot_native = shared_env.build_sysroots.standalone_sysroot_native
        vscode_ws_path = os.path.join(
            os.environ['HOME'], '.local', 'share', 'CMakeTools')
        cmake_kits_path = os.path.join(vscode_ws_path, 'cmake-tools-kits.json')
        oecmake_generator = "Ninja"
        env_script = os.path.join(
            deploy_dir_image, 'environment-setup-' + real_multimach_target_sys)

        if not os.path.isdir(vscode_ws_path):
            os.makedirs(vscode_ws_path)
        cmake_kits_old = []
        if os.path.exists(cmake_kits_path):
            with open(cmake_kits_path, 'r', encoding='utf-8') as cmake_kits_file:
                cmake_kits_old = json.load(cmake_kits_file)
        cmake_kits = cmake_kits_old.copy()

        cmake_kit_new = {
            "name": "OE " + real_multimach_target_sys,
            "environmentSetupScript": env_script,
            "toolchainFile": standalone_sysroot_native + datadir + "/cmake/OEToolchainConfig.cmake",
            "preferredGenerator": {
                "name": oecmake_generator
            }
        }

        def merge_kit(cmake_kits, cmake_kit_new):
            i = 0
            while i < len(cmake_kits):
                if 'environmentSetupScript' in cmake_kits[i] and \
                        cmake_kits[i]['environmentSetupScript'] == cmake_kit_new['environmentSetupScript']:
                    cmake_kits[i] = cmake_kit_new
                    return
                i += 1
            cmake_kits.append(cmake_kit_new)
        merge_kit(cmake_kits, cmake_kit_new)

        if cmake_kits != cmake_kits_old:
            logger.info("Updating: %s" % cmake_kits_path)
            with open(cmake_kits_path, 'w', encoding='utf-8') as cmake_kits_file:
                json.dump(cmake_kits, cmake_kits_file, indent=4)
        else:
            logger.info("Already up to date: %s" % cmake_kits_path)

        cmake_native = os.path.join(
            shared_env.build_sysroots.standalone_sysroot_native, 'usr', 'bin', 'cmake')
        if os.path.isfile(cmake_native):
            logger.info('cmake-kits call cmake by default. If the cmake provided by this SDK should be used, please add the following line to ".vscode/settings.json" file: "cmake.cmakePath": "%s"' % cmake_native)
        else:
            logger.error("Cannot find cmake native at: %s" % cmake_native)

    def dot_code_dir(self, modified_recipe):
        return os.path.join(modified_recipe.srctree, '.vscode')

    def __vscode_settings_meson(self, settings_dict, modified_recipe):
        if modified_recipe.build_tool is not BuildTool.MESON:
            return
        settings_dict["mesonbuild.mesonPath"] = modified_recipe.meson_wrapper

        confopts = modified_recipe.mesonopts.split()
        confopts += modified_recipe.meson_cross_file.split()
        confopts += modified_recipe.extra_oemeson.split()
        settings_dict["mesonbuild.configureOptions"] = confopts
        settings_dict["mesonbuild.buildFolder"] = modified_recipe.b

    def __vscode_settings_cmake(self, settings_dict, modified_recipe):
        """Add cmake specific settings to settings.json.

        Note: most settings are passed to the cmake preset.
        """
        if modified_recipe.build_tool is not BuildTool.CMAKE:
            return
        settings_dict["cmake.configureOnOpen"] = True
        settings_dict["cmake.sourceDirectory"] = modified_recipe.real_srctree

    def vscode_settings(self, modified_recipe, image_recipe):
        files_excludes = {
            "**/.git/**": True,
            "**/oe-logs/**": True,
            "**/oe-workdir/**": True,
            "**/source-date-epoch/**": True
        }
        python_exclude = [
            "**/.git/**",
            "**/oe-logs/**",
            "**/oe-workdir/**",
            "**/source-date-epoch/**"
        ]
        files_readonly = {
            modified_recipe.recipe_sysroot + '/**': True,
            modified_recipe.recipe_sysroot_native + '/**': True,
        }
        if image_recipe.rootfs_dbg is not None:
            files_readonly[image_recipe.rootfs_dbg + '/**'] = True
        settings_dict = {
            "files.watcherExclude": files_excludes,
            "files.exclude": files_excludes,
            "files.readonlyInclude": files_readonly,
            "python.analysis.exclude": python_exclude
        }
        self.__vscode_settings_cmake(settings_dict, modified_recipe)
        self.__vscode_settings_meson(settings_dict, modified_recipe)

        settings_file = 'settings.json'
        IdeBase.update_json_file(
            self.dot_code_dir(modified_recipe), settings_file, settings_dict)

    def __vscode_extensions_cmake(self, modified_recipe, recommendations):
        if modified_recipe.build_tool is not BuildTool.CMAKE:
            return
        recommendations += [
            "twxs.cmake",
            "ms-vscode.cmake-tools",
            "ms-vscode.cpptools",
            "ms-vscode.cpptools-extension-pack",
            "ms-vscode.cpptools-themes"
        ]

    def __vscode_extensions_meson(self, modified_recipe, recommendations):
        if modified_recipe.build_tool is not BuildTool.MESON:
            return
        recommendations += [
            'mesonbuild.mesonbuild',
            "ms-vscode.cpptools",
            "ms-vscode.cpptools-extension-pack",
            "ms-vscode.cpptools-themes"
        ]

    def vscode_extensions(self, modified_recipe):
        recommendations = []
        self.__vscode_extensions_cmake(modified_recipe, recommendations)
        self.__vscode_extensions_meson(modified_recipe, recommendations)
        extensions_file = 'extensions.json'
        IdeBase.update_json_file(
            self.dot_code_dir(modified_recipe), extensions_file, {"recommendations": recommendations})

    def vscode_c_cpp_properties(self, modified_recipe):
        properties_dict = {
            "name": modified_recipe.recipe_id_pretty,
        }
        if modified_recipe.build_tool is BuildTool.CMAKE:
            properties_dict["configurationProvider"] = "ms-vscode.cmake-tools"
        elif modified_recipe.build_tool is BuildTool.MESON:
            properties_dict["configurationProvider"] = "mesonbuild.mesonbuild"
            properties_dict["compilerPath"] = os.path.join(modified_recipe.staging_bindir_toolchain, modified_recipe.cxx.split()[0])
        else:  # no C/C++ build
            return

        properties_dicts = {
            "configurations": [
                properties_dict
            ],
            "version": 4
        }
        prop_file = 'c_cpp_properties.json'
        IdeBase.update_json_file(
            self.dot_code_dir(modified_recipe), prop_file, properties_dicts)

    def vscode_launch_bin_dbg(self, gdb_cross_config):
        modified_recipe = gdb_cross_config.modified_recipe

        launch_config = {
            "name": gdb_cross_config.id_pretty,
            "type": "cppdbg",
            "request": "launch",
            "program": os.path.join(modified_recipe.d, gdb_cross_config.binary.lstrip('/')),
            "stopAtEntry": True,
            "cwd": "${workspaceFolder}",
            "environment": [],
            "externalConsole": False,
            "MIMode": "gdb",
            "preLaunchTask": gdb_cross_config.id_pretty,
            "miDebuggerPath": modified_recipe.gdb_cross.gdb,
            "miDebuggerServerAddress": "%s:%d" % (modified_recipe.gdb_cross.host, gdb_cross_config.gdbserver_port)
        }

        # Search for header files in recipe-sysroot.
        src_file_map = {
            "/usr/include": os.path.join(modified_recipe.recipe_sysroot, "usr", "include")
        }
        # First of all search for not stripped binaries in the image folder.
        # These binaries are copied (and optionally stripped) by deploy-target
        setup_commands = [
            {
                "description": "sysroot",
                "text": "set sysroot " + modified_recipe.d
            }
        ]

        if gdb_cross_config.image_recipe.rootfs_dbg:
            launch_config['additionalSOLibSearchPath'] = modified_recipe.solib_search_path_str(
                gdb_cross_config.image_recipe)
            # First: Search for sources of this recipe in the workspace folder
            if modified_recipe.pn in modified_recipe.target_dbgsrc_dir:
                src_file_map[modified_recipe.target_dbgsrc_dir] = "${workspaceFolder}"
            else:
                logger.error(
                    "TARGET_DBGSRC_DIR must contain the recipe name PN.")
            # Second: Search for sources of other recipes in the rootfs-dbg
            if modified_recipe.target_dbgsrc_dir.startswith("/usr/src/debug"):
                src_file_map["/usr/src/debug"] = os.path.join(
                    gdb_cross_config.image_recipe.rootfs_dbg, "usr", "src", "debug")
            else:
                logger.error(
                    "TARGET_DBGSRC_DIR must start with /usr/src/debug.")
        else:
            logger.warning(
                "Cannot setup debug symbols configuration for GDB. IMAGE_GEN_DEBUGFS is not enabled.")

        launch_config['sourceFileMap'] = src_file_map
        launch_config['setupCommands'] = setup_commands
        return launch_config

    def vscode_launch(self, modified_recipe):
        """GDB Launch configuration for binaries (elf files)"""

        configurations = []
        for gdb_cross_config in self.gdb_cross_configs:
            if gdb_cross_config.modified_recipe is modified_recipe:
                configurations.append(self.vscode_launch_bin_dbg(gdb_cross_config))
        launch_dict = {
            "version": "0.2.0",
            "configurations": configurations
        }
        launch_file = 'launch.json'
        IdeBase.update_json_file(
            self.dot_code_dir(modified_recipe), launch_file, launch_dict)

    def vscode_tasks_cpp(self, args, modified_recipe):
        run_install_deploy = modified_recipe.gen_install_deploy_script(args)
        install_task_name = "install && deploy-target %s" % modified_recipe.recipe_id_pretty
        tasks_dict = {
            "version": "2.0.0",
            "tasks": [
                {
                    "label": install_task_name,
                    "type": "shell",
                    "command": run_install_deploy,
                    "problemMatcher": []
                }
            ]
        }
        for gdb_cross_config in self.gdb_cross_configs:
            if gdb_cross_config.modified_recipe is not modified_recipe:
                continue
            tasks_dict['tasks'].append(
                {
                    "label": gdb_cross_config.id_pretty,
                    "type": "shell",
                    "isBackground": True,
                    "dependsOn": [
                        install_task_name
                    ],
                    "command": gdb_cross_config.gdbserver_script,
                    "problemMatcher": [
                        {
                            "pattern": [
                                {
                                    "regexp": ".",
                                    "file": 1,
                                    "location": 2,
                                    "message": 3
                                }
                            ],
                            "background": {
                                "activeOnStart": True,
                                "beginsPattern": ".",
                                "endsPattern": ".",
                            }
                        }
                    ]
                })
        tasks_file = 'tasks.json'
        IdeBase.update_json_file(
            self.dot_code_dir(modified_recipe), tasks_file, tasks_dict)

    def vscode_tasks_fallback(self, args, modified_recipe):
        oe_init_dir = modified_recipe.oe_init_dir
        oe_init = ". %s %s > /dev/null && " % (modified_recipe.oe_init_build_env, modified_recipe.topdir)
        dt_build = "devtool build "
        dt_build_label = dt_build + modified_recipe.recipe_id_pretty
        dt_build_cmd = dt_build + modified_recipe.bpn
        clean_opt = " --clean"
        dt_build_clean_label = dt_build + modified_recipe.recipe_id_pretty + clean_opt
        dt_build_clean_cmd = dt_build + modified_recipe.bpn + clean_opt
        dt_deploy = "devtool deploy-target "
        dt_deploy_label = dt_deploy + modified_recipe.recipe_id_pretty
        dt_deploy_cmd = dt_deploy + modified_recipe.bpn
        dt_build_deploy_label = "devtool build & deploy-target %s" % modified_recipe.recipe_id_pretty
        deploy_opts = ' '.join(get_devtool_deploy_opts(args))
        tasks_dict = {
            "version": "2.0.0",
            "tasks": [
                {
                    "label": dt_build_label,
                    "type": "shell",
                    "command": "bash",
                    "linux": {
                        "options": {
                            "cwd": oe_init_dir
                        }
                    },
                    "args": [
                        "--login",
                        "-c",
                        "%s%s" % (oe_init, dt_build_cmd)
                    ],
                    "problemMatcher": []
                },
                {
                    "label": dt_deploy_label,
                    "type": "shell",
                    "command": "bash",
                    "linux": {
                        "options": {
                            "cwd": oe_init_dir
                        }
                    },
                    "args": [
                        "--login",
                        "-c",
                        "%s%s %s" % (
                            oe_init, dt_deploy_cmd, deploy_opts)
                    ],
                    "problemMatcher": []
                },
                {
                    "label": dt_build_deploy_label,
                    "dependsOrder": "sequence",
                    "dependsOn": [
                        dt_build_label,
                        dt_deploy_label
                    ],
                    "problemMatcher": [],
                    "group": {
                        "kind": "build",
                        "isDefault": True
                    }
                },
                {
                    "label": dt_build_clean_label,
                    "type": "shell",
                    "command": "bash",
                    "linux": {
                        "options": {
                            "cwd": oe_init_dir
                        }
                    },
                    "args": [
                        "--login",
                        "-c",
                        "%s%s" % (oe_init, dt_build_clean_cmd)
                    ],
                    "problemMatcher": []
                }
            ]
        }
        if modified_recipe.gdb_cross:
            for gdb_cross_config in self.gdb_cross_configs:
                if gdb_cross_config.modified_recipe is not modified_recipe:
                    continue
                tasks_dict['tasks'].append(
                    {
                        "label": gdb_cross_config.id_pretty,
                        "type": "shell",
                        "isBackground": True,
                        "dependsOn": [
                            dt_build_deploy_label
                        ],
                        "command": gdb_cross_config.gdbserver_script,
                        "problemMatcher": [
                            {
                                "pattern": [
                                    {
                                        "regexp": ".",
                                        "file": 1,
                                        "location": 2,
                                        "message": 3
                                    }
                                ],
                                "background": {
                                    "activeOnStart": True,
                                    "beginsPattern": ".",
                                    "endsPattern": ".",
                                }
                            }
                        ]
                    })
        tasks_file = 'tasks.json'
        IdeBase.update_json_file(
            self.dot_code_dir(modified_recipe), tasks_file, tasks_dict)

    def vscode_tasks(self, args, modified_recipe):
        if modified_recipe.build_tool.is_c_ccp:
            self.vscode_tasks_cpp(args, modified_recipe)
        else:
            self.vscode_tasks_fallback(args, modified_recipe)

    def setup_modified_recipe(self, args, image_recipe, modified_recipe):
        self.vscode_settings(modified_recipe, image_recipe)
        self.vscode_extensions(modified_recipe)
        self.vscode_c_cpp_properties(modified_recipe)
        if args.target:
            self.initialize_gdb_cross_configs(
                image_recipe, modified_recipe, gdb_cross_config_class=GdbCrossConfigVSCode)
            self.vscode_launch(modified_recipe)
            self.vscode_tasks(args, modified_recipe)


def register_ide_plugin(ide_plugins):
    ide_plugins['code'] = IdeVSCode