/*
 * Copyright (c) 2014-2017 The Linux Foundation. All rights reserved.
 *
 * Previously licensed under the ISC license by Qualcomm Atheros, Inc.
 *
 *
 * 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.
 */

/**
 *  DOC: wlan_hdd_green_ap.c
 *
 *  WLAN Host Device Driver Green AP implementation
 *
 */

/* Include Files */
#include <wlan_hdd_main.h>
#include <wlan_hdd_misc.h>
#include "wlan_hdd_green_ap.h"
#include "wma_api.h"
#include "wlan_policy_mgr_api.h"

#define GREEN_AP_PS_ON_TIME        (0)
#define GREEN_AP_PS_DELAY_TIME     (20)

/**
 * enum hdd_green_ap_ps_state - Green-AP power save states
 * @GREEN_AP_PS_IDLE_STATE: the Green_AP is not enabled
 * @GREEN_AP_PS_OFF_STATE: in Power Saving OFF state
 * @GREEN_AP_PS_WAIT_STATE: in transition to Power Saving ON state
 * @GREEN_AP_PS_ON_STATE: in Power Saving ON state
 */
enum hdd_green_ap_ps_state {
	GREEN_AP_PS_IDLE_STATE = 1,
	GREEN_AP_PS_OFF_STATE,
	GREEN_AP_PS_WAIT_STATE,
	GREEN_AP_PS_ON_STATE,
};

/**
 * enum hdd_green_ap_event - Green-AP power save events
 * @GREEN_AP_PS_START_EVENT: event to indicate to enable Green_AP
 * @GREEN_AP_PS_START_EVENT: event to indicate to disable Green_AP
 * @GREEN_AP_ADD_STA_EVENT: event to indicate a new STA connected
 * @GREEN_AP_DEL_STA_EVENT: event to indicate a STA disconnected
 * @GREEN_AP_PS_ON_EVENT: event to indicate to enter Power Saving state
 * @GREEN_AP_PS_WAIT_EVENT: event to indicate in the transition to Power Saving
 */
enum hdd_green_ap_event {
	GREEN_AP_PS_START_EVENT = 1,
	GREEN_AP_PS_STOP_EVENT,
	GREEN_AP_ADD_STA_EVENT,
	GREEN_AP_DEL_STA_EVENT,
	GREEN_AP_PS_ON_EVENT,
	GREEN_AP_PS_WAIT_EVENT,
};

/**
 * struct hdd_green_ap_ctx - Green-AP context
 * @ps_enable: Whether or not Green AP is enabled
 * @ps_on_time: Amount of time to stay in Green AP power saving state
 * @ps_delay_time: Amount of time to delay when changing states
 * @num_nodes: Number of connected clients
 * @ps_state: Current state
 * @ps_event: Event to trigger when timer expires
 * @ps_timer: Event timer
 * @egap_support: Enhanced Green AP support flag
 */
struct hdd_green_ap_ctx {
	uint8_t ps_enable;
	uint32_t ps_on_time;
	uint32_t ps_delay_time;
	uint32_t num_nodes;

	enum hdd_green_ap_ps_state ps_state;
	enum hdd_green_ap_event ps_event;

	qdf_mc_timer_t ps_timer;

	bool egap_support;
};

/**
 * hdd_green_ap_update() - update the current State and Event
 * @hdd_ctx: Global HDD context
 * @state: New state
 * @event: New event
 *
 * Return: none
 */
static void hdd_green_ap_update(struct hdd_context_s *hdd_ctx,
				enum hdd_green_ap_ps_state state,
				enum hdd_green_ap_event event)
{
	struct hdd_green_ap_ctx *green_ap = hdd_ctx->green_ap_ctx;

	green_ap->ps_state = state;
	green_ap->ps_event = event;
}

/**
 * hdd_green_ap_enable() - Send Green AP configuration to firmware
 * @adapter: Adapter upon which Green AP is being configured
 * @enable: Flag which indicates if Green AP is being enabled or disabled
 *
 * Return: 0 upon success, non-zero upon failure
 */
static int hdd_green_ap_enable(hdd_adapter_t *adapter, uint8_t enable)
{
	int ret;

	hdd_debug("Set Green-AP val: %d", enable);

	ret = wma_cli_set_command(adapter->sessionId,
				  WMI_PDEV_GREEN_AP_PS_ENABLE_CMDID,
				  enable, DBG_CMD);

	return ret;
}

/**
 * hdd_green_ap_mc() - Green AP state machine
 * @hdd_ctx: HDD global context
 * @event: New event being processed
 *
 * Return: none
 */
static void hdd_green_ap_mc(struct hdd_context_s *hdd_ctx,
			    enum hdd_green_ap_event event)
{
	struct hdd_green_ap_ctx *green_ap;
	hdd_adapter_t *adapter;

	green_ap = hdd_ctx->green_ap_ctx;
	if (green_ap == NULL)
		return;

	hdd_debug("Green-AP event: %d, state: %d, num_nodes: %d",
		   event, green_ap->ps_state, green_ap->num_nodes);

	/* handle the green ap ps event */
	switch (event) {
	case GREEN_AP_PS_START_EVENT:
		green_ap->ps_enable = 1;
		break;

	case GREEN_AP_PS_STOP_EVENT:
		green_ap->ps_enable = 0;
		break;

	case GREEN_AP_ADD_STA_EVENT:
		green_ap->num_nodes++;
		break;

	case GREEN_AP_DEL_STA_EVENT:
		if (green_ap->num_nodes)
			green_ap->num_nodes--;
		break;

	case GREEN_AP_PS_ON_EVENT:
	case GREEN_AP_PS_WAIT_EVENT:
		break;

	default:
		hdd_err("Invalid event: %d", event);
		break;
	}

	adapter = hdd_get_adapter(hdd_ctx, QDF_SAP_MODE);
	if (adapter == NULL) {
		hdd_err("Green-AP no SAP adapter");
		goto done;
	}

	/* Confirm that power save is enabled before doing state transitions */
	if (!green_ap->ps_enable) {
		hdd_debug("Green-AP is disabled");
		hdd_green_ap_update(hdd_ctx,
				    GREEN_AP_PS_OFF_STATE,
				    GREEN_AP_PS_WAIT_EVENT);
		if (hdd_green_ap_enable(adapter, 0))
			hdd_err("failed to set green ap mode");
		goto done;
	}

	/* handle the green ap ps state */
	switch (green_ap->ps_state) {
	case GREEN_AP_PS_IDLE_STATE:
		hdd_green_ap_update(hdd_ctx,
				    GREEN_AP_PS_OFF_STATE,
				    GREEN_AP_PS_WAIT_EVENT);
		break;

	case GREEN_AP_PS_OFF_STATE:
		if (!green_ap->num_nodes) {
			hdd_green_ap_update(hdd_ctx,
					    GREEN_AP_PS_WAIT_STATE,
					    GREEN_AP_PS_WAIT_EVENT);
			qdf_mc_timer_start(&green_ap->ps_timer,
					   green_ap->ps_delay_time);
		}
		break;

	case GREEN_AP_PS_WAIT_STATE:
		if (!green_ap->num_nodes) {
			hdd_green_ap_update(hdd_ctx,
					    GREEN_AP_PS_ON_STATE,
					    GREEN_AP_PS_WAIT_EVENT);

			hdd_green_ap_enable(adapter, 1);

			if (green_ap->ps_on_time) {
				hdd_green_ap_update(hdd_ctx,
						    0,
						    GREEN_AP_PS_WAIT_EVENT);
				qdf_mc_timer_start(&green_ap->ps_timer,
						   green_ap->ps_on_time);
			}
		} else {
			hdd_green_ap_update(hdd_ctx,
					    GREEN_AP_PS_OFF_STATE,
					    GREEN_AP_PS_WAIT_EVENT);
		}
		break;

	case GREEN_AP_PS_ON_STATE:
		if (green_ap->num_nodes) {
			if (hdd_green_ap_enable(adapter, 0)) {
				hdd_err("FAILED TO SET GREEN-AP mode");
				goto done;
			}
			hdd_green_ap_update(hdd_ctx,
					    GREEN_AP_PS_OFF_STATE,
					    GREEN_AP_PS_WAIT_EVENT);
		} else if ((green_ap->ps_event == GREEN_AP_PS_WAIT_EVENT)
			   && (green_ap->ps_on_time)) {

			/* ps_on_time timeout, switch to ps off */
			hdd_green_ap_update(hdd_ctx,
					    GREEN_AP_PS_WAIT_STATE,
					    GREEN_AP_PS_ON_EVENT);

			if (hdd_green_ap_enable(adapter, 0)) {
				hdd_err("FAILED TO SET GREEN-AP mode");
				goto done;
			}

			qdf_mc_timer_start(&green_ap->ps_timer,
					   green_ap->ps_delay_time);
		}
		break;

	default:
		hdd_err("invalid state %d", green_ap->ps_state);
		hdd_green_ap_update(hdd_ctx, GREEN_AP_PS_OFF_STATE,
				    GREEN_AP_PS_WAIT_EVENT);
		break;
	}

done:
	return;
}

/**
 * hdd_green_ap_timer_fn() - Green AP Timer handler
 * @ctx: Global HDD context
 *
 * Return: none
 */
static void hdd_green_ap_timer_fn(void *ctx)
{
	struct hdd_context_s *hdd_ctx = ctx;
	struct hdd_green_ap_ctx *green_ap;

	if (wlan_hdd_validate_context(hdd_ctx))
		return;

	green_ap = hdd_ctx->green_ap_ctx;
	if (green_ap)
		hdd_green_ap_mc(hdd_ctx, green_ap->ps_event);
}

/**
 * hdd_green_ap_attach() - Attach Green AP context to HDD context
 * @hdd_ctx: Global HDD contect
 *
 * Return: QDF_STATUS_SUCCESS on success, otherwise QDF_STATUS_E_** error
 */
static QDF_STATUS hdd_green_ap_attach(struct hdd_context_s *hdd_ctx)
{
	struct hdd_green_ap_ctx *green_ap;
	QDF_STATUS status = QDF_STATUS_SUCCESS;

	ENTER();

	green_ap = qdf_mem_malloc(sizeof(*green_ap));
	if (!green_ap) {
		hdd_err("Memory allocation for Green-AP failed!");
		status = QDF_STATUS_E_NOMEM;
		goto error;
	}

	green_ap->ps_state = GREEN_AP_PS_OFF_STATE;
	green_ap->ps_event = 0;
	green_ap->num_nodes = 0;
	green_ap->ps_on_time = GREEN_AP_PS_ON_TIME;
	green_ap->ps_delay_time = GREEN_AP_PS_DELAY_TIME;

	qdf_mc_timer_init(&green_ap->ps_timer,
			  QDF_TIMER_TYPE_SW,
			  hdd_green_ap_timer_fn, hdd_ctx);

error:
	hdd_ctx->green_ap_ctx = green_ap;

	EXIT();
	return status;
}

/**
 * hdd_green_ap_deattach() - Detach Green AP context from HDD context
 * @hdd_ctx: Global HDD contect
 *
 * Return: QDF_STATUS_SUCCESS on success, otherwise QDF_STATUS_E_** error
 */
static QDF_STATUS hdd_green_ap_deattach(struct hdd_context_s *hdd_ctx)
{
	struct hdd_green_ap_ctx *green_ap = hdd_ctx->green_ap_ctx;
	QDF_STATUS status = QDF_STATUS_SUCCESS;

	ENTER();

	if (green_ap == NULL) {
		hdd_debug("Green-AP is not enabled");
		status = QDF_STATUS_E_NOSUPPORT;
		goto done;
	}

	/* check if the timer status is destroyed */
	if (QDF_TIMER_STATE_RUNNING ==
	    qdf_mc_timer_get_current_state(&green_ap->ps_timer))
		qdf_mc_timer_stop(&green_ap->ps_timer);

	/* Destroy the Green AP timer */
	if (!QDF_IS_STATUS_SUCCESS(qdf_mc_timer_destroy(&green_ap->ps_timer)))
		hdd_debug("Cannot deallocate Green-AP's timer");

	/* release memory */
	qdf_mem_zero(green_ap, sizeof(*green_ap));
	qdf_mem_free(green_ap);
	hdd_ctx->green_ap_ctx = NULL;

done:

	EXIT();
	return status;
}

/*
 * hdd_green_ap_init() - Initialize Green AP feature
 * (public function documented in wlan_hdd_green_ap.h)
 */
void hdd_green_ap_init(struct hdd_context_s *hdd_ctx)
{
	if (!QDF_IS_STATUS_SUCCESS(hdd_green_ap_attach(hdd_ctx)))
		hdd_err("Failed to allocate Green-AP resource");
}

/*
 * hdd_green_ap_deinit() - De-initialize Green AP feature
 * (public function documented in wlan_hdd_green_ap.h)
 */
void hdd_green_ap_deinit(struct hdd_context_s *hdd_ctx)
{
	if (!QDF_IS_STATUS_SUCCESS(hdd_green_ap_deattach(hdd_ctx)))
		hdd_err("Cannot deallocate Green-AP resource");
}

/*
 * hdd_is_egap_enabled() - Get Enhance Green AP feature status
 * @fw_egap_support: flag whether firmware supports egap or not
 * @cfg: pointer to the struct hdd_config
 *
 * Return: true if firmware, feature_flag and ini are all enabled the egap
 */
static bool hdd_is_egap_enabled(bool fw_egap_support, struct hdd_config *cfg)
{
	/* check if the firmware and ini are both enabled the egap,
	 * and also the feature_flag enable.
	 */
	if (fw_egap_support && cfg->enable_egap &&
			cfg->egap_feature_flag)
		return true;
	return false;
}

/*
 * hdd_enable_egap() - Enable Enhance Green AP
 * @hdd_ctx: HDD global context
 *
 * Return: 0 on success, negative errno on failure
 */
int hdd_enable_egap(struct hdd_context_s *hdd_ctx)
{
	struct hdd_config *cfg;

	if (!hdd_ctx) {
		hdd_err("hdd context is NULL");
		return -EINVAL;
	}

	cfg = hdd_ctx->config;

	if (!cfg) {
		hdd_err("hdd cfg is NULL");
		return -EINVAL;
	}

	if (!hdd_ctx->green_ap_ctx) {
		hdd_err("green ap context is NULL");
		return -EINVAL;
	}

	if (!hdd_is_egap_enabled(hdd_ctx->green_ap_ctx->egap_support,
			hdd_ctx->config))
		return -ENOTSUPP;

	if (QDF_STATUS_SUCCESS != sme_send_egap_conf_params(cfg->enable_egap,
			cfg->egap_inact_time,
			cfg->egap_wait_time,
			cfg->egap_feature_flag))
		return -EINVAL;
	return 0;
}

/*
 * hdd_green_ap_start_bss() - Notify Green AP of Start BSS event
 * (public function documented in wlan_hdd_green_ap.h)
 */
void hdd_green_ap_start_bss(struct hdd_context_s *hdd_ctx)
{
	struct hdd_config *cfg;

	if (!hdd_ctx) {
		hdd_err("hdd context is NULL");
		return;
	}

	cfg = hdd_ctx->config;

	if (!cfg) {
		hdd_err("hdd cfg is NULL");
		return;
	}

	if (!hdd_ctx->green_ap_ctx) {
		hdd_err("Green AP is not enabled. green_ap_ctx = NULL");
		return;
	}

	if (hdd_is_egap_enabled(hdd_ctx->green_ap_ctx->egap_support,
			hdd_ctx->config))
		return;

	if ((hdd_ctx->concurrency_mode & QDF_SAP_MASK) &&
			!(hdd_ctx->concurrency_mode & (QDF_SAP_MASK)) &&
			cfg->enable2x2 && cfg->enableGreenAP) {
		hdd_debug("Green AP enabled - sta_con: %d, 2x2: %d, GAP: %d",
			QDF_STA_MASK & hdd_ctx->concurrency_mode,
			cfg->enable2x2, cfg->enableGreenAP);
		hdd_green_ap_mc(hdd_ctx, GREEN_AP_PS_START_EVENT);
	} else {
		hdd_green_ap_mc(hdd_ctx, GREEN_AP_PS_STOP_EVENT);
		hdd_debug("Green-AP: is disabled, due to sta_concurrency: %d, enable2x2: %d, enableGreenAP: %d",
			   QDF_STA_MASK & hdd_ctx->concurrency_mode,
			   cfg->enable2x2, cfg->enableGreenAP);
	}
}

/*
 * hdd_green_ap_stop_bss() - Notify Green AP of Stop BSS event
 * (public function documented in wlan_hdd_green_ap.h)
 */
void hdd_green_ap_stop_bss(struct hdd_context_s *hdd_ctx)
{
	struct hdd_config *cfg;

	if (!hdd_ctx) {
		hdd_err("hdd context is NULL");
		return;
	}

	cfg = hdd_ctx->config;

	if (!cfg) {
		hdd_err("hdd cfg is NULL");
		return;
	}

	if (!hdd_ctx->green_ap_ctx) {
		hdd_err("Green AP is not enabled. green_ap_ctx = NULL");
		return;
	}

	if (hdd_is_egap_enabled(hdd_ctx->green_ap_ctx->egap_support,
			hdd_ctx->config))
		return;

	/* For AP+AP mode, only trigger GREEN_AP_PS_STOP_EVENT, when the
	 * last AP stops.
	 */

	if (1 == (hdd_ctx->no_of_open_sessions[QDF_SAP_MODE]))
		hdd_green_ap_mc(hdd_ctx, GREEN_AP_PS_STOP_EVENT);
}

/*
 * hdd_green_ap_add_sta() - Notify Green AP of Add Station event
 * (public function documented in wlan_hdd_green_ap.h)
 */
void hdd_green_ap_add_sta(struct hdd_context_s *hdd_ctx)
{
	struct hdd_config *cfg;

	if (!hdd_ctx) {
		hdd_err("hdd context is NULL");
		return;
	}

	cfg = hdd_ctx->config;

	if (!cfg) {
		hdd_err("hdd cfg is NULL");
		return;
	}

	if (!hdd_ctx->green_ap_ctx) {
		hdd_err("Green AP is not enabled. green_ap_ctx = NULL");
		return;
	}

	if (hdd_is_egap_enabled(hdd_ctx->green_ap_ctx->egap_support,
			hdd_ctx->config))
		return;

	hdd_green_ap_mc(hdd_ctx, GREEN_AP_ADD_STA_EVENT);
}

/*
 * hdd_green_ap_del_sta() - Notify Green AP of Delete Station event
 * (public function documented in wlan_hdd_green_ap.h)
 */
void hdd_green_ap_del_sta(struct hdd_context_s *hdd_ctx)
{
	struct hdd_config *cfg;

	if (!hdd_ctx) {
		hdd_err("hdd context is NULL");
		return;
	}

	cfg = hdd_ctx->config;

	if (!cfg) {
		hdd_err("hdd cfg is NULL");
		return;
	}

	if (!hdd_ctx->green_ap_ctx) {
		hdd_err("Green AP is not enabled. green_ap_ctx = NULL");
		return;
	}

	if (hdd_is_egap_enabled(hdd_ctx->green_ap_ctx->egap_support,
			hdd_ctx->config))
		return;

	hdd_green_ap_mc(hdd_ctx, GREEN_AP_DEL_STA_EVENT);
}

/*
 * hdd_green_ap_target_config() - Handle Green AP target configuration
 * (public function documented in wlan_hdd_green_ap.h)
 *
 * Implementation notes:
 * Target indicates whether or not Enhanced Green AP (EGAP) is supported
 */
void hdd_green_ap_target_config(struct hdd_context_s *hdd_ctx,
				struct wma_tgt_cfg *target_config)
{
	struct hdd_green_ap_ctx *green_ap = hdd_ctx->green_ap_ctx;

	green_ap->egap_support = target_config->egap_support;
}
