/*
 * Copyright (C) 2002 The Free Software Initiative of Japan
 * Author: NIIBE Yutaka
 */

/*
 * ANTHY Low Level Agent
 */

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

#include <anthy/anthy.h>

#include "config.h"

/*
 *            connection        context 
 *  S -- open -> [ ] -- new ----> [ ] -- convert --> [ ] --> get_candidates
 *   <- close --     <- release -     <-- commit --      --> resize_segment
 *                                    <-- cancel --      --> select_candidate
 *                                                
 */

struct context {
  struct anthy_context *ac;
  int buflen;
  unsigned char *buf;
  int removed, inserted;
  int sellen;
  int *selection;
};

#define MAX_CONTEXT 16
static struct context contexts[MAX_CONTEXT];

extern int use_utf8;

#define INITIAL_BUFLEN 512
#define INITIAL_SELLEN 128

/*
 * Returns -1 on error.
 * Returns >= 0 on success, and the number is  context descriptor.
 */
static int
new_context (void)
{
  int i;

  for (i = 0; i < MAX_CONTEXT; i++)
    if (contexts[i].buflen == 0) { /* Found free one */
      struct context *c = &contexts[i];

      if ((c->buf = (unsigned char *)malloc (INITIAL_BUFLEN)) == NULL)
	return -1;

      if ((c->selection = (int *)malloc (INITIAL_SELLEN)) == NULL) {
	free (c->buf);
	c->buf = NULL;
	return -1;
      }

      if ((c->ac = anthy_create_context ()) == NULL) {
	free (c->buf);
	c->buf = NULL;
	free (c->selection);
	c->selection = NULL;
	return -1;
      }
      if (use_utf8) {
	anthy_context_set_encoding(c->ac, ANTHY_UTF8_ENCODING);
      }

      c->buflen = INITIAL_BUFLEN;
      c->sellen = INITIAL_SELLEN;
      return i;
    }

  /* No free context to be used */
  return -1;
}

static int
release_context (int c_desc)
{
  struct context *c = &contexts[c_desc];

  anthy_release_context(c->ac);
  free (c->buf);
  c->buf = NULL;
  c->buflen = 0;
  free (c->selection);
  c->selection = NULL;
  c->sellen = 0;

  return 0;
}

static struct context *
c_desc_to_context (int c_desc)
{
  return &contexts[c_desc];
}

static int
get_number_of_segments (struct context *c)
{
  struct anthy_conv_stat cs;

  if (anthy_get_stat(c->ac, &cs) < 0)
    return -1;

  return cs.nr_segment;
}

static int
begin_conversion (struct context *c, const char *input)
{
  int i;
  int seg_num;
  if (anthy_set_string(c->ac, (char *)input) < 0)
    return -1;

  seg_num = get_number_of_segments (c);
  if (seg_num >= c->sellen) {
    c->sellen *= 2;
    c->selection = realloc (c->selection, c->sellen);
    if (c->selection == NULL) { /* Fatal */
      c->sellen = -1;
      return -1;
    }
  }

  for (i = 0; i < seg_num; i++)
    c->selection[i] = 0;

  return 0;
}

static int
end_conversion (struct context *c, int cancel)
{
  int n;
  int seg_num;
  int can_num;

  if (!cancel) {
    n = get_number_of_segments (c);
    for (seg_num = 0; seg_num < n; seg_num++) {
      can_num = c->selection[seg_num];
      anthy_commit_segment(c->ac, seg_num, can_num);
    }
  }

  return 0;
}

static int
get_segment_number_of_candidates (struct context *c, int seg_num)
{
  struct anthy_segment_stat ss;

  if (anthy_get_segment_stat (c->ac, seg_num, &ss) != 0)
    return -1;

  return ss.nr_candidate;
}

static const unsigned char *
get_segment_candidate (struct context *c, int seg_num, int cand_num)
{
  int len;

  while (1) {
    len = anthy_get_segment (c->ac, seg_num, cand_num,
			     (char *)c->buf, c->buflen);

    if (len < 0)
      return NULL;

    if (len < c->buflen)
      return c->buf;

    c->buflen *= 2;
    c->buf = realloc (c->buf, c->buflen);
    if (c->buf == NULL) { /* Fatal */
      c->buflen = -1;
      return NULL;
    }
  }
}

static const unsigned char *
get_segment_yomi (struct context *c, int seg_num)
{
  return get_segment_candidate (c, seg_num, NTH_UNCONVERTED_CANDIDATE);
}

static const unsigned char *
get_segment_converted (struct context *c, int seg_num)
{
  return get_segment_candidate (c, seg_num, 0);
}

static int
resize_segment (struct context *c, int seg_num, int inc_dec)
{
  int i;
  struct anthy_conv_stat cs;

  if (anthy_get_stat(c->ac, &cs) < 0)
    return -1;

  /* Replace all segments after SEG_NUM */
  c->removed = cs.nr_segment - seg_num;
  anthy_resize_segment(c->ac, seg_num, inc_dec?-1:1);
  if (anthy_get_stat(c->ac, &cs) < 0)
    return -1;
  c->inserted = cs.nr_segment - seg_num;

  if (cs.nr_segment >= c->sellen) {
    c->sellen *= 2;
    c->selection = realloc (c->selection, c->sellen);
    if (c->selection == NULL) { /* Fatal */
      c->sellen = -1;
      return -1;
    }
  }
  for (i = seg_num; i < cs.nr_segment; i++)
    c->selection[i] = 0;

  return seg_num;
}

/* Only valid after call of resize_segment or select_candidate */
static int
get_number_of_segments_removed (struct context *c, int seg_num)
{
  (void)seg_num;
  return c->removed;
}

/* Only valid after call of resize_segment or select_candidate */
static int
get_number_of_segments_inserted (struct context *c, int seg_num)
{
  (void)seg_num;
  return c->inserted;
}

static int
select_candidate (struct context *c, int seg_num, int can_num)
{
  /*
   * Anthy does not have capability to affect the result of selection
   * to other segments.
   */
  c->removed = 0;
  c->inserted = 0;

  /*
   * Record, but not call anthy_commit_segment.
   */
  c->selection[seg_num] = can_num;

  return seg_num;
}

static int
say_hello (void)
{
  const char *options = "";

  printf ("Anthy (Version %s) [%s] : Nice to meet you.\r\n", VERSION, 
	  options);
  fflush (stdout);
  return 0;
}

#define ERROR_CODE_UNKNOWN	400
#define ERROR_CODE_UNSUPPOTED	401

static int
say_unknown (void)
{
  printf ("-ERR %d Unknown command.\r\n", ERROR_CODE_UNKNOWN);
  fflush (stdout);
  return 0;
}

static int
do_commit (const char *line)
{
  char *p;
  struct context *c;
  int c_desc, cancel, r;

  c_desc = strtol (line+7, &p, 10);
  c = c_desc_to_context (c_desc);
  cancel = strtol (p+1, &p, 10);
  r = end_conversion (c, cancel);
  if (r < 0)
    printf ("-ERR %d commit failed.\r\n", -r);
  else
    printf ("+OK\r\n");
  fflush (stdout);
  return 0;
}

static void
output_segments (struct context *c, int seg_num, int removed, int inserted)
{
  int i;

  printf ("+DATA %d %d %d\r\n", seg_num, removed, inserted);
  for (i = seg_num; i < seg_num + inserted; i++) {
    int nc;

    nc = get_segment_number_of_candidates (c, i);
    printf ("%d " ,nc);
    printf ("%s ", get_segment_converted (c, i));
    printf ("%s\r\n", get_segment_yomi (c, i));
  }
  printf ("\r\n");
}

static int
do_convert (const char *line)
{
  char *p;
  struct context *c;
  int c_desc, r;

  c_desc = strtol (line+8, &p, 10);
  c = c_desc_to_context (c_desc);
  r = begin_conversion (c, p+1);
  if (r < 0)
    printf ("-ERR %d convert failed.\r\n", -r);
  else
    {
      int n = get_number_of_segments (c);
      output_segments (c, 0, 0, n);
    }

  fflush (stdout);
  return 0;
}

static int
do_get_candidates (const char *line)
{
  char *p;
  struct context *c;
  int c_desc, seg_num, cand_offset, max_cands;
  int nc, i, max;

  c_desc = strtol (line+15, &p, 10);
  seg_num = strtol (p+1, &p, 10);
  cand_offset = strtol (p+1, &p, 10);
  max_cands = strtol (p+1, &p, 10);

  c = c_desc_to_context (c_desc);
  nc = get_segment_number_of_candidates (c, seg_num);

  max = cand_offset + max_cands;
  if (nc < cand_offset + max_cands)
    max = nc;

  printf ("+DATA %d %d\r\n", cand_offset, max);
  for (i = cand_offset; i < max; i++)
    printf ("%s\r\n", get_segment_candidate (c, seg_num, i));
  printf ("\r\n");  

  fflush (stdout);
  return 0;
}

static int
do_new_context (const char *line)
{
  int r;

  /* XXX: Should check arguments */
  if (strncmp (" INPUT=#18 OUTPUT=#18", line+11, 20) != 0) {
    printf ("-ERR %d unsupported context\r\n", ERROR_CODE_UNSUPPOTED);
    return 1;
  }

  r = new_context ();
  if (r < 0)
    printf ("-ERR %d new context failed.\r\n", -r);
  else
    printf ("+OK %d\r\n", r);

  fflush (stdout);
  return 0;
}

static int
do_release_context (const char *line)
{
  int c_desc;
  int r;
  char *p;

  c_desc = strtol (line+15, &p, 10);
  r = release_context (c_desc);
  if (r < 0)
    printf ("-ERR %d release context failed.\r\n", -r);
  else
    printf ("+OK\r\n");

  fflush (stdout);
  return 0;
}

static int
do_resize_segment (const char *line)
{
  char *p;
  struct context *c;
  int c_desc, seg_num, inc_dec, r;

  c_desc = strtol (line+15, &p, 10);
  seg_num = strtol (p+1, &p, 10);
  inc_dec= strtol (p+1, &p, 10);
  c = c_desc_to_context (c_desc);
  r = resize_segment (c, seg_num, inc_dec);

  if (r < 0)
    printf ("-ERR %d resize failed.\r\n", -r);
  else {
    int removed, inserted;

    seg_num = r;
    removed = get_number_of_segments_removed (c, seg_num);
    inserted = get_number_of_segments_inserted (c, seg_num);

    output_segments (c, seg_num, removed, inserted);
  }

  fflush (stdout);
  return 0;
}

static int
do_select_candidate (const char *line)
{
  char *p;
  struct context *c;
  int c_desc, seg_num, cand_num, r;

  c_desc = strtol (line+17, &p, 10);
  seg_num = strtol (p+1, &p, 10);
  cand_num = strtol (p+1, &p, 10);
  c = c_desc_to_context (c_desc);
  r = select_candidate (c, seg_num, cand_num);

  if (r < 0)
    printf ("-ERR %d select failed.\r\n", -r);
  else {
    int removed;

    seg_num = r;
    removed = get_number_of_segments_removed (c, seg_num);

    if (removed == 0)
      printf ("+OK\r\n");
    else {
      int inserted = get_number_of_segments_inserted (c, seg_num);

      output_segments (c, seg_num, removed, inserted);
    }
  }

  fflush (stdout);
  return 0;
}

static int
do_quit (const char *line)
{
  (void)line;
  return 1;
}

struct dispatch_table {
  const char *command;
  int size;
  int (*func)(const char *line);
};

static struct dispatch_table dt[] = {
  { "COMMIT", 6, do_commit },
  { "CONVERT", 7, do_convert },
  { "GET-CANDIDATES", 14, do_get_candidates },
  { "NEW-CONTEXT", 11, do_new_context },
  { "QUIT", 4, do_quit },
  { "RELEASE-CONTEXT", 15, do_release_context },
  { "RESIZE-SEGMENT", 14, do_resize_segment },
  { "SELECT-CANDIDATE", 16, do_select_candidate },
};

static int
dt_cmp (const char *line, struct dispatch_table *d)
{
  return strncmp (line, d->command, d->size);
}

#define MAX_LINE 512
static char line[MAX_LINE];

void egg_main (void);

void
egg_main (void)
{
  int done = 0;
  char *s, *p;

  say_hello ();

  while (!done) {
    struct dispatch_table *d;

    s = fgets (line, MAX_LINE, stdin);
    if (s == NULL) {
      fprintf (stderr, "null input\n");
      break;
    }
    if ((p = (char *)memchr(s, '\n', MAX_LINE)) == NULL) {
      fprintf (stderr, "no newline\n");
      break;
    }
    if (p > s && *(p-1) == '\r')
      *(p-1) = '\0';
    else
      *p = '\0';
    d = (struct dispatch_table *)
      bsearch (s, dt, 
	       sizeof (dt) / sizeof (struct dispatch_table), 
	       sizeof (struct dispatch_table), 
	       (int (*)(const void *, const void *))dt_cmp);
    if (d != NULL)
      done = d->func (s);
    else
      say_unknown ();
  }
}
