// --- Настройки программы ---

// Индекс главного (управляющего) процесса
#define MAIN_PROC 0

// Ширина полосы матрицы B, передаваемая процессам
#define BANDWIDTH 100

// Число запусков теста
#define TEST_COUNT 5

// Путь к файлу логов
#define LOG_PATH "log-1000.txt"

// Отключение предупреждений безопасности
#define _CRT_SECURE_NO_WARNINGS
#define _CRT_SECURE_NO_DEPRECATE
#define _CRT_NONSTDC_NO_DEPRECATE


#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <time.h>
#include <math.h>
#include <mpi.h>

int gen_random(float a, float b);
FILE* matrix_create(const char *path, int m, int n);
int write_result(const char *path, int *C, int m, int q);
void log_print(const char *message);
void print_errno(const char *message);

int main(int argc, char *argv[])
{
    // Число процессов и номер текущего процесса
    int procNum = 1;
    int procRank = MAIN_PROC;

    MPI_Init(&argc, &argv);
    MPI_Comm_size(MPI_COMM_WORLD, &procNum);
    MPI_Comm_rank(MPI_COMM_WORLD, &procRank);
    MPI_Status status;

    // Время начала работы главного процесса
    double startTime;     

    int i, j, k, p, h;

    // Размерности матриц А{mxn}, B{nxq}, C{mxq}
    int n = 1000, m = 1000, q = 1000;

    // Пути к файлам исходных данных и результата
    const char *A_path = "A.txt";
    const char *B_path = "B.txt";
    const char *C_path = "C.txt";

    FILE *Astream;
    FILE *Bstream;
    FILE *Cstream;
    FILE *Lstream;

    double *results;
    
    if(procRank == MAIN_PROC)
    {
        results = (double *)malloc(TEST_COUNT * sizeof(double));
        
        Lstream = fopen(LOG_PATH, "a");
        if(Lstream == NULL)
            return 0;
        fprintf(Lstream, "----------------------------------------------\n");
        fprintf(Lstream, "Размерности матриц m=%d n=%d q=%d\n", m, n, q);
        fprintf(Lstream, "Число процессоров  %d\n", procNum);
        fprintf(Lstream, "Ширина полосы      %d\n", BANDWIDTH);
        fclose(Lstream);
    }

    // Запускаем тест TEST_COUNT раз
    for(h = 0; h < TEST_COUNT; h++)
    {
        MPI_Barrier(MPI_COMM_WORLD);

        if(procRank == MAIN_PROC)
        {
            // Фиксируем время начала работы главного процесса
            startTime = MPI_Wtime();
            
            // Очищаем файл результата
            Cstream = fopen(C_path, "w");
            fclose(Cstream);
            
            // Пытаемся открыть файлы с матрицами A и B
            Astream = fopen(A_path, "r");
            Bstream = fopen(B_path, "r");
            
            
            // Обработка ошибок при открытии файлов
            if(Astream == NULL || Bstream == NULL)
            {
                if((Astream != NULL && fclose(Astream))
                || (Bstream != NULL && fclose(Bstream)))
                {
                    log_print("ERROR Не удалось закрыть потоки для матриц А и В");
                    MPI_Abort(MPI_COMM_WORLD, MPI_ERR_OTHER);
                    return 0;
                }

                // Генерация новых файлов с матрицами A и B
                srand((unsigned int) time(NULL));
                Astream = matrix_create(A_path, m, n);
                Bstream = matrix_create(B_path, n, q);

                if(Astream == NULL || Bstream == NULL)
                    return 0;

                // Не учитываем время, затраченное на генерацию матриц
                startTime = MPI_Wtime();
            }   
            
            int n1;

            if(fscanf(Astream, "%d %d\n", &m, &n)  <= 0 
            || fscanf(Bstream, "%d %d\n", &n1, &q) <= 0)
            {
                log_print("ERROR Не удалось считать размерности матриц");
                MPI_Abort(MPI_COMM_WORLD, MPI_ERR_OTHER);
                fclose(Astream);
                fclose(Bstream);
                return 0;
            }

            if(n != n1 || n <= 0 || m <= 0 || q <= 0 || n % BANDWIDTH)
            {
                if(n != n1)
                    log_print("ERROR Не верно заданы размерности матриц: Число столбцов в матрице A не совпадает с числом строк в матрице B");

                if(n <= 0 || m <= 0 || q <= 0)
                    log_print("ERROR Не верно заданы размерности матриц: Размерность не может быть меньше либо равна нулю");
                
                if(n % BANDWIDTH)
                    log_print("ERROR Недопустимая ширина полосы");
                
                MPI_Abort(MPI_COMM_WORLD, MPI_ERR_OTHER);
                fclose(Astream);
                fclose(Bstream);
                return 0;
            }
        }

        // Рассылка всем исполнителям размерностей матриц
        MPI_Bcast(&m, 1, MPI_INT, MAIN_PROC, MPI_COMM_WORLD);
        MPI_Bcast(&n, 1, MPI_INT, MAIN_PROC, MPI_COMM_WORLD);
        MPI_Bcast(&q, 1, MPI_INT, MAIN_PROC, MPI_COMM_WORLD);

        // Число строк матрицы A, передаваемое на один процессор
        int AprocRows = floor(((double) m) / ((double) procNum));
        int BprocRows = BANDWIDTH;

        int Asize = AprocRows * n;
        int Bsize = BprocRows * q;
        int Csize = AprocRows * q;

        // Число элементов, обрабатываемых текущим процессором
        int Arows = AprocRows;
        int As = Asize;
        int Bs = Bsize;
        int Cs = Csize;

        if(procRank == MAIN_PROC)
        {
            Arows = m - AprocRows * (procNum - 1);
            As = Arows * n;
            Cs = Arows * q;
        }

        // Выделение памяти
        int *A = (int *)malloc(As * sizeof(int));
        int *B = (int *)malloc(Bs * sizeof(int));
        int *C = (int *)malloc(Cs * sizeof(int));

        // Обработка ошибок выделения памяти
        if(A == NULL || B == NULL || C == NULL)
        {
            print_errno("Не удалось выделить память");
            MPI_Abort(MPI_COMM_WORLD, MPI_ERR_OTHER);
            if(procRank == MAIN_PROC)
            {
                fclose(Astream);
                fclose(Bstream);
            }
            if(A != NULL) free(A);
            if(B != NULL) free(B);
            if(C != NULL) free(C);
            return 0;
        }

        // Чтение матрицы A и распределение её между исполнителями
        if(procRank == MAIN_PROC)
        {
            for(p = 0; p < procNum; p++)
            {
                if(p != MAIN_PROC)
                {
                    for(i = 0; i < Asize; i++)
                        fscanf(Astream, "%d", &A[i]);
                    MPI_Send(A, Asize, MPI_INT, p, 0, MPI_COMM_WORLD);
                }
            }

            for(i = 0; i < As; i++)
                fscanf(Astream, "%d", &A[i]);

            fclose(Astream);
        }
        else
        {
            // Приём исполнителями своих фрагменов матрицы A
            MPI_Recv(A, Asize, MPI_INT, MAIN_PROC, MPI_ANY_TAG, MPI_COMM_WORLD, &status);
        }
        
        // Обнуление частей результирующей матрицы C
        for(i = 0; i < Cs; i++)
            C[i] = 0; 

        for(i = 0; i < n / BprocRows; i++)
        {
            // Чтение строк матрицы B главным процессом
            if(procRank == MAIN_PROC)
            {
                for(j = 0; j < Bsize; j++)
                    fscanf(Bstream, "%d", &B[j]);
            }

            // Синхронизация процессов
            MPI_Barrier(MPI_COMM_WORLD);

            // Рассылка всем исполнителям строк матрицы B
            MPI_Bcast(B, Bsize, MPI_INT, MAIN_PROC, MPI_COMM_WORLD);

            // Вычисление частей результата
            for(k = 0; k < BprocRows; k++)
            {
                for(p = 0; p < Arows; p++)
                {
                    for(j = 0; j < q; j++)
                    {
                        C[p * q + j] += A[p * n + i * BprocRows + k] * B[k * q + j]; 
                    }
                }
            }
        }

        // Освобождаем память
        free(A);
        free(B);

        if(procRank == MAIN_PROC)
            fclose(Bstream);

        // Сбор частей результирующей матрицы со всех процессов на главном процессе
        if(procRank != MAIN_PROC)
        {
            MPI_Send(C, Csize, MPI_INT, MAIN_PROC, 0, MPI_COMM_WORLD);
        }
        else
        {
            // Сохраняем часть результата, вычисленную на главном процессе
            int *buf = (int *)malloc(Cs * sizeof(int));
            if(buf == NULL)
            {
                print_errno("Не удалось выделить память для buf");
                return 0;
            }

            for(i = 0; i < Cs; i++)
                buf[i] = C[i];
            
            for(p = 0; p < procNum; p++)
            {
                if(p != MAIN_PROC)
                {
                    // Приём фрагментов результирующей матрицы на главном процессе
                    MPI_Recv(C, Csize, MPI_INT, p, MPI_ANY_TAG, MPI_COMM_WORLD, &status);
                    if(write_result(C_path, C, AprocRows, q) == 0)
                    {
                        MPI_Abort(MPI_COMM_WORLD, MPI_ERR_OTHER);
                        if(buf != NULL) free(buf);
                        free(C);
                        return 0;
                    }
                }
                else if(write_result(C_path, buf, Arows, q) == 0)
                {
                    MPI_Abort(MPI_COMM_WORLD, MPI_ERR_OTHER);
                    free(buf);
                    free(C);
                    return 0;
                }
            }

            free(buf);
        }

        free(C);

        // Записываем время работы главного процесса в лог        
        if(procRank == MAIN_PROC)
        {
            Lstream = fopen(LOG_PATH, "a");
            if(Lstream == NULL)
                return 0;
            
            if(h == 0)
                fprintf(Lstream, "Память             %d МБ\n\n", round((Asize + Bsize + Csize) / 1024.0 / 1024.0 * sizeof(int)));
            
            results[h] = MPI_Wtime() - startTime;
            fprintf(Lstream, "Время (%d): %f сек\n", h + 1, results[h]);

            fclose(Lstream);
        }
    }

    if(procRank == MAIN_PROC)
    {
        Lstream = fopen(LOG_PATH, "a");
        if(Lstream == NULL)
            return 0;
                    
        int avg = 0;
        for(i = 0; i < TEST_COUNT; i++)
            avg += results[i];
        free(results);
        avg /= TEST_COUNT;
        fprintf(Lstream, "Среднее  : %f сек\n\n", avg);
        fclose(Lstream);
    }
    
    MPI_Finalize();

    return 0;
}

// Генерирует псевдо-случайное число из интервала
//   a - левая граница интервала
//   b - правая граница интервала
// Возвращает сгенерированное число из интервала [a;b]
int gen_random(float a, float b)
{
    return a + (b - a) * ((float)rand() / RAND_MAX);
}

// Генерирует файл с матрицей m?n
//   path - путь к файлу (например, имя файла или абсолютный путь к файлу)
//   m    - число строк в матрице
//   n    - число столбцов в матрице 
// Возвращает указатель на открытый файл. Значение указателя, равное NULL, свидетельствует об ошибке.
FILE* matrix_create(const char *path, int m, int n)
{
    if(n <= 0 || m <= 0)
    {
        log_print("Не удалось сгенерировать файл с матрицей: Не верно задана размерность матрицы");
        return NULL;
    }
    
    FILE *stream = fopen(path, "w");
    if(stream == NULL) 
    {
        print_errno("Не удалось сгенерировать файл с матрицей");
        return NULL;
    }

    if(fprintf(stream, "%d %d\n", m, n) < 0)
    {
        fclose(stream);
        return NULL;
    }

    int i, j;
    for(i = 0; i < m; i++)
    {
        for(j = 0; j < n - 1; j++)
        {
            if(fprintf(stream, "%d ", gen_random(-10,10)) < 0) 
            {
                fclose(stream);
                return NULL;
            }
        }
        if(fprintf(stream, "%d\n", gen_random(-10,10)) < 0)
        {
            fclose(stream);
            return NULL;
        }
    }    

    if(fclose(stream))
    {
        print_errno("Не удалось закрыть поток");
        return NULL;
    }
    return fopen(path, "r");
}

// Дописывает часть результата в файл
//   С - матрица данных части результата
//   m - число строк
//   q - число столбцов
// Возвращает 0 в случае ошибки, иначе 1.
int write_result(const char *path, int *C, int m, int q)
{
    FILE *stream = fopen(path, "a");
    if (stream == NULL) 
    {
        print_errno("Не удалось создать файл для произведения матриц");
        return 0;
    }

    int i, j;
    for(i = 0; i < m; i++)
    {
        for(j = 0; j < q - 1; j++)
        {    
            if(fprintf(stream, "%d ", C[i * q + j]) < 0)
            {
                fclose(stream);
                return 0;
            }
        }
        if(fprintf(stream, "%d\n", C[(i + 1) * q - 1]) < 0)
        {
            fclose(stream);
            return 0;
        }
    }

    if(fclose(stream))
    {
        print_errno("Не удалось закрыть поток");
        return 0;
    }

    return 1;
}

// Запись сообщения в лог
//   message - текстовое сообщение об ошибке
void log_print(const char *message)
{
    FILE *stream = fopen(LOG_PATH, "a");
    if(stream == NULL) return;
    fprintf(stream, "%s\n", message);
    fclose(stream);
}

// Выводит сообщение по коду ошибки errno
//   message - необязательное дополнительное описание ошибки
void print_errno(const char *message)
{
    char *msg = (char *)"ERROR ";
    if(message != NULL)
        msg = strcat(msg, message);

    switch(errno)
    {
        case E2BIG          : msg = strcat(msg, "Список аргументов слишком длинный"); break;
        case EACCES         : msg = strcat(msg, "Отказ в доступе"); break;
        case EADDRINUSE     : msg = strcat(msg, "Адрес используется"); break;
        case EADDRNOTAVAIL  : msg = strcat(msg, "Адрес недоступен"); break;
        case EAFNOSUPPORT   : msg = strcat(msg, "Семейство адресов не поддерживается"); break;
        case EALREADY       : msg = strcat(msg, "Соединение уже устанавливается"); break;
        case EBADF          : msg = strcat(msg, "Неправильный дескриптор файла"); break;
        case EBADMSG        : msg = strcat(msg, "Неправильное сообщение"); break;
        case EBUSY          : msg = strcat(msg, "Ресурс занят"); break;
        case ECANCELED      : msg = strcat(msg, "Операция отменена"); break;
        case ECHILD         : msg = strcat(msg, "Нет дочернего процесса"); break;
        case ECONNABORTED   : msg = strcat(msg, "Соединение прервано"); break;
        case EDEADLK        : msg = strcat(msg, "Обход тупика ресурсов"); break;
        case EDESTADDRREQ   : msg = strcat(msg, "Требуется адрес назначения"); break;
        case EDOM           : msg = strcat(msg, "Ошибка области определения"); break;
        case EEXIST         : msg = strcat(msg, "Файл существует"); break;
        case EFAULT         : msg = strcat(msg, "Неправильный адрес"); break;
        case EFBIG          : msg = strcat(msg, "Файл слишком велик"); break;
        case EHOSTUNREACH   : msg = strcat(msg, "Хост недоступен"); break;
        case EIDRM          : msg = strcat(msg, "Идентификатор удален"); break;
        case EILSEQ         : msg = strcat(msg, "Ошибочная последовательность байтов"); break;
        case EINPROGRESS    : msg = strcat(msg, "Операция в процессе выполнения"); break;
        case EINTR          : msg = strcat(msg, "Прерванный вызов функции"); break;
        case EINVAL         : msg = strcat(msg, "Неправильный аргумент"); break;
        case EIO            : msg = strcat(msg, "Ошибка ввода-вывода"); break;
        case EISCONN        : msg = strcat(msg, "Сокет (уже) соединен"); break;
        case EISDIR         : msg = strcat(msg, "Это каталог"); break;
        case ELOOP          : msg = strcat(msg, "Слишком много уровней символических ссылок"); break;
        case EMFILE         : msg = strcat(msg, "Слишком много открытых файлов"); break;
        case EMLINK         : msg = strcat(msg, "Слишком много связей"); break;
        case EMSGSIZE       : msg = strcat(msg, "Неопределённая длина буфера сообщения"); break;
        case ENAMETOOLONG   : msg = strcat(msg, "Имя файла слишком длинное"); break;
        case ENETDOWN       : msg = strcat(msg, "Сеть не работает"); break;
        case ENETRESET      : msg = strcat(msg, "Соединение прервано сетью"); break;
        case ENETUNREACH    : msg = strcat(msg, "Сеть недоступна"); break;
        case ENFILE         : msg = strcat(msg, "Слишком много открытых файлов в системе"); break;
        case ENOBUFS        : msg = strcat(msg, "Буферное пространство недоступно"); break;
        case ENODEV         : msg = strcat(msg, "Нет такого устройства"); break;
        case ENOENT         : msg = strcat(msg, "Нет такого файла в каталоге"); break;
        case ENOEXEC        : msg = strcat(msg, "Ошибка формата исполняемого файла"); break;
        case ENOLCK         : msg = strcat(msg, "Блокировка недоступна"); break;
        case ENOLINK        : msg = strcat(msg, "Зарезервировано"); break;
        case ENOMEM         : msg = strcat(msg, "Недостаточно памяти"); break;
        case ENOMSG         : msg = strcat(msg, "Сообщение нужного типа отсутствует"); break;
        case ENOPROTOOPT    : msg = strcat(msg, "Протокол недоступен"); break;
        case ENOSPC         : msg = strcat(msg, "Памяти на устройстве не осталось"); break;
        case ENOSYS         : msg = strcat(msg, "Функция не реализована"); break;
        case ENOTCONN       : msg = strcat(msg, "Сокет не соединен"); break;
        case ENOTDIR        : msg = strcat(msg, "Это не каталог"); break;
        case ENOTEMPTY      : msg = strcat(msg, "Каталог непустой"); break;
        case ENOTSOCK       : msg = strcat(msg, "Это не сокет"); break;
        case ENOTTY         : msg = strcat(msg, "Неопределённая операция управления вводом-выводом"); break;
        case ENXIO          : msg = strcat(msg, "Нет такого устройства или адреса"); break;
        case EOPNOTSUPP     : msg = strcat(msg, "Операция сокета не поддерживается"); break;
        case EOVERFLOW      : msg = strcat(msg, "Слишком большое значение для типа данных"); break;
        case EPERM          : msg = strcat(msg, "Операция не разрешена"); break;
        case EPIPE          : msg = strcat(msg, "Разрушенный канал"); break;
        case EPROTO         : msg = strcat(msg, "Ошибка протокола"); break;
        case EPROTONOSUPPORT: msg = strcat(msg, "Протокол не поддерживается"); break;
        case EPROTOTYPE     : msg = strcat(msg, "Ошибочный тип протокола для сокета"); break;
        case ERANGE         : msg = strcat(msg, "Результат слишком велик"); break;
        case EROFS          : msg = strcat(msg, "Файловая система только на чтение"); break;
        case ESPIPE         : msg = strcat(msg, "Неправильное позиционирование"); break;
        case ESRCH          : msg = strcat(msg, "Нет такого процесса"); break;
        case ETIMEDOUT      : msg = strcat(msg, "Операция задержана"); break;
        case ETXTBSY        : msg = strcat(msg, "Текстовый файл занят"); break;
        case EWOULDBLOCK    : msg = strcat(msg, "Блокирующая операция"); break;
        case EXDEV          : msg = strcat(msg, "Неопределённая связь"); break;
        default             : msg = strcat(msg, "Неизвестная ошибка");
    }
    log_print(msg);
}