#
##
##  SPDX-FileCopyrightText: © 2007-2023 Benedict Verhegghe <bverheg@gmail.com>
##  SPDX-License-Identifier: GPL-3.0-or-later
##
##  This file is part of pyFormex 3.4  (Thu Nov 16 18:07:39 CET 2023)
##  pyFormex is a tool for generating, manipulating and transforming 3D
##  geometrical models by sequences of mathematical operations.
##  Home page: https://pyformex.org
##  Project page: https://savannah.nongnu.org/projects/pyformex/
##  Development: https://gitlab.com/bverheg/pyformex
##  Distributed under the GNU General Public License version 3 or later.
##
##  This program is free software: you can redistribute it and/or modify
##  it under the terms of the GNU General Public License as published by
##  the Free Software Foundation, either version 3 of the License, or
##  (at your option) any later version.
##
##  This program 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 General Public License for more details.
##
##  You should have received a copy of the GNU General Public License
##  along with this program.  If not, see http://www.gnu.org/licenses/.
##
# This is the only pyFormex module that is imported by the main script,
# so this is the place to put startup code
"""pyFormex main module
This module contains the main function of pyFormex, which is run by the
startup script.
"""
import sys
import os
import warnings
from inspect import cleandoc as dedent
from contextlib import redirect_stdout, redirect_stderr
import pyformex as pf
[docs]def whereami():
    """Report where pyFormex is installed"""
    return dedent(
        f"""{pf.fullVersion()}
        pyFormex executable: {pf.executable}
        pyFormex installation ({pf.installtype}): {pf.pyformexdir}
        Python sys.path: {sys.path}
        """) 
[docs]def run_docmodule(module):
    """Print autogenerated documentation for the module.
    module is a pyFormex module dotted path. The leading pyformex.
    may be omitted.
    """
    from . import py2rst
    out = py2rst.do_module(module)
    refdir = pf.cfg['sphinxdir'] / 'ref'
    if refdir.exists():
        outfile = refdir / module + '.rst'
        outfile.write_text(out)
        print(f"Wrote {outfile}")
    else:
        raise RuntimeError(
            f"The ref directory {refdir} does not exist") 
[docs]def loadUserConfig():  # noqa: C901
    """Load the pyFormex configuration
    Notes
    -----
    This function should be called to create a proper configuration
    when pyFormex is imported in Python and not started from the
    pyformex command.
    """
    pf.logger.info("Loading configuration files")
    from pyformex import Path
    from pyformex.config import Config
    # Set the config files
    if pf.options.nodefaultconfig:
        sysprefs = []
        userprefs = []
    else:
        sysprefs = [pf.cfg['siteprefs']]
        userprefs = [pf.cfg['userprefs']]
        if pf.cfg['localprefs'].exists():
            userprefs.append(pf.cfg['localprefs'])
    sysprefs = [f for f in sysprefs if f.exists()]
    userprefs = [f for f in userprefs if f.exists()]
    if pf.options.config:
        userprefs.append(Path(pf.options.config).expanduser())
    if len(userprefs) == 0:
        # We should always have a place to store the user preferences
        userprefs = [pf.cfg['userprefs']]
    # Use last one to save preferences
    pf.debug(f"System Preference Files: {sysprefs}", pf.DEBUG.CONFIG)
    pf.debug(f"User Preference Files: {userprefs}", pf.DEBUG.CONFIG)
    pf.preffile = Path.resolve(userprefs.pop())
    # Read sysprefs as reference
    for f in sysprefs:
        pf.debug(f"Reading config file {f}", pf.DEBUG.CONFIG)
        pf.cfg.load(f)
    # Set this as reference config
    pf.refcfg = pf.cfg
    pf.debug("=" * 60, pf.DEBUG.CONFIG)
    pf.debug(f"RefConfig: {pf.refcfg}", pf.DEBUG.CONFIG)
    pf.cfg = Config(default=pf.refcfg)
    # Read userprefs as reference
    for f in userprefs:
        if f.exists():
            pf.debug(f"Reading config file {f}", pf.DEBUG.CONFIG)
            pf.cfg.load(f)
        else:
            pf.debug(f"Skip non-existing config file {f}", pf.DEBUG.CONFIG)
    if pf.preffile.exists():
        pf.debug(f"Reading config file {pf.preffile}", pf.DEBUG.CONFIG)
        pf.cfg.load(pf.preffile)
    else:
        # Create the config file
        pf.debug(f"Creating config file {pf.preffile}", pf.DEBUG.CONFIG)
        try:
            Path.mkdir(pf.preffile.parent, parents=True, exist_ok=True)
            pf.preffile.touch()
        except Exception:
            pf.startup_warnings.append(
                f"Could not create the user configuration file {pf.preffile}.\n"
                "User preferences will not be saved.\n")
            pf.preffile = None
    # Set this as preferences config
    pf.prefcfg = pf.cfg
    pf.debug("=" * 60, pf.DEBUG.CONFIG)
    pf.debug(f"Config: {pf.prefcfg}", pf.DEBUG.CONFIG)
    pf.cfg = Config(default=pf.prefcfg)
    # Fix incompatible changes in configuration
    _sanitize_config(pf.prefcfg)
    # Make sure we have a writeable tmpdir
    if not pf.cfg['tmpdir'].is_writable_dir():
        tmpdir = Path(os.environ.get('TMPDIR', pf.cfg['homedir']))
        if tmpdir.is_writable_dir():
            tmpdir = tmpdir / 'pyformex_tmp'
            tmpdir.mkdir(parents=True, exist_ok=False)
            if tmpdir.is_writable_dir():
                pf.cfg['tmpdir'] = tmpdir
                return
        raise ValueError("""\
I could not find a writable path for temporary files.
You can specify one with the --tmpdir option of the pyformex command.
""") 
def _sanitize_warnings_filters(c):
    """Sanitize the 'warnings/filters' setting.
    The setting should be a set (to avoid doubles) and not contain
    DeprecationWarnings.
    Returns the corrected setting, possibly None.
    """
    if isinstance(c, set):
        # Accept as is
        pass
    elif isinstance(c, list):
        # Remove the obsolete filters
        c = set(f for f in c if isinstance(f, tuple) and
                not (len(f) > 2 and f[2] == 'D'))
    else:
        # Remove the setting
        c = None
    return c
def _sanitize_config(cfg):
    """Apply incompatible changes in the configuration
    cfg is the user configuration that is to be saved.
    """
    from pyformex import Path
    # Safety checks
    # Warning filters
    if 'warnings' in cfg and 'filters' in cfg['warnings']:
        # We need to check if the cfg really owns the setting
        pf.debug('Sanitizing settings', pf.DEBUG.CONFIG)
        c = _sanitize_warnings_filters(cfg['warnings/filters'])
        cfg['warnings/filters'] = c
    # Path required
    for p in ('workdir', ):
        if p in cfg and not isinstance(cfg[p], Path):
            cfg[p] = Path(p)
    # Adhoc changes
    if cfg['gui/plugins'] is not None:
        cfg['gui/pluginmenus'] = [
            p.split('_')[0].capitalize() for p in cfg['gui/plugins']]
        del cfg['gui/plugins']
    if not isinstance(cfg.get('gui/redirect', ''), str):
        del cfg['gui/redirect']
    if isinstance(cfg['gui/dynazoom'], str):
        cfg['gui/dynazoom'] = [cfg['gui/dynazoom'], '']
    for i in range(8):
        t = f"render/light{i}"
        try:
            cfg[t] = dict(cfg[t])
        except Exception:
            pass
    for d in ['scriptdirs', 'appdirs']:
        if d in cfg:
            scriptdirs = []
            for i in cfg[d]:
                if i[1] == '' or Path(i[1]).is_dir():
                    scriptdirs.append(tuple(i))
                elif i[0] == '' or Path(i[0]).is_dir():
                    scriptdirs.append((i[1], i[0]))
            cfg[d] = scriptdirs
    # Rename settings
    for old, new in (
            ('history', 'gui/scripthistory'),
            ('gui/history', 'gui/scripthistory'),
            ('raiseapploadexc', 'showapploaderrors'),
            ('webgl/xtkscript', 'webgl/script'),
    ):
        if old in cfg:
            if new not in cfg:
                cfg[new] = cfg[old]
            del cfg[old]
    # Delete settings
    for key in (
            'scriptmode',
            'fonts/ignore',
            'gui/console',
            'gui/interpreter',
            'gui/bdsize',
            'input/timeout',
            'filterwarnings',
            'render/ambient',
            'render/diffuse',
            'render/specular',
            'render/emission',
            'render/material',
            'canvas/propcolors',
            'Save changes',
            'canvas/bgmode',
            'canvas/bgcolor2',
            '_save_',
    ):
        if key in cfg:
            print(f"DELETING CONFIG VARIABLE {key}")
            del cfg[key]
[docs]def savePreferences():
    """Save the preferences.
    The name of the preferences file is determined at startup from
    the configuration files, and saved in ``pyformex.preffile``.
    If a local preferences file was read, it will be saved there.
    Otherwise, it will be saved as the user preferences, possibly
    creating that file.
    If ``pyformex.preffile`` is None, preferences are not saved.
    """
    pf.debug(f"savePreferences to: {pf.preffile}", pf.DEBUG.CONFIG)
    if pf.preffile is None:
        return
    # Create the user conf dir
    pf.preffile.parent.mkdir(parents=True, exist_ok=True)
    # Do not store the refcfg warning filters: we add them on startup
    a = set(pf.prefcfg['warnings/filters'])
    b = set(pf.refcfg['warnings/filters'])
    pf.prefcfg['warnings/filters'] = a-b
    # Currently erroroneously processed, therefore not saved
    del pf.prefcfg['render/light0']
    del pf.prefcfg['render/light1']
    del pf.prefcfg['render/light2']
    del pf.prefcfg['render/light3']
    pf.debug("=" * 60, pf.DEBUG.CONFIG)
    pf.debug(f"!!!Saving config:\n{pf.prefcfg}", pf.DEBUG.CONFIG)
    try:
        pf.debug(f"Saving preferences to file {pf.preffile}", pf.DEBUG.CONFIG)
        pf.prefcfg.write(pf.preffile)
        res = "Saved"
    except Exception:
        res = "Could not save"
        raise
    pf.debug(f"{res} preferences to file {pf.preffile}", pf.DEBUG.CONFIG)
    return res == "Saved" 
[docs]def activateWarningFilters():
    """Activate the warning filters
    First time activation of the warning filters and customized
    warning formatting.
    """
    from pyformex import utils
    utils.resetWarningFilters()
    def _format_warning(message, category, filename, lineno, line=None):
        """Replace the default warnings.formatwarning
        This allows the warnings being called using a simple mnemonic
        string. The full message is then found from the message module.
        """
        from pyformex import messages
        message = messages.getMessage(message)
        message = f"""..
pyFormex Warning
================
{message}
{category.__name__} called from: {filename} line: {lineno}
"""
        if line:
            message += f"{line}\n"
        return message
    if pf.cfg['warnings/nice']:
        warnings.formatwarning = _format_warning 
[docs]def override_config(option, setting, delete=True):
    """Override a config setting with a command line option
    Parameters
    ----------
    option: str
        Name of the option that should override a setting.
    setting: str
        Key of the setting to be overridden.
    delete: bool
        If True (Default), delete the option after use
    Notes
    -----
    If the option was given, its value is written into the corresponding
    setting and the option is deleted.
    """
    value = getattr(pf.options, option)
    if value is not None and value != pf.cfg[setting]:
        pf.debug(f"Override config value '{setting}={pf.cfg[setting]}"
                 f" with value '{value}'", pf.DEBUG.CONFIG)
        pf.cfg[setting] = value
    if delete:
        delattr(pf.options, option)  # avoid abuse 
###########################  main  ################################
[docs]def run(args=[]):  # noqa: C901
    """The pyFormex main function.
    After pyFormex launcher script has correctly set up the Python import
    paths, this function is executed. It is responsible for reading the
    configuration file(s), processing the command line options and starting
    the application.
    The basic configuration file is 'pyformexrc' located in the pyFormex
    main directory. It should always be present and be left unchanged.
    If you want to make changes, copy (parts of) this file to another location
    where you can change them. Then make sure pyFormex reads you modifications
    file. By default, pyFormex will try to read the following
    configuration files if they are present (and in this order)::
        default settings:     <pyformexdir>/pyformexrc   (always loaded)
        system-wide settings: /etc/pyformex.conf
        user settings:        <configdir>/pyformex/pyformex.conf
        local settings        $PWD/.pyformexrc
    Also, an extra config file can be specified in the command line, using
    the --config option. The system-wide and user settings can be skipped
    by using the --nodefaultconfig option.
    Config files are loaded in the above order. Settings always override
    those loaded from a previous file.
    When pyFormex exits, the preferences that were changed are written to the
    last read config file. Changed settings are those that differ from the
    settings obtained after loading all but the last config file.
    If none of the optional config files exists, a new user settings file
    will be created, and an error will occur if the <configdir> path is
    not writable.
    """
    if isinstance(args, (str, bytes)):
        args = args.split()
    elif not isinstance(args, list):
        args = sys.argv[1:]
    # if not pf.executable.stem == 'pyformex':
    #     # We did not do options parsing yet: do it now
    #     if not parseOptions(args):
    #         return 1
    # Parse the command line options
    from pyformex.options import parseOptions
    parseOptions(args)
    ## Process options that do not need/want config ##
    if pf.options.help:
        pf.parser.print_help()
        return 0
    if pf.options.usage:
        pf.parser.print_usage()
        return 0
    if pf.options.version:
        print(pf.fullVersion())
        return 0
    if pf.options.docmodule is not None:
        pf.sphinx = True
        for a in pf.options.docmodule:
            run_docmodule(a)
        # This is an all_exclusive option !!
        # So we immediately return
        return 0
    if pf.options.experimental:
        sys.path.insert(1, str(pf.pyformexdir / 'experimental'))
    ## Process special options that can be combined, but return
    ## if any is used
    ret = False
    if pf.options.whereami:
        print(whereami())
        ret = True
    if pf.options.debugitems:
        print([i.name for i in pf.DEBUG])
        ret = True
    if pf.options.detect:
        from pyformex import software
        print(whereami())
        override_config('bindings', 'gui/bindings')
        from pyformex import gui  # noqa: F401 (to get correct bindings)
        print("Detecting installed helper software")
        print(software.reportSoftware())
        ret = True
    if pf.options.listmodules is not None:
        from pyformex.cmdtools import list_modules
        print('\n'.join(list_modules(pf.options.listmodules)))
        ret = True
    if pf.options.pytest is not None:
        from pyformex.cmdtools import run_pytest
        run_pytest(pf.options.pytest)
        ret = True
    if pf.options.doctest is not None:
        from pyformex.cmdtools import run_doctest
        run_doctest(pf.options.doctest)
        ret = True
    if pf.options.remove:
        from pyformex.cmdtools import remove_pyFormex
        print(whereami())
        remove_pyFormex(pf.pyformexdir, pf.executable)
        # After this option, we can not continue,
        # so this should be the last option processed
        ret = True
    if ret:
        # Return for special options
        return 0
    ## Migrate the user configuration files ##
    # TODO: this could become a command tool??
    # migrateUserConfig()
    ## Load the user configuration ##
    loadUserConfig()
    ## Process special options which do not start pyFormex
    ## but depend on the user configuration
    if pf.options.search or pf.options.listfiles:
        from pyformex.utils import sourceFiles
        args = pf.options.args
        extended = False
        if len(args) > 0:
            opts = [a for a in args if a.startswith('-')]
            args = [a for a in args if a not in opts]
            if '-a' in opts:
                opts.remove('-a')
                extended = True
        if pf.options.search:
            search = args.pop(0)
        if len(args) > 0:
            files = args
        else:
            files = sourceFiles(relative=True, extended=extended)
        if pf.options.listfiles:
            print('\n'.join(files))
        else:
            if "'" in search:
                search.replace("'", "\'")
            print(f"SEARCH = [{search}]", file=sys.stderr)
            options = ' '.join(opts)
            quotedfiles = ' '.join([f"'{f}'" for f in files])
            cmd = f'grep {options} "{search}" {quotedfiles}'
            os.system(cmd)
        return 0
    ## If we get here, we want to start pyFormex
    ## Process options that override the config ##
    from pyformex import software, utils
    # Config settings that are overridden by the matching option
    for option, setting in (
            ('bindings', 'gui/bindings'),
            ('redirect', 'gui/redirect'),
            ('opengl', 'opengl/version'),
    ):
        override_config(option, setting)
    utils.setSaneLocale()
    ## Check required modules ##
    software.Module.has('numpy', fatal=True)
    # minimal supported numpy version
    software.Module.require('numpy', pf._numpy_version)
    import numpy
    numpy.set_printoptions(**pf.cfg['numpy/printoptions'])
    # with gui:
    # Minimal pyside2/pyqt5 is 5.11; recommended is 5.15
    # Initialize the libraries
    if pf.options.uselib is None:
        pf.options.uselib = pf.cfg['uselib']
    # Force initialisation of the library
    from pyformex import lib  # noqa: F401
    ## Activate the warning filters
    activateWarningFilters()
    # Make sure pf.PF is a Project
    from pyformex.project import Project
    pf.PF = Project()
    # Add a configured syspath
    if pf.cfg['syspath']:
        sys.path.extend(pf.cfg['syspath'])
    # Set application paths
    pf.debug("Loading AppDirs", pf.DEBUG.INFO)
    from pyformex import apps
    apps.setAppDirs()
    args = pf.options.args
    if pf.options.gui is None:
        # Set default value for options.gui:
        #   False if -c script option used or
        #   first remaining argument is a pyFormex script.
        pf.options.gui = not (pf.options.script or (
            len(args) > 0 and utils.is_pyFormex(args[0])))
    # Create the interpreter
    import code
    from pyformex import script
    pf.interpreter = code.InteractiveInterpreter(script.Globals())
    stdout = sys.stdout
    stderr = sys.stderr
    # Start the GUI if needed (should be done after the config is set)
    if pf.options.gui:
        if pf.options.mesa:
            os.environ['LIBGL_ALWAYS_SOFTWARE'] = '1'
        from pyformex.gui import guimain
        res = guimain.createGUI()
        if res != 0:
            print(f"Could not start the pyFormex GUI: {res}")
            return res  # EXIT
        if pf.GUI.console:
            # we can redirect output to the console
            if 'o' in pf.cfg['gui/redirect']:
                stdout = pf.GUI.console
            if 'e' in pf.cfg['gui/redirect']:
                stderr = pf.GUI.console.errorproxy
    sys.stdout.flush()
    sys.stderr.flush()
    with redirect_stdout(stdout), redirect_stderr(stderr):
        # Display the startup warnings
        for msg in pf.startup_warnings:
            utils.warn(msg)
        if pf.DEBUG.DETECT in pf.options.debuglevel:
            # NOTE: inside an if to avoid computing the report when not printed
            pf.debug(software.reportSoftware(), pf.DEBUG.DETECT)
        #
        # Qt may have changed the locale.
        # Since a LC_NUMERIC setting other than C may cause lots of troubles
        # with reading and writing files (formats become incompatible!)
        # we put it back to a sane setting
        #
        utils.setSaneLocale()
        # Startup done
        pf.started = True
        pf.verbose(2, f"pyFormex started from {pf.executable}")
        # Prepend the inline script
        if pf.options.script:
            args[0:0] = ['-c', pf.options.script]
        # Prepend the autorun script
        ar = pf.cfg['autorun']
        if ar and ar.exists():
            args[0:0] = [ar]
        # remaining args are interpreted as scripts/apps and their parameters
        res = 0
        if args:
            pf.debug(f"Remaining args: {args}", pf.DEBUG.INFO)
            from pyformex.script import processArgs
            res = processArgs(args)
            if res:
                if pf.options.gui:
                    print("There was an error while executing a script")
                else:
                    return res  # EXIT
        # if we have a gui, go into gui interactive mode
        if pf.GUI:
            res = pf.GUI.run()
            del pf.GUI
            del pf.app
            pf.GUI = pf.app = None
            # TODO: this should be moved to onExit functions
            # Save the preferences that have changed
            pf.debug("Saving preferences", pf.DEBUG.INFO)
            if not savePreferences():
                print("!!! Preferences could not be saved !!!")
    if pf.options.interactive:
        # Go into console interactive mode
        import readline  # noqa: F401 (allows Up/Down/History in the console)
        import code
        variables = globals().copy()
        variables.update(locals())
        shell = code.InteractiveConsole(variables)
        shell.interact()
    # Exit
    pf.debug("Running (nongui) exit functions", pf.DEBUG.INFO)
    for func in pf.on_exit:
        func()
    # pf.logger.info(f"{pf.fullVersion()} exit with status {res}")
    pf.debug(f"Exiting main with code {res}", pf.DEBUG.INFO)
    return res 
# End