import pandas as pd


def set_similarity(set1, set2, mode):
    if ((len(set1) == 0) and (len(set2) == 0)):
        return 0
    if mode == "count":
        return len(list(set(set1).intersection(set2)))
    if mode == "jaccard":
        intersection = set(set1).intersection(set2)
        return len(intersection) / (len(set1) + len(set2) - len(intersection))


def convert_entity_dict(dict_file, factorized_entities):
    with open(dict_file, 'rb') as f:
        original_ent_map = pd.read_csv(f, sep='\t', header=None, dtype={1: str})
    tmp = []
    for x in range(len(factorized_entities[1])):
        tmp.append(str(factorized_entities[1][x]).zfill(8))
    original_ent_map[1] = tmp
    original_ent_map.to_csv(dict_file, sep='\t', header=False, index=False)
    print('Entitis.dict file is changed and saved.')
    return


def tensor_similarity(t1, t2, mode='count'):
    compareview = t2.repeat(t1.shape[0], 1).T
    return t1[(compareview == t1).sum(0) == 1]


def find_neighbor_sets(tensor_triples, factorized_entities):
    neighbor_sets = {}
    # e simply goes from 0 to len(entities) as factorized_entities[0] is: [0,1,2, ... ,len(entities)]
    for e in factorized_entities[0]:
        neighbor_sets[e] = tensor_triples[tensor_triples[:, 0] == e][:, 2].numpy()
    return neighbor_sets


def find_neighbor_of_entity_df(df, entity):
    df_tmp = df.merge(df, how="inner", left_on=["tail"], right_on=["tail"])
    neighbors = df_tmp[(df_tmp["head_x"] == entity) & (df_tmp["head_y"] != entity)]["head_y"].values
    return neighbors


def find_hard_negatives(entity, neighbor_sets, all_entities, mode):
    '''
    finds a list of candidate entities that share common neighbours with the given entity.
    List is ordered based on the score of the similarity.
    Goal is to find the most probable hard negative entities
    Returns a list of tuple of entities and their scores. List is sorted based on score in a descending way
    '''
    hard_negatives = []
    for candidate_entity in all_entities:
        score = set_similarity(neighbor_sets[entity], neighbor_sets[candidate_entity], mode=mode)
        if score > 0:
            if candidate_entity != entity:
                hard_negatives.append((candidate_entity, score))
    hard_negatives.sort(key=lambda tup: tup[1], reverse=True)
    '''
    TODO: eliminate original triple if its contained in the set.
    currently it is handled while returning the hard negatives as [1:]
    '''
    return hard_negatives


def find_all_hard_negatives_per_entity(neighbor_sets, all_entities, mode):
    entity_to_hard_negatives = {}
    count = 0
    ent_s = len(all_entities)
    for ent in all_entities:
        count += 1
        if count % 500 == 0:
            print("Percentage: ", str((count) * 100 / ent_s))
        # TODO: BE VERY CAREFUL ABOUT WHICH METHOD YOU APPLY BELOW
        # this method uses local variables
        entity_to_hard_negatives[ent] = find_hard_negatives(ent, neighbor_sets, all_entities, mode)
        # this method uses global variable
        # entity_to_hard_negatives[ent] = find_hard_negatives_without_variables(ent, mode)
    return entity_to_hard_negatives

def filter_all_hard_negatives(all_hard_negatives, k):
    filtered_hard_negatives = {}
    for x in all_hard_negatives.items():
        filtered_hard_negatives[x[0]] = x[1][:k]
    return filtered_hard_negatives

def check_false_negatives(entity, hard_negatives, triples):
    found = False
    print("Length of the possible corrupted triples are: ", str(len(hard_negatives)))
    for x in hard_negatives:
        if not triples[(triples["head"] == entity) & (triples["tail"] == x[0])].empty:
            print(triples[(triples["head"] == entity) & (triples["tail"] == x[0])])
            found = True
    if not found:
        print("Not found!")
    return