/*
 * ====================================================================
 * 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.Collection;
import java.util.Iterator;
import java.util.TreeSet;
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.SVNURL;
import org.tmatesoft.svn.core.internal.wc.SVNClassLoader;
import org.tmatesoft.svn.core.internal.wc.SVNErrorManager;
import org.tmatesoft.svn.core.internal.wc.SVNEventFactory;
import org.tmatesoft.svn.core.internal.wc.SVNFileUtil;
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.util.SVNLogType;


/**
 * @version 1.3
 * @author  TMate Software Ltd.
 */
public abstract class SVNAdminAreaFactory implements Comparable {

    public static final int WC_FORMAT_13 = 4;
    public static final int WC_FORMAT_14 = 8;
    public static final int WC_FORMAT_15 = 9;
    public static final int WC_FORMAT_16 = 10;
    
    private static final Collection<SVNAdminAreaFactory> ourFactories = new TreeSet<SVNAdminAreaFactory>();
    private static boolean ourIsUpgradeEnabled = Boolean.valueOf(System.getProperty("svnkit.upgradeWC", System.getProperty("javasvn.upgradeWC", "true"))).booleanValue();
    private static ISVNAdminAreaFactorySelector ourSelector;
    private static ISVNAdminAreaFactorySelector ourDefaultSelector = new DefaultSelector();

    static {
        SVNAdminAreaFactory.registerFactories();
    }
    
    public static void setUpgradeEnabled(boolean enabled) {
        ourIsUpgradeEnabled = enabled;
    }

    public static boolean isUpgradeEnabled() {
        return ourIsUpgradeEnabled;
    }

    public static void setSelector(ISVNAdminAreaFactorySelector selector) {
        ourSelector = selector;
    }
    
    public static ISVNAdminAreaFactorySelector getSelector() {
        return ourSelector != null ? ourSelector : ourDefaultSelector;
    }
    
	public static int checkWC(File path, boolean useSelector) throws SVNException {
		return checkWC(path, useSelector, Level.FINE);
	}

    public static int checkWC(File path, boolean useSelector, Level logLevel) throws SVNException {
        Collection enabledFactories = ourFactories;
        if (useSelector) {
            enabledFactories = getSelector().getEnabledFactories(path, enabledFactories, false);
        }
        SVNErrorMessage error = null;
        int version = -1;
        for(Iterator factories = enabledFactories.iterator(); factories.hasNext();) {
            SVNAdminAreaFactory factory = (SVNAdminAreaFactory) factories.next();
            try {
                version = factory.doCheckWC(path, logLevel);
                if (version == 0) {
                    return version;
                }
                
                if (version > factory.getSupportedVersion()) {
                    SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_UNSUPPORTED_FORMAT, 
                            "The path ''{0}'' appears to be part of a Subversion 1.7 or greater\n" +
                            "working copy.  Please upgrade your Subversion client to use this\n" +
                            "working copy.", 
                            path);
                    SVNErrorManager.error(err, SVNLogType.WC);
                } else if (version < factory.getSupportedVersion()) {
                    SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_UNSUPPORTED_FORMAT, 
                            "Working copy format of {0} is too old ({1}); please check out your working copy again", 
                            new Object[] {path, version});
                    SVNErrorManager.error(err, SVNLogType.WC);
                } 
            } catch (SVNException e) {
                if (error != null) {
                    error.setChildErrorMessage(e.getErrorMessage());
                } else {
                    error = e.getErrorMessage();
                }
                continue;
            }
            return version;
        }
        if (error == null) {
            error = SVNErrorMessage.create(SVNErrorCode.WC_NOT_DIRECTORY, "''{0}'' is not a working copy", path);
        }
        if (error.getErrorCode() == SVNErrorCode.WC_UNSUPPORTED_FORMAT) {
            error.setChildErrorMessage(null);
        }
        SVNErrorManager.error(error, logLevel, SVNLogType.WC);
        return 0;
    }
    
    public static SVNAdminArea open(File path, Level logLevel) throws SVNException {
        SVNErrorMessage error = null;
        int wcFormatVersion = -1;
        Collection enabledFactories = getSelector().getEnabledFactories(path, ourFactories, false);
        File adminDir = new File(path, SVNFileUtil.getAdminDirectoryName());
        if (adminDir.isDirectory()) {
            for (Iterator factories = enabledFactories.iterator(); factories.hasNext();) {
                SVNAdminAreaFactory factory = (SVNAdminAreaFactory) factories.next();
                try {
                    wcFormatVersion = factory.getVersion(path);
                    if (wcFormatVersion > factory.getSupportedVersion()) {                        
                        SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_UNSUPPORTED_FORMAT, 
                                "The path ''{0}'' appears to be part of a Subversion 1.7 or greater\n" +
                                "working copy.  Please upgrade your Subversion client to use this\n" +
                                "working copy.", 
                                path);
                        SVNErrorManager.error(err, SVNLogType.WC);
                    } else if (wcFormatVersion < factory.getSupportedVersion()) {                        
                        SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_UNSUPPORTED_FORMAT,
                                "Working copy format of {0} is too old ({1}); please check out your working copy again",
                                new Object[]{path, wcFormatVersion});
                        SVNErrorManager.error(err, SVNLogType.WC);
                    }
                } catch (SVNException e) {
                    if (error != null) {
                        error.setChildErrorMessage(e.getErrorMessage());
                    } else {
                        error = e.getErrorMessage();
                    }
                    continue;
                }
    
                SVNAdminArea adminArea = factory.doOpen(path, wcFormatVersion);
                if (adminArea != null) {
                    adminArea.setWorkingCopyFormatVersion(wcFormatVersion);
                    return adminArea;
                }
            }
        }
        if (error == null) {
            error = SVNErrorMessage.create(SVNErrorCode.WC_NOT_DIRECTORY, "''{0}'' is not a working copy", path);
        }
        if (error.getErrorCode() == SVNErrorCode.WC_UNSUPPORTED_FORMAT) {
            error.setChildErrorMessage(null);
        }
        SVNErrorManager.error(error, logLevel, SVNLogType.WC);
        return null;
    }

    public static SVNAdminArea upgrade(SVNAdminArea area) throws SVNException {
        if (isUpgradeEnabled() && !ourFactories.isEmpty()) {
            Collection enabledFactories = getSelector().getEnabledFactories(area.getRoot(), ourFactories, true);
            if (!enabledFactories.isEmpty()) {
                SVNAdminAreaFactory newestFactory = (SVNAdminAreaFactory) enabledFactories.iterator().next();
                SVNAdminArea newArea = newestFactory.doChangeWCFormat(area);
                if (newArea != null && newArea != area && newArea.getWCAccess() != null) {
                    SVNEvent event = SVNEventFactory.createSVNEvent(newArea.getRoot(), SVNNodeKind.DIR, null, SVNRepository.INVALID_REVISION, SVNEventAction.UPGRADE, null, null, null);
                    newArea.getWCAccess().handleEvent(event, ISVNEventHandler.UNKNOWN);
                }
                area = newArea;
            }
        }
        return area;
    }

    public static SVNAdminArea changeWCFormat(SVNAdminArea adminArea, int format) throws SVNException {
        SVNAdminAreaFactory factory = getAdminAreaFactory(format);
        SVNAdminArea newArea = factory.doChangeWCFormat(adminArea);
        if (newArea != null && newArea != adminArea && newArea.getWCAccess() != null) {
            SVNEvent event = SVNEventFactory.createSVNEvent(newArea.getRoot(), SVNNodeKind.DIR, null, SVNRepository.INVALID_REVISION, SVNEventAction.UPGRADE, null, null, null);
            newArea.getWCAccess().handleEvent(event, ISVNEventHandler.UNKNOWN);
        }
        adminArea = newArea;
        return adminArea;
    }

    private static SVNAdminAreaFactory getAdminAreaFactory(int wcFormat) throws SVNException {
        for (Iterator<SVNAdminAreaFactory> ourFactoriesIter = ourFactories.iterator(); ourFactoriesIter.hasNext();) {
            SVNAdminAreaFactory nextFactory = ourFactoriesIter.next();
            if (nextFactory.getSupportedVersion() == wcFormat) {
                return nextFactory; 
            }
        }
        SVNErrorManager.error(SVNErrorMessage.create(SVNErrorCode.WC_UNSUPPORTED_FORMAT), SVNLogType.DEFAULT);
        return null;
    }

    private static int readFormatVersion(File adminDir) throws SVNException {
        SVNErrorMessage error = null;
        int version = -1;
        
        Collection enabledFactories = getSelector().getEnabledFactories(adminDir.getParentFile(), ourFactories, false);
        for(Iterator factories = enabledFactories.iterator(); factories.hasNext();) {
            SVNAdminAreaFactory factory = (SVNAdminAreaFactory) factories.next();
            try {
                version = factory.getVersion(adminDir);
            } catch (SVNException e) {
                error = e.getErrorMessage();
                continue;
            }
            return version;
        }

        if (error == null) {
            error = SVNErrorMessage.create(SVNErrorCode.WC_NOT_DIRECTORY, "''{0}'' is not a working copy", adminDir);
        }
        SVNErrorManager.error(error, SVNLogType.WC);
        return -1;
    }

    public static void createVersionedDirectory(File path, String url, String rootURL, String uuid, long revNumber, SVNDepth depth) throws SVNException {
        if (!ourFactories.isEmpty()) {
            if (!checkAdminAreaExists(path, url, revNumber)) {
                Collection enabledFactories = getSelector().getEnabledFactories(path, ourFactories, true);
                if (!enabledFactories.isEmpty()) {
                    SVNAdminAreaFactory newestFactory = (SVNAdminAreaFactory) enabledFactories.iterator().next();
                    newestFactory.doCreateVersionedDirectory(path, url, rootURL, uuid, revNumber, depth);
                }
            }
        }
    }
    
    public static void createVersionedDirectory(File path, SVNURL url, SVNURL rootURL, String uuid, long revNumber, SVNDepth depth) throws SVNException {
        createVersionedDirectory(path, url != null ? url.toString() : null, rootURL != null ? rootURL.toString() : null, uuid, revNumber, depth);
    }
        
    private static boolean checkAdminAreaExists(File dir, String url, long revision) throws SVNException {
        File adminDir = new File(dir, SVNFileUtil.getAdminDirectoryName());
        if (adminDir.exists() && !adminDir.isDirectory()) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_OBSTRUCTED_UPDATE, "''{0}'' is not a directory", dir);
            SVNErrorManager.error(err, SVNLogType.WC);
        } else if (!adminDir.exists()) {
            return false;
        } 
        
        boolean wcExists = false;
        try {
            readFormatVersion(adminDir);
            wcExists = true;
        } catch (SVNException svne) {
            return false;
        }
        
        if (wcExists) {
            SVNWCAccess wcAccess = SVNWCAccess.newInstance(null);
            SVNAdminArea adminArea = null;
            SVNEntry entry = null;
            try {
                adminArea = wcAccess.open(dir, false, 0);
                entry = adminArea.getVersionedEntry(adminArea.getThisDirName(), false);
            } finally {
                wcAccess.closeAdminArea(dir);
            }
            if (!entry.isScheduledForDeletion()) {
                if (entry.getRevision() != revision) {
                    SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_OBSTRUCTED_UPDATE, "Revision {0} doesn''t match existing revision {1} in ''{2}''", new Object[]{revision, entry.getRevision(), dir});
                    SVNErrorManager.error(err, SVNLogType.WC);
                }
                if (!url.equals(entry.getURL())) {
                    SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_OBSTRUCTED_UPDATE, "URL ''{0}'' doesn''t match existing URL ''{1}'' in ''{2}''", new Object[]{url, entry.getURL(), dir});
                    SVNErrorManager.error(err, SVNLogType.WC);
                }
            }
        }
        return wcExists;
    }

    public abstract int getSupportedVersion();
    
    protected abstract int getVersion(File path) throws SVNException;
    
    protected abstract SVNAdminArea doOpen(File path, int version) throws SVNException;

    protected abstract SVNAdminArea doChangeWCFormat(SVNAdminArea area) throws SVNException;

    protected abstract void doCreateVersionedDirectory(File path, String url, String rootURL, String uuid, long revNumber, SVNDepth depth) throws SVNException;

    protected abstract int doCheckWC(File path, Level logLevel) throws SVNException;

    protected static void registerFactory(SVNAdminAreaFactory factory) {
        if (factory != null) {
            ourFactories.add(factory);
        }
    }

    public int compareTo(Object o) {
        if (o == null || !(o instanceof SVNAdminAreaFactory)) {
            return -1;
        }
        int version = ((SVNAdminAreaFactory) o).getSupportedVersion(); 
        return getSupportedVersion() > version ? -1 : (getSupportedVersion() < version) ? 1 : 0; 
    }
    
    private static class DefaultSelector implements ISVNAdminAreaFactorySelector {

        public Collection getEnabledFactories(File path, Collection factories, boolean writeAccess) throws SVNException {
            return factories;
        }

    }
    
    private static void registerFactories() {
        ourFactories.addAll(SVNClassLoader.getAvailableAdminAreaFactories());
    }
    
}
