import pandas as pd
import numpy as np
from copy import copy
from Bio.Seq import Seq
from . import Parameters
from treasureisland.PreprocessData import PreprocessData


class IdentifyGI:

    def __init__(self, dna_sequence_list, dna_emb_model, classifier):
        '''Initialize variables'''

        self.dna_sequence_list = dna_sequence_list
        self.dna_emb_model = dna_emb_model
        self.classifier = classifier

    def get_dna_vectors(self, processed_dna_seq):
        '''Uses the DNA embedding model to get the DNA vector of a DNA segment
           processed_dna_seq : DNA segment after preprocessing step
        '''

        dna_vectors = []
        for segments in processed_dna_seq:
            inferred_vector = self.dna_emb_model.infer_vector(segments, epochs=20)
            dna_vectors.append(inferred_vector)

        return dna_vectors

    def get_dna_segment_probability(self, dna_vectors, segment_borders):
        ''' 
        Gets the probability of each DNA segment to be a GI using the classifier
        dna_vectors : DNA vectors 
        segment_borders : start and end points of DNA segments  
        '''

        probability = self.classifier.predict_proba(dna_vectors)
        prob_df = pd.DataFrame(np.column_stack([segment_borders, probability]), columns=['start', 'end', '0', '1'])
        prob_list = prob_df.values.tolist()

        return prob_list

    def get_GI_regions(self, dna_prob):
        ''' 
        Gets the DNA segments with probability above the upper threshold and 
        flanking sequences with probability between upper and lower thesholds 
        dna_prob : full list of DNA segments and their probabilities
        '''
        prev = -1
        gi_dict = {}
        gi_num = 1
        prev_gi = False
        for row in dna_prob:
            found_gi = False
            if row[3] >= Parameters.UPPER_THRESHOLD:
                found_gi = True
                if 'gi_' + str(gi_num) in gi_dict.keys():
                    gi_dict['gi_' + str(gi_num)].append(row)
                else:
                    if prev == -1:
                        gi_dict['gi_' + str(gi_num)] = [row]
                    else:
                        if prev[3] > Parameters.LOWER_THRESHOLD:
                            gi_dict['gi_' + str(gi_num)] = [prev]
                            gi_dict['gi_' + str(gi_num)].append(row)
                        else:
                            gi_dict['gi_' + str(gi_num)] = [row]
                prev_gi = True
            if found_gi == False and prev_gi == True:
                if row[3] > Parameters.LOWER_THRESHOLD:
                    gi_dict['gi_' + str(gi_num)].append(row)
                prev_gi = False
                gi_num += 1
            prev = row

        return gi_dict

    def find_fragment_probability(self, GI_borders, dna_sequence):
        '''
        Get a DNA fragment probability for class GI
        GI_borders : set of start and end points
        '''
        gi_start = int(GI_borders[0])
        gi_end = int(GI_borders[1])

        sequence = str(dna_sequence.seq).lower()
        fragment = sequence[gi_start:gi_end]
        pre_process = PreprocessData()
        kmers = pre_process.generate_kmers(fragment)

        inferred_vector = [self.dna_emb_model.infer_vector(kmers, epochs=20)]
        prob = self.classifier.predict_proba(inferred_vector)

        gi_prob = round(prob[0][1], 5)  # gi_prob probability of region belonging to class 1(GI)

        return gi_prob

    def merge(self, gi_regions, dna_sequence):
        '''
        Incrementally merge the DNA segments
        :param gi_regions: dictionary consisting of GEI regions along with flanking sequences
        :param dna_sequence: a single DNA sequence from the list of input DNA sequences
        :return: list of MergedGEI
        '''
        gi_merged_regions = []
        gi_name = 0
        for gi_id in gi_regions.keys():
            gi_region = gi_regions[gi_id]
            merged_start = merged_end = 0
            flanking_start = flanking_end = 0
            merged_probs = []
            count = 0
            for segment in gi_region:
                if count == 0: # for the first segment in the GEI region
                    if segment[3] < Parameters.UPPER_THRESHOLD: # it is a flanking sequence
                        flanking_start = [segment[0], segment[1]]
                        merged_start = gi_region[1][0]
                        count += 1
                        continue
                    else: # not a flanking sequence
                        merged_start = gi_region[0][0]
                else: # for every segment apart from first segment
                    if segment[3] < Parameters.UPPER_THRESHOLD: # check for flanking sequence at the end
                        flanking_end = [segment[0], segment[1]]
                        break
                # incrementally find prob of merged regions
                merged_prob = self.find_fragment_probability([merged_start, segment[1]], dna_sequence)
                if merged_prob >= Parameters.UPPER_THRESHOLD: # if merged region is a GEI
                    merged_end = segment[1]
                    merged_probs.append(merged_prob)
                else:  # merged region not a GEI, then save previous GEI region
                    gi_name += 1
                    if merged_probs: # if there is a previous GEI region
                        mergeObj = MergedGEI(gi_name, merged_start, merged_end, merged_probs[-1], flanking_start, flanking_end)
                        gi_merged_regions.append(mergeObj) # save previous GEI
                        flanking_start = 0 # re-initialise for next GEI
                        merged_start = segment[0]
                        merged_end = segment[1]
                        merged_probs.append(segment[3])
                    else: # the first segment categorized as GEI is not a GEI now
                        flanking_start = [segment[0], segment[1]] # let this now be a flanking segment
                        merged_start = segment[1]  # start it from the next segment
                count += 1
            if merged_end != 0: # if last GEI has not been saved
                gi_name += 1
                mergeObj = MergedGEI(gi_name, merged_start, merged_end, merged_probs[-1], flanking_start, flanking_end)
                gi_merged_regions.append(mergeObj)

        return gi_merged_regions

    def pre_fine_tune(self, mergedGEI):
        '''

        :param mergedGEI:
        :return: preFinedTuneGEI
        '''

        if mergedGEI.flanking_start != 0:
            start_limit = mergedGEI.start - (Parameters.WINDOW_SIZE / 2)
        else:
            start_limit = mergedGEI.start

        if mergedGEI.flanking_end != 0:
            end_limit = mergedGEI.end + (Parameters.WINDOW_SIZE / 2)
        else:
            end_limit = mergedGEI.end

        preFineTunedObj = PreFineTunedGEI(mergedGEI.name, mergedGEI.start, mergedGEI.end, mergedGEI.prob, start_limit, end_limit)

        return preFineTunedObj

    def fine_tune(self, preFineTunedGEI, dna_sequence):
        '''

        :param preFineTunedGEI:
        :return: fineTunedGEI
        '''

        #left border
        border_l = 1
        border_r = 0
        if preFineTunedGEI.start_limit == preFineTunedGEI.start:
            has_flanking_segment = False
        else:
            has_flanking_segment = True
        leftFineTunedGEI = self.fine_tune_helper(border_l, border_r, has_flanking_segment, preFineTunedGEI, dna_sequence)

        # right border
        border_l = 0
        border_r = 1
        if preFineTunedGEI.end_limit == preFineTunedGEI.end:
            has_flanking_segment = False
        else:
            has_flanking_segment = True

        preFineTunedGEI.start = leftFineTunedGEI.start

        rightFineTunedGEI = self.fine_tune_helper(border_l, border_r, has_flanking_segment, preFineTunedGEI, dna_sequence)

        frag_prob = self.find_fragment_probability([leftFineTunedGEI.start, rightFineTunedGEI.end], dna_sequence)

        fineTunedGEI = FineTunedGEI(preFineTunedGEI.name, leftFineTunedGEI.start, rightFineTunedGEI.end, frag_prob)

        return fineTunedGEI


    def fine_tune_helper(self, border_side_l, border_side_r, has_flanking_segment, preFineTunedGEI, dna_sequence):
        '''

        :return:
        '''

        current_obj = next_obj = FineTunedGEI(preFineTunedGEI.name, preFineTunedGEI.start, preFineTunedGEI.end,
                                              preFineTunedGEI.prob)
        significant_change = 0.1

        if has_flanking_segment:
            direction_left = border_side_l * -1
            direction_right = border_side_r * 1
            while_clause = next_obj.prob >= current_obj.prob or (current_obj.prob - next_obj.prob) < significant_change
        else:
            direction_left = border_side_l * 1
            direction_right = border_side_r * -1
            while_clause = next_obj.prob > current_obj.prob

        while(next_obj.prob >= Parameters.UPPER_THRESHOLD and
              next_obj.start >= preFineTunedGEI.start_limit and
              next_obj.end <= preFineTunedGEI.end_limit and
              (next_obj.end - next_obj.start) >= Parameters.MINIMUM_GI_SIZE and
              while_clause
        ):
            #print("enter while")
            current_obj = copy(next_obj)
            next_obj.start = next_obj.start + (Parameters.TUNE_METRIC * direction_left)
            next_obj.end = next_obj.end + Parameters.TUNE_METRIC * direction_right
            frag_prob = self.find_fragment_probability([next_obj.start, next_obj.end], dna_sequence)
            next_obj = FineTunedGEI(preFineTunedGEI.name, next_obj.start, next_obj.end, frag_prob)
            #print("current_obj" + str(current_obj))
            #print("next_obj" + str(next_obj))
        #print("current_obj" + str(current_obj))
        return current_obj

    def find_GI_borders(self, gi_regions, id, dna_sequence):
        ''' 
        Function to combine all the GI border finding steps
        gi_regions : GI fragments along with its flanking sequence
        '''

        gi_borders = {}
        mergedGEIS = self.merge(gi_regions, dna_sequence)
        #print("merged_regions")
        #print(mergedGEIS)
        for mergedGEI in mergedGEIS:
            preFineTuned = self.pre_fine_tune(mergedGEI)
            #print("preFineTuned")
            #print(preFineTuned)
            fineTuned = self.fine_tune(preFineTuned, dna_sequence)
            #print("fineTuned")
            #print(fineTuned)
            gi_borders[fineTuned.name] = [id, fineTuned.start, fineTuned.end, fineTuned.prob]

        return gi_borders

    def find_gi_predictions(self):
        ''' 
        Main function to call all other functions for identifying GI regions
        '''
        all_gi_borders = []
        org_count = 0
        for dna_sequence in self.dna_sequence_list:
            org_count += 1
            #id = str(org_count) + "_" + dna_sequence.id #commented out for evaluations
            id = dna_sequence.id
            pre_process = PreprocessData()
            processed_dna_seq, segment_borders = pre_process.split_dna_sequence(dna_sequence)
            dna_vectors = self.get_dna_vectors(processed_dna_seq)
            dna_prob = self.get_dna_segment_probability(dna_vectors, segment_borders)
            #print("dna_prob")
            #print(dna_prob)
            gi_regions = self.get_GI_regions(dna_prob)
            #print("gi_regions")
            #print(gi_regions)
            gi_borders = self.find_GI_borders(gi_regions, id, dna_sequence)
            #print("gi_borders")
            #print(gi_borders)
            all_gi_borders.append(gi_borders)

        return all_gi_borders


class MergedGEI:
    def __init__(self, name, start, end, prob, flanking_start,  flanking_end):
        self.name = name
        self.start = start
        self.end = end
        self.prob = prob
        self.flanking_start = flanking_start
        self.flanking_end = flanking_end

    def __repr__(self):
        return f"MergedGEI(name='{self.name}', start={self.start}, end={self.end},prob={self.prob}, flanking_start={self.flanking_start}, flanking_end={self.flanking_end})"


class PreFineTunedGEI:
    def __init__(self, name, start, end, prob, start_limit,  end_limit):
        self.name = name
        self.start = start
        self.end = end
        self.prob = prob
        self.start_limit = start_limit
        self.end_limit = end_limit

    def __repr__(self):
        return f"PreFineTunedGEI(name='{self.name}', start={self.start}, end={self.end},prob={self.prob}, start_limit={self.start_limit}, end_limit={self.end_limit})"


class FineTunedGEI:
    def __init__(self, name, start, end, prob):
        self.name = name
        self.start = start
        self.end = end
        self.prob = prob

    def __repr__(self):
        return f"FineTunedGEI(name='{self.name}', start={self.start}, end={self.end},prob={self.prob})"


