import Geoclue from 'gi://Geoclue';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GWeather from 'gi://GWeather';
import Shell from 'gi://Shell';
import * as Signals from './signals.js';

import * as PermissionStore from './permissionStore.js';

import {loadInterfaceXML} from './fileUtils.js';

Gio._promisify(Geoclue.Simple, 'new');

const WeatherIntegrationIface = loadInterfaceXML('org.gnome.Shell.WeatherIntegration');

const WEATHER_BUS_NAME = 'org.gnome.Weather';
const WEATHER_OBJECT_PATH = '/org/gnome/Weather';
const WEATHER_INTEGRATION_IFACE = 'org.gnome.Shell.WeatherIntegration';

const WEATHER_APP_ID = 'org.gnome.Weather.desktop';

// Minimum time between updates to show loading indication
const UPDATE_THRESHOLD = 10 * GLib.TIME_SPAN_MINUTE;

export class WeatherClient extends Signals.EventEmitter {
    constructor() {
        super();

        this._loading = false;
        this._locationValid = false;
        this._lastUpdate = GLib.DateTime.new_from_unix_local(0);

        this._autoLocationRequested = false;
        this._mostRecentLocation = null;

        this._gclueService = null;
        this._gclueStarted = false;
        this._gclueStarting = false;
        this._gclueLocationChangedId = 0;

        this._needsAuth = true;
        this._weatherAuthorized = false;
        this._permStore = new PermissionStore.PermissionStore(async (proxy, error) => {
            if (error) {
                log(`Failed to connect to permissionStore: ${error.message}`);
                return;
            }

            if (this._permStore.g_name_owner == null) {
                // Failed to auto-start, likely because xdg-desktop-portal
                // isn't installed; don't restrict access to location service
                this._weatherAuthorized = true;
                this._updateAutoLocation();
                return;
            }

            let [perms, data] = [{}, null];
            try {
                [perms, data] = await this._permStore.LookupAsync('gnome', 'geolocation');
            } catch (err) {
                log(`Error looking up permission: ${err.message}`);
            }

            const params = ['gnome', 'geolocation', false, data, perms];
            this._onPermStoreChanged(this._permStore, '', params);
        });
        this._permStore.connectSignal('Changed',
            this._onPermStoreChanged.bind(this));

        this._locationSettings = new Gio.Settings({schema_id: 'org.gnome.system.location'});
        this._locationSettings.connect('changed::enabled',
            this._updateAutoLocation.bind(this));

        this._world = GWeather.Location.get_world();

        const providers =
            GWeather.Provider.METAR |
            GWeather.Provider.MET_NO |
            GWeather.Provider.OWM;
        this._weatherInfo = new GWeather.Info({
            application_id: 'org.gnome.Shell',
            contact_info: 'https://gitlab.gnome.org/GNOME/gnome-shell/-/raw/HEAD/gnome-shell.doap',
            enabled_providers: providers,
        });
        this._weatherInfo.connect_after('updated', () => {
            this._lastUpdate = GLib.DateTime.new_now_local();
            this.emit('changed');
        });

        this._weatherApp = null;
        this._weatherProxy = null;

        this._createWeatherProxy();

        this._settings = new Gio.Settings({
            schema_id: 'org.gnome.shell.weather',
        });
        this._settings.connect('changed::automatic-location',
            this._onAutomaticLocationChanged.bind(this));
        this._onAutomaticLocationChanged();
        this._settings.connect('changed::locations',
            this._onLocationsChanged.bind(this));
        this._onLocationsChanged();

        this._appSystem = Shell.AppSystem.get_default();
        this._appSystem.connect('installed-changed',
            this._onInstalledChanged.bind(this));
        this._onInstalledChanged();
    }

    get available() {
        return this._weatherApp != null;
    }

    get loading() {
        return this._loading;
    }

    get hasLocation() {
        return this._locationValid;
    }

    get info() {
        return this._weatherInfo;
    }

    activateApp() {
        if (this._weatherApp)
            this._weatherApp.activate();
    }

    update() {
        if (!this._locationValid)
            return;

        const now = GLib.DateTime.new_now_local();
        // Update without loading indication if the current info is recent enough
        if (this._weatherInfo.is_valid() &&
            now.difference(this._lastUpdate) < UPDATE_THRESHOLD)
            this._weatherInfo.update();
        else
            this._loadInfo();
    }

    get _useAutoLocation() {
        return this._autoLocationRequested &&
               this._locationSettings.get_boolean('enabled') &&
               (!this._needsAuth || this._weatherAuthorized);
    }

    async _createWeatherProxy() {
        const nodeInfo = Gio.DBusNodeInfo.new_for_xml(WeatherIntegrationIface);
        try {
            this._weatherProxy = await Gio.DBusProxy.new(
                Gio.DBus.session,
                Gio.DBusProxyFlags.DO_NOT_AUTO_START | Gio.DBusProxyFlags.GET_INVALIDATED_PROPERTIES,
                nodeInfo.lookup_interface(WEATHER_INTEGRATION_IFACE),
                WEATHER_BUS_NAME,
                WEATHER_OBJECT_PATH,
                WEATHER_INTEGRATION_IFACE,
                null);
        } catch (e) {
            log(`Failed to create GNOME Weather proxy: ${e}`);
            return;
        }

        this._weatherProxy.connect('g-properties-changed',
            this._onWeatherPropertiesChanged.bind(this));
        this._onWeatherPropertiesChanged();
    }

    _onWeatherPropertiesChanged() {
        if (this._weatherProxy.g_name_owner == null)
            return;

        this._settings.set_boolean('automatic-location',
            this._weatherProxy.AutomaticLocation);
        this._settings.set_value('locations',
            new GLib.Variant('av', this._weatherProxy.Locations));
    }

    _onInstalledChanged() {
        const hadApp = this._weatherApp != null;
        this._weatherApp = this._appSystem.lookup_app(WEATHER_APP_ID);
        const haveApp = this._weatherApp != null;

        if (hadApp !== haveApp)
            this.emit('changed');

        const neededAuth = this._needsAuth;
        this._needsAuth = this._weatherApp === null ||
                          this._weatherApp.app_info.has_key('X-Flatpak');

        if (neededAuth !== this._needsAuth)
            this._updateAutoLocation();
    }

    _loadInfo() {
        const id = this._weatherInfo.connect('updated', () => {
            this._weatherInfo.disconnect(id);
            this._loading = false;
        });

        this._loading = true;
        this.emit('changed');

        this._weatherInfo.update();
    }

    _locationsEqual(loc1, loc2) {
        if (loc1 === loc2)
            return true;

        if (loc1 == null || loc2 == null)
            return false;

        return loc1.equal(loc2);
    }

    _setLocation(location) {
        if (this._locationsEqual(this._weatherInfo.location, location))
            return;

        this._weatherInfo.abort();
        this._weatherInfo.set_location(location);
        this._locationValid = location != null;

        if (location)
            this._loadInfo();
        else
            this.emit('changed');
    }

    _updateLocationMonitoring() {
        if (this._useAutoLocation) {
            if (this._gclueLocationChangedId !== 0 || this._gclueService == null)
                return;

            this._gclueLocationChangedId =
                this._gclueService.connect('notify::location',
                    this._onGClueLocationChanged.bind(this));
            this._onGClueLocationChanged();
        } else {
            if (this._gclueLocationChangedId)
                this._gclueService.disconnect(this._gclueLocationChangedId);
            this._gclueLocationChangedId = 0;
        }
    }

    async _startGClueService() {
        if (this._gclueStarting)
            return;

        this._gclueStarting = true;

        try {
            this._gclueService = await Geoclue.Simple.new(
                'org.gnome.Shell', Geoclue.AccuracyLevel.CITY, null);
        } catch (e) {
            log(`Failed to connect to Geoclue2 service: ${e.message}`);
            this._setLocation(this._mostRecentLocation);
            return;
        }
        this._gclueStarted = true;
        this._gclueService.get_client().distance_threshold = 100;
        this._updateLocationMonitoring();
    }

    _onGClueLocationChanged() {
        const geoLocation = this._gclueService.location;
        // Provide empty name so GWeather sets location name
        const location = GWeather.Location.new_detached('',
            null,
            geoLocation.latitude,
            geoLocation.longitude);
        this._setLocation(location);
    }

    _onAutomaticLocationChanged() {
        const useAutoLocation = this._settings.get_boolean('automatic-location');
        if (this._autoLocationRequested === useAutoLocation)
            return;

        this._autoLocationRequested = useAutoLocation;

        this._updateAutoLocation();
    }

    _updateAutoLocation() {
        this._updateLocationMonitoring();

        if (this._useAutoLocation)
            this._startGClueService();
        else
            this._setLocation(this._mostRecentLocation);
    }

    _onLocationsChanged() {
        const locations = this._settings.get_value('locations').deepUnpack();
        const serialized = locations.shift();
        let mostRecentLocation = null;

        if (serialized)
            mostRecentLocation = this._world.deserialize(serialized);

        if (this._locationsEqual(this._mostRecentLocation, mostRecentLocation))
            return;

        this._mostRecentLocation = mostRecentLocation;

        if (!this._useAutoLocation || !this._gclueStarted)
            this._setLocation(this._mostRecentLocation);
    }

    _onPermStoreChanged(proxy, sender, params) {
        const [table, id, deleted_, data_, perms] = params;

        if (table !== 'gnome' || id !== 'geolocation')
            return;

        const permission = perms['org.gnome.Weather'] || ['NONE'];
        const [accuracy] = permission;
        this._weatherAuthorized = accuracy !== 'NONE';

        this._updateAutoLocation();
    }
}
