/*
 * ====================================================================
 * Copyright (c) 2004-2010 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.patch;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;

import org.tmatesoft.svn.core.SVNDepth;
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.SVNProperty;
import org.tmatesoft.svn.core.internal.wc.SVNErrorManager;
import org.tmatesoft.svn.core.internal.wc.SVNEventFactory;
import org.tmatesoft.svn.core.internal.wc.SVNFileType;
import org.tmatesoft.svn.core.internal.wc.SVNFileUtil;
import org.tmatesoft.svn.core.internal.wc.SVNStatusUtil;
import org.tmatesoft.svn.core.internal.wc.SVNWCManager;
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.SVNTranslator;
import org.tmatesoft.svn.core.internal.wc.admin.SVNVersionedProperties;
import org.tmatesoft.svn.core.io.SVNRepository;
import org.tmatesoft.svn.core.wc.ISVNEventHandler;
import org.tmatesoft.svn.core.wc.SVNEvent;
import org.tmatesoft.svn.core.wc.SVNEventAction;
import org.tmatesoft.svn.core.wc.SVNStatus;
import org.tmatesoft.svn.core.wc.SVNStatusType;
import org.tmatesoft.svn.util.SVNLogType;

/**
 * @version 1.3
 * @author TMate Software Ltd.
 */
public class SVNPatchTarget {

    private static final int MAX_FUZZ = 2;

    private SVNPatch patch;
    private List lines = new ArrayList();
    private List hunks = new ArrayList();;

    private boolean localMods;
    private boolean executable;
    private boolean skipped;
    private String eolStr;
    private Map keywords;
    private String eolStyle;
    private SVNNodeKind kind;
    private int currentLine;
    private boolean modified;
    private boolean hadRejects;
    private boolean deleted;
    private boolean eof;
    private boolean added;

    private File absPath;
    private File relPath;
    private File canonPathFromPatchfile;

    private RandomAccessFile file;
    private SVNPatchFileStream stream;

    private File patchedPath;
    private OutputStream patchedRaw;
    private OutputStream patched;
    private File rejectPath;
    private SVNPatchFileStream reject;
    private boolean parentDirExists;

    private SVNPatchTarget() {
    }

    public boolean isLocalMods() {
        return localMods;
    }

    public String getEolStr() {
        return eolStr;
    }

    public Map getKeywords() {
        return keywords;
    }

    public String getEolStyle() {
        return eolStyle;
    }

    public RandomAccessFile getFile() {
        return file;
    }

    public OutputStream getPatchedRaw() {
        return patchedRaw;
    }

    public File getCanonPathFromPatchfile() {
        return canonPathFromPatchfile;
    }

    public SVNPatch getPatch() {
        return patch;
    }

    public int getCurrentLine() {
        return currentLine;
    }

    public boolean isModified() {
        return modified;
    }

    public boolean isEof() {
        return eof;
    }

    public List getLines() {
        return lines;
    }

    public boolean isSkipped() {
        return skipped;
    }

    public List getHunks() {
        return hunks;
    }

    public SVNNodeKind getKind() {
        return kind;
    }

    public SVNPatchFileStream getStream() {
        return stream;
    }

    public OutputStream getPatched() {
        return patched;
    }

    public SVNPatchFileStream getReject() {
        return reject;
    }

    public File getPatchedPath() {
        return patchedPath;
    }

    public boolean isAdded() {
        return added;
    }

    public boolean isDeleted() {
        return deleted;
    }

    public boolean isExecutable() {
        return executable;
    }

    public File getRejectPath() {
        return rejectPath;
    }

    public File getAbsPath() {
        return absPath;
    }

    public File getRelPath() {
        return relPath;
    }

    public boolean isHadRejects() {
        return hadRejects;
    }

    public boolean isParentDirExists() {
        return parentDirExists;
    }

    /**
     * Attempt to initialize a patch TARGET structure for a target file
     * described by PATCH. Use client context CTX to send notifiations and
     * retrieve WC_CTX. STRIP_COUNT specifies the number of leading path
     * components which should be stripped from target paths in the patch. Upon
     * success, return the patch target structure. Else, return NULL.
     * 
     * @throws SVNException
     * @throws IOException
     */
    public static SVNPatchTarget initPatchTarget(SVNPatch patch, File baseDir, int stripCount, SVNAdminArea wc) throws SVNException, IOException {

        final SVNPatchTarget new_target = new SVNPatchTarget();
        new_target.resolveTargetPath(patch.getNewFilename(), baseDir, stripCount, wc);

        new_target.localMods = false;
        new_target.executable = false;

        if (!new_target.skipped) {

            final String nativeEOLMarker = SVNFileUtil.getNativeEOLMarker(wc.getWCAccess().getOptions());
            new_target.eolStr = nativeEOLMarker;
            new_target.keywords = null;
            new_target.eolStyle = null;

            if (new_target.kind == SVNNodeKind.FILE) {

                /* Open the file. */
                new_target.file = SVNFileUtil.openRAFileForReading(new_target.absPath);
                /* Create a stream to read from the target. */
                new_target.stream = SVNPatchFileStream.openReadOnly(new_target.absPath);

                /* Handle svn:keyword and svn:eol-style properties. */
                SVNVersionedProperties props = wc.getProperties(new_target.absPath.getAbsolutePath());
                String keywords_val = props.getStringPropertyValue(SVNProperty.KEYWORDS);
                if (null != keywords_val) {
                    SVNEntry entry = wc.getEntry(new_target.absPath.getAbsolutePath(), false);
                    long changed_rev = entry.getRevision();
                    String author = entry.getAuthor();
                    String changed_date = entry.getCommittedDate();
                    String url = entry.getURL();
                    String repositoryRoot = entry.getRepositoryRoot();
                    String rev_str = Long.toString(changed_rev);
                    new_target.keywords = SVNTranslator.computeKeywords(keywords_val, url, repositoryRoot, author, changed_date, rev_str, wc.getWCAccess().getOptions());
                }

                String eol_style_val = props.getStringPropertyValue(SVNProperty.EOL_STYLE);
                if (null != eol_style_val) {
                    new_target.eolStyle = new String(SVNTranslator.getEOL(eol_style_val, wc.getWCAccess().getOptions()));
                } else {
                    /* Just use the first EOL sequence we can find in the file. */
                    new_target.eolStr = detectFileEOL(new_target.file);
                    /* But don't enforce any particular EOL-style. */
                    new_target.eolStyle = null;
                }

                if (new_target.eolStyle == null) {
                    /*
                     * We couldn't figure out the target files's EOL scheme,
                     * just use native EOL makers.
                     */
                    new_target.eolStr = nativeEOLMarker;
                    new_target.eolStyle = SVNProperty.EOL_STYLE_NATIVE;
                }

                /* Also check the file for local mods and the Xbit. */
                new_target.localMods = wc.hasTextModifications(new_target.absPath.getAbsolutePath(), false);
                new_target.executable = SVNFileUtil.isExecutable(new_target.absPath);

            }

            /*
             * Create a temporary file to write the patched result to. Expand
             * keywords in the patched file.
             */
            new_target.patchedPath = SVNFileUtil.createTempFile("", null);
            new_target.patchedRaw = SVNFileUtil.openFileForWriting(new_target.patchedPath);
            new_target.patched = SVNTranslator.getTranslatingOutputStream(new_target.patchedRaw, null, new_target.eolStr.getBytes(), new_target.eolStyle != null, new_target.keywords, true);

            /*
             * We'll also need a stream to write rejected hunks to. We don't
             * expand keywords, nor normalise line-endings, in reject files.
             */
            new_target.rejectPath = SVNFileUtil.createTempFile("", null);
            new_target.reject = SVNPatchFileStream.openForWrite(new_target.rejectPath);

            /* The reject stream needs a diff header. */
            String diff_header = "--- " + new_target.canonPathFromPatchfile + nativeEOLMarker + "+++ " + new_target.canonPathFromPatchfile + nativeEOLMarker;

            new_target.reject.write(diff_header);

        }

        new_target.patch = patch;
        new_target.currentLine = 1;
        new_target.modified = false;
        new_target.hadRejects = false;
        new_target.deleted = false;
        new_target.eof = false;
        new_target.lines = new ArrayList();
        new_target.hunks = new ArrayList();

        return new_target;

    }

    /**
     * Detect the EOL marker used in file and return it. If it cannot be
     * detected, return NULL.
     * 
     * The file is searched starting at the current file cursor position. The
     * first EOL marker found will be returnd. So if the file has inconsistent
     * EOL markers, this won't be detected.
     * 
     * Upon return, the original file cursor position is always preserved, even
     * if an error is thrown.
     */
    private static String detectFileEOL(RandomAccessFile file) throws IOException {
        /* Remember original file offset. */
        final long pos = file.getFilePointer();
        try {
            final BufferedInputStream stream = new BufferedInputStream(new FileInputStream(file.getFD()));
            final StringBuffer buf = new StringBuffer();
            int b1;
            while ((b1 = stream.read()) > 0) {
                final char c1 = (char) b1;
                if (c1 == '\n' || c1 == '\r') {
                    buf.append(c1);
                    if (c1 == '\r') {
                        final int b2 = stream.read();
                        if (b2 > 0) {
                            final char c2 = (char) b2;
                            if (c2 == '\n') {
                                buf.append(c2);
                            }
                        }
                    }
                    return buf.toString();
                }
            }
        } finally {
            file.seek(pos);
        }
        return null;
    }

    /**
     * Resolve the exact path for a patch TARGET at path PATH_FROM_PATCHFILE,
     * which is the path of the target as it appeared in the patch file. Put a
     * canonicalized version of PATH_FROM_PATCHFILE into
     * TARGET->CANON_PATH_FROM_PATCHFILE. WC_CTX is a context for the working
     * copy the patch is applied to. If possible, determine TARGET->WC_PATH,
     * TARGET->ABS_PATH, TARGET->KIND, TARGET->ADDED, and
     * TARGET->PARENT_DIR_EXISTS. Indicate in TARGET->SKIPPED whether the target
     * should be skipped. STRIP_COUNT specifies the number of leading path
     * components which should be stripped from target paths in the patch.
     * 
     * @throws SVNException
     * @throws IOException
     */
    private void resolveTargetPath(File pathFromPatchfile, File absWCPath, int stripCount, SVNAdminArea wc) throws SVNException, IOException {

        final SVNPatchTarget target = this;

        target.canonPathFromPatchfile = pathFromPatchfile;

        if ("".equals(target.canonPathFromPatchfile.getPath())) {
            /* An empty patch target path? What gives? Skip this. */
            target.skipped = true;
            target.kind = SVNNodeKind.FILE;
            target.absPath = null;
            target.relPath = null;
            return;
        }

        File stripped_path;
        if (stripCount > 0) {
            stripped_path = stripPath(target.canonPathFromPatchfile, stripCount);
        } else {
            stripped_path = target.canonPathFromPatchfile;
        }

        if (stripped_path.isAbsolute()) {

            target.relPath = getChildPath(absWCPath, stripped_path);

            if (null == target.relPath) {
                /*
                 * The target path is either outside of the working copy or it
                 * is the working copy itself. Skip it.
                 */
                target.skipped = true;
                target.kind = SVNNodeKind.FILE;
                target.absPath = null;
                target.relPath = stripped_path;
                return;
            }
        } else {
            target.relPath = stripped_path;
        }

        /*
         * Make sure the path is secure to use. We want the target to be inside
         * of the working copy and not be fooled by symlinks it might contain.
         */
        if (!isChildPath(absWCPath, target.relPath)) {
            /* The target path is outside of the working copy. Skip it. */
            target.skipped = true;
            target.kind = SVNNodeKind.FILE;
            target.absPath = null;
            return;
        }

        target.absPath = new File(absWCPath, target.relPath.getPath());

        /* Skip things we should not be messing with. */

        final SVNStatus status = SVNStatusUtil.getStatus(target.absPath, wc.getWCAccess());
        final SVNStatusType contentsStatus = status.getContentsStatus();

        if (contentsStatus == SVNStatusType.STATUS_UNVERSIONED || contentsStatus == SVNStatusType.STATUS_IGNORED || contentsStatus == SVNStatusType.STATUS_OBSTRUCTED) {
            target.skipped = true;
            target.kind = SVNFileType.getNodeKind(SVNFileType.getType(target.absPath));
            return;
        }

        target.kind = status.getKind();

        if (SVNNodeKind.FILE.equals(target.kind)) {

            target.added = false;
            target.parentDirExists = true;

        } else if (SVNNodeKind.NONE.equals(target.kind) || SVNNodeKind.UNKNOWN.equals(target.kind)) {

            /*
             * The file is not there, that's fine. The patch might want to
             * create it. Check if the containing directory of the target
             * exists. We may need to create it later.
             */
            target.added = true;
            File absDirname = target.absPath.getParentFile();

            final SVNStatus status2 = SVNStatusUtil.getStatus(absDirname, wc.getWCAccess());
            final SVNStatusType contentsStatus2 = status2.getContentsStatus();
            SVNNodeKind kind = status2.getKind();
            target.parentDirExists = (kind == SVNNodeKind.DIR && contentsStatus2 != SVNStatusType.STATUS_DELETED && contentsStatus2 != SVNStatusType.STATUS_MISSING);

        } else {
            target.skipped = true;
        }

        return;
    }

    public static boolean isChildPath(final File baseFile, final File file) throws IOException {
        if (null != file && baseFile != null) {
            final String basePath = baseFile.getCanonicalPath();
            final File childFile = new File(basePath, file.getPath());
            final String childPath = childFile.getCanonicalPath();
            return childPath.startsWith(basePath) && childPath.length() > basePath.length();
        }
        return false;
    }

    private File getChildPath(File basePath, File childPath) throws IOException {
        if (null != childPath && basePath != null) {
            final String base = basePath.getCanonicalPath();
            final String child = childPath.getCanonicalPath();
            if (child.startsWith(base) && child.length() > base.length()) {
                String substr = child.substring(base.length());
                File subPath = new File(substr);
                if (!subPath.isAbsolute()) {
                    return subPath;
                }
                if (substr.length() > 1) {
                    substr = substr.substring(1);
                    subPath = new File(substr);
                    if (!subPath.isAbsolute()) {
                        return subPath;
                    }
                }
            }
        }
        return null;
    }

    public static File stripPath(File path, int stripCount) {
        if (path != null && stripCount > 0) {
            final String[] components = decomposePath(path);
            final StringBuffer buf = new StringBuffer();
            if (stripCount <= components.length) {
                for (int i = stripCount; i < components.length; i++) {
                    if (i > stripCount) {
                        buf.append(File.separatorChar);
                    }
                    buf.append(components[i]);
                }
                return new File(buf.toString());
            }
        }
        return path;
    }

    /**
     * Write the diff text of the hunk described by HI to the reject stream of
     * TARGET, and mark TARGET as having had rejects.
     * 
     * @throws IOException
     * @throws SVNException
     */
    public void rejectHunk(final SVNPatchHunkInfo hi) throws SVNException, IOException {

        final SVNPatchTarget target = this;
        final SVNPatchHunk hunk = hi.getHunk();

        final StringBuffer hunk_header = new StringBuffer();
        hunk_header.append("@@");
        hunk_header.append(" -").append(hunk.getOriginal().getStart()).append(",").append(hunk.getOriginal().getLength());
        hunk_header.append(" +").append(hunk.getModified().getStart()).append(",").append(hunk.getModified().getLength());
        hunk_header.append(" ").append(target.eolStr);

        target.reject.write(hunk_header);

        boolean eof;
        final StringBuffer hunk_line = new StringBuffer();
        final StringBuffer eol_str = new StringBuffer();
        do {
            hunk_line.setLength(0);
            eol_str.setLength(0);

            eof = hunk.getDiffText().readLineWithEol(hunk_line, eol_str);

            if (!eof) {
                if (hunk_line.length() > 0) {
                    target.reject.tryWrite(hunk_line);
                }
                if (eol_str.length() > 0) {
                    target.reject.tryWrite(eol_str);
                }
            }
        } while (!eof);

        target.hadRejects = true;

    }

    /**
     * Write the modified text of hunk described by HI to the patched stream of
     * TARGET.
     * 
     * @throws SVNException
     * @throws IOException
     */
    public void applyHunk(final SVNPatchHunkInfo hi) throws SVNException, IOException {

        final SVNPatchTarget target = this;
        final SVNPatchHunk hunk = hi.getHunk();

        if (target.kind == SVNNodeKind.FILE) {
            /*
             * Move forward to the hunk's line, copying data as we go. Also copy
             * leading lines of context which matched with fuzz. The target has
             * changed on the fuzzy-matched lines, so we should retain the
             * target's version of those lines.
             */
            target.copyLinesToTarget(hi.getMatchedLine() + hi.getFuzz());

            /*
             * Skip the target's version of the hunk. Don't skip trailing lines
             * which matched with fuzz.
             */
            target.seekToLine(target.getCurrentLine() + hunk.getOriginal().getLength() - (2 * hi.getFuzz()));
        }

        /*
         * Write the hunk's version to the patched result. Don't write the lines
         * which matched with fuzz.
         */
        long lines_read = 0;
        boolean eof = false;

        final StringBuffer hunk_line = new StringBuffer();
        final StringBuffer eol_str = new StringBuffer();
        do {

            eof = hunk.getModifiedText().readLineWithEol(hunk_line, eol_str);

            lines_read++;

            if (!eof && lines_read > hi.getFuzz() && lines_read <= hunk.getModified().getLength() - hi.getFuzz()) {
                if (hunk_line.length() > 0) {
                    tryWrite(target.getPatched(), hunk_line);
                }
                if (eol_str.length() > 0) {
                    tryWrite(target.getPatched(), eol_str);
                }
            }
        } while (!eof);

    }

    /**
     * Seek to the specified LINE in TARGET. Mark any lines not read before in
     * TARGET->LINES.
     * 
     * @throws SVNException
     * @throws IOException
     */
    public void seekToLine(int line) throws SVNException, IOException {

        if (line <= 0) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.ASSERTION_FAIL, "Line to seek must be more than zero");
            SVNErrorManager.error(err, Level.FINE, SVNLogType.WC);
        }

        final SVNPatchTarget target = this;

        if (line == target.currentLine) {
            return;
        }

        if (line <= target.lines.size()) {
            final Long mark = (Long) target.lines.get(line - 1);
            target.stream.setSeekPosition(mark.longValue());
            target.currentLine = line;
        } else {
            final StringBuffer dummy = new StringBuffer();

            while (target.currentLine < line) {
                target.readLine(dummy);
            }
        }

    }

    /**
     * Read a *LINE from TARGET. If the line has not been read before mark the
     * line in TARGET->LINES.
     * 
     * @throws SVNException
     * @throws IOException
     */
    public void readLine(final StringBuffer line) throws SVNException, IOException {

        final SVNPatchTarget target = this;

        if (target.eof) {
            return;
        }

        if (target.currentLine > target.lines.size() + 1) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.ASSERTION_FAIL, "Lines reading isn't sequenced");
            SVNErrorManager.error(err, Level.FINE, SVNLogType.WC);
        }

        if (target.currentLine == target.lines.size() + 1) {
            final Long mark = target.stream.getSeekPosition();
            target.lines.add(mark);
        }

        final StringBuffer line_raw = new StringBuffer();
        target.eof = target.stream.readLine(line_raw, target.eolStr);

        /* Contract keywords. */
        final byte[] eol = target.eolStr.getBytes(); // TODO EOL bytes
        line.append(SVNTranslator.translateString(line_raw.toString(), eol, target.keywords, false, false));

        target.currentLine++;

    }

    /**
     * Copy lines to the patched stream until the specified LINE has been
     * reached. Indicate in *EOF whether end-of-file was encountered while
     * reading from the target. If LINE is zero, copy lines until end-of-file
     * has been reached.
     * 
     * @throws IOException
     */
    public void copyLinesToTarget(int line) throws SVNException, IOException {

        final SVNPatchTarget target = this;

        while ((target.currentLine < line || line == 0) && !target.eof) {
            final StringBuffer target_line = new StringBuffer();

            target.readLine(target_line);

            if (!target.eof) {
                target_line.append(target.eolStr);
            }

            tryWrite(target.patched, target_line);
        }

    }

    /**
     * Install a patched TARGET into the working copy at ABS_WC_PATH. Use client
     * context CTX to retrieve WC_CTX, and possibly doing notifications. If
     * DRY_RUN is TRUE, don't modify the working copy.
     * 
     * @throws SVNException
     */
    public void installPatchedTarget(File absWCPath, boolean dryRun, SVNAdminArea wc) throws SVNException {

        final SVNPatchTarget target = this;

        if (target.deleted) {
            if (!dryRun) {
                /*
                 * Schedule the target for deletion. Suppress notification,
                 * we'll do it manually in a minute. Also suppress cancellation.
                 */
                SVNWCManager.delete(wc.getWCAccess(), wc, target.getAbsPath(), false, false);
            }
        } else {
            /*
             * If the target's parent directory does not yet exist we need to
             * create it before we can copy the patched result in place.
             */
            if (target.isAdded() && !target.isParentDirExists()) {

                /* Check if we can safely create the target's parent. */
                File absPath = absWCPath;
                String[] components = decomposePath(target.getRelPath());
                int present_components = 0;
                for (int i = 0; i < components.length - 1; i++) {
                    final String component = components[i];
                    absPath = new File(absPath, component);

                    final SVNEntry entry = wc.getWCAccess().getEntry(absPath, false);
                    final SVNNodeKind kind = entry != null ? entry.getKind() : null;

                    if (kind == SVNNodeKind.FILE) {
                        /* Obstructed. */
                        target.skipped = true;
                        break;
                    } else if (kind == SVNNodeKind.DIR) {
                        /*
                         * ### wc-ng should eventually be able to replace
                         * directories in-place, so this schedule conflict check
                         * will go away. We could then also make the
                         * svn_wc__node_get_kind() call above ignore hidden
                         * nodes.
                         */
                        if (entry.isDeleted()) {
                            target.skipped = true;
                            break;
                        }

                        present_components++;
                    } else {
                        /*
                         * The WC_DB doesn't know much about this node. Check
                         * what's on disk.
                         */
                        final SVNFileType disk_kind = SVNFileType.getType(absPath);
                        if (disk_kind != SVNFileType.NONE) {
                            /* An unversioned item is in the way. */
                            target.skipped = true;
                            break;
                        }
                    }
                }

                if (!target.isSkipped()) {
                    absPath = absWCPath;
                    for (int i = 0; i < present_components; i++) {
                        final String component = components[i];
                        absPath = new File(absPath, component);
                        if (dryRun) {
                            /* Just do notification. */
                            SVNEvent mergeCompletedEvent = SVNEventFactory.createSVNEvent(absPath, SVNNodeKind.NONE, null, SVNRepository.INVALID_REVISION, SVNStatusType.INAPPLICABLE,
                                    SVNStatusType.INAPPLICABLE, SVNStatusType.LOCK_INAPPLICABLE, SVNEventAction.ADD, null, null, null);
                            wc.getWCAccess().handleEvent(mergeCompletedEvent);
                        } else {
                            /*
                             * Create the missing component and add it to
                             * version control. Suppress cancellation.
                             */
                            if (absPath.mkdirs()) {
                                SVNWCManager.add(absPath, wc, null, SVNRepository.INVALID_REVISION, SVNDepth.INFINITY);
                            }
                        }
                    }
                }
            }

            if (!dryRun && !target.isSkipped()) {
                /* Copy the patched file on top of the target file. */
                SVNFileUtil.copyFile(target.getPatchedPath(), target.getAbsPath(), false);
                if (target.isAdded()) {
                    /*
                     * The target file didn't exist previously, so add it to
                     * version control. Suppress notification, we'll do that
                     * later. Also suppress cancellation.
                     */
                    SVNWCManager.add(target.getAbsPath(), wc, null, SVNRepository.INVALID_REVISION, SVNDepth.INFINITY);
                }

                /* Restore the target's executable bit if necessary. */
                SVNFileUtil.setExecutable(target.getAbsPath(), target.isExecutable());
            }
        }

        /* Write out rejected hunks, if any. */
        if (!dryRun && !target.skipped && target.hadRejects) {
            final String rej_path = target.getAbsPath().getPath() + ".svnpatch.rej";
            SVNFileUtil.copyFile(target.getRejectPath(), new File(rej_path), true);
            /* ### TODO mark file as conflicted. */
        }

    }

    public static String[] decomposePath(File path) {
        String pathString = SVNFileUtil.getFilePath(path);
        if (pathString.endsWith("/")) {
            pathString = pathString.substring(0, pathString.length() - 1);
        }

        return pathString.split(String.valueOf(File.separatorChar));
    }

    /**
     * Apply a PATCH to a working copy at ABS_WC_PATH.
     * 
     * STRIP_COUNT specifies the number of leading path components which should
     * be stripped from target paths in the patch.
     * 
     * @throws SVNException
     * @throws IOException
     */
    public static SVNPatchTarget applyPatch(SVNPatch patch, File absWCPath, int stripCount, SVNAdminArea wc) throws SVNException, IOException {

        final SVNPatchTarget target = SVNPatchTarget.initPatchTarget(patch, absWCPath, stripCount, wc);

        if (target.skipped) {
            return target;
        }

        /* Match hunks. */
        for (final Iterator i = patch.getHunks().iterator(); i.hasNext();) {
            final SVNPatchHunk hunk = (SVNPatchHunk) i.next();

            SVNPatchHunkInfo hi;
            int fuzz = 0;

            /*
             * Determine the line the hunk should be applied at. If no match is
             * found initially, try with fuzz.
             */
            do {
                hi = target.getHunkInfo(hunk, fuzz);
                fuzz++;
            } while (hi.isRejected() && fuzz <= MAX_FUZZ);

            target.hunks.add(hi);
        }

        /* Apply or reject hunks. */
        for (final Iterator i = target.hunks.iterator(); i.hasNext();) {
            final SVNPatchHunkInfo hi = (SVNPatchHunkInfo) i.next();

            if (hi.isRejected()) {
                target.rejectHunk(hi);
            } else {
                target.applyHunk(hi);
            }
        }

        if (target.kind == SVNNodeKind.FILE) {
            /* Copy any remaining lines to target. */
            target.copyLinesToTarget(0);
            if (!target.eof) {
                /*
                 * We could not copy the entire target file to the temporary
                 * file, and would truncate the target if we copied the
                 * temporary file on top of it. Cancel any modifications to the
                 * target file and report is as skipped.
                 */
                target.skipped = true;
            }
        }

        /*
         * Close the streams of the target so that their content is flushed to
         * disk. This will also close underlying streams.
         */
        if (target.getKind() == SVNNodeKind.FILE) {
            target.stream.close();
        }
        target.patched.close();
        target.reject.close();

        if (!target.skipped) {

            /*
             * Get sizes of the patched temporary file and the working file.
             * We'll need those to figure out whether we should add or delete
             * the patched file.
             */
            final long patchedFileSize = target.patchedPath.length();
            final long workingFileSize = target.kind == SVNNodeKind.FILE ? target.absPath.length() : 0;

            if (patchedFileSize == 0 && workingFileSize > 0) {
                /*
                 * If a unidiff removes all lines from a file, that usually
                 * means deletion, so we can confidently schedule the target for
                 * deletion. In the rare case where the unidiff was really meant
                 * to replace a file with an empty one, this may not be
                 * desirable. But the deletion can easily be reverted and
                 * creating an empty file manually is not exactly hard either.
                 */
                target.deleted = target.kind != SVNNodeKind.NONE;
            } else if (workingFileSize == 0 && patchedFileSize == 0) {
                /*
                 * The target was empty or non-existent to begin with and
                 * nothing has changed by patching. Report this as skipped if it
                 * didn't exist.
                 */
                if (target.kind != SVNNodeKind.FILE)
                    target.skipped = true;
            } else if (target.kind != SVNNodeKind.FILE && patchedFileSize > 0) {
                /* The patch has created a file. */
                target.added = true;
            }

        }

        return target;

    }

    /**
     * Determine the line at which a HUNK applies to the TARGET file, and return
     * an appropriate hunk_info object in *HI, allocated from RESULT_POOL. Use
     * fuzz factor FUZZ. Set HI->FUZZ to FUZZ. If no correct line can be
     * determined, set HI->REJECTED to TRUE. When this function returns, neither
     * TARGET->CURRENT_LINE nor the file offset in the target file will have
     * changed.
     * 
     * @throws SVNException
     * @throws IOException
     */
    public SVNPatchHunkInfo getHunkInfo(final SVNPatchHunk hunk, final int fuzz) throws SVNException, IOException {

        final SVNPatchTarget target = this;

        int matchedLine;

        /*
         * An original offset of zero means that this hunk wants to create a new
         * file. Don't bother matching hunks in that case, since the hunk
         * applies at line 1. If the file already exists, the hunk is rejected.
         */
        if (hunk.getOriginal().getStart() == 0) {
            if (target.getKind() == SVNNodeKind.FILE) {
                matchedLine = 0;
            } else {
                matchedLine = 1;
            }
        } else if (hunk.getOriginal().getStart() > 0 && target.getKind() == SVNNodeKind.FILE) {

            int savedLine = target.getCurrentLine();
            boolean savedEof = target.isEof();

            /*
             * Scan for a match at the line where the hunk thinks it should be
             * going.
             */
            target.seekToLine(hunk.getOriginal().getStart());
            matchedLine = target.scanForMatch(hunk, true, hunk.getOriginal().getStart() + 1, fuzz);

            if (matchedLine != hunk.getOriginal().getStart()) {

                /* Scan the whole file again from the start. */
                target.seekToLine(1);

                /*
                 * Scan forward towards the hunk's line and look for a line
                 * where the hunk matches.
                 */
                matchedLine = target.scanForMatch(hunk, false, hunk.getOriginal().getStart(), fuzz);

                /*
                 * In tie-break situations, we arbitrarily prefer early matches
                 * to save us from scanning the rest of the file.
                 */
                if (matchedLine == 0) {
                    /*
                     * Scan forward towards the end of the file and look for a
                     * line where the hunk matches.
                     */
                    matchedLine = target.scanForMatch(hunk, true, 0, fuzz);

                }
            }

            target.seekToLine(savedLine);
            target.eof = savedEof;

        } else {
            /* The hunk wants to modify a file which doesn't exist. */
            matchedLine = 0;
        }

        return new SVNPatchHunkInfo(hunk, matchedLine, (matchedLine == 0), fuzz);
    }

    /**
     * Scan lines of TARGET for a match of the original text of HUNK, up to but
     * not including the specified UPPER_LINE. Use fuzz factor FUZZ. If
     * UPPER_LINE is zero scan until EOF occurs when reading from TARGET. Return
     * the line at which HUNK was matched in *MATCHED_LINE. If the hunk did not
     * match at all, set *MATCHED_LINE to zero. If the hunk matched multiple
     * times, and MATCH_FIRST is TRUE, return the line number at which the first
     * match occured in *MATCHED_LINE. If the hunk matched multiple times, and
     * MATCH_FIRST is FALSE, return the line number at which the last match
     * occured in *MATCHED_LINE.
     * 
     * @throws SVNException
     * @throws IOException
     */
    public int scanForMatch(SVNPatchHunk hunk, boolean matchFirst, int upperLine, int fuzz) throws SVNException, IOException {

        final SVNPatchTarget target = this;

        int matched_line = 0;

        while ((target.currentLine < upperLine || upperLine == 0) && !target.eof) {

            boolean matched = target.matchHunk(hunk, fuzz);

            if (matched) {
                boolean taken = false;

                /* Don't allow hunks to match at overlapping locations. */
                for (Iterator i = target.hunks.iterator(); i.hasNext();) {
                    final SVNPatchHunkInfo hi = (SVNPatchHunkInfo) i.next();
                    taken = (!hi.isRejected() && target.currentLine >= hi.getMatchedLine() && target.currentLine < hi.getMatchedLine() + hi.getHunk().getOriginal().getLength());
                    if (taken) {
                        break;
                    }
                }

                if (!taken) {
                    matched_line = target.currentLine;
                    if (matchFirst) {
                        break;
                    }
                }
            }

            target.seekToLine(target.currentLine + 1);

        }

        return matched_line;

    }

    /**
     * Indicate in *MATCHED whether the original text of HUNK matches the patch
     * TARGET at its current line. Lines within FUZZ lines of the start or end
     * of HUNK will always match. When this function returns, neither
     * TARGET->CURRENT_LINE nor the file offset in the target file will have
     * changed. HUNK->ORIGINAL_TEXT will be reset.
     * 
     * @throws SVNException
     * @throws IOException
     */
    private boolean matchHunk(SVNPatchHunk hunk, int fuzz) throws SVNException, IOException {

        final SVNPatchTarget target = this;

        final StringBuffer hunkLine = new StringBuffer();
        final StringBuffer targetLine = new StringBuffer();
        final StringBuffer eol_str = new StringBuffer();

        int linesRead;
        int savedLine;
        boolean hunkEof;
        boolean linesMatched;

        boolean matched = false;

        if (target.eof) {
            return matched;
        }

        savedLine = target.currentLine;
        linesRead = 0;
        linesMatched = false;
        hunk.getOriginalText().reset();

        do {
            String hunk_line_translated;

            hunkLine.setLength(0);
            eol_str.setLength(0);

            hunkEof = hunk.getOriginalText().readLineWithEol(hunkLine, eol_str);

            /* Contract keywords, if any, before matching. */
            final byte[] eol = eol_str.toString().getBytes();
            hunk_line_translated = SVNTranslator.translateString(hunkLine.toString(), eol, target.keywords, false, false);

            linesRead++;
            targetLine.setLength(0);
            target.readLine(targetLine);

            if (!hunkEof) {
                if (linesRead <= fuzz && hunk.getLeadingContext() > fuzz) {
                    linesMatched = true;
                } else if (linesRead > hunk.getOriginal().getLength() - fuzz && hunk.getTrailingContext() > fuzz) {
                    linesMatched = true;
                } else {
                    linesMatched = hunk_line_translated.equals(targetLine.toString());
                }
            }
        } while (linesMatched && !(hunkEof || target.eof));

        if (hunkEof) {
            matched = linesMatched;
        } else if (target.eof) {
            /*
             * If the target has no newline at end-of-file, we get an EOF
             * indication for the target earlier than we do get it for the hunk.
             */
            hunkEof = hunk.getOriginalText().readLineWithEol(hunkLine, null);
            if (hunkLine.length() == 0 && hunkEof) {
                matched = linesMatched;
            } else {
                matched = false;
            }
        }
        target.seekToLine(savedLine);
        target.eof = false;

        return matched;
    }

    /**
     * Attempt to write LEN bytes of DATA to STREAM, the underlying file of
     * which is at ABSPATH. Fail if not all bytes could be written to the
     * stream.
     */
    private void tryWrite(OutputStream stream, StringBuffer buffer) throws IOException {
        stream.write(buffer.toString().getBytes());
    }

    /**
     * Use client context CTX to send a suitable notification for a patch
     * TARGET.
     * 
     * @throws SVNException
     */
    public void sendPatchNotification(SVNAdminArea wc) throws SVNException {

        final SVNPatchTarget target = this;

        final ISVNEventHandler eventHandler = wc.getWCAccess().getEventHandler();

        if (eventHandler == null) {
            return;
        }

        SVNEventAction action;

        if (target.skipped) {
            action = SVNEventAction.SKIP;
        } else if (target.deleted) {
            action = SVNEventAction.DELETE;
        } else if (target.added) {
            action = SVNEventAction.ADD;
        } else {
            action = SVNEventAction.PATCH;
        }

        SVNStatusType contentState = SVNStatusType.INAPPLICABLE;

        if (action == SVNEventAction.SKIP) {
            if (target.parentDirExists && (target.kind == SVNNodeKind.NONE || target.kind == SVNNodeKind.UNKNOWN)) {
                contentState = SVNStatusType.MISSING;
            } else if (target.kind == SVNNodeKind.DIR) {
                contentState = SVNStatusType.OBSTRUCTED;
            } else {
                contentState = SVNStatusType.UNKNOWN;
            }
        } else {
            if (target.hadRejects) {
                contentState = SVNStatusType.CONFLICTED;
            } else if (target.localMods) {
                contentState = SVNStatusType.MERGED;
            } else {
                contentState = SVNStatusType.CHANGED;
            }
        }

        final SVNEvent notify = SVNEventFactory.createSVNEvent(target.absPath != null ? target.absPath : target.relPath, target.kind, null, 0, contentState, SVNStatusType.INAPPLICABLE,
                SVNStatusType.LOCK_INAPPLICABLE, action, null, null, null);

        eventHandler.handleEvent(notify, ISVNEventHandler.UNKNOWN);

        if (action == SVNEventAction.PATCH) {

            for (final Iterator i = target.hunks.iterator(); i.hasNext();) {
                final SVNPatchHunkInfo hi = (SVNPatchHunkInfo) i.next();

                if (hi.isRejected()) {
                    action = SVNEventAction.PATCH_REJECTED_HUNK;
                } else {
                    action = SVNEventAction.PATCH_APPLIED_HUNK;
                }

                final SVNEvent notify2 = SVNEventFactory.createSVNEvent(target.absPath != null ? target.absPath : target.relPath, target.kind, null, 0, action, null, null, null);
                notify2.setInfo(hi);

                eventHandler.handleEvent(notify2, ISVNEventHandler.UNKNOWN);
            }
        }

    }

}
