#!/usr/bin/python3.11
#
# Copyright (c) 2024 Stormux
# Copyright (c) 2010-2012 The Orca Team
# Copyright (c) 2012 Igalia, S.L.
# Copyright (c) 2005-2010 Sun Microsystems 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.
#
# Forked from Orca screen reader.
# Cthulhu project: https://git.stormux.org/storm/cthulhu

import argparse
import gi
gi.require_version("Atspi", "2.0")
from gi.repository import Atspi
import os
import shutil
import signal
import subprocess
import sys
import time

def setup_paths():
    """Configure paths for both installed and source directory execution."""
    currentDir = os.path.dirname(os.path.abspath(__file__))
    
    # Check if running from source
    if os.path.exists(os.path.join(currentDir, 'plugins')):
        # Running from source directory
        sys.path.insert(0, os.path.dirname(currentDir))
        pythondir = currentDir
        datadir = currentDir
    else:
        # Running installed - determine if local or system based on actual path
        if currentDir.startswith(os.path.expanduser('~/.local')):
            # Local installation (~/.local/bin/cthulhu)
            prefix = os.path.expanduser('~/.local')
        elif currentDir.startswith('/usr/local'):
            # /usr/local installation
            prefix = '/usr/local'
        else:
            # System installation (/usr/bin/cthulhu)
            prefix = '/usr'
            
        # Try to find Python modules in multiple possible locations
        python_version = f'python{sys.version_info.major}.{sys.version_info.minor}'
        possible_pythondirs = [
            os.path.join(prefix, 'lib64', python_version, 'site-packages'),
            os.path.join('/usr', 'lib64', python_version, 'site-packages'),  # System lib64 fallback
            os.path.join('/usr/local', 'lib64', python_version, 'site-packages'),  # /usr/local lib64 fallback
            os.path.join(prefix, 'lib', python_version, 'site-packages'),
            os.path.join('/usr', 'lib', python_version, 'site-packages'),  # System fallback
            os.path.join('/usr/local', 'lib', python_version, 'site-packages')  # /usr/local fallback
        ]
        
        # Use the first directory that contains the cthulhu module
        pythondir = None
        for candidate_dir in possible_pythondirs:
            if os.path.exists(os.path.join(candidate_dir, 'cthulhu', '__init__.py')):
                pythondir = candidate_dir
                break
                
        if pythondir is None:
            for candidate_dir in sys.path:
                if not candidate_dir:
                    continue
                if os.path.exists(os.path.join(candidate_dir, 'cthulhu', '__init__.py')):
                    pythondir = candidate_dir
                    break
        if pythondir is None:
            # Fallback to prefix-relative path if module not found
            pythondir = os.path.join(prefix, 'lib', python_version, 'site-packages')
            
        datadir = os.path.join(prefix, 'share', 'cthulhu')
    
    sys.path.insert(1, pythondir)
    
    # Set environment variables for resource paths
    if 'CTHULHU_DATA_DIR' not in os.environ:
        os.environ['CTHULHU_DATA_DIR'] = datadir

# Set up paths before importing Cthulhu modules
setup_paths()

from cthulhu import debug
from cthulhu import messages
from cthulhu import settings
from cthulhu.ax_object import AXObject
from cthulhu.ax_utilities import AXUtilities
from cthulhu.cthulhu_platform import version, revision

class ListApps(argparse.Action):
    def __call__(self, parser, namespace, values, option_string=None):
        desktop = AXUtilities.get_desktop()
        for app in AXObject.iter_children(desktop):
            pid = AXObject.get_process_id(app)
            try:
                name = Atspi.Accessible.get_name(app) or "(none)"
            except Exception:
                name = "[DEAD]"

            try:
                cmdline = subprocess.getoutput('cat /proc/%s/cmdline' % pid)
            except Exception:
                cmdline = '(exception encountered)'
            else:
                cmdline = cmdline.replace('\x00', ' ')

            print(time.strftime('%H:%M:%S', time.localtime()),
                  '  pid: %5s   %-25s  %s' % (pid, name, cmdline))

        parser.exit()

class PrintVersion(argparse.Action):
    """Action to print the version of Cthulhu."""

    def __call__(self, parser, namespace, values, option_string=None):
        msg = version
        if revision:
            msg += f" (rev {revision})"

        atspi_version = Atspi.get_version()
        msg += f", AT-SPI2 version: {atspi_version[0]}.{atspi_version[1]}.{atspi_version[2]}"

        session_type = os.environ.get("XDG_SESSION_TYPE") or ""
        session_desktop = os.environ.get("XDG_SESSION_DESKTOP") or ""
        session = f"{session_type} {session_desktop}".strip()
        if session:
            msg += f", Session: {session}"

        print(msg)
        parser.exit()

class Settings(argparse.Action):
    def __call__(self, parser, namespace, values, option_string=None):
        settingsDict = getattr(namespace, 'settings', {})
        invalid = getattr(namespace, 'invalid', [])
        for value in values.split(','):
            item = str.title(value).replace('-', '')
            try:
                test = 'enable%s' % item
                eval('settings.%s' % test)
            except AttributeError:
                try:
                    test = 'show%s' % item
                    eval('settings.%s' % test)
                except AttributeError:
                    invalid.append(value)
                    continue
            settingsDict[test] = self.const
        setattr(namespace, 'settings', settingsDict)
        setattr(namespace, 'invalid', invalid)

class HelpFormatter(argparse.HelpFormatter):
    def __init__(self, prog, indent_increment=2, max_help_position=32,
                 width=None):

        super().__init__(prog, indent_increment, max_help_position, width)

    def add_usage(self, usage, actions, groups, prefix=None):
        super().add_usage(usage, actions, groups, messages.CLI_USAGE)

class Parser(argparse.ArgumentParser):
    def __init__(self, *args, **kwargs):
        super(Parser, self).__init__(
            epilog=messages.CLI_EPILOG, formatter_class=HelpFormatter, add_help=False)
        self.add_argument(
            "-h", "--help", action="help", help=messages.CLI_HELP)
        self.add_argument(
            "-v", "--version", action=PrintVersion, nargs=0, help=messages.CLI_VERSION)
        self.add_argument(
            "-r", "--replace", action="store_true", help=messages.CLI_REPLACE)
        self.add_argument(
            "-s", "--setup", action="store_true", help=messages.CLI_GUI_SETUP)
        self.add_argument(
            "-l", "--list-apps", action=ListApps, nargs=0,
            help=messages.CLI_LIST_APPS)
        self.add_argument(
            "-e", "--enable", action=Settings, const=True,
            help=messages.CLI_ENABLE_OPTION, metavar=messages.CLI_OPTION)
        self.add_argument(
            "-d", "--disable", action=Settings, const=False,
            help=messages.CLI_DISABLE_OPTION, metavar=messages.CLI_OPTION)
        self.add_argument(
            "-p", "--profile", action="store",
            help=messages.CLI_LOAD_PROFILE, metavar=messages.CLI_PROFILE_NAME)
        self.add_argument(
            "-u", "--user-prefs", action="store",
            help=messages.CLI_LOAD_PREFS, metavar=messages.CLI_PREFS_DIR)
        self.add_argument(
            "--debug-file", action="store",
            help=messages.CLI_DEBUG_FILE, metavar=messages.CLI_DEBUG_FILE_NAME)
        self.add_argument(
            "--debug", action="store_true", help=messages.CLI_ENABLE_DEBUG)

        self._optionals.title = messages.CLI_OPTIONAL_ARGUMENTS

    def parse_known_args(self, *args, **kwargs):
        opts, invalid = super(Parser, self).parse_known_args(*args, **kwargs)
        try:
            invalid.extend(opts.invalid)
        except Exception:
            pass
        if invalid:
            print((messages.CLI_INVALID_OPTIONS + " ".join(invalid)))

        if opts.debug_file:
            opts.debug = True
        elif opts.debug:
            opts.debug_file = time.strftime('debug-%Y-%m-%d-%H:%M:%S.out')

        return opts, invalid

def setProcessName(name):
    """Attempts to set the process name to the specified name."""

    sys.argv[0] = name

    try:
        from setproctitle import setproctitle
    except ImportError:
        pass
    else:
        setproctitle(name)
        return True

    try:
        from ctypes import cdll, byref, create_string_buffer
        libc = cdll.LoadLibrary('libc.so.6')
        stringBuffer = create_string_buffer(len(name) + 1)
        stringBuffer.value = bytes(name, 'UTF-8')
        libc.prctl(15, byref(stringBuffer), 0, 0, 0)
        return True
    except Exception:
        pass

    return False

def inGraphicalDesktop():
    """Returns True if we are in a graphical desktop."""

    # TODO - JD: Make this desktop environment agnostic
    try:
        import gi
        gi.require_version("Gdk", "3.0")
        from gi.repository import Gdk
        display = Gdk.Display.get_default()
    except Exception:
        return False

    return display is not None

def getSessionType():
    sessionType = (os.environ.get("XDG_SESSION_TYPE") or "").strip().lower()
    if sessionType:
        return sessionType
    if os.environ.get("WAYLAND_DISPLAY"):
        return "wayland"
    if os.environ.get("DISPLAY"):
        return "x11"
    return "unknown"

def getXServerVendor():
    display = os.environ.get("DISPLAY")
    if not display:
        return None

    xdpyinfoPath = shutil.which("xdpyinfo")
    if not xdpyinfoPath:
        return None

    try:
        result = subprocess.run(
            [xdpyinfoPath, "-display", display],
            check=True,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True,
            timeout=1,
        )
    except Exception:
        return None

    for line in result.stdout.splitlines():
        if "vendor string:" in line:
            return line.split("vendor string:", 1)[1].strip()
    return None

def otherCthulhus():
    """Returns the pid of any other instances of Cthulhu owned by this user."""

    openFile = subprocess.Popen('pgrep -u %s -x cthulhu' % os.getuid(),
                                shell=True,
                                stdout=subprocess.PIPE).stdout
    pids = openFile.read()
    openFile.close()
    cthulhus = [int(p) for p in pids.split()]

    pid = os.getpid()
    return [p for p in cthulhus if p != pid]

def cleanup(sigval):
    """Tries to clean up any other running Cthulhu instances owned by this user."""

    cthulhusToKill = otherCthulhus()
    debug.printMessage(debug.LEVEL_INFO, "INFO: Cleaning up these PIDs: %s" % cthulhusToKill)

    def onTimeout(signum, frame):
        cthulhusToKill = otherCthulhus()
        debug.printMessage(debug.LEVEL_INFO, "INFO: Timeout cleaning up: %s" % cthulhusToKill)
        for pid in cthulhusToKill:
            os.kill(pid, signal.SIGKILL)

    for pid in cthulhusToKill:
        os.kill(pid, sigval)
    signal.signal(signal.SIGALRM, onTimeout)
    signal.alarm(2)
    while otherCthulhus():
        time.sleep(0.5)

def main():
    setProcessName('cthulhu')

    parser = Parser()
    args, invalid = parser.parse_known_args()

    if args.debug:
        debug.debugLevel = debug.LEVEL_ALL
        debug.eventDebugLevel = debug.LEVEL_OFF
        debug.debugFile = open(args.debug_file, 'w')

    if args.replace:
        cleanup(signal.SIGKILL)

    settingsDict = getattr(args, 'settings', {})

    if not inGraphicalDesktop():
        print(messages.CLI_NO_DESKTOP_ERROR)
        return 1

    sessionType = getSessionType()
    sessionDetails = []
    xdgSessionType = os.environ.get("XDG_SESSION_TYPE")
    if xdgSessionType:
        sessionDetails.append(f"XDG_SESSION_TYPE={xdgSessionType}")
    if sessionType == "wayland":
        waylandDisplay = os.environ.get("WAYLAND_DISPLAY")
        if waylandDisplay:
            sessionDetails.append(f"WAYLAND_DISPLAY={waylandDisplay}")
    elif sessionType == "x11":
        display = os.environ.get("DISPLAY")
        if display:
            sessionDetails.append(f"DISPLAY={display}")
        vendor = getXServerVendor()
        if vendor:
            sessionDetails.append(f"X server vendor={vendor}")

    if sessionDetails:
        msg = f"INFO: Session: {sessionType} ({', '.join(sessionDetails)})"
    else:
        msg = f"INFO: Session: {sessionType}"
    debug.printMessage(debug.LEVEL_INFO, msg, True)

    debug.printMessage(debug.LEVEL_INFO, "INFO: Preparing to launch.", True)

    from cthulhu import cthulhu
    manager = cthulhu.cthulhuApp.settingsManager

    if not manager:
        print(messages.CLI_SETTINGS_MANAGER_ERROR)
        return 1

    debug.printMessage(debug.LEVEL_INFO, "INFO: About to activate settings manager.", True)
    manager.activate(args.user_prefs, settingsDict)
    sys.path.insert(0, manager.getPrefsDir())

    if args.profile:
        try:
            manager.setProfile(args.profile)
        except Exception:
            print(messages.CLI_LOAD_PROFILE_ERROR % args.profile)
            manager.setProfile()

    if args.setup:
        cleanup(signal.SIGKILL)
        cthulhu.showPreferencesGUI()

    if otherCthulhus():
        print(messages.CLI_OTHER_CTHULHUS_ERROR)
        return 1

    debug.printMessage(debug.LEVEL_INFO, "INFO: About to launch Cthulhu.", True)
    return cthulhu.main()

if __name__ == "__main__":
    sys.exit(main())
