#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define MATRIX_SIZE 12

typedef struct {
  double **a;
  double **b;
  double **r;
  int start_row;
  int end_row;
  int size;
} ThreadArgs;

void mul_seq(double **a, double **b, double **r, int size) {
  for (int i = 0; i < size; i++) {
    for (int j = 0; j < size; j++) {
      r[i][j] = 0;
      for (int k = 0; k < size; k++)
        r[i][j] += a[i][k] * b[k][j];
    }
  }
}

void *mul_par_worker(void *args) {
  ThreadArgs *t_args = (ThreadArgs *)args;
  double **a = t_args->a;
  double **b = t_args->b;
  double **r = t_args->r;
  int start_row = t_args->start_row;
  int end_row = t_args->end_row;
  int size = t_args->size;

  for (int i = start_row; i < end_row; i++) {
    for (int j = 0; j < size; j++) {
      r[i][j] = 0;
      for (int k = 0; k < size; k++)
        r[i][j] += a[i][k] * b[k][j];
    }
  }

  pthread_exit(NULL);
}

void mul_par(double **a, double **b, double **r, int size, int n) {
  pthread_t threads[n];
  ThreadArgs args[n];
  int rows_per_thread = size / n;
  int rem_rows = size % n;
  int curr_row = 0;

  for (int i = 0; i < n; i++) {
    args[i].a = a;
    args[i].b = b;
    args[i].r = r;
    args[i].start_row = curr_row;
    args[i].end_row = curr_row + rows_per_thread + (i < rem_rows ? 1 : 0);
    args[i].size = size;

    if (pthread_create(&threads[i], NULL, mul_par_worker, &args[i]) != 0) {
      perror("Can't creat thread");
      exit(EXIT_FAILURE);
    }
    curr_row = args[i].end_row;
  }

  for (int i = 0; i < n; i++) {
    if (pthread_join(threads[i], NULL) != 0) {
      perror("Can't join thread");
      exit(EXIT_FAILURE);
    }
  }
}

double **alloc_matrix(int size) {
  double **matrix = (double **)malloc(size * sizeof(double *));
  for (int i = 0; i < size; i++)
    matrix[i] = (double *)malloc(size * sizeof(double));

  return matrix;
}

void free_matrix(double **matrix, int size) {
  for (int i = 0; i < size; i++)
    free(matrix[i]);

  free(matrix);
}

void init_matrix(double **matrix, int size) {
  for (int i = 0; i < size; i++)
    for (int j = 0; j < size; j++)
      matrix[i][j] = (double)rand() / RAND_MAX * 10.0;
}

typedef void (*mult_func_seq)(double **, double **, double **, int);
typedef void (*mult_func_par)(double **, double **, double **, int, int);

double measure_time_seq(mult_func_seq func, double **a, double **b, double **r,
                        int size) {
  struct timespec start, end;
  clock_gettime(CLOCK_MONOTONIC, &start);

  func(a, b, r, size);

  clock_gettime(CLOCK_MONOTONIC, &end);
  return (end.tv_sec - start.tv_sec) + (end.tv_nsec - start.tv_nsec) / 1e9;
}

double measure_time_par(mult_func_par func, double **a, double **b, double **r,
                        int size, int n) {
  struct timespec start, end;
  clock_gettime(CLOCK_MONOTONIC, &start);

  func(a, b, r, size, n);

  clock_gettime(CLOCK_MONOTONIC, &end);
  return (end.tv_sec - start.tv_sec) + (end.tv_nsec - start.tv_nsec) / 1e9;
}

int main() {
  double **a = alloc_matrix(MATRIX_SIZE);
  double **b = alloc_matrix(MATRIX_SIZE);
  double **r_seq = alloc_matrix(MATRIX_SIZE);
  double **r_par = alloc_matrix(MATRIX_SIZE);

  srand(time(NULL));
  init_matrix(a, MATRIX_SIZE);
  init_matrix(b, MATRIX_SIZE);

  printf("performing sequentional multiplication...\n");
  double sequential_time = measure_time_seq(mul_seq, a, b, r_seq, MATRIX_SIZE);
  printf("time: %f secinds\n", sequential_time);

  int num_threads = 8;
  printf("\nperforming parrallel multiplication (%d threads)...\n",
         num_threads);
  double parallel_time =
      measure_time_par(mul_par, a, b, r_par, MATRIX_SIZE, num_threads);
  printf("time: %f seconds\n", parallel_time);

  free_matrix(a, MATRIX_SIZE);
  free_matrix(b, MATRIX_SIZE);
  free_matrix(r_seq, MATRIX_SIZE);
  free_matrix(r_par, MATRIX_SIZE);

  return 0;
}
