/**
 *	 \file compress.c
 *	 \brief Implémentation de la (dé)compression d’images
 */

#include "compress.h"

/**
 *	\var uint32_t tolerance
 *	\brief Color tolerance
 *
 *	Cette variable est la valeur du pourcentage de tolérance couleur lors de la
 *	création de nouvelles zones. Cette variable contient une valeur située entre
 *	0 et 100 inclus.
 */
int32_t tolerance;

/**
 *	Cette fonction permet d’évaluer si le pixel passé en argument est éligible à
 *	la zone passée également en argument. Si la \ref tolerance a pour valeur 0,
 *	alors les couleurs doivent être strictements identiques. Sinon, leur
 *	différence doit être inférieure à la tolérance de couleur.
 *
 *	\param[in] t_pixel Pointeur vers le pixel dont l’éligibilité est testée
 *	\param[in] t_zone Zone à laquelle le pixel est éligible ou non
 *	\return Valeur booléenne, `1` si le pixel est éligible, `0` sinon
 */
int32_t sameColor(Pixel *t_pixel, Zone *t_zone) {
  int diff_red, diff_green, diff_blue;
  if (tolerance == 0) {
    return (t_pixel->red == t_zone->red && t_pixel->green == t_zone->green &&
            t_pixel->blue == t_zone->blue)
      ? 1
      : 0;
  }
  diff_red = (abs((int32_t)t_zone->red - (int32_t)t_pixel->red) * 100) / 255;
  diff_green =
    (abs((int32_t)t_zone->green - (int32_t)t_pixel->green) * 100) / 255;
  diff_blue = (abs((int32_t)t_zone->blue - (int32_t)t_pixel->blue) * 100) / 255;
  return ((diff_red + diff_green + diff_blue) / 3) <= tolerance;
}

/**
 *	Ajoute un pixel à la zone passé en argument si le pixel à l’index passé en
 *	argument est éligible à la zone. Si un pixel n’a pas encore été visité, cela
 *	veut dire également qu’il ne fait partie d’aucun segment, il sera donc
 *	ajouté à un nouveau segment auquel seront rajoutés tous les pixels connexes
 *	éligibles à la zone. Ensuite, le segment est ajouté à la zone, et la
 *	fonction actuelle est appelée sur tous les pixels supérieurs et inférieurs
 *	aux pixels du segment.
 *
 *	\param[in] t_img Image contenant les pixels explorés
 *	\param[in] t_idx Index du pixel actuel dans l’image `t_img`
 *	\param[out] t_zone Zone à laquelle sera potentiellement ajouté le pixel
 */
void addPixelToSelectedZone(Image *t_img, int64_t t_idx, Zone *t_zone) {
  const size_t img_size = darraySize(t_img->pixels);
  Pixel *current_pixel;
  const uint32_t y = (uint32_t)(t_idx / t_img->sizeX);
  int64_t left_limit, right_limit;
  const int64_t xd_limit = (int64_t)t_img->sizeX * (y + 1);
  if (t_idx >= (int64_t)img_size || t_idx < 0) { /* Pixel in range? */
    return;
  }
  current_pixel = darrayGet(t_img->pixels, (size_t)t_idx);

  /* Pixel already visited or of the right color? */
  if (current_pixel->visited || !sameColor(current_pixel, t_zone)) {
    return;
  }
  (*current_pixel).visited = 1;

  /* right limit */
  for (right_limit = t_idx; right_limit < xd_limit; ++right_limit) {
    current_pixel = darrayGet(t_img->pixels, (size_t)right_limit);
    if (!sameColor(current_pixel, t_zone)) {
      break;
    }
    current_pixel->visited = 1;
  }
  /* left limit */
  for (left_limit = t_idx; left_limit - (y - 1) * (int64_t)t_img->sizeX >= 0;
       --left_limit) {
    current_pixel = darrayGet(t_img->pixels, (size_t)left_limit);
    if (current_pixel->visited || !sameColor(current_pixel, t_zone)) {
      break;
    }
    (*current_pixel).visited = 1;
  }

  darrayPushBack(t_zone->segments,
                 newSegment((uint32_t)right_limit, (uint32_t)left_limit));
  /* for each pixel of the segment, test the pixel above */
  for (; left_limit <= right_limit; ++left_limit) {
    addPixelToSelectedZone(t_img, t_idx + t_img->sizeX, t_zone);
  }
  /* for each pixel of the segment, test the pixel below */
  for (; left_limit <= right_limit; ++left_limit) {
    addPixelToSelectedZone(t_img, t_idx - t_img->sizeX, t_zone);
  }
}

/**
 *	Sélectionne la zone correspondant à la couleur du pixel. Si aucune zone
 *	existante ne correspond, une nouvelle est créée et est ajoutée à l'image.
 *	Chaque pixel est itéré, et ignoré si le pixel a déjà été visité auparavant.
 *
 *	\param[out] t_img L’image contenant les pixels à tester
 *	\param[in] t_idx Index du pixel à tester
 *	\param[out] t_zones Liste des zones de l’image
 */
void chooseZoneForPixel(Image *t_img, int64_t t_idx, darray *t_zones) {
  Zone *current_zone;
  Pixel *pixel;
  size_t i;
  pixel = darrayGet(t_img->pixels, (size_t)t_idx);
  if (pixel->visited) {
    return;
  }
  for (i = 0; i < darraySize(t_zones); ++i) {
    current_zone = darrayGet(t_zones, i);
    if (sameColor(pixel, current_zone)) {
      addPixelToSelectedZone(t_img, t_idx, current_zone);
      return;
    }
  }
  current_zone = newZone(pixel->red, pixel->green, pixel->blue);
  darrayPushBack(t_zones, current_zone);
  addPixelToSelectedZone(t_img, t_idx, current_zone);
}

/**
 *	Génère les zones de l’image en titérant chaque pixel de l’image.
 *
 *	\param t_img Image à convertir en zones
 *	\return Pointeur vers un \ref darray de structures \ref Zone
 */
darray *imgToZones(Image *t_img) {
  darray *zones;
  const size_t nb_pixels = darraySize(t_img->pixels);
  int64_t i;
  zones = darrayNew(sizeof(Zone));
  for (i = 0; i < (int64_t)nb_pixels; ++i) {
    chooseZoneForPixel(t_img, i, zones);
  }
  return zones;
}

/**
 *	Cette fonction écrit dans \p t_output la taille en `uint64_t` de la zone,
 *	c’est à dire le nombre de segment qu’elle contient, puis écrit
 *	individuellement chaque segment dans \p t_output.
 *
 *	\param[out] t_output Fichier de sortie
 *	\param[in] t_segments Segments à écrire dans \p t_output
 */
void write_segments(FILE *t_output, darray *t_segments) {
  uint64_t nb_segments, j;
  Segment *segment;
  nb_segments = darraySize(t_segments);
  fwrite(&nb_segments, sizeof(nb_segments), 1, t_output);
  for (j = 0; j < darraySize(t_segments); ++j) {
    segment = darrayGet(t_segments, j);
    fwrite(&segment->left_limit, sizeof(Segment), 1, t_output);
  }
}

/**
 *	Écrit la taille de l’image en abscisse et ordonnées, les deux sous forme de
 *	`uint64_t` puis le nombre de zones sous forme de `uint64_t`. Puis, pour
 *	chaque zone son code couleur composé de trois `uint8_t` successifs
 *	représentant ses couleurs rouge, vert et bleu sont écrit dans le fichier de
 *	sortie \p t_output. Après chaque écriture de zone, l’ensemble des segments
 *	de la zone est libéré de la mémoire. Une fois toutes les zones écrites dans
 *	le fichier de sortie, \p t_zones et libéré de la mémoire.
 *
 *	\param[in] t_img \ref Image contenant les dimensions du fichier d’origine
 *	\param[out] t_output Fichier où sont écrites les données compressées
 *	\param[in] t_zones Tableau des \ref Zone à écrire puis libérer
 */
void write_compressed_file(Image *t_img, FILE *t_output, darray *t_zones) {
  uint64_t i, nb_zones = darraySize(t_zones);
  Zone *current_zone;
  fwrite(&t_img->sizeX, sizeof(t_img->sizeX), 2, t_output);
  fwrite(&nb_zones, sizeof(nb_zones), 1, t_output);
  for (i = 0; i < darraySize(t_zones); ++i) {
    current_zone = darrayGet(t_zones, i);
    fwrite(&current_zone->red, sizeof(current_zone->red) * 3, 1, t_output);
    write_segments(t_output, current_zone->segments);
    darrayDelete(current_zone->segments);
  }
  darrayDelete(t_zones);
}

/**
 *	Convertit une image en zones puis écrit ces zones dans un fichier,
 *	compressant ainsi l'image passée en argument.
 *
 *	\param[in] t_input_file Nom/chemin du fichier `.ppm` d'entrée
 *	\param[in] t_output_file Nom/chemin du fichier `.su` de sortie
 *	\param[in] t_tolerance Pourcentage de tolérance de couleur
 */
void compress(const char *t_input_file, const char *t_output_file,
              int32_t t_tolerance) {
  Image *img;
  darray *zones;
  FILE *output_file;
  if (!t_output_file) {
    t_output_file = DEFAULT_COMPRESSED_NAME;
  }
  tolerance = t_tolerance;
  img = newImage();
  imageLoadPPM(t_input_file, img);
  output_file = get_file(t_output_file, "wb");
  zones = imgToZones(img);
  write_compressed_file(img, output_file, zones);
  deleteImage(img);
  fclose(output_file);
}
