"""
    show page diffs
    generates line or word/char diffs depending on config.diff_by_line=0/1
    this is mostly taken from difflib and modified to output html difference

    markup:
    - whole wiki text of pages is shown
    - inserts use <u class="diff"> - underline, maybe coloured by css
    - deletes use <s class="diff"> - strike through, maybe coloured by css
    I intentionally didn't use <ins> and <del> as this is only in html >=4.0,
    so netscape 4.7x users would have problems.

    changes by ThomasWaldmann@gmx.de are GPL licensed

    changes compared to old behaviour:
    - changes in whitespace amount are generally ignored - and this should
      stay, as this diff avoids having "junk" in the diff it calculates, which
      gives more "natural" diffs in some cases.
      relating to wiki, the only problem with this is that indentation changes
      are not in the diff.
    - diffs are always "fancy", show_fancy_diff user setting not used any more

    todo:
    - add a difference/similarity ratio to RecentChanges (so one can immediately
      see if a change was minor, major or maybe even vandalism, I think ratio()
      of SequenceMatcher could easily do that.
      
    juergen: please review and move to wikiutil or whereever you think it fits
             best.
"""

from __future__ import generators

import re, string
from MoinMoin import config
from MoinMoin.support import difflib

class htmlDiffer:
    r"""
    htmlDiffer is a class for comparing sequences of lines of text, and
    producing human-readable difference markup.  htmlDiffer uses
    SequenceMatcher both to compare sequences of lines, and to compare
    sequences of characters within similar (near-matching) lines.

    Methods:

    __init__(linejunk=None, charjunk=None)
        Construct a text differencer, with optional filters.

    compare(a, b)
        Compare two sequences of lines; generate the diff markup.
    """

    def __init__(self, linejunk=None, charjunk=None):
        """
        Construct a text differencer, with optional filters.

        The two optional keyword parameters are for filter functions:

        - `linejunk`: A function that should accept a single string argument,
          and return true iff the string is junk. The module-level function
          `IS_LINE_JUNK` may be used to filter out lines without visible
          characters, except for at most one splat ('#').

        - `charjunk`: A function that should accept a string of length 1. The
          module-level function `IS_CHARACTER_JUNK` may be used to filter out
          whitespace characters (a blank or tab; **note**: bad idea to include
          newline in this!).
        """

        self.linejunk = linejunk
        self.charjunk = charjunk

    def compare(self, a, b):
        r"""
        Compare two sequences of lines; generate the resulting delta.

        Each sequence must contain individual single-line strings ending with
        newlines. Such sequences can be obtained from the `readlines()` method
        of file-like objects.  The delta generated also consists of newline-
        terminated strings, ready to be printed as-is via the writeline()
        method of a file-like object.
        """

        cruncher = difflib.SequenceMatcher(self.linejunk, a, b)
        for tag, alo, ahi, blo, bhi in cruncher.get_opcodes():
            if tag == 'replace':
                g = self._fancy_replace(a, alo, ahi, b, blo, bhi)
            elif tag == 'delete':
                g = self._dump('<s class="diff">', '</s>', a, alo, ahi)
            elif tag == 'insert':
                g = self._dump('<u class="diff">', '</u>', b, blo, bhi)
            elif tag == 'equal':
                g = self._dump('', '', a, alo, ahi)
            else:
                raise ValueError, 'unknown tag ' + `tag`

            for line in g:
                yield line

    def _dump(self, stag, etag, x, lo, hi):
        """Generate comparison results for a same-tagged range."""
        for i in xrange(lo, hi):
            yield '%s%s%s' % (stag, x[i], etag)

    def _plain_replace(self, a, alo, ahi, b, blo, bhi):
        assert alo < ahi and blo < bhi
        # dump the shorter block first -- reduces the burden on short-term
        # memory if the blocks are of very different sizes
        if bhi - blo < ahi - alo:
            first  = self._dump('<u class="diff">', '</u>', b, blo, bhi)
            second = self._dump('<s class="diff">', '</s>', a, alo, ahi)
        else:
            first  = self._dump('<s class="diff">', '</s>', a, alo, ahi)
            second = self._dump('<u class="diff">', '</u>', b, blo, bhi)

        for g in first, second:
            for line in g:
                yield line

    def _fancy_replace(self, a, alo, ahi, b, blo, bhi):
        r"""
        When replacing one block of lines with another, search the blocks
        for *similar* lines; the best-matching pair (if any) is used as a
        synch point, and intraline difference marking is done on the
        similar pair. Lots of work, but often worth it.
        """
        if config.diff_by_line: # do only a line diff, no word diff
            for line in self._plain_replace(a, alo, ahi, b, blo, bhi):
                yield line
            return

        # don't synch up unless the lines have a similarity score of at
        # least cutoff; best_ratio tracks the best score seen so far
        best_ratio, cutoff = 0.74, 0.75
        cruncher = difflib.SequenceMatcher(self.charjunk)
        eqi, eqj = None, None   # 1st indices of equal lines (if any)

        # search for the pair that matches best without being identical
        # (identical lines must be junk lines, & we don't want to synch up
        # on junk -- unless we have to)
        for j in xrange(blo, bhi):
            bj = b[j]
            cruncher.set_seq2(bj)
            for i in xrange(alo, ahi):
                ai = a[i]
                if ai == bj:
                    if eqi is None:
                        eqi, eqj = i, j
                    continue
                cruncher.set_seq1(ai)
                # computing similarity is expensive, so use the quick
                # upper bounds first -- have seen this speed up messy
                # compares by a factor of 3.
                # note that ratio() is only expensive to compute the first
                # time it's called on a sequence pair; the expensive part
                # of the computation is cached by cruncher
                if cruncher.real_quick_ratio() > best_ratio and \
                      cruncher.quick_ratio() > best_ratio and \
                      cruncher.ratio() > best_ratio:
                    best_ratio, best_i, best_j = cruncher.ratio(), i, j
        if best_ratio < cutoff:
            # no non-identical "pretty close" pair
            if eqi is None:
                # no identical pair either -- treat it as a straight replace
                for line in self._plain_replace(a, alo, ahi, b, blo, bhi):
                    yield line
                return
            # no close pair, but an identical pair -- synch up on that
            best_i, best_j, best_ratio = eqi, eqj, 1.0
        else:
            # there's a close pair, so forget the identical pair (if any)
            eqi = None

        # a[best_i] very similar to b[best_j]; eqi is None iff they're not
        # identical

        # pump out diffs from before the synch point
        for line in self._fancy_helper(a, alo, best_i, b, blo, best_j):
            yield line

        # do intraline marking on the synch pair
        aelt, belt = a[best_i], b[best_j]
        if eqi is None:
            # pump out output for the synched lines
            cruncher.set_seqs(aelt, belt)
	    line = ''
            for tag, ai1, ai2, bj1, bj2 in cruncher.get_opcodes():
                la, lb = ai2 - ai1, bj2 - bj1
                if tag == 'replace':
                    line += '<s class="diff">%s</s><u class="diff">%s</u>' % (aelt[ai1:ai2],belt[bj1:bj2])
                elif tag == 'delete':
                    line += '<s class="diff">%s</s>' % (aelt[ai1:ai2],)
                elif tag == 'insert':
                    line += '<u class="diff">%s</u>' % (belt[bj1:bj2],)
                elif tag == 'equal':
                    line += "%s" % (aelt[ai1:ai2],)
                else:
                    raise ValueError, 'unknown tag ' + `tag`
	    yield line
        else:
            # the synch pair is identical
            yield aelt

        # pump out diffs from after the synch point
        for line in self._fancy_helper(a, best_i+1, ahi, b, best_j+1, bhi):
            yield line

    def _fancy_helper(self, a, alo, ahi, b, blo, bhi):
        g = []
        if alo < ahi:
            if blo < bhi:
                g = self._fancy_replace(a, alo, ahi, b, blo, bhi)
            else:
                g = self._dump('<s class="diff">', '</s>', a, alo, ahi)
        elif blo < bhi:
            g = self._dump('<u class="diff">', '</u>', b, blo, bhi)

        for line in g:
            yield line

def IS_LINE_JUNK(line, pat=re.compile(r"\s*$").match):
    r"""
    Return 1 for ignorable line: iff `line` is blank.

    Examples:

    >>> IS_LINE_JUNK('\n')
    1
    >>> IS_LINE_JUNK('hello\n')
    0
    """

    return pat(line) is not None

def IS_CHARACTER_JUNK(ch, ws=" \t"):
    r"""
    Return 1 for ignorable character: iff `ch` is a space or tab.

    Examples:

    >>> IS_CHARACTER_JUNK(' ')
    1
    >>> IS_CHARACTER_JUNK('\t')
    1
    >>> IS_CHARACTER_JUNK('\n')
    0
    >>> IS_CHARACTER_JUNK('x')
    0
    """

    return ch in ws


def wikidiff(a, b, linejunk=IS_LINE_JUNK,charjunk=IS_CHARACTER_JUNK):
    r"""
    Compare `a` and `b` (lists of strings); return a `Differ`-style delta.

    Optional keyword parameters `linejunk` and `charjunk` are for filter
    functions (or None):

    - linejunk: A function that should accept a single string argument, and
      return true iff the string is junk. The default is module-level function
      IS_LINE_JUNK, which filters out lines without visible characters.
    """
    return htmlDiffer(linejunk,charjunk).compare(a, b)
