#!/usr/bin/gjs

/*
 * This is a part of CPUFreq Manager
 * Copyright (C) 2016-2019 konkor <konkor.github.io>
 *
 * Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * You should have received a copy of the GNU General Public License along
 * with this program. If not, see <http://www.gnu.org/licenses/>.
 */

const Gio     = imports.gi.Gio;
const GLib    = imports.gi.GLib;
const GObject = imports.gi.GObject;
const Lang    = imports.lang;
const Signals = imports.signals;
const System  = imports.system;
const BArray  = imports.byteArray;

const APPDIR = get_appdir ();
imports.searchPath.unshift(APPDIR);
const CPUFreq       = imports.common.HelperCPUFreq;
const ArrayToString = CPUFreq.ArrayToString;

const MONITOR_KEY   = 'monitor';
const FREQ_SHOW_KEY = 'frequency-show';
const GOVS_SHOW_KEY = 'governors-show';
const LOAD_SHOW_KEY = 'load-show';


var DEBUGING = true;
let monitor_timeout = 1000;
let frequency_show = true;
let governor_show = false;
let load_show = false;

let style_state = -1;  //-1 - trigger change, 0 - no color/style, 1,2,3 - default color/style, 4,5,6 - custom color/style

let monitor = null;
let freedesktop = null;

let cpu_present = 1;
let cpu_online = 1;
let gcc = 0;

let event = 0;
let event_long = 0;

const OBJECT_PATH = '/org/konkor/cpufreq/service';
const CpufreqServiceIface = '<node> \
<interface name="org.konkor.cpufreq.service"> \
<property name="Frequency" type="t" access="read"/> \
<signal name="MonitorEvent"> \
  <arg name="metrics" type="s"/> \
</signal> \
<signal name="OSPowerEvent"> \
  <arg name="type" type="t"/> \
</signal> \
</interface> \
</node>';
const CpufreqServiceInfo  = Gio.DBusInterfaceInfo.new_for_xml (CpufreqServiceIface);

var CpufreqService = new Lang.Class ({
  Name: 'CpufreqService',
  Extends: Gio.Application,

  _init: function (args) {
    GLib.set_prgname ("cpufreq-service");
    this.parent ({
      application_id: "org.konkor.cpufreq.service",
      flags: Gio.ApplicationFlags.IS_SERVICE
    });
    GLib.set_application_name ("CPUFreq Service");

  },

  vfunc_startup: function() {
    this.parent();
    this.init ();
    this.hold ();
  },

  vfunc_activate: function() {
    this.connect("destroy", () => {
      this.remove_events ();
    });
  },

  init: function() {
    debug ("init");
    this.state = 0;
    this.dbus = Gio.DBusExportedObject.wrapJSObject (CpufreqServiceInfo, this);
    this.dbus.export (Gio.DBus.session, OBJECT_PATH);
    monitor = new CpufreqMonitor ();
    this.add_event ();
    event_long = GLib.timeout_add (100, 15000, this.longloop.bind (this));
  },

  on_settings: function (o, key) {
    //TODO: configuration
    if (key == MONITOR_KEY) {
      monitor_timeout =  o.get_int (MONITOR_KEY);
      this.add_event ();
    } else if (key == LOAD_SHOW_KEY) {
      load_show = o.get_boolean (LOAD_SHOW_KEY);
    } else if (key == GOVS_SHOW_KEY) {
      governor_show = o.get_boolean (GOVS_SHOW_KEY);
    } else if (key == FREQ_SHOW_KEY) {
      frequency_show = o.get_boolean (FREQ_SHOW_KEY);
    }

    style_state = -1;
  },

  add_event: function () {
    if (event != 0) {
      GLib.Source.remove (event);
      event = 0;
    }
    if (monitor_timeout > 0)
      event = GLib.timeout_add (100, monitor_timeout, () => {
        this.update ();
        return true;
      });
    else {
      this.dbus.emit_signal ("FrequencyChanged", new GLib.Variant("(s)", [""]));
      this.quit ();
    }
  },

  update: function () {
    if (monitor.update ()) {
      debug ("monitor fire...");
      let metrics = monitor.metrics;
      this.dbus.emit_signal ("MonitorEvent", new GLib.Variant("(s)", [JSON.stringify (metrics)]));
      if (this.state != metrics.state) {
        // Send notification about critical states
        if (metrics.state == 2) this.notify (monitor.warnmsg);
        this.state = metrics.state;
      }
    }
    gcc++;
    if (gcc > 7) {
        gcc = 0;
        System.gc ();
    }
  },

  longloop: function () {
    monitor.update_throttle ();
    this.update ();
  },

  notify: function (msg) {
    if (!msg) msg = "";
    if (!freedesktop) freedesktop = Gio.DBusProxy.new_for_bus_sync (
      Gio.BusType.SESSION,0,null,"org.freedesktop.Notifications",
      "/org/freedesktop/Notifications", "org.freedesktop.Notifications", null
    );
    let id = freedesktop.call_sync ("Notify", new GLib.Variant("(susssasa{sv}i)", [
      "OSPower",
      42,
      "dialog-information",
      "OSPower critical state",
      msg,
      ["more", "More info"],
      {},
      5000
    ]), 0,-1,null);
    freedesktop.connectSignal ('ActionInvoked', (o, s, v) => {
      print (v);
    });
  },

  action_callback: function (o, action) {
    debug (action);
    GLib.spawn_command_line_async (APPDIR + "/cpufreq-application");
  },

  remove_events: function () {
    if (this.dbus) this.dbus.unexport ();
    if (event != 0) GLib.Source.remove (event);
    event = 0;
  }
});

var CpufreqMonitor = new Lang.Class ({
  Name: 'CpufreqMonitor',

  _init: function () {
    // TODO: INIT CPUFREQ module
    CPUFreq.init ();
    // CPU INFO

    //frequencies
    this.frequencies = [];
    this.frequency_average = 0;
    this.frequency_minimum = 0;
    this.frequency_maximum = 0;

    //governors
    this.governors = [];
    this.governor = "";

    //loading
    this.loading = new FileStream ("/proc/loadavg");
    this.loadavg = 0;

    //throthle
    this.tt = 0;
    this.tt_old = 0;
    this.tt_time = 0;

    // WARNINGS 0 - normal, 1 - warning, 2 - critical
    this.state = 0;
    this.warnmsg = "";

    this.init ();
    this.update ();
  },

  init: function () {
    debug ("init_sources " + CPUFreq.cpucount);
    this.frequencies.forEach (f => {f.close ()});
    this.frequencies = new Array (CPUFreq.cpucount);
    for (let i = 0; i < CPUFreq.cpucount; i++) {
      this.frequencies[i] = new FileStream ("/sys/devices/system/cpu/cpu" + i + "/cpufreq/scaling_cur_freq");
    }
    this.governors.forEach (g => {g.cancel ()});
    this.governors = new Array (CPUFreq.cpucount);
    for (let i = 0; i < CPUFreq.cpucount; i++) {
      this.governors[i] = new FileMonitor ("/sys/devices/system/cpu/cpu" + i + "/cpufreq/scaling_governor");
    }
    this.governor = this.get_governor ();
  },

  get_governor: function () {
    let g = this.governors[0].content, online = GLib.get_num_processors ();
    for (let i = 0; i < online; i++) {
      if (this.governors[i].content && this.governors[i].content != g) g = "mixed";
    }
    return g;
  },

  update: function () {
    let fire = false, online = GLib.get_num_processors ();
    let max = 0, min = 0, avg = 0, avg_count = 0, g;

    for (let i = 0; i < online; i++) {
      let f = this.frequencies[i].update ();
      if (f) {
        //TODO: should be a string? let n = parseInt (f.split ("\n")[0].trim ());
        let n = parseInt (f);
        if (Number.isInteger (n)) {
          if (n > max) max = n;
          if ((min == 0) || (n < min)) min = n;
          avg += Math.round (n / 1000);
          avg_count++;
        }
      }
    }
    if (avg_count) avg = Math.round (avg / avg_count);
    if (this.frequency_average != avg) {
      this.frequency_average = avg;
      this.frequency_minimum = min;
      this.frequency_maximum = max;
      fire = true;
    }
    let l = this.loading.update ();
    if (l) {
      l = l.toString ().split ("\n")[0].split (" ")[0].trim ();
      this.loadavg = Math.round (parseFloat (l) * 100);
    }
    g = this.get_governor ();
    if (g != this.governor) {
      this.governor = g;
      fire = true;
    }
    if (cpu_online != online) {
      cpu_online = online;
      fire = true;
    }
    g = this.get_state ();
    if (this.state != g) {
      this.state = g;
      fire = true;
    }

    return fire;
  },

  update_throttle: function () {
    this.tt_old = this.tt;
    if (!CPUFreq.thermal_throttle) {
      CPUFreq.get_throttle_events ((events) => {
        if (events) this.tt = events;
      });
    } else {
      this.tt = CPUFreq.get_throttle ();
    }
  },

  get_state: function () {
    let s = 0, msg = "", online = GLib.get_num_processors ();
    this.warnmsg = "";
    if (this.loadavg > online * 100) {
      s = 2;
      this.warnmsg = "SYSTEM OVERLOAD";
    } else if (this.loadavg > online * 75) {
      s = 1;
      this.warnmsg = "SYSTEM BUSY";
    }
    if (this.tt) {
      msg = "CPU THROTTLED: " + this.tt;
      if (s == 0) s = 1;
      if (this.tt_old != this.tt) {
        msg += "\nTHROTTLE SPEED: " + Math.round ((this.tt - this.tt_old) / 15);
        s = 2;
      }
      if (this.warnmsg.length > 0) this.warnmsg += "\n" + msg;
    }
    return s;
  },

  get metrics () {
    let o = {
      frequency_average: this.frequency_average,
      frequency_minimum: this.frequency_minimum,
      frequency_maximum: this.frequency_maximum,
      governor         : this.governor,
      state            : this.state
    }
    return o;
  }
});

var FileStream = new Lang.Class ({
  Name: 'FileStream',
  Extends: GObject.GObject,
  Signals: {
    'changed': {
      flags: GObject.SignalFlags.RUN_LAST | GObject.SignalFlags.DETAILED,
      param_types: [GObject.TYPE_STRING]},
  },

  _init: function (path) {
    this.filename = path;
    this.file = Gio.File.new_for_path (this.filename);
    this.stream = null;
    this.content = null;
    this.open ();
    this.update ();
  },

  open: function () {
    this.close ();
    if (this.file.query_exists (null))
      this.stream = new Gio.DataInputStream ({ base_stream: this.file.read (null) });
  },

  close: function () {
    if (this.stream) this.stream.close (null);
    this.stream = null;
    this.content = null;
  },

  update: function () {
    this.read_line ();
    return this.content;
  },

  read_line: function () {
    if (this.stream == null) return;
    try {
      this.stream.seek (0, GLib.SeekType.SET, null);
      this.stream.read_line_async (100, null, this.read_done.bind (this));
    } catch (e) {
      this.open.bind (this);
    }
  },

  read_done: function (stream, res) {
    try {
      let [line,] = stream.read_line_finish (res);
      if (line) {
        this.content = ArrayToString (line);
      }
    } catch (e) {}
    this.emit ("changed", this.content);
  }
});

Signals.addSignalMethods (FileStream.prototype);

var FileMonitor = new Lang.Class ({
  Name: 'FileMonitor',

  _init: function (path) {
    this.filename = path;
    this.file = Gio.File.new_for_path (this.filename);
    this.init ();
  },

  init: function () {
    this.cancel ();
    this.monitor = this.file.monitor_file (0, null);
    if (this.monitor) {
      this.monitor.set_rate_limit (4000);
      this.load_contents ();
      this.monitor_id = this.monitor.connect ("changed", this.on_changed.bind (this));
    }
  },

  cancel: function () {
    if (this.monitor_id) {
      this.monitor.disconnect (this.monitor_id);
      this.monitor.cancel ();
      this.monitor_id = 0;
    }
  },

  on_changed: function (o, file, other_file, event_type) {
    if (event_type == Gio.FileMonitorEvent.CHANGED) this.load_contents ();
  },

  load_contents: function () {
    let [ok,contents,] = this.file.load_contents (null);
    if (ok)
      this.content = ArrayToString (contents).toString().split ("\n")[0].trim();
    else this.content = null;
    return ok, this.content;
  }
});

function getCurrentFile () {
  let stack = (new Error()).stack;
  let stackLine = stack.split('\n')[1];
  if (!stackLine)
    throw new Error ('Could not find current file');
  let match = new RegExp ('@(.+):\\d+').exec(stackLine);
  if (!match)
    throw new Error ('Could not find current file');
  let path = match[1];
  let file = Gio.File.new_for_path (path);
  return [file.get_path(), file.get_parent().get_path(), file.get_basename()];
}

function get_appdir () {
  let s = getCurrentFile ()[1];
  if (GLib.file_test (s + "/extension.js", GLib.FileTest.EXISTS)) return s;
  s = GLib.get_home_dir () + "/.local/share/gnome-shell/extensions/cpufreq@konkor";
  if (GLib.file_test (s + "/extension.js", GLib.FileTest.EXISTS)) return s;
  s = "/usr/share/gnome-shell/extensions/cpufreq@konkor";
  if (GLib.file_test (s + "/extension.js", GLib.FileTest.EXISTS)) return s;
  throw "Installation not found...";
  return s;
}

function debug (msg) {
  if (msg && DEBUGING) print ("[cpufreq][service] " + msg);
}

function error (msg) {
  print ("[cpufreq][service] (EE) " + msg);
}

try {
  let app = new CpufreqService (ARGV);
  app.run (ARGV);
} catch (e) {
  print (e.message);
}
