# -*- coding: utf-8 -*-
#
# Copyright (c) 2007  François Ingelrest (Francois.Ingelrest@gmail.com)
# Copyright (c) 2010  Jendrik Seipp (jendrikseipp@web.de)
#
# This program 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; either version 2 of the License, or
# (at your option) any later version.
#
# This program 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 Library General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA

import os
import sys

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


if __name__ == '__main__':
    base_dir = os.path.abspath(os.path.join(os.path.abspath(__file__), '../../'))
    sys.path.insert(0, base_dir)

from pogo.gui.extTreeview import ExtTreeView

from pogo.tools import consts

# The format of a row in the treeview
(
    ROW_ICO,   # Item icon
    ROW_NAME,  # Item name
    ROW_TRK,   # The track object
) = list(range(3))

# Internal d'n'd (reordering)
DND_REORDERING_ID = 1024
DND_INTERNAL_TARGET = (consts.DND_INTERNAL_TARGET_NAME, Gtk.TargetFlags.SAME_WIDGET, DND_REORDERING_ID)

GObject.signal_new(
    'tracktreeview-dnd', Gtk.TreeView, GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE,
    (Gdk.DragContext, GObject.TYPE_INT, GObject.TYPE_INT, Gtk.SelectionData, GObject.TYPE_INT, GObject.TYPE_PYOBJECT))


class TrackTreeView(ExtTreeView):
    def __init__(self, colums, use_markup=True):
        ExtTreeView.__init__(self, colums, use_markup)

        # Drag'n'drop management
        self.dndTargets = list(consts.DND_TARGETS.values())
        self.motionEvtId = None
        self.dndStartPos = None

        self.dndStartPos = None
        self.isDraggableFunc = lambda: True

        if self.dndTargets:
            # Move one name around while dragging
            # self.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK, \
            #        self.dndTargets+[DND_INTERNAL_TARGET], Gdk.DragAction.MOVE)
            self.enable_model_drag_dest(self.dndTargets, Gdk.DragAction.DEFAULT)

        self.connect('drag-begin', self.onDragBegin)
        self.connect('drag-motion', self.onDragMotion)
        self.connect('drag-data-received', self.onDragDataReceived)

        self.mark = None
        self.dir_selected = True

    def insert(self, target, source_row, drop_mode=None):
        model = self.store
        if drop_mode == Gtk.TreeViewDropPosition.INTO_OR_BEFORE:
            new = model.prepend(target, source_row)
        elif drop_mode == Gtk.TreeViewDropPosition.INTO_OR_AFTER or drop_mode is None:
            new = model.append(target, source_row)
        elif drop_mode == Gtk.TreeViewDropPosition.BEFORE:
            new = model.insert_before(None, target, source_row)
        elif drop_mode == Gtk.TreeViewDropPosition.AFTER:
            new = model.insert_after(None, target, source_row)
        return new

    def appendRow(self, row, parent_iter=None):
        """ Append a row to the tree """
        return self.store.append(parent_iter, row)

    def removeRow(self, iter):
        """ Remove the given row """
        self.store.remove(iter)

    def select_synchronously(self, iter):
        self.get_selection().select_iter(iter)

    def getSelectedRows(self):
        sel_paths = self.selection.get_selected_rows()[1]
        return [self.store.get_iter(path) for path in sel_paths]

    def getFirstSelectedRow(self):
        sel_rows = self.getSelectedRows()
        if sel_rows:
            return sel_rows[0]
        return None

    def iterSelectedRows(self):
        """ Iterate on selected rows """
        for iter in self.getSelectedRows():
            yield iter

    def setItem(self, iter, colIndex, value):
        """ Change the value of the given item """
        self.store.set_value(iter, colIndex, value)

    def getItem(self, iter, colIndex):
        """ Return the value of the given item """
        return self.store.get_value(iter, colIndex)

    def getTrack(self, iter):
        return self.getItem(iter, ROW_TRK)

    def getLabel(self, iter):
        label = self.getItem(iter, ROW_NAME)
        from xml.sax.saxutils import unescape
        label = unescape(label)
        return label

    def setLabel(self, iter, label):
        label = label.replace('_', ' ')
        return self.setItem(iter, ROW_NAME, label)

    def scroll(self, iter):
        self.scroll_to_cell(self.store.get_path(iter))

    def scroll_to_first_selection(self):
        iter = self.getFirstSelectedRow()
        if not iter:
            return
        self.scroll(iter)

    def expand(self, iter):
        self.expand_to_path(self.store.get_path(iter))

    def get_nodename(self, iter):
        if not iter:
            return 'NoneValue'
        return self.store.get_value(iter, 1)

    def get_first_iter(self):
        return self.store.get_iter_first()

    def get_last_iter(self):
        lowest_root = self.get_last_root()
        if lowest_root is None:
            return None
        return self.get_lowest_descendant(lowest_root)

    def get_last_root(self):
        root_nodes = len(self.store)
        if root_nodes == 0:
            return None
        return self.store.iter_nth_child(None, root_nodes - 1)

    def get_all_parents(self, iter):
        """Returns a list of parent, grandparent, greatgrandparent, etc."""
        parent = self.store.iter_parent(iter)
        while parent:
            yield parent
            parent = self.store.iter_parent(parent)

    def iter_prev(self, iter):
        """Return the previous iter on the same level."""
        path = self.store.get_path(iter)
        position = path[-1]
        if position == 0:
            return None
        prev_path = list(path)[:-1]
        prev_path.append(position - 1)
        prev = self.store.get_iter(tuple(prev_path))
        return prev

    def get_prev_iter(self, iter=None):
        '''
        Look for the previous iter in the tree
        '''
        if iter is None:
            iter = self.getMark()
            if iter is None:
                return None

        # Check for a sibling or its children
        prev_iter = self.iter_prev(iter)
        if prev_iter:
            return self.get_lowest_descendant(prev_iter)

        # Check for the parent
        parent_iter = self.store.iter_parent(iter)
        if parent_iter:
            return parent_iter
        return None

    def get_next_iter(self, iter=None):
        '''
        Look for the next iter in the tree
        '''
        if iter is None:
            iter = self.getMark()
            if iter is None:
                return self.get_first_iter()

        # Check for a child
        if self.store.iter_has_child(iter):
            first_child = self.store.iter_nth_child(iter, 0)
            return first_child

        # Check for a sibling
        next_iter = self.store.iter_next(iter)
        if next_iter:
            return next_iter

        # iter has no following siblings -> return uncle
        return self.get_next_iter_on_higher_level(iter)

    def get_next_iter_on_higher_level(self, iter):
        """Goes up in the hierarchy until one parent has a next node."""
        while True:
            parent = self.store.iter_parent(iter)
            if parent is None:
                # We have reached the top of the tree
                return None
            uncle = self.store.iter_next(parent)
            if uncle:
                return uncle
            iter = parent

    def get_prev_iter_or_parent(self, iter):
        """
        Returns the previous node on the same level if there is one or the
        parent iter. If "iter" is the first iter, None is returned.
        """
        prev_iter = self.iter_prev(iter)
        if prev_iter:
            return prev_iter
        parent = self.store.iter_parent(iter)
        return parent

    def get_lowest_descendant(self, iter):
        '''
        Find lowest descendant or return iter
        - a    -> c
          - b  -> b
          - c  -> c
        - d    -> d
        '''
        descendant = None
        if self.store.iter_has_child(iter):
            last_child = self.get_last_child_iter(iter)
            descendant = self.get_lowest_descendant(last_child)
        return descendant or iter

    def get_last_child_iter(self, iter):
        ''''''
        if not self.store.iter_has_child(iter):
            return None
        children = self.store.iter_n_children(iter)
        return self.store.iter_nth_child(iter, children - 1)

    def get_last_iter_on_same_level(self, iter):
        ''''''
        parent = self.store.iter_parent(iter)
        if parent:
            return self.get_last_child_iter(parent)
        while True:
            sibling = self.store.iter_next(iter)
            if not sibling:
                return iter
            iter = sibling

    def iter_children(self, parent=None):
        """ Iterate on the children of the given iter """
        iter = self.store.iter_children(parent)

        while iter is not None:
            yield iter
            iter = self.store.iter_next(iter)

    # --== Mark management ==--

    def hasMark(self):
        """ True if a mark has been set """
        return self.mark is not None and self.mark.valid()

    def clearMark(self):
        """ Remove the mark """
        self.mark = None

    def getMark(self):
        """ Return the iter of the marked row """
        if not self.hasMark():
            return None
        return self.store.get_iter(self.mark.get_path())

    def setMark(self, iter):
        """ Put the mark on the given row, it will move with the row itself (e.g., D'n'D) """
        self.mark = Gtk.TreeRowReference(self.store, self.store.get_path(iter))

    def isAtMark(self, iter):
        '''
        Compare the marker path and the path of iter, because the iters alone
        will not predict equality
        '''
        if not self.hasMark():
            return False
        return self.store.get_path(self.getMark()) == self.store.get_path(iter)

    # DRAG AND DROP

    def select(self, iter):
        '''
        Select and highlight an iter
        '''
        GObject.idle_add(self.get_selection().select_iter, iter)

    def move_selected_rows(self, x, y):
        '''
        Method called when dnd happens inside the treeview
        '''
        drop = self.get_dest_row_at_pos(int(x), int(y))
        selection = self.getSelectedRows()

        model = self.store

        if drop:
            dest, drop_mode = drop
            dest = model.get_iter(dest)
        else:
            # Dropped on free space -> append
            dest, drop_mode = self.get_last_root(), Gtk.TreeViewDropPosition.AFTER

        self.freeze_child_notify()

        # filter selected tracks whose directories have been selected too
        iters = []
        for iter in selection:
            add = True
            for checked_iter in iters:
                if model.is_ancestor(checked_iter, iter):
                    add = False
            if model.is_ancestor(iter, dest):
                # Do not drop ancestors into children
                add = False
            if add:
                iters.append(iter)

        # Move the iters
        for index, iter in enumerate(iters):
            if index > 0:
                drop_mode = Gtk.TreeViewDropPosition.AFTER

            track = self.getTrack(iter)
            if track:
                dest = self.insert(dest, list(model[iter]), drop_mode)
                self.select(dest)

                # adjust track label to __new__ parent
                parent = self.store.iter_parent(dest)
                parent_label = self.getLabel(parent) if parent else None
                self.setLabel(dest, track.get_label(parent_label))

                # Handle Mark
                if self.isAtMark(iter):
                    self.setMark(dest)
            else:
                dest = self.move_dir(iter, dest, drop_mode)

        for iter in iters:
            model.remove(iter)

        self.thaw_child_notify()

        # We want to allow dropping tracks only when we are sure that no dir is
        # selected. This is needed for dnd from nautilus.
        self.dir_selected = True

    def move_dir(self, dir_iter, target, drop_mode):
        '''
        Recursive Method that moves a dir to target
        '''
        children = self.store[dir_iter].iterchildren()
        new_target = self.insert(target, list(self.store[dir_iter]), drop_mode)
        self.select(new_target)
        for child in children:
            child = child.iter
            track = self.getTrack(child)
            if track:
                dest = self.insert(new_target, list(self.store[child]), Gtk.TreeViewDropPosition.INTO_OR_AFTER)
                # Handle Mark
                if self.isAtMark(child):
                    self.setMark(dest)
            else:
                self.move_dir(child, new_target, Gtk.TreeViewDropPosition.INTO_OR_AFTER)
        return new_target

    def enableDNDReordering(self):
        """ Enable the use of Drag'n'Drop to reorder the list """
        self.dndTargets.append(DND_INTERNAL_TARGET)
        self.enable_model_drag_dest(self.dndTargets, Gdk.DragAction.DEFAULT)

    def onDragDataReceived(self, tree, context, x, y, selection, dndId, time):
        """ Some data has been dropped into the list """
        if dndId == DND_REORDERING_ID:
            self.move_selected_rows(x, y)
        else:
            self.emit('tracktreeview-dnd', context, int(x), int(y), selection, dndId, time)

    def onDragBegin(self, tree, context):
        dir_selected = False
        for row in self.getSelectedRows():
            track = self.getTrack(row)
            if not track:
                dir_selected = True
                break
        self.dir_selected = dir_selected
        if dir_selected:
            self.collapse_all()

    def onDragMotion(self, tree, context, x, y, time):
        """
        Allow the following drops:
        - tracks onto and into dir
        - tracks between dirs
        - dir between dirs

        -> Prevent the drops:
        - dir into dir
        - anything into track
        """
        # Disable default handler.
        self.stop_emission('drag-motion')

        drop = self.get_dest_row_at_pos(int(x), int(y))

        pos_ok = True

        if drop is not None:
            iter = self.store.get_iter(drop[0])
            depth = self.store.iter_depth(iter)
            track = self.getTrack(iter)

            drop_into = drop[1] in [Gtk.TreeViewDropPosition.INTO_OR_AFTER, Gtk.TreeViewDropPosition.INTO_OR_BEFORE]
            drop_around = not drop_into

            # At least one dir is being dropped
            if self.dir_selected:
                # Dirs can only be dropped at the top level
                if drop_into or ((drop_around) and depth > 0):
                    # do not let the user drop anything here
                    pos_ok = False
            else:
                # Tracks can also be dropped into dirs (but not into tracks)
                if track and drop_into:
                    pos_ok = False

        target_names = [atom.name() for atom in context.list_targets()]
        internal_reordering = (consts.DND_INTERNAL_TARGET_NAME in target_names)
        if internal_reordering:
            action = Gdk.DragAction.MOVE
        else:
            action = Gdk.DragAction.COPY
        Gdk.drag_status(context, action, time)

        # Return whether the cursor position is in a drop zone.
        return pos_ok


if __name__ == '__main__':
    from gi.repository import GdkPixbuf
    from gi.repository import GObject

    from pogo.tools import icons
    from pogo.media import getTracks

    tracks = getTracks(['/home/jendrik/Musik/Clearlake - Amber'])

    columns = (
        ('', [(Gtk.CellRendererPixbuf(), GdkPixbuf.Pixbuf), (Gtk.CellRendererText(), GObject.TYPE_STRING)], True),
        (None, [(None, GObject.TYPE_INT)], False),
        (None, [(None, GObject.TYPE_STRING)], False),
        (None, [(None, GObject.TYPE_PYOBJECT)], False),
    )

    tree = TrackTreeView(columns, True)

    track = None

    a = tree.appendRow((icons.nullMenuIcon(), 'a', 1, 'something', track), None)
    b = tree.appendRow((icons.nullMenuIcon(), 'b', 1, 'something', track), a)
    c = tree.appendRow((icons.nullMenuIcon(), 'c', 1, 'something', track), a)
    d = tree.appendRow((icons.nullMenuIcon(), 'd', 1, 'something', track), None)

    for iter in [a, b, c, d]:
        next_iter = tree.get_next_iter(iter)
        print(tree.get_nodename(iter), '->', tree.get_nodename(next_iter))

    for iter in [a, b, c, d]:
        uncle = tree.get_next_iter_on_higher_level(iter)
        print('Uncle(%s) = %s' % (tree.get_nodename(iter), tree.get_nodename(uncle)))

    for iter in [a, b, c, d]:
        prev = tree.get_prev_iter(iter)
        print(tree.get_nodename(prev), '<-', tree.get_nodename(iter))

    for iter in [a, b, c, d]:
        res = tree.get_last_iter_on_same_level(iter)
        print('Last Sibling(%s) = %s' % (tree.get_nodename(iter), tree.get_nodename(res)))

    for iter in [a, b, c, d]:
        res = tree.get_lowest_descendant(iter)
        print('Lowest Descendant(%s) = %s' % (tree.get_nodename(iter), tree.get_nodename(res)))

    res = tree.get_last_iter()
    print('Last node: %s' % tree.get_nodename(res))

    res = list(tree.iter_children())
    print('Children of root:', [tree.getLabel(iter) for iter in res])

    res = list(tree.iter_children(a))
    print('Children of a:', [tree.getLabel(iter) for iter in res])

    win = Gtk.Window()
    win.set_size_request(400, 300)
    win.connect('destroy', lambda x: sys.exit())
    win.add(tree)
    tree.expand_all()

    win.show_all()
    Gtk.main()
