# Copyright (C) 2018 Philipp Hörist <philipp AT hoerist.com>
#
# This file is part of Gajim.
#
# Gajim 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; version 3 only.
#
# Gajim 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 Gajim. If not, see <http://www.gnu.org/licenses/>.

from gi.repository import Gtk
from gi.repository import GLib
from gi.repository import Gdk
from gi.repository import Pango

from gajim.common import app
from gajim.common import passwords
from gajim.common.i18n import _
from gajim.common.i18n import Q_

from gajim import gtkgui_helpers

from .util import get_image_button
from .util import MaxWidthComboBoxText
from .util import open_window
from .const import SettingKind
from .const import SettingType


class SettingsDialog(Gtk.ApplicationWindow):
    def __init__(self, parent, title, flags, settings, account,
                 extend=None):
        Gtk.ApplicationWindow.__init__(self)
        self.set_application(app.app)
        self.set_show_menubar(False)
        self.set_title(title)
        self.set_transient_for(parent)
        self.set_resizable(False)
        self.set_default_size(250, -1)
        self.set_type_hint(Gdk.WindowTypeHint.DIALOG)
        self.get_style_context().add_class('settings-dialog')
        self.account = account
        if flags == Gtk.DialogFlags.MODAL:
            self.set_modal(True)
        elif flags == Gtk.DialogFlags.DESTROY_WITH_PARENT:
            self.set_destroy_with_parent(True)

        self.listbox = SettingsBox(account, extend=extend)
        self.listbox.set_hexpand(True)
        self.listbox.set_selection_mode(Gtk.SelectionMode.NONE)

        for setting in settings:
            self.listbox.add_setting(setting)
        self.listbox.update_states()

        self.add(self.listbox)

        self.show_all()
        self.connect_after('key-press-event', self.on_key_press)

    def on_key_press(self, _widget, event):
        if event.keyval == Gdk.KEY_Escape:
            self.destroy()

    def get_setting(self, name):
        return self.listbox.get_setting(name)


class SettingsBox(Gtk.ListBox):
    def __init__(self, account=None, jid=None, extend=None):
        Gtk.ListBox.__init__(self)
        self.get_style_context().add_class('settings-box')
        self.account = account
        self.jid = jid
        self.named_settings = {}

        self.map = {
            SettingKind.SWITCH: SwitchSetting,
            SettingKind.SPIN: SpinSetting,
            SettingKind.DIALOG: DialogSetting,
            SettingKind.ENTRY: EntrySetting,
            SettingKind.COLOR: ColorSetting,
            SettingKind.ACTION: ActionSetting,
            SettingKind.LOGIN: LoginSetting,
            SettingKind.FILECHOOSER: FileChooserSetting,
            SettingKind.CALLBACK: CallbackSetting,
            SettingKind.PRIORITY: PrioritySetting,
            SettingKind.HOSTNAME: CutstomHostnameSetting,
            SettingKind.CHANGEPASSWORD: ChangePasswordSetting,
            SettingKind.COMBO: ComboSetting,
            SettingKind.POPOVER: PopoverSetting,
            SettingKind.AUTO_AWAY: CutstomAutoAwaySetting,
            SettingKind.AUTO_EXTENDED_AWAY: CutstomAutoExtendedAwaySetting,
            SettingKind.USE_STUN_SERVER: CustomStunServerSetting,
            SettingKind.NOTIFICATIONS: NotificationsSetting,
        }

        if extend is not None:
            for setting, callback in extend:
                self.map[setting] = callback

        self.connect('row-activated', self.on_row_activated)

    @staticmethod
    def on_row_activated(_listbox, row):
        row.on_row_activated()

    def add_setting(self, setting):
        if not isinstance(setting, Gtk.ListBoxRow):
            if setting.props is not None:
                listitem = self.map[setting.kind](self.account,
                                                  self.jid,
                                                  *setting[1:-1],
                                                  **setting.props)
            else:
                listitem = self.map[setting.kind](self.account,
                                                  self.jid,
                                                  *setting[1:-1])

        if setting.name is not None:
            self.named_settings[setting.name] = listitem
        self.add(listitem)

    def get_setting(self, name):
        return self.named_settings[name]

    def update_states(self):
        for row in self.get_children():
            row.update_activatable()


class GenericSetting(Gtk.ListBoxRow):
    def __init__(self,
                 account,
                 jid,
                 label,
                 type_,
                 value,
                 name,
                 callback,
                 data,
                 desc,
                 bind,
                 inverted,
                 enabled_func):

        Gtk.ListBoxRow.__init__(self)
        self._grid = Gtk.Grid()
        self._grid.set_size_request(-1, 30)
        self._grid.set_column_spacing(12)

        self.callback = callback
        self.type_ = type_
        self.value = value
        self.data = data
        self.label = label
        self.account = account
        self.jid = jid
        self.name = name
        self.bind = bind
        self.inverted = inverted
        self.enabled_func = enabled_func
        self.setting_value = self.get_value()

        description_box = Gtk.Box(
            orientation=Gtk.Orientation.VERTICAL, spacing=0)
        description_box.set_valign(Gtk.Align.CENTER)

        settingtext = Gtk.Label(label=label)
        settingtext.set_hexpand(True)
        settingtext.set_halign(Gtk.Align.START)
        settingtext.set_valign(Gtk.Align.CENTER)
        settingtext.set_vexpand(True)
        description_box.add(settingtext)

        if desc is not None:
            description = Gtk.Label(label=desc)
            description.set_name('SubDescription')
            description.set_hexpand(True)
            description.set_halign(Gtk.Align.START)
            description.set_valign(Gtk.Align.CENTER)
            description.set_xalign(0)
            description.set_line_wrap(True)
            description.set_line_wrap_mode(Pango.WrapMode.WORD)
            description.set_max_width_chars(50)
            description_box.add(description)

        self._grid.add(description_box)

        self.setting_box = Gtk.Box(spacing=12)
        self.setting_box.set_size_request(200, -1)
        self.setting_box.set_valign(Gtk.Align.CENTER)
        self.setting_box.set_name('GenericSettingBox')
        self._grid.add(self.setting_box)
        self.add(self._grid)

        self._bind_sensitive_state()

    def _bind_sensitive_state(self):
        if self.bind is None:
            return

        bind_setting_type, setting, account, jid = self._parse_bind()

        app.settings.bind_signal(setting,
                                 self,
                                 'set_sensitive',
                                 account=account,
                                 jid=jid,
                                 inverted=self.inverted)

        if bind_setting_type == SettingType.CONTACT:
            value = app.settings.get_contact_setting(account, jid, setting)

        elif bind_setting_type == SettingType.GROUP_CHAT:
            value = app.settings.get_group_chat_setting(account, jid, setting)

        elif bind_setting_type == SettingType.ACCOUNT_CONFIG:
            value = app.settings.get_account_setting(account, setting)

        else:
            value = app.settings.get(setting)

        if self.inverted:
            value = not value
        self.set_sensitive(value)

    def _parse_bind(self):
        if '::' not in self.bind:
            return SettingType.CONFIG, self.bind, None, None

        bind_setting_type, setting = self.bind.split('::')
        if bind_setting_type == 'account':
            return SettingType.ACCOUNT_CONFIG, setting, self.account, None

        if bind_setting_type == 'contact':
            return SettingType.CONTACT, setting, self.account, self.jid

        if bind_setting_type == 'group_chat':
            return SettingType.GROUP_CHAT, setting, self.account, self.jid
        raise ValueError(f'Invalid bind argument: {self.bind}')

    def get_value(self):
        return self.__get_value(self.type_,
                                self.value,
                                self.account,
                                self.jid)

    @staticmethod
    def __get_value(type_, value, account, jid):
        if value is None:
            return None
        if type_ == SettingType.VALUE:
            return value

        if type_ == SettingType.CONTACT:
            return app.settings.get_contact_setting(account, jid, value)

        if type_ == SettingType.GROUP_CHAT:
            return app.settings.get_group_chat_setting(
                account, jid, value)

        if type_ == SettingType.CONFIG:
            return app.settings.get(value)

        if type_ == SettingType.ACCOUNT_CONFIG:
            if value == 'password':
                return passwords.get_password(account)
            if value == 'no_log_for':
                no_log = app.settings.get_account_setting(
                    account, 'no_log_for').split()
                return account not in no_log
            return app.settings.get_account_setting(account, value)

        if type_ == SettingType.ACTION:
            if value.startswith('-'):
                return account + value
            return value

        raise ValueError('Wrong SettingType?')

    def set_value(self, state):
        if self.type_ == SettingType.CONFIG:
            app.settings.set(self.value, state)

        elif self.type_ == SettingType.ACCOUNT_CONFIG:
            if self.value == 'password':
                passwords.save_password(self.account, state)
            if self.value == 'no_log_for':
                self.set_no_log_for(self.account, state)
            else:
                app.settings.set_account_setting(self.account,
                                                 self.value,
                                                 state)

        elif self.type_ == SettingType.CONTACT:
            app.settings.set_contact_setting(
                self.account, self.jid, self.value, state)

        elif self.type_ == SettingType.GROUP_CHAT:
            app.settings.set_group_chat_setting(
                self.account, self.jid, self.value, state)

        if self.callback is not None:
            self.callback(state, self.data)

    @staticmethod
    def set_no_log_for(account, state):
        no_log = app.settings.get_account_setting(account, 'no_log_for').split()
        if state and account in no_log:
            no_log.remove(account)
        elif not state and account not in no_log:
            no_log.append(account)
        app.settings.set_account_setting(account,
                                         'no_log_for',
                                         ' '.join(no_log))

    def on_row_activated(self):
        raise NotImplementedError

    def update_activatable(self):
        if self.enabled_func is None:
            return

        enabled_func_value = self.enabled_func()
        self.set_activatable(enabled_func_value)
        self.set_sensitive(enabled_func_value)

    def _add_action_button(self, kwargs):
        icon_name = kwargs.get('button-icon-name')
        button_text = kwargs.get('button-text')
        tooltip_text = kwargs.get('button-tooltip') or ''
        style = kwargs.get('button-style')

        if icon_name is not None:
            button = Gtk.Button.new_from_icon_name(icon_name, Gtk.IconSize.MENU)

        elif button_text is not None:
            button = Gtk.Button(label=button_text)

        else:
            return

        if style is not None:
            for css_class in style.split(' '):
                button.get_style_context().add_class(css_class)

        button.connect('clicked', kwargs['button-callback'])
        button.set_tooltip_text(tooltip_text)
        self.setting_box.add(button)


class SwitchSetting(GenericSetting):
    def __init__(self, *args, **kwargs):
        GenericSetting.__init__(self, *args)

        self.switch = Gtk.Switch()
        if self.type_ == SettingType.ACTION:
            self.switch.set_action_name('app.%s' % self.setting_value)
            state = app.app.get_action_state(self.setting_value)
            self.switch.set_active(state.get_boolean())
        else:
            self.switch.set_active(self.setting_value)
        self.switch.connect('notify::active', self.on_switch)
        self.switch.set_hexpand(True)
        self.switch.set_halign(Gtk.Align.END)
        self.switch.set_valign(Gtk.Align.CENTER)

        self._switch_state_label = Gtk.Label()
        self._switch_state_label.set_xalign(1)
        self._switch_state_label.set_valign(Gtk.Align.CENTER)
        self._set_label(self.setting_value)

        box = Gtk.Box(spacing=12)
        box.set_halign(Gtk.Align.END)
        box.add(self._switch_state_label)
        box.add(self.switch)
        self.setting_box.add(box)

        self._add_action_button(kwargs)

        self.show_all()

    def on_row_activated(self):
        state = self.switch.get_active()
        self.switch.set_active(not state)

    def on_switch(self, switch, *args):
        value = switch.get_active()
        self.set_value(value)
        self._set_label(value)

    def _set_label(self, active):
        text = Q_('?switch:On') if active else Q_('?switch:Off')
        self._switch_state_label.set_text(text)


class EntrySetting(GenericSetting):
    def __init__(self, *args):
        GenericSetting.__init__(self, *args)

        self.entry = Gtk.Entry()
        self.entry.set_text(str(self.setting_value))
        self.entry.connect('notify::text', self.on_text_change)
        self.entry.set_valign(Gtk.Align.CENTER)
        self.entry.set_alignment(1)

        if self.value == 'password':
            self.entry.set_invisible_char('*')
            self.entry.set_visibility(False)

        self.setting_box.pack_end(self.entry, True, True, 0)

        self.show_all()

    def on_text_change(self, *args):
        text = self.entry.get_text()
        self.set_value(text)

    def on_row_activated(self):
        self.entry.grab_focus()


class ColorSetting(GenericSetting):
    def __init__(self, *args):
        GenericSetting.__init__(self, *args)

        rgba = Gdk.RGBA()
        rgba.parse(self.setting_value)
        self.color_button = Gtk.ColorButton()
        self.color_button.set_rgba(rgba)
        self.color_button.connect('color-set', self.on_color_set)
        self.color_button.set_valign(Gtk.Align.CENTER)
        self.color_button.set_halign(Gtk.Align.END)

        self.setting_box.pack_end(self.color_button, True, True, 0)

        self.show_all()

    def on_color_set(self, button):
        rgba = button.get_rgba()
        self.set_value(rgba.to_string())
        app.css_config.refresh()

    def on_row_activated(self):
        self.color_button.grab_focus()


class DialogSetting(GenericSetting):
    def __init__(self, *args, dialog):
        GenericSetting.__init__(self, *args)
        self.dialog = dialog

        self.setting_value = Gtk.Label()
        self.setting_value.set_text(self.get_setting_value())
        self.setting_value.set_halign(Gtk.Align.END)
        self.setting_box.pack_start(self.setting_value, True, True, 0)

        self.show_all()

    def show_dialog(self, parent):
        if self.dialog:
            dialog = self.dialog(self.account, parent)
            dialog.connect('destroy', self.on_destroy)

    def on_destroy(self, *args):
        self.setting_value.set_text(self.get_setting_value())

    def get_setting_value(self):
        self.setting_value.hide()
        return ''

    def on_row_activated(self):
        self.show_dialog(self.get_toplevel())


class SpinSetting(GenericSetting):
    def __init__(self, *args, range_):
        GenericSetting.__init__(self, *args)

        lower, upper = range_
        adjustment = Gtk.Adjustment(value=0,
                                    lower=lower,
                                    upper=upper,
                                    step_increment=1,
                                    page_increment=10,
                                    page_size=0)

        self.spin = Gtk.SpinButton()
        self.spin.set_adjustment(adjustment)
        self.spin.set_numeric(True)
        self.spin.set_update_policy(Gtk.SpinButtonUpdatePolicy.IF_VALID)
        self.spin.set_value(self.setting_value)
        self.spin.set_halign(Gtk.Align.END)
        self.spin.set_valign(Gtk.Align.CENTER)
        self.spin.connect('notify::value', self.on_value_change)

        self.setting_box.pack_start(self.spin, True, True, 0)

        self.show_all()

    def on_row_activated(self):
        self.spin.grab_focus()

    def on_value_change(self, spin, *args):
        value = spin.get_value_as_int()
        self.set_value(value)


class FileChooserSetting(GenericSetting):
    def __init__(self, *args, filefilter):
        GenericSetting.__init__(self, *args)

        button = Gtk.FileChooserButton(title=self.label,
                                       action=Gtk.FileChooserAction.OPEN)
        button.set_halign(Gtk.Align.END)

        # GTK Bug: The FileChooserButton expands without limit
        # get the label and use set_max_wide_chars()
        label = button.get_children()[0].get_children()[0].get_children()[1]
        label.set_max_width_chars(20)

        if filefilter:
            name, pattern = filefilter
            filter_ = Gtk.FileFilter()
            filter_.set_name(name)
            filter_.add_pattern(pattern)
            button.add_filter(filter_)
            button.set_filter(filter_)

        filter_ = Gtk.FileFilter()
        filter_.set_name(_('All files'))
        filter_.add_pattern('*')
        button.add_filter(filter_)

        if self.setting_value:
            button.set_filename(self.setting_value)
        button.connect('selection-changed', self.on_select)

        clear_button = get_image_button(
            'edit-clear-all-symbolic', _('Clear File'))
        clear_button.connect('clicked', lambda *args: button.unselect_all())
        self.setting_box.pack_start(button, True, True, 0)
        self.setting_box.pack_start(clear_button, False, False, 0)

        self.show_all()

    def on_select(self, filechooser):
        self.set_value(filechooser.get_filename() or '')

    def on_row_activated(self):
        pass


class CallbackSetting(GenericSetting):
    def __init__(self, *args, callback):
        GenericSetting.__init__(self, *args)
        self.callback = callback
        self.show_all()

    def on_row_activated(self):
        self.callback()


class ActionSetting(GenericSetting):
    def __init__(self, *args, account):
        GenericSetting.__init__(self, *args)
        action_name = '%s%s' % (account, self.value)
        self.action = gtkgui_helpers.get_action(action_name)
        self.variant = GLib.Variant.new_string(account)
        self.on_enable()

        self.show_all()
        self.action.connect('notify::enabled', self.on_enable)

    def on_enable(self, *args):
        self.set_sensitive(self.action.get_enabled())

    def on_row_activated(self):
        self.action.activate(self.variant)


class LoginSetting(DialogSetting):
    def __init__(self, *args, **kwargs):
        DialogSetting.__init__(self, *args, **kwargs)
        self.setting_value.set_selectable(True)

    def get_setting_value(self):
        jid = app.get_jid_from_account(self.account)
        return jid


class PopoverSetting(GenericSetting):
    def __init__(self, *args, entries, **kwargs):
        GenericSetting.__init__(self, *args)

        self._entries = self._convert_to_dict(entries)

        box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL,
                      spacing=12)
        box.set_halign(Gtk.Align.END)
        box.set_hexpand(True)

        self._default_text = kwargs.get('default-text')

        self._current_label = Gtk.Label()
        self._current_label.set_valign(Gtk.Align.CENTER)
        image = Gtk.Image.new_from_icon_name('pan-down-symbolic',
                                             Gtk.IconSize.MENU)
        image.set_valign(Gtk.Align.CENTER)

        box.add(self._current_label)
        box.add(image)

        self._menu_listbox = Gtk.ListBox()
        self._menu_listbox.set_selection_mode(Gtk.SelectionMode.NONE)
        self._add_menu_entries()
        self._menu_listbox.connect('row-activated',
                                   self._on_menu_row_activated)

        scrolled_window = Gtk.ScrolledWindow()
        scrolled_window.set_propagate_natural_height(True)
        scrolled_window.set_propagate_natural_width(True)
        scrolled_window.set_max_content_height(400)
        scrolled_window.set_policy(Gtk.PolicyType.NEVER,
                                   Gtk.PolicyType.AUTOMATIC)
        scrolled_window.add(self._menu_listbox)
        scrolled_window.show_all()

        self._popover = Gtk.Popover()
        self._popover.get_style_context().add_class('combo')
        self._popover.set_relative_to(image)
        self._popover.set_position(Gtk.PositionType.BOTTOM)
        self._popover.add(scrolled_window)

        self.setting_box.add(box)

        self._add_action_button(kwargs)

        text = self._entries.get(self.setting_value, self._default_text or '')
        self._current_label.set_text(text)

        app.settings.connect_signal(self.value,
                                    self._on_setting_changed,
                                    account=self.account,
                                    jid=self.jid)

        self.connect('destroy', self._on_destroy)

        self.show_all()

    @staticmethod
    def _convert_to_dict(entries):
        if isinstance(entries, list):
            entries = {key: key for key in entries}
        return entries

    def _on_setting_changed(self, value, *args):
        text = self._entries.get(value)
        if text is None:
            text = self._default_text or ''

        self._current_label.set_text(text)

    def _add_menu_entries(self):
        if self._default_text is not None:
            self._menu_listbox.add(PopoverRow(self._default_text, ''))

        for value, label in self._entries.items():
            self._menu_listbox.add(PopoverRow(label, value))

        self._menu_listbox.show_all()

    def _on_menu_row_activated(self, listbox, row):
        listbox.unselect_all()
        self._popover.popdown()

        self.set_value(row.value)

    def on_row_activated(self):
        self._popover.popup()

    def update_entries(self, entries):
        self._entries = self._convert_to_dict(entries)
        self._menu_listbox.foreach(self._menu_listbox.remove)
        self._add_menu_entries()

    def _on_destroy(self, *args):
        app.settings.disconnect_signals(self)


class PopoverRow(Gtk.ListBoxRow):
    def __init__(self, label, value):
        Gtk.ListBoxRow.__init__(self)
        self.label = label
        self.value = value

        label = Gtk.Label(label=label)
        label.set_xalign(0)
        self.add(label)


class ComboSetting(GenericSetting):
    def __init__(self, *args, combo_items):
        GenericSetting.__init__(self, *args)

        self.combo = MaxWidthComboBoxText()
        self.combo.set_valign(Gtk.Align.CENTER)

        for index, value in enumerate(combo_items):
            if isinstance(value, tuple):
                value, label = value
                self.combo.append(value, _(label))
            else:
                self.combo.append(value, value)
            if value == self.setting_value or index == 0:
                self.combo.set_active(index)

        self.combo.connect('changed', self.on_value_change)

        self.setting_box.pack_start(self.combo, True, True, 0)
        self.show_all()

    def on_value_change(self, combo):
        self.set_value(combo.get_active_id())

    def on_row_activated(self):
        pass


class PrioritySetting(DialogSetting):
    def __init__(self, *args, **kwargs):
        DialogSetting.__init__(self, *args, **kwargs)

    def get_setting_value(self):
        adjust = app.settings.get_account_setting(
            self.account, 'adjust_priority_with_status')
        if adjust:
            return _('Adjust to Status')

        priority = app.settings.get_account_setting(self.account, 'priority')
        return str(priority)


class CutstomHostnameSetting(DialogSetting):
    def __init__(self, *args, **kwargs):
        DialogSetting.__init__(self, *args, **kwargs)

    def get_setting_value(self):
        custom = app.settings.get_account_setting(self.account,
                                                  'use_custom_host')
        return Q_('?switch:On') if custom else Q_('?switch:Off')


class ChangePasswordSetting(DialogSetting):
    def __init__(self, *args, **kwargs):
        DialogSetting.__init__(self, *args, **kwargs)

    def show_dialog(self, parent):
        parent.destroy()
        open_window('ChangePassword', account=self.account)

    def update_activatable(self):
        activatable = False
        if self.account in app.connections:
            con = app.connections[self.account]
            activatable = (con.state.is_available and
                           con.get_module('Register').supported)
        self.set_activatable(activatable)


class CutstomAutoAwaySetting(DialogSetting):
    def __init__(self, *args, **kwargs):
        DialogSetting.__init__(self, *args, **kwargs)

    def get_setting_value(self):
        value = app.settings.get('autoaway')
        return Q_('?switch:On') if value else Q_('?switch:Off')


class CutstomAutoExtendedAwaySetting(DialogSetting):
    def __init__(self, *args, **kwargs):
        DialogSetting.__init__(self, *args, **kwargs)

    def get_setting_value(self):
        value = app.settings.get('autoxa')
        return Q_('?switch:On') if value else Q_('?switch:Off')


class CustomStunServerSetting(DialogSetting):
    def __init__(self, *args, **kwargs):
        DialogSetting.__init__(self, *args, **kwargs)

    def get_setting_value(self):
        value = app.settings.get('use_stun_server')
        return Q_('?switch:On') if value else Q_('?switch:Off')


class NotificationsSetting(DialogSetting):
    def __init__(self, *args, **kwargs):
        DialogSetting.__init__(self, *args, **kwargs)

    def get_setting_value(self):
        value = app.settings.get('show_notifications')
        return Q_('?switch:On') if value else Q_('?switch:Off')
