##############################################################################
#
# Copyright (c) 2004 Zope Foundation and Contributors.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
""" Diff utilities for comparing configurations.
"""

from difflib import unified_diff
import re

from AccessControl.SecurityInfo import ClassSecurityInfo
from App.class_init import InitializeClass

from Products.GenericSetup.interfaces import SKIPPED_FILES

BLANKS_REGEX = re.compile( r'^\s*$' )

def unidiff( a
           , b
           , filename_a='original'
           , timestamp_a=None
           , filename_b='modified'
           , timestamp_b=None
           , ignore_blanks=False
           ):
    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.

    Note that the last line of a file may *not* have a newline;
    this is reported in the same way that GNU diff reports this.
    *This method only supports UNIX line ending conventions.*

        filename_a and filename_b are used to generate the header,
        allowing other tools to determine what 'files' were used
        to generate this output.

        timestamp_a and timestamp_b, when supplied, are expected
        to be last-modified timestamps to be inserted in the
        header, as floating point values since the epoch.

    Example:

    >>> print ''.join(UniDiffer().compare(
    ...     'one\ntwo\nthree\n'.splitlines(1),
    ...     'ore\ntree\nemu\n'.splitlines(1))),
    +++ original
    --- modified
    @@ -1,3 +1,3 @@
    -one
    +ore
    -two
    -three
    +tree
    +emu
    """
    if isinstance( a, basestring ):
        a = a.splitlines()

    if isinstance( b, basestring ):
        b = b.splitlines()

    if ignore_blanks:
        a = [ x for x in a if not BLANKS_REGEX.match( x ) ]
        b = [ x for x in b if not BLANKS_REGEX.match( x ) ]

    return unified_diff( a
                       , b
                       , filename_a
                       , filename_b
                       , timestamp_a
                       , timestamp_b
                       , lineterm=""
                       )


class ConfigDiff:

    security = ClassSecurityInfo()

    def __init__( self
                , lhs
                , rhs
                , missing_as_empty=False
                , ignore_blanks=False
                , skip=SKIPPED_FILES
                ):
        self._lhs = lhs
        self._rhs = rhs
        self._missing_as_empty = missing_as_empty
        self._ignore_blanks=ignore_blanks
        self._skip = skip

    security.declarePrivate( 'compareDirectories' )
    def compareDirectories( self, subdir=None ):

        lhs_files = self._lhs.listDirectory( subdir, self._skip )
        if lhs_files is None:
            lhs_files = []

        rhs_files = self._rhs.listDirectory( subdir, self._skip )
        if rhs_files is None:
            rhs_files = []

        added = [ f for f in rhs_files if f not in lhs_files ]
        removed = [ f for f in lhs_files if f not in rhs_files ]
        all_files = lhs_files + added
        all_files.sort()

        result = []

        for filename in all_files:

            if subdir is None:
                pathname = filename
            else:
                pathname = '%s/%s' % ( subdir, filename )

            if filename not in added:
                isDirectory = self._lhs.isDirectory( pathname )
            else:
                isDirectory = self._rhs.isDirectory( pathname )

            if not self._missing_as_empty and filename in removed:

                if isDirectory:
                    result.append( '** Directory %s removed\n' % pathname )
                    result.extend( self.compareDirectories( pathname ) )
                else:
                    result.append( '** File %s removed\n' % pathname )

            elif not self._missing_as_empty and filename in added:

                if isDirectory:
                    result.append( '** Directory %s added\n' % pathname )
                    result.extend( self.compareDirectories( pathname ) )
                else:
                    result.append( '** File %s added\n' % pathname )

            elif isDirectory:

                result.extend( self.compareDirectories( pathname ) )

                if ( filename not in added + removed and
                    not self._rhs.isDirectory( pathname ) ):

                    result.append( '** Directory %s replaced with a file of '
                                   'the same name\n' % pathname )

                    if self._missing_as_empty:
                        result.extend( self.compareFiles( filename, subdir ) )
            else:
                if ( filename not in added + removed and
                     self._rhs.isDirectory( pathname ) ):

                    result.append( '** File %s replaced with a directory of '
                                   'the same name\n' % pathname )

                    if self._missing_as_empty:
                        result.extend( self.compareFiles( filename, subdir ) )

                    result.extend( self.compareDirectories( pathname ) )
                else:
                    result.extend( self.compareFiles( filename, subdir ) )

        return result

    security.declarePrivate( 'compareFiles' )
    def compareFiles( self, filename, subdir=None ):

        if subdir is None:
            path = filename
        else:
            path = '%s/%s' % ( subdir, filename )

        lhs_file = self._lhs.readDataFile( filename, subdir )
        lhs_time = self._lhs.getLastModified( path )

        if lhs_file is None:
            assert self._missing_as_empty
            lhs_file = ''
            lhs_time = 0

        rhs_file = self._rhs.readDataFile( filename, subdir )
        rhs_time = self._rhs.getLastModified( path )

        if rhs_file is None:
            assert self._missing_as_empty
            rhs_file = ''
            rhs_time = 0

        if lhs_file == rhs_file:
            diff_lines = []
        else:
            diff_lines = unidiff( lhs_file
                                , rhs_file
                                , filename_a=path
                                , timestamp_a=lhs_time
                                , filename_b=path
                                , timestamp_b=rhs_time
                                , ignore_blanks=self._ignore_blanks
                                )
            diff_lines = list( diff_lines ) # generator

        if len( diff_lines ) == 0: # No *real* difference found
            return []

        diff_lines.insert( 0, 'Index: %s' % path )
        diff_lines.insert( 1, '=' * 67 )

        return diff_lines

    security.declarePrivate( 'compare' )
    def compare( self ):
        return '\n'.join( [str(line) for line in self.compareDirectories()] )

InitializeClass( ConfigDiff )
