/*
 * 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;

  SKIP_ASCII_WS(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;

  SKIP_ASCII_WS(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 (IS_ASCII_WS(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;

  SKIP_ASCII_WS(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;
  SKIP_ASCII_WS(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;
}
