/*
 * Copyright (c) 2013-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.
 */

/*
 * This file was originally distributed by Qualcomm Atheros, Inc.
 * under proprietary terms before Copyright ownership was assigned
 * to the Linux Foundation.
 */

/**
 * @file wlan_hdd_wowl.c
 *
 * @brief wake up on WLAN API file
 */

/* Include Files */

#include <wlan_hdd_includes.h>
#include <wlan_hdd_wowl.h>

/* Preprocessor Definitions and Constants */
#define WOWL_INTER_PTRN_TOKENIZER   ';'
#define WOWL_INTRA_PTRN_TOKENIZER   ':'

/* Type Declarations */

static char *g_hdd_wowl_ptrns[WOWL_MAX_PTRNS_ALLOWED];
static bool g_hdd_wowl_ptrns_debugfs[WOWL_MAX_PTRNS_ALLOWED] = { 0 };

static uint8_t g_hdd_wowl_ptrns_count;

static inline int find_ptrn_len(const char *ptrn)
{
	int len = 0;
	while (*ptrn != '\0' && *ptrn != WOWL_INTER_PTRN_TOKENIZER) {
		len++;
		ptrn++;
	}
	return len;
}

static void hdd_wowl_callback(void *pContext, QDF_STATUS qdf_ret_status)
{
	hdd_info("Return code = (%d)", qdf_ret_status);
}

#ifdef WLAN_WAKEUP_EVENTS
static void hdd_wowl_wake_indication_callback(void *pContext,
		tpSirWakeReasonInd wake_reason_ind)
{
	hdd_info("Wake Reason %d", wake_reason_ind->ulReason);
	hdd_exit_wowl((hdd_adapter_t *) pContext);
}
#endif

/**
 * dump_hdd_wowl_ptrn() - log wow patterns
 * @ptrn: pointer to wow pattern
 *
 * Return: none
 */
static void dump_hdd_wowl_ptrn(struct wow_add_pattern *ptrn)
{
	int i;

	hdd_info("Pattern Id = 0x%x", ptrn->pattern_id);
	hdd_info("Pattern Byte Offset = 0x%x", ptrn->pattern_byte_offset);
	hdd_info("Pattern_size = 0x%x", ptrn->pattern_size);
	hdd_info("Pattern_mask_size = 0x%x", ptrn->pattern_mask_size);
	hdd_info("Pattern: ");
	for (i = 0; i < ptrn->pattern_size; i++)
		hdd_info(" %02X", ptrn->pattern[i]);
	hdd_info("pattern_mask: ");
	for (i = 0; i < ptrn->pattern_mask_size; i++)
		hdd_info("%02X", ptrn->pattern_mask[i]);
}

/**
 * hdd_add_wowl_ptrn() - Function which will add the WoWL pattern to be
 *			 used when PBM filtering is enabled
 * @pAdapter: pointer to the adapter
 * @ptrn: pointer to the pattern string to be added
 *
 * Return: false if any errors encountered, true otherwise
 */
bool hdd_add_wowl_ptrn(hdd_adapter_t *pAdapter, const char *ptrn)
{
	struct wow_add_pattern localPattern;
	int i, first_empty_slot, len, offset;
	QDF_STATUS qdf_ret_status;
	const char *temp;
	tHalHandle hHal = WLAN_HDD_GET_HAL_CTX(pAdapter);
	uint8_t sessionId = pAdapter->sessionId;
	hdd_context_t *pHddCtx = pAdapter->pHddCtx;

	len = find_ptrn_len(ptrn);

	/* There has to have at least 1 byte for each field (pattern
	 * size, mask size, pattern, mask) e.g. PP:QQ:RR:SS ==> 11
	 * chars
	 */
	while (len >= 11) {
		/* Detect duplicate pattern */
		for (i = 0; i < pHddCtx->config->maxWoWFilters; i++) {
			if (g_hdd_wowl_ptrns[i] == NULL)
				continue;

			if (!memcmp(ptrn, g_hdd_wowl_ptrns[i], len)) {
				/* Pattern Already configured, skip to
				 * next pattern
				 */
				hdd_err("Trying to add duplicate WoWL pattern. Skip it!");
				ptrn += len;
				goto next_ptrn;
			}
		}

		first_empty_slot = -1;

		/* Find an empty slot to store the pattern */
		for (i = 0; i < pHddCtx->config->maxWoWFilters; i++) {
			if (g_hdd_wowl_ptrns[i] == NULL) {
				first_empty_slot = i;
				break;
			}
		}

		/* Maximum number of patterns have been configured already */
		if (first_empty_slot == -1) {
			hdd_err("Cannot add anymore patterns. No free slot!");
			return false;
		}
		/* Validate the pattern */
		if (ptrn[2] != WOWL_INTRA_PTRN_TOKENIZER ||
		    ptrn[5] != WOWL_INTRA_PTRN_TOKENIZER) {
			hdd_err("Malformed pattern string. Skip!");
			ptrn += len;
			goto next_ptrn;
		}
		/* Extract the pattern size */
		localPattern.pattern_size =
			(hex_to_bin(ptrn[0]) * 0x10) +
						hex_to_bin(ptrn[1]);

		/* Extract the pattern mask size */
		localPattern.pattern_mask_size =
			(hex_to_bin(ptrn[3]) * 0x10) +
						hex_to_bin(ptrn[4]);

		if (localPattern.pattern_size > SIR_WOWL_BCAST_PATTERN_MAX_SIZE
		    || localPattern.pattern_mask_size >
		    WOWL_PTRN_MASK_MAX_SIZE) {
			hdd_err("Invalid length specified. Skip!");
			ptrn += len;
			goto next_ptrn;
		}
		/* compute the offset of tokenizer after the pattern */
		offset = 5 + 2 * localPattern.pattern_size + 1;
		if ((offset >= len) ||
		    (ptrn[offset] != WOWL_INTRA_PTRN_TOKENIZER)) {
			hdd_err("Malformed pattern string..skip!");
			ptrn += len;
			goto next_ptrn;
		}
		/* compute the end of pattern sring */
		offset = offset + 2 * localPattern.pattern_mask_size;
		if (offset + 1 != len) {
			/* offset begins with 0 */
			hdd_err("Malformed pattern string...skip!");
			ptrn += len;
			goto next_ptrn;
		}

		temp = ptrn;

		/* Now advance to where pattern begins */
		ptrn += 6;

		/* Extract the pattern */
		for (i = 0; i < localPattern.pattern_size; i++) {
			localPattern.pattern[i] =
				(hex_to_bin(ptrn[0]) * 0x10) +
				hex_to_bin(ptrn[1]);
			ptrn += 2;      /* skip to next byte */
		}

		/* Skip over the ':' seperator after the pattern */
		ptrn++;

		/* Extract the pattern Mask */
		for (i = 0; i < localPattern.pattern_mask_size; i++) {
			localPattern.pattern_mask[i] =
				(hex_to_bin(ptrn[0]) * 0x10) +
				hex_to_bin(ptrn[1]);
			ptrn += 2;      /* skip to next byte */
		}

		/* All is good. Store the pattern locally */
		g_hdd_wowl_ptrns[first_empty_slot] =
			qdf_mem_malloc(len + 1);
		if (g_hdd_wowl_ptrns[first_empty_slot] == NULL) {
			hdd_err("memory allocation failure");
			return false;
		}

		memcpy(g_hdd_wowl_ptrns[first_empty_slot], temp, len);
		g_hdd_wowl_ptrns[first_empty_slot][len] = '\0';
		localPattern.pattern_id = first_empty_slot;
		localPattern.pattern_byte_offset = 0;
		localPattern.session_id = sessionId;

		/* Register the pattern downstream */
		qdf_ret_status =
			sme_wow_add_pattern(hHal, &localPattern,
						   sessionId);
		if (!QDF_IS_STATUS_SUCCESS(qdf_ret_status)) {
			/* Add failed, so invalidate the local storage */
			hdd_err("sme_wowl_add_bcast_pattern failed with error code (%d)",
				  qdf_ret_status);
			qdf_mem_free(g_hdd_wowl_ptrns[first_empty_slot]);
			g_hdd_wowl_ptrns[first_empty_slot] = NULL;
		}

		dump_hdd_wowl_ptrn(&localPattern);

next_ptrn:
		if (*ptrn == WOWL_INTER_PTRN_TOKENIZER) {
			/* move past the tokenizer */
			ptrn += 1;
			len = find_ptrn_len(ptrn);
			continue;
		} else
			break;
	}

	return true;
}

/**
 * hdd_del_wowl_ptrn() - Function which will remove a WoWL pattern
 * @pAdapter: pointer to the adapter
 * @ptrn: pointer to the pattern string to be removed
 *
 * Return: false if any errors encountered, true otherwise
 */
bool hdd_del_wowl_ptrn(hdd_adapter_t *pAdapter, const char *ptrn)
{
	struct wow_delete_pattern delPattern;
	unsigned char id;
	tHalHandle hHal = WLAN_HDD_GET_HAL_CTX(pAdapter);
	bool patternFound = false;
	QDF_STATUS qdf_ret_status;
	uint8_t sessionId = pAdapter->sessionId;
	hdd_context_t *pHddCtx = pAdapter->pHddCtx;

	/* Detect pattern */
	for (id = 0;
	     id < pHddCtx->config->maxWoWFilters
	     && g_hdd_wowl_ptrns[id] != NULL; id++) {
		if (!strcmp(ptrn, g_hdd_wowl_ptrns[id])) {
			patternFound = true;
			break;
		}
	}

	/* If pattern present, remove it from downstream */
	if (patternFound) {
		delPattern.pattern_id = id;
		delPattern.session_id = sessionId;
		qdf_ret_status =
			sme_wow_delete_pattern(hHal, &delPattern,
						   sessionId);
		if (QDF_IS_STATUS_SUCCESS(qdf_ret_status)) {
			/* Remove from local storage as well */
			hdd_err("Deleted pattern with id %d [%s]", id,
				  g_hdd_wowl_ptrns[id]);

			qdf_mem_free(g_hdd_wowl_ptrns[id]);
			g_hdd_wowl_ptrns[id] = NULL;
			return true;
		}
	}
	return false;
}

/**
 * hdd_add_wowl_ptrn_debugfs() - Function which will add a WoW pattern
 *				 sent from debugfs interface
 * @pAdapter: pointer to the adapter
 * @pattern_idx: index of the pattern to be added
 * @pattern_offset: offset of the pattern in the frame payload
 * @pattern_buf: pointer to the pattern hex string to be added
 * @pattern_mask: pointer to the pattern mask hex string
 *
 * Return: false if any errors encountered, true otherwise
 */
bool hdd_add_wowl_ptrn_debugfs(hdd_adapter_t *pAdapter, uint8_t pattern_idx,
			       uint8_t pattern_offset, char *pattern_buf,
			       char *pattern_mask)
{
	struct wow_add_pattern localPattern;
	QDF_STATUS qdf_ret_status;
	tHalHandle hHal = WLAN_HDD_GET_HAL_CTX(pAdapter);
	uint8_t session_id = pAdapter->sessionId;
	uint16_t pattern_len, mask_len, i;

	if (pattern_idx > (WOWL_MAX_PTRNS_ALLOWED - 1)) {
		hdd_err("WoW pattern index %d is out of range (0 ~ %d)",
			pattern_idx, WOWL_MAX_PTRNS_ALLOWED - 1);

		return false;
	}

	pattern_len = strlen(pattern_buf);

	/* Since the pattern is a hex string, 2 characters represent 1 byte. */
	if (pattern_len % 2) {
		hdd_err("Malformed WoW pattern!");

		return false;
	}

	pattern_len >>= 1;
	if (!pattern_len || pattern_len > WOWL_PTRN_MAX_SIZE) {
		hdd_err("WoW pattern length %d is out of range (1 ~ %d).",
			pattern_len, WOWL_PTRN_MAX_SIZE);

		return false;
	}

	localPattern.pattern_id = pattern_idx;
	localPattern.pattern_byte_offset = pattern_offset;
	localPattern.pattern_size = pattern_len;
	localPattern.session_id = session_id;

	if (localPattern.pattern_size > SIR_WOWL_BCAST_PATTERN_MAX_SIZE) {
		hdd_err("WoW pattern size (%d) greater than max (%d)",
			localPattern.pattern_size,
			SIR_WOWL_BCAST_PATTERN_MAX_SIZE);
		return false;
	}
	/* Extract the pattern */
	for (i = 0; i < localPattern.pattern_size; i++) {
		localPattern.pattern[i] =
			(hex_to_bin(pattern_buf[0]) << 4) +
			hex_to_bin(pattern_buf[1]);

		/* Skip to next byte */
		pattern_buf += 2;
	}

	/* Get pattern mask size by pattern length */
	localPattern.pattern_mask_size = pattern_len >> 3;
	if (pattern_len % 8)
		localPattern.pattern_mask_size += 1;

	mask_len = strlen(pattern_mask);
	if ((mask_len % 2)
	    || (localPattern.pattern_mask_size != (mask_len >> 1))) {
		hdd_err("Malformed WoW pattern mask!");

		return false;
	}
	if (localPattern.pattern_mask_size > WOWL_PTRN_MASK_MAX_SIZE) {
		hdd_err("WoW pattern mask size (%d) greater than max (%d)",
			localPattern.pattern_mask_size,
			WOWL_PTRN_MASK_MAX_SIZE);
		return false;
	}
	/* Extract the pattern mask */
	for (i = 0; i < localPattern.pattern_mask_size; i++) {
		localPattern.pattern_mask[i] =
			(hex_to_bin(pattern_mask[0]) << 4) +
			hex_to_bin(pattern_mask[1]);

		/* Skip to next byte */
		pattern_mask += 2;
	}

	/* Register the pattern downstream */
	qdf_ret_status =
		sme_wow_add_pattern(hHal, &localPattern, session_id);

	if (!QDF_IS_STATUS_SUCCESS(qdf_ret_status)) {
		hdd_err("sme_wowl_add_bcast_pattern failed with error code (%d).",
			  qdf_ret_status);

		return false;
	}

	/* All is good. */
	if (!g_hdd_wowl_ptrns_debugfs[pattern_idx]) {
		g_hdd_wowl_ptrns_debugfs[pattern_idx] = 1;
		g_hdd_wowl_ptrns_count++;
	}

	dump_hdd_wowl_ptrn(&localPattern);

	return true;
}

/**
 * hdd_del_wowl_ptrn_debugfs() - Function which will remove a WoW pattern
 *				 sent from debugfs interface
 * @pAdapter: pointer to the adapter
 * @pattern_idx: index of the pattern to be removed
 *
 * Return: false if any errors encountered, true otherwise
 */
bool hdd_del_wowl_ptrn_debugfs(hdd_adapter_t *pAdapter, uint8_t pattern_idx)
{
	struct wow_delete_pattern delPattern;
	tHalHandle hHal = WLAN_HDD_GET_HAL_CTX(pAdapter);
	QDF_STATUS qdf_ret_status;
	uint8_t sessionId = pAdapter->sessionId;

	if (pattern_idx > (WOWL_MAX_PTRNS_ALLOWED - 1)) {
		hdd_err("WoW pattern index %d is not in the range (0 ~ %d).",
			pattern_idx, WOWL_MAX_PTRNS_ALLOWED - 1);

		return false;
	}

	if (!g_hdd_wowl_ptrns_debugfs[pattern_idx]) {
		hdd_err("WoW pattern %d is not in the table.",
			pattern_idx);

		return false;
	}

	delPattern.pattern_id = pattern_idx;
	delPattern.session_id = sessionId;
	qdf_ret_status = sme_wow_delete_pattern(hHal, &delPattern,
						    sessionId);

	if (!QDF_IS_STATUS_SUCCESS(qdf_ret_status)) {
		hdd_err("sme_wowl_del_bcast_pattern failed with error code (%d).",
			 qdf_ret_status);

		return false;
	}

	g_hdd_wowl_ptrns_debugfs[pattern_idx] = 0;
	g_hdd_wowl_ptrns_count--;

	return true;
}

/**
 * hdd_enter_wowl() - Function which will enable WoWL. At least one
 *		      of MP and PBM must be enabled
 * @pAdapter: pointer to the adapter
 * @enable_mp: Whether to enable magic packet WoWL mode
 * @enable_pbm: Whether to enable pattern byte matching WoWL mode
 *
 * Return: false if any errors encountered, true otherwise
 */
bool hdd_enter_wowl(hdd_adapter_t *pAdapter, bool enable_mp, bool enable_pbm)
{
	tSirSmeWowlEnterParams wowParams;
	QDF_STATUS qdf_ret_status;
	tHalHandle hHal = WLAN_HDD_GET_HAL_CTX(pAdapter);

	qdf_mem_zero(&wowParams, sizeof(tSirSmeWowlEnterParams));

	wowParams.ucPatternFilteringEnable = enable_pbm;
	wowParams.ucMagicPktEnable = enable_mp;
	wowParams.sessionId = pAdapter->sessionId;
	if (enable_mp) {
		qdf_copy_macaddr(&wowParams.magic_ptrn,
				 &pAdapter->macAddressCurrent);
	}
#ifdef WLAN_WAKEUP_EVENTS
	wowParams.ucWoWEAPIDRequestEnable = true;
	wowParams.ucWoWEAPOL4WayEnable = true;
	wowParams.ucWowNetScanOffloadMatch = true;
	wowParams.ucWowGTKRekeyError = true;
	wowParams.ucWoWBSSConnLoss = true;
#endif /* WLAN_WAKEUP_EVENTS */

	/* Request to put FW into WoWL */
	qdf_ret_status = sme_enter_wowl(hHal, hdd_wowl_callback, pAdapter,
#ifdef WLAN_WAKEUP_EVENTS
					hdd_wowl_wake_indication_callback,
					pAdapter,
#endif /* WLAN_WAKEUP_EVENTS */
					&wowParams, pAdapter->sessionId);

	if (!QDF_IS_STATUS_SUCCESS(qdf_ret_status)) {
		if (QDF_STATUS_PMC_PENDING != qdf_ret_status) {
			/* We failed to enter WoWL */
			hdd_err("sme_enter_wowl failed with error code (%d)",
				qdf_ret_status);
			return false;
		}
	}
	return true;
}

/**
 * hdd_exit_wowl() - Function which will disable WoWL
 * @pAdapter: pointer to the adapter
 *
 * Return: false if any errors encountered, true otherwise
 */
bool hdd_exit_wowl(hdd_adapter_t *pAdapter)
{
	tSirSmeWowlExitParams wowParams;
	tHalHandle hHal = WLAN_HDD_GET_HAL_CTX(pAdapter);
	QDF_STATUS qdf_ret_status;

	wowParams.sessionId = pAdapter->sessionId;

	qdf_ret_status = sme_exit_wowl(hHal, &wowParams);
	if (!QDF_IS_STATUS_SUCCESS(qdf_ret_status)) {
		hdd_err("sme_exit_wowl failed with error code (%d)",
			qdf_ret_status);
		return false;
	}

	return true;
}

void hdd_deinit_wowl(void)
{
	int i;

	for (i = 0; i < WOWL_MAX_PTRNS_ALLOWED; ++i) {
		if (g_hdd_wowl_ptrns[i]) {
			qdf_mem_free(g_hdd_wowl_ptrns[i]);
			g_hdd_wowl_ptrns[i] = NULL;
		}
	}
}
