# Orca
#
# Copyright 2005-2008 Sun Microsystems Inc.
# Copyright 2016-2026 Igalia, S.L.
# Copyright 2024 GNOME Foundation Inc.
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
# Boston MA  02110-1301 USA.

# pylint: disable=too-many-locals

"""Module for presenting system information"""

from __future__ import annotations

import time
from enum import Enum
from typing import TYPE_CHECKING, Any

import gi

gi.require_version("Atspi", "2.0")
from gi.repository import Atspi

_PSUTIL_AVAILABLE = False  # pylint: disable=invalid-name
try:
    import psutil  # type: ignore

    _PSUTIL_AVAILABLE = True  # pylint: disable=invalid-name
except ModuleNotFoundError:
    pass

from . import (
    ax_device_manager,
    dbus_service,
    debug,
    gsettings_registry,
    guilabels,
    input_event,
    keynames,
    messages,
    presentation_manager,
    system_information_presenter_command_definitions,
)
from .extension import Extension

if TYPE_CHECKING:
    from .command import Command


class DateFormat(Enum):
    """Date format enumeration with format strings."""

    LOCALE = "%x"
    NUMBERS_DM = "%d/%m"
    NUMBERS_MD = "%m/%d"
    NUMBERS_DMY = "%d/%m/%Y"
    NUMBERS_MDY = "%m/%d/%Y"
    NUMBERS_YMD = "%Y/%m/%d"
    FULL_DM = "%A, %-d %B"
    FULL_MD = "%A, %B %-d"
    FULL_DMY = "%A, %-d %B, %Y"
    FULL_MDY = "%A, %B %-d, %Y"
    FULL_YMD = "%Y. %B %-d, %A"
    ABBREVIATED_DM = "%a, %-d %b"
    ABBREVIATED_MD = "%a, %b %-d"
    ABBREVIATED_DMY = "%a, %-d %b, %Y"
    ABBREVIATED_MDY = "%a, %b %-d, %Y"
    ABBREVIATED_YMD = "%Y. %b %-d, %a"

    @property
    def string_name(self) -> str:
        """Returns the lowercase string name for this enum value."""

        return self.name.lower()


class TimeFormat(Enum):
    """Time format enumeration with format strings."""

    LOCALE = "%X"
    TWELVE_HM = "%I:%M %p"
    TWELVE_HMS = "%I:%M:%S %p"
    TWENTYFOUR_HM = "%H:%M"
    TWENTYFOUR_HMS = "%H:%M:%S"
    TWENTYFOUR_HM_WITH_WORDS = messages.TIME_FORMAT_24_HM_WITH_WORDS
    TWENTYFOUR_HMS_WITH_WORDS = messages.TIME_FORMAT_24_HMS_WITH_WORDS

    @property
    def string_name(self) -> str:
        """Returns the lowercase string name for this enum value."""

        return self.name.lower()


if TYPE_CHECKING:
    from .scripts import default
    from .system_information_presenter_preferences_grid import TimeAndDatePreferencesGrid


@gsettings_registry.get_registry().gsettings_schema(
    "org.gnome.Orca.SystemInformation",
    name="system-information",
)
class SystemInformationPresenter(Extension):
    """Provides commands to present system information."""

    _SCHEMA = "system-information"
    KEY_DATE_FORMAT = "date-format"
    KEY_TIME_FORMAT = "time-format"

    def _get_setting(self, key: str, gtype: str, default: Any) -> Any:
        """Returns the dconf value for key, or default if not in dconf."""

        return gsettings_registry.get_registry().layered_lookup(
            self._SCHEMA,
            key,
            gtype,
            default=default,
        )

    GROUP_LABEL = guilabels.KB_GROUP_SYSTEM_INFORMATION

    def _get_commands(self) -> list[Command]:
        return system_information_presenter_command_definitions.get_commands(self)

    def create_time_and_date_preferences_grid(self) -> TimeAndDatePreferencesGrid:
        """Returns the GtkGrid containing the time and date preferences UI."""

        # pylint: disable-next=import-outside-toplevel
        from .system_information_presenter_preferences_grid import TimeAndDatePreferencesGrid

        return TimeAndDatePreferencesGrid(self)

    def _get_date_format_string(self) -> str:
        """Returns the current date format string for internal use."""

        return self._get_setting(self.KEY_DATE_FORMAT, "s", "%x")

    def _get_time_format_string(self) -> str:
        """Returns the current time format string for internal use."""

        return self._get_setting(self.KEY_TIME_FORMAT, "s", "%X")

    def _set_date_format_string(self, value: str) -> bool:
        """Sets the date format string directly for internal use."""

        gsettings_registry.get_registry().set_runtime_value(
            self._SCHEMA, self.KEY_DATE_FORMAT, value
        )
        return True

    def _set_time_format_string(self, value: str) -> bool:
        """Sets the time format string directly for internal use."""

        gsettings_registry.get_registry().set_runtime_value(
            self._SCHEMA, self.KEY_TIME_FORMAT, value
        )
        return True

    @gsettings_registry.get_registry().gsetting(
        key=KEY_DATE_FORMAT,
        schema="system-information",
        gtype="s",
        default="%x",
        summary="Date format string",
        migration_key="presentDateFormat",
    )
    @dbus_service.getter
    def get_date_format(self) -> str:
        """Returns the current date format name."""

        string_value = self._get_date_format_string()
        for fmt in DateFormat:
            if fmt.value == string_value:
                return fmt.string_name
        return string_value

    @dbus_service.setter
    def set_date_format(self, value: str) -> bool:
        """Sets the date format."""

        try:
            fmt = DateFormat[value.upper()]
        except KeyError:
            msg = f"SYSTEM INFORMATION PRESENTER: Invalid date format: {value}"
            debug.print_message(debug.LEVEL_WARNING, msg, True)
            return False

        msg = f"SYSTEM INFORMATION PRESENTER: Setting date format to {value}."
        debug.print_message(debug.LEVEL_INFO, msg, True)
        gsettings_registry.get_registry().set_runtime_value(
            self._SCHEMA, self.KEY_DATE_FORMAT, fmt.value
        )
        return True

    @dbus_service.getter
    def get_available_date_formats(self) -> list[str]:
        """Returns a list of available date format names."""

        return [fmt.string_name for fmt in DateFormat]

    @gsettings_registry.get_registry().gsetting(
        key=KEY_TIME_FORMAT,
        schema="system-information",
        gtype="s",
        default="%X",
        summary="Time format string",
        migration_key="presentTimeFormat",
    )
    @dbus_service.getter
    def get_time_format(self) -> str:
        """Returns the current time format name."""

        string_value = self._get_time_format_string()
        for fmt in TimeFormat:
            if fmt.value == string_value:
                return fmt.string_name
        return string_value

    @dbus_service.setter
    def set_time_format(self, value: str) -> bool:
        """Sets the time format."""

        try:
            fmt = TimeFormat[value.upper()]
        except KeyError:
            msg = f"SYSTEM INFORMATION PRESENTER: Invalid time format: {value}"
            debug.print_message(debug.LEVEL_WARNING, msg, True)
            return False

        msg = f"SYSTEM INFORMATION PRESENTER: Setting time format to {value}."
        debug.print_message(debug.LEVEL_INFO, msg, True)
        gsettings_registry.get_registry().set_runtime_value(
            self._SCHEMA, self.KEY_TIME_FORMAT, fmt.value
        )
        return True

    @dbus_service.getter
    def get_available_time_formats(self) -> list[str]:
        """Returns a list of available time format names."""

        return [fmt.string_name for fmt in TimeFormat]

    @dbus_service.command
    def present_time(
        self,
        script: default.Script,
        event: input_event.InputEvent | None = None,
        notify_user: bool = True,
    ) -> bool:
        """Presents the current time."""

        tokens = [
            "SYSTEM INFORMATION PRESENTER: present_time. Script:",
            script,
            "Event:",
            event,
            "notify_user:",
            notify_user,
        ]
        debug.print_tokens(debug.LEVEL_INFO, tokens, True)

        time_format = self._get_time_format_string()
        presentation_manager.get_manager().present_message(
            time.strftime(time_format, time.localtime()),
        )
        return True

    @dbus_service.command
    def present_date(
        self,
        script: default.Script,
        event: input_event.InputEvent | None = None,
        notify_user: bool = True,
    ) -> bool:
        """Presents the current date."""

        tokens = [
            "SYSTEM INFORMATION PRESENTER: present_date. Script:",
            script,
            "Event:",
            event,
            "notify_user:",
            notify_user,
        ]
        debug.print_tokens(debug.LEVEL_INFO, tokens, True)

        date_format = self._get_date_format_string()
        presentation_manager.get_manager().present_message(
            time.strftime(date_format, time.localtime()),
        )
        return True

    @dbus_service.command
    def present_battery_status(
        self,
        script: default.Script,
        event: input_event.InputEvent | None = None,
        notify_user: bool = True,
    ) -> bool:
        """Presents the battery status."""

        tokens = [
            "SYSTEM INFORMATION PRESENTER: present_battery_status. Script:",
            script,
            "Event:",
            event,
            "notify_user:",
            notify_user,
        ]
        debug.print_tokens(debug.LEVEL_INFO, tokens, True)

        battery = psutil.sensors_battery() if _PSUTIL_AVAILABLE else None
        if battery is None:
            presentation_manager.get_manager().present_message(messages.BATTERY_STATUS_UNKNOWN)
            return True
        if battery.power_plugged:
            msg = f"{messages.BATTERY_LEVEL % battery.percent} {messages.BATTERY_PLUGGED_IN_TRUE}"
        else:
            msg = f"{messages.BATTERY_LEVEL % battery.percent} {messages.BATTERY_PLUGGED_IN_FALSE}"

        presentation_manager.get_manager().present_message(msg)
        return True

    @dbus_service.command
    def present_cpu_and_memory_usage(
        self,
        script: default.Script,
        event: input_event.InputEvent | None = None,
        notify_user: bool = True,
    ) -> bool:
        """Presents the cpu and memory usage."""

        tokens = [
            "SYSTEM INFORMATION PRESENTER: present_cpu_and_memory_usage. Script:",
            script,
            "Event:",
            event,
            "notify_user:",
            notify_user,
        ]
        debug.print_tokens(debug.LEVEL_INFO, tokens, True)

        if not _PSUTIL_AVAILABLE:
            presentation_manager.get_manager().present_message(
                messages.CPU_AND_MEMORY_USAGE_UNKNOWN,
            )
            return True

        cpu_usage = round(psutil.cpu_percent())

        memory = psutil.virtual_memory()
        memory_percent = round(memory.percent)
        if memory.total > 1024**3:
            details = messages.memory_usage_gb(memory.used / (1024**3), memory.total / (1024**3))
        else:
            details = messages.memory_usage_mb(memory.used / (1024**2), memory.total / (1024**2))

        msg = f"{messages.CPU_AND_MEMORY_USAGE_LEVELS % (cpu_usage, memory_percent)}. {details}"
        presentation_manager.get_manager().present_message(msg)
        return True

    @dbus_service.command
    def present_modifier_keys_state(
        self,
        script: default.Script,
        event: input_event.InputEvent | None = None,
        notify_user: bool = True,
    ) -> bool:
        """Presents the state of locked modifier keys. Requires AT-SPI 2.59.0 or later."""

        tokens = [
            "SYSTEM INFORMATION PRESENTER: present_modifier_keys_state. Script:",
            script,
            "Event:",
            event,
            "notify_user:",
            notify_user,
        ]
        debug.print_tokens(debug.LEVEL_INFO, tokens, True)

        manager = ax_device_manager.get_manager()
        if not manager.is_active():
            presentation_manager.get_manager().present_message(
                messages.MODIFIER_KEYS_STATE_UNAVAILABLE,
            )
            return True

        locked = manager.get_locked_modifiers()
        modifiers = [
            ("Caps_Lock", Atspi.ModifierType.SHIFTLOCK),
            ("Num_Lock", Atspi.ModifierType.NUMLOCK),
        ]

        parts: list[str] = []
        for key_name, modifier_type in modifiers:
            bit = 1 << modifier_type
            is_locked = bool(locked & bit)
            msg = (
                f"SYSTEM INFORMATION PRESENTER: {key_name}: "
                f"modifier_type={modifier_type}, bit={bit:#x}, "
                f"locked & bit={locked & bit:#x}, is_locked={is_locked}"
            )
            debug.print_message(debug.LEVEL_INFO, msg, True)

            localized_name = keynames.get_key_name(key_name) or key_name
            if is_locked:
                state = messages.LOCKING_KEY_STATE_ON
            else:
                state = messages.LOCKING_KEY_STATE_OFF
            parts.append(f"{localized_name} {state}")

        presentation_manager.get_manager().present_message(". ".join(parts))
        return True


_presenter = SystemInformationPresenter()


def get_presenter() -> SystemInformationPresenter:
    """Returns the system-information-presenter singleton."""

    return _presenter
