from gi.repository import Gtk, Gdk, GObject
import logging

from gettext import gettext as _

from softwarecenter.ui.gtk3.session.appmanager import get_appmanager


from cellrenderers import (CellRendererAppView,
                           CellButtonRenderer,
                           CellButtonIDs)

from softwarecenter.ui.gtk3.em import em, StockEms
from softwarecenter.enums import (AppActions, Icons)
from softwarecenter.utils import ExecutionTime
from softwarecenter.backend import get_install_backend
from softwarecenter.netstatus import (get_network_watcher,
                                      network_state_is_connected)
from softwarecenter.ui.gtk3.models.appstore2 import (
    AppGenericStore, CategoryRowReference)


LOG = logging.getLogger(__name__)


class AppTreeView(Gtk.TreeView):

    """Treeview based view component that takes a AppStore and displays it"""

    VARIANT_INFO = 0
    VARIANT_REMOVE = 1
    VARIANT_INSTALL = 2
    VARIANT_PURCHASE = 3

    ACTION_BTNS = (VARIANT_REMOVE, VARIANT_INSTALL, VARIANT_PURCHASE)

    def __init__(self, app_view, db, icons, show_ratings, store=None):
        Gtk.TreeView.__init__(self)
        self._logger = logging.getLogger("softwarecenter.view.appview")

        self.app_view = app_view
        self.db = db

        self.pressed = False
        self.focal_btn = None
        self._action_block_list = []
        self._needs_collapse = []
        self.expanded_path = None
        self.selected_row_renderer = None

        # pixbuf for the icon that is displayed in the selected row
        self.selected_row_icon = None

        #~ # if this hacked mode is available everything will be fast
        #~ # and we can set fixed_height mode and still have growing rows
        #~ # (see upstream gnome #607447)
        try:
            self.set_property("ubuntu-almost-fixed-height-mode", True)
            self.set_fixed_height_mode(True)
        except:
            self._logger.warn(
                "ubuntu-almost-fixed-height-mode extension not available")

        self.set_headers_visible(False)

        # our custom renderer
        self._renderer = CellRendererAppView(icons,
                                 self.create_pango_layout(''),
                                 show_ratings,
                                 Icons.INSTALLED_OVERLAY)
        self._renderer.set_pixbuf_width(32)
        self._renderer.set_button_spacing(em(0.3))

        # create buttons and set initial strings
        info = CellButtonRenderer(self,
                                  name=CellButtonIDs.INFO)
        info.set_markup_variants(
                    {self.VARIANT_INFO: _('More Info')})

        action = CellButtonRenderer(self,
                                    name=CellButtonIDs.ACTION)

        action.set_markup_variants(
                {self.VARIANT_INSTALL: _('Install'),
                 self.VARIANT_REMOVE: _('Remove'),
                 self.VARIANT_PURCHASE: _(u'Buy\u2026')})

        self._renderer.button_pack_start(info)
        self._renderer.button_pack_end(action)

        self._column = Gtk.TreeViewColumn(
            "Applications", self._renderer,
            application=AppGenericStore.COL_ROW_DATA)
        self._column.set_cell_data_func(
            self._renderer, self._cell_data_func_cb)
        self._column.set_fixed_width(200)
        self._column.set_sizing(Gtk.TreeViewColumnSizing.FIXED)
        self.append_column(self._column)

        # network status watcher
        watcher = get_network_watcher()
        watcher.connect("changed", self._on_net_state_changed, self._renderer)

        # custom cursor
        self._cursor_hand = Gdk.Cursor.new(Gdk.CursorType.HAND2)

        self.connect("style-updated", self._on_style_updated, self._renderer)
        # button and motion are "special"
        self.connect("button-press-event", self._on_button_press_event,
                     self._renderer)
        self.connect("button-release-event", self._on_button_release_event,
                     self._renderer)
        self.connect("key-press-event", self._on_key_press_event,
                     self._renderer)
        self.connect("key-release-event", self._on_key_release_event,
                     self._renderer)
        self.connect("motion-notify-event", self._on_motion, self._renderer)
        self.connect("cursor-changed", self._on_cursor_changed, self._renderer)
        # our own "activate" handler
        self.connect("row-activated", self._on_row_activated, self._renderer)

        self.backend = get_install_backend()
        self._transactions_connected = False
        self.connect('realize', self._on_realize, self._renderer)

    @property
    def appmodel(self):
        model = self.get_model()
        # FIXME: would be nice if that would be less ugly,
        # because we use a treefilter we need to get the "real"
        # model first
        if isinstance(model, Gtk.TreeModelFilter):
            return model.get_model()
        return model

    def clear_model(self):
        vadjustment = self.get_scrolled_window_vadjustment()
        if vadjustment:
            vadjustment.set_value(0)
        self.expanded_path = None
        self._needs_collapse = []
        if self.appmodel:
            # before clearing the model, disconnect it from the view. this
            # avoids that the model gets a "cursor_changed" signal for each
            # removed row and consequently that _update_selected_row is called
            # for rows that are not really selected (LP: #969050)
            model = self.get_model()
            self.set_model(None)
            model.clear()
            self.set_model(model)

    def set_model(self, model):
        # the set_cell_data_func() calls here are a workaround for bug
        # LP: #986186 - once that is fixed upstream we can revert this
        # and remove the entire "set_model" again
        self._column.set_cell_data_func(self._renderer, None)
        Gtk.TreeView.set_model(self, model)
        self._column.set_cell_data_func(
            self._renderer, self._cell_data_func_cb)

    def expand_path(self, path):
        if path is not None and not isinstance(path, Gtk.TreePath):
            raise TypeError("Expects Gtk.TreePath or None, got %s" %
                              type(path))

        model = self.get_model()
        old = self.expanded_path
        self.expanded_path = path

        if old is not None:
            start, end = self.get_visible_range() or (None, None)
            if ((start and start.compare(old) != -1) or
                (end and end.compare(old) != 1)):
                self._needs_collapse.append(old)
            else:
                try:  # try... a lazy solution to Bug #846204
                    model.row_changed(old, model.get_iter(old))
                except:
                    LOG.exception(
                        "apptreeview.expand_path: Supplied 'old' "
                        "path is an invalid tree path: '%s'" % old)

        if path == None:
            return

        model.row_changed(path, model.get_iter(path))

    def get_scrolled_window_vadjustment(self):
        ancestor = self.get_ancestor(Gtk.ScrolledWindow)
        if ancestor:
            return ancestor.get_vadjustment()

    def get_rowref(self, model, path):
        if path is not None:
            return model[path][AppGenericStore.COL_ROW_DATA]

    def rowref_is_category(self, rowref):
        return isinstance(rowref, CategoryRowReference)

    def reset_action_button(self):
        """ Set the current row's action button sensitivity to the
            specified value
        """
        if self.selected_row_renderer:
            action_btn = self.selected_row_renderer.get_button_by_name(
                CellButtonIDs.ACTION)
            if action_btn:
                action_btn.set_sensitive(True)
                pkgname = self.db.get_pkgname(self.selected_doc)
                self._check_remove_pkg_from_blocklist(pkgname)

    def _on_realize(self, widget, tr):
        # connect to backend events once self is realized so handlers
        # have access to the TreeView's initialised Gdk.Window
        if self._transactions_connected:
            return
        self.backend.connect("transaction-started",
            self._on_transaction_started, tr)
        self.backend.connect("transaction-finished",
            self._on_transaction_finished, tr)
        self.backend.connect("transaction-stopped",
            self._on_transaction_stopped, tr)
        self._transactions_connected = True

    def _calc_row_heights(self, tr):
        ypad = StockEms.SMALL
        tr.set_property('xpad', StockEms.MEDIUM)
        tr.set_property('ypad', ypad)

        for btn in tr.get_buttons():
            # recalc button geometry and cache
            btn.configure_geometry(self.create_pango_layout(""))

        btn_h = btn.height

        tr.normal_height = max(32 + 2 * ypad, em(2.5) + ypad)
        tr.selected_height = tr.normal_height + btn_h + StockEms.MEDIUM

    def _on_style_updated(self, widget, tr):
        self._calc_row_heights(tr)

    def _on_motion(self, tree, event, tr):
        window = self.get_window()
        x, y = int(event.x), int(event.y)
        if not self._xy_is_over_focal_row(x, y):
            window.set_cursor(None)
            return

        path = tree.get_path_at_pos(x, y)
        if not path:
            window.set_cursor(None)
            return

        rowref = self.get_rowref(tree.get_model(), path[0])
        if not rowref:
            return

        if self.rowref_is_category(rowref):
            window.set_cursor(None)
            return

        model = tree.get_model()
        app = model[path[0]][AppGenericStore.COL_ROW_DATA]
        if (not network_state_is_connected() and
            not self.appmodel.is_installed(app)):
            for btn_id in self.ACTION_BTNS:
                btn_id = tr.get_button_by_name(CellButtonIDs.ACTION)
                btn_id.set_sensitive(False)

        use_hand = False
        for btn in tr.get_buttons():
            if btn.state == Gtk.StateFlags.INSENSITIVE:
                continue

            if btn.point_in(x, y):
                use_hand = True
                if self.focal_btn is btn:
                    btn.set_state(Gtk.StateFlags.ACTIVE)
                elif not self.pressed:
                    btn.set_state(Gtk.StateFlags.PRELIGHT)
            else:
                if btn.state != Gtk.StateFlags.NORMAL:
                    btn.set_state(Gtk.StateFlags.NORMAL)

        if use_hand:
            window.set_cursor(self._cursor_hand)
        else:
            window.set_cursor(None)

    def _on_cursor_changed(self, view, tr):
        model = view.get_model()
        sel = view.get_selection()
        path = view.get_cursor()[0]

        rowref = self.get_rowref(model, path)
        if not rowref:
            return

        if self.has_focus():
            self.grab_focus()

        if self.rowref_is_category(rowref):
            self.expand_path(None)
            return

        sel.select_path(path)
        self._update_selected_row(view, tr, path)

    def _update_selected_row(self, view, tr, path=None):
        # keep track of the currently selected row renderer and associated
        # doc for use when updating the widgets and for use with the Unity
        # integration feature
        self.selected_row_renderer = tr
        self.selected_doc = tr.application
        sel = view.get_selection()
        if not sel:
            return False
        model, rows = sel.get_selected_rows()
        if not rows:
            return False
        row = rows[0]
        if self.rowref_is_category(row):
            return False

        # update active app, use row-ref as argument
        self.expand_path(row)

        app = model[row][AppGenericStore.COL_ROW_DATA]

        # make sure this is not a category (LP: #848085)
        if self.rowref_is_category(app):
            return False

        action_btn = tr.get_button_by_name(
                            CellButtonIDs.ACTION)
        #if not action_btn: return False

        if self.appmodel.is_installed(app):
            action_btn.set_variant(self.VARIANT_REMOVE)
            action_btn.set_sensitive(True)
            action_btn.show()
        elif self.appmodel.is_available(app):
            if self.appmodel.is_purchasable(app):
                action_btn.set_variant(self.VARIANT_PURCHASE)
            else:
                action_btn.set_variant(self.VARIANT_INSTALL)

            action_btn.set_sensitive(True)
            action_btn.show()

            if not network_state_is_connected():
                action_btn.set_sensitive(False)
                self.app_view.emit("application-selected",
                                   self.appmodel.get_application(app))
                return
        else:
            action_btn.set_sensitive(False)
            action_btn.hide()
            self.app_view.emit("application-selected",
                               self.appmodel.get_application(app))
            return

        if self.appmodel.get_transaction_progress(app) > 0:
            action_btn.set_sensitive(False)
        elif self.pressed and self.focal_btn == action_btn:
            action_btn.set_state(Gtk.StateFlags.ACTIVE)
        else:
            action_btn.set_state(Gtk.StateFlags.NORMAL)

        self.app_view.emit(
            "application-selected", self.appmodel.get_application(app))
        return False

    def _on_row_activated(self, view, path, column, tr):
        rowref = self.get_rowref(view.get_model(), path)

        if not rowref:
            return
        elif self.rowref_is_category(rowref):
            return

        x, y = self.get_pointer()
        for btn in tr.get_buttons():
            if btn.point_in(x, y):
                return

        app = self.appmodel.get_application(rowref)
        if app:
            self.app_view.emit("application-activated", app)

    def _on_button_event_get_path(self, view, event, allow_categories=False):
        if event.button != 1:
            return False

        res = view.get_path_at_pos(int(event.x), int(event.y))
        if not res:
            return False

        # check the path is valid and is not a category row
        path = res[0]
        is_cat = self.rowref_is_category(
            self.get_rowref(view.get_model(), path))

        if path is None:
            return False

        if is_cat:
            if allow_categories:
                return path
            else:
                return False

        # only act when the selection is already there
        selection = view.get_selection()
        if not selection.path_is_selected(path):
            return False

        return path

    def _on_button_press_event(self, view, event, tr):
        path = self._on_button_event_get_path(view, event,
                                              allow_categories=False)
        if not path:
            path = self._on_button_event_get_path(view, event,
                                                  allow_categories=True)
            if path:
                if view.row_expanded(path):
                    view.collapse_row(path)
                else:
                    view.expand_row(path, True)
                return True  # swallow event to avoid double action when
                             # clicking on the expander arrow itself
            return

        self.pressed = True
        x, y = int(event.x), int(event.y)
        for btn in tr.get_buttons():
            if (btn.point_in(x, y) and
                (btn.state != Gtk.StateFlags.INSENSITIVE)):
                self.focal_btn = btn
                btn.set_state(Gtk.StateFlags.ACTIVE)
                view.queue_draw()
                return
        self.focal_btn = None

    def _on_button_release_event(self, view, event, tr):
        path = self._on_button_event_get_path(view, event)
        if not path:
            return

        self.pressed = False
        x, y = int(event.x), int(event.y)
        for btn in tr.get_buttons():
            if (btn.point_in(x, y) and
                (btn.state != Gtk.StateFlags.INSENSITIVE)):
                btn.set_state(Gtk.StateFlags.NORMAL)
                self.get_window().set_cursor(self._cursor_hand)
                if self.focal_btn is not btn:
                    break
                self._init_activated(btn, view.get_model(), path)
                view.queue_draw()
                break
        self.focal_btn = None

    def _on_key_press_event(self, widget, event, tr):
        kv = event.keyval
        #print kv
        r = False
        if kv == Gdk.KEY_Right:  # right-key
            btn = tr.get_button_by_name(CellButtonIDs.ACTION)
            if btn is None:
                return  # Bug #846779
            if btn.state != Gtk.StateFlags.INSENSITIVE:
                btn.has_focus = True
                btn = tr.get_button_by_name(CellButtonIDs.INFO)
                btn.has_focus = False
        elif kv == Gdk.KEY_Left:  # left-key
            btn = tr.get_button_by_name(CellButtonIDs.ACTION)
            if btn is None:
                return  # Bug #846779
            btn.has_focus = False
            btn = tr.get_button_by_name(CellButtonIDs.INFO)
            btn.has_focus = True
        elif kv == Gdk.KEY_space:  # spacebar
            for btn in tr.get_buttons():
                if (btn is not None and btn.has_focus and
                    btn.state != Gtk.StateFlags.INSENSITIVE):
                    btn.set_state(Gtk.StateFlags.ACTIVE)
                    sel = self.get_selection()
                    model, it = sel.get_selected()
                    path = model.get_path(it)
                    if path:
                        #self._init_activated(btn, self.get_model(), path)
                        r = True
                    break

        self.queue_draw()
        return r

    def _on_key_release_event(self, widget, event, tr):
        kv = event.keyval
        r = False
        if kv == 32:    # spacebar
            for btn in tr.get_buttons():
                if btn.has_focus and btn.state != Gtk.StateFlags.INSENSITIVE:
                    btn.set_state(Gtk.StateFlags.NORMAL)
                    sel = self.get_selection()
                    model, it = sel.get_selected()
                    path = model.get_path(it)
                    if path:
                        self._init_activated(btn, model, path)
                        btn.has_focus = False
                        r = True
                    break

        self.queue_draw()
        return r

    def _init_activated(self, btn, model, path):
        app = model[path][AppGenericStore.COL_ROW_DATA]
        s = Gtk.Settings.get_default()
        GObject.timeout_add(s.get_property("gtk-timeout-initial"),
                            self._app_activated_cb,
                            btn,
                            btn.name,
                            app,
                            path)

    def _cell_data_func_cb(self, col, cell, model, it, user_data):
        path = model.get_path(it)

        # this will pre-load data *only* on a AppListStore, it has
        # no effect with a AppTreeStore
        if model[path][0] is None:
            indices = path.get_indices()
            model.load_range(indices, 5)

        if path in self._needs_collapse:
            # collapse rows that were outside the visible range and
            # thus not immediately collapsed when expand_path was called
            cell.set_property('isactive', False)
            i = self._needs_collapse.index(path)
            del self._needs_collapse[i]
            model.row_changed(path, it)
            return

        # update active property
        cell.set_property('isactive', path == self.expanded_path)
        # update "text" property for a11y
        raw = model[path][AppGenericStore.COL_ROW_DATA]
        if self.rowref_is_category(raw):
            text = raw.display_name
        elif raw:
            text = self.db.get_pkgname(raw)
        else:
            # this can happen for empty/not-yet-loaded row, LP: #981992
            text = ""
        cell.set_property('text', text)

    def _app_activated_cb(self, btn, btn_id, app, path):
        if self.rowref_is_category(app):
            return
        model = self.appmodel
        # don't continue if we don't have a valid model (LP: #969907)
        if not model:
            return

        pkgname = model.get_pkgname(app)

        if btn_id == CellButtonIDs.INFO:
            self.app_view.emit("application-activated",
                               model.get_application(app))
        elif btn_id == CellButtonIDs.ACTION:
            btn.set_sensitive(False)
            model.row_changed(path, model.get_iter(path))
            app_manager = get_appmanager()
            # be sure we dont request an action for a pkg with
            # pre-existing actions
            if pkgname in self._action_block_list:
                logging.debug("Action already in progress for package:"
                              " '%s'" % pkgname)
                return False
            self._action_block_list.append(pkgname)
            if model.is_installed(app):
                action = AppActions.REMOVE
            elif model.is_purchasable(app):
                app_manager.buy_app(model.get_application(app))
                model.notify_action_request(app, path)
                return
            else:
                action = AppActions.INSTALL

            model.notify_action_request(app, path)

            app_manager.request_action(
                model.get_application(app), [], [],
                action)
        return False

    def _set_cursor(self, btn, cursor):
        # make sure we have a window instance (LP: #617004)
        window = self.get_window()
        if isinstance(window, Gdk.Window):
            x, y = self.get_pointer()
            if btn.point_in(x, y):
                window.set_cursor(cursor)

    def _on_transaction_started(self, backend, pkgname, appname, trans_id,
        trans_type, tr):
        """callback when an application install/remove transaction has
        started
        """
        action_btn = tr.get_button_by_name(CellButtonIDs.ACTION)
        if action_btn:
            action_btn.set_sensitive(False)
            self._set_cursor(action_btn, None)

    def _on_transaction_finished(self, backend, result, tr):
        """callback when an application install/remove transaction has
        finished
        """
        # need to send a cursor-changed so the row button is properly updated
        self.emit("cursor-changed")
        # remove pkg from the block list
        self._check_remove_pkg_from_blocklist(result.pkgname)

        action_btn = tr.get_button_by_name(CellButtonIDs.ACTION)
        if action_btn:
            action_btn.set_sensitive(True)
            self._set_cursor(action_btn, self._cursor_hand)

    def _on_transaction_stopped(self, backend, result, tr):
        """callback when an application install/remove transaction has
        stopped
        """
        # remove pkg from the block list
        self._check_remove_pkg_from_blocklist(result.pkgname)

        action_btn = tr.get_button_by_name(CellButtonIDs.ACTION)
        if action_btn:
            # this should be a function that decides action button state label
            if action_btn.current_variant == self.VARIANT_INSTALL:
                action_btn.set_markup(self.VARIANT_REMOVE)
            action_btn.set_sensitive(True)
            self._set_cursor(action_btn, self._cursor_hand)

    def _on_net_state_changed(self, watcher, state, tr):
        self._update_selected_row(self, tr)
        # queue a draw just to be sure the view is looking right
        self.queue_draw()

    def _check_remove_pkg_from_blocklist(self, pkgname):
        if pkgname in self._action_block_list:
            i = self._action_block_list.index(pkgname)
            del self._action_block_list[i]

    def _xy_is_over_focal_row(self, x, y):
        res = self.get_path_at_pos(x, y)
        #cur = self.get_cursor()
        if not res:
            return False
        return self.get_path_at_pos(x, y)[0] == self.get_cursor()[0]


def get_test_window():
    import softwarecenter.log
    softwarecenter.log.root.setLevel(level=logging.DEBUG)
    softwarecenter.log.add_filters_from_string("performance")
    fmt = logging.Formatter("%(name)s - %(message)s", None)
    softwarecenter.log.handler.setFormatter(fmt)

    from softwarecenter.testutils import (
        get_test_db, get_test_pkg_info, get_test_gtk3_icon_cache,
        get_test_categories)

    cache = get_test_pkg_info()
    db = get_test_db()
    icons = get_test_gtk3_icon_cache()

    # create a filter
    from softwarecenter.db.appfilter import AppFilter
    filter = AppFilter(db, cache)
    filter.set_supported_only(False)
    filter.set_installed_only(True)

    # get the TREEstore
    from softwarecenter.ui.gtk3.models.appstore2 import AppTreeStore
    store = AppTreeStore(db, cache, icons)

    # populate from data
    cats = get_test_categories(db)
    for cat in cats[:3]:
        with ExecutionTime("query cat '%s'" % cat.name):
            docs = db.get_docs_from_query(cat.query)
            store.set_category_documents(cat, docs)

    # ok, this is confusing - the AppView contains the AppTreeView that
    #                         is a tree or list depending on the model
    from softwarecenter.ui.gtk3.views.appview import AppView
    app_view = AppView(db, cache, icons, show_ratings=True)
    app_view.set_model(store)

    box = Gtk.VBox()
    box.pack_start(app_view, True, True, 0)

    win = Gtk.Window()
    win.add(box)
    win.connect("destroy", lambda x: Gtk.main_quit())
    win.set_size_request(600, 400)
    win.show_all()

    return win


if __name__ == "__main__":
    win = get_test_window()
    Gtk.main()
