#!/usr/bin/env python3
# --------------------( LICENSE                            )--------------------
# Copyright 2014-2017 by Alexis Pietak & Cecil Curry
# See "LICENSE" for further details.

'''
High-level constants describing BETSE's filesystem usage.

These constants provide the absolute paths of files and directories intended for
general use by both the CLI and GUI. For portability, such constants are
initialized in a system-aware manner guaranteed to be sane under various
installation environments -- including PyInstaller-frozen executables and
`setuptools`-installed script wrappers.
'''

#FIXME: The current globals-based approach is inefficient in the case of BETSE
#being installed as a compressed EGG rather than an uncompressed directory. In
#the former case, the current approach (namely, the call to
#resources.get_pathname() performed below) silently extracts the entirety of
#this egg to a temporary setuptools-specific cache directory. That's bad. To
#circumvent this, we'll need to refactor the codebase to directly require only
#"file"-like objects rather than indirectly requiring the absolute paths of
#data resources that are then opened as "file"-like objects.
#
#Specifically, whenever we require a "file"-like object for a codebase resource,
#we'll need to call the setuptools-specific pkg_resources.resource_stream()
#function rather than attempting to open the path given by a global below.
#Ultimately, *ALL* of the codebase-specific globals declared below (e.g.,
#"DATA_DIRNAME") should go away.

# ....................{ IMPORTS                            }....................
#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
# WARNING: To raise human-readable exceptions on missing mandatory dependencies,
# the top-level of this module may import *ONLY* from packages guaranteed to
# exist at installation time (i.e., stock Python and BETSE packages).
#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
import betse.util.py.freezers
from betse import metadata
from os import environ, path

# ....................{ CONSTANTS ~ dir                    }....................
#FIXME: Replace all existing calls to os.path.expanduser() with this constant.

HOME_DIRNAME = None   # initialized below
'''
Absolute path of the current user's home directory.
'''


DOT_DIRNAME = None  # initialized below
'''
Absolute path of BETSE's dot directory in the current user's home directory.

This directory contains application-external resources (e.g., configuration
files) created at application runtime and subsequently editable by external
users and utilities.

Locations
----------
This path is operating system-specific as follows:

* Under Linux, this is `~/.betse/`. BETSE does _not_ currently comply with the
  _XDG Base Directory Specification (e.g., `~/.local/share/betse`), which the
  principal authors of BETSE largely regard as unuseful -- if not harmful.
* Under OS X, this is `~/Library/Application Support/betse`.
* Under Windows, this is
  `C:\Documents and Settings\${User}\Application Data\betse`.

.. _XDG Base Directory Specification: http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html
'''

# ....................{ CONSTANTS ~ data                   }....................
DATA_DIRNAME = None   # initialized below
'''
Absolute path of BETSE's data directory.

Such directory contains application-internal resources (e.g., media files)
frozen with the end-user executable binaries generated by PyInstaller.
'''


DATA_YAML_DIRNAME = None   # initialized below
'''
Absolute path of BETSE's YAML data directory.

This directory contains:

* The YAML-formatted configuration file defining a simulation configuration.
* All assets referenced in and hence required by this file.
'''


DATA_DEFAULT_ASSET_DIRNAMES = None   # initialized below
'''
Tuple of the absolute paths of all assets referenced in and hence required by
BETSE's default application-wide configuration file.

This includes directories containing (in no particular order):

* YAML-formatted configuration files defining finer-grained configuration
  parameters, including:
  * The biochemical reaction network (BRN) to be simulated, if any.
  * The gene regulatory network (GRN) to be simulated, if any.
* Ancillary media files, including:
  * Sample images intended to be used as input geometries in tissue simulation
    configuration files. Each image defines a cell cluster shape typically
    corresponding to some species (e.g., planaria) or organ (e.g., heart),
    complete with internal cellular structure and exterior cellular boundary.
    Each spatial subdivision of this shape is then associated with a real-world
    tissue profile in the configuration file(s) referencing this image.
'''

# ....................{ CONSTANTS ~ file                   }....................
LOG_DEFAULT_FILENAME = None   # initialized below
'''
Absolute path of BETSE's default user-specific logfile.

This is the plaintext file to which BETSE logs all messages by default.
'''

REPL_HISTORY_FILENAMES = {} # initialized below
'''
Absolute path of BETSE's default user-specific REPL history file.

A dictionary of absolute paths to BETSE's default user-specific REPL history
files, as indexed by file format.

The history files are plaintext files that the REPLs use to allow persistent
histories between REPL sessions.
'''


PROFILE_DEFAULT_FILENAME = None   # initialized below
'''
Absolute path of BETSE's default user-specific profile dumpfile.

This is the binary file to which BETSE writes profiled statistics by default.
'''


CONFIG_DEFAULT_FILENAME = None   # initialized below
'''
Absolute path of BETSE's default application-wide configuration file.

This is the YAML file defining the default tissue simulation configuration.
'''

# ....................{ INITIALIZATION                     }....................
def init() -> None:
    '''
    Validate core directories and files required at program startup and
    initialize the corresponding module constants (e.g., `DOT_DIRNAME`).

    This function automatically creates non-existent paths where feasible and
    otherwise raises exceptions on such paths *not* being found or *not* having
    correct metadata (e.g., permissions).

    Such paths are required by both the CLI and GUI interfaces for BETSE. To
    support caller-specific exception handling, this function *must* be manually
    called early in program startup.
    '''

    # Define top-level *BEFORE* sub-level pathnames, as the latter depend upon
    # the former.
    _init_pathnames_top()
    _init_pathnames_sub()

    # Validate these pathnames.
    _test_pathnames()


def _init_pathnames_top() -> None:
    '''
    Define all top-level pathnames (e.g., `DOT_DIRNAME`) declared by this
    module as global constants.
    '''

    # Avoid circular import dependencies. Since all mandatory dependencies have
    # been validated by this point in application startup, all packages are
    # safely importable from here.
    from betse.lib.setuptools import resources
    from betse.util.os import oses
    from betse.util.path import paths

    # Declare these constants to be globals, permitting modification below.
    global HOME_DIRNAME, DOT_DIRNAME, DATA_DIRNAME

    # Absolute path of the current user's home directory.
    HOME_DIRNAME = path.expanduser('~')

    # Initialize the absolute path of betse's dot directory.
    #
    # If the current system is OS X, set this directory accordingly.
    if oses.is_macos():
        DOT_DIRNAME = paths.join(
            HOME_DIRNAME,
            'Library', 'Application Support',
            metadata.SCRIPT_NAME_CLI
        )
    # If the current system is Windows, set this directory accordingly.
    elif oses.is_windows():
        DOT_DIRNAME = paths.join(environ['APPDATA'], metadata.NAME)
    #FIXME: Explicitly assert POSIX compatibility here.
    # Else, assume the current system to be POSIX-compatible.
    else:
        DOT_DIRNAME = paths.join(HOME_DIRNAME, '.' + metadata.SCRIPT_NAME_CLI)

    # Basename of the top-level data directory relative to the directory
    # containing this submodule. Since setuptools-specific resource pathnames
    # expect the POSIX- rather than Windows-specific directory separator (i.e.,
    # "/" rather than "\"), this basename must *NOT* contain the latter.
    data_root_basename = 'data'

    # Initialize the absolute path of betse's data directory.
    #
    # If the current application is a PyInstaller-frozen executable binary,
    # defer to the PyInstaller-specific private attribute "_MEIPASS" added
    # to the canonical "sys" module by the PyInstaller bootloader embedded in
    # this binary. This attribute provides the absolute path of the temporary
    # directory containing all application data resources extracted from this
    # binary by this bootloader. "And it's turtles all the way down."
    if betse.util.py.freezers.is_frozen_pyinstaller():
        DATA_DIRNAME = paths.join(
            betse.util.py.freezers.get_app_dirname_pyinstaller(), data_root_basename)
    # If the current application is a setuptools-installed script wrapper, the
    # data directory will have been preserved as is in the setuptools-installed
    # copy of the current Python package tree. In this case, query setuptools to
    # obtain this directory's path in a cross-platform manner.
    elif resources.is_dir(__name__, data_root_basename):
        DATA_DIRNAME = resources.get_pathname(__name__, data_root_basename)
    # Else, the current application is either a setuptools-symlinked script
    # wrapper *OR* was invoked via the hidden "python3 -m betse.cli.cli"
    # command. In either case, such directory's path is directly obtainable
    # relative to the absolute path of the current module.
    else:
        DATA_DIRNAME = paths.join(
            paths.get_dirname(__file__), data_root_basename)


def _init_pathnames_sub() -> None:
    '''
    Define all top-level pathnames (e.g., `DOT_DIRNAME`) declared by this
    module as global constants.
    '''

    # Avoid circular import dependencies.
    from betse.util.path import paths

    # Declare these constants to be globals, permitting modification below.
    global\
        LOG_DEFAULT_FILENAME,\
        REPL_HISTORY_FILENAMES,\
        PROFILE_DEFAULT_FILENAME,\
        CONFIG_DEFAULT_FILENAME,\
        DATA_DEFAULT_ASSET_DIRNAMES

    # Initialize the absolute paths of data subdirectories.
    DATA_YAML_DIRNAME = paths.join(DATA_DIRNAME, 'yaml')
    DATA_DEFAULT_ASSET_DIRNAMES = (
        paths.join(DATA_YAML_DIRNAME, 'geo'),
        paths.join(DATA_YAML_DIRNAME, 'extra_configs'),
    )

    # Initialize the absolute paths of data files.
    CONFIG_DEFAULT_FILENAME = paths.join(DATA_YAML_DIRNAME, 'sim_config.yaml')

    # Absolute path of the default user-specific logfile, historyfile and
    # profile dumpfile.
    LOG_DEFAULT_FILENAME = paths.join(
        DOT_DIRNAME, metadata.SCRIPT_NAME_CLI + '.log')
    PROFILE_DEFAULT_FILENAME = paths.join(
        DOT_DIRNAME, metadata.SCRIPT_NAME_CLI + '.prof')
    REPL_HISTORY_FILENAMES = {
        'ptpython' : paths.join(DOT_DIRNAME, 'ptpython.hist'),
        'readline' : paths.join(DOT_DIRNAME, 'readline.hist'),
    }


def _test_pathnames() -> None:
    '''
    Validate all pathnames (e.g., `DOT_DIRNAME`) declared by this module.
    '''

    # Avoid circular import dependencies.
    from betse.util.path import dirs, files

    # Ensure all requisite paths exist.
    dirs.die_unless_dir(DATA_DIRNAME, *DATA_DEFAULT_ASSET_DIRNAMES)
    files.die_unless_file(CONFIG_DEFAULT_FILENAME)

    # Create BETSE's top-level dot directory if not found.
    dirs.make_unless_dir(DOT_DIRNAME)
