############################################################################
#   This file is part of the aGrUM/pyAgrum library.                        #
#                                                                          #
#   Copyright (c) 2005-2026 by                                             #
#       - Pierre-Henri WUILLEMIN(_at_LIP6)                                 #
#       - Christophe GONZALES(_at_AMU)                                     #
#                                                                          #
#   The aGrUM/pyAgrum library is free software; you can redistribute it    #
#   and/or modify it under the terms of either :                           #
#                                                                          #
#    - the GNU Lesser General Public License as published by               #
#      the Free Software Foundation, either version 3 of the License,      #
#      or (at your option) any later version,                              #
#    - the MIT license (MIT),                                              #
#    - or both in dual license, as here.                                   #
#                                                                          #
#   (see https://agrum.gitlab.io/articles/dual-licenses-lgplv3mit.html)    #
#                                                                          #
#   This aGrUM/pyAgrum library is distributed in the hope that it will be  #
#   useful, but WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,          #
#   INCLUDING BUT NOT LIMITED TO THE WARRANTIES MERCHANTABILITY or FITNESS #
#   FOR A PARTICULAR PURPOSE  AND NONINFRINGEMENT. IN NO EVENT SHALL THE   #
#   AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #
#   LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,        #
#   ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR  #
#   OTHER DEALINGS IN THE SOFTWARE.                                        #
#                                                                          #
#   See LICENCES for more details.                                         #
#                                                                          #
#   SPDX-FileCopyrightText: Copyright 2005-2026                            #
#       - Pierre-Henri WUILLEMIN(_at_LIP6)                                 #
#       - Christophe GONZALES(_at_AMU)                                     #
#   SPDX-License-Identifier: LGPL-3.0-or-later OR MIT                      #
#                                                                          #
#   Contact  : info_at_agrum_dot_org                                       #
#   homepage : http://agrum.gitlab.io                                      #
#   gitlab   : https://gitlab.com/agrumery/agrum                           #
#                                                                          #
############################################################################

"""
configuration tool for pyAgrum
"""

from configparser import ConfigParser
from typing import Any
import os


class GumSingleton(type):
  _instances: dict[type, Any] = {}

  def __call__(cls, *args, **kwargs):
    if cls not in cls._instances:
      cls._instances[cls] = super().__call__(*args, **kwargs)
    return cls._instances[cls]


class PyAgrumConfiguration(metaclass=GumSingleton):
  """PyAgrumConfiguration is a the pyAgrum configuration singleton.  The configuration is build
  as a classical ConfigParser with read-only structure. Then a value is adressable using a double key: ``[section,key]``.

  See `this notebook <https://lip6.fr/Pierre-Henri.Wuillemin/aGrUM/docs/last/notebooks/configForPyAgrum.ipynb.html>`_.

  Examples
  --------
    >>> import pyagrum
    >>> pyagrum.config["dynamicBN", "default_graph_size"] = 10
    >>> pyagrum.config["dynamicBN", "default_graph_size"]
    "10"
  """

  def _check_int(self, s):
    if s[0] in ("-", "+"):
      return s[1:].isdigit()
    return s.isdigit()

  def _check_float(self, s):
    t = s.split(".")
    if len(t) == 1:
      return self._check_int(t[0])
    elif len(t) == 2:
      return self._check_int(t[0]) and t[1].isdigit()
    else:
      return False

  def _check_bool(self, s):
    return self._check_bool_true(s) or self._check_bool_false(s)

  def _check_bool_true(self, s):
    return s.upper() in ["TRUE", "1", "ON", "YES"]

  def _check_bool_false(self, s):
    return s.upper() in ["FALSE", "0", "OFF", "NO"]

  class _Casterization:
    def __init__(self, container):
      self.container = container

  class _CastAsInt(_Casterization):
    def __getitem__(self, x):
      return int(self.container[x])

    def __setitem__(self, x, v):
      s = str(v)
      if self.container._check_int(s):
        self.container[x] = s
      else:
        raise ValueError(f"'{s}' must contain an int.")

  class _CastAsFloat(_Casterization):
    def __getitem__(self, x):
      return float(self.container[x])

    def __setitem__(self, x, v):
      s = str(v)
      if self.container._check_float(s):
        self.container[x] = s
      else:
        raise ValueError(f"'{s}' must contain a float.")

  class _CastAsBool(_Casterization):
    def __getitem__(self, x):
      return self.container._check_bool_true(self.container[x])

    def __setitem__(self, x, v):
      s = str(v)
      if self.container._check_bool(s):
        self.container[x] = s
      else:
        raise ValueError(f"'{s}' must contain a boolean (False/True, 0/1, Off/On).")

  def __init__(self):
    self.__parser = ConfigParser(allow_no_value=False)

    defaultsfn = os.path.dirname(__file__) + "/defaults.ini"
    self.__parser.read(defaultsfn)
    self.__defaults = self.__str__()
    self.__hooks = []
    self.__stacks = []

    self.asInt = self._CastAsInt(self)
    self.asFloat = self._CastAsFloat(self)
    self.asBool = self._CastAsBool(self)

  def add_hook(self, fn):
    self.__hooks.append(fn)

  def run_hooks(self):
    for fn in self.__hooks:
      fn()

  def set(self, section, option, value, no_hook=False):
    """set a property in a section. Preferably use ``__getitem__`` and ``__setitem__``.

    Examples
    --------
      >>> pyagrum.config["dynamicBN", "default_graph_size"] = 10
      >>> pyagrum.config["dynamicBN", "default_graph_size"]
      "10"

    Parameters
    ----------
    section : str
        The section name (has to exist in defaults).
    option : str
        The option/property name (has to exist in defaults).
    value : str
        The value (will be stored as string).
    no_hook : bool, optional
        Should this call trigger the hooks?

    Raises
    ------
    SyntaxError
        If the section name or the property name does not exist.
    """
    if section in self.__parser.sections():
      if option in self.__parser[section]:
        self.__parser.set(section, option, str(value))
        if not no_hook:
          self.run_hooks()
      else:
        raise SyntaxError(f"Key '{section},{option}' unknown in pyAgrum configuration.")
    else:
      raise SyntaxError(f"Section '{section}' unknown in pyAgrum configuration.")

  def get(self, section, option):
    """Give the value associated to section.option. Preferably use ``__getitem__`` and ``__setitem__``.

    Examples
    --------
      >>> pyagrum.config["dynamicBN", "default_graph_size"] = 10
      >>> pyagrum.config["dynamicBN", "default_graph_size"]
      "10"

    Parameters
    ----------
    section : str
        The section.
    option : str
        The property.

    Returns
    -------
    str
        The value (as string).
    """
    return self.__parser.get(section, option)

  def __diff(self):
    mine = self.__parser
    c = ConfigParser()
    c.read_string(self.__defaults)

    def aff_sec(section):
      return (
        "["
        + section
        + "]\n"
        + "\n".join(
          [
            f"  {key} = {mine[section][key]}"
            for key in mine[section].keys()
            if mine.get(section, key) != c.get(section, key)
          ]
        )
      )

    return "\n".join([sec for sec in [aff_sec(section) for section in mine.sections()] if "=" in sec])

  def save(self):
    """Save the diff with the defaults in ``pyagrum.ini`` in the current directory"""
    with open("pyagrum.ini", "w") as configfile:
      print(self.__diff(), file=configfile)

  def reset(self):
    """back to defaults"""
    self.__parser.read_string(self.__defaults)
    self.run_hooks()

  def load(self):
    """load pyagrum.ini in the current directory, and change the properties if needed

    Raises
    ------
    FileNotFoundError
        If there is no pyagrum.ini in the current directory.
    """
    if os.path.isfile("pyagrum.ini"):
      # to force to use the protected set() method
      c = ConfigParser()
      c.read("pyagrum.ini")
      error_found = False
      for section in c.sections():
        if section not in self.__parser.sections():
          error_found = True
          print(f"[pyagrum.ini] Section '{section}' does not exist.")
        for option in c[section]:
          try:
            self.set(section, option, c[section][option], no_hook=True)
          except SyntaxError:
            error_found = True
            print(f"[pyagrum.ini] Option '{section}.{option}' does not exist.")
        self.run_hooks()
      if error_found:
        self.save()
    else:
      raise FileNotFoundError("No file 'pyagrum.ini' in current directory.")

  def grep(self, search):
    """grep in the configuration any section or properties matching the argument. If a section match the argume, all the section is displayed.

    Parameters
    ----------
    search : str
        The string to find.
    """
    mine = self.__parser
    lowsearch = search.lower()

    def aff_sec(section, all):
      return (
        "["
        + section
        + "]\n"
        + "\n".join([f"  {key} = {mine[section][key]}" for key in mine[section].keys() if all or lowsearch in key])
      )

    print(
      "\n".join([sec for sec in [aff_sec(section, lowsearch in section) for section in mine.sections()] if "=" in sec])
    )

  def diff(self):
    """print the diff between actual configuration and the defaults. This is what is saved in the file ``pyagrum.ini`` by the method `PyAgrumConfiguration.save()`"""
    print(self.__diff())

  def __str__(self):
    mine = self.__parser

    def aff_sec(section):
      return "[" + section + "]\n" + "\n".join([f"  {key} = {mine[section][key]}" for key in mine[section].keys()])

    return "\n".join([aff_sec(section) for section in mine.sections()])

  def __repr__(self):
    res = self.__diff()
    if "=" in res:
      return res
    else:
      return "# no customized property\n" + self.__str__()

  def __getitem__(self, key):
    return self.get(key[0], key[1])

  def __setitem__(self, key, value):
    return self.set(key[0], key[1], value)

  def __delitem__(self, key):
    raise SyntaxError("No deletion of item in configuration")

  def pop(self):
    """
    Pop the last config from the stack and set it as the current configuration
    """
    if len(self.__stacks) > 0:
      self.__parser.read_string(self.__stacks.pop())
      self.run_hooks()
    else:
      raise IndexError("[pyAgrum] No configuration to pop")

  def push(self):
    """
    Push the current configuration in the stack
    """
    self.__stacks.append(str(self))
