# implementation of <<An integrated allocation method for the PCB assembly line balancing problem with nozzle changes>>
from base_optimizer.optimizer_common import *
from lineopt_hyperheuristic import *


def selective_initialization(component_points, component_feeders, population_size, machine_number):
    population = []    # population initialization
    for _ in range(population_size):
        individual = []
        for part_index, points in component_points:
            if points == 0:
                continue
            # 可用机器数
            avl_machine_num = random.randint(1, min(machine_number, component_feeders[part_index], points))

            selective_possibility = []
            for p in range(1, avl_machine_num + 1):
                selective_possibility.append(pow(2, avl_machine_num - p + 1))

            sel_machine_num = random_selective([p + 1 for p in range(avl_machine_num)], selective_possibility)  # 选择的机器数
            sel_machine_set = random.sample([p for p in range(machine_number)], sel_machine_num)

            sel_machine_points = [1 for _ in range(sel_machine_num)]
            for p in range(sel_machine_num - 1):
                if points == sum(sel_machine_points):
                    break
                assign_points = random.randint(1, points - sum(sel_machine_points))
                sel_machine_points[p] += assign_points

            if sum(sel_machine_points) < points:
                sel_machine_points[-1] += (points - sum(sel_machine_points))

            # code component allocation into chromosome
            for p in range(machine_number):
                if p in sel_machine_set:
                    individual += [0 for _ in range(sel_machine_points[0])]
                    sel_machine_points.pop(0)
                individual.append(1)
            individual.pop(-1)

        population.append(individual)
    return population


def selective_crossover(component_points, component_feeders, mother, father, machine_number, non_decelerating=True):
    assert len(mother) == len(father)

    offspring1, offspring2 = mother.copy(), father.copy()
    one_counter, feasible_cut_line = 0, []

    idx = 0
    for part_index, points in component_points.items():
        one_counter = 0

        idx_, mother_cut_line, father_cut_line = 0, [-1], [-1]
        for idx_, gene in enumerate(mother[idx: idx + points + machine_number - 1]):
            if gene:
                mother_cut_line.append(idx_)
        mother_cut_line.append(idx_ + 1)

        for idx_, gene in enumerate(father[idx: idx + points + machine_number - 1]):
            if gene:
                father_cut_line.append(idx_)
        father_cut_line.append(idx_ + 1)

        for offset in range(points + machine_number - 1):
            if mother[idx + offset] == 1:
                one_counter += 1
            if father[idx + offset] == 1:
                one_counter -= 1

            # first constraint: the total number of '1's (the number of partitions) in the chromosome is unchanged
            if one_counter != 0 or offset == 0 or offset == points + machine_number - 2:
                continue

            # the selected cut-line should guarantee there are the same or a larger number unassigned machine
            # for each component type
            n_bro, n_new = 0, 0
            if mother[idx + offset] and mother[idx + offset + 1]:
                n_bro += 1
            if father[idx + offset] and father[idx + offset + 1]:
                n_bro += 1
            if mother[idx + offset] and father[idx + offset + 1]:
                n_new += 1
            if father[idx + offset] and mother[idx + offset + 1]:
                n_new += 1

            # second constraint: non_decelerating or accelerating crossover
            # non_decelerating or accelerating means that the number of machine without workload is increased
            if n_new < n_bro or (n_new == n_bro and not non_decelerating):
                continue

            # third constraint (customized constraint):
            # no more than the maximum number of available machine for each component type
            new_mother_cut_line, new_father_cut_line = [], []
            for idx_ in range(machine_number + 1):
                if mother_cut_line[idx_] <= offset:
                    new_mother_cut_line.append(mother_cut_line[idx_])
                else:
                    new_father_cut_line.append(mother_cut_line[idx_])

                if father_cut_line[idx_] <= offset:
                    new_father_cut_line.append(father_cut_line[idx_])
                else:
                    new_mother_cut_line.append(father_cut_line[idx_])

            sorted(new_mother_cut_line, reverse=False)
            sorted(new_father_cut_line, reverse=False)
            n_mother_machine, n_father_machine = 0, 0

            for idx_ in range(machine_number):
                if new_mother_cut_line[idx_ + 1] - new_mother_cut_line[idx_] > 1:
                    n_mother_machine += 1

                if new_father_cut_line[idx_ + 1] - new_father_cut_line[idx_] > 1:
                    n_father_machine += 1

            if n_mother_machine > component_feeders[part_index] or n_father_machine > component_feeders[part_index]:
                continue

            feasible_cut_line.append(idx + offset)

        idx += (points + machine_number - 1)

    if len(feasible_cut_line) == 0:
        return offspring1, offspring2

    cut_line_idx = feasible_cut_line[random.randint(0, len(feasible_cut_line) - 1)]
    offspring1, offspring2 = mother[:cut_line_idx + 1] + father[cut_line_idx + 1:], father[:cut_line_idx + 1] + mother[
                                                                                                     cut_line_idx + 1:]
    return offspring1, offspring2


def cal_individual_val(component_points, component_nozzle, machine_number, individual, estimator):
    idx, objective_val = 0, []
    machine_component_points = [[] for _ in range(machine_number)]

    # decode the component allocation
    for part_index, points in component_points.items():
        component_gene = individual[idx: idx + points + machine_number - 1]
        machine_idx, component_counter = 0, 0
        for gene in component_gene:
            if gene:
                machine_component_points[machine_idx].append(component_counter)
                machine_idx += 1
                component_counter = 0
            else:
                component_counter += 1
        machine_component_points[-1].append(component_counter)
        idx += (points + machine_number - 1)

    objective_val = 0
    for machine_idx in range(machine_number):
        machine_points = sum(machine_component_points[machine_idx])         # num of placement points
        if machine_points == 0:
            continue

        cp_points, cp_nozzle = defaultdict(int), defaultdict(str)
        for part_index, points in enumerate(machine_component_points[machine_idx]):
            if points == 0:
                continue
            cp_points[part_index], cp_nozzle[part_index] = points, component_nozzle[part_index]
        objective_val = max(objective_val, estimator.predict(cp_points, cp_nozzle))
    return objective_val, machine_component_points


def individual_convert(component_points, individual):
    machine_number = len(individual)
    machine_component_points = [[] for _ in range(machine_number)]
    idx = 0
    # decode the component allocation
    for comp_idx, points in component_points:
        component_gene = individual[idx: idx + points + machine_number - 1]
        machine_idx, component_counter = 0, 0
        for gene in component_gene:
            if gene:
                machine_component_points[machine_idx].append(component_counter)
                machine_idx += 1
                component_counter = 0
            else:
                component_counter += 1
        machine_component_points[-1].append(component_counter)
        idx += (points + machine_number - 1)

    return machine_component_points


def line_optimizer_genetic(component_data, machine_number):
    # basic parameter
    # crossover rate & mutation rate: 80% & 10%
    # population size: 200
    # the number of generation: 500
    crossover_rate, mutation_rate = 0.8, 0.1
    population_size, n_generations = 200, 500

    estimator = HeuristicEstimator()
    # the number of placement points, the number of available feeders, and nozzle type of component respectively
    cp_points, cp_feeders, cp_nozzle = defaultdict(int), defaultdict(int), defaultdict(int)
    for part_index, data in component_data.iterrows():
        cp_points[part_index] += data.points
        cp_feeders[part_index], cp_nozzle[part_index] = data.fdn, data.nz

    # population initialization
    population = selective_initialization(sorted(cp_points.items(), key=lambda x: x[0]), cp_feeders, population_size,
                                          machine_number)
    # calculate fitness value
    pop_val = [cal_individual_val(cp_points, cp_nozzle, machine_number, individual, estimator)[0] for individual in
               population]

    with tqdm(total=n_generations) as pbar:
        pbar.set_description('genetic algorithm process for PCB assembly line balance')

        new_population = []
        for _ in range(n_generations):
            population += new_population
            for individual in new_population:
                val, _ = cal_individual_val(cp_points, cp_nozzle, machine_number, individual, estimator)
                pop_val.append(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 = selective_crossover(cp_points, cp_feeders,
                                                                 population[index1], population[index2], machine_number)

                    if np.random.random() < mutation_rate:
                        offspring1 = constraint_swap_mutation(cp_points, offspring1, machine_number)

                    if np.random.random() < mutation_rate:
                        offspring2 = constraint_swap_mutation(cp_points, offspring2, machine_number)

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

            pbar.update(1)

    best_individual = population[np.argmax(pop_val)]
    val, assignment_result = cal_individual_val(cp_points, cp_nozzle, machine_number, best_individual, estimator)

    print('final value: ', val)
    # available feeder check
    for part_index, data in component_data.iterrows():
        feeder_limit = data.fdn
        for machine_index in range(machine_number):
            if assignment_result[machine_index][part_index]:
                feeder_limit -= 1
        assert feeder_limit >= 0

    return assignment_result
