/* Command-line interface for lexically C-like languages -*- c -*- */

#include "cmdline.h"
#ifdef _AIX
# ifndef _XOPEN_SOURCE_EXTENDED
#  define _XOPEN_SOURCE_EXTENDED 1
# endif /* !_XOPEN_SOURCE_EXTENDED */
# include <strings.h> /* for bzero, used by FD_ZERO */
#endif /* _AIX */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <errno.h>
#include <assert.h>

/** @file cmdline.c
 * Command-line interface for lexically C-like languages
 */

/* Copyright © 1999-2002 Marko Mäkelä (msmakela@tcs.hut.fi).

   This file is part of MARIA, a reachability analyzer and model checker
   for high-level Petri nets.

   MARIA 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, or (at your option)
   any later version.

   MARIA 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.

   The GNU General Public License is often shipped with GNU software, and
   is generally kept in a file called COPYING or LICENSE.  If you do not
   have a copy of the license, write to the Free Software Foundation,
   59 Temple Place, Suite 330, Boston, MA 02111 USA. */

static struct {
  /** flag: is a continuation line being read? */
  unsigned cont;
  /** the line being read */
  char* line;
} terminal = { 0, 0 }
#ifdef __WIN32
;
#else /* __WIN32 */
, file = { 0, 0 };
/** file descriptor of the visualization command stream */
extern volatile int visfd;
/** file handle of the visualization command stream */
extern FILE* volatile visf;
#endif /* __WIN32 */

/** flag: is a line being read from the terminal? */
static unsigned line_pending = 0;

#ifdef HAS_READLINE
# include <readline/readline.h>
# include <readline/history.h>
/** the line read from the terminal */
static char* readline_line;
/** readline callback function
 * @param line		line captured by Readline
 */
static void
readline_callback (char* line)
{
  line_pending = 0;
  readline_line = line;
  rl_callback_handler_remove ();
}
#endif /* HAS_READLINE */

/** Read a line of input
 * @param f	the input stream
 * @return	a line of text (NULL on end of stream)
 */
static char*
inputline (FILE* f)
{
  size_t length = 0;
  char* buf = malloc (BUFSIZ);
  while (fgets (buf + length, BUFSIZ, f)) {
    char* last = buf + length;
    while (*last) last++;
    if (last > buf + length && last[-1] == '\n') {
      last[-1] = 0;
      return buf;
    }
    else {
      char* buf2 = realloc (buf, (length += last - buf) + BUFSIZ);
      if (!buf2)
        return buf;
      buf = buf2;
    }
  }

  if (length)
    return buf;
  else {
    free (buf);
    return NULL;
  }
}

/** Report a syntax error on the command line
 * @param prefix	message prefix
 * @param msg		message to be reported
 */
static void
error (const char* prefix, const char* msg) {
  if (!prefix) prefix = "process";
  fputs (prefix, stderr);
  fputs (": ", stderr);
  fputs (msg, stderr);
  fputc ('\n', stderr);
}

/** States of the input buffer */
enum state {
  /** Normal state */
  normal,
  /** Inside single quotes */
  single_quote,
  /** Inside double quotes */
  double_quote,
  /** Backslash-quoted character inside single quotes */
  squote_backslash,
  /** Backslash-quoted character inside double quotes */
  dquote_backslash,
  /** Backslash-quoted character outside quotes */
  backslash,
  /** Inside a C-style comment */
  comment,
  /** Inside a C++-style end-of-line comment */
  scomment
};

/** Determine the state of the input buffer
 * @param s		current state (input/output)
 * @param line		command line to be scanned
 * @param length	length of the command line
 * @return		state of line[length]
 */
static enum state
scanstate (enum state s, const char* line, unsigned length)
{
  unsigned i;

  for (i = 0; i < length; i++) {
    switch (line[i]) {
    case '\'':
      switch (s) {
      case normal:
	s = single_quote;
	break;
      case single_quote:
	s = normal;
	break;
      default:
	break;
      }
      break;
    case '"':
      switch (s) {
      case normal:
	s = double_quote;
	break;
      case double_quote:
	s = normal;
	break;
      default:
	break;
      }
      break;
    case '\\':
      switch (s) {
      case normal:
	s = backslash;
	break;
      case single_quote:
	s = squote_backslash;
	break;
      case double_quote:
	s = dquote_backslash;
	break;
      case squote_backslash:
	s = single_quote;
	break;
      case dquote_backslash:
	s = double_quote;
	break;
      case backslash:
	s = normal;
	break;
      case comment:
      case scomment:
	break;
      }
      break;
    case '/':
      if (s == normal) {
	if (line[i + 1] == '*') {
	  s = comment;
	  break;
	}
	else if (line[i + 1] == '/') {
	  s = scomment;
	  break;
	}
      }
      else if (s == comment && i > 0 && line[i - 1] == '*') {
	s = normal;
	break;
      }
      /* fall through */
    default:
      switch (s) {
      case squote_backslash:
	s = single_quote;
	break;
      case dquote_backslash:
	s = double_quote;
	break;
      case backslash:
	s = normal;
	break;
      default:
	break;
      }
    }
  }

  return s;
}

/** Determine the state of the input buffer
 * @param s		current state (input/output)
 * @param line		command line to be scanned (continuation or new line)
 * @return		strlen (line)
 */
static unsigned
getstate (enum state* s, const char* line) {
  unsigned length = strlen (line);
  if ((*s = scanstate (*s, line, length)) == scomment)
    *s = normal;
  return length;
}

/** Append a line to input read so far
 * @param line		first line of input
 * @param cont		the continuation line
 * @param prefix	error message prefix
 * @return		state of the last character in the input
 */
enum state
append (char** line, char* cont, const char* prefix)
{
  enum state s = normal;
  unsigned i = *line ? getstate (&s, *line) : 0;
  if (!cont) {
    /* EOF at beginning of line */
    if (prefix)
      fputc ('\n', stderr);
    switch (s) {
    case single_quote:
    case squote_backslash:
      error (prefix, "unexpected EOF while looking for matching `''");
      break;
    case double_quote:
    case dquote_backslash:
      error (prefix, "unexpected EOF while looking for matching `\"'");
      break;
    case comment:
      error (prefix, "unexpected EOF while looking for matching `*/'");
      break;
    case backslash:
      error (prefix, "unexpected EOF after `\\'");
      break;
    case normal:
    case scomment:
      return s;
    }
    **line = 0;
    return normal;
  }
  else if (!*line) {
    *line = cont;
    getstate (&s, cont);
  }
  else {
    unsigned j;
    char* newline;
    switch (s) {
    case normal:
    case comment:
    case single_quote:
    case double_quote:
      (*line)[i++] = '\n';
      break;
    case squote_backslash:
      s = single_quote;
      i--;
      break;
    case dquote_backslash:
      s = double_quote;
      i--;
      break;
    case backslash:
      s = normal;
      /* fall through */
    default:
      i--;
      break;
    }

    j = getstate (&s, cont);
    cont[j] = 0;

    if ((newline = realloc (*line, i + j + 1))) {
      memcpy ((*line = newline) + i, cont, j + 1);
      i += j;
    }
    else
      **line = 0;
    free (cont);
  }

  return s;
}

const char*
cmdline (const char* prompt, const char* prompt2)
{
#ifndef __WIN32
  fd_set fds;
# ifndef NDEBUG
  int i;
# endif /* NDEBUG */
#endif /* !__WIN32 */
  if (!terminal.cont && terminal.line)
    free (terminal.line), terminal.line = 0;
#ifndef __WIN32
  if (!file.cont && file.line)
    free (file.line), file.line = 0;
#endif /* !__WIN32 */

  for (;;) {
    if (!line_pending) {
      line_pending = 1;
#ifdef HAS_READLINE
      rl_callback_handler_install ((char*) (terminal.cont ? prompt2 : prompt),
				   readline_callback);
#else /* HAS_READLINE */
      fputs (terminal.cont ? prompt2 : prompt, stderr);
      fflush (stderr);
#endif /* HAS_READLINE */
    }
#ifdef __WIN32
    line_pending = 0;
    if (!(terminal.cont = append (&terminal.line, inputline (stdin),
				  "command")))
      return terminal.line;
#else /* __WIN32 */
    FD_ZERO (&fds);
    FD_SET (STDIN_FILENO, &fds);
    FD_SET (visfd, &fds);
    if (-1 == (
# ifndef NDEBUG
	       i =
# endif /* NDEBUG */
	       select (visfd + 1, &fds, 0, 0, 0))) {
      if (errno == EINTR)
	continue;
      perror ("select");
      return 0;
    }
    assert (i > 0);
    if (visfd && FD_ISSET (visfd, &fds)) {
      if (!(file.cont = append (&file.line, inputline (visf), 0)) &&
	  file.line)
	return file.line;
    }
    else {
# ifdef HAS_READLINE
      rl_callback_read_char ();
      if (!line_pending) {
	if (!(terminal.cont = append (&terminal.line, readline_line,
				      rl_readline_name))) {
	  if (terminal.line)
	    add_history (terminal.line);
	  return terminal.line;
	}
      }
# else /* HAS_READLINE */
      line_pending = 0;
      if (!(terminal.cont = append (&terminal.line, inputline (stdin),
				    "command")))
	return terminal.line;
# endif /* HAS_READLINE */
    }
#endif /* __WIN32 */
  }
}

#ifdef HAS_READLINE
/** Determine whether a character in the command line is quoted
 * @param text		the command line
 * @param index		index to the character of interest
 */
static int
char_is_quoted (char* text, int index)
{
  return scanstate (normal, text, index) != normal;
}

/** Quote a string using double quotes
 * @param string	string to be quoted
 * @param rtype		flag: SINGLE_MATCH or MULT_MATCH
 * @param qcp		pointer to the quote character
 * @return		a new string
 */
static char*
quote_filename (char* string, int rtype, char *qcp)
{
  register int c;
  char *result, *r;

  if (*qcp != '"')
    *qcp = '"';

  result = (char*) malloc (3 + (2 * strlen (string)));
  r = result;
  *r++ = '"';

  for (; (c = *string); string++) {
    switch (c) {
    case '"':
    case '\\':
      *r++ = '\\';
      /* fall through */
    default:
      *r++ = c;
      break;
    }
  }

  if (rtype != MULT_MATCH)
    *r++ = '"';
  *r = '\0';

  return result;
}

void
init_cmdline (const char* progname,
	      char** (*attempted_completer) (const char*, int, int),
	      char* (*entry_completer) (const char*, int))
{
  rl_readline_name = (char*) progname;
  rl_attempted_completion_function = attempted_completer;
  rl_completion_entry_function = entry_completer;
  rl_completer_quote_characters = "'\"";
  rl_filename_quote_characters =
    "\1\2\3\4\5\6\7\10\11\12\13\14\15\16\17"
    "\20\21\22\23\24\25\26\27\30\31\32\33\34\35\36\37"
    " !\"#$%&'()*+,-./[\\]^`{|}~\177"
    "\200\201\202\203\204\205\206\207"
    "\210\211\212\213\214\215\216\217"
    "\220\221\222\223\224\225\226\227"
    "\230\231\232\233\234\235\236\237"
    "\240\241\242\243\244\245\246\247"
    "\250\251\252\253\254\255\256\257"
    "\260\261\262\263\264\265\266\267"
    "\270\271\272\273\274\275\276\277"
    "\300\301\302\303\304\305\306\307"
    "\310\311\312\313\314\315\316\317"
    "\320\321\322\323\324\325\326\327"
    "\330\331\332\333\334\335\336\337"
    "\340\341\342\343\344\345\346\347"
    "\350\351\352\353\354\355\356\357"
    "\360\361\362\363\364\365\366\367"
    "\370\371\372\373\374\375\376\377";
  rl_basic_word_break_characters =
    " \t\n!\"#$%&'()*+,-./:;<=>?@[]^`{|}~";
  rl_basic_quote_characters = "\"'";
  rl_char_is_quoted_p = char_is_quoted;
  rl_filename_quoting_function = quote_filename;
}
#endif /* HAS_READLINE */
