import math


def calculate_icl_ex_jeffrey_hyperprior_undirected(partition):
    """
    Calculate the integrated complete likelihood (exact) from Etienne Come and Pierre Latouche
    Returns the result of formula (2.5):
    ICL_ex (Z,K) = log p(X,Z|K)
        = \sum_{k,l}^B\log \frac{\Gamma(\eta_{kl}^0+\zeta_{kl}^0)\Gamma(\eta_{kl})\Gamma(\zeta_{kl})}
        {\Gamma(\eta_{kl}+\zeta_{kl})\Gamma(\eta_{kl}^0)\Gamma(\zeta_{kl}^0)}
    + \log\frac{\Gamma(\sum_{r=1}^{B}n_k^0)\prod_{r=1}^B\Gamma(n_k)}
    {\Gamma(\sum_{r=1}^Bn_k)\prod_{r=1}^{B}\Gamma(n_k^0)}
    where
    n_k = n_k^0 + |b_k|,\\
    \eta_{kl} = \eta_{kl}^0 +e_{kl},\\
    \zeta_{kl} = \zeta_{kl}^0 + |b_k||b_l|-e_{kl}
    and n_k^0,\ \eta_{kl}^0 and \zeta_{kl}^0 are the hyperparameters of the prior distributions,
    which are set to 1/2 for a Jeffrey distribution.
    :param partition:
    :return:
    """
    likelihood = 0

    eta_kl_zero = .5
    zeta_kl_zero = .5
    n_k_zero = .5

    for from_block in range(partition.B):
        number_of_nodes_in_from_block = partition.get_number_of_nodes_in_block(
            from_block)
        for to_block in range(from_block + 1, partition.B):
            # first term
            eta_kl = eta_kl_zero + partition.get_edge_count(from_block, to_block)
            zeta_kl = zeta_kl_zero + number_of_nodes_in_from_block * partition.get_number_of_nodes_in_block(to_block) \
                - partition.get_edge_count(from_block, to_block)

            # math.lgamma(eta_kl_zero + zeta_kl_zero) = math.lgamma(1) = 0
            likelihood += math.lgamma(eta_kl) + math.lgamma(zeta_kl) \
                - math.lgamma(eta_kl + zeta_kl) - math.lgamma(eta_kl_zero) - math.lgamma(zeta_kl_zero)

        # add term for from_block->from_block
        # in undirected case those edges are counted twice cover this
        eta_kl = eta_kl_zero + partition.get_edge_count(from_block, from_block) / 2
        # and reduce the number of possibilities accordingly
        zeta_kl = zeta_kl_zero + number_of_nodes_in_from_block * (number_of_nodes_in_from_block + 1) / 2 \
            - partition.get_edge_count(from_block, from_block) / 2

        likelihood += math.lgamma(eta_kl) + math.lgamma(zeta_kl) \
            - math.lgamma(eta_kl + zeta_kl) - math.lgamma(eta_kl_zero) - math.lgamma(zeta_kl_zero)

        # calculate second term
        likelihood += math.lgamma(n_k_zero + number_of_nodes_in_from_block) - math.lgamma(n_k_zero)

    # add "global" terms of second term
    likelihood += math.lgamma(n_k_zero * partition.B) - math.lgamma(n_k_zero * partition.B + partition.get_number_of_nodes())

    return likelihood