"""
@package pmsco.config
infrastructure for configurable objects

@author Matthias Muntwiler

@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 collections.abc
import functools
import inspect
import logging
from pathlib import Path

logger = logging.getLogger(__name__)


def resolve_path(path, dirs):
    """
    resolve a file path by replacing placeholders

    placeholders are enclosed in curly braces.
    values for all possible placeholders are provided in a dictionary.

    @param path: str, Path or other path-like.
        example: '{work}/test/testfile.dat'.
    @param dirs: dictionary mapping placeholders to project paths.
        the paths can be str, Path or other path-like
        example: {'work': '/home/user/work'}
    @return: pathlib.Path object
    """
    return Path(*(p.format(**dirs) for p in Path(path).parts))


class ConfigurableObject(object):
    """
    Parent class for objects that can be configured by a run file

    the run file is a JSON file that contains object data in a nested dictionary structure.

    in the dictionary structure the keys are property or attribute names of the object to be initialized.
    keys starting with a non-alphabetic character (except for some special keys like __class__) are ignored.
    these can be used as comments, or they protect private attributes.

    the values can be numeric values, strings, lists or dictionaries.

    simple values are simply assigned using setattr.
    this may call a property setter if defined.

    lists are iterated. each item is appended to the attribute.
    the attribute must implement an append method in this case.

    if an item is a dictionary and contains the special key '__class__',
    an object of that class is instantiated and recursively initialized with the dictionary elements.
    this requires that the class can be found in the module scope passed to the parser methods,
    and that the class inherits from this class.

    cases that can't be covered easily using this mechanism
    should be implemented in a property setter.
    value-checking should also be done in a property setter (or the append method in sequence-like objects).
    """
    def __init__(self):
        pass

    def set_properties(self, module, data_dict, project):
        """
        set properties of this class.

        @param module: module reference that should be used to resolve class names.
            this is usually the project module.
        @param data_dict: dictionary of properties to set.
            see the class description for details.
        @param project: reference to the project object.
        @return: None
        """
        for key in data_dict:
            if key[0].isalpha():
                self.set_property(module, key, data_dict[key], project)

    def set_property(self, module, key, value, project):
        obj = self.parse_object(module, value, project)
        if hasattr(self, key):
            if obj is not None:
                if isinstance(obj, collections.abc.MutableSequence):
                    attr = getattr(self, key)
                    for item in obj:
                        attr.append(item)
                elif isinstance(obj, collections.abc.Mapping):
                    d = getattr(self, key)
                    if d is not None and isinstance(d, collections.abc.MutableMapping):
                        d.update(obj)
                    else:
                        setattr(self, key, obj)
                else:
                    setattr(self, key, obj)
            else:
                setattr(self, key, obj)
        else:
            logger.warning(f"class {self.__class__.__name__} does not have attribute {key}.")

    def parse_object(self, module, value, project):
        if isinstance(value, collections.abc.MutableMapping) and "__class__" in value:
            cn = value["__class__"].split('.')
            c = functools.reduce(getattr, cn, module)
            s = inspect.signature(c)
            if 'project' in s.parameters:
                o = c(project=project)
            else:
                o = c()
            o.set_properties(module, value, project)
        elif isinstance(value, collections.abc.MutableSequence):
            o = [self.parse_object(module, i, project) for i in value]
        else:
            o = value
        return o
