// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect
//
// SPDX-License-Identifier: GPL-2.0-or-later

import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';

import * as Components from '../components/index.js';
import Plugin from '../plugin.js';


export const Metadata = {
    label: _('Battery'),
    description: _('Exchange battery information'),
    id: 'org.gnome.Shell.Extensions.GSConnect.Plugin.Battery',
    incomingCapabilities: [
        'kdeconnect.battery',
        'kdeconnect.battery.request',
    ],
    outgoingCapabilities: [
        'kdeconnect.battery',
        'kdeconnect.battery.request',
    ],
    actions: {},
};


/**
 * Battery Plugin
 * https://github.com/KDE/kdeconnect-kde/tree/master/plugins/battery
 */
const BatteryPlugin = GObject.registerClass({
    GTypeName: 'GSConnectBatteryPlugin',
}, class BatteryPlugin extends Plugin {

    _init(device) {
        super._init(device, 'battery');

        // Setup Cache; defaults are 90 minute charge, 1 day discharge
        this._chargeState = [54, 0, -1];
        this._dischargeState = [864, 0, -1];
        this._thresholdLevel = 25;

        this.cacheProperties([
            '_chargeState',
            '_dischargeState',
            '_thresholdLevel',
        ]);

        // Export battery state as GAction
        this.__state = new Gio.SimpleAction({
            name: 'battery',
            parameter_type: new GLib.VariantType('(bsii)'),
            state: this.state,
        });
        this.device.add_action(this.__state);

        // Local Battery (UPower)
        this._upower = null;
        this._sendStatisticsId = this.settings.connect(
            'changed::send-statistics',
            this._onSendStatisticsChanged.bind(this)
        );
        this._onSendStatisticsChanged(this.settings);
    }

    get charging() {
        if (this._charging === undefined)
            this._charging = false;

        return this._charging;
    }

    get icon_name() {
        let icon;

        if (this.level === -1)
            return 'battery-missing-symbolic';
        else if (this.level === 100)
            return 'battery-full-charged-symbolic';
        else if (this.level < 3)
            icon = 'battery-empty';
        else if (this.level < 10)
            icon = 'battery-caution';
        else if (this.level < 30)
            icon = 'battery-low';
        else if (this.level < 60)
            icon = 'battery-good';
        else if (this.level >= 60)
            icon = 'battery-full';

        if (this.charging)
            return `${icon}-charging-symbolic`;

        return `${icon}-symbolic`;
    }

    get level() {
        // This is what KDE Connect returns if the remote battery plugin is
        // disabled or still being loaded
        if (this._level === undefined)
            this._level = -1;

        return this._level;
    }

    get time() {
        if (this._time === undefined)
            this._time = 0;

        return this._time;
    }

    get state() {
        return new GLib.Variant(
            '(bsii)',
            [this.charging, this.icon_name, this.level, this.time]
        );
    }

    cacheLoaded() {
        this._initEstimate();
        this._sendState();
    }

    clearCache() {
        this._chargeState = [54, 0, -1];
        this._dischargeState = [864, 0, -1];
        this._thresholdLevel = 25;
        this._initEstimate();
    }

    connected() {
        super.connected();

        this._requestState();
        this._sendState();
    }

    handlePacket(packet) {
        switch (packet.type) {
            case 'kdeconnect.battery':
                this._receiveState(packet);
                break;

            case 'kdeconnect.battery.request':
                this._sendState();
                break;
        }
    }

    _onSendStatisticsChanged() {
        if (this.settings.get_boolean('send-statistics'))
            this._monitorState();
        else
            this._unmonitorState();
    }

    /**
     * Recalculate and update the estimated time remaining, but not the rate.
     */
    _initEstimate() {
        let rate, level;

        // elision of [rate, time, level]
        if (this.charging)
            [rate,, level] = this._chargeState;
        else
            [rate,, level] = this._dischargeState;

        if (!Number.isFinite(rate) || rate < 1)
            rate = this.charging ? 864 : 90;

        if (!Number.isFinite(level) || level < 0)
            level = this.level;

        // Update the time remaining
        if (rate && this.charging)
            this._time = Math.floor(rate * (100 - level));
        else if (rate && !this.charging)
            this._time = Math.floor(rate * level);

        this.__state.state = this.state;
    }

    /**
     * Recalculate the (dis)charge rate and update the estimated time remaining.
     */
    _updateEstimate() {
        let rate, time, level;
        const newTime = Math.floor(Date.now() / 1000);
        const newLevel = this.level;

        // Load the state; ensure we have sane values for calculation
        if (this.charging)
            [rate, time, level] = this._chargeState;
        else
            [rate, time, level] = this._dischargeState;

        if (!Number.isFinite(rate) || rate < 1)
            rate = this.charging ? 54 : 864;

        if (!Number.isFinite(time) || time <= 0)
            time = newTime;

        if (!Number.isFinite(level) || level < 0)
            level = newLevel;

        // Update the rate; use a weighted average to account for missed changes
        // NOTE: (rate = seconds/percent)
        const ldiff = this.charging ? newLevel - level : level - newLevel;
        const tdiff = newTime - time;
        const newRate = tdiff / ldiff;

        if (newRate && Number.isFinite(newRate))
            rate = Math.floor((rate * 0.4) + (newRate * 0.6));

        // Store the state for the next recalculation
        if (this.charging)
            this._chargeState = [rate, newTime, newLevel];
        else
            this._dischargeState = [rate, newTime, newLevel];

        // Update the time remaining
        if (rate && this.charging)
            this._time = Math.floor(rate * (100 - newLevel));
        else if (rate && !this.charging)
            this._time = Math.floor(rate * newLevel);

        this.__state.state = this.state;
    }

    /**
     * Notify the user the remote battery is full.
     */
    _fullBatteryNotification() {
        if (!this.settings.get_boolean('full-battery-notification'))
            return;

        // Offer the option to ring the device, if available
        let buttons = [];

        if (this.device.get_action_enabled('ring')) {
            buttons = [{
                label: _('Ring'),
                action: 'ring',
                parameter: null,
            }];
        }

        this.device.showNotification({
            id: 'battery|full',
            // TRANSLATORS: eg. Google Pixel: Battery is full
            title: _('%s: Battery is full').format(this.device.name),
            // TRANSLATORS: when the battery is fully charged
            body: _('Fully Charged'),
            icon: Gio.ThemedIcon.new('battery-full-charged-symbolic'),
            buttons: buttons,
        });
    }

    /**
     * Notify the user the remote battery is at custom charge level.
     */
    _customBatteryNotification() {
        if (!this.settings.get_boolean('custom-battery-notification'))
            return;

        // Offer the option to ring the device, if available
        let buttons = [];

        if (this.device.get_action_enabled('ring')) {
            buttons = [{
                label: _('Ring'),
                action: 'ring',
                parameter: null,
            }];
        }

        this.device.showNotification({
            id: 'battery|custom',
            // TRANSLATORS: eg. Google Pixel: Battery has reached custom charge level
            title: _('%s: Battery has reached custom charge level').format(this.device.name),
            // TRANSLATORS: when the battery has reached custom charge level
            body: _('%d%% Charged').format(this.level),
            icon: Gio.ThemedIcon.new('battery-full-charged-symbolic'),
            buttons: buttons,
        });
    }

    /**
     * Notify the user the remote battery is low.
     */
    _lowBatteryNotification() {
        if (!this.settings.get_boolean('low-battery-notification'))
            return;

        // Offer the option to ring the device, if available
        let buttons = [];

        if (this.device.get_action_enabled('ring')) {
            buttons = [{
                label: _('Ring'),
                action: 'ring',
                parameter: null,
            }];
        }

        this.device.showNotification({
            id: 'battery|low',
            // TRANSLATORS: eg. Google Pixel: Battery is low
            title: _('%s: Battery is low').format(this.device.name),
            // TRANSLATORS: eg. 15% remaining
            body: _('%d%% remaining').format(this.level),
            icon: Gio.ThemedIcon.new('battery-caution-symbolic'),
            buttons: buttons,
        });
    }

    /**
     * Handle a remote battery update.
     *
     * @param {Core.Packet} packet - A kdeconnect.battery packet
     */
    _receiveState(packet) {
        // Charging state changed
        this._charging = packet.body.isCharging;

        // Level changed
        if (this._level !== packet.body.currentCharge) {
            this._level = packet.body.currentCharge;

            // If the level is above the threshold hide the notification
            if (this._level > this._thresholdLevel)
                this.device.hideNotification('battery|low');

            // The level just changed to/from custom level while charging
            if ((this._level === this.settings.get_uint('custom-battery-notification-value')) && this._charging)
                this._customBatteryNotification();
            else
                this.device.hideNotification('battery|custom');

            // The level just changed to/from full
            if (this._level === 100)
                this._fullBatteryNotification();
            else
                this.device.hideNotification('battery|full');
        }

        // Device considers the level low
        if (packet.body.thresholdEvent > 0) {
            this._lowBatteryNotification();
            this._thresholdLevel = this.level;
        }

        this._updateEstimate();
    }

    /**
     * Request the remote battery's current state
     */
    _requestState() {
        this.device.sendPacket({
            type: 'kdeconnect.battery.request',
            body: {request: true},
        });
    }

    /**
     * Report the local battery's current state
     */
    _sendState() {
        if (this._upower === null || !this._upower.is_present)
            return;

        this.device.sendPacket({
            type: 'kdeconnect.battery',
            body: {
                currentCharge: this._upower.level,
                isCharging: this._upower.charging,
                thresholdEvent: this._upower.threshold,
            },
        });
    }

    /*
     * UPower monitoring methods
     */
    _monitorState() {
        try {
            // Currently only true if the remote device is a desktop (rare)
            const incoming = this.device.settings.get_strv('incoming-capabilities');

            if (!incoming.includes('kdeconnect.battery'))
                return;

            this._upower = Components.acquire('upower');

            this._upowerId = this._upower.connect(
                'changed',
                this._sendState.bind(this)
            );

            this._sendState();
        } catch (e) {
            logError(e, this.device.name);
            this._unmonitorState();
        }
    }

    _unmonitorState() {
        try {
            if (this._upower === null)
                return;

            this._upower.disconnect(this._upowerId);
            this._upower = Components.release('upower');
        } catch (e) {
            logError(e, this.device.name);
        }
    }

    destroy() {
        this.device.remove_action('battery');
        this.settings.disconnect(this._sendStatisticsId);
        this._unmonitorState();

        super.destroy();
    }
});

export default BatteryPlugin;
