import importlib
from json import dump, dumps, load


class Recreatable:
    """Represents an object that keeps track of a set of parameters that will
    allow to be recreate it in the future.

    The values for each key provided in the :param:`config_dict`
    parameter can be a constant value or a reference to a function,
    which will be evaluated when the object configuration is requested.
    This is useful for cases where a parameter changes its value on
    runtime.
    """

    def __init__(self, config_dict=None):
        if config_dict is None:
            config_dict = dict()

        if not isinstance(config_dict, dict):
            raise TypeError("Argument must be a dictionary")

        config_dict.update(
            {
                "classname": self.__class__.__name__,
                "module": self.__module__,
                "version": "0.17.0",
            }
        )
        for k, v in config_dict.items():
            config_dict.update({k: v})

        self._config = config_dict

    @classmethod
    def from_config(cls, config_dict):
        """Creates an instance of a Recreatable object with parameters in the
        provided dictionary."""
        _ = config_dict.pop("version", None)
        cls_name = config_dict.pop("classname")
        module_name = config_dict.pop("module")

        message = f"Recreating {cls_name} (from {module_name})"
        message += f" with {config_dict}" if config_dict else ""
        print(message)

        main_cls = cls.import_class(module_name, cls_name)
        return main_cls(**config_dict)

    @classmethod
    def from_file(cls, path):
        """Creates an instance of an object using the JSON file from the
        provided path."""
        print(f"Loading configuration from {path}")
        with open(path) as json_file:
            config_dict = load(json_file)
        return cls.from_config(config_dict)

    def save_config(self, path):
        """Stores the set of parameters to recreate an object in a JSON
        file."""
        print(f"Storing configuration in {path}")
        with open(path, "w") as writer:
            dump(self.config, writer, indent=4)

    @property
    def config(self):
        """Returns a dictionary with the set of parameters that can be used to
        recreate an object."""
        cfg = {}
        for k, v in self._config.items():
            cfg[k] = v() if callable(v) else v
        return cfg

    @staticmethod
    def import_class(module_name, class_name):
        """Imports a class given a module and a class name."""
        module = importlib.import_module(module_name)
        return getattr(module, class_name)

    def print_config(self):
        print(dumps(self.config, indent=4))
