# -*- coding: utf-8 -*-
# Copyright 2021 IRT Saint Exupéry, https://www.irt-saintexupery.com
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License version 3 as published by the Free Software Foundation.
#
# 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

# Contributors:
#    INITIAL AUTHORS - initial API and implementation and/or initial
#                        documentation
#        :author: Francois Gallard
#    OTHER AUTHORS   - MACROSCOPIC CHANGES
"""
Scenario which drivers are Design of Experiments
************************************************
"""
from __future__ import division, unicode_literals

import logging

from gemseo.algos.doe.doe_factory import DOEFactory
from gemseo.core.scenario import Scenario

# The detection of formulations requires to import them,
# before calling get_formulation_from_name
LOGGER = logging.getLogger(__name__)


class DOEScenario(Scenario):
    """Design of Experiments scenario, based on MDO scenario but with a DOE driver.

    The main differences between Scenario and MDOScenario are the allowed
    inputs
    in the MDOScenario.json, which differs from DOEScenario.json, at least
    on the driver names

    MDO Problem description: links the disciplines and the formulation
    to create an optimization problem.
    Use the class by instantiation.
    Create your disciplines beforehand.

    Specify the formulation by giving the class name such as the string
    "MDF"

    The reference_input_data is the typical input data dict that is provided
    to the run method of the disciplines

    Specify the objective function name, which must be an output
    of a discipline of the scenario, with the "objective_name" attribute

    If you want to add additional design constraints,
    use the add_user_defined_constraint method

    To view the results, use the "post_process" method after execution.
    You can view:

    - the design variables history, the objective value, the constraints,
      by using: scenario.post_process("OptHistoryView", show=False, save=True)
    - Quadratic approximations of the functions close to the
      optimum, when using gradient based algorithms, by using:
      scenario.post_process("QuadApprox", method="SR1", show=False,
      save=True, function="my_objective_name",
      file_path="appl_dir")
    - Self Organizing Maps of the design space, by using:
      scenario.post_process("SOM", save=True, file_path="appl_dir")

    To list post processings on your setup,
    use the method :attr:`.Scenario.posts`.
    For more details on their options, go to the **gemseo.post** package.
    """

    # Constants for input variables in json schema
    N_SAMPLES = "n_samples"
    EVAL_JAC = "eval_jac"
    SEED = "seed"

    def __init__(
        self,
        disciplines,
        formulation,
        objective_name,
        design_space,
        name=None,
        **formulation_options
    ):
        """Constructor, initializes the DOE scenario Objects instantiation and checks
        are made before run intentionally.

        :param disciplines: the disciplines of the scenario
        :param formulation: the formulation name,
            the class name of the formulation in gemseo.formulations
        :param objective_name: the objective function name
        :param design_space: the design space
        :param name: scenario name
        :param formulation_options: options for creation of the formulation
        """
        # This loads the right json grammars from class name
        super(DOEScenario, self).__init__(
            disciplines,
            formulation,
            objective_name,
            design_space,
            name,
            **formulation_options
        )
        self.seed = 0
        self.default_inputs = {self.EVAL_JAC: False, self.ALGO: "lhs"}

    def _init_algo_factory(self):
        """Initalizes the algorithms factory."""
        self._algo_factory = DOEFactory()

    def _run_algorithm(self):
        """Runs the DOE algo."""
        self.seed += 1
        problem = self.formulation.opt_problem
        algo_name = self.local_data[self.ALGO]
        n_samples = self.local_data.get(self.N_SAMPLES)
        options = self.local_data.get(self.ALGO_OPTIONS)
        if options is None:
            options = {}
        if self.N_SAMPLES in options:
            LOGGER.warning(
                "Double definition of algorithm option n_samples, " "keeping value: %s",
                n_samples,
            )
            options.pop(self.N_SAMPLES)

        if self.ALGO_OPTIONS in self.local_data:
            options = self.local_data[self.ALGO_OPTIONS]
        lib = self._algo_factory.create(algo_name)
        lib.init_options_grammar(algo_name)
        if self.SEED in lib.opt_grammar.get_data_names():
            if self.SEED not in options:
                options[self.SEED] = self.seed
        self.optimization_result = lib.execute(problem, n_samples=n_samples, **options)
        return self.optimization_result

    def _run(self):
        """Execute the scenario and run the optimization problems."""
        LOGGER.info(" ")
        LOGGER.info("*** Start DOE Scenario execution ***")
        LOGGER.info("%s", repr(self))
        self._run_algorithm()
        LOGGER.info("*** DOE Scenario run terminated ***")
