/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.jackrabbit.vault.vlt;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintStream;
import java.io.Reader;
import java.io.Writer;
import java.util.Locale;
import java.util.Properties;

import org.apache.commons.io.FileUtils;
import org.apache.jackrabbit.vault.fs.VaultFileCopy;
import org.apache.jackrabbit.vault.fs.api.VaultFile;
import org.apache.jackrabbit.vault.util.Constants;
import org.apache.jackrabbit.vault.util.LineOutputStream;
import org.apache.jackrabbit.vault.util.MD5;
import org.apache.jackrabbit.vault.util.MimeTypes;
import org.apache.jackrabbit.vault.util.PathUtil;
import org.apache.jackrabbit.vault.util.diff.DiffWriter;
import org.apache.jackrabbit.vault.util.diff.Document;
import org.apache.jackrabbit.vault.util.diff.DocumentDiff;
import org.apache.jackrabbit.vault.util.diff.DocumentDiff3;
import org.apache.jackrabbit.vault.util.diff.DocumentSource;
import org.apache.jackrabbit.vault.util.diff.FileDocumentSource;
import org.apache.jackrabbit.vault.util.diff.Hunk3;
import org.apache.jackrabbit.vault.util.diff.LineElementsFactory;
import org.apache.jackrabbit.vault.vlt.meta.MetaFile;
import org.apache.jackrabbit.vault.vlt.meta.MetaFileDocSource;
import org.apache.jackrabbit.vault.vlt.meta.VltEntry;
import org.apache.jackrabbit.vault.vlt.meta.VltEntryInfo;

/**
 * {@code VltFile}...
 *
 */
public class VltFile implements DocumentSource {

    public static final String PROP_CONTENT_TYPE = "vlt:mime-type";

    /**
     * Possible state of this file
     */
    public enum State {
        CLEAN (" "),
        ADDED ("A"),
        CONFLICTED ("C"),
        DELETED ("D"),
        IGNORED ("I"),
        MODIFIED ("M"),
        REPLACED ("R"),
        UNKNOWN("?"),
        MISSING ("!"),
        OBSTRUCTED ("~"),
        VOID (" ");

        public final String letter;

        private State(String letter) {
            this.letter = letter;
        }

        public String toString() {
            return name().toLowerCase(Locale.ROOT) + " (" + letter + ")";
        }
    }

    private final VltDirectory parent;

    private final File file;

    private final String name;

    private VltEntry entry;

    public VltFile(VltDirectory parent, String name, VltEntry entry)
            throws VltException {
        this.parent = parent;
        this.name = name;
        this.entry = entry;
        this.file = new File(parent.getDirectory(), name);
    }

    public Properties getProperties() throws VltException {
        Properties props = new Properties();
        if (entry != null) {
            VltEntryInfo info = entry.work();
            String ct = info.getContentType();
            if (ct != null) {
                props.put(PROP_CONTENT_TYPE, ct);
            }
        }
        return props;
    }

    public String getProperty(String name) throws VltException {
        if (entry != null) {
            VltEntryInfo info = entry.work();
            if (name.equals(PROP_CONTENT_TYPE)) {
                return info.getContentType();
            }
        }
        return null;
    }

    public void setProperty(String name, String value) throws VltException {
        if (entry == null) {
            throw error("Can't set property to non controlled file.");
        }
        VltEntryInfo info = entry.work();
        if (info == null) {
            throw error("Can't set property to non controlled file.");
        }
        if (name.equals(PROP_CONTENT_TYPE)) {
            if (!file.isDirectory()) {
                // silently ignore directories
                info.setContentType(value);
                parent.getContext().printMessage(this, name + "=" + value);
            }
        } else {
            throw error("Generic properies not supported, yet");
        }
    }
    
    public State getStatus() throws VltException {
        State state = State.VOID;
        if (entry == null) {
            if (file.exists()) {
                // special check for jcr_root
                if (file.equals(parent.getContext().getExportRoot().getJcrRoot())) {
                    state = State.CLEAN;
                } else {
                    state = State.UNKNOWN;
                }
            } else {
                state = State.VOID;
            }
        } else {
            switch (entry.getState()) {
                case CLEAN:
                    if (file.exists()) {
                        if (file.isDirectory()) {
                            VltDirectory dir = descend();
                            if (dir.isControlled()) {
                                state = State.CLEAN;
                            } else {
                                state = State.OBSTRUCTED;
                            }
                        } else {
                            VltEntryInfo work = entry.work();
                            VltEntryInfo base = entry.base();
                            assert work != null;
                            assert base != null;

                            try {
                                work.update(file, false);
                            } catch (IOException e) {
                                throw exception("Error while calculating status.", e);
                            }
                            state = work.isSame(base) ? State.CLEAN : State.MODIFIED;
                        }
                    } else {
                        state = State.MISSING;
                    }
                    break;
                case ADDED:
                    if (file.exists()) {
                        state = State.ADDED;
                    } else {
                        state = State.MISSING;
                    }
                    break;
                case CONFLICT:
                    state = State.CONFLICTED;
                    break;
                case DELETED:
                    state = State.DELETED;
                    break;
            }
        }
        return state;
    }

    public String getName() {
        return name;
    }

    public File getFile() {
        return file;
    }

    public String getPath() {
        return file.getPath();
    }

    public MetaFile getBaseFile(boolean create) throws VltException {
        try {
            return parent.getMetaDirectory().getBaseFile(name, create);
        } catch (IOException e) {
            throw new VltException(getPath(), "Error opening base file.", e);
        }
    }

    public String getContentType() {
        if (entry != null && !file.isDirectory()) {
            VltEntryInfo work = entry.work();
            if (work != null) {
                return work.getContentType();
            }
        }
        return null;
    }

    /**
     * Checks if this file has binary content. It does not actually read the
     * file data but calls {@link MimeTypes#isBinary(String)} with the content
     * type of the work file.
     * @return {@code true} if this is binary
     */
    public boolean isBinary() {
        return MimeTypes.isBinary(getContentType());
    }


    public MetaFile getTmpFile() throws VltException {
        try {
            return parent.getMetaDirectory().getTmpFile(name, true);
        } catch (IOException e) {
            throw new VltException(getPath(), "Error opening tmp file.", e);
        }
    }

    public boolean canDescend() {
        return file.isDirectory();
    }

    public VltDirectory descend() throws VltException {
        if (!canDescend()) {
            throw new VltException("Cannot descend into non directory.");
        }
        return new VltDirectory(parent.getContext(), file);
    }

    public VltEntry getEntry() {
        return entry;
    }

    public void diff() throws VltException {
        State state = getStatus();
        if (entry == null || entry.isDirectory()) {
            return;
        }
        VltEntryInfo work = entry.work();
        VltEntryInfo base = entry.base();
        if (work == null || base == null) {
            return;
        }
        switch (state) {
            case ADDED:
            case CONFLICTED:
            case DELETED:
            case MODIFIED:
                break;
            case IGNORED:
            case MISSING:
            case OBSTRUCTED:
            case REPLACED:
            case UNKNOWN:
            case VOID:
            case CLEAN:
                return;
        }
        if (MimeTypes.isBinary(work.getContentType()) || MimeTypes.isBinary(base.getContentType())) {
            PrintStream s = parent.getContext().getStdout();
            s.printf(Locale.ENGLISH, "Index: %s%n", getName());
            s.println("===================================================================");
            s.println("Cannot display: file marked as binary type.");
            s.printf(Locale.ENGLISH, "vlt:mime-type = %s%n", work.getContentType());
            s.flush();
            return;
        }
        try {
            // do the actual diff
            PrintStream s = parent.getContext().getStdout();
            DiffWriter out = new DiffWriter(new OutputStreamWriter(s, Constants.ENCODING));
            out.write("Index: ");
            out.write(getName());
            out.writeNewLine();
            out.write("===================================================================");
            out.writeNewLine();

            try (Reader r0 = getBaseFile(false) == null ? null : getBaseFile(false).getReader();
                 Reader r1 = file.exists() ? new InputStreamReader(FileUtils.openInputStream(file), Constants.ENCODING) : null) {
                Document d0 = new Document(this, LineElementsFactory.create(this, r0, false));
                Document d1 = new Document(this, LineElementsFactory.create(this, r1, false));
                DocumentDiff diff = d0.diff(d1);
                diff.write(out, 3);
            }
            out.flush();
        } catch (IOException e) {
            throw exception("Error while writing diff.", e);
        }

    }

    public FileAction delete(boolean force) throws VltException {
        State state = getStatus();
        switch (state) {
            case ADDED:
            case CONFLICTED:
            case MODIFIED:
            case REPLACED:
                if (!force) {
                    parent.getContext().printMessage(this, "has local modification. use --force to delete anyway");
                    return FileAction.VOID;
                }
                break;
            case CLEAN:
            case MISSING:
            case DELETED:
                break;
            case IGNORED:
            case OBSTRUCTED:
            case UNKNOWN:
            case VOID:
                if (!force) {
                    parent.getContext().printMessage(this, "is not under version control. use --force to delete anyway");
                    return FileAction.VOID;
                }
                break;
        }
        if (entry != null && entry.delete(file)) {
            entry = null;
        }
        return FileAction.DELETED;
    }

    public FileAction commit(VaultFile remoteFile) throws VltException {
        if (remoteFile == null) {
            return doDelete(false);
        } else {
            return doUpdate(remoteFile, false);
        }
    }

    public boolean revert() throws VltException {
        State state = getStatus();
        switch (state) {
            case ADDED:
                doDelete(true);
                entry = null;
                return true;

            case CONFLICTED:
                resolved(true);
                // no break;
            case DELETED:
            case MISSING:
            case MODIFIED:
                doRevert();
                return true;

            case IGNORED:
            case CLEAN:
            case OBSTRUCTED:
            case REPLACED:
            case UNKNOWN:
            case VOID:
            default:
                return false;
        }
    }

    public boolean resolved(boolean force) throws VltException {
        if (getStatus() != State.CONFLICTED) {
            return false;
        }
        if (!force) {
            // check if the file still contains the diff markers
            boolean mayContainMarker = false;
            try (InputStreamReader reader = new InputStreamReader(new FileInputStream(file), Constants.ENCODING);
                 BufferedReader in = new BufferedReader(reader)) {
                String line;
                while ((line = in.readLine()) != null) {
                    if (line.startsWith(Hunk3.MARKER_B[0])
                            || line.startsWith(Hunk3.MARKER_L[0])
                            || line.startsWith(Hunk3.MARKER_R[0])
                            || line.startsWith(Hunk3.MARKER_M[0])) {
                        mayContainMarker = true;
                        break;
                    }
                }
            } catch (IOException e) {
                throw exception("Error while reading file.", e);
            }
            if (mayContainMarker) {
                throw error("File still contains conflict markers. use --force to force resolve.");
            }
        }

        // resolve entry
        try {
            entry.resolved(getTmpFile(), file, getBaseFile(false));
        } catch (IOException e) {
            throw exception("Error while copying files.", e);
        }

        return true;
    }

    public FileAction update(VaultFile remoteFile, boolean force)
            throws VltException {
        State state  = getStatus();
        switch (state) {
            case IGNORED:
            case OBSTRUCTED:
            case REPLACED:
                if (!force || remoteFile == null) {
                    throw error("update not possible. file is " + state.name().toLowerCase(Locale.ROOT) + ". " +
                            "Specify --force to overwrite existing files.");
                }
                return doUpdate(remoteFile, false);

            case ADDED:
                if (remoteFile != null) {
                    if (mergeableWithRemote(remoteFile) != FileAction.VOID) {
                        throw error("Failed to add file: object of the same name already exists.");
                    }
                    return doUpdate(remoteFile, false);
                } else {
                    return FileAction.VOID;
                }

            case CLEAN:
                if (remoteFile == null) {
                    return doDelete(false);
                } else {
                    if (file.isDirectory()) {
                        // do nothing
                        return FileAction.VOID;
                    } else {
                        return doUpdate(remoteFile, false);
                    }
                }

            case CONFLICTED:
                if (remoteFile == null) {
                    try {
                        if (!entry.revertConflict(file)) {
                            return FileAction.CONFLICTED;
                        }
                    } catch (IOException e) {
                        throw exception("Error during update.", e);
                    }
                    // refetch status, and delete file if clean
                    return doDelete(getStatus() != State.CLEAN);
                } else {
                    try {
                        if (!entry.revertConflict(file)) {
                            return doMerge(remoteFile, FileAction.CONFLICTED);
                        } else {
                            return doMerge(remoteFile, FileAction.UPDATED);
                        }
                    } catch (IOException e) {
                        throw exception("Error during update.", e);
                    }
                }

            case DELETED:
                if (remoteFile == null) {
                    // we can delete the entry since someone else deleted it as well
                    return doDelete(false);
                } else {
                    // just update base and entry, in case someone wants to revert
                    return doUpdate(remoteFile, true);
                }

            case MISSING:
                if (remoteFile == null) {
                    // if file is missing, just delete it
                    return doDelete(false);
                } else {
                    // do update
                    entry = null;
                    return doUpdate(remoteFile, false);
                }

            case MODIFIED:
                if (remoteFile == null) {
                    // keep the file
                    return doDelete(true);
                } else {
                    return doMerge(remoteFile, FileAction.VOID);
                }

            case UNKNOWN:
                if (remoteFile == null) {
                    // do nothing
                    return FileAction.VOID;
                } else {
                    // do update
                    if (file.exists() && !force) {
                        throw error("Failed to update: object of the same name already exists." +
                                " Specify --force to overwrite existing files.");
                    }
                    return doUpdate(remoteFile, false);
                }

            case VOID:
                // do update
                return doUpdate(remoteFile, false);

            default:
                throw exception("illegal state: " + state, null);
        }
    }

    public FileAction status(VaultFile remoteFile) throws VltException {
        State state  = getStatus();
        switch (state) {
            case IGNORED:
            case OBSTRUCTED:
            case REPLACED:
                return FileAction.CONFLICTED;

            case ADDED:
                if (remoteFile != null) {
                    return FileAction.CONFLICTED;
                } else {
                    return FileAction.VOID;
                }

            case CLEAN:
                if (remoteFile == null) {
                    return FileAction.DELETED;
                } else {
                    if (file.isDirectory()) {
                        // do nothing
                        return FileAction.VOID;
                    } else {
                        return equalsToRemote(remoteFile)
                                ? FileAction.VOID
                                : FileAction.UPDATED;
                    }
                }

            case CONFLICTED:
                // do not probe further
                return FileAction.CONFLICTED;

            case DELETED:
                return FileAction.VOID;

            case MISSING:
                return FileAction.ADDED;

            case MODIFIED:
                if (remoteFile == null) {
                    return FileAction.DELETED;
                } else {
                    return mergeableWithRemote(remoteFile);
                }

            case UNKNOWN:
                if (remoteFile == null) {
                    // do nothing
                    return FileAction.VOID;
                } else {
                    return FileAction.UPDATED;
                }

            case VOID:
                return FileAction.ADDED;

            default:
                throw exception("illegal state: " + state, null);
        }
    }

    public FileAction add(boolean force) throws VltException {
        State state = getStatus();
        switch (state) {
            case ADDED:
            case CLEAN:
            case CONFLICTED:
            case MISSING:
            case MODIFIED:
            case OBSTRUCTED:
            case REPLACED:
                parent.getContext().printMessage(this, "is already under version control");
                break;
            case DELETED:
                parent.getContext().printMessage(this, "replace not supported yet");
                break;
            case IGNORED:
                parent.getContext().printMessage(this, "failed to add. is ignored.");
                break;
            case UNKNOWN:
            case VOID:
                return doAdd(force);
        }
        return FileAction.VOID;
    }

    private FileAction doAdd(boolean force) throws VltException {
        assert entry == null;
        entry = parent.getEntries().update(getName(), null, null);
        VltEntryInfo work = entry.create(VltEntryInfo.Type.WORK);
        try {
            work.update(file, true);
        } catch (IOException e) {
            throw exception("Error while adding file", e);
        }
        String contentType = MimeTypes.getMimeType(file.getName(), MimeTypes.APPLICATION_OCTET_STREAM);
        work.setContentType(contentType);
        entry.put(work);
        return FileAction.ADDED;
    }

    private FileAction doDelete(boolean keepFile)
            throws VltException {
        // small hack to remove meta directory. should actually be somewhere else
        if (file.isDirectory()) {
            VltDirectory dir = new VltDirectory(parent.getContext(), file);
            dir.uncontrol();
        } else {
            try {
                if (getBaseFile(false) != null) {
                    getBaseFile(false).delete();
                }
            } catch (IOException e) {
                throw new VltException(getPath(), "Error while deleting base file.", e);
            }
        }
        if (!keepFile) {
            file.delete();
        }
        entry = null;
        return FileAction.DELETED;
    }

    private FileAction doMerge(VaultFile remoteFile, FileAction action)
            throws VltException {
        if (remoteFile.isDirectory()) {
            throw exception("Error while merging. remote is a directory.", null);
        }
        // abort merger if actions is already conflict
        if (action == FileAction.CONFLICTED) {
            return action;
        }
        MetaFile baseFile = getBaseFile(false);
        MetaFile tmpFile = getTmpFile();

        VltEntryInfo base = entry.base();
        VltEntryInfo work = entry.work();
        byte[] lineFeed = MimeTypes.isBinary(remoteFile.getContentType())
                ? null
                : LineOutputStream.LS_NATIVE;

        // get the remote file
        VaultFileCopy copy = null;
        boolean remoteUpdated = true;
        try {
            // first check size and last modified
            if (!base.checkModified(remoteFile)) {
                remoteUpdated = false;
            } else {
                File temp = tmpFile.openTempFile();
                copy = VaultFileCopy.copy(remoteFile, temp, lineFeed);
                // if tmp is equal to the base one, there was no update on the server
                if (copy.getMd5().equals(base.getMd5())) {
                    tmpFile.closeTempFile(tmpFile.length() >= 0);
                    remoteUpdated = false;
                } else {
                    tmpFile.closeTempFile(false);
                }
            }
        } catch (IOException e) {
            throw exception("Error while copying files.", e);
        }
        if (!remoteUpdated) {
            if (work.getMd5().equals(base.getMd5())) {
                // fix base
                base.setSize(work.getSize());
                base.setDate(work.getDate());
                return FileAction.VOID;
            } else if (remoteFile.lastModified() > 0) {
                // normal modification provided
                return action;
            }
        }

        try {
            // check if binary
            boolean remoteBT = getRemoteBinaryType(remoteFile, copy);
            boolean localBT = MimeTypes.isBinary(base.getContentType());
            if (remoteBT || localBT) {
                parent.getContext().printMessage(this, "can't merge. binary content");
                entry.conflict(file, baseFile, tmpFile);
                return FileAction.CONFLICTED;
            }

            DocumentDiff3 diff;
            // do a 3-way diff between the base, the local and the remote one.
            // we currently do not use document sources, since we don't really have
            // a label to provide (like rev. num, etc).
            try (Reader r0 = baseFile.getReader();
                 Reader r1 = tmpFile.getReader()) {
                Document baseDoc = new Document(null, LineElementsFactory.create(new MetaFileDocSource(baseFile), r0, false));
                Document leftDoc = new Document(null, LineElementsFactory.create(new FileDocumentSource(file), false, Constants.ENCODING));
                Document rightDoc = new Document(null, LineElementsFactory.create(new MetaFileDocSource(tmpFile), r1, false));
                diff = baseDoc.diff3(leftDoc, rightDoc);
            }

            // save the diff output
            try (Writer out = new OutputStreamWriter(FileUtils.openOutputStream(file), Constants.ENCODING)) {
                diff.write(new DiffWriter(out), false);
            }

            if (diff.hasConflicts()) {
                entry.conflict(file, baseFile, tmpFile);
                action = FileAction.CONFLICTED;
            } else {
                // make the tmp file the new base
                tmpFile.moveTo(baseFile);
                base.update(baseFile, true);
                action = FileAction.MERGED;
            }

            // and update the 'work'
            // check if MD5 changes and change action accordingly
            MD5 oldMd5 = work.getMd5();
            work.update(file, true);
            if (oldMd5.equals(work.getMd5())) {
                action = FileAction.VOID;
            }
            // check if remote file provided a last modified
            if (remoteFile.lastModified() == 0) {
                if (work.getMd5().equals(base.getMd5())) {
                    base.setDate(work.getDate());
                } else {
                    base.setDate(System.currentTimeMillis());
                }
            }

        } catch (IOException e) {
            throw exception("Error during merge operation.", e);
        }

        return action;
    }

    private boolean getRemoteBinaryType(VaultFile remoteFile, VaultFileCopy copy) {
        // check if binary
        boolean remoteBT = MimeTypes.isBinary(remoteFile.getContentType());
        if (copy != null && remoteBT != copy.isBinary()) {
            parent.getContext().printMessage(this, "Remote Binary type differs from actual data. Content Type: " + remoteFile.getContentType() + " Data is binary: " + copy.isBinary() + ". Using data type.");
            remoteBT = copy.isBinary();                
        }
        return remoteBT;
    }

    private FileAction mergeableWithRemote(VaultFile remoteFile)
            throws VltException {
        if (remoteFile.isDirectory() != file.isDirectory()) {
            return FileAction.CONFLICTED;
        }
        if (file.isDirectory()) {
            return FileAction.VOID;
        }

        MetaFile tmpFile = getTmpFile();

        VltEntryInfo base = entry.base();

        // get the remote file
        byte[] lineFeed = MimeTypes.isBinary(remoteFile.getContentType())
                ? null
                : LineOutputStream.LS_NATIVE;
        VaultFileCopy copy;
        try {
            File temp = tmpFile.openTempFile();
            copy = VaultFileCopy.copy(remoteFile, temp, lineFeed);
            if (base == null) {
                tmpFile.closeTempFile(true);
                // if base is null, file was only added so check the work entry
                VltEntryInfo work = entry.work();
                if (copy.getMd5().equals(work.getMd5())) {
                    return FileAction.VOID;
                } else {
                    return FileAction.CONFLICTED;
                }
            }
            
            // if tmp is equal to the base one, there was not update on the server
            if (copy.getMd5().equals(base.getMd5())) {
                tmpFile.closeTempFile(true);
                return FileAction.VOID;
            }
            // keep tmp file
            tmpFile.closeTempFile(false);

        } catch (IOException e) {
            throw exception("Error while copying files.", e);
        }

        // check if binary
        boolean remoteBT = getRemoteBinaryType(remoteFile, copy);
        if (remoteBT || MimeTypes.isBinary(base.getContentType())) {
            return FileAction.CONFLICTED;
        }

        MetaFile baseFile = getBaseFile(false);
        try (Reader r0 = baseFile.getReader();
            Reader r1 = tmpFile.getReader()) {
            // do a 3-way diff between the base, the local and the remote one.
            // we currently do not use document sources, since we don't really have
            // a label to provide (like rev. num, etc).
            
            Document baseDoc = new Document(null, LineElementsFactory.create(new MetaFileDocSource(baseFile), r0, false));
            Document leftDoc = new Document(null, LineElementsFactory.create(new FileDocumentSource(file), false, Constants.ENCODING));
            Document rightDoc = new Document(null, LineElementsFactory.create(new MetaFileDocSource(tmpFile), r1, false));

            DocumentDiff3 diff = baseDoc.diff3(leftDoc, rightDoc);

            if (diff.hasConflicts()) {
                return FileAction.CONFLICTED;
            } else {
                return FileAction.MERGED;
            }
        } catch (IOException e) {
            throw exception("Error during merge operation.", e);
        }
    }

    private void doRevert() throws VltException {
        if (entry.isDirectory()) {
            file.mkdir();
        } else {
            try {
                getBaseFile(false).copyTo(getFile(), true);
            } catch (IOException e) {
                throw exception("Error while copying files.", e);
            }
        }
        VltEntryInfo base = entry.base();
        entry.put(base.copyAs(VltEntryInfo.Type.WORK));
    }

    private boolean equalsToRemote(VaultFile remoteFile)
            throws VltException {
        MetaFile tmpFile = getTmpFile();

        // copy file
        byte[] lineFeed = MimeTypes.isBinary(remoteFile.getContentType())
                ? null
                : LineOutputStream.LS_NATIVE;
        VaultFileCopy copy;
        File temp = null;
        try {
            temp = tmpFile.openTempFile();
            copy = VaultFileCopy.copy(remoteFile, temp, lineFeed);
        } catch (IOException e) {
            throw exception("Error while copying files.", e);
        } finally {
            if (tmpFile != null) {
                try {
                    tmpFile.closeTempFile(true);
                } catch (IOException e) {
                    // ignore
                }
            }
        }
        // if md5 is equal, no update
        VltEntryInfo base = entry.base();
        return copy.getMd5().equals(base.getMd5());
    }

    private FileAction doUpdate(VaultFile remoteFile, boolean baseOnly)
            throws VltException {
        FileAction action;
        VltEntryInfo base;
        if (entry == null || entry.base() == null) {
            // new entry
            action = FileAction.ADDED;
            entry = parent.getEntries().update(getName(), remoteFile.getAggregatePath(), remoteFile.getRepoRelPath());
            base = entry.create(VltEntryInfo.Type.BASE);
            entry.put(base);
        } else {
            action = FileAction.UPDATED;
            base = entry.base();

            // quick check if modified
            if (!base.checkModified(remoteFile)) {
                return FileAction.VOID;
            }
        }
        long lastMod = remoteFile.lastModified();
        if (lastMod == 0) {
            lastMod = System.currentTimeMillis();
        }
        base.setDate(lastMod);

        if (remoteFile.isDirectory()) {
            if (!baseOnly) {
                // ensure controlled
                // todo: this does not belong here
                if (entry.work() != null) {
                    action = FileAction.VOID;
                } else {
                    entry.put(base.copyAs(VltEntryInfo.Type.WORK));
                }
                file.mkdir();
                file.setLastModified(base.getDate());
                VltDirectory dir = new VltDirectory(parent.getContext(), file);
                if (!dir.isControlled()) {
                    dir.control(remoteFile.getPath(), remoteFile.getControllingAggregate().getPath());
                    action = FileAction.ADDED;
                }
            }
        } else {
            MetaFile baseFile = getBaseFile(true);

            // copy file
            byte[] lineFeed = MimeTypes.isBinary(remoteFile.getContentType())
                    ? null
                    : LineOutputStream.LS_NATIVE;
            VaultFileCopy copy;
            try {
                File temp = baseFile.openTempFile();
                copy = VaultFileCopy.copy(remoteFile, temp, lineFeed);
                baseFile.closeTempFile(false);
            } catch (IOException e) {
                throw exception("Error while copying files.", e);
            }
            // if md5 is equal, no update
            if (copy.getMd5().equals(base.getMd5())) {
                action = FileAction.VOID;
            }

            if (action == FileAction.VOID
                    && (base.getContentType() != null || remoteFile.getContentType() != null)
                    && (base.getContentType() == null || !base.getContentType().equals(remoteFile.getContentType()))) {
                action = FileAction.UPDATED;
            }

            // update infos
            VltEntryInfo work = entry.work();
            base.setContentType(remoteFile.getContentType());
            base.setSize(copy.getLength());
            base.setMd5(copy.getMd5());
            if (!baseOnly) {
                // only copy if not equal
                if (work == null || !work.getMd5().equals(copy.getMd5()) || !getFile().exists()) {
                    try {
                        baseFile.copyTo(getFile(), true);
                        entry.put(base.copyAs(VltEntryInfo.Type.WORK));
                    } catch (IOException e) {
                        throw exception("Error while copying files.", e);
                    }
                }
            }
        }
        return action;
    }

    private VltException exception(String msg, Throwable cause) {
        return parent.getContext().exception(getPath(), msg, cause);
    }

    private VltException error(String msg) {
        return parent.getContext().error(getPath(), msg);
    }

    //-----------------------------------------------------< DocumentSource >---

    public String getLabel() {
        return getName();
    }

    public String getLocation() {
        File cwd = parent.getContext().getCwd();
        return PathUtil.getRelativeFilePath(cwd.getPath(), file.getPath());
    }
}