/*
 * Copyright 2000-2013 JetBrains s.r.o.
 *
 * Licensed 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.jetbrains.idea.svn.auth;

import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.MessageType;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.Getter;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.ThrowableComputable;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vcs.ui.VcsBalloonProblemNotifier;
import com.intellij.util.EventDispatcher;
import com.intellij.util.SystemProperties;
import com.intellij.util.messages.Topic;
import com.intellij.util.net.HttpConfigurable;
import com.intellij.util.proxy.CommonProxy;
import com.intellij.util.ui.UIUtil;
import com.trilead.ssh2.auth.AgentProxy;
import org.intellij.lang.annotations.MagicConstant;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.idea.svn.*;
import org.jetbrains.idea.svn.config.ProxyGroup;
import org.jetbrains.idea.svn.config.SvnServerFileKeys;
import org.tmatesoft.svn.core.SVNErrorMessage;
import org.tmatesoft.svn.core.SVNException;
import org.tmatesoft.svn.core.SVNProperties;
import org.tmatesoft.svn.core.SVNURL;
import org.tmatesoft.svn.core.auth.*;
import org.tmatesoft.svn.core.internal.wc.*;
import org.tmatesoft.svn.core.io.SVNRepository;

import java.io.File;
import java.io.IOException;
import java.net.*;
import java.util.*;

import static com.intellij.util.WaitForProgressToShow.runOrInvokeLaterAboveProgress;

/**
 * @author alex
 */
public class SvnAuthenticationManager extends DefaultSVNAuthenticationManager implements SvnAuthenticationListener, ISVNAuthenticationManagerExt {
  private static final Logger LOG = Logger.getInstance(SvnAuthenticationManager.class);
  // while Mac storage not working for IDEA, we use this key to check whether to prompt abt plaintext or just store
  public static final String SVN_SSH = "svn+ssh";
  public static final String HTTP = "http";
  public static final String HTTPS = "https";
  public static final String HTTP_PROXY_HOST = "http-proxy-host";
  public static final String HTTP_PROXY_PORT = "http-proxy-port";
  public static final String HTTP_PROXY_USERNAME = "http-proxy-username";
  public static final String HTTP_PROXY_PASSWORD = "http-proxy-password";
  private SvnVcs myVcs;
  private Project myProject;
  private File myConfigDirectory;
  private ISVNAuthenticationProvider myRuntimeCacheProvider;
  private PersistentAuthenticationProviderProxy myPersistentAuthenticationProviderProxy;
  private SvnConfiguration myConfig;
  private static final ThreadLocal<Boolean> ourJustEntered = new ThreadLocal<>();
  private SvnAuthenticationInteraction myInteraction;
  private EventDispatcher<SvnAuthenticationListener> myListener;
  private IdeaSVNHostOptionsProvider myLocalHostOptionsProvider;
  private final ThreadLocalSavePermissions mySavePermissions;
  private final Map<Thread, String> myKeyAlgorithm;
  private boolean myArtificialSaving;
  private ISVNAuthenticationProvider myProvider;
  public static final Topic<ISVNAuthenticationProviderListener> AUTHENTICATION_PROVIDER_LISTENER =
    new Topic<>("AUTHENTICATION_PROVIDER_LISTENER", ISVNAuthenticationProviderListener.class);
  private final static ThreadLocal<ISVNAuthenticationProvider> ourThreadLocalProvider = new ThreadLocal<>();

  public SvnAuthenticationManager(@NotNull SvnVcs vcs, final File configDirectory) {
    super(configDirectory, true, null, null);
    myVcs = vcs;
    myProject = myVcs.getProject();
    myConfigDirectory = configDirectory;
    myKeyAlgorithm = new HashMap<>();
    ensureListenerCreated();
    mySavePermissions = new ThreadLocalSavePermissions();
    myConfig = myVcs.getSvnConfiguration();
    if (myPersistentAuthenticationProviderProxy != null) {
      myPersistentAuthenticationProviderProxy.setProject(myProject);
    }
    myInteraction = new MySvnAuthenticationInteraction(myProject);
    Disposer.register(myProject, new Disposable() {
      @Override
      public void dispose() {
        myVcs = null;
        myProject = null;
        if (myPersistentAuthenticationProviderProxy != null) {
          myPersistentAuthenticationProviderProxy.myProject = null;
          ((MyKeyringMasterKeyProvider) myPersistentAuthenticationProviderProxy.myISVNGnomeKeyringPasswordProvider).myProject = null;
          myPersistentAuthenticationProviderProxy = null;
        }
        if (myInteraction instanceof MySvnAuthenticationInteraction) {
          ((MySvnAuthenticationInteraction) myInteraction).myProject = null;
        }
        if (myConfig != null) {
          myConfig.clear();
          myConfig = null;
        }
        myInteraction = null;

      }
    });
    // This is not the same instance as DefaultSVNAuthenticationManager.myProviders[1], but currently
    // DefaultSVNAuthenticationManager.CacheAuthenticationProvider uses only its outer class state - so we utilize necessary logic with
    // this new instance.
    myRuntimeCacheProvider = createRuntimeAuthenticationProvider();
  }

  public SVNAuthentication requestFromCache(String kind,
                                            SVNURL url,
                                            String realm,
                                            SVNErrorMessage errorMessage,
                                            SVNAuthentication previousAuth,
                                            boolean authMayBeStored) {
    return myRuntimeCacheProvider.requestClientAuthentication(kind, url, realm, errorMessage, previousAuth, authMayBeStored);
  }

  public String getDefaultUsername(String kind, SVNURL url) {
    String result = SystemProperties.getUserName();

    // USERNAME authentication is also requested in SVNSSHConnector.open()
    if (ISVNAuthenticationManager.SSH.equals(kind) ||
        (ISVNAuthenticationManager.USERNAME.equals(kind) && SVN_SSH.equals(url.getProtocol()))) {
      result = url != null && !StringUtil.isEmpty(url.getUserInfo()) ? url.getUserInfo() : getDefaultOptions().getDefaultSSHUserName();
    }

    return result;
  }

  @Override
  protected SVNSSHAuthentication getDefaultSSHAuthentication(SVNURL url) {
    String userName = getDefaultUsername(ISVNAuthenticationManager.SSH, url);

    // This is fully copied from base class - DefaultSVNAuthenticationManager - as there are no setters in Authentication classes
    // and there is no url parameter if overriding getDefaultOptions()
    String password = getDefaultOptions().getDefaultSSHPassword();
    String keyFile = getDefaultOptions().getDefaultSSHKeyFile();
    int port = getDefaultOptions().getDefaultSSHPortNumber();
    String passphrase = getDefaultOptions().getDefaultSSHPassphrase();

    if (userName != null && password != null) {
      return new SVNSSHAuthentication(userName, password, port, getHostOptionsProvider().getHostOptions(url).isAuthStorageEnabled(), url,
                                      false);
    }
    else if (userName != null && keyFile != null) {
      return new SVNSSHAuthentication(userName, new File(keyFile), passphrase, port,
                                      getHostOptionsProvider().getHostOptions(url).isAuthStorageEnabled(), url, false);
    }
    return null;
  }

  private class AuthenticationProviderProxy implements ISVNAuthenticationProvider {
    private final ISVNAuthenticationProvider myDelegate;

    private AuthenticationProviderProxy(ISVNAuthenticationProvider delegate) {
      myDelegate = delegate;
    }

    @Override
    public SVNAuthentication requestClientAuthentication(String kind,
                                                         SVNURL url,
                                                         String realm,
                                                         SVNErrorMessage errorMessage,
                                                         SVNAuthentication previousAuth, boolean authMayBeStored) {
      final SVNAuthentication authentication =
        myDelegate.requestClientAuthentication(kind, url, realm, errorMessage, previousAuth, authMayBeStored);
      if (myProject != null && ! myProject.isDisposed()) {
        myProject.getMessageBus().syncPublisher(AUTHENTICATION_PROVIDER_LISTENER)
          .requestClientAuthentication(kind, url, realm, errorMessage, previousAuth, authMayBeStored, authentication);
      }
      return authentication;
    }

    @Override
    public int acceptServerAuthentication(SVNURL url,
                                          String realm,
                                          Object certificate,
                                          boolean resultMayBeStored) {
      final int result = myDelegate.acceptServerAuthentication(url, realm, certificate, resultMayBeStored);
      if (myProject != null && ! myProject.isDisposed()) {
        myProject.getMessageBus().syncPublisher(AUTHENTICATION_PROVIDER_LISTENER)
          .acceptServerAuthentication(url, realm, certificate, resultMayBeStored, result);
      }
      return result;
    }
  }

  public interface ISVNAuthenticationProviderListener {
    void requestClientAuthentication(String kind, SVNURL url, String realm, SVNErrorMessage errorMessage,
      SVNAuthentication previousAuth, boolean authMayBeStored, SVNAuthentication authentication);
    void acceptServerAuthentication(SVNURL url, String realm, Object certificate, boolean resultMayBeStored, @MagicConstant int acceptResult);
  }

  @Override
  public void setAuthenticationProvider(ISVNAuthenticationProvider provider) {
    ISVNAuthenticationProvider useProvider = provider;
    if (! (provider instanceof AuthenticationProviderProxy)) {
      useProvider = new AuthenticationProviderProxy(provider);
    }
    myProvider = useProvider;
    super.setAuthenticationProvider(myProvider);
  }

  public ISVNAuthenticationProvider getProvider() {
    final ISVNAuthenticationProvider threadProvider = ourThreadLocalProvider.get();
    if (threadProvider != null) return threadProvider;
    return myProvider;
  }

  /**
   * Gets authentication provider without looking into thread local storage for providers.
   *
   * TODO:
   * Thread local storage is used "for some interaction with SVNKit" and is not always cleared correctly. So some threads contain
   * "passive provider" in thread local storage - and getProvider() returns this "passive provider". This occurs, for instance when
   * RemoteRevisionsCache is refreshed in background - after its execution, corresponding thread has "passive provider" in thread local
   * storage.
   *
   * As a result authentication fails in such cases (at least for command line implementation). To fix this, command line implementation is
   * updated not to check thread local storage at all.
   *
   * @return
   */
  public ISVNAuthenticationProvider getInnerProvider() {
    return myProvider;
  }

  @Override
  public ISVNAuthenticationStorage getRuntimeAuthStorage() {
    return super.getRuntimeAuthStorage();
  }

  // since set to null during dispose and we have background processes
  private SvnConfiguration getConfig() {
    if (myConfig == null) throw new ProcessCanceledException();
    return myConfig;
  }

  public void setArtificialSaving(boolean artificialSaving) {
    myArtificialSaving = artificialSaving;
  }

  private void ensureListenerCreated() {
    if (myListener == null) {
      myListener = EventDispatcher.create(SvnAuthenticationListener.class);
    }
  }

  @Override
  public IdeaSVNHostOptionsProvider getHostOptionsProvider() {
    if (myLocalHostOptionsProvider == null) {
      myLocalHostOptionsProvider = new IdeaSVNHostOptionsProvider();
    }
    return myLocalHostOptionsProvider;
  }

  public void addListener(final SvnAuthenticationListener listener) {
    myListener.addListener(listener);
  }

  @Override
  public void actualSaveWillBeTried(ProviderType type, SVNURL url, String realm, String kind) {
    myListener.getMulticaster().actualSaveWillBeTried(type, url, realm, kind);
  }

  @Override
  public void saveAttemptStarted(ProviderType type, SVNURL url, String realm, String kind) {
    myListener.getMulticaster().saveAttemptStarted(type, url, realm, kind);
  }

  @Override
  public void saveAttemptFinished(ProviderType type, SVNURL url, String realm, String kind) {
    myListener.getMulticaster().saveAttemptFinished(type, url, realm, kind);
  }

  @Override
  public void acknowledge(boolean accepted, String kind, String realm, SVNErrorMessage message, SVNAuthentication authentication) {
    myListener.getMulticaster().acknowledge(accepted, kind, realm, message, authentication);
  }

  @Override
  public void requested(ProviderType type, SVNURL url, String realm, String kind, boolean canceled) {
    if (ProviderType.interactive.equals(type) && (! canceled)) {
      ourJustEntered.set(true);
    }
    myListener.getMulticaster().requested(type, url, realm, kind, canceled);
  }

  @Override
  protected ISVNAuthenticationProvider createCacheAuthenticationProvider(File authDir, String userName) {
    // this is a hack due to the fact this method is called from super() constructor
    myConfigDirectory = new File(authDir.getParent());
    myPersistentAuthenticationProviderProxy = new PersistentAuthenticationProviderProxy(authDir, userName);
    return myPersistentAuthenticationProviderProxy;
  }

  private class PersistentAuthenticationProviderProxy implements ISVNAuthenticationProvider, ISVNPersistentAuthenticationProvider {
    private final ISVNAuthenticationProvider myDelegate;
    private final ISVNGnomeKeyringPasswordProvider myISVNGnomeKeyringPasswordProvider;
    private final File myAuthDir;
    private Project myProject;

    private PersistentAuthenticationProviderProxy(File authDir, String userName) {
      myISVNGnomeKeyringPasswordProvider = new MyKeyringMasterKeyProvider(myProject);
      ISVNAuthenticationStorageOptions delegatingOptions = new ISVNAuthenticationStorageOptions() {
        public boolean isNonInteractive() throws SVNException {
          return getAuthenticationStorageOptions().isNonInteractive();
        }

        public ISVNAuthStoreHandler getAuthStoreHandler() throws SVNException {
          return getAuthenticationStorageOptions().getAuthStoreHandler();
        }

        @Override
        public ISVNGnomeKeyringPasswordProvider getGnomeKeyringPasswordProvider() {
          return myISVNGnomeKeyringPasswordProvider;
        }

        @Override
        public boolean isSSLPassphrasePromptSupported() {
          return false;
        }
      };
      ensureListenerCreated();
      myDelegate = new DefaultSVNPersistentAuthenticationProvider(authDir, userName, delegatingOptions, getDefaultOptions(), getHostOptionsProvider()) {
        @Override
        protected IPasswordStorage[] createPasswordStorages(DefaultSVNOptions options) {
          final IPasswordStorage[] passwordStorages = super.createPasswordStorages(options);
          final IPasswordStorage[] proxied = new IPasswordStorage[passwordStorages.length];
          for (int i = 0; i < passwordStorages.length; i++) {
            final IPasswordStorage storage = passwordStorages[i];
            proxied[i] = new ProxyPasswordStorageForDebug(storage, myListener);
          }
          return proxied;
        }
      };
      myAuthDir = authDir;
    }

    public void setProject(Project project) {
      myProject = project;
    }

    public SVNAuthentication requestClientAuthentication(final String kind, final SVNURL url, final String realm, final SVNErrorMessage errorMessage,
                                                         final SVNAuthentication previousAuth, final boolean authMayBeStored) {
      try {
        return wrapNativeCall(new ThrowableComputable<SVNAuthentication, SVNException>() {
          @Override
          public SVNAuthentication compute() throws SVNException {
            final SVNAuthentication svnAuthentication =
              myDelegate.requestClientAuthentication(kind, url, realm, errorMessage, previousAuth, authMayBeStored);
            myListener.getMulticaster().requested(ProviderType.persistent, url, realm, kind, svnAuthentication == null);
            return svnAuthentication;
          }
        });
      }
      catch (SVNException e) {
        LOG.info(e);
        throw new RuntimeException(e);
      }
    }

    public int acceptServerAuthentication(final SVNURL url, final String realm, final Object certificate, final boolean resultMayBeStored) {
      return ACCEPTED_TEMPORARY;
    }

    private void actualSavePermissions(String realm, SVNAuthentication auth) {
      final String actualKind = auth.getKind();
          File dir = new File(myAuthDir, actualKind);
          String fileName = SVNFileUtil.computeChecksum(realm);
          File authFile = new File(dir, fileName);

          try {
            ((ISVNPersistentAuthenticationProvider) myDelegate).saveAuthentication(auth, actualKind, realm);
          }
          catch (SVNException e) {
            if (myProject != null) {
              ApplicationManager.getApplication().invokeLater(new VcsBalloonProblemNotifier(myProject,
                "<b>Problem when storing Subversion credentials:</b>&nbsp;" + e.getMessage(), MessageType.ERROR));
            }
          }
          finally {
            // do not make password file readonly
            setWriteable(authFile);
          }
    }

    public void saveAuthentication(final SVNAuthentication auth, final String kind, final String realm) throws SVNException {
      try {
        wrapNativeCall(new ThrowableComputable<Void, SVNException>() {
          @Override
          public Void compute() throws SVNException {
            final Boolean fromInteractive = ourJustEntered.get();
            ourJustEntered.set(null);
            if (! myArtificialSaving && ! Boolean.TRUE.equals(fromInteractive)) {
              // not what user entered
              return null;
            }
            myListener.getMulticaster().saveAttemptStarted(ProviderType.persistent, auth.getURL(), realm, auth.getKind());
            ((ISVNPersistentAuthenticationProvider) myDelegate).saveAuthentication(auth, kind, realm);
            myListener.getMulticaster().saveAttemptFinished(ProviderType.persistent, auth.getURL(), realm, auth.getKind());
            return null;
          }
        });
      }
      catch (SVNException e) {
        LOG.info(e);
        throw new RuntimeException(e);
      }
    }

    @Override
    public void saveFingerprints(final String realm, final byte[] fingerprints) {
      try {
        wrapNativeCall(new ThrowableComputable<Void, SVNException>() {
          @Override
          public Void compute() throws SVNException {
            ((ISVNPersistentAuthenticationProvider) myDelegate).saveFingerprints(realm, fingerprints);
            return null;
          }
        });
      }
      catch (SVNException e) {
        LOG.info(e);
        throw new RuntimeException(e);
      }
    }

    @Override
    public byte[] loadFingerprints(final String realm) {
      try {
        return wrapNativeCall(new ThrowableComputable<byte[], SVNException>() {
          @Override
          public byte[] compute() throws SVNException {
            return ((ISVNPersistentAuthenticationProvider) myDelegate).loadFingerprints(realm);
          }
        });
      }
      catch (SVNException e) {
        LOG.info(e);
        throw new RuntimeException(e);
      }
    }

    private final static int maxAttempts = 10;
    private void setWriteable(final File file) {
      if (! file.exists()) return;
      if (file.getParentFile() == null) {
        return;
      }
      for (int i = 0; i < maxAttempts; i++) {
        final File parent = file.getParentFile();
        try {
          final File tempFile = FileUtil.createTempFile(parent, "123", "1", true);
          FileUtil.delete(tempFile);
          if (! file.renameTo(tempFile)) continue;
          if (! file.createNewFile()) continue;
          FileUtil.copy(tempFile, file);
          FileUtil.delete(tempFile);
          return;
        }
        catch (IOException e) {
          //
        }
      }
    }
  }

  @Override
  public void verifyHostKey(String hostName, int port, String keyAlgorithm, byte[] hostKey) throws SVNException {
    myKeyAlgorithm.put(Thread.currentThread(), keyAlgorithm);
    try {
      super.verifyHostKey(hostName, port, keyAlgorithm, hostKey);
    } finally {
      myKeyAlgorithm.remove(Thread.currentThread());
    }
  }

  @Nullable
  public String getSSHKeyAlgorithm() {
    return myKeyAlgorithm.get(Thread.currentThread());
  }

  @Override
  public void acknowledgeConnectionSuccessful(SVNURL url, String method) {
    CommonProxy.getInstance().removeNoProxy(url.getProtocol(), url.getHost(), url.getPort());
    SSLExceptionsHelper.removeInfo();
    ourThreadLocalProvider.remove();
  }

  @Override
  public void acknowledgeAuthentication(boolean accepted,
                                        String kind,
                                        String realm,
                                        SVNErrorMessage errorMessage,
                                        SVNAuthentication authentication) throws SVNException {
    acknowledgeAuthentication(accepted, kind, realm, errorMessage, authentication, null);
  }

  @Override
  public void acknowledgeAuthentication(boolean accepted,
                                        String kind,
                                        String realm,
                                        SVNErrorMessage errorMessage,
                                        SVNAuthentication authentication,
                                        SVNURL url) throws SVNException {
    showSshAgentErrorIfAny(errorMessage, authentication);

    SSLExceptionsHelper.removeInfo();
    ourThreadLocalProvider.remove();
    if (url != null) {
      CommonProxy.getInstance().removeNoProxy(url.getProtocol(), url.getHost(), url.getPort());
    }
    boolean successSaving = false;
    myListener.getMulticaster().acknowledge(accepted, kind, realm, errorMessage, authentication);
    try {
      final boolean authStorageEnabled = getHostOptionsProvider().getHostOptions(authentication.getURL()).isAuthStorageEnabled();
      final SVNAuthentication proxy = ProxySvnAuthentication.proxy(authentication, authStorageEnabled, myArtificialSaving);
      super.acknowledgeAuthentication(accepted, kind, realm, errorMessage, proxy);
      successSaving = true;
    } finally {
      mySavePermissions.remove();
      if (myArtificialSaving) {
        myArtificialSaving = false;
        throw new CredentialsSavedException(successSaving);
      }
    }
  }

  /**
   * "Pageant is not running" error thrown in PageantConnector.query() method is caught and "eaten" in SVNKit logic.
   * So for both cases "Pageant is not running" and "There are no valid keys in agent (both no keys at all and no valid keys for host)"
   * we will get same "Credentials rejected by SSH server" error.
   */
  private void showSshAgentErrorIfAny(@Nullable SVNErrorMessage errorMessage, @Nullable SVNAuthentication authentication) {
    if (errorMessage != null && authentication instanceof SVNSSHAuthentication) {
      AgentProxy agentProxy = ((SVNSSHAuthentication)authentication).getAgentProxy();

      if (agentProxy != null) {
        // TODO: Most likely this should be updated with new VcsNotifier api.
        VcsBalloonProblemNotifier.showOverChangesView(myProject, errorMessage.getFullMessage(), MessageType.ERROR);
      }
    }
  }

  public void acknowledgeForSSL(boolean accepted, SVNAuthentication proxy) {
    if (accepted && proxy instanceof SVNSSLAuthentication && (((SVNSSLAuthentication) proxy).getCertificateFile() != null)) {
      final SVNSSLAuthentication svnsslAuthentication = (SVNSSLAuthentication)proxy;
      final SVNURL url = svnsslAuthentication.getURL();

      final IdeaSVNHostOptionsProvider provider = getHostOptionsProvider();
      final SVNCompositeConfigFile serversFile = provider.getServersFile();
      String groupName = getGroupName(serversFile.getProperties("groups"), url.getHost());

      groupName = StringUtil.isEmptyOrSpaces(groupName) ? "global" : groupName;
      serversFile
        .setPropertyValue(groupName, SvnServerFileKeys.SSL_CLIENT_CERT_FILE, svnsslAuthentication.getCertificateFile().getPath(), true);
      serversFile.save();
    }
  }

  public ISVNProxyManager getProxyManager(SVNURL url) throws SVNException {
    SSLExceptionsHelper.addInfo("Accessing URL: " + url.toString());
    ourThreadLocalProvider.set(myProvider);
    // in proxy creation, we need proxy information from common proxy. but then we should forbid common proxy to intercept
    final ISVNProxyManager proxy = createProxy(url);
    CommonProxy.getInstance().noProxy(url.getProtocol(), url.getHost(), url.getPort());
    return proxy;
  }

  private ISVNProxyManager createProxy(SVNURL url) {
    // this code taken from default manager (changed for system properties reading)
    String host = url.getHost();

    String proxyHost = getServersPropertyIdea(host, HTTP_PROXY_HOST);
    if (StringUtil.isEmptyOrSpaces(proxyHost)) {
      if (getConfig().isIsUseDefaultProxy()) {
        // ! use common proxy if it is set
        try {
          final List<Proxy> proxies = HttpConfigurable.getInstance().getOnlyBySettingsSelector().select(new URI(url.toString()));
          if (proxies != null && ! proxies.isEmpty()) {
            for (Proxy proxy : proxies) {
              if (HttpConfigurable.isRealProxy(proxy) && Proxy.Type.HTTP.equals(proxy.type())) {
                final SocketAddress address = proxy.address();
                if (address instanceof InetSocketAddress) {
                  return new MyPromptingProxyManager(((InetSocketAddress)address).getHostName(),
                                                     String.valueOf(((InetSocketAddress)address).getPort()), url.getProtocol());
                }
              }
            }
          }
        }
        catch (URISyntaxException e) {
          LOG.info(e);
        }
      }
      return null;
    }
    String proxyExceptions = getServersPropertyIdea(host, "http-proxy-exceptions");
    String proxyExceptionsSeparator = ",";
    if (proxyExceptions == null) {
        proxyExceptions = System.getProperty("http.nonProxyHosts");
        proxyExceptionsSeparator = "|";
    }
    if (proxyExceptions != null) {
      for(StringTokenizer exceptions = new StringTokenizer(proxyExceptions, proxyExceptionsSeparator); exceptions.hasMoreTokens();) {
          String exception = exceptions.nextToken().trim();
          if (DefaultSVNOptions.matches(exception, host)) {
              return null;
          }
      }
    }
    String proxyPort = getServersPropertyIdea(host, HTTP_PROXY_PORT);
    String proxyUser = getServersPropertyIdea(host, HTTP_PROXY_USERNAME);
    String proxyPassword = getServersPropertyIdea(host, HTTP_PROXY_PASSWORD);
    return new MySimpleProxyManager(proxyHost, proxyPort, proxyUser, proxyPassword);
  }


  private static class MyPromptingProxyManager extends MySimpleProxyManager {

    private final String myProtocol;

    private MyPromptingProxyManager(final String host, final String port, String protocol) {
      super(host, port, null, null);
      myProtocol = protocol;
    }

    @Override
    public String getProxyUserName() {
      if (myProxyUser != null) {
        return myProxyUser;
      }
      tryGetCredentials();
      return myProxyUser;
    }

    private void tryGetCredentials() {
      try {
        final InetAddress ia = InetAddress.getByName(getProxyHost());
        final PasswordAuthentication authentication =
          Authenticator.requestPasswordAuthentication(getProxyHost(), ia, getProxyPort(), myProtocol, getProxyHost(), myProtocol,
                                                      null, Authenticator.RequestorType.PROXY);
        if (authentication != null) {
          myProxyUser = authentication.getUserName();
          myProxyPassword = String.valueOf(authentication.getPassword());
        }
      }
      catch (UnknownHostException e) {
        //
      }
    }

    @Override
    public String getProxyPassword() {
      if (myProxyPassword != null) {
        return myProxyPassword;
      }
      tryGetCredentials();
      return myProxyPassword;
    }
  }

  private static class MySimpleProxyManager implements ISVNProxyManager {
      protected String myProxyHost;
      private final String myProxyPort;
      protected String myProxyUser;
      protected String myProxyPassword;

      public MySimpleProxyManager(String host, String port, String user, String password) {
          myProxyHost = host;
          myProxyPort = port == null ? "3128" : port;
          myProxyUser = user;
          myProxyPassword = password;
      }

      public String getProxyHost() {
          return myProxyHost;
      }

      public int getProxyPort() {
          try {
              return Integer.parseInt(myProxyPort);
          } catch (NumberFormatException nfe) {
              //
          }
          return 3128;
      }

      public String getProxyUserName() {
          return myProxyUser;
      }

      public String getProxyPassword() {
          return myProxyPassword;
      }

      public void acknowledgeProxyContext(boolean accepted, SVNErrorMessage errorMessage) {
      }
  }

  // 30 seconds
  private final static int DEFAULT_READ_TIMEOUT = 30 * 1000;

  public int getReadTimeout(@NotNull SVNURL url) {
    String protocol = url.getProtocol();
    if (HTTP.equals(protocol) || HTTPS.equals(protocol)) {
      String host = url.getHost();
      String timeout = getServersPropertyIdea(host, "http-timeout");
      if (timeout != null) {
        try {
          return Integer.parseInt(timeout) * 1000;
        }
        catch (NumberFormatException nfe) {
          // use default
        }
      }
      return DEFAULT_READ_TIMEOUT;
    }
    if (SVN_SSH.equals(protocol)) {
      return (int)getConfig().getSshReadTimeout();
    }
    return 0;
  }

  public int getConnectTimeout(@NotNull SVNURL url) {
    String protocol = url.getProtocol();
    if (SVN_SSH.equals(protocol)) {
      return (int)getConfig().getSshConnectionTimeout();
    }
    final int connectTimeout = getHostOptionsProvider().getHostOptions(url).getConnectTimeout();
    if ((HTTP.equals(protocol) || HTTPS.equals(protocol)) && (connectTimeout <= 0)) {
      return DEFAULT_READ_TIMEOUT;
    }
    return connectTimeout;
  }

  @Override
  public int getReadTimeout(@NotNull SVNRepository repository) {
    return getReadTimeout(repository.getLocation());
  }

  @Override
  public int getConnectTimeout(@NotNull SVNRepository repository) {
    return getConnectTimeout(repository.getLocation());
  }

  // taken from default manager as is
  private String getServersPropertyIdea(String host, final String name) {
    final SVNCompositeConfigFile serversFile = getHostOptionsProvider().getServersFile();
    return getPropertyIdea(host, serversFile, name);
  }

  private static String getPropertyIdea(String host, SVNCompositeConfigFile serversFile, final String name) {
    String groupName = getGroupName(serversFile.getProperties("groups"), host);
    if (groupName != null) {
      Map hostProps = serversFile.getProperties(groupName);
      final String value = (String)hostProps.get(name);
      if (value != null) {
        return value;
      }
    }
    Map globalProps = serversFile.getProperties("global");
    return (String) globalProps.get(name);
  }

  public static boolean checkHostGroup(final String url, final String patterns, final String exceptions) {
    final SVNURL svnurl;
    try {
      svnurl = SVNURL.parseURIEncoded(url);
    }
    catch (SVNException e) {
      return false;
    }

    final String host = svnurl.getHost();
    return matches(patterns, host) && (! matches(exceptions, host));
  }

  private static boolean matches(final String pattern, final String host) {
    final StringTokenizer tokenizer = new StringTokenizer(pattern, ",");
    while(tokenizer.hasMoreTokens()) {
      String token = tokenizer.nextToken();
      if (DefaultSVNOptions.matches(token, host)) {
          return true;
      }
    }
    return false;
  }

  @Nullable
  public static String getGroupForHost(final String host, final IdeaSVNConfigFile serversFile) {
    final Map<String,ProxyGroup> groups = serversFile.getAllGroups();
    for (Map.Entry<String, ProxyGroup> entry : groups.entrySet()) {
      if (matchesGroupPattern(host, entry.getValue().getPatterns())) return entry.getKey();
    }
    return null;
  }

  // taken from default manager as is
  private static String getGroupName(Map groups, String host) {
    for (Object o : groups.keySet()) {
      final String name = (String) o;
      final String pattern = (String) groups.get(name);
      if (matchesGroupPattern(host, pattern)) return name;
    }
      return null;
  }

  private static boolean matchesGroupPattern(String host, String pattern) {
    for(StringTokenizer tokens = new StringTokenizer(pattern, ","); tokens.hasMoreTokens();) {
        String token = tokens.nextToken();
        if (DefaultSVNOptions.matches(token, host)) {
          return true;
        }
    }
    return false;
  }

  // default = yes
  private static boolean isTurned(final String value) {
    return value == null || "yes".equalsIgnoreCase(value) || "on".equalsIgnoreCase(value) || "true".equalsIgnoreCase(value);
  }

  private static ModalityState getCurrent() {
    if (ApplicationManager.getApplication().isDispatchThread()) {
      return ModalityState.current();
    }
    final ProgressIndicator pi = ProgressManager.getInstance().getProgressIndicator();
    if (pi == null) {
      return ModalityState.defaultModalityState();
    }
    return pi.getModalityState();
  }

  public void setInteraction(SvnAuthenticationInteraction interaction) {
    myInteraction = interaction;
  }

  private static class MySvnAuthenticationInteraction implements SvnAuthenticationInteraction {
    private Project myProject;

    private MySvnAuthenticationInteraction(Project project) {
      myProject = project;
    }

    @Override
    public void warnOnAuthStorageDisabled(SVNURL url) {
      VcsBalloonProblemNotifier.showOverChangesView(myProject, "Cannot store credentials: forbidden by \"store-auth-creds=no\"", MessageType.ERROR);
    }

    @Override
    public void warnOnPasswordStorageDisabled(SVNURL url) {
      VcsBalloonProblemNotifier.showOverChangesView(myProject, "Cannot store password: forbidden by \"store-passwords=no\"", MessageType.ERROR);
    }

    @Override
    public void warnOnSSLPassphraseStorageDisabled(SVNURL url) {
      VcsBalloonProblemNotifier.showOverChangesView(myProject, "Cannot store passphrase: forbidden by \"store-ssl-client-cert-pp=no\"", MessageType.ERROR);
    }

    @Override
    public boolean promptForPlaintextPasswordSaving(SVNURL url, String realm) {
      final int answer = Messages.showYesNoDialog(myProject, String.format("Your password for authentication realm:\n" +
        "%s\ncan only be stored to disk unencrypted. Would you like to store it in plaintext?", realm),
        "Store the password in plaintext?", Messages.getQuestionIcon());
      return answer == Messages.YES;
    }

    @Override
    public boolean promptInAwt() {
      return true;
    }

    @Override
    public boolean promptForSSLPlaintextPassphraseSaving(SVNURL url, String realm, File certificateFile, String certificateName) {
      final int answer = Messages.showYesNoDialog(myProject,
        String.format("Your passphrase for " + certificateName + ":\n%s\ncan only be stored to disk unencrypted. Would you like to store it in plaintext?",
                                                            certificateFile.getPath()),
        "Store the passphrase in plaintext?", Messages.getQuestionIcon());
      return answer == Messages.YES;
    }

    @Override
    public void dispose() {
      myProject = null;
    }
  }

  public class IdeaSVNHostOptionsProvider extends DefaultSVNHostOptionsProvider {
    public IdeaSVNHostOptionsProvider() {
      super(myConfigDirectory);
    }

    @Override
    public SVNCompositeConfigFile getServersFile() {
      return super.getServersFile();
    }

    @Override
    public ISVNHostOptions getHostOptions(SVNURL url) {
      return new IdeaSVNHostOptions(getServersFile(), url);
    }
  }

  private static class ThreadLocalSavePermissions {
    private final Map<Thread, Boolean> myPlainTextAllowed;

    private ThreadLocalSavePermissions() {
      myPlainTextAllowed = Collections.synchronizedMap(new HashMap<Thread, Boolean>());
    }

    public void put(final boolean value) {
      myPlainTextAllowed.put(Thread.currentThread(), value);
    }

    public boolean have() {
      return myPlainTextAllowed.containsKey(Thread.currentThread());
    }

    public void remove() {
      myPlainTextAllowed.remove(Thread.currentThread());
    }

    public boolean allowed() {
      return Boolean.TRUE.equals(myPlainTextAllowed.get(Thread.currentThread()));
    }
  }

  private class IdeaSVNHostOptions extends DefaultSVNHostOptions {
    private SVNCompositeConfigFile myConfigFile;
    private final SVNURL myUrl;

    private IdeaSVNHostOptions(SVNCompositeConfigFile serversFile, SVNURL url) {
      super(serversFile, url);
      myUrl = url;
    }

    @Override
    public boolean isStorePlainTextPasswords(final String realm, SVNAuthentication auth) throws SVNException {
      if (USERNAME.equals(auth.getKind())) return true;

      final boolean superValue = super.isStorePlainTextPasswords(realm, auth);
      final boolean value = mySavePermissions.allowed() || superValue;
      if ((! value) && (! mySavePermissions.have())) {
        promptAndSaveWhenWeLackEncryption(realm, auth, new Getter<Boolean>() {
                                                @Override
                                                public Boolean get() {
                                                  return myInteraction.promptForPlaintextPasswordSaving(myUrl, realm);
                                                }
                                              });
      }
      return value;
    }

    @Override
    public boolean isStorePlainTextPassphrases(final String realm, final SVNAuthentication auth) throws SVNException {
      if (USERNAME.equals(auth.getKind())) return true;

      final boolean value = mySavePermissions.allowed() || super.isStorePlainTextPassphrases(realm, auth);
      if ((! value) && (! mySavePermissions.have())) {
        promptAndSaveWhenWeLackEncryption(realm, auth, new Getter<Boolean>() {
                                                @Override
                                                public Boolean get() {
                                                  File file = null;
                                                  String certificateName = null;
                                                  if (auth instanceof SVNSSLAuthentication) {
                                                    file = ((SVNSSLAuthentication) auth).getCertificateFile();
                                                    certificateName = "client certificate";
                                                  } else if (auth instanceof SVNSSHAuthentication) {
                                                    file = ((SVNSSHAuthentication) auth).getPrivateKeyFile();
                                                    certificateName = "private key file";
                                                  } else {
                                                    assert false;
                                                  }
                                                  return myInteraction.promptForSSLPlaintextPassphraseSaving(myUrl, realm,
                                                           file, certificateName);
                                                }
                                              });
      }
      return value;
    }

    @Override
    public boolean isAuthStorageEnabled() {
      final boolean value;
      if (hasAuthStorageEnabledOption()) {
        value = super.isAuthStorageEnabled();
      } else {
        value = isTurned(getConfigFile().getPropertyValue("auth", "store-auth-creds"));
      }
      if (! value) {
        myInteraction.warnOnAuthStorageDisabled(myUrl);
      }
      return value;
    }

    @Override
    public boolean isStorePasswords() {
      final String storePasswords = getStorePasswords();
      final boolean value;
      if (storePasswords != null) {
        value = isTurned(storePasswords);
      } else {
        final String configValue = getConfigFile().getPropertyValue("auth", "store-passwords");
        value = isTurned(configValue);
      }
      if (! value) {
        myInteraction.warnOnPasswordStorageDisabled(myUrl);
      }
      return value;
    }

    @Override
    public boolean isStoreSSLClientCertificatePassphrases() {
      final boolean value = super.isStoreSSLClientCertificatePassphrases();
      if (! value) {
        myInteraction.warnOnSSLPassphraseStorageDisabled(myUrl);
      }
      return value;
    }

    public String getStorePasswords() {
      return getServersPropertyIdea(getHost(), "store-passwords");
    }

    private SVNCompositeConfigFile getConfigFile() {
      if (myConfigFile == null) {
        final File config = new File(myConfigDirectory, "config");
        SVNConfigFile.createDefaultConfiguration(myConfigDirectory);
        SVNConfigFile userConfig = new SVNConfigFile(config);
        SVNConfigFile systemConfig = new SVNConfigFile(new File(SVNFileUtil.getSystemConfigurationDirectory(), "config"));
        myConfigFile = new SVNCompositeConfigFile(systemConfig, userConfig);
      }
      return myConfigFile;
    }

    private void promptAndSaveWhenWeLackEncryption(final String realm, final SVNAuthentication auth, final Getter<Boolean> prompt) {
      final Boolean[] saveOnce = new Boolean[1];
      final Runnable actualSave = new Runnable() {
        @Override
        public void run() {
          mySavePermissions.put(Boolean.TRUE.equals(saveOnce[0]));
          try {
            myPersistentAuthenticationProviderProxy.actualSavePermissions(realm, auth);
          }
          finally {
            mySavePermissions.remove();
          }
        }
      };

      if (myInteraction.promptInAwt()) {
        runOrInvokeLaterAboveProgress(() -> {
          saveOnce[0] = Boolean.TRUE.equals(prompt.get());
          ApplicationManager.getApplication().executeOnPooledThread(actualSave);
        }, getCurrent(), myProject);
      } else {
        saveOnce[0] = Boolean.TRUE.equals(prompt.get());
        actualSave.run();
      }
    }
  }

  private static class ProxyPasswordStorageForDebug implements DefaultSVNPersistentAuthenticationProvider.IPasswordStorage {
    private final DefaultSVNPersistentAuthenticationProvider.IPasswordStorage myDelegate;
    private final EventDispatcher<SvnAuthenticationListener> myListener;

    public ProxyPasswordStorageForDebug(final DefaultSVNPersistentAuthenticationProvider.IPasswordStorage delegate,
                                        final EventDispatcher<SvnAuthenticationListener> listener) {
      myDelegate = delegate;
      myListener = listener;
    }

    @Override
    public String getPassType() {
      return myDelegate.getPassType();
    }

    @Override
    public boolean savePassword(String realm, char[] password, SVNAuthentication auth, SVNProperties authParameters) throws SVNException {
      final boolean saved = myDelegate.savePassword(realm, password, auth, authParameters);
      if (saved) {
        myListener.getMulticaster().actualSaveWillBeTried(ProviderType.persistent, auth.getURL(), realm, auth.getKind()
        );
      }
      return saved;
    }

    @Override
    public char[] readPassword(String realm, String userName, SVNProperties authParameters) throws SVNException {
      return myDelegate.readPassword(realm, userName, authParameters);
    }

    @Override
    public boolean savePassphrase(String realm, char[] passphrase, SVNAuthentication auth, SVNProperties authParameters, boolean force)
      throws SVNException {
      final boolean saved = myDelegate.savePassphrase(realm, passphrase, auth, authParameters, force);
      if (saved) {
        myListener.getMulticaster().actualSaveWillBeTried(ProviderType.persistent, auth.getURL(), realm, auth.getKind()
        );
      }
      return saved;
    }

    @Override
    public char[] readPassphrase(String realm, SVNProperties authParameters) throws SVNException {
      return myDelegate.readPassphrase(realm, authParameters);
    }
  }

  private <T> T wrapNativeCall(final ThrowableComputable<T, SVNException> runnable) throws SVNException {
    try {
      NativeLogReader.startTracking();
      final T t = runnable.compute();
      final List<NativeLogReader.CallInfo> logged = NativeLogReader.getLogged();
      final StringBuilder sb = new StringBuilder();
      for (NativeLogReader.CallInfo info : logged) {
        final String message = SvnNativeCallsTranslator.getMessage(info);
        if (message != null) {
          if (sb.length() > 0) sb.append('\n');
          sb.append(message);
        }
      }
      if (sb.length() > 0) {
        VcsBalloonProblemNotifier.showOverChangesView(myProject, sb.toString(), MessageType.ERROR);
        LOG.info(sb.toString());
      }
      return t;
    } finally {
      NativeLogReader.clear();
      NativeLogReader.endTracking();
    }
  }

  private static class MyKeyringMasterKeyProvider implements ISVNGnomeKeyringPasswordProvider {
    private Project myProject;

    public MyKeyringMasterKeyProvider(Project project) {
      myProject = project;
    }

    @Override
    public char[] getKeyringPassword(final String keyringName) throws SVNException {
      final String message = keyringName != null ? SvnBundle.message("gnome.keyring.prompt.named", keyringName)
                                                 : SvnBundle.message("gnome.keyring.prompt.nameless");
      final Ref<String> result = Ref.create();
      UIUtil.invokeAndWaitIfNeeded(new Runnable() {
        @Override
        public void run() {
          result.set(Messages.showPasswordDialog(myProject, message, SvnBundle.message("subversion.name"), Messages.getQuestionIcon()));
        }
      });
      return !result.isNull() ? result.get().toCharArray() : null;
    }
  }

  public static class CredentialsSavedException extends RuntimeException {
    private final boolean mySuccess;

    public CredentialsSavedException(boolean success) {
      mySuccess = success;
    }

    public boolean isSuccess() {
      return mySuccess;
    }
  }
}
