/*
 *   teamd_per_port.c - Per-port data structures and actions
 *   Copyright (C) 2012-2015 Jiri Pirko <jiri@resnulli.us>
 *
 *   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 <stdlib.h>
#include <string.h>
#include <errno.h>
#include <inttypes.h>
#include <string.h>
#include <private/list.h>
#include <private/misc.h>
#include <team.h>

#include "teamd.h"

struct port_priv_item {
	struct list_item list;
	const struct teamd_port_priv *pp;
	void *creator_priv;
	long priv[0];
};

struct port_obj {
	struct teamd_port port; /* must be first */
	struct list_item list;
	struct list_item priv_list;
};

#define _port(port_obj) (&(port_obj)->port)
static int teamd_port_remove(struct teamd_context *ctx,
			     struct teamd_port *tdport);

int teamd_port_priv_create_and_get(void **ppriv, struct teamd_port *tdport,
				   const struct teamd_port_priv *pp,
				   void *creator_priv)
{
	struct port_priv_item *ppitem;
	struct port_obj *port_obj;

	ppitem = myzalloc(sizeof(*ppitem) + pp->priv_size);
	if (!ppitem)
		return -ENOMEM;
	ppitem->pp = pp;
	ppitem->creator_priv = creator_priv;
	port_obj = get_container(tdport, struct port_obj, port);
	list_add(&port_obj->priv_list, &ppitem->list);
	if (ppriv)
		*ppriv = ppitem->priv;
	return 0;
}
int teamd_port_priv_create(struct teamd_port *tdport,
			   const struct teamd_port_priv *pp, void *creator_priv)
{
	return teamd_port_priv_create_and_get(NULL, tdport, pp, creator_priv);
}

void *teamd_get_next_port_priv_by_creator(struct teamd_port *tdport,
					  void *creator_priv, void *priv)
{
	struct port_priv_item *ppitem = NULL;
	struct port_obj *port_obj;

	if (priv)
		ppitem = get_container(priv, struct port_priv_item, priv);
	port_obj = get_container(tdport, struct port_obj, port);

next_ppitem:
	ppitem = list_get_next_node_entry(&port_obj->priv_list, ppitem, list);
	if (!ppitem)
		return NULL;
	if (ppitem->creator_priv != creator_priv)
		goto next_ppitem;
	return ppitem->priv;
}

void *teamd_get_first_port_priv_by_creator(struct teamd_port *tdport,
					   void *creator_priv)
{
	return teamd_get_next_port_priv_by_creator(tdport, creator_priv, NULL);
}

static int port_priv_init_all(struct teamd_context *ctx, struct port_obj *port_obj)
{
	struct port_priv_item *ppitem;
	int err;

	list_for_each_node_entry(ppitem, &port_obj->priv_list, list) {
		if (!ppitem->pp->init)
			continue;
		err = ppitem->pp->init(ctx, _port(port_obj), &ppitem->priv,
				       ppitem->creator_priv);
		if (err) {
			teamd_log_err("Failed to init port priv.");
			goto rollback;
		}
	}
	return 0;
rollback:
	list_for_each_node_entry_continue_reverse(ppitem, &port_obj->priv_list,
						  list) {
		if (!ppitem->pp->fini)
			continue;
		ppitem->pp->fini(ctx, _port(port_obj), &ppitem->priv,
				 ppitem->creator_priv);
	}
	return err;
}

static void port_priv_fini_all(struct teamd_context *ctx, struct port_obj *port_obj)
{
	struct port_priv_item *ppitem;

	list_for_each_node_entry(ppitem, &port_obj->priv_list, list) {
		if (!ppitem->pp->fini)
			continue;
		ppitem->pp->fini(ctx, _port(port_obj), &ppitem->priv,
				 ppitem->creator_priv);
	}
}

static void port_priv_free_all(struct port_obj *port_obj)
{
	struct port_priv_item *ppitem, *tmp;

	list_for_each_node_entry_safe(ppitem, tmp, &port_obj->priv_list, list)
		free(ppitem);
}

static struct port_obj *port_obj_alloc(struct teamd_context *ctx,
				       uint32_t ifindex,
				       struct team_port *team_port)
{
	struct port_obj *port_obj;
	struct teamd_port *tdport;
	struct team_ifinfo *team_ifinfo;

	port_obj = myzalloc(sizeof(*port_obj));
	if (!port_obj) {
		teamd_log_err("Failed to alloc port object.");
		return NULL;
	}
	list_init(&port_obj->priv_list);
	tdport = _port(port_obj);
	tdport->ifindex = ifindex;
	team_ifinfo = team_get_port_ifinfo(team_port);
	tdport->ifname = team_get_ifinfo_ifname(team_ifinfo);
	tdport->team_port = team_port;
	tdport->team_ifinfo = team_ifinfo;
	return port_obj;
}

static void port_obj_free(struct port_obj *port_obj)
{
	port_priv_free_all(port_obj);
	free(port_obj);
}

static void port_obj_destroy(struct teamd_context *ctx,
			     struct port_obj *port_obj)
{
	list_del(&port_obj->list);
	ctx->port_obj_list_count--;
	port_priv_fini_all(ctx, port_obj);
}

static int port_obj_create(struct teamd_context *ctx,
			   struct port_obj **p_port_obj,
			   uint32_t ifindex,
			   struct team_port *team_port)
{
	struct port_obj *port_obj;
	struct teamd_port *tdport;
	int err;

	port_obj = port_obj_alloc(ctx, ifindex, team_port);
	if (!port_obj)
		return -ENOMEM;
	tdport = _port(port_obj);
	list_add(&ctx->port_obj_list, &port_obj->list);
	ctx->port_obj_list_count++;
	err = teamd_event_port_added(ctx, tdport);
	if (err)
		goto list_del;
	err = port_priv_init_all(ctx, port_obj);
	if (err)
		goto teamd_event_port_removed;
	*p_port_obj = port_obj;
	return 0;

teamd_event_port_removed:
	teamd_event_port_removed(ctx, tdport);
list_del:
	teamd_port_remove(ctx, tdport);
	port_obj_destroy(ctx, port_obj);
	port_obj_free(port_obj);
	return err;
}

static void port_obj_remove(struct teamd_context *ctx,
			    struct port_obj *port_obj)
{
	struct teamd_port *tdport = _port(port_obj);

	teamd_event_port_removed(ctx, tdport);
	teamd_port_remove(ctx, tdport);
	port_obj_destroy(ctx, port_obj);
	port_obj_free(port_obj);
}

static struct port_obj *get_port_obj(struct teamd_context *ctx,
				     uint32_t ifindex)
{
	struct port_obj *port_obj;

	list_for_each_node_entry(port_obj, &ctx->port_obj_list, list) {
		if (_port(port_obj)->ifindex == ifindex)
			return port_obj;
	}
	return NULL;
}

static struct port_obj *get_port_obj_by_ifname(struct teamd_context *ctx,
					       const char *ifname)
{
	struct port_obj *port_obj;

	list_for_each_node_entry(port_obj, &ctx->port_obj_list, list) {
		if (!strcmp(_port(port_obj)->ifname, ifname))
			return port_obj;
	}
	return NULL;
}

static int port_priv_change_handler_func(struct team_handle *th, void *priv,
					 team_change_type_mask_t type_mask)
{
	struct teamd_context *ctx = priv;
	struct team_port *port;
	struct port_obj *port_obj;
	int err;

	team_for_each_port(port, th) {
		uint32_t ifindex = team_get_port_ifindex(port);

		port_obj = get_port_obj(ctx, ifindex);
		if (!port_obj) {
			if (team_is_port_removed(port))
				continue;
			err = port_obj_create(ctx, &port_obj, ifindex, port);
			if (err)
				return err;
		}
		if (team_is_port_changed(port)) {
			err = teamd_event_port_changed(ctx, _port(port_obj));
			if (err)
				return err;
		}
		if (team_is_port_removed(port))
			port_obj_remove(ctx, port_obj);
	}
	return 0;
}

static const struct team_change_handler port_priv_change_handler = {
	.func = port_priv_change_handler_func,
	.type_mask = TEAM_PORT_CHANGE,
};

int teamd_per_port_init(struct teamd_context *ctx)
{
	int err;

	list_init(&ctx->port_obj_list);
	err = team_change_handler_register(ctx->th,
					   &port_priv_change_handler, ctx);
	return err;
}

void teamd_per_port_fini(struct teamd_context *ctx)
{
	team_change_handler_unregister(ctx->th,
				       &port_priv_change_handler, ctx);
}

struct teamd_port *teamd_get_port(struct teamd_context *ctx, uint32_t ifindex)
{
	struct port_obj *port_obj;

	port_obj = get_port_obj(ctx, ifindex);
	if (!port_obj)
		return NULL;
	return _port(port_obj);
}

struct teamd_port *teamd_get_port_by_ifname(struct teamd_context *ctx,
					    const char *ifname)
{
	struct port_obj *port_obj;

	port_obj = get_port_obj_by_ifname(ctx, ifname);
	if (!port_obj)
		return NULL;
	return _port(port_obj);
}

struct teamd_port *teamd_get_next_tdport(struct teamd_context *ctx,
					 struct teamd_port *tdport)
{
	struct port_obj *port_obj = NULL;

next_one:
	if (tdport)
		port_obj = get_container(tdport, struct port_obj, port);
	port_obj = list_get_next_node_entry(&ctx->port_obj_list, port_obj, list);
	if (!port_obj)
		return NULL;
	tdport = _port(port_obj);
	if (!teamd_port_present(ctx, tdport))
		goto next_one;
	return tdport;
}

int teamd_port_add_ifname(struct teamd_context *ctx, const char *port_name)
{
	uint32_t ifindex;

	ifindex = team_ifname2ifindex(ctx->th, port_name);
	teamd_log_dbg(ctx, "%s: Adding port (found ifindex \"%d\").",
		      port_name, ifindex);
	return team_port_add(ctx->th, ifindex);
}

static int teamd_port_remove(struct teamd_context *ctx,
			     struct teamd_port *tdport)
{
	int err;

	teamd_log_dbg(ctx, "%s: Removing port (found ifindex \"%d\").",
		      tdport->ifname, tdport->ifindex);
	err = team_port_remove(ctx->th, tdport->ifindex);
	if (err)
		teamd_log_err("%s: Failed to remove port.", tdport->ifname);
	return err;
}

int teamd_port_remove_all(struct teamd_context *ctx)
{
	struct port_obj *port_obj;
	int err;

	list_for_each_node_entry(port_obj, &ctx->port_obj_list, list) {
		err = teamd_port_remove(ctx, _port(port_obj));
		if (err)
			return err;
	}
	return 0;
}

void teamd_port_obj_remove_all(struct teamd_context *ctx)
{
	struct port_obj *port_obj;
	struct port_obj *tmp;

	list_for_each_node_entry_safe(port_obj, tmp, &ctx->port_obj_list, list)
		port_obj_remove(ctx, port_obj);
}

int teamd_port_remove_ifname(struct teamd_context *ctx, const char *port_name)
{
	struct teamd_port *tdport = teamd_get_port_by_ifname(ctx, port_name);

	if (!tdport)
		return -ENODEV;
	return teamd_port_remove(ctx, tdport);
}

int __teamd_port_enabled(struct teamd_context *ctx, struct teamd_port *tdport,
			 bool *enabled, bool may_fail)
{
	struct team_option *option;

	option = team_get_option(ctx->th, "np", "enabled", tdport->ifindex);
	if (!option) {
		if (!may_fail)
			teamd_log_err("%s: Failed to find \"enabled\" option.",
				      tdport->ifname);
		return -ENOENT;
	}
	if (team_get_option_type(option) != TEAM_OPTION_TYPE_BOOL) {
		if (!may_fail)
			teamd_log_err("Unexpected type of \"enabled\" option.");
		return -EINVAL;
	}

	*enabled = team_get_option_value_bool(option);
	return 0;
}

int teamd_port_enabled(struct teamd_context *ctx, struct teamd_port *tdport,
		       bool *enabled)
{
	return __teamd_port_enabled(ctx, tdport, enabled, false);
}

int teamd_port_enabled_check(struct teamd_context *ctx,
			     struct teamd_port *tdport, bool *enabled)
{
	return __teamd_port_enabled(ctx, tdport, enabled, true);
}

int teamd_port_prio(struct teamd_context *ctx, struct teamd_port *tdport)
{
	int prio;
	int err;

	err = team_get_port_priority(ctx->th, tdport->ifindex, &prio);
	if (err) {
		teamd_log_warn("%s: Can't get port priority. Using default.",
			       tdport->ifname);
		return 0; /* return default priority */
	}
	return prio;
}

int teamd_port_check_enable(struct teamd_context *ctx,
			    struct teamd_port *tdport,
			    bool should_enable, bool should_disable)
{
	bool new_enabled_state;
	int err;

	if (!teamd_port_present(ctx, tdport))
		return 0;

	if (should_enable)
		new_enabled_state = true;
	else if (should_disable)
		new_enabled_state = false;
	else
		return 0;

	teamd_log_dbg(ctx, "%s: %s port", tdport->ifname,
		      new_enabled_state ? "Enabling": "Disabling");
	err = team_set_port_enabled(ctx->th, tdport->ifindex,
				    new_enabled_state);
	if (err) {
		teamd_log_err("%s: Failed to %s port.", tdport->ifname,
			      new_enabled_state ? "enable": "disable");
		if (!TEAMD_ENOENT(err))
			return err;
	}
	return 0;
}
