# Copyright 2026, Jiri Eischmann <jiri@eischmann.cz> and the meshy contributors
# SPDX-License-Identifier: GPL-3.0-or-later

import gettext
import locale
import math
import re
import unicodedata


def ngettext(singular, plural, n):
    return gettext.dngettext("meshy", singular, plural, n)


from gi.repository import GLib

_URL_RE = re.compile(r'(https?://[^\s<>"\')\]]+)')
_SHARED_CONTACT_RE = re.compile(r"^<([0-9a-fA-F]{64}):(\d+):(.+)>$")
_SHARED_LOCATION_RE = re.compile(r"^(-?\d+\.\d+),\s*(-?\d+\.\d+)$")


def linkify(text: str) -> str:
    escaped = GLib.markup_escape_text(text)

    def _replace(m):
        url = m.group(1)
        trailing = ""
        while url and url[-1] in ".,;:!?":
            trailing = url[-1] + trailing
            url = url[:-1]
        return f'<a href="{url}">{url}</a>{trailing}'

    return _URL_RE.sub(_replace, escaped)


def parse_shared_contact(text: str) -> tuple[str, int, str] | None:
    m = _SHARED_CONTACT_RE.match(text.strip())
    if not m:
        return None
    return (m.group(1), int(m.group(2)), m.group(3))


def parse_shared_location(text: str) -> tuple[float, float] | None:
    m = _SHARED_LOCATION_RE.match(text.strip())
    if not m:
        return None
    lat, lon = float(m.group(1)), float(m.group(2))
    if -90 <= lat <= 90 and -180 <= lon <= 180:
        return (lat, lon)
    return None


def haversine_distance(lat1, lon1, lat2, lon2) -> float:
    """Return the great-circle distance in km between two points."""
    R = 6371.0
    dlat = math.radians(lat2 - lat1)
    dlon = math.radians(lon2 - lon1)
    a = (
        math.sin(dlat / 2) ** 2
        + math.cos(math.radians(lat1)) * math.cos(math.radians(lat2)) * math.sin(dlon / 2) ** 2
    )
    return R * 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))


def format_number(value: float, decimals: int = 1, sign: bool = False) -> str:
    fmt = f"%{'+' if sign else ''}.{decimals}f"
    return locale.format_string(fmt, value)


def parse_number(text: str) -> float:
    try:
        return locale.atof(text)
    except ValueError:
        return float(text)


def format_trip_time(ms: int | None) -> str:
    if not ms:
        return ""
    return locale.format_string("%.1f", ms / 1000) + " " + "s"


def format_duration(secs):
    d, secs = divmod(int(secs), 86400)
    h, secs = divmod(secs, 3600)
    m, secs = divmod(secs, 60)
    parts = []
    if d:
        parts.append(_("{d}d").format(d=d))
    if h:
        parts.append(_("{h}h").format(h=h))
    if m:
        parts.append(_("{m}m").format(m=m))
    parts.append(_("{secs}s").format(secs=secs))
    return " ".join(parts)


def format_time_ago(secs):
    secs = int(secs)
    if secs < 60:
        return ngettext("{} sec ago", "{} secs ago", secs).format(secs)
    if secs < 3600:
        mins = secs // 60
        return ngettext("{} min ago", "{} min ago", mins).format(mins)
    if secs < 86400:
        hours = secs // 3600
        return ngettext("{} hour ago", "{} hours ago", hours).format(hours)
    days = secs // 86400
    return ngettext("{} day ago", "{} days ago", days).format(days)


class TimeoutManager:
    def __init__(self):
        self._timers: dict[str, int] = {}

    def start(self, name, ms, callback, *args):
        self.cancel(name)
        self._timers[name] = GLib.timeout_add(ms, self._fire, name, callback, args)

    def _fire(self, name, callback, args):
        self._timers.pop(name, None)
        callback(*args)
        return False

    def cancel(self, name):
        tid = self._timers.pop(name, None)
        if tid is not None:
            GLib.source_remove(tid)

    def is_active(self, name):
        return name in self._timers

    def cancel_all(self):
        for tid in self._timers.values():
            GLib.source_remove(tid)
        self._timers.clear()


def clear_children(container):
    while child := container.get_first_child():
        container.remove(child)


def extract_emoji(name: str) -> str | None:
    if not name:
        return None
    for char in name:
        cat = unicodedata.category(char)
        if cat == "So":
            return char
        cp = ord(char)
        if 0x1F600 <= cp <= 0x1F64F:
            return char
        if 0x1F300 <= cp <= 0x1F5FF:
            return char
        if 0x1F680 <= cp <= 0x1F6FF:
            return char
        if 0x1F900 <= cp <= 0x1F9FF:
            return char
        if 0x1FA00 <= cp <= 0x1FAFF:
            return char
        if 0x2600 <= cp <= 0x27BF:
            return char
        if 0xFE00 <= cp <= 0xFE0F:
            continue
        if cp == 0x200D:
            continue
    return None
