pragma Singleton
import Quickshell
import QtQuick

// Shared utility helpers used across plugins. Pure functions only — no
// state. Anything stateful belongs on Color, Style, or a service.
QtObject {
  id: root

  function clamp(value, min, max) {
    var n = Number(value)
    if (!isFinite(n)) return min
    return Math.max(min, Math.min(max, n))
  }

  function clampAlpha(value) {
    return clamp(value, 0, 1)
  }

  function wheelSteps(accumulator, delta) {
    // Some mouse/compositor combinations scale a single notch well beyond
    // Qt's conventional 120 units. Keep one event to one step while still
    // accumulating the smaller deltas emitted by touchpads.
    delta = Math.max(-120, Math.min(120, delta))
    if (accumulator * delta < 0) accumulator = 0
    var total = accumulator + delta
    var steps = total < 0 ? Math.ceil(total / 120) : Math.floor(total / 120)
    return { steps: steps, remainder: total - steps * 120 }
  }

  // Compose a base color with an opacity. Accepts a color object or a hex
  // string; null/undefined yields transparent black at the requested alpha.
  function alpha(c, opacity) {
    var a = clampAlpha(opacity)
    if (!c) return Qt.rgba(0, 0, 0, a)
    if (typeof c === "string") c = Qt.color(c)
    return Qt.rgba(c.r, c.g, c.b, a)
  }

  // file:// URL with each path segment percent-encoded so spaces and
  // special chars in user paths don't break Image.source.
  function fileUrl(path) {
    if (!path) return ""
    return "file://" + String(path).split("/").map(encodeURIComponent).join("/")
  }

  // Single-quote a string for bash. The replace handles embedded single
  // quotes by closing, escaping, and re-opening the literal.
  function shellQuote(value) {
    return "'" + String(value || "").replace(/'/g, "'\\''") + "'"
  }

  function execDetached(command) {
    Quickshell.execDetached(["bash", "-lc", command])
  }

  // Run an argv vector without a shell interpreting it: the constant `exec "$@"`
  // means the args only ever land in positional parameters, which bash expands
  // without re-tokenizing — so untrusted data ($(id), a filename) stays literal.
  // The login shell (-l) keeps the PATH/session env GUI targets (tensaku, mpv,
  // xdg-open) need. Prefer this over execDetached for anything built from input.
  function execArgv(argv) {
    Quickshell.execDetached(["bash", "-lc", 'exec "$@"', "bash"].concat(argv))
  }

  function isPlainObject(value) {
    return value !== null && typeof value === "object" && !Array.isArray(value)
  }

  function canonicalWidgetId(id) {
    return String(id || "")
  }

  // Best-effort base64 decode. Returns "" on parse failure rather than
  // surfacing garbage downstream.
  function decodeBase64(value) {
    var s = String(value || "")
    if (!s) return ""
    try { return Qt.atob(s) } catch (e) { return "" }
  }

  function cloneJson(value) {
    return JSON.parse(JSON.stringify(value === undefined ? null : value))
  }

  // Parse the last line of a custom-module / indicator process output as
  // waybar-style JSON ({text, class, tooltip, ...}). Falls back to {text: raw}
  // when the output isn't JSON, and {} for empty output.
  function parseModuleJson(raw) {
    var text = String(raw || "").trim()
    if (!text) return {}
    var lines = text.split("\n")
    try {
      return JSON.parse(lines[lines.length - 1])
    } catch (e) {
      return { text: text }
    }
  }

  // Standard Qt text-editing keys shared by every searchable panel's filter:
  //   Backspace       delete previous character
  //   Ctrl+Backspace  delete previous word (Qt DeleteStartOfWord)
  //   Ctrl+U          clear the whole field
  // True only when the event would actually change the text, so an empty
  // filter never swallows the key — panels keep their own empty-filter
  // fallbacks (e.g. menu back-navigation) in later branches.
  function editsFilter(event, text) {
    if (!text) return false
    // Alt/Meta-modified sequences belong to other shortcuts — never edit here.
    if (event.modifiers & (Qt.AltModifier | Qt.MetaModifier)) return false
    if (event.key === Qt.Key_U)                     // Ctrl+U only (not Ctrl+Shift+U → Unicode input)
      return event.modifiers === Qt.ControlModifier
    return event.key === Qt.Key_Backspace           // plain, Shift, or Ctrl Backspace
  }

  // New filter text after applying an edit key. Assumes editsFilter(event, text).
  function editedFilter(event, text) {
    if (event.key === Qt.Key_U) return ""                        // Ctrl+U: clear
    if (event.modifiers & Qt.ControlModifier)                    // Ctrl+Backspace: word
      return text.replace(/\s+$/, "").replace(/\S+$/, "")
    return text.slice(0, -1)                                     // Backspace: char
  }

  // Layout normalization shared by bar config consumers
  // so the two never drift. Entries are deep-cloned to decouple from the
  // input config; consumers can mutate without leaking back to shell.json.
  function normalizeLayoutEntry(entry) {
    if (typeof entry === "string") return { id: canonicalWidgetId(entry) }
    if (isPlainObject(entry) && entry.id) {
      var copy = cloneJson(entry)
      copy.id = canonicalWidgetId(copy.id)
      return copy
    }
    return null
  }

  function normalizeLayoutSection(list) {
    if (!Array.isArray(list)) return []
    var out = []
    for (var i = 0; i < list.length; i++) {
      var e = normalizeLayoutEntry(list[i])
      if (e) out.push(e)
    }
    return out
  }

  function normalizeLayout(layout) {
    var src = isPlainObject(layout) ? layout : {}
    return {
      left:   normalizeLayoutSection(src.left),
      center: normalizeLayoutSection(src.center),
      right:  normalizeLayoutSection(src.right)
    }
  }
}
