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

import java.io.File;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.maven.scm.CommandParameters;
import org.apache.maven.scm.ScmBranch;
import org.apache.maven.scm.ScmBranchParameters;
import org.apache.maven.scm.ScmException;
import org.apache.maven.scm.ScmFileSet;
import org.apache.maven.scm.ScmTagParameters;
import org.apache.maven.scm.ScmVersion;
import org.apache.maven.scm.command.add.AddScmResult;
import org.apache.maven.scm.command.blame.BlameScmRequest;
import org.apache.maven.scm.command.blame.BlameScmResult;
import org.apache.maven.scm.command.branch.BranchScmResult;
import org.apache.maven.scm.command.changelog.ChangeLogScmRequest;
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.edit.EditScmResult;
import org.apache.maven.scm.command.export.ExportScmResult;
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.unedit.UnEditScmResult;
import org.apache.maven.scm.command.update.UpdateScmResult;
import org.apache.maven.scm.provider.ScmProvider;
import org.apache.maven.scm.provider.ScmProviderRepository;
import org.apache.maven.scm.provider.ScmUrlUtils;
import org.apache.maven.scm.repository.ScmRepository;
import org.apache.maven.scm.repository.ScmRepositoryException;
import org.apache.maven.scm.repository.UnknownRepositoryStructure;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static java.util.Objects.requireNonNull;

/**
 * @author <a href="mailto:trygvis@inamo.no">Trygve Laugst&oslash;l</a>
 * @author <a href="mailto:brett@apache.org">Brett Porter</a>
 * @author <a href="mailto:evenisse@apache.org">Emmanuel Venisse</a>
 */
public abstract class AbstractScmManager implements ScmManager {
    protected final Logger logger = LoggerFactory.getLogger(getClass());

    private final Map<String, ScmProvider> scmProviders = new ConcurrentHashMap<>();

    private final Map<String, String> userProviderTypes = new ConcurrentHashMap<>();

    protected void setScmProviders(Map<String, ScmProvider> providers) {
        requireNonNull(providers);
        this.scmProviders.clear();
        // first provider must not be overwritten by subsequent ones if they are registered for the same key
        providers.forEach(this.scmProviders::putIfAbsent);
    }

    /**
     * @param providerType the type of SCM, e.g. <code>svn</code>, <code>git</code>
     * @param provider     the provider that will be used for that SCM type
     * @deprecated use {@link #setScmProvider(String, ScmProvider)} instead
     */
    @Deprecated
    protected void addScmProvider(String providerType, ScmProvider provider) {
        setScmProvider(providerType, provider);
    }

    /**
     * Set a provider to be used for a type of SCM.
     * If there was already a designed provider for that type it will be replaced.
     *
     * @param providerType the type of SCM, e.g. <code>svn</code>, <code>git</code>
     * @param provider     the provider that will be used for that SCM type
     */
    @Override
    public void setScmProvider(String providerType, ScmProvider provider) {
        requireNonNull(providerType);
        requireNonNull(provider);
        scmProviders.put(providerType, provider);
    }

    // ----------------------------------------------------------------------
    // ScmManager Implementation
    // ----------------------------------------------------------------------

    /**
     * {@inheritDoc}
     */
    @Override
    public ScmProvider getProviderByUrl(String scmUrl) throws ScmRepositoryException, NoSuchScmProviderException {
        requireNonNull(scmUrl, "The scm url cannot be null.");

        String providerType = ScmUrlUtils.getProvider(scmUrl);

        return getProviderByType(providerType);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setScmProviderImplementation(String providerType, String providerImplementation) {
        requireNonNull(providerType);
        requireNonNull(providerImplementation);
        userProviderTypes.put(providerType, providerImplementation);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public ScmProvider getProviderByType(String providerType) throws NoSuchScmProviderException {
        String usedProviderType = System.getProperty("maven.scm.provider." + providerType + ".implementation");

        if (usedProviderType == null) {
            usedProviderType = userProviderTypes.getOrDefault(providerType, providerType);
        }

        ScmProvider scmProvider = scmProviders.get(usedProviderType);

        if (scmProvider == null) {
            throw new NoSuchScmProviderException(usedProviderType);
        }

        return scmProvider;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public ScmProvider getProviderByRepository(ScmRepository repository) throws NoSuchScmProviderException {
        return getProviderByType(repository.getProvider());
    }

    // ----------------------------------------------------------------------
    // Repository
    // ----------------------------------------------------------------------

    /**
     * {@inheritDoc}
     */
    @Override
    public ScmRepository makeScmRepository(String scmUrl) throws ScmRepositoryException, NoSuchScmProviderException {
        requireNonNull(scmUrl, "The scm url cannot be null.");

        char delimiter = ScmUrlUtils.getDelimiter(scmUrl).charAt(0);

        String providerType = ScmUrlUtils.getProvider(scmUrl);

        ScmProvider provider = getProviderByType(providerType);

        String scmSpecificUrl = cleanScmUrl(scmUrl.substring(providerType.length() + 5));

        ScmProviderRepository providerRepository = provider.makeProviderScmRepository(scmSpecificUrl, delimiter);

        return new ScmRepository(providerType, providerRepository);
    }

    /**
     * Clean the SCM url by removing all ../ in path.
     *
     * @param scmUrl the SCM url
     * @return the cleaned SCM url
     */
    protected String cleanScmUrl(String scmUrl) {
        requireNonNull(scmUrl, "The scm url cannot be null.");

        String pathSeparator = "";

        int indexOfDoubleDot = -1;

        // Clean Unix path
        if (scmUrl.indexOf("../") > 1) {
            pathSeparator = "/";

            indexOfDoubleDot = scmUrl.indexOf("../");
        }

        // Clean windows path
        if (scmUrl.indexOf("..\\") > 1) {
            pathSeparator = "\\";

            indexOfDoubleDot = scmUrl.indexOf("..\\");
        }

        if (indexOfDoubleDot > 1) {
            int startOfTextToRemove = scmUrl.substring(0, indexOfDoubleDot - 1).lastIndexOf(pathSeparator);

            String beginUrl = "";
            if (startOfTextToRemove >= 0) {
                beginUrl = scmUrl.substring(0, startOfTextToRemove);
            }

            String endUrl = scmUrl.substring(indexOfDoubleDot + 3);

            scmUrl = beginUrl + pathSeparator + endUrl;

            // Check if we have other double dot
            if (scmUrl.indexOf("../") > 1 || scmUrl.indexOf("..\\") > 1) {
                scmUrl = cleanScmUrl(scmUrl);
            }
        }

        return scmUrl;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public ScmRepository makeProviderScmRepository(String providerType, File path)
            throws ScmRepositoryException, UnknownRepositoryStructure, NoSuchScmProviderException {
        requireNonNull(providerType, "The provider type cannot be null.");

        ScmProvider provider = getProviderByType(providerType);

        ScmProviderRepository providerRepository = provider.makeProviderScmRepository(path);

        return new ScmRepository(providerType, providerRepository);
    }

    @Override
    public Optional<ScmRepository> makeProviderScmRepository(File workingDirectory) {
        //
        for (ScmProvider provider : scmProviders.values()) {
            logger.debug(
                    "Checking if SCM provider {} is suitable for processing: {}",
                    provider.getScmType(),
                    workingDirectory);
            try {
                ScmProviderRepository providerRepository = provider.makeProviderScmRepository(workingDirectory);
                return Optional.of(new ScmRepository(provider.getScmType(), providerRepository));
            } catch (ScmRepositoryException | UnknownRepositoryStructure e) {
                logger.debug(
                        "SCM provider {} is not suitable for processing: {}",
                        provider.getScmType(),
                        workingDirectory,
                        e);
            }
        }
        return Optional.empty();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<String> validateScmRepository(String scmUrl) {
        List<String> messages = new ArrayList<>(ScmUrlUtils.validate(scmUrl));

        String providerType = ScmUrlUtils.getProvider(scmUrl);

        ScmProvider provider;

        try {
            provider = getProviderByType(providerType);
        } catch (NoSuchScmProviderException e) {
            messages.add("No such provider installed '" + providerType + "'.");

            return messages;
        }

        String scmSpecificUrl = cleanScmUrl(scmUrl.substring(providerType.length() + 5));

        List<String> providerMessages = provider.validateScmUrl(
                scmSpecificUrl, ScmUrlUtils.getDelimiter(scmUrl).charAt(0));

        requireNonNull(providerMessages, "The SCM provider cannot return null from validateScmUrl().");

        messages.addAll(providerMessages);

        return messages;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public AddScmResult add(ScmRepository repository, ScmFileSet fileSet) throws ScmException {
        return this.getProviderByRepository(repository).add(repository, fileSet);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public AddScmResult add(ScmRepository repository, ScmFileSet fileSet, String message) throws ScmException {
        return this.getProviderByRepository(repository).add(repository, fileSet, message);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public BranchScmResult branch(ScmRepository repository, ScmFileSet fileSet, String branchName) throws ScmException {
        ScmBranchParameters scmBranchParameters = new ScmBranchParameters("");
        return this.getProviderByRepository(repository).branch(repository, fileSet, branchName, scmBranchParameters);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public BranchScmResult branch(ScmRepository repository, ScmFileSet fileSet, String branchName, String message)
            throws ScmException {
        ScmBranchParameters scmBranchParameters = new ScmBranchParameters(message);
        return this.getProviderByRepository(repository).branch(repository, fileSet, branchName, scmBranchParameters);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public ChangeLogScmResult changeLog(
            ScmRepository repository, ScmFileSet fileSet, Date startDate, Date endDate, int numDays, ScmBranch branch)
            throws ScmException {
        return this.getProviderByRepository(repository)
                .changeLog(repository, fileSet, startDate, endDate, numDays, branch);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public ChangeLogScmResult changeLog(
            ScmRepository repository,
            ScmFileSet fileSet,
            Date startDate,
            Date endDate,
            int numDays,
            ScmBranch branch,
            String datePattern)
            throws ScmException {
        return this.getProviderByRepository(repository)
                .changeLog(repository, fileSet, startDate, endDate, numDays, branch, datePattern);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public ChangeLogScmResult changeLog(ChangeLogScmRequest scmRequest) throws ScmException {
        return this.getProviderByRepository(scmRequest.getScmRepository()).changeLog(scmRequest);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public ChangeLogScmResult changeLog(
            ScmRepository repository, ScmFileSet fileSet, ScmVersion startVersion, ScmVersion endVersion)
            throws ScmException {
        return this.getProviderByRepository(repository).changeLog(repository, fileSet, startVersion, endVersion);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public ChangeLogScmResult changeLog(
            ScmRepository repository,
            ScmFileSet fileSet,
            ScmVersion startRevision,
            ScmVersion endRevision,
            String datePattern)
            throws ScmException {
        return this.getProviderByRepository(repository)
                .changeLog(repository, fileSet, startRevision, endRevision, datePattern);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public CheckInScmResult checkIn(ScmRepository repository, ScmFileSet fileSet, String message) throws ScmException {
        return this.getProviderByRepository(repository).checkIn(repository, fileSet, message);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public CheckInScmResult checkIn(ScmRepository repository, ScmFileSet fileSet, ScmVersion revision, String message)
            throws ScmException {
        return this.getProviderByRepository(repository).checkIn(repository, fileSet, revision, message);
    }

    @Override
    public CheckInScmResult checkIn(ScmRepository repository, ScmFileSet fileSet, CommandParameters commandParameters)
            throws ScmException {
        return this.getProviderByRepository(repository).checkIn(repository, fileSet, commandParameters);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public CheckOutScmResult checkOut(ScmRepository repository, ScmFileSet fileSet) throws ScmException {
        return this.getProviderByRepository(repository).checkOut(repository, fileSet);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public CheckOutScmResult checkOut(ScmRepository repository, ScmFileSet fileSet, ScmVersion version)
            throws ScmException {
        return this.getProviderByRepository(repository).checkOut(repository, fileSet, version);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public CheckOutScmResult checkOut(ScmRepository repository, ScmFileSet fileSet, boolean recursive)
            throws ScmException {
        return this.getProviderByRepository(repository).checkOut(repository, fileSet, recursive);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public CheckOutScmResult checkOut(
            ScmRepository repository, ScmFileSet fileSet, ScmVersion version, boolean recursive) throws ScmException {
        return this.getProviderByRepository(repository).checkOut(repository, fileSet, version, recursive);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public DiffScmResult diff(
            ScmRepository repository, ScmFileSet fileSet, ScmVersion startVersion, ScmVersion endVersion)
            throws ScmException {
        return this.getProviderByRepository(repository).diff(repository, fileSet, startVersion, endVersion);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public EditScmResult edit(ScmRepository repository, ScmFileSet fileSet) throws ScmException {
        return this.getProviderByRepository(repository).edit(repository, fileSet);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public ExportScmResult export(ScmRepository repository, ScmFileSet fileSet) throws ScmException {
        return this.getProviderByRepository(repository).export(repository, fileSet);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public ExportScmResult export(ScmRepository repository, ScmFileSet fileSet, ScmVersion version)
            throws ScmException {
        return this.getProviderByRepository(repository).export(repository, fileSet, version);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public ExportScmResult export(ScmRepository repository, ScmFileSet fileSet, String outputDirectory)
            throws ScmException {
        return this.getProviderByRepository(repository).export(repository, fileSet, (ScmVersion) null, outputDirectory);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public ExportScmResult export(
            ScmRepository repository, ScmFileSet fileSet, ScmVersion version, String outputDirectory)
            throws ScmException {
        return this.getProviderByRepository(repository).export(repository, fileSet, version, outputDirectory);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public ListScmResult list(ScmRepository repository, ScmFileSet fileSet, boolean recursive, ScmVersion version)
            throws ScmException {
        return this.getProviderByRepository(repository).list(repository, fileSet, recursive, version);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public MkdirScmResult mkdir(ScmRepository repository, ScmFileSet fileSet, String message, boolean createInLocal)
            throws ScmException {
        return this.getProviderByRepository(repository).mkdir(repository, fileSet, message, createInLocal);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public RemoveScmResult remove(ScmRepository repository, ScmFileSet fileSet, String message) throws ScmException {
        return this.getProviderByRepository(repository).remove(repository, fileSet, message);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public StatusScmResult status(ScmRepository repository, ScmFileSet fileSet) throws ScmException {
        return this.getProviderByRepository(repository).status(repository, fileSet);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public TagScmResult tag(ScmRepository repository, ScmFileSet fileSet, String tagName) throws ScmException {
        return this.tag(repository, fileSet, tagName, "");
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public TagScmResult tag(ScmRepository repository, ScmFileSet fileSet, String tagName, String message)
            throws ScmException {
        ScmTagParameters scmTagParameters = new ScmTagParameters(message);
        return this.getProviderByRepository(repository).tag(repository, fileSet, tagName, scmTagParameters);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public UnEditScmResult unedit(ScmRepository repository, ScmFileSet fileSet) throws ScmException {
        return this.getProviderByRepository(repository).unedit(repository, fileSet);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public UpdateScmResult update(ScmRepository repository, ScmFileSet fileSet) throws ScmException {
        return this.getProviderByRepository(repository).update(repository, fileSet);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public UpdateScmResult update(ScmRepository repository, ScmFileSet fileSet, ScmVersion version)
            throws ScmException {
        return this.getProviderByRepository(repository).update(repository, fileSet, version);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public UpdateScmResult update(ScmRepository repository, ScmFileSet fileSet, boolean runChangelog)
            throws ScmException {
        return this.getProviderByRepository(repository).update(repository, fileSet, runChangelog);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public UpdateScmResult update(
            ScmRepository repository, ScmFileSet fileSet, ScmVersion version, boolean runChangelog)
            throws ScmException {
        return this.getProviderByRepository(repository).update(repository, fileSet, version, runChangelog);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public UpdateScmResult update(ScmRepository repository, ScmFileSet fileSet, String datePattern)
            throws ScmException {
        return this.getProviderByRepository(repository).update(repository, fileSet, (ScmVersion) null, datePattern);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public UpdateScmResult update(ScmRepository repository, ScmFileSet fileSet, ScmVersion version, String datePattern)
            throws ScmException {
        return this.getProviderByRepository(repository).update(repository, fileSet, version, datePattern);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public UpdateScmResult update(ScmRepository repository, ScmFileSet fileSet, Date lastUpdate) throws ScmException {
        return this.getProviderByRepository(repository).update(repository, fileSet, (ScmVersion) null, lastUpdate);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public UpdateScmResult update(ScmRepository repository, ScmFileSet fileSet, ScmVersion version, Date lastUpdate)
            throws ScmException {
        return this.getProviderByRepository(repository).update(repository, fileSet, version, lastUpdate);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public UpdateScmResult update(ScmRepository repository, ScmFileSet fileSet, Date lastUpdate, String datePattern)
            throws ScmException {
        return this.getProviderByRepository(repository)
                .update(repository, fileSet, (ScmVersion) null, lastUpdate, datePattern);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public UpdateScmResult update(
            ScmRepository repository, ScmFileSet fileSet, ScmVersion version, Date lastUpdate, String datePattern)
            throws ScmException {
        return this.getProviderByRepository(repository).update(repository, fileSet, version, lastUpdate, datePattern);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public BlameScmResult blame(ScmRepository repository, ScmFileSet fileSet, String filename) throws ScmException {
        return this.getProviderByRepository(repository).blame(repository, fileSet, filename);
    }

    @Override
    public BlameScmResult blame(BlameScmRequest blameScmRequest) throws ScmException {
        return this.getProviderByRepository(blameScmRequest.getScmRepository()).blame(blameScmRequest);
    }
}
