/*
 * Copyright (C) 2009-2011 Julien BLACHE <jb@jblache.org>
 *
 * 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
 */

tree grammar DAAP2SQL;

options {
	tokenVocab = DAAP;
	ASTLabelType = pANTLR3_BASE_TREE;
	language = C;
}

@header {
	#include <stdio.h>
	#include <stdlib.h>
	#include <string.h>
	#include <limits.h>
	#include <errno.h>

	#include "logger.h"
	#include "db.h"
	#include "daap_query.h"
}

@members {
	struct dmap_query_field_map {
	  char *dmap_field;
	  char *db_col;
	  int as_int;
	};

	/* gperf static hash, daap_query.gperf */
	#include "daap_query_hash.h"
}

query	returns [ pANTLR3_STRING result ]
@init { $result = NULL; }
	:	e = expr
		{
			if (!$e.valid)
			{
				$result = NULL;
			}
			else
			{
				$result = $e.result->factory->newRaw($e.result->factory);
				$result->append8($result, "(");
				$result->appendS($result, $e.result);
				$result->append8($result, ")");
			}
		}
	;

expr	returns [ pANTLR3_STRING result, int valid ]
@init { $result = NULL; $valid = 1; }
	:	^(OPAND a = expr b = expr)
		{
			if ($a.valid && $b.valid)
			{
				$result = $a.result->factory->newRaw($a.result->factory);
				$result->append8($result, "(");
				$result->appendS($result, $a.result);
				$result->append8($result, " AND ");
				$result->appendS($result, $b.result);
				$result->append8($result, ")");
			}
			else if ($a.valid)
			{
				$result = $a.result->factory->newRaw($a.result->factory);
				$result->appendS($result, $a.result);
			}
			else if ($b.valid)
			{
				$result = $b.result->factory->newRaw($b.result->factory);
				$result->appendS($result, $b.result);
			}
			else
			{
				$valid = 0;
			}
		}
	|	^(OPOR a = expr b = expr)
		{
			if ($a.valid && $b.valid)
			{
				$result = $a.result->factory->newRaw($a.result->factory);
				$result->append8($result, "(");
				$result->appendS($result, $a.result);
				$result->append8($result, " OR ");
				$result->appendS($result, $b.result);
				$result->append8($result, ")");
			}
			else if ($a.valid)
			{
				$result = $a.result->factory->newRaw($a.result->factory);
				$result->appendS($result, $a.result);
			}
			else if ($b.valid)
			{
				$result = $b.result->factory->newRaw($b.result->factory);
				$result->appendS($result, $b.result);
			}
			else
			{
				$valid = 0;
			}
		}
	|	STR
		{
			pANTLR3_STRING str;
			pANTLR3_UINT8 field;
			pANTLR3_UINT8 val;
			pANTLR3_UINT8 escaped;
			ANTLR3_UINT8 op;
			int neg_op;
			const struct dmap_query_field_map *dqfm;
			char *end;
			long long llval;

			escaped = NULL;

			$result = $STR.text->factory->newRaw($STR.text->factory);

			str = $STR.text->toUTF8($STR.text);

			/* NOTE: the lexer delivers the string without quotes
			which may not be obvious from the grammar due to embedded code
			*/

			/* Make daap.songalbumid:0 a no-op */
			if (strcmp((char *)str->chars, "daap.songalbumid:0") == 0)
			{
				$result->append8($result, "1 = 1");

				goto STR_out;
			}

			field = str->chars;

			val = field;
			while ((*val != '\0') && ((*val == '.')
				|| (*val == '-')
				|| ((*val >= 'a') && (*val <= 'z'))
				|| ((*val >= 'A') && (*val <= 'Z'))
				|| ((*val >= '0') && (*val <= '9'))))
			{
				val++;
			}

			if (*field == '\0')
			{
				DPRINTF(E_LOG, L_DAAP, "No field name found in clause '\%s'\n", field);
				$valid = 0;
				goto STR_result_valid_0; /* ABORT */
			}

			if (*val == '\0')
			{
				DPRINTF(E_LOG, L_DAAP, "No operator found in clause '\%s'\n", field);
				$valid = 0;
				goto STR_result_valid_0; /* ABORT */
			}

			op = *val;
			*val = '\0';
			val++;

			if (op == '!')
			{
				if (*val == '\0')
				{
					DPRINTF(E_LOG, L_DAAP, "Negation found but operator missing in clause '\%s\%c'\n", field, op);
					$valid = 0;
					goto STR_result_valid_0; /* ABORT */
				}

				neg_op = 1;
				op = *val;
				val++;
			}
			else
				neg_op = 0;

			/* Lookup DMAP field in the query field map */
			dqfm = daap_query_field_lookup((char *)field, strlen((char *)field));
			if (!dqfm)
			{
				DPRINTF(E_LOG, L_DAAP, "DMAP field '\%s' is not a valid field in queries\n", field);
				$valid = 0;
				goto STR_result_valid_0; /* ABORT */
			}

			/* Empty values OK for string fields, NOK for integer */
			if (*val == '\0')
			{
				if (dqfm->as_int)
				{
					DPRINTF(E_LOG, L_DAAP, "No value given in clause '\%s\%s\%c'\n", field, (neg_op) ? "!" : "", op);
					$valid = 0;
					goto STR_result_valid_0; /* ABORT */
				}

				/* No need to exclude empty artist and album, as the server makes sure there always exists an artist/album. */
				if (neg_op && (op == ':' || op == '@')
					&& (strcmp((char *)field, "daap.songalbumartist") == 0 
						|| strcmp((char *)field, "daap.songartist") == 0 
						|| strcmp((char *)field, "daap.songalbum") == 0))
				{
					DPRINTF(E_DBG, L_DAAP, "Ignoring clause '\%s\%s\%c'\n", field, (neg_op) ? "!" : "", op);
					$valid = 0;
					goto STR_result_valid_0;
				}
				
				/* Need to check against NULL too */
				if (op == ':' || op == '@')
					$result->append8($result, "(");
			}

			/* Int field: check integer conversion */
			if (dqfm->as_int)
			{
				errno = 0;
				llval = strtoll((const char *)val, &end, 10);

				if (((errno == ERANGE) && ((llval == LLONG_MAX) || (llval == LLONG_MIN)))
					|| ((errno != 0) && (llval == 0)))
				{
					DPRINTF(E_LOG, L_DAAP, "Value '\%s' in clause '\%s\%s\%c\%s' does not convert to an integer type\n",
					val, field, (neg_op) ? "!" : "", op, val);
					$valid = 0;
					goto STR_result_valid_0; /* ABORT */
				}

				if (end == (char *)val)
				{
					DPRINTF(E_LOG, L_DAAP, "Value '\%s' in clause '\%s\%s\%c\%s' does not represent an integer value\n",
					val, field, (neg_op) ? "!" : "", op, val);
					$valid = 0;
					goto STR_result_valid_0; /* ABORT */
				}

				*end = '\0'; /* Cut out potential garbage - we're being kind */

				/* The server only has media_kind = 1 for music - so remove media_kind = 32 to imporve select query performance. */
				if (llval == 32
					&& (strcmp((char *)field, "com.apple.itunes.mediakind") == 0 
						|| strcmp((char *)field, "com.apple.itunes.extended-media-kind") == 0))
				{
					DPRINTF(E_DBG, L_DAAP, "Ignoring clause '\%s\%s\%c\%s'\n", field, (neg_op) ? "!" : "", op, val);
					
					if (neg_op)
						$result->append8($result, "1 = 1");
					else
						$result->append8($result, "1 = 0");
					
					goto STR_out;
				}
			}
			/* String field: escape string, check for '*' */
			else
			{
				/* With Apple Music 1.0.1.37, we observed some ':' queries using '@' instead, resulting in 'Unknown operator' errors */
				/* Ex: '/databases/1/containers?query=('com.apple.itunes.extended-media-kind@1', ...) */
				if (op != ':' && op != '@')
				{
					DPRINTF(E_LOG, L_DAAP, "Operation '\%c' not valid for string values\n", op);
					$valid = 0;
					goto STR_result_valid_0; /* ABORT */
				}

				escaped = (pANTLR3_UINT8)db_escape_string((char *)val);
				if (!escaped)
				{
					DPRINTF(E_LOG, L_DAAP, "Could not escape value\n");
					$valid = 0;
					goto STR_result_valid_0; /* ABORT */
				}

				val = escaped;

				if (val[0] == '*')
				{
					op = '\%';
					val[0] = '\%';
				}

				if (val[0] && val[1] && val[strlen((char *)val) - 1] == '*')
				{
					op = '\%';
					val[strlen((char *)val) - 1] = '\%';
				}
			}
			
			$result->append8($result, dqfm->db_col);

			switch(op)
			{
				case '@':
				case ':':
					if (neg_op)
						$result->append8($result, " <> ");
					else
						$result->append8($result, " = ");
					break;

				case '+':
					if (neg_op)
						$result->append8($result, " <= ");
					else
						$result->append8($result, " > ");
					break;

				case '-':
					if (neg_op)
						$result->append8($result, " >= ");
					else
						$result->append8($result, " < ");
					break;

				case '\%':
					$result->append8($result, " LIKE ");
					break;

				default:
					if (neg_op)
						DPRINTF(E_LOG, L_DAAP, "Missing or unknown operator '\%c' in clause '\%s!\%c\%s'\n", op, field, op, val);
					else
						DPRINTF(E_LOG, L_DAAP, "Unknown operator '\%c' in clause '\%s\%c\%s'\n", op, field, op, val);
					$valid = 0;
					goto STR_result_valid_0; /* ABORT */
					break;
			}

			if (!dqfm->as_int)
				$result->append8($result, "'");
	
			$result->append8($result, (const char *)val);
	
			if (!dqfm->as_int)
				$result->append8($result, "'");

			/* For empty string value, we need to check against NULL too */
			if ((*val == '\0') && (op == ':' || op == '@'))
			{
				if (neg_op)
					$result->append8($result, " AND ");
				else
					$result->append8($result, " OR ");

				$result->append8($result, dqfm->db_col);

				if (neg_op)
					$result->append8($result, " IS NOT NULL");
				else
					$result->append8($result, " IS NULL");

				$result->append8($result, ")");
			}

			STR_result_valid_0: /* bail out label */
				;

			if (escaped)
				free(escaped);

			STR_out: /* get out of here */
				;
		}
	;
