#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# Fenrir TTY screen reader
# By Chrys, Storm Dragon, and contributors.

import glob
import inspect
import os
import time

from fenrirscreenreader.core import debug
from fenrirscreenreader.utils import module_utils

currentdir = os.path.dirname(
    os.path.realpath(os.path.abspath(inspect.getfile(inspect.currentframe())))
)
fenrir_path = os.path.dirname(currentdir)


class CommandManager:
    def __init__(self):
        pass

    def initialize(self, environment):
        self.env = environment
        # commands
        self.env["commands"] = {}
        self.env["commandsIgnore"] = {}
        for command_folder in self.env["general"]["commandFolderList"]:
            self.env["runtime"]["CommandManager"].load_commands(command_folder)
            if (
                self.env["runtime"]["SettingsManager"].get_setting(
                    "general", "command_path"
                )
                != ""
            ):
                self.env["runtime"]["CommandManager"].load_commands(
                    command_folder,
                    self.env["runtime"]["SettingsManager"].get_setting(
                        "general", "command_path"
                    ),
                )

        # scripts for scriptKey
        self.env["runtime"]["CommandManager"].load_script_commands()

    def shutdown(self):
        for command_folder in self.env["general"]["commandFolderList"]:
            self.env["runtime"]["CommandManager"].shutdown_commands(
                command_folder
            )

    def load_file(self, filepath=""):
        if filepath == "":
            return None
        if not os.path.exists(filepath):
            self.env["runtime"]["DebugManager"].write_debug_out(
                "load_file: filepath not exists:" + filepath,
                debug.DebugLevel.WARNING,
            )
            return None
        if os.path.isdir(filepath):
            self.env["runtime"]["DebugManager"].write_debug_out(
                "load_file: filepath is a directory:" + filepath,
                debug.DebugLevel.ERROR,
            )
            return None
        if not os.access(filepath, os.R_OK):
            self.env["runtime"]["DebugManager"].write_debug_out(
                "load_file: filepath not readable:" + filepath,
                debug.DebugLevel.ERROR,
            )
            return None

        try:
            file_name, file_extension = os.path.splitext(filepath)
            file_name = file_name.split("/")[-1]
            if file_name.startswith("__"):
                return None
            if file_extension.lower() == ".py":
                command_mod = module_utils.import_module(file_name, filepath)
                command = command_mod.command()
                command.initialize(self.env)
                self.env["runtime"]["DebugManager"].write_debug_out(
                    "load_file: Load command:" + filepath,
                    debug.DebugLevel.INFO,
                    on_any_level=True,
                )
                return command
        except Exception as e:
            self.env["runtime"]["DebugManager"].write_debug_out(
                "load_file: Loading command:" + filepath,
                debug.DebugLevel.ERROR,
            )
            self.env["runtime"]["DebugManager"].write_debug_out(
                str(e), debug.DebugLevel.ERROR
            )
        return None

    def load_commands(self, section="commands", command_path=""):
        if command_path == "":
            command_path = fenrir_path + "/commands/"
        if not command_path.endswith("/"):
            command_path += "/"
        command_folder = command_path + section + "/"
        if not os.path.exists(command_folder):
            self.env["runtime"]["DebugManager"].write_debug_out(
                "load_commands: command_folder not exists:" + command_folder,
                debug.DebugLevel.WARNING,
            )
            return
        if not os.path.isdir(command_folder):
            self.env["runtime"]["DebugManager"].write_debug_out(
                "load_commands: command_folder not a directory:"
                + command_folder,
                debug.DebugLevel.ERROR,
            )
            return
        if not os.access(command_folder, os.R_OK):
            self.env["runtime"]["DebugManager"].write_debug_out(
                "load_commands: command_folder not readable:" + command_folder,
                debug.DebugLevel.ERROR,
            )
            return
        self.env["commands"][section] = {}
        self.env["commandsIgnore"][section] = {}
        command_list = glob.glob(command_folder + "*")
        for command in command_list:
            try:
                file_name, file_extension = os.path.splitext(command)
                file_name = file_name.split("/")[-1]
                if file_name.startswith("__"):
                    continue
                # Skip base classes that shouldn't be loaded as commands
                if file_name.endswith("_base"):
                    continue
                # Check if command already exists to prevent duplicate loading
                if (
                    file_name.upper() in self.env["commands"][section]
                    and self.env["commands"][section][file_name.upper()]
                    is not None
                ):
                    continue
                if file_extension.lower() == ".py":
                    command_mod = module_utils.import_module(
                        file_name, command
                    )
                    self.env["commands"][section][
                        file_name.upper()
                    ] = command_mod.command()
                    self.env["commandsIgnore"][section][
                        file_name.upper()[file_name.upper().find("-") + 1 :]
                        + "_IGNORE"
                    ] = False
                    self.env["commands"][section][
                        file_name.upper()
                    ].initialize(self.env)
                    self.env["runtime"]["DebugManager"].write_debug_out(
                        "load_commands: Load command:"
                        + section
                        + "."
                        + file_name.upper(),
                        debug.DebugLevel.INFO,
                        on_any_level=True,
                    )
            except Exception as e:
                self.env["runtime"]["DebugManager"].write_debug_out(
                    "load_commands: Loading command:" + command,
                    debug.DebugLevel.ERROR,
                )
                self.env["runtime"]["DebugManager"].write_debug_out(
                    str(e), debug.DebugLevel.ERROR
                )
                continue

    def get_script_paths(self, script_path=""):
        if script_path:
            candidate_paths = [script_path]
        else:
            settings_manager = self.env["runtime"]["SettingsManager"]
            candidate_paths = [
                settings_manager.get_user_script_path(),
                settings_manager.get_setting("general", "script_path"),
                os.path.join(fenrir_path, "../../config/scripts/"),
            ]

        script_paths = []
        seen_paths = set()
        for path in candidate_paths:
            if not path:
                continue
            normalized_path = os.path.abspath(os.path.expanduser(path))
            if normalized_path in seen_paths:
                continue
            script_paths.append(normalized_path)
            seen_paths.add(normalized_path)
        return script_paths

    def load_script_commands(self, section="commands", script_path=""):
        script_paths = self.get_script_paths(script_path)
        loaded_any_path = False
        loaded_script_names = set()
        for path in script_paths:
            loaded_any_path = (
                self.load_script_commands_from_path(
                    section, path, loaded_script_names
                )
                or loaded_any_path
            )
        if not loaded_any_path:
            self.env["runtime"]["DebugManager"].write_debug_out(
                "No script paths available:" + str(script_paths),
                debug.DebugLevel.WARNING,
            )

    def load_script_commands_from_path(
        self, section, script_path, loaded_script_names=None
    ):
        if loaded_script_names is None:
            loaded_script_names = set()
        if not os.path.exists(script_path):
            return False
        if not os.path.isdir(script_path):
            self.env["runtime"]["DebugManager"].write_debug_out(
                "scriptpath not a directory:" + script_path,
                debug.DebugLevel.ERROR,
            )
            return False
        if not os.access(script_path, os.R_OK):
            self.env["runtime"]["DebugManager"].write_debug_out(
                "scriptpath not readable:" + script_path,
                debug.DebugLevel.ERROR,
            )
            return False
        command_list = sorted(glob.glob(os.path.join(script_path, "*")))
        sub_command = fenrir_path + "/commands/commands/subprocess.py"
        for command in command_list:
            try:
                if not os.path.isfile(command):
                    continue
                file_name, file_extension = os.path.splitext(command)
                file_name = file_name.split("/")[-1]
                if file_name.startswith("__"):
                    continue
                command_name = file_name.upper()
                script_name = self.get_script_name(file_name)
                if script_name in loaded_script_names:
                    self.env["runtime"]["DebugManager"].write_debug_out(
                        "Skip script with duplicate script name:"
                        + command_name,
                        debug.DebugLevel.INFO,
                    )
                    continue
                if command_name in self.env["commands"][section]:
                    self.env["runtime"]["DebugManager"].write_debug_out(
                        "Skip script with duplicate command name:"
                        + command_name,
                        debug.DebugLevel.INFO,
                    )
                    continue
                shortcut = self.get_script_shortcut(file_name)
                if not shortcut:
                    continue
                shortcut_key = str(shortcut)
                if shortcut_key in self.env["bindings"]:
                    self.env["runtime"]["DebugManager"].write_debug_out(
                        "Skip script with duplicate shortcut:"
                        + command_name
                        + " "
                        + shortcut_key,
                        debug.DebugLevel.INFO,
                    )
                    continue
                command_mod = module_utils.import_module(
                    file_name, sub_command
                )
                self.env["commands"][section][command_name] = (
                    command_mod.command()
                )
                self.env["commands"][section][command_name].initialize(
                    self.env, command
                )
                self.env["bindings"][shortcut_key] = command_name
                self.env["rawBindings"][shortcut_key] = shortcut
                loaded_script_names.add(script_name)
                self.env["runtime"]["DebugManager"].write_debug_out(
                    "Load script:" + section + "." + command_name,
                    debug.DebugLevel.INFO,
                    on_any_level=True,
                )
            except Exception as e:
                self.env["runtime"]["DebugManager"].write_debug_out(
                    "Loading script:" + file_name, debug.DebugLevel.ERROR
                )
                self.env["runtime"]["DebugManager"].write_debug_out(
                    str(e), debug.DebugLevel.ERROR
                )
                continue
        return True

    def get_script_name(self, file_name):
        return file_name.upper().split("__-__", 1)[0]

    def get_script_shortcut(self, file_name):
        comm_settings = file_name.upper().split("__-__")
        if len(comm_settings) == 1:
            keys = comm_settings[0]
        elif len(comm_settings) == 2:
            keys = comm_settings[1]
        else:
            return None

        shortcut_keys = []
        for key in keys.split("__+__"):
            key = key.upper()
            if not key:
                return None
            if not self.env["runtime"]["InputManager"].is_valid_key(key):
                self.env["runtime"]["DebugManager"].write_debug_out(
                    "invalid key : " + key + " script:" + file_name,
                    debug.DebugLevel.WARNING,
                )
                return None
            shortcut_keys.append(key)
        if "KEY_SCRIPT" not in shortcut_keys:
            shortcut_keys.append("KEY_SCRIPT")
        return [1, sorted(shortcut_keys)]

    def shutdown_commands(self, section):
        # Check if the section exists in the commands dictionary
        if section not in self.env["commands"]:
            self.env["runtime"]["DebugManager"].write_debug_out(
                "shutdown_commands: section not found:" + section,
                debug.DebugLevel.WARNING,
            )
            return

        for command in sorted(self.env["commands"][section]):
            try:
                self.env["commands"][section][command].shutdown()
                del self.env["commands"][section][command]
            except Exception as e:
                self.env["runtime"]["DebugManager"].write_debug_out(
                    "Shutdown command:" + section + "." + command,
                    debug.DebugLevel.ERROR,
                )
                self.env["runtime"]["DebugManager"].write_debug_out(
                    str(e), debug.DebugLevel.ERROR
                )
                continue

    def execute_switch_trigger(self, trigger, unLoadScript, loadScript):
        if self.env["runtime"]["ScreenManager"].is_ignored_screen():
            return
        # unload
        old_script = unLoadScript
        if self.command_exists(old_script, trigger):
            try:
                self.env["runtime"]["DebugManager"].write_debug_out(
                    "Executing switchtrigger.unload:"
                    + trigger
                    + "."
                    + old_script,
                    debug.DebugLevel.INFO,
                )
                self.env["commands"][trigger][old_script].unload()
            except Exception as e:
                self.env["runtime"]["DebugManager"].write_debug_out(
                    "Executing trigger:" + trigger + "." + old_script,
                    debug.DebugLevel.ERROR,
                )
                self.env["runtime"]["DebugManager"].write_debug_out(
                    str(e), debug.DebugLevel.ERROR
                )
        # load
        new_script = loadScript
        if self.command_exists(new_script, trigger):
            try:
                self.env["runtime"]["DebugManager"].write_debug_out(
                    "Executing switchtrigger.load:"
                    + trigger
                    + "."
                    + new_script,
                    debug.DebugLevel.INFO,
                )
                self.env["commands"][trigger][new_script].load()
            except Exception as e:
                self.env["runtime"]["DebugManager"].write_debug_out(
                    "Executing trigger:" + trigger + "." + new_script,
                    debug.DebugLevel.ERROR,
                )
                self.env["runtime"]["DebugManager"].write_debug_out(
                    str(e), debug.DebugLevel.ERROR
                )

    def execute_default_trigger(self, trigger, force=False):
        if not force:
            if self.env["runtime"]["ScreenManager"].is_ignored_screen():
                return
        for command in sorted(self.env["commands"][trigger]):
            if self.command_exists(command, trigger):
                try:
                    if self.env["commandsIgnore"][trigger][
                        command[command.find("-") + 1 :] + "_IGNORE"
                    ]:
                        self.env["commandsIgnore"][trigger][
                            command[command.find("-") + 1 :] + "_IGNORE"
                        ] = False
                        self.env["runtime"]["DebugManager"].write_debug_out(
                            "ignore trigger.command:"
                            + trigger
                            + "."
                            + command,
                            debug.DebugLevel.INFO,
                        )
                    else:
                        self.env["runtime"]["DebugManager"].write_debug_out(
                            "Executing trigger.command:"
                            + trigger
                            + "."
                            + command,
                            debug.DebugLevel.INFO,
                        )
                        self.env["commands"][trigger][command].run()
                except Exception as e:
                    self.env["runtime"]["DebugManager"].write_debug_out(
                        "Executing trigger:"
                        + trigger
                        + "."
                        + command
                        + str(e),
                        debug.DebugLevel.ERROR,
                    )

    def execute_command(self, command, section="commands"):
        if self.env["runtime"]["ScreenManager"].is_ignored_screen():
            return
        if self.command_exists(command, section):
            try:
                if (
                    self.env["runtime"]["HelpManager"].is_tutorial_mode()
                    and section != "help"
                ):
                    self.env["runtime"]["DebugManager"].write_debug_out(
                        "Tutorial for command:" + section + "." + command,
                        debug.DebugLevel.INFO,
                    )
                    description = self.get_command_description(
                        command, section
                    )
                    self.env["runtime"]["OutputManager"].present_text(
                        description, interrupt=False
                    )
                else:
                    self.env["runtime"]["DebugManager"].write_debug_out(
                        "Executing command:" + section + "." + command,
                        debug.DebugLevel.INFO,
                    )
                    self.run_command(command, section)
            except Exception as e:
                self.env["runtime"]["DebugManager"].write_debug_out(
                    "Executing command:"
                    + section
                    + "."
                    + command
                    + " "
                    + str(e),
                    debug.DebugLevel.ERROR,
                )

    def run_command(self, command, section="commands"):
        if self.command_exists(command, section):
            try:
                self.env["runtime"]["DebugManager"].write_debug_out(
                    "run_command command:" + section + "." + command,
                    debug.DebugLevel.INFO,
                )
                self.env["commands"][section][command].run()
            except Exception as e:
                self.env["runtime"]["DebugManager"].write_debug_out(
                    "run_command command:"
                    + section
                    + "."
                    + command
                    + " "
                    + str(e),
                    debug.DebugLevel.ERROR,
                )
        self.env["commandInfo"]["lastCommandExecutionTime"] = time.time()

    def get_command_description(self, command, section="commands"):
        if self.command_exists(command, section):
            try:
                return self.env["commands"][section][command].get_description()
            except Exception as e:
                self.env["runtime"]["DebugManager"].write_debug_out(
                    "command_manager.get_command_description:" + str(e),
                    debug.DebugLevel.ERROR,
                )
        self.env["commandInfo"]["lastCommandExecutionTime"] = time.time()

    def command_exists(self, command, section="commands"):
        try:
            return command in self.env["commands"][section]
        except Exception as e:
            return False

    def get_shortcut_for_command(self, command, formatKeys=False):
        shortcut = []
        try:
            raw_shortcut = list(self.env["bindings"].keys())[
                list(self.env["bindings"].values()).index(command)
            ]
            # prefer numbers for multitap
            for k in ["2", "3", "4", "5", "6", "7", "8"]:
                if k in raw_shortcut:
                    formatted_key = k
                    if formatKeys:
                        formatted_key = formatted_key.lower()
                        formatted_key += " times "
                    shortcut.append(formatted_key)
                    raw_shortcut.remove(k)
            # prefer metha keys
            for k in [
                "KEY_FENRIR",
                "KEY_SCRIPT",
                "KEY_CTRL",
                "KEY_SHIFT",
                "KEY_ALT",
                "KEY_META",
            ]:
                if k in raw_shortcut:
                    formatted_key = k
                    if formatKeys:
                        formatted_key = formatted_key.lower()
                        formatted_key = formatted_key.replace(
                            "key_kp", " keypad "
                        )
                        formatted_key = formatted_key.replace("key_", " ")
                    shortcut.append(formatted_key)
                    raw_shortcut.remove(k)
            # handle other keys
            for k in raw_shortcut:
                formatted_key = k
                if formatKeys:
                    formatted_key = formatted_key.lower()
                    formatted_key = formatted_key.replace("key_kp", " keypad ")
                    formatted_key = formatted_key.replace("key_", " ")
                shortcut.append(formatted_key)
        except Exception as e:
            pass
        return shortcut
