# Copyright (c) Mengning Software. 2023. All rights reserved.
#
# Super IDE licensed under GNU Affero General Public License v3 (AGPL-3.0) .
# You can use this software according to the terms and conditions of the AGPL-3.0.
# You may obtain a copy of AGPL-3.0 at:
#
#    https://www.gnu.org/licenses/agpl-3.0.txt
#
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR
# PURPOSE.
# See the AGPL-3.0 for more details.

import os
import shutil
import time
from pathlib import Path

import semantic_version
from ajsonrpc.core import JSONRPC20DispatchException

from superide import app, exception, fs, __configfile__
from superide.home.rpc.handlers.app import AppRPC
from superide.home.rpc.handlers.base import BaseRPCHandler
from superide.home.rpc.handlers.piocore import PIOCoreRPC
from superide.package.manager.platform import PlatformPackageManager
from superide.platform.factory import PlatformFactory
from superide.project.config import ProjectConfig
from superide.project.exception import ProjectError
from superide.project.helpers import get_project_dir, is_platformio_project
from superide.project.integration.generator import ProjectGenerator
from superide.project.options import get_config_options_schema


class ProjectRPC(BaseRPCHandler):
    @staticmethod
    def config_call(init_kwargs, method, *args):
        assert isinstance(init_kwargs, dict)
        assert "path" in init_kwargs
        if os.path.isdir(init_kwargs["path"]):
            project_dir = init_kwargs["path"]
            init_kwargs["path"] = os.path.join(init_kwargs["path"], __configfile__)
        elif os.path.isfile(init_kwargs["path"]):
            project_dir = get_project_dir()
        else:
            project_dir = os.path.dirname(init_kwargs["path"])
        with fs.cd(project_dir):
            return getattr(ProjectConfig(**init_kwargs), method)(*args)

    @staticmethod
    def config_load(path):
        return ProjectConfig(
            path, parse_extra=False, expand_interpolations=False
        ).as_tuple()

    @staticmethod
    def config_dump(path, data):
        config = ProjectConfig(path, parse_extra=False, expand_interpolations=False)
        config.update(data, clear=True)
        return config.save()

    @staticmethod
    def config_update_description(path, text):
        config = ProjectConfig(path, parse_extra=False, expand_interpolations=False)
        if not config.has_section("superide"):
            config.add_section("superide")
        if text:
            config.set("superide", "description", text)
        else:
            if config.has_option("superide", "description"):
                config.remove_option("superide", "description")
            if not config.options("superide"):
                config.remove_section("superide")
        return config.save()

    @staticmethod
    def get_config_schema():
        return get_config_options_schema()

    @staticmethod
    def get_projects():
        def _get_project_data():
            data = {"boards": [], "envLibdepsDirs": [], "libExtraDirs": []}
            config = ProjectConfig()
            data["envs"] = config.envs()
            data["description"] = config.get("superide", "description")
            data["libExtraDirs"].extend(config.get("superide", "lib_extra_dirs", []))

            libdeps_dir = config.get("superide", "libdeps_dir")
            for section in config.sections():
                if not section.startswith("env:"):
                    continue
                data["envLibdepsDirs"].append(os.path.join(libdeps_dir, section[4:]))
                if config.has_option(section, "board"):
                    data["boards"].append(config.get(section, "board"))
                data["libExtraDirs"].extend(config.get(section, "lib_extra_dirs", []))

            # skip non existing folders and resolve full path
            for key in ("envLibdepsDirs", "libExtraDirs"):
                data[key] = [
                    fs.expanduser(d) if d.startswith("~") else os.path.abspath(d)
                    for d in data[key]
                    if os.path.isdir(d)
                ]

            return data

        def _path_to_name(path):
            return (os.path.sep).join(path.split(os.path.sep)[-2:])

        result = []
        pm = PlatformPackageManager()
        for project_dir in AppRPC.load_state()["storage"]["recentProjects"]:
            if not os.path.isdir(project_dir):
                continue
            data = {}
            boards = []
            try:
                with fs.cd(project_dir):
                    data = _get_project_data()
            except ProjectError:
                continue

            for board_id in data.get("boards", []):
                name = board_id
                try:
                    name = pm.board_config(board_id)["name"]
                except exception.PlatformioException:
                    pass
                boards.append({"id": board_id, "name": name})

            result.append(
                {
                    "path": project_dir,
                    "name": _path_to_name(project_dir),
                    "modified": int(os.path.getmtime(project_dir)),
                    "boards": boards,
                    "description": data.get("description"),
                    "envs": data.get("envs", []),
                    "envLibStorages": [
                        {"name": os.path.basename(d), "path": d}
                        for d in data.get("envLibdepsDirs", [])
                    ],
                    "extraLibStorages": [
                        {"name": _path_to_name(d), "path": d}
                        for d in data.get("libExtraDirs", [])
                    ],
                }
            )
        return result

    @staticmethod
    def get_project_examples():
        result = []
        pm = PlatformPackageManager()
        for pkg in pm.get_installed():
            examples_dir = os.path.join(pkg.path, "examples")
            if not os.path.isdir(examples_dir):
                continue
            items = []
            for project_dir, _, __ in os.walk(examples_dir):
                project_description = None
                try:
                    config = ProjectConfig(os.path.join(project_dir, __configfile__))
                    config.validate(silent=True)
                    project_description = config.get("superide", "description")
                except ProjectError:
                    continue

                path_tokens = project_dir.split(os.path.sep)
                items.append(
                    {
                        "name": "/".join(
                            path_tokens[path_tokens.index("examples") + 1 :]
                        ),
                        "path": project_dir,
                        "description": project_description,
                    }
                )
            manifest = pm.load_manifest(pkg)
            result.append(
                {
                    "platform": {
                        "title": manifest["title"],
                        "version": manifest["version"],
                    },
                    "items": sorted(items, key=lambda item: item["name"]),
                }
            )
        return sorted(result, key=lambda data: data["platform"]["title"])

    async def init(self, board, framework, project_dir):
        assert project_dir
        if not os.path.isdir(project_dir):
            os.makedirs(project_dir)
        args = ["init", "--board", board, "--sample-code"]
        if framework:
            args.extend(["--project-option", "framework = %s" % framework])
        ide = app.get_session_var("caller_id")
        if ide in ProjectGenerator.get_supported_ides():
            args.extend(["--ide", ide])
        await PIOCoreRPC.call(
            args, options={"cwd": project_dir, "force_subprocess": True}
        )
        return project_dir

    @staticmethod
    async def import_arduino(board, use_arduino_libs, arduino_project_dir):
        board = str(board)
        # don't import PIO Project
        if is_platformio_project(arduino_project_dir):
            return arduino_project_dir

        is_arduino_project = any(
            os.path.isfile(
                os.path.join(
                    arduino_project_dir,
                    "%s.%s" % (os.path.basename(arduino_project_dir), ext),
                )
            )
            for ext in ("ino", "pde")
        )
        if not is_arduino_project:
            raise JSONRPC20DispatchException(
                code=4000, message="Not an Arduino project: %s" % arduino_project_dir
            )

        state = AppRPC.load_state()
        project_dir = os.path.join(
            state["storage"]["projectsDir"], time.strftime("%y%m%d-%H%M%S-") + board
        )
        if not os.path.isdir(project_dir):
            os.makedirs(project_dir)
        args = ["init", "--board", board]
        args.extend(["--project-option", "framework = arduino"])
        if use_arduino_libs:
            args.extend(
                ["--project-option", "lib_extra_dirs = ~/Documents/Arduino/libraries"]
            )
        ide = app.get_session_var("caller_id")
        if ide in ProjectGenerator.get_supported_ides():
            args.extend(["--ide", ide])
        await PIOCoreRPC.call(
            args, options={"cwd": project_dir, "force_subprocess": True}
        )
        with fs.cd(project_dir):
            config = ProjectConfig()
            src_dir = config.get("superide", "src_dir")
            if os.path.isdir(src_dir):
                fs.rmtree(src_dir)
            shutil.copytree(arduino_project_dir, src_dir, symlinks=True)
        return project_dir

    @staticmethod
    async def import_pio(project_dir):
        if not project_dir or not is_platformio_project(project_dir):
            raise JSONRPC20DispatchException(
                code=4001, message="Not an superide project: %s" % project_dir
            )
        new_project_dir = os.path.join(
            AppRPC.load_state()["storage"]["projectsDir"],
            time.strftime("%y%m%d-%H%M%S-") + os.path.basename(project_dir),
        )
        shutil.copytree(project_dir, new_project_dir, symlinks=True)

        args = ["init"]
        ide = app.get_session_var("caller_id")
        if ide in ProjectGenerator.get_supported_ides():
            args.extend(["--ide", ide])
        await PIOCoreRPC.call(
            args, options={"cwd": new_project_dir, "force_subprocess": True}
        )
        return new_project_dir

    async def init_v2(self, configuration, options=None):
        project_dir = os.path.join(configuration["location"], configuration["name"])
        if not os.path.isdir(project_dir):
            os.makedirs(project_dir)

        envclone = os.environ.copy()
        envclone["PLATFORMIO_FORCE_ANSI"] = "true"
        options = options or {}
        options["spawn"] = {"env": envclone, "cwd": project_dir}

        args = ["project", "init"]
        ide = app.get_session_var("caller_id")
        if ide in ProjectGenerator.get_supported_ides():
            args.extend(["--ide", ide])

        if configuration.get("example"):
            await self.factory.notify_clients(
                method=options.get("stdoutNotificationMethod"),
                params=["Copying example files...\n"],
                actor="frontend",
            )
            await self._pre_init_example(configuration, project_dir)
        else:
            args.extend(self._pre_init_empty(configuration))

        return await self.factory.manager.dispatcher["core.exec"](args, options=options)

    @staticmethod
    def _pre_init_empty(configuration):
        project_options = []
        platform = configuration["platform"]
        board_id = configuration.get("board", {}).get("id")
        env_name = board_id or platform["name"]
        if configuration.get("description"):
            project_options.append(("description", configuration.get("description")))
        try:
            v = semantic_version.Version(platform.get("version"))
            assert not v.prerelease
            project_options.append(
                ("platform", "{name} @ ^{version}".format(**platform))
            )
        except (AssertionError, ValueError):
            project_options.append(
                ("platform", "{name} @ {version}".format(**platform))
            )
        if board_id:
            project_options.append(("board", board_id))
        if configuration.get("framework"):
            project_options.append(("framework", configuration["framework"]["name"]))

        args = ["-e", env_name, "--sample-code"]
        for name, value in project_options:
            args.extend(["-O", f"{name}={value}"])
        return args

    async def _pre_init_example(self, configuration, project_dir):
        for item in configuration["example"]["files"]:
            p = Path(project_dir).joinpath(item["path"])
            if not p.parent.is_dir():
                p.parent.mkdir(parents=True)
            p.write_text(
                await self.factory.manager.dispatcher["os.request_content"](
                    item["url"]
                ),
                encoding="utf-8",
            )
        return []

    @staticmethod
    def configuration(project_dir, env):
        assert is_platformio_project(project_dir)
        with fs.cd(project_dir):
            config = ProjectConfig(os.path.join(project_dir, __configfile__))
            platform = PlatformFactory.from_env(env, autoinstall=True)
            platform_pkg = PlatformPackageManager().get_package(platform.get_dir())
            board_id = config.get(f"env:{env}", "board", None)

            # frameworks
            frameworks = []
            for name in config.get(f"env:{env}", "framework", []):
                if name not in platform.frameworks:
                    continue
                f_pkg_name = platform.frameworks[name].get("package")
                if not f_pkg_name:
                    continue
                f_pkg = platform.get_package(f_pkg_name)
                if not f_pkg:
                    continue
                f_manifest = platform.pm.load_manifest(f_pkg)
                frameworks.append(
                    dict(
                        name=name,
                        title=f_manifest.get("title"),
                        version=str(f_pkg.metadata.version),
                    )
                )

            return dict(
                platform=dict(
                    ownername=platform_pkg.metadata.spec.owner
                    if platform_pkg.metadata.spec
                    else None,
                    name=platform.name,
                    title=platform.title,
                    version=str(platform_pkg.metadata.version),
                ),
                board=platform.board_config(board_id).get_brief_data()
                if board_id
                else None,
                frameworks=frameworks or None,
            )
