/* C-Ares integration to Quagga mainloop
 * Copyright (c) 2014-2015 Timo Teräs
 *
 * This file is free software: you may copy, redistribute and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 2 of the License, or
 * (at your option) any later version.
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include <ares.h>
#include <ares_version.h>

#include "vector.h"
#include "thread.h"
#include "lib_errors.h"
#include "resolver.h"
#include "command.h"

struct resolver_state {
	ares_channel channel;
	struct thread_master *master;
	struct thread *timeout;
	vector read_threads, write_threads;
};

static struct resolver_state state;
static bool resolver_debug;

#define THREAD_RUNNING ((struct thread *)-1)

static void resolver_update_timeouts(struct resolver_state *r);

static int resolver_cb_timeout(struct thread *t)
{
	struct resolver_state *r = THREAD_ARG(t);

	r->timeout = THREAD_RUNNING;
	ares_process(r->channel, NULL, NULL);
	r->timeout = NULL;
	resolver_update_timeouts(r);

	return 0;
}

static int resolver_cb_socket_readable(struct thread *t)
{
	struct resolver_state *r = THREAD_ARG(t);
	int fd = THREAD_FD(t);

	vector_set_index(r->read_threads, fd, THREAD_RUNNING);
	ares_process_fd(r->channel, fd, ARES_SOCKET_BAD);
	if (vector_lookup(r->read_threads, fd) == THREAD_RUNNING) {
		t = NULL;
		thread_add_read(r->master, resolver_cb_socket_readable, r, fd,
				&t);
		vector_set_index(r->read_threads, fd, t);
	}
	resolver_update_timeouts(r);

	return 0;
}

static int resolver_cb_socket_writable(struct thread *t)
{
	struct resolver_state *r = THREAD_ARG(t);
	int fd = THREAD_FD(t);

	vector_set_index(r->write_threads, fd, THREAD_RUNNING);
	ares_process_fd(r->channel, ARES_SOCKET_BAD, fd);
	if (vector_lookup(r->write_threads, fd) == THREAD_RUNNING) {
		t = NULL;
		thread_add_write(r->master, resolver_cb_socket_writable, r, fd,
				 &t);
		vector_set_index(r->write_threads, fd, t);
	}
	resolver_update_timeouts(r);

	return 0;
}

static void resolver_update_timeouts(struct resolver_state *r)
{
	struct timeval *tv, tvbuf;

	if (r->timeout == THREAD_RUNNING)
		return;

	THREAD_OFF(r->timeout);
	tv = ares_timeout(r->channel, NULL, &tvbuf);
	if (tv) {
		unsigned int timeoutms = tv->tv_sec * 1000 + tv->tv_usec / 1000;
		thread_add_timer_msec(r->master, resolver_cb_timeout, r,
				      timeoutms, &r->timeout);
	}
}

static void ares_socket_cb(void *data, ares_socket_t fd, int readable,
			   int writable)
{
	struct resolver_state *r = (struct resolver_state *)data;
	struct thread *t;

	if (readable) {
		t = vector_lookup_ensure(r->read_threads, fd);
		if (!t) {
			thread_add_read(r->master, resolver_cb_socket_readable,
					r, fd, &t);
			vector_set_index(r->read_threads, fd, t);
		}
	} else {
		t = vector_lookup(r->read_threads, fd);
		if (t) {
			if (t != THREAD_RUNNING) {
				THREAD_OFF(t);
			}
			vector_unset(r->read_threads, fd);
		}
	}

	if (writable) {
		t = vector_lookup_ensure(r->write_threads, fd);
		if (!t) {
			thread_add_read(r->master, resolver_cb_socket_writable,
					r, fd, &t);
			vector_set_index(r->write_threads, fd, t);
		}
	} else {
		t = vector_lookup(r->write_threads, fd);
		if (t) {
			if (t != THREAD_RUNNING) {
				THREAD_OFF(t);
			}
			vector_unset(r->write_threads, fd);
		}
	}
}


static void ares_address_cb(void *arg, int status, int timeouts,
			    struct hostent *he)
{
	struct resolver_query *query = (struct resolver_query *)arg;
	union sockunion addr[16];
	void (*callback)(struct resolver_query *, const char *, int,
			 union sockunion *);
	size_t i;

	callback = query->callback;
	query->callback = NULL;

	if (status != ARES_SUCCESS) {
		if (resolver_debug)
			zlog_debug("[%p] Resolving failed (%s)",
				   query, ares_strerror(status));

		callback(query, ares_strerror(status), -1, NULL);
		return;
	}

	for (i = 0; i < array_size(addr) && he->h_addr_list[i] != NULL; i++) {
		memset(&addr[i], 0, sizeof(addr[i]));
		addr[i].sa.sa_family = he->h_addrtype;
		switch (he->h_addrtype) {
		case AF_INET:
			memcpy(&addr[i].sin.sin_addr,
			       (uint8_t *)he->h_addr_list[i], he->h_length);
			break;
		case AF_INET6:
			memcpy(&addr[i].sin6.sin6_addr,
			       (uint8_t *)he->h_addr_list[i], he->h_length);
			break;
		}
	}

	if (resolver_debug)
		zlog_debug("[%p] Resolved with %d results", query, (int)i);

	callback(query, NULL, i, &addr[0]);
}

static int resolver_cb_literal(struct thread *t)
{
	struct resolver_query *query = THREAD_ARG(t);
	void (*callback)(struct resolver_query *, const char *, int,
			 union sockunion *);

	callback = query->callback;
	query->callback = NULL;

	callback(query, ARES_SUCCESS, 1, &query->literal_addr);
	return 0;
}

void resolver_resolve(struct resolver_query *query, int af,
		      const char *hostname,
		      void (*callback)(struct resolver_query *, const char *,
				       int, union sockunion *))
{
	int ret;

	if (query->callback != NULL) {
		flog_err(
			EC_LIB_RESOLVER,
			"Trying to resolve '%s', but previous query was not finished yet",
			hostname);
		return;
	}

	query->callback = callback;
	query->literal_cb = NULL;

	ret = str2sockunion(hostname, &query->literal_addr);
	if (ret == 0) {
		if (resolver_debug)
			zlog_debug("[%p] Resolving '%s' (IP literal)",
				   query, hostname);

		/* for consistency with proper name lookup, don't call the
		 * callback immediately; defer to thread loop
		 */
		thread_add_timer_msec(state.master, resolver_cb_literal,
				      query, 0, &query->literal_cb);
		return;
	}

	if (resolver_debug)
		zlog_debug("[%p] Resolving '%s'", query, hostname);

	ares_gethostbyname(state.channel, hostname, af, ares_address_cb, query);
	resolver_update_timeouts(&state);
}

DEFUN(debug_resolver,
      debug_resolver_cmd,
      "[no] debug resolver",
      NO_STR
      DEBUG_STR
      "Debug DNS resolver actions\n")
{
	resolver_debug = (argc == 2);
	return CMD_SUCCESS;
}

static int resolver_config_write_debug(struct vty *vty);
static struct cmd_node resolver_debug_node = {
	.name = "resolver debug",
	.node = RESOLVER_DEBUG_NODE,
	.prompt = "",
	.config_write = resolver_config_write_debug,
};

static int resolver_config_write_debug(struct vty *vty)
{
	if (resolver_debug)
		vty_out(vty, "debug resolver\n");
	return 1;
}


void resolver_init(struct thread_master *tm)
{
	struct ares_options ares_opts;

	state.master = tm;
	state.read_threads = vector_init(1);
	state.write_threads = vector_init(1);

	ares_opts = (struct ares_options){
		.sock_state_cb = &ares_socket_cb,
		.sock_state_cb_data = &state,
		.timeout = 2,
		.tries = 3,
	};

	ares_init_options(&state.channel, &ares_opts,
			  ARES_OPT_SOCK_STATE_CB | ARES_OPT_TIMEOUT
				  | ARES_OPT_TRIES);

	install_node(&resolver_debug_node);
	install_element(CONFIG_NODE, &debug_resolver_cmd);
	install_element(ENABLE_NODE, &debug_resolver_cmd);
}
