#!/usr/bin/env python
# pydiff 0.2 Copyright (c) 2005 by Matt Chisholm
#
# The latest version is available from:
#   http://www.theory.org/~matt/pydiff/
#
# Licensed under the Open Software License version 2.0
#   http://www.opensource.org/licenses/osl-2.0.php

import os
import sys

import gtk
import pango
import gobject

from difflib import Differ

RED   = '#e0a0a0'
GREEN = '#a0e0a0'
BLUE  = '#a0a0e0'

SPACING = 0
PADDING = 8
FONTSIZE = pango.SCALE_SMALL

VERSUS = 'vs.'

DEBUG = False

d = gtk.gdk.display_get_default()
s = d.get_default_screen()
MAX_WINDOW_HEIGHT = s.get_height()
MAX_WINDOW_WIDTH  = s.get_width()
del d, s

class Change(object):
    def __init__(self, mark, left, right):
        self.mark = mark
        self.start_left  = left
        self.end_left    = left
        self.start_right = right
        self.end_right   = right

    def __repr__(self):
        return str(self)

    def __str__(self):
        def rng(s,e):
            if e-s == 0:
                return str(s)
            else:
                return '%d-%d' % (s,e)
        
        return '%s %s %s (%s)'%(rng(self.start_left , self.end_left ),
                                VERSUS,
                                rng(self.start_right, self.end_right),
                                self.len_str())

    def __len__(self):
        return max(self.end_left  - self.start_left ,
                   self.end_right - self.start_right) + 1

    def len_str(self):
        l = len(self)
        if l == 1:
            return '1 line'
        else:
            return '%d lines'%l


class Buffer(gtk.TextBuffer):
    def __init__(self):
        gtk.TextBuffer.__init__(self)
        tt = self.get_tag_table()

        del_tag = gtk.TextTag('deleted')
        del_tag.set_property('background', RED)
        tt.add(del_tag)

        add_tag = gtk.TextTag('added')
        add_tag.set_property('background', GREEN)
        tt.add(add_tag)

        chg_tag = gtk.TextTag('changed')
        chg_tag.set_property('background', BLUE)
        tt.add(chg_tag)

        mono_tag = gtk.TextTag('mono')
        mono_tag.set_property('family', 'Monospace')
        mono_tag.set_property('scale', FONTSIZE)
        tt.add(mono_tag)


    def insert_at_end(self, text, *tags):
        tags = tags + ('mono',)
        self.insert_with_tags_by_name(self.get_end_iter(), text, *tags)


    def add_linenumber(self, number):
        self.insert_with_tags_by_name(self.get_end_iter(), str(number)+'\n', 'mono')



class DualBuffer(gtk.HBox):
    def __init__(self):
        gtk.HBox.__init__(self, spacing=4)

        self.numberbuffer = Buffer()

        self.numbertext = gtk.TextView(self.numberbuffer)
        self.numbertext.set_editable(False)
        self.numbertext.set_cursor_visible(False)
        self.numbertext.set_wrap_mode(gtk.WRAP_NONE)
        self.numbertext.set_justification(gtk.JUSTIFY_RIGHT)

        self.numberscroll = gtk.ScrolledWindow()
        self.numberscroll.set_policy(gtk.POLICY_NEVER, gtk.POLICY_NEVER)
        self.numberscroll.set_shadow_type(gtk.SHADOW_IN)
        self.numberscroll.add(self.numbertext)
        
        self.buffer = Buffer()
        
        self.text = gtk.TextView(self.buffer)
        self.text.set_editable(False)
        self.text.set_cursor_visible(False)
        self.text.set_wrap_mode(gtk.WRAP_NONE)
        self.text.set_justification(gtk.JUSTIFY_LEFT)

        self.scroll = gtk.ScrolledWindow()
        self.scroll.set_policy(gtk.POLICY_ALWAYS, gtk.POLICY_ALWAYS)
        self.scroll.set_shadow_type(gtk.SHADOW_IN)
        self.scroll.add(self.text)

        self.pack_start(self.numberscroll, expand=False, fill=False)
        self.pack_start(self.scroll, expand=True, fill=True)

        self.numberscroll.set_vadjustment(self.scroll.get_vadjustment())
        self.set_size_request(-1,10)

    def get_vadjustment(self):
        return self.scroll.get_vadjustment()

    def get_hadjustment(self):
        return self.scroll.get_hadjustment()

    def set_hadjustment(self, adjustment):
        self.scroll.set_hadjustment(adjustment)

    def set_vadjustment(self, adjustment):
        self.scroll.set_vadjustment(adjustment)
        self.numberscroll.set_vadjustment(adjustment)

    def set_size_request(self, x, y):
        self.scroll.set_size_request(x,y)
        self.numberscroll.set_size_request(x,y)

    def insert_at_end(self, text, *tags):
        self.buffer.insert_at_end(text, *tags)

    def add_linenumber(self, number):
        self.numberbuffer.add_linenumber(number)

    def add_blank_line(self, symbol= u'\u2022'):
        self.buffer.insert_at_end('\n')
        self.numberbuffer.insert_at_end('%s\n'%symbol)

    def create_mark(self, *args):
        mark = self.buffer.create_mark(*args)
        if DEBUG: mark.set_visible(True)
        return mark    

    def get_end_iter(self, *args):
        return self.buffer.get_end_iter(*args)

    def get_start_iter(self, *args):
        return self.buffer.get_start_iter(*args)

    def get_iter_at_line(self, *args):
        return self.buffer.get_iter_at_line(*args)

    def scroll_mark_onscreen(self, *args):
        return self.text.scroll_mark_onscreen(*args)

    def scroll_to_mark(self, *args):
        return self.text.scroll_to_mark(*args)


class MainWindow(gtk.Window):
    def __init__(self):
        gtk.Window.__init__(self)
        self.changes = []
        self.connect('destroy', lambda w: gtk.main_quit())
        self.connect('key_press_event', self.key_press_handler)

        self.set_size_request(320, 240)
        self.resize()
        
        self.vbox = gtk.VBox()

        self.toolbarbox = gtk.HBox()

        self.changelabel = gtk.Label()
        self.toolbarbox.pack_start(self.changelabel, padding=PADDING, expand=False, fill=False)
        
        self.changemenu = gtk.combo_box_new_text()
        self.changemenu.connect('changed', self.goto_change)
        self.toolbarbox.pack_start(self.changemenu, expand=False, fill=False)

        self.toolbar = gtk.Toolbar()
        self.toolbar.set_style(gtk.TOOLBAR_ICONS)

        self.upbutton = gtk.ToolButton(gtk.STOCK_GO_UP)
        self.upbutton.connect('clicked', lambda w: self.go(-1))
        self.toolbar.insert(self.upbutton, 0)

        self.dnbutton = gtk.ToolButton(gtk.STOCK_GO_DOWN)
        self.dnbutton.connect('clicked', lambda w: self.go( 1))
        self.toolbar.insert(self.dnbutton, 1)

        self.topbutton = gtk.ToolButton(gtk.STOCK_GOTO_TOP)
        self.topbutton.connect('clicked', lambda w: self.go_to_top())
        self.toolbar.insert(self.topbutton, 2)

        self.btmbutton = gtk.ToolButton(gtk.STOCK_GOTO_BOTTOM)
        self.btmbutton.connect('clicked', lambda w: self.go_to_btm())
        self.toolbar.insert(self.btmbutton, 3)

        self.toolbarbox.pack_start(self.toolbar)
        
        self.vbox.pack_start(self.toolbarbox, expand=False, fill=False)

        self.lbuffer = DualBuffer()        
        self.rbuffer = DualBuffer()
        self.rbuffer.set_hadjustment(self.lbuffer.get_hadjustment())
        self.rbuffer.set_vadjustment(self.lbuffer.get_vadjustment())

        self.hbox = gtk.HBox(homogeneous=True)
        self.hbox.pack_start(self.lbuffer)
        self.hbox.pack_end  (self.rbuffer)

        self.vbox.pack_end(self.hbox, expand=True, fill=True)

        self.add(self.vbox)


    def key_press_handler( self, window, event ):
        if event.keyval == ord('q'):
            self.destroy()


    def goto_change(self, w):
        change_index = self.changemenu.get_active()
        self.scroll_to_change(self.changes[change_index])
        if change_index == 0:
            self.upbutton.set_sensitive(False)
            self.topbutton.set_sensitive(False)
        else:
            self.upbutton.set_sensitive(True)
            self.topbutton.set_sensitive(True)

        if change_index >= len(self.changes) - 1:
            self.dnbutton.set_sensitive(False)
            self.btmbutton.set_sensitive(False)
        else:
            self.dnbutton.set_sensitive(True)
            self.btmbutton.set_sensitive(True)


    def go_to(self, i):
        self.changemenu.set_active(min(max(0,i), len(self.changes)-1))


    def go(self, i):
        self.go_to(self.changemenu.get_active() + i)


    def go_to_top(self):
        self.go_to(0)


    def go_to_btm(self):
        self.go_to(len(self.changes)-1)


    def register_changed_line(self, i, left, right):
        if len(self.changes):
            last_change = self.changes[-1]
            if max((left - last_change.end_left),
                   (right - last_change.end_right)) > min(max(2, len(last_change)//2), 10):
                itr = self.lbuffer.get_iter_at_line(i)
                mark = self.lbuffer.create_mark(None, itr)
                self.changes.append( Change(mark, left, right) )
            else:
                last_change.end_left = left
                last_change.end_right = right
        else:
            itr = self.lbuffer.get_iter_at_line(i)
            if i == -1:
                itr = self.lbuffer.get_iter_at_line(0)
            mark = self.lbuffer.create_mark(None, itr)
            self.changes.append( Change(mark, left, right) )


    def scroll_to_change(self, change):
        margin = 0.25
        self.lbuffer.scroll_to_mark(change.mark, margin)


    def set_title(self, left_name, right_name):
        gtk.Window.set_title(self, '%s %s %s'%(left_name, VERSUS, right_name))


    def resize(self,
               x=min(MAX_WINDOW_WIDTH ,1270),
               y=min(MAX_WINDOW_HEIGHT,1150)):
        gtk.Window.resize(self, x,y)


    def parse_diff(self, result):
        li, ri = 0, 0
        l, r = 1, 1
        lastkind, nextkind = ' ', ' '

        for i, line in enumerate(result):
            kind, line = line[0], line[2:]
            nextkind = ' '
            if len(result) > i+1:
                nextkind = result[i+1][0]
                    
            if DEBUG and (kind     in ('-+?') or
                          nextkind in ('-+?') or
                          lastkind in ('-+?')):
                print kind, line,
                if kind == ' ' and nextkind == ' ':
                    print '-'*75

            if kind == ' ':
                while li < ri:
                    self.lbuffer.add_blank_line()
                    li += 1
                while ri < li:
                    self.rbuffer.add_blank_line()
                    ri += 1
                self.lbuffer.add_linenumber(l)
                self.lbuffer.insert_at_end(line)
                l += 1
                li += 1
                self.rbuffer.add_linenumber(r)
                self.rbuffer.insert_at_end(line)
                r += 1
                ri += 1
                assert(li == ri)
            elif kind in ('+', '-'):
                if li > 0:
                    # if we aren't at the beginning of the buffer
                    self.register_changed_line(li-1, l, r)
                if kind == '+':
                    self.rbuffer.add_linenumber(r)
                    if nextkind == '?':
                        self.dochangeline(self.rbuffer, line, result[i+1][2:])
                    else:
                        self.rbuffer.insert_at_end(line, 'added')
                    r += 1
                    ri += 1
                    if nextkind in (' ', '+', '-'):
                        while li < ri:
                            self.lbuffer.add_blank_line()
                            li += 1
                elif kind == '-':
                    self.lbuffer.add_linenumber(l)
                    if nextkind == '?':
                        self.dochangeline(self.lbuffer, line, result[i+1][2:])
                    else:
                        self.lbuffer.insert_at_end(line, 'deleted')
                    l += 1
                    li += 1
                    if nextkind in (' ', '-'):
                        while ri < li:
                            self.rbuffer.add_blank_line()
                            ri += 1
                if li == 1:
                    # if we were at the beginning of the buffer above
                    # and didn't register a change there
                    self.register_changed_line(li-2, l-1, r-1)
            lastkind = kind


    def diff(self, left_text, right_text, left_name=None, right_name=None):
        d = Differ()
        result = list(d.compare(left_text, right_text))
        self.parse_diff(result)

        if len(self.changes) <= 0:
            print "No changes found between %s and %s" % (left_name,
                                                          right_name)
            sys.exit()

        for i, change in enumerate(self.changes):
            self.changemenu.append_text('#%d: %s'%(i+1,str(change)))
        self.changemenu.set_active(0)

        if len(self.changes) == 1:
            self.changelabel.set_text("1 Change:")
        else:
            self.changelabel.set_text("%d Changes:"%len(self.changes))            
        self.show_all()
        self.set_title(left_name, right_name)
        gobject.idle_add(lambda *a: self.changemenu.emit('changed'))
        gobject.idle_add(lambda *a: self.resize())


    def dochangeline(self, buffer, line, changes):
        p = 0
        last = ' '
        start = 0
        while p < len(changes):
            current = changes[p]
            if current != last:
                if last == '^':
                    buffer.insert_at_end(line[start:p], 'changed')
                elif last == '+':
                    buffer.insert_at_end(line[start:p], 'added'  )
                elif last == '-':
                    buffer.insert_at_end(line[start:p], 'deleted')
                elif last in(' ', '\t'):
                    buffer.insert_at_end(line[start:p])
                else:
                    raise 'found "%s" as last' % last
                start = p
            last = changes[p]
            p += 1
        buffer.insert_at_end(line[p-1:])
        

if len(sys.argv) != 3:
    sys.stderr.write('USAGE: %s file1 file2\n' %
                     os.path.split(sys.argv[0])[1])
    sys.exit()

l, r = sys.argv[1:]

fail_access = False
for f in l,r:
    if not(os.access(f, os.F_OK|os.R_OK)):
        #BUG: check to make sure file is binary
        sys.stderr.write('File "%s" not found.\n'%f)
        fail_access = True

if fail_access:
    sys.exit()

lfile = file(l).readlines()
rfile = file(r).readlines()

w = MainWindow()
w.diff(lfile, rfile, left_name=l, right_name=r)
try:
    gtk.main()
except KeyboardInterrupt:
    pass


