from base_optimizer.optimizer_interface import *
from generator import *
from estimator import *

os.environ['KMP_DUPLICATE_LIB_OK'] = 'True'


class Heuristic:
    @staticmethod
    def apply(cp_points, cp_nozzle, cp_assign, machine_assign):
        return -1


class LeastPoints(Heuristic):
    @staticmethod
    def apply(cp_points, cp_nozzle, cp_assign, machine_assign):
        machine_points = []
        for machine_idx in machine_assign:
            if len(cp_assign[machine_idx]) == 0:
                return machine_idx
            machine_points.append(sum([cp_points[cp_idx] for cp_idx in cp_assign[machine_idx]]))
        return machine_assign[np.argmin(machine_points)]


class LeastNzTypes(Heuristic):
    @staticmethod
    def apply(cp_points, cp_nozzle, cp_assign, machine_assign):
        machine_nozzle = []
        for machine_idx in machine_assign:
            if len(cp_assign[machine_idx]) == 0:
                return machine_idx
            machine_nozzle.append([cp_nozzle[cp_idx] for cp_idx in cp_assign[machine_idx]])
        index = np.argmin(
            [len(set(nozzle)) + 1e-5 * sum(cp_points[c] for c in cp_assign[machine_idx]) for machine_idx, nozzle in
             enumerate(machine_nozzle)])
        return machine_assign[index]


class LeastCpTypes(Heuristic):
    @staticmethod
    def apply(cp_points, cp_nozzle, cp_assign, machine_assign):
        machine_types = []
        for machine_idx in machine_assign:
            machine_types.append(
                len(cp_assign[machine_idx]) + 1e-5 * sum(cp_points[cp] for cp in cp_assign[machine_idx]))
        return machine_assign[np.argmin(machine_types)]


class LeastCpNzRatio(Heuristic):
    @staticmethod
    def apply(cp_points, cp_nozzle, cp_assign, machine_assign):
        machine_nz_type, machine_cp_type = [], []
        for machine_idx in machine_assign:
            if len(cp_assign[machine_idx]) == 0:
                return machine_idx
            machine_nz_type.append(set(cp_nozzle[cp_idx] for cp_idx in cp_assign[machine_idx]))
            machine_cp_type.append(len(cp_assign[machine_idx]))

        min_idx = np.argmin([(machine_cp_type[idx] + 1e-5 * sum(
            cp_points[c] for c in cp_assign[machine_assign[idx]])) / (len(machine_nz_type[idx]) + 1e-5) for idx in
                             range(len(machine_assign))])
        return machine_assign[min_idx]


def nozzle_assignment(cp_points, cp_nozzle, cp_assign):
    nozzle_heads, nozzle_points = defaultdict(int), defaultdict(int)

    for cp_idx in cp_assign:
        nozzle_points[cp_nozzle[cp_idx]] += cp_points[cp_idx]
        nozzle_heads[cp_nozzle[cp_idx]] = 1

    while sum(nozzle_heads.values()) != max_head_index:
        max_cycle_nozzle = None
        for nozzle, head_num in nozzle_heads.items():
            if max_cycle_nozzle is None or nozzle_points[nozzle] / head_num > nozzle_points[max_cycle_nozzle] / \
                    nozzle_heads[max_cycle_nozzle]:
                max_cycle_nozzle = nozzle

        assert max_cycle_nozzle is not None
        nozzle_heads[max_cycle_nozzle] += 1
    return nozzle_heads, nozzle_points


class LeastCycle(Heuristic):
    @staticmethod
    def apply(cp_points, cp_nozzle, cp_assign, machine_assign):
        machine_cycle = []
        for machine_idx in machine_assign:
            assign_component = cp_assign[machine_idx]
            if len(assign_component) == 0:
                return machine_idx

            nozzle_heads, nozzle_points = nozzle_assignment(cp_points, cp_nozzle, assign_component)
            machine_cycle.append(
                max(nozzle_points[nozzle] / head for nozzle, head in nozzle_heads.items()) + 1e-5 * sum(
                    cp_points[c] for c in cp_assign[machine_idx]))

        return machine_assign[np.argmin(machine_cycle)]


class LeastNzChange(Heuristic):
    @staticmethod
    def apply(cp_points, cp_nozzle, cp_assign, machine_assign):
        machine_nozzle_change = []
        for machine_idx in machine_assign:
            assign_component = cp_assign[machine_idx]
            if len(assign_component) == 0:
                return machine_idx

            heads_points = []
            nozzle_heads, nozzle_points = nozzle_assignment(cp_points, cp_nozzle, assign_component)
            for nozzle, head in nozzle_heads.items():
                for _ in range(head):
                    heads_points.append(nozzle_points[nozzle] / nozzle_heads[nozzle])
            machine_nozzle_change.append(np.std(heads_points) + 1e-5 * sum(cp_points[c] for c in cp_assign[machine_idx]))

        return machine_assign[np.argmin(machine_nozzle_change)]


class LeastPickup(Heuristic):
    @staticmethod
    def apply(cp_points, cp_nozzle, cp_assign, machine_assign):
        machine_pick_up = []
        for machine_idx in machine_assign:
            assign_component = cp_assign[machine_idx]
            if len(assign_component) == 0:
                return machine_idx
            nozzle_heads, nozzle_points = nozzle_assignment(cp_points, cp_nozzle, assign_component)

            nozzle_level, nozzle_counter = defaultdict(int), defaultdict(int)
            level_points = defaultdict(int)

            for cp_idx in sorted(assign_component, key=lambda x: cp_points[x], reverse=True):
                nozzle, points = cp_nozzle[cp_idx], cp_points[cp_idx]
                if nozzle_counter[nozzle] and nozzle_counter[nozzle] % nozzle_heads[nozzle] == 0:
                    nozzle_level[nozzle] += 1
                level = nozzle_level[nozzle]
                level_points[level] = max(level_points[level], points)
                nozzle_counter[nozzle] += 1

            machine_pick_up.append(sum(points for points in level_points.values()) + 1e-5 * sum(
                cp_points[idx] for idx in cp_assign[machine_idx]))
        return machine_assign[np.argmin(machine_pick_up)]


def generate_pattern(heuristic_map, cp_points):
    """
    Generates a random pattern.
    :return: The generated pattern string.
    """
    return "".join([random.choice(list(heuristic_map.keys())) for _ in range(random.randrange(1, len(cp_points)))])


def crossover(cp_points, parent1, parent2):
    """
    Attempt to perform crossover between two chromosomes.
    :param parent1: The first parent.
    :param parent2: The second parent.
    :return: The two individuals after crossover has been performed.
    """
    point1, point2 = random.randrange(len(parent1)), random.randrange(len(parent2))
    substr1, substr2 = parent1[point1:], parent2[point2:]
    offspring1, offspring2 = "".join((parent1[:point1], substr2)), "".join((parent2[:point2], substr1))
    return offspring1[:len(cp_points)], offspring2[:len(cp_points)]


def mutation(heuristic_map, cp_points, individual):
    """
    Attempts to mutate the individual by replacing a random heuristic in the chromosome by a generated pattern.
    :param individual: The individual to mutate.
    :return: The mutated individual.
    """
    pattern = list(individual)
    mutation_point = random.randrange(len(pattern))
    pattern[mutation_point] = generate_pattern(heuristic_map, cp_points)
    return ''.join(pattern)[:len(cp_points)]


def population_initialization(population_size, heuristic_map, cp_points):
    return [generate_pattern(heuristic_map, cp_points) for _ in range(population_size)]


def convert_assignment_result(heuristic_map, cp_index, cp_points, cp_nozzle, cp_feeders, component_list, individual,
                              machine_number):
    component_number = len(cp_feeders.keys())

    cp_assign = [[] for _ in range(machine_number)]
    component_machine_assign = [[0 for _ in range(machine_number)] for _ in range(component_number)]
    machine_assign_counter = [0 for _ in range(machine_number)]

    data_mgr = DataMgr()

    for idx, div_cp_idx in enumerate(component_list):
        h = individual[idx % len(individual)]
        cp_idx = cp_index[div_cp_idx]

        machine_assign = []  # 可被分配的机器索引
        if sum(component_machine_assign[cp_idx][:]) < cp_feeders[cp_idx]:
            for machine_idx in range(machine_number):
                if component_machine_assign[cp_idx][machine_idx] or machine_assign_counter[
                    machine_idx] < data_mgr.max_component_types:
                    machine_assign.append(machine_idx)
            machine_idx = heuristic_map[h].apply(cp_points, cp_nozzle, cp_assign, machine_assign)
        else:
            for machine_idx in range(machine_number):
                if component_machine_assign[cp_idx][machine_idx]:
                    machine_assign.append(machine_idx)
            machine_idx = heuristic_map[h].apply(cp_points, cp_nozzle, cp_assign, machine_assign)

        cp_assign[machine_idx].append(div_cp_idx)

        if component_machine_assign[cp_idx][machine_idx] == 0:
            machine_assign_counter[machine_idx] += 1
            component_machine_assign[cp_idx][machine_idx] = 1

    return cp_assign


def cal_individual_val(heuristic_map, cp_index, cp_points, cp_nozzle, cp_feeders, board_width, board_height,
                       component_list, individual, machine_number, estimator):
    machine_cp_assign = convert_assignment_result(heuristic_map, cp_index, cp_points, cp_nozzle, cp_feeders, component_list,
                                                  individual, machine_number)
    component_number = len(cp_feeders)

    machine_cp_points = [[0 for _ in range(component_number)] for _ in range(machine_number)]
    for machine_idx in range(machine_number):
        for idx in machine_cp_assign[machine_idx]:
            machine_cp_points[machine_idx][cp_index[idx]] += cp_points[idx]
    machine_cp_feeders = [[0 for _ in range(component_number)] for _ in range(machine_number)]

    for cp_idx in range(component_number):
        feeder_nums = cp_feeders[cp_idx]
        for machine_idx in range(machine_number):
            if machine_cp_points[machine_idx][cp_idx]:
                machine_cp_feeders[machine_idx][cp_idx] = 1
                feeder_nums -= 1
        while feeder_nums > 0:
            assign_machine = None
            for machine_idx in range(machine_number):
                if machine_cp_points[machine_idx][cp_idx] == 0:
                    continue
                if assign_machine is None:
                    assign_machine = machine_idx
                    continue
                if machine_cp_points[assign_machine][cp_idx] / machine_cp_feeders[assign_machine][cp_idx] \
                        < machine_cp_points[machine_idx][cp_idx] / machine_cp_feeders[machine_idx][cp_idx]:
                    assign_machine = machine_idx
            machine_cp_feeders[assign_machine][cp_idx] += 1
            feeder_nums -= 1
    nozzle_type = defaultdict(str)
    for idx, cp_idx in cp_index.items():
        nozzle_type[cp_idx] = cp_nozzle[idx]

    objective_val = []
    for machine_idx in range(machine_number):
        div_cp_points, div_cp_nozzle = defaultdict(int), defaultdict(str)
        idx = 0
        for cp_idx in range(component_number):
            total_points = machine_cp_points[machine_idx][cp_idx]
            if total_points == 0:
                continue
            div_index = 0
            div_points = [total_points // machine_cp_feeders[machine_idx][cp_idx] for _ in
                          range(machine_cp_feeders[machine_idx][cp_idx])]
            while sum(div_points) < total_points:
                div_points[div_index] += 1
                div_index += 1

            for points in div_points:
                div_cp_points[idx] = points
                div_cp_nozzle[idx] = nozzle_type[cp_idx]
                idx += 1
        objective_val.append(estimator.predict(div_cp_points, div_cp_nozzle, board_width, board_height))

    return objective_val


def line_optimizer_hyperheuristic(component_data, pcb_data, machine_number):
    heuristic_map = {
        'p': LeastPoints,
        'n': LeastNzTypes,
        'c': LeastCpTypes,
        'r': LeastCpNzRatio,
        'k': LeastCycle,
        'g': LeastNzChange,
        'u': LeastPickup,
    }
    division_part = []
    for _, data in component_data.iterrows():
        division_part.extend([data.points / data.fdn for _ in range(data.fdn)])
    division_points = sum(division_part) / len(division_part)

    # genetic-based hyper-heuristic
    crossover_rate, mutation_rate = 0.6, 0.1
    population_size, total_generation = 20, 50
    group_size = 10
    estimator = NeuralEstimator()

    best_val = None
    best_heuristic_list, best_component_list = None, None

    cp_feeders, cp_nozzle = defaultdict(int), defaultdict(str)
    cp_points, cp_index = defaultdict(int), defaultdict(int)

    division_component_data = pd.DataFrame(columns=component_data.columns)

    idx = 0
    for cp_idx, data in component_data.iterrows():
        cp_feeders[cp_idx] = data.fdn

        division_data = copy.deepcopy(data)
        feeder_limit, total_points = division_data.fdn, division_data.points
        if feeder_limit != 1:
            feeder_limit = round(min(max(total_points // division_points * 1.5, feeder_limit), total_points))
            # feeder_limit = total_points         # 小规模数据启用
        surplus_points = total_points % feeder_limit
        for _ in range(feeder_limit):
            division_data.fdn, division_data.points = 1, math.floor(total_points / feeder_limit)
            if surplus_points:
                division_data.points += 1
                surplus_points -= 1

            cp_points[idx], cp_nozzle[idx] = division_data.points, division_data.nz
            cp_index[idx] = cp_idx
            idx += 1
            division_component_data = pd.concat([division_component_data, pd.DataFrame(division_data).T])
    division_component_data = division_component_data.reset_index()

    component_list = [idx for idx, data in division_component_data.iterrows() if data.points > 0]
    board_width, board_height = pcb_data['x'].max() - pcb_data['x'].min(), pcb_data['y'].max() - pcb_data['y'].min()
    with tqdm(total=total_generation * group_size) as pbar:
        pbar.set_description('hyper-heuristic algorithm process for PCB assembly line balance')
        for _ in range(group_size):
            random.shuffle(component_list)
            new_population = []
            population = population_initialization(population_size, heuristic_map, cp_points)

            # calculate fitness value
            pop_val = []
            for individual in population:
                val = cal_individual_val(heuristic_map, cp_index, cp_points, cp_nozzle, cp_feeders, board_width,
                                         board_height, component_list, individual, machine_number, estimator)
                pop_val.append(max(val))

            for _ in range(total_generation):
                population += new_population
                for individual in new_population:
                    val = cal_individual_val(heuristic_map, cp_index, cp_points, cp_nozzle, cp_feeders, board_width,
                                             board_height, component_list, individual, machine_number, estimator)
                    pop_val.append(max(val))

                select_index = get_top_k_value(pop_val, population_size, reverse=False)
                population = [population[idx] for idx in select_index]
                pop_val = [pop_val[idx] for idx in select_index]

                # min-max convert
                max_val = max(pop_val)
                sel_pop_val = list(map(lambda v: max_val - v, pop_val))
                sum_pop_val = sum(sel_pop_val) + 1e-10
                sel_pop_val = [v / sum_pop_val + 1e-3 for v in sel_pop_val]

                # crossover and mutation
                new_population = []
                for pop in range(population_size):
                    if pop % 2 == 0 and np.random.random() < crossover_rate:
                        index1 = roulette_wheel_selection(sel_pop_val)
                        while True:
                            index2 = roulette_wheel_selection(sel_pop_val)
                            if index1 != index2:
                                break

                        offspring1, offspring2 = crossover(cp_points, population[index1], population[index2])

                        if np.random.random() < mutation_rate:
                            offspring1 = mutation(heuristic_map, cp_points, offspring1)

                        if np.random.random() < mutation_rate:
                            offspring2 = mutation(heuristic_map, cp_points, offspring2)

                        new_population.append(offspring1)
                        new_population.append(offspring2)

                pbar.update(1)

            val = cal_individual_val(heuristic_map, cp_index, cp_points, cp_nozzle, cp_feeders, board_width,
                                     board_height, component_list, population[0], machine_number, estimator)

            machine_assign = convert_assignment_result(heuristic_map, cp_index, cp_points, cp_nozzle, cp_feeders,
                                                       component_list, population[0], machine_number)

            assignment_result = [[0 for _ in range(len(component_data))] for _ in range(machine_number)]
            for machine_idx in range(machine_number):
                for idx in machine_assign[machine_idx]:
                    assignment_result[machine_idx][cp_index[idx]] += cp_points[idx]
            partial_pcb_data, partial_component_data = convert_line_assigment(pcb_data, component_data,
                                                                              assignment_result)
            max_machine_idx = np.argmax(val)
            val = exact_assembly_time(partial_pcb_data[max_machine_idx], partial_component_data[max_machine_idx])
            if best_val is None or val < best_val:
                for machine_idx in range(machine_number):
                    if machine_idx == max_machine_idx:
                        continue
                    val = max(val,
                              exact_assembly_time(partial_pcb_data[machine_idx], partial_component_data[machine_idx]))
                    if best_val is not None and val > best_val:
                        break

                if best_val is None or val < best_val:
                    best_val = val
                    best_heuristic_list = population[0]
                    best_component_list = component_list.copy()

    machine_cp_points = convert_assignment_result(heuristic_map, cp_index, cp_points, cp_nozzle, cp_feeders,
                                                  best_component_list, best_heuristic_list, machine_number)

    assignment_result = [[0 for _ in range(len(component_data))] for _ in range(machine_number)]
    for machine_idx in range(machine_number):
        for idx in machine_cp_points[machine_idx]:
            assignment_result[machine_idx][cp_index[idx]] += cp_points[idx]

    return assignment_result


