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

import java.io.File;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

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.SVNURL;
import org.tmatesoft.svn.core.internal.util.SVNSkel;
import org.tmatesoft.svn.core.internal.wc.SVNEventFactory;
import org.tmatesoft.svn.core.internal.wc.SVNFileListUtil;
import org.tmatesoft.svn.core.internal.wc.SVNFileType;
import org.tmatesoft.svn.core.internal.wc.SVNFileUtil;
import org.tmatesoft.svn.core.internal.wc17.db.ISVNWCDb.SVNWCDbKind;
import org.tmatesoft.svn.core.internal.wc17.db.ISVNWCDb.SVNWCDbLock;
import org.tmatesoft.svn.core.internal.wc17.db.ISVNWCDb.SVNWCDbStatus;
import org.tmatesoft.svn.core.internal.wc17.db.ISVNWCDb.WCDbBaseInfo;
import org.tmatesoft.svn.core.internal.wc17.db.ISVNWCDb.WCDbBaseInfo.BaseInfoField;
import org.tmatesoft.svn.core.internal.wc17.db.ISVNWCDb.WCDbInfo;
import org.tmatesoft.svn.core.internal.wc17.db.ISVNWCDb.WCDbInfo.InfoField;
import org.tmatesoft.svn.core.internal.wc17.db.ISVNWCDb.WCDbRepositoryInfo;
import org.tmatesoft.svn.core.internal.wc17.db.ISVNWCDb.WCDbRepositoryInfo.RepositoryInfoField;
import org.tmatesoft.svn.core.internal.wc17.db.SVNWCDb;
import org.tmatesoft.svn.core.internal.wc17.db.SVNWCDb.DirParsedInfo;
import org.tmatesoft.svn.core.internal.wc17.db.SVNWCDbRoot;
import org.tmatesoft.svn.core.internal.wc17.db.Structure;
import org.tmatesoft.svn.core.internal.wc17.db.StructureFields.AdditionInfo;
import org.tmatesoft.svn.core.internal.wc17.db.SvnWcDbShared;
import org.tmatesoft.svn.core.io.ISVNReporter;
import org.tmatesoft.svn.core.io.ISVNReporterBaton;
import org.tmatesoft.svn.core.wc.SVNConflictChoice;
import org.tmatesoft.svn.core.wc.SVNEventAction;
import org.tmatesoft.svn.core.wc2.SvnChecksum;
import org.tmatesoft.svn.util.ISVNDebugLog;

/**
 * @version 1.3
 * @author TMate Software Ltd.
 */
public class SVNReporter17 implements ISVNReporterBaton {

    private final File path;
    private SVNWCContext wcContext;
    private SVNDepth depth;
    private final boolean isRestoreFiles;
    private final boolean isUseDepthCompatibilityTrick;
    private final boolean isHonorDepthExclude;
    private boolean isUseCommitTimes;
    private int reportedFilesCount;
    private int totalFilesCount;

    public SVNReporter17(File path, SVNWCContext wcContext, boolean restoreFiles, boolean useDepthCompatibilityTrick, SVNDepth depth, boolean lockOnDemand, boolean isStatus,
            boolean isHonorDepthExclude, boolean isUseCommitTimes, ISVNDebugLog log) {
        this.path = path;
        this.wcContext = wcContext;
        this.isRestoreFiles = restoreFiles;
        this.isUseDepthCompatibilityTrick = useDepthCompatibilityTrick;
        this.depth = depth;
        this.isHonorDepthExclude = isHonorDepthExclude;
        this.isUseCommitTimes = isUseCommitTimes;
    }

    public int getReportedFilesCount() {
        return reportedFilesCount;
    }

    public int getTotalFilesCount() {
        return totalFilesCount;
    }

    public void report(ISVNReporter reporter) throws SVNException {
        assert (SVNWCDb.isAbsolute(path));
        
        SVNWCDbStatus status = null;
        SVNWCDbKind target_kind = null;
        long target_rev = 0;
        File repos_relpath = null;
        SVNURL repos_root = null;
        SVNDepth target_depth = SVNDepth.UNKNOWN;
        SVNWCDbLock target_lock = null;
        boolean start_empty;
        SVNErrorMessage err = null;
        
        try {

            final WCDbBaseInfo baseInfo = wcContext.getDb().getBaseInfo(path, 
                    BaseInfoField.status, BaseInfoField.kind, BaseInfoField.revision, 
                    BaseInfoField.reposRelPath, BaseInfoField.reposRootUrl,
                    BaseInfoField.depth, BaseInfoField.lock);

            status = baseInfo.status;
            target_kind = baseInfo.kind;
            target_rev = baseInfo.revision;
            repos_relpath = baseInfo.reposRelPath;
            repos_root = baseInfo.reposRootUrl;
            target_depth = baseInfo.depth;
            target_lock = baseInfo.lock;

        } catch (SVNException e) {
            if (e.getErrorMessage().getErrorCode() != SVNErrorCode.WC_PATH_NOT_FOUND) {
                throw e;
            }
            err = e.getErrorMessage();
        }
        if (err != null || (status != SVNWCDbStatus.Normal && status != SVNWCDbStatus.Incomplete)) {
            if (depth == SVNDepth.UNKNOWN) {
                depth = SVNDepth.INFINITY;
            }
            reporter.setPath("", null, 0, depth, false);
            reporter.deletePath("");
            reporter.finishReport();
            return;
        }
        if (target_depth == SVNDepth.UNKNOWN) {
            target_depth = SVNDepth.INFINITY;
        }
        start_empty = status == SVNWCDbStatus.Incomplete;
        if (isUseDepthCompatibilityTrick 
                && target_depth.compareTo(SVNDepth.IMMEDIATES) <= 0 
                && depth.compareTo(target_depth) > 0) {
            start_empty = true;
        }
        SVNFileType diskType = SVNFileType.UNKNOWN;
        if (isRestoreFiles) {
            diskType = SVNFileType.getType(path);
        } 
            
        if (isRestoreFiles && diskType == SVNFileType.NONE) {
            // restore node
            WCDbInfo wInfo = null;
            SVNWCDbKind wrkKind;
            SVNWCDbStatus wrkStatus;
            SvnChecksum checksum;
            try {
                wInfo = wcContext.getDb().readInfo(path, InfoField.status, InfoField.kind, InfoField.checksum);
                wrkStatus = wInfo.status;
                wrkKind = wInfo.kind;
                checksum = wInfo.checksum;
            } catch (SVNException e) {
                if (e.getErrorMessage().getErrorCode() == SVNErrorCode.WC_PATH_NOT_FOUND) {
                    wrkStatus = SVNWCDbStatus.NotPresent;
                    wrkKind = SVNWCDbKind.File;
                    checksum = null;
                } else {
                    throw e;
                }
            }
            if ((wrkStatus == SVNWCDbStatus.Normal
                    || wrkStatus == SVNWCDbStatus.Added
                    || wrkStatus == SVNWCDbStatus.Incomplete)
                    && (wrkKind == SVNWCDbKind.Dir || checksum != null)) {
                restoreNode(wcContext, path, wrkKind, target_rev, isUseCommitTimes);
            }
        }
        
        try {
            SVNDepth reportDepth = target_depth;
            if (isHonorDepthExclude && depth != SVNDepth.UNKNOWN && depth.compareTo(target_depth) < 0) {
                reportDepth = depth;
            }
            reporter.setPath("", null, target_rev, reportDepth, start_empty);
            if (target_kind == SVNWCDbKind.Dir) {
                if (depth != SVNDepth.EMPTY) {
                    DirParsedInfo rootInfo = ((SVNWCDb) wcContext.getDb()).obtainWcRoot(path);
                    reportRevisionsAndDepths(rootInfo.wcDbDir.getWCRoot(), path, rootInfo.localRelPath, 
                            SVNFileUtil.createFilePath(""), 
                            target_rev, repos_relpath, repos_root, reportDepth, reporter, isRestoreFiles, depth, start_empty);
                }
            } else if (target_kind == SVNWCDbKind.Symlink || target_kind == SVNWCDbKind.File) {
                String base = SVNFileUtil.getFileName(path);
                File parentPath = SVNFileUtil.getParentFile(path);
                WCDbBaseInfo parentInfo = wcContext.getDb().getBaseInfo(parentPath, BaseInfoField.reposRelPath);
                if (!repos_relpath.equals(SVNFileUtil.createFilePath(parentInfo.reposRelPath, base))) {
                    SVNURL url = SVNWCUtils.join(repos_root, repos_relpath);
                    reporter.linkPath(url, "", target_lock != null ? target_lock.token : null, target_rev, SVNDepth.INFINITY, false);
                } else if (target_lock != null) {
                    reporter.setPath("", target_lock.token, target_rev, SVNDepth.INFINITY, false);
                }
            } 
            reporter.finishReport();
        } catch (SVNException e) {
            // abort report
            try {
                reporter.abortReport();
            } catch (SVNException inner) {
                e.getErrorMessage().setChildErrorMessage(inner.getErrorMessage());
            }
            throw e;
        }
    }

    public static boolean restoreNode(SVNWCContext context, File local_abspath, SVNWCDbKind kind, long target_rev, boolean useCommitTimes) throws SVNException {
        boolean restored = false;

        /*
         * Currently we can only restore files, but we will be able to restore
         * directories after we move to a single database and pristine store.
         */
        if (kind == SVNWCDbKind.File || kind == SVNWCDbKind.Symlink) {
            /* ... recreate file from text-base, and ... */
            restoreFile(context, local_abspath, useCommitTimes, true);
            restored = true;
        } else if (kind == SVNWCDbKind.Dir) {
            /* Recreating a directory is just a mkdir */
            local_abspath.mkdirs();
            restored = true;
        }

        if (restored) {
            /* ... report the restoration to the caller. */
            if (context.getEventHandler() != null) {
                context.getEventHandler().handleEvent(SVNEventFactory.createSVNEvent(local_abspath, SVNNodeKind.FILE, null, target_rev, SVNEventAction.RESTORE, null, null, null), 0);
            }
        }
        return restored;
    }


    private void reportRevisionsAndDepths(SVNWCDbRoot root, File dirPath, File dirLocalRelPath, File reportRelPath, long dirRev, File dirReposRelPath, SVNURL dirReposRoot,  
            SVNDepth dirDepth, ISVNReporter reporter, boolean restoreFiles, SVNDepth depth, boolean reportEverything) throws SVNException {
        
        Map<String, WCDbBaseInfo> baseChildren = wcContext.getDb().getBaseChildrenMap(root, dirLocalRelPath, true);
        Set<String> dirEntries = null;
        if (restoreFiles) {
            dirEntries = new HashSet<String>();
            File[] list = SVNFileListUtil.listFiles(dirPath);
            if (list != null) {
                for (File file : list) {
                    dirEntries.add(SVNFileUtil.getFileName(file));
                }
            } else {
                SVNFileType fileType = SVNFileType.getType(dirPath);
                if (fileType != SVNFileType.DIRECTORY) {
                    restoreFiles = false;
                }
            }
        }
        for (String child : baseChildren.keySet()) {
            boolean thisSwitched = false;
            wcContext.checkCancelled();
            
            String thisReportRelpath = SVNFileUtil.getFilePath(SVNFileUtil.createFilePath(reportRelPath, child));
            File thisAbsPath = SVNFileUtil.createFilePath(dirPath, child);
            
            WCDbBaseInfo ths = baseChildren.get(child); 
            if (ths.updateRoot) {
                continue;
            }
            if (ths.status == SVNWCDbStatus.Excluded) {
                if (isHonorDepthExclude) {
                    reporter.setPath(thisReportRelpath, null, dirRev, SVNDepth.EXCLUDE, false);
                } else {
                    if (!reportEverything) {
                        reporter.deletePath(thisReportRelpath);
                    }
                }
                continue;
            }
            if (ths.status == SVNWCDbStatus.ServerExcluded || ths.status == SVNWCDbStatus.NotPresent) {
                if (!reportEverything) {
                    reporter.deletePath(thisReportRelpath);
                }
                continue;
            }
            if (restoreFiles && !dirEntries.contains(child)) {
                WCDbInfo wInfo = null;
                SVNWCDbKind wrkKind;
                SVNWCDbStatus wrkStatus;
                SvnChecksum checksum;
                try {
                    wInfo = wcContext.getDb().readInfo(thisAbsPath, InfoField.status, InfoField.kind, InfoField.checksum);
                    wrkStatus = wInfo.status;
                    wrkKind = wInfo.kind;
                    checksum = wInfo.checksum;
                } catch (SVNException e) {
                    if (e.getErrorMessage().getErrorCode() == SVNErrorCode.WC_PATH_NOT_FOUND) {
                        wrkStatus = SVNWCDbStatus.NotPresent;
                        wrkKind = SVNWCDbKind.File;
                        checksum = null;
                    } else {
                        throw e;
                    }
                }
                if ((wrkStatus == SVNWCDbStatus.Normal
                        || wrkStatus == SVNWCDbStatus.Added
                        || wrkStatus == SVNWCDbStatus.Incomplete)
                       && (wrkKind == SVNWCDbKind.Dir || checksum != null)) {
                    if (SVNFileType.getType(thisAbsPath) == SVNFileType.NONE) {
                        restoreNode(wcContext, thisAbsPath, wrkKind, dirRev, isUseCommitTimes);
                    }
                }                
            }
            if (ths.reposRelPath == null) {
                ths.reposRelPath = SVNFileUtil.createFilePath(dirReposRelPath, child);
            } else {
                String childName = SVNWCUtils.getPathAsChild(dirReposRelPath, ths.reposRelPath);
                if (childName == null || !child.equals(childName)) {
                    thisSwitched = true;
                }
            }
            if (ths.depth == SVNDepth.UNKNOWN) {
                ths.depth = SVNDepth.INFINITY;
            }
            if (ths.kind == SVNWCDbKind.File || ths.kind == SVNWCDbKind.Symlink) {
                 if (reportEverything) {
                     if (thisSwitched) {
                         SVNURL url = SVNWCUtils.join(dirReposRoot, ths.reposRelPath);
                         reporter.linkPath(url, thisReportRelpath, 
                                 ths.lock != null ? ths.lock.token : null, 
                                         ths.revision, ths.depth, false);
                     } else {
                         reporter.setPath(thisReportRelpath, ths.lock != null ? ths.lock.token : null, ths.revision, ths.depth, false);
                     }
                 } else if (thisSwitched) {
                     SVNURL url = SVNWCUtils.join(dirReposRoot, ths.reposRelPath);
                     reporter.linkPath(url, thisReportRelpath, 
                             ths.lock != null ? ths.lock.token : null, 
                                     ths.revision, ths.depth, false);
                 } else if (ths.revision != dirRev || ths.lock != null || dirDepth == SVNDepth.EMPTY) {
                     reporter.setPath(thisReportRelpath, ths.lock != null ? ths.lock.token : null, ths.revision, ths.depth, false);
                 }
            } else if (ths.kind == SVNWCDbKind.Dir && (depth == SVNDepth.UNKNOWN || depth.compareTo(SVNDepth.FILES) > 0)) {
                boolean isIncomplete = ths.status == SVNWCDbStatus.Incomplete;
                boolean startEmpty = isIncomplete;
                SVNDepth reportDepth = ths.depth;

                if (!depth.isRecursive()) {
                    reportDepth = SVNDepth.EMPTY;
                }

                if (isIncomplete && ths.revision < 0) {
                    ths.revision = dirRev;
                }
                if (isUseDepthCompatibilityTrick 
                        && ths.depth.compareTo(SVNDepth.FILES) <= 0
                        && depth.compareTo(ths.depth) > 0) {
                    startEmpty = true;
                }
                if (reportEverything) {
                    if (thisSwitched) {
                        SVNURL url = SVNWCUtils.join(dirReposRoot, ths.reposRelPath);
                        reporter.linkPath(url, thisReportRelpath, 
                                ths.lock != null ? ths.lock.token : null, 
                                        ths.revision, reportDepth, startEmpty);
                    } else {
                        reporter.setPath(thisReportRelpath, ths.lock != null ? ths.lock.token : null, ths.revision, reportDepth, startEmpty);
                    }
                } else if (thisSwitched) {
                    SVNURL url = SVNWCUtils.join(dirReposRoot, ths.reposRelPath);
                    reporter.linkPath(url, thisReportRelpath, 
                            ths.lock != null ? ths.lock.token : null, 
                                    ths.revision, reportDepth, startEmpty);
                } else if (ths.revision != dirRev
                        || isIncomplete
                        || ths.lock != null 
                        || dirDepth == SVNDepth.EMPTY
                        || dirDepth == SVNDepth.FILES
                        || (dirDepth == SVNDepth.IMMEDIATES && ths.depth != SVNDepth.EMPTY)
                        || (ths.depth.compareTo(SVNDepth.INFINITY) < 0 && depth.isRecursive())) {
                    reporter.setPath(thisReportRelpath, ths.lock != null ? ths.lock.token : null, ths.revision, reportDepth, startEmpty);
                }
                if (depth.isRecursive()) {
                    File reposRelPath = ths.reposRelPath;
                    if (reposRelPath == null) {
                        reposRelPath = SVNFileUtil.createFilePath(dirReposRelPath, child);
                    }
                    reportRevisionsAndDepths(root, thisAbsPath, SVNFileUtil.createFilePath(dirLocalRelPath, child), 
                            SVNFileUtil.createFilePath(thisReportRelpath), 
                            ths.revision, 
                            reposRelPath, 
                            dirReposRoot, ths.depth, reporter, restoreFiles, depth, startEmpty);
                }
            }
        }
    }

    private static void restoreFile(SVNWCContext context, File localAbsPath, boolean useCommitTimes, boolean removeTextConflicts) throws SVNException {
        SVNSkel workItem = context.wqBuildFileInstall(localAbsPath, null, useCommitTimes, true);
        context.getDb().addWorkQueue(localAbsPath, workItem);
        context.wqRun(localAbsPath);
        if (removeTextConflicts) {
            resolveTextConflict(context, localAbsPath);
        }
    }

    private static void resolveTextConflict(SVNWCContext context, File localAbsPath) throws SVNException {
        context.resolveConflictOnNode(localAbsPath, true, false, SVNConflictChoice.MERGED);
    }

}
