package org.tmatesoft.svn.core.internal.wc2;

import java.io.File;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import org.tmatesoft.svn.core.SVNErrorCode;
import org.tmatesoft.svn.core.SVNErrorMessage;
import org.tmatesoft.svn.core.SVNException;
import org.tmatesoft.svn.core.SVNMergeInfo;
import org.tmatesoft.svn.core.SVNMergeInfoInheritance;
import org.tmatesoft.svn.core.SVNMergeRange;
import org.tmatesoft.svn.core.SVNMergeRangeList;
import org.tmatesoft.svn.core.SVNURL;
import org.tmatesoft.svn.core.internal.wc.SVNErrorManager;
import org.tmatesoft.svn.core.internal.wc.SVNFileUtil;
import org.tmatesoft.svn.core.internal.wc.SVNMergeDriver;
import org.tmatesoft.svn.core.internal.wc17.SVNWCContext;
import org.tmatesoft.svn.core.internal.wc17.db.Structure;
import org.tmatesoft.svn.core.internal.wc17.db.StructureFields;
import org.tmatesoft.svn.core.internal.wc2.ng.SvnNgRepositoryAccess;
import org.tmatesoft.svn.core.internal.wc2.old.SvnOldRepositoryAccess;
import org.tmatesoft.svn.core.io.ISVNLocationSegmentHandler;
import org.tmatesoft.svn.core.io.SVNLocationEntry;
import org.tmatesoft.svn.core.io.SVNLocationSegment;
import org.tmatesoft.svn.core.io.SVNRepository;
import org.tmatesoft.svn.core.io.SVNRepositoryFactory;
import org.tmatesoft.svn.core.wc.SVNRevision;
import org.tmatesoft.svn.core.wc2.ISvnOperationOptionsProvider;
import org.tmatesoft.svn.core.wc2.SvnCopySource;
import org.tmatesoft.svn.core.wc2.SvnTarget;
import org.tmatesoft.svn.util.SVNLogType;

public abstract class SvnRepositoryAccess {
    
    private SVNWCContext context;
    private ISvnOperationOptionsProvider operationOptionsProvider;

    protected SvnRepositoryAccess(ISvnOperationOptionsProvider operationOptionsProvider, SVNWCContext context) throws SVNException {
        this.operationOptionsProvider = operationOptionsProvider;
        this.context = context;
    }
    
    protected ISvnOperationOptionsProvider getOperationOptionsProvider() {
        return this.operationOptionsProvider;
    }
    
    protected SVNWCContext getWCContext() {
        return this.context;
    }
    
    public enum RepositoryInfo {
        repository, revision, url;
    }
    
    public enum LocationsInfo {
        startUrl, startRevision, endUrl, endRevision;
    }
    
    public enum RevisionsPair {
        revNumber, youngestRevision;
    }
    
    public abstract SvnCopySource createRemoteCopySource(SVNWCContext context, SvnCopySource localCopySource) throws SVNException;
    
    public abstract Structure<RepositoryInfo> createRepositoryFor(SvnTarget target, SVNRevision revision, SVNRevision pegRevision, File baseDirectory) throws SVNException;    
   
    public abstract Structure<RevisionsPair> getRevisionNumber(SVNRepository repository, SvnTarget path, SVNRevision revision, Structure<RevisionsPair> youngestRevision) throws SVNException;
    
    public enum UrlInfo {
        url, pegRevision, dropRepsitory; 
    }
    
    public abstract Structure<UrlInfo> getURLFromPath(SvnTarget path, SVNRevision revision, SVNRepository repository) throws SVNException;

    
    protected SVNRevision[] resolveRevisions(SVNRevision pegRevision, SVNRevision revision, boolean isURL, boolean noticeLocalModifications) {
        if (!pegRevision.isValid()) {
            if (isURL) {
                pegRevision = SVNRevision.HEAD;
            } else {
                if (noticeLocalModifications) {
                    pegRevision = SVNRevision.WORKING;
                } else {
                    pegRevision = SVNRevision.BASE;
                }
            }
        }
        if (!revision.isValid()) {
            revision = pegRevision;
        }
        return new SVNRevision[] {
                pegRevision, revision
        };
    }

    public SVNRepository createRepository(SVNURL url, String expectedUuid, boolean mayReuse) throws SVNException {
        SVNRepository repository = null;
        if (getOperationOptionsProvider().getRepositoryPool() == null) {
            repository = SVNRepositoryFactory.create(url, null);
            repository.setAuthenticationManager(getOperationOptionsProvider().getAuthenticationManager());
        } else {
            repository = getOperationOptionsProvider().getRepositoryPool().createRepository(url, mayReuse);
        }
        if (expectedUuid != null) {
            String reposUUID = repository.getRepositoryUUID(true);
            if (!expectedUuid.equals(reposUUID)) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.RA_UUID_MISMATCH, "Repository UUID ''{0}'' doesn''t match expected UUID ''{1}''", new Object[] {
                        reposUUID, expectedUuid
                });
                SVNErrorManager.error(err, SVNLogType.WC);
            }
        }
        repository.setCanceller(getOperationOptionsProvider().getCanceller());
        return repository;
    }

    public Structure<LocationsInfo> getLocations(SVNRepository repository, SvnTarget path, SVNRevision revision, SVNRevision start, SVNRevision end) throws SVNException {
        if (!revision.isValid() || !start.isValid()) {
            SVNErrorManager.error(SVNErrorMessage.create(SVNErrorCode.CLIENT_BAD_REVISION), SVNLogType.DEFAULT);
        }
        
        long pegRevisionNumber = -1;
        long startRevisionNumber;
        long endRevisionNumber;
        SVNURL url = null;
        
        if (path.isFile()) {
            if (revision == SVNRevision.WORKING && getWCContext() != null) {
                Structure<StructureFields.NodeOriginInfo> nodeOrigin = getWCContext().getNodeOrigin(path.getFile(), false, StructureFields.NodeOriginInfo.isCopy, StructureFields.NodeOriginInfo.revision, StructureFields.NodeOriginInfo.reposRelpath, StructureFields.NodeOriginInfo.reposRootUrl);
                boolean isCopy = nodeOrigin.is(StructureFields.NodeOriginInfo.isCopy);
                long pegRevNum = nodeOrigin.lng(StructureFields.NodeOriginInfo.revision);
                File reposRelPath = nodeOrigin.get(StructureFields.NodeOriginInfo.reposRelpath);
                SVNURL reposRootUrl = nodeOrigin.get(StructureFields.NodeOriginInfo.reposRootUrl);

                if (reposRelPath != null) {
                    url = reposRootUrl.appendPath(SVNFileUtil.getFilePath(reposRelPath), false);
                } else {
                    url = null;
                }

                if (url != null && isCopy && repository != null) {
                    SVNURL sessionUrl = repository.getLocation();

                    if (!sessionUrl.equals(url)) {
                        repository = null;
                    }
                }
            } else {
                url = null;
            }

            if (url == null) {
                Structure<UrlInfo> urlInfo = getURLFromPath(path, revision, repository);
                if (urlInfo.hasValue(UrlInfo.dropRepsitory) && urlInfo.is(UrlInfo.dropRepsitory)) {
                    repository = null;
                }
                url = urlInfo.<SVNURL>get(UrlInfo.url);
                if (urlInfo.hasValue(UrlInfo.pegRevision)) {
                    pegRevisionNumber = urlInfo.lng(UrlInfo.pegRevision);
                }
                urlInfo.release();
            }

            if (url == null) {
                SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.ENTRY_MISSING_URL, "''{0}'' has no URL", path.getFile());
                SVNErrorManager.error(errorMessage, SVNLogType.WC);
            }

        } else {
            url = path.getURL();
        }
        
        if (repository == null) {
            repository = createRepository(url, null, true);
        }
    
        Structure<RevisionsPair> pair = null;
        if (pegRevisionNumber < 0) {
            pair = getRevisionNumber(repository, path, revision, pair);
            pegRevisionNumber = pair.lng(RevisionsPair.revNumber);
        }
        pair = getRevisionNumber(repository, path, start, pair);
        startRevisionNumber = pair.lng(RevisionsPair.revNumber);
        if (end == SVNRevision.UNDEFINED) {
            endRevisionNumber = startRevisionNumber;
        } else {
            pair = getRevisionNumber(repository, path, end, pair);
            endRevisionNumber = pair.lng(RevisionsPair.revNumber);
        }
        pair.release();
        
        Structure<LocationsInfo> result = Structure.obtain(LocationsInfo.class);
        result.set(LocationsInfo.startRevision, startRevisionNumber);
        if (end != SVNRevision.UNDEFINED) {
            result.set(LocationsInfo.startRevision, endRevisionNumber);
        }
        if (startRevisionNumber == pegRevisionNumber && (endRevisionNumber == pegRevisionNumber || !SVNRevision.isValidRevisionNumber(endRevisionNumber))) {
            result.set(LocationsInfo.startUrl, url);
            result.set(LocationsInfo.endUrl, url);
            return result;
        }

        SVNURL repositoryRootURL = repository.getRepositoryRoot(true);
        long[] revisionsRange = startRevisionNumber == endRevisionNumber ? 
                new long[] {startRevisionNumber} : new long[] {startRevisionNumber, endRevisionNumber};
                
        Map<?,?> locations = repository.getLocations("", (Map<?,?>) null, pegRevisionNumber, revisionsRange);
        
        SVNLocationEntry startPath = (SVNLocationEntry) locations.get(new Long(startRevisionNumber));
        SVNLocationEntry endPath = (SVNLocationEntry) locations.get(new Long(endRevisionNumber));
        if (startPath == null) {
            Object source = path != null ? (Object) path : (Object) url;
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.CLIENT_UNRELATED_RESOURCES, "Unable to find repository location for ''{0}'' in revision ''{1}''", new Object[] {
                    source, new Long(startRevisionNumber)
            });
            SVNErrorManager.error(err, SVNLogType.WC);
        }
        if (endPath == null) {
            Object source = path != null ? (Object) path : (Object) url;
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.CLIENT_UNRELATED_RESOURCES, "The location for ''{0}'' for revision {1} does not exist in the "
                    + "repository or refers to an unrelated object", new Object[] {
                    source, new Long(endRevisionNumber)
            });
            SVNErrorManager.error(err, SVNLogType.WC);
        }
        
        result.set(LocationsInfo.startUrl, repositoryRootURL.appendPath(startPath.getPath(), false));
        if (end.isValid()) {
            result.set(LocationsInfo.endUrl, repositoryRootURL.appendPath(endPath.getPath(), false));
        }
        return result;
    }

    public Map<String, SVNMergeRangeList> getReposMergeInfo(SVNRepository repository, String path, long revision, SVNMergeInfoInheritance inheritance, boolean squelchIncapable) throws SVNException {
        Map<String, SVNMergeInfo> reposMergeInfo = null;
        try {
            reposMergeInfo = repository.getMergeInfo(new String[] {path}, revision, inheritance, false);
        } catch (SVNException svne) {
            if (!squelchIncapable || svne.getErrorMessage().getErrorCode() != SVNErrorCode.UNSUPPORTED_FEATURE) {
                throw svne;
            }
        }
        String rootRelativePath = getPathRelativeToRoot(repository.getLocation(), repository.getRepositoryRoot(false), repository);
        Map<String, SVNMergeRangeList> targetMergeInfo = null;
        if (reposMergeInfo != null) {
            SVNMergeInfo mergeInfo = (SVNMergeInfo) reposMergeInfo.get(rootRelativePath);
            if (mergeInfo != null) {
                targetMergeInfo = mergeInfo.getMergeSourcesToMergeLists();
            }
        }
        return targetMergeInfo;
    }
    
    protected String getPathRelativeToRoot(SVNURL url, SVNURL reposRootURL, SVNRepository repos) throws SVNException {
        if (reposRootURL == null) {
            reposRootURL = repos.getRepositoryRoot(true);
        }
        String reposRootPath = reposRootURL.getPath();
        String absPath = url.getPath();
        if (!absPath.startsWith(reposRootPath)) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.CLIENT_UNRELATED_RESOURCES, "URL ''{0}'' is not a child of repository root URL ''{1}''", new Object[] {
                    url, reposRootURL
            });
            SVNErrorManager.error(err, SVNLogType.WC);
        }
        absPath = absPath.substring(reposRootPath.length());
        if (!absPath.startsWith("/")) {
            absPath = "/" + absPath;
        }
        return absPath;
    }

    public String getPathRelativeToSession(SVNURL url, SVNURL sessionURL, SVNRepository repos) {
        if (sessionURL == null) {
            sessionURL = repos.getLocation();
        }
        String reposPath = sessionURL.getPath();
        String absPath = url.getPath();
        if (!absPath.startsWith(reposPath + "/") && !absPath.equals(reposPath)) {
            return null;
        }
        absPath = absPath.substring(reposPath.length());
        if (absPath.startsWith("/")) {
            absPath = absPath.substring(1);
        }
        return absPath;
    }
    
    public SVNLocationSegment getYoungestCommonAncestor(SVNURL url1, long rev1, SVNURL url2, long rev2) throws SVNException {
        boolean[] hasZero1 = new boolean[1];
        boolean[] hasZero2 = new boolean[1];
        Map<String, SVNMergeRangeList> history1 = getHistoryAsMergeInfo(url1, SVNRevision.create(rev1), -1, -1, hasZero1, null);
        Map<String, SVNMergeRangeList> history2 = getHistoryAsMergeInfo(url2, SVNRevision.create(rev2), -1, -1, hasZero2, null);
        long ycRevision = -1;
        String ycPath = null;
        for (Iterator<String> paths = history1.keySet().iterator(); paths.hasNext();) {
            String path = paths.next();
            SVNMergeRangeList ranges1 = history1.get(path);
            SVNMergeRangeList ranges2 = history2.get(path);
            if (ranges2 != null) {
                SVNMergeRangeList intersection = ranges1.intersect(ranges2, true);
                if (intersection != null && !intersection.isEmpty()) {
                    SVNMergeRange ycRange = intersection.getRanges()[intersection.getSize() - 1];
                    if (ycRevision < 0 || ycRange.getEndRevision() > ycRevision) {
                        ycRevision = ycRange.getEndRevision();
                        ycPath = path.substring(1);
                    }
                }
            }
        }
        if (ycPath == null && hasZero1[0] && hasZero2[0]) {
            ycPath = "/";
            ycRevision = 0;
        }
        return new SVNLocationSegment(ycRevision, ycRevision, ycPath);
    }
    
    private Map<String, SVNMergeRangeList> getHistoryAsMergeInfo(SVNURL url, SVNRevision pegRevision, long rangeYoungest, long rangeOldest, boolean[] hasZero, SVNRepository repos) throws SVNException {
        long[] pegRevNum = new long[1];
        Structure<RevisionsPair> pair = getRevisionNumber(repos, SvnTarget.fromURL(url), pegRevision, null);
        pegRevNum[0] = pair.lng(RevisionsPair.revNumber);
        pair.release();
        
        boolean closeSession = false;
        try {
            if (repos == null) {
                repos = createRepository(url, null, false);
                closeSession = true;
            }
            if (!SVNRevision.isValidRevisionNumber(rangeYoungest)) {
                rangeYoungest = pegRevNum[0];
            }
            if (!SVNRevision.isValidRevisionNumber(rangeOldest)) {
                rangeOldest = 0;
            }
            
            List<SVNLocationSegment> segments = repos.getLocationSegments("", pegRevNum[0], rangeYoungest, rangeOldest);
            if (!segments.isEmpty() && hasZero != null && hasZero.length > 0) {
                SVNLocationSegment oldest = segments.get(0);
                hasZero[0] = oldest.getStartRevision() == 0;
            }
            return getMergeInfoFromSegments(segments);
        } finally {
            if (closeSession) {
                repos.closeSession();
            }
        }
    }

    public static Map<String, SVNMergeRangeList> getMergeInfoFromSegments(Collection<SVNLocationSegment> segments) {
        Map<String, Collection<SVNMergeRange>> mergeInfo = new TreeMap<String, Collection<SVNMergeRange>>();
        for (Iterator<SVNLocationSegment> segmentsIter = segments.iterator(); segmentsIter.hasNext();) {
            SVNLocationSegment segment = (SVNLocationSegment) segmentsIter.next();
            if (segment.getPath() == null) {
                continue;
            }
            String sourcePath = segment.getPath();
            Collection<SVNMergeRange> pathRanges = mergeInfo.get(sourcePath);
            if (pathRanges == null) {
                pathRanges = new LinkedList<SVNMergeRange>();
                mergeInfo.put(sourcePath, pathRanges);
            }
            SVNMergeRange range = new SVNMergeRange(Math.max(segment.getStartRevision() - 1, 0), 
                    segment.getEndRevision(), true);
            pathRanges.add(range);
        }
        Map<String, SVNMergeRangeList> result = new TreeMap<String, SVNMergeRangeList>();
        for (Iterator<String> paths = mergeInfo.keySet().iterator(); paths.hasNext();) {
            String path = (String) paths.next();
            Collection<SVNMergeRange> pathRanges = mergeInfo.get(path);
            result.put(path, SVNMergeRangeList.fromCollection(pathRanges));
        }
        return result;
    }

    public SVNLocationEntry getCopySource(SvnTarget target, SVNRevision revision) throws SVNException {
        Structure<RepositoryInfo> repositoryInfo = createRepositoryFor(target, revision, revision, null);
        SVNRepository repository = repositoryInfo.get(RepositoryInfo.repository);
        long atRev = repositoryInfo.lng(RepositoryInfo.revision);
        repositoryInfo.release();
        
        final Object[] copyFrom = new Object[3];
        try {
            repository.getLocationSegments("", atRev, atRev, -1, new ISVNLocationSegmentHandler() {
                public void handleLocationSegment(SVNLocationSegment locationSegment) throws SVNException {
                    // skip first.
                    if (copyFrom[0] == null) {
                        copyFrom[0] = Boolean.TRUE;
                    } else if (copyFrom[1] != null) {
                        return;
                    } else if (locationSegment.getPath() != null) {
                        copyFrom[1] = locationSegment.getPath();
                        copyFrom[2] = locationSegment.getEndRevision();
                    }
                }
            });
        } catch (SVNException e) {
            if (e.getErrorMessage().getErrorCode() == SVNErrorCode.FS_NOT_FOUND ||
                    e.getErrorMessage().getErrorCode() == SVNErrorCode.RA_DAV_REQUEST_FAILED) {
                return new SVNLocationEntry(-1, null);
            } else {
                throw e;
            }
        }
        if (copyFrom[1] != null) {
            return new SVNLocationEntry((Long) copyFrom[2], (String) copyFrom[1]);
        }
        return null;
    }

    public Map<String, SVNMergeRangeList> getHistoryAsMergeInfo(SVNRepository repos, SvnTarget target, long youngest, long oldest) throws SVNException {
        long pegRevnum = -1;
        if (repos == null) {
            Structure<RepositoryInfo> reposInfo = createRepositoryFor(target, SVNRevision.UNDEFINED, target.getResolvedPegRevision(), null);
            pegRevnum = reposInfo.lng(RepositoryInfo.revision);
            repos = reposInfo.get(RepositoryInfo.repository);
            reposInfo.release();
        } else {
            if (target.getPegRevision() == SVNRevision.HEAD || target.getPegRevision() == SVNRevision.UNDEFINED) {
                pegRevnum = repos.getLatestRevision();
            } else if (target.getPegRevision().getNumber() >= 0) {
                pegRevnum = target.getPegRevision().getNumber();
            } else if (target.getPegRevision().getDate() != null) {
                pegRevnum = repos.getDatedRevision(target.getPegRevision().getDate());
            } else {
                if (target.isURL()) {
                    SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.CLIENT_VERSIONED_PATH_REQUIRED);
                    SVNErrorManager.error(err, SVNLogType.WC);
                }
                Structure<RevisionsPair> pair = getRevisionNumber(repos, target, target.getPegRevision(), null);
                pegRevnum = pair.lng(RevisionsPair.revNumber);
                pair.release();
            }
        }
        
        if (youngest < 0) {
            youngest = pegRevnum;
        }
        if (oldest < 0) {
            oldest = 0;
        }
        List<SVNLocationSegment> segments = repos.getLocationSegments("", pegRevnum, youngest, oldest);
        return SVNMergeDriver.getMergeInfoFromSegments(segments);
    }
}
