#!/usr/bin/env python3

# build-mimeapps.py
# Copyright (C) 2019-2025, Jan Alexander Steffens (heftig) <heftig@archlinux.org>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, see <http://www.gnu.org/licenses/>.

from argparse import ArgumentParser
from configparser import ConfigParser, NoOptionError, NoSectionError, ParsingError
from contextlib import contextmanager
from pathlib import Path
from sys import exit, stderr

aparse = ArgumentParser(
    description="""
Transform default associations in custom format into standard XDG format.
"""
)
aparse.add_argument(
    "input", metavar="INPUT", type=Path, help="Custom format file to parse"
)
aparse.add_argument(
    "output", metavar="OUTPUT", type=Path, help="XDG format file to produce"
)
args = aparse.parse_args()

input_path: Path = args.input
output_path: Path = args.output
types: dict[str, list[str]] = {}
section: str = ""
status: int = 0


def perror(*args, **kwargs):
    global status

    print("ERROR:", *args, file=stderr, **kwargs)
    status = 1


class XDGParser(ConfigParser):
    def __init__(self):
        super().__init__(delimiters=("=",), comment_prefixes=("#",), interpolation=None)
        self.optionxform = str  # type: ignore


@contextmanager
def parse_xdg(path: Path):
    try:
        xparse = XDGParser()
        xparse.read(path)
        yield xparse
    except ParsingError:
        perror("Failed to parse", path)
    except NoSectionError as e:
        perror("No", e.section, "in", path)
    except NoOptionError as e:
        perror("No", e.option, "in", path)


def locate_file(line: str) -> Path | None:
    global status

    path = input_path.parent / line
    if path.exists():
        return path

    path = Path("/usr/share/applications") / line
    if path.exists():
        return path

    perror("Couldn't find", line)
    return None


def append_app(mime_type: str, app: str):
    apps = types.setdefault(mime_type, [])
    if app not in apps:
        apps.append(app)
    else:
        print(mime_type, "is already associated with", app, file=stderr)


def remove_app(mime_type: str, app: str):
    apps = types.setdefault(mime_type, [])
    try:
        apps.remove(app)
    except ValueError:
        print(mime_type, "was not associated with", app, file=stderr)


with input_path.open() as input_file:
    for line in input_file:
        # Handle whitespace and comments
        line = line.strip()
        if not line or line.startswith("#"):
            continue

        # Section headers
        if line.startswith("[") and line.endswith("]"):
            section = line[1:-1]
            continue

        # Load desktop files
        if section == "Desktop Entry Files":
            path = locate_file(line)
            if not path:
                continue
            with parse_xdg(path) as xparse:
                app = path.name
                mime_types = xparse.get("Desktop Entry", "MimeType")
                for mime_type in mime_types.split(";"):
                    if not mime_type:
                        continue
                    append_app(mime_type, app)
            continue

        # Load mimeapps.list files
        if section == "MIME Applications Files":
            path = locate_file(line)
            if not path:
                continue
            with parse_xdg(path) as xparse:
                for mime_type in xparse.options("Default Applications"):
                    apps = xparse.get("Default Applications", mime_type)
                    for app in apps.split(";"):
                        if not app:
                            continue
                        append_app(mime_type, app)
            continue

        # Explicit associations
        if section.endswith(".desktop"):
            if line.startswith("-"):
                remove_app(line[1:], section)
            else:
                append_app(line, section)
            continue

        raise RuntimeError(f"Bad section for line {line!r}")

with output_path.open("w") as output_file:
    print("[Default Applications]", file=output_file)

    for mime_type, apps in sorted(types.items()):
        if not apps:
            print("No associations left for", mime_type, file=stderr)
            continue

        if len(apps) > 1:
            print("Priority for", mime_type, end=": ", file=stderr)
            print(*apps, sep=" > ", file=stderr)

        print(mime_type, end="=", file=output_file)
        print(*apps, sep=";", end=";\n", file=output_file)

exit(status)

# Formatted using black
# vim:set et sw=4 tw=88:
