# mainwindow.py
#
# Copyright 2020-2024 Fabio Comuni, et al.
#
# 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 3 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 typing import Optional

import json
from time import time
from gettext import gettext as _

from gi.repository import GLib
from gi.repository import Gio
from gi.repository import GObject
from gi.repository import Gtk
from gi.repository import Adw
from gi.repository import Notify

from . import local
from . import models
from .settings import Settings
from .openwindow import OpenWindow
from .config import APP_NAME, APP_ID  # type:ignore # config is generated at compile time
from .networkconnectionmonitor import NetworkConnectionMonitor
from .widgets import LoaderPage
from .widgets import Navbar
from .widgets.navbar import NavbarItem
from .pages import PageInfo
from .pages import PageList
from .pages import PageEvents
from .utils import clean_markup

PAGES = dict(
    info=NavbarItem(name="info", label=_("Event info"), icon="emblem-system-symbolic"),
    days=NavbarItem(name="days", label=_("Days"), icon="x-office-calendar-symbolic"),
    tracks=NavbarItem(name="tracks", label=_("Tracks"), icon="format-justify-fill-symbolic"),
    rooms=NavbarItem(name="rooms", label=_("Rooms"), icon="display-projector-symbolic"),
    starred=NavbarItem(name="starred", label=_("Starred"), icon="starred-symbolic", group=1)
)


@Gtk.Template(resource_path="/net/kirgroup/confy/mainwindow.ui")
class MainWindow(Adw.ApplicationWindow):
    __gtype_name__ = "ConfyMainWindow"

    @GObject.Signal
    def conf_updated(self):
        pass

    @GObject.Property(type=bool, default=False)
    def has_recent(self):
        return self.recent_model.get_n_items() > 0

    overlay: Adw.ToastOverlay = Gtk.Template.Child()  # type: ignore
    # stack to switch between 'start', 'loader' and 'main' views
    loader_stack: Gtk.Stack = Gtk.Template.Child()  # type: ignore
    # the loader widget inside 'loader' view
    loader: LoaderPage = Gtk.Template.Child()  # type: ignore

    # main split view
    main_split_view: Adw.OverlaySplitView = Gtk.Template.Child()  # type: ignore

    # sidebar navbar
    navbar: Navbar = Gtk.Template.Child()  # type: ignore

    # main navigation view
    main_stack: Adw.NavigationView = Gtk.Template.Child()  # type: ignore

    # recent confs model and menu, in 'start' page
    recent_model: Gtk.NoSelection = Gtk.Template.Child()  # type: ignore
    recent_menu: Gio.MenuItem = Gtk.Template.Child()  # type: ignore

    # Monitor network connectivity
    nm = NetworkConnectionMonitor()

    # current opened conference
    conf: Optional[models.Conference] = None

    # dict of sent notifications.
    # Keeps Notification ojects around to be able to get activation events
    notifications: dict[str, Notify.Notification] = {}

    def __init__(self, application: Gtk.Application, **kwargs):
        super().__init__(application=application, **kwargs)

        size = Settings.instance().get_size()
        self.set_default_size(*size)

        # Check if there are notifications to send
        application.connect('tick', self.check_event_notification)

        # handle conf data updating, eg. reloading from remote
        self.connect('conf-updated', self._on_conf_updated)

        self._add_action("open-conference", None, ["<Primary>o",], self.open_conference)
        self._add_action("search", None, ["<Primary>f",], lambda *_: self.show_search_all())
        self._add_action("copy", None, ["<Primary>c",], lambda *_: self.copy_details_to_clipboard())
        # open recent action, used by recent menu
        self._add_action("open-recent", "i", None, self._on_recent_action)

        # TODO: serve?
        self.nm.connect("notify", self._on_networkconnectionmonitor_notify)

        model = Gio.ListStore.new(models.RecentConf)
        self.recent_model.set_model(model)

        self.recentmanager = Gtk.RecentManager.get_default()
        self.recentmanager.connect("changed", self._on_recent_changed)
        self._on_recent_changed()

    def _add_action(self, name, parameter_type, accels, activate_cbk):
        """Simplify adding an action to the window"""
        if parameter_type is not None:
            parameter_type = GLib.VariantType.new(parameter_type)
        action = Gio.SimpleAction.new(name, parameter_type)
        action.connect("activate", activate_cbk)
        self.add_action(action)
        if accels is not None:
            self.get_application().set_accels_for_action(F"win.{name}", accels)

        return action

    def _on_networkconnectionmonitor_notify(self, *_):
        # TODO: what should be here?
        ...

    def _on_recent_changed(self, *args):
        # empty recent confs model and menu...
        model = self.recent_model.get_model()
        menu = self.recent_menu
        menu.remove_all()
        while model.get_n_items() > 0:
            model.remove(0)

        # ... then rebuild model and menu
        n = 0
        for item in self.recentmanager.get_items():
            if item.has_application(APP_NAME):
                print(f"#{n} {item.get_uri()} {item.get_applications()}")
                recentconf = models.RecentConf(item)
                model.append(recentconf)
                # add menu item, trigger 'open-recent' action
                menu.append(recentconf.props.title, f"win.open-recent({n})")
                n = n + 1
        self.notify("has-recent")

    @Gtk.Template.Callback()
    def _on_recent_activated(self, listview, index):
        """Called by recent listview 'activated' signal"""
        recentconf = self.recent_model.get_item(index)
        conf = json.loads(recentconf.data)
        conf = models.Conference()._init(conf)
        # don't load conf if it is already loaded
        if self.conf is not None and self.conf.url == conf.url:
            print(f"Same url {self.conf.url} == {conf.url}. Skip")
            return
        self.load_conf(conf)

    def _on_recent_action(self, action, args):
        """
        Called by open-recent action activation

        first arg from action is int32 index in recent model
        """
        index = args.get_int32()
        self._on_recent_activated(None, index)

    def _add_recent(self, conf: models.Conference):
        """
        Add conference to recent.
        """

        # remove current conf if is already in recents
        # this way will be readded on top
        # TODO: is there a better way to do this?
        if self.recentmanager.has_item(conf.url):
            self.recentmanager.remove_item(conf.url)

        recentdata = Gtk.RecentData()
        recentdata.mime_type = "x-application/confy-event-uri"
        recentdata.app_exec = "confy"
        recentdata.is_private = True
        recentdata.app_name = APP_NAME
        recentdata.display_name = conf.title
        # misuse recentdata description to hold conf object, yay!
        recentdata.description = json.dumps(conf.to_json())

        self.recentmanager.add_full(conf.url, recentdata)

    def _send_desktop_notification(self, title: str, body: str, nid: str):
        """
        Send a notification and register it to react to user action
        """
        n = Notify.Notification.new(title, body, APP_ID)
        n.set_timeout(Notify.EXPIRES_NEVER)
        n.add_action("view", "View", self._on_notification_action, nid)
        n.show()
        self.notifications[nid] = n

    def _on_conf_updated(self, *args):
        """
        Callback when conference data is modified (eg. is reloaded)
        """

        self.navbar.set_item_cond(PAGES['info'], True)
        self.navbar.set_item_cond(PAGES['days'], models.Day.count() > 0)
        self.navbar.set_item_cond(PAGES['tracks'], models.Track.count() > 0)
        self.navbar.set_item_cond(PAGES['rooms'], models.Room.count() > 0)
        self.navbar.set_item_cond(PAGES['starred'], True)

    def _on_fetcher_done(self, *_):
        """
        Callback when conference has been open from remote url
        """
        self._add_recent(self.conf)
        self.emit("conf_updated")
        self.show_main()

    def _on_fetcher_error(self, fetcher, error):
        """
        Callback when there was an error fetching conference from remote url
        """
        self.show_error(error)

    def _on_notification_action(self, notification, action, data):
        """
        User clicked on "View" in notification
        """
        print(notification, action, data)
        if action == "view" and data.startswith("nextup-"):
            self.present_with_time(time())
            eventid = int(data.split("-")[1])
            event = models.Event.by_id(eventid)
            # if we are not on "events" subpage, force it as non navigatable,
            # we want to be back to where we were
            print(self.subpage_leaflet.get_visible_child(), self.subpage_events)
            can_we_go_back_to_events = self.subpage_leaflet.get_visible_child() == self.subpage_events
            self.show_details(event, can_we_go_back_to_events)

    @Gtk.Template.Callback()
    def _on_page_changed(self, navbar):
        self.navigate_to(navbar.get_current_item().name)

    @Gtk.Template.Callback()
    def _on_close_requested(self, *args):
        print("close-request", args)

    def open_conference(self, *_):
        """
        Show open window to open a conference
        """
        app = self.get_application()
        win = OpenWindow(modal=True, transient_for=self, application=app)
        win.show()

    def load_conf(self, conf: models.Conference):
        """
        Load conference

        Setup/reset window to display `conf`
        """
        self.conf = conf
        # we reset navigation because 'conf-updated' is emited, but
        # we don't really have a clear way to get the updated conf in pages
        # as we cannot attach a conf to the signal, as is not a GObject.
        # So existing pages will update values but with old conf object.
        # Reset navigation will force to create a new PageInfo with the new
        # conf object
        self.navigate_to(None)  # reset navigation

        self.show_loader()
        try:
            fetcher = local.openconf(conf, is_online=self.nm.get_isconnected())
        except Exception as e:
            self.show_error(e)
            return
        else:
            if fetcher is not None:
                # 'local.openconf' returns a `Fetcher` if there is no local cache
                # or the local cache is expired, and data must be downloaded
                # we listen to fetcher events
                fetcher.connect("done", self._on_fetcher_done)
                fetcher.connect("error", self._on_fetcher_error)
                # pass the fetcher to the loader to show progress
                self.loader.set_fetcher(fetcher)
            else:
                # there was a valid local cache, signal change in conf,
                # and show main page
                self._add_recent(self.conf)
                self.emit("conf-updated")
                self.show_main()

        self.notifications = {}
        self.check_event_notification()

    def check_event_notification(self, *args):
        """
        Notifiy starred talk staring in next 5 minutes
        """
        if self.conf is None:
            # no conf loaded, nothing to do
            return
        nextup = list(models.Event.nextup(5))
        for e in nextup:
            title = clean_markup(e.title)
            self._send_desktop_notification(
                _("Next up: {}").format(title),
                _("at {} in {}").format(
                    e.start_in_tz().strftime("%H:%M"),
                    e.room
                ),
                f"nextup-{e.id}")
            e.set_notified(True)

    def show_error(self, error=None):
        msg = Adw.MessageDialog(
            transient_for=self,
            modal=True,
            heading=_("Error loading event"),
            body=str(error) if error else "",
        )
        msg.add_response("ok", _("Ok"))
        msg.connect("response", self.open_conference)
        msg.present()

    def show_start(self):
        """Show 'start' page"""
        self.loader_stack.set_visible_child_name("start")

    def show_loader(self):
        """Show 'loader' page"""
        self.loader_stack.set_visible_child_name("loader")

    def show_main(self):
        """Show 'main' page"""
        self.navigate_to("info")
        self.loader_stack.set_visible_child_name("main")

    def navigate_to(self, name: Optional[str]):
        print("Navigate to", name)
        if name is None:
            self.main_stack.replace([Adw.NavigationPage()])
            return

        # we already into this navigation path?
        top = self.main_stack.find_page(name)
        if top is not None:
            self.main_stack.replace_with_tags([name])
            if self.main_split_view.get_collapsed():
                self.main_split_view.set_show_sidebar(False)
            return

        # no, start anew
        child: Adw.NavigationPage
        match name:
            case "info":
                info = PAGES[name]
                child = PageInfo(self, self.conf, title=info.label, tag=name)
            case "starred" | "search":
                child = PageEvents(None, model=name, tag=name)
            case _:
                info = PAGES[name]
                child = PageList(self, self.conf, model=name, title=info.label, tag=name)

        self.main_stack.replace([child])
        if self.main_split_view.get_collapsed():
            self.main_split_view.set_show_sidebar(False)

    def copy_details_to_clipboard(self):
        page = self.main_stack.get_visible_page()
        if page and page.get_tag() == "details":
            page.copy_to_clipboard()
            self.notification_show(_("Event details copied to the clipboard"))

    def notification_show(self, text):
        t = Adw.Toast()
        t.set_title(text)
        self.overlay.add_toast(t)

    def show_search_all(self):
        self.navigate_to("search")
