# -*- coding: utf-8 -*-

import logging
import sys
from collections import Counter
from math import ceil
from typing import AnyStr, Dict, Iterable, List, Optional, Tuple, Union

import numpy as np
from joblib import Parallel, delayed

from gseapy.stats import multiple_testing_correction


def enrichment_score(
    gene_list: Iterable[str],
    correl_vector: Iterable[float],
    gene_set: Dict[str, List[str]],
    weight: float = 1.0,
    nperm: int = 1000,
    seed: int = 123,
    single: bool = False,
    scale: bool = False,
):
    """This is the most important function of GSEApy. It has the same algorithm with GSEA and ssGSEA.

    :param gene_list:       The ordered gene list gene_name_list, rank_metric.index.values
    :param gene_set:        gene_sets in gmt file, please use gsea_gmt_parser to get gene_set.
    :param weight:  It's the same with gsea's weighted_score method. Weighting by the correlation
                            is a very reasonable choice that allows significant gene sets with less than perfect coherence.
                            options: 0(classic),1,1.5,2. default:1. if one is interested in penalizing sets for lack of
                            coherence or to discover sets with any type of nonrandom distribution of tags, a value p < 1
                            might be appropriate. On the other hand, if one uses sets with large number of genes and only
                            a small subset of those is expected to be coherent, then one could consider using p > 1.
                            Our recommendation is to use p = 1 and use other settings only if you are very experienced
                            with the method and its behavior.

    :param correl_vector:   A vector with the correlations (e.g. signal to noise scores) corresponding to the genes in
                            the gene list. Or rankings, rank_metric.values
    :param nperm:           Only use this parameter when computing esnull for statistical testing. Set the esnull value
                            equal to the permutation number.
    :param seed:            Random state for initializing gene list shuffling. Default: seed=None

    :return:

    ES: Enrichment score (real number between -1 and +1)

    ESNULL: Enrichment score calculated from random permutations.

    Hits_Indices: Index of a gene in gene_list, if gene is included in gene_set.

    RES: Numerical vector containing the running enrichment score for all locations in the gene list .

    """
    N = len(gene_list)
    # Test whether each element of a 1-D array is also present in a second array
    # It's more intuitive here than original enrichment_score source code.
    # use .astype to covert bool to integer
    tag_indicator = np.in1d(gene_list, gene_set, assume_unique=True).astype(
        int
    )  # notice that the sign is 0 (no tag) or 1 (tag)

    if weight == 0:
        correl_vector = np.repeat(1, N)
    else:
        correl_vector = np.abs(correl_vector) ** weight

    # get indices of tag_indicator
    hit_ind = np.flatnonzero(tag_indicator).tolist()
    # if used for compute esnull, set esnull equal to permutation number, e.g. 1000
    # else just compute enrichment scores
    # set axis to 1, because we have 2D array
    axis = 1
    tag_indicator = np.tile(tag_indicator, (nperm + 1, 1))
    correl_vector = np.tile(correl_vector, (nperm + 1, 1))
    # gene list permutation
    rs = np.random.RandomState(seed)
    for i in range(nperm):
        rs.shuffle(tag_indicator[i])
    # np.apply_along_axis(rs.shuffle, 1, tag_indicator)

    Nhint = tag_indicator.sum(axis=axis, keepdims=True)
    sum_correl_tag = np.sum(correl_vector * tag_indicator, axis=axis, keepdims=True)
    # compute ES score, the code below is identical to gsea enrichment_score method.
    no_tag_indicator = 1 - tag_indicator
    Nmiss = N - Nhint
    norm_tag = 1.0 / sum_correl_tag
    norm_no_tag = 1.0 / Nmiss

    RES = np.cumsum(
        tag_indicator * correl_vector * norm_tag - no_tag_indicator * norm_no_tag,
        axis=axis,
    )

    if scale:
        RES = RES / N
    if single:
        es_vec = RES.sum(axis=axis)
    else:
        max_ES, min_ES = RES.max(axis=axis), RES.min(axis=axis)
        es_vec = np.where(np.abs(max_ES) > np.abs(min_ES), max_ES, min_ES)
    # extract values
    es, esnull, RES = es_vec[-1], es_vec[:-1], RES[-1, :]

    return es, esnull, hit_ind, RES


def enrichment_score_tensor(
    gene_mat,
    cor_mat,
    gene_sets,
    weighted_score_type,
    nperm=1000,
    seed=None,
    single=False,
    scale=False,
):
    """Next generation algorithm of GSEA and ssGSEA. Works for 3d array

    :param gene_mat:        the ordered gene list(vector) with or without gene indices matrix.
    :param cor_mat:         correlation vector or matrix  (e.g. signal to noise scores)
                            corresponding to the genes in the gene list or matrix.
    :param dict gene_sets:  gmt file dict.
    :param float weighted_score_type:     weighting by the correlation.
                            options: 0(classic), 1, 1.5, 2. default:1 for GSEA and 0.25 for ssGSEA.
    :param int nperm:       permutation times.
    :param bool scale:      If True, normalize the scores by number of genes_mat.
    :param bool single:     If True, use ssGSEA algorithm, otherwise use GSEA.
    :param seed:              Random state for initialize gene list shuffling.
                            Default: seed=None
    :return: a tuple contains::

             | ES: Enrichment score (real number between -1 and +1), for ssGSEA, set scale eq to True.
             | ESNULL: Enrichment score calculated from random permutation.
             | Hits_Indices: Indices of genes if genes are included in gene_set.
             | RES: The running enrichment score for all locations in the gene list.

    """
    rs = np.random.RandomState(seed)
    # gene_mat -> 1d: prerank, ssSSEA or 2d: GSEA
    keys = sorted(gene_sets.keys())

    if weighted_score_type == 0:
        # don't bother doing calcuation, just set to 1
        cor_mat = np.ones(cor_mat.shape)
    elif weighted_score_type > 0:
        pass
    else:
        logging.error("Using negative values of weighted_score_type, not allowed")
        raise ValueError("weighted_score_type should be postive numerics")

    cor_mat = np.abs(cor_mat)
    if cor_mat.ndim == 1:
        # ssGSEA or Prerank
        # genestes->M, genes->N, perm-> axis=2
        N, M = len(gene_mat), len(keys)
        # generate gene hits matrix
        # for 1d ndarray of gene_mat, set assume_unique=True,
        # means the input arrays are both assumed to be unique,
        # which can speed up the calculation.
        tag_indicator = np.vstack(
            [np.in1d(gene_mat, gene_sets[key], assume_unique=True) for key in keys]
        )
        tag_indicator = tag_indicator.astype(int)
        # index of hits
        hit_ind = [np.flatnonzero(tag).tolist() for tag in tag_indicator]
        # generate permutated hits matrix
        perm_tag_tensor = np.repeat(tag_indicator, nperm + 1).reshape((M, N, nperm + 1))
        # shuffle matrix, last matrix is not shuffled when nperm > 0
        if nperm:
            np.apply_along_axis(
                lambda x: np.apply_along_axis(rs.shuffle, 0, x),
                1,
                perm_tag_tensor[:, :, :-1],
            )
        # missing hits
        no_tag_tensor = 1 - perm_tag_tensor
        # calculate numerator, denominator of each gene hits
        rank_alpha = (
            perm_tag_tensor * cor_mat[np.newaxis, :, np.newaxis]
        ) ** weighted_score_type

    elif cor_mat.ndim == 2:
        # GSEA
        # 2d ndarray, gene_mat and cor_mat are shuffled already
        # reshape matrix
        cor_mat = cor_mat.T
        # gene_mat is a tuple contains (gene_name, permuate_gene_name_indices)
        genes, genes_ind = gene_mat
        # genestes->M, genes->N, perm-> axis=2
        # don't use assume_unique=True in 2d array when use np.isin().
        # elements in gene_mat are not unique, or will cause unwanted results
        tag_indicator = np.vstack(
            [np.in1d(genes, gene_sets[key], assume_unique=True) for key in keys]
        )
        tag_indicator = tag_indicator.astype(int)
        perm_tag_tensor = np.stack(
            [tag.take(genes_ind).T for tag in tag_indicator], axis=0
        )
        # index of hits
        hit_ind = [np.flatnonzero(tag).tolist() for tag in perm_tag_tensor[:, :, -1]]
        # nohits
        no_tag_tensor = 1 - perm_tag_tensor
        # calculate numerator, denominator of each gene hits
        rank_alpha = (
            perm_tag_tensor * cor_mat[np.newaxis, :, :]
        ) ** weighted_score_type
    else:
        logging.error("Program die because of unsupported input")
        raise ValueError("Correlation vector or matrix (cor_mat) is not supported")

    # Nhint = tag_indicator.sum(1)
    # Nmiss =  N - Nhint
    axis = 1
    P_GW_denominator = np.sum(rank_alpha, axis=axis, keepdims=True)
    P_NG_denominator = np.sum(no_tag_tensor, axis=axis, keepdims=True)
    REStensor = np.cumsum(
        rank_alpha / P_GW_denominator - no_tag_tensor / P_NG_denominator, axis=axis
    )
    # ssGSEA: scale es by gene numbers ?
    # https://gist.github.com/gaoce/39e0907146c752c127728ad74e123b33
    if scale:
        REStensor = REStensor / len(gene_mat)
    if single:
        # ssGSEA
        esmatrix = REStensor.sum(axis=axis)
    else:
        # GSEA
        esmax, esmin = REStensor.max(axis=axis), REStensor.min(axis=axis)
        esmatrix = np.where(np.abs(esmax) > np.abs(esmin), esmax, esmin)

    es, esnull, RES = esmatrix[:, -1], esmatrix[:, :-1], REStensor[:, :, -1]

    return es, esnull, hit_ind, RES


def fast_ssgsea(tag_indicator, ranking):
    """
    tag_indicator: see `enrichment_score()`
    ranking: ranking values
    """
    n = len(ranking)  # number of genes
    k = tag_indicator.sum()  # number of overalped genes
    idxs = np.flatnonzero(tag_indicator)  # indices low to high
    step_cdf_in = np.sum(ranking[idxs] * (n - idxs)) / np.sum(ranking[idxs])
    step_cdf_out = (n * (n + 1) / 2 - np.sum(n - idxs)) / (n - k)
    es = step_cdf_in - step_cdf_out
    return es


def ranking_metric_tensor(
    exprs,
    method,
    permutation_num,
    pos,
    neg,
    classes,
    ascending,
    seed=None,
    skip_last=False,
):
    """Build shuffled ranking matrix when permutation_type eq to phenotype.
    Works for 3d array.

    :param exprs:   gene_expression DataFrame, gene_name indexed.
    :param str method:  calculate correlation or ranking. methods including:
                        1. 'signal_to_noise' (s2n) or 'abs_signal_to_noise' (abs_s2n).
                        2. 't_test'.
                        3. 'ratio_of_classes' (also referred to as fold change).
                        4. 'diff_of_classes'.
                        5. 'log2_ratio_of_classes'.
    :param int permuation_num: how many times of classes is being shuffled
    :param str pos: one of labels of phenotype's names.
    :param str neg: one of labels of phenotype's names.
    :param list classes:  a list of phenotype labels, to specify which column of
                          dataframe belongs to what class of phenotype.
    :param bool ascending:  bool. Sort ascending vs. descending.
    :param seed: random_state seed
    :param bool skip_last: (internal use only) whether to skip the permutation of the last rankings.

    :return:
             returns two 2d ndarray with shape (nperm, gene_num).

             | cor_mat_indices: the indices of sorted and permutated (exclude last row) ranking matrix.
             | cor_mat: sorted and permutated (exclude last row) ranking matrix.

    """
    rs = np.random.RandomState(seed)
    # S: samples, G: gene number
    G, S = exprs.shape
    # genes = exprs.index.values
    expr_mat = exprs.values.T
    perm_cor_tensor = np.tile(expr_mat, (permutation_num, 1, 1))
    if skip_last:
        # random shuffle on the first dim, the last matrix (expr_mat) is not shuffled
        for arr in perm_cor_tensor[:-1]:
            rs.shuffle(arr)
    else:
        for arr in perm_cor_tensor:
            rs.shuffle(arr)
    # metrics
    classes = np.array(classes)
    pos = classes == pos
    neg = classes == neg
    n_pos = np.sum(pos)
    n_neg = np.sum(neg)
    pos_cor_mean = perm_cor_tensor[:, pos, :].mean(axis=1)
    neg_cor_mean = perm_cor_tensor[:, neg, :].mean(axis=1)
    pos_cor_std = perm_cor_tensor[:, pos, :].std(axis=1, ddof=1)
    neg_cor_std = perm_cor_tensor[:, neg, :].std(axis=1, ddof=1)

    if method in ["signal_to_noise", "s2n"]:
        cor_mat = (pos_cor_mean - neg_cor_mean) / (pos_cor_std + neg_cor_std)
    elif method in ["abs_signal_to_noise", "abs_s2n"]:
        cor_mat = np.abs((pos_cor_mean - neg_cor_mean) / (pos_cor_std + neg_cor_std))
    elif method == "t_test":
        denom = np.sqrt((pos_cor_std**2) / n_pos + (neg_cor_std**2) / n_neg)
        cor_mat = (pos_cor_mean - neg_cor_mean) / denom
    elif method == "ratio_of_classes":
        cor_mat = pos_cor_mean / neg_cor_mean
    elif method == "diff_of_classes":
        cor_mat = pos_cor_mean - neg_cor_mean
    elif method == "log2_ratio_of_classes":
        cor_mat = np.log2(pos_cor_mean / neg_cor_mean)
    else:
        logging.error("Please provide correct method name!!!")
        raise LookupError("Input method: %s is not supported" % method)
    # return matix[nperm+1, perm_cors]
    cor_mat_ind = cor_mat.argsort()
    # ndarray: sort in place
    cor_mat.sort()
    # genes_mat = genes.take(cor_mat_ind)
    if ascending:
        return cor_mat_ind, cor_mat
    # descending order of ranking and genes
    # return genes_mat[:,::-1], cor_mat[:,::-1]
    return cor_mat_ind[:, ::-1], cor_mat[:, ::-1]


def gsea_compute_tensor(
    data,
    gmt,
    n,
    weighted_score_type,
    permutation_type,
    method,
    pheno_pos,
    pheno_neg,
    classes,
    ascending,
    processes=1,
    seed=None,
    single=False,
    scale=False,
):
    """compute enrichment scores and enrichment nulls.
    This function will split large array into smaller pieces to advoid memroy overflow.

     :param data: preprocessed expression dataframe or a pre-ranked file if prerank=True.
     :param dict gmt: all gene sets in .gmt file. need to call load_gmt() to get results.
     :param int n: permutation number. default: 1000.
     :param str method: ranking_metric method. see above.
     :param str pheno_pos: one of labels of phenotype's names.
     :param str pheno_neg: one of labels of phenotype's names.
     :param list classes: a list of phenotype labels, to specify which column of dataframe belongs to what category of phenotype.
     :param float weighted_score_type: default:1
     :param bool ascending: sorting order of rankings. Default: False.
     :param seed: random seed. Default: np.random.RandomState()
     :param bool scale: if true, scale es by gene number.

     :return: a tuple contains::

             | zipped results of es, nes, pval, fdr.
             | nested list of hit indices of input gene_list.
             | nested list of ranked enrichment score of each input gene_sets.
             | list of enriched terms

    """
    w = weighted_score_type
    subsets = sorted(gmt.keys())
    genes_mat, cor_mat = data.index.values, data.values
    base = 5 if data.shape[0] >= 5000 else 10
    ## phenotype permutation
    np.random.seed(seed)  # control the ranodm numbers

    if permutation_type == "phenotype":
        # shuffling classes and generate random correlation rankings
        logging.debug("Start to permutate classes..............................")

        if (n + 1) % base == 0:  # n+1: last permute is for orignial ES calculation
            num_bases = [base] * ((n + 1) // base)
            skip_last = [0] * (n // base) + [1]  # last is not permuted
        else:
            num_bases = [base] * ((n + 1) // base) + [(n + 1) % base]
            skip_last = [0] * ((n + 1) // base) + [(n + 1) % base]
        random_seeds = np.random.randint(np.iinfo(np.int32).max, size=len(num_bases))
        genes_ind = []
        cor_mat = []
        # split permutation array into smaller blocks to save memory
        temp_rnk = Parallel(n_jobs=processes)(
            delayed(ranking_metric_tensor)(
                data, method, b, pheno_pos, pheno_neg, classes, ascending, se, skip
            )
            for b, skip, se in zip(num_bases, skip_last, random_seeds)
        )

        for k, temp in enumerate(temp_rnk):
            gi, cor = temp
            genes_ind.append(gi)
            cor_mat.append(cor)
        genes_ind, cor_mat = np.vstack(genes_ind), np.vstack(cor_mat)
        # convert to tuple
        genes_mat = (data.index.values, genes_ind)

    logging.debug("Start to compute es and esnulls........................")
    # Prerank, ssGSEA, GSEA
    es = []
    RES = []
    hit_ind = []
    esnull = []
    temp_esnu = []

    # split gmt dataset, too
    block = ceil(len(subsets) / base)
    random_seeds = np.random.randint(np.iinfo(np.int32).max, size=block)
    # split large array into smaller blocks to avoid memory overflow
    i, m = 1, 0
    gmt_block = []
    while i <= block:
        # you have to reseed, or all your processes are sharing the same seed value
        rs = random_seeds[i - 1]
        gmtrim = {k: gmt.get(k) for k in subsets[m : base * i]}
        gmt_block.append(gmtrim)
        m = base * i
        i += 1
    ## if permutation_type == "phenotype": n = 0
    ## NOTE for GSEA: cor_mat is 2d array, it won't permute again when call enrichment_score_tensor
    temp_esnu = Parallel(n_jobs=processes)(
        delayed(enrichment_score_tensor)(
            genes_mat, cor_mat, gmtrim, w, n, rs, single, scale
        )
        for gmtrim, rs in zip(gmt_block, random_seeds)
    )

    # esn is a list, don't need to use append method.
    for si, temp in enumerate(temp_esnu):
        # e, enu, hit, rune = temp.get()
        e, enu, hit, rune = temp
        esnull.append(enu)
        es.append(e)
        RES.append(rune)
        hit_ind += hit
    # concate results
    es, esnull, RES = np.hstack(es), np.vstack(esnull), np.vstack(RES)

    return gsea_significance(es, esnull), hit_ind, RES, subsets


def gsea_compute(
    data,
    gmt,
    n,
    weighted_score_type,
    permutation_type,
    method,
    pheno_pos,
    pheno_neg,
    classes,
    ascending,
    processes=1,
    seed=None,
    single=False,
    scale=False,
):
    """compute enrichment scores and enrichment nulls.

    :param data: preprocessed expression dataframe or a pre-ranked file if prerank=True.
    :param dict gmt: all gene sets in .gmt file. need to call load_gmt() to get results.
    :param int n: permutation number. default: 1000.
    :param str method: ranking_metric method. see above.
    :param str pheno_pos: one of labels of phenotype's names.
    :param str pheno_neg: one of labels of phenotype's names.
    :param list classes: a list of phenotype labels, to specify which column of dataframe belongs to what category of phenotype.
    :param float weighted_score_type: default:1
    :param bool ascending: sorting order of rankings. Default: False.
    :param seed: random seed. Default: np.random.RandomState()
    :param bool scale: if true, scale es by gene number.

    :return: a tuple contains::

            | zipped results of es, nes, pval, fdr.
            | nested list of hit indices of input gene_list.
            | nested list of ranked enrichment score of each input gene_sets.
            | list of enriched terms

    """

    w = weighted_score_type
    subsets = sorted(gmt.keys())
    es = []
    RES = []
    hit_ind = []
    esnull = [[] for a in range(len(subsets))]
    np.random.seed(seed)  # control the ranodm numbers
    logging.debug("Start to compute enrichment scores......................")

    if permutation_type == "phenotype":
        logging.debug("Start to permutate classes..............................")
        # this version won't split large array into smaller ones
        genes_mat, cor_mat = ranking_metric_tensor(
            exprs=data,
            method=method,
            permutation_num=n + 1,
            pos=pheno_pos,
            neg=pheno_neg,
            classes=classes,
            ascending=ascending,
            seed=seed,
            skip_last=True,
        )

        # compute es, esnulls. hits, RES
        logging.debug("Start to compute enrichment nulls.......................")
        es, esnull, hit_ind, RES = enrichment_score_tensor(
            gene_mat=genes_mat,
            cor_mat=cor_mat,
            gene_sets=gmt,
            weighted_score_type=w,
            nperm=n,
            seed=seed,
            single=False,
            scale=False,
        )

    else:
        # Prerank, ssGSEA, GSEA with gene_set permutation
        gl, cor_vec = data.index.values, data.values
        logging.debug("Start to compute es and esnulls........................")
        ## this version don't split large array into smaller ones
        # es, esnull, hit_ind, RES = enrichment_score_tensor(gene_mat=gl,
        #                                                    cor_mat=cor_vec,
        #                                                    gene_sets=gmt,
        #                                                    weighted_score_type=w,
        #                                                    nperm=n, rs=rs
        #                                                    single=single, scale=scale)
        temp_esnu = []
        # you have to reseed, or all your processes are sharing the same seed value
        # np.random.seed(seed)
        random_seeds = np.random.randint(np.iinfo(np.int32).max, size=len(subsets))
        temp_esnu = Parallel(n_jobs=processes)(
            delayed(enrichment_score)(
                gl, cor_vec, gmt.get(subset), w, n, rs, single, scale
            )
            for subset, rs in zip(subsets, random_seeds)
        )
        # esn is a list, don't need to use append method.
        for si, temp in enumerate(temp_esnu):
            e, enu, hit, rune = temp
            esnull[si] = enu
            es.append(e)
            RES.append(rune)
            hit_ind.append(hit)

    return gsea_significance(es, esnull), hit_ind, RES, subsets


def normalize(es, esnull):
    """normalize the ES(S,pi) and the observed ES(S), separately rescaling
    the positive and negative scores by dividing the mean of the ES(S,pi).

    return: NES, NESnull
    """

    nEnrichmentScores = np.zeros(es.shape)
    nEnrichmentNulls = np.zeros(esnull.shape)
    # esnullmean = np.zeros(es.shape)
    # # calculate nESnulls
    # for i in range(esnull.shape[0]):
    #     # NES
    #     enrNull = esnull[i]
    #     if es[i] >= 0:
    #         mes = enrNull[enrNull >= 0].mean()
    #         nEnrichmentScores[i] = es[i] / mes
    #     else:
    #         mes = enrNull[enrNull < 0 ].mean()
    #         nEnrichmentScores[i] = - es[i] / mes
    #     esnullmean[i] = mes

    #     # NESnull
    #     for j in range(esnull.shape[1]):
    #         if esnull[i,j] >= 0:
    #             nEnrichmentNulls[i,j] = esnull[i,j] / esnullmean[i]
    #         else:
    #             nEnrichmentNulls[i,j] = - esnull[i,j] / esnullmean[i]

    esnull_pos = np.ma.MaskedArray(esnull, mask=(esnull < 0)).mean(axis=1)
    esnull_neg = np.ma.MaskedArray(esnull, mask=(esnull >= 0)).mean(axis=1)
    esnull_pos = np.array(esnull_pos)
    esnull_neg = np.array(esnull_neg)
    # NES
    nEnrichmentScores = np.where(es >= 0, es / esnull_pos, -es / esnull_neg)
    # NES_NULL
    nEnrichmentNulls = np.where(
        esnull >= 0,
        esnull / esnull_pos[:, np.newaxis],
        -esnull / esnull_neg[:, np.newaxis],
    )

    return nEnrichmentScores, nEnrichmentNulls


def gsea_pval(es, esnull):
    """Compute nominal p-value.

    From article (PNAS):
    estimate nominal p-value for S from esnull by using the positive
    or negative portion of the distribution corresponding to the sign
    of the observed ES(S).
    """

    # to speed up, using numpy function to compute pval in parallel.
    condlist = [es < 0, es >= 0]
    choicelist = [
        (esnull < es.reshape(len(es), 1)).sum(axis=1) / (esnull < 0).sum(axis=1),
        (esnull >= es.reshape(len(es), 1)).sum(axis=1) / (esnull >= 0).sum(axis=1),
    ]
    pvals = np.select(condlist, choicelist)

    return pvals


def gsea_fdr(nEnrichmentScores, nEnrichmentNulls):
    """Create a histogram of all NES(S,pi) over all S and pi.
       Use this null distribution to compute an FDR q value.

    :param nEnrichmentScores:  normalized ES
    :param nEnrichmentNulls:   normalized ESnulls
    :return: FDR
    """

    # FDR null distribution histogram
    # vals = reduce(lambda x,y: x+y, nEnrichmentNulls, [])
    # nvals = np.array(sorted(vals))
    # or
    nvals = np.sort(nEnrichmentNulls.flatten())
    nnes = np.sort(nEnrichmentScores)
    fdrs = []
    # FDR computation
    for i in range(len(nEnrichmentScores)):
        nes = nEnrichmentScores[i]
        # use the same pval method to calculate fdr
        if nes >= 0:
            allPos = int(len(nvals) - np.searchsorted(nvals, 0, side="left"))
            allHigherAndPos = int(len(nvals) - np.searchsorted(nvals, nes, side="left"))
            nesPos = len(nnes) - int(np.searchsorted(nnes, 0, side="left"))
            nesHigherAndPos = len(nnes) - int(np.searchsorted(nnes, nes, side="left"))
            # allPos = (nvals >= 0).sum()
            # allHigherAndPos = (nvals >= nes).sum()
            # nesPos = (nnes >=0).sum()
            # nesHigherAndPos = (nnes >= nes).sum()
        else:
            allPos = int(np.searchsorted(nvals, 0, side="left"))
            allHigherAndPos = int(np.searchsorted(nvals, nes, side="right"))
            nesPos = int(np.searchsorted(nnes, 0, side="left"))
            nesHigherAndPos = int(np.searchsorted(nnes, nes, side="right"))
            # allPos = (nvals < 0).sum()
            # allHigherAndPos = (nvals < nes).sum()
            # nesPos = (nnes < 0).sum()
            # nesHigherAndPos = (nnes < nes).sum()

        try:
            pi_norm = allHigherAndPos / float(allPos)
            pi_obs = nesHigherAndPos / float(nesPos)
            fdr = pi_norm / pi_obs
            fdrs.append(fdr if fdr < 1 else 1.0)
        except:
            fdrs.append(1000000000.0)

    logging.debug("Statistical testing finished.............................")

    return fdrs


def gsea_significance(enrichment_scores, enrichment_nulls):
    """Compute nominal pvals, normalized ES, and FDR q value.

    For a given NES(S) = NES* >= 0. The FDR is the ratio of the percentage of all (S,pi) with
    NES(S,pi) >= 0, whose NES(S,pi) >= NES*, divided by the percentage of
    observed S wih NES(S) >= 0, whose NES(S) >= NES*, and similarly if NES(S) = NES* <= 0.
    """
    # For a zero by zero division (undetermined, results in a NaN),
    np.seterr(divide="ignore", invalid="ignore")
    # import warnings
    # warnings.simplefilter("ignore")
    es = np.array(enrichment_scores)
    esnull = np.array(enrichment_nulls)
    logging.debug("Start to compute pvals..................................")
    # P-values.
    pvals = gsea_pval(es, esnull).tolist()

    logging.debug("Start to compute nes and nesnull........................")
    # NES
    nEnrichmentScores, nEnrichmentNulls = normalize(es, esnull)

    logging.debug("Start to compute fdrs..................................")
    # FDR
    fdrs = gsea_fdr(nEnrichmentScores, nEnrichmentNulls)

    # TODO: use multiple testing correction for ssgsea? ssGSEA2.0 use BH correction.
    # https://github.com/broadinstitute/ssGSEA2.0/blob/master/src/ssGSEA2.0.R
    # line 969
    # fdrs, _ = multiple_testing_correction(pvals, alpha=0.05)

    return zip(enrichment_scores, nEnrichmentScores, pvals, fdrs)
