import numpy as np
import pandas as pd
import time
import itertools
import functools

def run48(Vert):
    '''
    :param Vert:
    :return:
    '''
    print('PyCharm start')

    #global functions

    ##functions
    def conver_face1(i, dicti_vert):
        face = [k for k, v in dicti_vert.items() if i in v]
        face = ''.join(face)
        return face

    def conver_face2(i):
        my_dict = dicti_vert
        my_color = i
        my_face = [k for k, v in my_dict.items() if my_color in v]
        face = ''.join(my_face)
        return face

    def sort_order(i):
        i.sort()
        return i

    def IncreaseOne(x):
        return [i + 1 for i in x]

    def alphabet(x):
        k = ''.join(sorted(x, key=str.lower))
        return k

    def getIndexes(dfObj, value):
        listOfPos = []
        result = dfObj.isin([value])
        seriesObj = result.any()
        columnNames = list(seriesObj[seriesObj == True].index)
        for col in columnNames:
            rows = list(result[col][result[col] == True].index)
            for row in rows:
                listOfPos.append([row, col])
        return listOfPos

    def spl(t):
        a = int(t / 10)
        b = t % 10
        return [a, b]

    def stimultaneous_spl(t):
        c = [spl(_) for _ in t]
        d = []
        for i in range(len(t)):
            d += c[i]
        return set(d)

    # the following are per functions

    def key_list_after_sym(x):
        m = []
        for i in range(1, sym_cardinality + 1):
            exec("temp = sort_order([dir_" + str(i) + "[j] for j in x])")
            exec("m += [10*temp[0]+temp[1]]")
        return m

    merTOstr = lambda x: ''.join([str(_) for _ in x])

    def pre_generate(onedata, pos):
        pos = [i - 1 for i in pos]  # -1 due to a uniform indexing
        return [onedata[i] for i in pos]

    def generate(onedata):
        tempfunction = functools.partial(pre_generate, onedata)
        temp = np.apply_along_axis(tempfunction, 1, kl_columns)
        temp = [merTOstr(_) for _ in temp]
        return temp

    def conver_preresult(i):
        return [int(j) for j in data_after_per[i]]

    def replace7(row):
        to_modify = data.iloc[row, :]
        indexes = [i for i, x in enumerate(to_modify) if x == 7]
        replacements = [(i + 1) * 10 + 7 for i in range(len(indexes))]
        for index in range(len(indexes)):
            to_modify[indexes[index] + 1] = replacements[index]
        return ()

    #local functions

    def select_columns(i):
        return [dicti[j] for j in i]

    def infty2(x):
        if len(set([x[j] for j in [0, 5]]) & set(infty)) == 2 and len(
                set([x[j] for j in [1, 2, 3, 4]]) & set(infty)) == 0:
            return [x[j] for j in [1, 2, 3, 4]]
        if len(set([x[j] for j in [1, 4]]) & set(infty)) == 2 and len(
                set([x[j] for j in [0, 2, 3, 5]]) & set(infty)) == 0:
            return [x[j] for j in [0, 2, 3, 5]]
        if len(set([x[j] for j in [2, 3]]) & set(infty)) == 2 and len(
                set([x[j] for j in [0, 1, 4, 5]]) & set(infty)) == 0:
            return [x[j] for j in [0, 1, 4, 5]]
        return None

    def check_nonvert_all_edge(x):
        edge_list = list(itertools.combinations(non_vert_facets[x], 3))
        edge_list = [set(list(_)) for _ in edge_list]
        judge = [_ for _ in edge_list if _ in Edge]
        if edge_list == judge:
            return x
        return "F"

    def killer(j, df):
        col_check = select_columns(cdt[j])
        return list(df[[str(j) for j in col_check]]) not in lib

    def saver(j, df):
        col_check = select_columns(cdt[j])
        return list(df[[str(j) for j in col_check]]) in lib

    # data
    alpha_list = [chr(x) for x in range(ord('A'), ord('Z') + 1)]

    d = 8  # number of facets
    dp = 28  # number of all indexes 1+2+..+d-1
    dh = 6  # length of dihedral angle. eg. 3->3,4->6,5->10...

    print(Vert)

    key_list = [alpha_list[_] for _ in range(len(Vert))]
    value_list = Vert
    dicti_vert = {key: value for (key, value) in zip(key_list, value_list)}
    value_1 = [conver_face1(i, dicti_vert) for i in range(1, d + 1)]
    key_1 = range(d)
    dicti_1 = {key: value for (key, value) in zip(key_1, value_1)}
    permu_1 = [list(_) for _ in (itertools.combinations(range(d), 2))]

    matrix = [[1, 1, 1, 1, 1, 1, 1, 1],
              [1, 1, 1, 1, 1, 1, 1, 1],
              [1, 1, 1, 1, 1, 1, 1, 1],
              [1, 1, 1, 1, 1, 1, 1, 1],
              [1, 1, 1, 1, 1, 1, 1, 1],
              [1, 1, 1, 1, 1, 1, 1, 1],
              [1, 1, 1, 1, 1, 1, 1, 1],
              [1, 1, 1, 1, 1, 1, 1, 1]]

    for i in permu_1:
        matrix[i[0]][i[1]] = "".join(sorted(set.intersection(set(list(dicti_1[i[0]])), set(list(dicti_1[i[1]])))))
        if matrix[i[0]][i[1]] == '':
            matrix[i[0]][i[1]] = "0"

    flat_matrix = list(itertools.chain(*matrix))
    print("hyper-parallel number:", flat_matrix.count("0"))

    df_matrix = pd.DataFrame(matrix, columns=range(1, d + 1), index=range(1, d + 1))
    print("adjacent matrix:")
    print(df_matrix)

    listOfPositions = getIndexes(df_matrix, "0")
    print("hyper-parallel pairs:", listOfPositions)
    infty = [_[0] * 10 + _[1] for _ in listOfPositions]

    key_allin_list = [12, 13, 14, 15, 16, 17, 18,
                      23, 24, 25, 26, 27, 28,
                      34, 35, 36, 37, 38,
                      45, 46, 47, 48,
                      56, 57, 58,
                      67, 68,
                      78]
    lose_value = len(listOfPositions)
    value_select_list = range(1, (dp - lose_value + 1))
    key_select_list = [i for i in key_allin_list if i not in infty]
    dicti = {key: value for (key, value) in zip(key_select_list, value_select_list)}
    print("key_list", key_select_list)
    print("dicti", dicti)

    print()

    #library

    print("sphrical library", "*" * 100)

    path = "./ToolPolytope"
    s3lis = pd.read_csv(path + "/Slis/S3lis.txt", sep=' ', names=range(1, 4))
    s3lis = s3lis.values.tolist()
    ts3 = [[int(j) for j in s3lis[i]] for i in range(len(s3lis))]

    s4lis = pd.read_csv(path + "/Slis/S4lis.txt", sep=' ', names=range(1, 7))
    s4lis = s4lis.values.tolist()
    ts4 = [[int(j) for j in s4lis[i]] for i in range(len(s4lis))]

    s5lis = pd.read_csv(path + "/Slis/S5lis.txt", sep=' ', names=range(1, 11))
    s5lis = s5lis.values.tolist()
    ts5 = [[int(j) for j in s5lis[i]] for i in range(len(s5lis))]

    s6lis = pd.read_csv(path + "/Slis/S6lis.txt", sep=' ', names=range(1, 16))
    s6lis = s6lis.values.tolist()
    ts6 = [[int(j) for j in s6lis[i]] for i in range(len(s6lis))]

    s7lis = pd.read_csv(path + "/Slis/S7lis.txt", sep=' ', names=range(1, 21))
    s7lis = s7lis.values.tolist()
    ts7 = [[int(j) for j in s7lis[i]] for i in range(len(s7lis))]

    print("3-7 spherical sizes:", len(ts3), len(ts4), len(ts5), len(ts6), len(ts7))

    print("euclidean library", "*" * 100)
    e3lis = pd.read_csv(path + "/Elis/E3lis.txt", sep=' ', names=range(1, 4))
    e3lis = e3lis.values.tolist()
    te3 = [[int(j) for j in e3lis[i]] for i in range(len(e3lis))]

    e4lis = pd.read_csv(path + "/Elis/E4lis.txt", sep=' ', names=range(1, 7))
    e4lis = e4lis.values.tolist()
    te4 = [[int(j) for j in e4lis[i]] for i in range(len(e4lis))]

    e5lis = pd.read_csv(path + "/Elis/E5lis.txt", sep=' ', names=range(1, 11))
    e5lis = e5lis.values.tolist()
    te5 = [[int(j) for j in e5lis[i]] for i in range(len(e5lis))]

    e6lis = pd.read_csv(path + "/Elis/E6lis.txt", sep=' ', names=range(1, 16))
    e6lis = e6lis.values.tolist()
    te6 = [[int(j) for j in e6lis[i]] for i in range(len(e6lis))]

    e7lis = pd.read_csv(path + "/Elis/E7lis.txt", sep=' ', names=range(1, 21))
    e7lis = e7lis.values.tolist()
    te7 = [[int(j) for j in e7lis[i]] for i in range(len(e7lis))]

    print("3-7 euclidean sizes:", len(te3), len(te4), len(te5), len(te6), len(te7))

    print("lanner library", "*" * 100)
    l4lis = pd.read_csv(path + "/Lanner/L4lis.txt", sep=' ', names=range(1, 7))
    l4lis = l4lis.values.tolist()
    tl4 = [[int(j) for j in l4lis[i]] for i in range(len(l4lis))]
    print("4 lanner sizes:", len(tl4))

    tl4_basis = [[2]]
    print("4 lanner basis sizes:", len(tl4_basis))

    print("infty library", "*" * 100)
    ti4 = [[2] * 4]
    print("4 infty2 sizes:", len(ti4))
    print("*" * 150)

    Nv = len(Vert)
    Nr = len(key_select_list)
    Nl = len(ts4)  # ts4 is verify set and s4 is test set
    print("number of vert:", Nv, "number of non-hyp-parallel:", Nr, "number of spherical candidates:", Nl)

    print()

    #generate test sets

    print("killer set:", "*" * 100)
    virtual_vert_set = [set(list(_)) for _ in list(itertools.combinations(range(1, d + 1), 4))]
    vert_set = [set(_) for _ in Vert]
    non_vert_set = [_ for _ in virtual_vert_set if _ not in vert_set]
    non_vert = [sort_order(_) for _ in [list(_) for _ in non_vert_set]]
    pre_s4 = [[10 * i + j for i, j in list(itertools.combinations(k, 2))] for k in non_vert]
    s4 = [i for i in pre_s4 if list(set(i) & set(infty)) == []]
    non_vert_facets = [stimultaneous_spl(_) for _ in s4]
    s4cols = [select_columns(_) for _ in s4]
    print("s4", "(", len(s4), ")", ":", non_vert_facets)

    pre_e4 = [[10 * i + j for i, j in list(itertools.combinations(k, 2))] for k in
              list(itertools.combinations(range(1, d + 1), 4))]
    e4 = [i for i in pre_e4 if list(set(i) & set(infty)) == []]
    e4_facets = [stimultaneous_spl(_) for _ in e4]
    e4cols = [select_columns(_) for _ in e4]
    print("e4", "(", len(e4), "):", e4_facets)

    temp_i4 = [infty2(_) for _ in pre_e4]
    i4 = [x for x in temp_i4 if x is not None] ## 23 45 36..
    ## print("i41", i4)
    i4 = [sort_order(_) for _ in i4]
    ## print("i42", i4)
    i4_cols = [select_columns( _) for _ in i4]  ## 1,2,28..
    ## print("i4_cols", i4_cols)
    i4_facets = [stimultaneous_spl(_) for _ in i4]  ## {2,3,4,5}..
    print("i4", "(", len(i4), "):", i4_facets)

    Edge = []
    for i in Vert:
        Edge += [list(j) for j in list(itertools.combinations(i, 3))]
    Edge.sort()
    Edge = list(Edge for Edge, _ in itertools.groupby(Edge))
    Edge = [set(_) for _ in Edge]

    virtual_edge_set = [set(list(_)) for _ in list(itertools.combinations(range(1, d + 1), 3))]
    edge_set = [set(_) for _ in Edge]
    non_edge_set = [_ for _ in virtual_edge_set if _ not in edge_set]
    non_edge = [sort_order(_) for _ in [list(_) for _ in non_edge_set]]
    pre_s3 = [[10 * i + j for i, j in list(itertools.combinations(k, 2))] for k in non_edge]
    s3 = [i for i in pre_s3 if list(set(i) & set(infty)) == []]
    non_edge_facets = [stimultaneous_spl(_) for _ in s3]
    s3cols = [select_columns( _) for _ in s3]
    print("s3", "(", len(s3), "):", non_edge_facets)

    pre_e3 = [[10 * i + j for i, j in list(itertools.combinations(k, 2))] for k in
              list(itertools.combinations(range(1, d + 1), 3))]
    e3 = [i for i in pre_e3 if list(set(i) & set(infty)) == []]
    e3_facets = [stimultaneous_spl(_) for _ in e3]
    e3cols = [select_columns( _) for _ in e3]
    print("e3", "(", len(e3), "):", e3_facets)

    select_lanner = [check_nonvert_all_edge(_) for _ in range(len(non_vert_facets))]
    trick = ["F"]
    l_index = [_ for _ in select_lanner if _ not in trick]
    l4 = [s4[_] for _ in l_index]
    l4_cols = [select_columns( _) for _ in l4]
    l4_facets = [stimultaneous_spl(_) for _ in l4]
    print("l4", "(", len(l4), "):", l4_facets)

    l4_bf = ([_ + 1 for _ in range(len(value_1)) if len(value_1[_]) == 4])
    l4_basis = []
    l4_basis_facets = []

    for k in l4_bf:
        temp = sort_order([i + 1 for i in range(d) if matrix[k - 1][i] != 1 if matrix[k - 1][i] != '0'] +
                          [i + 1 for i in range(d) if matrix[i][k - 1] != 1 if matrix[i][k - 1] != '0'])
        l4_basis_facets += [temp]
        temp2 = [sort_order([k, q]) for q in temp]
        l4_basis += [10 * i + j for [i, j] in temp2]

    l4_basis = [[i] for i in sort_order(l4_basis)]

    print("l4_basis", "(", len(l4_bf), "):", [[l4_bf[i], l4_basis_facets[i]] for i in range(len(l4_bf))])

    pre_se5 = [[10 * i + j for i, j in list(itertools.combinations(k, 2))] for k in
               list(itertools.combinations(range(1, d + 1), 5))]
    se5 = [i for i in pre_se5 if list(set(i) & set(infty)) == []]
    se5_facets = [stimultaneous_spl(_) for _ in se5]
    se5cols = [select_columns( _) for _ in se5]
    print("se5", "(", len(se5), "):", se5_facets)

    pre_se6 = [[10 * i + j for i, j in list(itertools.combinations(k, 2))] for k in
               list(itertools.combinations(range(1, d + 1), 6))]
    se6 = [i for i in pre_se6 if list(set(i) & set(infty)) == []]
    se6_facets = [stimultaneous_spl(_) for _ in se6]
    se6cols = [select_columns( _) for _ in se6]
    print("se6", "(", len(se6), "):", se6_facets)

    pre_se7 = [[10 * i + j for i, j in list(itertools.combinations(k, 2))] for k in
               list(itertools.combinations(range(1, d + 1), 7))]
    se7 = [i for i in pre_se7 if list(set(i) & set(infty)) == []]
    se7_facets = [stimultaneous_spl(_) for _ in se7]
    se7cols = [select_columns( _) for _ in se7]
    print("se7", "(", len(se7), "):", se7_facets)

    vert = []
    link = []
    cover = []
    for i in range(Nv):
        vert = vert + [[10 * i + j for i, j in list(itertools.combinations(Vert[i], 2))]]

    cover = cover + [sort_order(list(set(vert[0]).union(set(vert[1]))))]
    link = [list(set(vert[0]).intersection(set(vert[1])))]

    for k in range(2, Nv):
        link = link + [list(set(cover[k - 2]).intersection(set(vert[k])))]
        cover = cover + [sort_order(list(set(cover[k - 2]).union(set(vert[k]))))]

    cover_list = [vert[0]] + cover

    print("link:", link)
    print("cover_list:", cover_list)

    range_s3 = range(len(s3))
    range_s4 = range(len(s4))
    range_se5 = range(len(se5))
    range_se6 = range(len(se6))
    range_se7 = range(len(se7))
    range_e3 = range(len(e3))
    range_e4 = range(len(e4))
    range_l4 = range(len(l4))
    range_l4_basis = range(len(l4_basis))
    range_i4 = range(len(i4))

    cancel_list = []
    for i in range(len(Vert)):
        temp = []
        temps3 = []
        temps4 = []
        tempse5 = []
        tempse6 = []
        tempse7 = []
        tempe3 = []
        tempe4 = []
        templ4 = []
        templ4_basis = []
        tempi4 = []

        for j in range_s3:
            temp1 = s3[j]
            judge_temp = [k for k in temp1 if k in cover_list[i]]
            if len(temp1) == len(judge_temp):
                temps3 += [j]
                range_s3 = [_ for _ in range_s3 if _ not in temps3]

        for j in range_s4:
            temp1 = s4[j]
            judge_temp = [k for k in temp1 if k in cover_list[i]]
            if len(temp1) == len(judge_temp):
                temps4 += [j]
                range_s4 = [_ for _ in range_s4 if _ not in temps4]

        for j in range_se5:
            temp1 = se5[j]
            judge_temp = [k for k in temp1 if k in cover_list[i]]
            if len(temp1) == len(judge_temp):
                tempse5 += [j]
                range_se5 = [_ for _ in range_se5 if _ not in tempse5]

        for j in range_se6:
            temp1 = se6[j]
            judge_temp = [k for k in temp1 if k in cover_list[i]]
            if len(temp1) == len(judge_temp):
                tempse6 += [j]
                range_se6 = [_ for _ in range_se6 if _ not in tempse6]

        for j in range_se7:
            temp1 = se7[j]
            judge_temp = [k for k in temp1 if k in cover_list[i]]
            if len(temp1) == len(judge_temp):
                tempse7 += [j]
                range_se7 = [_ for _ in range_se7 if _ not in tempse7]

        for j in range_e3:
            temp1 = e3[j]
            judge_temp = [k for k in temp1 if k in cover_list[i]]
            if len(temp1) == len(judge_temp):
                tempe3 += [j]
                range_e3 = [_ for _ in range_e3 if _ not in tempe3]

        for j in range_e4:
            temp1 = e4[j]
            judge_temp = [k for k in temp1 if k in cover_list[i]]
            if len(temp1) == len(judge_temp):
                tempe4 += [j]
                range_e4 = [_ for _ in range_e4 if _ not in tempe4]

        for j in range_l4:
            temp1 = l4[j]
            judge_temp = [k for k in temp1 if k in cover_list[i]]
            if len(temp1) == len(judge_temp):
                templ4 += [j]
                range_l4 = [_ for _ in range_l4 if _ not in templ4]

        for j in range_l4_basis:
            temp1 = l4_basis[j]
            judge_temp = [k for k in temp1 if k in cover_list[i]]
            if len(temp1) == len(judge_temp):
                templ4_basis += [j]
                range_l4_basis = [_ for _ in range_l4_basis if _ not in templ4_basis]

        for j in range_i4:
            temp1 = i4[j]
            judge_temp = [k for k in temp1 if k in cover_list[i]]
            if len(temp1) == len(judge_temp):
                tempi4 += [j]
                range_i4 = [_ for _ in range_i4 if _ not in tempi4]

        temp += [
            [i, temps3, temps4, tempse5, tempse6, tempse7, tempe3, tempe4, tempse5, tempse6, tempse7, templ4, templ4_basis, tempi4]]
        cancel_list += temp

    print("cancel_list:", cancel_list)

    print()

    condition_all = (s3, s4, se5, se6, se7, e3, e4, se5, se6, se7, l4, l4_basis, i4)  ## cols
    library_all = (ts3, ts4, ts5, ts6, ts7, te3, te4, te5, te6, te7, tl4, tl4_basis, ti4)  ## admitting angles
    kind_all = ("s3", "s4", "s5", "s6", "s7", "e3", "e4", "e5", "e6", "e7", "l4", "l4_basis", "i4")  ## type

    condition = [condition_all[x - 1] for x in [1, 6, 7, 13, 12, 11]]
    library = [library_all[x - 1] for x in [1, 6, 7, 13, 12, 11]]
    kind = [kind_all[x - 1] for x in [1, 6, 7, 13, 12, 11]]

    print("pasting + round 1 killing:", "*" * 100)

    select_cancel_list = [[i[j] for j in [0, 1, 6, 7, 13, 12, 11]] for i in cancel_list]
    print("select_cancel_list:", select_cancel_list)

    tstar = time.time()

    T = []
    t1 = time.time()
    sel_cols = select_columns(vert[0])
    df_1 = np.zeros([Nl, Nr])
    df_1[:, [j - 1 for j in sel_cols]] = pd.DataFrame(ts4, dtype=np.int8, columns=list(range(1, dh + 1)))  # 【6个二面角】
    data = pd.DataFrame(df_1, dtype=np.int8, columns=[str(_) for _ in list(range(1, Nr + 1))])
    t2 = time.time()
    Ttemp = ["df1:", data.shape, t2 - t1]
    T += [Ttemp]
    print(Ttemp)

    for k in range(len(select_cancel_list[0]) - 2, len(select_cancel_list[0])):
        if select_cancel_list[0][k] != []:
            cdt = condition[k - 1]
            lib = library[k - 1]
            for j in select_cancel_list[0][k]:
                t1 = time.time()
                saving = functools.partial(saver, j)
                data[str(Nr + 1)] = data.apply(saving, axis=1)
                # print(data)
                data = data.loc[data[str(Nr + 1)] == True][[str(_) for _ in range(1, Nr + 1)]]
                t2 = time.time()
                Ttemp = ["df1", kind[k - 1], select_cancel_list[0][k], data.shape[0], t2 - t1]
                T += [Ttemp]
                print(Ttemp)

    for k in range(1, len(select_cancel_list[0]) - 2):
        if select_cancel_list[0][k] != []:
            cdt = condition[k - 1]
            lib = library[k - 1]
            for j in select_cancel_list[0][k]:
                t1 = time.time()
                killing = functools.partial(killer, j)
                data[str(Nr + 1)] = data.apply(killing, axis=1)
                # print(data)
                data = data.loc[data[str(Nr + 1)] == True][[str(_) for _ in range(1, Nr + 1)]]
                t2 = time.time()
                Ttemp = ["df1", kind[k - 1], select_cancel_list[0][k], data.shape[0], t2 - t1]
                T += [Ttemp]
                print(Ttemp)

    t1 = time.time()
    sel_cols = select_columns(vert[1])
    df_2 = np.zeros([Nl, Nr])
    df_2[:, [j - 1 for j in sel_cols]] = pd.DataFrame(ts4, dtype=np.int8, columns=list(range(1, dh + 1)))
    df_2 = pd.DataFrame(df_2, dtype=int, columns=[str(name) for name in list(range(1, Nr + 1))])
    # merging by linking
    tem = sort_order(select_columns(link[0]))
    print(tem)
    tem1 = list(set(range(1, Nr + 1)) - set(tem))
    data = pd.merge(df_2, data, on=[str(_) for _ in tem])
    # selecting result columns and form data
    for j in tem1:
        data[str(j)] = data[str(j) + "_x"] + data[str(j) + "_y"]
    data = data[[str(_) for _ in range(1, Nr + 1)]]
    t2 = time.time()
    Ttemp = ["df2:", data.shape, t2 - t1]
    T += [Ttemp]
    print(Ttemp)

    for k in range(len(select_cancel_list[0]) - 2, len(select_cancel_list[0])):
        if select_cancel_list[1][k] != []:
            cdt = condition[k - 1]
            lib = library[k - 1]
            for j in select_cancel_list[1][k]:
                t1 = time.time()
                saving = functools.partial(saver, j)
                data[str(Nr + 1)] = data.apply(saving, axis=1)
                # print(data)
                data = data.loc[data[str(Nr + 1)] == True][[str(_) for _ in range(1, Nr + 1)]]
                t2 = time.time()
                Ttemp = ["df2", [kind[k - 1], select_cancel_list[1][k], data.shape[0], t2 - t1]]
                T += [Ttemp]
                print(Ttemp)

    for k in range(1, len(select_cancel_list[0]) - 2):
        if select_cancel_list[1][k] != []:  ###1
            cdt = condition[k - 1]
            lib = library[k - 1]
            for j in select_cancel_list[1][k]:
                t1 = time.time()
                killing = functools.partial(killer, j)
                data[str(Nr + 1)] = data.apply(killing, axis=1)
                # print(data)
                data = data.loc[data[str(Nr + 1)] == True][[str(_) for _ in range(1, Nr + 1)]]
                t2 = time.time()
                Ttemp = ["df2", [kind[k - 1], select_cancel_list[1][k], data.shape[0], t2 - t1]]  ###1
                T += [Ttemp]
                print(Ttemp)

    for i in range(2, Nv):
        t1 = time.time()
        sel_cols = select_columns(vert[i])
        temp = "df_" + str(i + 1)
        exec(temp + "=np.zeros([Nl,Nr])")
        exec(temp + "[:,[ j-1 for j in sel_cols]]=pd.DataFrame(ts4,dtype=np.int8,columns=list(range(1,dh+1)))")
        exec(temp + "=pd.DataFrame(" + temp + ",dtype=np.int8,columns=[str(_) for _ in list(range(1,Nr+1))])")
        tem = sort_order(select_columns(link[i - 1]))
        tem1 = list(set(range(1, Nr + 1)) - set(tem))
        exec("data_temp=pd.merge(" + temp + ",data,on=[str(_) for _ in tem])"); data=locals()['data_temp']
        for j in tem1:
            data[str(j)] = data[str(j) + "_x"] + data[str(j) + "_y"]
        data = data[[str(_) for _ in range(1, Nr + 1)]]
        t2 = time.time()
        Ttemp = ["df", i + 1, data.shape, t2 - t1]
        T += [Ttemp]
        print(Ttemp)

        if data.shape[0] == 0:
            break

        for k in range(len(select_cancel_list[0]) - 2, len(select_cancel_list[0])):
            if select_cancel_list[i][k] != []:
                cdt = condition[k - 1]
                lib = library[k - 1]
                for j in select_cancel_list[i][k]:
                    t1 = time.time()
                    saving = functools.partial(saver, j)
                    data[str(Nr + 1)] = data.apply(saving, axis=1)
                    # print(data)
                    data = data.loc[data[str(Nr + 1)] == True][[str(_) for _ in range(1, Nr + 1)]]
                    t2 = time.time()

                    Ttemp = ["df", i + 1, [kind[k - 1], select_cancel_list[i][k], data.shape[0], t2 - t1]]
                    T += [Ttemp]
                    print(Ttemp)

        for k in range(1, len(select_cancel_list[0]) - 2):
            if select_cancel_list[i][k] != []:
                cdt = condition[k - 1]
                lib = library[k - 1]
                for j in select_cancel_list[i][k]:
                    t1 = time.time()
                    killing = functools.partial(killer, j)
                    data[str(Nr + 1)] = data.apply(killing, axis=1)
                    # print(data)
                    data = data.loc[data[str(Nr + 1)] == True][[str(_) for _ in range(1, Nr + 1)]]
                    t2 = time.time()

                    Ttemp = ["df", i + 1, [kind[k - 1], select_cancel_list[i][k], data.shape[0], t2 - t1]]  ###1
                    T += [Ttemp]
                    print(Ttemp)

    tend = time.time()
    print("total time consuming:", tend - tstar)
    print("data round1:", data.shape)

    # after per
    per = pd.read_csv("P8_17_per.txt", sep=' ', names=range(1, d + 1))
    data.columns = range(1, Nr + 1)  # 更改column名是因为之前per和change7是在另一个文档按照这个列名写的
    sym_cardinality = int(per.shape[0])

    for i in range(sym_cardinality):
        sym_key_list = range(1, int(per.shape[1]) + 1)
        sym_value_list = list(per.iloc[i])
        nam = "dir_" + str(i + 1)
        exec(nam + "={key: value for (key, value) in zip(sym_key_list, sym_value_list)}")

    kl_spl = [spl(_) for _ in key_select_list]
    kl_matrix = np.transpose(np.array([key_list_after_sym(_) for _ in kl_spl]))
    kl_columns = np.apply_along_axis(select_columns, 1, kl_matrix)

    tstart = time.time()
    data_array = np.array(data)
    pre_result = set(frozenset(i) for i in [generate(_) for _ in data_array])
    print(len(pre_result))
    tend = time.time()
    print("permu time consuming1:", tend - tstart)

    tstart = time.time()
    pre_temp = [list(_) for _ in list(pre_result)]
    pre_temp = [pre_temp[i][0] for i in range(len(pre_result))]
    data_after_per = [list(j) for j in pre_temp]
    data_after_per = [conver_preresult(_) for _ in range(len(pre_temp))]
    # data_after_per = [conver_preresult(_) for _ in range(len(pre_result))]
    tend = time.time()
    print("permu time consuming2:", tend - tstart)

    data = pd.DataFrame(data_after_per, dtype=int,
                        columns=[str(_) for _ in range(1, Nr + 1)])  # columns name 不用str,否则replace函数的切片指标会差1
    data.to_csv('output/P8_17/P8_17_LSIEr1_per.txt', header=None, index=None, sep=' ', mode='a')

    # round 2
    print()
    print("pasting+round 2 killing:", "*" * 100)
    select_cancel_list = [[i[j] for j in [0, 2, 3, 4, 5, 8, 9, 10]] for i in cancel_list]
    print("select_cancel_list:", select_cancel_list)

    condition = [condition_all[x - 1] for x in [2, 3, 4, 5, 8, 9, 10]]
    library = [library_all[x - 1] for x in [2, 3, 4, 5, 8, 9, 10]]
    kind = [kind_all[x - 1] for x in [2, 3, 4, 5, 8, 9, 10]]

    tstart = time.time()

    for i in range(Nv):
        for k in range(1, len(select_cancel_list[0])):
            if select_cancel_list[i][k] != []:  ###1
                cdt = condition[k - 1]  # cdt,lib,kind都是i-1
                lib = library[k - 1]
                for j in select_cancel_list[i][k]:  ###这里是取0,后面是参数选取 ###1
                    t1 = time.time()
                    killing = functools.partial(killer, j)
                    data[str(Nr + 1)] = data.apply(killing, axis=1)
                    # print(data)
                    data = data.loc[data[str(Nr + 1)] == True][[str(_) for _ in range(1, Nr + 1)]]
                    t2 = time.time()
                    Ttemp = ["df", i + 1, ":", kind[k - 1], select_cancel_list[i][k], data.shape[0], t2 - t1]
                    T += [Ttemp]  ###1
                    print(Ttemp)

    tend = time.time()
    print("total time consuming:", tend - tstar)
    print("data round 2:", data.shape)

    data.to_csv('output/P8_17/P8_17_LSIEr2_per.txt', header=None, index=None, sep=' ', mode='a')

    # chenage 7

    tstart = time.time()
    data.columns = range(1, Nr + 1)
    temp = [replace7(_) for _ in range(len(data))]
    tend = time.time()
    print("change7 time consuming:", tend - tstart)

    data.to_csv('output/P8_17/P8_17_LSIE_per_change7.txt', header=None, index=None, sep=' ', mode='a')
    return

if __name__ == '__main__':
    Vert = [[1, 2, 4, 6], [1, 3, 4, 6], [2, 3, 4, 6], [1, 2, 5, 6], [1, 3, 5, 6], [2, 3, 5, 6],
            [1, 2, 5, 7], [1, 3, 5, 7], [1, 2, 4, 7], [1, 3, 4, 7], [2, 3, 5, 8], [2, 5, 7, 8],
            [3, 5, 7, 8], [2, 3, 4, 8], [2, 4, 7, 8], [3, 4, 7, 8]]
    run48(Vert)


import bz2
import queue
import threading
import mtda.constants as CONSTS
import zlib
import zstandard as zstd
import lzma

class AsyncImageWriter(queue.Queue):

    def __init__(self, mtda, storage, compression=CONSTS.IMAGE.RAW):
        self.mtda = mtda
        self.storage = storage
        self.compression = compression
        self._blksz = CONSTS.WRITER.WRITE_SIZE
        self._exiting = False
        self._failed = False
        self._thread = None
        self._writing = False
        self._written = 0
        self._zdec = None
        super().__init__(maxsize=CONSTS.WRITER.QUEUE_SLOTS)

    @property
    def compression(self):
        self.mtda.debug(3, "storage.writer.compression.get()")

        result = self._compression

        self.mtda.debug(3, "storage.writer.compression.get(): "
                           "%s" % str(result))
        return result

    @compression.setter
    def compression(self, compression):
        self.mtda.debug(3, "storage.writer.compression.set()")

        compression = CONSTS.IMAGE(compression)
        if compression == CONSTS.IMAGE.RAW:
            self._write = self.write_raw
        elif compression == CONSTS.IMAGE.BZ2:
            self._write = self.write_bz2
        elif compression == CONSTS.IMAGE.GZ:
            self._write = self.write_gz
        elif compression == CONSTS.IMAGE.ZST:
            self._write = self.write_zst
        elif compression == CONSTS.IMAGE.XZ:
            self._write = self.write_xz
        else:
            raise ValueError("unsupported image compression!")
        self._compression = compression

        result = compression
        self.mtda.debug(3, "storage.writer.compression.set(): "
                           "%s" % str(result))

    @property
    def failed(self):
        return self._failed

    def put(self, chunk, block=True, timeout=None):
        self.mtda.debug(3, "storage.writer.put()")

        if self.storage is None:
            self.mtda.debug(1, "storage.writer.put(): no storage!")
            raise IOError("no storage!")
        result = super().put(chunk, block, timeout)
        # if thread is started and put data is not empty
        if len(chunk) > 0 and self._exiting is False:
            self._writing = True
        self.mtda.debug(3, "storage.writer.put(): %s" % str(result))
        return result

    def start(self):
        self.mtda.debug(3, "mtda.storage.writer.start()")

        result = None
        self._thread = threading.Thread(target=self.worker,
                                        daemon=True, name='writer')
        self._thread.start()

        self.mtda.debug(3, "storage.writer.start(): %s" % str(result))
        return result

    def stop(self):
        self.mtda.debug(3, "storage.writer.stop()")

        result = None
        self.mtda.debug(2, "storage.writer.stop(): waiting on queue...")
        self.join()

        if self._thread is not None:
            self.mtda.debug(2, "storage.writer.stop(): waiting on thread...")
            self._exiting = True
            self.put(b'')
            self._thread.join()

        self.mtda.debug(2, "storage.writer.stop(): all done")
        self._thread = None
        self._zdec = None

        self.mtda.debug(3, "storage.writer.stop(): %s" % str(result))
        return result

    def worker(self):
        self.mtda.debug(3, "storage.writer.worker()")

        result = None
        self._exiting = False
        self._failed = False
        self._written = 0
        while self._exiting is False:
            if self.empty():
                self._writing = False
            chunk = self.get()
            if self._exiting is False:
                try:
                    self._write(chunk)
                except Exception as e:
                    self.mtda.debug(1, "storage.writer.worker(): {}".format(e))
                    self._failed = True
                    self._writing = False
                    pass
            self.task_done()
            if self._failed is True:
                self.mtda.debug(1, "storage.writer.worker(): "
                                   "write or decompression error!")

        self.mtda.debug(3, "storage.writer.worker(): %s" % str(result))
        return result

    def write_raw(self, data, session=None):
        self.mtda.debug(3, "storage.writer.write_raw()")

        result = None
        try:
            result = self.storage.write(data)
        except OSError as e:
            self.mtda.debug(1, "storage.writer.write_raw(): {}".format(e))
            raise

        self.mtda.debug(3, "storage.writer.write_raw(): %s" % str(result))
        return result

    def write_gz(self, data, session=None):
        self.mtda.debug(3, "storage.writer.write_gz()")

        # Create a zlib decompressor when called for the first time
        if self._zdec is None:
            self._zdec = zlib.decompressobj(16+zlib.MAX_WBITS)

        try:
            cont = True
            result = None
            while cont is True:
                uncompressed = self._zdec.decompress(data, self._blksz)
                data = self._zdec.unconsumed_tail
                cont = len(data) > 0
                result = self.storage.write(uncompressed)
        except (OSError, zlib.error) as e:
            self.mtda.debug(1, "storage.writer.write_gz(): {}".format(e))
            raise

        self.mtda.debug(3, "storage.writer.write_gz(): %s" % str(result))
        return result

    def write_bz2(self, data):
        self.mtda.debug(3, "storage.writer.write_bz2()")

        # Create a bz2 decompressor when called for the first time
        if self._zdec is None:
            self._zdec = bz2.BZ2Decompressor()

        try:
            cont = True
            result = None
            while cont is True:
                uncompressed = self._zdec.decompress(data, self._blksz)
                result = self.storage.write(uncompressed)
                cont = self._zdec.needs_input is False
                data = b''
        except EOFError:
            result = 0
        except OSError as e:
            self.mtda.debug(1, "storage.writer.write_bz2(): {}".format(e))
            raise

        self.mtda.debug(3, "storage.writer.write_bz2(): %s" % str(result))
        return result

    def write_zst(self, data):
        self.mtda.debug(3, "storage.writer.write_zst()")

        result = None
        # Create a decompressor when called for the first time
        if self._zdec is None:
            dctx = zstd.ZstdDecompressor()
            self._zdec = dctx.stream_writer(self.storage)
        try:
            result = self._zdec.write(data)
        except OSError as e:
            self.mtda.debug(1, "storage.writer.write_zst(): {}".format(e))
            raise

        self.mtda.debug(3, "storage.writer.write_zst(): %s" % str(result))
        return result

    def write_xz(self, data):
        self.mtda.debug(3, "storage.writer.write_xz()")

        # Create a xz decompressor when called for the first time
        if self._zdec is None:
            self._zdec = lzma.LZMADecompressor()

        try:
            cont = True
            result = None
            while cont is True:
                uncompressed = self._zdec.decompress(data, self._blksz)
                result = self.storage.write(uncompressed)
                cont = self._zdec.needs_input is False
                data = b''
        except EOFError:
            result = 0
        except OSError as e:
            self.mtda.debug(1, "storage.writer.write_xz(): {}".format(e))
            raise

        self.mtda.debug(3, "storage.writer.write_xz(): %s" % str(result))
        return result

    @property
    def writing(self):
        return self._writing

    @property
    def written(self):
        written = self.storage.tell()
        if written is not None:
            self._written = written
        return self._written


from bs4 import BeautifulSoup as bs
import requests
import csv
from modules.csv_writer import write_csv_file, create_csv_file
from modules.models import Product

def parse_html(url: str) -> csv:
    result = requests.get(url=url)
    soup = bs(result.text, 'lxml')
    category = soup.find("h1", class_="catalog-heading ng-star-inserted").text
    max_pages = get_max_pages(url=url)
    create_csv_file(category=category)

    page = 1
    count_pages = 0

    while page <= max_pages:
        list_products = []
        res = requests.get(f"{url}page={page}/")
        soup = bs(res.text, 'lxml')
        products = soup.find_all('li', class_='catalog-grid__cell catalog-grid__cell_type_slim ng-star-inserted')
        for product in products:
            id = product.find("div", class_="g-id display-none").text
            name = product.find("span", class_="goods-tile__title").text
            price = product.find("span", class_="goods-tile__price-value").text
            img = product.find("img", class_="ng-lazyloaded")
            image = img['src'] if img and 'src' in img.attrs else None
            link = product.find("a", class_="goods-tile__heading ng-star-inserted").get("href")
            list_products.append(
                Product(id=id, name=name, price=price, link=link, image=image)
            )
        write_csv_file(category=category, products=list_products)
        count_pages += 1
        page += 1
        if count_pages >= max_pages:
            break

def get_max_pages(url: str) -> int:
    r = requests.get(url)
    soup = bs.BeautifulSoup(r.text, "lxml")
    pages = soup.find_all("a", class_="pagination__link ng-star-inserted")
    pages_values = [page.text for page in pages]
    return int(pages_values[-1])


from django.urls import path
from .views import home, login, get_response, save_response, display

urlpatterns = [
    path("", login, name="login"),
    path("home/", home, name="home"),
    path("request/", get_response, name="request"),
    path("response/", save_response, name="response"),
    path("display/<user_id>/", display, name="display"),
]


"""
Alice and Bob want to safely exchange secret messages. To do this, they need to:

Agree on a symmetric key and establish a secure communication channel by running a Diffie-Hellman key exchange procedure
Use the established symmetric key to encrypt messages to each other using a block cipher
Your task is to help them out by implementing the SecureChannel class.
The SecureChannel class:

Receives  p  and  g , and randomizes an  a . Note that when Bob uses this function, the parameter  a  is really his  b
Its publish() function returns  ga(mod p)  - this output is what Alice sends to Bob; Bob uses the same function (except that he gets a different value)
Its encrypt() function receives  gb(mod p)  and some text, computes the shared secret S, and encrypts the text.
It converts the shared secret S into an encryption key for AES128 by casting it to a string of bytes, hashing it with SHA256,
and using the first 16 bytes of the digest as the key. It then picks a random IV of 16 bytes, and uses PyCrypto's AES128 in CBC mode
to encrypt the text. For simplicity's sake, the plaintext is always exactly 16 characters long. The encrypt() function returns the IV
prepended to the encrypted message.

"""
import random
import hashlib
import os
from Crypto import Random
from Crypto.Cipher import AES

# you can use the imports, and you can solve with your own imports

p = 283
g = 47

length = 16 #key

class SecureChannel:

    def __init__(self, p, g):
        self.p = p
        self.g = g
        self.a = random.randint(1, p)

    def publish(self):
        return pow(self.g,self.a,self.p)

    def encrypt(self, gb, text):
        S = pow(gb,self.a,self.p)
        sha256_S = hashlib.sha256()
        sha256_S.update(str(S).encode())
        hashed_S = sha256_S.digest()
        key = hashed_S[0:length]
        # generate IV
        IV = Random.new().read(AES.block_size)
        cipher = AES.new(key, AES.MODE_CBC,IV)
        ciphertxt = IV+cipher.encrypt(text)
        return ciphertxt # return iv + ciphertext (in bytes)


from typing import Dict, Optional, Tuple

import bokeh.models
import bokeh.plotting
import numpy as np
import pandas as pd
from numpydoc_decorator import doc
from pandas.io.common import infer_compression

from ..util import (
    Region,
    check_types,
    parse_multi_region,
    parse_single_region,
    read_gff3,
    unpack_gff3_attributes,
)
from . import base_params, gplt_params
from .base_params import DEFAULT
from .genome_sequence import AnophelesGenomeSequenceData

class AnophelesGenomeFeaturesData(AnophelesGenomeSequenceData):
    def __init__(
        self,
        *,
        gff_gene_type: str,
        gff_default_attributes: Tuple[str, ...],
        **kwargs,
    ):
        # N.B., this class is designed to work cooperatively, and
        # so it's important that any remaining parameters are passed
        # to the superclass constructor.
        super().__init__(**kwargs)

        # TODO Consider moving these parameters to configuration, as they could
        # change if the GFF ever changed.
        self._gff_gene_type = gff_gene_type
        self._gff_default_attributes = gff_default_attributes

        # Setup caches.
        self._cache_genome_features: Dict[Tuple[str, ...], pd.DataFrame] = dict()

    @property
    def _geneset_gff3_path(self):
        return self.config["GENESET_GFF3_PATH"]

    def geneset(self, *args, **kwargs):
        """Deprecated, this method has been renamed to genome_features()."""
        return self.genome_features(*args, **kwargs)

    def _genome_features(self, *, attributes: Tuple[str, ...]):
        try:
            df = self._cache_genome_features[attributes]

        except KeyError:
            path = f"{self._base_path}/{self._geneset_gff3_path}"
            compression = infer_compression(path, compression="infer")
            with self._fs.open(path, mode="rb") as f:
                df = read_gff3(f, compression=compression)
            if attributes:
                df = unpack_gff3_attributes(df, attributes=attributes)
            self._cache_genome_features[attributes] = df

        return df

    def _genome_features_for_contig(self, *, contig: str, attributes: Tuple[str, ...]):
        debug = self._log.debug

        df = self._genome_features(attributes=attributes)

        debug("Apply contig query.")
        return df.query(f"contig == '{contig}'")

    def _prep_gff_attributes(
        self, attributes: base_params.gff_attributes
    ) -> Tuple[str, ...]:
        if attributes is None:
            attributes_normed: Tuple[str, ...] = ()
        elif attributes == DEFAULT:
            attributes_normed = self._gff_default_attributes
        elif isinstance(attributes, str):
            attributes_normed = (attributes,)
        else:
            attributes_normed = tuple(attributes)
        return attributes_normed

    @check_types
    @doc(
        summary="Access genome feature annotations.",
        returns="A dataframe of genome annotations, one row per feature.",
    )
    def genome_features(
        self,
        region: Optional[base_params.regions] = None,
        attributes: base_params.gff_attributes = DEFAULT,
    ) -> pd.DataFrame:
        debug = self._log.debug

        attributes_normed = self._prep_gff_attributes(attributes)
        del attributes

        if region is not None:
            debug("Handle region.")
            regions = parse_multi_region(self, region)
            del region

            debug("Apply region query.")
            parts = []
            for r in regions:
                df_part = self._genome_features_for_contig(
                    contig=r.contig, attributes=attributes_normed
                )
                if r.end is not None:
                    df_part = df_part.query(f"start <= {r.end}")
                if r.start is not None:
                    df_part = df_part.query(f"end >= {r.start}")
                parts.append(df_part)
            df = pd.concat(parts, axis=0)
            return df.reset_index(drop=True).copy()

        return (
            self._genome_features(attributes=attributes_normed)
            .reset_index(drop=True)
            .copy()
        )

    def genome_feature_children(
        self, parent: str, attributes: base_params.gff_attributes = DEFAULT
    ) -> pd.DataFrame:
        # Normalise attributes and ensure Parent is included.
        attributes_normed = self._prep_gff_attributes(attributes)
        if "Parent" not in attributes_normed:
            attributes_normed += ("Parent",)

        # Obtain dataframe of all genome features.
        df_gf = self._genome_features(attributes=attributes_normed).copy()

        # Split the Parent column and explode.
        # See also https://github.com/malariagen/malariagen-data-python/issues/334
        df_gf["Parent"] = df_gf["Parent"].str.split(",")
        df_gf = df_gf.explode(column="Parent", ignore_index=True)

        # Query to find children of the requested parent.
        df_children = df_gf.query(f"Parent == '{parent}'")

        return df_children.copy()

    @check_types
    @doc(summary="Plot a transcript, using bokeh.")
    def plot_transcript(
        self,
        transcript: base_params.transcript,
        sizing_mode: gplt_params.sizing_mode = gplt_params.sizing_mode_default,
        width: gplt_params.width = gplt_params.width_default,
        height: gplt_params.height = 100,
        show: gplt_params.show = True,
        x_range: Optional[gplt_params.x_range] = None,
        toolbar_location: Optional[
            gplt_params.toolbar_location
        ] = gplt_params.toolbar_location_default,
        title: gplt_params.title = True,
    ) -> gplt_params.figure:
        debug = self._log.debug

        debug("Find the transcript annotation.")
        df_genome_features = self.genome_features().set_index("ID")
        parent = df_genome_features.loc[transcript]

        if title is True:
            title = f"{transcript} ({parent.strand})"

        if x_range is None:
            x_range = bokeh.models.Range1d(
                parent.start - 2_000, parent.end + 2_000, bounds="auto"
            )

        debug("Define tooltips for hover.")
        tooltips = [
            ("Type", "@type"),
            ("Location", "@contig:@start{,}-@end{,}"),
        ]

        debug("Make a figure.")
        xwheel_zoom = bokeh.models.WheelZoomTool(
            dimensions="width", maintain_focus=False
        )
        fig = bokeh.plotting.figure(
            title=title,
            sizing_mode=sizing_mode,
            width=width,
            height=height,
            tools=["xpan", "xzoom_in", "xzoom_out", xwheel_zoom, "reset", "hover"],
            toolbar_location=toolbar_location,
            active_scroll=xwheel_zoom,
            active_drag="xpan",
            tooltips=tooltips,
            x_range=x_range,
            y_range=bokeh.models.Range1d(-0.6, 0.6),
        )

        debug("Find child components of the transcript.")
        data = self.genome_feature_children(parent=transcript, attributes=None)
        data["bottom"] = -0.4
        data["top"] = 0.4

        debug("Plot exons.")
        exons = data.query("type == 'exon'")
        fig.quad(
            bottom="bottom",
            top="top",
            left="start",
            right="end",
            source=exons,
            fill_color=None,
            line_color="black",
            line_width=0.5,
            fill_alpha=0,
        )

        debug("Plot introns.")
        for intron_start, intron_end in zip(exons[:-1]["end"], exons[1:]["start"]):
            intron_midpoint = (intron_start + intron_end) / 2
            line_data = pd.DataFrame(
                {
                    "x": [intron_start, intron_midpoint, intron_end],
                    "y": [0, 0.1, 0],
                    "type": "intron",
                    "contig": parent.contig,
                    "start": intron_start,
                    "end": intron_end,
                }
            )
            fig.line(
                x="x",
                y="y",
                source=line_data,
                line_width=1,
                line_color="black",
            )

        debug("Plot UTRs.")
        fig.quad(
            bottom="bottom",
            top="top",
            left="start",
            right="end",
            source=data.query("type == 'five_prime_UTR'"),
            fill_color="green",
            line_width=0,
            fill_alpha=0.5,
        )
        fig.quad(
            bottom="bottom",
            top="top",
            left="start",
            right="end",
            source=data.query("type == 'three_prime_UTR'"),
            fill_color="red",
            line_width=0,
            fill_alpha=0.5,
        )

        debug("Plot CDSs.")
        fig.quad(
            bottom="bottom",
            top="top",
            left="start",
            right="end",
            source=data.query("type == 'CDS'"),
            fill_color="blue",
            line_width=0,
            fill_alpha=0.5,
        )

        debug("Tidy up the figure.")
        fig.yaxis.ticker = []
        self._bokeh_style_genome_xaxis(fig, parent.contig)

        if show:  # pragma: no cover
            bokeh.plotting.show(fig)
            return None
        else:
            return fig

    @check_types
    @doc(
        summary="Plot a genes track, using bokeh.",
    )
    def plot_genes(
        self,
        region: base_params.region,
        sizing_mode: gplt_params.sizing_mode = gplt_params.sizing_mode_default,
        width: gplt_params.width = gplt_params.width_default,
        height: gplt_params.genes_height = 120,
        show: gplt_params.show = True,
        toolbar_location: Optional[
            gplt_params.toolbar_location
        ] = gplt_params.toolbar_location_default,
        x_range: Optional[gplt_params.x_range] = None,
        title: Optional[gplt_params.title] = None,
        output_backend: gplt_params.output_backend = gplt_params.output_backend_default,
    ) -> gplt_params.figure:
        debug = self._log.debug

        debug("handle region parameter - this determines the genome region to plot")
        resolved_region: Region = parse_single_region(self, region)
        del region

        debug("handle region bounds")
        contig = resolved_region.contig
        start = resolved_region.start
        end = resolved_region.end
        if start is None:
            start = 0
        if end is None:
            end = len(self.genome_sequence(contig))

        debug("define x axis range")
        if x_range is None:
            x_range = bokeh.models.Range1d(start, end, bounds="auto")

        debug("select the genes overlapping the requested region")
        data, tooltips = self._plot_genes_setup_data(region=resolved_region)

        debug(
            "we're going to plot each gene as a rectangle, so add some additional columns"
        )
        data["bottom"] = np.where(data["strand"] == "+", 1, 0)
        data["top"] = data["bottom"] + 0.8

        debug("tidy up missing values for presentation")
        data.fillna("", inplace=True)

        debug("make a figure")
        xwheel_zoom = bokeh.models.WheelZoomTool(
            dimensions="width", maintain_focus=False
        )
        fig = bokeh.plotting.figure(
            title=title,
            sizing_mode=sizing_mode,
            width=width,
            height=height,
            tools=[
                "xpan",
                "xzoom_in",
                "xzoom_out",
                xwheel_zoom,
                "reset",
                "tap",
                "hover",
            ],
            toolbar_location=toolbar_location,
            active_scroll=xwheel_zoom,
            active_drag="xpan",
            tooltips=tooltips,
            x_range=x_range,
            y_range=bokeh.models.Range1d(-0.4, 2.2),
            output_backend=output_backend,
        )

        debug("add functionality to click through to vectorbase")
        url = "https://vectorbase.org/vectorbase/app/record/gene/@ID"
        taptool = fig.select(type=bokeh.models.TapTool)
        taptool.callback = bokeh.models.OpenURL(url=url)

        debug("now plot the genes as rectangles")
        fig.quad(
            bottom="bottom",
            top="top",
            left="start",
            right="end",
            source=data,
            line_width=0,
        )

        debug("tidy up the plot")
        fig.ygrid.visible = False
        yticks = [0.4, 1.4]
        yticklabels = ["-", "+"]
        fig.yaxis.ticker = yticks
        fig.yaxis.major_label_overrides = {k: v for k, v in zip(yticks, yticklabels)}
        fig.yaxis.axis_label = "Genes"
        self._bokeh_style_genome_xaxis(fig, contig)

        if show:  # pragma: no cover
            bokeh.plotting.show(fig)
            return None
        else:
            return fig

    def _plot_genes_setup_data(self, *, region):
        attributes = [a for a in self._gff_default_attributes if a != "Parent"]
        df_genome_features = self.genome_features(region=region, attributes=attributes)
        data = df_genome_features.query(f"type == '{self._gff_gene_type}'").copy()
        tooltips = [(a.capitalize(), f"@{a}") for a in attributes]
        tooltips += [("Location", "@contig:@start{,}-@end{,}")]
        return data, tooltips

    @staticmethod
    def _bokeh_style_genome_xaxis(fig, contig):
        """Standard styling for X axis of genome plots."""
        fig.xaxis.axis_label = f"Contig {contig} position (bp)"
        fig.xaxis.ticker = bokeh.models.AdaptiveTicker(min_interval=1)
        fig.xaxis.minor_tick_line_color = None
        fig.xaxis[0].formatter = bokeh.models.NumeralTickFormatter(format="0,0")


# coding: utf-8
"""
This module defines functions which can help filtering objects by using the
sampledb.datetypes types. Each function returns a SQLAlchemy object which can
be used for the filter_func in
VersionedJSONSerializableObjectTables.get_current_objects().

Filters that use equality cannot be expected to be exact due to floating point
precision. These filters use operators that include a range of EPSILON of the
left operand's magnitude in base units.
"""

from datetime import datetime, date
import operator
import json
import typing

import sqlalchemy as db

from . import datatypes
from . import languages
from .utils import get_translated_text
from ..models.files import File
from ..models.file_log import FileLogEntry

EPSILON = 1e-7

__author__ = 'Florian Rhiem <f.rhiem@fz-juelich.de>'

def float_operator_equals(left: typing.Any, right: typing.Any) -> typing.Any:
    return db.and_(
        left * (1 - db.func.sign(left) * EPSILON) <= right,
        left * (1 + db.func.sign(left) * EPSILON) >= right
    )

def float_operator_less_than_equals(left: typing.Any, right: typing.Any) -> typing.Any:
    return left * (1 - db.func.sign(left) * EPSILON) <= right

def float_operator_greater_than_equals(left: typing.Any, right: typing.Any) -> typing.Any:
    return left * (1 + db.func.sign(left) * EPSILON) >= right

def quantity_binary_operator(db_obj: typing.Any, other: datatypes.Quantity, operator: typing.Callable[[typing.Any, typing.Any], typing.Any]) -> typing.Any:
    return db.and_(
        db_obj['_type'].astext == 'quantity',
        db_obj['dimensionality'].astext == str(other.dimensionality),
        operator(db_obj['magnitude_in_base_units'].astext.cast(db.Float), other.magnitude_in_base_units)
    )

def quantity_equals(db_obj: typing.Any, other: datatypes.Quantity) -> typing.Any:
    return quantity_binary_operator(db_obj, other, float_operator_equals)

def quantity_less_than(db_obj: typing.Any, other: datatypes.Quantity) -> typing.Any:
    return quantity_binary_operator(db_obj, other, operator.lt)

def quantity_less_than_equals(db_obj: typing.Any, other: datatypes.Quantity) -> typing.Any:
    return quantity_binary_operator(db_obj, other, float_operator_less_than_equals)

def quantity_greater_than(db_obj: typing.Any, other: datatypes.Quantity) -> typing.Any:
    return quantity_binary_operator(db_obj, other, operator.gt)

def quantity_greater_than_equals(db_obj: typing.Any, other: datatypes.Quantity) -> typing.Any:
    return quantity_binary_operator(db_obj, other, float_operator_greater_than_equals)

def quantity_between(db_obj: typing.Any, left: datatypes.Quantity, right: datatypes.Quantity, including: bool = True) -> typing.Any:
    if left.dimensionality != right.dimensionality:
        return False
    if including:
        return db.and_(
            db_obj['_type'].astext == 'quantity',
            db_obj['dimensionality'].astext == str(left.dimensionality),
            db_obj['magnitude_in_base_units'].astext.cast(db.Float) * (1 + db.func.sign(db_obj['magnitude_in_base_units'].astext.cast(db.Float)) * EPSILON) >= left.magnitude_in_base_units,
            db_obj['magnitude_in_base_units'].astext.cast(db.Float) * (1 - db.func.sign(db_obj['magnitude_in_base_units'].astext.cast(db.Float)) * EPSILON) <= right.magnitude_in_base_units
        )
    else:
        return db.and_(
            db_obj['_type'].astext == 'quantity',
            db_obj['dimensionality'].astext == str(left.dimensionality),
            db_obj['magnitude_in_base_units'].astext.cast(db.Float) > left.magnitude_in_base_units,
            db_obj['magnitude_in_base_units'].astext.cast(db.Float) < right.magnitude_in_base_units
        )

def datetime_binary_operator(db_obj: typing.Any, other: typing.Union[datatypes.DateTime, datetime], operator: typing.Callable[[typing.Any, date], typing.Any]) -> typing.Any:
    if isinstance(other, datatypes.DateTime):
        other = other.utc_datetime
    other_date = other.date()
    return db.and_(
        db_obj['_type'].astext == 'datetime',
        operator(db.func.to_timestamp(db_obj['utc_datetime'].astext, 'YYYY-MM-DD'), other_date)
    )

def datetime_equals(db_obj: typing.Any, other: typing.Union[datatypes.DateTime, datetime]) -> typing.Any:
    return datetime_binary_operator(db_obj, other, operator.eq)

def datetime_less_than(db_obj: typing.Any, other: typing.Union[datatypes.DateTime, datetime]) -> typing.Any:
    return datetime_binary_operator(db_obj, other, operator.lt)

def datetime_less_than_equals(db_obj: typing.Any, other: typing.Union[datatypes.DateTime, datetime]) -> typing.Any:
    return datetime_binary_operator(db_obj, other, operator.le)

def datetime_greater_than(db_obj: typing.Any, other: typing.Union[datatypes.DateTime, datetime]) -> typing.Any:
    return datetime_binary_operator(db_obj, other, operator.gt)

def datetime_greater_than_equals(db_obj: typing.Any, other: typing.Union[datatypes.DateTime, datetime]) -> typing.Any:
    return datetime_binary_operator(db_obj, other, operator.ge)

def datetime_between(db_obj: typing.Any, left: typing.Union[datatypes.DateTime, datetime], right: typing.Union[datatypes.DateTime, datetime], including: bool = True) -> typing.Any:
    if isinstance(left, datatypes.DateTime):
        left = left.utc_datetime
    if isinstance(right, datatypes.DateTime):
        right = right.utc_datetime
    left_date = left.date()
    right_date = right.date()
    if including:
        return db.and_(
            db_obj['_type'].astext == 'datetime',
            db.func.to_timestamp(db_obj['utc_datetime'].astext, 'YYYY-MM-DD') >= left_date,
            db.func.to_timestamp(db_obj['utc_datetime'].astext, 'YYYY-MM-DD') <= right_date,
        )
    else:
        return db.and_(
            db_obj['_type'].astext == 'datetime',
            db.func.to_timestamp(db_obj['utc_datetime'].astext, 'YYYY-MM-DD') > left_date,
            db.func.to_timestamp(db_obj['utc_datetime'].astext, 'YYYY-MM-DD') < right_date,
        )

def boolean_equals(db_obj: typing.Any, value: typing.Union[datatypes.Boolean, bool]) -> typing.Any:
    if isinstance(value, datatypes.Boolean):
        value = value.value
    return db.and_(
        db_obj['_type'].astext == 'bool',
        db_obj['value'].astext.cast(db.Boolean) == value
    )

def boolean_true(db_obj: typing.Any) -> typing.Any:
    return boolean_equals(db_obj, True)

def boolean_false(db_obj: typing.Any) -> typing.Any:
    return boolean_equals(db_obj, False)

def text_equals(db_obj: typing.Any, text: typing.Union[datatypes.Text, str]) -> typing.Any:
    if isinstance(text, datatypes.Text):
        text_str = get_translated_text(text.text)
    else:
        text_str = text
    return db.or_(
        db.and_(
            db_obj['_type'].astext == 'text',
            db.or_(
                db.and_(
                    db.func.jsonb_typeof(db_obj['text']) == 'string',
                    db_obj['text'].astext == text_str,
                ),
                *[
                    db.and_(
                        db_obj['text'].has_key(lang_code),
                        db_obj['text'][lang_code].astext == text_str
                    )
                    for lang_code in languages.get_language_codes()
                ]
            )
        ),
        db.and_(
            db_obj['_type'].astext == 'plotly_chart',
            db_obj['plotly']['layout']['title']['text'].astext == text_str
        )
    )

def text_contains(db_obj: typing.Any, text: typing.Union[datatypes.Text, str]) -> typing.Any:
    if isinstance(text, datatypes.Text):
        text_str = get_translated_text(text.text)
    else:
        text_str = text
    return db.or_(
        db.and_(
            db_obj['_type'].astext == 'text',
            db.or_(
                db.and_(
                    db.func.jsonb_typeof(db_obj['text']) == 'string',
                    db_obj['text'].astext.like('%' + text_str + '%')
                ),
                *[
                    db.and_(
                        db_obj['text'].has_key(lang_code),
                        db_obj['text'][lang_code].astext.like('%' + text_str + '%')
                    )
                    for lang_code in languages.get_language_codes()
                ]
            )
        ),
        db.and_(
            db_obj['_type'].astext == 'plotly_chart',
            db_obj['plotly']['layout']['title']['text'].astext.like('%' + text_str + '%')
        )
    )

def sample_equals(db_obj: typing.Any, object_id: int) -> typing.Any:
    return db.and_(
        db_obj['_type'].astext == 'sample',
        db_obj['object_id'].astext.cast(db.Integer) == object_id
    )

def reference_equals(db_obj: typing.Any, reference_id: int) -> typing.Any:
    return db.or_(
        db.and_(
            db.or_(
                db_obj['_type'].astext == 'object_reference',
                db_obj['_type'].astext == 'sample',
                db_obj['_type'].astext == 'measurement'
            ),
            db_obj['object_id'].astext.cast(db.Integer) == reference_id
        ),
        db.and_(
            db_obj['_type'].astext == 'user',
            db_obj['user_id'].astext.cast(db.Integer) == reference_id
        ),
    )

def tags_contain(db_obj: typing.Any, tag: str) -> typing.Any:
    tag = tag.strip().lower()
    return db.and_(
        db_obj['_type'].astext == 'tags',
        db_obj['tags'].contains(json.dumps(tag))
    )

def attribute_not_set(db_obj: typing.Any) -> typing.Any:
    return db_obj == db.null()

def _has_file(db_obj: typing.Any, file_filter: db.ColumnElement[bool]) -> typing.Any:
    matching_files = db.select(
        File.object_id
    ).distinct().outerjoin(
        FileLogEntry,
        db.and_(
            FileLogEntry.object_id == File.object_id,
            FileLogEntry.file_id == File.id,
        )
    ).where(
        file_filter
    ).group_by(
        File.object_id,
        File.id
    ).having(
        db.func.sum(  # pylint: disable=not-callable
            db.case(
                {
                    'HIDE_FILE': 1,
                    'UNHIDE_FILE': -1
                },
                else_=0,
                value=FileLogEntry.type
            )
        ) == 0
    ).subquery()
    return db.and_(
        db_obj.object_id_column == matching_files.c.object_id,
    )

def file_name_contains(db_obj: typing.Any, text: typing.Union[datatypes.Text, str]) -> typing.Any:
    if isinstance(text, datatypes.Text):
        text_str = get_translated_text(text.text)
    else:
        text_str = text
    return _has_file(
        db_obj,
        db.or_(
            File.data.op("->>")("url").cast(db.Text).like('%' + text_str + '%'),
            File.data.op("->>")("original_file_name").cast(db.Text).like('%' + text_str + '%'),
            File.data.op("->>")("filepath").cast(db.Text).like('%' + text_str + '%')
        )
    )

def file_name_equals(db_obj: typing.Any, text: typing.Union[datatypes.Text, str]) -> typing.Any:
    if isinstance(text, datatypes.Text):
        text_str = get_translated_text(text.text)
    else:
        text_str = text
    return _has_file(
        db_obj,
        db.or_(
            File.data.op("->>")("url").cast(db.Text) == text_str,
            File.data.op("->>")("original_file_name").cast(db.Text) == text_str,
            File.data.op("->>")("filepath").cast(db.Text) == text_str
        )
    )


# -*- coding: utf-8 -*-
import csv
import phonenumbers
import pytz
from freezegun import freeze_time
from djmoney.money import Money
from datetime import date, datetime, timedelta
from django.core.cache import cache
from django.contrib.humanize.templatetags.humanize import intcomma
from django_fsm import FSMField, transition
from django.conf import settings
from django.core.validators import MaxValueValidator, MinValueValidator
from django.contrib.postgres.search import SearchVector, SearchVectorField
from django.db import models
from django.db import transaction
from django.db.models import Q
from django.utils import timezone
from django.utils.text import Truncator
from django.utils.translation import ugettext_lazy as _
from djmoney.models.fields import MoneyField
from djmoney.money import Money
from shared_foundation.constants import WORKERY_APP_DEFAULT_MONEY_CURRENCY
from shared_foundation.models import SharedUser
from tenant_foundation.constants import UNASSIGNED_JOB_TYPE_OF_ID, JOB_TYPE_OF_CHOICES, WORK_ORDER_PAID_TO_CHOICES
from tenant_foundation.utils import *

class WORK_ORDER_STATE:
    NEW = 'new'
    DECLINED = 'declined'
    PENDING = 'pending'
    CANCELLED = 'cancelled'
    ONGOING = 'ongoing'
    IN_PROGRESS = 'in_progress'
    COMPLETED_BUT_UNPAID = 'completed_and_unpaid'
    COMPLETED_AND_PAID = 'completed_and_paid'
    ARCHIVED = 'archived'

class WorkOrderManager(models.Manager):
    def delete_all(self):
        items = WorkOrder.objects.all()
        for item in items.all():
            item.delete()

    def partial_text_search(self, keyword):
        """Function performs partial text search of various textfields."""
        return WorkOrder.objects.filter(
            Q(indexed_text__icontains=keyword) |
            Q(indexed_text__istartswith=keyword) |
            Q(indexed_text__iendswith=keyword) |
            Q(indexed_text__exact=keyword) |
            Q(indexed_text__icontains=keyword)
        )

    def full_text_search(self, keyword):
        """Function performs full text search of various textfields."""
        # The following code will use the native 'PostgreSQL' library
        # which comes with Django to utilize the 'full text search' feature.
        # For more details please read:
        # https://docs.djangoproject.com/en/2.0/ref/contrib/postgres/search/
        return WorkOrder.objects.annotate(search=SearchVector(
            'indexed_text',
        ),).filter(search=keyword)

    def get_by_associate_with_user_id(self, user_id):
        cache_key  = 'associate_id_for_user_id_' + str(user_id)
        associate_id = cache.get(cache_key)
        if associate_id:
            return WorkOrder.objects.filter(associate__id=associate_id)

        associate = Associate.objects.filter(owner=obj).first()
        cache.set(cache_key, associate.id, None)
        return WorkOrder.objects.filter(associate__id=associate_id)

@transaction.atomic
def increment_order_id_number():
    """Function will generate a unique big-int."""
    last_job_order = WorkOrder.objects.all().order_by('id').last();
    if last_job_order:
        return last_job_order.id + 1
    return 1

@transaction.atomic
def get_todays_date(days=0):
    """Returns the current date plus paramter number of days."""
    return timezone.now() + timedelta(days=days)

class WorkOrder(models.Model):
    class Meta:
        app_label = 'tenant_foundation'
        db_table = 'workery_work_orders'
        verbose_name = _('Work Order')
        verbose_name_plural = _('Work Orders')
        default_permissions = ()
        permissions = (
            ("can_get_orders", "Can get work orders"),
            ("can_get_order", "Can get work order"),
            ("can_post_order", "Can create work order"),
            ("can_put_order", "Can update work order"),
            ("can_delete_order", "Can delete work order"),
        )

    objects = WorkOrderManager()
    id = models.BigAutoField(
       primary_key=True,
       default = increment_order_id_number,
       editable=False,
       db_index=True
    )

    #
    #  FIELDS
    #

    customer = models.ForeignKey(
        "Customer",
        help_text=_('The customer of our order.'),
        related_name="work_orders",
        on_delete=models.CASCADE
    )
    associate = models.ForeignKey(
        "Associate",
        help_text=_('The associate of our order.'),
        related_name="work_orders",
        on_delete=models.CASCADE,
        blank=True,
        null=True
    )
    description = models.TextField(
        _("Description"),
        help_text=_('A description of this job.'),
        blank=True,
        null=True,
        default='',
    )
    assignment_date = models.DateField(
        _('Assignment Date'),
        help_text=_('The date that an associate was assigned to the customer.'),
        blank=True,
        null=True
    )
    tags = models.ManyToManyField(
        "Tag",
        help_text=_('The category tags that this order belongs to.'),
        blank=True,
        related_name="%(app_label)s_%(class)s_tags_related",
    )
    is_ongoing = models.BooleanField(
        _("Is ongoing"),
        help_text=_('Track whether this order is ongoing job or one-time job.'),
        default=False,
        blank=True
    )
    is_home_support_service = models.BooleanField(
        _("Is Home Support Service"),
        help_text=_('Track whether this order is a home support service request.'),
        default=False,
        blank=True
    )
    start_date = models.DateField(
        _('Start Date'),
        help_text=_('The date that this order will begin.'),
        blank=True,
        default=get_todays_date
    )
    completion_date = models.DateField(
        _('Completion Date'),
        help_text=_('The date that this order was completed.'),
        blank=True,
        null=True
    )
    hours = models.DecimalField(
        _("Hours"),
        help_text=_('The total amount of hours worked on for this order by the associate.'),
        default=0,
        max_digits=7,
        decimal_places=1,
        blank=True,
        null=True
    )
    skill_sets = models.ManyToManyField(
        "SkillSet",
        help_text=_('The skill sets that belong to this order.'),
        blank=True,
        related_name="%(app_label)s_%(class)s_skill_sets_related",
    )
    type_of = models.PositiveSmallIntegerField(
        _("Type Of"),
        help_text=_('The type of job this is.'),
        default=UNASSIGNED_JOB_TYPE_OF_ID,
        choices=JOB_TYPE_OF_CHOICES,
        blank=True,
    )
    indexed_text = models.CharField(
        _("Indexed Text"),
        max_length=2047,
        help_text=_('The searchable content text used by the keyword searcher function.'),
        blank=True,
        null=True,
        db_index=True,
        unique=True
    )
    comments = models.ManyToManyField(
        "Comment",
        help_text=_('The comments belonging to this order made by other people.'),
        blank=True,
        through='WorkOrderComment',
        related_name="%(app_label)s_%(class)s_order_comments_related"
    )
    closing_reason = models.PositiveSmallIntegerField(
        _("Closing Reason"),
        help_text=_('The reason for this job order closing.'),
        blank=True,
        null=True,
        default=0,
    )
    closing_reason_other = models.CharField(
        _("Closing Reason other"),
        help_text=_('A specific reason this job order was closed.'),
        max_length=1024,
        blank=True,
        null=True,
        default='',
    )
    closing_reason_comment = models.CharField(
        _("Closing Reason comment"),
        help_text=_('Details as to why the job was closed.'),
        max_length=1024,
        blank=True,
        null=True,
        default='',
    )
    latest_pending_task = models.ForeignKey(
        "TaskItem",
        help_text=_('The latest pending task of our job order.'),
        related_name="work_orders",
        on_delete=models.SET_NULL,
        blank=True,
        null=True
    )
    activity_sheet = models.ManyToManyField(
        "Associate",
        help_text=_('The activity sheet items related to the associates who accepted or rejected this order.'),
        blank=True,
        through='ActivitySheetItem',
        related_name="%(app_label)s_%(class)s_activity_sheet_items_related"
    )

    #
    # State
    #

    state = FSMField(
        _('State'),
        help_text=_('The state of this job order.'),
        default=WORK_ORDER_STATE.NEW,
        blank=True,
        db_index=True,
    )

    #
    #  Satisfaction Survey & Score Fields
    #

    was_survey_conducted = models.BooleanField(
        _("Was Survey Conducted"),
        help_text=_('Track whether survey was conducted post completion (if completed).'),
        default=False,
        blank=True
    )
    no_survey_conducted_reason = models.PositiveSmallIntegerField(
        _("No Survey Conducted Reason"),
        help_text=_('The reason no survey was conducted.'),
        blank=True,
        null=True,
        # 1 = Other
        # 2 = Unable to reach client
        # 3 = Client did not want to complete survey
    )
    no_survey_conducted_reason_other = models.CharField(
        _("No Survey Conducted Reason (Other)"),
        help_text=_('The specific reason this job order had no survey conducted.'),
        max_length=1024,
        blank=True,
        null=True,
    )
    was_job_satisfactory = models.BooleanField(
        _("Was job satisfactory?"),
        help_text=_('Customer Survey Q1: Was the quality of the work satisfactory?'),
        default=True,
        blank=True
    )
    was_job_finished_on_time_and_on_budget = models.BooleanField(
        _("Was job finished on time and on budget?"),
        help_text=_('Customer Survey Q2: Was the work completed on time and on budget?'),
        default=True,
        blank=True
    )
    was_associate_punctual = models.BooleanField(
        _("Was associate punctual?"),
        help_text=_('Customer Survey Q3: Was the Associate Member punctual?'),
        default=True,
        blank=True
    )
    was_associate_professional = models.BooleanField(
        _("Was associate professional?"),
        help_text=_('Customer Survey Q4: Was the Associate Member professional?'),
        default=True,
        blank=True
    )
    would_customer_refer_our_organization = models.BooleanField(
        _("Would customer refer our organization?"),
        help_text=_('Customer Survey Q5: Would you refer Over55 to a friend of family member?'),
        default=True,
        blank=True
    )
    score = models.PositiveSmallIntegerField(
        _("Score"),
        help_text=_('The score number earned at the completion of this date.'),
        default=0,
        blank=True,
    )

    #
    #  Financial Fields
    #

    was_there_financials_inputted = models.BooleanField(
        _("Was there financials inputted?"),
        help_text=_('Track whether financials where inputted.'),
        default=True,
        blank=True
    )
    invoice_paid_to = models.PositiveSmallIntegerField(
        _("Invoice Paid to"),
        help_text=_('Whom was paid by the client for this invoice.'),
        choices=WORK_ORDER_PAID_TO_CHOICES,
        blank=True,
        null=True,
    )
    invoice_date = models.DateField(
        _('Invoice Date'),
        help_text=_('The date that this order was completed.'),
        blank=True,
        null=True
    )
    invoice_ids = models.CharField(
        _("Invoice ID(s)"),
        help_text=_('A list of invoice ID values associated with this order.'),
        max_length=127,
        blank=True,
        null=True,
        default='',
    )
    invoice_quote_amount = MoneyField(
        _("Invoice Original Quote Amount"),
        help_text=_('The original quote made by the associate for this job.'),
        max_digits=10,
        decimal_places=2,
        default_currency=WORKERY_APP_DEFAULT_MONEY_CURRENCY,
        default=Money(0,WORKERY_APP_DEFAULT_MONEY_CURRENCY),
        blank=True,
    )
    invoice_labour_amount = MoneyField(
        _("Invoice Labour Costs Amount"),
        help_text=_('The amount charged for labour by the associate for this job.'),
        max_digits=10,
        decimal_places=2,
        default_currency=WORKERY_APP_DEFAULT_MONEY_CURRENCY,
        default=Money(0,WORKERY_APP_DEFAULT_MONEY_CURRENCY),
        blank=True,
    )
    invoice_material_amount = MoneyField(
        _("Invoice Material Costs Amount"),
        help_text=_('The amount charged for material costs by the associate for this job.'),
        max_digits=10,
        decimal_places=2,
        default_currency=WORKERY_APP_DEFAULT_MONEY_CURRENCY,
        default=Money(0,WORKERY_APP_DEFAULT_MONEY_CURRENCY),
        blank=True,
    )
    invoice_other_costs_amount = MoneyField(
        _("Other Costs Amount"),
        help_text=_('The amount charged for other costs by the associate for this job.'),
        max_digits=10,
        decimal_places=2,
        default_currency=WORKERY_APP_DEFAULT_MONEY_CURRENCY,
        default=Money(0,WORKERY_APP_DEFAULT_MONEY_CURRENCY),
        blank=True,
    )
    invoice_quoted_material_amount = MoneyField(
        _("Invoice Quoted Material Costs Amount"),
        help_text=_('The quoted amount to charge for material costs by the associate for this job.'),
        max_digits=10,
        decimal_places=2,
        default_currency=WORKERY_APP_DEFAULT_MONEY_CURRENCY,
        default=Money(0,WORKERY_APP_DEFAULT_MONEY_CURRENCY),
        blank=True,
    )
    invoice_quoted_labour_amount = MoneyField(
        _("Invoice Quoted Labour Costs Amount"),
        help_text=_('The quoted amount to charge for labour by the associate for this job.'),
        max_digits=10,
        decimal_places=2,
        default_currency=WORKERY_APP_DEFAULT_MONEY_CURRENCY,
        default=Money(0,WORKERY_APP_DEFAULT_MONEY_CURRENCY),
        blank=True,
    )
    invoice_quoted_other_costs_amount = MoneyField(
        _("Other Costs Amount"),
        help_text=_('The quoted other costs amount to charge by the associate for this job.'),
        max_digits=10,
        decimal_places=2,
        default_currency=WORKERY_APP_DEFAULT_MONEY_CURRENCY,
        default=Money(0,WORKERY_APP_DEFAULT_MONEY_CURRENCY),
        blank=True,
    )
    invoice_total_quote_amount = MoneyField(
        _("Invoice Total Quoted Amount"),
        help_text=_('The quoted amount to charge for material costs by the associate for this job.'),
        max_digits=10,
        decimal_places=2,
        default_currency=WORKERY_APP_DEFAULT_MONEY_CURRENCY,
        default=Money(0,WORKERY_APP_DEFAULT_MONEY_CURRENCY),
        blank=True,
    )
    invoice_sub_total_amount = MoneyField(
        _("Invoice Sub-Total Amount"),
        help_text=_('The sub-total amount charged by the associate for this job. Essentially this is the sub-total without taxes.'),
        max_digits=10,
        decimal_places=2,
        default_currency=WORKERY_APP_DEFAULT_MONEY_CURRENCY,
        default=Money(0,WORKERY_APP_DEFAULT_MONEY_CURRENCY),
        blank=True,
    )
    invoice_tax_amount = MoneyField(
        _("Invoice Tax Amount"),
        help_text=_('The amount charged for taxes by the associate for this job.'),
        max_digits=10,
        decimal_places=2,
        default_currency=WORKERY_APP_DEFAULT_MONEY_CURRENCY,
        default=Money(0,WORKERY_APP_DEFAULT_MONEY_CURRENCY),
        blank=True,
    )
    invoice_total_amount = MoneyField(
        _("Invoice Total Amount"),
        help_text=_('The total amount charged by the associate for this job. Essentially this is the sub-total with taxes'),
        max_digits=10,
        decimal_places=2,
        default_currency=WORKERY_APP_DEFAULT_MONEY_CURRENCY,
        default=Money(0,WORKERY_APP_DEFAULT_MONEY_CURRENCY),
        blank=True,
    )
    invoice_deposit_amount = MoneyField(
        _("Invoice Deposit Amount"),
        help_text=_('The amount deposited.'),
        max_digits=10,
        decimal_places=2,
        default_currency=WORKERY_APP_DEFAULT_MONEY_CURRENCY,
        default=Money(0,WORKERY_APP_DEFAULT_MONEY_CURRENCY),
        blank=True,
    )
    invoice_amount_due = MoneyField(
        _("Invoice Amount Due"),
        help_text=_('The amount to be billed out for this invoice. This field is essentially `total` subtract `deposit`.'),
        max_digits=10,
        decimal_places=2,
        default_currency=WORKERY_APP_DEFAULT_MONEY_CURRENCY,
        default=Money(0,WORKERY_APP_DEFAULT_MONEY_CURRENCY),
        blank=True,
    )
    invoice_service_fee_amount = MoneyField(
        _("Invoice Service Fee Amount"),
        help_text=_('The invoice service fee amount that associate needs to pay.'),
        max_digits=10,
        decimal_places=2,
        default_currency=WORKERY_APP_DEFAULT_MONEY_CURRENCY,
        default=Money(0,WORKERY_APP_DEFAULT_MONEY_CURRENCY),
        blank=True,
    )
    invoice_actual_service_fee_amount_paid = MoneyField(
        _("Invoice Actual Service Fee Amount Paid"),
        help_text=_('The actual amount paid by the associate for service fee for this job.'),
        max_digits=10,
        decimal_places=2,
        default_currency=WORKERY_APP_DEFAULT_MONEY_CURRENCY,
        default=Money(0,WORKERY_APP_DEFAULT_MONEY_CURRENCY),
        blank=True,
    )
    invoice_service_fee = models.ForeignKey(
        "WorkOrderServiceFee",
        help_text=_('The service fee applied by the franchise on the total cost of this job order which will be paid by the associate member.'),
        related_name="work_orders",
        on_delete=models.SET_NULL,
        blank=True,
        null=True
    )
    invoice_service_fee_payment_date = models.DateField(
        _('Invoice Service Fee Payment Date'),
        help_text=_('The date when the service fee was paid by the associate.'),
        blank=True,
        null=True,
        db_index=True
    )
    invoice_balance_owing_amount = MoneyField(
        _("Invoice Balance Owing Amount"),
        help_text=_('The amount remaining to be paid by the associate for service fee for this job.'),
        max_digits=10,
        decimal_places=2,
        default_currency=WORKERY_APP_DEFAULT_MONEY_CURRENCY,
        default=Money(0,WORKERY_APP_DEFAULT_MONEY_CURRENCY),
        blank=True,
    )
    visits = models.PositiveSmallIntegerField(
        _("Visits"),
        help_text=_('The the number of visits that were made between the customer and associate for this particular work order.'),
        default=1,
        blank=True,
        validators=[
            MinValueValidator(1),
            MaxValueValidator(100)
        ],
    )
    cloned_from = models.ForeignKey(
        "self",
        help_text=_('The original work order this order was cloned'),
        related_name="cloned_work_orders",
        on_delete=models.SET_NULL,
        blank=True,
        null=True
    )

    #
    #  ONGOING WORK ORDER
    #

    ongoing_work_order = models.ForeignKey(
        "OngoingWorkOrder",
        help_text=_('The ongoing work order that this work order is a part of.'),
        related_name="work_orders",
        on_delete=models.SET_NULL,
        blank=True,
        null=True
    )

    #
    #  SYSTEM
    #

    created = models.DateTimeField(auto_now_add=True, db_index=True)
    created_by = models.ForeignKey(
        SharedUser,
        help_text=_('The user whom created this order.'),
        related_name="created_work_orders",
        on_delete=models.SET_NULL,
        blank=True,
        null=True
    )
    created_from = models.GenericIPAddressField(
        _("Created from"),
        help_text=_('The IP address of the creator.'),
        blank=True,
        null=True
    )
    created_from_is_public = models.BooleanField(
        _("Is the IP "),
        help_text=_('Is creator a public IP and is routable.'),
        default=False,
        blank=True
    )
    last_modified = models.DateTimeField(auto_now=True)
    last_modified_by = models.ForeignKey(
        SharedUser,
        help_text=_('The user whom last modified this order.'),
        related_name="last_modified_work_orders",
        on_delete=models.SET_NULL,
        blank=True,
        null=True
    )
    last_modified_from = models.GenericIPAddressField(
        _("Last modified from"),
        help_text=_('The IP address of the modifier.'),
        blank=True,
        null=True
    )
    last_modified_from_is_public = models.BooleanField(
        _("Is the IP "),
        help_text=_('Is modifier a public IP and is routable.'),
        default=False,
        blank=True
    )

    #
    #  FUNCTIONS
    #

    def __str__(self):
        return str(self.pk)

    def get_skill_sets_string(self):
        # Attach all the skill sets that are associated with each job.
        skill_set_count = self.skill_sets.count() - 1
        skill_set_string = ""
        for i, skill_set in enumerate(self.skill_sets.all()):

            skill_set_string += skill_set.sub_category

            if i != skill_set_count:
                skill_set_string += "|"
            else:
                pass # Skip last
        return skill_set_string

    def get_tags_string(self):
        # Attach all the tags that are associated with each job.
        tag_count = self.tags.count() - 1
        tag_string = ""
        for i, tag in enumerate(self.tags.all()):

            tag_string += str(tag.text)

            if i != tag_count:
                tag_string += "|"
            else:
                pass # Skip last
        return tag_string

    # def get_pretty_state(self):
    #     return dict(self.WORK_ORDER_STATE).get(self.state)

    def get_pretty_type_of(self):
        pretty_type_of = dict(JOB_TYPE_OF_CHOICES).get(self.type_of)

        if self.is_ongoing:
            pretty_type_of += " (Ongoing)"
        else:
            pretty_type_of += " (One-time)"
        return pretty_type_of

    def get_pretty_status(self):
        """
        Function returns the job status in a more user-friendly format.
        """
        if self.state == WORK_ORDER_STATE.PENDING:
            return 'Pending'
        elif self.state == WORK_ORDER_STATE.CANCELLED:
            if self.closing_reason == 2:
                return "Cancelled - Quote was too high"
            elif self.closing_reason == 3:
                return "Cancelled - Job completed by someone else"
            elif self.closing_reason == 5:
                return "Cancelled - Work no longer needed"
            elif self.closing_reason == 6:
                return "Cancelled - Client not satisfied with Associate"
            elif self.closing_reason == 7:
                return "Cancelled - Client did work themselves"
            elif self.closing_reason == 8:
                return "Cancelled - No Associate available"
            elif self.closing_reason == 9:
                return "Cancelled - Work environment unsuitable"
            elif self.closing_reason == 10:
                return "Cancelled - Client did not return call"
            elif self.closing_reason == 11:
                return "Cancelled - Associate did not have necessary equipment"
            elif self.closing_reason == 12:
                return "Cancelled - Repair not possible"
            elif self.closing_reason == 13:
                return "Cancelled - Could not meet deadline"
            elif self.closing_reason == 14:
                return "Cancelled - Associate did not call client"
            elif self.closing_reason == 15:
                return "Cancelled - Member issue"
            elif self.closing_reason == 16:
                return "Cancelled - Client billing issue"
            else:
                return "Cancelled - Other: "+str(self.closing_reason_other)
        elif self.state == WORK_ORDER_STATE.ONGOING:
            return 'Ongoing'
        elif self.state == WORK_ORDER_STATE.IN_PROGRESS:
            return 'In Progress'
        elif self.state == WORK_ORDER_STATE.COMPLETED_BUT_UNPAID:
            return 'Completed but unpaid'
        elif self.state == WORK_ORDER_STATE.COMPLETED_AND_PAID:
            return 'Completed and paid'
        elif self.state == WORK_ORDER_STATE.ARCHIVED:
            return 'Archived'
        elif self.state == WORK_ORDER_STATE.DECLINED:
            return 'Declined'
        elif self.state == WORK_ORDER_STATE.NEW:
            return 'New'
        else:
            return self.state

        return None

    def pretty_closing_reason(self):
        if self.closing_reason == 2:
            return "Quote was too high"
        elif self.closing_reason == 3:
            return "Job completed by someone else"
        elif self.closing_reason == 5:
            return "Work no longer needed"
        elif self.closing_reason == 6:
            return "Client not satisfied with Associate"
        elif self.closing_reason == 7:
            return "Client did work themselves"
        elif self.closing_reason == 8:
            return "No Associate available"
        elif self.closing_reason == 9:
            return "Work environment unsuitable"
        elif self.closing_reason == 10:
            return "Client did not return call"
        elif self.closing_reason == 11:
            return "Associate did not have necessary equipment"
        elif self.closing_reason == 12:
            return "Repair not possible"
        elif self.closing_reason == 13:
            return "Could not meet deadline"
        elif self.closing_reason == 14:
            return "Associate did not call client"
        elif self.closing_reason == 15:
            return "Member issue"
        elif self.closing_reason == 16:
            return "Client billing issue"
        else:
            return "Other: "+str(self.closing_reason_other)

    def get_pretty_invoice_paid_to(self):
        return dict(WORK_ORDER_PAID_TO_CHOICES).get(self.invoice_paid_to)

    """
    Override the `save` function to support save cached searchable terms.
    """
    def save(self, *args, **kwargs):
        '''
        The following code will populate our indexed_custom search text with
        the latest model data before we save.
        '''
        search_text = str(self.id)
        search_text += " " + intcomma(self.id)

        if self.description:
            search_text += " " + self.description

        if self.customer:
            if self.customer.organization:
                search_text += " " + self.customer.organization.name
            if self.customer.given_name:
                search_text += " " + self.customer.given_name
            if self.customer.middle_name:
                search_text += " " + self.customer.middle_name
            if self.customer.last_name:
                search_text += " " + self.customer.last_name
            if self.customer.email:
                search_text += " " + self.customer.email
            if self.customer.telephone:
                search_text += " " + phonenumbers.format_number(self.customer.telephone, phonenumbers.PhoneNumberFormat.NATIONAL)
                search_text += " " + phonenumbers.format_number(self.customer.telephone, phonenumbers.PhoneNumberFormat.INTERNATIONAL)
                search_text += " " + phonenumbers.format_number(self.customer.telephone, phonenumbers.PhoneNumberFormat.E164)
            if self.customer.other_telephone:
                search_text += " " + phonenumbers.format_number(self.customer.other_telephone, phonenumbers.PhoneNumberFormat.NATIONAL)
                search_text += " " + phonenumbers.format_number(self.customer.other_telephone, phonenumbers.PhoneNumberFormat.INTERNATIONAL)
                search_text += " " + phonenumbers.format_number(self.customer.other_telephone, phonenumbers.PhoneNumberFormat.E164)
            if self.customer.description:
                search_text += " " + self.customer.description
            search_text += " " + self.customer.get_postal_address()

        if self.associate:
            if self.associate.given_name:
                search_text += " " + self.associate.given_name
            if self.associate.middle_name:
                search_text += " " + self.associate.middle_name
            if self.associate.last_name:
                search_text += " " + self.associate.last_name
            if self.associate.email:
                search_text += " " + self.associate.email
            if self.associate.telephone:
                search_text += " " + phonenumbers.format_number(self.associate.telephone, phonenumbers.PhoneNumberFormat.NATIONAL)
                search_text += " " + phonenumbers.format_number(self.associate.telephone, phonenumbers.PhoneNumberFormat.INTERNATIONAL)
                search_text += " " + phonenumbers.format_number(self.associate.telephone, phonenumbers.PhoneNumberFormat.E164)
            if self.associate.other_telephone:
                search_text += " " + phonenumbers.format_number(self.associate.other_telephone, phonenumbers.PhoneNumberFormat.NATIONAL)
                search_text += " " + phonenumbers.format_number(self.associate.other_telephone, phonenumbers.PhoneNumberFormat.INTERNATIONAL)
                search_text += " " + phonenumbers.format_number(self.associate.other_telephone, phonenumbers.PhoneNumberFormat.E164)
            if self.associate.description:
                search_text += " " + self.associate.description
            search_text += " " + self.associate.get_postal_address()

        if self.invoice_ids:
            search_text += " " + str(self.invoice_ids)

        self.indexed_text = Truncator(search_text).chars(2047)

        '''
        Run our `save` function.
        '''
        super(WorkOrder, self).save(*args, **kwargs)

    def clone(self):
        from tenant_foundation.models.activity_sheet_item import ActivitySheetItem
        # from tenant_foundation.models.comment import Comment
        # from tenant_foundation.models.work_order_comment import WorkOrderComment
        from tenant_foundation.models.work_order_deposit import WorkOrderDeposit

        # This process doesn’t copy relations that aren’t part of the model’s
        # database table. For example, WorkOrder has a ManyToManyField to
        # SkillSet. After duplicating an entry, we must set the many-to-many
        # relations for the new entry:
        old_tags = self.tags.all()
        old_skill_sets = self.skill_sets.all()
        # old_comments = self.comments.all()
        old_activity_sheets = ActivitySheetItem.objects.filter(job=self)
        old_deposits = WorkOrderDeposit.objects.filter(order=self)

        # DEVELOPERS NOTE:
        # The following code will take a full clone of our original instance.
        # Special thanks to: https://docs.djangoproject.com/en/2.2/topics/db/queries/#copying-model-instances
        cloned_order = self
        cloned_order.pk = None
        cloned_order.id = None
        cloned_order.save()

        # Remember where we cloned our object from.
        cloned_order.cloned_from = WorkOrder.objects.get(id=self.id)
        cloned_order.latest_pending_task = None # Tasks will not be included, reason why is: https://github.com/over55/workery-front/issues/366
        cloned_order.save()

        # Re-assign our many-to-many.
        cloned_order.tags.set(old_tags)
        cloned_order.skill_sets.set(old_skill_sets)

        # DEVELOPER NOTE: Commented out because https://github.com/over55/workery-front/issues/390
        # # Cannot set values on a ManyToManyField which specifies an
        # # intermediary model, as a result we'll have to create them here.
        # # Start with handling comments and then activity sheets.
        # for old_comment in old_comments:
        #     with freeze_time(old_comment.created_at):
        #         copy_comment = Comment.objects.create(
        #             created_at=old_comment.created_at,
        #             created_by=old_comment.created_by,
        #             created_from = old_comment.created_from,
        #             created_from_is_public = old_comment.created_from_is_public,
        #             last_modified_at=old_comment.last_modified_at,
        #             last_modified_by=old_comment.last_modified_by,
        #             last_modified_from=old_comment.last_modified_from,
        #             last_modified_from_is_public=old_comment.last_modified_from_is_public,
        #             text=old_comment.text,
        #
        #         )
        #         WorkOrderComment.objects.create(
        #             about=cloned_order,
        #             comment=copy_comment,
        #         )

        for old_activity_sheet in old_activity_sheets:
            with freeze_time(old_activity_sheet.created_at):
                copy_activity_sheet = ActivitySheetItem.objects.create(
                    job = cloned_order,
                    associate = old_activity_sheet.associate,
                    comment = old_activity_sheet.comment,
                    state = old_activity_sheet.state,
                    created_at=old_activity_sheet.created_at,
                    created_by=old_activity_sheet.created_by,
                    created_from = old_activity_sheet.created_from,
                    created_from_is_public = old_activity_sheet.created_from_is_public,
                )

        for old_deposit in old_deposits:
            with freeze_time(old_deposit.created_at):
                copy_old_deposit = WorkOrderDeposit.objects.create(
                    order=cloned_order,
                    paid_at=old_deposit.paid_at,
                    deposit_method=old_deposit.deposit_method,
                    paid_to=old_deposit.paid_to,
                    paid_for=old_deposit.paid_for,
                    amount=old_deposit.amount,
                    created_by = old_deposit.created_by,
                    created_from = old_deposit.created_from,
                    created_from_is_public = old_deposit.created_from_is_public,
                    last_modified_by = old_deposit.last_modified_by,
                    last_modified_from = old_deposit.last_modified_from,
                    last_modified_from_is_public = old_deposit.last_modified_from_is_public,
                )
        return cloned_order