# repotreemodel.py - model for the reporegistry
#
# Copyright 2010 Adrian Buehlmann <adrian@cadifra.com>
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2 or any later version.

from mercurial import util, hg, ui

from tortoisehg.util import hglib, paths
from tortoisehg.hgqt.i18n import _
from tortoisehg.hgqt import qtlib

from repotreeitem import undumpObject, AllRepoGroupItem, RepoGroupItem
from repotreeitem import RepoItem, RepoTreeItem, SubrepoItem

from PyQt4.QtCore import *
from PyQt4.QtGui import *

import os


extractXmlElementName = 'reporegextract'
reporegistryXmlElementName = 'reporegistry'

repoRegMimeType = 'application/thg-reporegistry'
repoExternalMimeType = 'text/uri-list'


def writeXml(target, item, rootElementName):
    xw = QXmlStreamWriter(target)
    xw.setAutoFormatting(True)
    xw.setAutoFormattingIndent(2)
    xw.writeStartDocument()
    xw.writeStartElement(rootElementName)
    item.dumpObject(xw)
    xw.writeEndElement()
    xw.writeEndDocument()

def readXml(source, rootElementName):
    itemread = None
    xr = QXmlStreamReader(source)
    if xr.readNextStartElement():
        ele = str(xr.name().toString())
        if ele != rootElementName:
            print "unexpected xml element '%s' "\
                  "(was looking for %s)" % (ele, rootElementName)
            return
    if xr.hasError():
        print str(xr.errorString())
    if xr.readNextStartElement():
        itemread = undumpObject(xr)
        xr.skipCurrentElement()
    if xr.hasError():
        print str(xr.errorString())
    return itemread

def iterRepoItemFromXml(source):
    'Used by thgrepo.relatedRepositories to scan the XML file'
    xr = QXmlStreamReader(source)
    while not xr.atEnd():
        t = xr.readNext()
        if t == QXmlStreamReader.StartElement and xr.name() in ('repo', 'subrepo'):
            yield undumpObject(xr)

def getRepoItemList(root, includeSubRepos=False):
    if not includeSubRepos and isinstance(root, RepoItem):
        return [root]
    if not isinstance(root, RepoTreeItem):
        return []
    return reduce(lambda a, b: a + b,
                  (getRepoItemList(c, includeSubRepos=includeSubRepos) \
                    for c in root.childs), [])


class RepoTreeModel(QAbstractItemModel):

    updateProgress = pyqtSignal(int, int, QString, QString)

    def __init__(self, filename, parent, showSubrepos=False,
            showNetworkSubrepos=False, showShortPaths=False):
        QAbstractItemModel.__init__(self, parent)
        self.updateProgress.connect(parent.updateProgress)
        self.showSubrepos = showSubrepos
        self.showNetworkSubrepos = showNetworkSubrepos
        self.showShortPaths = showShortPaths

        root = None
        all = None

        if filename:
            f = QFile(filename)
            if f.open(QIODevice.ReadOnly):
                root = readXml(f, reporegistryXmlElementName)
                f.close()
                if root:
                    for c in root.childs:
                        if isinstance(c, AllRepoGroupItem):
                            all = c
                            break

                    if self.showSubrepos:
                        self.loadSubrepos(root)

        if not root:
            root = RepoTreeItem(self)
            all = AllRepoGroupItem(self)
            root.appendChild(all)

        self.rootItem = root
        self.allrepos = all
        self.updateCommonPaths()

    # see http://doc.qt.nokia.com/4.6/model-view-model-subclassing.html

    # overrides from QAbstractItemModel

    def index(self, row, column, parent):
        if not self.hasIndex(row, column, parent):
            return QModelIndex()
        if (not parent.isValid()):
            parentItem = self.rootItem
        else:
            parentItem = parent.internalPointer()
        childItem = parentItem.child(row)
        if childItem:
            return self.createIndex(row, column, childItem)
        else:
            return QModelIndex()

    def parent(self, index):
        if not index.isValid():
            return QModelIndex()
        childItem = index.internalPointer()
        parentItem = childItem.parent()
        if parentItem is self.rootItem:
            return QModelIndex()
        return self.createIndex(parentItem.row(), 0, parentItem)

    def rowCount(self, parent):
        if parent.column() > 0:
            return 0
        if not parent.isValid():
            parentItem = self.rootItem;
        else:
            parentItem = parent.internalPointer()
        return parentItem.childCount()

    def columnCount(self, parent):
        if parent.isValid():
            return parent.internalPointer().columnCount()
        else:
            return self.rootItem.columnCount()

    def data(self, index, role):
        if not index.isValid():
            return QVariant()
        if role not in (Qt.DisplayRole, Qt.EditRole, Qt.DecorationRole,
                Qt.FontRole):
            return QVariant()
        item = index.internalPointer()
        return item.data(index.column(), role)

    def headerData(self, section, orientation, role):
        if role == Qt.DisplayRole:
            if orientation == Qt.Horizontal:
                if section == 1:
                    return QString(_('Path'))
        return QVariant()

    def flags(self, index):
        if not index.isValid():
            return Qt.NoItemFlags
        item = index.internalPointer()
        return item.flags()

    def supportedDropActions(self):
        return Qt.CopyAction | Qt.MoveAction | Qt.LinkAction

    def removeRows(self, row, count, parent):
        item = parent.internalPointer()
        if item is None:
            item = self.rootItem
        self.beginRemoveRows(parent, row, row+count-1)
        res = item.removeRows(row, count)
        self.endRemoveRows()
        return res

    def mimeTypes(self):
        return QStringList([repoRegMimeType, repoExternalMimeType])

    def mimeData(self, indexes):
        i = indexes[0]
        item = i.internalPointer()
        buf = QByteArray()
        writeXml(buf, item, extractXmlElementName)
        d = QMimeData()
        d.setData(repoRegMimeType, buf)
        if isinstance(item, RepoItem):
            d.setUrls([QUrl.fromLocalFile(hglib.tounicode(item.rootpath()))])
        else:
            d.setText(QString(item.name))
        return d

    def dropMimeData(self, data, action, row, column, parent):
        group = parent.internalPointer()
        d = str(data.data(repoRegMimeType))
        if not data.hasUrls():
            # The source is a group
            if row < 0:
                # The group has been dropped on a group
                # In that case, place the group at the same level as the target
                # group
                row = parent.row()
                parent = parent.parent()
                group = parent.internalPointer()
                if row < 0 or not isinstance(group, RepoGroupItem):
                    # The group was dropped at the top level
                    group = self.rootItem
                    parent = QModelIndex()
        itemread = readXml(d, extractXmlElementName)
        if itemread is None:
            return False
        if group is None:
            return False
        # Avoid copying subrepos multiple times
        if Qt.CopyAction == action and self.getRepoItem(itemread.rootpath()):
            return False
        if row < 0:
            row = 0
        if self.showSubrepos:
            self.loadSubrepos(itemread)
        self.beginInsertRows(parent, row, row)
        group.insertChild(row, itemread)
        self.endInsertRows()
        if isinstance(itemread, AllRepoGroupItem):
            self.allrepos = itemread
        return True

    def setData(self, index, value, role):
        if not index.isValid() or role != Qt.EditRole:
            return False
        s = value.toString()
        if s.isEmpty():
            return False
        item = index.internalPointer()
        if item.setData(index.column(), value):
            self.dataChanged.emit(index, index)
            return True
        return False

    # functions not defined in QAbstractItemModel

    def allreposIndex(self):
        return self.createIndex(self.allrepos.row(), 0, self.allrepos)

    def addRepo(self, group, root, row=-1):
        grp = group
        if grp == None:
            grp = self.allreposIndex()
        rgi = grp.internalPointer()
        if row < 0:
            row = rgi.childCount()

        # make sure all paths are properly normalized
        root = os.path.normpath(root)

        # Check whether the repo that we are adding is a subrepo
        # This check could be expensive, particularly for network repositories
        # Thus, only perform this check on network repos if the showNetworkSubrepos
        # flag is set
        itemIsSubrepo = False
        if self.showNetworkSubrepos \
                or not paths.netdrive_status(root):
            outerrepopath = paths.find_root(os.path.dirname(root))
            if outerrepopath:
                # Check whether repo we are adding is a subrepo of
                # its containing (outer) repo
                # This check is currently quite imperfect, since it
                # only checks the current repo revision
                outerrepo = hg.repository(ui.ui(), path=outerrepopath)
                relroot = util.normpath(root[len(outerrepopath)+1:])
                if relroot in outerrepo['.'].substate:
                    itemIsSubrepo = True

        self.beginInsertRows(grp, row, row)
        if itemIsSubrepo:
            ri = SubrepoItem(root)
        else:
            ri = RepoItem(root)
        rgi.insertChild(row, ri)

        if not self.showSubrepos \
                or (not self.showNetworkSubrepos and paths.netdrive_status(root)):
            self.endInsertRows()
            return

        invalidRepoList = ri.appendSubrepos()

        self.endInsertRows()

        if invalidRepoList:
            if invalidRepoList[0] == root:
                qtlib.WarningMsgBox(_('Could not get subrepository list'),
                    _('It was not possible to get the subrepository list for '
                    'the repository in:<br><br><i>%s</i>') % root)
            else:
                qtlib.WarningMsgBox(_('Could not open some subrepositories'),
                    _('It was not possible to fully load the subrepository '
                    'list for the repository in:<br><br><i>%s</i><br><br>'
                    'The following subrepositories may be missing, broken or '
                    'on an inconsistent state and cannot be accessed:'
                    '<br><br><i>%s</i>')  %
                    (root, "<br>".join(invalidRepoList)))

    def getRepoItem(self, reporoot, lookForSubrepos=False):
        return self.rootItem.getRepoItem(os.path.normcase(reporoot),
                    lookForSubrepos=lookForSubrepos)

    def addGroup(self, name):
        ri = self.rootItem
        cc = ri.childCount()
        self.beginInsertRows(QModelIndex(), cc, cc + 1)
        ri.appendChild(RepoGroupItem(name, ri))
        self.endInsertRows()

    def write(self, fn):
        f = QFile(fn)
        f.open(QIODevice.WriteOnly)
        writeXml(f, self.rootItem, reporegistryXmlElementName)
        f.close()

    def depth(self, index):
        count = 1
        while True:
            index = index.parent()
            if index.row() < 0:
                return count
            count += 1

    def loadSubrepos(self, root, filterFunc=(lambda r: True)):
        repoList = getRepoItemList(root)
        for n, c in enumerate(repoList):
            QCoreApplication.processEvents()
            if filterFunc(c.rootpath()):
                if self.showNetworkSubrepos \
                        or not paths.netdrive_status(c.rootpath()):
                    self.updateProgress.emit(n, len(repoList),
                        _('Updating repository registry'),
                        _('Loading repository %s')
                        % hglib.tounicode(c.rootpath()))
                    QCoreApplication.processEvents()
                    self.removeRows(0, c.childCount(),
                        self.createIndex(c.row(), 0, c))
                    c.appendSubrepos()
        self.updateProgress.emit(len(repoList), len(repoList),
            _('Updating repository registry'),
            _('Repository Registry updated'))

    def updateCommonPaths(self, showShortPaths=None):
        if not showShortPaths is None:
            self.showShortPaths = showShortPaths
        for grp in self.rootItem.childs:
            if isinstance(grp, RepoGroupItem):
                if self.showShortPaths:
                    grp.updateCommonPath()
                else:
                    grp.updateCommonPath('')

    def sortchilds(self, childs, keyfunc):
        self.layoutAboutToBeChanged.emit()
        childs.sort(key=keyfunc)
        self.layoutChanged.emit()
