// SPDX-License-Identifier: GPL-2.0-only
/*
 * evdi_sysfs.c
 *
 * Copyright (c) 2020 DisplayLink (UK) Ltd.
 *
 * 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, see <http://www.gnu.org/licenses/>.
 */


#include <linux/device.h>
#include <linux/slab.h>
#include <linux/usb.h>

#include "evdi_sysfs.h"
#include "evdi_params.h"
#include "evdi_debug.h"
#include "evdi_platform_drv.h"

#define MAX_EVDI_USB_ADDR 10

static ssize_t version_show(__always_unused struct device *dev,
			    __always_unused struct device_attribute *attr,
			    char *buf)
{
	return snprintf(buf, PAGE_SIZE, "%u.%u.%u\n", DRIVER_MAJOR,
			DRIVER_MINOR, DRIVER_PATCH);
}

static ssize_t count_show(__always_unused struct device *dev,
			  __always_unused struct device_attribute *attr,
			  char *buf)
{
	return snprintf(buf, PAGE_SIZE, "%u\n", evdi_platform_device_count(dev));
}

struct evdi_usb_addr {
	int addr[MAX_EVDI_USB_ADDR];
	int len;
	struct usb_device *usb;
};

#ifdef CONFIG_USB_SUPPORT
static int evdi_platform_device_attach(struct device *device,
		struct evdi_usb_addr *parent_addr);

static ssize_t add_device_with_usb_path(struct device *dev,
			 const char *buf, size_t count)
{
	char *usb_path = kstrdup(buf, GFP_KERNEL);
	char *temp_path = usb_path;
	char *bus_token;
	char *usb_token;
	char *usb_token_copy = NULL;
	char *token;
	char *bus;
	char *port;
	struct evdi_usb_addr usb_addr;

	if (!usb_path)
		return -ENOMEM;

	memset(&usb_addr, 0, sizeof(usb_addr));
	temp_path = strnstr(temp_path, "usb:", count);
	if (!temp_path)
		goto err_parse_usb_path;


	temp_path = strim(temp_path);

	bus_token = strsep(&temp_path, ":");
	if (!bus_token)
		goto err_parse_usb_path;

	usb_token = strsep(&temp_path, ":");
	if (!usb_token)
		goto err_parse_usb_path;

	/* Separate trailing ':*' from usb_token */
	strsep(&temp_path, ":");

	token = usb_token_copy = kstrdup(usb_token, GFP_KERNEL);
	bus = strsep(&token, "-");
	if (!bus)
		goto err_parse_usb_path;
	if (kstrtouint(bus, 10, &usb_addr.addr[usb_addr.len++]))
		goto err_parse_usb_path;

	do {
		port = strsep(&token, ".");
		if (!port)
			goto err_parse_usb_path;
		if (kstrtouint(port, 10, &usb_addr.addr[usb_addr.len++]))
			goto err_parse_usb_path;
	} while (token && port && usb_addr.len < MAX_EVDI_USB_ADDR);

	if (evdi_platform_device_attach(dev, &usb_addr) != 0) {
		EVDI_ERROR("Unable to attach to: %s\n", buf);
		kfree(usb_path);
		kfree(usb_token_copy);
		return -EINVAL;
	}

	EVDI_INFO("Attaching to %s:%s\n", bus_token, usb_token);
	kfree(usb_path);
	kfree(usb_token_copy);
	return count;

err_parse_usb_path:
	EVDI_ERROR("Unable to parse usb path: %s", buf);
	kfree(usb_path);
	kfree(usb_token_copy);
	return -EINVAL;
}

static int find_usb_device_at_path(struct usb_device *usb, void *data)
{
	struct evdi_usb_addr *find_path = (struct evdi_usb_addr *)(data);
	struct usb_device *pdev = usb;
	int port = 0;
	int i;

	i = find_path->len - 1;
	while (pdev != NULL && i >= 0 && i < MAX_EVDI_USB_ADDR) {
		port = pdev->portnum;
		if (port == 0)
			port = pdev->bus->busnum;

		if (port != find_path->addr[i])
			return 0;

		if (pdev->parent == NULL && i == 0) {
			find_path->usb = usb;
			return 1;
		}
		pdev = pdev->parent;
		i--;
	}

	return 0;
}

static int evdi_platform_device_attach(struct device *device,
		struct evdi_usb_addr *parent_addr)
{
	struct device *parent = NULL;

	if (!parent_addr)
		return -EINVAL;

	if (!usb_for_each_dev(parent_addr, find_usb_device_at_path) ||
	    !parent_addr->usb)
		return -EINVAL;

	parent = &parent_addr->usb->dev;
	return evdi_platform_device_add(device, parent);
}

#else /* !CONFIG_USB_SUPPORT */

static ssize_t add_device_with_usb_path(struct device *dev,
			 const char *buf, size_t count)
{
	return -EINVAL;
}

#endif /* CONFIG_USB_SUPPORT */

static ssize_t add_store(struct device *dev,
			 __always_unused struct device_attribute *attr,
			 const char *buf, size_t count)
{
	unsigned int val;
	int ret;

	if (strnstr(buf, "usb:", count))
		return add_device_with_usb_path(dev, buf, count);

	if (kstrtouint(buf, 10, &val)) {
		EVDI_ERROR("Invalid device count \"%s\"\n", buf);
		return -EINVAL;
	}

	ret = evdi_platform_add_devices(dev, val);
	if (ret)
		return ret;

	return count;
}

static ssize_t remove_all_store(struct device *dev,
				__always_unused struct device_attribute *attr,
				__always_unused const char *buf,
				size_t count)
{
	evdi_platform_remove_all_devices(dev);
	return count;
}

static ssize_t loglevel_show(__always_unused struct device *dev,
			     __always_unused struct device_attribute *attr,
			     char *buf)
{
	return snprintf(buf, PAGE_SIZE, "%u\n", evdi_loglevel);
}

static ssize_t loglevel_store(__always_unused struct device *dev,
			      __always_unused struct device_attribute *attr,
			      const char *buf,
			      size_t count)
{
	unsigned int val;

	if (kstrtouint(buf, 10, &val)) {
		EVDI_ERROR("Unable to parse %u\n", val);
		return -EINVAL;
	}
	if (val > EVDI_LOGLEVEL_VERBOSE) {
		EVDI_ERROR("Invalid loglevel %u\n", val);
		return -EINVAL;
	}

	EVDI_INFO("Setting loglevel to %u\n", val);
	evdi_loglevel = val;
	return count;
}

static struct device_attribute evdi_device_attributes[] = {
	__ATTR_RO(count),
	__ATTR_RO(version),
	__ATTR_RW(loglevel),
	__ATTR_WO(add),
	__ATTR_WO(remove_all)
};

void evdi_sysfs_init(struct device *root)
{
	unsigned int i;

	if (!PTR_ERR_OR_ZERO(root))
		for (i = 0; i < ARRAY_SIZE(evdi_device_attributes); i++)
			device_create_file(root, &evdi_device_attributes[i]);
}

void evdi_sysfs_exit(struct device *root)
{
	unsigned int i;

	if (PTR_ERR_OR_ZERO(root)) {
		EVDI_ERROR("root device is null");
		return;
	}
	for (i = 0; i < ARRAY_SIZE(evdi_device_attributes); i++)
		device_remove_file(root, &evdi_device_attributes[i]);
}

