#!/usr/bin/env python
# License: GPLv3 Copyright: 2010, Kovid Goyal <kovid@kovidgoyal.net>

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
from calibre.utils.localization import _


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):
    _data: list

    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 Exception:
            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_action(-1))
        connect_lambda(self.action_down_button.clicked, self, lambda self: self._move_action(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):
        all_model = self.all_actions.model()
        assert all_model is not None
        tt = all_model.data(index, Qt.ItemDataRole.ToolTipRole) or ''
        self.help_text.setText(tt)

    def current_entered(self, index):
        cur_model = self.current_actions.model()
        assert cur_model is not None
        tt = cur_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):
        all_sm = self.all_actions.selectionModel()
        assert all_sm is not None
        self._add_action(all_sm.selectedIndexes())

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

    def _add_action(self, indices):
        all_model = self.all_actions.model()
        assert isinstance(all_model, AllModel)
        names = all_model.names(indices)
        if names:
            cur_model = self.current_actions.model()
            assert isinstance(cur_model, CurrentModel)
            not_added = cur_model.add(names)
            ns = {y.name for y in not_added}
            added = set(names) - ns
            all_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
                ca_model = ca.model()
                assert isinstance(ca_model, CurrentModel)
                idx = ca_model.index(ca_model.rowCount() - 1, 0)
                ca.scrollTo(idx)
                self.changed_signal.emit()

    def remove_action(self, *args):
        cur_sm = self.current_actions.selectionModel()
        assert cur_sm is not None
        self._remove_action(cur_sm.selectedIndexes())

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

    def _remove_action(self, indices):
        cur_model = self.current_actions.model()
        assert isinstance(cur_model, CurrentModel)
        names = cur_model.names(indices)
        if names:
            not_removed = cur_model.remove(indices)
            ns = {y.name for y in not_removed}
            removed = set(names) - ns
            all_model = self.all_actions.model()
            assert isinstance(all_model, AllModel)
            all_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_action(self, delta: int) -> None:
        sm = self.current_actions.selectionModel()
        assert sm is not None
        x = sm.selectedIndexes()
        if x and len(x):
            i = sm.currentIndex().row()
            m = self.current_actions.model()
            assert isinstance(m, CurrentModel)
            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, *args):
        # 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, *args):
        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')
