/*
 * ====================================================================
 * 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.cli.svn;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.text.MessageFormat;

import org.tmatesoft.svn.cli.SVNCommandUtil;
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.internal.wc.FSMergerBySequence;
import org.tmatesoft.svn.core.internal.wc.SVNDiffConflictChoiceStyle;
import org.tmatesoft.svn.core.internal.wc.SVNErrorManager;
import org.tmatesoft.svn.core.internal.wc.SVNFileUtil;
import org.tmatesoft.svn.core.internal.wc2.ng.ISvnPropertiesDiffHandler;
import org.tmatesoft.svn.core.wc.*;
import org.tmatesoft.svn.util.SVNLogType;

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 SVNCommandLineConflictHandler implements ISVNConflictHandler {
    private SVNConflictAcceptPolicy myAccept;
    private SVNCommandEnvironment mySVNEnvironment;
    private boolean myIsExternalFailed;
    
    public SVNCommandLineConflictHandler(SVNConflictAcceptPolicy accept, SVNCommandEnvironment environment) {
        myAccept = accept;
        mySVNEnvironment = environment;
    }

    public SVNConflictResult handleConflict(SVNConflictDescription conflictDescription) throws SVNException {
        SVNConflictResult conflictResult = handleConflictNoStats(conflictDescription);
        if (conflictResult != null && conflictResult.getConflictChoice() != SVNConflictChoice.POSTPONE) {
            SVNConflictStats conflictStats = getConflictStats();
            if (conflictDescription instanceof SVNTextConflictDescription) {
                conflictStats.incrementTextConflictsResolved(SVNFileUtil.getFilePath(conflictDescription.getPath()));
            } else if (conflictDescription instanceof SVNPropertyConflictDescription) {
                conflictStats.incrementPropConflictsResolved(SVNFileUtil.getFilePath(conflictDescription.getPath()));
            } else if (conflictDescription instanceof SVNTreeConflictDescription) {
                conflictStats.incrementTreeConflictsResolved(SVNFileUtil.getFilePath(conflictDescription.getPath()));
            } else {
                SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.ASSERTION_FAIL, "Invalid conflict kind on ''{0}''", conflictDescription.getPath());
                SVNErrorManager.error(errorMessage, SVNLogType.WC);
            }
        }
        return conflictResult;
    }

    public SVNConflictResult handleConflictNoStats(SVNConflictDescription conflictDescription) throws SVNException {
        if (conflictDescription.isTreeConflict()) {
            return null;
        }

        SVNMergeFileSet files = conflictDescription.getMergeFiles();
        if (myAccept == SVNConflictAcceptPolicy.POSTPONE) {
            return new SVNConflictResult(SVNConflictChoice.POSTPONE, null);
        } else if (myAccept == SVNConflictAcceptPolicy.BASE) {
            return new SVNConflictResult(SVNConflictChoice.BASE, null);
        } else if (myAccept == SVNConflictAcceptPolicy.WORKING) {
            return new SVNConflictResult(SVNConflictChoice.MERGED, null);
        } else if (myAccept == SVNConflictAcceptPolicy.MINE_CONFLICT) {
            return new SVNConflictResult(SVNConflictChoice.MINE_CONFLICT, null);
        } else if (myAccept == SVNConflictAcceptPolicy.THEIRS_CONFLICT) {
            return new SVNConflictResult(SVNConflictChoice.THEIRS_CONFLICT, null);
        } else if (myAccept == SVNConflictAcceptPolicy.MINE_FULL) {
            return new SVNConflictResult(SVNConflictChoice.MINE_FULL, null);
        } else if (myAccept == SVNConflictAcceptPolicy.THEIRS_FULL) {
            return new SVNConflictResult(SVNConflictChoice.THEIRS_FULL, null);
        } else if (myAccept == SVNConflictAcceptPolicy.EDIT) {
            if (files.getResultFile() != null) {
                if (myIsExternalFailed) {
                    return new SVNConflictResult(SVNConflictChoice.POSTPONE, null);
                }

                try {
                    SVNCommandUtil.editFileExternally(mySVNEnvironment, mySVNEnvironment.getEditorCommand(),
                            files.getResultFile().getAbsolutePath());
                } catch (SVNException svne) {
                    if (svne.getErrorMessage().getErrorCode() == SVNErrorCode.CL_NO_EXTERNAL_EDITOR) {
                        mySVNEnvironment.getErr().println(svne.getErrorMessage().getMessage() != null ?
                                svne.getErrorMessage().getMessage() : "No editor found, leaving all conflicts.");
                        myIsExternalFailed = true;
                    } else if (svne.getErrorMessage().getErrorCode() == SVNErrorCode.EXTERNAL_PROGRAM) {
                        String message = svne.getErrorMessage().getMessageTemplate() != null ? svne.getErrorMessage().getMessage() :
                            "Error running editor, leaving all conflicts.";
                        if (message.startsWith("svn: ")) {
                            //hack: use the original message template without any prefixes (like 'svn:', 'svn:warning')
                            //added to make update test 42 pass
                            message = message.substring("svn: ".length());
                        }
                        mySVNEnvironment.getErr().println(message);
                        myIsExternalFailed = true;
                    } else {
                        throw svne;
                    }
                }
                return new SVNConflictResult(SVNConflictChoice.MERGED, null);
            }
        } else if (myAccept == SVNConflictAcceptPolicy.LAUNCH) {
            if (files.getBaseFile() != null && files.getLocalFile() != null && files.getRepositoryFile() != null &&
                    files.getResultFile() != null) {
                if (myIsExternalFailed) {
                    return new SVNConflictResult(SVNConflictChoice.POSTPONE, null);
                }

                boolean[] remainsInConflict = { false };
                try {
                    SVNCommandUtil.mergeFileExternally(mySVNEnvironment, files.getBaseFile().getAbsolutePath(),
                            files.getRepositoryFile().getAbsolutePath(), files.getLocalFile().getAbsolutePath(),
                            files.getResultFile().getAbsolutePath(), files.getWCPath(), remainsInConflict);
                } catch (SVNException svne) {
                    if (svne.getErrorMessage().getErrorCode() == SVNErrorCode.CL_NO_EXTERNAL_MERGE_TOOL) {
                        mySVNEnvironment.getErr().println(svne.getErrorMessage().getMessage() != null ?
                                svne.getErrorMessage().getMessage() : "No merge tool found.");
                        myIsExternalFailed = true;
                    } else if (svne.getErrorMessage().getErrorCode() == SVNErrorCode.EXTERNAL_PROGRAM) {
                        mySVNEnvironment.getErr().println(svne.getErrorMessage().getMessage() != null ?
                                svne.getErrorMessage().getMessage() : "Error running merge tool.");
                        myIsExternalFailed = true;
                    }
                    throw svne;
                }

                if (remainsInConflict[0]) {
                    return new SVNConflictResult(SVNConflictChoice.POSTPONE, null);
                }

                return new SVNConflictResult(SVNConflictChoice.MERGED, null);
            }
        }

        boolean saveMerged = false;
        SVNConflictChoice choice = SVNConflictChoice.POSTPONE;
        if ((conflictDescription.getNodeKind() == SVNNodeKind.FILE &&
                conflictDescription.getConflictAction() == SVNConflictAction.EDIT &&
                conflictDescription.getConflictReason() == SVNConflictReason.EDITED) ||
                conflictDescription.isPropertyConflict()) {

            boolean performedEdit = false;
            boolean diffAllowed = false;
            boolean knowsSmth = false;
            String path = mySVNEnvironment.getRelativePath(files.getWCFile());
            path = SVNCommandUtil.getLocalPath(path);

            if (conflictDescription.isPropertyConflict()) {
                String message = "Conflict for property ''{0}'' discovered on ''{1}''.";
                message = MessageFormat.format(message, new Object[] { conflictDescription.getPropertyName(),
                        path });
                mySVNEnvironment.getErr().println(message);

                if ((files.getLocalFile() == null && files.getRepositoryFile() != null) ||
                        (files.getLocalFile() != null && files.getRepositoryFile() == null)) {
                    if (files.getLocalFile() != null) {
                        String myVal = SVNFileUtil.readFile(files.getLocalFile());
                        message = MessageFormat.format("They want to delete the property, you want to change the value to ''{0}''.",
                                new Object[] { myVal });
                        mySVNEnvironment.getErr().println(message);
                    } else {
                        String reposVal = SVNFileUtil.readFile(files.getRepositoryFile());
                        message = MessageFormat.format("They want to change the property value to ''{0}'', you want to delete the property.",
                                new Object[] { reposVal });
                        mySVNEnvironment.getErr().println(message);
                    }
                }
            } else {
                String message = "Conflict discovered in ''{0}''.";
                message = MessageFormat.format(message, new Object[] { path });
                mySVNEnvironment.getErr().println(message);
            }

            if ((files.getResultFile() != null && files.getBaseFile() != null) || (files.getBaseFile() != null &&
                    files.getLocalFile() != null && files.getRepositoryFile() != null)) {
                diffAllowed = true;
            }

            while (true) {
                String message = "Select: (p) postpone";
                if (diffAllowed) {
                    message += ", (df) diff-full, (e) edit";

                    if (knowsSmth) {
                        message += ", (r) resolved";
                    }

                    if (!files.isBinary() && !conflictDescription.isPropertyConflict()) {
                        message += ",\n        (mc) mine-conflict, (tc) theirs-conflict";
                    }
                } else {
                    if (knowsSmth) {
                        message += ", (r) resolved";
                    }
                    message += ",\n        (mf) mine-full, (tf) theirs-full";
                }

                message += ",\n        (s) show all options: ";

                String answer = SVNCommandUtil.prompt(message, mySVNEnvironment);

                if ("s".equals(answer)) {
                    mySVNEnvironment.getErr().println();
                    mySVNEnvironment.getErr().println("  (e)  edit             - change merged file in an editor");
                    mySVNEnvironment.getErr().println("  (df) diff-full        - show all changes made to merged file");
                    mySVNEnvironment.getErr().println("  (r)  resolved         - accept merged version of file");
                    mySVNEnvironment.getErr().println();
                    mySVNEnvironment.getErr().println("  (dc) display-conflict - show all conflicts (ignoring merged version)");
                    mySVNEnvironment.getErr().println("  (mc) mine-conflict    - accept my version for all conflicts (same)");
                    mySVNEnvironment.getErr().println("  (tc) theirs-conflict  - accept their version for all conflicts (same)");
                    mySVNEnvironment.getErr().println();
                    mySVNEnvironment.getErr().println("  (mf) mine-full        - accept my version of entire file (even non-conflicts)");
                    mySVNEnvironment.getErr().println("  (tf) theirs-full      - accept their version of entire file (same)");
                    mySVNEnvironment.getErr().println();
                    mySVNEnvironment.getErr().println("  (p)  postpone         - mark the conflict to be resolved later");
                    mySVNEnvironment.getErr().println("  (l)  launch           - launch external tool to resolve conflict");
                    mySVNEnvironment.getErr().println("  (s)  show all         - show this list");
                    mySVNEnvironment.getErr().println();
                } else  if ("p".equals(answer)) {
                    choice = SVNConflictChoice.POSTPONE;
                    break;
                } else if ("mc".equals(answer)) {
                    if (files.isBinary()) {
                        mySVNEnvironment.getErr().println("Invalid option; cannot choose based on conflicts in a binary file.");
                        mySVNEnvironment.getErr().println();
                        continue;
                    } else if (conflictDescription.isPropertyConflict()) {
                        mySVNEnvironment.getErr().println("Invalid option; cannot choose based on conflicts for properties.");
                        mySVNEnvironment.getErr().println();
                        continue;
                    }
                    choice = SVNConflictChoice.MINE_CONFLICT;
                    if (performedEdit) {
                        saveMerged = true;
                    }
                    break;
                } else if ("tc".equals(answer)) {
                    if (files.isBinary()) {
                        mySVNEnvironment.getErr().println("Invalid option; cannot choose based on conflicts in a binary file.");
                        mySVNEnvironment.getErr().println();
                        continue;
                    } else if (conflictDescription.isPropertyConflict()) {
                        mySVNEnvironment.getErr().println("Invalid option; cannot choose based on conflicts for properties.");
                        mySVNEnvironment.getErr().println();
                        continue;
                    }
                    choice = SVNConflictChoice.THEIRS_CONFLICT;
                    if (performedEdit) {
                        saveMerged = true;
                    }
                    break;
                } else if ("mf".equals(answer)) {
                    choice = SVNConflictChoice.MINE_FULL;
                    if (performedEdit) {
                        saveMerged = true;
                    }
                    break;
                } else if ("tf".equals(answer)) {
                    choice = SVNConflictChoice.THEIRS_FULL;
                    if (performedEdit) {
                        saveMerged = true;
                    }
                    break;
                } else if ("dc".equals(answer)) {
                    if (files.isBinary()) {
                        mySVNEnvironment.getErr().println("Invalid option; cannot display conflicts for a binary file.");
                        mySVNEnvironment.getErr().println();
                        continue;
                    } else if (conflictDescription.isPropertyConflict()) {
                        mySVNEnvironment.getErr().println("Invalid option; cannot display conflicts for properties.");
                        mySVNEnvironment.getErr().println();
                        continue;
                    } else if (files.getLocalFile() == null || files.getBaseFile() == null || files.getRepositoryFile() == null) {
                        mySVNEnvironment.getErr().println("Invalid option; original files not available.");
                        mySVNEnvironment.getErr().println();
                        continue;
                    }

                    //TODO: re-implement in future
                    showConflictedChunks(files);
                    knowsSmth = true;
                    continue;
                } else if ("df".equals(answer)) {
                    if (!diffAllowed) {
                        mySVNEnvironment.getErr().println("Invalid option; there's no merged version to diff.");
                        mySVNEnvironment.getErr().println();
                        continue;
                    }

                    File path1 = null;
                    File path2 = null;
                    if (files.getResultFile() != null && files.getBaseFile() != null) {
                        path1 = files.getBaseFile();
                        path2 = files.getResultFile();
                    } else {
                        path1 = files.getRepositoryFile();
                        path2 = files.getLocalFile();
                    }

                    DefaultSVNCommandLineDiffGenerator diffGenerator = new DefaultSVNCommandLineDiffGenerator(path1, path2);
                    diffGenerator.setDiffOptions(new SVNDiffOptions(false, false, true));
                    if (diffGenerator instanceof ISvnPropertiesDiffHandler) {
                        ((ISvnPropertiesDiffHandler) diffGenerator).handlePropertiesDiff(null, null);
                    }
                    diffGenerator.displayFileDiff("", path1, path2, null, null, null, null, System.out);
                    knowsSmth = true;
                } else if ("e".equals(answer)) {
                    if (files.getResultFile() != null) {
                        try {
                            String resultPath = files.getResultFile().getAbsolutePath();
                            SVNCommandUtil.editFileExternally(mySVNEnvironment, mySVNEnvironment.getEditorCommand(),
                                    resultPath);
                            performedEdit = true;
                        } catch (SVNException svne) {
                            if (svne.getErrorMessage().getErrorCode() == SVNErrorCode.CL_NO_EXTERNAL_EDITOR) {
                                mySVNEnvironment.getErr().println(svne.getErrorMessage().getMessage() != null ?
                                        svne.getErrorMessage().getMessage() : "No editor found.");
                            } else if (svne.getErrorMessage().getErrorCode() == SVNErrorCode.EXTERNAL_PROGRAM) {
                                mySVNEnvironment.getErr().println(svne.getErrorMessage().getMessage() != null ?
                                        svne.getErrorMessage().getMessage() : "Error running editor.");
                            } else {
                                throw svne;
                            }
                        }
                    } else {
                        mySVNEnvironment.getErr().println("Invalid option; there's no merged version to edit.");
                        mySVNEnvironment.getErr().println();
                    }
                    if (performedEdit) {
                        knowsSmth = true;
                    }
                } else if ("l".equals(answer)) {
                    if (files.getBaseFile() != null && files.getLocalFile() != null && files.getRepositoryFile() != null &&
                            files.getResultFile() != null) {
                        try {
                            SVNCommandUtil.mergeFileExternally(mySVNEnvironment, files.getBasePath(), files.getRepositoryPath(),
                                    files.getLocalPath(), files.getResultPath(), files.getWCPath(), null);
                            performedEdit = true;
                        } catch (SVNException svne) {
                            if (svne.getErrorMessage().getErrorCode() == SVNErrorCode.CL_NO_EXTERNAL_MERGE_TOOL) {
                                mySVNEnvironment.getErr().println(svne.getErrorMessage().getMessage() != null ?
                                        svne.getErrorMessage().getMessage() : "No merge tool found.");
                                myIsExternalFailed = true;
                            } else if (svne.getErrorMessage().getErrorCode() == SVNErrorCode.EXTERNAL_PROGRAM) {
                                mySVNEnvironment.getErr().println(svne.getErrorMessage().getMessage() != null ?
                                        svne.getErrorMessage().getMessage() : "Error running merge tool.");
                                myIsExternalFailed = true;
                            } else {
                                throw svne;
                            }
                        }
                    } else {
                        mySVNEnvironment.getErr().println("Invalid option.");
                        mySVNEnvironment.getErr().println();
                    }
                } else if ("r".equals(answer)) {
                    if (knowsSmth) {
                        choice = SVNConflictChoice.MERGED;
                        break;
                    }
                    mySVNEnvironment.getErr().println("Invalid option.");
                    mySVNEnvironment.getErr().println();
                }
            }
        } else if (conflictDescription.getConflictAction() == SVNConflictAction.ADD &&
                conflictDescription.getConflictReason() == SVNConflictReason.OBSTRUCTED) {
            String message = "Conflict discovered when trying to add ''{0}''.";
            message = MessageFormat.format(message, new Object[] { files.getWCFile() });
            mySVNEnvironment.getErr().println(message);
            mySVNEnvironment.getErr().println("An object of the same name already exists.");

            String prompt = "Select: (p) postpone, (mf) mine-full, (tf) theirs-full, (h) help:";
            while (true) {
                String answer = SVNCommandUtil.prompt(prompt, mySVNEnvironment);

                if ("h".equals(answer) || "?".equals(answer)) {
                    mySVNEnvironment.getErr().println("  (p)  postpone    - resolve the conflict later");
                    mySVNEnvironment.getErr().println("  (mf) mine-full   - accept pre-existing item (ignore upstream addition)");
                    mySVNEnvironment.getErr().println("  (tf) theirs-full - accept incoming item (overwrite pre-existing item)");
                    mySVNEnvironment.getErr().println("  (h)  help        - show this help");
                    mySVNEnvironment.getErr().println();
                }

                if ("p".equals(answer)) {
                    choice = SVNConflictChoice.POSTPONE;
                    break;
                }
                if ("mf".equals(answer)) {
                    choice = SVNConflictChoice.MINE_FULL;
                    break;
                }
                if ("tf".equals(answer)) {
                    choice = SVNConflictChoice.THEIRS_FULL;
                    break;
                }
            }
        } else {
            choice = SVNConflictChoice.POSTPONE;
        }

        return new SVNConflictResult(choice, null, saveMerged);
    }

    private void showConflictedChunks(SVNMergeFileSet files) throws SVNException {
        byte[] conflictStartMarker = "<<<<<<< MINE (select with 'mc')".getBytes();
        byte[] conflictSeparator = "=======".getBytes();
        byte[] conflictEndMarker = ">>>>>>> THEIRS (select with 'tc')".getBytes();
        byte[] conflictOriginalMarker = "||||||| ORIGINAL".getBytes();

        SVNDiffOptions options = new SVNDiffOptions(false, false, true);
        FSMergerBySequence merger = new FSMergerBySequence(conflictStartMarker, conflictSeparator, conflictEndMarker, conflictOriginalMarker);
        RandomAccessFile localIS = null;
        RandomAccessFile latestIS = null;
        RandomAccessFile baseIS = null;
        try {
            localIS = new RandomAccessFile(files.getWCFile(), "r");
            latestIS = new RandomAccessFile(files.getRepositoryFile(), "r");
            baseIS = new RandomAccessFile(files.getBaseFile(), "r");

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

    private SVNConflictStats getConflictStats() {
        return mySVNEnvironment.getConflictStats();
    }

}
