#!/usr/bin/env python3
#
# Copyright (c) 2024 Stormux
#
# 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.
#
# Cthulhu project: https://git.stormux.org/storm/cthulhu

"""Steam-specific utility helpers."""

from __future__ import annotations

from typing import Optional

from cthulhu import debug
from cthulhu.ax_object import AXObject
from cthulhu.ax_text import AXText
from cthulhu.ax_utilities import AXUtilities
from cthulhu.scripts.toolkits.Chromium.script_utilities import Utilities as ChromiumUtilities


class Utilities(ChromiumUtilities):
    def __init__(self, script) -> None:
        super().__init__(script)
        self._steamInferredButtonLabels: dict[int, str] = {}

    def clearCachedObjects(self) -> None:
        super().clearCachedObjects()
        self._steamInferredButtonLabels = {}

    def clearSteamVirtualizedListCaches(self) -> None:
        self.clearContentCache()
        self._steamInferredButtonLabels = {}
        self._isUselessImage = {}
        self._shouldFilter = {}

    def isSteamVirtualizedList(self, obj) -> bool:
        if not (obj and self.inDocumentContent(obj)):
            return False

        if not AXUtilities.is_list(obj):
            return False

        className = AXObject.get_attribute(obj, "class") or ""
        return "ReactVirtualized__Grid__innerScrollContainer" in className

    def displayedLabel(self, obj):
        label = super().displayedLabel(obj)
        if label or not self._shouldInferSteamButtonLabel(obj):
            return label

        inferredLabel = self._getSteamInferredButtonLabel(obj)
        if inferredLabel:
            self._displayedLabelText[hash(obj)] = inferredLabel
        return inferredLabel

    def displayedText(self, obj):
        text = super().displayedText(obj)
        if text or not self._shouldInferSteamButtonLabel(obj):
            return text

        inferredLabel = self._getSteamInferredButtonLabel(obj)
        if inferredLabel:
            cache = self._script.generatorCache.setdefault(self.DISPLAYED_TEXT, {})
            cache[obj] = inferredLabel
        return inferredLabel

    def isUselessImage(self, obj) -> bool:
        if not (obj and self.inDocumentContent(obj)):
            return super().isUselessImage(obj)

        cached = self._isUselessImage.get(hash(obj))
        if cached is not None:
            return cached

        rv = super().isUselessImage(obj)
        if not rv:
            rv = self._isRedundantSteamImage(obj)

        self._isUselessImage[hash(obj)] = rv
        return rv

    def _shouldInferSteamButtonLabel(self, obj) -> bool:
        if not (obj and self.inDocumentContent(obj)):
            return False

        if AXObject.get_name(obj):
            return False

        if not (AXUtilities.is_button(obj) or AXUtilities.is_push_button(obj)):
            return False

        className = AXObject.get_attribute(obj, "class") or ""
        return "FriendsListTab" in className or "AddFriendButton" in className

    def _getSteamInferredButtonLabel(self, obj) -> str:
        cached = self._steamInferredButtonLabels.get(hash(obj))
        if cached is not None:
            return cached

        className = AXObject.get_attribute(obj, "class") or ""
        inferredLabel = self._getSteamButtonLabelFromClass(className)
        if not inferredLabel:
            inferredLabel = self._getSteamNearbyButtonLabel(obj)

        inferredLabel = inferredLabel or ""
        self._steamInferredButtonLabels[hash(obj)] = inferredLabel
        if inferredLabel:
            tokens = ["STEAM: Inferred label for", obj, ":", inferredLabel]
            debug.printTokens(debug.LEVEL_INFO, tokens, True)

        return inferredLabel

    @staticmethod
    def _getSteamButtonLabelFromClass(className: str) -> str:
        if "AddFriendButton" in className:
            return "Add Friend"
        return ""

    def _isRedundantSteamImage(self, obj) -> bool:
        if not AXUtilities.is_image_or_canvas(obj):
            return False

        if AXObject.get_name(obj) or AXObject.get_description(obj):
            return False

        if AXObject.get_child_count(obj):
            return False

        if AXUtilities.is_focusable(obj):
            return False

        if not AXObject.has_action(obj, "click-ancestor"):
            return False

        roleDescription = self._normalizeSteamLabelText(AXObject.get_role_description(obj) or "")
        if roleDescription and roleDescription.casefold() not in ["unlabeled image", "image"]:
            return False

        nearbyLabel = self._getSteamNearbyImageLabel(obj)
        if not self._isUsefulSteamLabel(nearbyLabel):
            return False

        tokens = ["STEAM: Treating redundant image as useless:", obj, "(label:", nearbyLabel, ")"]
        debug.printTokens(debug.LEVEL_INFO, tokens, True)
        return True

    def _getSteamNearbyButtonLabel(self, obj) -> str:
        parent = AXObject.get_parent(obj)
        if parent is None:
            return ""

        siblingLabel = self._getSteamLabelFromChildren(parent, ignore=obj)
        if siblingLabel:
            return siblingLabel

        parentLabel = self._getSteamReadableText(parent)
        if self._isUsefulSteamLabel(parentLabel):
            return parentLabel

        grandParent = AXObject.get_parent(parent)
        if grandParent is None:
            return ""

        return self._getSteamLabelFromChildren(grandParent, ignore=parent)

    def _getSteamNearbyImageLabel(self, obj) -> str:
        parent = AXObject.get_parent(obj)
        if parent is None:
            return ""

        siblingLabel = self._getSteamLabelFromChildren(parent, ignore=obj)
        if siblingLabel:
            return siblingLabel

        parentLabel = self._getSteamReadableText(parent)
        if self._isUsefulSteamLabel(parentLabel):
            return parentLabel

        grandParent = AXObject.get_parent(parent)
        if grandParent is None:
            return ""

        return self._getSteamLabelFromChildren(grandParent, ignore=parent)

    def _getSteamLabelFromChildren(self, obj, ignore=None) -> str:
        for child in AXObject.iter_children(obj):
            if child == ignore:
                continue
            if AXUtilities.is_button(child) or AXUtilities.is_push_button(child):
                continue

            label = self._getSteamReadableText(child)
            if self._isUsefulSteamLabel(label):
                return label

        return ""

    def _getSteamReadableText(self, obj) -> str:
        if obj is None:
            return ""

        name = self._normalizeSteamLabelText(AXObject.get_name(obj) or "")
        if self._isUsefulSteamLabel(name):
            return name

        if not AXObject.supports_text(obj):
            return ""

        text = AXText.get_all_text(obj) or ""
        text = text.replace(self.EMBEDDED_OBJECT_CHARACTER, " ")
        text = self._normalizeSteamLabelText(text)
        if self._isUsefulSteamLabel(text):
            return text

        return ""

    @staticmethod
    def _normalizeSteamLabelText(text: str) -> str:
        return " ".join(text.split())

    @staticmethod
    def _isUsefulSteamLabel(text: Optional[str]) -> bool:
        if not text:
            return False

        return not text.isdigit() and text.casefold() != "unlabeled image"
