/* NOTA: la funzionalita' di deduplicazione dello scambio dei link e'
 * implementata, tuttavia, richiede che il parametro in input venga passato
 * senza la '/' in fondo.
 */
#include <dirent.h>
#include <fcntl.h>
#include <linux/limits.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/sendfile.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

#define INITIAL 4

#define err(e)                                                                 \
  do {                                                                         \
    perror(e);                                                                 \
    exit(EXIT_FAILURE);                                                        \
  } while (0)

char *mem;
size_t allo = INITIAL, n = 0;

void swap(const char *in, bool graceful) {
  int realfd, destfd;
  struct stat s;
  char real[PATH_MAX];

  if (lstat(in, &s) == -1)
    err("lstat");

  if (!S_ISLNK(s.st_mode)) {
    if (graceful)
      return;
    fprintf(stderr, "%s is not a symlink\n", in);
    exit(EXIT_FAILURE);
  }

  printf("swapping %s\n", in);
  if (realpath(in, real) == NULL)
    err("realpath");

  if ((realfd = open(real, O_RDONLY)) == -1)
    err("open/real");

  if (stat(real, &s) == -1)
    err("stat");

  if (unlink(in) == -1)
    err("unlink");

  if ((destfd = open(in, O_WRONLY | O_CREAT, 0666)) == -1)
    err("open/dest");

  if (sendfile(destfd, realfd, 0, s.st_size) == -1)
    err("sendfile");

  if (unlink(real) == -1)
    err("unlink");

  if (symlink(in, real) == -1)
    err("symlink");

  if (n >= allo) {
    allo *= 2;
    if ((mem = realloc(mem, PATH_MAX * allo)) == NULL)
      perror("realloc");
  }
  strcpy((char *)(mem + (n * PATH_MAX)), real);
  ++n;

  close(realfd);
  close(destfd);
}

void dir(const char *in) {
  DIR *d;
  struct dirent *e;
  char merged[PATH_MAX];

  if ((d = opendir(in)) == NULL)
    err("opendir");

  if ((mem = (char *)malloc(PATH_MAX * allo)) == NULL)
    perror("malloc");
  while ((e = readdir(d)) != NULL) {
    if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, ".."))
      continue;

    snprintf(merged, PATH_MAX, "%s/%s", in, e->d_name);
    bool already = false;
    for (size_t i = 0; i < n && !already; ++i) {
      char *ele = mem + (i * PATH_MAX);
      if (!strcmp(ele, merged))
        already = true;
    }

    if (!already)
      swap(merged, true);
  }
  free(mem);
  closedir(d);
}

int main(int argc, char *argv[]) {
  struct stat s;
  char cwd[PATH_MAX], in[PATH_MAX];
  if (argc != 2) {
    printf("Usage: %s [dir]", argv[0]);
    exit(EXIT_FAILURE);
  }

  if (getcwd(cwd, PATH_MAX) == NULL)
    err("getcwd");

  /* Absolute paths are required here as the new link will have to point to the
   * approriate file even if they aren't relative to one another. Example:
   * A/B/file
   * C/D/link -> ../../A/B/file
   *
   * Inside of C/ I run invsymlink and therefore the argv[1] will be "D/link".
   * This means that unless the path is conveted to absolute the new link
   * willdon point to "D/link" which cannot be resolved relative to A/B/
   * */
  snprintf(in, PATH_MAX, "%s/%s", cwd, argv[1]);

  if (lstat(in, &s) == -1)
    err("lstat");

  if (stat(in, &s) == -1)
    err("stat");

  if (S_ISDIR(s.st_mode))
    dir(in);
  else
    swap(in, false);

  return 0;
}