/*! \file talloc_ctx_vty.c
 * Osmocom talloc context introspection via VTY. */
/*
 * (C) 2017 by Vadim Yanitskiy <axilirator@gmail.com>
 *
 * All Rights Reserved
 *
 * SPDX-License-Identifier: GPL-2.0+
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 */

#include <stdio.h>
#include <regex.h>
#include <string.h>
#include <talloc.h>

#include <osmocom/vty/command.h>
#include <osmocom/vty/vty.h>

extern void *tall_vty_ctx;
extern struct host host;

enum walk_filter_type {
	WALK_FILTER_NONE = 0,
	WALK_FILTER_REGEXP,
	WALK_FILTER_TREE,
};

struct walk_cb_params {
	enum walk_filter_type filter;
	unsigned int depth_pass;
	const void *chunk_ptr;
	struct vty *vty;
	regex_t regexp;
};

/*!
 * Print a talloc memory hierarchy to the given VTY.
 * To be called by the talloc_report_depth_cb().
 * If one of supported filters is specified, then
 * only satisfying memory trees would be printed.
 *
 * @param chunk     The talloc chunk to be printed
 * @param depth     Current depth value
 * @param max_depth Maximal depth of report (negative means full)
 * @param is_ref    Is this chunk a reference?
 * @param data      The walk_cb_params struct instance
 */
static void talloc_ctx_walk_cb(const void *chunk, int depth,
	int max_depth, int is_ref, void *data)
{
	struct walk_cb_params *p = (struct walk_cb_params *) data;
	const char *chunk_name = talloc_get_name(chunk);
	struct vty *vty = p->vty;
	size_t chunk_blocks;
	size_t chunk_size;
	int rc;

	if (depth > 0 && p->filter) {
		/**
		 * A filter is being bypassed while current depth value
		 * is higher than the 'depth_pass', i.e. the callback does
		 * processing the child memory chunks. As soon as this
		 * condition becomes false, we need to 'enable' a filter,
		 * and resume the processing other chunks.
		 */
		if (p->depth_pass && depth > p->depth_pass)
			goto filter_bypass;
		else
			p->depth_pass = 0;

		switch (p->filter) {
		case WALK_FILTER_REGEXP:
			/* Filter chunks using a regular expression */
			rc = regexec(&p->regexp, chunk_name, 0, NULL, 0);
			if (rc)
				return;
			break;
		case WALK_FILTER_TREE:
			/* Print a specific memory tree only */
			if (chunk != p->chunk_ptr)
				return;
			break;
		default:
			/* Unsupported filter or incorrect value */
			return;
		}

		/**
		 * As soon as a filter passes any chunk, all the memory
		 * tree starting from one would be printed. To do that,
		 * we need to temporary 'disable' a filter for child
		 * chunks (current_depth > depth_pass).
		 */
		p->depth_pass = depth;
	}

filter_bypass:

	if (is_ref) {
		vty_out(vty, "%*sreference to: %s%s",
			depth * 2, "", chunk_name, VTY_NEWLINE);
		return;
	}

	chunk_blocks = talloc_total_blocks(chunk);
	chunk_size = talloc_total_size(chunk);

	if (depth == 0) {
		vty_out(vty, "%stalloc report on '%s' "
			"(total %6zu bytes in %3zu blocks)%s",
			(max_depth < 0 ? "full " : ""), chunk_name,
			chunk_size, chunk_blocks, VTY_NEWLINE);
		return;
	}

	vty_out(vty, "%*s%-30s contains %6zu bytes "
		"in %3zu blocks (ref %zu) %p%s", depth * 2, "",
		chunk_name, chunk_size, chunk_blocks,
		talloc_reference_count(chunk),
		chunk, VTY_NEWLINE);
}

/*!
 * Parse talloc context and depth values from a VTY command.
 *
 * @param ctx    The context to be printed (a string from argv)
 * @param depth  The report depth (a string from argv)
 * @param params The walk_cb_params struct instance
 */
static void talloc_ctx_walk(const char *ctx, const char *depth,
	struct walk_cb_params *params)
{
	const void *talloc_ctx = NULL;
	int max_depth;

	/* Determine a context for report */
	if (!strncmp(ctx, "app", 3))
		talloc_ctx = host.app_info->tall_ctx;
	else if (!strncmp(ctx, "all", 3))
		talloc_ctx = NULL;

	/* Determine report depth */
	if (depth[0] == 'f')
		max_depth = -1;
	else if (depth[0] == 'b')
		max_depth = 1;
	else
		max_depth = atoi(depth);

	talloc_report_depth_cb(talloc_ctx, 0, max_depth,
		&talloc_ctx_walk_cb, params);
}

#define BASE_CMD_STR \
	"show talloc-context (application|all) (full|brief|DEPTH)"

#define BASE_CMD_DESCR \
	SHOW_STR "Show talloc memory hierarchy\n" \
	"Application's context\n" \
	"All contexts, if NULL-context tracking is enabled\n" \
	"Display a full talloc memory hierarchy\n" \
	"Display a brief talloc memory hierarchy\n" \
	"Specify required maximal depth value\n"

DEFUN(show_talloc_ctx, show_talloc_ctx_cmd,
	BASE_CMD_STR, BASE_CMD_DESCR)
{
	struct walk_cb_params params = { 0 };

	/* Set up callback parameters */
	params.filter = WALK_FILTER_NONE;
	params.vty = vty;

	talloc_ctx_walk(argv[0], argv[1], &params);
	return CMD_SUCCESS;
}

DEFUN(show_talloc_ctx_filter, show_talloc_ctx_filter_cmd,
	BASE_CMD_STR " filter REGEXP", BASE_CMD_DESCR
	"Filter chunks using regular expression\n"
	"Regular expression\n")
{
	struct walk_cb_params params = { 0 };
	int rc;

	/* Attempt to compile a regular expression */
	rc = regcomp(&params.regexp, argv[2], REG_NOSUB);
	if (rc) {
		vty_out(vty, "Invalid expression%s", VTY_NEWLINE);
		return CMD_WARNING;
	}

	/* Set up callback parameters */
	params.filter = WALK_FILTER_REGEXP;
	params.vty = vty;

	talloc_ctx_walk(argv[0], argv[1], &params);
	regfree(&params.regexp);

	return CMD_SUCCESS;
}

DEFUN(show_talloc_ctx_tree, show_talloc_ctx_tree_cmd,
	BASE_CMD_STR " tree ADDRESS", BASE_CMD_DESCR
	"Display only a specific memory chunk\n"
	"Chunk address (e.g. 0xdeadbeef)\n")
{
	struct walk_cb_params params = { 0 };
	int rc;

	/* Attempt to parse an address */
	rc = sscanf(argv[2], "%p", &params.chunk_ptr);
	if (rc != 1) {
		vty_out(vty, "Invalid chunk address%s", VTY_NEWLINE);
		return CMD_WARNING;
	}

	/* Set up callback parameters */
	params.filter = WALK_FILTER_TREE;
	params.vty = vty;

	talloc_ctx_walk(argv[0], argv[1], &params);
	return CMD_SUCCESS;
}

/*!
 * Install VTY commands for talloc context introspection.
 *
 * This installs a set of VTY commands for introspection of
 * a talloc context. Call this once from your application
 * if you want to support those commands.
 */
void osmo_talloc_vty_add_cmds(void)
{
	install_element_ve(&show_talloc_ctx_cmd);
	install_element_ve(&show_talloc_ctx_tree_cmd);
	install_element_ve(&show_talloc_ctx_filter_cmd);
}
