//
// Fan Manager for VisoinFive2
//
// Copyright 2026, Jakob Østergaard Hegelund <joe@unthought.net>
// -----------------------------------------------------------------------
// This file is part of FanMan.
//
// FanMan 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.
//
// FanMan 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 FanMan. If not, see <https://www.gnu.org/licenses/>.
// -----------------------------------------------------------------------
//
// Theory of operation:
//
//  Monitors sys/class/thermal/thermal_zone0/temp for current CPU
//  temperature. Polls every second. Temperature is in m°C
//
//  Uses /sys/class/pwm/pwmchip0/ to manage a designated PWM channel
//  adjusting it from 0% to 100% duty cycle depending on cooling need.
//
//  PID control logic regulates fan speed to maintain CPU temperature
//  as close to ideal set temperature as possible
//

#include <exception>
#include <iostream>
#include <sstream>
#include <fstream>
#include <chrono>
#include <thread>
#include <systemd/sd-daemon.h>
#include <signal.h>
#include "cfgparser.hh"
#include "pid.hh"
#include "pwm.hh"
#include "sigh.hh"

int main(int argc, char **argv) try
{
  CfgParameter<std::string> temp_sensor("temp_sensor", "/sys/class/thermal/thermal_zone0/temp");
  CfgParameter<size_t> pwm_chip("pwm_chip", 0);
  CfgParameter<size_t> pwm_channel("pwm_channel", 0);
  CfgParameter<size_t> pwm_period("pwm_period", 5000); // 200kHz
  CfgParameter<int32_t> ideal_temp("ideal_temp", 48000); // mC°
  CfgParameter<double> fan_min_dc("fan_min_dc", .4);
  CfgParameter<double> fan_start_dc("fan_start_dc", .6);
  CfgParameter<double> pid_kp("pid_kp", .00080);
  CfgParameter<double> pid_ki("pid_ki", .00004);
  CfgParameter<double> pid_kd("pid_kd", .00030);
  CfgParameter<std::string> gnuplot_out("gnuplot_out", std::string());

  if (argc != 2)
    throw std::runtime_error("Expected configuration file argument");

  CfgParser cp(argv[1],
               {&temp_sensor,
                &pwm_chip,
                &pwm_channel,
                &pwm_period,
                &ideal_temp,
                &fan_min_dc,
                &fan_start_dc,
                &pid_kp,
                &pid_ki,
                &pid_kd,
                &gnuplot_out});
  cp.reload();

  // Validation
  if (fan_start_dc < fan_min_dc)
    throw std::runtime_error("Fan startup DC cannot be lower than minimum DC");
  if (fan_start_dc > 1)
    throw std::runtime_error("Fan startup DC cannot be above 1");

  // Install signal handlers
  std::condition_variable signalled;
  SigHandler sig_exit(SIGTERM,signalled);
  SigHandler sig_reload(SIGHUP,signalled);

  // Open temperature input
  std::ifstream tempfile(temp_sensor);

  // To be able to differentiate and integrate the process error, we
  // need a history
  ProcessHistory<int32_t> errorhist(10, 10*15); // 10 samps/sec, 15 secs

  // Start up PWM at 0% DC
  PWMChannel pwm(pwm_chip, pwm_channel, pwm_period);
  pwm.enable(true);
  double old_dc = 0;

  // If we're told to generate gnuplot output, do so
  std::ofstream gnuplot_file;
  auto tstart = std::chrono::steady_clock().now();

  // Let systemd know we're ready
  sd_notify(0, "READY=1");
  std::cerr << "Fan manager regulation active" << std::endl;

  while (!sig_exit.triggered()) {
    // Start of loop
    auto t_loop_start = std::chrono::steady_clock().now();

    // Deal with config file reloads
    if (sig_reload.triggered()) {
      // Notify systemd we heard the message
      { std::ostringstream str;
        str << "RELOADING=1" << std::endl
            << "MONOTONIC_USEC=" << std::chrono::duration_cast<std::chrono::microseconds>
          (std::chrono::steady_clock().now().time_since_epoch()).count();
        sd_notify(0, str.str().c_str());
      }
      // Reload configuration
      sig_reload.reset();
      cp.reload();
      std::cerr << "Configuration file reloaded" << std::endl;
      // We do not support changing PWM channel or temp sensor during
      // operation. However, PID parameters, DC parameters etc. will
      // take effect right away.
      if (gnuplot_file.is_open() == std::string(gnuplot_out).empty()) {
        // We need to either open or close GNUPlot file
        if (gnuplot_file.is_open()) {
          gnuplot_file.close();
        } else {
          gnuplot_file.open(gnuplot_out);
          if (!gnuplot_file.is_open())
            std::cerr << "Failed opening " << std::string(gnuplot_out) << std::endl;
          gnuplot_file << "# t Temperature e(t) Ie(t) De(t) DC" << std::endl;
        }
      }
      // We're a Type=notify-reload, so tell systemd we're done
      // reloading
      sd_notify(0, "READY=1");
    }

    //
    // PID control main loop; get temperature and adjust output
    //
    int32_t temperature;
    tempfile >> temperature;
    if (tempfile.rdstate() != std::ios_base::goodbit)
      throw std::runtime_error("Unable to read temperature");
    tempfile.seekg(0);

    // Note; usually one would calculate error as setpoint minus
    // process output; but since we want to *add* cooling in order to
    // *lower* temperature, we will reverse the error calculation
    errorhist.add(temperature - ideal_temp);

    double new_dc = pid_kp * errorhist.get()
      + pid_ki * errorhist.getint()
      + pid_kd * errorhist.getdiffSG9();
    if (new_dc < 0) new_dc = 0;
    if (new_dc > 1) new_dc = 1;

    //
    // Slew rate control; dampen output from PID to mitigate
    // over-compensation
    //
    // Be aware that more dampening will cause us to never switch off
    // the fan because of the fan_min_dc rounding logic below. If we
    // want further dampening, the rounding below should change to
    // truncation
    //
    new_dc = .6 * new_dc + .4 * old_dc;

    //
    // Adjustments for actually driving a real fan:
    //
    // 1. If the dc is less than fan_min_dc then we round the dc to
    //    either 0 or fan_min_dc. A fan is not going to operate at 1%
    //    speed if given 1% of the supply voltage, it's going to be
    //    stuck (turning the 1% into heat instead)
    //
    if (new_dc < fan_min_dc) {
      new_dc = new_dc < fan_min_dc/2
        ? 0.
        : fan_min_dc;
    }
    //
    // 2. If previous dc was 0, set new one to the fan_start_dc so
    //    that the fan gets a "push" to start (worn bearings need a
    //    little extra to get moving for example).  If the new dc is
    //    calculated to more than fan_start_dc (meaning we jump from 0
    //    to e.g. 100% abruptly) we still just set it to fan_start_dc
    //    as that will help prevent overshoot on the PWM output driver
    //
    if (old_dc == 0 && new_dc > 0)
      new_dc = fan_start_dc;

    //
    // If we are logging our output, do so
    //
    if (gnuplot_file.is_open()) {
      gnuplot_file << std::chrono::duration_cast<std::chrono::milliseconds>
        (std::chrono::steady_clock().now() - tstart)
        .count() / 1E3 << " "
                   << temperature << " "
                   << pid_kp * errorhist.get() << " "
                   << pid_ki * errorhist.getint() << " "
                   << pid_kd * errorhist.getdiffSG9() << " "
                   << new_dc << std::endl;
    }

    // If there is a change, update output
    if (old_dc != new_dc) {
      pwm.set_dc(new_dc);
      old_dc = new_dc;
    }

    // Let nature work...
    { std::mutex dummy;
      std::unique_lock dummyl(dummy);

      using namespace std::chrono_literals;
      signalled.wait_until(dummyl,
                           t_loop_start + 100ms,
                           [&]{ return sig_exit.triggered()
                               || sig_reload.triggered(); });
    }
  }
  std::cerr << "Normal exit - set fan DC to max" << std::endl;
  pwm.set_dc(1);

} catch(std::exception const &e) {
  std::ostringstream str;
  str << "STATUS=Fatal error: " << e.what() << std::endl;
  sd_notify(0, str.str().c_str());
  std::cerr << "FATAL: " << e.what() << std::endl;
  return 1;
}



void sighandler(int sig)
{

}

