# SPDX-FileCopyrightText: 2019-2026 Valéry Febvre
# SPDX-License-Identifier: GPL-3.0-or-later
# Author: Valéry Febvre <vfebvre@easter-eggs.com>

from gettext import gettext as _
import shutil

from gi.repository import Adw
from gi.repository import Gdk
from gi.repository import Gio
from gi.repository import GLib
from gi.repository import Gtk

from komikku.consts import BORDERS_CROP_THRESHOLDS
from komikku.models import Settings
from komikku.reader.controls import Controls
from komikku.reader.pager import Pager
from komikku.reader.pager.webtoon import WebtoonPager
from komikku.utils import get_file_mime_type


@Gtk.Template.from_resource('/info/febvre/Komikku/ui/reader.ui')
class ReaderPage(Adw.NavigationPage):
    __gtype_name__ = 'ReaderPage'

    headerbar_revealer = Gtk.Template.Child('headerbar_revealer')
    back_button = Gtk.Template.Child('back_button')
    title = Gtk.Template.Child('title')
    fullscreen_button = Gtk.Template.Child('fullscreen_button')
    menu_button = Gtk.Template.Child('menu_button')

    overlay = Gtk.Template.Child('reader_overlay')

    manga = None
    init_chapter = None
    chapters_consulted = None
    pager = None

    def __init__(self, window):
        Adw.NavigationPage.__init__(self)

        self.window = window
        self.builder = window.builder
        self.builder.add_from_resource('/info/febvre/Komikku/ui/menu/reader.xml')

        self.connect('hidden', self.on_hidden)
        self.connect('shown', self.on_shown)
        self.window.connect('notify::fullscreened', self.on_fullscreen_state_changed)
        self.window.controller_key.connect('key-pressed', self.on_key_pressed)

        # Double maximum distance allowed between two clicks (default 5)
        # Allows to zoom more easily (double tap) on touch screen
        Gtk.Settings.get_default().set_property('gtk-double-click-distance', 10)

        # Header bar
        self.back_button.connect('clicked', self.on_back_button_clicked)
        self.fullscreen_button.connect('clicked', self.window.toggle_fullscreen, None)
        self.menu_button.set_menu_model(self.builder.get_object('menu-reader'))
        # Focus is lost after showing popover submenu (bug?)
        self.menu_button.get_popover().connect('closed', lambda _popover: self.menu_button.grab_focus())

        # Page numbering
        self.page_numbering_defined = False
        self.page_numbering_label = Gtk.Label(halign=Gtk.Align.CENTER, can_focus=False, can_target=False)
        self.page_numbering_label.add_css_class('reader-page-number-indicator-label')
        self.page_numbering_label.set_valign(Gtk.Align.END)
        self.overlay.add_overlay(self.page_numbering_label)

        # Controls
        self.controls = Controls(self)

        # Settings dialog
        self.settings_dialog = SettingsDialog(self)

        self.window.navigationview.add(self)

    @property
    def background_color(self):
        return self.manga.background_color or Settings.get_default().background_color

    @property
    def borders_crop(self):
        if self.reading_mode == 'webtoon':
            # Borders crop is not an option in Webtoon reading mode
            # Ignore settings and return False
            return False

        if self.manga.borders_crop in (0, 1):
            return bool(self.manga.borders_crop)

        return Settings.get_default().borders_crop

    @property
    def borders_crop_threshold(self):
        return self.manga.borders_crop_threshold or Settings.get_default().borders_crop_threshold

    @property
    def landscape_zoom(self):
        if self.reading_mode == 'webtoon':
            # Landscape zoom is not an option in Webtoon reading mode
            # Ignore settings and return False
            return False

        if self.manga.landscape_zoom in (0, 1):
            return bool(self.manga.landscape_zoom)

        return Settings.get_default().landscape_zoom

    @property
    def page_numbering(self):
        if self.manga.page_numbering in (0, 1):
            return bool(self.manga.page_numbering)

        return Settings.get_default().page_numbering

    @property
    def reading_mode(self):
        return self.manga.reading_mode or Settings.get_default().reading_mode

    @property
    def scaling(self):
        if self.reading_mode == 'webtoon':
            # Scaling is not an option in Webtoon reading mode
            # Ignore settings and return 'width'
            return 'width'

        return self.manga.scaling or Settings.get_default().scaling

    @property
    def scaling_filter(self):
        return self.manga.scaling_filter or Settings.get_default().scaling_filter

    @property
    def size(self):
        width = self.window.get_width()
        height = self.window.get_height()

        if self.headerbar_revealer.get_child_revealed():
            height -= self.get_child().get_top_bar_height()

        return (width, height)

    def add_accelerators(self):
        self.window.application.set_accels_for_action('app.reader.save-page', ['<Primary>s'])

    def add_actions(self):
        # Reading mode
        variant = GLib.Variant.new_string('right-to-left')
        self.reading_mode_action = Gio.SimpleAction.new_stateful('reader.reading-mode', variant.get_type(), variant)
        self.reading_mode_action.connect('activate', self.on_reading_mode_changed)
        self.window.application.add_action(self.reading_mode_action)

        # Scaling
        variant = GLib.Variant.new_string('screen')
        self.scaling_action = Gio.SimpleAction.new_stateful('reader.scaling', variant.get_type(), variant)
        self.scaling_action.connect('activate', self.on_scaling_changed)
        self.window.application.add_action(self.scaling_action)

        # Scaling filter
        variant = GLib.Variant.new_string('linear')
        self.scaling_filter_action = Gio.SimpleAction.new_stateful('reader.scaling-filter', variant.get_type(), variant)
        self.scaling_filter_action.connect('activate', self.on_scaling_filter_changed)
        self.window.application.add_action(self.scaling_filter_action)

        # Landscape Pages Zoom
        self.landscape_zoom_action = Gio.SimpleAction.new_stateful('reader.landscape-zoom', None, GLib.Variant('b', False))
        self.landscape_zoom_action.connect('change-state', self.on_landscape_zoom_changed)
        self.window.application.add_action(self.landscape_zoom_action)

        # Background color
        variant = GLib.Variant.new_string('white')
        self.background_color_action = Gio.SimpleAction.new_stateful('reader.background-color', variant.get_type(), variant)
        self.background_color_action.connect('activate', self.on_background_color_changed)
        self.window.application.add_action(self.background_color_action)

        # Borders crop
        self.borders_crop_action = Gio.SimpleAction.new_stateful('reader.borders-crop', None, GLib.Variant('b', False))
        self.borders_crop_action.connect('change-state', self.on_borders_crop_changed)
        self.window.application.add_action(self.borders_crop_action)

        # Page numbering
        self.page_numbering_action = Gio.SimpleAction.new_stateful('reader.page-numbering', None, GLib.Variant('b', False))
        self.page_numbering_action.connect('change-state', self.on_page_numbering_changed)
        self.window.application.add_action(self.page_numbering_action)

        # Save page
        self.save_page_action = Gio.SimpleAction.new('reader.save-page', None)
        self.save_page_action.connect('activate', self.save_page)
        self.window.application.add_action(self.save_page_action)

        # Open settings dialog
        self.open_settings_dialog_action = Gio.SimpleAction.new('reader.open-settings-dialog', None)
        self.open_settings_dialog_action.connect('activate', self.open_settings_dialog)
        self.window.application.add_action(self.open_settings_dialog_action)

    def init(self, manga, chapter):
        self.manga = manga
        self.init_chapter = chapter

        # Reset list of chapters consulted
        self.chapters_consulted = set()

        # Init settings
        self.set_action_reading_mode()
        self.set_action_scaling()
        self.set_action_scaling_filter()
        self.set_action_landscape_zoom()
        self.set_action_borders_crop()
        self.set_action_page_numbering()

        if Settings.get_default().fullscreen:
            self.window.fullscreen()

        self.back_button.set_tooltip_text(self.manga.name)

        self.show()

    def init_pager(self, chapter):
        if self.pager:
            self.pager.dispose()

        if self.reading_mode == 'webtoon':
            self.pager = WebtoonPager(self)
        else:
            self.pager = Pager(self)
            self.set_orientation()

        self.set_action_background_color()

        self.overlay.set_child(self.pager)

        self.pager.init(chapter)

    def on_back_button_clicked(self, _btn):
        self.window.navigationview.pop()

    def on_background_color_changed(self, _action, variant):
        value = variant.get_string()
        if value == self.background_color:
            return

        self.manga.update({
            'background_color': value if value != Settings.get_default().background_color else None,
        })
        self.set_action_background_color()

    def on_borders_crop_changed(self, _action, variant):
        value = variant.get_boolean()
        if value == self.borders_crop:
            return

        self.manga.update({
            'borders_crop': value if value != Settings.get_default().borders_crop else None,
        })
        self.set_action_borders_crop()

        self.pager.crop_pages_borders()

    def on_fullscreen_state_changed(self, _window, gparam):
        if self.window.is_fullscreen():
            self.headerbar_revealer.set_reveal_child(False)
            self.fullscreen_button.set_icon_name('view-restore-symbolic')
        else:
            self.headerbar_revealer.set_reveal_child(True)
            self.fullscreen_button.set_icon_name('view-fullscreen-symbolic')

    def on_hidden(self, _page):
        if self.window.previous_page == self.props.tag:
            return

        if self.pager:
            self.pager.dispose()
            self.pager = None

        self.controls.hide()
        self.page_numbering_label.set_visible(False)
        self.window.unfullscreen()

        # Sync Card page
        if self.window.card in self.window.navigationview.get_navigation_stack():
            if self.chapters_consulted:
                # Refresh to update all previously chapters consulted (last page read may have changed also)
                # and update info like disk usage
                self.window.card.refresh(unread_chapters=True, info=True, chapters=self.chapters_consulted)

        # Sync History page
        if self.window.history in self.window.navigationview.get_navigation_stack():
            self.window.history.populate()

        # Sync Library page (root)
        if self.manga.in_library:
            self.window.library.update_flowbox_child(self.manga)
            self.window.library.flowbox.invalidate_sort()

    def on_key_pressed(self, _controller, keyval, _keycode, state):
        if self.window.page != self.props.tag:
            return Gdk.EVENT_PROPAGATE

        # Allow to navigate back with <Esc> key and <Alt+Left>
        modifiers = state & Gtk.accelerator_get_default_mod_mask()
        if keyval == Gdk.KEY_Escape or (modifiers == Gdk.ModifierType.ALT_MASK and keyval in (Gdk.KEY_Left, Gdk.KEY_KP_Left)):
            self.window.navigationview.pop()
            return Gdk.EVENT_STOP

        return Gdk.EVENT_PROPAGATE

    def on_landscape_zoom_changed(self, _action, variant):
        value = variant.get_boolean()
        if value == self.landscape_zoom:
            return

        self.manga.update({
            'landscape_zoom': value if value != Settings.get_default().landscape_zoom else None,
        })
        self.set_action_landscape_zoom()

        self.pager.rescale_pages()

    def on_page_numbering_changed(self, _action, variant):
        value = not variant.get_boolean()
        if value == self.page_numbering:
            return

        self.manga.update({
            'page_numbering': value if value != Settings.get_default().page_numbering else None,
        })
        self.set_action_page_numbering()

        if value and self.page_numbering_defined and not self.controls.is_visible:
            self.page_numbering_label.set_visible(True)
        else:
            self.page_numbering_label.set_visible(False)

    def on_reading_mode_changed(self, _action, variant):
        value = variant.get_string()
        if value == self.reading_mode:
            return

        prior_reading_mode = self.reading_mode

        self.manga.update({
            'reading_mode': value if value != Settings.get_default().reading_mode else None,
        })
        self.set_action_reading_mode()

        if value == 'webtoon' or prior_reading_mode == 'webtoon':
            self.init_pager(self.pager.current_page.chapter)
        else:
            if value == 'right-to-left' or prior_reading_mode == 'right-to-left':
                self.pager.reverse_pages()
            self.set_orientation()

    def on_scaling_changed(self, _action, variant):
        value = variant.get_string()
        if value == self.scaling:
            return

        self.manga.update({
            'scaling': value if value != Settings.get_default().scaling else None,
        })
        self.set_action_scaling()

        self.pager.rescale_pages()

    def on_scaling_filter_changed(self, _action, variant):
        value = variant.get_string()
        if value == self.scaling:
            return

        self.manga.update({
            'scaling_filter': value if value != Settings.get_default().scaling_filter else None,
        })
        self.set_action_scaling_filter()

        self.pager.rescale_pages()

    def on_shown(self, _page):
        def do_init_pager():
            if self.window.last_navigation_action != 'push':
                return

            # Wait page is shown (transition is ended) to init pager
            # Operation is resource intensive and could disrupt page transition
            self.init_pager(self.init_chapter)

        if not Gtk.Settings.get_default().get_property('gtk-enable-animations'):
            # When animations are disabled, popped/pushed events are sent after `shown` event (bug?)
            # Use idle_add to be sure that last `popped` or `pushed` event has been received
            GLib.idle_add(do_init_pager)
        else:
            do_init_pager()

    def open_settings_dialog(self, _action, _gparam):
        self.settings_dialog.open()

    def save_page(self, _action, _gparam):
        if self.window.page != self.props.tag:
            return

        page = self.pager.current_page
        if not page.image or page.error:
            return

        def do_save(dest_path):
            try:
                shutil.copy(page.path, dest_path)
            except Exception as error:
                self.window.add_notification(str(error))
            else:
                self.window.add_notification(_('Page successfully saved'))

        def on_ready(dialog, result):
            try:
                gfile = dialog.save_finish(result)
            except GLib.GError:
                # Cancel
                gfile = None

            if gfile:
                do_save(gfile.get_path())

        extension = get_file_mime_type(page.path).split('/')[-1]
        filename = f'{self.manga.name}_{page.chapter.title}_{str(page.index + 1)}.{extension}'
        xdg_pictures_dir = GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_PICTURES)

        dialog = Gtk.FileDialog(modal=True)
        dialog.set_initial_name(filename)
        if xdg_pictures_dir is not None:
            dialog.set_initial_folder(Gio.File.new_for_path(xdg_pictures_dir))

        dialog.save(self.window, None, on_ready)

    def set_action_background_color(self):
        self.background_color_action.set_state(GLib.Variant('s', self.background_color))

        if self.background_color == 'white':
            self.pager.set_css_classes(['background-white'])
        elif self.background_color == 'gray':
            self.pager.set_css_classes(['background-gray'])
        elif self.background_color == 'black':
            self.pager.set_css_classes(['background-black'])
        else:
            # System style
            self.pager.set_css_classes([])

    def set_action_borders_crop(self):
        self.borders_crop_action.set_state(GLib.Variant('b', self.borders_crop))

    def set_action_landscape_zoom(self):
        self.landscape_zoom_action.set_state(GLib.Variant('b', self.landscape_zoom))

    def set_action_page_numbering(self):
        self.page_numbering_action.set_state(GLib.Variant('b', not self.page_numbering))

    def set_action_reading_mode(self):
        self.reading_mode_action.set_state(GLib.Variant('s', self.reading_mode))

        # Scaling action is enabled in RTL/LTR/Vertical reading modes only
        self.scaling_action.set_enabled(self.reading_mode != 'webtoon')
        # Landscape pages zoom is enabled in RTL/LTR/Vertical reading modes only and when scaling is 'screen'
        self.landscape_zoom_action.set_enabled(self.reading_mode != 'webtoon' and self.scaling == 'screen')
        # Borders crop is enabled in RTL/LTR/Vertical reading modes only
        self.borders_crop_action.set_enabled(self.reading_mode != 'webtoon')

        # Additionally, direction of page slider in controls must be updated
        self.controls.set_scale_direction(inverted=self.reading_mode == 'right-to-left')

    def set_action_scaling(self, scaling=None):
        self.scaling_action.set_state(GLib.Variant('s', scaling or self.scaling))

        # Landscape pages zoom is enabled in RTL/LTR/Vertical reading modes only and when scaling is 'screen'
        self.landscape_zoom_action.set_enabled(self.reading_mode != 'webtoon' and self.scaling == 'screen')

    def set_action_scaling_filter(self, scaling_filter=None):
        self.scaling_filter_action.set_state(GLib.Variant('s', scaling_filter or self.scaling_filter))

    def set_orientation(self):
        if self.reading_mode in ('right-to-left', 'left-to-right'):
            orientation = Gtk.Orientation.HORIZONTAL
        else:
            orientation = Gtk.Orientation.VERTICAL

        self.pager.set_orientation(orientation)

    def show(self):
        self.window.navigationview.push(self)

    def toggle_controls(self, visible=None):
        if visible is None:
            visible = not self.controls.is_visible

        if visible:
            self.controls.show()
            self.page_numbering_label.set_visible(False)
        else:
            self.controls.hide()
            if self.page_numbering and self.page_numbering_defined:
                self.page_numbering_label.set_visible(True)

    def update_page_numbering(self, number=None, total=None):
        if number and total:
            self.page_numbering_label.set_text('{0}/{1}'.format(number, total))
            self.page_numbering_defined = True

            if self.page_numbering and not self.controls.is_visible:
                self.page_numbering_label.set_visible(True)
            else:
                self.page_numbering_label.set_visible(False)
        else:
            self.page_numbering_defined = False
            self.page_numbering_label.set_visible(False)

    def update_title(self, chapter):
        # Set title & subtitle (headerbar)
        self.title.set_title(chapter.manga.name)
        subtitle = chapter.title
        if chapter.manga.name in subtitle:
            subtitle = subtitle.replace(chapter.manga.name, '').strip()
        self.title.set_subtitle(subtitle)


class SettingsDialog(Adw.PreferencesDialog):
    def __init__(self, page):
        super().__init__()
        self.page = page
        self.settings = Settings.get_default()

        self.set_title(_('Settings'))
        self.set_presentation_mode(Adw.DialogPresentationMode.BOTTOM_SHEET)

        self.css_provider = Gtk.CssProvider.new()
        Gtk.StyleContext.add_provider_for_display(Gdk.Display.get_default(), self.css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)

        page = Adw.PreferencesPage(title=_('Filters'))
        self.add(page)

        # Filters
        group = Adw.PreferencesGroup(title=_('Filters'), separate_rows=True)

        filters = {
            'brightness': {
                'title': _('Brightness'),
                'subtitle': _('Make pages brighter or darker'),
                'min': 0,
                'max': 200,
                'step': 1,
                'default': 100,
            },
            'contrast': {
                'title': _('Contrast'),
                'subtitle': _('Increase or decrease the contrast of pages'),
                'min': 0,
                'max': 200,
                'step': 1,
                'default': 100,
            },
            'grayscale': {
                'title': _('Grayscale'),
                'subtitle': _('Convert pages to grayscale'),
                'min': 0,
                'max': 100,
                'step': 1,
                'default': 0,
            },
            'sepia': {
                'title': _('Sepia'),
                'subtitle': _('Give a more yellow/brown appearance to pages'),
                'min': 0,
                'max': 100,
                'step': 1,
                'default': 0,
            },
            'saturate': {
                'title': _('Saturation'),
                'subtitle': _('Super-saturate or desaturate pages'),
                'min': 0,
                'max': 400,
                'step': 1,
                'default': 100,
            },
        }

        for name, data in filters.items():
            erow = Adw.ExpanderRow()
            erow.set_title(data['title'])
            erow.set_subtitle(data['subtitle'])
            erow.set_enable_expansion(self.settings.page_filters.get(f'{name}-state'))
            erow.set_show_enable_switch(True)
            erow.connect('notify::enable-expansion', self.toggle_filter_state, name)

            row = Adw.SpinRow.new_with_range(data['min'], data['max'], data['step'])
            row.set_title(data['title'])
            row.set_value(self.settings.page_filters.get(name, data['default']))
            row.connect('notify::value', self.set_filter_value, name)

            erow.add_row(row)
            group.add(erow)

        page.add(group)

        self.apply_filters()

        # White borders cropping
        group = Adw.PreferencesGroup(title=_('White Borders Cropping'), separate_rows=True)

        self.threshold_row = Adw.ComboRow()
        self.threshold_row.set_title(_('Threshold'))
        self.threshold_row.set_subtitle(_("Control which pixels are classified as 'white' during borders detection"))
        model = Gtk.StringList()
        for threshold in BORDERS_CROP_THRESHOLDS:
            model.append(str(threshold))
        self.threshold_row.set_model(model)
        self.threshold_row.connect('notify::selected', self.set_borders_crop_threshold)

        group.add(self.threshold_row)
        page.add(group)

    def apply_filters(self):
        funcs = []
        for name, value in self.settings.page_filters.items():
            if name.endswith('-state'):
                # Ignore state key
                continue
            if not self.settings.page_filters.get(f'{name}-state'):
                # Filter is off
                continue

            funcs.append(f'{name}({value}%)')

        if funcs:
            self.css_provider.load_from_string(f'.page-filters {{filter: {" ".join(funcs)};}}')  # noqa
            self.page.overlay.add_css_class('page-filters')
        else:
            self.css_provider.load_from_string('')
            self.page.overlay.remove_css_class('page-filters')

    def open(self):
        # Init settings values
        self.threshold_row.set_selected(BORDERS_CROP_THRESHOLDS.index(self.page.borders_crop_threshold))

        self.present(self.page.window)

    def set_borders_crop_threshold(self, row, _gparam):
        threshold = BORDERS_CROP_THRESHOLDS[row.get_selected()]

        self.page.manga.update({
            'borders_crop_threshold': threshold if threshold != Settings.get_default().borders_crop_threshold else None,
        })

        for page in self.page.pager.pages:
            if page.image and page.error is None:
                page.image.crop_bbox = None
                page.image.textures_crop = None
                page.image.crop_threshold = threshold

    def set_filter_value(self, row, _gparam, name):
        filters = self.settings.page_filters
        filters[name] = row.get_value()
        self.settings.page_filters = filters

        self.apply_filters()

    def toggle_filter_state(self, row, _gparam, name):
        filters = self.settings.page_filters
        filters[f'{name}-state'] = row.get_enable_expansion()
        self.settings.page_filters = filters

        self.apply_filters()
