/*
 * 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/>.
 */

#ifndef _WIN32
# include <unistd.h>
# include <gio/gunixinputstream.h>
#else
# include <windows.h>
# include <gio/gwin32inputstream.h>
#endif /* _WIN32 */

#include "birbreadline.h"

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

struct _BirbReadline {
	GObject parent;

	GCancellable *cancellable;
	GInputStream *input;
	GTask *task;
	GString *line;

	char *prompt;
	char buffer;
};

/******************************************************************************
 * Reading
 *****************************************************************************/
static void birb_readline_read(BirbReadline *readline);

static void
birb_readline_read_cb(GObject *source, GAsyncResult *result, gpointer data) {
	BirbReadline *readline = data;
	GError *error = NULL;
	gssize n_bytes = 0;
	gboolean handled = FALSE;

	n_bytes = g_input_stream_read_finish(G_INPUT_STREAM(source), result,
	                                     &error);
	if(error != NULL) {
		g_warning("Failed to read input: %s", error->message);
		g_clear_error(&error);

		birb_readline_read(readline);

		return;
	}

	/* If we have a single char we have to check if it's a newline. */
	if((readline->buffer & 0x80) == 0) {
		if(readline->buffer == '\n') {
			GTask *task = readline->task;
			char *line = NULL;

			/* Clear the task on the readline so that it will be null when
			 * read_stream_sync is called from a callback handler.
			 */
			readline->task = NULL;

			/* Save the input and reset the line buffer. */
			line = g_string_free_and_steal(readline->line);
			readline->line = g_string_new("");

			/* Finish the task. */
			g_task_return_pointer(task, line, g_free);
			g_object_unref(task);

			g_object_notify_by_pspec(G_OBJECT(readline),
			                         properties[PROP_LINE]);

			return;
		} else if(readline->buffer == '\r') {
			/* We strip out carriage returns. */
			handled = TRUE;
		}
	}

	/* Process the normal data. */
	if(n_bytes == 1 && !handled) {
		g_string_append_c(readline->line, readline->buffer);

		g_object_notify_by_pspec(G_OBJECT(readline), properties[PROP_LINE]);
	}

	/* Continue reading. */
	birb_readline_read(readline);
}

static void
birb_readline_read(BirbReadline *readline) {
	g_input_stream_read_async(readline->input,
	                          &readline->buffer,
	                          sizeof(readline->buffer),
	                          G_PRIORITY_DEFAULT,
	                          readline->cancellable,
	                          birb_readline_read_cb,
	                          readline);
}

/******************************************************************************
 * GObject Implementation
 *****************************************************************************/
G_DEFINE_FINAL_TYPE(BirbReadline, birb_readline, G_TYPE_OBJECT)

static void
birb_readline_finalize(GObject *obj) {
	BirbReadline *readline = BIRB_READLINE(obj);

	if(readline->line != NULL) {
		g_string_free(readline->line, TRUE);
		readline->line = NULL;
	}

	g_clear_object(&readline->cancellable);
	g_clear_object(&readline->input);
	g_clear_pointer(&readline->prompt, g_free);

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

static void
birb_readline_get_property(GObject *obj,
                           guint param_id,
                           GValue *value,
                           GParamSpec *pspec)
{
	BirbReadline *readline = BIRB_READLINE(obj);

	switch(param_id) {
	case PROP_LINE:
		g_value_set_string(value, birb_readline_get_line(readline));
		break;
	case PROP_PROMPT:
		g_value_set_string(value, birb_readline_get_prompt(readline));
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
		break;
	}
}

static void
birb_readline_set_property(GObject *obj,
                           guint param_id,
                           const GValue *value,
                           GParamSpec *pspec)
{
	BirbReadline *readline = BIRB_READLINE(obj);

	switch(param_id) {
	case PROP_PROMPT:
		birb_readline_set_prompt(readline, g_value_get_string(value));
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
		break;
	}
}

static void
birb_readline_init(BirbReadline *readline) {
	readline->line = g_string_new("");
}

static void
birb_readline_class_init(BirbReadlineClass *klass) {
	GObjectClass *obj_class = G_OBJECT_CLASS(klass);

	obj_class->finalize = birb_readline_finalize;
	obj_class->get_property = birb_readline_get_property;
	obj_class->set_property = birb_readline_set_property;

	/**
	 * BirbReadline:line:
	 *
	 * The line that the user has input so far.
	 *
	 * Since: 0.9
	 */
	properties[PROP_LINE] = g_param_spec_string(
		"line", NULL, NULL,
		NULL,
		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

	/**
	 * BirbReadline:prompt:
	 *
	 * The prompt to display.
	 *
	 * Since: 0.9
	 */
	properties[PROP_PROMPT] = g_param_spec_string(
		"prompt", NULL, NULL,
		NULL,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);

	g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
}

/******************************************************************************
 * Public API
 *****************************************************************************/
const char *
birb_readline_get_line(BirbReadline *readline) {
	g_return_val_if_fail(BIRB_IS_READLINE(readline), NULL);

	return readline->line->str;
}

const char *
birb_readline_get_prompt(BirbReadline *readline) {
	g_return_val_if_fail(BIRB_IS_READLINE(readline), NULL);

	return readline->prompt;
}

BirbReadline *
birb_readline_new(const char *prompt) {
	return g_object_new(
		BIRB_TYPE_READLINE,
		"prompt", prompt,
		NULL);
}

void
birb_readline_read_stdin_async(BirbReadline *readline,
                               GCancellable *cancellable,
                               GAsyncReadyCallback callback,
                               gpointer data)
{
	GInputStream *input = NULL;

	g_return_if_fail(BIRB_IS_READLINE(readline));

#ifndef _WIN32
	input = g_unix_input_stream_new(STDIN_FILENO, TRUE);
	g_unix_input_stream_set_close_fd(G_UNIX_INPUT_STREAM(input), FALSE);
#else
	input = g_win32_input_stream_new(GetStdHandle(STD_INPUT_HANDLE), FALSE);
#endif

	birb_readline_read_stream_async(readline,
	                                input,
	                                cancellable,
	                                callback,
	                                data);

	g_clear_object(&input);
}

void
birb_readline_read_stream_async(BirbReadline *readline,
                                GInputStream *input,
                                GCancellable *cancellable,
                                GAsyncReadyCallback callback,
                                gpointer data)
{
	g_return_if_fail(BIRB_IS_READLINE(readline));
	g_return_if_fail(G_IS_INPUT_STREAM(input));

	g_return_if_fail(readline->task == NULL);

	readline->task = g_task_new(readline, cancellable, callback, data);

	g_set_object(&readline->cancellable, cancellable);
	g_set_object(&readline->input, input);

	birb_readline_read(readline);
}

char *
birb_readline_read_stream_finish(BirbReadline *readline,
                                 GAsyncResult *result,
                                 GError **error)
{
	g_return_val_if_fail(BIRB_IS_READLINE(readline), NULL);
	g_return_val_if_fail(g_task_is_valid(result, readline), NULL);

	return g_task_propagate_pointer(G_TASK(result), error);
}

void
birb_readline_set_prompt(BirbReadline *readline, const char *prompt) {
	g_return_if_fail(BIRB_IS_READLINE(readline));

	if(g_set_str(&readline->prompt, prompt)) {
		g_object_notify_by_pspec(G_OBJECT(readline), properties[PROP_PROMPT]);
	}
}
