"""
@package pmsco.reports.base
base class of project reports

@author Matthias Muntwiler, matthias.muntwiler@psi.ch

@copyright (c) 2021 by Paul Scherrer Institut @n
Licensed under the Apache License, Version 2.0 (the "License"); @n
  you may not use this file except in compliance with the License.
  You may obtain a copy of the License at
  http://www.apache.org/licenses/LICENSE-2.0
"""

import logging
from pathlib import Path
from string import Template
import pmsco.config as config

logger = logging.getLogger(__name__)


class ProjectReport(config.ConfigurableObject):
    """
    base class of project reports

    what do we need to know from project?
    - directories to resolve path names
    - database session factory
    - database job id
    - calculation id

    usage:
    1. assign public attributes as necessary, leave defaults at None.
    2. call validate() if necessary.
    3. call set_database() if necessary.
    4. load data into result_data by calling select_data() or by modifying result_data directly.
    5. call create_report()

    implementations of reports should not need to access any project members directly!

    this class is under development
    """

    ## @var _project
    # project object reference
    #

    ## @var _dba
    # database access (session factory)
    #

    ## @var _modes
    # compatible project modes
    #
    # set of modes in which the report can be used.
    # if an incompatible project is assigned, the enabled property is set to False.

    ## @var canvas
    # matplotlib canvas for graphical reports
    #
    # a FigureCanvas class reference of a matplotlib backend.
    # the default is matplotlib.backends.backend_agg.FigureCanvasAgg
    # which produces a bitmap file in PNG format.
    # some other options are
    # matplotlib.backends.backend_pdf.FigureCanvasPdf or
    # matplotlib.backends.backend_svg.FigureCanvasSVG.

    ## @var enabled
    # enable/disable the report
    #
    # the flag allows to temporarily enable or disable a report.
    #
    # the flag does not change the behaviour of the class.
    # it is up to the caller to respect it.
    #
    # the validate method can set the flag to False if the project is not compatible.

    ## @var report_dir
    # destination directory for report files
    #
    # this should be a Path or PathLike object.
    # by default, the project's directories['report'] entry is used.

    ## @var base_filename
    # base name of output files
    #
    # this value gets copied into the {base} placeholder of filename_format.
    #
    # by default, this is the stem of the output filename from the project settings.

    ## @var filename_format (str)
    # format of the output file name
    #
    # the format method of the string will be used to produce individual names.
    # possible fields are:
    # base, job_id, job_name, mode, model, gen, particle, param0, param1
    # where base corresponds to the stem of the output files produced by the calculators.
    #
    # a file extension according to the file format is appended.

    ## @var title_format (str)
    # title of the plot
    #
    # the format method of the string will be used to produce individual titles.
    # possible fields are:
    # base, job_id, job_name, mode, model, gen, particle, param0, param1

    ## @var trigger_levels (set of str)
    # events that may trigger creation of a report
    #
    # this attribute selects when a report is created.
    # the following values are currently recognized.
    # any other value disables the report.
    # further modes may be implemented in the future.
    #
    # - `model` - every time a new model has been calculated.
    # - `end` (default) - only once at the end of an optimization job.
    #
    # it is up to the calling code to respect this attribute.
    # it does not affect the behaviour of the class.

    def __init__(self):
        super().__init__()
        self._project = None
        self._dba = None
        self._modes = set()

        self.enabled = True
        self.trigger_levels = {'end'}
        self.canvas = None
        self.report_dir = None
        self.base_filename = None
        self.filename_format = "${base}"
        self.title_format = ""

    def get_session(self):
        """
        get a new database session context handler

        @return: context handler which provides an sqlalchemy database session,
            e.g. a pmsco.database.access.LockedSession() object.
        """
        return self._dba.session()

    def validate(self, project):
        """
        validate the configuration (object properties).

        @param project: pmsco.project.Project object,
            or any object that contains equivalent directories and output_file attributes.

        @return: None
        """
        self._project = project
        if self._project:
            if self.report_dir is None:
                self.report_dir = self._project.directories['report']
            if self.base_filename is None:
                self.base_filename = self._project.output_file.name

            if self.enabled and self._project.mode not in self._modes:
                self.enabled = False
                logger.warning(f"project mode {self._project.mode} incompatible with {self.__class__.__name__}")

        if self.report_dir:
            Path(self.report_dir).mkdir(exist_ok=True)

    def set_database(self, database_access):
        """
        preparation steps for the report.

        @param database_access: fully initialized pmsco.database.project.ProjectDatabase object
            which provides database sessions.

        @return: None
        """
        self._dba = database_access

    def resolve_template(self, template, mapping):
        """
        resolve placeholders in template string

        the function first tries to resolve using the project's resolve_path function
        and reverts to plain python Template strings if no project is set.

        @param template: template string using ${name}-style place holders.
            name must be declared in project.directories or mapping.
            non-string objects (e.g. Path) are converted to a string using the str function.
        @param mapping: dictionary of name-value pairs for placeholders.
        @return: resolved string
        """
        try:
            r = self._project.directories.resolve_path(template, mapping)
        except AttributeError:
            if template:
                r = Template(str(template)).substitute(mapping)
            else:
                r = ""
        return r

    def select_data(self, jobs=-1, calcs=None):
        """
        query data from the database

        this method must be implemented by the sub-class.

        @param jobs: filter by job.
            the argument can be a singleton or sequence of orm.Job objects or numeric id.
            if None, results from all jobs are loaded.
            if -1 (default), results from the most recent job (by datetime field) are loaded.

        @param calcs: filter by result.
            the argument can be a singleton or sequence of CalcID objects.
            if None (default), all results are loaded.
            if -1, the most recent result is loaded.

        @return: None
        """
        pass

    def create_report(self):
        """
        generate or update the report from stored data.

        this method must be implemented by the sub-class.

        @return: None
        """
        pass
