/*
 * 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.maven.scm.provider.svn;

import java.io.File;
import java.util.ArrayList;
import java.util.List;

import org.apache.commons.lang3.StringUtils;
import org.apache.maven.scm.CommandParameters;
import org.apache.maven.scm.ScmException;
import org.apache.maven.scm.ScmFileSet;
import org.apache.maven.scm.ScmResult;
import org.apache.maven.scm.command.add.AddScmResult;
import org.apache.maven.scm.command.blame.BlameScmResult;
import org.apache.maven.scm.command.branch.BranchScmResult;
import org.apache.maven.scm.command.changelog.ChangeLogScmResult;
import org.apache.maven.scm.command.checkin.CheckInScmResult;
import org.apache.maven.scm.command.checkout.CheckOutScmResult;
import org.apache.maven.scm.command.diff.DiffScmResult;
import org.apache.maven.scm.command.export.ExportScmResult;
import org.apache.maven.scm.command.info.InfoItem;
import org.apache.maven.scm.command.info.InfoScmResult;
import org.apache.maven.scm.command.list.ListScmResult;
import org.apache.maven.scm.command.mkdir.MkdirScmResult;
import org.apache.maven.scm.command.remove.RemoveScmResult;
import org.apache.maven.scm.command.status.StatusScmResult;
import org.apache.maven.scm.command.tag.TagScmResult;
import org.apache.maven.scm.command.untag.UntagScmResult;
import org.apache.maven.scm.command.update.UpdateScmResult;
import org.apache.maven.scm.provider.AbstractScmProvider;
import org.apache.maven.scm.provider.ScmProviderRepository;
import org.apache.maven.scm.provider.svn.command.SvnCommand;
import org.apache.maven.scm.provider.svn.repository.SvnScmProviderRepository;
import org.apache.maven.scm.provider.svn.util.SvnUtil;
import org.apache.maven.scm.repository.ScmRepository;
import org.apache.maven.scm.repository.ScmRepositoryException;
import org.apache.maven.scm.repository.UnknownRepositoryStructure;

/**
 * SCM Provider for Subversion.
 *
 * @author <a href="mailto:evenisse@apache.org">Emmanuel Venisse</a>
 */
public abstract class AbstractSvnScmProvider extends AbstractScmProvider {
    // ----------------------------------------------------------------------
    //
    // ----------------------------------------------------------------------

    private static class ScmUrlParserResult {
        private final List<String> messages = new ArrayList<>();

        private ScmProviderRepository repository;
    }

    public static final String CURRENT_WORKING_DIRECTORY = "scmCheckWorkingDirectoryUrl.currentWorkingDirectory";

    // ----------------------------------------------------------------------
    // ScmProvider Implementation
    // ----------------------------------------------------------------------

    /**
     * {@inheritDoc}
     */
    public String getScmSpecificFilename() {
        return ".svn";
    }

    /**
     * {@inheritDoc}
     */
    public ScmProviderRepository makeProviderScmRepository(String scmSpecificUrl, char delimiter)
            throws ScmRepositoryException {
        ScmUrlParserResult result = parseScmUrl(scmSpecificUrl);

        if (checkCurrentWorkingDirectoryUrl()) {
            logger.debug("Checking svn info 'URL:' field matches current sources directory");
            try {
                String workingDir = System.getProperty(CURRENT_WORKING_DIRECTORY);
                InfoScmResult info =
                        info(result.repository, new ScmFileSet(new File(workingDir)), new CommandParameters());

                String url = findUrlInfoItem(info);
                String comparison = "'" + url + "' vs. '" + scmSpecificUrl + "'";
                logger.debug("Comparing : " + comparison);
                if (url != null && !url.equals(scmSpecificUrl)) {
                    result.messages.add("Scm url does not match the value returned by svn info (" + comparison + ")");
                }
            } catch (ScmException e) {
                throw new ScmRepositoryException("An error occurred while trying to svn info", e);
            }
        }
        if (result.messages.size() > 0) {
            throw new ScmRepositoryException("The scm url is invalid.", result.messages);
        }

        return result.repository;
    }

    private boolean checkCurrentWorkingDirectoryUrl() {
        return StringUtils.isNotEmpty(System.getProperty(CURRENT_WORKING_DIRECTORY));
    }

    private String findUrlInfoItem(InfoScmResult infoScmResult) {
        for (InfoItem infoItem : infoScmResult.getInfoItems()) {
            if (infoItem.getURL() != null) {
                logger.debug("URL found: " + infoItem.getURL());
                return infoItem.getURL();
            }
        }
        logger.debug("URL not found (command output=" + infoScmResult.getCommandOutput() + ")");
        return null;
    }

    /**
     * {@inheritDoc}
     */
    public ScmProviderRepository makeProviderScmRepository(File path)
            throws ScmRepositoryException, UnknownRepositoryStructure {
        if (path == null) {
            throw new NullPointerException("Path argument is null");
        }

        if (!path.isDirectory()) {
            throw new ScmRepositoryException(path.getAbsolutePath() + " isn't a valid directory.");
        }

        if (!new File(path, ".svn").exists()) {
            throw new ScmRepositoryException(path.getAbsolutePath() + " isn't a svn checkout directory.");
        }

        try {
            return makeProviderScmRepository(getRepositoryURL(path), ':');
        } catch (ScmException e) {
            // XXX We should allow throwing of SCMException.
            throw new ScmRepositoryException("Error executing info command", e);
        }
    }

    protected abstract String getRepositoryURL(File path) throws ScmException;

    /**
     * {@inheritDoc}
     */
    public List<String> validateScmUrl(String scmSpecificUrl, char delimiter) {
        List<String> messages = new ArrayList<>();
        try {
            makeProviderScmRepository(scmSpecificUrl, delimiter);
        } catch (ScmRepositoryException e) {
            messages = e.getValidationMessages();
        }
        return messages;
    }

    /**
     * {@inheritDoc}
     */
    public String getScmType() {
        return "svn";
    }

    // ----------------------------------------------------------------------
    //
    // ----------------------------------------------------------------------

    private ScmUrlParserResult parseScmUrl(String scmSpecificUrl) {
        ScmUrlParserResult result = new ScmUrlParserResult();

        // ----------------------------------------------------------------------
        // Do some sanity checking of the SVN url
        // ----------------------------------------------------------------------

        if (scmSpecificUrl.startsWith("file")) {
            if (!scmSpecificUrl.startsWith("file://")) {
                result.messages.add("A svn 'file' url must be on the form 'file://[hostname]/'.");

                return result;
            }
        } else if (scmSpecificUrl.startsWith("https")) {
            if (!scmSpecificUrl.startsWith("https://")) {
                result.messages.add("A svn 'http' url must be on the form 'https://'.");

                return result;
            }
        } else if (scmSpecificUrl.startsWith("http")) {
            if (!scmSpecificUrl.startsWith("http://")) {
                result.messages.add("A svn 'http' url must be on the form 'http://'.");

                return result;
            }
        }
        // Support of tunnels: svn+xxx with xxx defined in subversion conf file
        else if (scmSpecificUrl.startsWith("svn+")) {
            if (scmSpecificUrl.indexOf("://") < 0) {
                result.messages.add("A svn 'svn+xxx' url must be on the form 'svn+xxx://'.");

                return result;
            } else {
                String tunnel = scmSpecificUrl.substring("svn+".length(), scmSpecificUrl.indexOf("://"));

                // ssh is always an allowed tunnel
                if (!"ssh".equals(tunnel)) {
                    SvnConfigFileReader reader = new SvnConfigFileReader();
                    if (SvnUtil.getSettings().getConfigDirectory() != null) {
                        reader.setConfigDirectory(new File(SvnUtil.getSettings().getConfigDirectory()));
                    }

                    if (StringUtils.isEmpty(reader.getProperty("tunnels", tunnel))) {
                        result.messages.add(
                                "The tunnel '" + tunnel + "' isn't defined in your subversion configuration file.");

                        return result;
                    }
                }
            }
        } else if (scmSpecificUrl.startsWith("svn")) {
            if (!scmSpecificUrl.startsWith("svn://")) {
                result.messages.add("A svn 'svn' url must be on the form 'svn://'.");

                return result;
            }
        } else {
            result.messages.add(scmSpecificUrl + " url isn't a valid svn URL.");

            return result;
        }

        result.repository = new SvnScmProviderRepository(scmSpecificUrl);

        return result;
    }

    protected abstract SvnCommand getAddCommand();

    /**
     * {@inheritDoc}
     */
    public AddScmResult add(ScmProviderRepository repository, ScmFileSet fileSet, CommandParameters parameters)
            throws ScmException {
        return (AddScmResult) executeCommand(getAddCommand(), repository, fileSet, parameters);
    }

    protected abstract SvnCommand getBranchCommand();

    /**
     * {@inheritDoc}
     */
    protected BranchScmResult branch(ScmProviderRepository repository, ScmFileSet fileSet, CommandParameters parameters)
            throws ScmException {
        return (BranchScmResult) executeCommand(getBranchCommand(), repository, fileSet, parameters);
    }

    protected abstract SvnCommand getChangeLogCommand();

    /**
     * {@inheritDoc}
     */
    public ChangeLogScmResult changelog(
            ScmProviderRepository repository, ScmFileSet fileSet, CommandParameters parameters) throws ScmException {
        return (ChangeLogScmResult) executeCommand(getChangeLogCommand(), repository, fileSet, parameters);
    }

    protected abstract SvnCommand getCheckInCommand();

    /**
     * {@inheritDoc}
     */
    public CheckInScmResult checkin(ScmProviderRepository repository, ScmFileSet fileSet, CommandParameters parameters)
            throws ScmException {
        return (CheckInScmResult) executeCommand(getCheckInCommand(), repository, fileSet, parameters);
    }

    protected abstract SvnCommand getCheckOutCommand();

    /**
     * {@inheritDoc}
     */
    public CheckOutScmResult checkout(
            ScmProviderRepository repository, ScmFileSet fileSet, CommandParameters parameters) throws ScmException {
        return (CheckOutScmResult) executeCommand(getCheckOutCommand(), repository, fileSet, parameters);
    }

    protected abstract SvnCommand getDiffCommand();

    /**
     * {@inheritDoc}
     */
    public DiffScmResult diff(ScmProviderRepository repository, ScmFileSet fileSet, CommandParameters parameters)
            throws ScmException {
        return (DiffScmResult) executeCommand(getDiffCommand(), repository, fileSet, parameters);
    }

    protected abstract SvnCommand getExportCommand();

    /**
     * {@inheritDoc}
     */
    protected ExportScmResult export(ScmProviderRepository repository, ScmFileSet fileSet, CommandParameters parameters)
            throws ScmException {
        return (ExportScmResult) executeCommand(getExportCommand(), repository, fileSet, parameters);
    }

    protected abstract SvnCommand getRemoveCommand();

    /**
     * {@inheritDoc}
     */
    public RemoveScmResult remove(ScmProviderRepository repository, ScmFileSet fileSet, CommandParameters parameters)
            throws ScmException {
        return (RemoveScmResult) executeCommand(getRemoveCommand(), repository, fileSet, parameters);
    }

    protected abstract SvnCommand getStatusCommand();

    /**
     * {@inheritDoc}
     */
    public StatusScmResult status(ScmProviderRepository repository, ScmFileSet fileSet, CommandParameters parameters)
            throws ScmException {
        return (StatusScmResult) executeCommand(getStatusCommand(), repository, fileSet, parameters);
    }

    protected abstract SvnCommand getTagCommand();

    /**
     * {@inheritDoc}
     */
    public TagScmResult tag(ScmProviderRepository repository, ScmFileSet fileSet, CommandParameters parameters)
            throws ScmException {
        return (TagScmResult) executeCommand(getTagCommand(), repository, fileSet, parameters);
    }

    protected abstract SvnCommand getUntagCommand();

    /**
     * {@inheritDoc}
     */
    @Override
    public UntagScmResult untag(ScmRepository repository, ScmFileSet fileSet, CommandParameters parameters)
            throws ScmException {
        return (UntagScmResult)
                executeCommand(getUntagCommand(), repository.getProviderRepository(), fileSet, parameters);
    }

    protected abstract SvnCommand getUpdateCommand();

    /**
     * {@inheritDoc}
     */
    public UpdateScmResult update(ScmProviderRepository repository, ScmFileSet fileSet, CommandParameters parameters)
            throws ScmException {
        return (UpdateScmResult) executeCommand(getUpdateCommand(), repository, fileSet, parameters);
    }

    protected ScmResult executeCommand(
            SvnCommand command, ScmProviderRepository repository, ScmFileSet fileSet, CommandParameters parameters)
            throws ScmException {
        return command.execute(repository, fileSet, parameters);
    }

    protected abstract SvnCommand getListCommand();

    /**
     * {@inheritDoc}
     */
    public ListScmResult list(ScmProviderRepository repository, ScmFileSet fileSet, CommandParameters parameters)
            throws ScmException {
        SvnCommand cmd = getListCommand();

        return (ListScmResult) executeCommand(cmd, repository, fileSet, parameters);
    }

    protected abstract SvnCommand getInfoCommand();

    public InfoScmResult info(ScmProviderRepository repository, ScmFileSet fileSet, CommandParameters parameters)
            throws ScmException {
        SvnCommand cmd = getInfoCommand();

        return (InfoScmResult) executeCommand(cmd, repository, fileSet, parameters);
    }

    /**
     * {@inheritDoc}
     */
    protected BlameScmResult blame(ScmProviderRepository repository, ScmFileSet fileSet, CommandParameters parameters)
            throws ScmException {
        SvnCommand cmd = getBlameCommand();

        return (BlameScmResult) executeCommand(cmd, repository, fileSet, parameters);
    }

    protected abstract SvnCommand getBlameCommand();

    /**
     * {@inheritDoc}
     */
    public MkdirScmResult mkdir(ScmProviderRepository repository, ScmFileSet fileSet, CommandParameters parameters)
            throws ScmException {
        SvnCommand cmd = getMkdirCommand();

        return (MkdirScmResult) executeCommand(cmd, repository, fileSet, parameters);
    }

    protected abstract SvnCommand getMkdirCommand();

    /**
     * @param repository
     * @param parameters
     * @return true if remote url exists
     * @throws ScmException
     * @since 1.8
     */
    public abstract boolean remoteUrlExist(ScmProviderRepository repository, CommandParameters parameters)
            throws ScmException;
}
