// SPDX-License-Identifier: (GPL-2.0+ OR MIT)
/*
 * drivers/amlogic/memory_ext/ram_dump.c
 *
 * Copyright (C) 2017 Amlogic, Inc. All rights reserved.
 *
 * 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.
 *
 */
#define DEBUG
#include <linux/version.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/platform_device.h>
#include <linux/mutex.h>
#include <linux/io.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/slab.h>
#include <linux/mm.h>
#include <linux/reboot.h>
#include <linux/memblock.h>
#include <linux/sched/clock.h>
#include <linux/amlogic/ramdump.h>
#include <linux/amlogic/reboot.h>
#include <linux/arm-smccc.h>
#include <linux/of.h>
#include <linux/highmem.h>
#include <linux/syscalls.h>
#include <linux/fs_struct.h>
#include <linux/mount.h>
#include <linux/namei.h>
#include <linux/capability.h>
#include <asm/cacheflush.h>
#include <linux/amlogic/gki_module.h>
#include <trace/hooks/debug.h>
#include <linux/panic_notifier.h>
#include <linux/of_address.h>

static unsigned long ramdump_base;
static unsigned long ramdump_size;
static bool ramdump_disable			=	1;

#define WAIT_TIMEOUT		(40ULL * 1000 * 1000 * 1000)
#define SAVE_DATA_BY_INIT_RC_SHELL			1
#define RESERVE_MEM_BY_RAMDUMP_DTS_NODE		1

struct ramdump {
	unsigned long		mem_size;
	unsigned long		mem_base;
#ifdef SAVE_DATA_BY_INIT_RC_SHELL
	//void __iomem		*mem_base;
	struct mutex		lock;
	struct kobject		*kobj;
#else
	unsigned long long	tick;
	void				*mnt_buf;
	const char			*storage_device;
#endif
	struct delayed_work	work;
	int			disable;
};

static struct ramdump *ram;

static void ramdump_parse_info(void)
{
#if !IS_MODULE(CONFIG_AMLOGIC_MEMORY_DEBUG)
	pr_info("%s, .text : 0x%px - 0x%px, pa(.text): 0x%lx\n",
			__func__, (unsigned long *)_text,
			(unsigned long *)_etext, (unsigned long)__pa_symbol(_text));
	pr_info("%s, kimage_voffset = 0x%px\n",
			__func__, (unsigned long)_text - (unsigned long)__pa_symbol(_text));
#endif

#ifdef CONFIG_ARM64
	pr_info("%s, kimage_vaddr: 0x%px, v2p: 0x%lx\n",
			__func__, (unsigned long *)kimage_vaddr,
			(unsigned long)__pa_symbol(kimage_vaddr));
	pr_info("%s, KIMAGE_VADDR: 0x%px, v2p: 0x%lx\n",
			__func__, (unsigned long *)KIMAGE_VADDR,
			(unsigned long)__pa_symbol(KIMAGE_VADDR));
	pr_info("%s, kimage_voffset = 0x%lx\n", __func__,
			(unsigned long)kimage_vaddr - (unsigned long)__pa_symbol(kimage_vaddr));
	pr_info("%s, kaslr_offset   = 0x%lx\n", __func__, kaslr_offset());
	pr_info("%s, vabits_actual  = %d\n", __func__, (unsigned int)vabits_actual);
#endif
}

static int early_ramdump_para(char *buf)
{
	int ret;
#ifdef	RESERVE_MEM_BY_RAMDUMP_DTS_NODE
	struct device_node *node;
	struct resource res;
#endif

	if (!buf)
		return -EINVAL;

	pr_info("%s:%s\n", __func__, buf);
	if (strcmp(buf, "disabled") == 0) {
		ramdump_disable = 1;
	} else {
		ret = sscanf(buf, "%lx,%lx", &ramdump_base, &ramdump_size);
		if (ret != 2) {
			pr_err("invalid boot args\n");
			ramdump_disable = 1;
		}
		ramdump_disable = 0;
		pr_info("%s, base:%lx, size:%lx\n",
			__func__, ramdump_base, ramdump_size);
#ifdef	RESERVE_MEM_BY_RAMDUMP_DTS_NODE
		node = of_find_node_by_path("/reserved-memory/ramdump_bl33z");
		ret = of_address_to_resource(node, 0, &res);
		pr_info("%s, dts reserved-memory %lx - %lx\n",
					__func__, (unsigned long)res.start, (unsigned long)res.end);
#else
		ret = memblock_reserve(ramdump_base, PAGE_ALIGN(ramdump_size));
		if (ret < 0) {
			pr_info("%s, reserve memblock %lx - %lx failed\n",
				__func__, ramdump_base,
				ramdump_base + PAGE_ALIGN(ramdump_size));
			ramdump_disable = 1;
		} else {
			pr_info("%s, reserve memblock %lx - %lx OK\n",
				__func__, ramdump_base,
				ramdump_base + PAGE_ALIGN(ramdump_size));
		}
#endif
		ramdump_parse_info();
	}
	return 0;
}

early_param("ramdump", early_ramdump_para);

#ifdef SAVE_DATA_BY_INIT_RC_SHELL
static ssize_t ramdump_bin_read(struct file *filp, struct kobject *kobj,
				struct bin_attribute *attr,
				char *buf, loff_t off, size_t count)
{
	void *p = NULL;

	if (!ram->mem_base || off >= ram->mem_size) {
		pr_info("%s, crash sysfsnode data err.\n", __func__);
		return 0;
	}

	if (off + count > ram->mem_size)
		count = ram->mem_size - off;

	p = (void *)phys_to_virt(ram->mem_base + off);

	mutex_lock(&ram->lock);
	memcpy(buf, p, count);
	mutex_unlock(&ram->lock);

	/* debug when read end */
	if (off + count >= ram->mem_size)
		pr_info("%s, p=%p %p, off:%lli, c:%zi\n",
			__func__, buf, p, off, count);

	return count;
}

int ramdump_disabled(void)
{
	if (ram)
		return ram->disable;
	return 0;
}
EXPORT_SYMBOL(ramdump_disabled);

static void meson_set_reboot_reason(int reboot_reason)
{
	struct arm_smccc_res smccc;

	arm_smccc_smc(SET_REBOOT_REASON,
		      reboot_reason, 0, 0, 0, 0, 0, 0, &smccc);
}

static ssize_t ramdump_bin_write(struct file *filp,
				 struct kobject *kobj,
				 struct bin_attribute *bin_attr,
				 char *buf, loff_t off, size_t count)
{
	if (ram->mem_base && !strncmp("reboot", buf, 6))
		kernel_restart("RAM-DUMP finished\n");

	if (!strncmp("disable", buf, 7)) {
		ram->disable = 1;
		meson_set_reboot_reason(MESON_NORMAL_BOOT);
	}
	if (!strncmp("enable", buf, 6)) {
		ram->disable = 0;
		meson_set_reboot_reason(MESON_KERNEL_PANIC);
	}

	return count;
}

static struct bin_attribute ramdump_attr = {
	.attr = {
		.name = "compmsg",
		.mode = 0664,
	},
	.read  = ramdump_bin_read,
	.write = ramdump_bin_write,
};

#ifdef CONFIG_ARM64
noinline void ramdump_sync_data(void)
{
	/*
	 * back port from old kernel version for function
	 * flush_cache_all(), we need it for ram dump
	 */
	asm volatile
		("mov	x12, x30\n"
		"dsb	sy\n"
		"mrs	x0, clidr_el1\n"
		"and	x3, x0, #0x7000000\n"
		"lsr	x3, x3, #23\n"
		"cbz	x3, finished\n"
		"mov	x10, #0\n"
	"loop1:\n"
		"add	x2, x10, x10, lsr #1\n"
		"lsr	x1, x0, x2\n"
		"and	x1, x1, #7\n"
		"cmp	x1, #2\n"
		"b.lt	skip\n"
		"mrs	x9, daif\n"
		"msr    daifset, #2\n"
		"msr	csselr_el1, x10\n"
		"isb\n"
		"mrs	x1, ccsidr_el1\n"
		"msr    daif, x9\n"
		"and	x2, x1, #7\n"
		"add	x2, x2, #4\n"
		"mov	x4, #0x3ff\n"
		"and	x4, x4, x1, lsr #3\n"
		"clz	w5, w4\n"
		"mov	x7, #0x7fff\n"
		"and	x7, x7, x1, lsr #13\n"
	"loop2:\n"
		"mov	x9, x4\n"
	"loop3:\n"
		"lsl	x6, x9, x5\n"
		"orr	x11, x10, x6\n"
		"lsl	x6, x7, x2\n"
		"orr	x11, x11, x6\n"
		"dc	cisw, x11\n"
		"subs	x9, x9, #1\n"
		"b.ge	loop3\n"
		"subs	x7, x7, #1\n"
		"b.ge	loop2\n"
	"skip:\n"
		"add	x10, x10, #2\n"
		"cmp	x3, x10\n"
		"b.gt	loop1\n"
	"finished:\n"
		"mov	x10, #0\n"
		"msr	csselr_el1, x10\n"
		"dsb	sy\n"
		"isb\n"
		"mov	x0, #0\n"
		"ic	ialluis\n"
		"ret	x12\n");
}
#else
noinline void ramdump_sync_data(void)
{
	flush_cache_all();
}
#endif

#endif /* end SAVE_DATA_BY_INIT_RC_SHELL */

/*
 * clear memory to avoid large amount of memory not used.
 * for random data, it's hard to compress
 */
static void lazy_clear_work(struct work_struct *work)
{
	struct page *page;
	struct list_head head, *pos, *next;
	void *virt;
	int order;
	gfp_t flags = __GFP_NORETRY   |
		      __GFP_NOWARN    |
		      __GFP_MOVABLE;
	unsigned long clear = 0, size = 0, free = 0, tick;

	INIT_LIST_HEAD(&head);
	order = MAX_ORDER - 3;
	tick = sched_clock();
	do {
		page = alloc_pages(flags, order);
		if (page) {
			list_add(&page->lru, &head);
			virt = page_address(page);
			size = (1 << order) * PAGE_SIZE;
			memset(virt, 0, size);
			clear += size;
		}
	} while (page);
	tick = sched_clock() - tick;

	list_for_each_safe(pos, next, &head) {
		page = list_entry(pos, struct page, lru);
		list_del(&page->lru);
		__free_pages(page, order);
		free += size;
	}
	pr_info("ramdump, clear:%lx, free:%lx, tick:%ld us\n",
		clear, free, tick / 1000);
}

#if defined(CONFIG_TRACEPOINTS) && defined(CONFIG_ANDROID_VENDOR_HOOKS)
static void do_flush_cpu_cache(void)
{
	int cpu = smp_processor_id();

	pr_info("ramdump: CPU-%d flush cache ...\n", cpu);
	ramdump_sync_data();
	pr_info("ramdump: CPU-%d flush cache finish.\n", cpu);
}

static void flush_all_cache_hook(void *data, struct pt_regs *regs)
{
	do_flush_cpu_cache();
}

static int panic_notify(struct notifier_block *self,
			unsigned long cmd, void *ptr)
{
	do_flush_cpu_cache();
	return NOTIFY_DONE;
}

static struct notifier_block panic_notifier = {
	.notifier_call	= panic_notify,
};
#endif

static int __init ramdump_probe(struct platform_device *pdev)
{
	unsigned long total_mem;
	struct resource *res;
	unsigned int dump_set;
	void __iomem *base;
	void *vaddr = NULL;
	int ret = 0;

	total_mem = get_num_physpages() << PAGE_SHIFT;
	pr_info("ramdump, Total Memory:[%ld MB]\n", total_mem / 1024 / 1024);

#if defined(CONFIG_TRACEPOINTS) && defined(CONFIG_ANDROID_VENDOR_HOOKS)
	if (!ramdump_disable) {
		/* flush cache for online cpu */
		ret = register_trace_android_vh_ipi_stop(flush_all_cache_hook, NULL);
		if (ret)
			pr_err("%s, register_trace_android_vh_ipi_stop err(%d).\n", __func__, ret);

		/* flush cache for panic cpu */
		ret = atomic_notifier_chain_register(&panic_notifier_list, &panic_notifier);
		if (ret)
			pr_err("%s, register panic_notifier err(%d).\n", __func__, ret);
	}
#endif

	ram = kzalloc(sizeof(*ram), GFP_KERNEL);
	if (!ram)
		return -ENOMEM;

	if (ramdump_disable)
		ram->disable = 1;

	ram->mem_base = 0;
	ram->mem_size = ramdump_size;
	if (!ramdump_base || !ramdump_size) {
		pr_info("NO valid ramdump args:%lx %lx\n",
			ramdump_base, ramdump_size);
	} else {
		pr_info("%s, memremap start, paddr area: 0x%08lx - 0x%08lx\n",
				__func__, ramdump_base, ramdump_base + PAGE_ALIGN(ramdump_size));
		//vaddr = ioremap_cache(ramdump_base, PAGE_ALIGN(ramdump_size));
		vaddr = memremap(ramdump_base, PAGE_ALIGN(ramdump_size), MEMREMAP_WB);
		if (vaddr)
			ram->mem_base = (unsigned long)vaddr;

		pr_info("%s, memremap end, vaddr_base:%lx, size:%lx\n",
			__func__, ram->mem_base, ram->mem_size);
	}

	if (!ram->disable) {
		if (!ram->mem_base) {	/* No compressed data */
			INIT_DELAYED_WORK(&ram->work, lazy_clear_work);
			schedule_delayed_work(&ram->work, msecs_to_jiffies(100));
		} else {		/* with compressed data */
#ifdef	SAVE_DATA_BY_INIT_RC_SHELL
			pr_info("%s, SAVE_DATA_BY_INIT_RC_SHELL\n", __func__);
			ram->kobj = kobject_create_and_add("mdump", kernel_kobj);
			if (!ram->kobj) {
				pr_err("%s, create sysfs /mdump failed\n", __func__);
				goto err;
			}

			ramdump_attr.size = ram->mem_size;
			pr_info("%s, creat /sys/kernel/mdump/compmsg\n", __func__);
			if (sysfs_create_bin_file(ram->kobj, &ramdump_attr)) {
				pr_err("%s, create sysfs compmsg failed\n", __func__);
				goto err1;
			}
#endif	/* end SAVE_DATA_BY_INIT_RC_SHELL */
		}

		/* if ramdump is disabled in env, no need to set sticky reg */
		res = platform_get_resource_byname(pdev, IORESOURCE_MEM,
						   "SYSCTRL_STICKY_REG6");
		if (res) {
			base = devm_ioremap(&pdev->dev, res->start,
					    res->end - res->start);
			if (!base) {
				pr_err("%s, map reg failed\n", __func__);
				goto err;
			}
			dump_set = readl(base);
			dump_set &= ~RAMDUMP_STICKY_DATA_MASK;
			dump_set |= ((total_mem >> 20) | AMLOGIC_KERNEL_BOOTED);
			writel(dump_set, base);
			pr_info("%s, set sticky(0x%08x) to 0x%x\n",
					__func__, (unsigned int)res->start, dump_set);
		}
	}
	return ret;
#ifdef	SAVE_DATA_BY_INIT_RC_SHELL
err1:
	kobject_put(ram->kobj);
#endif

err:
	kfree(ram);

	return -EINVAL;
}

static int ramdump_remove(struct platform_device *pdev)
{
#ifdef SAVE_DATA_BY_INIT_RC_SHELL
	sysfs_remove_bin_file(ram->kobj, &ramdump_attr);
	iounmap((void *)ram->mem_base);
	kobject_put(ram->kobj);
#endif
	kfree(ram);
	return 0;
}

#ifdef CONFIG_OF
static const struct of_device_id ramdump_dt_match[] = {
	{
		.compatible = "amlogic, ram_dump",
	},
	{},
};
#endif

static struct platform_driver ramdump_driver = {
	.driver = {
		.name  = "mdump",
		.owner = THIS_MODULE,
	#ifdef CONFIG_OF
		.of_match_table = ramdump_dt_match,
	#endif
	},
	.remove = ramdump_remove,
};

int __init ramdump_init(void)
{
	int ret;

	ret = platform_driver_probe(&ramdump_driver, ramdump_probe);

	return ret;
}

void __exit ramdump_uninit(void)
{
	platform_driver_unregister(&ramdump_driver);
}

