/**
 * This file is part of Eclipse Steady.
 *
 * 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.
 *
 * SPDX-License-Identifier: Apache-2.0
 * SPDX-FileCopyrightText: Copyright (c) 2018-2020 SAP SE or an SAP affiliate company and Eclipse Steady contributors
 */
package org.eclipse.steady.svn;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import org.apache.commons.configuration.Configuration;
import org.apache.commons.io.FileUtils;
import org.apache.logging.log4j.Logger;
import org.eclipse.steady.shared.util.VulasConfiguration;
import org.eclipse.steady.vcs.FileChange;
import org.eclipse.steady.vcs.IVCSClient;
import org.eclipse.steady.vcs.RepoMismatchException;
import org.tmatesoft.svn.core.SVNDepth;
import org.tmatesoft.svn.core.SVNDirEntry;
import org.tmatesoft.svn.core.SVNException;
import org.tmatesoft.svn.core.SVNLogEntry;
import org.tmatesoft.svn.core.SVNLogEntryPath;
import org.tmatesoft.svn.core.SVNPropertyValue;
import org.tmatesoft.svn.core.SVNRevisionProperty;
import org.tmatesoft.svn.core.SVNURL;
import org.tmatesoft.svn.core.auth.BasicAuthenticationManager;
import org.tmatesoft.svn.core.auth.ISVNAuthenticationManager;
import org.tmatesoft.svn.core.internal.util.SVNDate;
import org.tmatesoft.svn.core.io.SVNRepository;
import org.tmatesoft.svn.core.io.SVNRepositoryFactory;
import org.tmatesoft.svn.core.wc.SVNRevision;
import org.tmatesoft.svn.core.wc.SVNUpdateClient;
import org.tmatesoft.svn.core.wc2.SvnCheckout;
import org.tmatesoft.svn.core.wc2.SvnOperationFactory;
import org.tmatesoft.svn.core.wc2.SvnTarget;

/**
 * <p>SvnClient class.</p>
 */
public class SvnClient implements IVCSClient {

  private static final Logger log = org.apache.logging.log4j.LogManager.getLogger();

  private static final String TYPE = "SVN";

  private final String id = new Double(Math.random()).toString();
  private Path workDir = null;

  private URL url = null;

  // private SVNURL repoUrl = null;
  private SVNRepository rootRepo = null;
  private Collection<SVNLogEntry> logEntries = null;
  private BasicAuthenticationManager authManager = null;
  private Date asOf = null;

  private Configuration cfg = null;

  /**
   * <p>getType.</p>
   *
   * @return a {@link java.lang.String} object
   */
  public String getType() {
    return SvnClient.TYPE;
  }

  /**
   * <p>Constructor for SvnClient.</p>
   */
  public SvnClient() {
    this.cfg = VulasConfiguration.getGlobal().getConfiguration();
  }

  /** {@inheritDoc} */
  public void setRepoUrl(URL _u) throws RepoMismatchException {
    if (_u == null) throw new IllegalArgumentException("Invalid url: " + _u);
    this.url = _u;

    // Prepare repository setup (authentication and HTTP proxy)
    authManager = new BasicAuthenticationManager("login", "password");
    final String phost = this.cfg.getString("http.proxyHost", null);
    final String pport = this.cfg.getString("http.proxyPort", null);
    if (phost != null && pport != null) {
      authManager.setProxy(phost, new Integer(pport).intValue(), "", "");
      SvnClient.log.info("Using proxy " + phost + ":" + pport);
    }

    // Set up repo for trunk (used for searching log entries)
    this.rootRepo = this.setupRepo(null);
  }

  /** {@inheritDoc} */
  public void setWorkDir(Path _dir) {
    if (_dir != null) this.workDir = _dir;
    else {
      try {
        this.workDir = java.nio.file.Files.createTempDirectory("patcha_" + this.id);
      } catch (IOException e) {
        throw new IllegalStateException("Unable to create work directory", e);
      }
    }
  }

  private SVNRepository setupRepo(String _path) throws RepoMismatchException {

    // Set repository for given URL (+ trunk or tag suffix)
    String tmp = null;

    // Build repository URL (incl. path)
    if (_path == null) tmp = this.url.toString();
    else tmp = this.url.toString() + (this.url.toString().endsWith("/") ? "" : "/") + _path;

    SVNRepository repo = null;
    try {
      SVNURL url = SVNURL.parseURIEncoded(tmp);

      // SVN2Client.log.debug("Environment proxy (host:port): " +
      // System.getProperty("http.proxyHost") + ":" + System.getProperty("http.proxyPort"));
      SvnClient.log.info(
          "SVNKIT proxy configuration (host:port): "
              + authManager.getProxyManager(url).getProxyHost()
              + ":"
              + authManager.getProxyManager(url).getProxyPort());

      repo = SVNRepositoryFactory.create(url);
      repo.setAuthenticationManager(authManager);
      repo.testConnection();
    } catch (SVNException e) {
      e.printStackTrace();
      throw new RepoMismatchException(
          this, tmp,
          e); // "Cannot create SVN repository from URL '" + tmp + "': " + e.getMessage());
    }
    return repo;
  }

  private void updateCommitLog(Date _as_of) throws SVNException {
    // In all of the following cases, no further log entries must be collected
    if (this.logEntries != null
        && (this.asOf == null || (_as_of != null && this.asOf.before(_as_of)))) return;

    // Retrieve the log
    this.asOf = _as_of;
    final long latest_rev = this.rootRepo.getLatestRevision();

    // Search as of ...
    long first_rev = 0;
    if (this.asOf != null) first_rev = this.rootRepo.getDatedRevision(this.asOf);

    this.logEntries = rootRepo.log(new String[] {""}, null, first_rev, latest_rev, true, true);
  }

  /**
   * {@inheritDoc}
   *
   * Performs a search in the repository root.
   */
  public Map<String, String> searchCommitLog(String _str, Date _asOf) {
    final Map<String, String> hits = new HashMap<String, String>();
    try {
      // Update revision log
      this.updateCommitLog(_asOf);

      for (SVNLogEntry logEntry : this.logEntries) {
        if (logEntry.getMessage() != null && logEntry.getMessage().contains(_str)) {
          SvnClient.log.info(
              "Revision '" + logEntry.getRevision() + "' : " + logEntry.getMessage().trim());
          hits.put(String.valueOf(logEntry.getRevision()), logEntry.getMessage().trim());
        }
      }

    } catch (SVNException e) {
      SvnClient.log.error("Error while searching commit log: " + e.getMessage());
    }
    return hits;
  }

  /** {@inheritDoc} */
  public Map<String, String> getCommitLogEntries(Set<String> _revs) { // String[] _ids) {
    final Map<String, String> hits = new HashMap<String, String>();
    try {
      // Update revision log (null indicates to retrieve all log entries as of rev. 0)
      this.updateCommitLog(null);

      String id = null;
      for (SVNLogEntry logEntry : this.logEntries) {
        id = String.valueOf(logEntry.getRevision());
        for (String sid : _revs) {
          if (!sid.equals("") && sid.equals(id)) {
            hits.put(sid, logEntry.getMessage().trim());
            continue;
          }
        }
      }
    } catch (SVNException e) {
      SvnClient.log.error("Error while searching commit log: " + e.getMessage());
    }
    return hits;
  }

  private SVNLogEntry getLogEntry(String _rev) {
    SVNLogEntry e = null;
    final long rev = Long.valueOf(_rev);
    for (SVNLogEntry logEntry : this.logEntries)
      if (logEntry.getRevision() == rev) {
        e = logEntry;
        break;
      }
    return e;
  }

  /** {@inheritDoc} */
  public Path checkout(String _rev) throws Exception {
    if (_rev.contains(":")) {
      throw new Exception(
          "The SVN client does not support manually specified branches. Please remove the branch"
              + " from the commit definition in -e");
    }
    final SvnOperationFactory svnOperationFactory = new SvnOperationFactory();
    Path rev_dir = null;
    try {
      // Create subdir for given rev
      Path dir = getRevisionDirectory(_rev);
      rev_dir = Paths.get(dir.toString(), "_src");
      if (!Files.exists(rev_dir)) rev_dir = Files.createDirectory(rev_dir);

      // Perform checkout
      SVNRevision revision = SVNRevision.create(Long.valueOf(_rev));
      final SvnCheckout checkout = svnOperationFactory.createCheckout();
      checkout.setSingleTarget(SvnTarget.fromFile(rev_dir.toFile()));
      checkout.setSource(SvnTarget.fromURL(this.setupRepo("trunk/").getLocation()));
      checkout.setDepth(SVNDepth.INFINITY);
      checkout.setRevision(revision);
      // checkout
      checkout.run();
    } catch (Exception e) {
      SvnClient.log.error("Error while checking out revision " + _rev + ": " + e.getMessage());
    } finally {
      svnOperationFactory.dispose();
    }
    return rev_dir;
  }

  /** {@inheritDoc} */
  public Map<String, String> listEntries(String _path, String _asof, String _until) {
    Map<String, String> l = new HashMap<String, String>();
    // String rel_path = url.toString().replace(rootRepo.getDir("", -1, null,
    // (Collection<SVNDirEntry>)null).iterator().next(), "");
    try {
      Collection<SVNDirEntry> entries =
          this.rootRepo.getDir(
              _path, -1, null, (Collection<SVNDirEntry>) null); // this.getDirEntries(path);
      Iterator<SVNDirEntry> iterator = entries.iterator();
      String name = null, path = null;

      long rev = -1;
      // Use an iterator to get all directory entries in a certain path ("tags/")

      int asof = (_asof == null ? -1 : Integer.parseInt(_asof));
      int until = (_until == null ? -1 : Integer.parseInt(_until));

      while (iterator.hasNext()) {
        SVNDirEntry entry = (SVNDirEntry) iterator.next();

        SvnClient.log.debug(
            "Path mess: "
                + entry.getPath()
                + " | "
                + entry.getRelativePath()
                + " | "
                + entry.getRepositoryRoot()
                + " | "
                + entry.getExternalParentUrl()
                + " | "
                + entry.getURL());

        name = entry.getName();
        rev = entry.getRevision();
        path = entry.getURL().toString().substring(entry.getRepositoryRoot().toString().length());
        if (asof < rev && (_until == null || rev < until)) l.put(path, Long.toString(rev));
      }
    } catch (Exception e) {
      SvnClient.log.error("Error while getting directory entries: " + e.getMessage());
    }

    // return the tag name and the corresponding revision number
    return l;
  }

  /** {@inheritDoc} */
  public File checkoutFile(String _rev, String _rel_path) {
    final SvnOperationFactory svnOperationFactory = new SvnOperationFactory();
    File f = null;
    SVNURL url = null;
    try {
      // Create subdir for given rev
      final String rel_dir = _rel_path.substring(0, _rel_path.lastIndexOf('/'));
      final Path rev_dir = Paths.get(this.workDir.toString(), _rev, rel_dir);
      Path p = Files.createDirectories(rev_dir);

      // Create SVNURL for specific file
      url = SVNURL.parseURIEncoded(this.rootRepo.getRepositoryRoot(false) + "/" + rel_dir);

      // Perform checkout
      SVNRevision revision = SVNRevision.create(Long.valueOf(_rev));

      SVNUpdateClient clnt =
          new SVNUpdateClient((ISVNAuthenticationManager) this.authManager, null);
      clnt.doCheckout(
          url,
          p.toFile(),
          revision,
          revision,
          SVNDepth.FILES,
          false); // IMMEDIATES, FILES, INFINITY

      //
      //			final SvnCheckout checkout = svnOperationFactory.createCheckout();
      //			checkout.setSingleTarget(SvnTarget.fromFile(p.toFile()));
      //			checkout.setSource(SvnTarget.fromURL(url));
      //			checkout.setDepth(SVNDepth.IMMEDIATES); //INFINITY
      //			checkout.setRevision(revision);
      //
      //			// Checkout and get file
      //			checkout.run();
      f = Paths.get(this.workDir.toString(), _rev, _rel_path).toFile();
    } catch (Exception e) {
      SvnClient.log.error(
          "Error while checking out URL '" + url + "', revision " + _rev + ": " + e.getMessage());
    } finally {
      svnOperationFactory.dispose();
    }
    return f;
  }

  /**
   * Deletes the temp. directory into which the repository files where checked out.
   */
  public void cleanup() {
    try {
      // Does not work as the directory must be empty. Solution to be implemented: Create a file
      // walker deleting every other file or directory.
      // Files.deleteIfExists(this.workDir);
      //
      FileUtils.deleteDirectory(this.workDir.toFile());
      SvnClient.log.info("Deleted temp. directory [" + this.workDir + "]");
    } catch (IOException e) {
      SvnClient.log.error(
          "Error while deleting temp. directory '" + this.workDir + "': " + e.getMessage());
    }
  }

  private Path getRevisionDirectory(String _rev) {
    return Paths.get(this.workDir.toString(), _rev);
  }

  private File getLocalFile(String _rel_path, String _rev) {
    return Paths.get(this.workDir.toString(), _rev, _rel_path).toFile();
  }

  /** {@inheritDoc} */
  public Set<FileChange> getFileChanges(String _rev) {
    final Set<FileChange> changes = new HashSet<FileChange>();
    try {
      // Update revision log (using this.asOf as argument will result in an actual SVN call only if
      // it has not been done before)
      this.updateCommitLog(this.asOf);

      // Determine prev. revision
      final long l = Long.valueOf(_rev).longValue() - 1;
      final String prev_rev = new Long(l).toString();

      // Get changed paths for revision
      SVNLogEntry entry = this.getLogEntry(_rev);
      final Map<String, SVNLogEntryPath> changedPaths = entry.getChangedPaths();

      // Loop all changed path and the check the corresponding files out
      SVNLogEntryPath entryPath = null;
      String rel_path = null;
      File oldf = null, newf = null;
      for (Map.Entry<String, SVNLogEntryPath> p : changedPaths.entrySet()) {
        entryPath = p.getValue();
        rel_path = p.getKey();

        // Checkout file(s), depending on the modification type
        switch (entryPath.getType()) {
          case 'M':
            oldf = this.checkoutFile(prev_rev, rel_path);
            newf = this.checkoutFile(_rev, rel_path);
            break;
          case 'A':
            oldf = null;
            newf = this.checkoutFile(_rev, rel_path);
            break;
          case 'D':
            oldf = this.checkoutFile(prev_rev, rel_path);
            newf = null;
            break;
          default:
            oldf = null;
            newf = null;
            break;
        }

        // If one of them exists and is not a directory, create a new FileChange
        if ((oldf != null && !oldf.isDirectory()) || (newf != null && !newf.isDirectory()))
          changes.add(new FileChange(this.rootRepo.getLocation().toString(), rel_path, oldf, newf));
      }
    } catch (Exception e) {
      SvnClient.log.error("Error while getting file changes: " + e.getMessage());
    }
    return changes;
  }

  /**
   * <p>getRepoRelativePath.</p>
   *
   * @return a {@link java.lang.String} object
   */
  public String getRepoRelativePath() {
    /*
    String rel_path = null;
    Collection<SVNDirEntry> SVNentry = this.rootRepo.getDir("tags/", -1, null,(Collection<SVNDirEntry>) null);
    Iterator<SVNDirEntry> iterator = SVNentry.iterator();
    if (iterator.hasNext()) {
    	SVNDirEntry entry = (SVNDirEntry) iterator.next();
    	// remove the root repository part from the whole url (object: SVNDirEntry)
    	rel_path = entry.getURL().toString().replaceAll(entry.getRepositoryRoot().toString(), "");
    }else{
    	SvnClient.log.error("[Error] while getting the relative path for tag directory");
    }
    rel_path = rel_path.substring(0, rel_path.lastIndexOf('/'));
    return rel_path;
     */
    return url.toString().replace(rootRepo.toString(), "");
  }

  //	public Collection<SVNDirEntry> getDirEntries(String path) throws SVNException{
  //		return rootRepo.getDir(path, -1, null,(Collection<SVNDirEntry>) null);
  //	}

  /** {@inheritDoc} */
  public long getRevisionTimeStamp(String revision) {
    long revisionTimeStampMilliSecond = 0;

    try {
      // An "svn:date" revision property that is a date & time stamp representing the time when the
      // revision was created.
      SVNPropertyValue propertyValue =
          rootRepo.getRevisionPropertyValue(Long.parseLong(revision), SVNRevisionProperty.DATE);
      String stringValue = SVNPropertyValue.getPropertyAsString(propertyValue);
      // SVNDate date = SVNDate.parseDate(stringValue);
      revisionTimeStampMilliSecond = SVNDate.parseDateAsMilliseconds(stringValue);
      // revisionTimeStampMicroSecond = date.getTimeInMicros();
    } catch (SVNException e) {
      // TODO Auto-generated catch block
      // e.printStackTrace();
      SvnClient.log.error(
          "Error when retrieving time stamp for revision : " + revision + " " + e.getMessage());
    }
    return revisionTimeStampMilliSecond;
  }
}
