# Path: lantern/analysis/frequency.py
# def frequency_analyze(text, n=1):
# def frequency_to_probability(frequency_map, decorator=lambda f: f):
# def index_of_coincidence(*texts):
# def _calculate_index_of_coincidence(frequency_map, length):
# def chi_squared(source_frequency, target_frequency):
# def _calculate_chi_squared(source_freq, target_prob, source_len):
# def _load_ngram(name):
# ENGLISH_IC = _calculate_index_of_coincidence(english.unigrams, sum(english.unigrams.values()))
# 
# Path: lantern/structures/dynamicdict.py
# class DynamicDict:
#     """Dictionary which builds values when they are accessed for the first time.
# 
#     Example:
#         >>> ngrams = DynamicDict({
#         ...     'trigrams': lambda: load_ngrams('trigrams'),
#         ...     'quadgrams': lambda: load_ngrams('quadgrams')
#         ... })
# 
#     Since trigrams and quadgrams are large files, its expensive to load them in
#     if theyre not needed. Using the DynamicDict ensures they are only loaded when
#     they are accessed for the first time.
#     """
# 
#     def __init__(self, builders={}):
#         """Instantiate dict with mapping of keys to builders.
# 
#         Args:
#             builders (dict): key to function mapping
#         """
#         self.builders = builders
# 
#     def __getattr__(self, name):
#         """Attempt to build values that are not already created."""
#         try:
#             attribute = self.builders[name]()
#         except KeyError:
#             raise AttributeError("'DynamicDict' object has no attribute '{}'".format(name))
#         else:
#             setattr(self, name, attribute)
#             return attribute
# 
# Path: lantern/util.py
# def remove(text, exclude):
#     """Remove ``exclude`` symbols from ``text``.
# 
#     Example:
#         >>> remove("example text", string.whitespace)
#         'exampletext'
# 
#     Args:
#         text (str): The text to modify
#         exclude (iterable): The symbols to exclude
# 
#     Returns:
#         ``text`` with ``exclude`` symbols removed
#     """
#     exclude = ''.join(str(symbol) for symbol in exclude)
#     return text.translate(str.maketrans('', '', exclude))
# 
# def iterate_ngrams(text, n):
#     """Generator to yield ngrams in ``text``.
# 
#     Example:
#         >>> for ngram in iterate_ngrams("example", 4):
#         ...     print(ngram)
#         exam
#         xamp
#         ampl
#         mple
# 
#     Args:
#         text (str): text to iterate over
#         n (int): size of window for iteration
# 
#     Returns:
#         Generator expression to yield the next ngram in the text
# 
#     Raises:
#         ValueError: If n is non positive
#     """
#     if n <= 0:
#         raise ValueError("n must be a positive integer")
# 
#     return [text[i: i + n] for i in range(len(text) - n + 1)]


import math
import string
from lantern.analysis import frequency
from lantern.structures import DynamicDict
from lantern.util import remove, iterate_ngrams


def NgramScorer(frequency_map):
    """Compute the score of a text by using the frequencies of ngrams.

    Example:
        >>> fitness = NgramScorer(english.unigrams)
        >>> fitness("ABC")
        -4.3622319742618245

    Args:
        frequency_map (dict): ngram to frequency mapping
    """
    # Calculate the log probability
    length = len(next(iter(frequency_map)))
    # TODO: 0.01 is a magic number. Needs to be better than that.
    floor = math.log10(0.01 / sum(frequency_map.values()))
    ngrams = frequency.frequency_to_probability(frequency_map, decorator=math.log10)

    def inner(text):
        # I dont like this, it is only for the .upper() to work,
        # But I feel as though this can be removed in later refactoring
        text = ''.join(text)
        text = remove(text.upper(), string.whitespace + string.punctuation)
        return sum(ngrams.get(ngram, floor) for ngram in iterate_ngrams(text, length))

    return inner


english = DynamicDict({

# Path: lantern/analysis/frequency.py
# def frequency_analyze(text, n=1):
# def frequency_to_probability(frequency_map, decorator=lambda f: f):
# def index_of_coincidence(*texts):
# def _calculate_index_of_coincidence(frequency_map, length):
# def chi_squared(source_frequency, target_frequency):
# def _calculate_chi_squared(source_freq, target_prob, source_len):
# def _load_ngram(name):
# ENGLISH_IC = _calculate_index_of_coincidence(english.unigrams, sum(english.unigrams.values()))
# 
# Path: lantern/structures/dynamicdict.py
# class DynamicDict:
#     """Dictionary which builds values when they are accessed for the first time.
# 
#     Example:
#         >>> ngrams = DynamicDict({
#         ...     'trigrams': lambda: load_ngrams('trigrams'),
#         ...     'quadgrams': lambda: load_ngrams('quadgrams')
#         ... })
# 
#     Since trigrams and quadgrams are large files, its expensive to load them in
#     if theyre not needed. Using the DynamicDict ensures they are only loaded when
#     they are accessed for the first time.
#     """
# 
#     def __init__(self, builders={}):
#         """Instantiate dict with mapping of keys to builders.
# 
#         Args:
#             builders (dict): key to function mapping
#         """
#         self.builders = builders
# 
#     def __getattr__(self, name):
#         """Attempt to build values that are not already created."""
#         try:
#             attribute = self.builders[name]()
#         except KeyError:
#             raise AttributeError("'DynamicDict' object has no attribute '{}'".format(name))
#         else:
#             setattr(self, name, attribute)
#             return attribute
# 
# Path: lantern/util.py
# def remove(text, exclude):
#     """Remove ``exclude`` symbols from ``text``.
# 
#     Example:
#         >>> remove("example text", string.whitespace)
#         'exampletext'
# 
#     Args:
#         text (str): The text to modify
#         exclude (iterable): The symbols to exclude
# 
#     Returns:
#         ``text`` with ``exclude`` symbols removed
#     """
#     exclude = ''.join(str(symbol) for symbol in exclude)
#     return text.translate(str.maketrans('', '', exclude))
# 
# def iterate_ngrams(text, n):
#     """Generator to yield ngrams in ``text``.
# 
#     Example:
#         >>> for ngram in iterate_ngrams("example", 4):
#         ...     print(ngram)
#         exam
#         xamp
#         ampl
#         mple
# 
#     Args:
#         text (str): text to iterate over
#         n (int): size of window for iteration
# 
#     Returns:
#         Generator expression to yield the next ngram in the text
# 
#     Raises:
#         ValueError: If n is non positive
#     """
#     if n <= 0:
#         raise ValueError("n must be a positive integer")
# 
#     return [text[i: i + n] for i in range(len(text) - n + 1)]


# Path: lantern/fitness/ngram.py
import math
import string
from lantern.analysis import frequency
from lantern.structures import DynamicDict
from lantern.util import remove, iterate_ngrams



def NgramScorer(frequency_map):
    """Compute the score of a text by using the frequencies of ngrams.

    Example:
        >>> fitness = NgramScorer(english.unigrams)
        >>> fitness("ABC")
        -4.3622319742618245

    Args:
        frequency_map (dict): ngram to frequency mapping
    """
    # Calculate the log probability
    length = len(next(iter(frequency_map)))
    # TODO: 0.01 is a magic number. Needs to be better than that.
    floor = math.log10(0.01 / sum(frequency_map.values()))
    ngrams = frequency.frequency_to_probability(frequency_map, decorator=math.log10)

    def inner(text):
        # I dont like this, it is only for the .upper() to work,
        # But I feel as though this can be removed in later refactoring
        text = ''.join(text)
        text = remove(text.upper(), string.whitespace + string.punctuation)
        return sum(ngrams.get(ngram, floor) for ngram in iterate_ngrams(text, length))

    return inner


english = DynamicDict({

    'unigrams': lambda: NgramScorer(frequency.english.unigrams),