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

"""Shared view utilities."""

import json
import threading
import weakref
from datetime import datetime, timedelta
from urllib.request import Request, urlopen

import gi

gi.require_version("Adw", "1")
gi.require_version("Gtk", "4.0")
gi.require_version("Shumate", "1.0")
from gi.repository import Adw, GLib, GObject, Gtk, Shumate

_OFM_BASE = "https://tiles.openfreemap.org"
_OFM_TILEJSON_URL = f"{_OFM_BASE}/planet"
_OFM_STYLE_URLS = {
    "bright": f"{_OFM_BASE}/styles/bright",
    "dark": f"{_OFM_BASE}/styles/dark",
}

_source_cache = {}
_source_failed = set()
_tile_urls_cache = None
_map_widgets = []
_theme_listener_connected = False


def _format_date_text(dt):
    """Format a datetime as a human-friendly date string for dividers."""
    today = datetime.now().date()
    d = dt.date() if isinstance(dt, datetime) else dt
    if d == today:
        return "Today"
    if d == today - timedelta(days=1):
        return "Yesterday"
    if d.year == today.year:
        return d.strftime("%A, %B %-d")
    return d.strftime("%A, %B %-d, %Y")


def _get_osm_source():
    registry = Shumate.MapSourceRegistry.new_with_defaults()
    return registry.get_by_id(Shumate.MAP_SOURCE_OSM_MAPNIK)


def _fix_interpolations(obj):
    """Fix ["linear", N] -> ["linear"] for Shumate compatibility."""
    if isinstance(obj, list):
        if (
            len(obj) >= 2
            and obj[0] == "interpolate"
            and isinstance(obj[1], list)
            and obj[1][0] == "linear"
            and len(obj[1]) > 1
        ):
            obj[1] = ["linear"]
        for item in obj:
            _fix_interpolations(item)
    elif isinstance(obj, dict):
        for v in obj.values():
            _fix_interpolations(v)


def _patch_style_json(style_text, tile_urls):
    """Patch OpenFreeMap style JSON for Shumate compatibility."""
    style = json.loads(style_text)
    sources = style.get("sources", {})
    vector_key = None
    for key, src in sources.items():
        if src.get("type") == "vector":
            vector_key = key
            break
    if not vector_key:
        return None
    vector_src = sources[vector_key]
    vector_src.pop("url", None)
    vector_src["tiles"] = tile_urls
    style["sources"] = {vector_key: vector_src}
    style.pop("sprite", None)
    style.pop("glyphs", None)
    for layer in style.get("layers", []):
        _fix_interpolations(layer)
    return json.dumps(style)


def _fetch_and_create_ofm_source(style, callback):
    """Fetch an OFM style + TileJSON and create a VectorRenderer asynchronously."""
    if style in _source_cache:
        callback(_source_cache[style])
        return
    if style in _source_failed:
        callback(None)
        return

    def _worker():
        global _tile_urls_cache
        try:
            ua = "Meshy/1.0 (https://codeberg.org/sesivany/meshy)"
            style_url = _OFM_STYLE_URLS[style]
            style_text = (
                urlopen(Request(style_url, headers={"User-Agent": ua}), timeout=10)
                .read()
                .decode("utf-8")
            )
            if _tile_urls_cache is None:
                tilejson_text = (
                    urlopen(Request(_OFM_TILEJSON_URL, headers={"User-Agent": ua}), timeout=10)
                    .read()
                    .decode("utf-8")
                )
                tilejson = json.loads(tilejson_text)
                _tile_urls_cache = tilejson.get("tiles", [])
            if not _tile_urls_cache:
                raise ValueError("No tile URLs")
            patched = _patch_style_json(style_text, _tile_urls_cache)
            if not patched:
                raise ValueError("Style patching failed")

            def _finish():
                try:
                    renderer = Shumate.VectorRenderer.new(f"ofm-{style}", patched)
                    renderer.set_license("© OpenFreeMap © OpenStreetMap (ODbL)")
                    renderer.set_license_uri("https://www.openstreetmap.org/copyright")
                    _source_cache[style] = renderer
                    callback(renderer)
                except Exception:
                    _source_failed.add(style)
                    callback(None)
                return False

            GLib.idle_add(_finish)
        except Exception:
            _source_failed.add(style)
            GLib.idle_add(lambda: callback(None) or False)

    threading.Thread(target=_worker, daemon=True).start()


_MAX_ZOOM = 14


def _apply_map_source(map_widget, source):
    if source:
        map_widget.set_map_source(source)
        map_widget.get_viewport().set_max_zoom_level(_MAX_ZOOM)


def _is_dark():
    return Adw.StyleManager.get_default().get_dark()


def _on_theme_changed(*_args):
    live = [(ref, ref()) for ref in _map_widgets]
    _map_widgets[:] = [ref for ref, w in live if w is not None]
    widgets = [w for _, w in live if w is not None]
    if not widgets:
        return
    style = "dark" if _is_dark() else "bright"

    def _apply(source):
        s = source if source else _get_osm_source()
        for w in widgets:
            _apply_map_source(w, s)

    _fetch_and_create_ofm_source(style, _apply)


def create_shumate_map(zoom_level=14, lat=0.0, lon=0.0):
    """Create a Shumate SimpleMap with theme-aware tiles."""
    global _theme_listener_connected

    map_widget = Shumate.SimpleMap()
    map_widget.set_vexpand(True)

    osm_source = _get_osm_source()
    if osm_source:
        _apply_map_source(map_widget, osm_source)

    viewport = map_widget.get_viewport()
    viewport.set_zoom_level(zoom_level)
    viewport.set_location(lat, lon)

    marker_layer = Shumate.MarkerLayer.new(viewport)
    map_widget.get_map().add_layer(marker_layer)

    _map_widgets.append(weakref.ref(map_widget))

    style = "dark" if _is_dark() else "bright"

    def _apply_ofm(source):
        if source:
            _apply_map_source(map_widget, source)

    _fetch_and_create_ofm_source(style, _apply_ofm)

    if not _theme_listener_connected:
        Adw.StyleManager.get_default().connect("notify::dark", _on_theme_changed)
        _theme_listener_connected = True

    return map_widget, marker_layer, viewport


def deregister_map_widget(map_widget):
    """Remove a map widget from the theme-tracking list."""
    _map_widgets[:] = [ref for ref in _map_widgets if ref() is not None and ref() is not map_widget]


def open_map_picker(window, title, button_label, init_lat, init_lon, on_pick):
    """Open a map dialog with crosshair, call on_pick(lat, lon) on confirm."""
    dialog = Adw.Dialog()
    dialog.set_title(title)
    win_w = window.get_width() or 800
    win_h = window.get_height() or 600
    dialog.set_content_width(max(500, win_w - 60))
    dialog.set_content_height(max(400, win_h - 60))

    toolbar_view = Adw.ToolbarView()
    header = Adw.HeaderBar()
    confirm_btn = Gtk.Button(label=button_label)
    confirm_btn.add_css_class("suggested-action")
    header.pack_end(confirm_btn)
    toolbar_view.add_top_bar(header)

    init_zoom = 14 if (abs(init_lat) > 1e-6 or abs(init_lon) > 1e-6) else 2
    map_widget, _ml, viewport = create_shumate_map(zoom_level=init_zoom, lat=init_lat, lon=init_lon)
    map_widget.set_hexpand(True)

    crosshair = Gtk.Label()
    crosshair.set_markup('<span size="24000" color="#e05050">✚</span>')
    crosshair.set_halign(Gtk.Align.CENTER)
    crosshair.set_valign(Gtk.Align.CENTER)

    overlay = Gtk.Overlay()
    overlay.set_child(map_widget)
    overlay.add_overlay(crosshair)

    toolbar_view.set_content(overlay)
    dialog.set_child(toolbar_view)

    def _on_confirm(*_args):
        on_pick(viewport.get_latitude(), viewport.get_longitude())
        dialog.close()

    confirm_btn.connect("clicked", _on_confirm)
    dialog.connect("closed", lambda _d: deregister_map_widget(map_widget))
    dialog.present(window)


def _add_hover_swap(row, button, badge, get_count, on_long_press):
    """Add hover show/hide behavior: show button and hide badge on hover,
    reverse on leave. Also add long-press handler for touchscreens."""
    motion = Gtk.EventControllerMotion()
    motion.connect("enter", lambda *_: (button.set_visible(True), badge.set_visible(False)))
    motion.connect(
        "leave", lambda *_: (button.set_visible(False), badge.set_visible(get_count() > 0))
    )
    row.add_controller(motion)
    long_press = Gtk.GestureLongPress()
    long_press.connect("pressed", lambda g, x, y: on_long_press())
    row.add_controller(long_press)


_CONTACT_TYPE_ICONS = {
    1: "avatar-default-symbolic",
    2: "network-server-symbolic",
    3: "user-home-symbolic",
    4: "weather-clear-symbolic",
}


class _ContactPickItem(GObject.Object):
    def __init__(self, contact):
        super().__init__()
        self.contact = contact


def _contact_pick_factory_setup(factory, list_item, button_label):
    row = Adw.ActionRow()
    icon = Gtk.Image()
    row.add_prefix(icon)
    btn = Gtk.Button(label=button_label, valign=Gtk.Align.CENTER)
    btn.add_css_class("suggested-action")
    btn.set_visible(False)
    row.add_suffix(btn)
    row._icon = icon
    row._action_btn = btn
    row._action_handler_id = None

    hover = Gtk.EventControllerMotion()
    hover.connect("enter", lambda *_a: btn.set_visible(True))
    hover.connect("leave", lambda *_a: btn.set_visible(False))
    row.add_controller(hover)

    list_item.set_child(row)


def _contact_pick_factory_bind(factory, list_item, on_click, subtitle_func=None):
    row = list_item.get_child()
    contact = list_item.get_item().contact

    row.set_title(GLib.markup_escape_text(contact.name) if contact.name else _("Unknown"))
    subtitle = subtitle_func(contact) if subtitle_func else contact.type_label
    row.set_subtitle(subtitle)
    row._icon.set_from_icon_name(_CONTACT_TYPE_ICONS.get(contact.type, "avatar-default-symbolic"))

    row._action_handler_id = row._action_btn.connect("clicked", lambda _b: on_click(contact))


def _contact_pick_factory_unbind(factory, list_item):
    row = list_item.get_child()
    if row._action_handler_id is not None:
        row._action_btn.disconnect(row._action_handler_id)
        row._action_handler_id = None
