#!/usr/bin/python3

# Parse the command line, figure out program settings and then launch the menu.

import os
import os.path
import argparse
import time

from strings import _tr

program_start = time.perf_counter()

# Accepted languages. These are permitted for the --lang parameter and
# environment variables, and we try to load strings from YAML/JSON/
# .desktop files for each of these.
LANGUAGES = frozenset(("en", "fi", "sv", "de"))

# Some kind of a help text
epilog = """
Development versus production mode
----------------------------------
PuavoMenu, by default, assumes it's running in development mode. To override
this, use the --prod command line switch. When running in production mode, you
also need to specify the log file with --log (console logging is only available
in development mode, but file logging can be used in development mode if
desired), and --autohide to actually make the menu window hide itself when it
loses focus. --autohide is never enabled by default, because it considerably
hinders development.

Socket names and IPC commands
-----------------------------
PuavoMenu uses a Unix domain socket for IPC. You can use it to send simple
commands to it. See send_command for more information. The main PuavoMenu
instance uses a socket file XDG_RUNTIME_DIR/puavomenu; if you're running other
copies of PuavoMenu, you *need* to use other socket names, so the commands
you send to your copies do not interfere with the system menu.

Known IPC commands are:

    show <pos>: Show the menu
    hide: Hide the menu
    toggle <pos>: Show or hide the menu, depending on its current visibility
    update-puavopkg: Reloads puavo-pkg program install states
    reload-menudata: Completely reloads the menudata
    reload-userprogs: Reloads the contents of the user programs directory

'show' and 'toggle' also accept a position parameter, which tells the
menu where the window should be shown at. Known positions are:

    center: Center the window around the mouse cursor
    corner <X> <Y>: Position the lower left corner at (X, Y)

Examples on positioning:

    show corner 115 68: This shows the window and moves it so that the
                        lower left corner is located at (115, 68).
    show corner: Shows the window at whatever position it happens to
                 be at.
    show center: Shows the window and centers it around the mouse cursor.

Language codes
--------------
PuavoMenu supports multiple languages. The language code is simply
a two-letter code, like "fi", or "en". You can use --lang=<code> to
specify which language you want to use. If --lang is omitted, PuavoMenu
looks up LANG and GDM_LANG environment variables (in that order) and
takes the first two letters form its value, lowercases them, and then
compares them to the list of known languages. If the environment
variable(s) are missing, or the code is invalid, the language defaults
to English (en). Accepted language codes are:
    $LANGS

User programs
-------------
By default, PuavoMenu checks the presence of ~/.local/share/applications
and ~/.local/share/flatpak directories. If they exist, PuavoMenu recursively
scans them for .desktop files, and adds everything it finds to a custom
category for user programs.

You can use --user_progs argument to specify an additional direcfory that
should be scanned for.
"""

epilog = epilog.replace("$LANGS", ", ".join(LANGUAGES))

# ------------------------------------------------------------------------------
# Parse the command-line arguments

parser = argparse.ArgumentParser(
    formatter_class=argparse.RawDescriptionHelpFormatter, epilog=epilog
)

parser.add_argument(
    "--prod",
    action="store_true",
    required=False,
    help="enable production mode; by default PuavoMenu is always in development mode",
)

parser.add_argument(
    "--log",
    type=str,
    required=False,
    help="log file name (logs to console if omitted, required in production mode)",
)

parser.add_argument(
    "--autohide",
    action="store_true",
    required=False,
    help="enable window autohide (not automatically implied in production mode!)",
)

parser.add_argument(
    "--lang",
    type=str,
    required=False,
    default=None,  # force autodetection
    help="language code (see documentation)",
)

required = parser.add_argument_group("required arguments")

required.add_argument(
    "--res_dir", required=True, type=str, help="location of PuavoMenu's own resources"
)

required.add_argument(
    "--menu_dir", required=True, type=str, help="location of the menu data"
)

required.add_argument(
    "--user_conf",
    required=True,
    type=str,
    help="location of user-specific configuration",
)

required.add_argument(
    "--user_progs",
    required=False,
    type=str,
    help="directory for additional user-defined programs",
)

required.add_argument("--socket", required=True, help="IPC command socket name")

args = parser.parse_args()

# ------------------------------------------------------------------------------
# Setup logging

if args.prod and not args.log:
    print("\nERROR: You must use the --log switch in production mode!")
    exit(1)

import logging

if args.log:
    # log to file
    logging.basicConfig(
        filename=args.log,
        format="%(asctime)s %(levelname)s: %(message)s",
        level=logging.DEBUG,
    )

    # don't let others see what you're doing
    try:
        os.chmod(args.log, 0o600)
    except:
        pass
else:
    # log to console
    logging.basicConfig(
        format="%(asctime)s %(levelname)s: %(message)s", level=logging.DEBUG
    )

    # colored warnings and errors
    logging.addLevelName(
        logging.WARNING, "\033[1;93m%s\033[0m" % logging.getLevelName(logging.WARNING)
    )
    logging.addLevelName(
        logging.ERROR, "\033[1;91m%s\033[0m" % logging.getLevelName(logging.ERROR)
    )
    logging.addLevelName(
        logging.CRITICAL,
        "\033[1;93;101m%s\033[0m" % logging.getLevelName(logging.CRITICAL),
    )

# ------------------------------------------------------------------------------
# Create settings and detect the environment

import settings

settings = settings.Settings()

settings.prod_mode = args.prod
settings.autohide = args.autohide
settings.res_dir = os.path.join(args.res_dir, "")
settings.menu_dir = os.path.join(args.menu_dir, "")
settings.user_conf = os.path.join(args.user_conf, "")
settings.socket = args.socket

logging.info("Logging starts")
logging.info("This is PuavoMenu v1.0 (c) Opinsys Oy 2017-2025")

# Detect the non-configurable settings
settings.detect_environment()

if args.lang:
    # Override autodetection
    settings.language = args.lang.lower()
else:
    # Autodetect from system
    if "LANG" in os.environ:
        settings.language = os.environ["LANG"][0:2].lower()
    elif "GDM_LANG" in os.environ:
        settings.language = os.environ["GDM_LANG"][0:2].lower()

# Regardless of where the language code comes from we validate it
if settings.language not in LANGUAGES:
    logging.error('Invalid language code "%s", defaulting to "en"', settings.language)
    settings.language = "en"

_tr.set_language(settings.language)

# Additional directories for user programs, if the session allows them
if not (settings.is_guest or settings.is_webkiosk):
    home = os.path.expanduser('~')

    # Autodetect the common places
    try_paths = [
        os.path.join(home, '.local', 'share', 'applications'),
        os.path.join(home, '.local', 'share', 'flatpak')
    ]

    if args.user_progs:
        try_paths.append(args.user_progs)

    for path in try_paths:
        if os.path.isdir(path) and os.access(path, os.R_OK):
            settings.user_programs_dir.append(path)

logging.info('Language...........: "%s"', settings.language)

if args.log:
    logging.info('Log file...........: "%s"', args.log)

logging.info('Resources..........: "%s"', settings.res_dir)
logging.info('Menu data..........: "%s"', settings.menu_dir)
logging.info('User config........: "%s"', settings.user_conf)

if settings.user_programs_dir:
    logging.info('User programs......: %s', ' '.join(settings.user_programs_dir))

logging.info('IPC socket.........: "%s"', settings.socket)
logging.info('Desktop directory..: "%s"', settings.desktop_dir)

logging.info('User roles..........: %s', ' '.join(settings.user_roles))

if not settings.prod_mode:
    logging.info("Running in development mode")

if settings.autohide:
    logging.info("Window autohide enabled")

if settings.is_personally_administered and settings.is_user_primary_user:
    logging.info("Device primary user using their personally administered device")

# ------------------------------------------------------------------------------
# Set up a domain socket

# This is done early, because if it fails, we simply exit.
# We can't do *anything* without it.

import syslog
import socket

sock = None

try:
    # Clean leftovers
    os.unlink(settings.socket)
except OSError:
    pass

try:
    # A domain socket in stream mode!
    sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sock.bind(settings.socket)
    sock.listen(10)
except Exception as exception:
    # Oh dear...
    logging.error("Unable to create the domain socket for IPC!")
    logging.error("Reason: %s", str(exception))
    logging.error("This is a fatal error, stopping here.")

    syslog.syslog(
        syslog.LOG_CRIT,
        'PuavoMenu IPC socket "{0}" creation failed: {1}'.format(
            settings.socket, str(exception)
        ),
    )

    syslog.syslog(syslog.LOG_CRIT, "PuavoMenu stops here. Contact Opinsys support.")

    logging.info("Logging ends")
    logging.shutdown()
    exit(1)

# ------------------------------------------------------------------------------
# And go!

import traceback
import utils

logging.info("=" * 50)

exitstatus = 0

try:
    import_start = time.perf_counter()
    import main

    import_end = time.perf_counter()

    utils.log_elapsed_time("Main import time", import_start, import_end)
    status = main.run_puavomenu(settings, sock, program_start)
    if status.pop("has_resolution_changed", False):
        exitstatus = 2
    elif "error" in status:
        exitstatus = 1
except Exception as exception:
    # This is bad. Very bad. The exception bubbled all the way up to here,
    # so this is fatal.
    exitstatus = 1
    logging.fatal(str(exception), exc_info=True)

    if settings.prod_mode:
        # Log fatal exceptions to to syslog
        syslog.syslog(syslog.LOG_CRIT, "Top-level exception caught in puavomenu!")
        syslog.syslog(syslog.LOG_CRIT, traceback.format_exc())

logging.info("=" * 50)

# ------------------------------------------------------------------------------
# Clean exit

logging.info("Shutdown complete")
logging.info("Logging ends")
logging.shutdown()

exit(exitstatus)
