# dialog_add.py.in - dialog to add a new repository
#
#  Copyright (c) 2006 FSF Europe
#
#  Authors:
#       Sebastian Heinlein <glatzor@ubuntu.com>
#
#  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, write to the Free Software
#  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
#  USA

import os
import gi
gi.require_version("Gtk", "4.0")
from gi.repository import GObject, Gtk, Gio, GLib
from gettext import gettext as _
import threading

import softwareproperties.gtk.dialogs as dialogs
from softwareproperties.MirrorTest import MirrorTest
from softwareproperties.gtk.utils import setup_ui
from softwareproperties.CountryInformation import CountryInformation

(COLUMN_PROTO, COLUMN_DIR) = range(2)

class MirrorItem(GObject.GObject):
    __gtype_name__ = 'MirrorItem'

    name = GObject.Property(type=str, default='')

    def __init__(self, name, mirror=None, is_country=False):
        super().__init__()
        self.props.name = name
        self.mirror = mirror
        self.is_country = is_country
        self.children = Gio.ListStore.new(MirrorItem) if is_country else None

class DialogMirror:
    def __init__(self, parent, datadir, distro):
        setup_ui(self, os.path.join(datadir, "gtkbuilder", "dialog-mirror.ui"),
                 domain="software-properties")

        self.dialog = self.dialog_mirror
        self.dialog.set_transient_for(parent)

        self.dialog_test = self.dialog_mirror_test
        self.dialog_test.set_transient_for(self.dialog)

        self.distro = distro
        self._setup_ui_references()
        self._setup_protocol_combo()
        self._setup_mirror_tree()
        self._connect_signals()

    def _setup_ui_references(self):
        self.listview = self.listview_mirrors
        self.button_choose = self.button_mirror_choose
        self.button_cancel = self.button_test_cancel
        self.label_test = self.label_test_mirror
        self.progressbar_test = self.progressbar_test_mirror
        self.combobox = self.combobox_mirror_proto

    def _setup_protocol_combo(self):
        model_proto = Gtk.ListStore(GObject.TYPE_STRING, GObject.TYPE_STRING)
        self.combobox.set_model(model_proto)

        renderer = Gtk.CellRendererText()
        self.combobox.pack_start(renderer, True)
        self.combobox.add_attribute(renderer, "markup", 0)

    def _setup_mirror_tree(self):
        root_store = self._build_mirror_tree()

        self.treelist = Gtk.TreeListModel.new(
            root_store, False, True, self._get_children_func)
        self.selection = Gtk.SingleSelection.new(self.treelist)
        self.listview.set_model(self.selection)

        factory = Gtk.SignalListItemFactory()
        factory.connect("setup", self._on_factory_setup)
        factory.connect("bind", self._on_factory_bind)
        self.listview.set_factory(factory)

    def _build_mirror_tree(self):
        """Build the hierarchical mirror data structure. The mirror data are kept in python-apt-common
        at /usr/share/python-apt/templates/ubuntu.mirrors"""
        country_info = CountryInformation()
        countries = {}  # ISO code -> MirrorItem
        orphaned_mirrors = []

        # Group mirrors by country
        for hostname, mirror in self.distro.source_template.mirror_set.items():
            mirror_item = MirrorItem(hostname, mirror, is_country=False)

            country_code = getattr(mirror, 'location', None)
            if country_code:
                if country_code not in countries:
                    country_name = country_info.get_country_name(country_code)
                    countries[country_code] = MirrorItem(country_name, is_country=True)
                countries[country_code].children.append(mirror_item)
            else:
                orphaned_mirrors.append(mirror_item)

        for country_item in countries.values():
            if len(country_item.children) > 1:
                sorter = Gtk.StringSorter.new(Gtk.PropertyExpression.new(MirrorItem, None, "name"))
                sorted_children = Gtk.SortListModel.new(country_item.children, sorter)
                country_item.children = sorted_children

        root_store = Gio.ListStore.new(MirrorItem)

        for country_item in countries.values():
            root_store.append(country_item)

        for mirror_item in orphaned_mirrors:
            root_store.append(mirror_item)

        root_sorter = Gtk.StringSorter.new(Gtk.PropertyExpression.new(MirrorItem, None, "name"))
        sorted_root = Gtk.SortListModel.new(root_store, root_sorter)

        return sorted_root

    def _get_children_func(self, item):
        """TreeListModel child function"""
        return item.children

    def _on_factory_setup(self, factory, list_item):
        """Setup list item widget hierarchy"""
        expander = Gtk.TreeExpander()
        label = Gtk.Label(halign=Gtk.Align.START)
        expander.set_child(label)
        list_item.set_child(expander)

    def _on_factory_bind(self, factory, list_item):
        """Bind data to list item"""
        expander = list_item.get_child()
        label = expander.get_child()
        row = list_item.get_item()

        expander.set_list_row(row)
        item = row.get_item()
        label.set_text(item.props.name)

    def _connect_signals(self):
        """Connect all signal handlers"""
        self.button_mirror_test.connect("clicked", self._on_test_clicked)
        self.button_cancel.connect("clicked", self._on_cancel_test_clicked)
        self.selection.connect("notify::selected", self._on_selection_changed)

    def _on_selection_changed(self, selection, pspec):
        row = selection.get_selected_item()
        if not row:
            return

        item = row.get_item()
        self._update_protocol_combo(item)

    def _update_protocol_combo(self, item):
        model = self.combobox.get_model()
        model.clear()

        self.button_choose.set_sensitive(False)
        self.combobox.set_sensitive(False)

        # Only enable for actual mirrors (not countries)
        if item.is_country or not item.mirror:
            return

        # Populate protocols
        protocols = set()
        for repo in item.mirror.repositories:
            if repo.proto not in protocols:
                protocols.add(repo.proto)
                model.append(repo.get_info())

        if len(model) > 0:
            self.combobox.set_active(0)
            self.combobox.set_sensitive(True)
            self.button_choose.set_sensitive(True)

    def _get_selected_mirror_url(self):
        row = self.selection.get_selected_item()
        if not row:
            return None

        item = row.get_item()
        if item.is_country or not item.mirror:
            return None

        model = self.combobox.get_model()
        active = self.combobox.get_active()
        if active < 0:
            return None

        iter_proto = model.get_iter_from_string(str(active))
        proto = model.get_value(iter_proto, COLUMN_PROTO)
        directory = model.get_value(iter_proto, COLUMN_DIR)

        return f"{proto}://{item.mirror.hostname}/{directory}"

    def present(self, on_response):
        def _on_response(dialog, response_id):
            self.dialog.hide()
            url = None
            if response_id == Gtk.ResponseType.OK:
                url = self._get_selected_mirror_url()
            if callable(on_response):
                on_response(url)

        self.dialog.connect('response', _on_response)
        self.dialog.present()

    def _on_test_clicked(self, button):
        """Start mirror speed test"""
        self.button_cancel.set_sensitive(True)
        self.dialog_test.present()

        self.running = threading.Event()
        self.running.set()
        progress_update = threading.Event()

        # Get test parameters
        arch = os.popen("dpkg --print-architecture").read().strip()
        test_file = "dists/%s/%s/binary-%s/Packages.gz" % (
            self.distro.source_template.name,
            self.distro.source_template.components[0].name,
            arch,
        )

        # Start test
        mirrors = list(self.distro.source_template.mirror_set.values())
        test = MirrorTest(mirrors, test_file, progress_update, self.running)
        test.start()

        # Monitor progress
        GLib.timeout_add(100, self._update_test_progress, test, progress_update)

    def _update_test_progress(self, test, progress_update):
        if progress_update.is_set():
            progress_text = _("Completed %s of %s tests") % (test.progress[0], test.progress[1])
            self.progressbar_test.set_text(progress_text)
            self.progressbar_test.set_fraction(test.progress[2])
            progress_update.clear()

        if self.running.is_set():
            return True  # Continue updating

        # Test completed
        self.dialog_test.hide()
        self.label_test.set_label("")

        if test.best:
            self._select_best_mirror(test.best)
        else:
            dialogs.show_error_dialog(
                self.dialog,
                _("No suitable download server was found"),
                _("Please check your Internet connection.")
            )
        return False  # Stop updating

    def _select_best_mirror(self, best_hostname):
        for i in range(self.treelist.get_n_items()):
            row = self.treelist.get_item(i)
            item = row.get_item()

            if not item.is_country and item.mirror and item.mirror.hostname == best_hostname:
                self.selection.set_selected(i)
                self._update_protocol_combo(item)
                self.listview.scroll_to(i, Gtk.ListScrollFlags.FOCUS, None)
                return

    def _on_cancel_test_clicked(self, button):
        self.running.clear()
        self.label_test.set_label(f"<i>{_('Canceling...')}</i>")
        self.button_cancel.set_sensitive(False)
        self.progressbar_test.set_fraction(1.0)

    def on_dialog_mirror_test_delete_event(self, dialog, event, data=None):
        self._on_cancel_test_clicked(None)
        return True
