#call all meth informations for each cpg

import fileinput
from multiprocessing import Pool

import pysam
import time
import sys
import h5py
import argparse
import numpy as np
import math
import os
import re
from functools import partial

from bisect import bisect_left
from collections import defaultdict, namedtuple
from itertools import islice

Hit = namedtuple("Hit", "typ, rel_pos, qual, index")


def get_positions(left, gc):
    #perform a bidirectional search for gc positions
    pos = bisect_left(gc, left)

    while pos < len(gc):
        yield (gc[pos], pos)
        pos += 1


def parse_cigar(cigar):
    finds = re.findall("[0-9]*[A-Z]", cigar, flags=0)
    return [(x[-1], int(x[:-1])) for x in finds]


def relative_pos(read_pos, parsed_cigar, pos):
    # calculate the position of the base, without deletions and insertions
    relative_pos = pos - read_pos
    curr_pos     = 0

    for c in parsed_cigar:
        if curr_pos + c[1] <= relative_pos:
            if c[0] == "D" or c[0] == "S": #deletion or soft clipping
                relative_pos -= c[1]
            if c[0] == "I": #insertion
                relative_pos += c[1]
            else: #match
                curr_pos += c[1]
        else:
            if c[0] == "D": #cpg inside deletion
                return None
            else:
                return relative_pos
    return None


def get_hits(split, positions):
    read_qname = split[0]
    read_flags = int(split[1])
    read_chrom = split[2]
    read_pos = int(split[3])
    read_cigar = split[5]
    read_sequence = split[9].upper()
    read_qual = split[10]
    read_is_read1 = (read_flags & 64) > 0
    read_is_reverse = (read_flags & 16) > 0

    parsed_cigar = parse_cigar(read_cigar)

    #ignore last position on forward and last position on reverse
    chrom_positions = positions[read_chrom]
    curr_index = bisect_left(chrom_positions, read_pos + 1)

    curr_cpg_pos = chrom_positions[curr_index]
    rel_pos = relative_pos(read_pos, parsed_cigar, curr_cpg_pos-1)

    while rel_pos is not None and rel_pos < len(read_sequence) - 1:
        if (rel_pos == None):
            return

        if rel_pos > len(read_sequence) - 2:
            return

        pair_orientation = read_is_read1 == read_is_reverse

        rel_pos += pair_orientation
        rel_next_pos =  rel_pos + 1 - pair_orientation * 2

        try:
            char = read_sequence[rel_pos]
            next_char = read_sequence[rel_next_pos]
        except:
            print("error", read_qname, read_pos, read_cigar, rel_pos, file=sys.stderr)
            continue

        if pair_orientation and char == 'A' and next_char == 'C':
            typ = 3
        elif not pair_orientation and char == 'C' and next_char == 'G':
            typ = 0
        elif pair_orientation and char == 'G' and next_char == 'C':
            typ = 2
        elif not pair_orientation and char == 'T' and next_char == 'G':
            typ = 1
        elif pair_orientation and next_char == "T":
            typ = 5
        elif not pair_orientation and next_char == "A":
            typ = 4
        else:
            # SNPs
            typ = -1

        ref = "C" if not pair_orientation else "G"

        if rel_pos >= 0:
            yield Hit(typ, rel_pos, ord(read_qual[rel_pos]), curr_index)

        curr_index += 1
        curr_cpg_pos = chrom_positions[curr_index]
        rel_pos = relative_pos(read_pos, parsed_cigar, curr_cpg_pos-1)

l = []


def process(reads, cpg_positions):
    splits = [read.split() for read in reads]

    # filter valid reads
    splits = [split for split in splits if
                (int(split[1]) & 1028 == 0) and # no duplicate, not unmapped
                (int(split[4]) >= 30) # mapq; TODO: replace by mmq
             ]

#    sequences = [split[9] for split in splits]

    for split in splits:
        # for each cpg position on the read
        for h in get_hits(split, cpg_positions):
            print(h)
    #        # add only if minumum quality is reached
    #        if h.qual < mbq or h.typ == -1: continue
    #            cpg_ctga[read.tid][h.index, h.typ] += 1

    return None
#    return sequences


def out(x, l=l):
#    print("foo")
    print(x)


def check_files_exists(*filenames):
    # check filenames
    for f in filenames:
        if not os.path.isfile(f):
            raise Exception("{} not found".format(f))


def run(args):
#    bam_filename = args.input
    index_filename = args.index
    output_filename = args.output
    mbq = args.mbq
    mmq = args.mmq
    max_snp_fraction = args.max_snp_fraction


    check_files_exists(index_filename)

    print("load index", file=sys.stderr)
    index = h5py.File(index_filename, 'r')


    print("create datastructures", file=sys.stderr)

    #check if cpg_positions exist / correct index
    for r in index:
        if not "cpg_positions" in index[r]:
            raise Exeption("cpg_positions missing in {}. Please call camel index to create a valid camel-index-file".format(index_filename))
        if args.nome and not "gpc_positions" in index[r]:
            raise Exeption("cpg_positions missing in {}. Please call camel index with --nome param to create a valid camel-index-file for nome calling".format(index_filename))

    cpg_positions = {r:index[r]["cpg_positions"][:].tolist() for r in ["1"]} #index
    cpg_ctga = [np.zeros((len(index[r]["cpg_positions"]),6), dtype=np.int) for r in ["1"]]



    print("call", file=sys.stderr)
    f = islice(fileinput.input("-"), 10000000)
#    with Pool(40) as pool:
#        while True:
#            reads = list(islice(f, 50000))
#            print(len(reads))
#            if len(reads) == 0:
#                break
#            pool.apply_async(partial(process, cpg_positions=cpg_positions), [reads], callback=out)

#        pool.close()
#        pool.join()


    while True:
        reads = list(islice(f, 50000))
        if len(reads) == 0:
            break
        ret = process(reads, cpg_positions=cpg_positions)

def sub(parser):
    parser.add_argument('index', help='The camel-index-file.')
    parser.add_argument('output', help='The output camel-data file.')
    parser.add_argument('-mbq', nargs='?', help='minimum pred-scaled base quality score', default=17, type=int)
    parser.add_argument('-mmq', nargs='?', help='minimum pred-scaled mapping quality score', default=30, type=int)
    parser.add_argument('--nome', '-n', action='store_true', help='perform an additional nome calling')
    parser.add_argument('--samout', '-s', action='store_true', help='print sam to stdout')
    parser.add_argument('--ignore-duplicates', '-d', action='store_true', help='Activate the ignore-duplication mode to perform own pcr-duplication detection by read positions using a bloomfilter. The setup of the bloomfilter requires an aproximate read-count and an error-rate. Note that the default value requires 3.35GB of memory.')
    parser.add_argument('--read-count', '-r', metavar="N", nargs='?', help='Set the input read count, used for size determination of the bloomfilter (default: 1000000000). Only needed in ignore-duplication mode', default=1000000000, type=int)
    parser.add_argument('--error-rate', '-e', metavar="N", nargs='?', help='Set the error-rate, used for size determination of the bloomfilter (default: 0.00001). Only needed in ignore-duplication mode.', default=0.0001, type=float)
    parser.add_argument('--max_snp_fraction', "-f", help="Set the maximum fraction of basepairs to not discard a CpG as SNP. (CpGs with a snp fraction higher than this values get discarded)", default=0.2, type=int)


if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Calculate methylation level for each CpG.')
    sub(parser)
    args = parser.parse_args()
    run(args)
