from functools import wraps
from collections import defaultdict
from tqdm import tqdm

from sklearn.linear_model import LinearRegression
from sklearn.svm import SVR

import os
import time
import math
import random
import copy
import torch
import torch.nn
import argparse
import joblib
import pickle
import warnings
import heapq
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib
import traceback
import openpyxl
import itertools

matplotlib.use('TkAgg')

# 机器参数
max_head_index, max_slot_index = 6, 120
interval_ratio = 2
slot_interval = 15
head_interval = slot_interval * interval_ratio
head_nozzle = ['' for _ in range(max_head_index)]  # 头上已经分配吸嘴

# 位置信息
slotf1_pos, slotr1_pos = [-31.267, 44.], [807., 810.545]  # F1(前基座最左侧)、R1(后基座最右侧)位置
fix_camera_pos = [269.531, 694.823]  # 固定相机位置
anc_marker_pos = [336.457, 626.230]  # ANC基准点位置
stopper_pos = [665.150, 124.738]  # 止档块位置

# 算法权重参数
e_nz_change, e_gang_pick = 4, 0.6

# 电机参数
head_rotary_velocity = 8e-5  # 贴装头R轴旋转时间
x_max_velocity, y_max_velocity = 1.4, 1.2
x_max_acceleration, y_max_acceleration = x_max_velocity / 0.079, y_max_velocity / 0.079

# TODO: 不同种类供料器宽度
# feeder_width = {'SM8': (7.25, 7.25), 'SM12': (7.25, 7.25), 'SM16': (7.25, 7.25),
                # 'SM24': (7.25, 7.25), 'SM32': (7.25, 7.25)}
feeder_width = {'SM8': (7.25, 7.25), 'SM12': (7.00, 20.00), 'SM16': (7.00, 22.00),
                'SM24': (7.00, 29.00), 'SM32': (7.00, 44.00)}

# 可用吸嘴数量限制
nozzle_limit = {'CN065': 6, 'CN040': 6, 'CN020': 6, 'CN400': 6, 'CN140': 6}

# 时间参数
t_cycle = 0.3
t_anc = 0.6
t_pick, t_place = .078, .051  # 贴装/拾取用时
t_nozzle_put, t_nozzle_pick = 0.9, 0.75  # 装卸吸嘴用时
t_nozzle_change = t_nozzle_put + t_nozzle_pick
t_fix_camera_check = 0.12  # 固定相机检测时间

# 时间参数（整线相关）
T_pp, T_tr, T_nc, T_pl = 2, 5, 25, 0

# 时间参数 （数据拟合获得）
# Fit_cy, Fit_nz, Fit_pu, Fit_pl, Fit_mv = 0.326, 0.870, 0.159, 0.041, 0.030
Fit_cy, Fit_nz, Fit_pu, Fit_pl, Fit_mv = 0.326, 0.870, 0.159, 0.041, 0.035


class Point:
    def __init__(self, _x, _y, _r=0, _h=None):
        self.x = _x
        self.y = _y
        self.r = _r
        self.h = _h


class OptResult:
    def __init__(self, cp_assign=None, cycle_assign=None, slot_assign=None, place_assign=None, sequence_assign=None):
        self.component_assign = [] if cp_assign is None else cp_assign
        self.cycle_assign = [] if cycle_assign is None else cycle_assign
        self.feeder_slot_assign = [] if slot_assign is None else slot_assign
        self.placement_assign = [] if place_assign is None else place_assign
        self.head_sequence = [] if sequence_assign is None else sequence_assign


class OptInfo:
    def __init__(self):
        self.total_time = .0        # 总组装时间
        self.total_points = 0       # 总贴装点数
        self.total_components = 0   # 总元件数

        self.pickup_time = .0       # 拾取过程运动时间
        self.round_time = .0        # 往返基座/基板运动时间
        self.place_time = .0        # 贴装过程运动时间
        self.operation_time = .0    # 拾取/贴装/换吸嘴等机械动作用时

        self.cycle_counter = 0           # 周期数
        self.nozzle_change_counter = 0   # 吸嘴更换次数
        self.anc_round_counter = 0       # 前往ANC次数
        self.pickup_counter = 0          # 拾取次数

        self.total_distance = .0         # 总移动路径
        self.place_distance = .0         # 贴装移动路径
        self.pickup_distance = .0        # 拾取移动路径

    def print(self):
        print('-Cycle counter: {}'.format(self.cycle_counter))

        print(f'-Nozzle change counter: {self.nozzle_change_counter: d}')
        print(f'-ANC round: {self.anc_round_counter: d}')
        print(f'-Pick operation counter: {self.pickup_counter: d}')
        print(f'-Pick time: {self.pickup_time: .3f}, Pick distance: {self.pickup_distance: .3f}')
        print(f'-Place time: {self.place_time: .3f}, Place distance: {self.place_distance: .3f}')
        print(
            f'-Round time: {self.total_time - self.operation_time - self.pickup_time - self.place_time: .3f}, Round distance: '
            f'{self.total_distance - self.pickup_distance - self.place_distance: .3f}')

        print(f'-Round & place time per cycle: {(self.total_time - self.pickup_time - self.operation_time) * 1000.0 / (self.cycle_counter + 1e-10): .3f}, ', end='')
        print(f'-Round & place distance per cycle: {(self.total_distance - self.pickup_distance) / (self.cycle_counter + 1e-10): .3f}')

        minutes, seconds = int(self.total_time // 60), int(self.total_time) % 60
        millisecond = int((self.total_time - minutes * 60 - seconds) * 60)

        print(f'-Operation time: {self.operation_time: .3f}, ', end='')
        if minutes > 0:
            print(f'Total time:  {minutes: d} min {seconds} s {millisecond: 2d} ms ({self.total_time: .3f}s)')
        else:
            print(f'Total time:  {seconds} s {millisecond :2d} ms ({self.total_time :.3f}s)')

    def metric(self):
        return Fit_cy * self.cycle_counter + Fit_nz * self.nozzle_change_counter + Fit_pu * self.pickup_counter + \
               Fit_pl * self.total_points + Fit_mv * self.pickup_distance


def axis_moving_time(distance, axis=0):
    distance = abs(distance) * 1e-3
    Lamax = x_max_velocity ** 2 / x_max_acceleration if axis == 0 else y_max_velocity ** 2 / y_max_acceleration
    Tmax = x_max_velocity / x_max_acceleration if axis == 0 else y_max_velocity / y_max_acceleration
    if axis == 0:
        return 2 * math.sqrt(distance / x_max_acceleration) if distance < Lamax else 2 * Tmax + (
                    distance - Lamax) / x_max_velocity
    else:
        return 2 * math.sqrt(distance / y_max_acceleration) if distance < Lamax else 2 * Tmax + (
                    distance - Lamax) / y_max_velocity


def head_rotary_time(angle):
    while -180 > angle > 180:
        if angle > 180:
            angle -= 360
        else:
            angle += 360
    return abs(angle) * head_rotary_velocity


def find_commonpart(head_group, feeder_group):
    feeder_group_len = len(feeder_group)

    max_length, max_common_part = -1, []
    for offset in range(-max_head_index + 1, feeder_group_len - 1):
        # offset: head_group相对于feeder_group的偏移量
        length, common_part = 0, []
        for hd_index in range(max_head_index):
            fd_index = hd_index + offset
            if fd_index < 0 or fd_index >= feeder_group_len:
                common_part.append(-1)
                continue

            if head_group[hd_index] == feeder_group[fd_index] and head_group[hd_index] != -1:
                length += 1
                common_part.append(head_group[hd_index])
            else:
                common_part.append(-1)
        if length > max_length:
            max_length = length
            max_common_part = common_part

    return max_common_part


def timer_wrapper(func):
    @wraps(func)
    def measure_time(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        hinter = True
        for key, val in kwargs.items():
            if key == 'hinter':
                hinter = val
        if hinter:
            print(f"function {func.__name__} running time :  {time.time() - start_time:.3f} s")
        return result

    return measure_time


def feeder_assignment(component_data, pcb_data, component_result, cycle_result):
    # Section: 供料器分配结果
    feeder_slot_result, feeder_group_result = [], []
    feeder_limit = defaultdict(int)
    for component in range(len(component_data)):
        feeder_limit[component] = component_data.loc[component].fdn

    for component_cycle in component_result:
        new_feeder_group = []
        for component in component_cycle:
            if component == -1 or feeder_limit[component] == 0 or new_feeder_group.count(component) >= feeder_limit[
                component]:
                new_feeder_group.append(-1)
            else:
                new_feeder_group.append(component)

        if len(new_feeder_group) == 0:
            continue

        while sum(i >= 0 for i in new_feeder_group) != 0:
            max_common_part, index = [], -1
            max_common_length = -1
            for feeder_index in range(len(feeder_group_result)):
                common_part = find_commonpart(new_feeder_group, feeder_group_result[feeder_index])
                if sum(i > 0 for i in common_part) > max_common_length:
                    max_common_length = sum(i > 0 for i in common_part)
                    max_common_part, index = common_part, feeder_index

            new_feeder_length = 0
            for feeder in new_feeder_group:
                if feeder != -1 and feeder_limit[feeder] > 0:
                    new_feeder_length += 1

            if new_feeder_length > max_common_length:
                # 新分配供料器
                feeder_group_result.append([])
                for feeder_index in range(len(new_feeder_group)):
                    feeder = new_feeder_group[feeder_index]
                    if feeder != -1 and feeder_limit[feeder] > 0:
                        feeder_group_result[-1].append(feeder)
                        new_feeder_group[feeder_index] = -1
                        feeder_limit[feeder] -= 1
                    else:
                        feeder_group_result[-1].append(-1)
            else:
                # 使用旧供料器
                for feeder_index, feeder_part in enumerate(max_common_part):
                    if feeder_part != -1:
                        new_feeder_group[feeder_index] = -1

    # 去除多余的元素
    for feeder_group in feeder_group_result:
        while len(feeder_group) > 0 and feeder_group[0] == -1:
            feeder_group.pop(0)

        while len(feeder_group) > 0 and feeder_group[-1] == -1:
            feeder_group.pop(-1)

    # 确定供料器组的安装位置
    point_num = len(pcb_data)
    component_pos = [[] for _ in range(len(component_data))]
    for point_cnt in range(point_num):
        part = pcb_data.loc[point_cnt, 'part']
        index = np.where(component_data['part'].values == part)[0]
        component_pos[index[0]].append(pcb_data.loc[point_cnt, 'x'] + stopper_pos[0])

    # 元件使用的头
    CT_Head = defaultdict(list)
    for component_cycle in component_result:
        for head, component in enumerate(component_cycle):
            if component == -1:
                continue
            if component not in CT_Head:
                CT_Head[component] = [head, head]
            CT_Head[component][0] = min(CT_Head[component][0], head)
            CT_Head[component][1] = max(CT_Head[component][1], head)

    # 供料器组分配的优先顺序
    feeder_assign_sequence = []
    for i in range(len(feeder_group_result)):
        for j in range(len(feeder_group_result)):
            if j in feeder_assign_sequence:
                continue

            if len(feeder_assign_sequence) == i:
                feeder_assign_sequence.append(j)
            else:
                seq = feeder_assign_sequence[-1]
                if cycle_result[seq] * len([k for k in feeder_group_result[seq] if k >= 0]) < cycle_result[j] * len(
                        [k for k in feeder_group_result[seq] if k >= 0]):
                    feeder_assign_sequence.pop(-1)
                    feeder_assign_sequence.append(j)

    # TODO: 暂未考虑机械限位
    feeder_group_slot = [-1] * len(feeder_group_result)
    feeder_lane_state = [0] * max_slot_index  # 0表示空，1表示已占有
    for index in feeder_assign_sequence:
        feeder_group = feeder_group_result[index]
        best_slot = []
        for cp_index, component in enumerate(feeder_group):
            if component == -1:
                continue
            best_slot.append(round((sum(component_pos[component]) / len(component_pos[component]) - slotf1_pos[
                0]) / slot_interval) + 1 - cp_index * interval_ratio)
        best_slot = round(sum(best_slot) / len(best_slot))

        search_dir, step = 0, 0  # dir: 1-向右, 0-向左
        left_out_range, right_out_range = False, False
        while True:
            assign_slot = best_slot + step if search_dir else best_slot - step
            # 出现越界，反向搜索
            if assign_slot + (len(feeder_group) - 1) * interval_ratio >= max_slot_index / 2:
                right_out_range = True
                search_dir = 0
                step += 1
            elif assign_slot < 0:
                left_out_range = True
                search_dir = 1
                step += 1
            else:
                if left_out_range or right_out_range:
                    step += 1       # 单向搜索
                else:
                    search_dir = 1 - search_dir     # 双向搜索
                    if search_dir == 0:
                        step += 1

            assign_available = True

            # === 分配对应槽位 ===
            for slot in range(assign_slot, assign_slot + interval_ratio * len(feeder_group), interval_ratio):
                feeder_index = int((slot - assign_slot) / interval_ratio)
                pick_part = feeder_group[feeder_index]
                if feeder_lane_state[slot] == 1 and pick_part != -1:
                    assign_available = False
                    break

                if pick_part != -1 and (slot - CT_Head[pick_part][0] * interval_ratio <= 0 or
                                              slot + (max_head_index - CT_Head[pick_part][1] - 1) * interval_ratio > max_slot_index // 2):
                    assign_available = False
                    break

            if assign_available:
                for idx, part in enumerate(feeder_group):
                    if part != -1:
                        feeder_lane_state[assign_slot + idx * interval_ratio] = 1
                feeder_group_slot[index] = assign_slot
                break

        if feeder_group_slot[index] == -1:
            raise Exception('feeder assign error!')

    # 按照最大匹配原则，确定各元件周期拾取槽位
    for component_cycle in component_result:
        feeder_slot_result.append([-1] * max_head_index)
        head_index = [head for head, component in enumerate(component_cycle) if component >= 0]
        while head_index:
            max_overlap_counter = 0
            overlap_feeder_group_index, overlap_feeder_group_offset = -1, -1
            for feeder_group_idx, feeder_group in enumerate(feeder_group_result):
                # offset 头1 相对于 供料器组第一个元件的偏移量
                for offset in range(-max_head_index + 1, max_head_index + len(feeder_group)):
                    overlap_counter = 0
                    for head in head_index:
                        if 0 <= head + offset < len(feeder_group) and component_cycle[head] == \
                                feeder_group[head + offset]:
                            overlap_counter += 1

                    if overlap_counter > max_overlap_counter:
                        max_overlap_counter = overlap_counter
                        overlap_feeder_group_index, overlap_feeder_group_offset = feeder_group_idx, offset

            feeder_group = feeder_group_result[overlap_feeder_group_index]
            head_index_cpy = copy.deepcopy(head_index)

            for idx, head in enumerate(head_index_cpy):
                if 0 <= head + overlap_feeder_group_offset < len(feeder_group) and component_cycle[head] == \
                        feeder_group[head + overlap_feeder_group_offset]:
                    feeder_slot_result[-1][head] = feeder_group_slot[overlap_feeder_group_index] + interval_ratio * (
                                head + overlap_feeder_group_offset)
                    head_index.remove(head)

    return feeder_slot_result


def optimal_nozzle_assignment(component_data, pcb_data):
    # === Nozzle Assignment ===
    # number of points for nozzle & number of heads for nozzle
    nozzle_points, nozzle_assigned_counter = defaultdict(int), defaultdict(int)
    if len(pcb_data) == 0:
        return nozzle_assigned_counter
    for _, step in pcb_data.iterrows():
        part = step['part']
        idx = component_data[component_data['part'] == part].index.tolist()[0]
        nozzle = component_data.loc[idx]['nz']

        nozzle_assigned_counter[nozzle] = 0
        nozzle_points[nozzle] += 1

    assert len(nozzle_points.keys()) <= max_head_index
    total_points, available_head = len(pcb_data), max_head_index
    # S1: set of nozzle types which are sufficient to assign one nozzle to the heads
    # S2: temporary nozzle set
    # S3: set of nozzle types which already have the maximum reasonable nozzle amounts.
    S1, S2, S3 = [], [], []

    for nozzle in nozzle_points.keys():     # Phase 1
        if nozzle_points[nozzle] * max_head_index < total_points:
            nozzle_assigned_counter[nozzle] = 1
            available_head -= 1
            total_points -= nozzle_points[nozzle]

            S1.append(nozzle)
        else:
            S2.append(nozzle)

    available_head_ = available_head        # Phase 2
    for nozzle in S2:
        nozzle_assigned_counter[nozzle] = math.floor(available_head * nozzle_points[nozzle] / total_points)
        available_head_ = available_head_ - nozzle_assigned_counter[nozzle]

    S2.sort(key=lambda x: nozzle_points[x] / (nozzle_assigned_counter[x] + 1e-10), reverse=True)
    while available_head_ > 0:
        nozzle = S2[0]
        nozzle_assigned_counter[nozzle] += 1

        S2.remove(nozzle)
        S3.append(nozzle)
        available_head_ -= 1

    phase_iteration = len(S2) - 1
    while phase_iteration > 0:                     # Phase 3
        nozzle_i_val, nozzle_j_val = 0, 0
        nozzle_i, nozzle_j = None, None
        for nozzle in S2:
            if nozzle_i is None or nozzle_points[nozzle] / nozzle_assigned_counter[nozzle] > nozzle_i_val:
                nozzle_i_val = nozzle_points[nozzle] / nozzle_assigned_counter[nozzle]
                nozzle_i = nozzle

            if nozzle_assigned_counter[nozzle] > 1:
                if nozzle_j is None or nozzle_points[nozzle] / (nozzle_assigned_counter[nozzle] - 1) < nozzle_j_val:
                    nozzle_j_val = nozzle_points[nozzle] / (nozzle_assigned_counter[nozzle] - 1)
                    nozzle_j = nozzle

        if nozzle_i and nozzle_j and nozzle_points[nozzle_j] / (nozzle_assigned_counter[nozzle_j] - 1) < \
                nozzle_points[nozzle_i] / nozzle_assigned_counter[nozzle_i]:
            nozzle_assigned_counter[nozzle_j] -= 1
            nozzle_assigned_counter[nozzle_i] += 1
            S2.remove(nozzle_i)
            S3.append(nozzle_i)
        else:
            break

    return nozzle_assigned_counter


# === 遗传算法公用函数 ===
def sigma_scaling(pop_val, c: float):
    # function: f' = max(f - (avg(f) - c · sigma(f), 0)
    avg_val = sum(pop_val) / len(pop_val)
    sigma_val = math.sqrt(sum(abs(v - avg_val) for v in pop_val) / len(pop_val))

    for idx, val in enumerate(pop_val):
        pop_val[idx] = max(val - (avg_val - c * sigma_val), 0)
    return pop_val


def directed_edge_recombination_crossover(c, individual1, individual2):
    assert len(individual1) == len(individual2)
    left_edge_list, right_edge_list = defaultdict(list), defaultdict(list)

    for index in range(len(individual1) - 1):
        elem1, elem2 = individual1[index], individual1[index + 1]
        right_edge_list[elem1].append(elem2)
        left_edge_list[elem2].append(elem1)

    for index in range(len(individual2) - 1):
        elem1, elem2 = individual2[index], individual2[index + 1]
        right_edge_list[elem1].append(elem2)
        left_edge_list[elem2].append(elem1)

    offspring = []
    while len(offspring) != len(individual1):
        while True:
            center_element = np.random.choice(individual1)
            if center_element not in offspring:        # 避免重复选取
                break
        direction, candidate = 1, [center_element]
        parent = center_element
        for edge_list in left_edge_list.values():
            while parent in edge_list:
                edge_list.remove(parent)

        for edge_list in right_edge_list.values():
            while parent in edge_list:
                edge_list.remove(parent)

        while True:
            max_len, max_len_neighbor = -1, 0
            if direction == 1:
                if len(right_edge_list[parent]) == 0:
                    direction, parent = -1, center_element
                    continue
                for neighbor in right_edge_list[parent]:
                    if max_len < len(right_edge_list[neighbor]):
                        max_len_neighbor = neighbor
                        max_len = len(right_edge_list[neighbor])
                candidate.append(max_len_neighbor)
                parent = max_len_neighbor
            elif direction == -1:
                if len(left_edge_list[parent]) == 0:
                    direction, parent = 0, center_element
                    continue
                for neighbor in left_edge_list[parent]:
                    if max_len < len(left_edge_list[neighbor]):
                        max_len_neighbor = neighbor
                        max_len = len(left_edge_list[neighbor])
                candidate.insert(0, max_len_neighbor)
                parent = max_len_neighbor
            else:
                break

            # 移除重复元素
            for edge_list in left_edge_list.values():
                while max_len_neighbor in edge_list:
                    edge_list.remove(max_len_neighbor)

            for edge_list in right_edge_list.values():
                while max_len_neighbor in edge_list:
                    edge_list.remove(max_len_neighbor)

        offspring += candidate

    return offspring


def partially_mapped_crossover(parent1, parent2):
    range_ = np.random.randint(0, len(parent1), 2)      # 前闭后开
    range_ = sorted(range_)

    parent1_cpy, parent2_cpy = [-1 for _ in range(len(parent1))], [-1 for _ in range(len(parent2))]

    parent1_cpy[range_[0]: range_[1] + 1] = copy.deepcopy(parent2[range_[0]: range_[1] + 1])
    parent2_cpy[range_[0]: range_[1] + 1] = copy.deepcopy(parent1[range_[0]: range_[1] + 1])

    for index in range(len(parent1)):
        if range_[0] <= index <= range_[1]:
            continue

        cur_ptr, cur_elem = 0, parent1[index]
        while True:
            parent1_cpy[index] = cur_elem
            if parent1_cpy.count(cur_elem) == 1:
                break
            parent1_cpy[index] = -1

            if cur_ptr == 0:
                cur_ptr, cur_elem = 1, parent2[index]
            else:
                index_ = parent1_cpy.index(cur_elem)
                cur_elem = parent2[index_]

    for index in range(len(parent2)):
        if range_[0] <= index <= range_[1]:
            continue

        cur_ptr, cur_elem = 0, parent2[index]
        while True:
            parent2_cpy[index] = cur_elem
            if parent2_cpy.count(cur_elem) == 1:
                break
            parent2_cpy[index] = -1

            if cur_ptr == 0:
                cur_ptr, cur_elem = 1, parent1[index]
            else:
                index_ = parent2_cpy.index(cur_elem)
                cur_elem = parent1[index_]

    return parent1_cpy, parent2_cpy


def cycle_crossover(parent1, parent2):
    offspring1, offspring2 = [-1 for _ in range(len(parent1))], [-1 for _ in range(len(parent2))]

    idx = 0
    while True:
        if offspring1[idx] != -1:
            break
        offspring1[idx] = parent1[idx]
        idx = parent1.index(parent2[idx])

    for idx, gene in enumerate(offspring1):
        if gene == -1:
            offspring1[idx] = parent2[idx]

    idx = 0
    while True:
        if offspring2[idx] != -1:
            break
        offspring2[idx] = parent2[idx]
        idx = parent2.index(parent1[idx])

    for idx, gene in enumerate(offspring2):
        if gene == -1:
            offspring2[idx] = parent1[idx]

    return offspring1, offspring2


def swap_mutation(parent):
    range_ = np.random.randint(0, len(parent), 2)
    parent[range_[0]], parent[range_[1]] = parent[range_[1]], parent[range_[0]]
    return parent


def constraint_swap_mutation(component_points, individual, machine_number):
    offspring = individual.copy()

    idx, component_index = 0, random.randint(0, len(component_points) - 1)
    for points in component_points.values():
        if component_index == 0:
            while True:
                index1, index2 = random.sample(range(points + machine_number - 1), 2)
                if offspring[idx + index1] != offspring[idx + index2]:
                    break

            clip = offspring[idx: idx + points + machine_number - 1].copy()
            avl_machine = 0
            for idx_, gene in enumerate(clip):
                if gene == 0 and (idx_ == 0 or clip[idx_ - 1] != 0):
                    avl_machine += 1

            clip[index1], clip[index2] = clip[index2], clip[index1]
            for idx_, gene in enumerate(clip):
                if gene == 0 and (idx_ == 0 or clip[idx_ - 1] != 0):
                    avl_machine -= 1

            if avl_machine != 0:
                return offspring

            offspring[idx + index1], offspring[idx + index2] = offspring[idx + index2], offspring[idx + index1]
            break

        component_index -= 1
        idx += (points + machine_number - 1)

    return offspring


def random_selective(data, possibility):        # 依概率选择随机数
    assert len(data) == len(possibility) and len(data) > 0

    sum_val = sum(possibility)
    possibility = [p / sum_val for p in possibility]

    random_val = random.random()
    idx = 0
    for idx, val in enumerate(possibility):
        random_val -= val
        if random_val <= 0:
            break
    return data[idx]


def insert_mutation(parent):
    pos, val = np.random.randint(0, len(parent), 1), parent[-1]
    parent[pos: len(parent) - 1] = parent[pos + 1:]
    parent[pos] = val
    return parent


def roulette_wheel_selection(pop_eval):
    # Roulette wheel
    random_val = np.random.random() * sum(pop_eval)
    for idx, val in enumerate(pop_eval):
        random_val -= val
        if random_val <= 0:
            return idx
    return len(pop_eval) - 1


def get_top_k_value(pop_val, k: int, reverse=True):
    res = []
    pop_val_cpy = copy.deepcopy(pop_val)
    pop_val_cpy.sort(reverse=reverse)

    for i in range(min(len(pop_val_cpy), k)):
        for j in range(len(pop_val)):
            if abs(pop_val_cpy[i] - pop_val[j]) < 1e-9 and j not in res:
                res.append(j)
                break
    return res


def get_line_config_number(machine_number, component_number):
    div_counter = 0
    div_set = set()
    for div1 in range(component_number - 2):
        for div2 in range(div1 + 1, component_number - 1):
            machine_div = [div1 + 1, div2 - div1, component_number - div2 - 1]
            machine_div.sort()
            div_str = "".join(str(s) + '|' for s in machine_div)
            if div_str in div_set:
                continue

            div_set.add(div_str)
            assign_component_counter = defaultdict(list)
            for div in machine_div:
                assign_component_counter[div] += 1

            case_div_counter, case_comp_number = 1, component_number
            for idx in range(machine_number - 1):
                div = 1
                while machine_div[idx]:
                    div *= (case_comp_number / machine_div[idx])
                    case_comp_number -= 1
                    machine_div[idx] -= 1

                case_div_counter *= div

            for key, val in assign_component_counter.values():
                div = 1
                while val:
                    div *= val
                    val -= 1
                case_div_counter /= div
            div_counter += case_div_counter
        return div_counter


def convert_line_assigment(pcb_data, component_data, assignment_result):
    machine_number = len(assignment_result)

    placement_points = []
    partial_pcb_data, partial_component_data = defaultdict(pd.DataFrame), defaultdict(pd.DataFrame)
    for machine_index in range(machine_number):
        if pcb_data is not None:
            partial_pcb_data[machine_index] = pd.DataFrame(columns=pcb_data.columns)
        partial_component_data[machine_index] = component_data.copy(deep=True)
        placement_points.append(sum(assignment_result[machine_index]))

    if pcb_data is not None:
        assert sum(placement_points) == len(pcb_data)

    # === averagely assign available feeder ===
    for part_index, data in component_data.iterrows():
        feeder_limit = data.fdn
        feeder_points = [assignment_result[machine_index][part_index] for machine_index in range(machine_number)]

        for machine_index in range(machine_number):
            if pcb_data is None:
                partial_component_data[machine_index].loc[part_index, 'points'] = assignment_result[machine_index][
                    part_index]
            else:
                partial_component_data[machine_index].loc[part_index, 'points'] = 0

        for machine_index in range(machine_number):
            if feeder_points[machine_index] == 0:
                continue

            partial_component_data[machine_index].loc[part_index].fdn = 1
            feeder_limit -= 1

        while feeder_limit:
            assign_machine = None
            for machine_index in range(machine_number):
                if feeder_limit <= 0:
                    break

                if feeder_points[machine_index] == 0:
                    continue

                if assign_machine is None or feeder_points[machine_index] / \
                        partial_component_data[machine_index].loc[part_index].fdn > feeder_points[
                    assign_machine] / partial_component_data[assign_machine].loc[part_index].fdn:
                    assign_machine = machine_index
            assert assign_machine is not None
            partial_component_data[assign_machine].loc[part_index, 'fdn'] += 1

            feeder_limit -= 1

        for machine_index in range(machine_number):
            if feeder_points[machine_index] > 0:
                assert partial_component_data[machine_index].loc[part_index].fdn > 0

    # === assign placements ===
    if pcb_data is not None:
        part2idx = defaultdict(int)
        for idx, data in component_data.iterrows():
            part2idx[data.part] = idx

        machine_average_pos = [[0, 0] for _ in range(machine_number)]
        machine_step_counter = [0 for _ in range(machine_number)]
        part_pcb_data = defaultdict(list)
        for _, data in pcb_data.iterrows():
            part_pcb_data[part2idx[data.part]].append(data)

        multiple_component_index = []
        for part_index in range(len(component_data)):
            machine_assign_set = []
            for machine_index in range(machine_number):
                if assignment_result[machine_index][part_index]:
                    machine_assign_set.append(machine_index)

            if len(machine_assign_set) == 1:
                for data in part_pcb_data[part_index]:
                    machine_index = machine_assign_set[0]

                    machine_average_pos[machine_index][0] += data.x
                    machine_average_pos[machine_index][1] += data.y

                    machine_step_counter[machine_index] += 1

                    partial_component_data[machine_index].loc[part_index, 'points'] += 1
                    partial_pcb_data[machine_index] = pd.concat([partial_pcb_data[machine_index], pd.DataFrame(data).T])

            elif len(machine_assign_set) > 1:
                multiple_component_index.append(part_index)

        for machine_index in range(machine_number):
            if machine_step_counter[machine_index] == 0:
                continue
            machine_average_pos[machine_index][0] /= machine_step_counter[machine_index]
            machine_average_pos[machine_index][1] /= machine_step_counter[machine_index]

        for part_index in multiple_component_index:
            for data in part_pcb_data[part_index]:
                idx = -1
                min_dist = None
                for machine_index in range(machine_number):
                    if partial_component_data[machine_index].loc[part_index, 'points'] >= \
                            assignment_result[machine_index][part_index]:
                        continue
                    dist = (data.x - machine_average_pos[machine_index][0]) ** 2 + (
                                data.y - machine_average_pos[machine_index][1]) ** 2
                    if min_dist is None or dist < min_dist:
                        min_dist, idx = dist, machine_index

                assert idx >= 0
                machine_step_counter[idx] += 1
                machine_average_pos[idx][0] += (1 - 1 / machine_step_counter[idx]) * machine_average_pos[idx][0] \
                                               + data.x / machine_step_counter[idx]
                machine_average_pos[idx][1] += (1 - 1 / machine_step_counter[idx]) * machine_average_pos[idx][1] \
                                               + data.y / machine_step_counter[idx]

                partial_component_data[idx].loc[part_index, 'points'] += 1
                partial_pcb_data[idx] = pd.concat([partial_pcb_data[idx], pd.DataFrame(data).T])

        for machine_index in range(machine_number):
            partial_component_data[machine_index] = partial_component_data[machine_index][
                partial_component_data[machine_index]['points'] != 0].reset_index(drop=True)

    return partial_pcb_data, partial_component_data


def random_division(num, div):
    assert num >= div
    res = [1 for _ in range(num)]
    while sum(res) < num:
        pos = random.randint(0, num - 1)
        val = random.randint(1, num - sum(res))
        res[pos] = val

    return res


def list_range(start, end=None):
    return list(range(start)) if end is None else list(range(start, end))


def kth_indices_partition(num, kth):
    if len(num) > kth:
        return np.argpartition(num, kth)
    else:
        return np.array(range(len(num)))