// SPDX-License-Identifier: GPL-2.0
/* Copyright (C) 2021 Gerhard Engleder <gerhard@engleder-embedded.com> */

#include "tsnep.h"

#include <net/pkt_sched.h>

/* save one operation at the end for additional operation at list change */
#define TSNEP_MAX_GCL_NUM (TSNEP_GCL_COUNT - 1)

static int tsnep_validate_gcl(struct tc_taprio_qopt_offload *qopt)
{
	int i;
	u64 cycle_time;

	if (!qopt->cycle_time)
		return -ERANGE;
	if (qopt->num_entries > TSNEP_MAX_GCL_NUM)
		return -EINVAL;
	cycle_time = 0;
	for (i = 0; i < qopt->num_entries; i++) {
		if (qopt->entries[i].command != TC_TAPRIO_CMD_SET_GATES)
			return -EINVAL;
		if (qopt->entries[i].gate_mask & ~TSNEP_GCL_MASK)
			return -EINVAL;
		if (qopt->entries[i].interval < TSNEP_GCL_MIN_INTERVAL)
			return -EINVAL;
		cycle_time += qopt->entries[i].interval;
	}
	if (qopt->cycle_time != cycle_time)
		return -EINVAL;
	if (qopt->cycle_time_extension >= qopt->cycle_time)
		return -EINVAL;

	return 0;
}

static void tsnep_write_gcl_operation(struct tsnep_gcl *gcl, int index,
				      u32 properties, u32 interval, bool flush)
{
	void __iomem *addr = gcl->addr +
			     sizeof(struct tsnep_gcl_operation) * index;

	gcl->operation[index].properties = properties;
	gcl->operation[index].interval = interval;

	iowrite32(properties, addr);
	iowrite32(interval, addr + sizeof(u32));

	if (flush) {
		/* flush write with read access */
		ioread32(addr);
	}
}

static u64 tsnep_change_duration(struct tsnep_gcl *gcl, int index)
{
	u64 duration;
	int count;

	/* change needs to be triggered one or two operations before start of
	 * new gate control list
	 * - change is triggered at start of operation (minimum one operation)
	 * - operation with adjusted interval is inserted on demand to exactly
	 *   meet the start of the new gate control list (optional)
	 *
	 * additionally properties are read directly after start of previous
	 * operation
	 *
	 * therefore, three operations needs to be considered for the limit
	 */
	duration = 0;
	count = 3;
	while (count) {
		duration += gcl->operation[index].interval;

		index--;
		if (index < 0)
			index = gcl->count - 1;

		count--;
	}

	return duration;
}

static void tsnep_write_gcl(struct tsnep_gcl *gcl,
			    struct tc_taprio_qopt_offload *qopt)
{
	int i;
	u32 properties;
	u64 extend;
	u64 cut;

	gcl->base_time = ktime_to_ns(qopt->base_time);
	gcl->cycle_time = qopt->cycle_time;
	gcl->cycle_time_extension = qopt->cycle_time_extension;

	for (i = 0; i < qopt->num_entries; i++) {
		properties = qopt->entries[i].gate_mask;
		if (i == (qopt->num_entries - 1))
			properties |= TSNEP_GCL_LAST;

		tsnep_write_gcl_operation(gcl, i, properties,
					  qopt->entries[i].interval, true);
	}
	gcl->count = qopt->num_entries;

	/* calculate change limit; i.e., the time needed between enable and
	 * start of new gate control list
	 */

	/* case 1: extend cycle time for change
	 * - change duration of last operation
	 * - cycle time extension
	 */
	extend = tsnep_change_duration(gcl, gcl->count - 1);
	extend += gcl->cycle_time_extension;

	/* case 2: cut cycle time for change
	 * - maximum change duration
	 */
	cut = 0;
	for (i = 0; i < gcl->count; i++)
		cut = max(cut, tsnep_change_duration(gcl, i));

	/* use maximum, because the actual case (extend or cut) can be
	 * determined only after limit is known (chicken-and-egg problem)
	 */
	gcl->change_limit = max(extend, cut);
}

static u64 tsnep_gcl_start_after(struct tsnep_gcl *gcl, u64 limit)
{
	u64 start = gcl->base_time;
	u64 n;

	if (start <= limit) {
		n = div64_u64(limit - start, gcl->cycle_time);
		start += (n + 1) * gcl->cycle_time;
	}

	return start;
}

static u64 tsnep_gcl_start_before(struct tsnep_gcl *gcl, u64 limit)
{
	u64 start = gcl->base_time;
	u64 n;

	n = div64_u64(limit - start, gcl->cycle_time);
	start += n * gcl->cycle_time;
	if (start == limit)
		start -= gcl->cycle_time;

	return start;
}

static u64 tsnep_set_gcl_change(struct tsnep_gcl *gcl, int index, u64 change,
				bool insert)
{
	/* previous operation triggers change and properties are evaluated at
	 * start of operation
	 */
	if (index == 0)
		index = gcl->count - 1;
	else
		index = index - 1;
	change -= gcl->operation[index].interval;

	/* optionally change to new list with additional operation in between */
	if (insert) {
		void __iomem *addr = gcl->addr +
				     sizeof(struct tsnep_gcl_operation) * index;

		gcl->operation[index].properties |= TSNEP_GCL_INSERT;
		iowrite32(gcl->operation[index].properties, addr);
	}

	return change;
}

static void tsnep_clean_gcl(struct tsnep_gcl *gcl)
{
	int i;
	u32 mask = TSNEP_GCL_LAST | TSNEP_GCL_MASK;
	void __iomem *addr;

	/* search for insert operation and reset properties */
	for (i = 0; i < gcl->count; i++) {
		if (gcl->operation[i].properties & ~mask) {
			addr = gcl->addr +
			       sizeof(struct tsnep_gcl_operation) * i;

			gcl->operation[i].properties &= mask;
			iowrite32(gcl->operation[i].properties, addr);

			break;
		}
	}
}

static u64 tsnep_insert_gcl_operation(struct tsnep_gcl *gcl, int ref,
				      u64 change, u32 interval)
{
	u32 properties;

	properties = gcl->operation[ref].properties & TSNEP_GCL_MASK;
	/* change to new list directly after inserted operation */
	properties |= TSNEP_GCL_CHANGE;

	/* last operation of list is reserved to insert operation */
	tsnep_write_gcl_operation(gcl, TSNEP_GCL_COUNT - 1, properties,
				  interval, false);

	return tsnep_set_gcl_change(gcl, ref, change, true);
}

static u64 tsnep_extend_gcl(struct tsnep_gcl *gcl, u64 start, u32 extension)
{
	int ref = gcl->count - 1;
	u32 interval = gcl->operation[ref].interval + extension;

	start -= gcl->operation[ref].interval;

	return tsnep_insert_gcl_operation(gcl, ref, start, interval);
}

static u64 tsnep_cut_gcl(struct tsnep_gcl *gcl, u64 start, u64 cycle_time)
{
	u64 sum = 0;
	int i;

	/* find operation which shall be cutted */
	for (i = 0; i < gcl->count; i++) {
		u64 sum_tmp = sum + gcl->operation[i].interval;
		u64 interval;

		/* sum up operations as long as cycle time is not exceeded */
		if (sum_tmp > cycle_time)
			break;

		/* remaining interval must be big enough for hardware */
		interval = cycle_time - sum_tmp;
		if (interval > 0 && interval < TSNEP_GCL_MIN_INTERVAL)
			break;

		sum = sum_tmp;
	}
	if (sum == cycle_time) {
		/* no need to cut operation itself or whole cycle
		 * => change exactly at operation
		 */
		return tsnep_set_gcl_change(gcl, i, start + sum, false);
	}
	return tsnep_insert_gcl_operation(gcl, i, start + sum,
					  cycle_time - sum);
}

static int tsnep_enable_gcl(struct tsnep_adapter *adapter,
			    struct tsnep_gcl *gcl, struct tsnep_gcl *curr)
{
	u64 system_time;
	u64 timeout;
	u64 limit;

	/* estimate timeout limit after timeout enable, actually timeout limit
	 * in hardware will be earlier than estimate so we are on the safe side
	 */
	tsnep_get_system_time(adapter, &system_time);
	timeout = system_time + TSNEP_GC_TIMEOUT;

	if (curr)
		limit = timeout + curr->change_limit;
	else
		limit = timeout;

	gcl->start_time = tsnep_gcl_start_after(gcl, limit);

	/* gate control time register is only 32bit => time shall be in the near
	 * future (no driver support for far future implemented)
	 */
	if ((gcl->start_time - system_time) >= U32_MAX)
		return -EAGAIN;

	if (curr) {
		/* change gate control list */
		u64 last;
		u64 change;

		last = tsnep_gcl_start_before(curr, gcl->start_time);
		if ((last + curr->cycle_time) == gcl->start_time)
			change = tsnep_cut_gcl(curr, last,
					       gcl->start_time - last);
		else if (((gcl->start_time - last) <=
			  curr->cycle_time_extension) ||
			 ((gcl->start_time - last) <= TSNEP_GCL_MIN_INTERVAL))
			change = tsnep_extend_gcl(curr, last,
						  gcl->start_time - last);
		else
			change = tsnep_cut_gcl(curr, last,
					       gcl->start_time - last);

		WARN_ON(change <= timeout);
		gcl->change = true;
		iowrite32(change & 0xFFFFFFFF, adapter->addr + TSNEP_GC_CHANGE);
	} else {
		/* start gate control list */
		WARN_ON(gcl->start_time <= timeout);
		gcl->change = false;
		iowrite32(gcl->start_time & 0xFFFFFFFF,
			  adapter->addr + TSNEP_GC_TIME);
	}

	return 0;
}

static int tsnep_taprio(struct tsnep_adapter *adapter,
			struct tc_taprio_qopt_offload *qopt)
{
	struct tsnep_gcl *gcl;
	struct tsnep_gcl *curr;
	int retval;

	if (!adapter->gate_control)
		return -EOPNOTSUPP;

	if (qopt->cmd == TAPRIO_CMD_DESTROY) {
		/* disable gate control if active */
		mutex_lock(&adapter->gate_control_lock);

		if (adapter->gate_control_active) {
			iowrite8(TSNEP_GC_DISABLE, adapter->addr + TSNEP_GC);
			adapter->gate_control_active = false;
		}

		mutex_unlock(&adapter->gate_control_lock);

		return 0;
	} else if (qopt->cmd != TAPRIO_CMD_REPLACE) {
		return -EOPNOTSUPP;
	}

	retval = tsnep_validate_gcl(qopt);
	if (retval)
		return retval;

	mutex_lock(&adapter->gate_control_lock);

	gcl = &adapter->gcl[adapter->next_gcl];
	tsnep_write_gcl(gcl, qopt);

	/* select current gate control list if active */
	if (adapter->gate_control_active) {
		if (adapter->next_gcl == 0)
			curr = &adapter->gcl[1];
		else
			curr = &adapter->gcl[0];
	} else {
		curr = NULL;
	}

	for (;;) {
		/* start timeout which discards late enable, this helps ensuring
		 * that start/change time are in the future at enable
		 */
		iowrite8(TSNEP_GC_ENABLE_TIMEOUT, adapter->addr + TSNEP_GC);

		retval = tsnep_enable_gcl(adapter, gcl, curr);
		if (retval) {
			mutex_unlock(&adapter->gate_control_lock);

			return retval;
		}

		/* enable gate control list */
		if (adapter->next_gcl == 0)
			iowrite8(TSNEP_GC_ENABLE_A, adapter->addr + TSNEP_GC);
		else
			iowrite8(TSNEP_GC_ENABLE_B, adapter->addr + TSNEP_GC);

		/* done if timeout did not happen */
		if (!(ioread32(adapter->addr + TSNEP_GC) &
		      TSNEP_GC_TIMEOUT_SIGNAL))
			break;

		/* timeout is acknowledged with any enable */
		iowrite8(TSNEP_GC_ENABLE_A, adapter->addr + TSNEP_GC);

		if (curr)
			tsnep_clean_gcl(curr);

		/* retry because of timeout */
	}

	adapter->gate_control_active = true;

	if (adapter->next_gcl == 0)
		adapter->next_gcl = 1;
	else
		adapter->next_gcl = 0;

	mutex_unlock(&adapter->gate_control_lock);

	return 0;
}

static int tsnep_tc_query_caps(struct tsnep_adapter *adapter,
			       struct tc_query_caps_base *base)
{
	switch (base->type) {
	case TC_SETUP_QDISC_TAPRIO: {
		struct tc_taprio_caps *caps = base->caps;

		if (!adapter->gate_control)
			return -EOPNOTSUPP;

		caps->gate_mask_per_txq = true;

		return 0;
	}
	default:
		return -EOPNOTSUPP;
	}
}

int tsnep_tc_setup(struct net_device *netdev, enum tc_setup_type type,
		   void *type_data)
{
	struct tsnep_adapter *adapter = netdev_priv(netdev);

	switch (type) {
	case TC_QUERY_CAPS:
		return tsnep_tc_query_caps(adapter, type_data);
	case TC_SETUP_QDISC_TAPRIO:
		return tsnep_taprio(adapter, type_data);
	default:
		return -EOPNOTSUPP;
	}
}

int tsnep_tc_init(struct tsnep_adapter *adapter)
{
	if (!adapter->gate_control)
		return 0;

	/* open all gates */
	iowrite8(TSNEP_GC_DISABLE, adapter->addr + TSNEP_GC);
	iowrite32(TSNEP_GC_OPEN | TSNEP_GC_NEXT_OPEN, adapter->addr + TSNEP_GC);

	adapter->gcl[0].addr = adapter->addr + TSNEP_GCL_A;
	adapter->gcl[1].addr = adapter->addr + TSNEP_GCL_B;

	return 0;
}

void tsnep_tc_cleanup(struct tsnep_adapter *adapter)
{
	if (!adapter->gate_control)
		return;

	if (adapter->gate_control_active) {
		iowrite8(TSNEP_GC_DISABLE, adapter->addr + TSNEP_GC);
		adapter->gate_control_active = false;
	}
}