/*
 * ====================================================================
 * 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.internal.wc;

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.text.MessageFormat;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import org.tmatesoft.svn.core.SVNErrorCode;
import org.tmatesoft.svn.core.SVNErrorMessage;
import org.tmatesoft.svn.core.SVNException;
import org.tmatesoft.svn.core.SVNNodeKind;
import org.tmatesoft.svn.core.SVNProperties;
import org.tmatesoft.svn.core.SVNProperty;
import org.tmatesoft.svn.core.SVNPropertyValue;
import org.tmatesoft.svn.core.internal.util.SVNEncodingUtil;
import org.tmatesoft.svn.core.internal.util.SVNMergeInfoUtil;
import org.tmatesoft.svn.core.internal.wc.admin.SVNAdminArea;
import org.tmatesoft.svn.core.internal.wc.admin.SVNEntry;
import org.tmatesoft.svn.core.internal.wc.admin.SVNLog;
import org.tmatesoft.svn.core.internal.wc.admin.SVNTranslator;
import org.tmatesoft.svn.core.wc.ISVNConflictHandler;
import org.tmatesoft.svn.core.wc.ISVNMerger;
import org.tmatesoft.svn.core.wc.SVNConflictAction;
import org.tmatesoft.svn.core.wc.SVNConflictChoice;
import org.tmatesoft.svn.core.wc.SVNConflictDescription;
import org.tmatesoft.svn.core.wc.SVNConflictReason;
import org.tmatesoft.svn.core.wc.SVNConflictResult;
import org.tmatesoft.svn.core.wc.SVNDiffOptions;
import org.tmatesoft.svn.core.wc.SVNMergeFileSet;
import org.tmatesoft.svn.core.wc.SVNMergeResult;
import org.tmatesoft.svn.core.wc.SVNStatusType;
import org.tmatesoft.svn.core.wc.SVNTextConflictDescription;
import org.tmatesoft.svn.core.wc.SVNPropertyConflictDescription;
import org.tmatesoft.svn.util.SVNLogType;

import de.regnis.q.sequence.line.QSequenceLineRAByteData;
import de.regnis.q.sequence.line.QSequenceLineRAData;
import de.regnis.q.sequence.line.QSequenceLineRAFileData;


/**
 * @version 1.3
 * @author  TMate Software Ltd.
 * @since   1.2.0
 */
public class DefaultSVNMerger extends AbstractSVNMerger implements ISVNMerger {

	private static List STATUS_ORDERING = new LinkedList(); 
	static {
		STATUS_ORDERING.add(SVNStatusType.UNKNOWN);
		STATUS_ORDERING.add(SVNStatusType.UNCHANGED);
		STATUS_ORDERING.add(SVNStatusType.INAPPLICABLE);
		STATUS_ORDERING.add(SVNStatusType.CHANGED);
		STATUS_ORDERING.add(SVNStatusType.MERGED);
		STATUS_ORDERING.add(SVNStatusType.OBSTRUCTED); 
		STATUS_ORDERING.add(SVNStatusType.CONFLICTED);
	}
	
    private ISVNConflictHandler myConflictCallback;
    private SVNDiffConflictChoiceStyle myDiffConflictStyle;
    
    /**
     * 
     * @param start
     * @param sep
     * @param end
     * @param callback
     * @deprecated use {@link #DefaultSVNMerger(byte[], byte[], byte[], ISVNConflictHandler, SVNDiffConflictChoiceStyle)} instead
     */
    public DefaultSVNMerger(byte[] start, byte[] sep, byte[] end) {
        this(start, sep, end, null);
    }

    /**
     * 
     * @param start
     * @param sep
     * @param end
     * @param callback
     * @deprecated use {@link #DefaultSVNMerger(byte[], byte[], byte[], ISVNConflictHandler, SVNDiffConflictChoiceStyle)} instead
     */
    public DefaultSVNMerger(byte[] start, byte[] sep, byte[] end, ISVNConflictHandler callback) {
        this(start, sep, end, callback, SVNDiffConflictChoiceStyle.CHOOSE_MODIFIED_LATEST);
    }

    public DefaultSVNMerger(byte[] start, byte[] sep, byte[] end, ISVNConflictHandler callback, SVNDiffConflictChoiceStyle style) {
        super(start, sep, end);
        myConflictCallback = callback;
        myDiffConflictStyle = style;
    }

	public SVNMergeResult mergeProperties(String localPath, SVNProperties workingProperties, 
			SVNProperties baseProperties, SVNProperties serverBaseProps, SVNProperties propDiff, 
			SVNAdminArea adminArea, SVNLog log, boolean baseMerge, boolean dryRun) throws SVNException {
        propDiff = propDiff == null ? new SVNProperties() : propDiff;
        
        if (baseProperties == null) {
            baseProperties = adminArea.getBaseProperties(localPath).asMap();
        }
        
        if (workingProperties == null) {
            workingProperties = adminArea.getProperties(localPath).asMap();
        }
        
        if (serverBaseProps == null) {
            serverBaseProps = baseProperties != null ? new SVNProperties(baseProperties) : new SVNProperties();
        }
        
        boolean isDir = adminArea.getThisDirName().equals(localPath);
        
        List conflicts = new LinkedList();
        List conflict = new LinkedList();
        SVNStatusType status = SVNStatusType.UNCHANGED;

        for (Iterator propEntries = propDiff.nameSet().iterator(); propEntries.hasNext();) {
            String propName = (String) propEntries.next();
            SVNPropertyValue toValue = propDiff.getSVNPropertyValue(propName);
            SVNPropertyValue fromValue = serverBaseProps.getSVNPropertyValue(propName);
            SVNPropertyValue workingValue = workingProperties.getSVNPropertyValue(propName);
            SVNPropertyValue baseValue = baseProperties.getSVNPropertyValue(propName);
            boolean isNormal = SVNProperty.isRegularProperty(propName);
            if (baseMerge) {
                changeProperty(baseProperties, propName, toValue);
            }            

            if (isNormal) {
            	status = getPropMergeStatus(status, SVNStatusType.CHANGED);
            }
            
            SVNStatusType newStatus = null;
            if (fromValue == null) {
                newStatus = applySinglePropertyAdd(localPath, isDir, isNormal ? status : null, 
                		workingProperties, propName, baseValue, toValue, workingValue, adminArea, log, conflict, dryRun);
            } else if (toValue == null) {
            	newStatus = applySinglePropertyDelete(localPath, isDir, isNormal ? status : null, 
            			workingProperties, propName, baseValue, fromValue, workingValue, adminArea, log, conflict, dryRun);
            } else {
            	newStatus = applySinglePropertyChange(localPath, isDir, status, workingProperties, propName, 
            			baseValue, fromValue, toValue, workingValue, adminArea, log, conflict, dryRun);
            }
            if (isNormal) {
            	status = newStatus;
            }
            
            if (!conflict.isEmpty()) {
            	if (isNormal) {
            		status = getPropMergeStatus(status, SVNStatusType.CONFLICTED);
            	}
            	
            	Object conflictDescription = conflict.remove(0);
            	if (dryRun) {
            		continue;
            	}
            	conflicts.add(conflictDescription);
            }
        }
        
        if (dryRun) {
            return SVNMergeResult.createMergeResult(status, null);
        }
        
        log = log == null ? adminArea.getLog() : log;
        adminArea.installProperties(localPath, baseProperties, workingProperties, log, baseMerge, true);

        if (!conflicts.isEmpty()) {
            SVNEntry entry = adminArea.getVersionedEntry(localPath, false);
        	String prejTmpPath = adminArea.getThisDirName().equals(localPath) ? 
            		"tmp/dir_conflicts" : "tmp/props/" + localPath;
            File prejTmpFile = SVNFileUtil.createUniqueFile(adminArea.getAdminDirectory(),  prejTmpPath, ".prej", false);

            prejTmpPath = SVNFileUtil.getBasePath(prejTmpFile);
            String prejPath = entry.getPropRejectFile();

            if (prejPath == null) {
                prejPath = adminArea.getThisDirName().equals(localPath) ? "dir_conflicts" : localPath;
                File prejFile = SVNFileUtil.createUniqueFile(adminArea.getRoot(), prejPath, ".prej", false);
                prejPath = SVNFileUtil.getBasePath(prejFile);
            }
            File file = adminArea.getFile(prejTmpPath);

            OutputStream os = SVNFileUtil.openFileForWriting(file);
            try {
                for (Iterator lines = conflicts.iterator(); lines.hasNext();) {
                    String line = (String) lines.next();
                    os.write(SVNEncodingUtil.fuzzyEscape(line).getBytes("UTF-8"));
                }
                os.write(SVNEncodingUtil.fuzzyEscape("\n").getBytes("UTF-8"));
            } catch (IOException e) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, "Cannot write properties conflict file: {1}", e.getLocalizedMessage());
                SVNErrorManager.error(err, e, SVNLogType.WC);
            } finally {
                SVNFileUtil.closeFile(os);
            }

            SVNProperties command = new SVNProperties();
            command.put(SVNLog.NAME_ATTR, prejTmpPath);
            command.put(SVNLog.DEST_ATTR, prejPath);
            log.addCommand(SVNLog.APPEND, command, false);
            command.clear();

            command.put(SVNLog.NAME_ATTR, prejTmpPath);
            log.addCommand(SVNLog.DELETE, command, false);
            command.clear();

            command.put(SVNLog.NAME_ATTR, localPath);
            command.put(SVNProperty.shortPropertyName(SVNProperty.PROP_REJECT_FILE),
                        prejPath);
            log.addCommand(SVNLog.MODIFY_ENTRY, command, false);
        }
        return SVNMergeResult.createMergeResult(status, null);
	}

	public SVNDiffConflictChoiceStyle getDiffConflictStyle() {
        return myDiffConflictStyle;
    }
    
    public void setDiffConflictStyle(SVNDiffConflictChoiceStyle diffConflictStyle) {
        myDiffConflictStyle = diffConflictStyle;
    }

    protected SVNStatusType mergeBinary(File baseFile, File localFile, File repositoryFile, SVNDiffOptions options, File resultFile) throws SVNException {
        return SVNStatusType.CONFLICTED;
    }

    protected SVNStatusType mergeText(File baseFile, File localFile, File latestFile, SVNDiffOptions options, File resultFile) throws SVNException {
        FSMergerBySequence merger = new FSMergerBySequence(getConflictStartMarker(), getConflictSeparatorMarker(), getConflictEndMarker());
        int mergeResult = 0;
        RandomAccessFile localIS = null;
        RandomAccessFile latestIS = null;
        RandomAccessFile baseIS = null;
        OutputStream result = null;
        try {
            result = SVNFileUtil.openFileForWriting(resultFile);
            localIS = new RandomAccessFile(localFile, "r");
            latestIS = new RandomAccessFile(latestFile, "r");
            baseIS = new RandomAccessFile(baseFile, "r");

            QSequenceLineRAData baseData = new QSequenceLineRAFileData(baseIS);
            QSequenceLineRAData localData = new QSequenceLineRAFileData(localIS);
            QSequenceLineRAData latestData = new QSequenceLineRAFileData(latestIS);
            mergeResult = merger.merge(baseData, localData, latestData, options, result, getDiffConflictStyle());
        } catch (IOException e) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, e.getLocalizedMessage());
            SVNErrorManager.error(err, e, SVNLogType.WC);
        } finally {
            SVNFileUtil.closeFile(result);
            SVNFileUtil.closeFile(localIS);
            SVNFileUtil.closeFile(baseIS);
            SVNFileUtil.closeFile(latestIS);
        }

        SVNStatusType status = SVNStatusType.UNCHANGED;
        if (mergeResult == FSMergerBySequence.CONFLICTED) {
            status = SVNStatusType.CONFLICTED;
        } else if (mergeResult == FSMergerBySequence.MERGED) {
            status = SVNStatusType.MERGED;
        }
        return status;
    }

	protected SVNMergeResult processMergedFiles(SVNMergeFileSet files, SVNMergeResult mergeResult) throws SVNException {
	    DefaultSVNMergerAction mergeAction = getMergeAction(files, mergeResult);

	    if (mergeAction == DefaultSVNMergerAction.MARK_CONFLICTED) {
	        mergeResult = handleMarkConflicted(files);
	    } else if (mergeAction == DefaultSVNMergerAction.CHOOSE_BASE) {
	        mergeResult = handleChooseBase(files);
	    } else if (mergeAction == DefaultSVNMergerAction.CHOOSE_REPOSITORY) {
	        mergeResult = handleChooseRepository(files);
	    } else if (mergeAction == DefaultSVNMergerAction.CHOOSE_WORKING) {
	        mergeResult = handleChooseWorking(files);
	    } else if (mergeAction == DefaultSVNMergerAction.CHOOSE_MERGED_FILE) {
	        mergeResult = handleChooseMerged(files, mergeResult);
	    } else if (mergeAction == DefaultSVNMergerAction.MARK_RESOLVED) {
	        mergeResult = handleMarkResolved(files, mergeResult);
	    } else if (mergeAction == DefaultSVNMergerAction.CHOOSE_REPOSITORY_CONFLICTED) {
	        mergeResult = handleChooseConflicted(false, files);
	    } else if (mergeAction == DefaultSVNMergerAction.CHOOSE_WORKING_CONFLICTED) {
	        mergeResult = handleChooseConflicted(true, files);
	    }

	    postMergeCleanup(files);
	    return mergeResult;
	}

	protected DefaultSVNMergerAction getMergeAction(SVNMergeFileSet files, SVNMergeResult mergeResult) throws SVNException {
	    if (mergeResult.getMergeStatus() == SVNStatusType.CONFLICTED) {
	        if (myConflictCallback != null) {
                SVNConflictDescription descr = new SVNTextConflictDescription(files, SVNNodeKind.FILE, 
                        SVNConflictAction.EDIT, SVNConflictReason.EDITED);
                
                SVNConflictResult result = myConflictCallback.handleConflict(descr);
                if (result == null) {
                    SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_CONFLICT_RESOLVER_FAILURE,
                            "Conflict callback violated API: returned no results.");
                    SVNErrorManager.error(err, SVNLogType.DEFAULT);
                }
                
                if (result.isIsSaveMerged()) {
                    File mergedFile = result.getMergedFile() != null ? result.getMergedFile() : files.getResultFile();
                    File mergeTarget = files.getWCFile();
                    File editedMergedFile = SVNFileUtil.createUniqueFile(mergeTarget.getParentFile(), mergeTarget.getName(), ".edited", false);
                    SVNLog log = files.getLog();
                    SVNProperties command = new SVNProperties();
                    command.put(SVNLog.NAME_ATTR, SVNFileUtil.getBasePath(mergedFile));
                    command.put(SVNLog.DEST_ATTR, editedMergedFile.getName());
                    log.addCommand(SVNLog.COPY_AND_TRANSLATE, command, false);
                    command.clear();
                }
                
                SVNConflictChoice choice = result.getConflictChoice();
                if (choice == SVNConflictChoice.BASE) {
                    return DefaultSVNMergerAction.CHOOSE_BASE;                        
                } else if (choice == SVNConflictChoice.MERGED) {
                    return DefaultSVNMergerAction.CHOOSE_MERGED_FILE;                        
                } else if (choice == SVNConflictChoice.MINE_FULL) {
                    return DefaultSVNMergerAction.CHOOSE_WORKING;                        
                } else if (choice == SVNConflictChoice.THEIRS_FULL) {
                    return DefaultSVNMergerAction.CHOOSE_REPOSITORY;                        
                } else if (choice == SVNConflictChoice.MINE_CONFLICT) {
                    return DefaultSVNMergerAction.CHOOSE_WORKING_CONFLICTED;
                } else if (choice == SVNConflictChoice.THEIRS_CONFLICT) {
                    return DefaultSVNMergerAction.CHOOSE_REPOSITORY_CONFLICTED;
                }
	        }
	        return DefaultSVNMergerAction.MARK_CONFLICTED;
	    }
	    return DefaultSVNMergerAction.CHOOSE_MERGED_FILE;
    }
    
    protected SVNMergeResult handleChooseBase(SVNMergeFileSet files) throws SVNException {
        SVNLog log = files.getLog();
        if (log != null) {
            SVNProperties command = new SVNProperties();
            command.put(SVNLog.NAME_ATTR, files.getBasePath());
            command.put(SVNLog.DEST_ATTR, files.getWCPath());
            log.addCommand(SVNLog.COPY_AND_TRANSLATE, command, false);
            command.clear();
        }
        return SVNMergeResult.createMergeResult(SVNStatusType.MERGED, null);
    }
    
    protected SVNMergeResult handleChooseRepository(SVNMergeFileSet files) throws SVNException {
        SVNLog log = files.getLog();
        if (log != null) {
            SVNProperties command = new SVNProperties();
            command.put(SVNLog.NAME_ATTR, files.getRepositoryPath());
            command.put(SVNLog.DEST_ATTR, files.getWCPath());
            log.addCommand(SVNLog.COPY_AND_TRANSLATE, command, false);
            command.clear();
        }

        return SVNMergeResult.createMergeResult(SVNStatusType.MERGED, null);
    }

    protected SVNMergeResult handleChooseConflicted(boolean chooseMine, SVNMergeFileSet files) throws SVNException {
        File tmpFile = SVNAdminUtil.createTmpFile(files.getAdminArea());
        
        setDiffConflictStyle(chooseMine ? SVNDiffConflictChoiceStyle.CHOOSE_MODIFIED : SVNDiffConflictChoiceStyle.CHOOSE_LATEST);

        File leftFile = files.getBaseFile();
        File rightFile = files.getRepositoryFile();
        File detranslatedTarget = files.getLocalFile();
        

        mergeText(leftFile, detranslatedTarget, rightFile, getDiffOptions(), tmpFile);

        SVNLog log = files.getLog();
        if (log != null) {
            SVNProperties command = new SVNProperties();
            String tmpBasePath = SVNFileUtil.getBasePath(tmpFile);
            command.put(SVNLog.NAME_ATTR, tmpBasePath);
            command.put(SVNLog.DEST_ATTR, files.getWCPath());
            log.addCommand(SVNLog.COPY_AND_TRANSLATE, command, false);
            command.clear();

            command.put(SVNLog.NAME_ATTR, tmpBasePath);
            log.addCommand(SVNLog.DELETE, command, false);
            command.clear();
        }

        return SVNMergeResult.createMergeResult(SVNStatusType.MERGED, null);
    }

    protected SVNMergeResult handleChooseWorking(SVNMergeFileSet files) throws SVNException {
        if (files == null) {
            SVNErrorManager.cancel("", SVNLogType.WC);
        }
        return SVNMergeResult.createMergeResult(SVNStatusType.MERGED, null);        
    }

    protected SVNMergeResult handleMarkConflicted(SVNMergeFileSet files) throws SVNException {
        if (files.isBinary()) {
            return handleMarkBinaryConflicted(files);
        }
        return handleMarkTextConflicted(files);                

    }

    protected SVNMergeResult handleMarkBinaryConflicted(SVNMergeFileSet files) throws SVNException {
        SVNProperties command = new SVNProperties();
        File root = files.getAdminArea().getRoot();
        SVNLog log = files.getLog();

        File oldFile = SVNFileUtil.createUniqueFile(root, files.getWCPath(), files.getBaseLabel(), false);
        File newFile = SVNFileUtil.createUniqueFile(root, files.getWCPath(), files.getRepositoryLabel(), false);
        SVNFileUtil.copyFile(files.getBaseFile(), oldFile, false);
        SVNFileUtil.copyFile(files.getRepositoryFile(), newFile, false);
        
        
        if (!files.getLocalPath().equals(files.getWCPath())) {
            File mineFile = SVNFileUtil.createUniqueFile(root, files.getWCPath(), files.getLocalLabel(), false);
            String minePath = SVNFileUtil.getBasePath(mineFile);
            if (log != null) {
                command.put(SVNLog.NAME_ATTR, files.getLocalPath());
                command.put(SVNLog.DEST_ATTR, minePath);
                log.addCommand(SVNLog.MOVE, command, false);
                command.clear();
            }
            command.put(SVNProperty.shortPropertyName(SVNProperty.CONFLICT_WRK), minePath);
        } else {
            command.put(SVNProperty.shortPropertyName(SVNProperty.CONFLICT_WRK), "");
        }

        String newPath = SVNFileUtil.getBasePath(newFile);
        String oldPath = SVNFileUtil.getBasePath(oldFile);
        
        makeBinaryConflictEntry(files, newPath, oldPath);

        return SVNMergeResult.createMergeResult(SVNStatusType.CONFLICTED, null);
    }
    
    protected void makeBinaryConflictEntry(SVNMergeFileSet files, String newFilePath, String oldFilePath) throws SVNException {
        SVNProperties command = new SVNProperties();
        SVNLog log = files.getLog();
        if (log != null) {
            command.put(SVNProperty.shortPropertyName(SVNProperty.CONFLICT_NEW), newFilePath);
            command.put(SVNProperty.shortPropertyName(SVNProperty.CONFLICT_OLD), oldFilePath);
            log.logChangedEntryProperties(files.getWCPath(), command);
            command.clear();
        }
        files.getAdminArea().saveEntries(false);
    }

    protected SVNMergeResult handleMarkTextConflicted(SVNMergeFileSet files) throws SVNException {
        SVNProperties command = new SVNProperties();
        File root = files.getAdminArea().getRoot();
        SVNLog log = files.getLog();
        
        if (files.getCopyFromFile() != null) {
            String copyFromPath = files.getCopyFromPath();
            String detranslatedPath = files.getWCPath();
            if (log != null) {
                command.put(SVNLog.NAME_ATTR, copyFromPath);
                command.put(SVNLog.DEST_ATTR, detranslatedPath);
                log.addCommand(SVNLog.COPY_AND_TRANSLATE, command, false);
                command.clear();
            }
        }

        File mineFile = SVNFileUtil.createUniqueFile(root, files.getWCPath(), files.getLocalLabel(), false);
        File oldFile = SVNFileUtil.createUniqueFile(root, files.getWCPath(), files.getBaseLabel(), false);
        File newFile = SVNFileUtil.createUniqueFile(root, files.getWCPath(), files.getRepositoryLabel(), false);
        
        String newPath = SVNFileUtil.getBasePath(newFile);
        String oldPath = SVNFileUtil.getBasePath(oldFile);
        String minePath = SVNFileUtil.getBasePath(mineFile);
        
        String basePath = files.getBasePath();
        String latestPath = files.getRepositoryPath();
        File tmpTargetCopy = SVNTranslator.getTranslatedFile(files.getAdminArea(), files.getWCPath(), files.getWCFile(), 
                false, false, false, true);
        String tmpTargetCopyPath = SVNFileUtil.getBasePath(tmpTargetCopy);

        if (log != null) {
            command.put(SVNLog.NAME_ATTR, basePath);
            command.put(SVNLog.DEST_ATTR, oldPath);
            command.put(SVNLog.ATTR2, files.getWCPath());
            log.addCommand(SVNLog.COPY_AND_TRANSLATE, command, false);
            command.clear();

            command.put(SVNLog.NAME_ATTR, latestPath);
            command.put(SVNLog.DEST_ATTR, newPath);
            command.put(SVNLog.ATTR2, files.getWCPath());
            log.addCommand(SVNLog.COPY_AND_TRANSLATE, command, false);
            command.clear();

            command.put(SVNLog.NAME_ATTR, tmpTargetCopyPath);
            command.put(SVNLog.DEST_ATTR, minePath);
            command.put(SVNLog.ATTR2, files.getWCPath());
            log.addCommand(SVNLog.COPY_AND_TRANSLATE, command, false);
            command.clear();

            if (!tmpTargetCopy.equals(files.getLocalFile())) {
                command.put(SVNLog.NAME_ATTR, tmpTargetCopyPath);
                log.addCommand(SVNLog.DELETE, command, false);
                command.clear();
            }

            command.put(SVNLog.NAME_ATTR, files.getResultPath());
            command.put(SVNLog.DEST_ATTR, files.getWCPath());
            command.put(SVNLog.ATTR2, files.getWCPath());
            log.addCommand(SVNLog.COPY_AND_TRANSLATE, command, false);
            command.clear();
        }

        makeTextConflictEntry(files, minePath, newPath, oldPath);
        
        return SVNMergeResult.createMergeResult(SVNStatusType.CONFLICTED, null);
    }

    protected void makeTextConflictEntry(SVNMergeFileSet files, String mineFilePath, String newFilePath, String oldFilePath) throws SVNException {
        SVNLog log = files.getLog();
        if (log != null) {
            SVNProperties command = new SVNProperties();
            command.put(SVNProperty.shortPropertyName(SVNProperty.CONFLICT_WRK), mineFilePath);
            command.put(SVNProperty.shortPropertyName(SVNProperty.CONFLICT_NEW), newFilePath);
            command.put(SVNProperty.shortPropertyName(SVNProperty.CONFLICT_OLD), oldFilePath);
            log.logChangedEntryProperties(files.getWCPath(), command);
            command.clear();
        }
    }
    
    protected SVNMergeResult handleChooseMerged(SVNMergeFileSet files, SVNMergeResult mergeResult) throws SVNException {
        SVNProperties command = new SVNProperties();
        SVNLog log = files.getLog();
        if (mergeResult.getMergeStatus() != SVNStatusType.CONFLICTED) {
            // do normal merge.
            if (mergeResult.getMergeStatus() != SVNStatusType.UNCHANGED) {
                if (log != null) {
                    command.put(SVNLog.NAME_ATTR, files.getResultPath());
                    command.put(SVNLog.DEST_ATTR, files.getWCPath());
                    log.addCommand(SVNLog.COPY_AND_TRANSLATE, command, false);
                    command.clear();
                }
            }
            return mergeResult;
        } else if (files.isBinary()) {
            // this action is not applicable for binary conflited files.
            return handleMarkConflicted(files);
        } else {
            if (log != null) {
                // for text file we could use merged version in case of conflict.
                command.put(SVNLog.NAME_ATTR, files.getResultPath());
                command.put(SVNLog.DEST_ATTR, files.getWCPath());
                log.addCommand(SVNLog.COPY_AND_TRANSLATE, command, false);
                command.clear();
            }
            return SVNMergeResult.createMergeResult(SVNStatusType.MERGED, null);
        }
    }

    protected SVNMergeResult handleMarkResolved(SVNMergeFileSet files, SVNMergeResult mergeResult) throws SVNException {
        if (!files.isBinary()) {
            // same as choose merged.
            return handleChooseMerged(files, mergeResult);
        }
        // same as choose working.
        return handleChooseWorking(files);
    }

    protected void postMergeCleanup(SVNMergeFileSet files) throws SVNException {
        SVNProperties command = new SVNProperties();
        SVNLog log = files.getLog();

        if (!files.getLocalPath().equals(files.getWCPath())) {
            if (log != null) {
                command.put(SVNLog.NAME_ATTR, files.getLocalPath());
                log.addCommand(SVNLog.DELETE, command, false);
                command.clear();
            }
        }
        
        if (log != null) {
            command.put(SVNLog.NAME_ATTR, files.getWCPath());
            log.addCommand(SVNLog.MAYBE_EXECUTABLE, command, false);
            command.clear();

            command.put(SVNLog.NAME_ATTR, files.getWCPath());
            log.addCommand(SVNLog.MAYBE_READONLY, command, false);
            command.clear();

            command.put(SVNLog.NAME_ATTR, files.getResultPath());
            log.addCommand(SVNLog.DELETE, command, false);
            command.clear();
        }
    }

    private SVNStatusType applySinglePropertyAdd(String localPath, boolean isDir, SVNStatusType status, 
    		SVNProperties workingProps, String propName, SVNPropertyValue baseValue, 
    		SVNPropertyValue newValue, SVNPropertyValue workingValue, SVNAdminArea adminArea, 
    		SVNLog log,	Collection conflicts, boolean dryRun) throws SVNException {
        boolean gotConflict = false;
    	
        if (workingValue != null) {
            if (workingValue.equals(newValue)) {
                status = getPropMergeStatus(status, SVNStatusType.MERGED);
                changeProperty(workingProps, propName, newValue);
            } else {
                if (SVNProperty.MERGE_INFO.equals(propName)) {
                    newValue = SVNPropertyValue.create(
                            SVNMergeInfoUtil.combineMergeInfoProperties(workingValue.getString(),
                    				newValue.getString()));
                    changeProperty(workingProps, propName, newValue);
                    status = getPropMergeStatus(status, SVNStatusType.MERGED);
                } else {
                    gotConflict = maybeGeneratePropConflict(localPath, propName, workingProps, null, newValue, 
                    		baseValue, workingValue,  adminArea, log, isDir, dryRun);
                    if (gotConflict) {
                        conflicts.add(MessageFormat.format("Trying to add new property ''{0}'' with value ''{1}'',\n" +
                                "but property already exists with value ''{2}''.",
                                new Object[] { propName, SVNPropertyValue.getPropertyAsString(newValue),
                                SVNPropertyValue.getPropertyAsString(workingValue) }));
                    	
                    }
                }
            }
    	} else if (baseValue != null) {
    		gotConflict = maybeGeneratePropConflict(localPath, propName, workingProps, null, newValue, baseValue, 
    				null, adminArea, log, isDir, dryRun);
    		if (gotConflict) {
                conflicts.add(MessageFormat.format("Trying to create property ''{0}'' with value ''{1}'',\n" +
                        "but it has been locally deleted.",
                        new Object[] { propName, SVNPropertyValue.getPropertyAsString(newValue) }));
    		}
    	} else {
    		changeProperty(workingProps, propName, newValue);
    	}
        return status;
    }

    private void changeProperty(SVNProperties properties, String propName, SVNPropertyValue propValue) {
        if (propValue == null) {
            properties.remove(propName);
        } else {
            properties.put(propName, propValue);
        }
    }
    
    private SVNStatusType applySinglePropertyChange(String localPath, boolean isDir, SVNStatusType status, 
            SVNProperties workingProps, String propName, SVNPropertyValue baseValue, 
            SVNPropertyValue oldValue, SVNPropertyValue newValue, SVNPropertyValue workingValue, 
            SVNAdminArea adminArea, SVNLog log, Collection conflicts, boolean dryRun) throws SVNException {
        if (SVNProperty.MERGE_INFO.equals(propName)) {
            return applySingleMergeInfoPropertyChange(localPath, isDir, status, workingProps, propName, baseValue, oldValue, 
                    newValue, workingValue, adminArea, log, conflicts, dryRun);
        } 
        return applySingleGenericPropertyChange(localPath, isDir, status, workingProps, propName, baseValue, oldValue, newValue, 
                workingValue, adminArea, log, conflicts, dryRun);
    }

    private SVNStatusType applySingleMergeInfoPropertyChange(String localPath, boolean isDir, SVNStatusType status, 
            SVNProperties workingProps, String propName, SVNPropertyValue baseValue, 
            SVNPropertyValue oldValue, SVNPropertyValue newValue, SVNPropertyValue workingValue, 
            SVNAdminArea adminArea, SVNLog log, Collection conflicts, boolean dryRun) throws SVNException {
        boolean gotConflict = false;
        
        if ((workingValue != null && baseValue == null) || 
                (workingValue == null && baseValue != null) ||
                (workingValue != null && baseValue != null && !workingValue.equals(baseValue))) {
            if (workingValue != null) {
                if (workingValue.equals(newValue)) {
                    status = getPropMergeStatus(status, SVNStatusType.MERGED);
                } else {
                    newValue = SVNPropertyValue.create(SVNMergeInfoUtil.combineForkedMergeInfoProperties(oldValue.getString(),
                            workingValue.getString(), newValue.getString()));
                    changeProperty(workingProps, propName, newValue);
                    status = getPropMergeStatus(status, SVNStatusType.MERGED);
                }
            } else {
                gotConflict = maybeGeneratePropConflict(localPath, propName, workingProps, oldValue, newValue, 
                        baseValue, workingValue, adminArea, log, isDir, dryRun);
                if (gotConflict) {
                    conflicts.add(MessageFormat.format("Trying to change property ''{0}'' from ''{1}'' to ''{2}'',\n" +
                            "but it has been locally deleted.", new Object[] { propName, SVNPropertyValue.getPropertyAsString(oldValue), 
                            SVNPropertyValue.getPropertyAsString(newValue) }));
                }
            }
        } else if (workingValue == null) {
            Map addedMergeInfo = new TreeMap();
            SVNMergeInfoUtil.diffMergeInfoProperties(null, addedMergeInfo, oldValue.getString(), null, newValue.getString(), null);
            newValue = SVNPropertyValue.create(SVNMergeInfoUtil.formatMergeInfoToString(addedMergeInfo, null));
            changeProperty(workingProps, propName, newValue);
        } else {
            if (baseValue.equals(oldValue)) {
                changeProperty(workingProps, propName, newValue);
            } else {
                newValue = SVNPropertyValue.create(SVNMergeInfoUtil.combineForkedMergeInfoProperties(oldValue.getString(),
                        workingValue.getString(), newValue.getString()));
               changeProperty(workingProps, propName, newValue);
               status = getPropMergeStatus(status, SVNStatusType.MERGED);
            }
        }
        return status;
    }

    private SVNStatusType applySingleGenericPropertyChange(String localPath, boolean isDir, SVNStatusType status, 
    		SVNProperties workingProps, String propName, SVNPropertyValue baseValue, 
    		SVNPropertyValue oldValue, SVNPropertyValue newValue, SVNPropertyValue workingValue, 
    		SVNAdminArea adminArea, SVNLog log, Collection conflicts, boolean dryRun) throws SVNException {
    	boolean gotConflict = false;
    	if ((workingValue == null && oldValue == null) || (workingValue != null && oldValue != null && 
    	        workingValue.equals(oldValue))) {
            changeProperty(workingProps, propName, newValue);
    	} else {
            gotConflict = maybeGeneratePropConflict(localPath, propName, workingProps, oldValue, 
                    newValue, baseValue, workingValue, adminArea, log, isDir, dryRun);
            if (gotConflict) {
                if (workingValue != null && baseValue != null && workingValue.equals(baseValue)) {
                    conflicts.add(MessageFormat.format("Trying to change property ''{0}'' from ''{1}'' to ''{2}'',\n" +
                            "but property already exists with value ''{3}''.",
                            new Object[] { propName, SVNPropertyValue.getPropertyAsString(oldValue),
                            SVNPropertyValue.getPropertyAsString(newValue),
                            SVNPropertyValue.getPropertyAsString(workingValue) }));
                } else if (workingValue != null && baseValue != null) {
                    conflicts.add(MessageFormat.format("Trying to change property ''{0}'' from ''{1}'' to ''{2}'',\n" +
                            "but the property has been locally changed from ''{3}'' to ''{4}''.", new Object[] { propName, 
                            SVNPropertyValue.getPropertyAsString(oldValue), SVNPropertyValue.getPropertyAsString(newValue), 
                            SVNPropertyValue.getPropertyAsString(baseValue), SVNPropertyValue.getPropertyAsString(workingValue) }));
                } else if (workingValue != null) {
                    conflicts.add(MessageFormat.format("Trying to change property ''{0}'' from ''{1}'' to ''{2}'',\n" +
                            "but property has been locally added with value ''{3}''.", new Object[] { propName, 
                            SVNPropertyValue.getPropertyAsString(oldValue), SVNPropertyValue.getPropertyAsString(newValue), 
                            SVNPropertyValue.getPropertyAsString(workingValue) }));
                } else if (baseValue != null) {
                    conflicts.add(MessageFormat.format("Trying to change property ''{0}'' from ''{1}'' to ''{2}'',\n" +
                            "but it has been locally deleted.", new Object[] { propName, 
                            SVNPropertyValue.getPropertyAsString(oldValue), SVNPropertyValue.getPropertyAsString(newValue) }));
                } else {
                    conflicts.add(MessageFormat.format("Trying to change property ''{0}'' from ''{1}'' to ''{2}'',\n" +
                            "but the property does not exist.", new Object[] { propName, SVNPropertyValue.getPropertyAsString(oldValue),
                            SVNPropertyValue.getPropertyAsString(newValue) }));
                }
            }

    	}
    	return status;
    }
    
    private SVNStatusType applySinglePropertyDelete(String localPath, boolean isDir, SVNStatusType status, 
    		SVNProperties workingProps, String propName, SVNPropertyValue baseValue, 
    		SVNPropertyValue oldValue, SVNPropertyValue workingValue, SVNAdminArea adminArea, 
    		SVNLog log,	Collection conflicts, boolean dryRun) throws SVNException {
    	boolean gotConflict = false;

    	if (baseValue == null) {
            changeProperty(workingProps, propName, (SVNPropertyValue) null); 
    		if (oldValue != null) {
    			status = getPropMergeStatus(status, SVNStatusType.MERGED);
    		}
    	} else if (baseValue.equals(oldValue)) {
    		if (workingValue != null) {
    			if (workingValue.equals(oldValue)) {
    				changeProperty(workingProps, propName, (SVNPropertyValue) null);
    			} else {
    				gotConflict = maybeGeneratePropConflict(localPath, propName, workingProps, oldValue, null, 
    						baseValue, workingValue, adminArea, log, isDir, dryRun);
    				if (gotConflict) {
                        conflicts.add(MessageFormat.format("Trying to delete property ''{0}'' with value ''{1}''\n " +
                        		"but it has been modified from ''{2}'' to ''{3}''.",
                        		new Object[] { propName, SVNPropertyValue.getPropertyAsString(oldValue),
                        		SVNPropertyValue.getPropertyAsString(baseValue),
                        		SVNPropertyValue.getPropertyAsString(workingValue) }));
    				}
    			}
    		} else {
    			status = getPropMergeStatus(status, SVNStatusType.MERGED);
    		}
    	} else {
    		gotConflict = maybeGeneratePropConflict(localPath, propName, workingProps, oldValue, null, 
    				baseValue, workingValue, adminArea, log, isDir, dryRun);
    		if (gotConflict) {
                conflicts.add(MessageFormat.format("Trying to delete property ''{0}'' with value ''{1}''\n " +
                        "but the local value is ''{2}''.",
                        new Object[] { propName, SVNPropertyValue.getPropertyAsString(baseValue),
                                SVNPropertyValue.getPropertyAsString(workingValue) }));
    		}
    	}
    	return status;
    }
    
    private static SVNStatusType getPropMergeStatus(SVNStatusType status, SVNStatusType newStatus) {
    	if (status == null) {
    		return null;
    	}
    	
    	int statusInd = STATUS_ORDERING.indexOf(status);
    	int newStatusInd = STATUS_ORDERING.indexOf(newStatus);
    	if (newStatusInd <= statusInd) {
    		return status;
    	}
    	return newStatus;
    	
    }

    private boolean maybeGeneratePropConflict(String localPath, String propName, SVNProperties workingProps, 
            SVNPropertyValue oldValue, SVNPropertyValue newValue, SVNPropertyValue baseValue, 
            SVNPropertyValue workingValue, SVNAdminArea adminArea, SVNLog log, boolean isDir, boolean dryRun) throws SVNException {
        boolean conflictRemains = true;
        if (myConflictCallback == null || dryRun) {
            return conflictRemains;
        }

        File path = adminArea.getFile(localPath);
        File workingFile = null;
        File newFile = null;
        File baseFile = null;
        File mergedFile = null;
        try {
            if (workingValue != null) {
                workingFile = SVNFileUtil.createUniqueFile(path.getParentFile(), path.getName(), ".tmp", false);
                OutputStream os = SVNFileUtil.openFileForWriting(workingFile);
                try {
                    os.write(SVNPropertyValue.getPropertyAsBytes(workingValue));
                } catch (IOException e) {
                    SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR,
                            "Cannot write a working property value file: {1}", e.getLocalizedMessage());
                    SVNErrorManager.error(err, e, SVNLogType.WC);
                } finally {
                    SVNFileUtil.closeFile(os);
                }
            }

            if (newValue != null) {
                newFile = SVNFileUtil.createUniqueFile(path.getParentFile(), path.getName(), ".tmp", false);
                OutputStream os = SVNFileUtil.openFileForWriting(newFile);
                try {
                    os.write(SVNPropertyValue.getPropertyAsBytes(newValue));
                } catch (IOException e) {
                    SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR,
                            "Cannot write a new property value file: {1}", e.getLocalizedMessage());
                    SVNErrorManager.error(err, e, SVNLogType.WC);
                } finally {
                    SVNFileUtil.closeFile(os);
                }
            }

            if ((baseValue != null && oldValue == null) ||
                    (baseValue == null && oldValue != null)) {
                SVNPropertyValue theValue = baseValue != null ? baseValue : oldValue;
                baseFile = SVNFileUtil.createUniqueFile(path.getParentFile(), path.getName(), ".tmp", false);
                OutputStream os = SVNFileUtil.openFileForWriting(baseFile);
                try {
                    os.write(SVNPropertyValue.getPropertyAsBytes(theValue));
                } catch (IOException e) {
                    SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR,
                            "Cannot write a base property value file: {1}", e.getLocalizedMessage());
                    SVNErrorManager.error(err, e, SVNLogType.WC);
                } finally {
                    SVNFileUtil.closeFile(os);
                }
            } else if (baseValue != null && oldValue != null) {
                SVNPropertyValue theValue = baseValue;
                if (!baseValue.equals(oldValue)) {
                    if (workingValue != null && baseValue.equals(workingValue)) {
                        theValue = oldValue;
                    }
                }
                baseFile = SVNFileUtil.createUniqueFile(path.getParentFile(), path.getName(), ".tmp", false);
                OutputStream os = SVNFileUtil.openFileForWriting(baseFile);
                try {
                    os.write(SVNPropertyValue.getPropertyAsBytes(theValue));
                } catch (IOException e) {
                    SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR,
                            "Cannot write a base property value file: {1}", e.getLocalizedMessage());
                    SVNErrorManager.error(err, e, SVNLogType.WC);
                } finally {
                    SVNFileUtil.closeFile(os);
                }

                if (workingValue != null && newValue != null) {
                    FSMergerBySequence merger = new FSMergerBySequence(getConflictStartMarker(),
                            getConflictSeparatorMarker(), getConflictEndMarker());
                    OutputStream result = null;
                    try {
                        mergedFile = SVNFileUtil.createUniqueFile(path.getParentFile(), path.getName(), ".tmp", false);
                        result = SVNFileUtil.openFileForWriting(mergedFile);

                        QSequenceLineRAData baseData = new QSequenceLineRAByteData(SVNPropertyValue.getPropertyAsBytes(theValue));
                        QSequenceLineRAData localData = new QSequenceLineRAByteData(SVNPropertyValue.getPropertyAsBytes(workingValue));
                        QSequenceLineRAData latestData = new QSequenceLineRAByteData(SVNPropertyValue.getPropertyAsBytes(newValue));
                        merger.merge(baseData, localData, latestData, null, result, null);
                    } catch (IOException e) {
                        SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, e.getLocalizedMessage());
                        SVNErrorManager.error(err, e, SVNLogType.WC);
                    } finally {
                        SVNFileUtil.closeFile(result);
                    }
                }
            }

            String mimeType = null;
            if (!isDir && workingProps != null) {
                mimeType = workingProps.getStringValue(SVNProperty.MIME_TYPE);
            }
            SVNMergeFileSet fileSet = new SVNMergeFileSet(adminArea, log, baseFile, workingFile, localPath, 
                    newFile, mergedFile, null, mimeType);

            SVNConflictAction action = SVNConflictAction.EDIT;
            if (oldValue == null && newValue != null) {
                action = SVNConflictAction.ADD;
            } else if (oldValue != null && newValue == null) {
                action = SVNConflictAction.DELETE;
            }

            SVNConflictReason reason = SVNConflictReason.EDITED;
            if (baseValue != null && workingValue == null) {
                reason = SVNConflictReason.DELETED;
            } else if (baseValue == null && workingValue != null) {
                reason = SVNConflictReason.OBSTRUCTED;
            }
            SVNConflictDescription description = new SVNPropertyConflictDescription(fileSet,
                    isDir ? SVNNodeKind.DIR : SVNNodeKind.FILE, propName, action, reason);
            SVNConflictResult result = myConflictCallback.handleConflict(description);
            if (result == null) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_CONFLICT_RESOLVER_FAILURE,
                        "Conflict callback violated API: returned no results.");
                SVNErrorManager.error(err, SVNLogType.DEFAULT);
            }
            SVNConflictChoice choice = result.getConflictChoice();
            if (choice == SVNConflictChoice.MINE_FULL) {
                conflictRemains = false;
            } else if (choice == SVNConflictChoice.THEIRS_FULL) {
                changeProperty(workingProps, propName, newValue);
                conflictRemains = false;
            } else if (choice == SVNConflictChoice.BASE) {
                changeProperty(workingProps, propName, baseValue);
                conflictRemains = false;
            } else if (choice == SVNConflictChoice.MERGED) {
                if (mergedFile == null && result.getMergedFile() == null) {
                    SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_CONFLICT_RESOLVER_FAILURE,
                            "Conflict callback violated API: returned no merged file.");
                    SVNErrorManager.error(err, SVNLogType.DEFAULT);
                }

                String mergedString = SVNFileUtil.readFile(mergedFile != null ? mergedFile : result.getMergedFile());
                changeProperty(workingProps, propName, SVNPropertyValue.create(mergedString));
                conflictRemains = false;
            }
            return conflictRemains;
        } finally {
            SVNFileUtil.deleteFile(workingFile);
            SVNFileUtil.deleteFile(newFile);
            SVNFileUtil.deleteFile(baseFile);
            SVNFileUtil.deleteFile(mergedFile);
        }
    }
}
