/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
 *
 * Copyright (C) 2015-2017 Richard Hughes <richard@hughsie.com>
 * Copyright (C) 2016 Mario Limonciello <mario.limonciello@dell.com>
 * Copyright (C) 2017 Peichen Huang <peichenhuang@tw.synaptics.com>
 *
 * Licensed under the GNU Lesser General Public License Version 2.1
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA
 */

#include "config.h"

#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <stdlib.h>
#include <unistd.h>
#include <glib-object.h>
#include "synapticsmst-common.h"
#include "synapticsmst-device.h"

#define UNIT_SIZE		32
#define MAX_WAIT_TIME 		3  /* unit : second */

struct _SynapticsMSTConnection {
	gint		 fd;		/* not owned by the connection */
	guint8		 layer;
	guint8		 remain_layer;
	guint8		 rad;
};

guint8
synapticsmst_common_aux_node_read (SynapticsMSTConnection *connection,
				   gint offset, gint *buf, gint length)
{
	if (lseek (connection->fd, offset, SEEK_SET) != offset)
		return DPCD_SEEK_FAIL;

	if (read (connection->fd, buf, length) != length)
		return DPCD_ACCESS_FAIL;

	return DPCD_SUCCESS;
}

static guint8
synapticsmst_common_aux_node_write (SynapticsMSTConnection *connection,
				    gint offset, const gint *buf, gint length)
{
	if (lseek (connection->fd, offset, SEEK_SET) != offset)
		return DPCD_SEEK_FAIL;

	if (write (connection->fd, buf, length) != length)
		return DPCD_ACCESS_FAIL;

	return DPCD_SUCCESS;
}

void
synapticsmst_common_free (SynapticsMSTConnection *connection)
{
	g_free (connection);
}

SynapticsMSTConnection *
synapticsmst_common_new (gint fd, guint8 layer, guint rad)
{
	SynapticsMSTConnection *connection = g_new0 (SynapticsMSTConnection, 1);
	connection->fd = fd;
	connection->layer = layer;
	connection->remain_layer = layer;
	connection->rad = rad;
	return connection;
}

guint8
synapticsmst_common_read_dpcd (SynapticsMSTConnection *connection,
			       gint offset, gint *buf, gint length)
{
	if (connection->layer && connection->remain_layer) {
		guint8 rc, node;

		connection->remain_layer--;
		node = (connection->rad >> connection->remain_layer * 2) & 0x03;
		rc =  synapticsmst_common_rc_get_command (connection,
							  UPDC_READ_FROM_TX_DPCD + node,
							  length, offset, (guint8 *)buf);
		connection->remain_layer++;
		return rc;
	}
	return synapticsmst_common_aux_node_read (connection, offset, buf, length);
}

guint8
synapticsmst_common_write_dpcd (SynapticsMSTConnection *connection,
				gint offset,
				const gint *buf,
				gint length)
{
	if (connection->layer && connection->remain_layer) {
		guint8 rc, node;

		connection->remain_layer--;
		node = (connection->rad >> connection->remain_layer * 2) & 0x03;
		rc =  synapticsmst_common_rc_set_command (connection,
							  UPDC_WRITE_TO_TX_DPCD + node,
							  length, offset, (guint8 *)buf);
		connection->remain_layer++;
		return rc;
	}
	return synapticsmst_common_aux_node_write (connection, offset, buf, length);
}

guint8
synapticsmst_common_rc_set_command (SynapticsMSTConnection *connection,
				    gint rc_cmd,
				    gint length,
				    gint offset,
				    const guint8 *buf)
{
	guint8 rc = 0;
	gint cur_offset = offset;
	gint cur_length;
	gint data_left = length;
	gint cmd;
	gint readData = 0;
	long deadline;
	struct timespec t_spec;

	do{
		if (data_left > UNIT_SIZE) {
			cur_length = UNIT_SIZE;
		} else {
			cur_length = data_left;
		}

		if (cur_length) {
			/* write data */
			rc = synapticsmst_common_write_dpcd (connection, REG_RC_DATA, (gint *)buf, cur_length);
			if (rc)
				break;

			/* write offset */
			rc = synapticsmst_common_write_dpcd (connection,
							     REG_RC_OFFSET,
							     &cur_offset, 4);
			if (rc)
				break;

			/* write length */
			rc = synapticsmst_common_write_dpcd (connection,
							     REG_RC_LEN,
							     &cur_length, 4);
			if (rc)
				break;
		}

		/* send command */
		cmd = 0x80 | rc_cmd;
		rc = synapticsmst_common_write_dpcd (connection,
						     REG_RC_CMD,
						     &cmd, 1);
		if (rc)
			break;

		/* wait command complete */
		clock_gettime (CLOCK_REALTIME, &t_spec);
		deadline = t_spec.tv_sec + MAX_WAIT_TIME;

		do {
			rc = synapticsmst_common_read_dpcd (connection,
							    REG_RC_CMD,
							    &readData, 2);
			clock_gettime (CLOCK_REALTIME, &t_spec);
			if (t_spec.tv_sec > deadline) {
				rc = -1;
			}
		} while (rc == 0 && readData & 0x80);

		if (rc)
			break;
		else if (readData & 0xFF00) {
			rc = (readData >> 8) & 0xFF;
			break;
		}

		buf += cur_length;
		cur_offset += cur_length;
		data_left -= cur_length;
	} while (data_left);

	return rc;
}

guint8
synapticsmst_common_rc_get_command (SynapticsMSTConnection *connection,
				    gint rc_cmd,
				    gint length,
				    gint offset,
				    guint8 *buf)
{
	guint8 rc = 0;
	gint cur_offset = offset;
	gint cur_length;
	gint data_need = length;
	gint cmd;
	gint readData = 0;
	long deadline;
	struct timespec t_spec;

	while (data_need) {
		if (data_need > UNIT_SIZE) {
			cur_length = UNIT_SIZE;
		} else {
			cur_length = data_need;
		}

		if (cur_length) {
			/* write offset */
			rc = synapticsmst_common_write_dpcd (connection,
							     REG_RC_OFFSET,
							     &cur_offset, 4);
			if (rc)
				break;

			/* write length */
			rc = synapticsmst_common_write_dpcd (connection,
							     REG_RC_LEN,
							     &cur_length, 4);
			if (rc)
				break;
		}

		/* send command */
		cmd = 0x80 | rc_cmd;
		rc = synapticsmst_common_write_dpcd (connection,
						     REG_RC_CMD,
						     &cmd, 1);
		if (rc)
			break;

		/* wait command complete */
		clock_gettime (CLOCK_REALTIME, &t_spec);
		deadline = t_spec.tv_sec + MAX_WAIT_TIME;

		do {
			rc = synapticsmst_common_read_dpcd (connection,
							    REG_RC_CMD,
							    &readData, 2);
			clock_gettime (CLOCK_REALTIME, &t_spec);
			if (t_spec.tv_sec > deadline) {
				rc = -1;
			}
		} while (rc == 0 && readData & 0x80);

		if (rc)
			break;
		else if (readData & 0xFF00) {
			rc = (readData >> 8) & 0xFF;
			break;
		}

		if (cur_length) {
			rc = synapticsmst_common_read_dpcd (connection,
							    REG_RC_DATA,
							    (gint *)buf,
							    cur_length);
			if (rc)
				break;
		}

		buf += cur_length;
		cur_offset += cur_length;
		data_need -= cur_length;
	}

	return rc;
}

guint8
synapticsmst_common_rc_special_get_command (SynapticsMSTConnection *connection,
					    gint rc_cmd,
					    gint cmd_length,
					    gint cmd_offset,
					    guint8 *cmd_data,
					    gint length,
					    guint8 *buf)
{
	guint8 rc = 0;
	gint readData = 0;
	gint cmd;
	long deadline;
	struct timespec t_spec;

	if (cmd_length) {
		/* write cmd data */
		if (cmd_data != NULL) {
			rc = synapticsmst_common_write_dpcd (connection,
							     REG_RC_DATA,
							     (gint *)cmd_data,
							     cmd_length);
			if (rc)
				return rc;
		}

		/* write offset */
		rc = synapticsmst_common_write_dpcd (connection,
						     REG_RC_OFFSET,
						     &cmd_offset, 4);
		if (rc)
			return rc;

		/* write length */
		rc = synapticsmst_common_write_dpcd (connection,
						     REG_RC_LEN,
						     &cmd_length, 4);
		if (rc)
			return rc;
	}

	/* send command */
	cmd = 0x80 | rc_cmd;
	rc = synapticsmst_common_write_dpcd (connection, REG_RC_CMD, &cmd, 1);
	if (rc)
		return rc;

	/* wait command complete */
	clock_gettime (CLOCK_REALTIME, &t_spec);
	deadline = t_spec.tv_sec + MAX_WAIT_TIME;
	do {
		rc = synapticsmst_common_read_dpcd (connection,
						    REG_RC_CMD,
						    &readData, 2);
		clock_gettime (CLOCK_REALTIME, &t_spec);
		if (t_spec.tv_sec > deadline)
			return -1;
	} while (readData & 0x80);

	if (rc)
		return rc;
	else if (readData & 0xFF00) {
		rc = (readData >> 8) & 0xFF;
		return rc;
	}

	if (length) {
		rc = synapticsmst_common_read_dpcd (connection,
						    REG_RC_DATA,
						    (gint *)buf, length);
		if (rc)
			return rc;
	}

	return rc;
}

guint8
synapticsmst_common_enable_remote_control (SynapticsMSTConnection *connection)
{
	const gchar *sc = "PRIUS";
	guint8 rc = 0;

	for (gint i = 0; i <= connection->layer; i++) {
		g_autoptr(SynapticsMSTConnection) connection_tmp = NULL;
		connection_tmp = synapticsmst_common_new (connection->fd, i, connection->rad);
		rc = synapticsmst_common_rc_set_command (connection_tmp,
							 UPDC_ENABLE_RC,
							 5, 0, (guint8*)sc);
		if (rc)
			break;
	}
	return rc;
}

guint8
synapticsmst_common_disable_remote_control (SynapticsMSTConnection *connection)
{
	guint8 rc = 0;

	for (gint i = connection->layer; i >= 0; i--) {
		g_autoptr(SynapticsMSTConnection) connection_tmp = NULL;
		connection_tmp = synapticsmst_common_new (connection->fd, i, connection->rad);
		rc = synapticsmst_common_rc_set_command (connection_tmp,
							 UPDC_DISABLE_RC,
							 0, 0, NULL);
		if (rc)
			break;
	}
	return rc;
}
