#include <arpa/inet.h>
#include <ctype.h>
#include <inttypes.h>
#include <netdb.h>
#include <netinet/in.h>
#include <pthread.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>

#include "screen.h"

#define MAX_NICK_LEN 20
/* structure de liste chainée pour l'historique */
struct linked_list {
  char *line;
  struct linked_list *next;
};

/* ajout d'une ligne de texte à la liste.
  Attention, en tant que telle, elle n'est pas thread safe.
 */
static void append_line(struct linked_list **lst, char *s) {
  struct linked_list *nlist = malloc(sizeof(struct linked_list));
  nlist->next = *lst;
  nlist->line = s;
  *lst = nlist;
}

/* Les informations pour nos deux threads */
struct thread_data {
  int socket;    // socket vers le serveur
  char *nick;    // nick choisi
  FILE *input;   // descripteur de fichier vers le socket en lecture
  FILE *output;  // descripteur de fichier vers le socket en écriture
  struct linked_list *history;  // historique
  struct screen *screen;        //écran
  pthread_mutex_t lock;         // verrou pour l'accès exclusif à la structure
};

/* Ajout d'une ligne dans l'historique puis redessiner l'écran */
static void push_history(struct thread_data *data, char *line) {
  // attention à protéger l'accès notemment à la liste chainée.
  pthread_mutex_lock(&data->lock);
  append_line(&data->history, line);
  int ln, cl;
  screen_get_size(data->screen, &ln, &cl);
  ln -= 2;
  struct linked_list *lst = data->history;
  while (lst != NULL && ln > 0) {
    screen_print_line(data->screen, ln, lst->line);
    ln--;
    lst = lst->next;
  }
  pthread_mutex_unlock(&data->lock);
}
/* variables globales pour stocker les threads d'envoi et reception */
pthread_t send_thread;
pthread_t recv_thread;

/* thread d'envoi */
static void *send_thread_fun(void *arg) {
  struct thread_data *data = arg;
  char *line;
  for (;;) {
    line = screen_read_input(data->screen);
    if (strncmp("/QUIT", line, 5) == 0) {
      break;
    }
    push_history(data, line);
    fprintf(data->output, "TEXT:%s\n", line);
    fflush(data->output);
  }
  // sorti par /QUIT, on demande à l'autre thread de se terminer.
  pthread_cancel(recv_thread);
  return NULL;
}
/* thread de reception */
static void *recv_thread_fun(void *arg) {
  struct thread_data *data = arg;
  char *line = NULL;
  size_t line_len = 0;
  while (getline(&line, &line_len, data->input) >= 0) {
    if (strncmp("TEXT:", line, 5) == 0) {
      line[line_len - 1] = 0;        //écrase le '\n' final
      push_history(data, line + 5);  // saute "TEXT:"
    } else {
      break;
    }
  }
  // sortie sur message invalide on demande à l'autre thread de se terminer
  pthread_cancel(send_thread);
  return NULL;
}

static int is_valid_nick(const char *nick) {
  for (int i = 0; i <= MAX_NICK_LEN; i++) {
    switch (nick[i]) {
      case 0:
        return 1;
      case '_':
      case '[':
      case ']':
      case '-':
        break;
      default:
        if (!isalnum(nick[i])) return 0;
    }
  }
  return 0;
}

int main(int argc, char **argv) {
  int sockfd, rv;
  struct addrinfo hints, *servinfo, *p;
  struct thread_data *thread_data;
  char *line = NULL;
  char prompt[25];
  size_t line_len = 0;

  if (argc != 4) {
    fprintf(stderr, "Usage: %s <addr> <port> <nick>\n", argv[0]);
    exit(1);
  }

  if (!is_valid_nick(argv[3])) {
    fprintf(stderr, "Invalid nickname: %s\n", argv[3]);
    exit(1);
  }

  memset(&hints, 0, sizeof(hints));
  hints.ai_family = AF_UNSPEC;
  hints.ai_socktype = SOCK_STREAM;
  rv = getaddrinfo(argv[1], argv[2], &hints, &servinfo);

  if (rv != 0) {
    fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rv));
    exit(2);
  }

  for (p = servinfo; p != NULL; p = p->ai_next) {
    if ((sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) == -1) {
      perror("client: socket");
      continue;
    }
    if ((connect(sockfd, p->ai_addr, p->ai_addrlen)) == -1) {
      close(sockfd);
      perror("client: connect");
      continue;
    }
    break;
  }

  if (p == NULL) {
    fprintf(stderr, "server: failed to bind\n");
    exit(2);
  }

  thread_data = malloc(sizeof(struct thread_data));
  thread_data->socket = sockfd;
  thread_data->nick = argv[3];
  thread_data->input = fdopen(dup(sockfd), "rb");
  thread_data->output = fdopen(dup(sockfd), "wb");
  thread_data->history = NULL;
  fprintf(thread_data->output, "CONNECT:%s\n", thread_data->nick);
  fflush(thread_data->output);
  getline(&line, &line_len, thread_data->input);

  if (strncmp(line, "CONNECT:OK\n", 11) != 0) {
    fprintf(stderr, "server: incorrect answer");
    exit(2);
  }
  free(line);
  sprintf(prompt, "%s> ", argv[3]);
  thread_data->screen = screen_init(prompt);
  pthread_create(&recv_thread, NULL, recv_thread_fun, thread_data);
  pthread_create(&send_thread, NULL, send_thread_fun, thread_data);
  pthread_join(recv_thread, NULL);
  pthread_join(send_thread, NULL);
  screen_free(thread_data->screen);
  exit(0);
}
