/*
 * Copyright (C) Birb Developers
 *
 * Birb is the legal property of its developers, whose names are too numerous
 * to list here. Please refer to the AUTHORS file distributed with this source
 * distribution
 *
 * This library 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 2.1 of the License, or (at your
 * option) any later version.
 *
 * This library 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 library; if not, see <https://www.gnu.org/licenses/>.
 */

#include "birbresponsestream.h"

#include "birbstr.h"

enum {
	PROP_0,
	PROP_INPUT_STREAM,
	N_PROPERTIES,
};
static GParamSpec *properties[N_PROPERTIES] = {NULL, };

struct _BirbResponseStream {
	GOutputStream parent;

	GString *buffer;

	GInputStream *input;

	GPtrArray *responses;
};

typedef struct {
	GRegex *pattern;
	char *response;
} BirbResponseStreamItem;

/******************************************************************************
 * ResponseStreamItem API
 *****************************************************************************/
static BirbResponseStreamItem *
birb_response_stream_item_new(const char *pattern, const char *response,
                              GError **error)
{
	BirbResponseStreamItem *item = NULL;
	GRegex *regex = NULL;

	if(!birb_str_is_empty(pattern)) {
		GError *local_error = NULL;

		regex = g_regex_new(pattern, G_REGEX_DEFAULT, G_REGEX_MATCH_DEFAULT,
		                    &local_error);

		if(local_error != NULL) {
			g_propagate_error(error, local_error);

			return NULL;
		}
	}

	item = g_new0(BirbResponseStreamItem, 1);
	item->pattern = regex;
	item->response = g_strdup(response);

	return item;
}

static void
birb_response_stream_item_free(BirbResponseStreamItem *item) {
	g_clear_pointer(&item->pattern, g_regex_unref);
	g_free(item->response);
	g_free(item);
}

/******************************************************************************
 * Helpers
 *****************************************************************************/
static void
birb_response_stream_match(BirbResponseStream *stream) {
	BirbResponseStreamItem *item = NULL;
	gboolean matched = FALSE;

	g_return_if_fail(BIRB_IS_RESPONSE_STREAM(stream));

	/* We shouldn't get here normally, but just in case... */
	if(stream->responses->len == 0) {
		g_output_stream_close(G_OUTPUT_STREAM(stream), NULL, NULL);

		return;
	}

	item = g_ptr_array_index(stream->responses, 0);
	if(item->pattern == NULL) {
		/* If the pattern is null, we assume everything in the buffer is
		 * matched.
		 */
		matched = TRUE;
		g_string_truncate(stream->buffer, 0);
	} else {
		/* If the regex matches we remove the text in group 0. */
		GMatchInfo *info = NULL;

		matched = g_regex_match(item->pattern, stream->buffer->str,
		                        G_REGEX_MATCH_DEFAULT, &info);

		if(matched) {
			int end = 0;

			g_match_info_fetch_pos(info, 0, NULL, &end);
			g_string_erase(stream->buffer, 0, end);
		}

		g_clear_pointer(&info, g_match_info_unref);
	}

	/* If we didn't get a match, we wait for more data. */
	if(!matched) {
		return;
	}

	if(item->response != NULL) {
		/* Since we have a match and a response we need to add the response to
		 * the input stream. We have to give a copy, as we kill the data after
		 * this.
		 */
		g_memory_input_stream_add_data(G_MEMORY_INPUT_STREAM(stream->input),
		                               g_strdup(item->response),
		                               strlen(item->response),
		                               g_free);
	}

	/* Remove the item from the response. */
	g_ptr_array_remove_index(stream->responses, 0);
}

/******************************************************************************
 * GOutputStream Implementation
 *****************************************************************************/
static gboolean
birb_response_stream_close(GOutputStream *ostream,
                           G_GNUC_UNUSED GCancellable *cancellable,
                           G_GNUC_UNUSED GError **error)
{
	BirbResponseStream *stream = BIRB_RESPONSE_STREAM(ostream);

	g_string_assign(stream->buffer, "");

	return TRUE;
}

static gssize
birb_response_stream_write(GOutputStream *ostream, gconstpointer buffer,
                           gsize count, GCancellable *cancellable,
                           G_GNUC_UNUSED GError **error)
{
	BirbResponseStream *stream = BIRB_RESPONSE_STREAM(ostream);

	if(g_cancellable_is_cancelled(cancellable)) {
		return 0;
	}

	if(count == 0) {
		return 0;
	}

	g_string_append_len(stream->buffer, buffer, count);

	birb_response_stream_match(stream);

	return (gssize)count;
}

/******************************************************************************
 * GObject Implementation
 *****************************************************************************/
G_DEFINE_FINAL_TYPE(BirbResponseStream, birb_response_stream,
                    G_TYPE_OUTPUT_STREAM)

static void
birb_response_stream_finalize(GObject *obj) {
	BirbResponseStream *stream = BIRB_RESPONSE_STREAM(obj);

	g_string_free(stream->buffer, TRUE);
	stream->buffer = NULL;

	g_clear_object(&stream->input);
	g_clear_pointer(&stream->responses, g_ptr_array_unref);

	G_OBJECT_CLASS(birb_response_stream_parent_class)->finalize(obj);
}

static void
birb_response_stream_get_property(GObject *obj, guint param_id, GValue *value,
                                  GParamSpec *pspec)
{
	BirbResponseStream *stream = BIRB_RESPONSE_STREAM(obj);

	switch(param_id) {
	case PROP_INPUT_STREAM:
		g_value_set_object(value,
		                   birb_response_stream_get_input_stream(stream));
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
		break;
	}
}

static void
birb_response_stream_init(BirbResponseStream *stream) {
	stream->buffer = g_string_new("");
	stream->input = g_memory_input_stream_new();

	stream->responses = g_ptr_array_new_full(10,
	                                         (GDestroyNotify)birb_response_stream_item_free);
}

static void
birb_response_stream_class_init(BirbResponseStreamClass *klass) {
	GObjectClass *obj_class = G_OBJECT_CLASS(klass);
	GOutputStreamClass *output_stream_class = G_OUTPUT_STREAM_CLASS(klass);

	obj_class->finalize = birb_response_stream_finalize;
	obj_class->get_property = birb_response_stream_get_property;

	output_stream_class->write_fn = birb_response_stream_write;
	output_stream_class->close_fn = birb_response_stream_close;

	/**
	 * BirbResponseStream:input-stream:
	 *
	 * The input stream that users should write to.
	 *
	 * Since: 0.6
	 */
	properties[PROP_INPUT_STREAM] = g_param_spec_object(
		"input-stream", NULL, NULL,
		G_TYPE_INPUT_STREAM,
		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

	g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
}

/******************************************************************************
 * Public API
 *****************************************************************************/
GOutputStream *
birb_response_stream_new(void) {
	return g_object_new(BIRB_TYPE_RESPONSE_STREAM, NULL);
}

GIOStream *
birb_response_stream_new_iostream(void) {
	BirbResponseStream *response = NULL;
	GOutputStream *output = NULL;
	GIOStream *stream = NULL;

	output = birb_response_stream_new();
	response = BIRB_RESPONSE_STREAM(output);

	stream = g_simple_io_stream_new(response->input, output);
	g_clear_object(&output);

	return stream;
}

gboolean
birb_response_stream_add_response(BirbResponseStream *stream,
                                  const char *pattern,
                                  const char *response,
                                  GError **error)
{
	BirbResponseStreamItem *item = NULL;
	GError *local_error = NULL;

	g_return_val_if_fail(BIRB_IS_RESPONSE_STREAM(stream), FALSE);

	item = birb_response_stream_item_new(pattern, response, &local_error);
	if(local_error != NULL) {
		g_propagate_error(error, local_error);

		return FALSE;
	}

	g_ptr_array_add(stream->responses, item);

	if(!g_input_stream_has_pending(stream->input)) {
		birb_response_stream_match(stream);
	}

	return TRUE;
}

GInputStream *
birb_response_stream_get_input_stream(BirbResponseStream *stream) {
	g_return_val_if_fail(BIRB_IS_RESPONSE_STREAM(stream), NULL);

	return stream->input;
}
