/*
 * Copyright (C) 2020 Kevin J. McCarthy <kevin@8t8.us>
 *
 *     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., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */

#if HAVE_CONFIG_H
# include "config.h"
#endif

#include "mutt.h"
#include "mutt_lisp.h"

#include <ctype.h>
#include <stdlib.h>

static int read_backticks (BUFFER *dest, BUFFER *line)
{
  char ch;
  int rc = -1, matched = 0;

  if (*line->dptr != '`')
    return -1;

  mutt_buffer_addch (dest, *line->dptr++);

  while (*line->dptr && !matched)
  {
    ch = *line->dptr;

    if (ch == '`')
      matched = 1;
    else if (ch == '\\')
    {
      mutt_buffer_addch (dest, *line->dptr++);
      if (!*line->dptr)
        break;
    }

    mutt_buffer_addch (dest, *line->dptr++);
  }

  if (matched)
    rc = 0;
  else
  {
    /* L10N:
       Printed when backticks in MuttLisp are not matched.
    */
    mutt_error (_("MuttLisp: unclosed backticks: %s"), mutt_b2s (line));
  }

  return rc;
}

static int read_list (BUFFER *list, BUFFER *line)
{
  int rc = -1, level = 0;
  char ch, quotechar = 0;

  mutt_buffer_clear (list);
  if (!line->dptr || !*line->dptr)
    return 0;

  SKIPWS (line->dptr);

  if (*line->dptr != '(')
    return -1;

  mutt_buffer_addch (list, *line->dptr++);
  level = 1;

  while (*line->dptr && level)
  {
    ch = *line->dptr;

    if (ch == quotechar)
      quotechar = 0;
    else if (ch == '\\' && quotechar != '\'')
    {
      mutt_buffer_addch (list, *line->dptr++);
      if (!*line->dptr)
        break;
    }
    else if (ch == '`' && (!quotechar || quotechar == '"'))
    {
      read_backticks (list, line);
      continue;
    }
    else if (!quotechar)
    {
      if (ch == '"' || ch == '\'')
        quotechar = ch;
      else if (ch == '(')
        level++;
      else if (ch == ')')
        level--;
    }

    mutt_buffer_addch (list, *line->dptr++);
  }

  if (level == 0)
    rc = 0;
  else
  {
    /* L10N:
       Printed when a MuttLisp list is missing a matching closing
       parenthesis.  For example: (a
    */
    mutt_error (_("MuttLisp: unclosed list: %s"), mutt_b2s (line));
  }

  return rc;
}

static int read_atom (BUFFER *atom, BUFFER *line)
{
  char ch, quotechar = 0;
  int rc = 0;

  mutt_buffer_clear (atom);
  if (!line->dptr || !*line->dptr)
    return 0;

  SKIPWS (line->dptr);

  while (*line->dptr)
  {
    ch = *line->dptr;

    if (ch == quotechar)
      quotechar = 0;
    else if (ch == '\\' && quotechar != '\'')
    {
      mutt_buffer_addch (atom, *line->dptr++);
      if (!*line->dptr)
        break;
    }
    else if (ch == '`' && (!quotechar || quotechar == '"'))
    {
      rc = read_backticks (atom, line);
      continue;
    }
    else if (!quotechar)
    {
      if (ISSPACE (ch) || ch == '(' || ch == ')')
        break;
      if (ch == '"' || ch == '\'')
        quotechar = ch;
    }

    mutt_buffer_addch (atom, *line->dptr++);
  }

  return rc;
}

/* returns -1 on an illegal sexp
 *         0 end of line (denoted by an empty sexp)
 *         1 valid sexp read in
 */
static int read_sexp (BUFFER *sexp, BUFFER *line)
{
  int rc = 0;

  mutt_buffer_clear (sexp);
  if (!line->dptr || !*line->dptr)
    return 0;

  SKIPWS (line->dptr);

  if (*line->dptr == '(')
    rc = read_list (sexp, line);
  else if (*line->dptr)
    rc = read_atom (sexp, line);

  if (rc == 0 && mutt_buffer_len (sexp))
    rc = 1;

  return rc;
}

/* note that a valid sexp might have been read in, but expand to an empty one.
 *
 * returns -1 on an illegal sexp
 *         0 end of line (denoted by an empty sexp)
 *         1 valid sexp read in
 */
static int read_eval_sexp (BUFFER *sexp, BUFFER *line)
{
  int rc = -1;
  BUFFER *temp_sexp = NULL;

  mutt_buffer_clear (sexp);
  if (!line->dptr || !*line->dptr)
    return 0;

  temp_sexp = mutt_buffer_new ();
  mutt_buffer_increase_size (temp_sexp, mutt_buffer_len (line));

  rc = read_sexp (temp_sexp, line);
  if (rc <= 0)
    goto cleanup;

  mutt_buffer_rewind (temp_sexp);

  /* We send evaluation back through the muttrc parser.
   * Variables/expressions will be handled there, while list evaluation
   * will be done by calling mutt_lisp_eval_list().
   *
   * If we ever decide to add types (e.g. lists or variables), we
   * could instead directly handle their evaluation internally.  The
   * external mutt_lisp_eval_list() would always return the string
   * form, while the internal version could handle more.
   */
  if (mutt_extract_token (sexp, temp_sexp,
                          MUTT_TOKEN_LISP | MUTT_TOKEN_COMMENT |
                          MUTT_TOKEN_SEMICOLON) != 0)
  {
    rc = -1;
    goto cleanup;
  }

  /* It's possible mutt_extract_token() did not process the entire
   * temp_sexp.  For instance backticks might have generated multiple
   * words of output that weren't all processed. Prepend that to line
   * so it is processed later.
   */
  if (*temp_sexp->dptr)
  {
    BUFFER *extra = mutt_buffer_pool_get ();
    mutt_buffer_strcpy (extra, temp_sexp->dptr);
    mutt_buffer_addstr (extra, line->dptr);
    mutt_buffer_strcpy (line, mutt_b2s (extra));
    mutt_buffer_rewind (line);
    mutt_buffer_pool_release (&extra);
  }

  rc = 1;

cleanup:
  mutt_buffer_free (&temp_sexp);
  return rc;
}

static int lisp_concat (BUFFER *result, BUFFER *list)
{
  BUFFER *arg = NULL;
  int rc;

  mutt_buffer_clear (result);

  arg = mutt_buffer_new ();

  rc = read_eval_sexp (arg, list);
  while (rc > 0)
  {
    mutt_buffer_addstr (result, mutt_b2s (arg));
    rc = read_eval_sexp (arg, list);
  }

  mutt_buffer_free (&arg);

  return rc;
}

static int lisp_quote (BUFFER *result, BUFFER *list)
{
  mutt_buffer_strcpy (result, list->dptr);
  return 0;
}

static int lisp_equal (BUFFER *result, BUFFER *list)
{
  BUFFER *first_arg = NULL, *arg = NULL;
  int rc;

  mutt_buffer_clear (result);
  mutt_buffer_addch (result, 't');

  first_arg = mutt_buffer_new ();
  rc = read_eval_sexp (first_arg, list);
  if (rc <= 0)
    goto cleanup;

  arg = mutt_buffer_new ();
  rc = read_eval_sexp (arg, list);
  while (rc > 0)
  {
    if (mutt_strcmp (mutt_b2s (first_arg), mutt_b2s (arg)))
    {
      mutt_buffer_clear (result);
      break;
    }
    rc = read_eval_sexp (arg, list);
  }

cleanup:
  mutt_buffer_free (&first_arg);
  mutt_buffer_free (&arg);

  return rc;
}

static int lisp_not (BUFFER *result, BUFFER *list)
{
  BUFFER *arg = NULL;
  int rc;

  mutt_buffer_clear (result);

  arg = mutt_buffer_new ();

  rc = read_eval_sexp (arg, list);
  if (rc > 0 && !mutt_buffer_len (arg))
    mutt_buffer_addstr (result, "t");

  mutt_buffer_free (&arg);

  return rc;
}

static int lisp_and (BUFFER *result, BUFFER *list)
{
  BUFFER *arg = NULL;
  int rc;

  mutt_buffer_clear (result);
  mutt_buffer_addch (result, 't');

  arg = mutt_buffer_new ();

  rc = read_eval_sexp (arg, list);
  while (rc > 0)
  {
    mutt_buffer_strcpy (result, mutt_b2s (arg));
    if (!mutt_buffer_len (result))
      break;
    rc = read_eval_sexp (arg, list);
  }

  mutt_buffer_free (&arg);

  return rc;
}

static int lisp_or (BUFFER *result, BUFFER *list)
{
  BUFFER *arg = NULL;
  int rc;

  mutt_buffer_clear (result);

  arg = mutt_buffer_new ();

  rc = read_eval_sexp (arg, list);
  while (rc > 0)
  {
    mutt_buffer_strcpy (result, mutt_b2s (arg));
    if (mutt_buffer_len (result))
      break;
    rc = read_eval_sexp (arg, list);
  }

  mutt_buffer_free (&arg);

  return rc;
}

static int lisp_if (BUFFER *result, BUFFER *list)
{
  BUFFER *cond = NULL;
  int rc;

  mutt_buffer_clear (result);

  cond = mutt_buffer_new ();

  rc = read_eval_sexp (cond, list);
  if (rc <= 0)
  {
    /* L10N:
       An error printed for the 'if' function if the condition is missing.
       For example (if) by itself.
    */
    mutt_error (_("MuttLisp: missing if condition: %s"), mutt_b2s (list));
    goto cleanup;
  }

  if (!mutt_buffer_len (cond))
    read_sexp (result, list);
  rc = read_eval_sexp (result, list);

cleanup:
  mutt_buffer_free (&cond);

  return rc;
}

typedef struct
{
  char *name;
  int (*func) (BUFFER *, BUFFER *);
} lisp_func_t;

const lisp_func_t LispFunctions[] = {
  {"concat",       lisp_concat},
  {"quote",        lisp_quote},
  {"equal",        lisp_equal},
  {"not",          lisp_not},
  {"and",          lisp_and},
  {"or",           lisp_or},
  {"if",           lisp_if},
  {NULL,           NULL}
};

static int eval_function (BUFFER *result, const char *func, BUFFER *list)
{
  int i, rc = -1;

  for (i = 0; LispFunctions[i].name; i++)
  {
    if (!mutt_strcmp (func, LispFunctions[i].name))
    {
      rc = LispFunctions[i].func (result, list);
      break;
    }
  }
  if (!LispFunctions[i].name)
  {
    /* L10N:
       Printed when a function is called that is not recognized by MuttLisp.
    */
    mutt_error (_("MuttLisp: no such function %s"), NONULL (func));
  }

  return rc;
}

int mutt_lisp_eval_list (BUFFER *result, BUFFER *line)
{
  int rc = -1;
  BUFFER *list = NULL, *function = NULL;

  mutt_buffer_clear (result);
  if (!line->dptr || !*line->dptr)
    return 0;

  list = mutt_buffer_new ();
  mutt_buffer_increase_size (list, mutt_buffer_len (line));

  if (read_list (list, line) != 0)
    goto cleanup;

  /* Rewind list and trim the outer parens */
  *(list->dptr - 1) = '\0';
  list->dptr = list->data + 1;

  function = mutt_buffer_new ();
  if (read_sexp (function, list) <= 0)
    goto cleanup;
  SKIPWS (list->dptr);

  if (eval_function (result, mutt_b2s (function), list) < 0)
    goto cleanup;

  rc = 0;

cleanup:
  mutt_buffer_free (&list);
  mutt_buffer_free (&function);
  return rc;
}
