import numpy as np
import math
import h5py
import os
import sys
import operator
from functools import reduce
import argparse

from numpy.lib.recfunctions import merge_arrays

def load(filename, chrom, min_cov=10):
    f = h5py.File(filename, "r")
    values = f[chrom]["methylation"][:]
    m = np.sum(values[:,(0,2)], axis=1)
    cov = np.sum(values, axis=1)
    invalid = cov < min_cov

    cov[cov == 0] = 1
    m_values = m / cov
    return m_values, invalid


def savitzky_golay(y, window_size, order, deriv=0, rate=1):
    import numpy as np
    from math import factorial

    window_size = np.abs(np.int(window_size))
    order = np.abs(np.int(order))

    order_range = range(order+1)
    half_window = (window_size -1) // 2
    # precompute coefficients
    b = np.mat([[k**i for i in order_range] for k in range(-half_window, half_window+1)])
    m = np.linalg.pinv(b).A[deriv] * rate**deriv * factorial(deriv)
    # pad the signal at the extremes with
    # values taken from the signal itself
    firstvals = y[0] - np.abs( y[1:half_window+1][::-1] - y[0] )
    lastvals = y[-1] + np.abs(y[-half_window-1:-1][::-1] - y[-1])
    y = np.concatenate((firstvals, y, lastvals))
    return np.convolve( m[::-1], y, mode='valid')


def run(args):
    return run_direct(args.index, args.case, args.control, args.min_cpg, args.min_diff, args.min_cov, args.smooth)


def run_direct(index_filename, case, control, min_cpg=1, min_diff=0, min_cov=10, smooth=False):
    index = h5py.File(index_filename, "r")

#    print(np.percentile(finite_t, 0.05))
#    exit()

    chromosomes = map(str, list(range(1,23)) + ["X"])
    #chromosomes = ["12"]

    
    for chrom in chromosomes: #range(1,23)
        print(chrom, file=sys.stderr)
        values_case, invalid_case = zip(*[load(s, chrom, min_cov) for s in case])
        values_control, invalid_control = zip(*[load(s, chrom, min_cov) for s in control])

        #smooth values
        if smooth:
            values_case = list(map(lambda x: savitzky_golay(x, 3, 3), values_case))
            values_control = list(map(lambda x: savitzky_golay(x, 3, 3), values_control))

        #invalid stored a vector for each position, where at least one coverage is to low
        invalid = reduce(operator.or_, invalid_case + invalid_control)

        #"filter" low coverage
        for value in values_case + values_control:
            value[invalid] = 0

        n_case = len(values_case)
        n_control = len(values_control)

        # calculate means
        mean_case = np.mean(values_case, axis=0)
        mean_control = np.mean(values_control, axis=0)

        # calculate differences and standard deviation
        betas = mean_control - mean_case
#        omega = (np.std(values_case, axis=0) + np.std(values_control, axis=0)) / 2
        omega = len(values_case) * np.var(values_case, axis=0) + len(values_control) * np.var(values_control, axis=0)
        omega /= (len(values_case) + len(values_control))
        omega = np.sqrt(omega)

        #calculate the omega 75th percentile
        omega_perc = np.percentile(omega, 75)

        #cap the omega values to the percentile
        omega[omega > omega_perc] = omega_perc

        #TODO: smooth the omegas with running mean 101 window

        omega_zero = omega == 0
        omega[omega_zero] = 1

        #form t statistic
        t = np.divide(betas, omega * math.sqrt(1/n_case + 1/n_control))

#        print("t-perc_pos:", np.nanpercentile(t[t>=0], 100-2.5))
#        print("t-perc_neg:", np.nanpercentile(-t[t<=0], 100-2.5))

#        exit()

#        if smooth:
#            t = savitzky_golay(t, 10, 3)

        #make sure every dmr has a start and an end
#        t_extend = np.concatenate(([0], t, [0])) 
        t_extend = t
        t_extend[0] = 0
        t_extend[-1] = 0

        #TODO: do not delete for further analysis
        #for v, b in zip(*np.histogram(t, range=(-10, 10), bins=100)):
        #    print(b, v, sep="\t")


        #calculate thresholds for t values
        threshold_hyper = np.nanpercentile(t, 100-2.5)
        threshold_hypo = np.nanpercentile(t, 2.5)

        cpg_pos = index[chrom]["cpg_positions"][:]
        #print(np.where(cpg_pos == 50303600)) # 437033

        #print(threshold_hyper)
        #print(betas[437033:437033+20])
        #print(t[437033-5:437033+20])
        #print(t[437033-5:437033+20] > threshold_hyper)
        #print(cpg_pos[437033-5:437033+20])
#        exit()

        for over in [(t_extend > threshold_hyper), (t_extend < threshold_hypo)]:
            # fill holes in over
            over_range = np.arange(len(over))
            dist_left = over_range - np.maximum.accumulate(over * over_range)
            dist_right = (over_range - np.maximum.accumulate(over[::-1] * over_range))[::-1]
            comb_dist = dist_left + dist_right - 1
            over_fill = (comb_dist <= 2).astype(np.int8)

            over_positions = np.where(over)[0]
            #correct possible border fills
            over_fill[0:over_positions[0]] = 0
            over_fill[over_positions[-1]:len(over_fill)] = 0

            # search for dmr stars and ends
            over_diff = np.diff(over_fill)
            starts = np.where(over_diff==1)[0]+1
            ends = np.where(over_diff==-1)[0]+1           

            # the numbers of cpg per dmr
            cpg_count = ends - starts

    #        # determine the beta value between the dmrs
    #        cumsum = np.cumsum(betas)
    #        dmr_betas = np.divide(cumsum[ends] - cumsum[starts - 1], cpg_count)

            # determine the core diff
            core_omega = np.std(np.array(values_case + values_control), axis=0)
            core_beta = betas * core_omega
            core_cumsum = np.cumsum(core_beta)
            core_omega_cumsum = np.cumsum(core_omega)
            core_dmr = np.divide(core_cumsum[ends] - core_cumsum[starts - 1], core_omega_cumsum[ends] - core_omega_cumsum[starts - 1])

            # correct first DMR beta value if the first t-value is significant and the "start" -1 therefore adresses -1, which means the last index
            if starts[0] == 0:
                dmr_betas[0] = cumsum[ends[0] - 1] / cpg_count[0]

            # only dmrs with beta > x and more than y cpg
            selection = (np.abs(core_dmr) >= min_diff) & (cpg_count >= min_cpg)

            starts_filtered = starts[selection]
            ends_filtered = ends[selection]
            cpg_count = cpg_count[selection]
    #        dmr_betas = dmr_betas[selection]
            core_dmr = core_dmr[selection]

            # further filtering
            selection = []
            for (start, end, c) in zip(starts_filtered, ends_filtered, cpg_count):
                sign_count = np.sum(over[start:end])
                if sign_count >= min_cpg:
                    selection.append(True)
                else:
                    selection.append(False)

            starts_filtered = starts_filtered[selection]
            ends_filtered = ends_filtered[selection]
            cpg_count = cpg_count[selection]
    #        dmr_betas = dmr_betas[selection]
            core_dmr = core_dmr[selection]

            m = merge_arrays((np.repeat(chrom, len(starts_filtered)), cpg_pos[starts_filtered], cpg_pos[ends_filtered-1]+1, cpg_count, core_dmr), asrecarray=True)
            m.dtype.names = ("chrom", "start", "stop", "n", "core_diff")

            yield m

def out(x):
    for i in x:
        for a in i:
            print(*a, sep="\t")


def sub(parser):
    # add items to a parser or subparser
    parser.add_argument('index', help='the camel index file')
    parser.add_argument("--case", nargs="+", help="the case samples in camel format")
    parser.add_argument("--control", nargs="+", help="the control samples in camel format")
    parser.add_argument("--min_cpg", default=1, type=int, help="the minimum number of CpGs to call a region")
    parser.add_argument("--min_diff", default=0.0, type=float, help="the minimum mean difference to call a region")
    parser.add_argument("--min_cov", default=10, type=int, help="the minimum coverage for each cpg")
    parser.add_argument("--smooth", action="store_true", help="the methylation varlues get smoothed")



def main():
    parser = argparse.ArgumentParser(description='Calculate methylation level for each CpG.')
    sub(parser)
    args = parser.parse_args()
    out(run(args))


if __name__ == '__main__':
    main()
