"""
Base classes, mixins, and other inheritables.
"""
import abc
import logging
from sklearn.base import BaseEstimator
from automatminer.utils.log import initialize_logger, \
    initialize_null_logger, AMM_LOGGER_BASENAME

__authors__ = ["<NAME> <<EMAIL>>", "<NAME> <<EMAIL>>"]


class LoggableMixin:
    """A mixin class for easy logging (or absence of it)."""

    @property
    def logger(self):
        """Get the class logger.
        If the logger is None, the logging calls will be redirected to a dummy
        logger that has no output.
        """
        if hasattr(self, "_logger"):
            return self._logger
        else:
            raise AttributeError("Loggable object has no _logger attribute!")

    @staticmethod
    def get_logger(logger, level=None):
        """Set the class logger.
        Args:
            logger (Logger, bool): A custom logger object to use for logging.
                Alternatively, if set to True, the default automatminer logger
                will be used. If set to False, then no logging will occur.
            level (int): The log level. For example logging.DEBUG.
        """
        # need comparison to True and False to avoid overwriting Logger objects
        if logger is True:
            logger = logging.getLogger(AMM_LOGGER_BASENAME)

            if not logger.handlers:
                initialize_logger(AMM_LOGGER_BASENAME, level=level)

        elif logger is False:
            logger = logging.getLogger(AMM_LOGGER_BASENAME + "_null")

            if not logger.handlers:
                initialize_null_logger(AMM_LOGGER_BASENAME)

        logger.setLevel(logging.INFO)
        return logger

    @property
    def _log_prefix(self):
        return self.__class__.__name__ + ": "


class DFTransformer(abc.ABC, BaseEstimator):
    """ A base class to allow easy transformation in the same way as
    TransformerMixin and BaseEstimator in sklearn, but for pandas dataframes.

    When implementing a base class adaptor, make sure to use @check_fitted
    and @set_fitted if necessary!
    """

    @abc.abstractmethod
    def fit(self, df, target, **fit_kwargs):
        """
        Fits the transformer to a dataframe, given a target.

        Args:
            df (pandas.DataFrame): The pandas dataframe to be fit.
            target (str): the target string specifying the ML target.
            fit_kwargs: Keyword paramters for fitting

        Returns:
            (DataFrameTransformer) This object (self)

        """
        pass

    @abc.abstractmethod
    def transform(self, df, target, **transform_kwargs):
        """
        Transforms a dataframe.

        Args:
            df (pandas.DataFrame): The pandas dataframe to be fit.
            target (str): the target string specifying the ML target.
            transform_kwargs: Keyword paramters for transforming

        Returns:
            (pandas.DataFrame): The transformed dataframe.

        """
        pass

    def fit_transform(self, df, target):
        """
        Combines the fitting and transformation of a dataframe.

        Args:
            df (pandas.DataFrame): The pandas dataframe to be fit.
            target (str): the target string specifying the ML target.

        Returns:
            (pandas.DataFrame): The transformed dataframe.

        """
        return self.fit(df, target).transform(df, target)


class DFMLAdaptor(DFTransformer):
    """
    A base class to adapt from an AutoML backend to a sklearn-style fit/predict
    scheme and add a few extensions for pandas dataframes.

    When implementing a base class adaptor, make sure to use @check_fitted
    and @set_fitted if necessary!
    """

    @abc.abstractmethod
    def predict(self, df, target):
        """
        Using a fitted object, use the best model available to transform a
        dataframe not containing the target to a dataframe containing the
        predicted target values.

        Analagous to DFTransformer.transform

        Args:
            df (pandas.DataFrame): The dataframe to-be-predicted
            target: The target metric to be predicted. The output column for
                the data will be "predicted {target}".

        Returns:
            (pandas.DataFrame): The dataframe updated with predictions of the
                target property.

        """
        pass

    def transform(self, df, target):
        return self.predict(df, target)

    @property
    @abc.abstractmethod
    def features(self):
        """
        The features being used for machine learning.

        Returns:
            ([str]): The feature labels
        """
        pass

    @property
    @abc.abstractmethod
    def ml_data(self):
        """
        The raw ML-data being passed to the backend.

        Returns:
            (dict): At minimum, the raw X and y matrices being used to train.
                May also contain other data.
        """
        pass

    @property
    @abc.abstractmethod
    def best_pipeline(self):
        """
        The best pipeline returned by the automl backend. Should implement fit
        and predict methods and be able to make predictions.

        Returns:
            sklearn.pipeline.Pipeline or BaseEstimator:
        """
        pass

    @property
    @abc.abstractmethod
    def backend(self):
        """
        The raw, fitted backend object, if it exists.

        Returns:
            Backend object (e.g., TPOTClassifier)

        """
        pass

"""
Base classes, mixins, and other inheritables.
"""
import abc
import logging
from sklearn.base import BaseEstimator
from automatminer.utils.log import initialize_logger, \
    initialize_null_logger, AMM_LOGGER_BASENAME

__authors__ = ["<NAME> <<EMAIL>>", "<NAME> <<EMAIL>>"]


class LoggableMixin:
    """A mixin class for easy logging (or absence of it)."""

    @property
    def logger(self):
        """Get the class logger.
        If the logger is None, the logging calls will be redirected to a dummy
        logger that has no output.
        """
        if hasattr(self, "_logger"):
            return self._logger
        else:
            raise AttributeError("Loggable object has no _logger attribute!")

    @staticmethod
    def get_logger(logger, level=None):
        """Set the class logger.
        Args:
            logger (Logger, bool): A custom logger object to use for logging.
                Alternatively, if set to True, the default automatminer logger
                will be used. If set to False, then no logging will occur.
            level (int): The log level. For example logging.DEBUG.
        """
        # need comparison to True and False to avoid overwriting Logger objects
        if logger is True:
            logger = logging.getLogger(AMM_LOGGER_BASENAME)

            if not logger.handlers:
                initialize_logger(AMM_LOGGER_BASENAME, level=level)

        elif logger is False:
            logger = logging.getLogger(AMM_LOGGER_BASENAME + "_null")

            if not logger.handlers:
                initialize_null_logger(AMM_LOGGER_BASENAME)

        logger.setLevel(logging.INFO)
        return logger

    @property
    def _log_prefix(self):
        return self.__class__.__name__ + ": "


class DFTransformer(abc.ABC, BaseEstimator):
    """ A base class to allow easy transformation in the same way as
    TransformerMixin and BaseEstimator in sklearn, but for pandas dataframes.

    When implementing a base class adaptor, make sure to use @check_fitted
    and @set_fitted if necessary!
    """

    @abc.abstractmethod
    def fit(self, df, target, **fit_kwargs):
        """
        Fits the transformer to a dataframe, given a target.

        Args:
            df (pandas.DataFrame): The pandas dataframe to be fit.
            target (str): the target string specifying the ML target.
            fit_kwargs: Keyword paramters for fitting

        Returns:
            (DataFrameTransformer) This object (self)

        """
        pass

    @abc.abstractmethod
    def transform(self, df, target, **transform_kwargs):
        """
        Transforms a dataframe.

        Args:
            df (pandas.DataFrame): The pandas dataframe to be fit.
            target (str): the target string specifying the ML target.
            transform_kwargs: Keyword paramters for transforming

        Returns:
            (pandas.DataFrame): The transformed dataframe.

        """
        pass

    def fit_transform(self, df, target):
        """
        Combines the fitting and transformation of a dataframe.

        Args:
            df (pandas.DataFrame): The pandas dataframe to be fit.
            target (str): the target string specifying the ML target.

        Returns:
            (pandas.DataFrame): The transformed dataframe.

        """
        return self.fit(df, target).transform(df, target)


class DFMLAdaptor(DFTransformer):
    """
    A base class to adapt from an AutoML backend to a sklearn-style fit/predict
    scheme and add a few extensions for pandas dataframes.

    When implementing a base class adaptor, make sure to use @check_fitted
    and @set_fitted if necessary!
    """

    @abc.abstractmethod
    def predict(self, df, target):
        """
        Using a fitted object, use the best model available to transform a
        dataframe not containing the target to a dataframe containing the
        predicted target values.

        Analagous to DFTransformer.transform

        Args:
            df (pandas.DataFrame): The dataframe to-be-predicted
            target: The target metric to be predicted. The output column for
                the data will be "predicted {target}".

        Returns:
            (pandas.DataFrame): The dataframe updated with predictions of the
                target property.

        """
        pass

    def transform(self, df, target):
        return self.predict(df, target)

    @property
    @abc.abstractmethod
    def features(self):
        """
        The features being used for machine learning.

        Returns:
            ([str]): The feature labels
        """
        pass

    @property
    @abc.abstractmethod
    def ml_data(self):
        """
        The raw ML-data being passed to the backend.

        Returns:
            (dict): At minimum, the raw X and y matrices being used to train.
                May also contain other data.
        """
        pass

    @property
    @abc.abstractmethod
    def best_pipeline(self):
        """
        The best pipeline returned by the automl backend. Should implement fit
        and predict methods and be able to make predictions.

        Returns:
            sklearn.pipeline.Pipeline or BaseEstimator:
        """
        pass

    @property
    @abc.abstractmethod
    def backend(self):
        """
        The raw, fitted backend object, if it exists.

        Returns:
            Backend object (e.g., TPOTClassifier)

        """
        pass

Base classes, mixins, and other inheritables. A mixin class for easy logging (or absence of it). Get the class logger.         If the logger is None, the logging calls will be redirected to a dummy         logger that has no output. Set the class logger.         Args:             logger (Logger, bool): A custom logger object to use for logging.                 Alternatively, if set to True, the default automatminer logger                 will be used. If set to False, then no logging will occur.             level (int): The log level. For example logging.DEBUG. # need comparison to True and False to avoid overwriting Logger objects A base class to allow easy transformation in the same way as     TransformerMixin and BaseEstimator in sklearn, but for pandas dataframes.      When implementing a base class adaptor, make sure to use @check_fitted     and @set_fitted if necessary! Fits the transformer to a dataframe, given a target.          Args:             df (pandas.DataFrame): The pandas dataframe to be fit.             target (str): the target string specifying the ML target.             fit_kwargs: Keyword paramters for fitting          Returns:             (DataFrameTransformer) This object (self) Transforms a dataframe.          Args:             df (pandas.DataFrame): The pandas dataframe to be fit.             target (str): the target string specifying the ML target.             transform_kwargs: Keyword paramters for transforming          Returns:             (pandas.DataFrame): The transformed dataframe. Combines the fitting and transformation of a dataframe.          Args:             df (pandas.DataFrame): The pandas dataframe to be fit.             target (str): the target string specifying the ML target.          Returns:             (pandas.DataFrame): The transformed dataframe. A base class to adapt from an AutoML backend to a sklearn-style fit/predict     scheme and add a few extensions for pandas dataframes.      When implementing a base class adaptor, make sure to use @check_fitted     and @set_fitted if necessary! Using a fitted object, use the best model available to transform a         dataframe not containing the target to a dataframe containing the         predicted target values.          Analagous to DFTransformer.transform          Args:             df (pandas.DataFrame): The dataframe to-be-predicted             target: The target metric to be predicted. The output column for                 the data will be "predicted {target}".          Returns:             (pandas.DataFrame): The dataframe updated with predictions of the                 target property. The features being used for machine learning.          Returns:             ([str]): The feature labels The raw ML-data being passed to the backend.          Returns:             (dict): At minimum, the raw X and y matrices being used to train.                 May also contain other data. The best pipeline returned by the automl backend. Should implement fit         and predict methods and be able to make predictions.          Returns:             sklearn.pipeline.Pipeline or BaseEstimator: The raw, fitted backend object, if it exists.          Returns:             Backend object (e.g., TPOTClassifier)