# -*-python-*-
#
# Copyright (C) 1999-2026 The ViewCVS Group. All Rights Reserved.
#
# By using this file, you agree to the terms and conditions set forth in
# the LICENSE.html file which can be found at the top level of the ViewVC
# distribution or at http://viewvc.org/license-1.html.
#
# For more information, visit http://viewvc.org/
#
# -----------------------------------------------------------------------
#
# idiff: display differences between files highlighting intraline changes
#
# -----------------------------------------------------------------------

import difflib
import re

from common import _item, _RCSDIFF_NO_CHANGES
import ezt
import sapi


def sidebyside(fromlines, tolines, context):
    """Generate side by side diff"""

    # for some reason mdiff chokes on \n's in input lines
    def _stripnl(line):
        return line.rstrip("\n")

    fromlines = list(map(_stripnl, fromlines))
    tolines = list(map(_stripnl, tolines))
    had_changes = 0

    gap = False
    for fromdata, todata, flag in difflib._mdiff(fromlines, tolines, context):
        if fromdata is None and todata is None and flag is None:
            gap = True
        else:
            from_item = _mdiff_split(flag, fromdata)
            to_item = _mdiff_split(flag, todata)
            had_changes = 1
            yield _item(gap=ezt.boolean(gap), columns=(from_item, to_item), type="intraline")
            gap = False
    if not had_changes:
        yield _item(type=_RCSDIFF_NO_CHANGES)


_re_mdiff = re.compile("\0([+-^])(.*?)\1")


def _mdiff_split(flag, line_number_text):
    """Break up row from mdiff output into segments"""
    line_number, text = line_number_text
    segments = []
    pos = 0
    while True:
        m = _re_mdiff.search(text, pos)
        if not m:
            segments.append(_item(text=sapi.escape(text[pos:]), type=None))
            break

        if m.start() > pos:
            segments.append(_item(text=sapi.escape(text[pos : m.start()]), type=None))

        if m.group(1) == "+":
            segments.append(_item(text=sapi.escape(m.group(2)), type="add"))
        elif m.group(1) == "-":
            segments.append(_item(text=sapi.escape(m.group(2)), type="remove"))
        elif m.group(1) == "^":
            segments.append(_item(text=sapi.escape(m.group(2)), type="change"))

        pos = m.end()

    return _item(segments=segments, line_number=line_number)


def unified(fromlines, tolines, context):
    """Generate unified diff"""

    diff = difflib.Differ().compare(fromlines, tolines)
    lastrow = None
    had_changes = 0

    for row in _trim_context(diff, context):
        if row[0].startswith("? "):
            had_changes = 1
            yield _differ_split(lastrow, row[0])
            lastrow = None
        else:
            if lastrow:
                had_changes = 1
                yield _differ_split(lastrow, None)
            lastrow = row

    if lastrow:
        had_changes = 1
        yield _differ_split(lastrow, None)

    if not had_changes:
        yield _item(type=_RCSDIFF_NO_CHANGES)


def _trim_context(lines, context_size):
    """Trim context lines that don't surround changes from Differ results

    yields (line, leftnum, rightnum, gap) tuples"""

    # circular buffer to hold context lines
    context_buffer = [None] * (context_size or 0)
    context_start = context_len = 0

    # number of context lines left to print after encountering a change
    context_owed = 0

    # current line numbers
    leftnum = rightnum = 0

    # whether context lines have been dropped
    gap = False

    for line in lines:
        row = save = None

        if line.startswith("- "):
            leftnum = leftnum + 1
            row = line, leftnum, None
            context_owed = context_size

        elif line.startswith("+ "):
            rightnum = rightnum + 1
            row = line, None, rightnum
            context_owed = context_size

        else:
            if line.startswith("  "):
                leftnum = leftnum = leftnum + 1
                rightnum = rightnum = rightnum + 1
                if context_owed > 0:
                    context_owed = context_owed - 1
                elif context_size is not None:
                    save = True

            row = line, leftnum, rightnum

        if save:
            # don't yield row right away, store it in buffer
            context_buffer[(context_start + context_len) % context_size] = row
            if context_len == context_size:
                context_start = (context_start + 1) % context_size
                gap = True
            else:
                context_len = context_len + 1
        else:
            # yield row, but first drain stuff in buffer
            context_len == context_size
            while context_len:
                yield context_buffer[context_start] + (gap,)
                gap = False
                context_start = (context_start + 1) % context_size
                context_len = context_len - 1
            yield row + (gap,)
            gap = False


_re_differ = re.compile(r"[+-^]+")


def _differ_split(row, guide):
    """Break row into segments using guide line"""
    line, left_number, right_number, gap = row

    if left_number and right_number:
        type = ""
    elif left_number:
        type = "remove"
    elif right_number:
        type = "add"

    segments = []
    pos = 2

    if guide:
        assert guide.startswith("? ")

        for m in _re_differ.finditer(guide, pos):
            if m.start() > pos:
                segments.append(_item(text=sapi.escape(line[pos : m.start()]), type=None))
            segments.append(_item(text=sapi.escape(line[m.start() : m.end()]), type="change"))
            pos = m.end()

    segments.append(_item(text=sapi.escape(line[pos:]), type=None))

    return _item(
        gap=ezt.boolean(gap),
        type=type,
        segments=segments,
        left_number=left_number,
        right_number=right_number,
    )


try:
    # Using difflib._mdiff function here was the easiest way of obtaining
    # intraline diffs for use in ViewVC, but it doesn't exist prior to
    # Python 2.4 and is not part of the public difflib API, so for now
    # fall back if it doesn't exist.
    difflib._mdiff
except AttributeError:
    sidebyside = None  # noqa: F811
