/*
 * Copyright 2018 Duan Hao
 * Copyright 2018 Con Kolivas <kernel@kolivas.org>
 *
 * 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 3 of the License, or (at your option)
 * any later version.  See COPYING for more details.
 */

/******************************************************************************
 * Description:	fan control using simple PID
 ******************************************************************************/

#include <stdio.h>
#include <stdint.h>
#include <unistd.h>

#include "dragonmint_t1.h"

#include "dm_temp_ctrl.h"
#include "dm_fan_ctrl.h"

/******************************************************************************
 * Macros & Constants
 ******************************************************************************/
#define FAN_MODE_DEF			FAN_MODE_AUTO		// default fan control mode

#define WORK_CYCLE_DEF			(2)			// default time interval between temperature checks
#define DEV_TMP_CHK_CNT			(3)
#define DEV_TMP_CHK_SPAN		(6)
#define TIMEOUT_GET_TMP			(3)

/******************************************************************************
 * Global variables
 ******************************************************************************/
volatile c_fan_cfg	g_fan_cfg;				// fan config
volatile int		g_fan_profile;			// fan profile: normal / overheat / preheat

static c_temp			g_dev_tmp;	// device temperature sequence
static c_temp			g_dev_last_tmp;	// device temperature sequence

extern int			chain_flag[MAX_CHAIN_NUM];

/******************************************************************************
 * Prototypes
 ******************************************************************************/
static bool dm_fanctrl_get_tmp(void);
static void dm_fanctrl_update_fan_speed(void);
static bool dm_fanctrl_check_overheat(void);
static bool dm_fanctrl_check_preheat(void);


/******************************************************************************
 * Implementations
 ******************************************************************************/
void dm_fanctrl_get_defcfg(c_fan_cfg *p_cfg)
{
	p_cfg->fan_mode = FAN_MODE_DEF;
	p_cfg->fan_speed = FAN_SPEED_DEF;
	p_cfg->fan_speed_preheat = FAN_SPEED_PREHEAT;
	p_cfg->fan_ctrl_cycle = WORK_CYCLE_DEF;
	p_cfg->preheat = true;
}

void dm_fanctrl_init(c_fan_cfg *p_cfg)
{
	if (NULL == p_cfg) {
		c_fan_cfg cfg;
		dm_fanctrl_get_defcfg(&cfg);  // avoid to pass volatile pointer directly
		g_fan_cfg = cfg;
	} else
		g_fan_cfg = *p_cfg;

	g_fan_profile = FAN_PF_NORMAL;
	g_dev_tmp.tmp_avg = g_dev_last_tmp.tmp_avg = g_tmp_cfg.tmp_target;
}

void *dm_fanctrl_thread(void __maybe_unused *argv)
{
	int timeout_get_tmp = 0;

	// set default fan speed
//	dm_fanctrl_set_fan_speed(g_fan_cfg.fan_speed);

	while(true) {
		if (dm_fanctrl_get_tmp()) {
			dm_fanctrl_update_fan_speed();
			timeout_get_tmp = 0;
		} else
			timeout_get_tmp++;

		// force fan speed to 100% when failed to get temperature
		if (timeout_get_tmp >= TIMEOUT_GET_TMP && g_fan_cfg.fan_speed < FAN_SPEED_MAX) {
			applog(LOG_WARNING,
				"WARNING: unable to read temperature, force fan speed to %d", FAN_SPEED_MAX);
			dm_fanctrl_set_fan_speed(FAN_SPEED_MAX);
			timeout_get_tmp = 0;
		}

		sleep(g_fan_cfg.fan_ctrl_cycle);
	}

	return NULL;
}

void dm_fanctrl_set_fan_speed(char speed)
{
	if (speed > FAN_SPEED_MAX)
		speed = FAN_SPEED_MAX;
	else if (speed < g_fan_cfg.fan_speed_preheat)
		speed = g_fan_cfg.fan_speed_preheat;

	if (speed != g_fan_cfg.fan_speed) {
		g_fan_cfg.fan_speed = speed;
		mcompat_fan_speed_set(0, g_fan_cfg.fan_speed);   // fan id is ignored
		applog(LOG_ERR, "fan speed set to %d", g_fan_cfg.fan_speed);
	}
}

static bool dm_fanctrl_get_tmp(void)
{
	bool retval = false;
	int  i, chain_num = 0;
	c_temp dev_temp;

	// init
	chain_num = 0;
	dev_temp.tmp_hi	 = g_tmp_cfg.tmp_min;
	dev_temp.tmp_lo  = g_tmp_cfg.tmp_max;
	dev_temp.tmp_avg = 0;

	for(i = 0; i < MAX_CHAIN_NUM; ++i) {
		if (chain_flag[i]
			&& g_chain_tmp[i].tmp_avg > g_tmp_cfg.tmp_min
			&& g_chain_tmp[i].tmp_avg < g_tmp_cfg.tmp_max) {
			// temperature stat.
			dev_temp.tmp_lo = MIN(dev_temp.tmp_lo, g_chain_tmp[i].tmp_lo);
			dev_temp.tmp_hi = MAX(dev_temp.tmp_hi, g_chain_tmp[i].tmp_hi);
			dev_temp.tmp_avg = MAX(dev_temp.tmp_avg, g_chain_tmp[i].tmp_avg);
			chain_num++;
		}
	}

	if (chain_num > 0) {
		g_dev_tmp = dev_temp;

		retval = true;
	}

	return retval;
}

static bool dm_fanctrl_check_overheat(void)
{
	int tmp_tolerance = 0;

	// if already in overheat mode, apply a small tolerance
	if (FAN_PF_OVERHEAT == g_fan_profile)
		tmp_tolerance = TEMP_TOLERANCE;

	// overheat mode: force to max fan speed while tmp_hi >= tmp_thr_hi
	if (g_dev_tmp.tmp_hi >= g_tmp_cfg.tmp_thr_hi - tmp_tolerance) {
		dm_fanctrl_set_fan_speed(FAN_SPEED_MAX);
		if (FAN_PF_OVERHEAT != g_fan_profile) {
			g_fan_profile = FAN_PF_OVERHEAT;
			applog(LOG_ERR, "OVERHEAT: temp_hi over %d, force fan speed to %d", 
				g_tmp_cfg.tmp_thr_hi, FAN_SPEED_MAX);
		}
		return true;
	}

	g_fan_profile = FAN_PF_NORMAL;

	return false;
}

static bool dm_fanctrl_check_preheat(void)
{
	int tmp_tolerance = 0;

	// preheat mode: do preheating when tmp_avg < tmp_thr_lo
	if (FAN_PF_PREHEAT != g_fan_profile)
		tmp_tolerance = TEMP_TOLERANCE;

	if (g_dev_tmp.tmp_avg < g_tmp_cfg.tmp_thr_lo - tmp_tolerance) {
		dm_fanctrl_set_fan_speed(FAN_SPEED_PREHEAT);
		g_fan_profile = FAN_PF_PREHEAT;
		applog(LOG_ERR, "PREHEAT: tmp_avg under %d, force fan speed to %d", 
			g_tmp_cfg.tmp_thr_lo, FAN_SPEED_PREHEAT);
		return true;
	}

	g_fan_profile = FAN_PF_NORMAL;

	return false;
}

static int8_t last_tmp_rise[8];
static int64_t *last_tmp_int = (int64_t *)last_tmp_rise;
static int tmp_rise_cnt;

static void dm_fanctrl_update_fan_speed(void)
{
	int fan_speed;
	int delta_tmp_avg, delta_tmp_hi;
	int tmp_rise, hi_raise;

	// detect overheat first
	if (dm_fanctrl_check_overheat())
		return;
	
	// preheat
	if (g_fan_cfg.preheat && dm_fanctrl_check_preheat())
		return;

	// check average temperature rising to determining fan speed target
	tmp_rise = g_dev_tmp.tmp_avg - g_dev_last_tmp.tmp_avg;
	delta_tmp_avg = g_dev_tmp.tmp_avg - g_tmp_cfg.tmp_target;
	hi_raise = g_dev_tmp.tmp_hi - g_dev_last_tmp.tmp_hi;
	delta_tmp_hi = g_dev_tmp.tmp_hi - g_tmp_cfg.tmp_thr_hi;

	/* If we have a hot spot, use that for fan speed control
	 * instead of the average temperature */
	if (hi_raise > tmp_rise || delta_tmp_hi > delta_tmp_avg) {
		tmp_rise = hi_raise;
		delta_tmp_avg = delta_tmp_hi;
	}

	g_dev_last_tmp.tmp_avg = g_dev_tmp.tmp_avg;
	g_dev_last_tmp.tmp_hi = g_dev_tmp.tmp_hi;
	g_dev_last_tmp.tmp_lo = g_dev_tmp.tmp_lo;

	if (delta_tmp_avg > 0) {
		/* Over target temperature */

		/* Is the temp already coming down */
		if (tmp_rise < 0)
			goto out;
		/* Adjust fanspeed by temperature over and any further rise */
		fan_speed = g_fan_cfg.fan_speed + delta_tmp_avg + tmp_rise;
	} else {
		/* Below target temperature */
		int diff = tmp_rise;

		if (tmp_rise > 0) {
			int divisor = -delta_tmp_avg / TEMP_TOLERANCE + 1;

			/* Adjust fanspeed by temperature change proportional to
			 * diff from optimal. */
			diff /= divisor;
		} else if (!tmp_rise) {
			/* Is the temp below optimal and unchanging, gently
			 * lower speed. Allow tighter temperature tolerance if
			 * temperature is unchanged for longer. */
			if ((g_dev_tmp.tmp_avg < g_tmp_cfg.tmp_target - TEMP_TOLERANCE) ||
			    (!(*last_tmp_int) && (g_dev_tmp.tmp_avg < g_tmp_cfg.tmp_target))) {
				*last_tmp_int = 0xFFFFFFFFFFFFFFFF;
				diff -= 1;
			}
		}
		fan_speed = g_fan_cfg.fan_speed + diff;
	}

	// set fan speed
	dm_fanctrl_set_fan_speed(fan_speed);
out:
	last_tmp_rise[(tmp_rise_cnt++) % 8] = tmp_rise;
}