#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
#The MIT License (MIT)
#
#Copyright (c) <2013-2014> <Colin Duquesnoy and others, see AUTHORS.txt>
#
#Permission is hereby granted, free of charge, to any person obtaining a copy
#of this software and associated documentation files (the "Software"), to deal
#in the Software without restriction, including without limitation the rights
#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#copies of the Software, and to permit persons to whom the Software is
#furnished to do so, subject to the following conditions:
#
#The above copyright notice and this permission notice shall be included in
#all copies or substantial portions of the Software.
#
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
#THE SOFTWARE.
#
"""
This module contains the implementation of a tab widget specialised to
show code editor tabs.
"""
import os
import sys
import pyqode.core
from pyqode.qt import QtCore, QtGui

# needed to build doc on readthedocs; should be fixed with the next version
# of pyqode.core
try:
    from pyqode.qt.QtGui import QDialog, QTabBar, QTabWidget
except:
    class QDialog(object):
        pass

    class QTabBar(object):
        pass

    class QTabWidget:
        pass

from pyqode.widgets.ui.dlg_unsaved_files_ui import Ui_Dialog


class DlgUnsavedFiles(QDialog, Ui_Dialog):
    def __init__(self, parent, files=None):
        if files is None:
            files = []
        QtGui.QDialog.__init__(self, parent)
        Ui_Dialog.__init__(self)
        self.setupUi(self)
        self.btSaveAll = self.buttonBox.button(QtGui.QDialogButtonBox.SaveAll)
        self.btSaveAll.clicked.connect(self.accept)
        self.discarded = False
        self.btDiscard = self.buttonBox.button(QtGui.QDialogButtonBox.Discard)
        self.btDiscard.clicked.connect(self.__setDiscared)
        self.btDiscard.clicked.connect(self.accept)
        for f in files:
            self.addFile(f)
        self.listWidget.itemSelectionChanged.connect(self.__onSelectionChanged)
        self.__onSelectionChanged()

    def addFile(self, filePath):
        icon = QtGui.QFileIconProvider().icon(QtCore.QFileInfo(filePath))
        item = QtGui.QListWidgetItem(icon, filePath)
        self.listWidget.addItem(item)

    def __setDiscared(self):
        self.discarded = True

    def __onSelectionChanged(self):
        nbItems = len(self.listWidget.selectedItems())
        if nbItems == 0:
            self.btSaveAll.setText("Save")
            self.btSaveAll.setEnabled(False)
        else:
            self.btSaveAll.setEnabled(True)
            self.btSaveAll.setText("Save selected")
            if nbItems == self.listWidget.count():
                self.btSaveAll.setText("Save all")


class ClosableTabBar(QTabBar):
    def __init__(self, parent):
        QtGui.QTabBar.__init__(self, parent)
        self.setTabsClosable(True)

    def mousePressEvent(self, qMouseEvent):
        QtGui.QTabBar.mousePressEvent(self, qMouseEvent)
        if qMouseEvent.button() == QtCore.Qt.MiddleButton:
            self.parentWidget().tabCloseRequested.emit(self.tabAt(
                qMouseEvent.pos()))


class QCodeEditTabWidget(QTabWidget):
    """
    Adds support for tab widgets based on any subclass of pyqode.core.QCodeEdit.

    It ensures that there is only one open editor tab for a specific filePath,
    it adds a few utility methods to quickly manipulate the current editor
    widget.

    It handles the tab close requests automatically and show a message box when
    a dirty tab widget is being closed. It also adds a convenience tabBar with a
    "close", "close others" and "close all" menu. (You can add custom actions by
    using the addAction and addSeparator methods).

    It exposes a variety of signal and slots for a better integration with
    your applications( dirtyChanged, saveCurrent, saveAll, closeAll,
    closeCurrent, closeOthers).
    """

    dirtyChanged = QtCore.Signal(bool)
    lastTabClosed = QtCore.Signal()
    tabClosed = QtCore.Signal(QtGui.QWidget)

    @property
    def currentEditor(self):
        """
        Returns the current editor widget or None if the current tab widget is
        not a subclass of QCodeEdit or if there is no open tab.
        """
        return self.__currentTab

    def __init__(self, parent):
        QtGui.QTabWidget.__init__(self, parent)
        self.__currentTab = None
        self.currentChanged.connect(self.__onCurrentChanged)
        self.__watcher = QtCore.QFileSystemWatcher()
        self.__watcher.fileChanged.connect(self.__onFileChanged)
        self.tabCloseRequested.connect(self.__onTabCloseRequested)
        tabBar = ClosableTabBar(self)
        tabBar.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
        tabBar.customContextMenuRequested.connect(self.__showTabContextMenu)
        self.setTabBar(tabBar)
        self.contextMenu = QtGui.QMenu()
        a = QtGui.QAction("Close", self)
        a.triggered.connect(self.close)
        self.contextMenu.addAction(a)
        a = QtGui.QAction("Close others", self)
        a.triggered.connect(self.closeOthers)
        self.addAction(a)
        a = QtGui.QAction("Close all", self)
        a.triggered.connect(self.closeAll)
        self.addAction(a)
        self._widgets = []  # keep a list of widgets (to avoid PyQt bug where
                            # the C++ class loose the wrapped obj type).

    @QtCore.Slot()
    def close(self):
        self.tabCloseRequested.emit(self.currentIndex())

    @QtCore.Slot()
    def closeOthers(self):
        cw = self.currentWidget()
        self.tryCloseDirtyTabs(exept=cw)
        i = 0
        while self.count() > 1:
            w = self._widgets[i]
            if w != cw:
                self.removeTab(i)
            else:
                i = 1

    @QtCore.Slot()
    def closeAll(self):
        if self.tryCloseDirtyTabs():
            while self.count():
                widget = self._widgets[0]
                self.removeTab(0)
                self.tabClosed.emit(widget)
            return True
        return False

    @QtCore.Slot()
    def saveCurrent(self, filePath=None, force=False):
        """
        Save current tab contend. Specify filePath and set force to true to save as.
        """
        try:
            self.__currentTab.saveToFile(filePath, force)
            # adapt tab title on save as
            if filePath:
                self.setTabText(self.currentIndex(),
                                QtCore.QFileInfo(filePath).fileName())
            return True
        except AttributeError:  # not an editor widget
            pass
        return False

    @QtCore.Slot()
    def saveAll(self, filePath=None):
        for i in range(self.count()):
            try:
                self._widgets[i].saveToFile(filePath)
                self.setTabText(i, self._widgets[i].fileName)
            except AttributeError:
                pass

    def addAction(self, action):
        self.contextMenu.addAction(action)

    def addSeparator(self):
        return self.contextMenu.addSeparator()

    def isFileOpen(self, filePath):
        """
        Checks if the filePath is already open in an editor tab.

        :returns: The tab index if found or -1
        """
        for i, widget in enumerate(self._widgets):
            try:
                if widget.filePath == filePath:
                    return i
            except AttributeError:
                pass  # not an editor widget
        return -1

    def addEditorTab(self, qCodeEdit, icon=None):
        """
        Adds an editor tab widget, set its text as the editor.fileName and
        set it as the active tab.

        The widget is only added if there is no other editor tab open with the
        same filename, else the already open tab is set as current.

        :param qCodeEdit: The code editor widget tab to add
        :type qCodeEdit: pyqode.core.QCodeEdit

        :param icon: The tab widget icon. Optional
        :type icon: QtGui.QIcon or None
        """
        index = self.isFileOpen(qCodeEdit.filePath)
        if index != -1:
            # already open, just show it
            self.setCurrentIndex(index)
            # no need to keep this instance
            qCodeEdit.deleteLater()
            del qCodeEdit
            return
        if not icon:
            icon = QtGui.QFileIconProvider().icon(
                QtCore.QFileInfo(qCodeEdit.filePath))
        name = qCodeEdit.fileName
        if self.hasSameName(name):
            name = self.renameDuplicateTabs(name, qCodeEdit.filePath)
        qCodeEdit._tab_name = name
        index = self.addTab(qCodeEdit, icon, name)
        self.setCurrentIndex(index)
        self.setTabText(index, name)
        qCodeEdit.setFocus(True)
        if hasattr(qCodeEdit, "fileWatcherMode"):
            qCodeEdit.fileWatcherMode.fileDeleted.connect(self._onFileDeleted)

    def addTab(self, elem, icon, name):
        self._widgets.append(elem)
        return super().addTab(elem, icon, name)

    def hasSameName(self, name):
        for i in range(self.count()):
            if self.tabText(i) == name:
                return True
        return False

    def renameDuplicateTabs(self, name, path):
        for i in range(self.count()):
            if self.tabText(i) == name:
                filePath = self._widgets[i].filePath
                parentDir = os.path.split(os.path.abspath(
                    os.path.join(filePath, os.pardir)))[1]
                new_name = os.path.join(parentDir, name)
                self.setTabText(i, new_name)
                self._widgets[i]._tab_name = new_name
                break
        parentDir = os.path.split(os.path.abspath(
            os.path.join(path, os.pardir)))[1]
        return os.path.join(parentDir, name)

    def resetSettings(self, settings):
        """
        Resets the settings of all opened editor
        """
        for widget in self._widgets:
            try:
                widget.settings = settings
            except AttributeError:
                pass

    def resetStyle(self, style):
        """
        Resets the style of all opened editor
        """
        for widget in self._widgets:
            try:
                widget.style = style
                widget.repaint()
                widget.syntaxHighlighterMode.rehighlight()
            except AttributeError:
                pass

    def refreshIcons(self, useTheme=True):
        for widget in self._widgets:
            try:
                widget.refreshIcons(useTheme=useTheme)
            except AttributeError:
                pass

    def __onCurrentChanged(self, index):
        widget = self._widgets[index]
        if widget == self.__currentTab:
            return
        try:
            if self.__currentTab:
                self.__currentTab.dirtyChanged.disconnect(self.__onDirtyChanged)
        except AttributeError:
            pass  # not an editor widget
        self.__currentTab = widget
        try:
            if self.__currentTab:
                self.__currentTab.dirtyChanged.connect(self.__onDirtyChanged)
                self.__onDirtyChanged(self.__currentTab.dirty)
                self.__currentTab.setFocus()
        except AttributeError:
            pass  # not an editor widget

    def __onFileChanged(self, filePath):
        # check if this is a real external change, we actually got called when
        # the user save the file from the application
        # add it to the dialog list, the dialog will be show when the editor
        # will get focus again.
        pass

    def removeTab(self, p_int):
        QTabWidget.removeTab(self, p_int)
        widget = self._widgets.pop(p_int)
        if widget == self.__currentTab:
            if self.__currentTab:
                self.__currentTab.dirtyChanged.disconnect(self.__onDirtyChanged)
            self.__currentTab = None
        self.tabClosed.emit(widget)
        widget.deleteLater()
        del widget

    def __onTabCloseRequested(self, index):
        widget = self._widgets[index]
        try:
            if not widget.dirty:
                self.removeTab(index)
            else:
                dlg = DlgUnsavedFiles(self, files=[widget.filePath])
                if dlg.exec_() == dlg.Accepted:
                    if not dlg.discarded:
                        widget.saveToFile()
                    self.removeTab(index)
        except AttributeError as e:
            pyqode.core.logger.warning(e)
            pass  # do nothing, let the user handle this case
        if self.count() == 0:
            self.lastTabClosed.emit()

    def __showTabContextMenu(self, position):
        self.contextMenu.popup(self.mapToGlobal(position))

    def tryCloseDirtyTabs(self, exept=None):
        widgets, filenames = self.__collectDirtyWidgets(exept=exept)
        if not len(filenames):
            return True
        dlg = DlgUnsavedFiles(self, files=filenames)
        if dlg.exec_() == dlg.Accepted:
            if not dlg.discarded:
                for item in dlg.listWidget.selectedItems():
                    filename = item.text()
                    for w in widgets:
                        if w.filePath == filename:
                            break
                    if w != exept:
                        w.saveToFile()
                        self.removeTab(self.indexOf(w))
            return True
        return False

    def __collectDirtyWidgets(self, exept=None):
        widgets = []
        filenames = []
        for i in range(self.count()):
            widget = self._widgets[i]
            try:
                if widget.dirty and widget != exept:
                    widgets.append(widget)
                    filenames.append(widget.filePath)
            except AttributeError:
                pass
        return widgets, filenames

    def __onDirtyChanged(self, dirty):
        try:
            title = self.currentWidget()._tab_name
            if dirty:
                self.setTabText(self.currentIndex(), "* " + title)
            else:
                self.setTabText(self.currentIndex(), title)
        except AttributeError:
            pass
        self.dirtyChanged.emit(dirty)

    def closeEvent(self, event):
        if not self.tryCloseDirtyTabs():
            event.ignore()
        else:
            event.accept()

    def _onFileDeleted(self, editor):
        self.removeTab(self.indexOf(editor))


if __name__ == "__main__":

    def main():
        app = QtGui.QApplication(sys.argv)
        tw = QCodeEditTabWidget(None)
        e = pyqode.core.QGenericCodeEdit(tw)
        e.openFile(__file__)
        tw.addEditorTab(e)

        e = pyqode.core.QGenericCodeEdit(tw)
        e.openFile(pyqode.core.__file__)
        tw.addEditorTab(e)

        e = pyqode.core.QGenericCodeEdit(tw)
        e.openFile(pyqode.widgets.__file__)
        tw.addEditorTab(e)

        tw.showMaximized()
        app.exec_()

    main()
