#!/usr/bin/env bash

# A website, as an application.
#
#   panama-webapp install https://app.example.com "Example"
#   panama-webapp list
#   panama-webapp remove "Example"
#
# Both macOS and Windows ship this now -- Safari's "Add to Dock", Edge's
# "Install this site as an app" -- and the dock and launcher here had nothing
# to feed them but installed packages. A web app gets its own icon, its own
# window with no browser chrome, and its own entry in the launcher, which is
# most of what "installed" means in practice.
#
# The desktop entry is ordinary and inspectable: it lives in
# ~/.local/share/applications with everything else, and `remove` finds its own
# entries by the launcher line rather than by keeping a list somewhere.
#
# Chromium-family browsers implement --app. Firefox does not, and there is no
# honest equivalent, so a machine whose default browser is Firefox is told
# rather than given something that opens a normal window and pretends.

set -uo pipefail

APPLICATIONS="${XDG_DATA_HOME:-$HOME/.local/share}/applications"
ICONS="${XDG_DATA_HOME:-$HOME/.local/share}/icons/hicolor/256x256/apps"
LAUNCH_MARKER="panama-webapp"

err() { printf 'panama-webapp: %s\n' "$*" >&2; }

# A filename that cannot escape the applications directory. Everything that is
# not a letter or a digit becomes a hyphen, which also makes the result
# predictable enough for `remove` to find.
slugify() {
    printf '%s' "$1" \
        | tr '[:upper:]' '[:lower:]' \
        | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//'
}

# The browser to open the app window with. Follows the desktop's own default
# rather than naming one, and refuses rather than degrading when that browser
# cannot do app mode.
resolve_browser() {
    local desktop exec_line binary
    desktop="$(xdg-settings get default-web-browser 2>/dev/null || true)"

    if [[ -n "$desktop" ]]; then
        local file
        for dir in "$APPLICATIONS" /usr/local/share/applications /usr/share/applications; do
            file="$dir/$desktop"
            [[ -r "$file" ]] || continue
            exec_line="$(sed -n 's/^Exec=//p' "$file" | head -1)"
            binary="$(awk '{ print $1 }' <<<"$exec_line")"
            break
        done
    fi

    [[ -n "${binary:-}" ]] || binary="$(command -v chromium || command -v google-chrome || true)"
    [[ -n "$binary" ]] || return 1

    # Chromium-family only. The name check is crude but the alternative is
    # launching a browser to ask it, which is worse.
    case "$(basename "$binary")" in
        *firefox*|*librewolf*|*zen*) return 2 ;;
    esac
    printf '%s' "$binary"
}

# Four attempts, in descending order of how much the site had to say about it.
# An icon is never worth failing an install over: a web app with the generic
# icon still works.
fetch_icon() {
    local url="$1" slug="$2" origin html href target
    origin="$(sed -E 's#^(https?://[^/]+).*#\1#' <<<"$url")"
    target="$ICONS/$slug.png"
    mkdir -p "$ICONS"

    html="$(curl -fsSL --max-time 10 "$url" 2>/dev/null || true)"
    href="$(grep -oiE '<link[^>]+rel="[^"]*apple-touch-icon[^"]*"[^>]*>' <<<"$html" \
        | grep -oiE 'href="[^"]+"' | head -1 | sed 's/href="//I; s/"$//' || true)"

    if [[ -n "$href" ]]; then
        case "$href" in
            http*) ;;
            /*)    href="$origin$href" ;;
            *)     href="$origin/$href" ;;
        esac
        curl -fsSL --max-time 10 -o "$target" "$href" 2>/dev/null && { printf '%s' "$slug"; return 0; }
    fi

    curl -fsSL --max-time 10 -o "$target" "$origin/apple-touch-icon.png" 2>/dev/null \
        && { printf '%s' "$slug"; return 0; }

    local host
    host="$(sed -E 's#^https?://([^/]+).*#\1#' <<<"$url")"
    curl -fsSL --max-time 10 -o "$target" \
        "https://www.google.com/s2/favicons?sz=256&domain=$host" 2>/dev/null \
        && { printf '%s' "$slug"; return 0; }

    rm -f "$target"
    printf 'applications-internet'
}

cmd_install() {
    local url="${1:-}" name="${2:-}"
    [[ -n "$url" ]] || { err 'install needs a URL'; return 2; }
    [[ "$url" =~ ^https?:// ]] || { err 'the URL must begin with http:// or https://'; return 2; }

    # Default the name from the host, so `install https://app.example.com` is
    # enough for the common case.
    [[ -n "$name" ]] || name="$(sed -E 's#^https?://(www\.)?([^/]+).*#\2#' <<<"$url")"

    local slug; slug="$(slugify "$name")"
    [[ -n "$slug" ]] || { err 'that name has no usable characters in it'; return 2; }

    local browser status
    browser="$(resolve_browser)"; status=$?
    if (( status == 2 )); then
        err 'the default browser cannot open a site as its own application.'
        err 'Chromium-family browsers implement --app; Firefox does not.'
        return 1
    fi
    [[ -n "$browser" ]] || { err 'no browser found'; return 1; }

    local icon; icon="$(fetch_icon "$url" "$slug")"

    mkdir -p "$APPLICATIONS"
    local entry="$APPLICATIONS/$LAUNCH_MARKER-$slug.desktop"
    cat >"$entry" <<ENTRY
[Desktop Entry]
Type=Application
Name=$name
Comment=$url
Exec=$browser --app=$url
Icon=$icon
Terminal=false
Categories=Network;
StartupWMClass=$browser
X-Panama-WebApp=$url
ENTRY

    command -v update-desktop-database >/dev/null 2>&1 \
        && update-desktop-database "$APPLICATIONS" >/dev/null 2>&1
    command -v gtk-update-icon-cache >/dev/null 2>&1 \
        && gtk-update-icon-cache -f -t "${XDG_DATA_HOME:-$HOME/.local/share}/icons/hicolor" 2>/dev/null

    printf 'Installed %s\n' "$name"
    printf '  %s\n' "$entry"
}

cmd_list() {
    local entry name url
    shopt -s nullglob
    for entry in "$APPLICATIONS/$LAUNCH_MARKER-"*.desktop; do
        name="$(sed -n 's/^Name=//p' "$entry" | head -1)"
        url="$(sed -n 's/^X-Panama-WebApp=//p' "$entry" | head -1)"
        printf '%s\t%s\n' "$name" "$url"
    done
}

cmd_remove() {
    local name="${1:-}"
    [[ -n "$name" ]] || { err 'remove needs a name'; return 2; }
    local slug; slug="$(slugify "$name")"
    local entry="$APPLICATIONS/$LAUNCH_MARKER-$slug.desktop"

    # Only ever removes an entry this command installed. The prefix and the
    # X-Panama-WebApp key both have to be there, so a name collision with a
    # real application cannot delete it.
    [[ -f "$entry" ]] || { err "no web app named '$name'"; return 1; }
    grep -q '^X-Panama-WebApp=' "$entry" || { err "$entry is not a Panama web app"; return 1; }

    rm -f "$entry" "$ICONS/$slug.png"
    command -v update-desktop-database >/dev/null 2>&1 \
        && update-desktop-database "$APPLICATIONS" >/dev/null 2>&1
    printf 'Removed %s\n' "$name"
}

case "${1:-}" in
    install) shift; cmd_install "$@" ;;
    list)    shift; cmd_list "$@" ;;
    remove)  shift; cmd_remove "$@" ;;
    -h|--help|"")
        cat <<'USAGE'
usage: panama-webapp install <url> [name]
       panama-webapp list
       panama-webapp remove <name>

Turns a website into an application: its own icon, its own window with no
browser chrome, and its own entry in the launcher.
USAGE
        ;;
    *) err "unknown command: $1"; exit 2 ;;
esac
