/** @license (c) aylur. GPL v3 */

import Adw from "gi://Adw";
import Gio from "gi://Gio";
import Gdk from "gi://Gdk";
import Gtk from "gi://Gtk";
import GObject from "gi://GObject";

// GNOME imports
import { gettext as _ } from "resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js";

// Shared state
import { Logger } from "../shared/logger.js";

export class PreferencesPage extends Adw.PreferencesPage {
  static {
    GObject.registerClass(this);
  }

  add_group({ title, description = "", children, header_suffix = "" }) {
    const group = new Adw.PreferencesGroup({ title, description });
    for (const child of children) group.add(child);
    if (header_suffix) group.set_header_suffix(header_suffix);
    this.add(group);
    return group;
  }
}

export class SwitchRow extends Adw.ActionRow {
  static {
    GObject.registerClass(this);
  }

  constructor({ title, settings, bind, subtitle = "", experimental = false }) {
    super({ title, subtitle });
    const gswitch = new Gtk.Switch({
      active: settings.get_boolean(bind),
      valign: Gtk.Align.CENTER,
    });
    settings.bind(bind, gswitch, "active", Gio.SettingsBindFlags.DEFAULT);
    if (experimental) {
      const icon = new Gtk.Image({ icon_name: "bug-symbolic" });
      icon.set_tooltip_markup(
        _("<b>CAUTION</b>: Enabling this setting can lead to bugs or cause the shell to crash")
      );
      this.add_suffix(icon);
    }
    this.add_suffix(gswitch);
    this.activatable_widget = gswitch;
  }
}

export class ColorRow extends Adw.ActionRow {
  static {
    GObject.registerClass(this);
  }

  constructor({ title, init, onChange, subtitle = "" }) {
    super({ title, subtitle });
    let rgba = new Gdk.RGBA();
    rgba.parse(init);
    this.colorButton = new Gtk.ColorButton({ rgba, use_alpha: true, valign: Gtk.Align.CENTER });
    this.colorButton.connect("color-set", () => {
      onChange(this.colorButton.get_rgba().to_string());
    });
    this.add_suffix(this.colorButton);
    this.activatable_widget = this.colorButton;
  }
}

export class SpinButtonRow extends Adw.ActionRow {
  static {
    GObject.registerClass(this);
  }

  constructor({
    title,
    range: [low, high, step],
    subtitle = "",
    init = undefined,
    onChange = undefined,
    max_width_chars = undefined,
    max_length = undefined,
    width_chars = undefined,
    xalign = undefined,
    settings = undefined,
    bind = undefined,
  }) {
    super({ title, subtitle });
    const gspin = Gtk.SpinButton.new_with_range(low, high, step);
    gspin.xalign = 1;
    if (bind && settings) {
      settings.bind(bind, gspin, "value", Gio.SettingsBindFlags.DEFAULT);
    } else if (init) {
      gspin.value = init;
      gspin.connect("value-changed", (widget) => {
        onChange?.(widget.value);
      });
    }
    this.add_suffix(gspin);
    this.set_css_classes(["spin"]);
    this.activatable_widget = gspin;
  }
}

export class DropDownRow extends Adw.ActionRow {
  static {
    GObject.registerClass(this);
  }

  /**
   * @type {string}
   * Name of the gsetting key to bind to
   */
  bind;

  /**
   * @type {'b'|'y'|'n'|'q'|'i'|'u'|'x'|'t'|'h'|'d'|'s'|'o'|'g'|'?'|'a'|'m'}
   * - b: the type string of G_VARIANT_TYPE_BOOLEAN; a boolean value.
   * - y: the type string of G_VARIANT_TYPE_BYTE; a byte.
   * - n: the type string of G_VARIANT_TYPE_INT16; a signed 16 bit integer.
   * - q: the type string of G_VARIANT_TYPE_UINT16; an unsigned 16 bit integer.
   * - i: the type string of G_VARIANT_TYPE_INT32; a signed 32 bit integer.
   * - u: the type string of G_VARIANT_TYPE_UINT32; an unsigned 32 bit integer.
   * - x: the type string of G_VARIANT_TYPE_INT64; a signed 64 bit integer.
   * - t: the type string of G_VARIANT_TYPE_UINT64; an unsigned 64 bit integer.
   * - h: the type string of G_VARIANT_TYPE_HANDLE; a signed 32 bit value that, by convention, is used as an index into an array of file descriptors that are sent alongside a D-Bus message.
   * - d: the type string of G_VARIANT_TYPE_DOUBLE; a double precision floating point value.
   * - s: the type string of G_VARIANT_TYPE_STRING; a string.
   * - o: the type string of G_VARIANT_TYPE_OBJECT_PATH; a string in the form of a D-Bus object path.
   * - g: the type string of G_VARIANT_TYPE_SIGNATURE; a string in the form of a D-Bus type signature.
   * - ?: the type string of G_VARIANT_TYPE_BASIC; an indefinite type that is a supertype of any of the basic types.
   * - v: the type string of G_VARIANT_TYPE_VARIANT; a container type that contain any other type of value.
   * - a: used as a prefix on another type string to mean an array of that type; the type string “ai”, for example, is the type of an array of signed 32-bit integers.
   * - m: used as a prefix on another type string to mean a “maybe”, or “nullable”, version of that type; the type string “ms”, for example, is the type of a value that maybe contains a string, or maybe contains nothing.
   */
  type;

  selected = 0;

  /** @type {{name: string; id: string}[]} */
  items;

  model = new Gtk.StringList();

  /** @type {Gtk.DropDown} */
  dropdown;

  constructor({ title, settings, bind, items, subtitle = "", type }) {
    super({ title, subtitle });
    this.settings = settings;
    this.items = items;
    this.bind = bind;
    this.type = type ?? this.settings.get_value(bind)?.get_type() ?? "?";
    this.#build();
    this.add_suffix(this.dropdown);
    this.add_suffix(new ResetButton({ settings, bind, onReset: () => this.reset() }));
  }

  reset() {
    this.dropdown.selected = 0;
    this.selected = 0;
  }

  #build() {
    for (const { name, id } of this.items) {
      this.model.append(name);
      if (this.#get() === id) this.selected = this.items.findIndex((x) => x.id === id);
    }
    const { model, selected } = this;
    this.dropdown = new Gtk.DropDown({ valign: Gtk.Align.CENTER, model, selected });
    this.dropdown.connect("notify::selected", () => this.#onSelected());
    this.activatable_widget = this.dropdown;
  }

  #onSelected() {
    this.selected = this.dropdown.selected;
    const { id } = this.items[this.selected];
    Logger.debug("setting", id, this.selected);
    this.#set(this.bind, id);
  }

  static #settingsTypes = {
    b: "boolean",
    y: "byte",
    n: "int16",
    q: "uint16",
    i: "int32",
    u: "uint",
    x: "int64",
    t: "uint64",
    d: "double",
    s: "string",
    o: "objv",
  };

  /**
   * @param {string} x
   */
  #get(x = this.bind) {
    const methodName = `get_${DropDownRow.#settingsTypes[this.type] ?? "value"}`;
    return this.settings[methodName]?.(x);
  }

  /**
   * @param {string} x
   * @param {unknown} y
   */
  #set(x, y) {
    const methodName = `set_${DropDownRow.#settingsTypes[this.type] ?? "value"}`;
    Logger.log(`${methodName}(${x}, ${y})`);
    return this.settings[methodName]?.(x, y);
  }
}

export class ClearButton extends Gtk.Button {
  static {
    GObject.registerClass(this);
  }
  constructor({ settings = undefined, bind = undefined, onClear }) {
    super({
      icon_name: "edit-clear-symbolic",
      tooltip_text: _("Clear shortcut"),
      css_classes: ["flat", "circular"],
      valign: Gtk.Align.CENTER,
    });
    this.connect("clicked", () => {
      onClear?.();
    });
  }
}

export class ResetButton extends Gtk.Button {
  static {
    GObject.registerClass(this);
  }
  constructor({ settings = undefined, bind = undefined, onReset }) {
    super({
      icon_name: "edit-undo-symbolic",
      tooltip_text: _("Reset to default"),
      css_classes: ["flat", "circular"],
      valign: Gtk.Align.CENTER,
    });
    this.connect("clicked", () => {
      settings?.reset(bind);
      onReset?.();
    });
  }
}

export class RemoveButton extends Gtk.Button {
  static {
    GObject.registerClass(this);
  }
  constructor({ item, parent, onRemove }) {
    super({
      icon_name: "edit-delete-symbolic",
      tooltip_text: _("Remove Item"),
      css_classes: ["flat", "circular"],
      valign: Gtk.Align.CENTER,
    });
    this.connect("clicked", () => {
      onRemove?.(item, parent);
    });
  }
}

export class EntryRow extends Adw.EntryRow {
  static {
    GObject.registerClass(this);
  }

  constructor({ title, settings, bind, map }) {
    super({ title });
    this.connect("changed", () => {
      const text = this.get_text();
      if (typeof text === "string")
        if (map) {
          map.to(settings, bind, text);
        } else {
          settings.set_string(bind, text);
        }
    });
    const current = map ? map.from(settings, bind) : settings.get_string(bind);
    this.set_text(current ?? "");
    this.add_suffix(
      new ClearButton({
        settings,
        bind,
        onClear: () => {
          this.set_text("");
        },
      })
    );
    this.add_suffix(
      new ResetButton({
        settings,
        bind,
        onReset: () => {
          this.set_text((map ? map.from(settings, bind) : settings.get_string(bind)) ?? "");
        },
      })
    );
  }
}

export class RadioRow extends Adw.ActionRow {
  static {
    GObject.registerClass(this);
  }

  static orientation = Gtk.Orientation.HORIZONTAL;

  static spacing = 3;

  static valign = Gtk.Align.CENTER;

  constructor({ title, subtitle = "", settings, bind, options }) {
    super({ title, subtitle });
    const current = settings.get_string(bind);
    const labels = Object.fromEntries(Object.entries(options).map(([k, v]) => [v, k]));
    const { orientation, spacing, valign } = RadioRow;
    const hbox = new Gtk.Box({ orientation, spacing, valign });
    let group;
    for (const [key, label] of Object.entries(options)) {
      const toggle = new Gtk.ToggleButton({ label, ...(group && { group }) });
      group ||= toggle;
      toggle.active = key === current;
      toggle.set_css_classes(["flat"]);
      toggle.connect("clicked", () => {
        if (toggle.active) {
          settings.set_string(bind, labels[toggle.label]);
        }
      });
      hbox.append(toggle);
    }
    this.add_suffix(hbox);
  }
}

export class RemoveItemRow extends Adw.ActionRow {
  static {
    GObject.registerClass(this);
  }

  constructor({ title, subtitle = "", onRemove = undefined }) {
    super({ title, subtitle });
    const rmbutton = new RemoveButton({
      item: subtitle,
      parent: this,
      onRemove: onRemove,
    });

    this.add_suffix(rmbutton);
  }
}
