/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*- */
/*
 * trace.c
 * Copyright (C) Canonical LTD 2011
 * 
 * Author: Robert Carr <racarr@canonical.com>
 * 
unity-webapps is free software: you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published
 * by the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * unity-webapps 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 Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.";
 */

#include <string.h>
#include <glib/gstdio.h>

#include "uwa-trace.h"

static gchar *uwa_trace_substitute_variables (UwaTrace *trace,
											  const gchar *input_line);



struct _UwaTrace {
  gchar *name;
  
  gint index;
  GList *traces;
  GList *matches;
  
  GHashTable *variables;
};

typedef struct _UwaTracePoint {
  gchar *line_match;
  gchar *conditional;
  gchar *post_action;
} UwaTracePoint;

#include "uwa-macros.h"

static gchar *
extract_conditional_from_line (const gchar *trace_line)
{
  GRegex *conditional_regex;
  GMatchInfo *conditional_match_info;
  gchar *matched_conditional, *extracted_conditional;
  gint matched_conditional_len;
  gboolean has_conditional;
  
  conditional_regex = g_regex_new("^<\\?(.*)\\?>", 0, 0, NULL);
  
  has_conditional = g_regex_match (conditional_regex, trace_line, 0, 
								   &conditional_match_info);
  
  if (has_conditional == FALSE)
	{
	  g_regex_unref (conditional_regex);
	  
	  return NULL;
	}

  matched_conditional = g_match_info_fetch (conditional_match_info, 0);
  matched_conditional_len = strlen (matched_conditional);
  
  extracted_conditional = g_strndup (matched_conditional +2, matched_conditional_len - 4);

  g_free (matched_conditional);
  g_match_info_free (conditional_match_info);
  g_regex_unref (conditional_regex);
  
  return extracted_conditional;
}

static gchar *
extract_post_action_from_line (const gchar *trace_line)
{
  GRegex *post_action_regex;
  GMatchInfo *post_action_match_info;
  gchar *matched_action, *extracted_action;
  gint matched_action_len;
  gboolean has_action;
  
  post_action_regex = g_regex_new("<\\|(.*)\\|>$", 0, 0, NULL);
  
  has_action = g_regex_match (post_action_regex, trace_line, 0,
							  &post_action_match_info);
  
  if (has_action == FALSE)
	{
	  g_regex_unref (post_action_regex);
	  
	  return NULL;
	}
  
  matched_action = g_match_info_fetch (post_action_match_info, 0);
  matched_action_len = strlen (matched_action);
  
  extracted_action = g_strndup (matched_action + 2, matched_action_len - 4);
  
  g_free (matched_action);
  g_match_info_free (post_action_match_info);
  g_regex_unref (post_action_regex);
  
  return extracted_action;
}


static gboolean
parse_trace_line (const gchar *trace_line, gchar **line_match, gchar **conditional, gchar **action)
{
  gchar *extracted_conditional, *extracted_action;
  
  *conditional = *action = *line_match = NULL;
  
  extracted_conditional = extract_conditional_from_line (trace_line);

  if (extracted_conditional != NULL)
	{
	  *conditional = extracted_conditional;
	  trace_line += strlen (extracted_conditional) + 4;
	}
  
  extracted_action = extract_post_action_from_line (trace_line);
  
  if (extracted_action != NULL)
	{
	  *action = extracted_action;
	  *line_match = g_strndup (trace_line, strlen (trace_line) - strlen(extracted_action) - 4);
	}
  else
	{
	  *line_match = g_strdup (trace_line);
	}
  
  return TRUE;
}

static gboolean
uwa_trace_point_has_conditional (UwaTracePoint *trace_point)
{
  return (trace_point->conditional != NULL);
}

static gboolean
uwa_trace_point_conditionals_satisfied (UwaTracePoint *trace_point,
										UwaTrace *trace)
{
  
  if (uwa_trace_point_has_conditional (trace_point) == FALSE)
	{
	  return TRUE;
	}
  
  if (g_list_find_custom (trace->matches, trace_point->conditional, (GCompareFunc)g_strcmp0) != NULL)
	{
	  return TRUE;
	}
  
  return FALSE;
}

static gboolean
uwa_trace_point_do_post_action (UwaTrace *trace, UwaTracePoint *trace_point)
{
  gchar *command_line, *substituted_post_action;
  gint exit_status;
  GError *error;
  
  if (trace_point->post_action == NULL)
	{
	  return TRUE;
	}
  
  substituted_post_action = uwa_trace_substitute_variables (trace, trace_point->post_action);
  
  command_line = g_strdup_printf("test-runner/post-actions/%s", substituted_post_action);
  
  printf("Doing post action: %s \n", command_line);
  
  error = NULL;
  
  g_spawn_command_line_sync (command_line, NULL, NULL, &exit_status, &error);
  
  if (error != NULL)
	{
	  g_error ("Error spawning trace point post action: %s", error->message);
	  
	  g_error_free (error);
	  
	  g_free (command_line);
	  g_free (substituted_post_action);
	  
	  return FALSE;
	}
  
  if (exit_status != 0)
	{
	  g_error ("Trace point post action returned non zero exit status (%d)", exit_status);

	  g_free (command_line);
	  g_free (substituted_post_action);
	  
	  return FALSE;
	}

  g_free (command_line);
  g_free (substituted_post_action);
  
  return TRUE;  
}

static gboolean
uwa_trace_point_extract_named_variables (UwaTrace *trace, UwaTracePoint *trace_point, GMatchInfo *match_info)
{
  GRegex *named_match_regex;
  GMatchInfo *named_match_info;
  gchar *identifier, *original_match;
  gboolean matched;
  GError *error;
  
  error = NULL;
  
  // TODO: FIXME: Support multiple matches
  named_match_regex = g_regex_new ("\\?<(?P<identifier>[a-zA-Z]+)>", 0, 0, &error);
  
  if (error != NULL)
	{
	  g_error ("Error compiling named match regex: %s", error->message);
	  
	  g_error_free (error);
	  
	  return FALSE;
	}
  
  matched = g_regex_match (named_match_regex, trace_point->line_match, 0, &named_match_info);
  
  if (matched == FALSE)
	{
	  g_regex_unref (named_match_regex);
	  
	  return TRUE;
	}
  
  identifier = g_match_info_fetch_named (named_match_info, "identifier");
  original_match = g_match_info_fetch_named (match_info, identifier);
  
  g_hash_table_insert (trace->variables, identifier, original_match);
  
  g_match_info_free (named_match_info);
  g_regex_unref (named_match_regex);

  
  return TRUE;
}



static UwaTracePoint *
uwa_trace_point_new (const gchar *trace_line)
{
  UwaTracePoint *trace_point;
  
  trace_point = g_malloc0 (sizeof (UwaTracePoint));
  
  if (parse_trace_line (trace_line, &(trace_point->line_match), &(trace_point->conditional), &(trace_point->post_action)) == FALSE)
	{
	  g_error ("Error parsing trace line: %s", trace_line);
	  
	  return NULL;
	}
  
  return trace_point;
}

static void
uwa_trace_point_free (UwaTracePoint *trace_point)
{
  g_free (trace_point->line_match);
  
  g_free (trace_point);
}

static gchar *
macro_get_identifier (const gchar *trace_line)
{
  return g_strndup (trace_line + 2, strlen (trace_line) - 4);
}

static gchar *
substitute_macro_line (const gchar *trace_line)
{
  gchar *substitution;
  gchar *identifier;
  gint i;
  
  identifier = macro_get_identifier (trace_line);
  
  substitution = NULL;
  
  for (i = 0; i < G_N_ELEMENTS (trace_macros); i++)
	{
	  if (g_strcmp0 (identifier, trace_macros[i].identifier) == 0)
		{
		  substitution = g_strdup (trace_macros[i].substitution);
		}
	}
  
  g_free (identifier);
  
  return substitution;
}

static gchar *
preprocess_trace (const gchar *trace)
{
  GString *preprocessed_trace;
  gchar **components, *retval;
  gint i, len;
  
  components = g_strsplit (trace, "\n", -1);
  
  len = g_strv_length (components);
  
  g_assert (len);
  
  preprocessed_trace = g_string_new (NULL);
  
  for (i = 0; i < len; i++)
	{
	  const gchar *trace_line;
	  
	  trace_line = components[i];
	  
	  if (strlen (trace_line) == 0)
		{
		  continue;
		}
	  
	  if (g_str_has_prefix (trace_line, "<!"))
		{
		  gchar *substituted_line;
		  
		  substituted_line = substitute_macro_line (trace_line);
		  
		  if (substituted_line == NULL)
			{
			  g_error ("No macro substitution found for: %s", substituted_line);
			}
		  
		  g_string_append (preprocessed_trace, substituted_line);
		  
		  g_free (substituted_line);
		  
		}
	  else
		{
		  g_string_append (preprocessed_trace, components[i]);
		  g_string_append (preprocessed_trace, "\n");
		}
	}
  
  retval = preprocessed_trace->str;
  
  g_string_free (preprocessed_trace, FALSE);
  g_strfreev (components);
  
  return retval;
}

static gchar *
strip_trace (const gchar *trace, const gchar *channel)
{
  gchar *header, *footer, *stripped;
  const gchar *header_loc, *footer_loc, *channel_loc;
  
  header = g_strdup_printf("<[%s]>", channel);

  stripped = NULL;
  footer = NULL;
  
  header_loc = strstr (trace, header);
  
  if (header_loc == NULL)
	{
	  goto out;
	}
  
  g_assert ( ((header_loc-trace)+strlen(header)) < strlen(trace) );
  
  footer = g_strdup_printf("</[%s]>", channel);
  
  footer_loc = strstr (trace, footer);
  
  if (footer_loc == NULL)
	{
	  goto out;
	}
  
  channel_loc = header_loc + strlen (header) + 1;
  
  size_t stripped_size = footer_loc > channel_loc
	  ? (footer_loc-channel_loc)-1
	  : 0;
  stripped = g_strndup (channel_loc, stripped_size);
  
 out:
  g_free (header);
  g_free (footer);
  
  return stripped;
}

static gchar *
get_trace_file_contents (UwaTrace *trace, const gchar *channel)
{
	gchar *trace_file_name, *contents, *stripped_trace, *processed_trace;
  GError *error;
  
  trace_file_name = g_strdup_printf("%s.trace", trace->name);
  
  contents = NULL;
  error = NULL;
  
  g_file_get_contents (trace_file_name, &contents,
					   NULL, &error);
  
  if (error != NULL)
	{
	  g_warning ("Failed to load trace file: %s", error->message);
	  
	  g_error_free (error);
	  
	  return NULL;
	}
  
  stripped_trace = strip_trace (contents, channel);
  
  processed_trace = preprocess_trace (stripped_trace);
  
  g_free (contents);
  g_free (trace_file_name);
  g_free (stripped_trace);
  
  return processed_trace;
}

static gboolean
load_test_metadata (UwaTrace *trace, const gchar *channel)
{
  gchar *trace_contents, **components;
  gint i, length;
  
  trace_contents = get_trace_file_contents (trace, channel);
  
  components = g_strsplit (trace_contents, "\n", -1);

  g_assert (components);
  
  length = g_strv_length (components);
  
  if (length == 0)
	{
	  g_warning ("No trace data");
	  
	  return FALSE;
	}
  
  for (i = 0; i < length; i++)
	{
	  UwaTracePoint *trace_point;
	  
	  if (strlen(components[i]) == 0)
		{
		  continue;
		}
	  
	  trace_point = uwa_trace_point_new (components[i]);
	  
	  if (trace_point == NULL)
		{
		  g_error ("Error parsing trace point: %s", components[i]);
		  
		  return FALSE;
		}

	  trace->traces = g_list_append (trace->traces, trace_point);
	}
  
  g_free (components);
  g_free (trace_contents);
  
  return TRUE;
}

static gchar *
uwa_trace_substitute_variables (UwaTrace *trace,
								const gchar *input_line)
{
  GRegex *substitution_regex;
  GMatchInfo *match_info;
  gchar *identifier, *substitution, *substituted;
  gboolean matched;
  GError *error;
  
  error = NULL;
  
  // TODO: FIXME: Support multiple variables per line.
  substitution_regex = g_regex_new("\\$\\{(?P<identifier>[a-zA-Z]+)\\}", 0, 0, &error);
  
  if (error != NULL)
	{
	  g_error ("Error compiling substitution regex: %s", error->message);
	  
	  g_error_free (error);
	  
	  return NULL;
	}
  
  matched = g_regex_match (substitution_regex, input_line, 0, &match_info);
  
  if (matched == FALSE)
	{
	  g_regex_unref (substitution_regex);
	  
	  return g_strdup (input_line);
	}
  
  substituted = NULL;
  
  identifier = g_match_info_fetch_named (match_info, "identifier");
  substitution = g_hash_table_lookup (trace->variables, identifier);
  
  if (substitution == NULL)
	{
	  printf("No substitution for: %s \n", identifier);
	  goto out;
	}
  
  error = NULL;

  substituted = g_regex_replace (substitution_regex,
								 input_line,
								 -1,
								 0,
								 substitution,
								 0, &error);
  
  if (error != NULL)
	{
	  g_error ("Error applying substitution regex: %s", error->message);
	  g_error_free (error);
	  
	  substituted = NULL;
	}
								 
  
 out:
  g_free (identifier);
  g_match_info_free (match_info);
  g_regex_unref (substitution_regex);
  
  return substituted;
}



UwaTrace *
uwa_trace_new_for_test_name (const gchar *test_name, const gchar *channel)
{
  UwaTrace *trace;
  
  trace = g_malloc0 (sizeof (UwaTrace));
  
  trace->name = g_strdup (test_name);
  
  trace->index = 0;

  trace->traces = NULL;
  trace->matches = NULL;
  
  trace->variables = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
  
  if (load_test_metadata (trace, channel) == FALSE)
	{
	  return NULL;
	}
  
  return trace;
}

static void
trace_list_foreach_free (gpointer data, gpointer user_data)
{
  uwa_trace_point_free (data);
}

static void
match_list_foreach_free (gpointer data, gpointer user_data)
{
  g_free (data);
}

void
uwa_trace_free (UwaTrace *trace)
{
  g_list_foreach (trace->traces, trace_list_foreach_free, NULL);
  g_list_foreach (trace->matches, match_list_foreach_free, NULL);
  
  g_list_free (trace->traces);
  g_list_free (trace->matches);

  g_hash_table_destroy (trace->variables);

  g_free (trace->name);
  g_free (trace);
}

gboolean
trace_line_satisfies_conditionals (UwaTrace *trace, const gchar *trace_line)
{
  GRegex *conditional_regex;
  GMatchInfo *conditional_match_info;
  gchar *conditional;
  gint conditional_len;
  gboolean has_conditional, satisfied;
  
  conditional_regex = g_regex_new ("^<\?(.*)\?>", 0, 0, NULL);
  
  g_assert (conditional_regex);
  
  has_conditional = g_regex_match (conditional_regex, trace_line, 0,
								   &conditional_match_info);
  
  if (has_conditional == FALSE)
	{
	  g_regex_unref (conditional_regex);

	  return TRUE;
	}
  
  conditional = g_match_info_fetch (conditional_match_info, 0);
  
  conditional_len = strlen (conditional);
  
  conditional = conditional + 2; // Skip the <?
  conditional[conditional_len-2] = '\0'; // End the string where the ending ? was.
  
  satisfied = FALSE;
  
  if (g_list_find_custom (trace->matches, conditional, (GCompareFunc)g_strcmp0) != NULL)
	{
	  satisfied = TRUE;
	}
  conditional = conditional - 2;

  g_free (conditional);
  g_match_info_free (conditional_match_info);
  g_regex_unref (conditional_regex);
  
  return satisfied;
}

gboolean
trace_line_has_conditional (const gchar *trace_line)
{
  return g_str_has_prefix (trace_line, "<?");
}

gboolean
uwa_trace_process_line (UwaTrace *trace, const gchar *line)
{
  GRegex *trace_regex;
  GMatchInfo *match_info;
  UwaTracePoint *trace_point;
  const gchar *current_trace_match;
  gboolean matched;
  GError *error;
  
  if (trace->index >= g_list_length (trace->traces))
	{
	  return TRUE;
	}


  trace_point = (UwaTracePoint *) g_list_nth_data (trace->traces, trace->index);
  
  if (uwa_trace_point_has_conditional (trace_point))
	{
	  if (uwa_trace_point_conditionals_satisfied (trace_point, trace) == FALSE)
		{
		  trace->index++;
		  
		  if (trace->index >= g_list_length (trace->traces))
			{
			  return TRUE;
			}
		  
		  trace_point = (UwaTracePoint *) g_list_nth_data (trace->traces, trace->index);
		}
	}

  current_trace_match = trace_point->line_match;
  
  error = NULL;
  trace_regex = g_regex_new (current_trace_match, 0, 0, &error);
  
  if (error != NULL)
	{
	  g_error ("Error compiling trace regex: %s", error->message);
	  
	  g_error_free (error);
	  
	  return FALSE;
	}
  
  matched = g_regex_match (trace_regex, line, 0, &match_info);
  
  if (matched)
	{
	  gchar *match;
	  
	  match = g_match_info_fetch (match_info, 0);
	  
	  trace->matches = g_list_append (trace->matches, match);	  

	  uwa_trace_point_extract_named_variables (trace, trace_point, match_info);
	  
	  trace->index++;
	  
	  if (uwa_trace_point_do_post_action (trace, trace_point) == FALSE)
		{
		  g_error ("Error executing post trace action for trace point");
		  
		  g_match_info_free (match_info);
		  g_regex_unref (trace_regex);
		  
		  return FALSE;
		}
	}
  
  g_match_info_free (match_info);
  
  g_regex_unref (trace_regex);
  
  return TRUE;
}

gboolean
uwa_trace_passed (UwaTrace *trace, gboolean print_next)
{
  UwaTracePoint *trace_point;

  if (trace->index >= g_list_length (trace->traces))
	{
	  return TRUE;
	}
  
  trace_point = (UwaTracePoint *)g_list_nth_data (trace->traces, trace->index);
  
  if (print_next)
	{
	  printf ("Trace failed, next pending match: %s (conditional: %s)\n", trace_point->line_match, trace_point->conditional);
	}
  
  return FALSE;
}
