import abc
import codecs
import json
import os
from typing import TYPE_CHECKING, Optional, Sequence

if TYPE_CHECKING:
    import frida_tools.repl


class Magic(abc.ABC):
    @property
    def description(self) -> str:
        return "no description"

    @abc.abstractproperty
    def required_args_count(self) -> int:
        pass

    def execute(self, repl: "frida_tools.repl.REPLApplication", args: Sequence[str]) -> Optional[bool]:
        pass


class Resume(Magic):
    @property
    def description(self) -> str:
        return "resume execution of the spawned process"

    @property
    def required_args_count(self) -> int:
        return 0

    def execute(self, repl: "frida_tools.repl.REPLApplication", args: Sequence[str]) -> None:
        repl._reactor.schedule(lambda: repl._resume())


class Load(Magic):
    @property
    def description(self) -> str:
        return "Load an additional script and reload the current REPL state"

    @property
    def required_args_count(self) -> int:
        return 1

    def execute(self, repl: "frida_tools.repl.REPLApplication", args: Sequence[str]) -> None:
        try:
            proceed = repl._get_confirmation(
                "Are you sure you want to load a new script and discard all current state?"
            )
            if not proceed:
                repl._print("Discarding load command")
                return

            repl._user_scripts.append(args[0])
            repl._perform_on_reactor_thread(lambda: repl._load_script())
        except Exception as e:
            repl._print(f"Failed to load script: {e}")


class Reload(Magic):
    @property
    def description(self) -> str:
        return "reload (i.e. rerun) the script that was given as an argument to the REPL"

    @property
    def required_args_count(self) -> int:
        return 0

    def execute(self, repl: "frida_tools.repl.REPLApplication", args: Sequence[str]) -> bool:
        try:
            repl._perform_on_reactor_thread(lambda: repl._load_script())
            return True
        except Exception as e:
            repl._print(f"Failed to load script: {e}")
            return False


class Unload(Magic):
    @property
    def required_args_count(self) -> int:
        return 0

    def execute(self, repl: "frida_tools.repl.REPLApplication", args: Sequence[str]) -> None:
        repl._unload_script()


class Autoperform(Magic):
    @property
    def description(self) -> str:
        return (
            "receive on/off as first and only argument, when switched on will wrap any REPL code with Java.performNow()"
        )

    @property
    def required_args_count(self) -> int:
        return 1

    def execute(self, repl: "frida_tools.repl.REPLApplication", args: Sequence[str]) -> None:
        repl._autoperform_command(args[0])


class Autoreload(Magic):
    _VALID_ARGUMENTS = ("on", "off")

    @property
    def description(self) -> str:
        return "disable or enable auto reloading of script files"

    @property
    def required_args_count(self) -> int:
        return 1

    def execute(self, repl: "frida_tools.repl.REPLApplication", args: Sequence[str]) -> None:
        if args[0] not in self._VALID_ARGUMENTS:
            raise ValueError("Autoreload command only receive on or off as an argument")

        required_state = args[0] == "on"
        if required_state == repl._autoreload:
            repl._print("Autoreloading is already in the desired state")
            return

        if required_state:
            repl._monitor_all()
        else:
            repl._demonitor_all()
        repl._autoreload = required_state


class Exec(Magic):
    @property
    def description(self) -> str:
        return "execute the given file path in the context of the currently loaded scripts"

    @property
    def required_args_count(self) -> int:
        return 1

    def execute(self, repl: "frida_tools.repl.REPLApplication", args: Sequence[str]) -> None:
        if not os.path.exists(args[0]):
            repl._print("Can't read the given file because it does not exist")
            return

        try:
            with codecs.open(args[0], "rb", "utf-8") as f:
                if not repl._exec_and_print(repl._evaluate_expression, f.read()):
                    repl._errors += 1
        except PermissionError:
            repl._print("Can't read the given file because of a permission error")


class Time(Magic):
    @property
    def description(self) -> str:
        return "measure the execution time of the given expression and print it to the screen"

    @property
    def required_args_count(self) -> int:
        return -2

    def execute(self, repl: "frida_tools.repl.REPLApplication", args: Sequence[str]) -> None:
        repl._exec_and_print(
            repl._evaluate_expression,
            """
            (() => {{
                const _startTime = Date.now();
                const _result = eval({expression});
                const _endTime = Date.now();
                console.log('Time: ' + (_endTime - _startTime) + ' ms.');
                return _result;
            }})();""".format(
                expression=json.dumps(" ".join(args))
            ),
        )


class Help(Magic):
    @property
    def description(self) -> str:
        return "print a list of available REPL commands"

    @property
    def required_args_count(self) -> int:
        return 0

    def execute(self, repl: "frida_tools.repl.REPLApplication", args: Sequence[str]) -> None:
        repl._print("Available commands: ")
        for name, command in repl._magic_command_args.items():
            if command.required_args_count >= 0:
                required_args = f"({command.required_args_count})"
            else:
                required_args = f"({abs(command.required_args_count) - 1}+)"

            repl._print(f"  %{name}{required_args} - {command.description}")

        repl._print("")
        repl._print("For help with Frida scripting API, check out https://frida.re/docs/")
        repl._print("")
