/*  compose.c -- Keysym composing

    Copyright (C) 2003  Marco Gerards
   
    Written by Marco Gerards <metgerards@student.han.nl>
    
    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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/

#include <stdio.h>
#include <stdlib.h>
#include <X11/keysymdef.h>
#include "xkb.h"
#include <ctype.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <locale.h>
#include <assert.h>

/* Tokens that can be recognised by the scanner.  */
enum tokentype
  {
    UNKNOWN,
    EOL,
    REQKS,
    SEMICOL,
    STR,
    PRODKS,
    END
  };

/* The current token.  */
struct token
{
  enum tokentype toktype;
  char value[50];
} tok;

/* The linenumber of the line currently parsed, used for returning
   errors and warnings.  */
static int linenum;

/* Read a token from the file CF.  */
static void
read_token (FILE *cf)
{
  int c = fgetc (cf);
  int pos = 0;

  /* Remove whitespaces.  */
  while (c == ' ' || c == '\t')
    c = fgetc (cf);

  /* Comment, remove until end of line and return a EOL token.  */
  if (c == '#')
    {
      while (c != '\n')
	c = fgetc (cf);
      tok.toktype = EOL;
      linenum++;
      return;
    }

  /* End of file.  */
  if (c == EOF)
    {
      tok.toktype = END;
      return;
    }

  /* Semicolon.  */
  if (c == ':')
    {
      tok.toktype = SEMICOL;
      return;
    }

  /* End of line.  */
  if (c == '\n')
    {
      linenum++;
      tok.toktype = EOL;
      return;
    }
  

  /* Required keysym.  */
  if (c == '<')
    {
      while ((c = fgetc (cf)) != '>')
	tok.value[pos++] = c;
      tok.value[pos] = '\0';
      tok.toktype = REQKS;
      return;
    }

  /* Character string.  */
  if (c == '"')
    {
      while ((c = fgetc (cf)) != '"')
	{
 	  if (c == '\\')
	    c = fgetc (cf);

	  tok.value[pos++] = c;
	}
      tok.value[pos] = '\0';
      tok.toktype = STR;
      return;
    }

  /* Produced keysym.  */
  if (isalpha (c))
    {
      tok.value[pos++] = c;
      while (isgraph (c = fgetc (cf)))
	tok.value[pos++] = c;
      tok.value[pos] = '\0';
      tok.toktype = PRODKS;
      ungetc (c, cf);
      return;
    }

  /* Unknown token.  */
  tok.toktype = UNKNOWN;
  return;
}

/* Compose sequence.  */
struct compose
{
  struct compose *left;
  struct compose *right;
  symbol *expected;
  symbol produced;
} *compose_tree;


/* Compare symbol sequence s1 to symbol sequence s1. This function
   works just like the strcmp function.  */
static int
symbolscmp (symbol *s1, symbol *s2)
{
  while (*s1 && *s2 && (*s1 == *s2))
    {
      s1++;s2++;
    }
  if (*s1 < *s2)
    return -1;
  if (*s1 > *s2)
    return 1;
  return 0;  
}

/* Compare symbol sequence s1 to symbol sequence s1, compare a maximum
   of N symbols. This function works just like the strcmp function.
   */
static int
symbolsncmp (symbol *s1, symbol *s2, int n)
{
  int cnt = 0;
  while (*s1 && *s2 && (*s1 == *s2))
    {
      if (++cnt == n)
	break;
      s1++;s2++;
    }

  if (*s1 < *s2)
    return -1;
  if (*s1 > *s2)
    return 1;
  return 0;  
}


/* Add the compose sequence EXP to the binary tree, store RESULT as
   the keysym produced by EXP.  */
static struct compose *
composetree_add (struct compose *tree, symbol *exp, symbol result)
{
  int cmp;

  if (tree == NULL)
    {
      tree = malloc (sizeof (struct compose));
      tree->expected = exp;
      tree->produced = result;
      tree->left = tree->right = NULL;
      
      return tree;
    }
  
  cmp = symbolscmp (exp, tree->expected);
  if (cmp == 0)
    {
      printf ("Warning: line %d: Double sequence.\n", linenum);
      free (exp);
    }
  else if (cmp < 0)
    tree->left = composetree_add (tree->left, exp, result);
  else
    tree->right = composetree_add (tree->right, exp, result);
  return tree;
}

/* Parse the composefile CF and put all sequences in the binary tree
   COMPOSE_TREE. This function may never fail because of syntactical or
   lexalical errors, generate a warning instead.  */
static error_t
parse_composefile (FILE *cf)
{
  void skip_line (void)
    {
      while (tok.toktype != EOL && tok.toktype != END)
	read_token (cf);
    }

  for (;;)
    {  
      /* Expected keysyms.  */
      symbol exp[50];
      symbol *exps;
      size_t expcnt = 0;
      symbol sym;

      read_token (cf);
      /* Blank line.  */
      if (tok.toktype == EOL)
	continue;

      /* End of file, done parsing.  */
      if (tok.toktype == END)
	return 0;

      if (tok.toktype != REQKS)
	{
	  printf ("Warning: line %d: Keysym expected on beginning of line.\n",
		  linenum);
	  skip_line ();
	  continue;
	}

      /* Keysym was recognised, add it.  */
      sym = XStringToKeysym (tok.value);
      if (!sym)
	{
	  printf ("Warning: line %d: Unknown keysym \"%s\".\n", linenum, 
		  tok.value);
	  skip_line ();
	  continue;
	}
      exp[expcnt++] = sym;

      do
	{
	  read_token (cf);
	  /* If another required keysym is recognised, add it.  */
	  if (tok.toktype == REQKS)
	    {
	      sym = XStringToKeysym (tok.value);
	      if (!sym)
		{
		  printf ("Warning: line %d: Unknown keysym \"%s\".\n", 
			  linenum, tok.value);
		  skip_line ();
		  continue;
		}
	      exp[expcnt++] = sym;
	    }
	} while (tok.toktype == REQKS);

      if (tok.toktype != SEMICOL)
	{
	  printf ("Warning: line %d: Semicolon expected.\n", linenum);
	  skip_line ();
	  continue;
	}

      read_token (cf);
      /* Force token and ignore it.  */
      if (tok.toktype != STR)
	{
	  printf ("Warning: line %d: string expected.\n", linenum);
	  skip_line ();
	  continue;
	}

      read_token (cf);
      if (tok.toktype != PRODKS)
	{
	  printf ("Warning: line %d: keysym expected.\n", linenum);
	  skip_line ();
	  continue;
	}
      sym = XStringToKeysym (tok.value);
      if (!sym)
	{
	  printf ("Warning: line %d: Unknown keysym \"%s\".\n", linenum,
		  tok.value);
	  skip_line ();
	  continue;
	}

      read_token (cf);
      if (tok.toktype != EOL && tok.toktype != END)
	{
	  printf ("Warning: line %d: end of line or end of file expected.\n",
		  linenum);
	  skip_line ();
	  continue;
	}

      /* Add the production rule.  */
      exp[expcnt++] = 0;
      exps = malloc (sizeof (symbol) * expcnt);
      memcpy (exps, exp, sizeof (symbol) * expcnt);
      compose_tree = composetree_add (compose_tree, exps, sym);
    }
  return 0;
}

/* Read keysyms passed to this function by S until a keysym can be
   composed. If the first keysym cannot start a compose sequence return
   the keysym.  */
symbol
compose_symbols (symbol s)
{
  /* Current position in the compose tree.  */
  static struct compose *treepos = NULL;
  /* Current compose sequence.  */
  static symbol syms[100];
  /* Current position in the compose sequence.  */
  static int pos = 0;
  int cmp;

  if (!treepos)
    treepos = compose_tree;

  /* Maximum sequence length reached. Some idiot typed this many
     symbols and now we throw it all away, wheee!!!  */
  if (pos == 99)
    {
      treepos = compose_tree;
      pos = 0;
    }

  /* Put the keysym in the compose sequence array.  */
  syms[pos++] = s;
  syms[pos] = 0;

  /* Search the tree for a keysym sequence that can match the current one.  */
  while (treepos)
    {
      cmp = symbolsncmp (syms, treepos->expected, pos);
      if (cmp == 0)
	{
	  /* The keysym sequence was partially recognised, check if it
	     can completely match.  */
	  if (!symbolscmp (syms, treepos->expected))
	    {
	      symbol ret = treepos->produced;
	      treepos = compose_tree;
	      pos = 0;
	      return ret;
	    }

	  /* The sequence was partially recognised.  */
	  return -1;
	}

      if (cmp < 0)
	treepos = treepos->left;
      else
	treepos = treepos->right;
    }  

  /* Nothing can be found.  */
  treepos = compose_tree;

  /* This ks should've started a sequence but couldn't be found,
     just return it.  */
  if (pos == 1)
    {
      pos = 0;   
      return s;
    }

  debug_printf ("Invalid\n");
  /* Invalid keysym sequence.  */
  pos = 0;   
  return -1;
}

struct map_entry
{
  const char *left;
  const char *right;
};

enum callback_result
  {
    NEXT,
    DONE
  };

typedef enum callback_result (*map_callback) (void *context, struct map_entry *entry);

static error_t
map_iterate(const char *map_path, map_callback action, void *context)
{
  FILE *map;
  char *buffer = NULL;
  size_t buffer_size = 0;
  size_t line_length = 0;

  assert (map_path != NULL);
  assert (action != NULL);

  map = fopen (map_path, "r");

  if (map == NULL)
    return errno;

  while ( (line_length = getline (&buffer, &buffer_size, map)) != -1)
    {
      /* skips empty lines and comments */
      if (line_length < 1 || buffer[0] == '#')
        continue;
      else
        {
          struct map_entry entry = {NULL, NULL};
          char *end = buffer + line_length;
          char *p = buffer;

          while (p != end && isspace(*p)) p++;

          if (p == end)
            continue;

          entry.left = p;

          while (p != end && !isspace(*p)) p++;

          if (p != end)
            {
              *(p++) = 0;
              while (p != end && isspace(*p)) p++;

              if (p != end)
                {
                  entry.right = p;
                  while (p != end && !isspace(*p)) p++;
                  if (p != end)
                    *p = 0;
                }
            }

          if (action (context, &entry) == DONE)
            break;
        }
    }
  free (buffer);
  fclose (map);
  return 0;
}

struct matcher_context
{
  char *value;
  char *result;
};

static enum callback_result
match_left_set_right (void *context, struct map_entry *entry)
{
  struct matcher_context *ctx = (struct matcher_context *) context;

  if (strcmp (ctx->value, entry->left) == 0)
    {
      ctx->result = strdup (entry->right);
      return DONE;
    }
  return NEXT;
}

static enum callback_result
match_right_set_left (void *context, struct map_entry *entry)
{
  struct matcher_context *ctx = (struct matcher_context *) context;

  if (strcmp (ctx->value, entry->right) == 0)
    {
      ctx->result = strdup (entry->left);
      return DONE;
    }
  return NEXT;
}

/* Search for a compose file.

   According to Compose(5) man page the compose file searched in the
   following locations:
     - XCOMPOSEFILE variable.
     - .XCompose at $HOME.
     - System wide compose file for the current locale. */
static char *
get_compose_file_for_locale()
{
  struct matcher_context context = { NULL };
  char *xcomposefile;
  char *to_be_freed;
  char *home;
  int err;

  xcomposefile = getenv ("XCOMPOSEFILE");
  if (xcomposefile != NULL)
    return strdup (xcomposefile);

  home = getenv ("HOME");
  if (home != NULL)
    {
      err = asprintf (&xcomposefile, "%s/.XCompose", home);
      if (err != -1)
        {
          if (faccessat(AT_FDCWD, xcomposefile, R_OK, AT_EACCESS) == 0)
            return xcomposefile;
          else
            {
              free (xcomposefile);
              /* TODO: check and report whether the compose file doesn't exist or
                 read permission was not granted to us. */
            }
        }
    }

  context.value = setlocale (LC_ALL, NULL);
  map_iterate (X11_PREFIX "/share/X11/locale/locale.alias", match_left_set_right, &context);
  to_be_freed = context.result;

  if (context.result != NULL)
    {
      /* Current locale is an alias. Use the real name to index the database. */
      context.value = context.result;
    }
  context.result = NULL;
  map_iterate (X11_PREFIX "/share/X11/locale/compose.dir", match_right_set_left, &context);
  free (to_be_freed);

  /* compose.dir contains relative paths to compose files. */
  to_be_freed = context.result;
  err = asprintf (&context.result, X11_PREFIX "/share/X11/locale/%s", context.result);
  if (err == -1)
    context.result = NULL;

  free (to_be_freed);
  return context.result;
}

/* Read a Compose file.  */
error_t
read_composefile (char *composefn)
{
  FILE *cf;

  error_t err;

  if (composefn == NULL)
    composefn = get_compose_file_for_locale ();

  cf = fopen (composefn, "r");
  if (cf == NULL)
    return errno;
  
  err = parse_composefile (cf);
  fclose (cf);
  
  return err;
}