# 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_DARK_STYLE_URL = 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 _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)
    return json.dumps(style)


def _fetch_and_create_dark_source(callback):
    """Fetch the OFM dark style + TileJSON and create a VectorRenderer asynchronously."""
    if 'dark' in _source_cache:
        callback(_source_cache['dark'])
        return
    if 'dark' in _source_failed:
        callback(None)
        return

    def _worker():
        global _tile_urls_cache
        try:
            ua = 'Meshy/1.0 (https://codeberg.org/sesivany/meshy)'
            style_text = urlopen(Request(_OFM_DARK_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('ofm-dark', patched)
                    renderer.set_license('© OpenMapTiles © OpenStreetMap contributors')
                    renderer.set_license_uri('https://www.openstreetmap.org/copyright')
                    _source_cache['dark'] = renderer
                    callback(renderer)
                except Exception:
                    _source_failed.add('dark')
                    callback(None)
                return False

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

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


def _apply_map_source(map_widget, source):
    if source:
        map_widget.set_map_source(source)


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


def _on_theme_changed(*_args):
    dark = _is_dark()
    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
    if dark:
        def _apply_dark(source):
            s = source if source else _get_osm_source()
            for w in widgets:
                _apply_map_source(w, s)

        _fetch_and_create_dark_source(_apply_dark)
    else:
        osm = _get_osm_source()
        for w in widgets:
            _apply_map_source(w, osm)


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:
        map_widget.set_map_source(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))

    if _is_dark():
        def _apply(source):
            if source:
                _apply_map_source(map_widget, source)

        _fetch_and_create_dark_source(_apply)

    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 _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
