/*
 * Copyright (c) 2022 Trackunit Corporation
 *
 * SPDX-License-Identifier: Apache-2.0
 */

/*
 * This library uses CMUX to create multiple data channels, called DLCIs, on a single serial bus.
 * Each DLCI has an address from 1 to 63. DLCI address 0 is reserved for control commands.
 *
 * Design overview:
 *
 *     DLCI1 <-----------+                              +-------> DLCI1
 *                       v                              v
 *     DLCI2 <---> CMUX instance <--> Serial bus <--> Client <--> DLCI2
 *                       ^                              ^
 *     DLCI3 <-----------+                              +-------> DLCI3
 *
 * Writing to and from the CMUX instances is done using the modem_pipe API.
 */

#include <zephyr/kernel.h>
#include <zephyr/types.h>
#include <zephyr/sys/ring_buffer.h>
#include <zephyr/sys/atomic.h>

#include <zephyr/modem/pipe.h>
#include <zephyr/modem/stats.h>

#ifndef ZEPHYR_MODEM_CMUX_
#define ZEPHYR_MODEM_CMUX_

#ifdef __cplusplus
extern "C" {
#endif

/**
 * @brief Modem CMUX
 * @defgroup modem_cmux Modem CMUX
 * @ingroup modem
 * @{
 */

struct modem_cmux;

enum modem_cmux_event {
	MODEM_CMUX_EVENT_CONNECTED = 0,
	MODEM_CMUX_EVENT_DISCONNECTED,
};

typedef void (*modem_cmux_callback)(struct modem_cmux *cmux, enum modem_cmux_event event,
				    void *user_data);

/**
 * @cond INTERNAL_HIDDEN
 */

enum modem_cmux_state {
	MODEM_CMUX_STATE_DISCONNECTED = 0,
	MODEM_CMUX_STATE_CONNECTING,
	MODEM_CMUX_STATE_CONNECTED,
	MODEM_CMUX_STATE_DISCONNECTING,
};

enum modem_cmux_receive_state {
	MODEM_CMUX_RECEIVE_STATE_SOF = 0,
	MODEM_CMUX_RECEIVE_STATE_RESYNC,
	MODEM_CMUX_RECEIVE_STATE_ADDRESS,
	MODEM_CMUX_RECEIVE_STATE_ADDRESS_CONT,
	MODEM_CMUX_RECEIVE_STATE_CONTROL,
	MODEM_CMUX_RECEIVE_STATE_LENGTH,
	MODEM_CMUX_RECEIVE_STATE_LENGTH_CONT,
	MODEM_CMUX_RECEIVE_STATE_DATA,
	MODEM_CMUX_RECEIVE_STATE_FCS,
	MODEM_CMUX_RECEIVE_STATE_DROP,
	MODEM_CMUX_RECEIVE_STATE_EOF,
};

enum modem_cmux_dlci_state {
	MODEM_CMUX_DLCI_STATE_CLOSED,
	MODEM_CMUX_DLCI_STATE_OPENING,
	MODEM_CMUX_DLCI_STATE_OPEN,
	MODEM_CMUX_DLCI_STATE_CLOSING,
};

struct modem_cmux_dlci {
	sys_snode_t node;

	/* Pipe */
	struct modem_pipe pipe;

	/* Context */
	uint16_t dlci_address;
	struct modem_cmux *cmux;

	/* Receive buffer */
	struct ring_buf receive_rb;
	struct k_mutex receive_rb_lock;

	/* Work */
	struct k_work_delayable open_work;
	struct k_work_delayable close_work;

	/* State */
	enum modem_cmux_dlci_state state;

	/* Statistics */
#if CONFIG_MODEM_STATS
	struct modem_stats_buffer receive_buf_stats;
#endif
};

struct modem_cmux_frame {
	uint8_t dlci_address;
	bool cr;
	bool pf;
	uint8_t type;
	const uint8_t *data;
	uint16_t data_len;
};

struct modem_cmux_work {
	struct k_work_delayable dwork;
	struct modem_cmux *cmux;
};

struct modem_cmux {
	/* Bus pipe */
	struct modem_pipe *pipe;

	/* Event handler */
	modem_cmux_callback callback;
	void *user_data;

	/* DLCI channel contexts */
	sys_slist_t dlcis;

	/* State */
	enum modem_cmux_state state;
	bool flow_control_on;

	/* Work lock */
	bool attached;
	struct k_spinlock work_lock;

	/* Receive state*/
	enum modem_cmux_receive_state receive_state;

	/* Receive buffer */
	uint8_t *receive_buf;
	uint16_t receive_buf_size;
	uint16_t receive_buf_len;

	uint8_t work_buf[CONFIG_MODEM_CMUX_WORK_BUFFER_SIZE];

	/* Transmit buffer */
	struct ring_buf transmit_rb;
	struct k_mutex transmit_rb_lock;

	/* Received frame */
	struct modem_cmux_frame frame;
	uint8_t frame_header[5];
	uint16_t frame_header_len;

	/* Work */
	struct k_work_delayable receive_work;
	struct k_work_delayable transmit_work;
	struct k_work_delayable connect_work;
	struct k_work_delayable disconnect_work;

	/* Synchronize actions */
	struct k_event event;

	/* Statistics */
#if CONFIG_MODEM_STATS
	struct modem_stats_buffer receive_buf_stats;
	struct modem_stats_buffer transmit_buf_stats;
#endif
};

/**
 * @endcond
 */

/**
 * @brief Contains CMUX instance configuration data
 */
struct modem_cmux_config {
	/** Invoked when event occurs */
	modem_cmux_callback callback;
	/** Free to use pointer passed to event handler when invoked */
	void *user_data;
	/** Receive buffer */
	uint8_t *receive_buf;
	/** Size of receive buffer in bytes [127, ...] */
	uint16_t receive_buf_size;
	/** Transmit buffer */
	uint8_t *transmit_buf;
	/** Size of transmit buffer in bytes [149, ...] */
	uint16_t transmit_buf_size;
};

/**
 * @brief Initialize CMUX instance
 * @param cmux CMUX instance
 * @param config Configuration to apply to CMUX instance
 */
void modem_cmux_init(struct modem_cmux *cmux, const struct modem_cmux_config *config);

/**
 * @brief CMUX DLCI configuration
 */
struct modem_cmux_dlci_config {
	/** DLCI channel address */
	uint8_t dlci_address;
	/** Receive buffer used by pipe */
	uint8_t *receive_buf;
	/** Size of receive buffer used by pipe [127, ...] */
	uint16_t receive_buf_size;
};

/**
 * @brief Initialize DLCI instance and register it with CMUX instance
 *
 * @param cmux CMUX instance which the DLCI will be registered to
 * @param dlci DLCI instance which will be registered and configured
 * @param config Configuration to apply to DLCI instance
 */
struct modem_pipe *modem_cmux_dlci_init(struct modem_cmux *cmux, struct modem_cmux_dlci *dlci,
					const struct modem_cmux_dlci_config *config);

/**
 * @brief Attach CMUX instance to pipe
 *
 * @param cmux CMUX instance
 * @param pipe Pipe instance to attach CMUX instance to
 */
int modem_cmux_attach(struct modem_cmux *cmux, struct modem_pipe *pipe);

/**
 * @brief Connect CMUX instance
 *
 * @details This will send a CMUX connect request to target on the serial bus. If successful,
 * DLCI channels can be now be opened using modem_pipe_open()
 *
 * @param cmux CMUX instance
 *
 * @note When connected, the bus pipe must not be used directly
 */
int modem_cmux_connect(struct modem_cmux *cmux);

/**
 * @brief Connect CMUX instance asynchronously
 *
 * @details This will send a CMUX connect request to target on the serial bus. If successful,
 * DLCI channels can be now be opened using modem_pipe_open().
 *
 * @param cmux CMUX instance
 *
 * @note When connected, the bus pipe must not be used directly
 */
int modem_cmux_connect_async(struct modem_cmux *cmux);

/**
 * @brief Close down and disconnect CMUX instance
 *
 * @details This will close all open DLCI channels, and close down the CMUX connection.
 *
 * @param cmux CMUX instance
 *
 * @note The bus pipe must be released using modem_cmux_release() after disconnecting
 * before being reused.
 */
int modem_cmux_disconnect(struct modem_cmux *cmux);

/**
 * @brief Close down and disconnect CMUX instance asynchronously
 *
 * @details This will close all open DLCI channels, and close down the CMUX connection.
 *
 * @param cmux CMUX instance
 *
 * @note The bus pipe must be released using modem_cmux_release() after disconnecting
 * before being reused.
 */
int modem_cmux_disconnect_async(struct modem_cmux *cmux);

/**
 * @brief Release CMUX instance from pipe
 *
 * @details Releases the pipe and hard resets the CMUX instance internally. CMUX should
 * be disconnected using modem_cmux_disconnect().
 *
 * @param cmux CMUX instance
 *
 * @note The bus pipe can be used directly again after CMUX instance is released.
 */
void modem_cmux_release(struct modem_cmux *cmux);

/**
 * @}
 */

#ifdef __cplusplus
}
#endif

#endif /* ZEPHYR_MODEM_CMUX_ */
