// SPDX-FileCopyrightText: 2023 Deminder <tremminder@gmail.com>
// SPDX-License-Identifier: GPL-3.0-or-later

import GObject from 'gi://GObject';
import St from 'gi://St';
import Gio from 'gi://Gio';
import Clutter from 'gi://Clutter';
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js';
import * as Slider from 'resource:///org/gnome/shell/ui/slider.js';
import * as QuickSettings from 'resource:///org/gnome/shell/ui/quickSettings.js';

import { InfoFetcher } from './info-fetcher.js';
import { Textbox } from './text-box.js';
import { gettext as _, ngettext as _n, pgettext as C_ } from './translation.js';
import * as SessionModeAware from './session-mode-aware.js';
import { getShutdownScheduleFromSettings } from './schedule-info.js';
import {
  ShutdownTimerName,
  ShutdownTimerObjectPath,
} from '../dbus-service/shutdown-timer-dbus.js';

import {
  foregroundActive,
  observeForegroundActive,
  unobserveForegroundActive,
} from './session-mode-aware.js';
import {
  durationString,
  longDurationString,
  absoluteTimeString,
  getSliderMinutesFromSettings,
  ScheduleInfo,
} from './schedule-info.js';

import { logDebug, proxyPromise } from './util.js';
import {
  WAKE_ACTIONS,
  actionLabel,
  ACTIONS,
  mapLegacyAction,
} from '../dbus-service/action.js';

/**
 * The ShutdownTimerItem controls wake/shutdown action time and mode.
 * Additionally, it shows wake and external/internal shutdown schedules.
 *
 * The external schedule of the `shutdown` command is fetched by the InfoFetcher.
 * Note that there is no external schedule for `suspend`.
 */
const ShutdownTimerItem = GObject.registerClass(
  {
    Properties: {
      'shutdown-text': GObject.ParamSpec.string(
        'shutdown-text',
        '',
        '',
        GObject.ParamFlags.READWRITE,
        ''
      ),
      'indicator-icon-name': GObject.ParamSpec.string(
        'indicator-icon-name',
        '',
        '',
        GObject.ParamFlags.READWRITE,
        'go-down-symbolic'
      ),
    },
    Signals: {
      'open-preferences': {},
      shutdown: {
        param_types: [GObject.TYPE_BOOLEAN, GObject.TYPE_STRING],
      },
      wake: {
        param_types: [GObject.TYPE_BOOLEAN],
      },
    },
  },
  class ShutdownTimerItem extends QuickSettings.QuickMenuToggle {
    _init({ path, settings }) {
      const gicon = Gio.icon_new_for_string(
        `${path}/icons/shutdown-timer-symbolic.svg`
      );
      super._init({ gicon, accessible_name: _('Shutdown Timer') });
      this.info = {
        internalShutdown: getShutdownScheduleFromSettings(settings),
        externalShutdown: new ScheduleInfo({ external: true }),
        externalWake: new ScheduleInfo({ mode: 'wake' }),
        state: 'inactive',
      };
      this._settings = settings;
      this.shutdownTimerIcon = gicon;
      this._updatingSwitcherState = false;

      // submenu in status area menu with slider and toggle button
      this.sliderItems = {};
      this.sliders = {};
      ['shutdown', 'wake'].forEach(prefix => {
        const [item, slider] = this._createSliderItem(prefix);
        this.sliderItems[prefix] = item;
        this.sliders[prefix] = slider;
        this._onShowSliderChanged(prefix);
      });
      this.switcher = new PopupMenu.PopupSwitchMenuItem('', false);
      // start/stop shutdown timer
      this.switcher.connect('toggled', () => {
        if (!this._updatingSwitcherState) {
          this.emit(
            'shutdown',
            this.switcher.state,
            this.info.internalShutdown.mode
          );
        }
      });
      this.switcherSettingsButton = new St.Button({
        reactive: true,
        can_focus: true,
        track_hover: true,
        accessible_name: _('Settings'),
        style_class: 'icon-button settings-button',
      });
      this.switcherSettingsButton.child = new St.Icon({
        icon_name: 'applications-system-symbolic',
      });
      this.switcherSettingsButton.connect('clicked', () =>
        this.emit('open-preferences')
      );
      this.switcher.add_child(this.switcherSettingsButton);

      this.menu.addMenuItem(this.switcher);
      // make switcher toggle without popup menu closing
      this.switcher.activate = __ => {
        if (this.switcher._switch.mapped) {
          this.switcher.toggle();
        }
      };
      this.menu.addMenuItem(this.sliderItems['shutdown']);

      this.modeItems = Object.keys(ACTIONS).map(action => {
        const modeItem = new PopupMenu.PopupMenuItem(actionLabel(action));
        modeItem.connect('activate', () => {
          this.emit('shutdown', true, action);
        });
        this.menu.addMenuItem(modeItem);
        return [action, modeItem];
      });

      this.wakeItems = [
        new PopupMenu.PopupSeparatorMenuItem(),
        this.sliderItems['wake'],
        ...Object.keys(WAKE_ACTIONS).map(action => {
          const modeItem = new PopupMenu.PopupMenuItem(actionLabel(action));
          if (action === 'wake') {
            this.wakeModeItem = modeItem;
          }
          modeItem.connect('activate', () =>
            this.emit('wake', action === 'wake')
          );
          return modeItem;
        }),
      ];
      this.wakeItems.forEach(item => {
        this.menu.addMenuItem(item);
      });
      this._updateWakeModeItem();
      this._updateSwitchLabel();

      // handlers for changed values in settings
      const settingsHandlerIds = [
        [
          [
            'shutdown-max-timer-value',
            'nonlinear-shutdown-slider-value',
            'shutdown-ref-timer-value',
            'show-shutdown-absolute-timer-value',
            'root-mode-value',
            'shutdown-slider-value',
          ],
          () => this._updateSwitchLabel(),
        ],
        [
          [
            'wake-max-timer-value',
            'wake-ref-timer-value',
            'show-wake-absolute-timer-value',
            'nonlinear-wake-slider-value',
            'wake-slider-value',
          ],
          () => this._updateWakeModeItem(),
        ],
        [['shutdown-slider-value'], () => this._updateSlider('shutdown')],
        [['wake-slider-value'], () => this._updateSlider('wake')],
        [
          [
            'show-wake-items-value',
            'show-shutdown-mode-value',
            'show-shutdown-slider-value',
            'show-wake-slider-value',
            'show-shutdown-indicator-value',
            'show-settings-value',
          ],
          () => this._sync(),
        ],
      ]
        .flatMap(([names, func]) => names.map(n => [n, func]))
        .map(([name, func]) =>
          this._settings.connect(`changed::${name}`, func)
        );

      this.connect('clicked', () => this.switcher.toggle());
      observeForegroundActive(this, () => this._sync());
      const tickHandlerId = setInterval(() => this.updateShutdownInfo(), 1000);
      this.connect('destroy', () => {
        unobserveForegroundActive(this);
        clearInterval(tickHandlerId);
        settingsHandlerIds.forEach(handlerId => {
          this._settings.disconnect(handlerId);
        });
      });
    }

    _sync() {
      // Update wake mode items
      this.wakeItems.forEach(item => {
        item.visible = this._settings.get_boolean('show-wake-items-value');
      });
      this._onShowSliderChanged('wake');

      // Update shutdown mode items
      const activeActions = this._settings
        .get_string('show-shutdown-mode-value')
        .split(',')
        .map(s => mapLegacyAction(s.trim()))
        .filter(action => action in ACTIONS);
      logDebug('[menu-item]', activeActions);
      this.modeItems.forEach(([action, item]) => {
        const position = activeActions.indexOf(action);
        if (position > -1) {
          this.menu.moveMenuItem(item, position + 2);
        }
        item.visible = position > -1;
      });

      this._onShowSliderChanged('shutdown');

      this.switcherSettingsButton.visible =
        foregroundActive() && this._settings.get_boolean('show-settings-value');

      this.updateShutdownInfo();
    }

    updateShutdownInfo() {
      const schedule = this.info.internalShutdown;
      const externalSchedule = this.info.externalShutdown;
      const externalWake = this.info.externalWake;
      const urgendSchedule = externalSchedule.isMoreUrgendThan(schedule)
        ? externalSchedule
        : schedule;
      const active = schedule.scheduled || this.info.state !== 'inactive';
      const showIndicator = this._settings.get_boolean(
        'show-shutdown-indicator-value'
      );

      // Update Indicator
      this.set({
        checked: active,
        shutdownText:
          urgendSchedule.scheduled && showIndicator
            ? urgendSchedule.secondsLeft > 0
              ? durationString(urgendSchedule.secondsLeft)
              : _('now')
            : '',
        indicatorIconName:
          showIndicator &&
          (this.info.internalShutdown.scheduled ||
            this.info.externalShutdown.scheduled)
            ? 'go-down-symbolic'
            : '',
      });

      // Update selected shutdown mode
      this.modeItems.forEach(([mode, item]) => {
        item.setOrnament(
          mode === schedule.mode
            ? PopupMenu.Ornament.DOT
            : PopupMenu.Ornament.NONE
        );
      });

      // Update title with scheduled action
      this.title = actionLabel(schedule.mode);

      // Update toggle state of switcher
      this._updatingSwitcherState = true;
      this.switcher.setToggleState(active);
      this._updatingSwitcherState = false;

      // Update long status description in menu
      this.menu.setHeader(
        this.shutdownTimerIcon,
        _('Shutdown Timer'),
        [schedule.label, externalWake.label].filter(v => !!v).join('\n')
      );
      if (this._settings.get_boolean('show-shutdown-absolute-timer-value')) {
        this._updateSwitchLabel();
      }
      if (this._settings.get_boolean('show-wake-absolute-timer-value')) {
        this._updateWakeModeItem();
      }
      this._updateSubtitle();
    }

    // update timer value if slider has changed
    _updateSlider(prefix) {
      this.sliders[prefix].value =
        this._settings.get_double(`${prefix}-slider-value`) / 100.0;
    }

    _createSliderItem(settingsPrefix) {
      const valueName = `${settingsPrefix}-slider-value`;
      const sliderValue = this._settings.get_double(valueName) / 100.0;
      const item = new PopupMenu.PopupBaseMenuItem({ activate: false });
      const sliderIcon = new St.Icon({
        icon_name:
          settingsPrefix === 'wake'
            ? 'alarm-symbolic'
            : 'system-shutdown-symbolic',
        style_class: 'popup-menu-icon',
      });
      item.add_child(sliderIcon);
      const slider = new Slider.Slider(sliderValue);
      slider.connect('notify::value', () => {
        const v = slider.value * 100;
        if (v !== this._settings.get_double(valueName))
          this._settings.set_double(valueName, v);
      });
      item.add_child(slider);
      return [item, slider];
    }

    get shutdownTimeStr() {
      const minutes = Math.abs(
        getSliderMinutesFromSettings(this._settings, 'shutdown')
      );
      return this._settings.get_boolean('show-shutdown-absolute-timer-value')
        ? absoluteTimeString(minutes, C_('absolute time notation', '%a, %R'))
        : longDurationString(
            minutes,
            h => _n('%s hr', '%s hrs', h),
            m => _n('%s min', '%s mins', m)
          );
    }

    _updateSwitchLabel() {
      this.switcher.label.text = this._settings.get_boolean('root-mode-value')
        ? _('%s (protect)').format(this.shutdownTimeStr)
        : this.shutdownTimeStr;

      if (this._settings.get_string('wake-ref-timer-value') === 'shutdown') {
        this._updateWakeModeItem();
      }
      if (!this.info.internalShutdown.scheduled) {
        this._updateSubtitle();
      }
    }

    _updateSubtitle() {
      this.subtitle = this.info.internalShutdown.scheduled
        ? this._settings.get_boolean('show-shutdown-absolute-timer-value')
          ? this.info.internalShutdown.absoluteTimeString
          : durationString(this.info.internalShutdown.secondsLeft)
        : this.shutdownTimeStr;
    }

    _updateWakeModeItem() {
      const minutes = getSliderMinutesFromSettings(this._settings, 'wake');
      const abs = this._settings.get_boolean('show-wake-absolute-timer-value');
      this.wakeModeItem.label.text = (
        abs
          ? C_('WakeButtonText', '%s at %s')
          : C_('WakeButtonText', '%s after %s')
      ).format(
        actionLabel('wake'),
        abs
          ? absoluteTimeString(minutes, C_('absolute time notation', '%a, %R'))
          : longDurationString(
              minutes,
              h => _n('%s hour', '%s hours', h),
              m => _n('%s minute', '%s minutes', m)
            )
      );
    }

    _onShowSliderChanged(settingsPrefix) {
      this.sliderItems[settingsPrefix].visible =
        (settingsPrefix !== 'wake' ||
          this._settings.get_boolean('show-wake-items-value')) &&
        this._settings.get_boolean(`show-${settingsPrefix}-slider-value`);
    }
  }
);

export const ShutdownTimerIndicator = GObject.registerClass(
  {
    Properties: {
      'wake-minutes': GObject.ParamSpec.int(
        'wake-minutes',
        '',
        '',
        GObject.ParamFlags.READABLE,
        0
      ),
      'shutdown-minutes': GObject.ParamSpec.int(
        'shutdown-minutes',
        '',
        '',
        GObject.ParamFlags.READABLE,
        0
      ),
    },
    Signals: {
      'open-preferences': {},
      shutdown: {
        param_types: [GObject.TYPE_BOOLEAN, GObject.TYPE_STRING],
      },
      wake: {
        param_types: [GObject.TYPE_BOOLEAN],
      },
    },
  },
  class ShutdownTimerIndicator extends QuickSettings.SystemIndicator {
    _init({ path, settings }) {
      super._init();
      this._state = 'inactive';
      this._settings = settings;
      this._textbox = new Textbox({ settings });
      const item = new ShutdownTimerItem({ path, settings });
      const infoFetcher = new InfoFetcher();
      const proxyCancel = new Gio.Cancellable();
      this._sdtProxy = null;
      this._initProxy(item, this._textbox, infoFetcher, proxyCancel).catch(
        err => {
          if (!proxyCancel.is_cancelled()) console.error('[sdt-proxy]', err);
        }
      );
      // React to changes in external shutdown and wake schedule
      infoFetcher.connect('changed', () => this._syncShutdownInfo());
      this._infoFetcher = infoFetcher;
      this._shutdownTimerItem = item;

      item.connect('open-preferences', () => this.emit('open-preferences'));

      const icon = new St.Icon({ style_class: 'system-status-icon' });
      const updateIcon = () => {
        const name = item.indicatorIconName;
        icon.visible = !!name;
        if (icon.visible) icon.iconName = name;
        this._syncIndicatorsVisible();
      };

      const scheduleLabel = new St.Label({
        y_expand: true,
        y_align: Clutter.ActorAlign.CENTER,
      });
      const updateLabel = () => {
        const text = item.shutdownText;
        scheduleLabel.text = text;
        scheduleLabel.visible = !!text;
        this._syncIndicatorsVisible();
      };

      this.add_child(icon);
      this.add_child(scheduleLabel);

      item.connect('notify::indicator-icon-name', () => updateIcon());
      item.connect('notify::shutdown-text', () => updateLabel());

      updateLabel();
      updateIcon();

      this.quickSettingsItems.push(item);
      this.connect('destroy', () => {
        proxyCancel.cancel();
        infoFetcher.destroy();
        this._infoFetcher = null;
        this._textbox.destroy();
        this.quickSettingsItems.forEach(i => i.destroy());
        this.quickSettingsItems = [];
        this._shutdownTimerItem = null;
        if (this._sdtProxy !== null) {
          this._sdtProxy.destroy();
          this._sdtProxy = null;
        }
        this._settings = null;
      });
      item.connect('destroy', () => {
        // Mitigate already destroyed error when logging out
        // Does js/ui/layout.js:256 destroy quickSettingsItems?
        this.quickSettingsItems = [];
      });
    }

    async _initProxy(item, textbox, infoFetcher, cancellable) {
      const proxy = await proxyPromise(
        ShutdownTimerName,
        Gio.DBus.session,
        'org.gnome.Shell',
        ShutdownTimerObjectPath,
        cancellable
      );
      if (cancellable?.is_cancelled()) return;
      item.connect('shutdown', (__, shutdown, action) => {
        logDebug(
          '[menu-item] [shutdown-signal] shutdown: ',
          shutdown,
          'action:',
          action
        );
        proxy
          .ScheduleShutdownAsync(shutdown, action)
          .catch(err => console.error('[shutdown]', err));
      });
      item.connect('wake', (__, wake) => {
        proxy
          .ScheduleWakeAsync(wake)
          .catch(err => console.error('[wake]', err));
      });

      const signalIds = [
        proxy.connectSignal('OnMessage', (__, ___, [msg]) => {
          textbox.showTextbox(msg);
        }),
        proxy.connectSignal('OnStateChange', (__, ___, [state]) => {
          logDebug('[menu-item] [OnStateChange-signal] state:', state);
          this._state = state;
          this._syncShutdownInfo();
        }),
        proxy.connectSignal('OnExternalChange', () => {
          infoFetcher.refresh();
        }),
      ];
      proxy.destroy = function () {
        signalIds.forEach(signalId => this.disconnectSignal(signalId));
      };

      this._sdtProxy = proxy;
      [this._state] = await this._sdtProxy.GetStateAsync();
      if (cancellable?.is_cancelled()) return;
      logDebug('[sdt-proxy] state', this._state);
      this._syncShutdownInfo();
      infoFetcher.refresh();
    }

    _syncShutdownInfo() {
      const item = this._shutdownTimerItem;
      if (this._state === 'action') {
        if (SessionModeAware.foregroundActive()) Main.overview.hide();
        this._textbox.hideAll();
      }
      item.info = {
        internalShutdown: getShutdownScheduleFromSettings(this._settings),
        externalShutdown: item.info.externalShutdown.copy({
          ...this._infoFetcher.shutdownInfo,
        }),
        externalWake: item.info.externalWake.copy({
          ...this._infoFetcher.wakeInfo,
        }),
        state: this._state,
      };
      item.updateShutdownInfo();
    }
  }
);
