import copy
import matplotlib.pyplot as plt
import ruptures as rpt
from data_generator import *
import random
import json


def avg(target_list):
    return sum(target_list) / len(target_list)


def pelt_detection(model, target_pen, target_seq):
    """
    使用pelt对给定的序列进行检测
    :param model:
    :param target_pen:
    :param target_seq:
    :return:
    """
    algo = rpt.Pelt(model=model).fit(target_seq)
    detection_result = algo.predict(pen=target_pen)
    while len(target_seq) in detection_result:
        detection_result.remove(len(target_seq))
    return detection_result


def is_spike_percentage(x1, x2, spike_percentage):
    """
    根据变点前后段的均值，判断是否存在跳变
    :param x1:
    :param x2:
    :param spike_percentage:
    :return:
    """
    if cal_relative_diff(x1, x2) >= spike_percentage:
        return True
    else:
        return False


def cal_relative_diff(x1, x2):
    """
    计算两个数据的相对偏差
    :param x1:
    :param x2:
    :return:
    """
    return abs(abs(x1-x2) / ((x1 + x2) / 2))


def point_classification(slice_sequence, slice_sequence_avg, spike_percentage, spike_length):
    """

    :param slice_sequence:
    :param slice_sequence_avg:
    :param spike_percentage:
    :param spike_length:
    :return:
    """
    result_dict = {'spike_up_point': [], 'spike_down_point': [], 'step_up_point': [], 'step_down_point': [],
                   'slop_up_tuple': [], 'slop_down_tuple': []}
    spike_up_flag, spike_down_flag = False, False
    for i in range(len(slice_sequence_avg) - 1):
        if spike_up_flag:
            result_dict['spike_up_point'].append(i)
            spike_up_flag = False
            continue
        if spike_down_flag:
            result_dict['spike_down_point'].append(i)
            spike_down_flag = False
            continue
        now_slice_avg, next_slice_avg = slice_sequence_avg[i], slice_sequence_avg[i + 1]
        if next_slice_avg > now_slice_avg \
                and is_spike_percentage(next_slice_avg, now_slice_avg, spike_percentage):     # 向上跳变
            if i + 2 < len(slice_sequence_avg):      # 仍然存在更多变点
                further_slice_avg = slice_sequence_avg[i + 2]
                if further_slice_avg < next_slice_avg and len(slice_sequence[i + 1]) <= spike_length and \
                        is_spike_percentage(next_slice_avg, further_slice_avg, spike_percentage):
                    # 向上跳变以后向下跳变，则为激增点
                    result_dict['spike_up_point'].append(i)
                    spike_up_flag = True
                    continue
                else:
                    # 向上跳变以后没有显著向下跳变，该点为上升点
                    result_dict['step_up_point'].append(i)
                    continue
            else:
                result_dict['step_up_point'].append(i)

        elif now_slice_avg > next_slice_avg \
                and is_spike_percentage(now_slice_avg, next_slice_avg, spike_percentage):     # 向下跳变
            if i + 2 < len(slice_sequence_avg):      # 仍然存在更多变点
                further_slice_avg = slice_sequence_avg[i + 2]
                if further_slice_avg > next_slice_avg and len(slice_sequence[i + 1]) <= spike_length and \
                        is_spike_percentage(further_slice_avg, next_slice_avg, spike_percentage):
                    # 向下跳变以后向上跳变，则为骤降点
                    result_dict['spike_down_point'].append(i)
                    spike_down_flag = True
                    continue
                else:
                    # 向下跳变以后没有显著向上跳变，该点为下降点
                    result_dict['step_down_point'].append(i)
                    continue
            else:
                result_dict['step_down_point'].append(i)
                continue
        elif next_slice_avg > now_slice_avg:
            result_dict['step_up_point'].append(i)
        elif next_slice_avg < now_slice_avg:
            result_dict['step_down_point'].append(i)
    return result_dict


def is_slop_detection(sequence, slop_tuple, strip_num, min_slop_val, min_interval):
    """
    输入一个序列以及一个缓坡的开始结束点，判断是否确实是缓坡
    :param sequence:
    :param slop_tuple:
    :param strip_num:
    :param min_slop_val:
    :param min_interval:
    :return:
    """
    if (slop_tuple[1] - slop_tuple[0]) > 2 * min_interval:
        return True
    else:
        return False


def strip_step_interval(target_list, min_interval, is_og_content=True):
    """
    去除间隔过小的序列
    :param is_og_content:
    :param target_list:
    :param min_interval:
    :return:
    """
    result_list = []
    target_list.sort()
    continue_flag = False
    for i in range(len(target_list) - 1):
        if continue_flag:
            continue_flag = False
            continue
        now_val, next_val = target_list[i], target_list[i + 1]
        if abs(next_val - now_val) < min_interval:
            if is_og_content:
                result_list.append(now_val)
            else:
                result_list.append(int(avg([now_val, next_val])))
            continue_flag = True
            continue
        else:
            result_list.append(now_val)
    if not continue_flag and target_list:
        result_list.append(target_list[-1])
    return result_list


def enlarge_step_interval(target_list, interval):
    """
    将序列中过小的间隔进行放大处理
    :param interval:
    :param target_list:
    :return:
    """
    result_list = []
    target_list.sort()
    continue_flag = False
    for i in range(len(target_list) - 1):
        if continue_flag:
            continue_flag = False
            continue
        now_val, next_val = target_list[i], target_list[i + 1]
        if abs(next_val - now_val) < interval:
            result_list.append(now_val - interval)
            result_list.append(next_val + interval)
            continue_flag = True
            continue
        else:
            result_list.append(now_val)
    if not continue_flag and target_list:
        result_list.append(target_list[-1])
    return result_list


def find_spike_point(sequence, target_list, min_interval):
    """
    寻找一段激增/骤降区间中的极值
    :param sequence:
    :param target_list:
    :param min_interval:
    :return:
    """
    result_list = []
    target_list.sort()
    continue_flag = False
    for i in range(len(target_list) - 1):
        if continue_flag:
            continue_flag = False
            continue
        now_val, next_val = target_list[i], target_list[i + 1]
        if abs(next_val - now_val) < min_interval:
            # 寻找序列中与均值差异最大的点，为极值点
            seq_slice = [sequence[i] for i in range(now_val, next_val + 1)]
            temp_list = [abs(i - avg(seq_slice)) for i in seq_slice]
            temp_item = temp_list.index(max(temp_list))
            result_list.append(now_val + temp_item)
            continue_flag = True
            continue
        else:
            result_list.append(now_val)
    if not continue_flag and target_list:
        result_list.append(target_list[-1])
    return result_list


def find_sequential_sub_list(target):
    if not target:
        return []
    result_list = []
    temp_list = [target[0]]
    for i in range(1, len(target)):
        if target[i] == temp_list[-1] + 1:
            temp_list.append(target[i])
        else:
            result_list.append(copy.deepcopy(temp_list))
            temp_list = [target[i]]
    result_list.append(copy.deepcopy(temp_list))
    return result_list


def new_slop_detection(sequence, pelt_result, point_list, strip_num, slop_threshold, min_slop_val, min_interval):
    """
    根据变点段落得斜率计算出理论上下一段的起始值，判断是否存在跳变。不存在则为斜坡，否则为跳变
    :param sequence:
    :param pelt_result:
    :param point_list:
    :param strip_num:
    :param slop_threshold:
    :param min_slop_val:
    :param min_interval:
    :return:
    """
    sequential_list = find_sequential_sub_list(point_list)
    slop_list, step_list = [], []

    for seq_list in sequential_list:
        slop_point_list, step_point_list = [], []
        seq_loc_list = [pelt_result[i] for i in seq_list]
        if seq_list[0] == 0:
            seq_loc_list = [0] + seq_loc_list
        else:
            seq_loc_list = [pelt_result[pelt_result.index(seq_loc_list[0]) - 1]] + seq_loc_list
        seq_loc_list = strip_step_interval(seq_loc_list, 2 * strip_num + 1, False)

        # # 只有一个变点时，通过变点前后的均值判断是否存在跳变
        # if len(seq_loc_list) == 1:
        #     next_slice_end_loc = min(seq_loc_list[0] + min_interval * 5, len(sequence) - 1)
        #     now_slice_start_loc = max(seq_loc_list[0] - min_interval * 5, 0)
        #     now_slice_avg = avg(sequence[now_slice_start_loc:seq_loc_list[0]])
        #     next_slice_avg = avg(sequence[seq_loc_list[0]:next_slice_end_loc])
        #     relative_diff = cal_relative_diff(now_slice_avg, next_slice_avg) * 2
        #     if relative_diff >= slop_threshold:
        #         step_point_list.append(seq_loc_list[0])

        # 只有一个变点时，强行取变点前3倍长度的min_interval作为区间进行判断
        if len(seq_loc_list) == 1:
            seq_loc_list = [max(seq_loc_list[0] - min_interval * 3, 0)] + seq_loc_list

        for loc in range(1, len(seq_loc_list)):
            now_slice_start_loc = seq_loc_list[loc - 1] + strip_num
            now_slice_end_loc = seq_loc_list[loc] - strip_num
            next_slice_start_loc = seq_loc_list[loc] + strip_num
            slice_length = seq_loc_list[loc] - seq_loc_list[loc - 1] - 2 * strip_num
            now_slice_slop = (sequence[now_slice_end_loc] - sequence[now_slice_start_loc]) / slice_length
            theoretical_next_slop_start_val = sequence[now_slice_start_loc] + \
                                              (slice_length + 2 * strip_num) * now_slice_slop
            relative_diff = cal_relative_diff(theoretical_next_slop_start_val, sequence[next_slice_start_loc])
            print(relative_diff, theoretical_next_slop_start_val, sequence[next_slice_start_loc])
            if relative_diff >= slop_threshold:
                step_point_list.append(seq_loc_list[loc])
            else:
                slop_point_list.append(loc)

        slop_sequence_list = find_sequential_sub_list(slop_point_list)
        if not slop_sequence_list:
            for loc in slop_point_list:
                step_list.append(seq_loc_list[loc])
        else:
            for slop_sequence in slop_sequence_list:
                slop_tuple = copy.deepcopy([seq_loc_list[slop_sequence[0]], seq_loc_list[slop_sequence[-1]]])
                if is_slop_detection(sequence, slop_tuple, strip_num, min_slop_val, min_interval):
                    slop_list.append(slop_tuple)
        step_list += copy.deepcopy(step_point_list)
    return {'slop_list': slop_list, 'step_list': step_list}


def seq_classification(sequence, pelt_result, spike_percentage=0.27, spike_length=0.05, strip_num=5, slop_threshold=0.28, min_interval=8, min_slop_val=0.05):
    slice_sequence = []
    start_point = 0
    spike_length = int(spike_length * len(sequence))
    for change_point in pelt_result:
        slice_sequence.append(list(sequence[start_point: change_point:1]))
        start_point = change_point
    slice_sequence.append(list(sequence[start_point:-1:1]) + [sequence[-1]])
    slice_sequence_avg = [avg(item) for item in slice_sequence]
    result_dict = point_classification(slice_sequence, slice_sequence_avg, spike_percentage, spike_length)

    # step_down_result = new_slop_detection(sequence, pelt_result, result_dict['step_down_point'], strip_num,
    #                                       slop_threshold, min_slop_val, min_interval)
    step_up_result = new_slop_detection(sequence, pelt_result, result_dict['step_up_point'], strip_num,
                                        slop_threshold, min_slop_val, min_interval)
    step_down_result = new_slop_detection(sequence, pelt_result, result_dict['step_down_point'], strip_num,
                                          slop_threshold, min_slop_val, min_interval)

    result_dict['step_up_point'] = strip_step_interval(step_up_result['step_list'], min_interval)
    result_dict['step_down_point'] = strip_step_interval(step_down_result['step_list'], min_interval)

    result_dict['slop_up_tuple'] += step_up_result['slop_list']
    result_dict['slop_down_tuple'] += step_down_result['slop_list']

    result_dict['spike_up_point'] = find_spike_point(sequence, [pelt_result[i] for i in result_dict['spike_up_point']],
                                                     min_interval)
    result_dict['spike_down_point'] = find_spike_point(sequence,
                                                       [pelt_result[i] for i in result_dict['spike_down_point']],
                                                       min_interval)

    return result_dict


def load_data_from_file(file_path):
    f = open(file_path, 'r')
    content = json.loads(f.readlines()[0])
    f.close()
    return np.array(content)


def detection_visualization(sequence, visualization_result, show_change_point=False):
    # 可视化展示结果
    plt.title('Change Point Detection')
    plt.plot(sequence)

    for point in visualization_result['step_down_point']:
        plt.annotate('Step Down', xy=(point, seq[point] - 2), xytext=(point, seq[point] - 15),
                     arrowprops=dict(facecolor='green', shrink=0.005, headlength=10, headwidth=10))

    for point in visualization_result['step_up_point']:
        plt.annotate('Step Up', xy=(point, seq[point] - 2), xytext=(point, seq[point] - 15),
                     arrowprops=dict(facecolor='red', shrink=0.005, headlength=10, headwidth=10))

    for point_tuple in visualization_result['slop_up_tuple']:
        plt.axvline(x=point_tuple[0], color='red', linestyle='--')
        plt.axvline(x=point_tuple[1], color='red', linestyle='--')
        midpoint = int(avg(point_tuple)) - 20
        plt.annotate('Slop Up', xy=(point_tuple[0], 0), xytext=(midpoint, 0),
                     arrowprops=dict(facecolor='red', shrink=0.005, headlength=10, headwidth=10))
        plt.annotate('Slop Up', xy=(point_tuple[1], 0), xytext=(midpoint, 0),
                     arrowprops=dict(facecolor='red', shrink=0.005, headlength=10, headwidth=10))

    for point_tuple in visualization_result['slop_down_tuple']:
        plt.axvline(x=point_tuple[0], color='green', linestyle='--')
        plt.axvline(x=point_tuple[1], color='green', linestyle='--')
        midpoint = int(avg(point_tuple)) - 20
        plt.annotate('Slop Down', xy=(point_tuple[0], 0), xytext=(midpoint, 0),
                     arrowprops=dict(facecolor='green', shrink=0.005, headlength=10, headwidth=10))
        plt.annotate('Slop Down', xy=(point_tuple[1], 0), xytext=(midpoint, 0),
                     arrowprops=dict(facecolor='green', shrink=0.005, headlength=10, headwidth=10))

    for point in visualization_result['spike_up_point']:
        plt.annotate('Spike Up', xy=(point, seq[point]), xytext=(point + 25, seq[point]),
                     arrowprops=dict(facecolor='red', shrink=0.005, headlength=10, headwidth=10))

    for point in visualization_result['spike_down_point']:
        plt.annotate('Spike Down', xy=(point, seq[point]), xytext=(point + 25, seq[point]),
                     arrowprops=dict(facecolor='green', shrink=0.005, headlength=10, headwidth=10))

    if show_change_point:
        for point in pelt_change_point_result:
            plt.axvline(x=point, color='black', linestyle=':')
            plt.annotate(pelt_change_point_result.index(point), xy=(point, -10), xytext=(point-5, -10))

    plt.show()


if __name__ == "__main__":
    size = 500
    pen = 20

    seq = gen_full_sequence(size, True)
    # seq = load_data_from_file('test_data_debug.txt')

    pelt_change_point_result = pelt_detection(model='l2', target_pen=pen, target_seq=seq)
    classification_result = seq_classification(seq, pelt_change_point_result)

    # detection_visualization(seq, classification_result, True)
    detection_visualization(seq, classification_result, False)

    file = open('test_data_debug.txt', 'w')
    file.write(json.dumps(list(seq)))
    file.close()

    print(pelt_change_point_result)
    print([i for i in range(len(pelt_change_point_result))])
    print('done')





