/* -*- mode: c; c-file-style: "openbsd" -*- */
/*
 * Copyright (c) 2013 Vincent Bernat <bernat@luffy.cx>
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include <unistd.h>
#include <string.h>

#include "client.h"
#include "../log.h"

static int
cmd_medpower(struct lldpctl_conn_t *conn, struct writer *w,
    struct cmd_env *env, void *arg)
{
	log_debug("lldpctl", "set MED power");
	lldpctl_atom_t *port;
	const char *name;
	while ((port = cmd_iterate_on_ports(conn, env, &name))) {
		lldpctl_atom_t *med_power;
		const char *what = NULL;

		med_power = lldpctl_atom_get(port, lldpctl_k_port_med_power);
		if (med_power == NULL) {
			log_warnx("lldpctl", "unable to set LLDP-MED power: support seems unavailable");
			continue; /* Need to finish the loop */
		}

		if ((what = "device type", lldpctl_atom_set_str(med_power,
			    lldpctl_k_med_power_type,
			    cmdenv_get(env, "device-type"))) == NULL ||
		    (what = "power source", lldpctl_atom_set_str(med_power,
			lldpctl_k_med_power_source,
			cmdenv_get(env, "source"))) == NULL ||
		    (what = "power priority", lldpctl_atom_set_str(med_power,
			lldpctl_k_med_power_priority,
			cmdenv_get(env, "priority"))) == NULL ||
		    (what = "power value", lldpctl_atom_set_str(med_power,
			lldpctl_k_med_power_val,
			cmdenv_get(env, "value"))) == NULL)
			log_warnx("lldpctl",
			    "unable to set LLDP MED power value for %s on %s. %s.",
			    what, name, lldpctl_last_strerror(conn));
		else {
			if (lldpctl_atom_set(port, lldpctl_k_port_med_power,
				med_power) == NULL) {
				log_warnx("lldpctl", "unable to set LLDP MED power on %s. %s.",
				    name, lldpctl_last_strerror(conn));
			} else
				log_info("lldpctl", "LLDP-MED power has been set for port %s",
				    name);
		}

		lldpctl_atom_dec_ref(med_power);
	}
	return 1;
}

static int
cmd_store_powerpairs_env_value_and_pop2(struct lldpctl_conn_t *conn, struct writer *w,
    struct cmd_env *env, void *value)
{
	return cmd_store_something_env_value_and_pop2("powerpairs", env, value);
}
static int
cmd_store_class_env_value_and_pop2(struct lldpctl_conn_t *conn, struct writer *w,
    struct cmd_env *env, void *value)
{
	return cmd_store_something_env_value_and_pop2("class", env, value);
}
static int
cmd_store_prio_env_value_and_pop2(struct lldpctl_conn_t *conn, struct writer *w,
    struct cmd_env *env, void *value)
{
	return cmd_store_something_env_value_and_pop2("priority", env, value);
}

static int
cmd_dot3power(struct lldpctl_conn_t *conn, struct writer *w,
    struct cmd_env *env, void *arg)
{
	log_debug("lldpctl", "set dot3 power");
	lldpctl_atom_t *port;
	const char *name;
	while ((port = cmd_iterate_on_ports(conn, env, &name))) {
		lldpctl_atom_t *dot3_power;
		const char *what = NULL;
		int ok = 1;

		dot3_power = lldpctl_atom_get(port, lldpctl_k_port_dot3_power);
		if (dot3_power == NULL) {
			log_warnx("lldpctl", "unable to set Dot3 power: support seems unavailable");
			continue; /* Need to finish the loop */
		}

		if ((what = "device type", lldpctl_atom_set_str(dot3_power,
			    lldpctl_k_dot3_power_devicetype,
			    cmdenv_get(env, "device-type"))) == NULL ||
		    /* Flags */
		    (what = "supported flag", lldpctl_atom_set_int(dot3_power,
			lldpctl_k_dot3_power_supported,
			cmdenv_get(env, "supported")?1:0)) == NULL ||
		    (what = "enabled flag", lldpctl_atom_set_int(dot3_power,
			lldpctl_k_dot3_power_enabled,
			cmdenv_get(env, "enabled")?1:0)) == NULL ||
		    (what = "paircontrol flag", lldpctl_atom_set_int(dot3_power,
			lldpctl_k_dot3_power_paircontrol,
			cmdenv_get(env, "paircontrol")?1:0)) == NULL ||
		    /* Powerpairs */
		    (what = "power pairs", lldpctl_atom_set_str(dot3_power,
			lldpctl_k_dot3_power_pairs,
			cmdenv_get(env, "powerpairs"))) == NULL ||
		    /* Class */
		    (what = "power class", cmdenv_get(env, "class")?
			lldpctl_atom_set_str(dot3_power,
			    lldpctl_k_dot3_power_class,
			    cmdenv_get(env, "class")):
			lldpctl_atom_set_int(dot3_power,
			    lldpctl_k_dot3_power_class, 0)) == NULL ||
		    (what = "802.3at type", lldpctl_atom_set_int(dot3_power,
			lldpctl_k_dot3_power_type, 0)) == NULL) {
			log_warnx("lldpctl",
			    "unable to set LLDP Dot3 power value for %s on %s. %s.",
			    what, name, lldpctl_last_strerror(conn));
			ok = 0;
		} else if (cmdenv_get(env, "typeat")) {
			int typeat = cmdenv_get(env, "typeat")[0] - '0';
			const char *source = cmdenv_get(env, "source");
			if ((what = "802.3at type", lldpctl_atom_set_int(dot3_power,
				    lldpctl_k_dot3_power_type,
				    typeat)) == NULL ||
			    (what = "source", lldpctl_atom_set_int(dot3_power,
				lldpctl_k_dot3_power_source,
				(!strcmp(source, "primary"))?LLDP_DOT3_POWER_SOURCE_PRIMARY:
				(!strcmp(source, "backup"))? LLDP_DOT3_POWER_SOURCE_BACKUP:
				(!strcmp(source, "pse"))?    LLDP_DOT3_POWER_SOURCE_PSE:
				(!strcmp(source, "local"))?  LLDP_DOT3_POWER_SOURCE_LOCAL:
				(!strcmp(source, "both"))?   LLDP_DOT3_POWER_SOURCE_BOTH:
				LLDP_DOT3_POWER_SOURCE_UNKNOWN)) == NULL ||
			    (what = "priority", lldpctl_atom_set_str(dot3_power,
				lldpctl_k_dot3_power_priority,
				cmdenv_get(env, "priority"))) == NULL ||
			    (what = "requested power", lldpctl_atom_set_str(dot3_power,
				lldpctl_k_dot3_power_requested,
				cmdenv_get(env, "requested"))) == NULL ||
			    (what = "allocated power", lldpctl_atom_set_str(dot3_power,
				lldpctl_k_dot3_power_allocated,
				cmdenv_get(env, "allocated"))) == NULL) {
				log_warnx("lldpctl", "unable to set LLDP Dot3 power value for %s on %s. %s.",
				    what, name, lldpctl_last_strerror(conn));
				ok = 0;
			}
		}
		if (ok) {
			if (lldpctl_atom_set(port, lldpctl_k_port_dot3_power,
				dot3_power) == NULL) {
				log_warnx("lldpctl", "unable to set LLDP Dot3 power on %s. %s.",
				    name, lldpctl_last_strerror(conn));
			} else
				log_info("lldpctl", "LLDP Dot3 power has been set for port %s",
				    name);
		}

		lldpctl_atom_dec_ref(dot3_power);
	}
	return 1;
}

static int
cmd_check_type_but_no(struct cmd_env *env, void *arg)
{
	const char *what = arg;
	if (!cmdenv_get(env, "device-type")) return 0;
	if (cmdenv_get(env, what)) return 0;
	return 1;
}
static int
cmd_check_typeat_but_no(struct cmd_env *env, void *arg)
{
	const char *what = arg;
	if (!cmdenv_get(env, "typeat")) return 0;
	if (cmdenv_get(env, what)) return 0;
	return 1;
}
static int
cmd_check_type(struct cmd_env *env, const char *type)
{
	const char *etype = cmdenv_get(env, "device-type");
	if (!etype) return 0;
	return (!strcmp(type, etype));
}
static int
cmd_check_pse(struct cmd_env *env, void *arg)
{
	return cmd_check_type(env, "pse");
}
static int
cmd_check_pd(struct cmd_env *env, void *arg)
{
	return cmd_check_type(env, "pd");
}

static void
register_commands_pow_source(struct cmd_node *source)
{
	commands_new(source,
	    "unknown", "Unknown power source",
	    NULL, cmd_store_env_value_and_pop2, "source");
	commands_new(source,
	    "primary", "Primary power source",
	    cmd_check_pse, cmd_store_env_value_and_pop2, "source");
	commands_new(source,
	    "backup", "Backup power source",
	    cmd_check_pse, cmd_store_env_value_and_pop2, "source");
	commands_new(source,
	    "pse", "Power source is PSE",
	    cmd_check_pd, cmd_store_env_value_and_pop2, "source");
	commands_new(source,
	    "local", "Local power source",
	    cmd_check_pd, cmd_store_env_value_and_pop2, "source");
	commands_new(source,
	    "both", "Both PSE and local source available",
	    cmd_check_pd, cmd_store_env_value_and_pop2, "source");
}

static void
register_commands_pow_priority(struct cmd_node *priority, int key)
{
	for (lldpctl_map_t *prio_map =
		 lldpctl_key_get_map(key);
	     prio_map->string;
	     prio_map++) {
		char *tag = strdup(totag(prio_map->string));
		SUPPRESS_LEAK(tag);
		commands_new(
			priority,
			tag,
			prio_map->string,
			NULL, cmd_store_prio_env_value_and_pop2, prio_map->string);
	}
}

/**
 * Register `configure med power` commands.
 */
void
register_commands_medpow(struct cmd_node *configure_med)
{
	struct cmd_node *configure_medpower = commands_new(
		configure_med,
		"power", "MED power configuration",
		NULL, NULL, NULL);

	commands_new(
		configure_medpower,
		NEWLINE, "Apply new MED power configuration",
		cmd_check_env, cmd_medpower, "device-type,source,priority,value");

	/* Type: PSE or PD */
	commands_new(
		configure_medpower,
		"pd", "MED power consumer",
		cmd_check_no_env, cmd_store_env_value_and_pop, "device-type");
	commands_new(
		configure_medpower,
		"pse", "MED power provider",
		cmd_check_no_env, cmd_store_env_value_and_pop, "device-type");

	/* Source */
	struct cmd_node *source = commands_new(
		configure_medpower,
		"source", "MED power source",
		cmd_check_type_but_no, NULL, "source");
	register_commands_pow_source(source);

	/* Priority */
	struct cmd_node *priority = commands_new(
		configure_medpower,
		"priority", "MED power priority",
		cmd_check_type_but_no, NULL, "priority");
	register_commands_pow_priority(priority, lldpctl_k_med_power_priority);

	/* Value */
	commands_new(
		commands_new(configure_medpower,
		    "value", "MED power value",
		    cmd_check_type_but_no, NULL, "value"),
		NULL, "MED power value in milliwatts",
		NULL, cmd_store_env_value_and_pop2, "value");
}

static int
cmd_check_env_power(struct cmd_env *env, void *nothing)
{
	/* We need type and powerpair but if we have typeat, we also request
	 * source, priority, requested and allocated. */
	if (!cmdenv_get(env, "device-type")) return 0;
	if (!cmdenv_get(env, "powerpairs")) return 0;
	if (cmdenv_get(env, "typeat")) {
		return (!!cmdenv_get(env, "source") &&
		    !!cmdenv_get(env, "priority") &&
		    !!cmdenv_get(env, "requested") &&
		    !!cmdenv_get(env, "allocated"));
	}
	return 1;
}

/**
 * Register `configure med dot3` commands.
 */
void
register_commands_dot3pow(struct cmd_node *configure_dot3)
{
	struct cmd_node *configure_dot3power = commands_new(
		configure_dot3,
		"power", "Dot3 power configuration",
		NULL, NULL, NULL);

	commands_new(
		configure_dot3power,
		NEWLINE, "Apply new Dot3 power configuration",
		cmd_check_env_power, cmd_dot3power, NULL);

	/* Type: PSE or PD */
	commands_new(
		configure_dot3power,
		"pd", "Dot3 power consumer",
		cmd_check_no_env, cmd_store_env_value_and_pop, "device-type");
	commands_new(
		configure_dot3power,
		"pse", "Dot3 power provider",
		cmd_check_no_env, cmd_store_env_value_and_pop, "device-type");

	/* Flags */
	commands_new(
		configure_dot3power,
		"supported", "MDI power support present",
		cmd_check_type_but_no, cmd_store_env_and_pop, "supported");
	commands_new(
		configure_dot3power,
		"enabled", "MDI power support enabled",
		cmd_check_type_but_no, cmd_store_env_and_pop, "enabled");
	commands_new(
		configure_dot3power,
		"paircontrol", "MDI power pair can be selected",
		cmd_check_type_but_no, cmd_store_env_and_pop, "paircontrol");

	/* Power pairs */
	struct cmd_node *powerpairs = commands_new(
		configure_dot3power,
		"powerpairs", "Which pairs are currently used for power (mandatory)",
		cmd_check_type_but_no, NULL, "powerpairs");
	for (lldpctl_map_t *pp_map =
		 lldpctl_key_get_map(lldpctl_k_dot3_power_pairs);
	     pp_map->string;
	     pp_map++) {
		commands_new(
			powerpairs,
			pp_map->string,
			pp_map->string,
			NULL, cmd_store_powerpairs_env_value_and_pop2, pp_map->string);
	}

	/* Class */
	struct cmd_node *class = commands_new(
		configure_dot3power,
		"class", "Power class",
		cmd_check_type_but_no, NULL, "class");
	for (lldpctl_map_t *class_map =
		 lldpctl_key_get_map(lldpctl_k_dot3_power_class);
	     class_map->string;
	     class_map++) {
		const char *tag = strdup(totag(class_map->string));
		SUPPRESS_LEAK(tag);
		commands_new(
			class,
			tag,
			class_map->string,
			NULL, cmd_store_class_env_value_and_pop2, class_map->string);
	}

	/* 802.3at type */
	struct cmd_node *typeat = commands_new(
		configure_dot3power,
		"type", "802.3at device type",
		cmd_check_type_but_no, NULL, "typeat");
	commands_new(typeat,
	    "1", "802.3at type 1",
	    NULL, cmd_store_env_value_and_pop2, "typeat");
	commands_new(typeat,
	    "2", "802.3at type 2",
	    NULL, cmd_store_env_value_and_pop2, "typeat");

	/* Source */
	struct cmd_node *source = commands_new(
		configure_dot3power,
		"source", "802.3at dot3 power source (mandatory)",
		cmd_check_typeat_but_no, NULL, "source");
	register_commands_pow_source(source);

	/* Priority */
	struct cmd_node *priority = commands_new(
		configure_dot3power,
		"priority", "802.3at dot3 power priority (mandatory)",
		cmd_check_typeat_but_no, NULL, "priority");
	register_commands_pow_priority(priority, lldpctl_k_dot3_power_priority);

	/* Values */
	commands_new(
		commands_new(configure_dot3power,
		    "requested", "802.3at dot3 power value requested (mandatory)",
		    cmd_check_typeat_but_no, NULL, "requested"),
		NULL, "802.3at power value requested in milliwatts",
		NULL, cmd_store_env_value_and_pop2, "requested");
	commands_new(
		commands_new(configure_dot3power,
		    "allocated", "802.3at dot3 power value allocated (mandatory)",
		    cmd_check_typeat_but_no, NULL, "allocated"),
		NULL, "802.3at power value allocated in milliwatts",
		NULL, cmd_store_env_value_and_pop2, "allocated");
}
