"""
filebrowser --- Dock with file system tree
==========================================
"""
import sys
import os
import os.path
import operator
import logging

from PyQt5.QtCore import QDir, QModelIndex, QObject, Qt, QTimer, \
    pyqtSignal
from PyQt5.QtWidgets import QAction, QCompleter, QDirModel, \
    QFrame, QFileSystemModel, \
    QComboBox, \
    QShortcut, QTreeView, QVBoxLayout, QWidget
from PyQt5.QtGui import QIcon, QKeySequence
from PyQt5.QtCore import QSortFilterProxyModel, QItemSelectionModel

from enki.widgets.dockwidget import DockWidget

from enki.core.defines import CONFIG_DIR
from enki.core.core import core
import enki.core.json_wrapper


def _getCurDir():
    """Get process current directory
    """
    try:
        return os.path.abspath(str(os.curdir))
    except OSError:  # current directory might have been deleted
        return ''


class Plugin(QObject):
    """File system tree.

    Allows to open files quickly
    """

    def __init__(self):
        """Create and install the plugin
        """
        QObject.__init__(self)
        # create dock
        self.dock = DockFileBrowser(core.mainWindow())
        self.dock.hide()
        # add dock to dock toolbar entry
        core.mainWindow().addDockWidget(Qt.LeftDockWidgetArea, self.dock)

    def terminate(self):
        """Uninstall the plugin
        """
        self.dock.terminate()


class FileBrowserFilteredModel(QSortFilterProxyModel):
    """Model filters out files using negative filter.
    i.e. does not show .o .pyc and other temporary files
    """

    def __init__(self, parent):
        QSortFilterProxyModel.__init__(self, parent)
        core.fileFilter().regExpChanged.connect(self.invalidate)

    def columnCount(self, parent=QModelIndex()):  # pylint: disable=W0613
        """Column count for the model
        """
        return 1

    def hasChildren(self, parent=QModelIndex()):
        """Check if node has children. QAbstractItemModel standard method
        """
        return self.sourceModel().hasChildren(self.mapToSource(parent))

    def filterAcceptsRow(self, sourceRow, sourceParent):
        """ Main method. Check if file matches filter
        """
        if sourceParent == QModelIndex():
            return True
        return not core.fileFilter().regExp().match(sourceParent.child(sourceRow, 0).data())


class SmartRecents(QObject):
    """Class stores File Browser recent directories and provides variants for combo box.

    "Active directory" in this class means the last directory, where one or more files has been opened
    """
    MAX_RECENTS_SIZE = 5
    FILE_PATH = os.path.join(CONFIG_DIR, 'file_browser_popular_dirs.json')

    STATISTICS_SIZE = 50.
    BONUS_FOR_OPENING = 1.
    MAX_POINTS_COUNT = 100.

    _recentsChanged = pyqtSignal(list)

    def __init__(self, fileBrowser):
        QObject.__init__(self)
        self._currDir = None
        self._currIsActive = False
        self._popularDirs = None
        self._loadPopularDirs()
        core.workspace().currentDocumentChanged.connect(self._updateRecents)

        # incoming connections
        fileBrowser.rootChanged.connect(self._onRootChanged)
        fileBrowser.fileActivated.connect(self._onFileActivated)
        # outgoing connections
        self._recentsChanged.connect(fileBrowser.updateComboItems)

    def _loadPopularDirs(self):
        """Load popular directories from the config
        """
        self._popularDirs = enki.core.json_wrapper.load(self.FILE_PATH, 'file browser popular directories', {})

        for k in self._popularDirs.keys():
            try:
                self._popularDirs[k] = float(self._popularDirs[k])
            except ValueError as ex:
                logging.error('Invalid PopularDirs value: ' + str(ex))
                self._popularDirs[k] = 0.0

    def _savePopularDirs(self):
        """Save dirs to file
        """
        enki.core.json_wrapper.dump(self.FILE_PATH, 'file browser popular directories', self._popularDirs)

    def _dirsByPopularity(self):
        """Return list of dirrectories, sorted by popularity
        """
        if not self._popularDirs:
            return ()

        dirAndPopularity = sorted(iter(self._popularDirs.items()), key=operator.itemgetter(1), reverse=True)
        dirs = [dp[0] for dp in dirAndPopularity]  # take only first elements
        return dirs

    def _onFileActivated(self):
        """FileBrowserDock notifies SmartRecents that file has been activated
        """
        if self._currIsActive:  # statistics already has been updated
            return

        self._currIsActive = True

        # Replace the least popular
        if self._currDir not in self._popularDirs:
            if len(self._popularDirs) == self.STATISTICS_SIZE:
                leastPopular = self._dirsByPopularity()[-1]
                del self._popularDirs[leastPopular]
            self._popularDirs[self._currDir] = 0

        self._popularDirs[self._currDir] += self.BONUS_FOR_OPENING

        # Normalization
        pointsSum = sum(self._popularDirs.values())
        multiplier = self.MAX_POINTS_COUNT / pointsSum
        if multiplier < 1:
            for k in self._popularDirs.keys():
                self._popularDirs[k] *= multiplier

        self._savePopularDirs()

        # History update is not scheduled here, because it will be scheduled when workspace changes current file

    def _onRootChanged(self, newCurrDir):
        """FileBrowserDock notifies SmartRecents that user changed current directory
        """
        self._currDir = newCurrDir
        self._currIsActive = False
        self._updateRecents()

    def _updateRecents(self):
        """Generate new list of directories, which will be shown in the combo box.
        Emit this list
        """
        # Popular directories
        history = [path for path in self._dirsByPopularity()
                   if os.path.isdir(path) and
                   path != self._currDir]
        # leave not more than MAX_RECENTS_SIZE
        if len(history) > self.MAX_RECENTS_SIZE:
            history = history[:self.MAX_RECENTS_SIZE]

        # now sort by path
        history = sorted(history)

        self._recentsChanged.emit(history)


class SmartHistory(QObject):
    """Class remembers file browser history and manages Back and Forward buttons
    """

    def __init__(self, fileBrowser):
        QObject.__init__(self)
        self._fileBrowser = fileBrowser
        self._currDir = None
        self._currIsActive = False
        self._history = []
        self._historyIndex = -1

        fileBrowser.titleBarWidget().addSeparator()
        self._aBack = QAction(QIcon(':enkiicons/previous.png'),
                              self.tr("Back"),
                              self)
        self._aBack.setShortcut('Alt+Left')
        fileBrowser.titleBarWidget().addAction(self._aBack)
        core.actionManager().addAction("mNavigation/mFileBrowser/aBack", self._aBack)
        self._aBack.triggered.connect(self._onTbBackTriggered)

        self._aForward = QAction(QIcon(':enkiicons/next.png'),
                                 self.tr("Forward"),
                                 self)
        self._aForward.setShortcut('Alt+Right')
        fileBrowser.titleBarWidget().addAction(self._aForward)
        core.actionManager().addAction("mNavigation/mFileBrowser/aForward", self._aForward)
        self._aForward.triggered.connect(self._onTbForwardTriggered)

        fileBrowser.titleBarWidget().addSeparator()

        # incoming connections
        fileBrowser.rootChanged.connect(self._onRootChanged)
        fileBrowser.fileActivated.connect(self._onFileActivated)

    def terminate(self):
        """Explicitly called destructor
        """
        core.actionManager().removeAction("mNavigation/mFileBrowser/aBack")
        core.actionManager().removeAction("mNavigation/mFileBrowser/aForward")

    def _onRootChanged(self, newCurrDir):
        """FileBrowserDock notifies SmartHistory that root has been changed
        """
        self._currDir = newCurrDir
        self._currIsActive = False
        self._updateActions()

    def _onFileActivated(self):
        """FileBrowserDock notifies SmartHistory that file has been activated
        """
        if self._currIsActive:
            return
        self._currIsActive = True
        self._updateHistory()

    def _updateHistory(self):
        """Directory has been activated. Update history
        """
        if(self._history and
           self._history[self._historyIndex] == self._currDir):
            return  # Do nothing, if moved back or forward

        if (self._historyIndex + 1) < len(self._history):  # not on the top of the stack
            # Cut history
            self._history = self._history[:self._historyIndex + 1]

        # Append new root to the history
        self._history.append(self._currDir)
        self._historyIndex += 1

        self._updateActions()

    def _onTbBackTriggered(self):
        """Back action handler
        """
        if not self._currIsActive:
            self._updateHistory()

        self._historyIndex -= 1
        self._fileBrowser.setCurrentPath(self._history[self._historyIndex])

    def _onTbForwardTriggered(self):
        """Forward action handler
        """
        self._historyIndex += 1
        self._fileBrowser.setCurrentPath(self._history[self._historyIndex])

    def _updateActions(self):
        """Update actions enabled state
        """
        if self._history and self._currDir != self._history[self._historyIndex]:
            self._aBack.setEnabled(True)
            self._aBack.setStatusTip(self._history[-1])
            self._aBack.setToolTip(self._history[-1])
        elif self._history and self._historyIndex > 0:
            self._aBack.setEnabled(True)
            self._aBack.setStatusTip(self._history[self._historyIndex - 1])
            self._aBack.setToolTip(self._history[self._historyIndex - 1])
        else:
            self._aBack.setEnabled(False)
            self._aBack.setStatusTip(self.tr("Back"))
            self._aBack.setToolTip(self.tr("Back"))

        if (self._historyIndex + 1) < len(self._history):
            self._aForward.setEnabled(True)
            self._aForward.setStatusTip(self._history[self._historyIndex + 1])
            self._aForward.setToolTip(self._history[self._historyIndex + 1])
        else:
            self._aForward.setEnabled(False)
            self._aForward.setStatusTip(self.tr("Forward"))
            self._aForward.setToolTip(self.tr("Forward"))


class _FileSystemModel(QFileSystemModel):
    """Extended QFileSystemModel.
    Shows full path on tool tips
    """

    def __init__(self, *args):
        QFileSystemModel.__init__(self, *args)

    def data(self, index, role):
        if role == Qt.ToolTipRole:
            return self.filePath(index)
        else:
            return QFileSystemModel.data(self, index, role)


class Tree(QTreeView):
    """File system tree
    """

    _fileActivated = pyqtSignal()

    def __init__(self, fileBrowser):
        QTreeView.__init__(self, fileBrowser)

        self._fileBrowser = fileBrowser

        self.setAttribute(Qt.WA_MacShowFocusRect, False)
        self.setAttribute(Qt.WA_MacSmallSize)
        self.setContextMenuPolicy(Qt.ActionsContextMenu)
        self.setHeaderHidden(True)
        self.setUniformRowHeights(True)
        self.setTextElideMode(Qt.ElideMiddle)

        # dir model
        self._dirsModel = _FileSystemModel(self)
        self._dirsModel.setNameFilterDisables(False)
        self._dirsModel.setFilter(QDir.AllDirs | QDir.AllEntries | QDir.CaseSensitive | QDir.NoDotAndDotDot)
        # self._dirsModel.directoryLoaded.connect(self.setFocus)  TODO don't have this signal in my Qt version

        # create proxy model
        self._filteredModel = FileBrowserFilteredModel(self)
        self._filteredModel.setSourceModel(self._dirsModel)

        self.setModel(self._filteredModel)

        if not sys.platform.startswith('win'):
            self._dirsModel.setRootPath("/")
        else:
            self._dirsModel.setRootPath('')

        # shortcut accessible only when self._tree has focus
        self._upShortcut = QShortcut(QKeySequence("BackSpace"), self)
        self._upShortcut.setContext(Qt.WidgetShortcut)
        self._upShortcut.activated.connect(self.moveUp)

        # shortcut accessible only when self._tree has focus
        self._homeShortcut = QShortcut(QKeySequence("`"), self)
        self._homeShortcut.setContext(Qt.WidgetShortcut)
        self._homeShortcut.activated.connect(self._goUserHomeDir)

        # shortcut accessible only when self._tree has focus
        self._homeShortcut = QShortcut(QKeySequence("."), self)
        self._homeShortcut.setContext(Qt.WidgetShortcut)
        self._homeShortcut.activated.connect(self._goCurrentDir)

        self.activated.connect(self._onActivated)
        self._fileActivated.connect(fileBrowser.fileActivated)

        # QDirModel loads item asynchronously, therefore we need timer for setting focus to the first item
        self._setFocusTimer = QTimer()
        self._setFocusTimer.timeout.connect(self._setFirstItemAsCurrent)
        self._setFocusTimer.setInterval(50)
        self._timerAttempts = 0

    def term(self):
        self._setFocusTimer.stop()

    def _onActivated(self, idx):
        """File or directory doubleClicked
        """
        index = self._filteredModel.mapToSource(idx)
        path = self._dirsModel.filePath(index)

        if os.path.isdir(path):
            self._fileBrowser.setCurrentPath(path)
        else:
            self._fileActivated.emit()
            core.workspace().openFile(path)
            core.workspace().focusCurrentDocument()

    def moveUp(self):
        """User pressed Up key or button. Move focus and root up
        """
        current = self.currentIndex()
        if not current.isValid():
            current = self.rootIndex().child(0, 0)
            self.setCurrentIndex(current)

        if current.parent() == self.rootIndex() or \
           current == self.rootIndex():  # need to move root up
            if self.rootIndex().parent().isValid():  # not reached root of the FS tree
                newRoot = self.rootIndex().parent()
                parentPath = self._filteredModelIndexToPath(current.parent())
                self._fileBrowser.setCurrentPath(self._filteredModelIndexToPath(newRoot))
                self.collapseAll()  # if moving root up - collapse all items
                parentIndex = self._filteredModel.mapFromSource(self._dirsModel.index(parentPath))
                self._setCurrentItem(parentIndex)
        else:  # need to move selection up
            parentOfCurrent = self.currentIndex().parent()
            self._setCurrentItem(parentOfCurrent)  # move selection up

    def _goUserHomeDir(self):
        """Go to home directory
        """
        self._fileBrowser.setCurrentPath(os.path.expanduser("~"))
        self.collapseAll()

    def _goCurrentDir(self):
        """Go to current directory
        """
        self._fileBrowser.setCurrentPath(_getCurDir())
        self.collapseAll()

    def _filteredModelIndexToPath(self, index):
        """Map index to file path
        """
        srcIndex = self._filteredModel.mapToSource(index)
        return self._dirsModel.filePath(srcIndex)

    def currentPath(self):
        """Get current path (root of the tree)
        """
        index = self.rootIndex()
        index = self._filteredModel.mapToSource(index)
        return self._dirsModel.filePath(index)

    def _isDescendant(self, child, parent):
        """Check if child is descendant of parent
        """
        while child.isValid():
            if child.parent() == parent:
                return True
            child = child.parent()
        return False

    def _setFirstItemAsCurrent(self):
        """QDirModel loads items asynchronously.
        Therefore we select current item by timer
        """
        if not self.currentIndex().isValid() or \
           not self._isDescendant(self.currentIndex(), self.rootIndex()):
            firstChild = self.rootIndex().child(0, 0)
            if firstChild.isValid():
                self._setFocusTimer.stop()
                self._setCurrentItem(self.rootIndex().child(0, 0))
            else:
                self._timerAttempts -= 1
                if not self._timerAttempts:
                    self._setFocusTimer.stop()
        else:  # nothing to do, have focus
            self._setFocusTimer.stop()

    def setCurrentPath(self, path):
        """Set current path (root of the tree)
        """
        # get index
        index = self._dirsModel.index(path)

        # set current path
        self._filteredModel.invalidate()
        newRoot = self._filteredModel.mapFromSource(index)
        self.setRootIndex(newRoot)

        self._timerAttempts = 10
        self._setFocusTimer.start()

    def _setCurrentItem(self, index):
        """Make the item current and select it
        """
        self.setCurrentIndex(index)
        self.selectionModel().select(index, QItemSelectionModel.SelectCurrent)


class ComboBox(QComboBox):
    """File browser combo box.
    Widget and functionality
    """

    def __init__(self, fileBrowser):
        QComboBox.__init__(self, fileBrowser)

        self._fileBrowser = fileBrowser

        self.setAttribute(Qt.WA_MacShowFocusRect, False)
        self.setAttribute(Qt.WA_MacSmallSize)
        self.setEditable(True)
        self.setMinimumContentsLength(1)
        self.setSizeAdjustPolicy(QComboBox.AdjustToMinimumContentsLengthWithIcon)
        self.lineEdit().setReadOnly(False)
        self._completionModel = QDirModel(self.lineEdit())
        self._completionModel.setFilter(QDir.AllDirs | QDir.NoDotAndDotDot)
        self.lineEdit().setCompleter(QCompleter(self._completionModel,
                                                self.lineEdit()))
        # TODO QDirModel is deprecated but QCompleter does not yet handle
        # QFileSystemModel - please update when possible.
        self._count = 0

        # Show popup action
        self._showPopupAction = QAction(QIcon(':enkiicons/filtered.png'), "File browser history", self)
        self._showPopupAction.setShortcut('Ctrl+H')
        core.actionManager().addAction("mNavigation/mFileBrowser/aMenuShow", self._showPopupAction)
        self._showPopupAction.triggered.connect(self._onShowPopup)

        # reconnected in self.updateComboItems()
        self.currentIndexChanged[int].connect(self._onItemSelected)

    def terminate(self):
        """Explicitly called destructor
        """
        core.actionManager().removeAction(self._showPopupAction)

    def _onShowPopup(self, triggered):
        """Handler for self._showPopupAction
        """
        self._fileBrowser.show()
        self.showPopup()

    def _onItemSelected(self, index):
        """Handler of item selection in the combo box
        """
        if self.count() > self._count:  # It is user input
            path = self.itemText(index)
            if os.path.isdir(path):
                self._fileBrowser.setCurrentPath(path)
        else:
            path = self.itemData(index)
            self._fileBrowser.setCurrentPath(path)

    def updateItems(self, items):
        """Update items in the combo box according to current history
        """
        self.currentIndexChanged[int].disconnect()
        self.clear()
        # Current text
        self.addItem(os.path.normpath(self._fileBrowser.currentPath()))
        self.setItemData(0, self._fileBrowser.currentPath())
        self.insertSeparator(self.count())

        for index, path in enumerate(items):
            self.addItem(os.path.normpath(path))
            self.setItemData(index + 2, path)
        self._count = self.count()
        self.currentIndexChanged[int].connect(self._onItemSelected)


class DockFileBrowser(DockWidget):
    """UI interface of FileBrowser plugin.

    Dock with file system tree, Box, navigation in a file system
    tree, for moving root of tree to currently selected directory and
    up (relatively for current directory)
    """
    rootChanged = pyqtSignal(str)
    """
    rootChanged(path)

    **Signal** emitted, when tree root has been changed
    """  # pylint: disable=W0105

    fileActivated = pyqtSignal()
    """
    rootChanged(path)

    **Signal** emitted, when file has been activated
    """  # pylint: disable=W0105

    def __init__(self, parent):
        DockWidget.__init__(self, parent, "&File Browser", QIcon(':/enkiicons/open.png'), "Alt+F")

        self._comboBox = None
        self._tree = None
        self._smartRecents = None
        self._smartHistory = None

        # restrict areas
        self.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)

        core.actionManager().addAction("mView/aFileBrowser", self.showAction())

        core.mainWindow().directoryDropt.connect(self._onDirectoryDropt)
        core.project().changed.connect(self.setCurrentPath)

        self.visibilityChanged.connect(self._onVisibilityChanged)

    def terminate(self):
        """Explicitly called destructor
        """
        if self._smartHistory is not None:
            self._smartHistory.terminate()
        if self._comboBox is not None:
            self._comboBox.terminate()
        core.actionManager().removeAction("mView/aFileBrowser")
        if self._tree is not None:
            self._tree.term()

    def _onVisibilityChanged(self, visible):
        """Postnoted widget initialization.
        Create element, when widget appears first timer
        """
        if visible:
            self._initialize()
            self.visibilityChanged.disconnect(self._onVisibilityChanged)

    def _initialize(self):
        """Delayed initialization of the widget for quicker start of application
        """
        # central widget
        wdg = QWidget(self)
        self.setWidget(wdg)

        # vertical layout
        vertLayout = QVBoxLayout(wdg)
        vertLayout.setContentsMargins(0, 0, 0, 0)
        vertLayout.setSpacing(3)

        # combo
        self._comboBox = ComboBox(self)
        vertLayout.addWidget(self._comboBox)

        # hline
        hline = QFrame(self)
        hline.setFrameStyle(QFrame.HLine | QFrame.Sunken)
        vertLayout.addWidget(hline)

        # files view
        self._tree = Tree(self)
        vertLayout.addWidget(self._tree)

        # cd up button
        self._aCdUp = QAction(QIcon(':enkiicons/go-up.png'),
                              self.tr("Up"),
                              self)
        self.titleBarWidget().addAction(self._aCdUp)
        self._aCdUp.triggered.connect(self.moveUp)

        # redirirect focus proxy
        self.setFocusProxy(self._tree)

        self._smartRecents = SmartRecents(self)
        self._smartHistory = SmartHistory(self)

        self.setCurrentPath(core.project().path() or _getCurDir())
        core.actionManager().action('mNavigation/mFileBrowser').setVisible(True)

    def _onDirectoryDropt(self, path):
        """Directory drag-n-dropt to main window. Show it
        """
        self.setCurrentPath(path)
        self.show()

    def updateComboItems(self, items):
        """Update items in the combo box according to current history
        """
        self._comboBox.updateItems(items)

    def currentPath(self):
        """Get current path (root of the tree)

        None, if tree wasn't created
        """

        if self._tree is not None:
            return self._tree.currentPath()
        else:
            return None

    def setCurrentPath(self, path):
        """Set current path (root of the tree)
        If there are no documents on workspace, also changes process current directory
        """
        if self._tree is None:
            return

        if self._tree.currentPath() == path:
            return

        self._tree.setCurrentPath(path)
        self._tree.setFocus()

        # set lineedit path
        self._comboBox.setToolTip(path)

        # notify SmartRecents and own slots
        self.rootChanged.emit(path)

        core.project().open(path)

    def moveUp(self):
        """Move tree root up, or only move focus"""
        self.setCurrentPath(os.path.dirname(self._tree.currentPath()))
