#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <strings.h>
#include <stdarg.h>

#include "cell.h"
#include "utils.h"
#include "plugin_utils.h"

struct cell_group {
    /* Nombre total de cellules : */
    unsigned long len;
    /* Tableau des cellules : */
    struct cell **tab;
};

struct cell_groups {
    /* Nombre total de groupes : */
    unsigned long len;
    /* Tableau des groupes : */
    struct cell_group *tab;
};

void liberer_groupes(struct cell_groups **ptab_groups)
{
    struct cell_groups *ptab = *ptab_groups;
    unsigned long i;
    for (i = 0; i < ptab->len; i++) {
        free(ptab->tab[i].tab);
    }
    free(ptab->tab);
    free(*ptab_groups);
    *ptab_groups = NULL;
}

void print_groupes(struct cell_groups *groups)
{
    unsigned long i, j;
    struct cell_group *pgroup;
    struct cell *c;

    if (groups == NULL) {
        printf("Pas encore initialisé\n");
        return;
    }
    printf("-------------------------------------\n");
    for (i = 0; i < groups->len; i++) {
        pgroup = &(groups->tab[i]);
        printf("Groupe %lu : %lu element(s) :\n", i, pgroup->len);
        for (j = 0; j < pgroup->len; j++) {
            c = pgroup->tab[j];
            cell_print(c);
        }
    }
}

int groups_fusionner(struct cell_groups *groups,
                     unsigned long tag_groupe_a,
                     unsigned long tag_groupe_b,
                     char *buffer_erreur, int maxlen_erreur)
{
    unsigned long i;
    unsigned long indexe_a = (groups->len + 1);
    unsigned long indexe_b = (groups->len + 1);
    struct cell **tab_tmp;
    struct cell_group *psrc;
    struct cell_group *pdst;
    struct cell_group *pgroup_tmp;

    /* Rechercher les indexes */
    for (i = 0; i < groups->len; i++) {
        if (groups->tab[i].len > 0) {
            if (groups->tab[i].tab[0]->tag == tag_groupe_a) {
                indexe_a = i;
                if (indexe_b != (groups->len + 1)) {
                    break;
                }
            }
            if (groups->tab[i].tab[0]->tag == tag_groupe_b) {
                indexe_b = i;
                if (indexe_a != (groups->len + 1)) {
                    break;
                }
            }
        }
    }
    if ((indexe_a == (groups->len + 1)) && (indexe_b == (groups->len + 1))) {
        snprintf(buffer_erreur, maxlen_erreur,
                 "Demande de fusionner un groupe qui "
                 "a des cellules tag n.%lu\n"
                 "avec un groupe qui a des cellules "
                 "tag n.%lu. L'un des deux tags "
                 "est introuvable parmi les groupes.\n",
                 tag_groupe_a, tag_groupe_b);
        return -1;
    }
    pdst = &(groups->tab[indexe_a]);
    psrc = &(groups->tab[indexe_b]);
    /* Groupe les cell src = groupe cell dst : */
    for (i = 0; i < psrc->len; i++) {
        psrc->tab[i]->tag = tag_groupe_a;
    }
    /* Allocation dst = (taille dst + taille src) : */
    tab_tmp =
        (struct cell **) realloc(pdst->tab,
                                 (pdst->len + psrc->len) *
                                 sizeof(struct cell *));
    if (tab_tmp == NULL) {
        snprintf(buffer_erreur, sizeof(buffer_erreur) - 1,
                 "Erreur de ré-allocation d\'un groupe, "
                 "plus assez de mémoire");
        return -1;
    }
    pdst->tab = tab_tmp;

    /* Copie mémoire de la src à la fin de la dst : */
    memcpy(&(pdst->tab[pdst->len]), psrc->tab,
           psrc->len * sizeof(struct cell *));

    /* Libération de la source */
    free(psrc->tab);
    pdst->len += psrc->len;
    psrc->tab = groups->tab[groups->len - 1].tab;
    psrc->len = groups->tab[groups->len - 1].len;
    pgroup_tmp =
        (struct cell_group *) realloc(groups->tab,
                                      (groups->len -
                                       1) * sizeof(struct cell_group));
    if (pgroup_tmp == NULL) {
        snprintf(buffer_erreur, sizeof(buffer_erreur) - 1,
                 "Erreur de ré-allocation, plus assez de mémoire");
        return -1;
    }
    groups->tab = pgroup_tmp;
    groups->len--;
    return 0;
}

/* Ajouter une cellule dans un groupe
 */
int group_mettre_cell_dans_groupe(struct cell_groups *groups,
                                  struct cell *pcell,
                                  unsigned long no_groupe_dst,
                                  char *buffer_erreur, int maxlen_erreur)
{
    struct cell_group *pgroup;
    struct cell **ppcell_tmp;
/*
    printf("group_mettre_cell_dans_groupe()\n");
    printf("AVANT\n");
    print_groupes(groups);
*/
    if (no_groupe_dst > groups->len) {
        snprintf(buffer_erreur, maxlen_erreur,
                 "Demande d'ajouter la cellule no %lu\n"
                 "dans le groupe no %lu, alors\n"
                 "qu'il n'y a que %lu groupe(s).\n"
                 "(La demande doit être comprise "
                 "entre 1 et %lu).\n",
                 pcell->no, no_groupe_dst, groups->len, groups->len);
        return -1;
    }
    /* Réallouer la taille du groupe : */
    pgroup = &(groups->tab[no_groupe_dst - 1]);
    ppcell_tmp =
        (struct cell **) realloc(pgroup->tab,
                                 (pgroup->len +
                                  1) * sizeof(struct cell *));
    if (ppcell_tmp == NULL) {
        snprintf(buffer_erreur, sizeof(buffer_erreur) - 1,
                 "Erreur d'allocation d\'un groupe, "
                 "plus assez de mémoire");
        return -1;
    }
    pgroup->tab = ppcell_tmp;
    /* Y ajouter la cellule */
    pgroup->tab[pgroup->len] = pcell;
    pgroup->len++;
    pcell->tag = no_groupe_dst;
/*
    printf("APRES\n");
    print_groupes(groups);
*/
    return 0;
}

/* Créer un groupe qui contient les cellules
 * passées en paramètre (nombre de paramètres
 * variables = autant de cellules qu'on veut)
 * (!) *Toujours* dernier param = NULL
 */
int group_creer(struct cell_groups **pgroups,
                char *buffer_erreur,
                int maxlen_erreur, struct cell *pcell, ...)
{
    va_list ap;
    struct cell *c = pcell;
    struct cell_group *pgroup_tmp;

    if (*pgroups == NULL) {
        /* Allouer le conteneur de groupes : */
        *pgroups = calloc(1, sizeof(struct cell_groups));
        if (*pgroups == NULL) {
            snprintf(buffer_erreur, sizeof(buffer_erreur) - 1,
                     "Erreur d'allocation d\'un groupe, "
                     "plus assez de mémoire");
            return -1;
        }
        (*pgroups)->tab = NULL;
        (*pgroups)->len = 0;
    }

    pgroup_tmp = NULL;
    va_start(ap, pcell);
    while (c) {
        /* Ajouter l'élément c */
        /* Créer un groupe supplémentaire : */
        if (pgroup_tmp == NULL) {
            pgroup_tmp =
                realloc((*pgroups)->tab,
                        ((*pgroups)->len + 1) * sizeof(struct cell_group));
            if (pgroup_tmp == NULL) {
                snprintf(buffer_erreur, sizeof(buffer_erreur) - 1,
                         "Erreur d'allocation d\'un groupe, "
                         "plus assez de mémoire");
                return -1;
            }
            (*pgroups)->len++;
            (*pgroups)->tab = pgroup_tmp;
            /* Faire pointer sur le dernier groupe */
            pgroup_tmp = &((*pgroups)->tab[(*pgroups)->len - 1]);
            bzero(pgroup_tmp, sizeof(struct cell_group));
        }

        /* Conteneur des groupes ok = ajouter dans les groupes */
        if (group_mettre_cell_dans_groupe
            (*pgroups, c, (*pgroups)->len, buffer_erreur, maxlen_erreur)) {
            return -1;
        }
        c = va_arg(ap, struct cell *);
    }
    va_end(ap);
    return 0;
}

int plugin_algo_groupes(unsigned long total_cellules,
                        unsigned long total_liens,
                        struct cell *tabcell,
                        char *params,
                        unsigned long nb_option_force_link,
                        struct cell_option_force_link
                        *tab_option_force_link,
                        char *buffer_erreur, int maxlen_erreur)
{
    /* retour = ok par défaut : */
    int ret = 0;
    /* la gestion des groupes */
    struct cell_groups *tab_groups = NULL;
    struct cell_group *pgroup;
    struct cell **tab_to_do = NULL;

    /* les variables 'XXX_tmp' sont utilisées pour les realloc */
    struct cell_option_force_link *c_fl;
    struct cell *pcell;
    struct cell *plink;
    unsigned long nb_cells_to_do;
    unsigned long i, j, k, m;
    long taille_x = -1;
    long taille_y = -1;
    long taille_z = -1;

    /* le poids des tags des liens (pour influencer
       le tirage aléatoire) :
     */
#define MAX_POIDS (unsigned long)50
    unsigned long nb_poids = 0;
    unsigned long poids[MAX_POIDS];

    char *paramtmp = params;
    /* pour le calcul du rand() : */
    unsigned long poids_rand_max;
    long poids_rand;

    if (!params) {
        snprintf(buffer_erreur, maxlen_erreur,
                 "Il faut obligatoirement des paramètres "
                 "de poids pour l'algorithme pondéré.\n");
        return -1;
    }
    if (total_liens < 1) {
        snprintf(buffer_erreur, maxlen_erreur,
                 "Le nombre de liens < 1. "
                 "Impossible de créer quoi que ce soit.\n");
        return -1;
    }
    ret =
        recherche_valeurs(&paramtmp, &taille_x, &taille_y, &taille_z,
                          buffer_erreur, maxlen_erreur);
    if (!paramtmp) {
        snprintf(buffer_erreur, maxlen_erreur,
                 "Vous avez défini la largeur, la longueur\n"
                 "et la hauteur mais c'est insuffisant.\n"
                 "Il faut obligatoirement des paramètres\n"
                 "de poids pour l'algorithme pondéré.\n");
        return -1;
    }
    while ((ret = decode_assoc(&paramtmp, &i, &poids[nb_poids])) == 0) {
        /* ok on a ajouté un param => tout de suite incrémenter : */
        nb_poids++;
        if (paramtmp == NULL)
            break;
        if (i != nb_poids) {
            snprintf(buffer_erreur, maxlen_erreur,
                     "Erreur : les associations "
                     "en paramètre de type 'xx=yy' doivent "
                     "se suivre de manière incrémentale "
                     "en partant de 1.\n"
                     "Exemple : 1=25 2=50 3=12 4=10\n"
                     "Ce n'est pas le cas, le 'xx' de 'xx=yy' "
                     "qui pose problème est le numéro %lu.\n"
                     "Il a été trouvé en position %lu donc 'xx' aurait dû "
                     "avoir la valeur %lu.\n"
                     "(Il aurait fallu lire quelque "
                     "chose du genre '%lu=yy').\n",
                     i, nb_poids, nb_poids, nb_poids);
            ret = -1;
        }
        if (nb_poids > MAX_POIDS) {
            snprintf(buffer_erreur, maxlen_erreur,
                     "Erreur : le nombre max. "
                     "d'associations de type 'xx=yy'"
                     "en paramètres est dépassé.\n"
                     "Le maximum d'associations est %lu.\n", MAX_POIDS);
            ret = -1;
        }
    }
    if (ret) {
        snprintf(buffer_erreur, maxlen_erreur,
                 "Erreur : les paramètres doivent "
                 "être des associations explicitant le poids "
                 "des tags des liens du labyrinthe.\n"
                 "Ce n'est pas le cas.\n"
                 "Elle doivent être du type 'xx=yy' et "
                 "se suivre de manière incrémentale "
                 "en partant de 1.\n"
                 "Exemple : 1=25 2=50 3=12 4=10\n"
                 "Cela n'est pas le cas.\n");
        ret = -1;
    }
    /* Ok le poids des tags des liens est chargé */
    /* On vérifie que tous les liens ont un tag valide 
       (tag valide = un tag qui est dans le tableau des poids) */
    for (i = 0; i < total_cellules; i++) {
        pcell = &tabcell[i];
        for (j = 0; j < pcell->nb_links; j++) {
            k = pcell->links[j].tag;
            if ((k < 1) || (k > nb_poids)) {
                snprintf(buffer_erreur, maxlen_erreur,
                         "Il y a %lu associations de type 'xx=yy' "
                         "déclarées en paramètres pour les poids des liens.\n"
                         "La valeur pour chaque lien doit être comprise entre 1 et %lu.\n"
                         "Pourtant la valeur pour le lien %lu de la cellule %lu "
                         "vaut %lu.\n"
                         "Cette valeur n'est pas valide.\n",
                         nb_poids, nb_poids, j + 1, i + 1, k);
                return -1;
            }
        }
    }

/* Ok, l'analyse peut vraiment commencer */

    /* Le tag indique son n. de groupe : */
    for (i = 0; i < total_cellules; i++) {
        tabcell[i].tag = 0;
    }
/*
| (1) Pour chaque cellule qui a un lien forcé (a) :
|     {
|       si elle ne fait pas partie d'un groupe
|       - si sa cellule opposée (b) fait partie d'un groupe
|         => mettre (a) dans le groupe de (b)
|       - sinon créer un groupe avec (a) et (b) dedans
|     }
*/
    c_fl = tab_option_force_link;
    for (i = 0; i < nb_option_force_link; i++) {
        pcell = &tabcell[c_fl->id_cell];
        plink = pcell->links[c_fl->id_link].link;
        if (plink->tag) {
            if (group_mettre_cell_dans_groupe(tab_groups, pcell,
                                              plink->tag,
                                              buffer_erreur,
                                              maxlen_erreur)) {
                ret = -1;
                goto plugin_algo_groupes_fin;
            }
            cell_cell_deplacer_link_vers_paths(pcell, plink);
        } else {
            if (group_creer(&tab_groups,
                            buffer_erreur, maxlen_erreur, pcell, plink,
                            NULL)) {
                ret = -1;
                goto plugin_algo_groupes_fin;
            }
            cell_cell_deplacer_link_vers_walls(pcell, plink);
        }
        c_fl++;
    }

/*
|
| (2) Créer un tableau contenant toutes les cellules qui ne
|     sont *pas* dans un groupe
|
|     Tant qu'il y a des cellules dans ce tableau :
|     {
|         Prendre une cellule au hasard (a)
|         Tirer un lien (en fonction de la pondération)
|         - Si cellule opposée (b) fait partie d'un groupe
|           => mettre (a) dans le groupe de (b)
|         - sinon
|           => créer un groupe contenant (a) et (b)
|     }
|
*/
    /* Compter le nombre de cellules à faire */
    nb_cells_to_do = 0;
    for (i = 0; i < total_cellules; i++) {
        if (tabcell[i].tag == 0) {
            nb_cells_to_do++;
        }
    }

    /* Allouer le tableau qui contiendra les cellules à faire */
    tab_to_do = calloc(nb_cells_to_do, sizeof(struct cell *));
    if (tab_to_do == NULL) {
        snprintf(buffer_erreur, maxlen_erreur,
                 "Problème d'allocation.\n"
                 "Impossible d'allouer %lu "
                 "pointeurs vers des cellules.\n", nb_cells_to_do);
        ret = -1;
        goto plugin_algo_groupes_fin;
    }

    /* Mettre toutes les cellules à faire dans le tableau */
    j = 0;
    for (i = 0; i < total_cellules; i++) {
        if (tabcell[i].tag == 0) {
            tab_to_do[j++] = &tabcell[i];
        }
    }
    while (nb_cells_to_do) {
        /* Prendre une cellule au hasard : */
        i = rand() % nb_cells_to_do;
        pcell = tab_to_do[i];
        /* Si la cellule a déjà été faite, l'ignorer : */
        if (pcell->tag > 0) {
            nb_cells_to_do--;
            tab_to_do[i] = tab_to_do[nb_cells_to_do];
            continue;
        }
        /* Tirer un lien au hasard en se servant des poids
         * Poids total = somme des poids de chaque lien :
         */
        poids_rand_max = 0;
        for (j = 0; j < pcell->nb_links; j++) {
            poids_rand_max += poids[pcell->links[j].tag - 1];
        }
        if (!poids_rand_max) {
            snprintf(buffer_erreur, maxlen_erreur,
                     "Erreur n.3 :\n"
                     "Forcer tirage :\n"
                     "poids_rand_max = 0 pour cellule %lu\n", i);
            cell_print(pcell);
            ret = -1;
            goto plugin_algo_groupes_fin;
        }
        poids_rand = rand() % poids_rand_max;
        m = poids_rand;
        j = 0;
        while (1) {
            k = poids[pcell->links[j].tag - 1];
            poids_rand -= k;
            if (poids_rand < 0) {
                break;
            }
            j++;
        }
        plink = pcell->links[j].link;

        /* - Si cellule link (b) fait partie d'un groupe
         *   => mettre (a) dans le groupe de (b)
         * - sinon
         *   => créer un groupe contenant (a) et (b)
         */
        if (plink->tag) {
            /* => mettre (a) dans le groupe de (b) : */
            if (group_mettre_cell_dans_groupe(tab_groups, pcell,
                                              plink->tag,
                                              buffer_erreur,
                                              maxlen_erreur)) {
                ret = -1;
                goto plugin_algo_groupes_fin;
            }
        } else {
            /* => créer un groupe contenant (a) et (b) :  */
            if (group_creer(&tab_groups,
                            buffer_erreur, maxlen_erreur, pcell, plink,
                            NULL)) {
                ret = -1;
                goto plugin_algo_groupes_fin;
            }
        }
        /* Dans les deux cas, relier les cellules : */
        k = 0;
        while (k < plink->nb_links) {
            if (plink->links[k].link == pcell) {
                break;
            }
            k++;
        }
        if (k == plink->nb_links) {
            printf("?? introuvable\n");
            cell_print(pcell);
            cell_print(plink);
            snprintf(buffer_erreur, maxlen_erreur,
                     "Erreur lors du déplacement\n");
            ret = -1;
            goto plugin_algo_groupes_fin;
        }
        cell_deplacer_link_vers_paths(pcell, j);
        cell_deplacer_link_vers_paths(plink, k);
        nb_cells_to_do--;
        tab_to_do[i] = tab_to_do[nb_cells_to_do];
    }
    free(tab_to_do);
    tab_to_do = NULL;

/*
|  Tous les groupes sont crées, il n'y a
|  plus aucune cellule n'ayant pas de groupe.
*/
    for (i = 0; i < total_cellules; i++) {
        if (tabcell[i].tag == 0) {
            snprintf(buffer_erreur, maxlen_erreur,
                     "Problème lors de la vérification que tout est ok...\n"
                     "Erreur sur la cellule %lu\n", i);
            ret = -1;
            goto plugin_algo_groupes_fin;
        }
    }

/*
| 
| (3) Fusion finale des groupes :
|
|     Tant que total_groupes > 1
|     {
|         Prendre un groupe au hasard
|         Pour chaque cellule (a) du groupe :
|         {
|             Pour les liens à faire :
|             - si cellule opposée fait partie
|               d'un groupe différent
|               => joindre les deux groupes
|               => refaire la boucle du début
|         }
|     }
*/
    while (tab_groups->len > 1) {
        i = rand() % tab_groups->len;
        pgroup = &(tab_groups->tab[i]);
        m = 0;
        /* Parcourir tout le groupe :
           - pour chaque cellule, si un lien
           pointe vers une cellule du même groupe,
           poser un mur : */
/*
        for (j = 0; j < pgroup->len; j++) {
            pcell = pgroup->tab[j];
            k = 0;
            while (k < pcell->nb_links) {
                plink = pcell->links[k].link;
                if (pcell->tag == plink->tag) {
                    cell_cell_deplacer_link_vers_walls(pcell, plink);
                    k = 0;
                } else {
                    k++;
                }
            }
        }
*/
        /* Parcourir le groupe à la recherche de
           la première cellule collée à un autre groupe : */
        for (j = 0; j < pgroup->len; j++) {
            pcell = pgroup->tab[j];
            for (k = 0; k < pcell->nb_links; k++) {
                plink = pcell->links[k].link;
                if (pcell->tag != plink->tag) {
                    cell_cell_deplacer_link_vers_paths(pcell, plink);
                    if (groups_fusionner(tab_groups,
                                         pcell->tag,
                                         plink->tag,
                                         buffer_erreur, maxlen_erreur)) {
                        ret = -1;
                        goto plugin_algo_groupes_fin;
                    }
                    m = 1;
                    break;
                }
            }
            if (m == 1) {
                break;
            }
        }
    }
    pgroup = tab_groups->tab;
    m = 0;
    /* Parcourir tout le groupe :
       - pour chaque cellule, si un lien
       pointe vers une cellule du même groupe,
       poser un mur : */
    for (j = 0; j < pgroup->len; j++) {
        pcell = pgroup->tab[j];
        k = 0;
        while (k < pcell->nb_links) {
            plink = pcell->links[k].link;
            if (pcell->tag == plink->tag) {
                cell_cell_deplacer_link_vers_walls(pcell, plink);
                k = 0;
            } else {
                k++;
            }
        }
    }
  plugin_algo_groupes_fin:
    /* Libérations */
    if (tab_to_do != NULL) {
        free(tab_to_do);
    }
    if (tab_groups != NULL) {
        liberer_groupes(&tab_groups);
    }
    return ret;
}
