#!/usr/bin/env python3
"""Query detailed state of UI elements via AT-SPI.

Usage:
    query-state.py --role button --name "OK"
    query-state.py --window "Firefox" --focused
    query-state.py --all-states

Options:
    --role ROLE        Filter by role
    --name PATTERN     Filter by name
    --window PATTERN   Limit to matching window
    --focused          Only show focused element
    --all-states       Show all state flags (verbose)
    --json             Output as JSON
"""

import argparse
import json
import sys

import pyatspi

from common import find_elements, set_debug, log_debug

# Human-readable state descriptions
STATE_DESCRIPTIONS = {
    "active": "Currently active/foreground",
    "armed": "Ready to be activated",
    "busy": "Processing/loading",
    "checked": "Checkbox/toggle is checked",
    "collapsed": "Collapsed (can be expanded)",
    "defunct": "Object no longer valid",
    "editable": "Text can be edited",
    "enabled": "Interactive (not grayed out)",
    "expandable": "Can be expanded",
    "expanded": "Currently expanded",
    "focusable": "Can receive keyboard focus",
    "focused": "Has keyboard focus",
    "has-tooltip": "Has tooltip available",
    "horizontal": "Horizontal orientation",
    "iconified": "Minimized to icon",
    "indeterminate": "Neither checked nor unchecked",
    "invalid-entry": "Contains invalid data",
    "is-default": "Default action element",
    "modal": "Modal dialog",
    "multi-line": "Multi-line text",
    "multiselectable": "Multiple selection allowed",
    "opaque": "Fully opaque (not transparent)",
    "pressed": "Button is pressed",
    "resizable": "Can be resized",
    "selectable": "Can be selected",
    "selected": "Currently selected",
    "sensitive": "Responds to user input",
    "showing": "Visible on screen",
    "single-line": "Single-line text",
    "stale": "Data may be outdated",
    "transient": "Temporary element",
    "vertical": "Vertical orientation",
    "visible": "Rendered (may be scrolled off)",
    "visited": "Link has been visited",
}


def get_all_states(accessible):
    """Get all states for an accessible."""
    state = accessible.getState()
    states = {}
    for s in pyatspi.STATE_VALUE_TO_NAME:
        name = pyatspi.STATE_VALUE_TO_NAME[s]
        if state.contains(s):
            states[name] = True
    return states


def get_element_details(element, all_states=False):
    """Get detailed info about an element."""
    info = {
        "role": element.getRoleName(),
        "name": element.name or "",
    }

    if element.description:
        info["description"] = element.description

    # States
    states = get_all_states(element)
    if all_states:
        info["states"] = states
    else:
        # Just key interactive states
        key_states = {k: v for k, v in states.items()
                      if k in ("focused", "selected", "checked", "expanded",
                               "enabled", "editable", "pressed", "active",
                               "busy", "invalid-entry", "indeterminate")}
        info["states"] = key_states

    # State summary for quick reading
    active_states = [k for k, v in states.items() if v]
    info["state_summary"] = ", ".join(sorted(active_states))

    # Geometry
    try:
        component = element.queryComponent()
        if component:
            rect = component.getExtents(pyatspi.DESKTOP_COORDS)
            info["geometry"] = {
                "x": rect.x, "y": rect.y,
                "width": rect.width, "height": rect.height,
            }
    except Exception as e:
        log_debug(f"Failed to get geometry: {e}")

    # Text content
    try:
        text_iface = element.queryText()
        if text_iface and text_iface.characterCount > 0:
            info["text"] = text_iface.getText(0, min(50, text_iface.characterCount))
            info["text_length"] = text_iface.characterCount
            # Caret position if editable
            if states.get("editable"):
                info["caret_offset"] = text_iface.caretOffset
    except Exception as e:
        log_debug(f"Failed to get text: {e}")

    # Value (for sliders, progress bars, etc.)
    try:
        value_iface = element.queryValue()
        if value_iface:
            info["value"] = {
                "current": value_iface.currentValue,
                "min": value_iface.minimumValue,
                "max": value_iface.maximumValue,
            }
    except Exception as e:
        log_debug(f"Failed to get value: {e}")

    # Actions available
    try:
        action_iface = element.queryAction()
        if action_iface:
            actions = []
            for i in range(action_iface.nActions):
                actions.append(action_iface.getName(i))
            if actions:
                info["actions"] = actions
    except Exception as e:
        log_debug(f"Failed to get actions: {e}")

    return info


def find_focused_element(accessible, depth=0, max_depth=20):
    """Find the focused element in the tree."""
    if depth > max_depth:
        return None

    try:
        states = get_all_states(accessible)
        if states.get("focused"):
            return accessible

        for i in range(accessible.childCount):
            child = accessible.getChildAtIndex(i)
            if child:
                result = find_focused_element(child, depth + 1, max_depth)
                if result:
                    return result
    except Exception as e:
        log_debug(f"Error finding focused element at depth {depth}: {e}")

    return None


def print_element(info, all_states=False):
    """Print element info in readable format."""
    print(f"\n[{info['role']}] {info['name'] or '(unnamed)'}")
    print("-" * 40)

    if info.get("description"):
        print(f"Description: {info['description']}")

    if info.get("text"):
        text_preview = info['text'][:50] + "..." if len(info.get('text', '')) > 50 else info['text']
        print(f"Text: {text_preview}")
        if info.get("text_length", 0) > 50:
            print(f"      ({info['text_length']} chars total)")
        if "caret_offset" in info:
            print(f"Caret: position {info['caret_offset']}")

    if info.get("value"):
        v = info["value"]
        print(f"Value: {v['current']} (range: {v['min']} - {v['max']})")

    geo = info.get("geometry", {})
    if geo:
        print(f"Position: ({geo['x']}, {geo['y']}) Size: {geo['width']}x{geo['height']}")

    if info.get("actions"):
        print(f"Actions: {', '.join(info['actions'])}")

    print(f"\nStates: {info.get('state_summary', 'none')}")

    if all_states and info.get("states"):
        print("\nState Details:")
        for state, active in sorted(info["states"].items()):
            if active:
                desc = STATE_DESCRIPTIONS.get(state, "")
                print(f"  ✓ {state}: {desc}" if desc else f"  ✓ {state}")


def main():
    parser = argparse.ArgumentParser(description="Query UI element states via AT-SPI")
    parser.add_argument("--role", "-r", help="Filter by role")
    parser.add_argument("--name", "-n", help="Filter by name")
    parser.add_argument("--window", "-w", help="Limit to window")
    parser.add_argument("--focused", "-f", action="store_true", help="Show focused element only")
    parser.add_argument("--all-states", "-a", action="store_true", help="Show all state flags")
    parser.add_argument("--json", action="store_true", help="Output as JSON")
    parser.add_argument("--debug", action="store_true",
                        help="Show debug messages for AT-SPI errors")
    args = parser.parse_args()

    if args.debug:
        set_debug(True)

    if not args.role and not args.name and not args.focused:
        parser.print_help()
        print("\nExamples:")
        print("  query-state.py --focused              # Current focused element")
        print("  query-state.py --role checkbox        # All checkboxes with states")
        print("  query-state.py --name 'Save' -a       # 'Save' elements, all states")
        sys.exit(1)

    desktop = pyatspi.Registry.getDesktop(0)

    # Build list of windows to search
    windows = []
    for i in range(desktop.childCount):
        app = desktop.getChildAtIndex(i)
        if not app:
            continue
        for j in range(app.childCount):
            win = app.getChildAtIndex(j)
            if not win:
                continue
            if args.window and args.window.lower() not in (win.name or "").lower():
                continue
            windows.append(win)

    if not windows:
        print("No matching windows found.", file=sys.stderr)
        sys.exit(1)

    results = []

    if args.focused:
        # Find focused element
        for win in windows:
            focused = find_focused_element(win)
            if focused:
                results.append(get_element_details(focused, args.all_states))
                break
        if not results:
            print("No focused element found.", file=sys.stderr)
            sys.exit(1)
    else:
        # Find by role/name
        for win in windows:
            elements = find_elements(win, args.role, args.name, limit=10 - len(results))
            for elem in elements:
                results.append(get_element_details(elem, args.all_states))
            if len(results) >= 10:
                break

    if args.json:
        print(json.dumps(results, indent=2))
    else:
        for info in results:
            print_element(info, args.all_states)


if __name__ == "__main__":
    main()
