#!/usr/local/bin/python3.12


__license__   = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'

from qt.core import QAbstractItemView, QAbstractListModel, QIcon, QItemSelectionModel, Qt

from calibre import force_unicode
from calibre.gui2 import error_dialog, gprefs, warning_dialog
from calibre.gui2.preferences import AbortCommit, ConfigWidgetBase, test_widget
from calibre.gui2.preferences.toolbar_ui import Ui_Form
from calibre.startup import connect_lambda
from calibre.utils.icu import primary_sort_key


def sort_key_for_action(ac):
    q = getattr(ac, 'action_spec', None)
    try:
        q = ac.name if q is None else q[0]
        return primary_sort_key(force_unicode(q))
    except Exception:
        return primary_sort_key('')


class FakeAction:

    def __init__(self, name, gui_name, icon, tooltip=None,
            dont_add_to=frozenset(), dont_remove_from=frozenset()):
        self.name = name
        self.action_spec = (gui_name, icon, tooltip, None)
        self.dont_remove_from = dont_remove_from
        self.dont_add_to = dont_add_to


class BaseModel(QAbstractListModel):

    def name_to_action(self, name, gui):
        if name == 'Donate':
            return FakeAction(
                'Donate', _('Donate'), 'donate.png', tooltip=_('Donate to support the development of calibre'),
                dont_add_to=frozenset(['context-menu', 'context-menu-device', 'searchbar']))
        if name == 'Location Manager':
            return FakeAction('Location Manager', _('Location Manager'), 'reader.png',
                    _('Switch between library and device views'),
                    dont_add_to=frozenset(['menubar', 'toolbar',
                        'toolbar-child', 'context-menu', 'searchbar',
                        'context-menu-device']))
        if name is None:
            return FakeAction('--- '+('Separator')+' ---',
                    '--- '+_('Separator')+' ---', None,
                    dont_add_to=frozenset(['menubar', 'menubar-device']))
        try:
            return gui.iactions[name]
        except:
            return None

    def rowCount(self, parent):
        return len(self._data)

    def data(self, index, role):
        row = index.row()
        action = self._data[row].action_spec
        if role == Qt.ItemDataRole.DisplayRole:
            text = action[0]
            text = text.replace('&', '')
            if text == _('%d books'):
                text = _('Choose library')
            return (text)
        if role == Qt.ItemDataRole.DecorationRole:
            if hasattr(self._data[row], 'qaction'):
                icon = self._data[row].qaction.icon()
                if not icon.isNull():
                    return (icon)
            ic = action[1]
            if ic is None:
                ic = 'blank.png'
            return (QIcon.ic(ic))
        if role == Qt.ItemDataRole.ToolTipRole and action[2] is not None:
            return (action[2])
        return None

    def names(self, indexes):
        rows = [i.row() for i in indexes]
        ans = []
        for i in rows:
            n = self._data[i].name
            if n.startswith('---'):
                n = None
            ans.append(n)
        return ans

    def has_action(self, name):
        for a in self._data:
            if a.name == name:
                return True
        return False


class AllModel(BaseModel):

    def __init__(self, key, gui):
        BaseModel.__init__(self)
        self.gprefs_name = 'action-layout-'+key
        current = gprefs[self.gprefs_name]
        self.gui = gui
        self.key = key
        self._data = self.get_all_actions(current)

    def get_all_actions(self, current):
        all = list(self.gui.iactions.keys()) + ['Donate', 'Location Manager']
        all = [x for x in all if x not in current] + [None]
        all = [self.name_to_action(x, self.gui) for x in all]
        all = [x for x in all if self.key not in x.dont_add_to]

        all.sort(key=sort_key_for_action)
        return all

    def add(self, names):
        actions = []
        for name in names:
            if name is None or name.startswith('---'):
                continue
            actions.append(self.name_to_action(name, self.gui))
        self.beginResetModel()
        self._data.extend(actions)
        self._data.sort(key=sort_key_for_action)
        self.endResetModel()

    def remove(self, indices, allowed):
        rows = [i.row() for i in indices]
        remove = set()
        for row in rows:
            ac = self._data[row]
            if ac.name.startswith('---'):
                continue
            if ac.name in allowed:
                remove.add(row)
        ndata = []
        for i, ac in enumerate(self._data):
            if i not in remove:
                ndata.append(ac)
        self.beginResetModel()
        self._data = ndata
        self.endResetModel()

    def restore_defaults(self):
        current = gprefs.defaults[self.gprefs_name]
        self.beginResetModel()
        self._data = self.get_all_actions(current)
        self.endResetModel()


class CurrentModel(BaseModel):

    def __init__(self, key, gui):
        BaseModel.__init__(self)
        self.gprefs_name = 'action-layout-'+key
        current = gprefs[self.gprefs_name]
        self._data = [self.name_to_action(x, gui) for x in current]
        self._data = [x for x in self._data if x is not None]
        self.key = key
        self.gui = gui

    def move_single(self, idx, delta):
        row = idx.row()
        nrow = (row + delta + len(self._data)) % len(self._data)
        if row + delta < 0:
            x = self._data.pop(row)
            self._data.append(x)
        elif row + delta >= len(self._data):
            x = self._data.pop(row)
            self._data.insert(0, x)
        else:
            self._data[row], self._data[nrow] = self._data[nrow], self._data[row]

    def move_many(self, indices, delta):
        rows = [i.row() for i in indices]
        items = [self._data[x.row()] for x in indices]
        self.beginResetModel()
        indices = sorted(indices, key=lambda i: i.row(), reverse=delta > 0)
        for item in items:
            self.move_single(self.index(self._data.index(item)), delta)
        self.endResetModel()
        ans = {}
        for item, row in zip(items, rows):
            ans[row] = self.index(self._data.index(item))
        return ans

    def add(self, names):
        actions = []
        reject = set()
        for name in names:
            ac = self.name_to_action(name, self.gui)
            if self.key in ac.dont_add_to:
                reject.add(ac)
            else:
                actions.append(ac)

        self.beginResetModel()
        self._data.extend(actions)
        self.endResetModel()
        return reject

    def remove(self, indices):
        rows = [i.row() for i in indices]
        remove, rejected = set(), set()
        for row in rows:
            ac = self._data[row]
            if self.key in ac.dont_remove_from:
                rejected.add(ac)
                continue
            remove.add(row)
        ndata = []
        for i, ac in enumerate(self._data):
            if i not in remove:
                ndata.append(ac)
        self.beginResetModel()
        self._data = ndata
        self.endResetModel()
        return rejected

    def commit(self):
        old = gprefs[self.gprefs_name]
        new = []
        for x in self._data:
            n = x.name
            if n.startswith('---'):
                n = None
            new.append(n)
        new = tuple(new)
        if new != old:
            defaults = gprefs.defaults[self.gprefs_name]
            if defaults == new:
                del gprefs[self.gprefs_name]
            else:
                gprefs[self.gprefs_name] = new

    def restore_defaults(self):
        current = gprefs.defaults[self.gprefs_name]
        self.beginResetModel()
        self._data = [self.name_to_action(x, self.gui) for x in current]
        self.endResetModel()


class ConfigWidget(ConfigWidgetBase, Ui_Form):

    LOCATIONS = [
            ('toolbar', _('The main toolbar')),
            ('toolbar-device', _('The main toolbar when a device is connected')),
            ('toolbar-child', _('The optional second toolbar')),
            ('searchbar', _('The buttons on the search bar')),
            ('menubar', _('The menubar')),
            ('menubar-device', _('The menubar when a device is connected')),
            ('context-menu', _('The context menu for the books in the '
                'calibre library')),
            ('context-menu-split', _('The context menu for the split book list')),
            ('context-menu-device', _('The context menu for the books on '
                'the device')),
            ('context-menu-cover-browser', _('The context menu for the Cover '
                'browser')),
            ]

    def genesis(self, gui):
        self.all_actions.doubleClicked.connect(self.add_single_action)
        self.current_actions.doubleClicked.connect(self.remove_single_action)
        self.models = {}
        self.what.addItem(_('Click to choose toolbar or menu to customize'),
                'blank')
        for key, text in self.LOCATIONS:
            self.what.addItem(text, key)
            all_model = AllModel(key, gui)
            current_model = CurrentModel(key, gui)
            self.models[key] = (all_model, current_model)
        self.what.setCurrentIndex(0)
        self.what.currentIndexChanged.connect(self.what_changed)
        self.what_changed(0)

        self.add_action_button.clicked.connect(self.add_action)
        self.remove_action_button.clicked.connect(self.remove_action)
        connect_lambda(self.action_up_button.clicked, self, lambda self: self.move(-1))
        connect_lambda(self.action_down_button.clicked, self, lambda self: self.move(1))
        self.all_actions.setMouseTracking(True)
        self.current_actions.setMouseTracking(True)
        self.all_actions.entered.connect(self.all_entered)
        self.current_actions.entered.connect(self.current_entered)

    def all_entered(self, index):
        tt = self.all_actions.model().data(index, Qt.ItemDataRole.ToolTipRole) or ''
        self.help_text.setText(tt)

    def current_entered(self, index):
        tt = self.current_actions.model().data(index, Qt.ItemDataRole.ToolTipRole) or ''
        self.help_text.setText(tt)

    def what_changed(self, idx):
        key = str(self.what.itemData(idx) or '')
        if key == 'blank':
            self.actions_widget.setVisible(False)
            self.spacer_widget.setVisible(True)
        else:
            self.actions_widget.setVisible(True)
            self.spacer_widget.setVisible(False)
            self.all_actions.setModel(self.models[key][0])
            self.current_actions.setModel(self.models[key][1])

    def add_action(self, *args):
        self._add_action(self.all_actions.selectionModel().selectedIndexes())

    def add_single_action(self, index):
        self._add_action([index])

    def _add_action(self, indices):
        names = self.all_actions.model().names(indices)
        if names:
            not_added = self.current_actions.model().add(names)
            ns = {y.name for y in not_added}
            added = set(names) - ns
            self.all_actions.model().remove(indices, added)
            if not_added:
                warning_dialog(self, _('Cannot add'),
                        _('Cannot add the actions %s to this location') %
                        ','.join([a.action_spec[0] for a in not_added]),
                        show=True)
            if added:
                ca = self.current_actions
                idx = ca.model().index(ca.model().rowCount(None)-1)
                ca.scrollTo(idx)
                self.changed_signal.emit()

    def remove_action(self, *args):
        self._remove_action(self.current_actions.selectionModel().selectedIndexes())

    def remove_single_action(self, index):
        self._remove_action([index])

    def _remove_action(self, indices):
        names = self.current_actions.model().names(indices)
        if names:
            not_removed = self.current_actions.model().remove(indices)
            ns = {y.name for y in not_removed}
            removed = set(names) - ns
            self.all_actions.model().add(removed)
            if not_removed:
                warning_dialog(self, _('Cannot remove'),
                        _('Cannot remove the actions %s from this location') %
                        ','.join([a.action_spec[0] for a in not_removed]),
                        show=True)
            else:
                self.changed_signal.emit()

    def move(self, delta, *args):
        sm = self.current_actions.selectionModel()
        x = sm.selectedIndexes()
        if x and len(x):
            i = sm.currentIndex().row()
            m = self.current_actions.model()
            idx_map = m.move_many(x, delta)
            newci = idx_map.get(i)
            sm.clear()
            for idx in idx_map.values():
                sm.select(idx, QItemSelectionModel.SelectionFlag.Select)
            if newci is not None:
                sm.setCurrentIndex(newci, QItemSelectionModel.SelectionFlag.SelectCurrent)
            if newci is not None:
                self.current_actions.scrollTo(newci, QAbstractItemView.ScrollHint.EnsureVisible)
            self.changed_signal.emit()

    def commit(self):
        # Ensure preferences are showing in either the toolbar or
        # the menubar.
        pref_in_toolbar = self.models['toolbar'][1].has_action('Preferences')
        pref_in_menubar = self.models['menubar'][1].has_action('Preferences')
        lm_in_toolbar = self.models['toolbar-device'][1].has_action('Location Manager')
        lm_in_menubar = self.models['menubar-device'][1].has_action('Location Manager')
        if not pref_in_toolbar and not pref_in_menubar:
            error_dialog(self, _('Preferences missing'), _(
                'The Preferences action must be in either the main toolbar or the menubar.'), show=True)
            raise AbortCommit()
        if not lm_in_toolbar and not lm_in_menubar:
            error_dialog(self, _('Location manager missing'), _(
                'The Location manager must be in either the main toolbar or the menubar when a device is connected.'), show=True)
            raise AbortCommit()

        # Save data.
        for am, cm in self.models.values():
            cm.commit()
        return False

    def restore_defaults(self):
        for am, cm in self.models.values():
            cm.restore_defaults()
            am.restore_defaults()
        self.changed_signal.emit()

    def refresh_gui(self, gui):
        gui.bars_manager.init_bars()
        gui.bars_manager.update_bars()


if __name__ == '__main__':
    from calibre.gui2 import Application
    app = Application([])
    test_widget('Interface', 'Toolbar')
