""" The Configuration class stores the CLI configuration provided by the user.
"""

import os
import time
import demandimport

from perun.collect.trace.collect_engine import CollectEngine
from perun.collect.trace.systemtap.engine import SystemTapEngine
from perun.collect.trace.probes import Probes
from perun.collect.trace.values import OutputHandling

from perun.utils.exceptions import InvalidBinaryException
from perun.utils import find_executable
import perun.logic.temp as temp

# Import on demand since eBPF support is optional
with demandimport.enabled():
    import perun.collect.trace.ebpf.engine as bpf


class Configuration:
    """ A class that stores the Tracer configuration provided by the CLI.

    :ivar Probes probes: the collection probes configuration
    :ivar bool keep_temps: keep the temporary files after the collection is finished
    :ivar bool zip_temps: zip and store the temporary files before they are deleted
    :ivar bool verbose_trace: the raw performance data collected will be more verbose
    :ivar bool quiet: the collection progress output will be less verbose
    :ivar bool watchdog: enables detailed logging during the collection
    :ivar bool diagnostics: enables detailed surveillance mode of the collector
    :ivar OutputHandling output_handling: store or discard the profiling command stdout and stderr
    :ivar CollectEngine engine: the collection engine to be used, e.g. SystemTap or eBPF
    :ivar bool stap_cache_off: specifies if systemtap cache should be enabled or disabled
    :ivar bool generate_dynamic_cg: specifies whether dynamic CG should be reconstructed from trace
    :ivar bool no_profile: disables profile generation
    :ivar list run_optimizations: list of run-phase optimizations that are enabled
    :ivar dict run_optimization_parameters: optimization parameter name -> value mapping
    :ivar float or None timeout: the timeout for the profiled command or None if indefinite
    :ivar str binary: the path to the binary file to be probed
    :ivar Executable executable: the Executable object containing the profiled command, args, etc.
    :ivar list libs: additional libraries that are profiled with the given binary
    :ivar str timestamp: the time of the collection start
    :ivar int pid: the PID of the Tracer process
    :ivar str files_dir: the directory path of the temporary files
    :ivar str locks_dir: the directory path of the lock files
    :ivar dict stats_data: compactly stores data necessary for building dynamic stats
    """
    def __init__(self, executable, **cli_config):
        """ Constructs the Configuration object from the supplied CLI configuration

        :param Executable executable: an object containing the profiled command, args, etc.
        :param cli_config: the CLI configuration
        """
        # Set the some default values if not provided
        self.keep_temps = cli_config.get('keep_temps', False)
        self.zip_temps = cli_config.get('zip_temps', False)
        self.verbose_trace = cli_config.get('verbose_trace', False)
        self.quiet = cli_config.get('quiet', False)
        self.watchdog = cli_config.get('watchdog', False)
        self.diagnostics = cli_config.get('diagnostics', False)
        self.output_handling = cli_config.get('output_handling', OutputHandling.DEFAULT.value)
        self.engine = cli_config.get('engine', CollectEngine.default())
        self.stap_cache_off = cli_config.get('stap_cache_off', False)
        self.generate_dynamic_cg = cli_config.get('generate_dynamic_cg', False)
        self.no_profile = cli_config.get('no_profile', False)
        self.cg_extraction = cli_config.get('only_extract_cg', False)
        # TODO: temporary
        self.maximum_threads = cli_config.get('max_simultaneous_threads', 5)
        self.extract_mcg = cli_config.get('extract_mixed_cg', False)
        self.no_ds_update = cli_config.get('no_ds_update', False)
        # The run optimization values should be provided by the Optimization module, if enabled
        self.run_optimizations = []
        self.run_optimization_parameters = {}

        # Enable some additional flags if diagnostics is enabled
        if self.diagnostics:
            self.zip_temps = True
            self.verbose_trace = True
            self.watchdog = True
            self.output_handling = OutputHandling.CAPTURE.value

        # Transform the output handling value to the enum element
        self.output_handling = OutputHandling(self.output_handling)

        # Normalize timeout value
        self.timeout = cli_config.get('timeout', None)
        if self.timeout <= 0:
            self.timeout = None

        # Set the executable and binary
        self.binary = find_executable(cli_config.get('binary', None))
        self.executable = executable
        self.executable.cmd = find_executable(self.executable.cmd)
        self.libs = list(cli_config.get('libs', ''))
        self._resolve_executables()

        # Update the configuration with some additional values
        self.timestamp = time.strftime("%Y-%m-%d-%H-%M-%S", time.localtime())
        self.pid = os.getpid()
        self.files_dir = temp.temp_path(os.path.join('trace', 'files'))
        self.locks_dir = temp.temp_path(os.path.join('trace', 'locks'))

        # Build the probes configuration
        self.probes = Probes(self.binary, self.libs, **cli_config)
        self.stats_data = {}

    def engine_factory(self):
        """ Instantiates the engine object based on the string representation.

        This function should be invoked separately after the Configuration object exists, so that
        the Engine object is not lost due to a received signal etc., which would prevent successful
        cleanup of engine resources.
        """
        if self.engine == 'stap':
            self.engine = SystemTapEngine(self)
        else:
            self.engine = bpf.BpfEngine(self)

    def get_functions(self):
        """Access the configuration of the function probes

        :return dict: the function probes dictionary
        """
        return self.probes.func

    def prune_functions(self, remaining):
        """ Remove function probes not present in the 'remaining' set from the instrumentation

        :param dict remaining: the set of remaining functions and their sampling configuration
        """
        for func_name in list(self.probes.func.keys()):
            if func_name not in remaining:
                del self.probes.func[func_name]
            elif func_name not in self.probes.user_func and remaining[func_name] > 1:
                self.probes.func[func_name]['sample'] = remaining[func_name]

    def set_run_optimization(self, optimizations, parameters):
        """ Allows the Optimization module to set run optimizations and their parameters
        directly in the Configuration object.

        :param list optimizations: list of optimization names
        :param dict parameters: optimization parameter name -> parameter value
        """
        self.run_optimizations = optimizations
        self.run_optimization_parameters = parameters

    def get_target(self):
        """ Obtain the target executable file.

        :return str: a path to the binary executable file
        """
        return self.binary

    def _resolve_executables(self):
        """ Check that all of the supplied executables (command, binary, libraries)
        are accessible and executable, and obtain their real paths (absolute and no symlinks)
        """
        # No runnable command was given, terminate the collection
        if self.binary is None and not self.executable.cmd:
            raise InvalidBinaryException('')
        # Otherwise copy the cmd or binary parameter
        if not self.executable.cmd:
            self.executable.cmd = self.binary
        elif self.binary is None:
            self.binary = self.executable.cmd

        # Check that all of the supplied libraries exist and are executable
        resolved_libs = []
        for lib in self.libs:
            lib = find_executable(lib)
            if lib is not None:
                resolved_libs.append(lib)
        self.libs = resolved_libs
