# -*- coding: utf-8 -*-

# Copyright (c) 2015 - 2019 Detlev Offenbach <detlev@die-offenbachs.de>
#

"""
Module implementing a class to store and parse diff output.
"""

from __future__ import unicode_literals

import re
import os


class GitDiffParser(object):
    """
    Class implementing a class to store and parse diff output.
    """
    HunkHeaderRegexp = re.compile(r'^@@ -([0-9,]+) \+([0-9,]+) @@(.*)',
                                  re.DOTALL)
    
    def __init__(self, diff):
        """
        Constructor
        
        @param diff output of the diff command (list of string)
        """
        self.__diff = diff[:]
        
        self.__headerLines = []
        self.__hunks = []
        self.__parsed = False
        # diff parsing is done on demand
    
    def __initHunk(self):
        """
        Private method to initialize a hunk data structure.
        
        @return hunk data structure (dictionary)
        """
        hunk = {
            "start": -1,
            "end": -1,
            "lines": [],
            "oldStart": -1,
            "oldCount": -1,
            "newStart": -1,
            "newCount": -1,
            "heading": "",
        }
        return hunk
    
    def __parseRange(self, headerRange):
        """
        Private method to parse the hunk header range part.
        
        @param headerRange hunk header range (string)
        @return tuple of hunk start and hunk length (integer, integer)
        """
        if ',' in headerRange:
            begin, end = headerRange.split(',', 1)
            return int(begin), int(end)
        else:
            return int(headerRange), 1
    
    def __parseDiff(self):
        """
        Private method to parse the diff output.
        
        @exception AssertionError raised when a malformed hunk header is
            encountered
        """
        if not self.__parsed:
            # step 1: extract the diff header
            for line in self.__diff:
                if not line.startswith("@@ "):
                    self.__headerLines.append(line)
                else:
                    break
            
            # step 2: break the rest into diff hunks
            for lineIdx, line in enumerate(
                    self.__diff[len(self.__headerLines):]):
                # disect the hunk header line
                m = self.HunkHeaderRegexp.match(line)
                if m:
                    self.__hunks.append(self.__initHunk())
                    self.__hunks[-1]["start"] = lineIdx
                    (self.__hunks[-1]["oldStart"],
                     self.__hunks[-1]["oldCount"]) = \
                        self.__parseRange(m.group(1))
                    (self.__hunks[-1]["newStart"],
                     self.__hunks[-1]["newCount"]) = \
                        self.__parseRange(m.group(2))
                    self.__hunks[-1]["heading"] = m.group(3)
                elif not self.__hunks:
                    raise AssertionError("Malformed hunk header: '{0}'"
                                         .format(line))
                self.__hunks[-1]["lines"].append(line)
            # step 3: calculate hunk end lines
            for hunk in self.__hunks:
                hunk["end"] = hunk["start"] + len(hunk["lines"]) - 1
    
    def __generateRange(self, start, count):
        """
        Private method to generate a hunk header range.
        
        @param start start line (integer)
        @param count line count (integer)
        @return hunk header range (string)
        """
        if count == 1:
            return "{0}".format(start)
        else:
            return "{0},{1}".format(start, count)
    
    def __generateHunkHeader(self, oldStart, oldCount, newStart, newCount,
                             heading=os.linesep):
        """
        Private method to generate a hunk header line.
        
        @param oldStart start line of the old part (integer)
        @param oldCount line count of the old part (integer)
        @param newStart start line of the new part (integer)
        @param newCount line count of the new part (integer)
        @param heading hunk heading (string)
        @return hunk header (string)
        """
        return "@@ -{0} +{1} @@{2}".format(
            self.__generateRange(oldStart, oldCount),
            self.__generateRange(newStart, newCount),
            heading)
    
    def headerLength(self):
        """
        Public method to get the header length.
        
        @return length of the header (integer)
        """
        self.__parseDiff()
        return len(self.__headerLines)
    
    def createHunkPatch(self, lineIndex):
        """
        Public method to create a hunk based patch.
        
        @param lineIndex line number of the hunk (integer)
        @return diff lines of the patch (string)
        """
        self.__parseDiff()
        
        patch = self.__headerLines[:]
        for hunk in self.__hunks:
            if hunk["start"] <= lineIndex <= hunk["end"]:
                patch.extend(hunk["lines"])
                break
        
        return "".join(patch)
    
    def createLinesPatch(self, startIndex, endIndex, reverse=False):
        """
        Public method to create a selected lines based patch.
        
        @param startIndex start line number (integer)
        @param endIndex end line number (integer)
        @param reverse flag indicating a reverse patch (boolean)
        @return diff lines of the patch (string)
        """
        self.__parseDiff()
        
        ADDITION = "+"
        DELETION = "-"
        CONTEXT = " "
        NONEWLINE = "\\"
        
        patch = []
        startOffset = 0
        
        for hunk in self.__hunks:
            if hunk["end"] < startIndex:
                # skip hunks before the selected lines
                continue
            
            if hunk["start"] > endIndex:
                # done, exit the loop
                break
            
            counts = {
                ADDITION: 0,
                DELETION: 0,
                CONTEXT: 0,
            }
            previousLineSkipped = False
            processedLines = []
            
            for lineIndex, line in enumerate(hunk["lines"][1:],
                                             start=hunk["start"] + 1):
                lineType = line[0]
                lineContent = line[1:]
                
                if not (startIndex <= lineIndex <= endIndex):
                    if (not reverse and lineType == ADDITION) or \
                            (reverse and lineType == DELETION):
                        previousLineSkipped = True
                        continue
                    
                    elif (not reverse and lineType == DELETION) or \
                            (reverse and lineType == ADDITION):
                        lineType = CONTEXT
                
                if lineType == NONEWLINE and previousLineSkipped:
                    continue
                
                processedLines.append(lineType + lineContent)
                counts[lineType] += 1
                previousLineSkipped = False
            
            # hunks consisting of pure context lines are excluded
            if counts[ADDITION] == 0 and counts[DELETION] == 0:
                continue
            
            oldCount = counts[CONTEXT] + counts[DELETION]
            newCount = counts[CONTEXT] + counts[ADDITION]
            oldStart = hunk["oldStart"]
            newStart = oldStart + startOffset
            if oldCount == 0:
                newStart += 1
            if newCount == 0:
                newStart -= 1
            
            startOffset += counts[ADDITION] - counts[DELETION]
            
            patch.append(self.__generateHunkHeader(oldStart, oldCount,
                                                   newStart, newCount,
                                                   hunk["heading"]))
            patch.extend(processedLines)
        
        if not patch:
            return ""
        else:
            return "".join(self.__headerLines[:] + patch)
