/** @file */
#include "disk.h"
#include "env.h"
#include "struct.h"
#include <linux/limits.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

int init_inode_in_disk(disk *d, int inode_index, int filetype,
                       unsigned short perms, int owned_bloc) {
  if (d->inodes[inode_index].filetype != TYPE_NULL)
    return -1;

  d->owned_blocs[owned_bloc] = 1;

  d->inodes[inode_index].filetype = filetype;
  d->inodes[inode_index].perms = perms;

  for (int i = 0; i < MAX_BLOCS; i++) {
    d->inodes[inode_index].blocs[i] = -1;
  }
  d->inodes[inode_index].blocs[0] = owned_bloc;

  return 0;
}

int find_free_bloc(disk *d) {
  for (int i = 0; i < MAX_BLOCS; i++) {
    if (d->owned_blocs[i] == 0) {
      d->owned_blocs[i] = 1;
      return i;
    }
  }
  return -1;
}

int find_offset_in_parent_bloc(bloc *parent_bloc) {
  int entry_size = MAX_INODE_NAME + sizeof(int);
  int max_entries = MAX_BYTES_PER_BLOC / entry_size;

  for (int i = 0; i < max_entries; i++) {
    int current_offset = i * entry_size;
    // Si le premier caractère est '\0', la case est libre
    if (parent_bloc->datas[current_offset] == '\0') {
      return current_offset;
    }
  }
  return -1;
}

int is_dir_empty(disk *d, int bloc_index) {
  for (int i = 0; i < MAX_BYTES_PER_BLOC; i += MAX_INODE_NAME + sizeof(int)) {
    char name[MAX_INODE_NAME];
    strncpy(name, &d->blocs[bloc_index].datas[i], MAX_INODE_NAME);
    if (strcmp(name, "..") != 0 && strcmp(name, ".") != 0 && name[0] != 0) {
      return 0;
    }
  }
  return 1;
}

int is_file_empty(disk *d, int bloc_index) {
  for (int i = 0; i < MAX_BYTES_PER_BLOC; i++) {
    if (d->blocs[bloc_index].datas[i] != 0) {
      return 0;
    }
  }
  return 1;
}

int allocate_node(disk *disk, inode *parent, char *name, char type) {
  int inode_index = 0;
  while (inode_index < MAX_INODE &&
         disk->inodes[inode_index].filetype != TYPE_NULL) {
    inode_index++;
  }
  if (inode_index >= MAX_INODE) {
    dprintf(STDERR_FILENO, "No free inodes\n");
    return -1;
  }

  int new_bloc_idx = find_free_bloc(disk);
  if (new_bloc_idx == -1) {
    dprintf(STDERR_FILENO, "Disk Full\n");
    return -1;
  }

  int p_bloc_idx = parent->blocs[0];
  int offset = find_offset_in_parent_bloc(&disk->blocs[p_bloc_idx]);
  if (offset == -1) {
    dprintf(STDERR_FILENO, "Parent directory full\n");
    return -1;
  }

  strncpy(&disk->blocs[p_bloc_idx].datas[offset], name, MAX_INODE_NAME);
  memcpy(&disk->blocs[p_bloc_idx].datas[offset + MAX_INODE_NAME], &inode_index,
         sizeof(int));

  init_inode_in_disk(disk, inode_index, type, 0b111111111, new_bloc_idx);

  return inode_index;
}

int remove_inode(disk *disk, inode *parent, inode *inode_to_remove,
                 int inode_to_remove_idx) {

  if (parent == inode_to_remove) {
    dprintf(STDERR_FILENO, "rm: can't delete root\n");
    return -1;
  }
  int parent_bloc_idx = parent->blocs[0];
  if (parent_bloc_idx == -1) {
    return -1;
  }

  int found = 0;

  // on efface la référence de l'inode dans son parent
  for (int i = 0; i < MAX_BYTES_PER_BLOC && !found;
       i += MAX_INODE_NAME + sizeof(int)) {
    if (*(int *)&disk->blocs[parent_bloc_idx].datas[i + MAX_INODE_NAME] ==
        inode_to_remove_idx) {
      memset(&disk->blocs[parent_bloc_idx].datas[i], 0,
             MAX_INODE_NAME + sizeof(int));
      found = 1;
    }
  }

  // on efface tout le contenu de l'inode
  int bloc_inode_idx = inode_to_remove->blocs[0];
  memset(&disk->blocs[bloc_inode_idx].datas[0], 0, MAX_BYTES_PER_BLOC);

  disk->owned_blocs[inode_to_remove->blocs[0]] = 0;
  inode_to_remove->blocs[0] = -1;
  inode_to_remove->perms = 0b000000000;
  inode_to_remove->filetype = TYPE_NULL;

  return 1;
}

int do_rmdir(disk *disk, char *filepath) {
  char *parent_path = NULL;
  char *dirname = NULL;

  if (get_dirname_and_parent_path_by_absolute_path(filepath, &parent_path,
                                                   &dirname) == -1) {
    dprintf(STDERR_FILENO, "rmdir: path error\n");
    return -1;
  }

  int parent_index = 0;
  char *token = strtok(parent_path, "/");

  while (token != NULL) {
    parent_index = find_dir_inode_by_name(token, parent_index, disk);

    if (parent_index == -1) {
      dprintf(STDERR_FILENO, "rmdir: path '%s' does not exist\n", token);
      free(parent_path);
      free(dirname);
      return -1;
    }

    if (disk->inodes[parent_index].filetype != TYPE_DIRECTORY) {
      dprintf(STDERR_FILENO, "rmdir: '%s' is not a directory\n", token);
      free(parent_path);
      free(dirname);
      return -1;
    }

    token = strtok(NULL, "/");
  }

  int file_index = find_dir_inode_by_name(dirname, parent_index, disk);

  if (file_index == -1) {
    dprintf(STDERR_FILENO, "rmdir: can't delete '%s': no such directory\n",
            dirname);
    free(parent_path);
    free(dirname);
    return -1;
  }

  if (disk->inodes[file_index].filetype != TYPE_DIRECTORY) {
    dprintf(STDERR_FILENO, "rmdir: can't delete '%s': not a directory\n",
            dirname);
    free(parent_path);
    free(dirname);
    return -1;
  }

  if (!(is_dir_empty(disk, disk->inodes[file_index].blocs[0]) == 1)) {
    dprintf(STDERR_FILENO, "rmdir: directory not empty\n");
    return -1;
  }

  int res = remove_inode(disk, &disk->inodes[parent_index],
                         &disk->inodes[file_index], file_index);

  if (res == 1) {
  } else {
    dprintf(STDERR_FILENO, "rmdir: can't delete because of a fatal error\n");
  }

  free(parent_path);
  free(dirname);

  return (res == 1) ? 0 : -1;
}

int do_rm(disk *disk, char *filepath) {
  char *parent_path = NULL;
  char *dirname = NULL;

  if (get_dirname_and_parent_path_by_absolute_path(filepath, &parent_path,
                                                   &dirname) == -1) {
    dprintf(STDERR_FILENO, "rm: erreur de chemin\n");
    return -1;
  }

  int parent_index = 0;
  char *token = strtok(parent_path, "/");

  while (token != NULL) {
    parent_index = find_dir_inode_by_name(token, parent_index, disk);

    if (parent_index == -1) {
      dprintf(STDERR_FILENO, "rm: path '%s' does not exist\n", token);
      free(parent_path);
      free(dirname);
      return -1;
    }

    if (disk->inodes[parent_index].filetype != TYPE_DIRECTORY) {
      dprintf(STDERR_FILENO, "rm: '%s' is not a directory\n", token);
      free(parent_path);
      free(dirname);
      return -1;
    }

    token = strtok(NULL, "/");
  }

  int file_index = find_dir_inode_by_name(dirname, parent_index, disk);

  if (file_index == -1) {
    dprintf(STDERR_FILENO, "rm: can't delete '%s': no such file\n", dirname);
    free(parent_path);
    free(dirname);
    return -1;
  }

  if (disk->inodes[file_index].filetype != TYPE_FILE &&
      disk->inodes[file_index].filetype != TYPE_SYMBOLIC_LINK) {
    dprintf(STDERR_FILENO, "rm: can't delete '%s': not a file\n", dirname);
    free(parent_path);
    free(dirname);
    return -1;
  }

  int res = remove_inode(disk, &disk->inodes[parent_index],
                         &disk->inodes[file_index], file_index);

  if (res == 1) {
  } else {
    dprintf(STDERR_FILENO, "rm: can't delete because of a fatal error\n");
  }

  free(parent_path);
  free(dirname);

  return (res == 1) ? 0 : -1;
}

int create_file(disk *disk, inode *parent, char *filename) {
  return allocate_node(disk, parent, filename, TYPE_FILE);
}

int create_directory(disk *disk, inode *parent, char *dirname) {
  int new_idx = allocate_node(disk, parent, dirname, TYPE_DIRECTORY);

  if (new_idx == -1)
    return -1;

  int bloc_idx = disk->inodes[new_idx].blocs[0];
  int entry_size = MAX_INODE_NAME + sizeof(int);
  int p_idx = (int)(parent - disk->inodes);

  strncpy(&disk->blocs[bloc_idx].datas[0], ".", MAX_INODE_NAME);
  memcpy(&disk->blocs[bloc_idx].datas[MAX_INODE_NAME], &new_idx, sizeof(int));

  strncpy(&disk->blocs[bloc_idx].datas[entry_size], "..", MAX_INODE_NAME);
  memcpy(&disk->blocs[bloc_idx].datas[entry_size + MAX_INODE_NAME], &p_idx,
         sizeof(int));

  return new_idx;
}

int do_touch(disk *d, char *filepath) {
  char *parent_path;
  char *dirname;

  if (get_name_and_parent_path_by_absolute_path(filepath, &parent_path,
                                                &dirname) == -1) {
    dprintf(STDERR_FILENO, "touch: erreur jsp\n");
    return -1;
  }

  char *token = strtok(parent_path, "/");
  int dir_index = 0;

  while (token != NULL) {

    dir_index = find_dir_inode_by_name(token, dir_index, d);

    if (dir_index == -1) {
      dprintf(STDERR_FILENO, "touch: %s file does not exist\n", token);
      return -1;
    }

    if (d->inodes[dir_index].filetype != TYPE_DIRECTORY) {
      dprintf(STDERR_FILENO, "touch: %s is not a directory\n", token);
      return -1;
    }

    token = strtok(NULL, "/");
  }

  create_file(d, &d->inodes[dir_index], dirname);
  return 0;
}

int do_mkdir(disk *d, char *dirpath) {
  char *parent_path;
  char *dirname;

  if (get_name_and_parent_path_by_absolute_path(dirpath, &parent_path,
                                                &dirname) == -1) {
    dprintf(STDERR_FILENO, "mkdir: erreur jsp");
    return -1;
  }

  char *token = strtok(parent_path, "/");
  int dir_index = 0;

  while (token != NULL) {

    dir_index = find_dir_inode_by_name(token, dir_index, d);

    if (dir_index == -1) {
      dprintf(STDERR_FILENO, "mkdir: %s directory does not exist\n", token);
      return -1;
    }

    if (d->inodes[dir_index].filetype != TYPE_DIRECTORY) {
      dprintf(STDERR_FILENO, "mkdir: %s is not a directory\n", token);
      return -1;
    }

    token = strtok(NULL, "/");
  }

  create_directory(d, &d->inodes[dir_index], dirname);

  return 0;
}

void write_in_file(disk *d, int file_index, char *data /*, char mode*/) {
  inode file = d->inodes[file_index];
  if (file.filetype != TYPE_FILE) {
    dprintf(STDERR_FILENO, "Can only write in files\n");
    return;
  }

  // mode 'w' (on écrase tout ^^)
  // les blocs sont possédés par un seul inode donc c'est ok de tout écraser
  // TODO: regarder si l'inode est sur plusieurs blocs et écraser tous les blocs
  // possédé

  int bloc_index_to_overwrite = file.blocs[0];
  if (bloc_index_to_overwrite == -1) {
    dprintf(STDERR_FILENO, "This inode does not have any bloc\n");
    return;
  }

  strncpy(&d->blocs[bloc_index_to_overwrite].datas[0], data, strlen(data));
}

int get_number_of_free_blocs(disk *d) {
  int out = 0;
  for (int i = 0; i < MAX_BLOCS; i++) {
    if (d->owned_blocs[i] == 0) {
      out++;
    }
  }
  return out;
}

int get_number_of_inode_left(disk *d) {
  int out = 0;
  for (int i = 0; i < MAX_INODE; i++) {
    if (d->inodes[i].filetype == TYPE_NULL) {
      out++;
    }
  }
  return out;
}

int do_df(disk *d) {
  int free_blocs = get_number_of_free_blocs(d);
  int inodes_left = get_number_of_inode_left(d);
  float space_left = (float)(free_blocs * MAX_BYTES_PER_BLOC) / 1000;

  dprintf(STDOUT_FILENO,
          "  Free blocs : %i\n  Inode left : %i\n  Space left : %.2fKb\n",
          free_blocs, inodes_left, space_left);

  return 0;
}

int find_dir_inode_by_name(char *name, int dir_index, disk *d) {
  if (dir_index < 0 || dir_index >= MAX_INODE)
    return -1;

  int bloc = d->inodes[dir_index].blocs[0];

  for (int i = 0; i < MAX_BYTES_PER_BLOC; i += MAX_INODE_NAME + sizeof(int)) {
    char inode_name[MAX_INODE_NAME];
    strncpy(inode_name, &d->blocs[bloc].datas[i], MAX_INODE_NAME);
    if (strcmp(name, inode_name) == 0) {
      int inode_id;
      memcpy(&inode_id, &d->blocs[bloc].datas[i + MAX_INODE_NAME], sizeof(int));
      return inode_id;
    }
  }

  return -1;
}

int do_ls(disk *d, char *path) {

  char *path_to_process = malloc(1024);
  if (path_to_process == NULL) {
    return -1;
  }

  if (path == NULL) {
    path = strdup(get_env_value("PWD"));
  }

  format_path(path_to_process, path, 1024);

  int dir_index = 0;
  char *token = strtok(path_to_process, "/");

  while (token != NULL) {
    dir_index = find_dir_inode_by_name(token, dir_index, d);

    if (dir_index == -1) {
      dprintf(STDERR_FILENO, "ls: This directory does not exist\n");
      free(path_to_process);
      return -1;
    }

    if (d->inodes[dir_index].filetype != TYPE_DIRECTORY) {
      dprintf(STDERR_FILENO, "ls: '%s' is not a directory\n", token);
      free(path_to_process);
      return -1;
    }

    token = strtok(NULL, "/");
  }

  inode dir_to_list = d->inodes[dir_index];
  int bloc = dir_to_list.blocs[0];

  if (bloc == -1) {
    free(path_to_process);
    return 1;
  }

  for (int i = 0; i < MAX_BYTES_PER_BLOC; i += MAX_INODE_NAME + sizeof(int)) {
    if (d->blocs[bloc].datas[i] != '\0') {
      int inode_id;
      memcpy(&inode_id, &d->blocs[bloc].datas[i + MAX_INODE_NAME], sizeof(int));
      dprintf(STDOUT_FILENO, "%s%-16s %s%d\n",
              d->inodes[inode_id].filetype == TYPE_FILE ? COLOR_GREEN
                                                        : COLOR_BLUE,
              &d->blocs[bloc].datas[i], ESCAPE_COLOR, inode_id);
    }
  }

  free(path_to_process);
  return 1;
}

char *read_in_file(disk *d, int file_index) {
  inode file = d->inodes[file_index];
  if (file.filetype != TYPE_FILE) {
    dprintf(STDERR_FILENO, "Can only read in files\n");
    return NULL;
  }

  // TODO: chercher sur tous les blocs possédés
  int bloc = file.blocs[0];
  char *out;
  int c_index = 0;
  while (d->blocs[bloc].datas[c_index] != '\0' ||
         c_index < MAX_BYTES_PER_BLOC) {
    c_index++;
  }

  out = malloc(c_index * sizeof(char));
  if (out == NULL) {
    dprintf(STDERR_FILENO, "Error while allocating with malloc\n");
    exit(EXIT_FAILURE);
  }

  strncpy(out, d->blocs[bloc].datas, c_index);

  return out;
}

int do_cat(disk *disk, char *path_to_file) {

  if (path_to_file == NULL) {
    char buffer[4096];
    ssize_t bytes_read;

    while ((bytes_read = read(STDIN_FILENO, buffer, sizeof(buffer))) > 0) {
      write(STDOUT_FILENO, buffer, bytes_read);
    }
  }

  char full_path[1024] = {0};

  format_path(full_path, path_to_file, 1024);

  char *path_tmp = strdup(full_path);
  int inode_index = 0;
  char *token = strtok(path_tmp, "/");

  while (token != NULL) {
    inode_index = find_dir_inode_by_name(token, inode_index, disk);

    token = strtok(NULL, "/");
  }

  if (disk->inodes[inode_index].filetype == TYPE_DIRECTORY) {
    dprintf(STDERR_FILENO, "cat: '%s' is a directory\n", full_path);
    return -1;
  }

  dprintf(STDOUT_FILENO, "%s\n", read_in_file(disk, inode_index));

  return 1;
}

/**
 * @brief Créer un fichier "disk" de taille MAX_BYTES_PER_BLOC * MAX_BLOCS,
 * renvoi ce disque
 * @return Le disque créé
 */
disk create_disk() {
  FILE *fptr;
  fptr = fopen("disk", "wb");

  disk d;
  for (int i = 0; i < MAX_INODE; i++) {
    d.inodes[i].filetype = TYPE_NULL;
    d.inodes[i].perms = 0b000000000;
    for (int j = 0; j < MAX_BLOCS; j++) {
      d.inodes[i].blocs[j] = -1;
    }
  }

  for (int i = 0; i < MAX_BLOCS; i++) {
    d.owned_blocs[i] = 0;
  }

  int root_inode = 0;

  // création de la racine '/'
  d.inodes[root_inode].filetype = TYPE_DIRECTORY;
  d.inodes[root_inode].perms = 0b111111111; // tlm fait ce qu'il veut
  d.inodes[root_inode].blocs[0] = 0;        // utilise le bloc 0
  d.owned_blocs[root_inode] = 1;            // le bloc 1 est maintenant occupé

  // création de '.' dans '/'
  strncpy(&d.blocs[0].datas[0], ".", MAX_INODE_NAME);
  memcpy(&d.blocs[0].datas[MAX_INODE_NAME], &root_inode, sizeof(int));

  // création de '..' dans '/'
  strncpy(&d.blocs[0].datas[MAX_INODE_NAME + sizeof(int)], "..",
          MAX_INODE_NAME);
  memcpy(&d.blocs[0].datas[2 * (MAX_INODE_NAME) + sizeof(int)], &root_inode,
         sizeof(int));

  int test_file_inode = create_file(&d, &d.inodes[0], "test");
  write_in_file(&d, test_file_inode, "coucou petit\ntest!!");

  int n = fwrite(&d, sizeof(disk), 1, fptr);
  if (n == 1) {
    dprintf(STDOUT_FILENO, "Disk of size %.2fKb created successfully.\n",
            (float)(MAX_BYTES_PER_BLOC * MAX_BLOCS) / 1000);
  }

  fclose(fptr);
  return d;
}

/**
 * @brief Si un fichier filename existe, alors on le lit et le stocke en mémoire
 * @param char *filename Le nom du fichier à lire
 * @return Le disque lu
 */
disk open_disk(char *filename) {
  disk d;
  FILE *fptr = fopen(filename, "r");
  if (fptr == NULL)
    exit(EXIT_FAILURE);
  fread(&d, sizeof(d), 1, fptr);
  fclose(fptr);
  return d;
}

void persist_on_disk(disk *d) {
  FILE *fptr;
  fptr = fopen("disk", "wb");

  int n = fwrite(d, sizeof(disk), 1, fptr);
  if (n != 1) {
    dprintf(STDERR_FILENO, "Failed to persist datas on disk.\n");
  }
}