#include <assert.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>

#include <json_object.h>

#include "vali.h"
#include "conn.h"

struct vali_client_call {
	struct vali_client *client;
};

struct vali_client {
	struct vali_conn conn;
	struct vali_client_call *pending_call;
};

struct vali_client *vali_client_connect_fd(int fd) {
	struct vali_client *client = calloc(1, sizeof(*client));
	if (client == NULL) {
		return NULL;
	}

	conn_init(&client->conn, fd);
	return client;
}

struct vali_client *vali_client_connect_unix(const char *path) {
	struct sockaddr_un addr;
	if (!set_sockaddr_un(&addr, path)) {
		errno = -EINVAL;
		return false;
	}

	int fd = socket(AF_UNIX, SOCK_STREAM, 0);
	if (fd < 0) {
		return NULL;
	}

	if (!set_cloexec(fd)) {
		goto err_fd;
	}

	if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
		goto err_fd;
	}

	struct vali_client *client = vali_client_connect_fd(fd);
	if (client == NULL) {
		goto err_fd;
	}

	return client;

err_fd:
	close(fd);
	return NULL;
}

void vali_client_destroy(struct vali_client *client) {
	conn_finish(&client->conn);
	free(client);
}

static bool client_send(struct vali_client *client, struct json_object *req) {
	return conn_enqueue(&client->conn, req) && conn_flush(&client->conn);
}

static bool client_receive(struct vali_client *client,
		struct json_object **out, struct vali_error *err, bool *continues) {
	if (out != NULL) {
		*out = NULL;
	}
	if (err != NULL) {
		*err = (struct vali_error){0};
	}

	bool eof = false;
	struct json_object *resp = NULL;
	if (!conn_receive(&client->conn, &eof) || !conn_dequeue(&client->conn, &resp) || resp == NULL) {
		return false;
	}

	struct json_object *params = json_object_object_get(resp, "parameters");
	const char *error_name = json_object_get_string(json_object_object_get(resp, "error"));
	*continues = json_object_get_boolean(json_object_object_get(resp, "continues"));
	bool ok = error_name == NULL;
	if (out != NULL && ok) {
		*out = json_object_get(params);
	}
	if (err != NULL && !ok) {
		vali_error_set(err, error_name, params);
	}
	json_object_put(resp);
	return ok;
}

bool vali_client_call(struct vali_client *client,
		const char *method, struct json_object *in, struct json_object **out,
		struct vali_error *err) {
	assert(in == NULL || json_object_get_type(in) == json_type_object);

	struct json_object *req = json_object_new_object();
	struct json_object *method_obj = json_object_new_string(method);
	if (req == NULL || method_obj == NULL) {
		return false;
	}
	json_object_object_add(req, "method", method_obj);
	json_object_object_add(req, "parameters", in);

	bool continues = false;
	return client_send(client, req) && client_receive(client, out, err, &continues) && !continues;
}

struct vali_client_call *vali_client_call_more(struct vali_client *client,
		const char *method, struct json_object *in) {
	assert(in == NULL || json_object_get_type(in) == json_type_object);

	struct json_object *req = json_object_new_object();
	struct json_object *method_obj = json_object_new_string(method);
	struct json_object *more_obj = json_object_new_boolean(true);
	if (req == NULL || method_obj == NULL || more_obj == NULL) {
		return NULL;
	}
	json_object_object_add(req, "method", method_obj);
	json_object_object_add(req, "parameters", in);
	json_object_object_add(req, "more", more_obj);

	if (!client_send(client, req)) {
		return NULL;
	}

	struct vali_client_call *call = calloc(1, sizeof(*call));
	if (call == NULL) {
		return NULL;
	}

	call->client = client;

	assert(client->pending_call == NULL);
	client->pending_call = call;
	return call;
}

bool vali_client_call_oneway(struct vali_client *client,
		const char *method, struct json_object *in) {
	assert(in == NULL || json_object_get_type(in) == json_type_object);

	struct json_object *req = json_object_new_object();
	struct json_object *method_obj = json_object_new_string(method);
	struct json_object *oneway_obj = json_object_new_boolean(true);
	if (req == NULL || method_obj == NULL || oneway_obj == NULL) {
		return false;
	}
	json_object_object_add(req, "method", method_obj);
	json_object_object_add(req, "parameters", in);
	json_object_object_add(req, "oneway", oneway_obj);

	return client_send(client, req);
}

void vali_error_finish(struct vali_error *err) {
	free(err->name);
	json_object_put(err->parameters);
}

void vali_error_set(struct vali_error *err, const char *name,
		struct json_object *parameters) {
	vali_error_finish(err);
	*err = (struct vali_error){
		.name = strdup(name),
		.parameters = json_object_get(parameters),
	};
}

bool vali_client_call_wait(struct vali_client_call *call,
		struct json_object **out, struct vali_error *err) {
	assert(!vali_client_call_is_done(call));

	bool continues = false;
	if (!client_receive(call->client, out, err, &continues)) {
		return false;
	}

	if (!continues) {
		call->client->pending_call = NULL;
	}

	return true;
}

void vali_client_call_destroy(struct vali_client_call *call) {
	assert(vali_client_call_is_done(call));
	free(call);
}

bool vali_client_call_is_done(struct vali_client_call *call) {
	return call->client->pending_call != call;
}
