/*
 * ====================================================================
 * Copyright (c) 2004-2012 TMate Software Ltd.  All rights reserved.
 *
 * This software is licensed as described in the file COPYING, which
 * you should have received as part of this distribution.  The terms
 * are also available at http://svnkit.com/license.html
 * If newer versions of this license are posted there, you may use a
 * newer version instead, at your option.
 * ====================================================================
 */
package org.tmatesoft.svn.core.internal.wc.admin;

import java.io.File;
import java.util.Iterator;

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.SVNProperty;
import org.tmatesoft.svn.core.SVNURL;
import org.tmatesoft.svn.core.internal.util.SVNEncodingUtil;
import org.tmatesoft.svn.core.internal.util.SVNPathUtil;
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.io.ISVNReporter;
import org.tmatesoft.svn.core.io.ISVNReporterBaton;
import org.tmatesoft.svn.core.wc.SVNEventAction;
import org.tmatesoft.svn.core.wc.SVNRevision;
import org.tmatesoft.svn.util.ISVNDebugLog;
import org.tmatesoft.svn.util.SVNLogType;


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

    private SVNAdminAreaInfo myInfo;
    private SVNDepth myDepth;
    private boolean myIsRestore;
    private boolean myUseDepthCompatibilityTrick;
    private boolean myIsStatus;
    private boolean myIsHonorDepthExclude;
    private File myTarget;
    private ISVNDebugLog myLog;
    private boolean myIsLockOnDemand;
    private long myTotalFilesCount;
    private long myReportedFilesCount;

    public SVNReporter(SVNAdminAreaInfo info, File file, boolean restoreFiles, 
            boolean useDepthCompatibilityTrick, SVNDepth depth, boolean lockOnDemand, 
            boolean isStatus, boolean isHonorDepthExclude, ISVNDebugLog log) {
        myInfo = info;
        myDepth = depth;
        myIsRestore = restoreFiles;
        myIsStatus = isStatus;
        myUseDepthCompatibilityTrick = useDepthCompatibilityTrick;
        myLog = log;
        myTarget = file;
        myIsLockOnDemand = lockOnDemand;
        myIsHonorDepthExclude = isHonorDepthExclude;
        myTotalFilesCount = 0;
        myReportedFilesCount = 0;
    }

    public void report(ISVNReporter reporter) throws SVNException {
        try {
            SVNAdminArea targetArea = myInfo.getTarget();
            SVNWCAccess wcAccess = myInfo.getWCAccess();
            SVNEntry targetEntry = wcAccess.getEntry(myTarget, false);
            if (targetEntry == null || (targetEntry.isDirectory() && targetEntry.isScheduledForAddition())) {
                SVNEntry parentEntry = wcAccess.getVersionedEntry(myTarget.getParentFile(), false);
                long revision = parentEntry.getRevision();
                if (myDepth == SVNDepth.UNKNOWN) {
                    myDepth = SVNDepth.INFINITY;
                }
                reporter.setPath("", null, revision, myDepth, targetEntry == null || targetEntry.isIncomplete());
                if (targetEntry == null || targetEntry.isIncomplete()) {
                    myInfo.addIncompleteEntry("");
                }
                reporter.deletePath("");
                reporter.finishReport();
                myReportedFilesCount++;
                myTotalFilesCount++;
                return;
            }
            
            SVNEntry parentEntry = null;
            boolean startEmpty = targetEntry.isIncomplete();
            if (myUseDepthCompatibilityTrick && targetEntry.getDepth().compareTo(SVNDepth.IMMEDIATES) <= 0 &&
                    myDepth.compareTo(targetEntry.getDepth()) > 0) {
                startEmpty = true;
            }
            long revision = targetEntry.getRevision();
            if (!SVNRevision.isValidRevisionNumber(revision)) {
                parentEntry = wcAccess.getVersionedEntry(myTarget.getParentFile(), false);
                revision = parentEntry.getRevision();
            }
            reporter.setPath("", null, revision, targetEntry.getDepth(), startEmpty);
            if (startEmpty) {
                myInfo.addIncompleteEntry("");
            }
            boolean missing = false; 
            if (!targetEntry.isScheduledForDeletion()) {
                SVNFileType fileType = SVNFileType.getType(myTarget);
                missing = fileType == SVNFileType.NONE;
            }
            myTotalFilesCount = 1;
            myReportedFilesCount = 1;            
            if (targetEntry.isDirectory()) {
                if (!myIsStatus && !missing && !targetEntry.isThisDir()) {
                    missing = true;
                }
                
                if (missing) {
                    reporter.deletePath("");
                } else if (myDepth != SVNDepth.EMPTY) {
                    reportEntries(reporter, targetArea, "", revision, startEmpty, myDepth);
                }
            } else if (targetEntry.isFile()) {
                if (missing) {
                    restoreFile(targetArea, targetEntry.getName());
                }
                // report either linked path or entry path
                parentEntry = parentEntry == null ? wcAccess.getEntry(myTarget.getParentFile(), false) : parentEntry;
                String url = targetEntry.getURL();
                String parentURL = parentEntry.getURL();
                String expectedURL = SVNPathUtil.append(parentURL, SVNEncodingUtil.uriEncode(targetEntry.getName()));
                if (parentEntry != null && !expectedURL.equals(url)) {
                    SVNURL svnURL = SVNURL.parseURIEncoded(url);
                    reporter.linkPath(svnURL, "", targetEntry.getLockToken(), targetEntry.getRevision(), targetEntry.getDepth(), false);
                } else if (targetEntry.getRevision() != revision || targetEntry.getLockToken() != null) {
                    reporter.setPath("", targetEntry.getLockToken(), targetEntry.getRevision(), targetEntry.getDepth(), false);
                }
            }
            reporter.finishReport();
        } catch (SVNException e) {
            try {
                reporter.abortReport();
            } catch (SVNException inner) {
                myLog.logFine(SVNLogType.WC, inner);
            }
            throw e;
        } catch (Throwable th) {
            myLog.logFine(SVNLogType.WC, th);
            try {
                reporter.abortReport();
            } catch (SVNException e) {
                myLog.logFine(SVNLogType.WC, e);
            }
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.UNKNOWN, "WC report failed: {0}", th.getMessage());
            SVNErrorManager.error(err, th, SVNLogType.WC);
        }
    }

    private void reportEntries(ISVNReporter reporter, SVNAdminArea adminArea, String dirPath, long dirRevision, 
            boolean reportAll, SVNDepth depth) throws SVNException {
        SVNWCAccess wcAccess = myInfo.getWCAccess();
        String externalsProperty = adminArea.getProperties(adminArea.getThisDirName()).getStringPropertyValue(SVNProperty.EXTERNALS);
        SVNEntry thisEntry = adminArea.getEntry(adminArea.getThisDirName(), true);
        if (externalsProperty != null) {
            // use owners path as a key.
            String areaPath = adminArea.getRelativePath(myInfo.getAnchor());
            myInfo.addExternal(areaPath, externalsProperty, externalsProperty);
            myInfo.addDepth(areaPath, thisEntry.getDepth());
        }

        String parentURL = thisEntry.getURL();
        for (Iterator e = adminArea.entries(true); e.hasNext();) {
            SVNEntry entry = (SVNEntry) e.next();
            if (adminArea.getThisDirName().equals(entry.getName())) {
                continue;
            }
            myTotalFilesCount++;
            String path = "".equals(dirPath) ? entry.getName() : SVNPathUtil.append(dirPath, entry.getName());
            if (entry.isDeleted() || entry.isAbsent()) {
                if (!reportAll) {
                    reporter.deletePath(path);
                    myReportedFilesCount++;
                }
                continue;
            }
            
            if (entry.getDepth() == SVNDepth.EXCLUDE) {
                if (myIsHonorDepthExclude) {
                    reporter.setPath(path, null, dirRevision, SVNDepth.EXCLUDE, false);
                } else {
                    if (!reportAll) {
                        reporter.deletePath(path);
                    }
                }
                continue;
            }
            
            if (entry.isScheduledForAddition()) {
                continue;
            }
            File file = adminArea.getFile(entry.getName());
            SVNFileType fileType = SVNFileType.getType(file);
            boolean missing = fileType == SVNFileType.NONE;
            String expectedURL = SVNPathUtil.append(parentURL, SVNEncodingUtil.uriEncode(entry.getName()));

            if (entry.isFile()) {
                if (missing && !entry.isScheduledForDeletion() && !entry.isScheduledForReplacement()) {
                    adminArea = restoreFile(adminArea, entry.getName());
                }
                String url = entry.getURL();
                if (reportAll) {
                    if (!url.equals(expectedURL)) {
                        SVNURL svnURL = SVNURL.parseURIEncoded(url);
                        reporter.linkPath(svnURL, path, entry.getLockToken(), entry.getRevision(), entry.getDepth(), false);
                    } else {
                        reporter.setPath(path, entry.getLockToken(), entry.getRevision(), entry.getDepth(), false);
                        myReportedFilesCount++;
                    }
                } else if (!entry.isScheduledForAddition() && !entry.isScheduledForReplacement() && !url.equals(expectedURL)) {
                    // link path
                    SVNURL svnURL = SVNURL.parseURIEncoded(url);
                    reporter.linkPath(svnURL, path, entry.getLockToken(), entry.getRevision(), entry.getDepth(), false);
                } else if (entry.getRevision() != dirRevision || 
                           entry.getLockToken() != null || 
                           thisEntry.getDepth() == SVNDepth.EMPTY) {
                    reporter.setPath(path, entry.getLockToken(), entry.getRevision(), 
                                     entry.getDepth(), false);
                    myReportedFilesCount++;
                }
            } else if (entry.isDirectory() && (depth.compareTo(SVNDepth.FILES) > 0 || depth == SVNDepth.UNKNOWN)) {
                if (missing) {
                    if (myIsRestore) {
                        boolean dropDeletedSchedule = myInfo.getAnchor().getWCAccess().getMaxFormatVersion() < SVNAdminArea16.WC_FORMAT; 
                        if (entry.isScheduledForReplacement()  || (dropDeletedSchedule && entry.isScheduledForDeletion())) {
                            // remove dir schedule if it is 'scheduled for deletion' but missing.
                            entry.setSchedule(null);
                            adminArea.saveEntries(false);
                        }
                    }
                    if (!reportAll) {
                        reporter.deletePath(path);
                        myReportedFilesCount++;
                    }
                    continue;
                }
                
                if (wcAccess.isMissing(adminArea.getFile(entry.getName()))) {
                    if (!myIsStatus) {
                        reporter.deletePath(path);
                        myReportedFilesCount++;
                    }
                    continue;
                }
                
                SVNAdminArea childArea = wcAccess.retrieve(adminArea.getFile(entry.getName()));
                SVNEntry childEntry = childArea.getEntry(childArea.getThisDirName(), true);
                String url = childEntry.getURL();
                boolean startEmpty = childEntry.isIncomplete();
                if (myUseDepthCompatibilityTrick && childEntry.getDepth().compareTo(SVNDepth.FILES) <= 0 && 
                        depth.compareTo(childEntry.getDepth()) > 0) {
                    startEmpty = true;
                    myInfo.addIncompleteEntry(path);
                }
                
                if (reportAll) {
                    if (!url.equals(expectedURL)) {
                        SVNURL svnURL = SVNURL.parseURIEncoded(url);
                        reporter.linkPath(svnURL, path, childEntry.getLockToken(), childEntry.getRevision(), 
                                childEntry.getDepth(), startEmpty);
                    } else {
                        reporter.setPath(path, childEntry.getLockToken(), childEntry.getRevision(), 
                                childEntry.getDepth(), startEmpty);
                        myReportedFilesCount++;
                    }
                } else if (!url.equals(expectedURL)) {
                    SVNURL svnURL = SVNURL.parseURIEncoded(url);
                    reporter.linkPath(svnURL, path, childEntry.getLockToken(), childEntry.getRevision(), 
                            childEntry.getDepth(), startEmpty);
                } else if (childEntry.getLockToken() != null || 
                           childEntry.getRevision() != dirRevision ||
                           childEntry.isIncomplete() ||
                           thisEntry.getDepth() == SVNDepth.EMPTY ||
                           thisEntry.getDepth() == SVNDepth.FILES ||
                           (thisEntry.getDepth() == SVNDepth.IMMEDIATES && 
                            childEntry.getDepth() != SVNDepth.EMPTY) ||
                            (SVNDepth.INFINITY.compareTo(childEntry.getDepth()) > 0 && depth == SVNDepth.INFINITY)) {
                    reporter.setPath(path, childEntry.getLockToken(), childEntry.getRevision(), 
                            childEntry.getDepth(), startEmpty);
                    myReportedFilesCount++;
                }

                if (depth == SVNDepth.INFINITY || depth == SVNDepth.UNKNOWN) {
                    reportEntries(reporter, childArea, path, childEntry.getRevision(), startEmpty, depth);
                }
            }
        }
    }
    
    private SVNAdminArea restoreFile(SVNAdminArea adminArea, String name) throws SVNException {
        if (!myIsRestore) {
            return adminArea;
        }
        if (myIsLockOnDemand && !adminArea.isLocked()) {
            adminArea.lock(false);
            adminArea = myInfo.getWCAccess().upgrade(adminArea.getRoot());
        }
        adminArea.restoreFile(name);
            
        SVNEntry entry = adminArea.getEntry(name, true);
        myInfo.getWCAccess().handleEvent(SVNEventFactory.createSVNEvent(adminArea.getFile(entry.getName()), entry.getKind(), null, entry.getRevision(), SVNEventAction.RESTORE, null, null, null));
        return adminArea;
    }

    public long getReportedFilesCount() {
        return myReportedFilesCount;
    }

    public long getTotalFilesCount() {
        return myTotalFilesCount;
    }
}
