/*
   "server.c" - part of "RAGMAAN, een anagrammengenerator"

   Copyright (C) 1999 Raymond Zandbergen

   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; either version 2 of the License, or
   (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program; if not, write to the Free Software
   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.

   The maintainer of this program is Raymond Zandbergen (ray@wirehub.nl)
 */

#include <stdlib.h>
#include <stdio.h>
#include <signal.h>
#include <string.h>
#include <regex.h>
#include <unistd.h>
#include "ragmaan.h"
#include "cache.h"
#include "hash_func.h"
#include "search.h"
#include "db_files.h"
#include "ipc.h"

#define	RM_SERVER_ID	"*** Anagram Server 1.0.0 ***"

#define	NICE_VALUE	(10)
/*
 * this thing renices itself ...
 */

#define	NO_IDX	(-1)

/***	server states	***/

#define	B_READ		(1001)	/* read client command, blocking */
#define	NB_READ		(1002)	/* ... non-blocking              */
#define	BT_READ		(1003)	/* ... blocking with timeout     */
#define	EXPAND		(1004)	/* do background processing      */
#define	INTERPRET 	(1005)	/* interpret client command      */

static HASH_TABLE_T hash_table;

/***	some statistics, not really used, but reported (CMD_STATUS) ***/

struct file_stats_s
  {
    int loaded;			/* OK, this one _is_ actually used */
    char name[MAX_WORD_LENGTH];
    int nof_words;
    size_t size;
  };

struct m_stats_s
  {
    int pre_exp_hits;
    int pre_exp_misses;
    int filter_hits;
    int filter_misses;
  };

static struct file_stats_s file_stats =
{0, "\0", 0, 0};
static struct m_stats_s m_stats =
{0, 0, 0, 0};

static anagram_set_t *current_anagram_set;

static int target_present = 0;
static int working_idx;

void
server_cleanup (void)
{
  shm_detach (shm);
}

static int
anagram_compare (const void *a1, const void *a2)

/*
 *  needed by qsort
 */

{
  char *an1, *an2;

  an1 = current_anagram_set->anagram[*(int *) a1];
  an2 = current_anagram_set->anagram[*(int *) a2];
  return strcmp (an1, an2);
}

static void
do_sort (void)
{
  int i;

  if (!current_anagram_set->sort_valid)
    {
      for (i = 0; i < current_anagram_set->nof_anagrams; i++)
	current_anagram_set->sorted_idx[i] = i;
      qsort (current_anagram_set->sorted_idx,
	     current_anagram_set->nof_anagrams,
	     sizeof (int),
	     anagram_compare);
      current_anagram_set->sort_valid = 1;
    }
}

static void
do_filter_sort (void)
{
  int i, matches, idx;

  if (!current_anagram_set->filter_sort_valid)
    {

      if (!current_anagram_set->regexp[0] ||
	  !current_anagram_set->sort_valid)
	{
	  puts (" cannot do filter_sort");
	  return;
	}

      matches = 0;
      for (i = 0; i < current_anagram_set->nof_anagrams; i++)
	{
	  idx = current_anagram_set->sorted_idx[i];
	  if (current_anagram_set->regexp_match[idx])
	    {
	      current_anagram_set->
		filtered_sorted_idx[matches++] = idx;
	    }
	}
      current_anagram_set->filter_sort_valid = 1;
    }
}

static void
select_expansion_candidate (int *p)

/*
 * try to find the most useful thing to do when idle ...
 *
 * should I take the sorting selections into account?
 *
 */

{
  int i;
  int idx = *p;

  if (current_anagram_set->use_filter)
    {

      for (i = idx; i < current_anagram_set->nof_anagrams; i++)
	{

	  if (current_anagram_set->regexp_match[i] &&
	      current_anagram_set->expansion[i] == '?')
	    {

	      *p = i;
	      return;
	    }
	}
      for (i = 0; i < idx; i++)
	{

	  if (current_anagram_set->regexp_match[i] &&
	      current_anagram_set->expansion[i] == '?')
	    {

	      *p = i;
	      return;
	    }
	}
    }
  for (i = idx; i < current_anagram_set->nof_anagrams; i++)
    {

      if (current_anagram_set->expansion[i] == '?')
	{

	  *p = i;
	  return;
	}
    }
  for (i = 0; i < idx; i++)
    {

      if (current_anagram_set->expansion[i] == '?')
	{

	  *p = i;
	  return;
	}
    }
  *p = NO_IDX;			/* nothing to do ... */
}

static void
expansion2string (char *s, int idx)
{
  char rest[MAX_WORD_LENGTH];

  s[0] = current_anagram_set->expansion[idx];
  s[1] = ' ';
  strcpy (s + 2, current_anagram_set->anagram[idx]);
  strcpy (rest, current_anagram_set->target);
  subtract (rest, current_anagram_set->anagram[idx]);
  if (rest[0])
    {
      strcat (s, " (");
      strcat (s, rest);
      strcat (s, ")");
    }
}

static void
expand_and_store (idx)
{
  if (idx >= current_anagram_set->nof_anagrams ||
      current_anagram_set->expansion[idx] != '?')
    return;

/*      printf(" expanding %d\n", idx); */
/*      usleep(200000); */
  expand_result (&hash_table,
		 current_anagram_set->target,
		 current_anagram_set->anagram[idx],
		 current_anagram_set->expansion + idx, 0);
}

/************ server commands **************/

static void
cmd_load (int *new_state)
{
  char *all_words;
  size_t file_size;

  if (file_stats.loaded)
    {

      all_words = hash_table.dictionary_start;
      free_hash_table (&hash_table);
      free (all_words);
    }

  all_words = read_words_from_file (shm->cmd.string_arg1, &file_size);

  if (!all_words)
    {
      strcpy (shm->reply.string_reply, "file load failed");
      shm->reply.error = 1;
      write_reply ();
      *new_state = B_READ;
      file_stats.loaded = 0;
      return;
    }

  if (-1 == create_hash (&hash_table, all_words, file_size,
			 report_progress))
    {
      strcpy (shm->reply.string_reply, "invalid file format");
      shm->reply.error = 1;
      write_reply ();
      *new_state = B_READ;
      file_stats.loaded = 0;
      return;
    }

  file_stats.size = file_size;
  file_stats.nof_words = hash_table.nof_words;
  strcpy (file_stats.name, shm->cmd.string_arg1);

  cache_flush ();
  strcpy (shm->reply.string_reply, "list loaded");
  shm->reply.error = 0;
  file_stats.loaded = 1;
  target_present = 0;
  write_reply ();
}

static void
cmd_search (int *new_state)
{
  SEARCH_STATE_T search_state;
  char *s;
  int i;

  if (!file_stats.loaded)
    {
      strcpy (shm->reply.string_reply, "no database");
      shm->reply.error = 1;
      write_reply ();
      *new_state = B_READ;
      return;
    }

  working_idx = 0;

  target_present = 1;

  current_anagram_set = get_working_set (shm->cmd.string_arg1);
  current_anagram_set->use_filter = 0;
  current_anagram_set->use_sort = 0;
  current_anagram_set->use_reverse = 0;
  if (!current_anagram_set->cache_hit)
    {

      strcpy (current_anagram_set->target, shm->cmd.string_arg1);

      init_search (&hash_table, &search_state,
		   current_anagram_set->target,
		   strlen (current_anagram_set->target), 1, 0);

      i = 0;

      while ((s = find_word_wl (&search_state, &hash_table)))
	{

	  current_anagram_set->anagram[i] = s;
	  current_anagram_set->expansion[i] = '?';
	  i++;
	  if (i >= MAX_WORKING_SET)
	    break;
	}
      current_anagram_set->nof_anagrams = i;
    }

  strcpy (shm->reply.string_reply, "search done");
  shm->reply.error = 0;
  shm->reply.int_reply = current_anagram_set->nof_anagrams;
  write_reply ();
}

static void
cmd_expand (int *new_state)
{
  int max;
  int req_idx;

  if (!target_present)
    {
      strcpy (shm->reply.string_reply, "no target");
      shm->reply.error = 1;
      write_reply ();
      *new_state = B_READ;
      return;
    }

  req_idx = shm->cmd.int_arg1;

  max = current_anagram_set->use_filter ?
    current_anagram_set->nof_filtered_anagrams :
    current_anagram_set->nof_anagrams;

  if (req_idx < 0 ||
      req_idx >= max)
    {

      snprintf (shm->reply.string_reply, SERVER_CMD_LENGTH - 1,
		"index out of range: %d should be 0 - %d", req_idx, max - 1);
      shm->reply.error = 1;
      write_reply ();
      return;
    }

  if (current_anagram_set->use_reverse &&
      max > 1)
    req_idx = max - req_idx - 1;

  if (current_anagram_set->use_sort &&
      current_anagram_set->use_filter)
    {

      req_idx = current_anagram_set->filtered_sorted_idx[req_idx];

    }
  else if (current_anagram_set->use_sort &&
	   !current_anagram_set->use_filter)
    {

      req_idx = current_anagram_set->sorted_idx[req_idx];

    }
  else if (!current_anagram_set->use_sort &&
	   current_anagram_set->use_filter)
    {

      req_idx = current_anagram_set->filtered_idx[req_idx];
    }

  if (current_anagram_set->expansion[req_idx] == '?')
    {

      m_stats.pre_exp_misses++;
      working_idx = req_idx;
      expand_and_store (working_idx);
    }
  else
    {
      m_stats.pre_exp_hits++;
    }

  expansion2string (shm->reply.string_reply, req_idx);
  shm->reply.int_reply = 1;
  shm->reply.error = 0;
  write_reply ();
}

static void
cmd_filter (int *new_state)
{
  if (!file_stats.loaded)
    {
      strcpy (shm->reply.string_reply, "no database");
      shm->reply.error = 1;
      write_reply ();
      return;
    }

  if (!target_present)
    {
      strcpy (shm->reply.string_reply, "no target");
      shm->reply.error = 1;
      write_reply ();
      *new_state = B_READ;
      return;
    }

  if (shm->cmd.string_arg1[0])
    {

      if (strcmp (shm->cmd.string_arg1,
		  current_anagram_set->regexp) != 0)
	{

	  int i;
	  int matches = 0;
	  int error_code;
	  regex_t preg;
	  char *regex = shm->cmd.string_arg1;

	  error_code = regcomp (&preg, regex,
				REG_EXTENDED | REG_ICASE | REG_NOSUB);

	  if (error_code != 0)
	    {
	      strcpy (shm->reply.string_reply, "invalid regexp: ");
	      regerror (error_code,
			&preg, shm->reply.string_reply + 16,
			SERVER_CMD_LENGTH - 16 - 1);
	      shm->reply.error = 1;
	      write_reply ();
	      *new_state = NB_READ;
	      regfree (&preg);
	      return;
	    }

	  strcpy (current_anagram_set->regexp, shm->cmd.string_arg1);
	  for (i = 0; i < current_anagram_set->nof_anagrams; i++)
	    {

	      char *a = current_anagram_set->anagram[i];

	      error_code = regexec (&preg, a, 0, 0, 0);

	      switch (error_code)
		{

		case 0:

		  current_anagram_set->regexp_match[i] = 1;
		  current_anagram_set->filtered_idx[matches++] = i;
		  break;

		case REG_NOMATCH:

		  current_anagram_set->regexp_match[i] = 0;
		  break;

		default:

		  current_anagram_set->regexp_match[i] = 0;
		  fputs (" ?? regexec error ??", stderr);

		}
	    }
	  regfree (&preg);
	  current_anagram_set->nof_filtered_anagrams = matches;
	  current_anagram_set->filter_sort_valid = 0;
	  m_stats.filter_misses++;
	}
      else
	{
	  m_stats.filter_hits++;
/*
   printf(" cache hit for regexp '%s'"
   " applied to '%s'\n",
   current_anagram_set->regexp,
   current_anagram_set->target);
 */
	}
      if (current_anagram_set->sort_valid)
	{
	  do_filter_sort ();
	}
      strcpy (shm->reply.string_reply, "filtering done");
      current_anagram_set->use_filter = 1;
      shm->reply.int_reply =
	current_anagram_set->nof_filtered_anagrams;
    }
  else
    {
      current_anagram_set->use_filter = 0;
      strcpy (shm->reply.string_reply, "filtering off");
      shm->reply.int_reply =
	current_anagram_set->nof_anagrams;
    }
  shm->reply.error = 0;
  write_reply ();
  *new_state = BT_READ;
}

static void
interpret_command (CMD_T * cmd, int *new_state)
{
/*      printf(" interpreting\n"); */
  switch (cmd->cmd)
    {

    case CMD_LOAD:

      cmd_load (new_state);
      break;

    case CMD_QUIT:

      shm->reply.error = 0;
      strcpy (shm->reply.string_reply, "quitting");
      write_reply ();
      server_cleanup ();
      exit (0);
      break;

    case CMD_SEARCH:

      cmd_search (new_state);
      break;

    case CMD_EXPAND:

      cmd_expand (new_state);
      break;

    case CMD_FILTER:

      cmd_filter (new_state);
      break;

    case CMD_SORT:

      if (shm->cmd.int_arg1 == 0)
	{
	  current_anagram_set->use_sort = 0;
	  shm->reply.error = 0;
	  strcpy (shm->reply.string_reply, "sort OFF");
	}
      else
	{
	  current_anagram_set->use_sort = 1;
	  do_sort ();
	  if (current_anagram_set->regexp[0])
	    {

	      do_filter_sort ();
	    }
	  shm->reply.error = 0;
	  strcpy (shm->reply.string_reply, "sort ON");
	}
      write_reply ();
      break;

    case CMD_REVERSE:

      if (shm->cmd.int_arg1 == 0)
	{
	  current_anagram_set->use_reverse = 0;
	  strcpy (shm->reply.string_reply, "reverse OFF");
	}
      else
	{
	  current_anagram_set->use_reverse = 1;
	  strcpy (shm->reply.string_reply, "reverse ON");
	}
      shm->reply.error = 0;
      write_reply ();
      break;

    case CMD_CACHE_SYNC:

      sync_cache_flag = 1;
      strcpy (shm->reply.string_reply, "cache synched");
      shm->reply.error = 0;
      write_reply ();
      break;

    case CMD_STATUS:

      snprintf (shm->reply.string_reply, SERVER_CMD_LENGTH - 1,
		"SERVER: ID = '" RM_SERVER_ID "', PID = %d\n"
		"FILE: %s, %d words, %d KB\n",
		getpid (),
		file_stats.name,
		file_stats.nof_words,
		file_stats.size / 1024
	);
      {
	int n = strlen (shm->reply.string_reply);
	cache_stats (shm->reply.string_reply + n, SERVER_CMD_LENGTH - n - 1);
	n = strlen (shm->reply.string_reply);
	snprintf (shm->reply.string_reply + n, SERVER_CMD_LENGTH - n - 1,
		  "LCACHE: filter hits: %d / %d, pre_exp_hits: %d / %d\n",
	   m_stats.filter_hits, m_stats.filter_hits + m_stats.filter_misses,
	 m_stats.pre_exp_hits, m_stats.pre_exp_hits + m_stats.pre_exp_misses
	  );
      }
      shm->reply.error = 0;
      write_reply ();
      break;

    default:

      strcpy (shm->reply.string_reply, "unknown server command");
      shm->reply.error = 1;
      write_reply ();

    }
}

void
alarm_handler (int dummy)
{
  signal (SIGALRM, alarm_handler);
}

int
main (int argc, char *argv[])
{

  int state = B_READ;
  int new_state = -1;

  int shm_id;

  if (argc != 2)
    {
      puts ("\n" RM_SERVER_ID "\n");
      fputs ("I need a shared mem id to talk to you ...\n\n",
	     stderr);
      exit (1);
    }
  cache_init ();
  shm_id = atoi (argv[1]);
  shm = shm_attach (shm_id);

  if (-1 == (int) shm)
    {
      perror ("could not attach to shared mem");
      exit (1);
    }

  signal (SIGALRM, alarm_handler);
  nice (NICE_VALUE);
  shm->server_running = 1;
  strncpy (shm->signature, RM_SERVER_ID, SIGNATURE_LENGTH);
  sem_up (SERVER_INIT);

  for (;;)
    switch (state)
      {

      case NB_READ:

	if (read_command (NONBLOCKING) == 1)
	  {
	    state = INTERPRET;
	    break;
	  }

	state = EXPAND;
	break;

      case BT_READ:

	if (tread_command (0, 200000) == 0)
	  {

	    state = EXPAND;
	    break;
	  }
	state = INTERPRET;
	break;

      case EXPAND:

	if (!file_stats.loaded)
	  {

	    state = B_READ;
	    break;
	  }

	if (!target_present)
	  {

	    state = B_READ;
	    break;
	  }

	select_expansion_candidate (&working_idx);

	if (working_idx == NO_IDX)
	  {

	    state = B_READ;
	    break;
	  }

	expand_and_store (working_idx);

	state = NB_READ;
	break;

      case B_READ:

	shm->async_data.server_working = 0;
	read_command (BLOCKING);
	shm->async_data.server_working = 1;
	state = INTERPRET;

	break;

      case INTERPRET:

	new_state = BT_READ;
	interpret_command (&shm->cmd, &new_state);
	state = new_state;
	break;
      }
  return 0;
}
