/* icalparse.y -- icalendar (RFC 5545) parser
 *
 * This code is Copyright (c) 2014, by the authors of nmh.  See the
 * COPYRIGHT file in the root directory of the nmh distribution for
 * complete copyright information.
 */

%{
    /*
     * Use these yy* #defines, instead of the -p command line
     * option, to allow multiple parsers in a program.  yyval
     * is generated by Solaris yacc and is of type YYSTYPE.
     * All other yy* symbols are data of a built-in type and
     * are initialized by yyparse(), so they can be shared
     * between different parsers.
     */
#define yydebug icaldebug
#define yyerror icalerror
#define yylex   icallex
#define yylval  icallval
#define yyval   icalval
#define yyparse icalparse
#define YYDEBUG 1
#define YYERROR_DEBUG
#define YYERROR_VERBOSE
#define YY_NO_LEAKS

    /*
     * To quiet compile warnings with Solaris yacc:
#ifdef sun
# define lint 1
# undef YYDEBUG
#endif
    */

#include "h/mh.h"
#include "h/icalendar.h"
#include "h/utils.h"

static char *append (contentline *, const char *, const size_t);
static void new_content_line (contentline **);
static void new_vevent (vevent *);
static void free_param_names (param_list *);
static void free_param_values (value_list *);
static int icalerror (const char *);
%}

%token ICAL_NAME ICAL_COLON ICAL_VALUE ICAL_CRLF ICAL_SEMICOLON
%token ICAL_PARAM_NAME ICAL_EQUAL ICAL_PARAM_VALUE ICAL_COMMA

%start contentline_list

%%

    /* Instead of rigorous definition, cheat based on fact that every
       icalbody line looks like a contentline.  And we don't need to
       parse values. */
contentline_list
    : contentline
    | contentline_list contentline

    /* contentline = name *(";" param ) ":" value CRLF */
contentline
    : ICAL_NAME {
          new_content_line (&vevents.last->contentlines);
          append (vevents.last->contentlines->last, $1, strlen ($1));
          vevents.last->contentlines->last->name = $1;
      } ICAL_COLON {
          append (vevents.last->contentlines->last, $3, strlen ($3));
      } ICAL_VALUE {
          append (vevents.last->contentlines->last, $5, strlen ($5));
          vevents.last->contentlines->last->value = $5;
      } ICAL_CRLF {
          append (vevents.last->contentlines->last, $7, strlen ($7));
          if (vevents.last->contentlines->cr_before_lf == CR_UNSET) {
              vevents.last->contentlines->cr_before_lf =
                  $7[0] == '\r'  ?  CR_BEFORE_LF  :  LF_ONLY;
          }
          /* END:VEVENT doesn't have a param_list so we don't need
             to check for it below. */
          if (vevents.last->contentlines->last->name  &&
              vevents.last->contentlines->last->value  &&
              ! strcasecmp (vevents.last->contentlines->last->name, "END")  &&
              ! strcasecmp (vevents.last->contentlines->last->value,
                            "VEVENT")) {
              new_vevent (&vevents);
          }
      }
    | ICAL_NAME {
          new_content_line (&vevents.last->contentlines);
          append (vevents.last->contentlines->last, $1, strlen ($1));
          vevents.last->contentlines->last->name = $1;
      } param_list ICAL_COLON {
          append (vevents.last->contentlines->last, $4, strlen ($4));
      } ICAL_VALUE {
          append (vevents.last->contentlines->last, $6, strlen ($6));
          vevents.last->contentlines->last->value = $6;
      } ICAL_CRLF {
          append (vevents.last->contentlines->last, $8, strlen ($8));
          if (vevents.last->contentlines->cr_before_lf == CR_UNSET) {
              vevents.last->contentlines->cr_before_lf =
                  $8[0] == '\r'  ?  CR_BEFORE_LF  :  LF_ONLY;
          }
      }

param_list
    : ICAL_SEMICOLON {
          append (vevents.last->contentlines->last, $1, strlen ($1));
      } param
    | param_list ICAL_SEMICOLON {
          append (vevents.last->contentlines->last, $2, strlen ($2));
      } param

    /* param = param-name "=" param-value *("," param-value) */
param
    : ICAL_PARAM_NAME {
          append (vevents.last->contentlines->last, $1, strlen ($1));
          add_param_name (vevents.last->contentlines->last, $1);
      } ICAL_EQUAL {
          append (vevents.last->contentlines->last, $3, strlen ($3));
      } param_value_list

param_value_list
    : ICAL_PARAM_VALUE {
          append (vevents.last->contentlines->last, $1, strlen ($1));
          add_param_value (vevents.last->contentlines->last, $1);
      }
    | param_value_list ICAL_COMMA {
          append (vevents.last->contentlines->last, $2, strlen ($2));
      } ICAL_PARAM_VALUE {
          append (vevents.last->contentlines->last, $4, strlen ($4));
          add_param_value (vevents.last->contentlines->last, $4);
      }

%%

/*
 * Remove the contentline node (by setting its name to NULL).
 */
void
remove_contentline (contentline *node) {
    free (node->name);
    node->name = NULL;
}

contentline *
add_contentline (contentline *node, const char *name) {
    contentline *new_node;

    NEW0(new_node);
    new_node->name  = mh_xstrdup (name);
    new_node->next = node->next;
    node->next = new_node;

    return new_node;
}

/*
 * Remove the value from a value_list.
 */
void
remove_value (value_list *node) {
    free (node->value);
    node->value = NULL;
}

/*
 * Find the contentline with the specified name, and optionally,
 * the specified value and/or parameter name.
 */
contentline *
find_contentline (contentline *contentlines, const char *name,
                  const char *val) {
    contentline *node;

    for (node = contentlines; node; node = node->next) {
        /* node->name will be NULL if the line was "deleted". */
        if (node->name  &&  ! strcasecmp (name, node->name)) {
            if (val  &&  node->value) {
                if (! strcasecmp (val, node->value)) {
                    return node;
                }
            } else {
                return node;
            }
        }
    }

    return NULL;
}

static char *
append (contentline *cline, const char *src, const size_t src_len) {
    if (src_len > 0) {
        const size_t len = cline->input_line_len + src_len;

        while (len >= cline->input_line_size) {
            cline->input_line_size = cline->input_line_size == 0
                ?  NMH_BUFSIZ
                :  2 * cline->input_line_size;
            cline->input_line =
                mh_xrealloc (cline->input_line, cline->input_line_size);
        }

        memcpy (cline->input_line + cline->input_line_len, src, src_len);
        cline->input_line[len] = '\0';
        cline->input_line_len = len;
    }

    return cline->input_line;
}

static void
new_content_line (contentline **cline) {
    contentline *new_node;

    NEW0(new_node);
    if (*cline) {
        /* Append the new node to the end of the list. */
        (*cline)->last->next = new_node;
    } else {
        /* First line:  save the root node in *cline. */
        *cline = new_node;
    }

    /* Only maintain the pointer to the last node in the root node. */
    (*cline)->last = new_node;
}

static void
new_vevent (vevent *event) {
    vevent *new_node, *node;

    NEW0(new_node);

    /* Append the new node to the end of the list. */
    for (node = event; node->next; node = node->next) { continue; }
    event->last =  node->next = new_node;
}

void
add_param_name (contentline *cline, char *name) {
    param_list *new_node;
    param_list *p;

    NEW0(new_node);
    new_node->param_name = name;

    if (cline->params) {
        for (p = cline->params; p->next; p = p->next) { continue; }
        /* The loop terminated at, not after, the last node. */
        p->next = new_node;
    } else {
        cline->params = new_node;
    }
}

/*
 * Add a value to the last parameter seen.
 */
void
add_param_value (contentline *cline, char *value) {
    value_list *new_node;
    param_list *p;
    value_list *v;

    NEW0(new_node);
    new_node->value = value;

    if (cline->params) {
        for (p = cline->params; p->next; p = p->next) { continue; }
        /* The loop terminated at, not after, the last param_list node. */

        if (p->values) {
            for (v = p->values; v->next; v = v->next) { continue; }
            /* The loop terminated at, not after, the last value_list node. */
            v->next = new_node;
        } else {
            p->values = new_node;
        }
    } else {
        /* Never should get here because a param value is always
           preceded by a param name. */
        free (new_node);
    }
}

void
free_contentlines (contentline *root) {
    contentline *i, *next;

    for (i = root; i; i = next) {
        free (i->name);
        if (i->params) {
            free_param_names (i->params);
        }
        free (i->value);
        free (i->input_line);
        charstring_free (i->unexpected);
        next = i->next;
        free (i);
    }
}

static void
free_param_names (param_list *p) {
    param_list *next;

    for ( ; p; p = next) {
        free (p->param_name);
        free_param_values (p->values);
        next = p->next;
        free (p);
    }
}

static void
free_param_values (value_list *v) {
    value_list *next;

    for ( ; v; v = next) {
        free (v->value);
        next = v->next;
        free (v);
    }
}

static int
icalerror (const char *error) {
    contentline *c;
    charstring_t context = NULL;

    /* Find last chunk of unexpected text. */
    for (c = vevents.last->contentlines; c; c = c->next) {
        if (c->unexpected) {
            context = c->unexpected;
        }
    }

    if (! strcmp ("syntax error, unexpected $end, expecting ICAL_NAME",
                  error)) {
        /* Empty input: produce no output. */
    } else {
        if (context) {
            inform ("%s after \"%s\"", error, charstring_buffer (context));
        } else {
            inform ("%s", error);
        }
        parser_status = -1;
        return -1;
    }

    return 0;  /* The return value isn't used anyway. */
}

/*
 * In case YYDEBUG is disabled above; mhical refers to icaldebug.
 */
#if ! defined YYDEBUG  ||  ! YYDEBUG
int icaldebug;
#endif /* ! YYDEBUG */
