/*
 *   teamd_lw_psr.c - Team port periodic send/receive link watcher
 *   Copyright (C) 2012-2015 Jiri Pirko <jiri@resnulli.us>
 *   Copyright (C) 2014 Erik Hugne <erik.hugne@ericsson.com>
 *
 *   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, write to the Free Software
 *   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */

#include <private/misc.h>
#include "teamd.h"
#include "teamd_link_watch.h"
#include "teamd_config.h"

/*
 * Generic periodic send/receive link watch "template"
 */

static const struct timespec lw_psr_default_init_wait = { 0, 1 };
#define LW_PSR_DEFAULT_INTERVAL   1000
#define LW_PSR_DEFAULT_MISSED_MAX 3

#define LW_PERIODIC_CB_NAME "lw_periodic"
static int lw_psr_callback_periodic(struct teamd_context *ctx, int events, void *priv)
{
	struct lw_common_port_priv *common_ppriv = priv;
	struct lw_psr_port_priv *psr_ppriv = priv;
	struct teamd_port *tdport = common_ppriv->tdport;
	bool link_up = common_ppriv->link_up;
	int err;

	if (psr_ppriv->reply_received) {
		link_up = true;
		psr_ppriv->missed = 0;
	} else {
		psr_ppriv->missed++;
		if (psr_ppriv->missed > psr_ppriv->missed_max && link_up) {
			teamd_log_dbg(ctx, "%s: Missed %u replies (max %u).",
				      tdport->ifname, psr_ppriv->missed,
				      psr_ppriv->missed_max);
			link_up = false;
		}
	}
	err = teamd_link_watch_check_link_up(ctx, tdport,
					     common_ppriv, link_up);
	if (err)
		return err;
	psr_ppriv->reply_received = false;

	return psr_ppriv->ops->send(psr_ppriv);
}

#define LW_SOCKET_CB_NAME "lw_socket"
static int lw_psr_callback_socket(struct teamd_context *ctx, int events, void *priv)
{
	struct lw_psr_port_priv *psr_ppriv = priv;

	return psr_ppriv->ops->receive(psr_ppriv);
}

static int lw_psr_load_options(struct teamd_context *ctx,
			       struct teamd_port *tdport,
			       struct lw_psr_port_priv *psr_ppriv)
{
	struct teamd_config_path_cookie *cpcookie = psr_ppriv->common.cpcookie;
	int err;
	int tmp;

	err = teamd_config_int_get(ctx, &tmp, "@.interval", cpcookie);
	if (!err) {
		if (tmp < 0) {
			teamd_log_err("\"interval\" must not be negative number.");
			return -EINVAL;
		}
	} else {
		tmp = LW_PSR_DEFAULT_INTERVAL;
	}
	teamd_log_dbg(ctx, "interval \"%d\".", tmp);
	ms_to_timespec(&psr_ppriv->interval, tmp);

	err = teamd_config_int_get(ctx, &tmp, "@.init_wait", cpcookie);
	if (!err)
		ms_to_timespec(&psr_ppriv->init_wait, tmp);
	/* if init_wait is set to 0, use default_init_wait */
	if (err || !tmp)
		psr_ppriv->init_wait = lw_psr_default_init_wait;
	teamd_log_dbg(ctx, "init_wait \"%d\".", timespec_to_ms(&psr_ppriv->init_wait));

	err = teamd_config_int_get(ctx, &tmp, "@.missed_max", cpcookie);
	if (!err) {
		if (tmp < 0) {
			teamd_log_err("\"missed_max\" must not be negative number.");
			return -EINVAL;
		}
	} else {
		tmp = LW_PSR_DEFAULT_MISSED_MAX;
	}
	teamd_log_dbg(ctx, "missed_max \"%d\".", tmp);
	psr_ppriv->missed_max = tmp;

	return 0;
}

struct lw_psr_port_priv *
lw_psr_ppriv_get(struct lw_common_port_priv *common_ppriv)
{
	return (struct lw_psr_port_priv *) common_ppriv;
}


int lw_psr_port_added(struct teamd_context *ctx, struct teamd_port *tdport,
		      void *priv, void *creator_priv)
{
	struct lw_psr_port_priv *psr_ppriv = priv;
	int err;

	err = lw_psr_load_options(ctx, tdport, psr_ppriv);
	if (err) {
		teamd_log_err("Failed to load options.");
		return err;
	}

	err = psr_ppriv->ops->load_options(ctx, tdport, psr_ppriv);
	if (err) {
		teamd_log_err("Failed to load options.");
		return err;
	}

	err = psr_ppriv->ops->sock_open(psr_ppriv);
	if (err) {
		teamd_log_err("Failed to create socket.");
		return err;
	}

	err = teamd_loop_callback_fd_add(ctx, LW_SOCKET_CB_NAME, psr_ppriv,
					 lw_psr_callback_socket,
					 psr_ppriv->sock,
					 TEAMD_LOOP_FD_EVENT_READ);
	if (err) {
		teamd_log_err("Failed add socket callback.");
		goto close_sock;
	}

	err = teamd_loop_callback_timer_add_set(ctx, LW_PERIODIC_CB_NAME,
						psr_ppriv,
						lw_psr_callback_periodic,
						&psr_ppriv->interval,
						&psr_ppriv->init_wait);
	if (err) {
		teamd_log_err("Failed add callback timer");
		goto socket_callback_del;
	}

	err = team_set_port_user_linkup_enabled(ctx->th, tdport->ifindex, true);
	if (err) {
		teamd_log_err("%s: Failed to enable user linkup.",
			      tdport->ifname);
		goto periodic_callback_del;
	}

	teamd_loop_callback_enable(ctx, LW_SOCKET_CB_NAME, psr_ppriv);
	teamd_loop_callback_enable(ctx, LW_PERIODIC_CB_NAME, psr_ppriv);
	return 0;

periodic_callback_del:
	teamd_loop_callback_del(ctx, LW_PERIODIC_CB_NAME, psr_ppriv);
socket_callback_del:
	teamd_loop_callback_del(ctx, LW_SOCKET_CB_NAME, psr_ppriv);
close_sock:
	psr_ppriv->ops->sock_close(psr_ppriv);
	return err;
}

void lw_psr_port_removed(struct teamd_context *ctx, struct teamd_port *tdport,
			 void *priv, void *creator_priv)
{
	struct lw_psr_port_priv *psr_ppriv = priv;

	teamd_loop_callback_del(ctx, LW_PERIODIC_CB_NAME, psr_ppriv);
	teamd_loop_callback_del(ctx, LW_SOCKET_CB_NAME, psr_ppriv);
	psr_ppriv->ops->sock_close(psr_ppriv);
}

int lw_psr_state_interval_get(struct teamd_context *ctx,
			      struct team_state_gsc *gsc,
			      void *priv)
{
	struct lw_common_port_priv *common_ppriv = priv;
	struct lw_psr_port_priv *psr_ppriv = lw_psr_ppriv_get(common_ppriv);

	gsc->data.int_val = timespec_to_ms(&psr_ppriv->interval);
	return 0;
}

int lw_psr_state_init_wait_get(struct teamd_context *ctx,
			       struct team_state_gsc *gsc,
			       void *priv)
{
	struct lw_common_port_priv *common_ppriv = priv;
	struct lw_psr_port_priv *psr_ppriv = lw_psr_ppriv_get(common_ppriv);

	gsc->data.int_val = timespec_to_ms(&psr_ppriv->init_wait);
	return 0;
}

int lw_psr_state_missed_max_get(struct teamd_context *ctx,
				struct team_state_gsc *gsc,
				void *priv)
{
	struct lw_common_port_priv *common_ppriv = priv;
	struct lw_psr_port_priv *psr_ppriv = lw_psr_ppriv_get(common_ppriv);

	gsc->data.int_val = psr_ppriv->missed_max;
	return 0;
}

int lw_psr_state_missed_get(struct teamd_context *ctx,
			    struct team_state_gsc *gsc,
			    void *priv)
{
	struct lw_common_port_priv *common_ppriv = priv;
	struct lw_psr_port_priv *psr_ppriv = lw_psr_ppriv_get(common_ppriv);

	gsc->data.int_val = psr_ppriv->missed;
	return 0;
}
