/*
 * ====================================================================
 * Copyright (c) 2004-2012 TMate Software Ltd.  All rights reserved.
 *
 * This software is licensed as described in the file COPYING, which
 * you should have received as part of this distribution.  The terms
 * are also available at http://svnkit.com/license.html
 * If newer versions of this license are posted there, you may use a
 * newer version instead, at your option.
 * ====================================================================
 */
package org.tmatesoft.svn.core;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;

import org.tmatesoft.svn.core.internal.util.SVNDate;
import org.tmatesoft.svn.core.internal.util.SVNPathUtil;
import org.tmatesoft.svn.core.internal.wc.SVNErrorManager;
import org.tmatesoft.svn.core.internal.wc.SVNEventFactory;
import org.tmatesoft.svn.core.internal.wc.SVNFileUtil;
import org.tmatesoft.svn.core.internal.wc.admin.SVNTranslator;
import org.tmatesoft.svn.core.internal.wc.admin.SVNTranslatorInputStream;
import org.tmatesoft.svn.core.io.ISVNFileRevisionHandler;
import org.tmatesoft.svn.core.io.SVNFileRevision;
import org.tmatesoft.svn.core.io.SVNRepository;
import org.tmatesoft.svn.core.io.diff.SVNDeltaProcessor;
import org.tmatesoft.svn.core.io.diff.SVNDiffWindow;
import org.tmatesoft.svn.core.wc.ISVNAnnotateHandler;
import org.tmatesoft.svn.core.wc.ISVNEventHandler;
import org.tmatesoft.svn.core.wc.SVNDiffOptions;
import org.tmatesoft.svn.core.wc.SVNEvent;
import org.tmatesoft.svn.core.wc.SVNEventAction;
import org.tmatesoft.svn.util.SVNLogType;

import de.regnis.q.sequence.QSequenceDifferenceBlock;
import de.regnis.q.sequence.line.QSequenceLineMedia;
import de.regnis.q.sequence.line.QSequenceLineRAFileData;
import de.regnis.q.sequence.line.QSequenceLineResult;
import de.regnis.q.sequence.line.simplifier.QSequenceLineDummySimplifier;
import de.regnis.q.sequence.line.simplifier.QSequenceLineEOLUnifyingSimplifier;
import de.regnis.q.sequence.line.simplifier.QSequenceLineSimplifier;
import de.regnis.q.sequence.line.simplifier.QSequenceLineTeeSimplifier;
import de.regnis.q.sequence.line.simplifier.QSequenceLineWhiteSpaceReducingSimplifier;
import de.regnis.q.sequence.line.simplifier.QSequenceLineWhiteSpaceSkippingSimplifier;


/**
 * The <b>SVNAnnotationGenerator</b> class is used to annotate files - that is
 * to place author and revision information in-line for the specified
 * file.
 * 
 * <p>
 * Since <b>SVNAnnotationGenerator</b> implements <b>ISVNFileRevisionHandler</b>,
 * it is merely passed to a {@link org.tmatesoft.svn.core.io.SVNRepository#getFileRevisions(String, long, long, ISVNFileRevisionHandler) getFileRevisions()} 
 * method of <b>SVNRepository</b>. After that you handle the resultant annotated 
 * file line-by-line providing an <b>ISVNAnnotateHandler</b> implementation to the {@link #reportAnnotations(ISVNAnnotateHandler, String) reportAnnotations()}
 * method:
 * <pre class="javacode">
 * <span class="javakeyword">import</span> org.tmatesoft.svn.core.SVNAnnotationGenerator;
 * <span class="javakeyword">import</span> org.tmatesoft.svn.core.io.SVNRepositoryFactory;
 * <span class="javakeyword">import</span> org.tmatesoft.svn.core.io.SVNRepository;
 * <span class="javakeyword">import</span> org.tmatesoft.svn.core.wc.SVNAnnotateHandler;
 * ...
 * 
 *     File tmpFile;
 *     SVNRepository repos;
 *     ISVNAnnotateHandler annotateHandler;
 *     ISVNEventHandler cancelHandler;
 *     <span class="javakeyword">long</span> startRev = 0;
 *     <span class="javakeyword">long</span> endRev = 150;
 *     ...
 *     
 *     SVNAnnotationGenerator generator = <span class="javakeyword">new</span> SVNAnnotationGenerator(path, tmpFile, cancelHandler);
 *     <span class="javakeyword">try</span> {
 *         repos.getFileRevisions(<span class="javastring">""</span>, startRev, endRev, generator);
 *         generator.reportAnnotations(annotateHandler, <span class="javakeyword">null</span>);
 *     } <span class="javakeyword">finally</span> {
 *         generator.dispose();
 *     }
 * ...</pre>
 *   
 * @version 1.3
 * @since   1.2
 * @author  TMate Software Ltd.
 */
public class SVNAnnotationGenerator implements ISVNFileRevisionHandler {

    private File myTmpDirectory;
    private boolean myIsTmpDirCreated;
    private String myPath;

    private long myCurrentRevision;
    private String myCurrentAuthor;
    private Date myCurrentDate;
    private boolean myIsCurrentResultOfMerge;
    private String myCurrentPath;
    
    private File myPreviousFile;
    private File myPreviousOriginalFile;
    private File myCurrentFile;

    private List myMergeBlameChunks;
    private List myBlameChunks;
    private SVNDeltaProcessor myDeltaProcessor;
    private ISVNEventHandler myCancelBaton;
    private long myStartRevision;
    private boolean myIsForce;
    private boolean myIncludeMergedRevisions;
    private SVNDiffOptions myDiffOptions;
    private QSequenceLineSimplifier mySimplifier;
    private ISVNAnnotateHandler myFileHandler;
    private String myEncoding;
    private boolean myIsLastRevisionReported;
    
    /**
     * Constructs an annotation generator object. 
     * 
     * <p>
     * This constructor is equivalent to 
     * <code>SVNAnnotationGenerator(path, tmpDirectory, startRevision, false, cancelBaton)</code>.
     * 
     * @param path           a file path (relative to a repository location)
     * @param tmpDirectory   a revision to stop at
     * @param startRevision  a start revision to begin annotation with
     * @param cancelBaton    a baton which is used to check if an operation 
     *                       is cancelled
     */
    public SVNAnnotationGenerator(String path, File tmpDirectory, long startRevision, ISVNEventHandler cancelBaton) {
        this(path, tmpDirectory, startRevision, false, cancelBaton);
        
    }
    
    /**
     * Constructs an annotation generator object. 
     * 
     * <p/>
     * This constructor is identical to <code>SVNAnnotationGenerator(path, tmpDirectory, startRevision, force, new SVNDiffOptions(), cancelBaton)</code>.
     * 
     * @param path           a file path (relative to a repository location)
     * @param tmpDirectory   a revision to stop at
     * @param startRevision  a start revision to begin annotation with
     * @param force          forces binary files processing  
     * @param cancelBaton    a baton which is used to check if an operation 
     *                       is cancelled
     */
    public SVNAnnotationGenerator(String path, File tmpDirectory, long startRevision, boolean force, ISVNEventHandler cancelBaton) {
        this(path, tmpDirectory, startRevision, force, new SVNDiffOptions(), cancelBaton);
    }

    /**
     * Constructs an annotation generator object.
     * 
     * <p/>
     * This constructor is identical to <code>SVNAnnotationGenerator(path, tmpDirectory, startRevision, force, false, diffOptions, null, null, cancelBaton)</code>.
     * 
     * @param path           a file path (relative to a repository location)
     * @param tmpDirectory   a revision to stop at
     * @param startRevision  a start revision to begin annotation with
     * @param force          forces binary files processing  
     * @param diffOptions    diff options 
     * @param cancelBaton    a baton which is used to check if an operation 
     *                       is cancelled
     */
    public SVNAnnotationGenerator(String path, File tmpDirectory, long startRevision, boolean force, SVNDiffOptions diffOptions, ISVNEventHandler cancelBaton) {
        this(path, tmpDirectory, startRevision, force, false, diffOptions, null, null, cancelBaton);
    }

    /**
     * Constructs an annotation generator object.
     * 
     * @param path                    a file path (relative to a repository location) or URL-encoded URL
     * @param tmpDirectory            a revision to stop at
     * @param startRevision           a start revision to begin annotation with
     * @param force                   forces binary files processing  
     * @param includeMergedRevisions  whether to include merged revisions or not
     * @param diffOptions             diff options 
     * @param encoding                charset name to use to encode annotation result
     * @param handler                 caller's annotation handler implementation 
     * @param cancelBaton             a baton which is used to check if an operation 
     *                                is cancelled
     * @since                         1.2.0 
     */
    public SVNAnnotationGenerator(String path, File tmpDirectory, long startRevision, boolean force, boolean includeMergedRevisions, 
            SVNDiffOptions diffOptions, String encoding, ISVNAnnotateHandler handler, ISVNEventHandler cancelBaton) {
        myTmpDirectory = tmpDirectory;
        myCancelBaton = cancelBaton;
        myPath = path;
        myIsForce = force;
        
        // TODO fail if file has been specified.
        if (!myTmpDirectory.isDirectory()) {
            myTmpDirectory.mkdirs();
            myIsTmpDirCreated = true;
        }
        myMergeBlameChunks = new ArrayList();
        myBlameChunks = new ArrayList();
        myDeltaProcessor = new SVNDeltaProcessor();
        myStartRevision = startRevision;
        myDiffOptions = diffOptions == null ? new SVNDiffOptions() : diffOptions;
        myIncludeMergedRevisions = includeMergedRevisions;
        myFileHandler = handler;
        myEncoding = encoding;
    }
    
    /**
     * Handles a next revision.
     * @param fileRevision
     * @throws SVNException if one of the following occurs:
     *                      <ul>
     *                      <li>exception with {@link SVNErrorCode#CLIENT_IS_BINARY_FILE} error code - if the file is binary and no 
     *                      forcing is specified 
     *                      <li>operation is cancelled
     *                      </ul>
     */
    public void openRevision(SVNFileRevision fileRevision) throws SVNException {
        SVNProperties propDiff = fileRevision.getPropertiesDelta();
        String newMimeType = propDiff != null ? propDiff.getStringValue(SVNProperty.MIME_TYPE) : null;
        if (!myIsForce && SVNProperty.isBinaryMimeType(newMimeType)) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.CLIENT_IS_BINARY_FILE, "Cannot calculate blame information for binary file ''{0}''", myPath);
            SVNErrorManager.error(err, SVNLogType.DEFAULT);
        }
        if (fileRevision.getRevision() < myStartRevision) {
            myCurrentRevision = SVNRepository.INVALID_REVISION;
        } else {
            myCurrentRevision = fileRevision.getRevision();
        }
        boolean known = fileRevision.getRevision() >= myStartRevision;
        SVNProperties props = fileRevision.getRevisionProperties();
        if (myCancelBaton != null) {
            File file = SVNPathUtil.isURL(myPath) ? null : new File(myPath);
            SVNEvent event = SVNEventFactory.createSVNEvent(file, SVNNodeKind.NONE, null, myCurrentRevision, SVNEventAction.ANNOTATE, null, null, null, props, null);
            if (file == null) {
                event.setURL(SVNURL.parseURIEncoded(myPath));
            }
            myCancelBaton.handleEvent(event, ISVNEventHandler.UNKNOWN);
            myCancelBaton.checkCancelled();
        }
        if (known && props != null && props.getStringValue(SVNRevisionProperty.AUTHOR) != null) {
            myCurrentAuthor = props.getStringValue(SVNRevisionProperty.AUTHOR);
        } else {
            myCurrentAuthor = null;
        }
        if (known && props != null && props.getStringValue(SVNRevisionProperty.DATE) != null) {
            myCurrentDate = SVNDate.parseDate(fileRevision.getRevisionProperties().getStringValue(SVNRevisionProperty.DATE));
        } else {
            myCurrentDate = null;
        }
        
        myIsCurrentResultOfMerge = fileRevision.isResultOfMerge();
        if (myIncludeMergedRevisions) {
            myCurrentPath = fileRevision.getPath();
        }
    }
    
    /**
     * Does nothing.
     * 
     * @param token       
     * @throws SVNException
     */
    public void closeRevision(String token) throws SVNException {
    }
    
    /**
     * Creates a temporary file for delta application.
     * 
     * @param  token             not used in this method 
     * @param  baseChecksum      not used in this method
     * @throws SVNException 
     */
    public void applyTextDelta(String token, String baseChecksum) throws SVNException {
        if (myCurrentFile == null) {
            myCurrentFile = SVNFileUtil.createUniqueFile(myTmpDirectory, "annotate", ".tmp", false);
        }
        myDeltaProcessor.applyTextDelta(myPreviousFile, myCurrentFile, false);
    }

    /**
     * Applies a next text delta chunk.
     *  
     * @param  token          not used in this method 
     * @param  diffWindow     next diff window 
     * @return                dummy output stream
     * @throws SVNException 
     */
    public OutputStream textDeltaChunk(String token, SVNDiffWindow diffWindow) throws SVNException {
        return myDeltaProcessor.textDeltaChunk(diffWindow);
    }
    
    /**
     * Marks the end of the text delta series.
     * @param token          not used in this method
     * @throws SVNException 
     */
    public void textDeltaEnd(String token) throws SVNException {
	    myIsLastRevisionReported = false;
        myDeltaProcessor.textDeltaEnd();
        
        if (myIncludeMergedRevisions) {
            myMergeBlameChunks = addFileBlame(myPreviousFile, myCurrentFile, myMergeBlameChunks);
            if (!myIsCurrentResultOfMerge) {
                myBlameChunks = addFileBlame(myPreviousOriginalFile, myCurrentFile, myBlameChunks);
                if (myPreviousOriginalFile == null) {
                    myPreviousOriginalFile = myCurrentFile;
                    myCurrentFile = null;
                } else {
                    SVNFileUtil.rename(myCurrentFile, myPreviousOriginalFile);    
                }
                
                myPreviousFile = myPreviousOriginalFile;
            } else {
                if (myPreviousFile != null && myPreviousFile != myPreviousOriginalFile) {
                    SVNFileUtil.rename(myCurrentFile, myPreviousFile);    
                } else {
                    myPreviousFile = myCurrentFile;
                    myCurrentFile = null;
                }
            }
        } else {
            myBlameChunks = addFileBlame(myPreviousFile, myCurrentFile, myBlameChunks);
            if (myPreviousFile == null) {
                myPreviousFile = myCurrentFile;
                myCurrentFile = null;
            } else {
                SVNFileUtil.rename(myCurrentFile, myPreviousFile);
            }
        }

        if (myFileHandler != null) {
            boolean generate = myFileHandler.handleRevision(myCurrentDate, myCurrentDate != null ? myCurrentRevision : -1, myCurrentAuthor, myPreviousFile);
            if (generate) {
                myIsLastRevisionReported = true;
                reportAnnotations(myFileHandler, myEncoding);
            }
        }
    }
    
    public void addFileBlame(InputStream contents) throws SVNException {
        if (myCurrentFile == null) {
            myCurrentFile = SVNFileUtil.createUniqueFile(myTmpDirectory, "annotate", ".tmp", false);
        }
        OutputStream os = null;
        try {
            os = SVNFileUtil.openFileForWriting(myCurrentFile);
            SVNTranslator.copy(contents, os);
        } catch (IOException e) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, e);
            SVNErrorManager.error(err, SVNLogType.WC);
        } finally {
            SVNFileUtil.closeFile(os);
        }
        myBlameChunks = addFileBlame(myPreviousFile, myCurrentFile, myBlameChunks);
        if (myPreviousFile == null) {
            myPreviousFile = myCurrentFile;
            myCurrentFile = null;
        } else {
            SVNFileUtil.rename(myCurrentFile, myPreviousFile);
        }
    }
    
    /**
     * This method is used by <code>SVNKit</code> internals and is not intended for API users.
     * @return whether the last revision was reported or not yet
     * @since  1.2.0
     */
    public boolean isLastRevisionReported() {
        return myIsLastRevisionReported;
    }
       
    /**
     * Dispatches file lines along with author & revision info to the provided
     * annotation handler.  
     * 
     * <p>
     * If <code>inputEncoding</code> is <span class="javakeyword">null</span> then 
     * <span class="javastring">"file.encoding"</span> system property is used. 
     * 
     * @param  handler        an annotation handler that processes file lines with
     *                        author & revision info
     * @param  inputEncoding  a desired character set (encoding) of text lines
     * @throws SVNException
     */
    public void reportAnnotations(ISVNAnnotateHandler handler, String inputEncoding) throws SVNException {
        if (handler == null) {
            return;
        }

        SVNErrorManager.assertionFailure(myPreviousFile != null, null, SVNLogType.WC);
        int mergedCount = -1;
        if (myIncludeMergedRevisions) {
            if (myBlameChunks.isEmpty()) {
                BlameChunk chunk = new BlameChunk();
                chunk.blockStart = 0;
                chunk.author = myCurrentAuthor;
                chunk.date = myCurrentDate;
                chunk.revision = myCurrentRevision;
                chunk.path = myCurrentPath;
                myBlameChunks.add(chunk);
            }
            normalizeBlames(myBlameChunks, myMergeBlameChunks);
            mergedCount = 0;
        }
        
        inputEncoding = inputEncoding == null ? System.getProperty("file.encoding") : inputEncoding;
        CharsetDecoder decoder = Charset.forName(inputEncoding).newDecoder();

        InputStream stream = null;
        try {
            stream = new SVNTranslatorInputStream(SVNFileUtil.openFileForReading(myPreviousFile), 
                                                  SVNProperty.EOL_LF_BYTES, true, null, false);
            
            StringBuffer buffer = new StringBuffer();
            for (int i = 0; i < myBlameChunks.size(); i++) {
                BlameChunk chunk = (BlameChunk) myBlameChunks.get(i);
                String mergedAuthor = null;
                long mergedRevision = SVNRepository.INVALID_REVISION;
                Date mergedDate = null;
                String mergedPath = null;
                if (mergedCount >= 0) {
                    BlameChunk mergedChunk = (BlameChunk) myMergeBlameChunks.get(mergedCount++);
                    mergedAuthor = mergedChunk.author;
                    mergedRevision = mergedChunk.revision;
                    mergedDate = mergedChunk.date;
                    mergedPath = mergedChunk.path;
                }
                
                BlameChunk nextChunk = null;
                if (i < myBlameChunks.size() - 1) {
                    nextChunk = (BlameChunk) myBlameChunks.get(i + 1);
                }
                
                for (int lineNo = chunk.blockStart; nextChunk == null || lineNo < nextChunk.blockStart; lineNo++) {
                    myCancelBaton.checkCancelled();
                    buffer.setLength(0);
                    String line = SVNFileUtil.readLineFromStream(stream, buffer, decoder);
                    boolean isEOF = false;
                    if (line == null) {
                        isEOF = true;
                        if (buffer.length() > 0) {
                            line = buffer.toString();
                        }
                    }
                    
                    if (!isEOF || line != null) {
                        handler.handleLine(chunk.date, chunk.revision, chunk.author, 
                                           line, mergedDate, mergedRevision, mergedAuthor, 
                                           mergedPath, lineNo);
                    }                    
                    if (isEOF) {
                        break;
                    }
                }
            }
            handler.handleEOF();
        } catch (IOException ioe) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, ioe.getLocalizedMessage());
            SVNErrorManager.error(err, ioe, SVNLogType.DEFAULT);
        } finally {
            SVNFileUtil.closeFile(stream);
        }
    }

    /**
     * Finalizes an annotation operation releasing resources involved
     * by this generator. Should be called after {@link #reportAnnotations(ISVNAnnotateHandler, String) reportAnnotations()}. 
     *
     */
    public void dispose() {
        myIsCurrentResultOfMerge = false;
        if (myCurrentFile != null) {
            SVNFileUtil.deleteAll(myCurrentFile, true);
        }
        if (myPreviousFile != null) {
            SVNFileUtil.deleteAll(myPreviousFile, true);
            myPreviousFile = null;
        }
        if (myPreviousOriginalFile != null) {
            SVNFileUtil.deleteAll(myPreviousOriginalFile, true);
            myPreviousOriginalFile = null;
        }
        if (myIsTmpDirCreated) {
            SVNFileUtil.deleteAll(myTmpDirectory, true);
        }        
        myBlameChunks.clear();
        myMergeBlameChunks.clear();
    }

    private List addFileBlame(File previousFile, File currentFile, List chain) throws SVNException {
        if (previousFile == null) {
            BlameChunk chunk = new BlameChunk();
            chunk.author = myCurrentAuthor;
            chunk.revision = myCurrentDate != null ? myCurrentRevision : -1;
            chunk.date = myCurrentDate;
            chunk.blockStart = 0;
            chunk.path = myCurrentPath;
            chain.add(chunk);
            return chain;
        }
        
        RandomAccessFile left = null;
        RandomAccessFile right = null;
        try {
            left = new RandomAccessFile(previousFile, "r");
            right = new RandomAccessFile(currentFile, "r");

            final QSequenceLineResult result = QSequenceLineMedia.createBlocks(new QSequenceLineRAFileData(left), new QSequenceLineRAFileData(right), createSimplifier());
            try {
                List blocksList = result.getBlocks();
                for(int i = 0; i < blocksList.size(); i++) {
                    QSequenceDifferenceBlock block = (QSequenceDifferenceBlock) blocksList.get(i);
                    if (block.getLeftSize() > 0) {
                        deleteBlameChunk(block.getRightFrom(), block.getLeftSize(), chain);
                    }
                    if (block.getRightSize() > 0) {
                        insertBlameChunk(myCurrentRevision, myCurrentAuthor, 
                                         myCurrentDate, myCurrentPath, 
                                         block.getRightFrom(), block.getRightSize(), chain);
                    }
                }
            } finally {
                result.close();
            }
        } catch (Throwable e) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.UNKNOWN, "Exception while generating annotation: {0}", e.getMessage());
            SVNErrorManager.error(err, e, SVNLogType.DEFAULT);
        } finally {
            if (left != null) {
                SVNFileUtil.closeFile(left);
            }
            if (right != null) {
                SVNFileUtil.closeFile(right);
            }
        }

        return chain;
    }
    
    private void insertBlameChunk(long revision, String author, Date date, String path, int start, int length, List chain) {
        int[] index = new int[1];
        BlameChunk startPoint = findBlameChunk(chain, start, index);
        int adjustFromIndex = -1;
        if (startPoint.blockStart == start) {
            BlameChunk insert = new BlameChunk();
            insert.copy(startPoint);
            insert.blockStart = start + length;
            chain.add(index[0] + 1, insert);

            startPoint.author = author;
            startPoint.revision = revision;
            startPoint.date = date;
            startPoint.path = path;
            adjustFromIndex = index[0] + 2;
        } else {
            BlameChunk middle = new BlameChunk();
            middle.author = author;
            middle.revision = revision;
            middle.date = date;
            middle.path = path;
            middle.blockStart = start;
            
            BlameChunk insert = new BlameChunk();
            insert.copy(startPoint);
            insert.blockStart = start + length;
            chain.add(index[0] + 1, middle);
            chain.add(index[0] + 2, insert);
            adjustFromIndex = index[0] + 3;
        }
        
        adjustBlameChunks(chain, adjustFromIndex, length);
    }
    
    private void deleteBlameChunk(int start, int length, List chain) {
        int[] ind = new int[1];
        
        BlameChunk first = findBlameChunk(chain, start, ind);
        int firstInd = ind[0];
        
        BlameChunk last = findBlameChunk(chain, start + length, ind);
        int lastInd = ind[0];
        
        if (first != last) {
            int deleteCount = lastInd - firstInd - 1;
            for (int i = 0; i < deleteCount; i++) {
                chain.remove(firstInd + 1);
            }
            lastInd -= deleteCount;
            
            last.blockStart = start;
            if (first.blockStart == start) {
                first.copy(last);
                chain.remove(lastInd);
                lastInd--;
                last = first;
            }
        }

        int tailInd = lastInd < chain.size() - 1 ? lastInd + 1 : -1;
        BlameChunk tail = tailInd > 0 ? (BlameChunk)chain.get(tailInd) : null;

        if (tail != null && tail.blockStart == last.blockStart + length) {
            last.copy(tail);
            chain.remove(tail);
            tailInd--;
            tail = last;
        }
        
        if (tail != null) {
            adjustBlameChunks(chain, tailInd, -length);
        }
    }
    
    private void adjustBlameChunks(List chain, int startIndex, int adjust) {
        for (int i = startIndex; i < chain.size(); i++) {
            BlameChunk curChunk = (BlameChunk) chain.get(i);
            curChunk.blockStart += adjust;
        }
    }
    
    private BlameChunk findBlameChunk(List chain, int offset, int[] index) {
        BlameChunk prevChunk = null;
        index[0] = -1; 
        for (Iterator chunks = chain.iterator(); chunks.hasNext();) {
            BlameChunk chunk = (BlameChunk) chunks.next();
            if (chunk.blockStart > offset) {
                break;
            }
            prevChunk = chunk;
            index[0]++;
        }
        return prevChunk;
    }
    
    private void normalizeBlames(List chain, List mergedChain) throws SVNException {
        int i = 0, k = 0;
        for (; i < chain.size() - 1 && k < mergedChain.size() - 1; i++, k++) {
            BlameChunk chunk = (BlameChunk) chain.get(i);
            BlameChunk mergedChunk = (BlameChunk) mergedChain.get(k);
            SVNErrorManager.assertionFailure(chunk.blockStart == mergedChunk.blockStart, null, SVNLogType.WC);

            BlameChunk nextChunk = (BlameChunk) chain.get(i + 1);
            BlameChunk nextMergedChunk = (BlameChunk) mergedChain.get(k + 1);
            if (nextChunk.blockStart < nextMergedChunk.blockStart) {
                BlameChunk tmpChunk = new BlameChunk();
                tmpChunk.copy(mergedChunk);
                tmpChunk.blockStart = nextChunk.blockStart;
                mergedChain.add(k + 1, tmpChunk);
                nextMergedChunk = tmpChunk;
            }
            if (nextChunk.blockStart > nextMergedChunk.blockStart) {
                BlameChunk tmpChunk = new BlameChunk();
                tmpChunk.copy(chunk);
                tmpChunk.blockStart = nextMergedChunk.blockStart;
                chain.add(i + 1, tmpChunk);
            }
        }

        if ((i == chain.size() - 1) && (k == mergedChain.size() - 1)) {
            return;
        }
        
        if (k == mergedChain.size() - 1) {
            for (i += 1; i < chain.size(); i++) {
                BlameChunk chunk = (BlameChunk) chain.get(i);
                BlameChunk mergedChunk = (BlameChunk) mergedChain.get(mergedChain.size() - 1);

                BlameChunk insert = new BlameChunk();
                insert.copy(mergedChunk);
                insert.blockStart = chunk.blockStart;
                mergedChain.add(insert);
                k++;
            }
        }

        if (i == chain.size() - 1) {
            for (k += 1; k < mergedChain.size(); k++) {
                BlameChunk mergedChunk = (BlameChunk) mergedChain.get(k);
                BlameChunk chunk = (BlameChunk) chain.get(chain.size() - 1);

                BlameChunk insert = new BlameChunk();
                insert.copy(chunk);
                insert.blockStart = mergedChunk.blockStart;
                chain.add(insert);
                i++;
            }
        }
    }
    
    private QSequenceLineSimplifier createSimplifier() {
        if (mySimplifier == null) {
            QSequenceLineSimplifier first = myDiffOptions.isIgnoreEOLStyle() ? 
                    (QSequenceLineSimplifier) new QSequenceLineEOLUnifyingSimplifier() :
                    (QSequenceLineSimplifier) new QSequenceLineDummySimplifier();
            QSequenceLineSimplifier second = new QSequenceLineDummySimplifier();
            if (myDiffOptions.isIgnoreAllWhitespace()) {
                second = new QSequenceLineWhiteSpaceSkippingSimplifier();
            } else if (myDiffOptions.isIgnoreAmountOfWhitespace()) {
                second = new QSequenceLineWhiteSpaceReducingSimplifier();
            }
            mySimplifier = new QSequenceLineTeeSimplifier(first, second);
        }
        return mySimplifier;
    }

    private static class BlameChunk {
        public int blockStart;
        public long revision;
        public String author;
        public Date date;
        public String path;
        
        public void copy(BlameChunk chunk) {
            author = chunk.author;
            date = chunk.date;
            revision = chunk.revision;
            path = chunk.path;
            blockStart = chunk.blockStart;
        }
        
        public String toString() {
            StringBuffer buf = new StringBuffer();
            buf.append("\n----\nPath: " + path);
            buf.append("\nRevision: " + revision);
            buf.append("\nAuthor: " + author);
            buf.append("\nDate: " + SVNDate.formatConsoleShortDate(date));
            buf.append("\nBlock start: " + blockStart);
            buf.append("\n");
            return buf.toString();
        }
    }
    
}
