import random
import time
import threading
import copy
from multiprocessing import Pool


class Matrix:
    def __init__(self) -> None:
        self.matrix_100 = [[0] * 100 for _ in range(100)]
        self.matrix_300 = [[0] * 300 for _ in range(300)]
        self.matrix_500 = [[0] * 500 for _ in range(500)]
        
    def str_matrix(self, type_list: str):
        _str = ""
        
        current_matrix = getattr(self, type_list)
                
        for i in range(len(current_matrix)):
            _str += "[ "
            
            for j in range(len(current_matrix[0])):
                _str += str(current_matrix[i][j]) + " "
                
            _str += " ]\n"
            
        return _str

# Глобальный объект класса для хранения результата работы потоков
result_matrix = copy.deepcopy(Matrix())

def init_matrix(matrix: Matrix, size: int):
    support_list_main = []
    
    for i in range(size):
        support_list_column = []
        
        for j in range(size):
            support_list_column.append(random.randint(0, 10))
            
        support_list_main.append(support_list_column)
        
    if size == 100:
        matrix.matrix_100 = support_list_main
    elif size == 300:
        matrix.matrix_300 = support_list_main
    elif size == 500:
        matrix.matrix_500 = support_list_main
            

# Простая функция перемножения матриц         
def matrix_multiplication(matrix_first: Matrix, matrix_second: Matrix, type_matrix: str):
    result_matrix = Matrix()
    
    first_matrix = getattr(matrix_first, type_matrix)
    second_matrix = getattr(matrix_second, type_matrix)
    result = getattr(result_matrix, type_matrix)
    
    for i in range(len(first_matrix)):
        for j in range(len(second_matrix[0])):
            for k in range(len(second_matrix)):
                result[i][j] += first_matrix[i][k] * second_matrix[k][j]
                
    return result_matrix


# Функция перемножения матриц в потоке
def matrix_multiplication_worker(args):
    first_matrix, second_matrix, type_matrix, support_index, number_thread = args
    global result_matrix
    
    result = getattr(result_matrix, type_matrix)
    
    for i in range(support_index[0], support_index[1]):
        for j in range(len(second_matrix[0])):
            for k in range(len(second_matrix)):
                result[i][j] += first_matrix[i - support_index[0]][k] * second_matrix[k][j]

    return f"Worker completed task of {number_thread}"

def matrix_multiplication_treads(matrix_first: Matrix, matrix_second: Matrix, type_matrix: str, count_thread: int):
    first_matrix = getattr(matrix_first, type_matrix)
    second_matrix = getattr(matrix_second, type_matrix)
    
    # кол-во строк с конца
    last_row = 0
    
    if len(first_matrix) % count_thread == 0:
        index_rows_by_thread = len(first_matrix) // count_thread
    else:
        index_rows_by_thread = len(first_matrix) // count_thread
        last_row = len(first_matrix) % count_thread
        
    # "распиливаем" первую матрицу на кол-во потоков. Результатом работы каждого потока будет являться часть перемноженной матрицы
    support_matrix = []
    
    for i in range(count_thread):
        start_index = i * index_rows_by_thread
        
        if i == count_thread - 1 and last_row > 0:
            end_index = start_index + last_row
        else:
            end_index = start_index + index_rows_by_thread
        
        support_matrix.append((first_matrix[start_index:end_index], second_matrix,  type_matrix, [start_index, end_index], i))
    
        
    # Попытка запуска нескольких ПОТОКОВ. Gil не дал получить выйгрыш, так как из-за его особенностей не давал параллельно вычислить перемножение двух матриц
    # формируем пул потоков
    # workers = [threading.Thread(target=matrix_multiplication_worker, args=(support_matrix[f], second_matrix, type_matrix, support_matrix_index[f], f), daemon=True) for f in range(count_thread)]

    # for worker in workers:
    #     worker.start()

    # for worker in workers:
    #     worker.join()
    
    # Создание пула процессов и запуск параллельного выполнения
    with Pool(processes=count_thread) as pool:
        pool.map(matrix_multiplication_worker, support_matrix)
        
    return "Done."
            
def run_program():
    matrix_first = Matrix()
    init_matrix(matrix_first, 100)
    init_matrix(matrix_first, 300)
    init_matrix(matrix_first, 500)
    
    matrix_second = Matrix()
    init_matrix(matrix_second, 100)
    init_matrix(matrix_second, 300)
    init_matrix(matrix_second, 500)
    
    start_time = time.time()
    matrix_multiplication(matrix_first, matrix_second, "matrix_100")
    end_time = time.time()
    print("Time 100x100: ", end_time - start_time)
    
    start_time = time.time()
    matrix_multiplication_treads(matrix_first, matrix_second, "matrix_100", 3)
    end_time = time.time()
    print("Time 100x100, 3 threads (processes): ", end_time - start_time)
    
    start_time = time.time()
    matrix_multiplication_treads(matrix_first, matrix_second, "matrix_100", 5)
    end_time = time.time()
    print("Time 100x100, 5 threads (processes): ", end_time - start_time)
    
    start_time = time.time()
    matrix_multiplication_treads(matrix_first, matrix_second, "matrix_100", 8)
    end_time = time.time()
    print("Time 100x100, 8 threads (processes): ", end_time - start_time)
    
    # ----------------------------------------------------------------------------------------------------
    print("\n" + "-" * 50 + "\n")
    
    start_time = time.time()
    matrix_multiplication(matrix_first, matrix_second, "matrix_300")
    end_time = time.time()
    print("Time 300x300: ", end_time - start_time)
    
    start_time = time.time()
    matrix_multiplication_treads(matrix_first, matrix_second, "matrix_300", 3)
    end_time = time.time()
    print("Time 300x300, 3 threads (processes): ", end_time - start_time)
    
    start_time = time.time()
    matrix_multiplication_treads(matrix_first, matrix_second, "matrix_300", 5)
    end_time = time.time()
    print("Time 300x300, 5 threads (processes): ", end_time - start_time)
    
    start_time = time.time()
    matrix_multiplication_treads(matrix_first, matrix_second, "matrix_300", 8)
    end_time = time.time()
    print("Time 300x300, 8 threads (processes): ", end_time - start_time)
    
    # ----------------------------------------------------------------------------------------------------
    print("\n" + "-" * 50 + "\n")
    
    start_time = time.time()
    matrix_multiplication(matrix_first, matrix_second, "matrix_500")
    end_time = time.time()
    print("Time 500x500: ", end_time - start_time)
    
    start_time = time.time()
    matrix_multiplication_treads(matrix_first, matrix_second, "matrix_500", 3)
    end_time = time.time()
    print("Time 500x500, 3 threads (processes): ", end_time - start_time)
    
    start_time = time.time()
    matrix_multiplication_treads(matrix_first, matrix_second, "matrix_500", 5)
    end_time = time.time()
    print("Time 500x500, 5 threads (processes): ", end_time - start_time)
    
    start_time = time.time()
    matrix_multiplication_treads(matrix_first, matrix_second, "matrix_500", 8)
    end_time = time.time()
    print("Time 500x500, 8 threads (processes): ", end_time - start_time)
    
run_program()