import ast
import math
import collections
from radon.visitors import HalsteadVisitor, ComplexityVisitor
from radon.raw import analyze


# Halstead metrics
Halstead = collections.namedtuple('Halstead', 'h1 h2 N1 N2 vocabulary length '
                                              'calculated_length volume '
                                              'difficulty effort time bugs')


def h_visit(code):
    '''Compile the code into an AST tree and then pass it to
    :func:`~radon.metrics.h_visit_ast`.
    '''
    return h_visit_ast(ast.parse(code))


def h_visit_ast(ast_node):
    '''Visit the AST node using the :class:`~radon.visitors.HalsteadVisitor`
    visitor. A namedtuple with the following fields is returned:

        * h1: the number of distinct operators
        * h2: the number of distinct operands
        * N1: the total number of operators
        * N2: the total number of operands
        * h: the vocabulary, i.e. h1 + h2
        * N: the length, i.e. N1 + N2
        * calculated_length: h1 * log2(h1) + h2 * log2(h2)
        * volume: V = N * log2(h)
        * difficulty: D = h1 / 2 * N2 / h2
        * effort: E = D * V
        * time: T = E / 18 seconds
        * bugs: B = V / 3000 - an estimate of the errors in the implementation
    '''
    visitor = HalsteadVisitor.from_ast(ast_node)
    h1, h2 = visitor.distinct_operators, visitor.distinct_operands
    N1, N2 = visitor.operators, visitor.operands
    h = h1 + h2
    N = N1 + N2
    if all((h1, h2)):
        length = h1 * math.log(h1, 2) + h2 * math.log(h2, 2)
    else:
        length = 0
    volume = N * math.log(h, 2) if h != 0 else 0
    difficulty = (h1 * N2) / float(2 * h2) if h2 != 0 else 0
    effort = difficulty * volume
    return Halstead(
        h1, h2, N1, N2, h, N, length, volume, difficulty, effort,
        effort / 18., volume / 3000.
    )


def mi_compute(halstead_volume, complexity, sloc, comments):
    '''Compute the Maintainability Index (MI) given the Halstead Volume, the
    Cyclomatic Complexity, the SLOC number and the number of comment lines.
    Usually it is not used directly but instead
    :func:`~radon.metrics.mi_visit` is preferred.
    '''
    if any(metric <= 0 for metric in (halstead_volume, sloc)):
        return 100.
    sloc_scale = math.log(sloc)
    volume_scale = math.log(halstead_volume)
    comments_scale = math.sqrt(2.46 * math.radians(comments))
    # Non-normalized MI
    nn_mi = (171 - 5.2 * volume_scale - .23 * complexity - 16.2 * sloc_scale +
             50 * math.sin(comments_scale))
    return min(max(0., nn_mi * 100 / 171.), 100.)


def mi_parameters(code, count_multi=True):
    '''Given a source code snippet, compute the necessary parameters to
    compute the Maintainability Index metric. These include:

        * the Halstead Volume
        * the Cyclomatic Complexity
        * the number of LLOC (Logical Lines of Code)
        * the percent of lines of comment

    :param multi: If True, then count multiline strings as comment lines as
        well. This is not always safe because Python multiline strings are not
        always docstrings.
    '''
    ast_node = ast.parse(code)
    raw = analyze(code)
    comments_lines = raw.comments + (raw.multi if count_multi else 0)
    comments = comments_lines / float(raw.sloc) * 100 if raw.sloc != 0 else 0
    return (h_visit_ast(ast_node).volume,
            ComplexityVisitor.from_ast(ast_node).total_complexity, raw.lloc,
            comments)


def mi_visit(code, multi):
    '''Visit the code and compute the Maintainability Index (MI) from it.
    '''
    return mi_compute(*mi_parameters(code, multi))


def mi_rank(score):
    '''Rank the score with a letter:

        * A if :math:`\\text{score} > 19`;
        * B if :math:`9 < \\text{score} \le 19`;
        * C if :math:`\\text{score} \le 9`.
    '''
    return chr(65 + (9 - score >= 0) + (19 - score >= 0))
