const Gtk = imports.gi.Gtk;
const Gio = imports.gi.Gio;
const Gdk = imports.gi.Gdk;
const GLib = imports.gi.GLib;
const ByteArray = imports.byteArray;
const Config = imports.misc.config;

const Gettext = imports.gettext.domain('system-monitor');

let extension = imports.misc.extensionUtils.getCurrentExtension();
let convenience = extension.imports.convenience;

const _ = Gettext.gettext;
const N_ = function (e) {
    return e;
};

let Schema;

const shellMajorVersion = parseInt(Config.PACKAGE_VERSION.split('.')[0]);

function init() {
    convenience.initTranslations();
    Schema = convenience.getSettings();
}

String.prototype.capitalize = function () {
    return this.replace(/(^|\s)([a-z])/g, function (m, p1, p2) {
        return p1 + p2.toUpperCase();
    });
};

function color_to_hex(color) {
    var output = N_('#%02x%02x%02x%02x').format(
        255 * color.red,
        255 * color.green,
        255 * color.blue,
        255 * color.alpha);
    return output;
}

function parse_bytearray(bytearray) {
    if (!ByteArray.toString(bytearray).match(/GjsModule byteArray/)) {
        return ByteArray.toString(bytearray);
    }
    return bytearray
}

function check_sensors(sensor_type) {
    const hwmon_path = '/sys/class/hwmon/';
    const hwmon_dir = Gio.file_new_for_path(hwmon_path);

    const sensor_files = [];
    const sensor_labels = [];

    function get_label_from(file) {
        if (file.query_exists(null)) {
            // load_contents (and even cat) fails with "Invalid argument" for some label files
            try {
                let [success, contents] = file.load_contents(null);
                if (success) {
                    return String(parse_bytearray(contents)).split('\n')[0];
                }
            } catch (e) {
                log('[System monitor] error loading label from file ' + file.get_path() + ': ' + e);
            }
        }
        return null;
    }

    function add_sensors_from(chip_dir, chip_label) {
        const chip_children = chip_dir.enumerate_children(
            'standard::name,standard::type', Gio.FileQueryInfoFlags.NONE, null);
        if (!chip_children) {
            log('[System monitor] error enumerating children of chip ' + chip_dir.get_path());
            return false;
        }

        const input_entry_regex = new RegExp('^' + sensor_type + '(\\d+)_input$');
        let info;
        let added = false;
        while ((info = chip_children.next_file(null))) {
            if (info.get_file_type() !== Gio.FileType.REGULAR) {
                continue;
            }
            const matches = info.get_name().match(input_entry_regex);
            if (!matches) {
                continue;
            }
            const input_ordinal = matches[1];
            const input = chip_children.get_child(info);
            const input_label = get_label_from(chip_dir.get_child(sensor_type + input_ordinal + '_label'));

            sensor_files.push(input.get_path());
            sensor_labels.push(chip_label + ' - ' + (input_label || input_ordinal));
            added = true;
        }
        return added;
    }

    const hwmon_children = hwmon_dir.enumerate_children(
        'standard::name,standard::type', Gio.FileQueryInfoFlags.NONE, null);
    if (!hwmon_children) {
        log('[System monitor] error enumerating hwmon children');
        return [[], []];
    }

    let info;
    while ((info = hwmon_children.next_file(null))) {
        if (info.get_file_type() !== Gio.FileType.DIRECTORY || !info.get_name().match(/^hwmon\d+$/)) {
            continue;
        }
        const chip = hwmon_children.get_child(info);
        const chip_label = get_label_from(chip.get_child('name')) || chip.get_basename();

        if (!add_sensors_from(chip, chip_label)) {
            // This is here to provide compatibility with previous code, but I can't find any
            // information about sensors being stored in chip/device directory. Can we delete it?
            const chip_device = chip.get_child('device');
            if (chip_device.query_exists(null)) {
                add_sensors_from(chip_device, chip_label);
            }
        }
    }
    return [sensor_files, sensor_labels];
}

/**
 * @param args.hasBorder Whether the box has a border (true) or not
 * @param args.horizontal Whether the box is horizontal (true)
 *      or vertical (false)
 * @param args.shouldPack Determines whether a horizontal box should have
 *      uniform spacing for its children. Only applies to horizontal boxes
 * @param args.spacing The amount of spacing for a given box
 * @returns a new Box with settings specified by args
 */
function box(args = {}) {
    const options = { };

    if (typeof args.spacing !== 'undefined') {
        options.spacing = args.spacing;
    }

    if (shellMajorVersion < 40) {
        if (args.hasBorder) {
            options.border_width = 10;
        }

        return args.horizontal ?
            new Gtk.HBox(options) : new Gtk.VBox(options);
    }

    if (args.hasBorder) {
        options.margin_top = 10;
        options.margin_bottom = 10;
        options.margin_start = 10;
        options.margin_end = 10;
    }

    options.orientation = args.horizontal ?
        Gtk.Orientation.HORIZONTAL : Gtk.Orientation.VERTICAL;

    const aliasBox = new Gtk.Box(options);

    if (args.shouldPack) {
        aliasBox.set_homogeneous(true);
    }


    aliasBox.add = aliasBox.append;
    aliasBox.pack_start = aliasBox.prepend;
    // normally, this would be append; it is aliased to prepend because
    // that appears to yield the same behavior as version < 40
    aliasBox.pack_end = aliasBox.prepend;

    return aliasBox;
}

const ColorSelect = class SystemMonitor_ColorSelect {
    constructor(name) {
        this.label = new Gtk.Label({label: name + _(':')});
        this.picker = new Gtk.ColorButton();
        this.actor = box({horizontal: true, spacing: 5});
        this.actor.add(this.label);
        this.actor.add(this.picker);
        this.picker.set_use_alpha(true);
    }
    set_value(value) {
        let color = new Gdk.RGBA();

        if (Gtk.get_major_version() >= 4) {
            // GDK did not support parsing hex colours with alpha before GTK 4.
            color.parse(value);
        } else {
            // Use the Compat only when GTK 4 is not available,
            // since it depends on the deprecated Clutter library.
            let Compat = extension.imports.compat;
            let clutterColor = Compat.color_from_string(value);
            let ctemp = [clutterColor.red, clutterColor.green, clutterColor.blue, clutterColor.alpha / 255];
            color.parse('rgba(' + ctemp.join(',') + ')');
        }

        this.picker.set_rgba(color);
    }
}

const IntSelect = class SystemMonitor_IntSelect {
    constructor(name) {
        this.label = new Gtk.Label({label: name + _(':')});
        this.spin = new Gtk.SpinButton();
        this.actor = box({horizontal: true, shouldPack: true, });
        this.actor.add(this.label);
        this.actor.add(this.spin);
        this.spin.set_numeric(true);
    }
    set_args(minv, maxv, incre, page) {
        this.spin.set_range(minv, maxv);
        this.spin.set_increments(incre, page);
    }
    set_value(value) {
        this.spin.set_value(value);
    }
}

const Select = class SystemMonitor_Select {
    constructor(name) {
        this.label = new Gtk.Label({label: name + _(':')});
        // this.label.set_justify(Gtk.Justification.RIGHT);
        this.selector = new Gtk.ComboBoxText();
        this.actor = box({horizontal: true, shouldPack: true, spacing: 5});
        this.actor.add(this.label);
        this.actor.add(this.selector);
    }
    set_value(value) {
        this.selector.set_active(value);
    }
    add(items) {
        items.forEach((item) => {
            this.selector.append_text(item);
        })
    }
}

function set_enum(combo, schema, name) {
    Schema.set_enum(name, combo.get_active());
}

function set_color(color, schema, name) {
    Schema.set_string(name, color_to_hex(color.get_rgba()))
}

function set_string(combo, schema, name, _slist) {
    Schema.set_string(name, _slist[combo.get_active()]);
}

const SettingFrame = class SystemMonitor {
    constructor(name, schema) {
        this.schema = schema;
        this.label = new Gtk.Label({label: name});

        this.vbox = box({horizontal: false, shouldPack: true, spacing: 20});
        this.hbox0 = box({horizontal: true, shouldPack: true, spacing: 20});
        this.hbox1 = box({horizontal: true, shouldPack: true, spacing: 20});
        this.hbox2 = box({horizontal: true, shouldPack: true, spacing: 20});
        this.hbox3 = box({horizontal: true, shouldPack: true, spacing: 20});

        if (shellMajorVersion < 40) {
            this.frame = new Gtk.Frame({border_width: 10});
            this.frame.add(this.vbox);
        } else {
            this.frame = new Gtk.Frame({
                margin_top: 10,
                margin_bottom: 10,
                margin_start: 10,
                margin_end: 10
            });
            this.frame.set_child(this.vbox);
        }


        if (shellMajorVersion < 40) {
            this.vbox.pack_start(this.hbox0, true, false, 0);
            this.vbox.pack_start(this.hbox1, true, false, 0);
            this.vbox.pack_start(this.hbox2, true, false, 0);
            this.vbox.pack_start(this.hbox3, true, false, 0);
        } else {
            this.vbox.append(this.hbox0);
            this.vbox.append(this.hbox1);
            this.vbox.append(this.hbox2);
            this.vbox.append(this.hbox3);
        }
    }

    /** Enforces child ordering of first 2 boxes by label */
    _reorder() {
        if (shellMajorVersion < 40) {
            /** @return {string} label of/inside component */
            const labelOf = el => {
                if (el.get_children) {
                    return labelOf(el.get_children()[0]);
                }
                return el && el.label || '';
            };
            [this.hbox0, this.hbox1].forEach(hbox => {
                hbox.get_children()
                    .sort((c1, c2) => labelOf(c1).localeCompare(labelOf(c2)))
                    .forEach((child, index) => hbox.reorder_child(child, index));
            });
        } else {
            /** @return {string} label of/inside component */
            const labelOf = el => {
                if (el.get_label) {
                    return el.get_label();
                }
                return labelOf(el.get_first_child());
            }

            [this.hbox0, this.hbox1].forEach(hbox => {
                const children = [];
                let next = hbox.get_first_child();

                while (next !== null) {
                    children.push(next);
                    next = next.get_next_sibling();
                }

                const sorted = children
                    .sort((c1, c2) => labelOf(c1).localeCompare(labelOf(c2)));

                sorted
                    .forEach((child, index) => {
                        hbox.reorder_child_after(child, sorted[index - 1] || null);
                    });
            });
        }
    }

    add(key) {
        const configParent = key.substring(0, key.indexOf('-'));
        const config = key.substring(configParent.length + 1);

        // hbox0
        if (config === 'display') {
            let item = new Gtk.CheckButton({label: _('Display')});
            this.hbox0.add(item);
            Schema.bind(key, item, 'active', Gio.SettingsBindFlags.DEFAULT);
        } else if (config === 'show-text') {
            let item = new Gtk.CheckButton({label: _('Show Text')});
            this.hbox0.add(item);
            Schema.bind(key, item, 'active', Gio.SettingsBindFlags.DEFAULT);
        } else if (config === 'show-menu') {
            let item = new Gtk.CheckButton({label: _('Show In Menu')});
            this.hbox0.add(item);
            Schema.bind(key, item, 'active', Gio.SettingsBindFlags.DEFAULT);
        // hbox1
        } else if (config === 'refresh-time') {
            let item = new IntSelect(_('Refresh Time'));
            item.set_args(50, 100000, 1000, 5000);
            this.hbox1.add(item.actor);
            Schema.bind(key, item.spin, 'value', Gio.SettingsBindFlags.DEFAULT);
        } else if (config === 'graph-width') {
            let item = new IntSelect(_('Graph Width'));
            item.set_args(1, 1000, 1, 10);
            this.hbox1.add(item.actor);
            Schema.bind(key, item.spin, 'value', Gio.SettingsBindFlags.DEFAULT);
        } else if (config === 'style') {
            let item = new Select(_('Display Style'));
            item.add([_('digit'), _('graph'), _('both')]);
            item.set_value(this.schema.get_enum(key));
            this.hbox1.add(item.actor);
            item.selector.connect('changed', function (style) {
                set_enum(style, Schema, key);
            });
            // Schema.bind(key, item.selector, 'active', Gio.SettingsBindFlags.DEFAULT);
        // hbox2
        } else if (config.match(/-color$/)) {
            let item = new ColorSelect(_(config.split('-')[0].capitalize()));
            item.set_value(this.schema.get_string(key));
            if (shellMajorVersion < 40) {
                this.hbox2.pack_end(item.actor, true, false, 0);
            } else {
                this.hbox2.append(item.actor);
            }
            item.picker.connect('color-set', function (color) {
                set_color(color, Schema, key);
            });
        } else if (config.match(/sensor/)) {
            let sensor_type = configParent === 'fan' ? 'fan' : 'temp';
            let [_slist, _strlist] = check_sensors(sensor_type);
            let item = new Select(_('Sensor'));
            if (_slist.length === 0) {
                item.add([_('Please install lm-sensors')]);
            } else if (_slist.length === 1) {
                this.schema.set_string(key, _slist[0]);
            }
            item.add(_strlist);
            try {
                item.set_value(_slist.indexOf(this.schema.get_string(key)));
            } catch (e) {
                item.set_value(0);
            }
            // this.hbox3.add(item.actor);
            if (configParent === 'fan') {
                if (shellMajorVersion < 40) {
                    this.hbox2.pack_end(item.actor, true, false, 0);
                } else {
                    this.hbox2.append(item.actor);
                }
            } else if (shellMajorVersion < 40) {
                this.hbox2.pack_start(item.actor, true, false, 0);
            } else {
                this.hbox2.prepend(item.actor);
            }
            item.selector.connect('changed', function (combo) {
                set_string(combo, Schema, key, _slist);
            });
        // hbox3
        } else if (config === 'speed-in-bits') {
            let item = new Gtk.CheckButton({label: _('Show network speed in bits')});
            this.hbox3.add(item);
            Schema.bind(key, item, 'active', Gio.SettingsBindFlags.DEFAULT);
        } else if (config === 'individual-cores') {
            let item = new Gtk.CheckButton({label: _('Display Individual Cores')});
            this.hbox3.add(item);
            Schema.bind(key, item, 'active', Gio.SettingsBindFlags.DEFAULT);
        } else if (config === 'time') {
            let item = new Gtk.CheckButton({label: _('Show Time Remaining')});
            this.hbox3.add(item);
            Schema.bind(key, item, 'active', Gio.SettingsBindFlags.DEFAULT);
        } else if (config === 'hidesystem') {
            let item = new Gtk.CheckButton({label: _('Hide System Icon')});
            this.hbox3.add(item);
            Schema.bind(key, item, 'active', Gio.SettingsBindFlags.DEFAULT);
        } else if (config === 'usage-style') {
            let item = new Select(_('Usage Style'));
            item.add([_('pie'), _('bar'), _('none')]);
            item.set_value(this.schema.get_enum(key));
            if (shellMajorVersion < 40) {
                this.hbox3.pack_end(item.actor, false, false, 20);
            } else {
                this.hbox3.append(item.actor);
            }

            item.selector.connect('changed', function (style) {
                set_enum(style, Schema, key);
            });
        } else if (config === 'fahrenheit-unit') {
            let item = new Gtk.CheckButton({label: _('Display temperature in Fahrenheit')});
            this.hbox3.add(item);
            Schema.bind(key, item, 'active', Gio.SettingsBindFlags.DEFAULT);
        } else if (config === 'threshold') {
            let item = new IntSelect(_('Temperature threshold (0 to disable)'));
            item.set_args(0, 300, 5, 5);
            this.hbox3.add(item.actor);
            Schema.bind(key, item.spin, 'value', Gio.SettingsBindFlags.DEFAULT);
        }
        if (configParent.indexOf('gpu') !== -1 &&
            config === 'display') {
            let item = new Gtk.Label({label: _('** Only Nvidia GPUs supported so far **')});
            this.hbox3.add(item);
        }
        this._reorder();
    }
}

const App = class SystemMonitor_App {
    constructor() {
        let setting_items = ['cpu', 'memory', 'swap', 'net', 'disk', 'gpu', 'thermal', 'fan', 'freq', 'battery'];
        let keys = Schema.list_keys();

        this.items = [];
        this.settings = [];

        setting_items.forEach((setting) => {
            this.settings[setting] = new SettingFrame(_(setting.capitalize()), Schema);
        });

        this.main_vbox = box({
            hasBorder: true, horizontal: false, spacing: 10});
        this.hbox1 = box({
            hasBorder: true, horizontal: true, shouldPack: true, spacing: 20});
        if (shellMajorVersion < 40) {
            this.main_vbox.pack_start(this.hbox1, false, false, 0);
        } else {
            this.main_vbox.prepend(this.hbox1);
        }

        keys.forEach((key) => {
            if (key === 'icon-display') {
                let item = new Gtk.CheckButton({label: _('Display Icon')});
                this.items.push(item)
                this.hbox1.add(item)
                Schema.bind(key, item, 'active', Gio.SettingsBindFlags.DEFAULT);
            } else if (key === 'center-display') {
                let item = new Gtk.CheckButton({label: _('Display in the Middle')})
                this.items.push(item)
                this.hbox1.add(item)
                Schema.bind(key, item, 'active', Gio.SettingsBindFlags.DEFAULT);
            } else if (key === 'compact-display') {
                let item = new Gtk.CheckButton({label: _('Compact Display')})
                this.items.push(item)
                this.hbox1.add(item)
                Schema.bind(key, item, 'active', Gio.SettingsBindFlags.DEFAULT);
            } else if (key === 'show-tooltip') {
                let item = new Gtk.CheckButton({label: _('Show tooltip')})
                item.set_active(Schema.get_boolean(key))
                this.items.push(item)
                this.hbox1.add(item)
                Schema.bind(key, item, 'active', Gio.SettingsBindFlags.DEFAULT);
            } else if (key === 'move-clock') {
                let item = new Gtk.CheckButton({label: _('Move the clock')})
                this.items.push(item)
                this.hbox1.add(item)
                Schema.bind(key, item, 'active', Gio.SettingsBindFlags.DEFAULT);
            } else if (key === 'background') {
                let item = new ColorSelect(_('Background Color'))
                item.set_value(Schema.get_string(key))
                this.items.push(item)
                if (shellMajorVersion < 40) {
                    this.hbox1.pack_start(item.actor, true, false, 0)
                } else {
                    this.hbox1.prepend(item.actor)
                }
                item.picker.connect('color-set', function (color) {
                    set_color(color, Schema, key);
                });
            } else {
                let sections = key.split('-');
                if (setting_items.indexOf(sections[0]) >= 0) {
                    this.settings[sections[0]].add(key);
                }
            }
        });
        this.notebook = new Gtk.Notebook()
        setting_items.forEach((setting) => {
            this.notebook.append_page(this.settings[setting].frame, this.settings[setting].label)
            if (shellMajorVersion < 40) {
                this.main_vbox.show_all();
                this.main_vbox.pack_start(this.notebook, true, true, 0)
            } else {
                this.main_vbox.append(this.notebook);
            }
        });
        if (shellMajorVersion < 40) {
            this.main_vbox.show_all();
        }
    }
}

function buildPrefsWidget() {
    let widget = new App();
    return widget.main_vbox;
}
