/*
 *  GNOME Logs - View and search logs
 *  Copyright (C) 2015  Lars Uebernickel <lars@uebernic.de>
 *
 *  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 3 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, see <http://www.gnu.org/licenses/>.
 */

#include "gl-journal-model.h"
#include "gl-journal.h"
#include "gl-util.h"

/* Details of match fields */
typedef struct GlQueryItem
{
    gchar *field_name;
    gchar *field_value;
    GlQuerySearchType search_type;
} GlQueryItem;

struct _GlRowEntry
{
    GObject parent_instance;

    GlJournalEntry *journal_entry;
    GlRowEntryType row_type;

    /* Number of compressed entries represented by
     * a header
     */
    guint compressed_entries;
};

struct _GlJournalModel
{
    GObject parent_instance;

    guint batch_size;

    GlJournal *journal;
    GPtrArray *entries;

    GlQuery *query;
    GPtrArray *token_array;

    guint n_entries_to_fetch;
    gboolean fetched_all;
    guint idle_source;

    guint compressed_entries_counter;

    /* export variable is used to identify when total number of entries,
     * to display, changes from 0 to a non zero number and only then
     * emit 'ENABLE_EXPORT'signal once instead of emitting it repeatedly
     * when new entries are continuously added */
    gboolean export;
};

enum
{
    DISABLE_EXPORT,
    ENABLE_EXPORT,
    LAST_SIGNAL
};

static guint signals[LAST_SIGNAL] = { 0 };

static void gl_journal_model_interface_init (GListModelInterface *iface);
static GPtrArray *tokenize_search_string (gchar *search_text);
static gboolean search_in_entry (GlJournalEntry *entry, GlJournalModel *model);
static gboolean gl_query_check_journal_end (GlQuery *query, GlJournalEntry *entry);
static gboolean gl_row_entry_check_message_similarity (GlRowEntry *current_row_entry,
                                                       GlRowEntry *prev_row_entry);
static void gl_journal_model_add_header (GlJournalModel *model);
static void gl_query_free (GlQuery *query);


G_DEFINE_TYPE (GlRowEntry, gl_row_entry, G_TYPE_OBJECT);

G_DEFINE_TYPE_WITH_CODE (GlJournalModel, gl_journal_model, G_TYPE_OBJECT,
                         G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, gl_journal_model_interface_init))

enum
{
    PROP_0,
    PROP_LOADING,
    N_PROPERTIES
};

/* We define these two enum values as 2 and 3 to avoid the
 * conflict with TRUE and FALSE */
typedef enum
{
    LOGICAL_OR = 2,
    LOGICAL_AND = 3
} GlQueryLogic;

static GParamSpec *properties[N_PROPERTIES];

static gboolean
gl_journal_model_fetch_idle (gpointer user_data)
{
    GlJournalModel *model = user_data;
    GlJournalEntry *entry;
    GlRowEntry *row_entry;
    guint last;

    g_assert (model->n_entries_to_fetch > 0);

    last = model->entries->len;

    if (last == 0)
    {
        model->export = FALSE;
        g_signal_emit (model, signals[DISABLE_EXPORT], 0);
    }
    else if (last > 0 && model->export == FALSE)
    {
        model->export = TRUE;
        g_signal_emit (model, signals[ENABLE_EXPORT], 0);
    }

    if ((entry = gl_journal_previous (model->journal)) && gl_query_check_journal_end (model->query, entry))
    {
        if (search_in_entry (entry, model))
        {
            row_entry = gl_row_entry_new ();
            row_entry->journal_entry = entry;

            if (last > 0)
            {
		GlJournalEntry *previous_entry;
                gchar *previous_entry_time_label;
                gchar *current_entry_time_label;
                GDateTime *now;

                GlRowEntry *prev_row_entry = g_ptr_array_index (model->entries, last - 1);

                if (gl_row_entry_check_message_similarity (row_entry,
                                                           prev_row_entry))
                {

                    /* Previously similar messages were detected */
                    if (prev_row_entry->row_type == GL_ROW_ENTRY_TYPE_COMPRESSED)
                    {

                        model->compressed_entries_counter++;
                        row_entry->row_type = GL_ROW_ENTRY_TYPE_COMPRESSED;
                    }
                    /* First time a similar group of messages is detected */
                    else
                    {

                        model->compressed_entries_counter = model->compressed_entries_counter + 2;
                        prev_row_entry->row_type = GL_ROW_ENTRY_TYPE_COMPRESSED;

                        if (model->query->order == GL_SORT_ORDER_ASCENDING_TIME)
                        {
                            g_list_model_items_changed (G_LIST_MODEL (model), 0, 1, 1);
                        }
                        else
                        {
                            g_list_model_items_changed (G_LIST_MODEL (model), last - 1, 1, 1);
                        }

                        row_entry->row_type = GL_ROW_ENTRY_TYPE_COMPRESSED;
                    }
                }
                else
                {

                    /* Add a compressed row header if a group of compressed row entries
                     * was detected. */
                    gl_journal_model_add_header (model);

                    /* Reset the count of compressed entries */
                    model->compressed_entries_counter = 0;

                    model->n_entries_to_fetch--;

                }

		previous_entry = gl_row_entry_get_journal_entry (prev_row_entry);

		now = g_date_time_new_now_local ();

                previous_entry_time_label = gl_util_timestamp_to_display (gl_journal_entry_get_timestamp (previous_entry),
                                                                          now, GL_UTIL_CLOCK_FORMAT_24HR, FALSE);

                current_entry_time_label = gl_util_timestamp_to_display (gl_journal_entry_get_timestamp (entry),
                                                                         now, GL_UTIL_CLOCK_FORMAT_24HR, FALSE);

                /* TODO: Timestamp should be compared directly in future. */
                if (g_strcmp0 (previous_entry_time_label, current_entry_time_label) == 0)
                {
                    gl_journal_entry_set_display_time_label (entry, FALSE);
		}
                else
		{
                    gl_journal_entry_set_display_time_label (entry, TRUE);
		}

                g_free (previous_entry_time_label);
                g_free (current_entry_time_label);
                g_date_time_unref (now);
            }

            last = model->entries->len;
            g_ptr_array_add (model->entries, row_entry);

            if (model->query->order == GL_SORT_ORDER_ASCENDING_TIME)
            {
                g_list_model_items_changed (G_LIST_MODEL (model), 0, 0, 1);
            }
            else
            {
                g_list_model_items_changed (G_LIST_MODEL (model), last, 0, 1);
            }
        }
    }
    else
    {
        model->fetched_all = TRUE;
        model->n_entries_to_fetch = 0;

        /* If the last read entry was in a compressed group
         * then add a row header representing that group. */
        gl_journal_model_add_header (model);
    }

    if (model->n_entries_to_fetch > 0)
    {
        return G_SOURCE_CONTINUE;
    }
    else
    {
        model->idle_source = 0;
        g_object_notify_by_pspec (G_OBJECT (model), properties[PROP_LOADING]);
        return G_SOURCE_REMOVE;
    }
}

static void
gl_journal_model_init (GlJournalModel *model)
{
    model->batch_size = 50;
    model->entries = g_ptr_array_new_with_free_func (g_object_unref);
    model->export = FALSE;
}

static void
gl_journal_model_get_property (GObject    *object,
                               guint       property_id,
                               GValue     *value,
                               GParamSpec *pspec)
{
    GlJournalModel *model = GL_JOURNAL_MODEL (object);

    switch (property_id)
    {
    case PROP_LOADING:
        g_value_set_boolean (value, gl_journal_model_get_loading (model));
        break;

    default:
        g_assert_not_reached ();
    }
}

static void
gl_journal_model_stop_idle (GlJournalModel *model)
{
    if (model->idle_source)
    {
        g_source_remove (model->idle_source);
        model->idle_source = 0;
        g_object_notify_by_pspec (G_OBJECT (model), properties[PROP_LOADING]);
    }
}

static void
gl_journal_model_dispose (GObject *object)
{
    GlJournalModel *model = GL_JOURNAL_MODEL (object);

    gl_journal_model_stop_idle (model);

    if (model->query)
    {
        gl_query_free (model->query);
    }

    if (model->entries)
    {
        g_ptr_array_free (model->entries, TRUE);
        model->entries = NULL;
    }

    if (model->token_array != NULL)
    {
        g_ptr_array_free (model->token_array, TRUE);
    }

    G_OBJECT_CLASS (gl_journal_model_parent_class)->dispose (object);
}

static GType
gl_journal_model_get_item_type (GListModel *list)
{
  return GL_TYPE_ROW_ENTRY;
}

static guint
gl_journal_model_get_n_items (GListModel *list)
{
  GlJournalModel *model = GL_JOURNAL_MODEL (list);

  return model->entries->len;
}

static gpointer
gl_journal_model_get_item (GListModel *list,
                           guint       position)
{
    GlJournalModel *model = GL_JOURNAL_MODEL (list);

    if (position < model->entries->len)
    {
        if (model->query->order == GL_SORT_ORDER_ASCENDING_TIME &&
            model->entries->len)
        {
            guint last = model->entries->len;

            /* read the array in reverse direction */
            return g_object_ref (g_ptr_array_index (model->entries,
                                                    (last - 1) - position));
        }
        else
        {
            return g_object_ref (g_ptr_array_index (model->entries, position));
        }

    }

    return NULL;
}

static void
gl_journal_model_class_init (GlJournalModelClass *class)
{
    GObjectClass *object_class = G_OBJECT_CLASS (class);
    GParamFlags default_flags = G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY;

    object_class->dispose = gl_journal_model_dispose;
    object_class->get_property = gl_journal_model_get_property;

    properties[PROP_LOADING] = g_param_spec_boolean ("loading", "", "", TRUE,
                                                     G_PARAM_READABLE | default_flags);

    g_object_class_install_properties (object_class, N_PROPERTIES, properties);

    signals[DISABLE_EXPORT] = g_signal_new ("disable-export",
					    G_TYPE_FROM_CLASS (class),
					    G_SIGNAL_RUN_LAST,
					    0,
					    NULL,
					    NULL,
					    NULL,
					    G_TYPE_NONE,
					    0);

    signals[ENABLE_EXPORT]  = g_signal_new ("enable-export",
					    G_TYPE_FROM_CLASS (class),
					    G_SIGNAL_RUN_LAST,
					    0,
					    NULL,
					    NULL,
					    NULL,
					    G_TYPE_NONE,
					    0);

}

static void
gl_journal_model_interface_init (GListModelInterface *iface)
{
    iface->get_item_type = gl_journal_model_get_item_type;
    iface->get_n_items = gl_journal_model_get_n_items;
    iface->get_item = gl_journal_model_get_item;
}

GlJournalModel *
gl_journal_model_new (void)
{
    return g_object_new (GL_TYPE_JOURNAL_MODEL, NULL);
}

/* Free the given @queryitem */
static void
gl_query_item_free (GlQueryItem *queryitem)
{
    g_free (queryitem->field_name);
    g_free (queryitem->field_value);

    g_slice_free (GlQueryItem, queryitem);
}

/* Free the given @query */
static void
gl_query_free (GlQuery *query)
{
    g_ptr_array_free (query->queryitems, TRUE);

    g_slice_free (GlQuery, query);
}

GlQuery *
gl_query_new (void)
{
    GlQuery *query;

    query = g_slice_new (GlQuery);

    query->queryitems = g_ptr_array_new_with_free_func ((GDestroyNotify) gl_query_item_free);
    query->search_type = GL_QUERY_SEARCH_TYPE_SUBSTRING;
    query->start_timestamp = 0;
    query->end_timestamp = 0;

    return query;
}

static GlQueryItem *
gl_query_item_new (const gchar *field_name,
                   const gchar *field_value,
                   GlQuerySearchType search_type)
{
    GlQueryItem *queryitem;

    queryitem = g_slice_new (GlQueryItem);

    queryitem->field_name = g_strdup (field_name);
    queryitem->field_value = g_strdup (field_value);
    queryitem->search_type = search_type;

    return queryitem;
}

void
gl_query_set_search_type (GlQuery *query, GlQuerySearchType search_type)
{
    query->search_type = search_type;
}

void
gl_query_set_sort_order (GlQuery *query, GlSortOrder order)
{
    query->order = order;
}

static gchar *
gl_query_item_create_match_string (GlQueryItem *queryitem)
{
    gchar *str;

    if (queryitem->field_value)
    {
        str = g_strdup_printf ("%s=%s", queryitem->field_name, queryitem->field_value);
    }
    else
    {
        str = g_strdup (queryitem->field_name);
    }

    return str;
}

static void
populate_exact_matches (GlQueryItem *queryitem, GPtrArray *matches)
{
    gchar *match;

    if (queryitem->search_type == GL_QUERY_SEARCH_TYPE_EXACT)
    {
        match = gl_query_item_create_match_string (queryitem);

        g_ptr_array_add (matches, match);
    }
}

/* Get exact matches from the query object */
static GPtrArray *
gl_query_get_exact_matches (GlQuery *query)
{
    GPtrArray *matches;

    matches = g_ptr_array_new_with_free_func ((GDestroyNotify) g_free);

    g_ptr_array_foreach (query->queryitems, (GFunc) populate_exact_matches, matches);

    return matches;
}

static void
populate_substring_matches (GlQueryItem *queryitem, GPtrArray *matches)
{
    if (queryitem->search_type == GL_QUERY_SEARCH_TYPE_SUBSTRING)
    {
        g_ptr_array_add (matches, queryitem);
    }
}

/* Get exact matches from the query object */
static GPtrArray *
gl_query_get_substring_matches (GlQuery *query)
{
    GPtrArray *matches;

    matches = g_ptr_array_new ();

    g_ptr_array_foreach (query->queryitems, (GFunc) populate_substring_matches, matches);

    return matches;
}

/* Process the newly assigned query and repopulate the journal model */
static void
gl_journal_model_process_query (GlJournalModel *model)
{
    GPtrArray *category_matches;

    /* Set the exact matches first */
    category_matches = gl_query_get_exact_matches (model->query);

    /* Get the search string of the exact match field */
    if (model->query->search_type == GL_QUERY_SEARCH_TYPE_EXACT)
    {
        gchar *search_match;
        gchar *field_value_pos;

        /* Get the search match string */
        search_match = g_ptr_array_index (category_matches, category_matches->len - 1);

        field_value_pos = strchr (search_match, '=');

        /* If it has invalid string value remove it from the matches */
        if (!field_value_pos || !*(field_value_pos + 1))
            g_ptr_array_remove (category_matches, search_match);
    }

    gl_journal_set_matches (model->journal, category_matches);

    gl_journal_set_start_position (model->journal, model->query->start_timestamp);

    /* Reset the count of compressed entries */
    model->compressed_entries_counter = 0;

    /* Start re-population of the journal */
    gl_journal_model_fetch_more_entries (model, FALSE);

    /* Free array */
    g_ptr_array_free (category_matches, TRUE);
}

/**
 * gl_journal_model_take_query:
 * @model: a #GlJournalModel
 * @query: (transfer full) : query object populated from view
 *
 * Takes the query object from the view.
 * Clears the previous entries in the journal model.
 * Sets query object on the journal model and processes the newly set query.
 */
void
gl_journal_model_take_query (GlJournalModel *model,
                             GlQuery *query)
{
    g_return_if_fail (GL_JOURNAL_MODEL (model));

    gl_journal_model_stop_idle (model);
    model->fetched_all = FALSE;

    if (model->entries->len > 0)
    {
        g_list_model_items_changed (G_LIST_MODEL (model), 0, model->entries->len, 0);

        g_ptr_array_free (model->entries, TRUE);

        model->entries = g_ptr_array_new_with_free_func (g_object_unref);
    }

    /* Clear the previous query */
    if (model->query)
    {
        gl_query_free (model->query);
    }

    /* Set new query */
    model->query = query;

    /* Tokenize the entered input only if search type is substring */
    if (query->search_type == GL_QUERY_SEARCH_TYPE_SUBSTRING)
    {
        GlQueryItem *search_match;
        GPtrArray *search_matches;

        search_matches = gl_query_get_substring_matches (model->query);

        /* Get search text from a search match */
        search_match = g_ptr_array_index (search_matches, 0);

        if (search_match->field_value != NULL)
        {
            model->token_array = tokenize_search_string (search_match->field_value);
        }

         g_ptr_array_free (search_matches, TRUE);

    }

    /* Start processing the new query */
    gl_journal_model_process_query (model);


}

/* Add a new queryitem to query */
void
gl_query_add_match (GlQuery *query,
                    const gchar *field_name,
                    const gchar *field_value,
                    GlQuerySearchType search_type)
{
    GlQueryItem *queryitem;

    queryitem = gl_query_item_new (field_name, field_value, search_type);

    g_ptr_array_add (query->queryitems, queryitem);
}

/* Check if current entry timestamp is less than the end timestamp */
static gboolean
gl_journal_entry_check_journal_end (GlJournalEntry *entry,
                                    guint64 end_timestamp)
{
    guint64 entry_timestamp = gl_journal_entry_get_timestamp (entry);

    if (end_timestamp)
    {
        /* Check if we have reached the end of given journal range */
        if (end_timestamp >= entry_timestamp)
            return FALSE;
    }

    return TRUE;
}

static gboolean
gl_query_check_journal_end (GlQuery *query, GlJournalEntry *entry)
{
    return gl_journal_entry_check_journal_end (entry, query->end_timestamp);
}

void
gl_query_set_journal_timestamp_range (GlQuery *query,
                                      guint64 start_timestamp,
                                      guint64 end_timestamp)
{
    query->start_timestamp = start_timestamp;
    query->end_timestamp = end_timestamp;
}

static gboolean
is_string_case_sensitive (const gchar *user_text)
{

    const gchar *search_text;

    for (search_text = user_text; search_text && *search_text;
         search_text = g_utf8_next_char (search_text))
    {
        gunichar c;

        c = g_utf8_get_char (search_text);

        if (g_unichar_isupper (c))
        {
            return TRUE;
        }
    }

    return FALSE;
}

static gboolean
utf8_strcasestr (const gchar *potential_hit,
                 const gchar *search_term)
{
  gchar *folded;
  gboolean matches;

  folded = g_utf8_casefold (potential_hit, -1);
  matches = strstr (folded, search_term) != NULL;

  g_free (folded);
  return matches;
}

static GPtrArray *
tokenize_search_string (gchar *search_text)
{
    gchar *field_name;
    gchar *field_value;
    GPtrArray *token_array;
    GScanner *scanner;

    token_array = g_ptr_array_new_with_free_func (g_free);
    scanner = g_scanner_new (NULL);
    scanner->config->cset_skip_characters = " =\t\n";

    /* All the characters used in the journal field values */
    scanner->config->cset_identifier_first = (
                                              G_CSET_a_2_z
                                              G_CSET_A_2_Z
                                              G_CSET_DIGITS
                                              "/_.-@:\\+"
                                              );

    scanner->config->cset_identifier_nth = (
                                            G_CSET_a_2_z
                                            G_CSET_A_2_Z
                                            G_CSET_DIGITS
                                            "/_.-@:\\"
                                            );

    g_scanner_input_text (scanner, search_text, strlen (search_text));

    do
    {
        g_scanner_get_next_token (scanner);
        if (scanner->value.v_identifier == NULL && scanner->token != '+')
        {
            break;
        }
        else if (scanner->token == '+')
        {
            g_ptr_array_add (token_array, g_strdup ("+"));

            g_scanner_get_next_token (scanner);
            if (scanner->value.v_identifier != NULL)
            {
                field_name = g_strdup (scanner->value.v_identifier);
                g_ptr_array_add (token_array, field_name);
            }
            else
            {
                field_name = NULL;
            }
        }
        else if (scanner->token == G_TOKEN_INT)
        {
            field_name = g_strdup_printf ("%lu", scanner->value.v_int);
            g_ptr_array_add (token_array, field_name);
        }
        else if (scanner->token == G_TOKEN_FLOAT)
        {
            field_name = g_strdup_printf ("%g", scanner->value.v_float);
            g_ptr_array_add (token_array, field_name);
        }
        else if (scanner->token == G_TOKEN_IDENTIFIER)
        {
            if (token_array->len != 0)
            {
                g_ptr_array_add (token_array, g_strdup (" "));
            }

            field_name = g_strdup (scanner->value.v_identifier);
            g_ptr_array_add (token_array, field_name);
        }
        else
        {
            field_name = NULL;
        }

        g_scanner_get_next_token (scanner);
        if (scanner->token == G_TOKEN_INT)
        {
            field_value = g_strdup_printf ("%lu", scanner->value.v_int);
            g_ptr_array_add (token_array, field_value);
        }
        else if (scanner->token == G_TOKEN_FLOAT)
        {
            field_value = g_strdup_printf ("%g", scanner->value.v_float);
            g_ptr_array_add (token_array, field_value);
        }
        else if (scanner->token == G_TOKEN_IDENTIFIER)
        {
            field_value = g_strdup (scanner->value.v_identifier);
            g_ptr_array_add (token_array, field_value);
        }
        else
        {
            field_value = NULL;
        }
    } while (field_name != NULL && field_value != NULL);

    g_scanner_destroy (scanner);

    return token_array;
}

/* Functions for replacing the big if queries in calculate token match */
static const gchar *
gl_query_item_get_entry_parameter (GlQueryItem *search_match,
                                   GlJournalEntry *entry,
                                   gboolean case_sensitive)
{
    const gchar *comm;
    const gchar *message;
    const gchar *kernel_device;
    const gchar *audit_session;
    const gchar *pid;
    const gchar *uid;
    const gchar *gid;
    const gchar *systemd_unit;
    const gchar *executable_path;

    comm = gl_journal_entry_get_command_line (entry);
    message = gl_journal_entry_get_message (entry);
    kernel_device = gl_journal_entry_get_kernel_device (entry);
    audit_session = gl_journal_entry_get_audit_session (entry);
    systemd_unit = gl_journal_entry_get_systemd_unit (entry);
    pid = gl_journal_entry_get_pid (entry);
    uid = gl_journal_entry_get_uid (entry);
    gid = gl_journal_entry_get_gid (entry);
    executable_path = gl_journal_entry_get_executable_path (entry);

    if (case_sensitive)
    {
        if (strstr ("_MESSAGE", search_match->field_name))
        {
            return message;
        }
        else if (strstr ("_COMM", search_match->field_name))
        {
            return comm;
        }
        else if (strstr ("_KERNEL_DEVICE", search_match->field_name))
        {
            return kernel_device;
        }
        else if (strstr ("_AUDIT_SESSION", search_match->field_name))
        {
            return audit_session;
        }
        else if (strstr ("_SYSTEMD_UNIT", search_match->field_name))
        {
            return systemd_unit;
        }
        else if (strstr ("_PID", search_match->field_name))
        {
            return pid;
        }
        else if (strstr ("_UID", search_match->field_name))
        {
            return uid;
        }
        else if (strstr ("_GID", search_match->field_name))
        {
            return gid;
        }
        else if (strstr ("_EXE", search_match->field_name))
        {
            return executable_path;
        }
    }
    else
    {
        if (utf8_strcasestr ("_message", search_match->field_name))
        {
            return message;
        }
        else if (utf8_strcasestr ("_comm", search_match->field_name))
        {
            return comm;
        }
        else if (utf8_strcasestr ("_kernel_device", search_match->field_name))
        {
            return kernel_device;
        }
        else if (utf8_strcasestr ("_audit_session", search_match->field_name))
        {
            return audit_session;
        }
        else if (utf8_strcasestr ("_systemd_unit", search_match->field_name))
        {
            return systemd_unit;
        }
        else if (utf8_strcasestr ("_pid", search_match->field_name))
        {
            return pid;
        }
        else if (utf8_strcasestr ("_uid", search_match->field_name))
        {
            return uid;
        }
        else if (utf8_strcasestr ("_gid", search_match->field_name))
        {
            return gid;
        }
        else if (utf8_strcasestr ("_exe", search_match->field_name))
        {
            return executable_path;
        }
    }

    return NULL;
}

static gboolean
gl_query_item_get_match (GlQueryItem *search_match,
                         const gchar *field_value,
                         gboolean case_sensitive)
{
    if (field_value)
    {
        if (case_sensitive)
        {
            if (strstr (field_value, search_match->field_value))
            {
                return TRUE;
            }
        }
        else
        {
            if (utf8_strcasestr (field_value, search_match->field_value))
            {
                return TRUE;
            }
        }
    }

    return FALSE;
}

static gboolean
gl_query_items_check_parameters (GPtrArray *search_matches,
                                 GlJournalEntry *entry)
{
    GlQueryItem *search_match;
    const gchar *entry_parameter;
    gboolean field_value_case;
    gboolean field_name_case;
    gint i;

    for (i=0; i < search_matches->len ;i++)
    {
        search_match = g_ptr_array_index (search_matches, i);

        field_name_case = is_string_case_sensitive (search_match->field_name);
        field_value_case = is_string_case_sensitive (search_match->field_value);

        entry_parameter = gl_query_item_get_entry_parameter (search_match, entry, field_name_case);

        if (gl_query_item_get_match (search_match, entry_parameter, field_value_case))
        {
            return TRUE;
        }
    }

    return FALSE;
}

static gboolean
calculate_match (GlJournalEntry *entry,
                 GPtrArray *token_array,
                 GPtrArray *search_matches)
{
    GlQueryItem *token_match;
    gchar *field_name;
    gchar *field_value;
    gboolean field_name_case;
    gboolean field_value_case;
    const gchar *entry_parameter;
    gboolean matches;
    gint match_stack[10];
    guint match_count = 0;
    guint token_index = 0;
    gint i;

    /* No logical AND or OR used in search text */
    if (token_array->len == 1)
    {
        return gl_query_items_check_parameters (search_matches, entry);
    }

    /* If multiple tokens are present : execute the token mode */
    while (token_index < token_array->len)
    {
        field_name = g_ptr_array_index (token_array, token_index);
        token_index++;

        if (token_index == token_array->len)
        {
            break;
        }

        field_value = g_ptr_array_index (token_array, token_index);
        token_index++;


        /* check for matches */
        token_match = gl_query_item_new (field_name, field_value, GL_QUERY_SEARCH_TYPE_SUBSTRING);

        field_name_case = is_string_case_sensitive (field_name);
        field_value_case = is_string_case_sensitive (field_value);

        entry_parameter = gl_query_item_get_entry_parameter (token_match, entry, field_name_case);

        matches = gl_query_item_get_match (token_match, entry_parameter, field_value_case);

        gl_query_item_free (token_match);

        match_stack[match_count] = matches;
        match_count++;

        if (token_index == token_array->len)
        {
            break;
        }

        if (g_strcmp0 (g_ptr_array_index (token_array, token_index), " ") == 0)
        {
            match_stack[match_count] = LOGICAL_AND;
            match_count++;
            token_index++;
        }
        else if (g_strcmp0 (g_ptr_array_index (token_array, token_index),
                            "+") == 0)
        {
            match_stack[match_count] = LOGICAL_OR;
            match_count++;
            token_index++;
        }
    }

    /* match_count > 2 means there are still matches to be calculated in the
     * stack */
    if (match_count > 2)
    {
        /* calculate the expression with logical AND */
        for (i = 0; i < match_count; i++)
        {
            if (match_stack[i] == LOGICAL_AND)
            {
                int j;

                match_stack[i - 1] = match_stack[i - 1] && match_stack[i + 1];

                for (j = i; j < match_count - 2; j++)
                {
                    if (j == match_count - 3)
                    {
                        match_stack[j] = match_stack[j + 2];
                        /* We use -1 to represent the values that are not
                         * useful */
                        match_stack[j + 1] = -1;

                        break;
                    }

                    match_stack[j] = match_stack[j + 2];
                    match_stack[j + 2] = -1;
                }
            }
        }

        /* calculate the expression with logical OR */
        for (i = 0; i < match_count; i++)
        {
            /* We use -1 to represent the values that are not useful */
            if ((match_stack[i] == LOGICAL_OR) && (i != token_index - 1) &&
                (match_stack[i + 1] != -1))
            {
                int j;

                match_stack[i - 1] = match_stack[i - 1] || match_stack[i + 1];

                for (j = i; j < match_count - 2; j++)
                {
                    match_stack[j] = match_stack[j + 2];
                    match_stack[j + 2] = -1;
                }
            }
        }
    }

    matches = match_stack[0];

    return matches;
}

static gboolean
search_in_entry (GlJournalEntry *entry,
                 GlJournalModel *model)
{
    GlQueryItem *search_match;
    gboolean matches;
    GPtrArray *search_matches;

    search_matches = gl_query_get_substring_matches (model->query);

    /* Check if there is atleast one substring queryitem */
    if (search_matches->len)
    {
        /* Get search text from a search match */
        search_match = g_ptr_array_index (search_matches, 0);

        /* check for null and empty strings */
        if (!search_match->field_value || !*(search_match->field_value))
        {
            matches = TRUE;
        }
        else
        {
            /* calculate match depending on the number of tokens */
            matches = calculate_match (entry, model->token_array,
                                       search_matches);
        }
    }
    else
    {
        matches = TRUE;
    }

    g_ptr_array_free (search_matches, TRUE);

    return matches;
}

gchar *
gl_journal_model_get_boot_time (GlJournalModel *model,
                                const gchar *boot_match)
{
    return gl_journal_get_boot_time (model->journal, boot_match);
}

GArray *
gl_journal_model_get_boot_ids (GlJournalModel *model)
{
    return gl_journal_get_boot_ids (model->journal);
}

/**
 * gl_journal_model_get_loading:
 * @model: a #GlJournalModel
 *
 * Returns %TRUE if @model is currently loading entries from the
 * journal. That means that @model will grow in the near future.
 *
 * Returns: %TRUE if the model is loading entries from the journal
 */
gboolean
gl_journal_model_get_loading (GlJournalModel *model)
{
    g_return_val_if_fail (GL_IS_JOURNAL_MODEL (model), FALSE);

    return model->idle_source > 0;
}

/**
 * gl_journal_model_fetch_more_entries:
 * @model: a #GlJournalModel
 * @all: whether to fetch all available entries
 *
 * @model doesn't loads all entries at once, but in batches. This
 * function triggers it to load the next batch, or all remaining entries
 * if @all is %TRUE.
 */
void
gl_journal_model_fetch_more_entries (GlJournalModel *model,
                                     gboolean        all)
{
    g_return_if_fail (GL_IS_JOURNAL_MODEL (model));

    if (model->fetched_all)
      return;

    if (all)
        model->n_entries_to_fetch = G_MAXUINT32;
    else
        model->n_entries_to_fetch = model->batch_size;

    if (model->idle_source == 0)
    {
        model->idle_source = g_idle_add_full (G_PRIORITY_LOW, gl_journal_model_fetch_idle, model, NULL);
        g_object_notify_by_pspec (G_OBJECT (model), properties[PROP_LOADING]);
    }
}

void
gl_journal_model_load_journal (GlJournalModel *model,
                               GlJournal *journal)
{
    model->journal = journal;
    gl_journal_model_fetch_more_entries (model, FALSE);
}

GlRowEntry *
gl_row_entry_new (void)
{
    return g_object_new (GL_TYPE_ROW_ENTRY, NULL);
}

GlJournalEntry *
gl_row_entry_get_journal_entry (GlRowEntry *entry)
{
    g_return_val_if_fail (GL_IS_ROW_ENTRY (entry), NULL);

    return entry->journal_entry;
}

GlRowEntryType
gl_row_entry_get_row_type (GlRowEntry *entry)
{
    return entry->row_type;
}

guint
gl_row_entry_get_compressed_entries (GlRowEntry *entry)
{
    return entry->compressed_entries;
}

static void
gl_row_entry_init (GlRowEntry *entry)
{
    entry->journal_entry = NULL;
    entry->row_type = GL_ROW_ENTRY_TYPE_UNCOMPRESSED;
    entry->compressed_entries = 0;
}

static void
gl_row_entry_finalize (GObject *object)
{
  GlRowEntry *row_entry = GL_ROW_ENTRY (object);

  g_clear_object (&row_entry->journal_entry);

  G_OBJECT_CLASS (gl_row_entry_parent_class)->finalize (object);
}

static void
gl_row_entry_class_init (GlRowEntryClass *class)
{
  GObjectClass *object_class = G_OBJECT_CLASS (class);

  object_class->finalize = gl_row_entry_finalize;
}


/**
 * gl_row_entry_check_message_similarity:
 * @current_row_entry: a #GlRowEntry
 * @prev_row_entry: a #GlRowEntry
 *
 * Compares two adjacent row entries for grouping them under a
 * common compressed row header. Returns %TRUE if their messages
 * have same first word or if they have been sent by the same
 * sending process.
 */
static gboolean
gl_row_entry_check_message_similarity (GlRowEntry *current_row_entry,
                                       GlRowEntry *prev_row_entry)
{
    GlJournalEntry *current_entry;
    GlJournalEntry *prev_entry;
    const gchar *current_entry_message;
    const gchar *prev_entry_message;
    const gchar *current_entry_sender;
    const gchar *prev_entry_sender;
    const gchar *first_whitespace_index;
    GString *current_entry_word;
    GString *prev_entry_word;
    gboolean result;

    current_entry = gl_row_entry_get_journal_entry (current_row_entry);
    prev_entry = gl_row_entry_get_journal_entry (prev_row_entry);

    current_entry_message = gl_journal_entry_get_message (current_entry);
    prev_entry_message = gl_journal_entry_get_message (prev_entry);

    current_entry_sender = gl_journal_entry_get_command_line (current_entry);
    prev_entry_sender = gl_journal_entry_get_command_line (prev_entry);

    current_entry_word = g_string_new (NULL);
    prev_entry_word = g_string_new (NULL);

    /* Get the position of first whitespace in the messages of the adjacent
     * journal entries and copy all the characters before that position
     * into a new string buffer. */
    first_whitespace_index = strchr (current_entry_message, ' ');

    g_string_append_len (current_entry_word, current_entry_message,
                         first_whitespace_index - current_entry_message);

    first_whitespace_index = strchr (prev_entry_message, ' ');

    g_string_append_len (prev_entry_word, prev_entry_message,
                        first_whitespace_index - prev_entry_message);

    result = FALSE;

    /* Check for similarity criteria */
    if (g_string_equal (current_entry_word, prev_entry_word))
    {
        result = TRUE;
    }
    else if (current_entry_sender && prev_entry_sender)
    {
        if (g_strcmp0 (current_entry_sender, prev_entry_sender) == 0)
        {
            result = TRUE;
        }
    }

    g_string_free (current_entry_word, TRUE);
    g_string_free (prev_entry_word, TRUE);

    return result;
}

/**
 * gl_journal_model_add_header:
 * @model: a #GlJournalModel
 *
 * Adds a compressed row header to the @model, which represents
 * a group of compressed row entries. Depending upon the sorting
 * order, the compressed row header is inserted in the @model
 * so that it will always appear before the compressed entries
 * represented by it in the view.
 */
static void
gl_journal_model_add_header (GlJournalModel *model)
{
    guint last;
    last = model->entries->len;

    /* A group of compressed row entries is detected. */
    if (model->compressed_entries_counter >= 2)
    {
        GlJournalEntry *prev_entry;
        GlRowEntry *prev_row_entry;
        GlRowEntry *header;

        /* Get the row entry from the compressed entries group, whose
         * journal entry details will be stored in the compressed row
         * header. */
        if (model->query->order == GL_SORT_ORDER_ASCENDING_TIME)
        {
            prev_row_entry = g_ptr_array_index (model->entries, last - 1);
        }
        else
        {
            prev_row_entry = g_ptr_array_index (model->entries,
                                                last - model->compressed_entries_counter);
        }

        prev_entry = prev_row_entry->journal_entry;

        /* Create the compressed row header */
        header = gl_row_entry_new ();
        header->journal_entry = g_object_ref (prev_entry);
        header->row_type = GL_ROW_ENTRY_TYPE_HEADER;
        header->compressed_entries = model->compressed_entries_counter;

        /* Insert it at a appropriate positon in the model */
        if (model->query->order == GL_SORT_ORDER_ASCENDING_TIME)
        {
            g_ptr_array_add (model->entries, header);
            g_list_model_items_changed (G_LIST_MODEL (model), 0, 0, 1);
        }
        else
        {
            g_ptr_array_insert (model->entries,
                                last - model->compressed_entries_counter,
                                header);

            g_list_model_items_changed (G_LIST_MODEL (model),
                                        last - model->compressed_entries_counter,
                                        0, 1);
        }
    }
}
