# widgets.py
#
# Copyright 2020 Fabio
#
# 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 os import path
import datetime
import html
import math

from dateutil.tz import UTC

from gi.repository import GLib
from gi.repository import GObject
from gi.repository import Gio
from gi.repository import Gtk
from gi.repository import GdkPixbuf
from gi.repository import Handy

from .config import FMT_DAY_LIST


class ListRowHeader(Gtk.VBox):
    def __init__(self, text):
        super().__init__(margin=16)
        label = Gtk.Label("", halign=Gtk.Align.END)
        label.set_markup("<b>{}</b>".format(text))
        sep = Gtk.Separator()
        self.pack_start(label, True, True, 2)
        self.pack_start(sep, False, False, 2)
        self.show_all()


class WebLinkActionRow(Handy.ActionRow):
    def __init__(self, title, url, icontype=None, **kwargs):
        opts = dict(activatable=True, selectable=False)
        opts.update(kwargs)
        super().__init__(**opts)
        self.url = url
        if title is None:
            title = path.basename(url)
        self.set_title(html.unescape(title))
        self.set_subtitle(url)

        #if icontype is None:
        normurl = url.split("#")[0].split("?")[0]
        ftype, _ = Gio.content_type_guess(normurl)
        fmaj, fmin = ftype.split("/")
        icontype = "text/html"
        if fmaj in ('text','video', 'audio', 'image'):
            icontype = ftype
        if fmin == 'pdf' or 'document' in fmin:
            icontype = ftype

        self.content_type = icontype
        ficon = Gio.content_type_get_icon(icontype)

        # I have to manage icon by hand.. ouf
        image = Gtk.Image.new_from_gicon(ficon, Gtk.IconSize.DIALOG)
        self.add_prefix(image)


def weblinkactionrowactivated(listbox, actionrow):
    appinfo = Gio.app_info_get_default_for_type(actionrow.content_type, True)
    if appinfo is not None:
        appinfo.launch_uris([actionrow.url], None)
    else:
        Gio.app_info_launch_default_for_uri(actionrow.url, None)


class EventActionRow(Handy.ActionRow):
    def __init__(self, obj):
        super().__init__(activatable=True, selectable=False)

        subtitle = "{}-{}".format(
            obj.start_in_tz().strftime("%a %H:%M"),
            obj.end_in_tz().strftime("%H:%M"))

        if obj.room:
            subtitle = "{} ({})".format(
                subtitle,
                obj.room)

        if obj.track:
            subtitle = "{} '{}'".format(
                subtitle,
                obj.track)

        ps = list(obj.persons())
        if len(ps) > 0:
            subtitle = "{} - {}".format(
                subtitle,
                ", ".join([str(p) for p in ps]))

        self.set_title(html.unescape(obj.title))
        self.set_subtitle(html.unescape(subtitle))
        self.obj = obj

        obj.connect('update', self.on_update)

        icon_name = "starred-symbolic" if obj.starred else "non-starred-symbolic"
        self.toggle_btn = Gtk.Button.new_from_icon_name(icon_name, Gtk.IconSize.BUTTON)
        self.toggle_btn.set_halign(Gtk.Align.END)
        self.toggle_btn.set_valign(Gtk.Align.CENTER)
        self.toggle_btn.connect("clicked", self.toggle_starred)
        self.add(self.toggle_btn)

        Gio.Application.get_default().connect(
            "tick", lambda *_: self.queue_draw()
        )

        self.connect("draw", self._on_draw)

    def _roundedclippath(self, cr, x, y, w, h, rtop, rbottom):
        cr.arc(x+rtop, y+rtop, rtop, math.pi, 3*math.pi/2)
        cr.line_to(x+w, y)
        cr.line_to(x+w, y+h)
        cr.line_to(x+w, y+h)
        #cr.arc(x+w-rtop, y+rtop, rtop, 3*math.pi/2, 0) # top-right round corner
        #cr.arc(x+w-rbottom, y+h-rbottom, rbottom, 0, math.pi/2) # bottom-right
        cr.arc(x+rbottom, y+h-rbottom, rbottom, math.pi/2, math.pi)
        cr.close_path()
        cr.clip()

    def _on_draw(self, widget, cr):
        """
        Draw event progress indicator in widget background.
        It's a tick line on left border from top whose lenght is relative to the
        percentage of event time elapsed.

        We need to handle list round corners. Gtk doesn't clip element like HTML
        when rendering with "border-radius" value, so our line will overflow the
        corner.

        I've tried some way to do this:
        -   Drawin a clip path with a rounder corner, getting the rounding from css,
            defining a css rule like "row:first-child { border-top-left-radius: 8px; }"
            but we can't fetch "border-top-left-radius" value via pygobject
            (should be `self.get_style_context().get_property("border-top-left-radius", self.get_style_context().get_state())`
            but we get an exception, I think because it should return a GtkCssValue which pygobject doesn't know)
        -   I tought I could save current style context, add a new class, then render
            the backgrund with `Gtk.render_background()` wich can clip the draw, then
            restore the style context.
            I would then define the style matching the added class defining rounding
            and color, but it doesn't work.

        So now the row get a "first" or "last" class in `pages.BaseListPage.update_list()`.
        Color and corder radii are defined here in code instead of css.
        I use the clip path method. It get a top rounded corner if `self` has "first" class
        and has no header: a list header is rendered before the row itself, thus
        occupying the rounded corner spot. It get a bottom rounded corner if has
        "last" class. The a line of required lenght, color and width is drawn
        from top.
        """

        now = datetime.datetime.now(UTC).replace(tzinfo=None)
        obj = self.obj

        totalh = self.get_allocated_height()

        dur = (obj.end.timestamp() - obj.start.timestamp())
        pos = (now.timestamp() - obj.start.timestamp())
        if dur > 0:
            prc = min(max(pos/dur, 0.0), 1.0)
        else:
            # not a valid event duration
            # set prc to 0 if the event is in the future or 1 if in the past
            prc = int(pos >= 0)

        x = 0
        y = 0
        h = totalh * prc
        w = 10

        # clip at rounded corners
        stylectx = self.get_style_context()
        rtop = 8 if stylectx.has_class("first") and self.get_header() is None else 0
        rbottom = 8 if stylectx.has_class("last") else 0

        cr.save()
        # rounded clip path
        self._roundedclippath(cr, x, y-1, w, totalh+2, rtop, rbottom)

        # draw line
        cr.set_source_rgba(0,0,0,0.2)
        cr.set_line_width(w)
        cr.move_to(x, y)
        cr.line_to(x, h)
        cr.stroke()

        cr.restore()

    def on_update(self, *args):
        icon_name = "starred-symbolic" if self.obj.starred else "non-starred-symbolic"
        self.toggle_btn.get_image().set_from_icon_name(icon_name, Gtk.IconSize.BUTTON)

    def toggle_starred(self, button):
        self.obj.set_star(not self.obj.starred)


class MenuBoxRow(Gtk.ListBoxRow):
    def __init__(self, title, icon):
        super().__init__()
        box = Gtk.HBox(margin=16)
        lbl = Gtk.Label(title)
        icn = Gtk.Image.new_from_icon_name(icon, Gtk.IconSize.BUTTON )
        box.pack_start(icn, False, True, 0)
        box.pack_start(lbl, False, True, 8)
        self.add(box)


class PlaceholderWidget(Gtk.VBox):
    def __init__(self, title=None, text=None, icon_name='net.kirgroup.confy'):
        super().__init__(valign = Gtk.Align.CENTER)
        img = Gtk.Image.new_from_pixbuf(
            Gtk.IconTheme.get_default().load_icon(
                icon_name, 128, 0))
        self.pack_start(img, False, False, 16)

        LBL_PROPS = {
            'justify': Gtk.Justification.CENTER,
            'halign' : Gtk.Align.CENTER,
            'wrap' : True,
        }

        if title is not None:
            label = Gtk.Label(**LBL_PROPS)
            label.set_markup(_("<b>{}</b>").format(title))
            self.pack_start(label, False, False, 8)

        if text is not None:
            label = Gtk.Label(**LBL_PROPS)
            label.set_markup("<small>{}</small>".format(text))
            label.get_style_context().add_class("dim-label")
            self.pack_start(label, False, False, 8)

        self.show_all()


class LoadingWidget(Handy.Clamp):
    def __init__(self, fetcher):
        super().__init__(maximum_size=400)
        box = Gtk.VBox(valign = Gtk.Align.CENTER)
        img = Gtk.Image.new_from_pixbuf(
            Gtk.IconTheme.get_default().load_icon(
                'net.kirgroup.confy', 256, 0))
        self.pb = pb = Gtk.ProgressBar()
        label = Gtk.Label()
        label.set_markup(_("<b>Loading…</b>"))
        button = Gtk.Button("Cancel")

        fetcher.connect("notify", lambda *args: pb.set_fraction(fetcher.props.fraction))
        button.connect("clicked", lambda *args: fetcher.cancel())

        box.pack_start(img, False, False, 16)
        box.pack_start(label, False, False, 8)
        box.pack_start(pb, False, False, 8)
        box.pack_start(button, False, False, 16)
        self.add(box)

        GLib.timeout_add(200, self._do_pulse)

    def _do_pulse(self,*args):
        if self.pb.get_fraction() > 0:
            return False
        self.pb.pulse()
        return True


class ErrorWidget(Gtk.VBox):
    def __init__(self, msg, error):
        super().__init__()
        self.set_valign(Gtk.Align.CENTER)
        img = Gtk.Image.new_from_icon_name('dialog-warning-symbolic', Gtk.IconSize.DIALOG )
        lbl1 = Gtk.Label()
        lbl1.set_markup("<b>{}</b>".format(msg))
        lbl2 = Gtk.Label(str(error))
        lbl2.get_style_context().add_class("dim-label")

        self.pack_start(img, False, False, 16)
        self.pack_start(lbl1, False, False, 0)
        self.pack_start(lbl2, False, False, 0)


class NotificationOverlay(Gtk.Revealer):
    def __init__(self):
        super().__init__(halign=Gtk.Align.CENTER, valign=Gtk.Align.START)
        box = Gtk.HBox(spacing=20)
        box.get_style_context().add_class("app-notification")
        label = Gtk.Label("")
        button = Gtk.Button.new_from_icon_name("window-close-symbolic", Gtk.IconSize.BUTTON)
        box.pack_start(label,False, True, 0)
        box.pack_start(button, False, False, 0)
        self.add(box)

        button.connect("clicked", self.close)
        self.label = label

        self.show_all()

    def show(self, text):
        self.label.set_markup(text)
        self.set_reveal_child(True)

    def close(self, *args):
        self.set_reveal_child(False)


class LoadingOverlay(Gtk.Revealer):
    def __init__(self):
        super().__init__(halign=Gtk.Align.CENTER, valign=Gtk.Align.START)
        box = Gtk.HBox(spacing=20)
        box.get_style_context().add_class("app-notification")
        spinner = Gtk.Spinner.new()
        spinner.start()
        label = Gtk.Label(_("Loading…"), margin_right=16)
        button = Gtk.Button.new_from_icon_name("process-stop-symbolic", Gtk.IconSize.BUTTON)
        box.pack_start(spinner,False, False, 0)
        box.pack_start(label,False, True, 0)
        box.pack_start(button, False, False, 0)
        self.add(box)

        button.connect("clicked", self.cancel)

        self.cancellable = None
        self.show_all()

    def show(self,cancellable):
        self.cancellable = cancellable
        if cancellable is not None:
            self.cancellable.connect("done", self.close)
            self.cancellable.connect("error", self.close)
        self.set_reveal_child(True)

    def cancel(self, *args):
        if self.cancellable is not None:
            self.cancellable.cancel()
        self.close()

    def close(self, *args):
        self.cancellable = None
        self.set_reveal_child(False)


class SearchBarOverlay(Handy.SearchBar):
    def __init__(self, **kwargs):
        opts = dict(
            halign = Gtk.Align.FILL,
            valign = Gtk.Align.START,
            show_close_button = True
        )
        opts.update(kwargs)
        super().__init__(**opts)
        clamp  = Handy.Clamp()
        self.entry = Gtk.SearchEntry(can_focus=True)
        clamp.add(self.entry)
        self.add(clamp)
        self.show_all()

        self.connect('notify::search-mode-enabled', self._on_search_mode_changed)

    def _on_search_mode_changed(self, *args):
        if self.get_search_mode():
            self.entry.set_text("")
            self.entry.grab_focus()

class OkCancelDialog(Gtk.Dialog):
    CANCEL = 0
    OK = 1
    title = "Dialog"

    def __init__(self, parent, **kwargs):
        opts = dict(
            transient_for=parent,
            can_focus=False, modal=True,
            use_header_bar=True,
            default_height=20,
            destroy_with_parent=True
        )
        opts.update(kwargs)
        super().__init__(**opts)

        self._btn_cancel = cancel = Gtk.Button(name="cancel_button", label=_("Cancel"), receives_default=True)
        self._btn_ok = ok = Gtk.Button(name="done_button", label=_("Ok"), receives_default=True)
        ok.get_style_context().add_class("suggested-action")

        cancel.connect("clicked", self._do_cancel)
        ok.connect("clicked", self._do_ok)

        hb = self.get_header_bar()
        hb.pack_start(cancel)
        hb.pack_end(ok)
        hb.set_show_close_button(False)
        hb.set_title(self.title)

        self.setup_ui()
        self.show_all()

    def _do_cancel(self, *args):
        self.response(self.CANCEL)

    def _do_ok(self, *args):
        self.response(self.OK)

    def setup_ui(self):
        ...


class InputDialog(OkCancelDialog):
    title = _("Schedule URL")

    def setup_ui(self):
        self._input = Gtk.Entry()
        self.get_content_area().add(self._input)

    def get_text(self):
        return self._input.get_text()


ACTION_ROW_PROPS = dict(
    selectable=False,
    activatable=True,
)

ACTION_ROW_ENTRY_PROPS = dict(
    valign=Gtk.Align.CENTER,
    halign=Gtk.Align.FILL,
    vexpand=False,
    hexpand=True,
)


class ConferenceEditDialog(OkCancelDialog):
    DELETE=3
    title = _("Edit Conference")

    def __init__(self, parent, conf, can_cancel=False, can_delete=False):
        self._err = set()
        self.conf = conf
        self._can_delete = can_delete
        super().__init__(parent, default_height=355)
        if not can_cancel:
            self._btn_cancel.destroy()

    def get_conf(self):
        return self.conf

    def _do_delete(self, *args):
        self.response(self.DELETE)

    def _do_ok(self, *args):
        self.conf.url = self._entry_url.get_text()
        self.conf.title = self._entry_title.get_text()
        self.conf.start = self._entry_start.get_date()
        self.conf.end = self._entry_end.get_date()
        if self._entry_icn:
            if self._entry_icn.get_text() != "":
                self.conf.metadata['icon'] = self._entry_icn.get_text()
            elif 'icon' in self.conf.metadata:
                del self.conf.metadata['icon']
        self.response(self.OK)

    def _validate_entry_required(self, entry):
        if len(entry.get_text()) > 0:
            entry.get_style_context().remove_class("error")
            if entry in self._err:
                self._err.remove(entry)
        else:
            entry.get_style_context().add_class("error")
            self._err.add(entry)
        self._update_ok()

    def _validate_date_order(self, entry):
        if self._entry_start.get_date() > self._entry_end.get_date():
            entry.get_style_context().add_class("error")
            self._err.add(entry)
        else:
            for e in (self._entry_start, self._entry_end):
                e.get_style_context().remove_class("error")
                if e in self._err:
                    self._err.remove(e)
        self._update_ok()

    def _update_ok(self):
        self._btn_ok.set_sensitive(len(self._err) == 0)

    def setup_ui(self):
        scroll = Gtk.ScrolledWindow(
            hexpand=True,
            vexpand=True,
            kinetic_scrolling=True,
            hscrollbar_policy=Gtk.PolicyType.NEVER)

        column = Handy.Clamp(
            margin=16,
            maximum_size=600,
            tightening_threshold=400)

        box = Gtk.VBox(valign=Gtk.Align.START, spacing=8)

        listbox = Gtk.ListBox()
        listbox.get_style_context().add_class("content")

        self._entry_url = entry = Gtk.Entry(text=self.conf.url, editable=False, **ACTION_ROW_ENTRY_PROPS)
        entry.connect("changed", self._validate_entry_required)
        self._validate_entry_required(entry)
        row = Handy.ActionRow(title=_("URL"), **ACTION_ROW_PROPS)
        row.add(entry)
        row.set_activatable_widget(entry)
        listbox.add(row)

        self._entry_title = entry = Gtk.Entry(text=self.conf.title, **ACTION_ROW_ENTRY_PROPS)
        entry.connect("changed", self._validate_entry_required)
        self._validate_entry_required(entry)
        row = Handy.ActionRow(title=_("Title"), **ACTION_ROW_PROPS)
        row.add(entry)
        row.set_activatable_widget(entry)
        listbox.add(row)

        self._entry_start = entry = DatePickerButton(date=self.conf.start, **ACTION_ROW_ENTRY_PROPS)
        entry.get_style_context().add_class("list-button")
        entry.connect("changed", self._validate_date_order)
        row = Handy.ActionRow(title=_("Start"), **ACTION_ROW_PROPS)
        row.add(entry)
        row.set_activatable_widget(entry)
        listbox.add(row)

        self._entry_end = entry = DatePickerButton(date=self.conf.end, **ACTION_ROW_ENTRY_PROPS)
        entry.get_style_context().add_class("list-button")
        entry.connect("changed", self._validate_date_order)
        self._validate_date_order(entry)
        row = Handy.ActionRow(title=_("End"), **ACTION_ROW_PROPS)
        row.add(entry)
        row.set_activatable_widget(entry)
        listbox.add(row)

        if self.conf.metadata is not None:
            self._entry_icn = entry = Gtk.Entry(text=self.conf.metadata.get('icon',''), **ACTION_ROW_ENTRY_PROPS)
            row = Handy.ActionRow(title=_("Icon URL"), **ACTION_ROW_PROPS)
            row.add(entry)
            row.set_activatable_widget(entry)
            listbox.add(row)
        else:
            self._entry_icn = None

        box.add(listbox)

        if self._can_delete:
            btn_delete = Gtk.Button(
                name="delete_button", label=_("Delete"),
                valign=Gtk.Align.START,
                halign=Gtk.Align.END)
            btn_delete.get_style_context().add_class("destructive-action")
            btn_delete.connect("clicked", self._do_delete)
            box.add(btn_delete)

        column.add(box)
        scroll.add(column)
        self.get_content_area().add(scroll)


class DatePickerButton(Gtk.Button):
    @GObject.Signal
    def changed(self):
        ...

    def __init__(self, date, **kwargs):
        super().__init__(**kwargs)
        self._is_popup = False

        self.pop = Gtk.Popover.new(self)
        self.cal = Gtk.Calendar()
        self.cal.show_all()
        self.pop.add(self.cal)

        self.pop.connect("closed", self._cal_closed)
        self.connect("clicked", self._toggle_cal)
        self.set_date(date)

    def get_date(self):
        return self.date

    def get_datetime(self):
        return datetime.datetime.combine(self.date, datetime.time.min)

    def set_date(self, date):
        self.date = date
        if date is None:
            lbl = ""
            date = datetime.date.today()
        else:
            lbl = str(date)

        self.cal.select_month(date.month-1, date.year)
        self.cal.select_day(date.day)
        self.set_label(lbl)
        self.emit("changed")

    def _cal_closed(self, *args):
        date = self.cal.get_date()
        date = datetime.date(year=date.year, month=date.month+1, day=date.day)
        self.set_date(date)

    def _toggle_cal(self, *args):
        if self._is_popup:
            self.pop.popdown()
        else:
            self.pop.popup()
        self._is_popup = not self._is_popup


class SimpleScalableImage(Gtk.Image):

    @GObject.property(type=float)
    def scale(self):
        return self._scale

    @scale.setter
    def scale(self, value):
        self._scale = value
        pixbuf = self._originalpb.scale_simple(
            self._w * value,
            self._h * value,
            GdkPixbuf.InterpType.BILINEAR)
        super().set_from_pixbuf(pixbuf)


    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.gesture = Gtk.GestureZoom.new(self)
        self.gesture.set_propagation_phase(Gtk.PropagationPhase.BUBBLE)
        self.gesture.connect("scale-changed", self.on_scale_changed)

    def on_scale_changed(self, gesture, scale):
        self.scale_by(scale)

    def scale_by(self, scale):
        self.scale = self.scale * scale

    def set_from_pixbuf(self, pixbuf):
        self._scale = 1
        self._w = pixbuf.get_width()
        self._h = pixbuf.get_height()
        self._originalpb = pixbuf
        super().set_from_pixbuf(pixbuf)


class MaxSizedImage(Gtk.Image):
    def __init__(self, max_width, max_height, **kwargs):
        super().__init__(**kwargs)
        self.max_width = max_width
        self.max_height = max_height

    def set_from_pixbuf(self, pixbuf):
        w = pixbuf.get_width()
        h = pixbuf.get_height()
        r = w/h
        if r>=1 and w > self.max_width:
            w = self.max_width
            h = w / r
            pixbuf = pixbuf.scale_simple(
                w,
                h,
                GdkPixbuf.InterpType.BILINEAR)
        if r<1 and h > self.max_height:
            h = self.max_height
            w = h * r
            pixbuf = pixbuf.scale_simple(
                w,
                h,
                GdkPixbuf.InterpType.BILINEAR)
        super().set_from_pixbuf(pixbuf)

