from __future__ import unicode_literals

from snips_nlu.common.from_dict import FromDict
from snips_nlu.constants import (
    CUSTOM_ENTITY_PARSER_USAGE, NOISE, STEMS, STOP_WORDS, WORD_CLUSTERS)
from snips_nlu.entity_parser.custom_entity_parser import (
    CustomEntityParserUsage)
from snips_nlu.pipeline.configs import Config, ProcessingUnitConfig
from snips_nlu.resources import merge_required_resources


class LogRegIntentClassifierConfig(FromDict, ProcessingUnitConfig):
    """Configuration of a :class:`.LogRegIntentClassifier`"""

    # pylint: disable=line-too-long
    def __init__(self, data_augmentation_config=None, featurizer_config=None,
                 noise_reweight_factor=1.0):
        """
        Args:
            data_augmentation_config (:class:`IntentClassifierDataAugmentationConfig`):
                    Defines the strategy of the underlying data augmentation
            featurizer_config (:class:`FeaturizerConfig`): Configuration of the
                :class:`.Featurizer` used underneath
            noise_reweight_factor (float, optional): this parameter allows to
                change the weight of the None class. By default, the class
                weights are computed using a "balanced" strategy. The
                noise_reweight_factor allows to deviate from this strategy.
        """
        if data_augmentation_config is None:
            data_augmentation_config = IntentClassifierDataAugmentationConfig()
        if featurizer_config is None:
            featurizer_config = FeaturizerConfig()
        self._data_augmentation_config = None
        self.data_augmentation_config = data_augmentation_config
        self._featurizer_config = None
        self.featurizer_config = featurizer_config
        self.noise_reweight_factor = noise_reweight_factor

    # pylint: enable=line-too-long

    @property
    def data_augmentation_config(self):
        return self._data_augmentation_config

    @data_augmentation_config.setter
    def data_augmentation_config(self, value):
        if isinstance(value, dict):
            self._data_augmentation_config = \
                IntentClassifierDataAugmentationConfig.from_dict(value)
        elif isinstance(value, IntentClassifierDataAugmentationConfig):
            self._data_augmentation_config = value
        else:
            raise TypeError("Expected instance of "
                            "IntentClassifierDataAugmentationConfig or dict"
                            "but received: %s" % type(value))

    @property
    def featurizer_config(self):
        return self._featurizer_config

    @featurizer_config.setter
    def featurizer_config(self, value):
        if isinstance(value, dict):
            self._featurizer_config = \
                FeaturizerConfig.from_dict(value)
        elif isinstance(value, FeaturizerConfig):
            self._featurizer_config = value
        else:
            raise TypeError("Expected instance of FeaturizerConfig or dict"
                            "but received: %s" % type(value))

    @property
    def unit_name(self):
        from snips_nlu.intent_classifier import LogRegIntentClassifier
        return LogRegIntentClassifier.unit_name

    def get_required_resources(self):
        resources = self.data_augmentation_config.get_required_resources()
        resources = merge_required_resources(
            resources, self.featurizer_config.get_required_resources())
        return resources

    def to_dict(self):
        return {
            "unit_name": self.unit_name,
            "data_augmentation_config":
                self.data_augmentation_config.to_dict(),
            "featurizer_config": self.featurizer_config.to_dict(),
            "noise_reweight_factor": self.noise_reweight_factor,
        }


class IntentClassifierDataAugmentationConfig(FromDict, Config):
    """Configuration used by a :class:`.LogRegIntentClassifier` which defines
        how to augment data to improve the training of the classifier"""

    def __init__(self, min_utterances=20, noise_factor=5,
                 add_builtin_entities_examples=True, unknown_word_prob=0,
                 unknown_words_replacement_string=None,
                 max_unknown_words=None):
        """
        Args:
            min_utterances (int, optional): The minimum number of utterances to
                automatically generate for each intent, based on the existing
                utterances. Default is 20.
            noise_factor (int, optional): Defines the size of the noise to
                generate to train the implicit *None* intent, as a multiplier
                of the average size of the other intents. Default is 5.
            add_builtin_entities_examples (bool, optional): If True, some
                builtin entity examples will be automatically added to the
                training data. Default is True.
        """
        self.min_utterances = min_utterances
        self.noise_factor = noise_factor
        self.add_builtin_entities_examples = add_builtin_entities_examples
        self.unknown_word_prob = unknown_word_prob
        self.unknown_words_replacement_string = \
            unknown_words_replacement_string
        if max_unknown_words is not None and max_unknown_words < 0:
            raise ValueError("max_unknown_words must be None or >= 0")
        self.max_unknown_words = max_unknown_words
        if unknown_word_prob > 0 and unknown_words_replacement_string is None:
            raise ValueError("unknown_word_prob is positive (%s) but the "
                             "replacement string is None" % unknown_word_prob)

    @staticmethod
    def get_required_resources():
        return {
            NOISE: True,
            STOP_WORDS: True
        }

    def to_dict(self):
        return {
            "min_utterances": self.min_utterances,
            "noise_factor": self.noise_factor,
            "add_builtin_entities_examples":
                self.add_builtin_entities_examples,
            "unknown_word_prob": self.unknown_word_prob,
            "unknown_words_replacement_string":
                self.unknown_words_replacement_string,
            "max_unknown_words": self.max_unknown_words
        }


class FeaturizerConfig(FromDict, ProcessingUnitConfig):
    """Configuration of a :class:`.Featurizer` object"""

    # pylint: disable=line-too-long
    def __init__(self, tfidf_vectorizer_config=None,
                 cooccurrence_vectorizer_config=None,
                 pvalue_threshold=0.4,
                 added_cooccurrence_feature_ratio=0):
        """
        Args:
            tfidf_vectorizer_config (:class:`.TfidfVectorizerConfig`, optional):
                empty configuration of the featurizer's
                :attr:`tfidf_vectorizer`
            cooccurrence_vectorizer_config: (:class:`.CooccurrenceVectorizerConfig`, optional):
                configuration of the featurizer's
                :attr:`cooccurrence_vectorizer`
            pvalue_threshold (float): after fitting the training set to
                extract tfidf features, a univariate feature selection is
                applied. Features are tested for independence using a Chi-2
                test, under the null hypothesis that each feature should be
                equally present in each class. Only features having a p-value
                lower than the threshold are kept
            added_cooccurrence_feature_ratio (float, optional): proportion of
                cooccurrence features to add with respect to the number of
                tfidf features. For instance with a ratio of 0.5, if 100 tfidf
                features are remaining after feature selection, a maximum of 50
                cooccurrence features will be added
        """
        self.pvalue_threshold = pvalue_threshold
        self.added_cooccurrence_feature_ratio = \
            added_cooccurrence_feature_ratio

        if tfidf_vectorizer_config is None:
            tfidf_vectorizer_config = TfidfVectorizerConfig()
        elif isinstance(tfidf_vectorizer_config, dict):
            tfidf_vectorizer_config = TfidfVectorizerConfig.from_dict(
                tfidf_vectorizer_config)
        self.tfidf_vectorizer_config = tfidf_vectorizer_config

        if cooccurrence_vectorizer_config is None:
            cooccurrence_vectorizer_config = CooccurrenceVectorizerConfig()
        elif isinstance(cooccurrence_vectorizer_config, dict):
            cooccurrence_vectorizer_config = CooccurrenceVectorizerConfig \
                .from_dict(cooccurrence_vectorizer_config)
        self.cooccurrence_vectorizer_config = cooccurrence_vectorizer_config

    # pylint: enable=line-too-long

    @property
    def unit_name(self):
        from snips_nlu.intent_classifier import Featurizer
        return Featurizer.unit_name

    def get_required_resources(self):
        required_resources = self.tfidf_vectorizer_config \
            .get_required_resources()
        if self.cooccurrence_vectorizer_config:
            required_resources = merge_required_resources(
                required_resources,
                self.cooccurrence_vectorizer_config.get_required_resources())
        return required_resources

    def to_dict(self):
        return {
            "unit_name": self.unit_name,
            "pvalue_threshold": self.pvalue_threshold,
            "added_cooccurrence_feature_ratio":
                self.added_cooccurrence_feature_ratio,
            "tfidf_vectorizer_config": self.tfidf_vectorizer_config.to_dict(),
            "cooccurrence_vectorizer_config":
                self.cooccurrence_vectorizer_config.to_dict(),
        }


class TfidfVectorizerConfig(FromDict, ProcessingUnitConfig):
    """Configuration of a :class:`.TfidfVectorizerConfig` object"""

    def __init__(self, word_clusters_name=None, use_stemming=False):
        """
        Args:
            word_clusters_name (str, optional): if a word cluster name is
                provided then the featurizer will use the word clusters IDs
                detected in the utterances and add them to the utterance text
                before computing the tfidf. Default to None
            use_stemming (bool, optional): use stemming before computing the
                tfdif. Defaults to False (no stemming used)
        """
        self.word_clusters_name = word_clusters_name
        self.use_stemming = use_stemming

    @property
    def unit_name(self):
        from snips_nlu.intent_classifier import TfidfVectorizer
        return TfidfVectorizer.unit_name

    def get_required_resources(self):
        resources = {STEMS: True if self.use_stemming else False}
        if self.word_clusters_name:
            resources[WORD_CLUSTERS] = {self.word_clusters_name}
        return resources

    def to_dict(self):
        return {
            "unit_name": self.unit_name,
            "word_clusters_name": self.word_clusters_name,
            "use_stemming": self.use_stemming
        }


class CooccurrenceVectorizerConfig(FromDict, ProcessingUnitConfig):
    """Configuration of a :class:`.CooccurrenceVectorizer` object"""

    def __init__(self, window_size=None, unknown_words_replacement_string=None,
                 filter_stop_words=True, keep_order=True):
        """
        Args:
            window_size (int, optional): if provided, word cooccurrences will
                be taken into account only in a context window of size
                :attr:`window_size`. If the window size is 3 then given a word
                w[i], the vectorizer will only extract the following pairs:
                (w[i], w[i + 1]), (w[i], w[i + 2]) and (w[i], w[i + 3]).
                Defaults to None, which means that we consider all words
            unknown_words_replacement_string (str, optional)
            filter_stop_words (bool, optional): if True, stop words are ignored
                when computing cooccurrences
            keep_order (bool, optional): if True then cooccurrence are computed
                taking the words order into account, which means the pairs
                (w1, w2) and (w2, w1) will count as two separate features.
                Defaults to `True`.
        """
        self.window_size = window_size
        self.unknown_words_replacement_string = \
            unknown_words_replacement_string
        self.filter_stop_words = filter_stop_words
        self.keep_order = keep_order

    @property
    def unit_name(self):
        from snips_nlu.intent_classifier import CooccurrenceVectorizer
        return CooccurrenceVectorizer.unit_name

    def get_required_resources(self):
        return {
            STOP_WORDS: self.filter_stop_words,
            # We require the parser to be trained without stems because we
            # don't normalize and stem when processing in the
            # CooccurrenceVectorizer (in order to run the builtin and
            # custom parser on the same unormalized input).
            # Requiring no stems ensures we'll be able to parse the unstemmed
            # input
            CUSTOM_ENTITY_PARSER_USAGE: CustomEntityParserUsage.WITHOUT_STEMS
        }

    def to_dict(self):
        return {
            "unit_name": self.unit_name,
            "unknown_words_replacement_string":
                self.unknown_words_replacement_string,
            "window_size": self.window_size,
            "filter_stop_words": self.filter_stop_words,
            "keep_order": self.keep_order
        }
