#!/usr/bin/python
# -*- coding: utf-8 -*-

import sys, re, sip, os
from PyKDE4.kdecore import ki18n, KAboutData, KCmdLineArgs
from PyKDE4 import kdeui, kdecore, kparts, kio, ktexteditor
from PyQt4 import QtGui, QtCore
 
about = KAboutData(
    "PyKate",
    "",
    ki18n ("PyKate"),
    "1.0",
    ki18n ("PyKate Editor"),
    KAboutData.License_GPL,
    ki18n ("(c) 2010 Luke Campagnola"),
    ki18n ("none"),
    "",
    ""
)

KCmdLineArgs.init (sys.argv[:1], about)
docs = []
currentDoc = None
 
app = kdeui.KApplication ()
app.setStyleSheet("""
    KMainWindow {
        background: solid black;
        color: #CCC;
        alternate-background-color: #111;
    }

    QLineEdit {
        border: 2px solid #080; 
        border-radius: 5; 
        color: #CCC;
        background: black;
    }

    QLabel {
        color: #CCC;
        background: black;
    }

    QPushButton {
        color: #CCC;
        background: black;
    }

    QComboBox {
        color: #CCC;
        background: black;
    }
""")

win = kparts.KParts.MainWindow()

win.setStyleSheet("""
    background: solid black; 
    color: #CCC;
    alternate-background-color: #111;
""")

class PyKateEditor(QtGui.QWidget):
    def __init__(self, parent=None):
        QtGui.QWidget.__init__(self, parent)
        self.vl = QtGui.QVBoxLayout()
        self.setLayout(self.vl)
        self.vl.setMargin(10)

        self.title = QtGui.QLabel()
        self.title.setStyleSheet('color: white; font-size: 13pt;')
        self.title.setAlignment(QtCore.Qt.AlignHCenter)
        self.vl.addWidget(self.title)

        #self.hl = QtGui.QHBoxLayout()
        #self.vl.addLayout(self.hl)
        self.stack = QtGui.QStackedWidget()
        self.vl.addWidget(self.stack)

        #self.navstack = QtGui.QStackedWidget()
        #self.hl.addWidget(self.navstack)
        #self.navstack.setFixedWidth(250)
    
    
class View(QtGui.QWidget):
    """
    Wrapper around KTextEditor.View to provide some basic support
    
    """
    def __init__(self, doc, parent=None):
        QtGui.QWidget.__init__(self, parent)
        
        self.doc = doc
        self.view = doc.createView(self)
        self.layout = QtGui.QGridLayout()
        self.layout.setSpacing(3)
        self.layout.setContentsMargins(0,0,0,0)
        self.setLayout(self.layout)
        self.layout.addWidget(self.view, 0, 0)
        
        self.view.setContextMenu(menu)
        self.frame = None
        self.hScrollBar = None
        self.vScrollBar = None
        
        for ch in self.view.children():
            if isinstance(ch, QtGui.QScrollBar):
                ch.hide()
                if ch.orientation() == QtCore.Qt.Horizontal:
                    self.hScrollBar = ch
                else:
                    self.vScrollBar = ch
            if self.frame is None and isinstance(ch, QtGui.QFrame):
                ch.setFrameStyle(0)   ## hide outer frame
                self.frame = ch
                self.lineNumbers = ch.children()[1]
                self.textArea = ch.children()[2]
                
        # for some reason these do not work
        #self.vScrollBar.rangeChanged.connect(self.scrollChanged)
        #self.vScrollBar.valueChanged.connect(self.scrollChanged)
                
    def visibleRange(self):
        ## return the range of lines visible on screen by querying the scroll bar
        start = self.vScrollBar.value()
        end = start + self.vScrollBar.pageStep()
        return start, end
    
    def lastLine(self):
        return self.doc.documentEnd().line()
    
    def lineToCoord(self, line):
        c = ktexteditor.KTextEditor.Cursor()
        c.setLine(min(line, self.lastLine()))
        return self.view.cursorToCoordinate(c).y()
    
    def coordToLine(self, y):
        l0 = self.vScrollBar.value()
        y0 = self.lineToCoord(l0+1)
        y1 = self.lineToCoord(l0+2)
        dy = y1-y0
        if dy <= 0:
            return 0
        return l0 + int((y - y0) / float(dy))
        
    
        
class DocView(View):
    """
    Document view combined with navigation view at the right.
    
    """
    def __init__(self, doc, parent=None):
        View.__init__(self, doc, parent)
        self.nav = NavView(doc, self)
        self.layout.addWidget(self.nav, 0, 1)
        self.view.verticalScrollPositionChanged.connect(self.scrollChanged)
        self.nav.lineChanged.connect(self.navLineChanged)
        self.nav.lineClicked.connect(self.navLineClicked)
        self.nav.wheel.connect(self.navWheelEvent)
        self.view.cursorPositionChanged.connect(self.cursorMoved)
        #self.lastScroll = 0
        #self.scrollTarget = 0
        #self.scrollTimer = QtCore.QTimer()
        #self.scrollTimer.timeout.connect(self.animateScroll)
        #self.scrollChanged()
        
    def scrollChanged(self, *args):
        self.updateNav()
        ## unexpected change in scroll position. 
        ## revert to last pos, animate to new pos
        #self.scrollTarget = self.vScrollBar.value()
        #print self.lastScroll, self.scrollTarget
        #if abs(self.scrollTarget - self.lastScroll) < 4:
            #print "  - direct to target"
            #self.forceScroll(self.scrollTarget)
        #else:
            #print "  - animate to target"
            #self.forceScroll(self.lastScroll)
            #self.scrollTimer.start(16)
        
    #def forceScroll(self, v):
        ### set the scroll bar position without triggering scrollChanged
        ### also update nav view
        #try:
            #self.vScrollBar.blockSignals(True)
            #self.vScrollBar.setValue(v)
        #finally:
            #self.vScrollBar.blockSignals(False)
            
        #self.lastScroll = v
        #self.updateNav()
        
    def updateNav(self):
        start, end = self.visibleRange()
        max = self.vScrollBar.maximum()
        self.nav.setLineRange(start, end, max)
        
    #def animateScroll(self):
        #diff = (self.scrollTarget - self.lastScroll) / 4
        #if abs(diff) < 1:
            #self.scrollTimer.stop()
            #self.forceScroll(self.scrollTarget)
        #else:
            #print diff
            #self.forceScroll(self.lastScroll + diff/4)
        

    def navLineChanged(self, line):
        self.vScrollBar.setValue(self.vScrollBar.maximum() * line)
        
    def navLineClicked(self, line):
        ## put line at center of screen
        s,e = self.visibleRange()
        h = e-s
        line = line-(h/2)
        self.vScrollBar.setValue(line)

    def resizeEvent(self, ev):
        self.scrollChanged()
        
    def navWheelEvent(self, ev):
        app.notify(self.textArea, ev)
        
    def cursorMoved(self, view, pos):
        self.nav.view.setCursorPosition(pos)

class NavView(View):
    """
    Navigation view showing zoomed-out document and page indicator.
    """
    lineChanged = QtCore.pyqtSignal(object)
    lineClicked = QtCore.pyqtSignal(object)
    wheel = QtCore.pyqtSignal(object)

    
    def __init__(self, doc, parent=None):
        View.__init__(self, doc, parent)
        self.lineNumbers.hide()
        
        ## zoom text to minimum size
        ev = QtGui.QWheelEvent(QtCore.QPoint(10,10), -120, QtCore.Qt.NoButton, QtCore.Qt.ControlModifier)
        for i in range(10):
            app.notify(self.textArea, ev)  
        
        self.textArea.setEnabled(False)
        self.overlay = NavOverlay(self)
        self.overlay.startChanged.connect(self.overlayMoved)
        self.overlay.clicked.connect(self.overlayClicked)
        self.overlay.wheel.connect(self.wheel)
        self.setFixedWidth(150)
        
        self.targetRange = None
        self.currentRange = None
        self.animationTimer = QtCore.QTimer()
        self.animationTimer.timeout.connect(self.animate)
    
    def resizeEvent(self, ev):
        self.overlay.setGeometry(0, 0, self.width(), self.height())

    def setLineRange(self, start, end, max):
        self.targetRange = (start, end, max)
        if self.currentRange is None:
            self.currentRange = (start, end, max)
        #print "start:", self.currentRange, self.targetRange
        if not self.animationTimer.isActive():
            self.animationTimer.start(16)
        
    def animate(self):
        #print "animate"
        diff = (self.targetRange[0] - self.currentRange[0]) * 0.3
        if abs(diff) < 1:
            diff = (self.targetRange[0] - self.currentRange[0])
            self.animationTimer.stop()
            
        start = self.currentRange[0] + diff
        end = start + self.targetRange[1] - self.targetRange[0]
        max = self.targetRange[2]
        self.currentRange = (start, end, max)
        self._setLineRange(start, end, max)
        
        
    def _setLineRange(self, start, end, max):
        # scroll to correct position first
        maxRange = (self.lastLine() - (end-start))
        if maxRange == 0:
            return
        frac = float(start) / maxRange
        scroll = frac * self.vScrollBar.maximum()
        self.vScrollBar.setValue(int(scroll))
        
        # update overlay
        start = self.lineToCoord(start)
        end = self.lineToCoord(end)
        max = self.lineToCoord(max)
        self.overlay.setRange(start, end, max)
        
    def overlayMoved(self, start):
        self.lineChanged.emit(start)

    def overlayClicked(self, pos):
        line = self.coordToLine(pos)
        self.lineClicked.emit(line)

class NavOverlay(QtGui.QWidget):
    startChanged = QtCore.pyqtSignal(object)
    clicked = QtCore.pyqtSignal(object)
    wheel = QtCore.pyqtSignal(object)
    
    def __init__(self, parent=None):
        QtGui.QWidget.__init__(self, parent)
        self.start = 30
        self.span = 100
        self.max = 200
        
    def setRange(self, start, end, max):
        self.start = start
        self.span = end-start
        if max == -1:
            max = self.height()
        self.max = min(max, self.height()-self.span)
        self.update()
        
    def paintEvent(self, ev):
        p = QtGui.QPainter(self)
        rgn = self.rect()
        rgn.setTop(self.start)
        rgn.setBottom(self.start+self.span)
        rgn.setRight(rgn.right()-1)
        p.setBrush(QtGui.QBrush(QtGui.QColor(100, 100, 255, 50)))
        p.setPen(QtGui.QPen(QtGui.QColor(50, 50, 150), 1))
        p.drawRect(rgn)
        
    #def mousePressEvent(self, ev):
        #ev.accept()
        #self.start = ev.pos().y() - self.span/2
        #self.moved()
        
    #def mouseMoveEvent(self, ev):
        #self.start = ev.pos().y() - self.span/2
        #self.moved()
        
    def mousePressEvent(self, ev):
        self.pressValue = self.start
        self.pressPos = ev.pos()
        self.mouseMoved = False
        ev.accept()
        
    def mouseReleaseEvent(self, ev):
        if self.mouseMoved:
            return
        self.clicked.emit(ev.pos().y())
        
    def mouseMoveEvent(self, ev):
        self.mouseMoved = True
        self.start = self.pressValue + (ev.pos() - self.pressPos).y()
        self.moved()
        
    def wheelEvent(self, ev):
        self.wheel.emit(ev)
        
    def moved(self):
        #self.start = max(0, self.start)
        #self.start = min(self.max, self.start)
        #self.update()
        self.startChanged.emit(self.start / float(self.max))
        
class FileMonitor:
    def __init__(self):
        self.docs = []
        self.timer = QtCore.QTimer()
        self.timer.timeout.connect(self.check)
        self.timer.start(1000)
        
    def add(self, doc):
        self.docs.append(doc)
        self.update(doc)
        
    def remove(self, doc):
        self.docs.remove(doc)
        
    def update(self, doc):
        url = str(doc.url().url())
        if url.startswith('file://'):
            url = url[7:]
            doc.last_mtime = os.stat(url).st_mtime
        else:
            doc.last_mtime = None
        
    def check(self):
        for doc in self.docs:
            url = str(doc.url().url())
            if url.startswith('file://'):
                url = url[7:]
                mtime = os.stat(url).st_mtime
                if mtime > doc.last_mtime:
                    doc.documentReload()
                    doc.last_mtime = mtime
            else:
                pass


cw = PyKateEditor()
win.setCentralWidget(cw)
monitor = FileMonitor()

config = kdecore.KConfig('pykaterc')
shortcutGroup = config.group('shortcuts')
katepartShortcutGroup =  config.group('katepart_shortcuts')
mainGroup = config.group('main')



ed = ktexteditor.KTextEditor.EditorChooser.editor()

partconfig = kdecore.KConfig('pykate_katepartrc')
ed.readConfig(partconfig)


def showConfig():
    ed.configDialog(win)
    ed.writeConfig(partconfig)
    partconfig.sync()

def showShortcuts():
    dlg = kdeui.KShortcutsDialog(kdeui.KShortcutsEditor.AllActions, kdeui.KShortcutsEditor.LetterShortcutsDisallowed, win)
    dlg.addCollection(ac, 'PyKate')
    dlg.addCollection(docs[0][1].view.actionCollection(), 'KatePart')
    #for name, collection in self.allActionCollections():
        #dlg.addCollection(collection, name)
    dlg.configure()
    #kdeui.KShortcutsDialog.configure( ac )
    ac.writeSettings(shortcutGroup)
    docs[0][1].view.actionCollection().writeSettings(katepartShortcutGroup)
    config.sync()

def load(url=None, lineno=None):
    if url is None:
        lastDir = mainGroup.readEntry('lastDir')
        url = kio.KFileDialog.getOpenUrl(kdecore.KUrl(lastDir))
        mainGroup.writeEntry('lastDir', os.path.dirname(str(url.toString())))
    
    if isinstance(url, basestring):
        urlStr = url
        url = kdecore.KUrl(url)
    else:
        urlStr = str(url.toString())
        
    if urlStr == '':
        return
    for i in range(len(docs)):
        if url == docs[i][0].url():
            showDoc(i, lineno=lineno)
            return True
    cd = docs[currentDoc][0]
    if not cd.url().isEmpty() or cd.isModified() or not cd.isEmpty():
        new()
    docs[currentDoc][0].openUrl(url)
    docs[currentDoc][1].scrollChanged()
    addRecentDoc(url)
    showDoc(lineno=lineno)
    monitor.update(cd)
    
def addRecentDoc(url):
    url = str(url.toString())
    if ';' in url:
        return
    docs = recentDocuments()
    if url in docs:
        #print "  remove first"
        docs.remove(url)
    docs = [url] + docs[:9]
    mainGroup.writeEntry('recentDocuments', ';'.join(docs))
    mainGroup.sync()

def updateRecentList():
    recentMenu.clear()
    docs = recentDocuments()
    for d in docs:
        if len(d) > 100:
            d = '...' + d[-100:]
        recentMenu.addAction(d, mkLoadFn(d))

def mkLoadFn(url):
    return lambda: load(url)

def recentDocuments():
    return str(mainGroup.readEntry('recentDocuments')).split(';')
    

def new():
    doc = ed.createDocument(None)
    monitor.add(doc)
    
    view = DocView(doc, cw.stack)
    
    QtCore.QObject.connect(doc, QtCore.SIGNAL("modifiedChanged(KTextEditor::Document*)"), updateTitle)
    
    cw.stack.addWidget(view)
    docs.append((doc, view))
    showDoc(len(docs) - 1)
    
    
def save():
    cd = docs[currentDoc][0]
    cd.save()
    monitor.update(cd)
    
def saveAs():
    url = kio.KFileDialog.getSaveUrl()
    if str(url.toString()) == '':
        return
    docs[currentDoc][0].saveAs(url)
    showDoc(currentDoc)
    monitor.update(docs[currentDoc][0])
    
    
    
def nextDoc():
    showDoc((currentDoc+1) % len(docs))
    
def prevDoc():
    showDoc((currentDoc-1) % len(docs))
    
def showDoc(n=None, lineno=None):
    global currentDoc, docs
    if n is None:
        n = currentDoc
    currentDoc = n
    if n < 0:
        updateTitle()
        return
    cw.stack.setCurrentIndex(n)
    updateTitle()
    
    if lineno is not None:
        view = docs[currentDoc][1]
        cur = view.cursorPosition()
        cur.setLine(int(lineno)-1)
        view.setCursorPosition(cur)

def updateTitle():
    #print "update!"
    global currentDoc
    if currentDoc is None or currentDoc == -1:
        cw.title.setText('')
        return
    doc = docs[currentDoc][0]
    url = str(doc.url().pathOrUrl())
    if doc.isModified():
        url = '* ' + url + ' *'
    cw.title.setText(url)

    

def closeDoc(n=None, createNew=True):
    if n is None:
        n = currentDoc
    if not docs[n][0].closeUrl():
        return False
    #docs[n][1].setContextMenu(None)
    QtCore.QObject.disconnect(docs[n][0], QtCore.SIGNAL("modifiedChanged(KTextEditor::Document*)"), updateTitle)
    cw.stack.removeWidget(docs[n][1])
    docs[n][1].close()
    doc = docs.pop(n)
    monitor.remove(doc[0])
    
    if len(docs) == 0 and createNew:
        new()
        nextDoc = 0
    elif n == 0 and len(docs) > 0:
        nextDoc = 0
    else:
        nextDoc = n-1
    showDoc(nextDoc)
    return True
    
def quit():
    for i in range(len(docs)):
        if not closeDoc(createNew=False):
            return False
    sip.delete(menu)  ## Not sure why this needs to happen, but we crash otherwise.
    win.close()
    app.quit()
    
    
#def find():
    #findEdit.move(cw.width() - findEdit.width(), 50)
    #findEdit.show()
    #findEdit.setFocus()
    #findEdit.selectAll()

ac = kdeui.KActionCollection(win)
ac.addAssociatedWidget(win)

kdeui.KStandardAction.open(load, ac)
kdeui.KStandardAction.close(closeDoc, ac)
kdeui.KStandardAction.openNew(new, ac)
kdeui.KStandardAction.save(save, ac)   #causes error about ambiguous shortcuts
kdeui.KStandardAction.saveAs(saveAs, ac)
kdeui.KStandardAction.quit(quit, ac)
#kdeui.KStandardAction.find(find, ac)
#kdeui.KStandardAction.findNext(app.closeAllWindows, ac)
#kdeui.KStandardAction.findPrev(app.closeAllWindows, ac)
#kdeui.KStandardAction.gotoLine(app.closeAllWindows, ac)
#kdeui.KStandardAction.replace(app.closeAllWindows, ac)
kdeui.KStandardAction.preferences(app.closeAllWindows, ac)
kdeui.KStandardAction.back(prevDoc, ac)
kdeui.KStandardAction.forward(nextDoc, ac)
#kdeui.KStandardAction.zoomIn(load, ac)
#kdeui.KStandardAction.zoomOut(load, ac)

menu = QtGui.QMenu()
menu.addAction("Settings...", showConfig)
menu.addAction("Shortcuts...", showShortcuts)

recentMenu = menu.addMenu('Recent')


updateRecentList()

ac.readSettings(shortcutGroup)

## Use window manager to expand window instead.
#kdeui.KToggleFullScreenAction.setFullScreen(win, True)

win.show()
new()

docs[0][1].view.actionCollection().readSettings(katepartShortcutGroup)

for fn in sys.argv[1:]:
    if ':' in fn:
        fn, lineno = fn.split(':')
    else:
        lineno = None
    
    if os.path.isfile(fn):
        load(os.path.abspath(fn), lineno=lineno)

if sys.flags.interactive == 0:
    app.exec_()
